133 lines
4.5 KiB
Svelte
133 lines
4.5 KiB
Svelte
<script lang="ts">
|
|
import { scale } from 'svelte/transition';
|
|
|
|
import type { components } from '$lib/api/schema';
|
|
import { formatMoney, formatValue } from '$lib/format';
|
|
|
|
type Point = components['schemas']['PaymentHistoryPoint'];
|
|
let { data }: { data: Point[] } = $props();
|
|
let active = $state<number | null>(null);
|
|
|
|
const width = 760;
|
|
const height = 250;
|
|
const pad = { x: 34, y: 28 };
|
|
const max = $derived(Math.max(1, ...data.map((point) => point.bankPaidSessions)));
|
|
const positions = $derived(
|
|
data.map((point, index) => ({
|
|
...point,
|
|
x: pad.x + (index * (width - pad.x * 2)) / Math.max(1, data.length - 1),
|
|
y: height - pad.y - (point.bankPaidSessions / max) * (height - pad.y * 2),
|
|
})),
|
|
);
|
|
const path = $derived(
|
|
positions.map((point, index) => `${index ? 'L' : 'M'} ${point.x} ${point.y}`).join(' '),
|
|
);
|
|
|
|
function persianDate(value: string) {
|
|
return new Intl.DateTimeFormat('fa-IR-u-ca-persian', { month: 'short', day: 'numeric' }).format(
|
|
new Date(`${value}T12:00:00Z`),
|
|
);
|
|
}
|
|
|
|
function tooltipPosition(point: (typeof positions)[number]) {
|
|
const horizontalPercent = (point.x / width) * 100;
|
|
const verticalPercent = (point.y / height) * 100;
|
|
const placeBelow = verticalPercent < 34;
|
|
const top = placeBelow ? verticalPercent + 5 : verticalPercent - 5;
|
|
return `left:clamp(5.75rem, ${horizontalPercent}%, calc(100% - 5.75rem)); top:${top}%; transform:translate(-50%, ${placeBelow ? '0' : '-100%'});`;
|
|
}
|
|
</script>
|
|
|
|
<div class="relative min-h-[260px] w-full overflow-hidden" dir="ltr">
|
|
{#if data.length === 0}
|
|
<div class="text-muted-foreground grid h-[260px] place-items-center text-sm">
|
|
دادهای برای این بازه ثبت نشده است.
|
|
</div>
|
|
{:else}
|
|
<svg
|
|
viewBox={`0 0 ${width} ${height}`}
|
|
class="h-auto min-h-[250px] w-full overflow-visible"
|
|
role="img"
|
|
aria-label="روند پرداختهای موفق"
|
|
>
|
|
<defs>
|
|
<linearGradient id="payment-area" x1="0" x2="0" y1="0" y2="1">
|
|
<stop offset="0" stop-color="var(--primary)" stop-opacity=".22" />
|
|
<stop offset="1" stop-color="var(--primary)" stop-opacity="0" />
|
|
</linearGradient>
|
|
</defs>
|
|
{#each [0, 1, 2, 3, 4] as row}
|
|
<line
|
|
x1={pad.x}
|
|
x2={width - pad.x}
|
|
y1={pad.y + (row * (height - pad.y * 2)) / 4}
|
|
y2={pad.y + (row * (height - pad.y * 2)) / 4}
|
|
stroke="var(--border)"
|
|
stroke-dasharray="4 6"
|
|
/>
|
|
{/each}
|
|
<path
|
|
d={`${path} L ${positions.at(-1)?.x} ${height - pad.y} L ${positions[0]?.x} ${height - pad.y} Z`}
|
|
fill="url(#payment-area)"
|
|
/>
|
|
<path
|
|
d={path}
|
|
fill="none"
|
|
stroke="var(--primary)"
|
|
stroke-width="3"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class="chart-line"
|
|
/>
|
|
{#each positions as point, index}
|
|
<circle
|
|
cx={point.x}
|
|
cy={point.y}
|
|
r={active === index ? 6 : 3.5}
|
|
fill="var(--card)"
|
|
stroke="var(--primary)"
|
|
stroke-width="3"
|
|
tabindex="0"
|
|
role="button"
|
|
aria-label={`${persianDate(point.localDate)}، ${formatValue(point.bankPaidSessions)} پرداخت موفق`}
|
|
onmouseenter={() => (active = index)}
|
|
onmouseleave={() => (active = null)}
|
|
onfocus={() => (active = index)}
|
|
onblur={() => (active = null)}
|
|
/>
|
|
{/each}
|
|
</svg>
|
|
{#if active !== null && positions[active]}
|
|
<div
|
|
class="pointer-events-none absolute z-10 w-44 transition-[left,top,transform] duration-150 ease-out motion-reduce:transition-none"
|
|
style={tooltipPosition(positions[active])}
|
|
dir="rtl"
|
|
>
|
|
<div
|
|
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>
|
|
<span class="block text-white/75"
|
|
>{formatValue(positions[active].bankPaidSessions)} پرداخت موفق</span
|
|
>
|
|
<span class="block text-white/75">{formatMoney(positions[active].bankPaidAmountRial)}</span>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.chart-line {
|
|
stroke-dasharray: 1800;
|
|
stroke-dashoffset: 1800;
|
|
animation: draw-line 900ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
|
}
|
|
@keyframes draw-line {
|
|
to {
|
|
stroke-dashoffset: 0;
|
|
}
|
|
}
|
|
</style>
|