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

343 lines
12 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { onMounted, ref } from 'vue';
import axios from 'axios';
import {
Alert as AAlert,
Button as AButton,
Card as ACard,
Checkbox as ACheckbox,
Col as ACol,
Divider as ADivider,
Empty as AEmpty,
Flex as AFlex,
Form as AForm,
FormItem as AFormItem,
Input as AInput,
Row as ARow,
Select as ASelect,
Space as ASpace,
Table as ATable,
Tag as ATag,
Typography,
} from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { api } from '@/lib/api';
import type { FinancialAccountDto, FinancialReceivableDto } from '@mento/shared';
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
import { toman } from './treasury-format';
import { formatJalaliNumeric } from '@/lib/jalali';
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
const ASelectOption = ASelect.Option;
type InstallmentRow = FinancialReceivableDto['installments'][number];
const rows = ref<FinancialReceivableDto[]>([]);
const accounts = ref<FinancialAccountDto[]>([]);
const error = ref('');
const ok = ref('');
const saving = ref(false);
const form = ref({
title: '',
partyName: '',
totalAmount: '',
prepaidAmount: '',
accountId: '',
count: '3',
interval: 'monthly',
startDate: '',
});
const collect = ref({ installmentId: '', receivableId: '', amount: '', accountId: '' });
const custom = ref(false);
const customLines = ref([{ amount: '', dueDate: '' }]);
const statusLabel: Record<string, string> = {
open: 'باز',
partial: 'جزئی',
settled: 'تسویه‌شده',
overdue: 'معوق',
void: 'باطل',
};
const installmentColumns: TableColumnsType<InstallmentRow> = [
{ title: 'قسط', dataIndex: 'position', key: 'position' },
{
title: 'سررسید',
key: 'dueDate',
customRender: ({ record }) => formatJalaliNumeric(new Date(record.dueDate)),
},
{
title: 'مبلغ',
key: 'amount',
customRender: ({ record }) => toman(record.amount),
},
{
title: 'مانده',
key: 'remaining',
customRender: ({ record }) => toman(record.remaining),
},
{
title: 'وضعیت',
key: 'status',
customRender: ({ record }) => statusLabel[record.status] ?? record.status,
},
{ title: '', key: 'actions' },
];
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 || 'عملیات ناموفق بود.';
}
async function load() {
const [list, acc] = await Promise.all([
api.get<FinancialReceivableDto[]>('/treasury/receivables'),
api.get<FinancialAccountDto[]>('/treasury/accounts'),
]);
rows.value = list.data;
accounts.value = acc.data.filter((row) => row.isActive);
}
async function downloadExcel() {
const { data } = await api.get<Blob>('/treasury/receivables/export', { responseType: 'blob' });
const url = URL.createObjectURL(data);
const link = document.createElement('a');
link.href = url;
link.download = 'installments.xlsx';
link.click();
URL.revokeObjectURL(url);
}
async function createContract() {
saving.value = true;
error.value = '';
ok.value = '';
try {
await api.post('/treasury/receivables', {
title: form.value.title,
partyName: form.value.partyName || null,
totalAmount: form.value.totalAmount,
prepaidAmount: form.value.prepaidAmount || 0,
accountId: form.value.accountId || null,
...(custom.value
? { installments: customLines.value }
: {
count: Number(form.value.count),
interval: form.value.interval,
startDate: form.value.startDate || undefined,
}),
});
ok.value = 'قرارداد و برنامه اقساط ثبت شد.';
form.value.title = '';
form.value.totalAmount = '';
form.value.prepaidAmount = '';
await load();
} catch (e) {
error.value = apiMessage(e);
} finally {
saving.value = false;
}
}
async function collectInstallment() {
if (!collect.value.installmentId) return;
saving.value = true;
error.value = '';
try {
await api.post(`/treasury/installments/${collect.value.installmentId}/collect`, {
amount: collect.value.amount,
accountId: collect.value.accountId,
clientKey: crypto.randomUUID(),
});
ok.value = 'دریافت قسط در همان حساب ثبت شد.';
collect.value = { installmentId: '', receivableId: '', amount: '', accountId: '' };
await load();
} catch (e) {
error.value = apiMessage(e);
} finally {
saving.value = false;
}
}
function startCollect(row: FinancialReceivableDto, item: InstallmentRow) {
collect.value = {
installmentId: item.id,
receivableId: row.id,
amount: item.remaining,
accountId: accounts.value[0]?.id || '',
};
}
onMounted(() => {
void load().catch(() => {
error.value = 'بارگذاری مطالبات ناموفق بود.';
});
});
</script>
<template>
<div>
<a-flex vertical :gap="16">
<div>
<a-typography-title :level="3" :style="{ margin: 0 }">اقساط و مطالبات</a-typography-title>
<a-typography-text type="secondary">
دریافت هر قسط فقط یک بار وارد حساب می‌شود و مانده همان قسط را کم می‌کند.
</a-typography-text>
</div>
<TreasuryNav />
<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>
<a-form layout="vertical" @finish="createContract">
<a-row :gutter="[16, 0]">
<a-col :xs="24" :md="12">
<a-form-item label="عنوان" name="title">
<a-input v-model:value="form.title" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="طرف حساب" name="partyName">
<a-input v-model:value="form.partyName" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="مبلغ کل" name="totalAmount">
<a-input v-model:value="form.totalAmount" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="پیش‌پرداخت" name="prepaidAmount">
<a-input v-model:value="form.prepaidAmount" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="حساب پیش‌پرداخت" name="accountId">
<a-select v-model:value="form.accountId" allow-clear style="width: 100%" placeholder="انتخاب">
<a-select-option value="">انتخاب</a-select-option>
<a-select-option v-for="account in accounts" :key="account.id" :value="account.id">
{{ account.name }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="تعداد قسط" name="count">
<a-input v-model:value="form.count" :disabled="custom" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="دوره" name="interval">
<a-select v-model:value="form.interval" style="width: 100%" :disabled="custom">
<a-select-option value="monthly">ماهانه</a-select-option>
<a-select-option value="weekly">هفتگی</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="شروع" name="startDate">
<a-input v-model:value="form.startDate" type="date" :disabled="custom" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :xs="24">
<a-form-item>
<a-checkbox v-model:checked="custom">اقساط نامساوی</a-checkbox>
</a-form-item>
</a-col>
</a-row>
<template v-if="custom">
<a-row v-for="(line, index) in customLines" :key="index" :gutter="[16, 0]">
<a-col :xs="24" :md="12">
<a-form-item :label="`مبلغ قسط ${index + 1}`">
<a-input v-model:value="line.amount" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="سررسید">
<a-input v-model:value="line.dueDate" type="date" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-form-item>
<a-button @click="customLines.push({ amount: '', dueDate: '' })">قسط دیگر</a-button>
</a-form-item>
</template>
<a-space>
<a-button type="primary" html-type="submit" :loading="saving">ساخت اقساط</a-button>
<a-button @click="downloadExcel">خروجی اکسل</a-button>
</a-space>
</a-form>
<template v-if="collect.installmentId">
<a-divider orientation="right">دریافت قسط</a-divider>
<a-form layout="vertical" @finish="collectInstallment">
<a-row :gutter="[16, 0]">
<a-col :xs="24" :md="8">
<a-form-item label="مبلغ" name="amount">
<a-input v-model:value="collect.amount" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="حساب مقصد" name="accountId">
<a-select v-model:value="collect.accountId" style="width: 100%">
<a-select-option v-for="account in accounts" :key="account.id" :value="account.id">
{{ account.name }}
</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>
</template>
<a-divider orientation="right">قراردادها</a-divider>
<a-empty v-if="!rows.length" description="قراردادی ثبت نشده است." />
<template v-else>
<div v-for="row in rows" :key="row.id">
<a-flex justify="space-between" align="flex-start" wrap="wrap" :gap="8">
<div>
<a-typography-text strong>{{ row.title }}</a-typography-text>
<br />
<a-typography-text type="secondary">
{{ row.partyName || 'بدون طرف حساب' }} · مانده {{ toman(row.remaining) }}
</a-typography-text>
</div>
<a-typography-text strong>{{ toman(row.totalAmount) }}</a-typography-text>
</a-flex>
<a-table
:columns="installmentColumns"
:data-source="row.installments"
:pagination="false"
row-key="id"
:scroll="{ x: true }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag>{{ statusLabel[record.status] ?? record.status }}</a-tag>
</template>
<template v-else-if="column.key === 'actions'">
<a-button
v-if="record.status !== 'settled'"
type="link"
@click="startCollect(row, record as InstallmentRow)"
>
دریافت
</a-button>
</template>
</template>
</a-table>
<a-divider />
</div>
</template>
</a-card>
</a-flex>
</div>
</template>