2026-10-18 18:49:49 +03:30
|
|
|
<script setup lang="ts">
|
2026-10-18 21:06:18 +03:30
|
|
|
import { onMounted, ref } from 'vue';
|
|
|
|
|
import axios from 'axios';
|
|
|
|
|
import {
|
|
|
|
|
Alert as AAlert,
|
|
|
|
|
Button as AButton,
|
|
|
|
|
Card as ACard,
|
|
|
|
|
Col as ACol,
|
|
|
|
|
Divider as ADivider,
|
|
|
|
|
Empty as AEmpty,
|
|
|
|
|
Flex as AFlex,
|
|
|
|
|
Form as AForm,
|
|
|
|
|
FormItem as AFormItem,
|
|
|
|
|
Input as AInput,
|
|
|
|
|
List as AList,
|
|
|
|
|
Row as ARow,
|
|
|
|
|
Select as ASelect,
|
|
|
|
|
Typography,
|
2026-10-21 21:47:00 +03:30
|
|
|
message,
|
2026-10-18 21:06:18 +03:30
|
|
|
} from 'ant-design-vue';
|
|
|
|
|
import { api } from '@/lib/api';
|
2026-11-04 11:36:55 +03:30
|
|
|
import type {
|
|
|
|
|
FinancialAccountDto,
|
|
|
|
|
FinancialCategoryDto,
|
|
|
|
|
FinancialDocumentDto,
|
|
|
|
|
FinancialPartyKind,
|
|
|
|
|
TreasuryPartyBalanceDto,
|
|
|
|
|
} from '@mento/shared';
|
2026-10-21 21:47:00 +03:30
|
|
|
import MoneyInput from '@/components/MoneyInput.vue';
|
2026-11-04 11:36:55 +03:30
|
|
|
import TreasuryPartyBalanceTable from '@/components/treasury/TreasuryPartyBalanceTable.vue';
|
|
|
|
|
import TreasuryPartySelect from '@/components/treasury/TreasuryPartySelect.vue';
|
|
|
|
|
import { useSettleDefault } from '@/components/treasury/useSettleDefault';
|
2026-10-18 21:06:18 +03:30
|
|
|
import { toman } from './treasury-format';
|
2026-10-21 21:47:00 +03:30
|
|
|
import { parseMoneyNumber } from '@/lib/money';
|
2026-10-18 21:06:18 +03:30
|
|
|
|
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;
|
|
|
|
|
const ASelectOption = ASelect.Option;
|
|
|
|
|
|
|
|
|
|
const kind = 'expense' as const;
|
|
|
|
|
const rows = ref<FinancialDocumentDto[]>([]);
|
2026-11-04 11:36:55 +03:30
|
|
|
const balances = ref<TreasuryPartyBalanceDto[]>([]);
|
2026-10-18 21:06:18 +03:30
|
|
|
const categories = ref<FinancialCategoryDto[]>([]);
|
|
|
|
|
const accounts = ref<FinancialAccountDto[]>([]);
|
|
|
|
|
const error = ref('');
|
|
|
|
|
const ok = ref('');
|
|
|
|
|
const saving = ref(false);
|
|
|
|
|
const q = ref('');
|
|
|
|
|
const form = ref({
|
|
|
|
|
title: '',
|
|
|
|
|
amount: '',
|
|
|
|
|
categoryId: '',
|
2026-11-04 11:36:55 +03:30
|
|
|
partyKind: undefined as FinancialPartyKind | undefined,
|
|
|
|
|
partyId: undefined as string | undefined,
|
2026-10-18 21:06:18 +03:30
|
|
|
accountId: '',
|
|
|
|
|
settleAmount: '',
|
|
|
|
|
method: 'transfer',
|
|
|
|
|
notes: '',
|
|
|
|
|
});
|
2026-11-04 11:36:55 +03:30
|
|
|
const settleMirror = useSettleDefault(form);
|
2026-10-18 21:06:18 +03:30
|
|
|
|
|
|
|
|
const settleLabel = 'مبلغ پرداختشده';
|
|
|
|
|
const statusLabel: Record<string, string> = {
|
|
|
|
|
open: 'پرداختنشده',
|
|
|
|
|
partial: 'جزئی',
|
|
|
|
|
settled: 'پرداختشده',
|
|
|
|
|
void: 'باطلشده',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const methodOptions = [
|
|
|
|
|
{ value: 'cash', label: 'نقدی' },
|
|
|
|
|
{ value: 'card_to_card', label: 'کارتبهکارت' },
|
|
|
|
|
{ value: 'pos', label: 'کارتخوان' },
|
|
|
|
|
{ value: 'transfer', label: 'حواله' },
|
|
|
|
|
{ value: 'other', label: 'سایر' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function apiMessage(e: unknown) {
|
|
|
|
|
if (!axios.isAxiosError(e)) return 'ذخیره ناموفق بود.';
|
|
|
|
|
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
|
|
|
|
return Array.isArray(msg) ? msg.join('، ') : msg || 'ذخیره ناموفق بود.';
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-04 11:36:55 +03:30
|
|
|
function docLine(item: FinancialDocumentDto) {
|
|
|
|
|
const parts = [
|
2026-11-04 13:11:09 +03:30
|
|
|
item.reference ? `پیگیری ${item.reference}` : '',
|
2026-11-04 11:36:55 +03:30
|
|
|
item.partyName || 'بدون طرف حساب',
|
|
|
|
|
statusLabel[item.status] ?? item.status,
|
|
|
|
|
`مانده ${toman(item.remaining)}`,
|
2026-11-04 13:11:09 +03:30
|
|
|
].filter(Boolean);
|
2026-11-04 11:36:55 +03:30
|
|
|
if (item.categoryName) parts.push(item.categoryName);
|
|
|
|
|
return parts.join(' · ');
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-18 21:06:18 +03:30
|
|
|
async function load() {
|
2026-11-04 11:36:55 +03:30
|
|
|
const [docs, cats, acc, bals] = await Promise.all([
|
2026-10-18 21:06:18 +03:30
|
|
|
api.get<FinancialDocumentDto[]>('/treasury/documents', { params: { kind, q: q.value || undefined } }),
|
|
|
|
|
api.get<FinancialCategoryDto[]>('/treasury/categories', { params: { kind } }),
|
|
|
|
|
api.get<FinancialAccountDto[]>('/treasury/accounts'),
|
2026-11-04 11:36:55 +03:30
|
|
|
api.get<TreasuryPartyBalanceDto[]>('/treasury/party-balances', { params: { kind } }).catch((e: unknown) => {
|
|
|
|
|
if (axios.isAxiosError(e) && e.response?.status === 404) return { data: [] as TreasuryPartyBalanceDto[] };
|
|
|
|
|
throw e;
|
|
|
|
|
}),
|
2026-10-18 21:06:18 +03:30
|
|
|
]);
|
|
|
|
|
rows.value = docs.data;
|
2026-11-04 11:36:55 +03:30
|
|
|
balances.value = bals.data;
|
2026-10-18 21:06:18 +03:30
|
|
|
categories.value = cats.data.filter((row) => row.isActive);
|
|
|
|
|
accounts.value = acc.data.filter((row) => row.isActive);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-21 21:47:00 +03:30
|
|
|
function onFinishFailed() {
|
|
|
|
|
message.warning('لطفاً فیلدهای الزامی را کامل کنید.');
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-18 21:06:18 +03:30
|
|
|
async function createDoc() {
|
|
|
|
|
saving.value = true;
|
|
|
|
|
error.value = '';
|
|
|
|
|
ok.value = '';
|
|
|
|
|
try {
|
2026-10-21 21:47:00 +03:30
|
|
|
const amount = parseMoneyNumber(String(form.value.amount ?? ''), 0);
|
|
|
|
|
if (amount == null || amount <= 0) {
|
|
|
|
|
error.value = 'مبلغ کل معتبر نیست.';
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-11-04 11:36:55 +03:30
|
|
|
if (!form.value.partyKind || (form.value.partyKind !== 'other' && !form.value.partyId)) {
|
|
|
|
|
error.value = 'طرف حساب را انتخاب کنید.';
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-10-21 21:47:00 +03:30
|
|
|
const settleAmount = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
2026-11-04 11:36:55 +03:30
|
|
|
if (form.value.partyKind === 'other' && settleAmount !== amount) {
|
|
|
|
|
error.value = 'برای سایر، مبلغ پرداختشده باید با مبلغ کل یکی باشد.';
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-11-04 13:11:09 +03:30
|
|
|
const { data } = await api.post<FinancialDocumentDto>('/treasury/documents', {
|
2026-10-18 21:06:18 +03:30
|
|
|
kind,
|
|
|
|
|
title: form.value.title,
|
2026-10-21 21:47:00 +03:30
|
|
|
amount,
|
2026-10-18 21:06:18 +03:30
|
|
|
categoryId: form.value.categoryId || null,
|
2026-11-04 11:36:55 +03:30
|
|
|
partyKind: form.value.partyKind,
|
|
|
|
|
personId: form.value.partyKind === 'person' ? form.value.partyId : null,
|
|
|
|
|
companyId: form.value.partyKind === 'company' ? form.value.partyId : null,
|
|
|
|
|
userId: form.value.partyKind === 'user' ? form.value.partyId : null,
|
2026-10-18 21:06:18 +03:30
|
|
|
accountId: form.value.accountId || null,
|
2026-10-21 21:47:00 +03:30
|
|
|
settleAmount,
|
2026-10-18 21:06:18 +03:30
|
|
|
method: form.value.method,
|
|
|
|
|
notes: form.value.notes || null,
|
|
|
|
|
});
|
2026-11-04 11:36:55 +03:30
|
|
|
settleMirror.reset();
|
2026-10-18 21:06:18 +03:30
|
|
|
form.value = {
|
|
|
|
|
title: '',
|
|
|
|
|
amount: '',
|
|
|
|
|
categoryId: '',
|
2026-11-04 11:36:55 +03:30
|
|
|
partyKind: undefined,
|
|
|
|
|
partyId: undefined,
|
2026-10-18 21:06:18 +03:30
|
|
|
accountId: '',
|
|
|
|
|
settleAmount: '',
|
|
|
|
|
method: 'transfer',
|
|
|
|
|
notes: '',
|
|
|
|
|
};
|
2026-11-04 13:11:09 +03:30
|
|
|
ok.value = data.reference
|
|
|
|
|
? `سند ثبت شد. شماره پیگیری: ${data.reference}`
|
|
|
|
|
: 'سند ثبت شد.';
|
2026-10-18 21:06:18 +03:30
|
|
|
await load();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
error.value = apiMessage(e);
|
|
|
|
|
} finally {
|
|
|
|
|
saving.value = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function voidDoc(id: string) {
|
|
|
|
|
if (!window.confirm('این سند و گردش متصل به آن باطل شود؟')) return;
|
|
|
|
|
try {
|
|
|
|
|
await api.post(`/treasury/documents/${id}/void`, { reason: 'ابطال توسط مدیر' });
|
|
|
|
|
await load();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
error.value = apiMessage(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
void load().catch(() => {
|
|
|
|
|
error.value = 'بارگذاری ناموفق بود.';
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-10-18 18:49:49 +03:30
|
|
|
</script>
|
2026-10-18 21:06:18 +03:30
|
|
|
|
2026-10-18 18:49:49 +03:30
|
|
|
<template>
|
2026-10-21 21:47:00 +03:30
|
|
|
<a-flex vertical :gap="16">
|
2026-10-18 21:06:18 +03:30
|
|
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
|
|
|
|
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
|
|
|
|
|
|
|
|
|
<a-card :bordered="false">
|
|
|
|
|
<a-divider orientation="right">سند جدید</a-divider>
|
2026-10-21 21:47:00 +03:30
|
|
|
<a-form
|
|
|
|
|
layout="vertical"
|
|
|
|
|
:model="form"
|
|
|
|
|
@finish="createDoc"
|
|
|
|
|
@finishFailed="onFinishFailed"
|
|
|
|
|
>
|
2026-10-18 21:06:18 +03:30
|
|
|
<a-row :gutter="[16, 0]">
|
|
|
|
|
<a-col :xs="24" :md="12">
|
2026-10-21 21:47:00 +03:30
|
|
|
<a-form-item
|
|
|
|
|
label="عنوان"
|
|
|
|
|
name="title"
|
|
|
|
|
:rules="[{ required: true, message: 'عنوان الزامی است' }]"
|
|
|
|
|
>
|
2026-10-18 21:06:18 +03:30
|
|
|
<a-input v-model:value="form.title" />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :xs="24" :md="8">
|
2026-10-21 21:47:00 +03:30
|
|
|
<a-form-item
|
|
|
|
|
label="مبلغ کل"
|
|
|
|
|
name="amount"
|
|
|
|
|
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
|
|
|
|
>
|
|
|
|
|
<MoneyInput v-model="form.amount" :decimals="0" />
|
2026-10-18 21:06:18 +03:30
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
<a-form-item :label="settleLabel" name="settleAmount">
|
2026-11-04 11:36:55 +03:30
|
|
|
<MoneyInput v-model="form.settleAmount" :decimals="0" :disabled="form.partyKind === 'other'" />
|
2026-10-18 21:06:18 +03:30
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
<a-form-item label="دستهبندی" name="categoryId">
|
2026-11-04 10:02:31 +03:30
|
|
|
<a-select show-search option-filter-prop="label"
|
2026-10-21 21:47:00 +03:30
|
|
|
v-model:value="form.categoryId"
|
|
|
|
|
allow-clear
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
placeholder="بدون دسته"
|
|
|
|
|
>
|
2026-11-04 10:02:31 +03:30
|
|
|
<a-select-option v-for="cat in categories" :key="cat.id" :value="cat.id" :label="cat.name">
|
2026-10-18 21:06:18 +03:30
|
|
|
{{ cat.name }}
|
|
|
|
|
</a-select-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :xs="24" :md="8">
|
2026-11-04 11:36:55 +03:30
|
|
|
<a-form-item label="طرف حساب" name="partyKind">
|
|
|
|
|
<TreasuryPartySelect v-model:kind="form.partyKind" v-model:party-id="form.partyId" />
|
2026-10-18 21:06:18 +03:30
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
<a-form-item label="حساب" name="accountId">
|
2026-11-04 10:02:31 +03:30
|
|
|
<a-select show-search option-filter-prop="label"
|
2026-10-21 21:47:00 +03:30
|
|
|
v-model:value="form.accountId"
|
|
|
|
|
allow-clear
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
placeholder="انتخاب حساب"
|
|
|
|
|
>
|
2026-11-04 10:02:31 +03:30
|
|
|
<a-select-option v-for="account in accounts" :key="account.id" :value="account.id" :label="account.name">
|
2026-10-18 21:06:18 +03:30
|
|
|
{{ account.name }}
|
|
|
|
|
</a-select-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
<a-form-item label="روش" name="method">
|
2026-11-04 10:02:31 +03:30
|
|
|
<a-select show-search option-filter-prop="label" v-model:value="form.method" style="width: 100%">
|
|
|
|
|
<a-select-option v-for="opt in methodOptions" :key="opt.value" :value="opt.value" :label="opt.label">
|
2026-10-18 21:06:18 +03:30
|
|
|
{{ opt.label }}
|
|
|
|
|
</a-select-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
</a-row>
|
|
|
|
|
<a-button type="primary" html-type="submit" :loading="saving">ثبت</a-button>
|
|
|
|
|
</a-form>
|
|
|
|
|
|
2026-11-04 11:36:55 +03:30
|
|
|
<a-divider orientation="right">مانده پرداختنی</a-divider>
|
|
|
|
|
<TreasuryPartyBalanceTable :rows="balances" settled-label="پرداختشده" />
|
|
|
|
|
|
2026-10-18 21:06:18 +03:30
|
|
|
<a-divider orientation="right">فهرست اسناد</a-divider>
|
|
|
|
|
<a-form layout="vertical">
|
|
|
|
|
<a-row :gutter="[16, 0]">
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
<a-form-item label="جستجو">
|
|
|
|
|
<a-input
|
|
|
|
|
v-model:value="q"
|
|
|
|
|
allow-clear
|
2026-11-04 13:11:09 +03:30
|
|
|
placeholder="عنوان یا شماره پیگیری"
|
2026-10-18 21:06:18 +03:30
|
|
|
@press-enter="load"
|
|
|
|
|
@change="load"
|
|
|
|
|
/>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-col>
|
|
|
|
|
</a-row>
|
|
|
|
|
</a-form>
|
|
|
|
|
|
|
|
|
|
<a-empty v-if="!rows.length" description="سندی نیست." />
|
|
|
|
|
<a-list v-else item-layout="horizontal" :data-source="rows">
|
|
|
|
|
<template #renderItem="{ item }">
|
|
|
|
|
<a-list-item>
|
|
|
|
|
<template #actions>
|
|
|
|
|
<a-typography-text strong>{{ toman(item.amount) }}</a-typography-text>
|
|
|
|
|
<a-button
|
|
|
|
|
v-if="item.status !== 'void'"
|
|
|
|
|
type="link"
|
|
|
|
|
danger
|
|
|
|
|
@click="voidDoc(item.id)"
|
|
|
|
|
>
|
|
|
|
|
ابطال
|
|
|
|
|
</a-button>
|
|
|
|
|
</template>
|
|
|
|
|
<a-list-item-meta>
|
|
|
|
|
<template #title>{{ item.title }}</template>
|
2026-11-04 11:36:55 +03:30
|
|
|
<template #description>{{ docLine(item) }}</template>
|
2026-10-18 21:06:18 +03:30
|
|
|
</a-list-item-meta>
|
|
|
|
|
</a-list-item>
|
|
|
|
|
</template>
|
|
|
|
|
</a-list>
|
|
|
|
|
</a-card>
|
2026-10-21 21:47:00 +03:30
|
|
|
</a-flex>
|
2026-10-18 18:49:49 +03:30
|
|
|
</template>
|