feat: migrate CRM screens to Ant Design Vue and expand home dashboard
Preserve the local Ant UI work across views/components and add the new home dashboard chart panels before merging latest main.
This commit is contained in:
@@ -1,53 +1,311 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import {
|
||||
Alert as AAlert,
|
||||
Avatar as AAvatar,
|
||||
Card as ACard,
|
||||
Col as ACol,
|
||||
Divider as ADivider,
|
||||
Empty as AEmpty,
|
||||
Flex as AFlex,
|
||||
List as AList,
|
||||
Row as ARow,
|
||||
Statistic as AStatistic,
|
||||
Spin as ASpin,
|
||||
Typography,
|
||||
theme,
|
||||
} from 'ant-design-vue';
|
||||
import type { Component } from 'vue';
|
||||
import {
|
||||
AccountBookOutlined,
|
||||
ArrowDownOutlined,
|
||||
ArrowUpOutlined,
|
||||
BankOutlined,
|
||||
CalendarOutlined,
|
||||
FallOutlined,
|
||||
RiseOutlined,
|
||||
WalletOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { use } from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { BarChart, PieChart } from 'echarts/charts';
|
||||
import {
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
TooltipComponent,
|
||||
} from 'echarts/components';
|
||||
import type { ComposeOption } from 'echarts/core';
|
||||
import type { BarSeriesOption, PieSeriesOption } from 'echarts/charts';
|
||||
import type {
|
||||
GridComponentOption,
|
||||
LegendComponentOption,
|
||||
TooltipComponentOption,
|
||||
} from 'echarts/components';
|
||||
import VChart from 'vue-echarts';
|
||||
import { api } from '@/lib/api';
|
||||
import type { TreasuryDashboardDto } from '@mento/shared';
|
||||
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
||||
import {
|
||||
TREASURY_DASHBOARD_MOCK,
|
||||
isTreasuryDashboardEmpty,
|
||||
} from './treasury-dashboard-mock';
|
||||
import { toman } from './treasury-format';
|
||||
|
||||
use([CanvasRenderer, BarChart, PieChart, GridComponent, TooltipComponent, LegendComponent]);
|
||||
|
||||
type ChartOption = ComposeOption<
|
||||
| BarSeriesOption
|
||||
| PieSeriesOption
|
||||
| GridComponentOption
|
||||
| TooltipComponentOption
|
||||
| LegendComponentOption
|
||||
>;
|
||||
|
||||
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
||||
const AListItem = AList.Item;
|
||||
const AListItemMeta = AList.Item.Meta;
|
||||
const { token } = theme.useToken();
|
||||
|
||||
function num(value: string | number | null | undefined) {
|
||||
const n = Number(value ?? 0);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
|
||||
type MetricKey = keyof Pick<
|
||||
TreasuryDashboardDto,
|
||||
| 'bookedIncome'
|
||||
| 'actualReceipts'
|
||||
| 'bookedExpense'
|
||||
| 'actualPayments'
|
||||
| 'operatingProfit'
|
||||
| 'receivables'
|
||||
| 'payables'
|
||||
| 'totalBalance'
|
||||
>;
|
||||
|
||||
type MetricCard = {
|
||||
key: MetricKey;
|
||||
label: string;
|
||||
hint: string;
|
||||
icon: Component;
|
||||
};
|
||||
|
||||
const data = ref<TreasuryDashboardDto | null>(null);
|
||||
const error = ref('');
|
||||
const loading = ref(true);
|
||||
const usingMock = ref(false);
|
||||
|
||||
const cards = [
|
||||
{ key: 'bookedIncome', label: 'درآمد ثبتشده', hint: 'تعهدی' },
|
||||
{ key: 'actualReceipts', label: 'دریافت واقعی', hint: 'ورود به حساب' },
|
||||
{ key: 'bookedExpense', label: 'هزینه ثبتشده', hint: 'تعهدی' },
|
||||
{ key: 'actualPayments', label: 'پرداخت واقعی', hint: 'خروج از حساب' },
|
||||
{ key: 'operatingProfit', label: 'سود یا زیان عملیاتی', hint: 'درآمد ثبتشده − هزینه ثبتشده' },
|
||||
{ key: 'receivables', label: 'مطالبات وصولنشده', hint: 'درآمد بدون دریافت کامل' },
|
||||
{ key: 'payables', label: 'تعهدات پرداخت', hint: 'هزینه بدون پرداخت کامل' },
|
||||
{ key: 'totalBalance', label: 'موجودی حسابها', hint: 'صندوق و بانک' },
|
||||
] as const;
|
||||
const heroCards: MetricCard[] = [
|
||||
{
|
||||
key: 'totalBalance',
|
||||
label: 'موجودی حسابها',
|
||||
hint: 'صندوق و بانک',
|
||||
icon: WalletOutlined,
|
||||
},
|
||||
{
|
||||
key: 'operatingProfit',
|
||||
label: 'سود یا زیان عملیاتی',
|
||||
hint: 'درآمد ثبتشده − هزینه ثبتشده',
|
||||
icon: RiseOutlined,
|
||||
},
|
||||
{
|
||||
key: 'receivables',
|
||||
label: 'مطالبات وصولنشده',
|
||||
hint: 'درآمد بدون دریافت کامل',
|
||||
icon: CalendarOutlined,
|
||||
},
|
||||
];
|
||||
|
||||
const secondaryCards: MetricCard[] = [
|
||||
{
|
||||
key: 'bookedIncome',
|
||||
label: 'درآمد ثبتشده',
|
||||
hint: 'تعهدی',
|
||||
icon: ArrowDownOutlined,
|
||||
},
|
||||
{
|
||||
key: 'actualReceipts',
|
||||
label: 'دریافت واقعی',
|
||||
hint: 'ورود به حساب',
|
||||
icon: BankOutlined,
|
||||
},
|
||||
{
|
||||
key: 'bookedExpense',
|
||||
label: 'هزینه ثبتشده',
|
||||
hint: 'تعهدی',
|
||||
icon: FallOutlined,
|
||||
},
|
||||
{
|
||||
key: 'actualPayments',
|
||||
label: 'پرداخت واقعی',
|
||||
hint: 'خروج از حساب',
|
||||
icon: ArrowUpOutlined,
|
||||
},
|
||||
{
|
||||
key: 'payables',
|
||||
label: 'تعهدات پرداخت',
|
||||
hint: 'هزینه بدون پرداخت کامل',
|
||||
icon: AccountBookOutlined,
|
||||
},
|
||||
];
|
||||
|
||||
const softCardStyle = computed(() => ({
|
||||
background: token.value.colorPrimaryBg,
|
||||
borderRadius: `${token.value.borderRadiusLG}px`,
|
||||
boxShadow: token.value.boxShadow,
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
/** کارت سفید بدون بردر (KPI فرعی و چارتها) */
|
||||
const secondaryCardStyle = computed(() => ({
|
||||
background: token.value.colorBgContainer,
|
||||
borderRadius: `${token.value.borderRadiusLG}px`,
|
||||
boxShadow: token.value.boxShadow,
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
/** Soft teal family — primary + muted companions (not loud rainbow) */
|
||||
const chartColors = computed(() => {
|
||||
const primary = token.value.colorPrimary;
|
||||
return {
|
||||
primary,
|
||||
primarySoft: token.value.colorPrimaryBg,
|
||||
muted: token.value.colorTextQuaternary || '#a7f3d0',
|
||||
ink: token.value.colorTextSecondary,
|
||||
axis: token.value.colorBorderSecondary,
|
||||
tooltipBg: token.value.colorBgElevated,
|
||||
font: token.value.fontFamily,
|
||||
};
|
||||
});
|
||||
|
||||
const flowChartOption = computed<ChartOption>(() => {
|
||||
if (!data.value) return {};
|
||||
const c = chartColors.value;
|
||||
const categories = ['درآمد تعهدی', 'دریافت واقعی', 'هزینه تعهدی', 'پرداخت واقعی'];
|
||||
const values = [
|
||||
num(data.value.bookedIncome),
|
||||
num(data.value.actualReceipts),
|
||||
num(data.value.bookedExpense),
|
||||
num(data.value.actualPayments),
|
||||
];
|
||||
return {
|
||||
color: [c.primary, '#34d399', '#6ee7b7', '#a7f3d0'],
|
||||
textStyle: { fontFamily: c.font, color: c.ink },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: c.tooltipBg,
|
||||
borderColor: c.axis,
|
||||
textStyle: { fontFamily: c.font, color: c.ink },
|
||||
valueFormatter: (v) => toman(Number(v)),
|
||||
},
|
||||
grid: { left: 8, right: 12, top: 24, bottom: 8, containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisLine: { lineStyle: { color: c.axis } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
fontFamily: c.font,
|
||||
color: c.ink,
|
||||
interval: 0,
|
||||
rotate: 0,
|
||||
fontSize: 11,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: { lineStyle: { color: c.axis, type: 'dashed' } },
|
||||
axisLabel: {
|
||||
fontFamily: c.font,
|
||||
color: c.ink,
|
||||
formatter: (v: number) =>
|
||||
new Intl.NumberFormat('fa-IR', { notation: 'compact' }).format(v),
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
data: values.map((value, index) => ({
|
||||
value,
|
||||
itemStyle: {
|
||||
color: [c.primary, '#34d399', '#6ee7b7', '#a7f3d0'][index],
|
||||
borderRadius: [8, 8, 0, 0],
|
||||
},
|
||||
})),
|
||||
barMaxWidth: 36,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const mixChartOption = computed<ChartOption>(() => {
|
||||
if (!data.value) return {};
|
||||
const c = chartColors.value;
|
||||
const slices = [
|
||||
{ name: 'موجودی حسابها', value: Math.max(0, num(data.value.totalBalance)) },
|
||||
{ name: 'مطالبات', value: Math.max(0, num(data.value.receivables)) },
|
||||
{ name: 'تعهدات', value: Math.max(0, num(data.value.payables)) },
|
||||
];
|
||||
const hasValue = slices.some((s) => s.value > 0);
|
||||
return {
|
||||
color: [c.primary, '#34d399', '#a7f3d0'],
|
||||
textStyle: { fontFamily: c.font, color: c.ink },
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: c.tooltipBg,
|
||||
borderColor: c.axis,
|
||||
textStyle: { fontFamily: c.font, color: c.ink },
|
||||
formatter: (params: unknown) => {
|
||||
const p = params as { name?: string; value?: number; percent?: number };
|
||||
return `${p.name}<br/>${toman(p.value)} (${p.percent ?? 0}٪)`;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
bottom: 0,
|
||||
icon: 'circle',
|
||||
itemWidth: 8,
|
||||
itemHeight: 8,
|
||||
textStyle: { fontFamily: c.font, color: c.ink, fontSize: 12 },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['52%', '72%'],
|
||||
center: ['50%', '46%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {
|
||||
borderRadius: 8,
|
||||
borderColor: token.value.colorBgContainer,
|
||||
borderWidth: 3,
|
||||
},
|
||||
label: { show: false },
|
||||
data: hasValue
|
||||
? slices
|
||||
: [{ name: 'بدون داده', value: 1, itemStyle: { color: c.primarySoft } }],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
usingMock.value = false;
|
||||
try {
|
||||
const res = await api.get<TreasuryDashboardDto>('/treasury/dashboard');
|
||||
data.value = res.data;
|
||||
if (isTreasuryDashboardEmpty(res.data)) {
|
||||
data.value = TREASURY_DASHBOARD_MOCK;
|
||||
usingMock.value = true;
|
||||
} else {
|
||||
data.value = res.data;
|
||||
}
|
||||
} catch {
|
||||
error.value = 'بارگذاری داشبورد مالی ناموفق بود.';
|
||||
data.value = TREASURY_DASHBOARD_MOCK;
|
||||
usingMock.value = true;
|
||||
error.value = '';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a-flex vertical :gap="16">
|
||||
<a-flex vertical :gap="token.margin">
|
||||
<div>
|
||||
<a-typography-title :level="3" :style="{ margin: 0 }">مدیریت مالی</a-typography-title>
|
||||
<a-typography-text type="secondary">
|
||||
@@ -58,105 +316,262 @@ onMounted(async () => {
|
||||
<TreasuryNav />
|
||||
|
||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||
<a-alert
|
||||
v-if="usingMock"
|
||||
type="info"
|
||||
show-icon
|
||||
message="در حال نمایش دادهٔ نمونه (ماک) برای پیشنمایش داشبورد هستید."
|
||||
description="وقتی سند و حساب واقعی ثبت شود، همین صفحه دادهٔ واقعی API را نشان میدهد."
|
||||
/>
|
||||
|
||||
<template v-if="data">
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col
|
||||
v-for="card in cards"
|
||||
:key="card.key"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="8"
|
||||
:xl="6"
|
||||
>
|
||||
<a-card :bordered="false">
|
||||
<a-statistic :title="card.label" :value="toman(data[card.key])" />
|
||||
<a-typography-text type="secondary">{{ card.hint }}</a-typography-text>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-divider orientation="right">اقساط نزدیک یا معوق</a-divider>
|
||||
<a-empty
|
||||
v-if="!data.upcomingInstallments.length"
|
||||
description="قسط بازی نیست."
|
||||
/>
|
||||
<a-list
|
||||
v-else
|
||||
item-layout="horizontal"
|
||||
:data-source="data.upcomingInstallments"
|
||||
<a-spin :spinning="loading">
|
||||
<template v-if="data">
|
||||
<a-row :gutter="[token.marginSM, token.marginSM]">
|
||||
<a-col
|
||||
v-for="card in heroCards"
|
||||
:key="card.key"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:lg="8"
|
||||
>
|
||||
<a-card
|
||||
:bordered="false"
|
||||
:body-style="{ padding: `${token.padding}px` }"
|
||||
:style="softCardStyle"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #title>قسط {{ item.position }}</template>
|
||||
<template #description>
|
||||
{{ item.status === 'overdue' ? 'معوق' : 'باز' }} · مانده {{ toman(item.remaining) }}
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a-typography-text strong>{{ toman(item.amount) }}</a-typography-text>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-divider orientation="right">پرداختهای آینده</a-divider>
|
||||
<a-empty
|
||||
v-if="!data.upcomingPayables.length"
|
||||
description="تعهد پرداخت بازی نیست."
|
||||
/>
|
||||
<a-list
|
||||
v-else
|
||||
item-layout="horizontal"
|
||||
:data-source="data.upcomingPayables"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #title>{{ item.title }}</template>
|
||||
<template #description>مانده {{ toman(item.remaining) }}</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a-typography-text strong>{{ toman(item.amount) }}</a-typography-text>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
<a-flex align="center" justify="space-between" :gap="token.marginSM">
|
||||
<div :style="{ minWidth: 0, flex: 1 }">
|
||||
<a-typography-text type="secondary">{{ card.label }}</a-typography-text>
|
||||
<div>
|
||||
<a-typography-text
|
||||
strong
|
||||
:style="{
|
||||
fontSize: '22px',
|
||||
lineHeight: 1.3,
|
||||
color: token.colorPrimary,
|
||||
}"
|
||||
>
|
||||
{{ toman(data[card.key]) }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
|
||||
{{ card.hint }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
<a-avatar
|
||||
:size="48"
|
||||
:style="{
|
||||
background: token.colorPrimary,
|
||||
color: token.colorTextLightSolid,
|
||||
flexShrink: 0,
|
||||
}"
|
||||
>
|
||||
<template #icon><component :is="card.icon" /></template>
|
||||
</a-avatar>
|
||||
</a-flex>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-divider orientation="right">آخرین اسناد</a-divider>
|
||||
<a-empty
|
||||
v-if="!data.recent.length"
|
||||
description="هنوز سندی ثبت نشده است."
|
||||
/>
|
||||
<a-list
|
||||
v-else
|
||||
item-layout="horizontal"
|
||||
:data-source="data.recent"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #title>{{ item.title }}</template>
|
||||
<template #description>
|
||||
{{ item.kind === 'income' ? 'درآمد' : 'هزینه' }} · {{ item.status }}
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a-typography-text strong>{{ toman(item.amount) }}</a-typography-text>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</template>
|
||||
<a-row :gutter="[token.marginSM, token.marginSM]" :style="{ marginTop: `${token.margin}px` }">
|
||||
<a-col
|
||||
v-for="card in secondaryCards"
|
||||
:key="card.key"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="8"
|
||||
:lg="8"
|
||||
>
|
||||
<a-card
|
||||
:bordered="false"
|
||||
:body-style="{ padding: `${token.paddingSM}px` }"
|
||||
:style="secondaryCardStyle"
|
||||
>
|
||||
<a-flex align="center" justify="space-between" :gap="8">
|
||||
<div :style="{ minWidth: 0, flex: 1 }">
|
||||
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
|
||||
{{ card.label }}
|
||||
</a-typography-text>
|
||||
<div>
|
||||
<a-typography-text
|
||||
strong
|
||||
:style="{ fontSize: '15px', color: token.colorPrimary }"
|
||||
>
|
||||
{{ toman(data[card.key]) }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
<a-typography-text type="secondary" :style="{ fontSize: '11px' }">
|
||||
{{ card.hint }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
<a-avatar
|
||||
:size="36"
|
||||
:style="{
|
||||
background: token.colorPrimaryBg,
|
||||
color: token.colorPrimary,
|
||||
flexShrink: 0,
|
||||
}"
|
||||
>
|
||||
<template #icon><component :is="card.icon" /></template>
|
||||
</a-avatar>
|
||||
</a-flex>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="[token.marginSM, token.marginSM]" :style="{ marginTop: `${token.margin}px` }">
|
||||
<a-col :xs="24" :lg="14">
|
||||
<a-card
|
||||
:bordered="false"
|
||||
:style="secondaryCardStyle"
|
||||
>
|
||||
<a-typography-text strong>جریان تعهدی و نقدی</a-typography-text>
|
||||
<a-typography-text
|
||||
type="secondary"
|
||||
:style="{ display: 'block', marginBottom: `${token.marginSM}px`, fontSize: '12px' }"
|
||||
>
|
||||
مقایسه درآمد/هزینه ثبتشده با دریافت و پرداخت واقعی
|
||||
</a-typography-text>
|
||||
<VChart
|
||||
:option="flowChartOption"
|
||||
autoresize
|
||||
:style="{ height: '260px', width: '100%' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="10">
|
||||
<a-card
|
||||
:bordered="false"
|
||||
:style="secondaryCardStyle"
|
||||
>
|
||||
<a-typography-text strong>ترکیب وضعیت مالی</a-typography-text>
|
||||
<a-typography-text
|
||||
type="secondary"
|
||||
:style="{ display: 'block', marginBottom: `${token.marginSM}px`, fontSize: '12px' }"
|
||||
>
|
||||
سهم موجودی، مطالبات و تعهدات
|
||||
</a-typography-text>
|
||||
<VChart
|
||||
:option="mixChartOption"
|
||||
autoresize
|
||||
:style="{ height: '260px', width: '100%' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="[token.marginSM, token.marginSM]" :style="{ marginTop: `${token.margin}px` }">
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card
|
||||
:bordered="false"
|
||||
title="اقساط نزدیک یا معوق"
|
||||
:style="secondaryCardStyle"
|
||||
:body-style="{ paddingBlock: `${token.paddingSM}px` }"
|
||||
>
|
||||
<a-empty
|
||||
v-if="!data.upcomingInstallments.length"
|
||||
description="قسط بازی نیست."
|
||||
:image-style="{ height: '48px' }"
|
||||
/>
|
||||
<a-flex v-else vertical :gap="token.marginXS">
|
||||
<a-flex
|
||||
v-for="item in data.upcomingInstallments"
|
||||
:key="item.id"
|
||||
justify="space-between"
|
||||
align="flex-start"
|
||||
:gap="8"
|
||||
>
|
||||
<div :style="{ minWidth: 0 }">
|
||||
<a-typography-text>قسط {{ item.position }}</a-typography-text>
|
||||
<div>
|
||||
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
|
||||
{{ item.status === 'overdue' ? 'معوق' : item.status === 'partial' ? 'جزئی' : 'باز' }}
|
||||
· مانده {{ toman(item.remaining) }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
</div>
|
||||
<a-typography-text strong :style="{ color: token.colorPrimary, whiteSpace: 'nowrap' }">
|
||||
{{ toman(item.amount) }}
|
||||
</a-typography-text>
|
||||
</a-flex>
|
||||
</a-flex>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card
|
||||
:bordered="false"
|
||||
title="پرداختهای آینده"
|
||||
:style="secondaryCardStyle"
|
||||
:body-style="{ paddingBlock: `${token.paddingSM}px` }"
|
||||
>
|
||||
<a-empty
|
||||
v-if="!data.upcomingPayables.length"
|
||||
description="تعهد پرداخت بازی نیست."
|
||||
:image-style="{ height: '48px' }"
|
||||
/>
|
||||
<a-flex v-else vertical :gap="token.marginXS">
|
||||
<a-flex
|
||||
v-for="item in data.upcomingPayables"
|
||||
:key="item.id"
|
||||
justify="space-between"
|
||||
align="flex-start"
|
||||
:gap="8"
|
||||
>
|
||||
<div :style="{ minWidth: 0 }">
|
||||
<a-typography-text>{{ item.title }}</a-typography-text>
|
||||
<div>
|
||||
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
|
||||
مانده {{ toman(item.remaining) }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
</div>
|
||||
<a-typography-text strong :style="{ color: token.colorPrimary, whiteSpace: 'nowrap' }">
|
||||
{{ toman(item.amount) }}
|
||||
</a-typography-text>
|
||||
</a-flex>
|
||||
</a-flex>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card
|
||||
:bordered="false"
|
||||
title="آخرین اسناد"
|
||||
:style="secondaryCardStyle"
|
||||
:body-style="{ paddingBlock: `${token.paddingSM}px` }"
|
||||
>
|
||||
<a-empty
|
||||
v-if="!data.recent.length"
|
||||
description="هنوز سندی ثبت نشده است."
|
||||
:image-style="{ height: '48px' }"
|
||||
/>
|
||||
<a-flex v-else vertical :gap="token.marginXS">
|
||||
<a-flex
|
||||
v-for="item in data.recent"
|
||||
:key="item.id"
|
||||
justify="space-between"
|
||||
align="flex-start"
|
||||
:gap="8"
|
||||
>
|
||||
<div :style="{ minWidth: 0 }">
|
||||
<a-typography-text>{{ item.title }}</a-typography-text>
|
||||
<div>
|
||||
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
|
||||
{{ item.kind === 'income' ? 'درآمد' : 'هزینه' }} · {{ item.status }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
</div>
|
||||
<a-typography-text strong :style="{ color: token.colorPrimary, whiteSpace: 'nowrap' }">
|
||||
{{ toman(item.amount) }}
|
||||
</a-typography-text>
|
||||
</a-flex>
|
||||
</a-flex>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-spin>
|
||||
</a-flex>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import type {
|
||||
FinancialDocumentDto,
|
||||
FinancialInstallmentDto,
|
||||
TreasuryDashboardDto,
|
||||
} from '@mento/shared';
|
||||
|
||||
function daysFromNow(days: number) {
|
||||
const d = new Date();
|
||||
d.setHours(12, 0, 0, 0);
|
||||
d.setDate(d.getDate() + days);
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
function doc(
|
||||
partial: Pick<
|
||||
FinancialDocumentDto,
|
||||
'id' | 'kind' | 'title' | 'amount' | 'settledAmount' | 'remaining' | 'status' | 'categoryName' | 'partyName'
|
||||
> &
|
||||
Partial<FinancialDocumentDto>,
|
||||
): FinancialDocumentDto {
|
||||
return {
|
||||
bookedAt: daysFromNow(-3),
|
||||
dueAt: daysFromNow(10),
|
||||
categoryId: null,
|
||||
personId: null,
|
||||
accountId: null,
|
||||
accountName: 'صندوق اصلی',
|
||||
method: 'transfer',
|
||||
reference: null,
|
||||
notes: null,
|
||||
createdAt: daysFromNow(-2),
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
function installment(
|
||||
partial: FinancialInstallmentDto,
|
||||
): FinancialInstallmentDto {
|
||||
return partial;
|
||||
}
|
||||
|
||||
/** دیتای نمایشی برای پیشنمایش داشبورد وقتی API خالی است */
|
||||
export const TREASURY_DASHBOARD_MOCK: TreasuryDashboardDto = {
|
||||
bookedIncome: '185000000',
|
||||
actualReceipts: '142500000',
|
||||
bookedExpense: '98000000',
|
||||
actualPayments: '87500000',
|
||||
operatingProfit: '87000000',
|
||||
receivables: '42500000',
|
||||
payables: '18500000',
|
||||
totalBalance: '256000000',
|
||||
upcomingInstallments: [
|
||||
installment({
|
||||
id: 'mock-inst-1',
|
||||
position: 2,
|
||||
dueDate: daysFromNow(-2),
|
||||
amount: '12000000',
|
||||
paidAmount: '0',
|
||||
remaining: '12000000',
|
||||
status: 'overdue',
|
||||
}),
|
||||
installment({
|
||||
id: 'mock-inst-2',
|
||||
position: 3,
|
||||
dueDate: daysFromNow(5),
|
||||
amount: '12000000',
|
||||
paidAmount: '4000000',
|
||||
remaining: '8000000',
|
||||
status: 'partial',
|
||||
}),
|
||||
installment({
|
||||
id: 'mock-inst-3',
|
||||
position: 4,
|
||||
dueDate: daysFromNow(35),
|
||||
amount: '12000000',
|
||||
paidAmount: '0',
|
||||
remaining: '12000000',
|
||||
status: 'open',
|
||||
}),
|
||||
],
|
||||
upcomingPayables: [
|
||||
doc({
|
||||
id: 'mock-pay-1',
|
||||
kind: 'expense',
|
||||
title: 'اجاره دفتر — مهر',
|
||||
amount: '45000000',
|
||||
settledAmount: '30000000',
|
||||
remaining: '15000000',
|
||||
status: 'partial',
|
||||
categoryName: 'اجاره',
|
||||
partyName: 'موجر ساختمان',
|
||||
}),
|
||||
doc({
|
||||
id: 'mock-pay-2',
|
||||
kind: 'expense',
|
||||
title: 'بیمه تکمیلی کارکنان',
|
||||
amount: '8500000',
|
||||
settledAmount: '5000000',
|
||||
remaining: '3500000',
|
||||
status: 'partial',
|
||||
categoryName: 'بیمه',
|
||||
partyName: 'بیمه ایران',
|
||||
}),
|
||||
],
|
||||
recent: [
|
||||
doc({
|
||||
id: 'mock-doc-1',
|
||||
kind: 'income',
|
||||
title: 'پیشپرداخت پروژه آلفا',
|
||||
amount: '75000000',
|
||||
settledAmount: '75000000',
|
||||
remaining: '0',
|
||||
status: 'settled',
|
||||
categoryName: 'فروش خدمات',
|
||||
partyName: 'شرکت آریا',
|
||||
bookedAt: daysFromNow(-1),
|
||||
}),
|
||||
doc({
|
||||
id: 'mock-doc-2',
|
||||
kind: 'expense',
|
||||
title: 'خرید تجهیزات شبکه',
|
||||
amount: '18500000',
|
||||
settledAmount: '18500000',
|
||||
remaining: '0',
|
||||
status: 'settled',
|
||||
categoryName: 'تجهیزات',
|
||||
partyName: 'فروشگاه سختافزار',
|
||||
bookedAt: daysFromNow(-2),
|
||||
}),
|
||||
doc({
|
||||
id: 'mock-doc-3',
|
||||
kind: 'income',
|
||||
title: 'قسط قرارداد بتا',
|
||||
amount: '12000000',
|
||||
settledAmount: '0',
|
||||
remaining: '12000000',
|
||||
status: 'open',
|
||||
categoryName: 'فروش خدمات',
|
||||
partyName: 'گروه سپهر',
|
||||
bookedAt: daysFromNow(-4),
|
||||
}),
|
||||
doc({
|
||||
id: 'mock-doc-4',
|
||||
kind: 'expense',
|
||||
title: 'حقوق مهر — تیم فروش',
|
||||
amount: '42000000',
|
||||
settledAmount: '42000000',
|
||||
remaining: '0',
|
||||
status: 'settled',
|
||||
categoryName: 'حقوق',
|
||||
partyName: null,
|
||||
bookedAt: daysFromNow(-5),
|
||||
}),
|
||||
doc({
|
||||
id: 'mock-doc-5',
|
||||
kind: 'income',
|
||||
title: 'فروش پکیج پشتیبانی',
|
||||
amount: '28000000',
|
||||
settledAmount: '10000000',
|
||||
remaining: '18000000',
|
||||
status: 'partial',
|
||||
categoryName: 'پشتیبانی',
|
||||
partyName: 'کارخانه نوین',
|
||||
bookedAt: daysFromNow(-6),
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export function isTreasuryDashboardEmpty(data: TreasuryDashboardDto | null | undefined) {
|
||||
if (!data) return true;
|
||||
const keys = [
|
||||
data.bookedIncome,
|
||||
data.actualReceipts,
|
||||
data.bookedExpense,
|
||||
data.actualPayments,
|
||||
data.totalBalance,
|
||||
data.receivables,
|
||||
data.payables,
|
||||
] as const;
|
||||
const allZero = keys.every((v) => !Number(v));
|
||||
return (
|
||||
allZero &&
|
||||
data.recent.length === 0 &&
|
||||
data.upcomingInstallments.length === 0 &&
|
||||
data.upcomingPayables.length === 0
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user