feat(finance): show payroll details and search payment periods
Keep long reasons out of the report table, open them with payments in a details view, and let a payment period be found by name or month. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,21 +6,24 @@ import {
|
||||
Button as AButton,
|
||||
Card as ACard,
|
||||
Col as ACol,
|
||||
Descriptions as ADescriptions,
|
||||
DescriptionsItem as ADescriptionsItem,
|
||||
Empty as AEmpty,
|
||||
Flex as AFlex,
|
||||
Form as AForm,
|
||||
FormItem as AFormItem,
|
||||
Input as AInput,
|
||||
Modal as AModal,
|
||||
Row as ARow,
|
||||
Select as ASelect,
|
||||
Space as ASpace,
|
||||
Table as ATable,
|
||||
Textarea as ATextarea,
|
||||
Tooltip as ATooltip,
|
||||
Typography,
|
||||
message,
|
||||
theme,
|
||||
} from 'ant-design-vue';
|
||||
import { EditOutlined, WalletOutlined } from '@ant-design/icons-vue';
|
||||
import { EditOutlined, EyeOutlined, 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';
|
||||
@@ -99,6 +102,7 @@ const form = ref({
|
||||
const pay = ref({ id: '', amount: '', accountId: undefined as string | undefined });
|
||||
const editingId = ref('');
|
||||
const editingPaymentId = ref('');
|
||||
const detail = ref<PayrollRow | null>(null);
|
||||
const filterUserId = ref<string | undefined>(undefined);
|
||||
const filterYear = ref<number | undefined>(undefined);
|
||||
const filterMonth = ref<number | undefined>(undefined);
|
||||
@@ -203,33 +207,36 @@ const paymentColumns: TableColumnsType<PayrollPayment> = [
|
||||
{ title: 'عملیات', key: 'action' },
|
||||
];
|
||||
|
||||
const detailPaymentColumns: TableColumnsType<PayrollPayment> = [
|
||||
{ title: 'تاریخ', key: 'occurredAt' },
|
||||
{ title: 'حساب', dataIndex: 'accountName', key: 'accountName' },
|
||||
{ title: 'مبلغ', key: 'amount' },
|
||||
];
|
||||
|
||||
function moneyFilled(value: string) {
|
||||
return (parseMoneyNumber(String(value ?? ''), 0) ?? 0) > 0;
|
||||
}
|
||||
|
||||
function normalizeSearch(value: string) {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/[يى]/g, 'ی')
|
||||
.replace(/ك/g, 'ک')
|
||||
.replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - 0x06f0))
|
||||
.replace(/[٠-٩]/g, (digit) => String(digit.charCodeAt(0) - 0x0660))
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function filterPayOption(input: string, option?: { label?: string }) {
|
||||
return normalizeSearch(String(option?.label ?? '')).includes(normalizeSearch(input));
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -512,24 +519,48 @@ onMounted(() => {
|
||||
<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-col>
|
||||
<a-col v-if="moneyFilled(form.benefits)" :span="24">
|
||||
<a-form-item label="دلیل مزایا" style="margin-bottom: 8px">
|
||||
<a-textarea
|
||||
v-model:value="form.benefitsReason"
|
||||
placeholder="اختیاری"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="moneyFilled(form.bonus)" :span="24">
|
||||
<a-form-item label="دلیل پاداش" style="margin-bottom: 8px">
|
||||
<a-textarea
|
||||
v-model:value="form.bonusReason"
|
||||
placeholder="اختیاری"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="moneyFilled(form.deductions)" :span="24">
|
||||
<a-form-item label="دلیل کسورات" style="margin-bottom: 8px">
|
||||
<a-textarea
|
||||
v-model:value="form.deductionsReason"
|
||||
placeholder="اختیاری"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
@@ -575,8 +606,9 @@ onMounted(() => {
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:filter-option="filterPayOption"
|
||||
style="width: 100%"
|
||||
placeholder="انتخاب دوره"
|
||||
placeholder="جستجوی نام یا دوره"
|
||||
:options="payOptions"
|
||||
@change="onPayPick"
|
||||
/>
|
||||
@@ -729,6 +761,16 @@ onMounted(() => {
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space :size="4">
|
||||
<a-tooltip title="جزئیات">
|
||||
<a-button
|
||||
type="text"
|
||||
aria-label="جزئیات"
|
||||
:style="{ color: token.colorWarning }"
|
||||
@click="detail = record as PayrollRow"
|
||||
>
|
||||
<template #icon><EyeOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip v-if="record.status !== 'void'" title="ویرایش">
|
||||
<a-button
|
||||
type="text"
|
||||
@@ -751,17 +793,6 @@ onMounted(() => {
|
||||
</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>
|
||||
@@ -771,5 +802,58 @@ onMounted(() => {
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
:open="detail != null"
|
||||
:title="detail ? `جزئیات حقوق · ${detail.fullName}` : ''"
|
||||
:footer="null"
|
||||
:width="'min(640px, calc(100vw - 32px))'"
|
||||
@cancel="detail = null"
|
||||
>
|
||||
<a-flex v-if="detail" vertical :gap="16">
|
||||
<a-descriptions bordered :column="1">
|
||||
<a-descriptions-item label="پرسنل">{{ detail.fullName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="دوره">{{ formatPeriod(detail.period) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="وضعیت">{{ statusLabel[detail.status] ?? detail.status }}</a-descriptions-item>
|
||||
<a-descriptions-item label="پایه">{{ toman(detail.baseAmount) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="مزایا">{{ toman(detail.benefits) }}</a-descriptions-item>
|
||||
<a-descriptions-item v-if="detail.benefitsReason?.trim()" label="دلیل مزایا">
|
||||
<span style="white-space: pre-wrap">{{ detail.benefitsReason }}</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="پاداش">{{ toman(detail.bonus) }}</a-descriptions-item>
|
||||
<a-descriptions-item v-if="detail.bonusReason?.trim()" label="دلیل پاداش">
|
||||
<span style="white-space: pre-wrap">{{ detail.bonusReason }}</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="کسورات">{{ toman(detail.deductions) }}</a-descriptions-item>
|
||||
<a-descriptions-item v-if="detail.deductionsReason?.trim()" label="دلیل کسورات">
|
||||
<span style="white-space: pre-wrap">{{ detail.deductionsReason }}</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="قابل پرداخت">{{ toman(detail.payable) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="پرداختشده">{{ toman(detail.paidAmount) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="مانده">{{ toman(detail.remaining) }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-typography-text strong>پرداختها</a-typography-text>
|
||||
<a-empty v-if="!detail.payments.length" description="پرداختی ثبت نشده است." />
|
||||
<a-table
|
||||
v-else
|
||||
:columns="detailPaymentColumns"
|
||||
:data-source="detail.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>
|
||||
{{ record.accountName }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-flex>
|
||||
</a-modal>
|
||||
</a-flex>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user