Files
CRM_Montzar/apps/web/src/views/treasury/TreasuryReportsView.vue
T

288 lines
10 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import axios from 'axios';
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,
Tabs as ATabs,
Typography,
} from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { DownloadOutlined } from '@ant-design/icons-vue';
import * as XLSX from 'xlsx';
import type { TreasuryPartyBalanceDto } from '@mento/shared';
import { api } from '@/lib/api';
import TreasuryPartyBalanceTable from '@/components/treasury/TreasuryPartyBalanceTable.vue';
import { toman } from './treasury-format';
const { Text: ATypographyText } = Typography;
const AListItem = AList.Item;
const AListItemMeta = AList.Item.Meta;
const ATabPane = ATabs.TabPane;
type Period = {
bookedIncome: string;
invoiceSales: string;
invoiceCredits: string;
bookedExpense: string;
operatingProfit: string;
cashIn: string;
cashOut: string;
cashNet: string;
expenseCategories: Array<{ name: string; amount: string; share: string }>;
};
type MetricRow = {
key: string;
label: string;
current: string;
previous: string;
};
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 }>>([]);
const debtors = ref<TreasuryPartyBalanceDto[]>([]);
const creditors = ref<TreasuryPartyBalanceDto[]>([]);
const reportTab = ref('periods');
const error = ref('');
const balanceError = ref('');
const kindLabel: Record<string, string> = {
person: 'مشتری',
company: 'شرکت',
user: 'کاربر',
};
const lines = [
['درآمد ثبت‌شده', 'bookedIncome'],
['فروش فاکتور', 'invoiceSales'],
['فاکتور برگشتی', 'invoiceCredits'],
['هزینه ثبت‌شده', 'bookedExpense'],
['سود عملیاتی', 'operatingProfit'],
['ورود وجه', 'cashIn'],
['خروج وجه', 'cashOut'],
['خالص جریان نقدی', 'cashNet'],
] as const;
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,
current: toman(current.value![key] || '0'),
previous: toman(previous.value![key] || '0'),
}));
});
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;
}
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;
}
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 = 'گزارش بارگذاری نشد.';
});
void loadBalances().catch((e: unknown) => {
if (axios.isAxiosError(e) && (e.response?.status === 404 || e.response?.status === 403)) {
debtors.value = [];
creditors.value = [];
return;
}
balanceError.value = 'مانده بدهکاران و بستانکاران بارگذاری نشد.';
});
});
</script>
<template>
<a-flex vertical :gap="16">
<a-flex justify="flex-end">
<a-button @click="download">
<template #icon><DownloadOutlined /></template>
خروجی اکسل
</a-button>
</a-flex>
<a-alert v-if="error" type="error" show-icon :message="error" />
<a-alert v-if="balanceError" type="error" show-icon :message="balanceError" />
<a-card :bordered="false">
<a-tabs v-model:activeKey="reportTab">
<a-tab-pane key="periods" tab="مقایسه دوره‌ها">
<a-flex vertical :gap="16">
<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>
</a-flex>
</a-tab-pane>
<a-tab-pane key="debtors" tab="بدهکاران">
<a-flex vertical :gap="16">
<a-typography-text type="secondary">
ماندهٔ فاکتور صادرشده و اسناد دریافتِ باز. صدور فاکتور سند درآمد جدا نمی‌سازد.
</a-typography-text>
<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">
<a-typography-text type="secondary">
ماندهٔ فاکتور برگشتی و اسناد هزینهٔ باز. برگشتی سند هزینهٔ جدا نمی‌سازد.
</a-typography-text>
<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>
</a-card>
</a-flex>
</template>