From 8c8fd6e298521f1032a739d42cb0acdf0b8a1522 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Mon, 7 Sep 2026 10:13:18 +0800 Subject: [PATCH] feat(admin): link subscriptions to user usage --- .../src/views/admin/SubscriptionsView.vue | 9 +- .../SubscriptionsView.userUsageLink.spec.ts | 143 ++++++++++++++++++ 2 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 frontend/src/views/admin/__tests__/SubscriptionsView.userUsageLink.spec.ts diff --git a/frontend/src/views/admin/SubscriptionsView.vue b/frontend/src/views/admin/SubscriptionsView.vue index 66569dad94..eba7992b7e 100644 --- a/frontend/src/views/admin/SubscriptionsView.vue +++ b/frontend/src/views/admin/SubscriptionsView.vue @@ -190,12 +190,15 @@ }} - + {{ userColumnMode === 'email' ? (row.user?.email || t('admin.redeem.userPrefix', { id: row.user_id })) - : (row.user?.username || '-') + : (row.user?.username || t('admin.redeem.userPrefix', { id: row.user_id })) }} - + diff --git a/frontend/src/views/admin/__tests__/SubscriptionsView.userUsageLink.spec.ts b/frontend/src/views/admin/__tests__/SubscriptionsView.userUsageLink.spec.ts new file mode 100644 index 0000000000..0d89d0d4ca --- /dev/null +++ b/frontend/src/views/admin/__tests__/SubscriptionsView.userUsageLink.spec.ts @@ -0,0 +1,143 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { flushPromises, mount } from '@vue/test-utils' +import { defineComponent } from 'vue' + +import SubscriptionsView from '../SubscriptionsView.vue' + +const { listSubscriptions, getAllGroups } = vi.hoisted(() => ({ + listSubscriptions: vi.fn(), + getAllGroups: vi.fn() +})) + +vi.mock('@/api/admin', () => ({ + adminAPI: { + subscriptions: { list: listSubscriptions }, + groups: { getAll: getAllGroups } + } +})) + +vi.mock('@/stores/app', () => ({ + useAppStore: () => ({ + showError: vi.fn(), + showSuccess: vi.fn() + }) +})) + +vi.mock('vue-i18n', async () => { + const actual = await vi.importActual('vue-i18n') + return { + ...actual, + useI18n: () => ({ + t: (key: string, params?: { id?: number }) => + key === 'admin.redeem.userPrefix' ? `User #${params?.id}` : key + }) + } +}) + +const DataTableStub = { + props: ['data'], + template: ` +
+
+ +
+
+ ` +} + +const RouterLinkStub = defineComponent({ + name: 'RouterLink', + props: { to: { type: Object, required: true } }, + template: '' +}) + +describe('admin subscription user usage link', () => { + beforeEach(() => { + vi.clearAllMocks() + localStorage.clear() + listSubscriptions.mockResolvedValue({ + items: [{ + id: 9, + user_id: 42, + group_id: 3, + status: 'active', + starts_at: '2026-01-01T00:00:00Z', + expires_at: null, + daily_usage_usd: 0, + weekly_usage_usd: 0, + monthly_usage_usd: 0, + daily_window_start: null, + weekly_window_start: null, + monthly_window_start: null, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + user: { email: 'reader@example.com', username: 'Reader' } + }], + total: 1, + pages: 1 + }) + getAllGroups.mockResolvedValue([]) + }) + + const mountView = () => mount(SubscriptionsView, { + global: { + stubs: { + AppLayout: { template: '
' }, + TablePageLayout: { template: '
' }, + DataTable: DataTableStub, + RouterLink: RouterLinkStub, + Pagination: true, + BaseDialog: true, + ConfirmDialog: true, + EmptyState: true, + Select: true, + GroupBadge: true, + GroupOptionItem: true, + Icon: true, + Teleport: true + } + } + }) + + it('renders the user email as a link to that user filtered usage records', async () => { + const wrapper = mountView() + + await flushPromises() + + const link = wrapper.getComponent(RouterLinkStub) + expect(link.text()).toBe('reader@example.com') + expect(link.props('to')).toEqual({ path: '/admin/usage', query: { user_id: 42 } }) + }) + + it('uses the user ID label for the usage link when username mode has no username', async () => { + localStorage.setItem('subscription-user-column-mode', 'username') + listSubscriptions.mockResolvedValue({ + items: [{ + id: 9, + user_id: 42, + group_id: 3, + status: 'active', + starts_at: '2026-01-01T00:00:00Z', + expires_at: null, + daily_usage_usd: 0, + weekly_usage_usd: 0, + monthly_usage_usd: 0, + daily_window_start: null, + weekly_window_start: null, + monthly_window_start: null, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + user: { email: 'reader@example.com' } + }], + total: 1, + pages: 1 + }) + + const wrapper = mountView() + await flushPromises() + + const link = wrapper.getComponent(RouterLinkStub) + expect(link.text()).toBe('User #42') + expect(link.props('to')).toEqual({ path: '/admin/usage', query: { user_id: 42 } }) + }) +})