feat(web): migrate finance treasury pages to Ant Design Vue
Replace legacy Ui* chrome on treasury routes with Ant layout, and polish mobile incomes stats colors plus nav visibility. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,13 +1,36 @@
|
||||
<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,
|
||||
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 { UiButton, UiCard, UiField, UiInput, UiPageHeader } from '@/components/ui';
|
||||
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('');
|
||||
@@ -35,6 +58,31 @@ const statusLabel: Record<string, string> = {
|
||||
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;
|
||||
@@ -101,6 +149,15 @@ async function collectInstallment() {
|
||||
}
|
||||
}
|
||||
|
||||
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 = 'بارگذاری مطالبات ناموفق بود.';
|
||||
@@ -109,112 +166,163 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<UiPageHeader
|
||||
title="اقساط و مطالبات"
|
||||
description="دریافت هر قسط فقط یک بار وارد حساب میشود و مانده همان قسط را کم میکند."
|
||||
/>
|
||||
<TreasuryNav />
|
||||
<p v-if="error" class="err">{{ error }}</p>
|
||||
<p v-if="ok" class="ok">{{ ok }}</p>
|
||||
<UiCard title="قرارداد جدید">
|
||||
<div class="grid">
|
||||
<UiField label="عنوان"><UiInput v-model="form.title" /></UiField>
|
||||
<UiField label="طرف حساب"><UiInput v-model="form.partyName" /></UiField>
|
||||
<UiField label="مبلغ کل"><UiInput v-model="form.totalAmount" /></UiField>
|
||||
<UiField label="پیشپرداخت"><UiInput v-model="form.prepaidAmount" /></UiField>
|
||||
<UiField label="حساب پیشپرداخت">
|
||||
<select v-model="form.accountId" class="select">
|
||||
<option value="">انتخاب</option>
|
||||
<option v-for="account in accounts" :key="account.id" :value="account.id">{{ account.name }}</option>
|
||||
</select>
|
||||
</UiField>
|
||||
<UiField label="تعداد قسط"><UiInput v-model="form.count" :disabled="custom" /></UiField>
|
||||
<UiField label="دوره">
|
||||
<select v-model="form.interval" class="select">
|
||||
<option value="monthly">ماهانه</option>
|
||||
<option value="weekly">هفتگی</option>
|
||||
</select>
|
||||
</UiField>
|
||||
<UiField label="شروع"><UiInput v-model="form.startDate" type="date" /></UiField>
|
||||
<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>
|
||||
<label class="check"><input v-model="custom" type="checkbox" /> اقساط نامساوی</label>
|
||||
<div v-if="custom" class="custom">
|
||||
<div v-for="(line, index) in customLines" :key="index" class="grid">
|
||||
<UiField label="مبلغ قسط"><UiInput v-model="line.amount" /></UiField>
|
||||
<UiField label="سررسید"><UiInput v-model="line.dueDate" type="date" /></UiField>
|
||||
</div>
|
||||
<UiButton variant="secondary" @click="customLines.push({ amount: '', dueDate: '' })">قسط دیگر</UiButton>
|
||||
</div>
|
||||
<UiButton :loading="saving" @click="createContract">ساخت اقساط</UiButton>
|
||||
</UiCard>
|
||||
|
||||
<article v-for="row in rows" :key="row.id" class="contract">
|
||||
<header>
|
||||
<div>
|
||||
<strong>{{ row.title }}</strong>
|
||||
<small>{{ row.partyName || 'بدون طرف حساب' }} · مانده {{ toman(row.remaining) }}</small>
|
||||
</div>
|
||||
<b>{{ toman(row.totalAmount) }}</b>
|
||||
</header>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>قسط</th>
|
||||
<th>سررسید</th>
|
||||
<th>مبلغ</th>
|
||||
<th>مانده</th>
|
||||
<th>وضعیت</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in row.installments" :key="item.id">
|
||||
<td>{{ item.position }}</td>
|
||||
<td>{{ formatJalaliNumeric(new Date(item.dueDate)) }}</td>
|
||||
<td>{{ toman(item.amount) }}</td>
|
||||
<td>{{ toman(item.remaining) }}</td>
|
||||
<td>{{ statusLabel[item.status] }}</td>
|
||||
<td>
|
||||
<button
|
||||
v-if="item.status !== 'settled'"
|
||||
type="button"
|
||||
@click="collect = { installmentId: item.id, receivableId: row.id, amount: item.remaining, accountId: accounts[0]?.id || '' }"
|
||||
>
|
||||
دریافت
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
<TreasuryNav />
|
||||
|
||||
<UiCard v-if="collect.installmentId" title="دریافت قسط">
|
||||
<div class="grid">
|
||||
<UiField label="مبلغ"><UiInput v-model="collect.amount" /></UiField>
|
||||
<UiField label="حساب مقصد">
|
||||
<select v-model="collect.accountId" class="select">
|
||||
<option v-for="account in accounts" :key="account.id" :value="account.id">{{ account.name }}</option>
|
||||
</select>
|
||||
</UiField>
|
||||
</div>
|
||||
<UiButton :loading="saving" @click="collectInstallment">ثبت دریافت</UiButton>
|
||||
</UiCard>
|
||||
</section>
|
||||
<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-button type="primary" html-type="submit" :loading="saving">ساخت اقساط</a-button>
|
||||
</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>
|
||||
|
||||
<style scoped>
|
||||
.page { display: grid; gap: 1rem; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 0.7rem; margin-bottom: 0.8rem; }
|
||||
.select { width: 100%; min-height: var(--control-h); border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; }
|
||||
.contract { background: #fff; border: 1px solid #e7eef2; border-radius: 16px; padding: 0.8rem 1rem; overflow: auto; }
|
||||
.contract header, .contract header div { display: flex; justify-content: space-between; gap: 0.75rem; }
|
||||
.contract small { display: block; color: #64748b; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 0.6rem; }
|
||||
th, td { text-align: right; padding: 0.45rem; border-top: 1px solid #f1f5f9; white-space: nowrap; }
|
||||
button { border: 0; background: transparent; color: #0f766e; font: inherit; font-weight: 700; cursor: pointer; }
|
||||
.check { display: flex; gap: 0.4rem; align-items: center; margin: 0.4rem 0 0.8rem; font-weight: 700; }
|
||||
.custom { display: grid; gap: 0.5rem; margin-bottom: 0.8rem; }
|
||||
.err { color: #b91c1c; font-weight: 700; }
|
||||
.ok { color: #0f766e; font-weight: 700; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user