feat: show hourly labels for daily charts

This commit is contained in:
2026-08-21 16:28:13 +08:00
parent 3db3bd18bb
commit 5616a330cd
4 changed files with 67 additions and 7 deletions

View File

@@ -723,6 +723,8 @@ export interface components {
PaymentHistoryPoint: {
/** Format: date */
localDate: string;
/** @description Tehran-local hour bucket, present only for DAY periods. */
localHour?: number;
/** Format: int64 */
totalSessions: number;
/** Format: int64 */

View File

@@ -177,14 +177,18 @@
><ChartNoAxesCombinedIcon class="text-primary size-5" /> روند پرداخت‌های موفق</CardTitle
>
<p class="text-muted-foreground mt-1 text-xs">
برای دیدن مبلغ و تعداد هر روز، روی نقطه‌های نمودار بروید.
برای دیدن مبلغ و تعداد هر {run.period.periodType === 'DAY' ? 'ساعت' : 'روز'}، روی نقطه‌های
نمودار بروید.
</p>
</div>
<Badge variant="secondary" class="rounded-md">{run.period.jalaliLabelFa}</Badge>
</div>
</CardHeader>
<CardContent class="px-3 pt-5 sm:px-6"
><PaymentHistoryChart data={overview.data.paymentHistory} /></CardContent
><PaymentHistoryChart
data={overview.data.paymentHistory}
periodType={run.period.periodType}
/></CardContent
>
</Card>

View File

@@ -5,7 +5,8 @@
import { formatMoney, formatValue } from '$lib/format';
type Point = components['schemas']['PaymentHistoryPoint'];
let { data }: { data: Point[] } = $props();
type PeriodType = components['schemas']['PeriodType'];
let { data, periodType }: { data: Point[]; periodType: PeriodType } = $props();
let active = $state<number | null>(null);
const width = 760;
@@ -40,6 +41,27 @@
);
}
function persianHour(value: number) {
const hour = new Intl.NumberFormat('fa-IR', {
minimumIntegerDigits: 2,
maximumFractionDigits: 0,
useGrouping: false,
}).format(value);
return `${hour}:۰۰`;
}
function axisLabel(point: Point) {
return periodType === 'DAY' && point.localHour !== undefined
? persianHour(point.localHour)
: persianDate(point.localDate);
}
function fullPointLabel(point: Point) {
return periodType === 'DAY' && point.localHour !== undefined
? `${persianDate(point.localDate)}، ساعت ${persianHour(point.localHour)}`
: persianDate(point.localDate);
}
function tooltipPosition(point: (typeof positions)[number]) {
const horizontalPercent = (point.x / width) * 100;
const verticalPercent = (point.y / height) * 100;
@@ -104,7 +126,7 @@
fill="var(--muted-foreground)"
font-size="9"
text-anchor="middle"
direction="rtl">{persianDate(tick.localDate)}</text
direction="rtl">{axisLabel(tick)}</text
>
{/each}
<text
@@ -113,7 +135,7 @@
fill="var(--muted-foreground)"
font-size="10"
text-anchor="middle"
direction="rtl">تاریخ</text
direction="rtl">{periodType === 'DAY' ? 'ساعت' : 'تاریخ'}</text
>
<text
x={-((pad.top + height - pad.bottom) / 2)}
@@ -147,7 +169,7 @@
stroke-width="3"
tabindex="0"
role="button"
aria-label={`${persianDate(point.localDate)}، ${formatValue(point.bankPaidSessions)} پرداخت موفق`}
aria-label={`${fullPointLabel(point)}، ${formatValue(point.bankPaidSessions)} پرداخت موفق`}
onmouseenter={() => (active = index)}
onmouseleave={() => (active = null)}
onfocus={() => (active = index)}
@@ -165,7 +187,7 @@
class="rounded-md bg-zinc-950 p-3 text-right text-xs leading-6 text-white shadow-xl"
transition:scale={{ duration: 150, start: 0.96 }}
>
<strong class="block">{persianDate(positions[active].localDate)}</strong>
<strong class="block">{fullPointLabel(positions[active])}</strong>
<span class="block text-white/75"
>{formatValue(positions[active].bankPaidSessions)} پرداخت موفق</span
>

View File

@@ -0,0 +1,32 @@
import { render, screen } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import PaymentHistoryChart from './PaymentHistoryChart.svelte';
describe('PaymentHistoryChart', () => {
it('uses Persian hour labels for a one-day period', () => {
render(PaymentHistoryChart, {
periodType: 'DAY',
data: [
{
localDate: '2026-07-01',
localHour: 0,
totalSessions: 10,
bankPaidSessions: 8,
bankPaidAmountRial: 800_000,
},
{
localDate: '2026-07-01',
localHour: 12,
totalSessions: 14,
bankPaidSessions: 11,
bankPaidAmountRial: 1_100_000,
},
],
});
expect(screen.getByText('۰۰:۰۰')).not.toBeNull();
expect(screen.getByText('۱۲:۰۰')).not.toBeNull();
expect(screen.getByText('ساعت')).not.toBeNull();
});
});