feat: show hourly labels for daily charts
This commit is contained in:
2
src/lib/api/schema.d.ts
vendored
2
src/lib/api/schema.d.ts
vendored
@@ -723,6 +723,8 @@ export interface components {
|
|||||||
PaymentHistoryPoint: {
|
PaymentHistoryPoint: {
|
||||||
/** Format: date */
|
/** Format: date */
|
||||||
localDate: string;
|
localDate: string;
|
||||||
|
/** @description Tehran-local hour bucket, present only for DAY periods. */
|
||||||
|
localHour?: number;
|
||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
totalSessions: number;
|
totalSessions: number;
|
||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
|
|||||||
@@ -177,14 +177,18 @@
|
|||||||
><ChartNoAxesCombinedIcon class="text-primary size-5" /> روند پرداختهای موفق</CardTitle
|
><ChartNoAxesCombinedIcon class="text-primary size-5" /> روند پرداختهای موفق</CardTitle
|
||||||
>
|
>
|
||||||
<p class="text-muted-foreground mt-1 text-xs">
|
<p class="text-muted-foreground mt-1 text-xs">
|
||||||
برای دیدن مبلغ و تعداد هر روز، روی نقطههای نمودار بروید.
|
برای دیدن مبلغ و تعداد هر {run.period.periodType === 'DAY' ? 'ساعت' : 'روز'}، روی نقطههای
|
||||||
|
نمودار بروید.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="secondary" class="rounded-md">{run.period.jalaliLabelFa}</Badge>
|
<Badge variant="secondary" class="rounded-md">{run.period.jalaliLabelFa}</Badge>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="px-3 pt-5 sm:px-6"
|
<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>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
import { formatMoney, formatValue } from '$lib/format';
|
import { formatMoney, formatValue } from '$lib/format';
|
||||||
|
|
||||||
type Point = components['schemas']['PaymentHistoryPoint'];
|
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);
|
let active = $state<number | null>(null);
|
||||||
|
|
||||||
const width = 760;
|
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]) {
|
function tooltipPosition(point: (typeof positions)[number]) {
|
||||||
const horizontalPercent = (point.x / width) * 100;
|
const horizontalPercent = (point.x / width) * 100;
|
||||||
const verticalPercent = (point.y / height) * 100;
|
const verticalPercent = (point.y / height) * 100;
|
||||||
@@ -104,7 +126,7 @@
|
|||||||
fill="var(--muted-foreground)"
|
fill="var(--muted-foreground)"
|
||||||
font-size="9"
|
font-size="9"
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
direction="rtl">{persianDate(tick.localDate)}</text
|
direction="rtl">{axisLabel(tick)}</text
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
<text
|
<text
|
||||||
@@ -113,7 +135,7 @@
|
|||||||
fill="var(--muted-foreground)"
|
fill="var(--muted-foreground)"
|
||||||
font-size="10"
|
font-size="10"
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
direction="rtl">تاریخ</text
|
direction="rtl">{periodType === 'DAY' ? 'ساعت' : 'تاریخ'}</text
|
||||||
>
|
>
|
||||||
<text
|
<text
|
||||||
x={-((pad.top + height - pad.bottom) / 2)}
|
x={-((pad.top + height - pad.bottom) / 2)}
|
||||||
@@ -147,7 +169,7 @@
|
|||||||
stroke-width="3"
|
stroke-width="3"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="button"
|
role="button"
|
||||||
aria-label={`${persianDate(point.localDate)}، ${formatValue(point.bankPaidSessions)} پرداخت موفق`}
|
aria-label={`${fullPointLabel(point)}، ${formatValue(point.bankPaidSessions)} پرداخت موفق`}
|
||||||
onmouseenter={() => (active = index)}
|
onmouseenter={() => (active = index)}
|
||||||
onmouseleave={() => (active = null)}
|
onmouseleave={() => (active = null)}
|
||||||
onfocus={() => (active = index)}
|
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"
|
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 }}
|
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"
|
<span class="block text-white/75"
|
||||||
>{formatValue(positions[active].bankPaidSessions)} پرداخت موفق</span
|
>{formatValue(positions[active].bankPaidSessions)} پرداخت موفق</span
|
||||||
>
|
>
|
||||||
|
|||||||
32
src/lib/components/PaymentHistoryChart.test.ts
Normal file
32
src/lib/components/PaymentHistoryChart.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user