Files
CRM_Montzar/apps/web/src/components/dashboard/HomeSalesTrend.vue
T

182 lines
4.6 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue';
import type { HomeDashboardTrendPointDto } from '@mento/shared';
const props = defineProps<{
trend: HomeDashboardTrendPointDto[];
changePercent: number | null;
formatChange: (value: number | null) => string;
formatAmount: (value: number) => string;
}>();
const demoTrend: HomeDashboardTrendPointDto[] = [
{ monthKey: '01', monthLabel: 'فروردین', amount: 980_000_000 },
{ monthKey: '02', monthLabel: 'اردیبهشت', amount: 1_250_000_000 },
{ monthKey: '03', monthLabel: 'خرداد', amount: 1_480_000_000 },
{ monthKey: '04', monthLabel: 'تیر', amount: 1_860_000_000 },
{ monthKey: '05', monthLabel: 'مرداد', amount: 2_450_000_000 },
];
const trend = computed(() => (props.trend.length ? props.trend : demoTrend));
const change = computed(() => props.changePercent ?? 14);
const points = computed(() => {
const rows = trend.value;
const max = Math.max(...rows.map((p) => p.amount), 1);
const w = 360;
const h = 150;
const padX = 16;
const padY = 36;
return rows.map((p, i) => ({
...p,
x: padX + (i / Math.max(rows.length - 1, 1)) * (w - padX * 2),
y: h - padY - (p.amount / max) * (h - padY * 2),
}));
});
const line = computed(() => points.value.map((p) => `${p.x},${p.y}`).join(' '));
const area = computed(() => {
if (!points.value.length) return '';
const first = points.value[0]!;
const last = points.value[points.value.length - 1]!;
return `${line.value} ${last.x},160 ${first.x},160`;
});
const highlight = computed(() => {
if (!points.value.length) return null;
return points.value.reduce((best, cur) => (cur.amount >= best.amount ? cur : best));
});
</script>
<template>
<article class="panel">
<header class="panel-head">
<h2>روند فروش</h2>
<span class="mini-badge" :class="{ down: change < 0 }">
{{ formatChange(change) }} نسبت به ماه قبل
</span>
</header>
<div class="chart-wrap">
<svg viewBox="0 0 360 170" class="line-chart" aria-hidden="true">
<defs>
<linearGradient id="homeTrendFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#14b8a6" stop-opacity="0.28" />
<stop offset="100%" stop-color="#14b8a6" stop-opacity="0" />
</linearGradient>
</defs>
<line
v-for="y in [40, 75, 110, 145]"
:key="y"
x1="12"
:y1="y"
x2="348"
:y2="y"
stroke="#eef2f7"
stroke-width="1"
/>
<polygon v-if="area" :points="area" fill="url(#homeTrendFill)" />
<polyline
v-if="line"
:points="line"
fill="none"
stroke="#14b8a6"
stroke-width="3.2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<g v-for="(p, i) in points" :key="i">
<circle :cx="p.x" :cy="p.y" r="4.5" fill="#fff" stroke="#14b8a6" stroke-width="2.4" />
</g>
<g v-if="highlight">
<rect
:x="Math.min(Math.max(highlight.x - 52, 4), 256)"
:y="highlight.y - 32"
width="104"
height="24"
rx="8"
fill="#0f766e"
/>
<text
:x="Math.min(Math.max(highlight.x, 56), 308)"
:y="highlight.y - 16"
text-anchor="middle"
fill="#fff"
font-size="9.5"
font-family="IRANSans, Tahoma, sans-serif"
>
{{ formatAmount(highlight.amount) }}
</text>
</g>
</svg>
<div class="chart-labels">
<span v-for="p in trend" :key="p.monthKey">{{ p.monthLabel }}</span>
</div>
</div>
</article>
</template>
<style scoped>
.panel {
background: #fff;
border-radius: 20px;
padding: 1rem 1.1rem 0.9rem;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
border: 1px solid #eef2f7;
min-width: 0;
height: 100%;
display: flex;
flex-direction: column;
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0;
}
h2 {
margin: 0;
font-size: 0.95rem;
color: #0f172a;
}
.mini-badge {
font-size: 0.7rem;
font-weight: 700;
color: #16a34a;
background: #dcfce7;
border-radius: 999px;
padding: 0.18rem 0.5rem;
white-space: nowrap;
}
.mini-badge.down {
color: #dc2626;
background: #fee2e2;
}
.chart-wrap {
display: grid;
gap: 0.15rem;
margin-top: auto;
padding-top: 2.25rem;
}
.line-chart {
width: 100%;
height: auto;
}
.chart-labels {
display: flex;
justify-content: space-between;
color: #94a3b8;
font-size: 0.72rem;
padding: 0 0.35rem;
}
</style>