feat(finance): open payroll details from mobile cards

Show each payroll as a card on a phone and slide its details up from the bottom so a short sheet leaves the top of the screen clear.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-08 18:13:38 +03:30
co-authored by Cursor
parent d33d88e76d
commit 06036b00d3
@@ -12,6 +12,7 @@ import {
Flex as AFlex,
Form as AForm,
FormItem as AFormItem,
Grid,
Modal as AModal,
Row as ARow,
Select as ASelect,
@@ -46,6 +47,8 @@ import { parseMoneyNumber } from '@/lib/money';
const { Text: ATypographyText } = Typography;
const { useToken } = theme;
const { token } = useToken();
const screens = Grid.useBreakpoint();
const isMobile = computed(() => !screens.value.md);
type PayrollPayment = {
id: string;
@@ -207,12 +210,6 @@ 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;
}
@@ -745,6 +742,95 @@ onMounted(() => {
</a-col>
</a-row>
</a-form>
<a-row>
<a-col :xs="24" :md="0">
<a-empty v-if="!filtered.length" description="رکوردی با این فیلتر نیست." />
<a-flex v-else vertical :gap="8">
<a-card
v-for="row in filtered"
:key="row.id"
hoverable
@click="detail = row"
>
<a-flex vertical :gap="8">
<a-flex justify="space-between" align="center" :gap="8">
<a-typography-text strong>{{ row.fullName }}</a-typography-text>
<a-typography-text type="secondary">{{ statusLabel[row.status] ?? row.status }}</a-typography-text>
</a-flex>
<a-typography-text>{{ formatPeriod(row.period) }}</a-typography-text>
<a-row :gutter="[8, 8]">
<a-col :span="12">
<a-flex vertical>
<a-typography-text type="secondary">پایه</a-typography-text>
<span>{{ toman(row.baseAmount) }}</span>
</a-flex>
</a-col>
<a-col :span="12">
<a-flex vertical>
<a-typography-text type="secondary">مزایا</a-typography-text>
<span>{{ toman(row.benefits) }}</span>
</a-flex>
</a-col>
<a-col :span="12">
<a-flex vertical>
<a-typography-text type="secondary">پاداش</a-typography-text>
<span>{{ toman(row.bonus) }}</span>
</a-flex>
</a-col>
<a-col :span="12">
<a-flex vertical>
<a-typography-text type="secondary">کسورات</a-typography-text>
<span>{{ toman(row.deductions) }}</span>
</a-flex>
</a-col>
<a-col :span="12">
<a-flex vertical>
<a-typography-text type="secondary">قابل پرداخت</a-typography-text>
<span>{{ toman(row.payable) }}</span>
</a-flex>
</a-col>
<a-col :span="12">
<a-flex vertical>
<a-typography-text type="secondary">پرداخت‌شده</a-typography-text>
<span>{{ toman(row.paidAmount) }}</span>
</a-flex>
</a-col>
<a-col :span="24">
<a-flex justify="space-between" align="center" :gap="8">
<a-flex vertical>
<a-typography-text type="secondary">مانده</a-typography-text>
<a-typography-text strong>{{ toman(row.remaining) }}</a-typography-text>
</a-flex>
<a-flex :gap="4" align="center" @click.stop>
<a-tooltip v-if="row.status !== 'void'" title="ویرایش">
<a-button
type="text"
aria-label="ویرایش"
:style="{ color: token.colorInfo }"
@click="startEdit(row)"
>
<template #icon><EditOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip v-if="row.status === 'open' || row.status === 'partial'" title="پرداخت">
<a-button
type="text"
aria-label="پرداخت"
:style="{ color: token.colorPrimary }"
@click="onPayPick(row.id)"
>
<template #icon><WalletOutlined /></template>
</a-button>
</a-tooltip>
</a-flex>
</a-flex>
</a-col>
</a-row>
</a-flex>
</a-card>
</a-flex>
</a-col>
<a-col :xs="0" :md="24">
<a-table
:columns="reportColumns"
:data-source="filtered"
@@ -801,19 +887,32 @@ onMounted(() => {
</template>
</template>
</a-table>
</a-col>
</a-row>
</a-card>
<a-modal
:open="detail != null"
:title="detail ? `جزئیات حقوق · ${detail.fullName}` : ''"
:footer="null"
:width="'min(640px, calc(100vw - 32px))'"
:width="isMobile ? '100%' : 'min(640px, calc(100vw - 32px))'"
:wrap-style="isMobile ? { overflow: 'hidden' } : undefined"
:style="isMobile ? { position: 'absolute', top: 'auto', bottom: 0, left: 0, right: 0, width: '100%', maxWidth: '100%', height: 'auto', maxHeight: '100%', margin: 0, paddingBottom: 0, overflow: 'hidden' } : undefined"
:body-style="isMobile ? { maxHeight: 'calc(100svh - 72px)', overflowX: 'hidden', overflowY: 'auto', scrollbarWidth: 'none' } : undefined"
@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>
<template #title>
<a-flex v-if="detail" justify="space-between" align="center" wrap="wrap" :gap="8" :style="{ paddingInlineEnd: '32px' }">
<span>جزئیات حقوق · {{ detail.fullName }}</span>
<span>{{ formatPeriod(detail.period) }}</span>
</a-flex>
</template>
<a-flex v-if="detail" vertical :gap="16" style="max-width: 100%; overflow-x: hidden">
<a-descriptions
bordered
:column="1"
:label-style="{ whiteSpace: 'normal' }"
:content-style="{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }"
>
<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>
@@ -834,25 +933,22 @@ onMounted(() => {
</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 v-else vertical :gap="8">
<a-flex
v-for="payment in detail.payments"
:key="payment.id"
justify="space-between"
align="center"
wrap="wrap"
:gap="8"
>
<a-flex vertical>
<span>{{ formatJalaliNumeric(new Date(payment.occurredAt)) }}</span>
<a-typography-text type="secondary">{{ payment.accountName }}</a-typography-text>
</a-flex>
<span>{{ toman(payment.amount) }}</span>
</a-flex>
</a-flex>
</a-flex>
</a-modal>
</a-flex>