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

776 lines
27 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,
Col as ACol,
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,
Tooltip as ATooltip,
Typography,
message,
theme,
} from 'ant-design-vue';
import { EditOutlined, WalletOutlined } from '@ant-design/icons-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { api } from '@/lib/api';
import type { FinancialAccountDto, OrgUserDto } from '@mento/shared';
import MoneyInput from '@/components/MoneyInput.vue';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import {
addJalaliMonths,
dateToJalali,
formatLocalDateInput,
jalaliMonthLength,
jalaliMonthName,
jalaliToDate,
parseLocalInput,
toFaDigits,
formatJalaliNumeric,
} from '@/lib/jalali';
import { toman } from './treasury-format';
import { parseMoneyNumber } from '@/lib/money';
const { Text: ATypographyText } = Typography;
const { useToken } = theme;
const { token } = useToken();
type PayrollPayment = {
id: string;
amount: string;
occurredAt: string;
accountId?: string;
accountName: string;
};
type PayrollRow = {
id: string;
userId: string;
fullName: string;
period: string;
baseAmount: string;
benefits: string;
benefitsReason?: string | null;
bonus: string;
bonusReason?: string | null;
deductions: string;
deductionsReason?: string | null;
payable: string;
paidAmount: string;
remaining: string;
status: string;
payments: PayrollPayment[];
};
const statusLabel: Record<string, string> = {
open: 'پرداخت‌نشده',
partial: 'پرداخت جزئی',
settled: 'تسویه‌شده',
void: 'باطل',
};
const rows = ref<PayrollRow[]>([]);
const users = ref<OrgUserDto[]>([]);
const accounts = ref<FinancialAccountDto[]>([]);
const error = ref('');
const ok = ref('');
const saving = ref(false);
const todayJalali = dateToJalali(new Date());
const periodDate = ref(formatLocalDateInput(jalaliToDate(todayJalali.jy, todayJalali.jm, 1)));
const form = ref({
userId: undefined as string | undefined,
baseAmount: '',
benefits: '',
benefitsReason: '',
bonus: '',
bonusReason: '',
deductions: '',
deductionsReason: '',
});
const pay = ref({ id: '', amount: '', accountId: undefined as string | undefined });
const editingId = ref('');
const editingPaymentId = ref('');
const filterUserId = ref<string | undefined>(undefined);
const filterYear = ref<number | undefined>(undefined);
const filterMonth = ref<number | undefined>(undefined);
const filterStatus = ref('all');
const filterMin = ref('');
const filterMax = ref('');
const statusOptions = [
{ value: 'all', label: 'همه وضعیت‌ها' },
{ value: 'open', label: 'پرداخت‌نشده' },
{ value: 'partial', label: 'پرداخت جزئی' },
{ value: 'settled', label: 'تسویه‌شده' },
{ value: 'void', label: 'باطل' },
];
const userOptions = computed(() =>
users.value.map((user) => ({ value: user.id, label: user.fullName })),
);
const monthOptions = computed(() =>
Array.from({ length: 12 }, (_, index) => ({
value: index + 1,
label: jalaliMonthName(index + 1),
})),
);
const yearOptions = computed(() => {
const year = todayJalali.jy;
return [year - 1, year, year + 1].map((value) => ({
value,
label: toFaDigits(value),
}));
});
const periodParts = computed(() => {
const date = parseLocalInput(periodDate.value);
return date ? dateToJalali(date) : { jy: todayJalali.jy, jm: todayJalali.jm, jd: 1 };
});
const builtPeriod = computed(() =>
buildPeriod(periodParts.value.jy, periodParts.value.jm, periodParts.value.jd),
);
const payablePreview = computed(() => {
const base = parseMoneyNumber(String(form.value.baseAmount ?? ''), 0) ?? 0;
const benefits = parseMoneyNumber(String(form.value.benefits ?? ''), 0) ?? 0;
const bonus = parseMoneyNumber(String(form.value.bonus ?? ''), 0) ?? 0;
const deductions = parseMoneyNumber(String(form.value.deductions ?? ''), 0) ?? 0;
return base + benefits + bonus - deductions;
});
const openRows = computed(() =>
rows.value.filter((row) => row.status === 'open' || row.status === 'partial'),
);
const payOptions = computed(() =>
rows.value
.filter((row) => row.status === 'open' || row.status === 'partial' || row.id === pay.value.id)
.filter((row) => row.status !== 'void')
.map((row) => ({
value: row.id,
label: `${row.fullName} · ${formatPeriod(row.period)} · مانده ${toman(row.remaining)}`,
})),
);
const selectedPay = computed(() => rows.value.find((row) => row.id === pay.value.id) ?? null);
const filtered = computed(() => {
const min = parseMoneyNumber(String(filterMin.value ?? ''), 0);
const max = parseMoneyNumber(String(filterMax.value ?? ''), 0);
return rows.value.filter((row) => {
if (filterUserId.value && row.userId !== filterUserId.value) return false;
if (!periodMatches(row.period, filterYear.value, filterMonth.value)) return false;
if (filterStatus.value !== 'all' && row.status !== filterStatus.value) return false;
const payable = Number(row.payable);
if (min != null && filterMin.value !== '' && payable < min) return false;
if (max != null && filterMax.value !== '' && payable > max) return false;
return true;
});
});
const moneyKeys = ['baseAmount', 'benefits', 'bonus', 'deductions', 'payable', 'paidAmount', 'remaining'] as const;
const reportColumns: TableColumnsType<PayrollRow> = [
{ title: 'پرسنل', dataIndex: 'fullName', key: 'fullName' },
{ title: 'دوره', dataIndex: 'period', key: 'period' },
{ title: 'پایه', key: 'baseAmount' },
{ title: 'مزایا', key: 'benefits' },
{ title: 'پاداش', key: 'bonus' },
{ title: 'کسورات', key: 'deductions' },
{ title: 'قابل پرداخت', key: 'payable' },
{ title: 'پرداخت‌شده', key: 'paidAmount' },
{ title: 'مانده', key: 'remaining' },
{ title: 'وضعیت', key: 'status' },
{ title: 'عملیات', key: 'action' },
];
const paymentColumns: TableColumnsType<PayrollPayment> = [
{ title: 'تاریخ', key: 'occurredAt' },
{ title: 'حساب', dataIndex: 'accountName', key: 'accountName' },
{ title: 'مبلغ', key: 'amount' },
{ title: 'عملیات', key: 'action' },
];
function moneyFilled(value: string) {
return (parseMoneyNumber(String(value ?? ''), 0) ?? 0) > 0;
}
function reasonFor(amount: string, reason: string) {
if (!moneyFilled(amount)) return null;
const text = reason.trim();
return text || null;
}
function amountReason(
row: {
benefits?: string;
benefitsReason?: string | null;
bonus?: string;
bonusReason?: string | null;
deductions?: string;
deductionsReason?: string | null;
},
key: 'benefits' | 'bonus' | 'deductions',
) {
if (!Number(row[key])) return '';
if (key === 'benefits') return row.benefitsReason?.trim() || '';
if (key === 'bonus') return row.bonusReason?.trim() || '';
return row.deductionsReason?.trim() || '';
}
function buildPeriod(year: number, month: number, day: number | null) {
const startDay = Math.min(Math.max(1, Math.trunc(Number(day)) || 1), jalaliMonthLength(year, month));
const next = addJalaliMonths(year, month, 1);
const endDay = Math.min(startDay, jalaliMonthLength(next.jy, next.jm));
const pad = (value: number) => String(value).padStart(2, '0');
return {
key: `${year}/${pad(month)}/${pad(startDay)}-${next.jy}/${pad(next.jm)}/${pad(endDay)}`,
};
}
function formatPeriod(period: string) {
const range = period.match(/^(\d{4})\/(\d{2})\/(\d{2})-(\d{4})\/(\d{2})\/(\d{2})$/);
if (range) {
const startYear = Number(range[1]);
const startMonth = Number(range[2]);
const startDay = Number(range[3]);
const endYear = Number(range[4]);
const endMonth = Number(range[5]);
const endDay = Number(range[6]);
const start = `${toFaDigits(startDay)} ${jalaliMonthName(startMonth)}${startYear === endYear ? '' : ` ${toFaDigits(startYear)}`}`;
return `${start} تا ${toFaDigits(endDay)} ${jalaliMonthName(endMonth)} ${toFaDigits(endYear)}`;
}
const legacy = period.match(/^(\d{4})-(\d{2})$/);
if (legacy) return formatPeriod(buildPeriod(Number(legacy[1]), Number(legacy[2]), 1).key);
return period;
}
function periodMatches(period: string, year?: number, month?: number) {
if (year == null && month == null) return true;
const range = period.match(/^(\d{4})\/(\d{2})\//);
const legacy = period.match(/^(\d{4})-(\d{2})$/);
const periodYearValue = Number(range?.[1] ?? legacy?.[1]);
const periodMonthValue = Number(range?.[2] ?? legacy?.[2]);
if (!periodYearValue || !periodMonthValue) return false;
if (year != null && periodYearValue !== year) return false;
if (month != null && periodMonthValue !== month) return false;
return true;
}
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 onFinishFailed() {
message.warning('لطفاً فیلدهای الزامی را کامل کنید.');
}
async function load() {
const [payroll, people, acc] = await Promise.all([
api.get<PayrollRow[]>('/treasury/payroll'),
api.get<OrgUserDto[]>('/users'),
api.get<FinancialAccountDto[]>('/treasury/accounts'),
]);
rows.value = payroll.data.map((row) => ({ ...row, payments: row.payments ?? [] }));
users.value = people.data.filter((row) => row.isActive !== false);
accounts.value = acc.data.filter((row) => row.isActive);
}
function moneyField(value: string) {
const amount = Number(value);
return amount ? String(amount) : '';
}
function periodStartDate(period: string) {
const range = period.match(/^(\d{4})\/(\d{2})\/(\d{2})/);
if (range) {
return formatLocalDateInput(jalaliToDate(Number(range[1]), Number(range[2]), Number(range[3])));
}
const legacy = period.match(/^(\d{4})-(\d{2})$/);
if (legacy) return formatLocalDateInput(jalaliToDate(Number(legacy[1]), Number(legacy[2]), 1));
return periodDate.value;
}
function blankForm() {
form.value = {
userId: undefined,
baseAmount: '',
benefits: '',
benefitsReason: '',
bonus: '',
bonusReason: '',
deductions: '',
deductionsReason: '',
};
periodDate.value = formatLocalDateInput(jalaliToDate(todayJalali.jy, todayJalali.jm, 1));
}
function payrollPayload() {
return {
userId: form.value.userId,
period: builtPeriod.value.key,
baseAmount: parseMoneyNumber(String(form.value.baseAmount ?? ''), 0) ?? 0,
benefits: parseMoneyNumber(String(form.value.benefits ?? ''), 0) ?? 0,
benefitsReason: reasonFor(form.value.benefits, form.value.benefitsReason),
bonus: parseMoneyNumber(String(form.value.bonus ?? ''), 0) ?? 0,
bonusReason: reasonFor(form.value.bonus, form.value.bonusReason),
deductions: parseMoneyNumber(String(form.value.deductions ?? ''), 0) ?? 0,
deductionsReason: reasonFor(form.value.deductions, form.value.deductionsReason),
};
}
function scrollToPayroll(id: string) {
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function startEdit(row: PayrollRow) {
editingId.value = row.id;
form.value = {
userId: row.userId,
baseAmount: moneyField(row.baseAmount),
benefits: moneyField(row.benefits),
benefitsReason: row.benefitsReason ?? '',
bonus: moneyField(row.bonus),
bonusReason: row.bonusReason ?? '',
deductions: moneyField(row.deductions),
deductionsReason: row.deductionsReason ?? '',
};
periodDate.value = periodStartDate(row.period);
editingPaymentId.value = '';
pay.value = { id: row.id, amount: '', accountId: undefined };
scrollToPayroll('payroll-setup');
}
function cancelEdit() {
editingId.value = '';
blankForm();
}
async function createRow() {
error.value = '';
ok.value = '';
saving.value = true;
const id = editingId.value;
try {
if (id) await api.patch(`/treasury/payroll/${id}`, payrollPayload());
else await api.post('/treasury/payroll', payrollPayload());
editingId.value = '';
blankForm();
ok.value = id
? 'حقوق به‌روز شد.'
: 'حقوق ثبت شد. تا وقتی پرداخت نکنید موجودی حساب عوض نمی‌شود.';
await load();
if (!id) pay.value = { id: '', amount: '', accountId: undefined };
} catch (e) {
error.value = apiMessage(e);
} finally {
saving.value = false;
}
}
function onPayPick(value: unknown) {
editingPaymentId.value = '';
if (value == null || value === '') {
pay.value = { id: '', amount: '', accountId: undefined };
return;
}
const row = rows.value.find((item) => item.id === String(value));
if (!row) {
pay.value = { id: '', amount: '', accountId: undefined };
return;
}
pay.value = {
id: row.id,
amount: row.remaining,
accountId: accounts.value[0]?.id,
};
}
function startEditPayment(payment: PayrollPayment) {
if (!pay.value.id) return;
editingPaymentId.value = payment.id;
pay.value = {
id: pay.value.id,
amount: moneyField(payment.amount),
accountId: payment.accountId,
};
scrollToPayroll('payroll-pay');
}
function cancelPaymentEdit() {
editingPaymentId.value = '';
pay.value = { id: pay.value.id, amount: '', accountId: undefined };
}
async function payRow() {
error.value = '';
ok.value = '';
saving.value = true;
const accountId = pay.value.accountId;
const keepId = pay.value.id;
try {
const amount = parseMoneyNumber(String(pay.value.amount ?? ''), 0);
if (amount == null || amount <= 0) {
error.value = 'مبلغ پرداخت معتبر نیست.';
return;
}
const paymentId = editingPaymentId.value;
if (paymentId) {
await api.patch(`/treasury/payroll/${keepId}/payments/${paymentId}`, { amount, accountId });
} else {
await api.post(`/treasury/payroll/${keepId}/pay`, {
amount,
accountId,
clientKey: crypto.randomUUID(),
});
}
editingPaymentId.value = '';
ok.value = paymentId
? 'پرداخت حقوق به‌روز شد.'
: 'پرداخت حقوق از حساب ثبت شد. اگر مانده مانده باشد، مرحلهٔ بعد را همین‌جا بزنید.';
await load();
const still = rows.value.find((row) => row.id === keepId && row.status !== 'void');
pay.value = still
? { id: still.id, amount: paymentId ? '' : still.status === 'settled' ? '' : still.remaining, accountId: paymentId ? undefined : accountId }
: { id: '', amount: '', accountId: undefined };
} catch (e) {
error.value = apiMessage(e);
} finally {
saving.value = false;
}
}
onMounted(() => {
void load().catch(() => {
error.value = 'بارگذاری حقوق ناموفق بود.';
});
});
</script>
<template>
<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 id="payroll-setup" title="تعیین حقوق" :bordered="false">
<a-form layout="vertical" :model="form" @finish="createRow" @finishFailed="onFinishFailed">
<a-row :gutter="[8, 0]">
<a-col :xs="24" :sm="12">
<a-form-item
label="پرسنل"
name="userId"
style="margin-bottom: 8px"
:rules="[{ required: true, message: 'پرسنل الزامی است' }]"
>
<a-select
v-model:value="form.userId"
show-search
option-filter-prop="label"
allow-clear
style="width: 100%"
placeholder="انتخاب"
:options="userOptions"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12">
<a-form-item label="دوره" style="margin-bottom: 8px">
<JalaliDateInput
v-model="periodDate"
start-view="months"
:clearable="false"
:display-text="formatPeriod(builtPeriod.key)"
placeholder="انتخاب دوره"
/>
</a-form-item>
</a-col>
<a-col :xs="12" :md="6">
<a-form-item
label="پایه"
name="baseAmount"
style="margin-bottom: 8px"
:rules="[{ required: true, message: 'مبلغ پایه الزامی است' }]"
>
<MoneyInput v-model="form.baseAmount" addon-after="تومان" :decimals="0" />
</a-form-item>
</a-col>
<a-col :xs="12" :md="6">
<a-form-item label="مزایا" name="benefits" style="margin-bottom: 8px">
<MoneyInput v-model="form.benefits" addon-after="تومان" :decimals="0" />
</a-form-item>
<a-form-item v-if="moneyFilled(form.benefits)" label="دلیل" style="margin-bottom: 8px">
<a-input v-model:value="form.benefitsReason" placeholder="اختیاری" />
</a-form-item>
</a-col>
<a-col :xs="12" :md="6">
<a-form-item label="پاداش" name="bonus" style="margin-bottom: 8px">
<MoneyInput v-model="form.bonus" addon-after="تومان" :decimals="0" />
</a-form-item>
<a-form-item v-if="moneyFilled(form.bonus)" label="دلیل" style="margin-bottom: 8px">
<a-input v-model:value="form.bonusReason" placeholder="اختیاری" />
</a-form-item>
</a-col>
<a-col :xs="12" :md="6">
<a-form-item label="کسورات" name="deductions" style="margin-bottom: 8px">
<MoneyInput v-model="form.deductions" addon-after="تومان" :decimals="0" />
</a-form-item>
<a-form-item v-if="moneyFilled(form.deductions)" label="دلیل" style="margin-bottom: 8px">
<a-input v-model:value="form.deductionsReason" placeholder="اختیاری" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-flex align="center" justify="space-between" wrap="wrap" :gap="8">
<a-typography-text>
قابل پرداخت
<a-typography-text strong>{{ toman(payablePreview) }}</a-typography-text>
</a-typography-text>
<a-space>
<a-button v-if="editingId" :disabled="saving" @click="cancelEdit">انصراف</a-button>
<a-button type="primary" html-type="submit" :loading="saving">
{{ editingId ? 'ذخیره تغییرات' : 'ثبت حقوق' }}
</a-button>
</a-space>
</a-flex>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card id="payroll-pay" title="پرداخت" :bordered="false">
<a-flex vertical :gap="16">
<a-typography-text type="secondary">
می‌توانید کل مانده را یکجا بدهید، یا هر بار بخشی را تا تمام شدن مانده ثبت کنید.
</a-typography-text>
<a-empty v-if="!openRows.length && !pay.id" description="حقوق باز برای پرداخت نیست." />
<a-form
v-else
layout="vertical"
:model="pay"
@finish="payRow"
@finishFailed="onFinishFailed"
>
<a-row :gutter="[16, 0]">
<a-col :xs="24" :md="12">
<a-form-item
label="دورهٔ حقوق"
name="id"
:rules="[{ required: true, message: 'دوره الزامی است' }]"
>
<a-select
:value="pay.id || undefined"
allow-clear
show-search
option-filter-prop="label"
style="width: 100%"
placeholder="انتخاب دوره"
:options="payOptions"
@change="onPayPick"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="6">
<a-form-item
label="مبلغ این مرحله"
name="amount"
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
>
<MoneyInput v-model="pay.amount" addon-after="تومان" :decimals="0" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="6">
<a-form-item
label="حساب"
name="accountId"
:rules="[{ required: true, message: 'حساب الزامی است' }]"
>
<a-select
v-model:value="pay.accountId"
show-search
option-filter-prop="label"
allow-clear
style="width: 100%"
placeholder="انتخاب حساب"
:options="accounts.map((account) => ({ value: account.id, label: account.name }))"
/>
</a-form-item>
</a-col>
</a-row>
<a-flex v-if="selectedPay" :gap="16" wrap="wrap">
<a-typography-text>قابل پرداخت {{ toman(selectedPay.payable) }}</a-typography-text>
<a-typography-text>پرداخت‌شده {{ toman(selectedPay.paidAmount) }}</a-typography-text>
<a-typography-text strong>مانده {{ toman(selectedPay.remaining) }}</a-typography-text>
</a-flex>
<a-space>
<a-button v-if="editingPaymentId" :disabled="saving" @click="cancelPaymentEdit">انصراف</a-button>
<a-button type="primary" html-type="submit" :loading="saving" :disabled="!pay.id">
{{ editingPaymentId ? 'ذخیره پرداخت' : 'ثبت پرداخت' }}
</a-button>
</a-space>
</a-form>
<a-table
v-if="selectedPay?.payments.length"
:columns="paymentColumns"
:data-source="selectedPay.payments"
:pagination="false"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'occurredAt'">
{{ formatJalaliNumeric(new Date(record.occurredAt)) }}
</template>
<template v-else-if="column.key === 'amount'">
{{ toman(record.amount) }}
</template>
<template v-else-if="column.key === 'action'">
<a-tooltip title="ویرایش">
<a-button
type="text"
aria-label="ویرایش"
:style="{ color: token.colorInfo }"
@click="startEditPayment(record as PayrollPayment)"
>
<template #icon><EditOutlined /></template>
</a-button>
</a-tooltip>
</template>
<template v-else>
{{ record.accountName }}
</template>
</template>
</a-table>
</a-flex>
</a-card>
<a-card title="گزارش حقوق" :bordered="false">
<a-form layout="vertical">
<a-row :gutter="[16, 0]">
<a-col :xs="24" :md="8">
<a-form-item label="پرسنل">
<a-select
v-model:value="filterUserId"
allow-clear
show-search
option-filter-prop="label"
style="width: 100%"
placeholder="همه"
:options="userOptions"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="دوره">
<a-row :gutter="[8, 0]">
<a-col :span="12">
<a-select
v-model:value="filterYear"
allow-clear
placeholder="سال"
style="width: 100%"
:options="yearOptions"
/>
</a-col>
<a-col :span="12">
<a-select
v-model:value="filterMonth"
allow-clear
placeholder="ماه"
style="width: 100%"
:options="monthOptions"
/>
</a-col>
</a-row>
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="وضعیت">
<a-select v-model:value="filterStatus" style="width: 100%" :options="statusOptions" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="حداقل قابل پرداخت">
<MoneyInput v-model="filterMin" addon-after="تومان" :decimals="0" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="حداکثر قابل پرداخت">
<MoneyInput v-model="filterMax" addon-after="تومان" :decimals="0" />
</a-form-item>
</a-col>
</a-row>
</a-form>
<a-table
:columns="reportColumns"
:data-source="filtered"
row-key="id"
:scroll="{ x: 'max-content' }"
:locale="{ emptyText: 'رکوردی با این فیلتر نیست.' }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
{{ statusLabel[record.status] ?? record.status }}
</template>
<template v-else-if="column.key === 'period'">
{{ formatPeriod(record.period) }}
</template>
<template v-else-if="column.key === 'action'">
<a-space :size="4">
<a-tooltip v-if="record.status !== 'void'" title="ویرایش">
<a-button
type="text"
aria-label="ویرایش"
:style="{ color: token.colorInfo }"
@click="startEdit(record as PayrollRow)"
>
<template #icon><EditOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip v-if="record.status === 'open' || record.status === 'partial'" title="پرداخت">
<a-button
type="text"
aria-label="پرداخت"
:style="{ color: token.colorPrimary }"
@click="onPayPick(record.id)"
>
<template #icon><WalletOutlined /></template>
</a-button>
</a-tooltip>
</a-space>
</template>
<template v-else-if="column.key === 'benefits' || column.key === 'bonus' || column.key === 'deductions'">
<a-flex vertical>
<span>{{ toman(record[column.key as 'benefits' | 'bonus' | 'deductions']) }}</span>
<a-typography-text
v-if="amountReason(record, column.key as 'benefits' | 'bonus' | 'deductions')"
type="secondary"
>
{{ amountReason(record, column.key as 'benefits' | 'bonus' | 'deductions') }}
</a-typography-text>
</a-flex>
</template>
<template v-else-if="moneyKeys.includes(column.key as (typeof moneyKeys)[number])">
{{ toman(record[column.key as (typeof moneyKeys)[number]]) }}
</template>
<template v-else>
{{ column.dataIndex ? record[column.dataIndex as 'fullName' | 'period'] : '' }}
</template>
</template>
</a-table>
</a-card>
</a-flex>
</template>