fix: scope page insights to visible periods

This commit is contained in:
2026-08-22 17:25:12 +08:00
parent 7524406e79
commit e7655262ac
4 changed files with 37 additions and 14 deletions

View File

@@ -1076,7 +1076,7 @@ export interface components {
}; };
AIInsightRefreshRequest: { AIInsightRefreshRequest: {
terminalId?: string; terminalId?: string;
/** @description Used only by the transactions module. */ /** @description Server-offered reporting period used to match the visible analytics run. */
periodId?: string; periodId?: string;
}; };
AIInsightJob: { AIInsightJob: {
@@ -2469,7 +2469,7 @@ export interface operations {
parameters: { parameters: {
query?: { query?: {
terminalId?: string; terminalId?: string;
/** @description Used only by the transactions module to match the selected reporting period. */ /** @description Server-offered reporting period used to match the visible analytics run. */
periodId?: string; periodId?: string;
}; };
header?: { header?: {

View File

@@ -1,6 +1,6 @@
import { render, screen, waitFor } from '@testing-library/svelte'; import { cleanup, render, screen, waitFor } from '@testing-library/svelte';
import { tick } from 'svelte'; import { tick } from 'svelte';
import { describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
import type { AIInsight } from '$lib/api/client'; import type { AIInsight } from '$lib/api/client';
import AIInsightPanel from './AIInsightPanel.svelte'; import AIInsightPanel from './AIInsightPanel.svelte';
@@ -57,6 +57,8 @@ const insight: AIInsight = {
generatedAt: '2026-08-22T00:00:00Z', generatedAt: '2026-08-22T00:00:00Z',
}; };
afterEach(cleanup);
describe('AIInsightPanel', () => { describe('AIInsightPanel', () => {
it('leaves the skeleton after loading an insight and does not fetch it again', async () => { it('leaves the skeleton after loading an insight and does not fetch it again', async () => {
latestAIInsight.mockReset(); latestAIInsight.mockReset();
@@ -64,24 +66,33 @@ describe('AIInsightPanel', () => {
render(AIInsightPanel, { module: 'overview' }); render(AIInsightPanel, { module: 'overview' });
await screen.findByText('فروش در این بازه رشد کرده است.'); await screen.findByText('آخرین روز ثبت‌شده باثبات بوده است.');
await tick(); await tick();
await waitFor(() => expect(screen.queryByLabelText('در حال دریافت تحلیل هوشمند')).toBeNull()); await waitFor(() => expect(screen.queryByLabelText('در حال دریافت تحلیل هوشمند')).toBeNull());
expect(latestAIInsight).toHaveBeenCalledTimes(1); expect(latestAIInsight).toHaveBeenCalledTimes(1);
expect(screen.getByText('فروش در این بازه رشد کرده است.')).not.toBeNull();
expect(screen.queryByText('روند کوتاه‌مدت باثبات پیش‌بینی می‌شود.')).toBeNull();
expect(screen.queryByText('یافته‌ها')).toBeNull();
}); });
it('loads a transaction insight with the resolved period scope', async () => { it('loads every page insight with the resolved period scope', async () => {
latestAIInsight.mockReset(); latestAIInsight.mockReset();
latestAIInsight.mockResolvedValueOnce({ latestAIInsight.mockResolvedValueOnce({
...insight, ...insight,
module: 'transactions', module: 'overview',
scope: { terminalId: 'T83', periodId: 'period-7' }, scope: { terminalId: 'T83', periodId: 'period-7' },
}); });
render(AIInsightPanel, { module: 'transactions', terminalId: 'T83', periodId: 'period-7' }); render(AIInsightPanel, {
module: 'overview',
terminalId: 'T83',
periodId: 'period-7',
periodLabel: 'مرداد ۱۴۰۵',
});
await screen.findByText('فروش در این بازه رشد کرده است.'); await screen.findByText('آخرین روز ثبت‌شده باثبات بوده است.');
expect(latestAIInsight).toHaveBeenCalledWith('transactions', 'T83', 'period-7'); expect(screen.getByText('مرداد ۱۴۰۵')).not.toBeNull();
expect(latestAIInsight).toHaveBeenCalledWith('overview', 'T83', 'period-7');
}); });
}); });

View File

@@ -120,8 +120,6 @@
<PageHeader title={titles[kind][0]} description={titles[kind][1]} icon={icons[kind]} /> <PageHeader title={titles[kind][0]} description={titles[kind][1]} icon={icons[kind]} />
<AIInsightPanel module={kind} terminalId={filters.terminal === 'all' ? undefined : filters.terminal} />
{#if loading} {#if loading}
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4" role="status" aria-label="در حال محاسبه گزارش"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4" role="status" aria-label="در حال محاسبه گزارش">
{#each Array(4) as _}<Skeleton class="h-40 rounded-lg" />{/each} {#each Array(4) as _}<Skeleton class="h-40 rounded-lg" />{/each}
@@ -140,6 +138,13 @@
{@const fees = run.sections.fees} {@const fees = run.sections.fees}
{@const peers = run.sections.peerComparison} {@const peers = run.sections.peerComparison}
<AIInsightPanel
module={kind}
terminalId={filters.terminal === 'all' ? undefined : filters.terminal}
periodId={run.period.periodId}
periodLabel={run.period.jalaliLabelFa}
/>
{#if kind === 'overview' && overview.state === 'READY'} {#if kind === 'overview' && overview.state === 'READY'}
<div class="kpi-grid grid gap-3 sm:gap-4"> <div class="kpi-grid grid gap-3 sm:gap-4">
<KpiCard <KpiCard

View File

@@ -16,6 +16,7 @@
import { useDashboardContext } from '$lib/dashboard-context.svelte'; import { useDashboardContext } from '$lib/dashboard-context.svelte';
import { parseDashboardFilters } from '$lib/dashboard-filters'; import { parseDashboardFilters } from '$lib/dashboard-filters';
import { formatPercent, formatValue } from '$lib/format'; import { formatPercent, formatValue } from '$lib/format';
import AIInsightPanel from './AIInsightPanel.svelte';
import KpiCard from './KpiCard.svelte'; import KpiCard from './KpiCard.svelte';
import PageHeader from './PageHeader.svelte'; import PageHeader from './PageHeader.svelte';
import TransactionStatusChart from './TransactionStatusChart.svelte'; import TransactionStatusChart from './TransactionStatusChart.svelte';
@@ -79,6 +80,12 @@
</Card> </Card>
{:else if run} {:else if run}
{@const transactions = run.sections.transactions} {@const transactions = run.sections.transactions}
<AIInsightPanel
module="transactions"
terminalId={filters.terminal === 'all' ? undefined : filters.terminal}
periodId={run.period.periodId}
periodLabel={run.period.jalaliLabelFa}
/>
{#if transactions.state === 'READY'} {#if transactions.state === 'READY'}
<div class="kpi-grid grid gap-3 sm:gap-4"> <div class="kpi-grid grid gap-3 sm:gap-4">
<KpiCard <KpiCard
@@ -146,8 +153,8 @@
><ChartColumnBigIcon class="text-primary size-5" /> روند ترکیب وضعیت‌ها</CardTitle ><ChartColumnBigIcon class="text-primary size-5" /> روند ترکیب وضعیت‌ها</CardTitle
> >
<p class="text-muted-foreground mt-2 max-w-2xl text-xs leading-6"> <p class="text-muted-foreground mt-2 max-w-2xl text-xs leading-6">
هر خط، تغییر تعداد یک وضعیت را در طول زمان نشان می‌دهد؛ جزئیات تعداد و سهم هر نقطه با هر خط، تغییر تعداد یک وضعیت را در طول زمان نشان می‌دهد؛ جزئیات تعداد و سهم هر نقطه با تمرکز
تمرکز صفحه‌کلید یا اشاره‌گر در دسترس است. صفحه‌کلید یا اشاره‌گر در دسترس است.
</p> </p>
</div> </div>
<Badge variant="secondary" class="rounded-md">{run.period.jalaliLabelFa}</Badge> <Badge variant="secondary" class="rounded-md">{run.period.jalaliLabelFa}</Badge>