Keep the local Ant screens and icon pack. Take treasury parties, routine self-service, Tahesab auto-sync, public lead registration, and contact merge from main.
728 lines
26 KiB
Vue
728 lines
26 KiB
Vue
<script setup lang="ts">
|
||
import { computed, h, onMounted, ref } from 'vue';
|
||
import axios from 'axios';
|
||
import {
|
||
Alert as AAlert,
|
||
Avatar as AAvatar,
|
||
Button as AButton,
|
||
Card as ACard,
|
||
Col as ACol,
|
||
Divider as ADivider,
|
||
Empty as AEmpty,
|
||
Flex as AFlex,
|
||
Form as AForm,
|
||
FormItem as AFormItem,
|
||
Grid,
|
||
Input as AInput,
|
||
List as AList,
|
||
Row as ARow,
|
||
Segmented as ASegmented,
|
||
Select as ASelect,
|
||
Space as ASpace,
|
||
Textarea as ATextarea,
|
||
Typography,
|
||
message,
|
||
theme,
|
||
} from 'ant-design-vue';
|
||
import type { FormInstance } from 'ant-design-vue';
|
||
import {
|
||
ArrowDownOutlined,
|
||
BankOutlined,
|
||
CheckOutlined,
|
||
ClockCircleOutlined,
|
||
CreditCardOutlined,
|
||
FileTextOutlined,
|
||
InfoCircleOutlined,
|
||
TagOutlined,
|
||
} from '@/icons/ant-bridge';
|
||
import { api } from '@/lib/api';
|
||
import { formatLocalDateInput } from '@/lib/jalali';
|
||
import type {
|
||
FinancialAccountDto,
|
||
FinancialCategoryDto,
|
||
FinancialDocumentDto,
|
||
FinancialPartyKind,
|
||
TreasuryDashboardDto,
|
||
TreasuryPartyBalanceDto,
|
||
} from '@mento/shared';
|
||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||
import MoneyInput from '@/components/MoneyInput.vue';
|
||
import TreasuryPartyBalanceTable from '@/components/treasury/TreasuryPartyBalanceTable.vue';
|
||
import TreasuryPartySelect from '@/components/treasury/TreasuryPartySelect.vue';
|
||
import { useSettleDefault } from '@/components/treasury/useSettleDefault';
|
||
import { toman } from './treasury-format';
|
||
import { parseMoneyNumber } from '@/lib/money';
|
||
|
||
const { Text: ATypographyText } = Typography;
|
||
const AListItem = AList.Item;
|
||
const AListItemMeta = AList.Item.Meta;
|
||
const ASelectOption = ASelect.Option;
|
||
const { token } = theme.useToken();
|
||
const screens = Grid.useBreakpoint();
|
||
const isMobile = computed(() => !screens.value.md);
|
||
|
||
const kind = 'income' as const;
|
||
const rows = ref<FinancialDocumentDto[]>([]);
|
||
const balances = ref<TreasuryPartyBalanceDto[]>([]);
|
||
const categories = ref<FinancialCategoryDto[]>([]);
|
||
const accounts = ref<FinancialAccountDto[]>([]);
|
||
const dashboard = ref<TreasuryDashboardDto | null>(null);
|
||
const error = ref('');
|
||
const ok = ref('');
|
||
const saving = ref(false);
|
||
const q = ref('');
|
||
const formTab = ref<'main' | 'extra'>('main');
|
||
const mobileFormRef = ref<FormInstance>();
|
||
|
||
function emptyForm() {
|
||
return {
|
||
title: '',
|
||
amount: '',
|
||
categoryId: undefined as string | undefined,
|
||
partyKind: undefined as FinancialPartyKind | undefined,
|
||
partyId: undefined as string | undefined,
|
||
accountId: undefined as string | undefined,
|
||
settleAmount: '',
|
||
method: 'transfer',
|
||
notes: '',
|
||
bookedAt: formatLocalDateInput(new Date()),
|
||
};
|
||
}
|
||
|
||
const form = ref(emptyForm());
|
||
const settleMirror = useSettleDefault(form);
|
||
|
||
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: 'سایر' },
|
||
];
|
||
|
||
const formTabOptions = [
|
||
{
|
||
value: 'main',
|
||
payload: 'main',
|
||
label: h('span', {}, [h(InfoCircleOutlined), ' اطلاعات اصلی']),
|
||
},
|
||
{
|
||
value: 'extra',
|
||
payload: 'extra',
|
||
label: h('span', {}, [h(FileTextOutlined), ' جزئیات تکمیلی']),
|
||
},
|
||
];
|
||
|
||
const remainingAmount = computed(() => {
|
||
const total = parseMoneyNumber(String(form.value.amount ?? ''), 0) ?? 0;
|
||
const settled = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
||
return Math.max(0, total - settled);
|
||
});
|
||
|
||
function onFinishFailed() {
|
||
message.warning('لطفاً فیلدهای الزامی را کامل کنید.');
|
||
}
|
||
|
||
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 || 'ذخیره ناموفق بود.';
|
||
}
|
||
|
||
function docLine(item: FinancialDocumentDto) {
|
||
const parts = [
|
||
item.reference ? `پیگیری ${item.reference}` : '',
|
||
item.partyName || 'بدون طرف حساب',
|
||
statusLabel[item.status] ?? item.status,
|
||
`مانده ${toman(item.remaining)}`,
|
||
].filter(Boolean);
|
||
if (item.categoryName) parts.push(item.categoryName);
|
||
return parts.join(' · ');
|
||
}
|
||
|
||
async function load() {
|
||
const [docs, cats, acc, dash, bals] = await Promise.all([
|
||
api.get<FinancialDocumentDto[]>('/treasury/documents', { params: { kind, q: q.value || undefined } }),
|
||
api.get<FinancialCategoryDto[]>('/treasury/categories', { params: { kind } }),
|
||
api.get<FinancialAccountDto[]>('/treasury/accounts'),
|
||
api.get<TreasuryDashboardDto>('/treasury/dashboard').catch(() => null),
|
||
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;
|
||
}),
|
||
]);
|
||
rows.value = docs.data;
|
||
balances.value = bals.data;
|
||
categories.value = cats.data.filter((row) => row.isActive);
|
||
accounts.value = acc.data.filter((row) => row.isActive);
|
||
if (dash) dashboard.value = dash.data;
|
||
}
|
||
|
||
function resetForm() {
|
||
settleMirror.reset();
|
||
form.value = emptyForm();
|
||
formTab.value = 'main';
|
||
error.value = '';
|
||
ok.value = '';
|
||
}
|
||
|
||
async function createDoc() {
|
||
saving.value = true;
|
||
error.value = '';
|
||
ok.value = '';
|
||
try {
|
||
const amount = parseMoneyNumber(String(form.value.amount ?? ''), 0);
|
||
if (amount == null || amount <= 0) {
|
||
error.value = 'مبلغ کل معتبر نیست.';
|
||
return;
|
||
}
|
||
if (!form.value.partyKind || (form.value.partyKind !== 'other' && !form.value.partyId)) {
|
||
error.value = 'طرف حساب را انتخاب کنید.';
|
||
return;
|
||
}
|
||
const settleAmount = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
||
if (form.value.partyKind === 'other' && settleAmount !== amount) {
|
||
error.value = 'برای سایر، مبلغ دریافتشده باید با مبلغ کل یکی باشد.';
|
||
return;
|
||
}
|
||
const { data } = await api.post<FinancialDocumentDto>('/treasury/documents', {
|
||
kind,
|
||
title: form.value.title,
|
||
amount,
|
||
categoryId: form.value.categoryId || null,
|
||
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,
|
||
accountId: form.value.accountId || null,
|
||
settleAmount,
|
||
method: form.value.method,
|
||
notes: form.value.notes || null,
|
||
bookedAt: form.value.bookedAt || undefined,
|
||
});
|
||
resetForm();
|
||
ok.value = data.reference
|
||
? `سند ثبت شد. شماره پیگیری: ${data.reference}`
|
||
: 'سند ثبت شد.';
|
||
await load();
|
||
} catch (e) {
|
||
error.value = apiMessage(e);
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
}
|
||
|
||
async function submitMobileForm() {
|
||
try {
|
||
await mobileFormRef.value?.validate();
|
||
} catch {
|
||
formTab.value = 'main';
|
||
return;
|
||
}
|
||
await createDoc();
|
||
}
|
||
|
||
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 = 'بارگذاری ناموفق بود.';
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div
|
||
:style="
|
||
isMobile
|
||
? { paddingBottom: 'calc(96px + var(--mobile-tabbar-h, 3.65rem))' }
|
||
: undefined
|
||
"
|
||
>
|
||
<!-- ─── Mobile app experience ─── -->
|
||
<template v-if="isMobile">
|
||
<a-flex vertical :gap="token.margin">
|
||
<a-row :gutter="[12, 12]">
|
||
<a-col :span="12">
|
||
<a-card
|
||
:bordered="false"
|
||
:body-style="{ padding: `${token.paddingSM}px` }"
|
||
:style="{
|
||
background: token.colorPrimaryBg,
|
||
borderRadius: `${token.borderRadiusLG}px`,
|
||
boxShadow: token.boxShadow,
|
||
}"
|
||
>
|
||
<a-flex align="center" justify="space-between" :gap="8">
|
||
<div :style="{ minWidth: 0, flex: 1 }">
|
||
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
|
||
دریافتهای این ماه
|
||
</a-typography-text>
|
||
<div>
|
||
<a-typography-text
|
||
strong
|
||
:style="{ fontSize: '15px', color: token.colorPrimary }"
|
||
>
|
||
{{ toman(dashboard?.actualReceipts) }}
|
||
</a-typography-text>
|
||
</div>
|
||
</div>
|
||
<a-avatar
|
||
:size="36"
|
||
:style="{
|
||
background: token.colorPrimary,
|
||
color: token.colorTextLightSolid,
|
||
flexShrink: 0,
|
||
}"
|
||
>
|
||
<template #icon><ArrowDownOutlined /></template>
|
||
</a-avatar>
|
||
</a-flex>
|
||
</a-card>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-card
|
||
:bordered="false"
|
||
:body-style="{ padding: `${token.paddingSM}px` }"
|
||
:style="{
|
||
background: token.colorPrimaryBg,
|
||
borderRadius: `${token.borderRadiusLG}px`,
|
||
boxShadow: token.boxShadow,
|
||
}"
|
||
>
|
||
<a-flex align="center" justify="space-between" :gap="8">
|
||
<div :style="{ minWidth: 0, flex: 1 }">
|
||
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
|
||
در انتظار دریافت
|
||
</a-typography-text>
|
||
<div>
|
||
<a-typography-text
|
||
strong
|
||
:style="{ fontSize: '15px', color: token.colorPrimary }"
|
||
>
|
||
{{ toman(dashboard?.receivables) }}
|
||
</a-typography-text>
|
||
</div>
|
||
</div>
|
||
<a-avatar
|
||
:size="36"
|
||
:style="{
|
||
background: token.colorPrimary,
|
||
color: token.colorTextLightSolid,
|
||
flexShrink: 0,
|
||
}"
|
||
>
|
||
<template #icon><ClockCircleOutlined /></template>
|
||
</a-avatar>
|
||
</a-flex>
|
||
</a-card>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<a-alert v-if="error" type="error" show-icon :message="error" closable @close="error = ''" />
|
||
<a-alert v-if="ok" type="success" show-icon :message="ok" closable @close="ok = ''" />
|
||
|
||
<a-segmented
|
||
v-model:value="formTab"
|
||
block
|
||
:options="formTabOptions"
|
||
:style="{ background: token.colorFillTertiary }"
|
||
/>
|
||
|
||
<a-card :bordered="false">
|
||
<a-form ref="mobileFormRef" layout="vertical" :model="form" @finish="createDoc">
|
||
<div v-show="formTab === 'main'">
|
||
<a-form-item
|
||
label="عنوان درآمد"
|
||
name="title"
|
||
:rules="[{ required: true, message: 'عنوان را وارد کنید' }]"
|
||
>
|
||
<a-input
|
||
v-model:value="form.title"
|
||
placeholder="مثلاً: دریافت قسط پروژه الف"
|
||
>
|
||
<template #prefix><FileTextOutlined /></template>
|
||
</a-input>
|
||
</a-form-item>
|
||
|
||
<a-row :gutter="[12, 0]">
|
||
<a-col :span="12">
|
||
<a-form-item label="طرف حساب" name="partyKind">
|
||
<TreasuryPartySelect v-model:kind="form.partyKind" v-model:party-id="form.partyId" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item name="method">
|
||
<template #label>
|
||
<a-space :size="6">
|
||
<CreditCardOutlined />
|
||
<span>روش دریافت</span>
|
||
</a-space>
|
||
</template>
|
||
<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"
|
||
>
|
||
{{ opt.label }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<a-form-item name="categoryId">
|
||
<template #label>
|
||
<a-space :size="6">
|
||
<TagOutlined />
|
||
<span>دستهبندی درآمد</span>
|
||
</a-space>
|
||
</template>
|
||
<a-select show-search option-filter-prop="label"
|
||
v-model:value="form.categoryId"
|
||
allow-clear
|
||
placeholder="انتخاب دسته"
|
||
style="width: 100%"
|
||
>
|
||
<a-select-option v-for="cat in categories" :key="cat.id" :value="cat.id" :label="cat.name">
|
||
{{ cat.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
|
||
<a-row :gutter="[12, 0]">
|
||
<a-col :span="12">
|
||
<a-form-item
|
||
label="مبلغ کل (تومان)"
|
||
name="amount"
|
||
:rules="[{ required: true, message: 'مبلغ کل را وارد کنید' }]"
|
||
>
|
||
<MoneyInput v-model="form.amount" :decimals="0" placeholder="۰" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item
|
||
:label="`${settleLabel} (تومان)`"
|
||
name="settleAmount"
|
||
:rules="[{ required: true, message: 'مبلغ دریافتشده را وارد کنید' }]"
|
||
>
|
||
<MoneyInput
|
||
v-model="form.settleAmount"
|
||
:decimals="0"
|
||
:disabled="form.partyKind === 'other'"
|
||
placeholder="۰"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<a-flex
|
||
align="center"
|
||
justify="space-between"
|
||
:style="{
|
||
background: token.colorSuccessBg,
|
||
borderRadius: `${token.borderRadiusLG * 2}px`,
|
||
padding: `${token.paddingSM}px ${token.padding}px`,
|
||
marginBottom: `${token.margin}px`,
|
||
}"
|
||
>
|
||
<a-space :size="8">
|
||
<span
|
||
:style="{
|
||
width: '8px',
|
||
height: '8px',
|
||
borderRadius: '50%',
|
||
background: token.colorSuccess,
|
||
display: 'inline-block',
|
||
}"
|
||
/>
|
||
<a-typography-text type="secondary">مانده قابل دریافت</a-typography-text>
|
||
</a-space>
|
||
<a-typography-text strong :style="{ color: token.colorSuccess }">
|
||
{{ toman(remainingAmount) }}
|
||
</a-typography-text>
|
||
</a-flex>
|
||
|
||
<a-row :gutter="[12, 0]">
|
||
<a-col :span="12">
|
||
<a-form-item label="تاریخ دریافت" name="bookedAt">
|
||
<JalaliDateInput v-model="form.bookedAt" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item name="accountId">
|
||
<template #label>
|
||
<a-space :size="6">
|
||
<BankOutlined />
|
||
<span>حساب مقصد</span>
|
||
</a-space>
|
||
</template>
|
||
<a-select show-search option-filter-prop="label"
|
||
v-model:value="form.accountId"
|
||
allow-clear
|
||
placeholder="انتخاب حساب"
|
||
style="width: 100%"
|
||
>
|
||
<a-select-option
|
||
v-for="account in accounts"
|
||
:key="account.id"
|
||
:value="account.id"
|
||
:label="account.name"
|
||
>
|
||
{{ account.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
|
||
<div v-show="formTab === 'extra'">
|
||
<a-form-item label="توضیحات" name="notes">
|
||
<a-textarea
|
||
v-model:value="form.notes"
|
||
:rows="4"
|
||
placeholder="توضیحات اختیاری…"
|
||
/>
|
||
</a-form-item>
|
||
</div>
|
||
</a-form>
|
||
</a-card>
|
||
|
||
<a-card :bordered="false">
|
||
<a-divider orientation="right">مانده دریافتنی</a-divider>
|
||
<TreasuryPartyBalanceTable :rows="balances" settled-label="دریافتشده" />
|
||
<a-divider orientation="right">فهرست اسناد</a-divider>
|
||
<a-form layout="vertical">
|
||
<a-form-item label="جستجو">
|
||
<a-input
|
||
v-model:value="q"
|
||
allow-clear
|
||
placeholder="عنوان یا شماره پیگیری"
|
||
@press-enter="load"
|
||
@change="load"
|
||
/>
|
||
</a-form-item>
|
||
</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>
|
||
<template #description>{{ docLine(item) }}</template>
|
||
</a-list-item-meta>
|
||
</a-list-item>
|
||
</template>
|
||
</a-list>
|
||
</a-card>
|
||
</a-flex>
|
||
|
||
<div
|
||
:style="{
|
||
position: 'fixed',
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 'var(--mobile-tabbar-h, 3.65rem)',
|
||
zIndex: 90,
|
||
background: token.colorBgElevated,
|
||
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||
padding: `${token.paddingSM}px ${token.padding}px`,
|
||
boxShadow: token.boxShadowSecondary,
|
||
}"
|
||
>
|
||
<a-row :gutter="12">
|
||
<a-col :span="8">
|
||
<a-button block @click="resetForm">انصراف</a-button>
|
||
</a-col>
|
||
<a-col :span="16">
|
||
<a-button type="primary" block :loading="saving" @click="submitMobileForm">
|
||
<template #icon><CheckOutlined /></template>
|
||
ثبت دریافت
|
||
</a-button>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ─── Desktop (unchanged interaction model) ─── -->
|
||
<template v-else>
|
||
<a-flex vertical :gap="16">
|
||
<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"
|
||
:model="form"
|
||
@finish="createDoc"
|
||
@finishFailed="onFinishFailed"
|
||
>
|
||
<a-row :gutter="[16, 0]">
|
||
<a-col :xs="24" :md="12">
|
||
<a-form-item
|
||
label="عنوان"
|
||
name="title"
|
||
:rules="[{ required: true, message: 'عنوان الزامی است' }]"
|
||
>
|
||
<a-input v-model:value="form.title" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :xs="24" :md="8">
|
||
<a-form-item
|
||
label="مبلغ کل"
|
||
name="amount"
|
||
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
||
>
|
||
<MoneyInput v-model="form.amount" :decimals="0" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :xs="24" :md="8">
|
||
<a-form-item :label="settleLabel" name="settleAmount">
|
||
<MoneyInput v-model="form.settleAmount" :decimals="0" :disabled="form.partyKind === 'other'" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :xs="24" :md="8">
|
||
<a-form-item label="دستهبندی" name="categoryId">
|
||
<a-select show-search option-filter-prop="label"
|
||
v-model:value="form.categoryId"
|
||
allow-clear
|
||
style="width: 100%"
|
||
placeholder="بدون دسته"
|
||
>
|
||
<a-select-option v-for="cat in categories" :key="cat.id" :value="cat.id" :label="cat.name">
|
||
{{ cat.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :xs="24" :md="8">
|
||
<a-form-item label="طرف حساب" name="partyKind">
|
||
<TreasuryPartySelect v-model:kind="form.partyKind" v-model:party-id="form.partyId" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :xs="24" :md="8">
|
||
<a-form-item label="حساب" name="accountId">
|
||
<a-select show-search option-filter-prop="label"
|
||
v-model:value="form.accountId"
|
||
allow-clear
|
||
style="width: 100%"
|
||
placeholder="انتخاب حساب"
|
||
>
|
||
<a-select-option
|
||
v-for="account in accounts"
|
||
:key="account.id"
|
||
:value="account.id"
|
||
:label="account.name"
|
||
>
|
||
{{ account.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :xs="24" :md="8">
|
||
<a-form-item label="روش" name="method">
|
||
<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"
|
||
>
|
||
{{ opt.label }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :xs="24" :md="8">
|
||
<a-form-item label="تاریخ" name="bookedAt">
|
||
<JalaliDateInput v-model="form.bookedAt" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :xs="24" :md="16">
|
||
<a-form-item label="توضیحات" name="notes">
|
||
<a-input v-model:value="form.notes" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-button type="primary" html-type="submit" :loading="saving">ثبت</a-button>
|
||
</a-form>
|
||
|
||
<a-divider orientation="right">مانده دریافتنی</a-divider>
|
||
<TreasuryPartyBalanceTable :rows="balances" settled-label="دریافتشده" />
|
||
<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
|
||
placeholder="عنوان یا شماره پیگیری"
|
||
@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>
|
||
<template #description>{{ docLine(item) }}</template>
|
||
</a-list-item-meta>
|
||
</a-list-item>
|
||
</template>
|
||
</a-list>
|
||
</a-card>
|
||
</a-flex>
|
||
</template>
|
||
|
||
</div>
|
||
</template>
|