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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user