2026-10-18 18:49:49 +03:30
|
|
|
|
<script setup lang="ts">
|
2026-10-18 21:06:18 +03:30
|
|
|
|
import { computed, onMounted, ref } from 'vue';
|
2026-11-04 14:16:38 +03:30
|
|
|
|
import axios from 'axios';
|
2026-10-18 21:06:18 +03:30
|
|
|
|
import {
|
|
|
|
|
|
Alert as AAlert,
|
|
|
|
|
|
Button as AButton,
|
|
|
|
|
|
Card as ACard,
|
|
|
|
|
|
Divider as ADivider,
|
|
|
|
|
|
Empty as AEmpty,
|
|
|
|
|
|
Flex as AFlex,
|
|
|
|
|
|
List as AList,
|
|
|
|
|
|
Table as ATable,
|
2026-11-04 14:16:38 +03:30
|
|
|
|
Tabs as ATabs,
|
2026-10-18 21:06:18 +03:30
|
|
|
|
Typography,
|
|
|
|
|
|
} from 'ant-design-vue';
|
|
|
|
|
|
import type { TableColumnsType } from 'ant-design-vue';
|
|
|
|
|
|
import { DownloadOutlined } from '@ant-design/icons-vue';
|
2026-11-04 14:16:38 +03:30
|
|
|
|
import * as XLSX from 'xlsx';
|
|
|
|
|
|
import type { TreasuryPartyBalanceDto } from '@mento/shared';
|
2026-10-18 18:49:49 +03:30
|
|
|
|
import { api } from '@/lib/api';
|
2026-11-04 14:16:38 +03:30
|
|
|
|
import TreasuryPartyBalanceTable from '@/components/treasury/TreasuryPartyBalanceTable.vue';
|
2026-10-18 18:49:49 +03:30
|
|
|
|
import { toman } from './treasury-format';
|
|
|
|
|
|
|
2026-10-21 21:47:00 +03:30
|
|
|
|
const { Text: ATypographyText } = Typography;
|
2026-10-18 21:06:18 +03:30
|
|
|
|
const AListItem = AList.Item;
|
|
|
|
|
|
const AListItemMeta = AList.Item.Meta;
|
2026-11-04 14:16:38 +03:30
|
|
|
|
const ATabPane = ATabs.TabPane;
|
2026-10-18 21:06:18 +03:30
|
|
|
|
|
2026-10-18 18:49:49 +03:30
|
|
|
|
type Period = {
|
|
|
|
|
|
bookedIncome: string;
|
2026-11-08 14:20:00 +03:30
|
|
|
|
invoiceSales: string;
|
|
|
|
|
|
invoiceCredits: string;
|
2026-10-18 18:49:49 +03:30
|
|
|
|
bookedExpense: string;
|
|
|
|
|
|
operatingProfit: string;
|
|
|
|
|
|
cashIn: string;
|
|
|
|
|
|
cashOut: string;
|
|
|
|
|
|
cashNet: string;
|
|
|
|
|
|
expenseCategories: Array<{ name: string; amount: string; share: string }>;
|
|
|
|
|
|
};
|
2026-10-18 21:06:18 +03:30
|
|
|
|
|
|
|
|
|
|
type MetricRow = {
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
current: string;
|
|
|
|
|
|
previous: string;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-10-18 18:49:49 +03:30
|
|
|
|
const current = ref<Period | null>(null);
|
|
|
|
|
|
const previous = ref<Period | null>(null);
|
|
|
|
|
|
const payablesByMonth = ref<Array<{ month: string; amount: string }>>([]);
|
|
|
|
|
|
const receivablesByParty = ref<Array<{ name: string; amount: string }>>([]);
|
2026-11-04 14:16:38 +03:30
|
|
|
|
const debtors = ref<TreasuryPartyBalanceDto[]>([]);
|
|
|
|
|
|
const creditors = ref<TreasuryPartyBalanceDto[]>([]);
|
|
|
|
|
|
const reportTab = ref('periods');
|
2026-10-18 18:49:49 +03:30
|
|
|
|
const error = ref('');
|
2026-11-04 14:16:38 +03:30
|
|
|
|
const balanceError = ref('');
|
|
|
|
|
|
|
|
|
|
|
|
const kindLabel: Record<string, string> = {
|
|
|
|
|
|
person: 'مشتری',
|
|
|
|
|
|
company: 'شرکت',
|
|
|
|
|
|
user: 'کاربر',
|
|
|
|
|
|
};
|
2026-10-18 18:49:49 +03:30
|
|
|
|
|
|
|
|
|
|
const lines = [
|
|
|
|
|
|
['درآمد ثبتشده', 'bookedIncome'],
|
2026-11-08 14:20:00 +03:30
|
|
|
|
['فروش فاکتور', 'invoiceSales'],
|
|
|
|
|
|
['فاکتور برگشتی', 'invoiceCredits'],
|
2026-10-18 18:49:49 +03:30
|
|
|
|
['هزینه ثبتشده', 'bookedExpense'],
|
|
|
|
|
|
['سود عملیاتی', 'operatingProfit'],
|
|
|
|
|
|
['ورود وجه', 'cashIn'],
|
|
|
|
|
|
['خروج وجه', 'cashOut'],
|
|
|
|
|
|
['خالص جریان نقدی', 'cashNet'],
|
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
2026-10-18 21:06:18 +03:30
|
|
|
|
const metricColumns: TableColumnsType<MetricRow> = [
|
|
|
|
|
|
{ title: 'شاخص', dataIndex: 'label', key: 'label' },
|
|
|
|
|
|
{ title: 'این دوره', dataIndex: 'current', key: 'current' },
|
|
|
|
|
|
{ title: 'دوره قبل', dataIndex: 'previous', key: 'previous' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const metricRows = computed<MetricRow[]>(() => {
|
|
|
|
|
|
if (!current.value || !previous.value) return [];
|
|
|
|
|
|
return lines.map(([label, key]) => ({
|
|
|
|
|
|
key,
|
|
|
|
|
|
label,
|
2026-11-08 14:20:00 +03:30
|
|
|
|
current: toman(current.value![key] || '0'),
|
|
|
|
|
|
previous: toman(previous.value![key] || '0'),
|
2026-10-18 21:06:18 +03:30
|
|
|
|
}));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-18 18:49:49 +03:30
|
|
|
|
async function load() {
|
|
|
|
|
|
const { data } = await api.get<{
|
|
|
|
|
|
current: Period;
|
|
|
|
|
|
previous: Period;
|
|
|
|
|
|
payablesByMonth: Array<{ month: string; amount: string }>;
|
|
|
|
|
|
receivablesByParty: Array<{ name: string; amount: string }>;
|
|
|
|
|
|
}>('/treasury/reports');
|
|
|
|
|
|
current.value = data.current;
|
|
|
|
|
|
previous.value = data.previous;
|
|
|
|
|
|
payablesByMonth.value = data.payablesByMonth;
|
|
|
|
|
|
receivablesByParty.value = data.receivablesByParty;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 14:16:38 +03:30
|
|
|
|
function sumRemaining(rows: TreasuryPartyBalanceDto[]) {
|
|
|
|
|
|
return rows.reduce((sum, row) => sum + Number(row.remaining || 0), 0).toFixed(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function exportParties(
|
|
|
|
|
|
rows: TreasuryPartyBalanceDto[],
|
|
|
|
|
|
title: string,
|
|
|
|
|
|
settledLabel: string,
|
|
|
|
|
|
filename: string,
|
|
|
|
|
|
) {
|
|
|
|
|
|
const body = rows.map((row) => [
|
|
|
|
|
|
row.partyName,
|
|
|
|
|
|
(row.partyKind && kindLabel[row.partyKind]) || 'ثبت قبلی',
|
|
|
|
|
|
Number(row.amount),
|
|
|
|
|
|
Number(row.settledAmount),
|
|
|
|
|
|
Number(row.remaining),
|
|
|
|
|
|
]);
|
|
|
|
|
|
const totals = rows.reduce(
|
|
|
|
|
|
(sum, row) => ({
|
|
|
|
|
|
amount: sum.amount + Number(row.amount || 0),
|
|
|
|
|
|
settled: sum.settled + Number(row.settledAmount || 0),
|
|
|
|
|
|
remaining: sum.remaining + Number(row.remaining || 0),
|
|
|
|
|
|
}),
|
|
|
|
|
|
{ amount: 0, settled: 0, remaining: 0 },
|
|
|
|
|
|
);
|
|
|
|
|
|
body.push(['جمع', '', totals.amount, totals.settled, totals.remaining]);
|
|
|
|
|
|
const sheet = XLSX.utils.aoa_to_sheet([
|
|
|
|
|
|
['طرف حساب', 'نوع', 'مبلغ کل', settledLabel, 'مانده'],
|
|
|
|
|
|
...body,
|
|
|
|
|
|
]);
|
|
|
|
|
|
const book = XLSX.utils.book_new();
|
|
|
|
|
|
XLSX.utils.book_append_sheet(book, sheet, title);
|
|
|
|
|
|
XLSX.writeFile(book, filename);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadBalances() {
|
|
|
|
|
|
const [income, expense] = await Promise.all([
|
|
|
|
|
|
api.get<TreasuryPartyBalanceDto[]>('/treasury/party-balances', { params: { kind: 'income' } }),
|
|
|
|
|
|
api.get<TreasuryPartyBalanceDto[]>('/treasury/party-balances', { params: { kind: 'expense' } }),
|
|
|
|
|
|
]);
|
|
|
|
|
|
debtors.value = income.data;
|
|
|
|
|
|
creditors.value = expense.data;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 18:49:49 +03:30
|
|
|
|
async function download() {
|
|
|
|
|
|
const { data } = await api.get<Blob>('/treasury/reports/export', { responseType: 'blob' });
|
|
|
|
|
|
const url = URL.createObjectURL(data);
|
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
|
link.href = url;
|
|
|
|
|
|
link.download = 'treasury-report.xlsx';
|
|
|
|
|
|
link.click();
|
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
void load().catch(() => {
|
|
|
|
|
|
error.value = 'گزارش بارگذاری نشد.';
|
|
|
|
|
|
});
|
2026-11-04 14:16:38 +03:30
|
|
|
|
void loadBalances().catch((e: unknown) => {
|
|
|
|
|
|
if (axios.isAxiosError(e) && (e.response?.status === 404 || e.response?.status === 403)) {
|
|
|
|
|
|
debtors.value = [];
|
|
|
|
|
|
creditors.value = [];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
balanceError.value = 'مانده بدهکاران و بستانکاران بارگذاری نشد.';
|
|
|
|
|
|
});
|
2026-10-18 18:49:49 +03:30
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-10-21 21:47:00 +03:30
|
|
|
|
<a-flex vertical :gap="16">
|
|
|
|
|
|
<a-flex justify="flex-end">
|
2026-10-18 21:06:18 +03:30
|
|
|
|
<a-button @click="download">
|
|
|
|
|
|
<template #icon><DownloadOutlined /></template>
|
|
|
|
|
|
خروجی اکسل
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-flex>
|
2026-10-18 18:49:49 +03:30
|
|
|
|
|
2026-10-18 21:06:18 +03:30
|
|
|
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
2026-11-04 14:16:38 +03:30
|
|
|
|
<a-alert v-if="balanceError" type="error" show-icon :message="balanceError" />
|
2026-10-18 21:06:18 +03:30
|
|
|
|
|
|
|
|
|
|
<a-card :bordered="false">
|
2026-11-04 14:16:38 +03:30
|
|
|
|
<a-tabs v-model:activeKey="reportTab">
|
|
|
|
|
|
<a-tab-pane key="periods" tab="مقایسه دورهها">
|
|
|
|
|
|
<a-flex vertical :gap="16">
|
2026-10-18 21:06:18 +03:30
|
|
|
|
<a-table
|
|
|
|
|
|
v-if="metricRows.length"
|
|
|
|
|
|
:columns="metricColumns"
|
|
|
|
|
|
:data-source="metricRows"
|
|
|
|
|
|
:pagination="false"
|
|
|
|
|
|
row-key="key"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<a-empty v-else description="دادهای برای نمایش نیست." />
|
|
|
|
|
|
|
|
|
|
|
|
<a-divider orientation="right">مطالبات به تفکیک طرف حساب</a-divider>
|
|
|
|
|
|
<a-empty v-if="!receivablesByParty.length" description="مورد بازی نیست." />
|
|
|
|
|
|
<a-list v-else item-layout="horizontal" :data-source="receivablesByParty">
|
|
|
|
|
|
<template #renderItem="{ item }">
|
|
|
|
|
|
<a-list-item>
|
|
|
|
|
|
<template #actions>
|
|
|
|
|
|
<a-typography-text strong>{{ toman(item.amount) }}</a-typography-text>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<a-list-item-meta>
|
|
|
|
|
|
<template #title>{{ item.name }}</template>
|
|
|
|
|
|
</a-list-item-meta>
|
|
|
|
|
|
</a-list-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list>
|
|
|
|
|
|
|
|
|
|
|
|
<a-divider orientation="right">تعهدات به تفکیک ماه</a-divider>
|
|
|
|
|
|
<a-empty v-if="!payablesByMonth.length" description="تعهد بازی نیست." />
|
|
|
|
|
|
<a-list v-else item-layout="horizontal" :data-source="payablesByMonth">
|
|
|
|
|
|
<template #renderItem="{ item }">
|
|
|
|
|
|
<a-list-item>
|
|
|
|
|
|
<template #actions>
|
|
|
|
|
|
<a-typography-text strong>{{ toman(item.amount) }}</a-typography-text>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<a-list-item-meta>
|
|
|
|
|
|
<template #title>{{ item.month }}</template>
|
|
|
|
|
|
</a-list-item-meta>
|
|
|
|
|
|
</a-list-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-if="current">
|
|
|
|
|
|
<a-divider orientation="right">سهم هزینهها</a-divider>
|
|
|
|
|
|
<a-empty v-if="!current.expenseCategories.length" description="دستهای نیست." />
|
|
|
|
|
|
<a-list v-else item-layout="horizontal" :data-source="current.expenseCategories">
|
|
|
|
|
|
<template #renderItem="{ item }">
|
|
|
|
|
|
<a-list-item>
|
|
|
|
|
|
<template #actions>
|
|
|
|
|
|
<a-typography-text strong>
|
|
|
|
|
|
{{ toman(item.amount) }} · {{ item.share }}٪
|
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<a-list-item-meta>
|
|
|
|
|
|
<template #title>{{ item.name }}</template>
|
|
|
|
|
|
</a-list-item-meta>
|
|
|
|
|
|
</a-list-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list>
|
|
|
|
|
|
</template>
|
2026-11-04 14:16:38 +03:30
|
|
|
|
</a-flex>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<a-tab-pane key="debtors" tab="بدهکاران">
|
|
|
|
|
|
<a-flex vertical :gap="16">
|
2026-11-08 14:20:00 +03:30
|
|
|
|
<a-typography-text type="secondary">
|
|
|
|
|
|
ماندهٔ فاکتور صادرشده و اسناد دریافتِ باز. صدور فاکتور سند درآمد جدا نمیسازد.
|
|
|
|
|
|
</a-typography-text>
|
2026-11-04 14:16:38 +03:30
|
|
|
|
<a-flex justify="space-between" align="center">
|
|
|
|
|
|
<a-typography-text type="secondary">
|
|
|
|
|
|
{{ debtors.length }} طرف حساب · جمع مانده {{ toman(sumRemaining(debtors)) }}
|
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
|
<a-button @click="exportParties(debtors, 'بدهکاران', 'دریافتشده', 'debtors.xlsx')">
|
|
|
|
|
|
<template #icon><DownloadOutlined /></template>
|
|
|
|
|
|
خروجی اکسل
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
<TreasuryPartyBalanceTable :rows="debtors" settled-label="دریافتشده" />
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<a-tab-pane key="creditors" tab="بستانکاران">
|
|
|
|
|
|
<a-flex vertical :gap="16">
|
2026-11-08 14:20:00 +03:30
|
|
|
|
<a-typography-text type="secondary">
|
|
|
|
|
|
ماندهٔ فاکتور برگشتی و اسناد هزینهٔ باز. برگشتی سند هزینهٔ جدا نمیسازد.
|
|
|
|
|
|
</a-typography-text>
|
2026-11-04 14:16:38 +03:30
|
|
|
|
<a-flex justify="space-between" align="center">
|
|
|
|
|
|
<a-typography-text type="secondary">
|
|
|
|
|
|
{{ creditors.length }} طرف حساب · جمع مانده {{ toman(sumRemaining(creditors)) }}
|
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
|
<a-button @click="exportParties(creditors, 'بستانکاران', 'پرداختشده', 'creditors.xlsx')">
|
|
|
|
|
|
<template #icon><DownloadOutlined /></template>
|
|
|
|
|
|
خروجی اکسل
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
<TreasuryPartyBalanceTable :rows="creditors" settled-label="پرداختشده" />
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
</a-tabs>
|
2026-10-18 21:06:18 +03:30
|
|
|
|
</a-card>
|
2026-10-21 21:47:00 +03:30
|
|
|
|
</a-flex>
|
2026-10-18 21:06:18 +03:30
|
|
|
|
</template>
|