feat(finance): edit payroll with month ranges, reasons, and toman
Let salary and payments be corrected after they are saved, pick a Jalali period as a month range, and show toman on finance amounts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "FinancialPayroll" ADD COLUMN "benefitsReason" TEXT;
|
||||||
|
ALTER TABLE "FinancialPayroll" ADD COLUMN "bonusReason" TEXT;
|
||||||
|
ALTER TABLE "FinancialPayroll" ADD COLUMN "deductionsReason" TEXT;
|
||||||
@@ -2447,8 +2447,11 @@ model FinancialPayroll {
|
|||||||
period String
|
period String
|
||||||
baseAmount Decimal @db.Decimal(18, 2)
|
baseAmount Decimal @db.Decimal(18, 2)
|
||||||
benefits Decimal @default(0) @db.Decimal(18, 2)
|
benefits Decimal @default(0) @db.Decimal(18, 2)
|
||||||
|
benefitsReason String?
|
||||||
bonus Decimal @default(0) @db.Decimal(18, 2)
|
bonus Decimal @default(0) @db.Decimal(18, 2)
|
||||||
|
bonusReason String?
|
||||||
deductions Decimal @default(0) @db.Decimal(18, 2)
|
deductions Decimal @default(0) @db.Decimal(18, 2)
|
||||||
|
deductionsReason String?
|
||||||
payable Decimal @db.Decimal(18, 2)
|
payable Decimal @db.Decimal(18, 2)
|
||||||
dueDate DateTime
|
dueDate DateTime
|
||||||
notes String?
|
notes String?
|
||||||
|
|||||||
@@ -13,11 +13,20 @@ function dec(value: Prisma.Decimal | number | string | null | undefined) {
|
|||||||
function money(value: Prisma.Decimal | number | string) {
|
function money(value: Prisma.Decimal | number | string) {
|
||||||
return new Prisma.Decimal(value).toFixed(2);
|
return new Prisma.Decimal(value).toFixed(2);
|
||||||
}
|
}
|
||||||
|
function reasonText(value?: string | null) {
|
||||||
|
const text = value?.trim() ?? '';
|
||||||
|
return text ? text.slice(0, 500) : null;
|
||||||
|
}
|
||||||
function parseDate(iso?: string | null) {
|
function parseDate(iso?: string | null) {
|
||||||
if (!iso) return null;
|
if (!iso) return null;
|
||||||
const d = new Date(iso);
|
const d = new Date(iso);
|
||||||
return Number.isNaN(d.getTime()) ? null : d;
|
return Number.isNaN(d.getTime()) ? null : d;
|
||||||
}
|
}
|
||||||
|
function payrollStatus(payable: Prisma.Decimal, paid: Prisma.Decimal) {
|
||||||
|
if (paid.lessThanOrEqualTo(0)) return 'open';
|
||||||
|
if (paid.greaterThanOrEqualTo(payable)) return 'settled';
|
||||||
|
return 'partial';
|
||||||
|
}
|
||||||
function periodKey(date: Date) {
|
function periodKey(date: Date) {
|
||||||
return date.toISOString().slice(0, 10);
|
return date.toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
@@ -62,8 +71,11 @@ export class TreasuryOpsService {
|
|||||||
period: string;
|
period: string;
|
||||||
baseAmount: string | number;
|
baseAmount: string | number;
|
||||||
benefits?: string | number;
|
benefits?: string | number;
|
||||||
|
benefitsReason?: string | null;
|
||||||
bonus?: string | number;
|
bonus?: string | number;
|
||||||
|
bonusReason?: string | null;
|
||||||
deductions?: string | number;
|
deductions?: string | number;
|
||||||
|
deductionsReason?: string | null;
|
||||||
dueDate?: string;
|
dueDate?: string;
|
||||||
notes?: string | null;
|
notes?: string | null;
|
||||||
},
|
},
|
||||||
@@ -88,8 +100,11 @@ export class TreasuryOpsService {
|
|||||||
period: input.period.trim(),
|
period: input.period.trim(),
|
||||||
baseAmount: base,
|
baseAmount: base,
|
||||||
benefits,
|
benefits,
|
||||||
|
benefitsReason: benefits.greaterThan(0) ? reasonText(input.benefitsReason) : null,
|
||||||
bonus,
|
bonus,
|
||||||
|
bonusReason: bonus.greaterThan(0) ? reasonText(input.bonusReason) : null,
|
||||||
deductions,
|
deductions,
|
||||||
|
deductionsReason: deductions.greaterThan(0) ? reasonText(input.deductionsReason) : null,
|
||||||
payable,
|
payable,
|
||||||
dueDate: parseDate(input.dueDate) ?? new Date(),
|
dueDate: parseDate(input.dueDate) ?? new Date(),
|
||||||
notes: input.notes?.trim() || null,
|
notes: input.notes?.trim() || null,
|
||||||
@@ -137,7 +152,7 @@ export class TreasuryOpsService {
|
|||||||
if (!account) throw new BadRequestException('حساب پرداخت پیدا نشد.');
|
if (!account) throw new BadRequestException('حساب پرداخت پیدا نشد.');
|
||||||
const balance = await this.balance(account.id);
|
const balance = await this.balance(account.id);
|
||||||
if (balance.lessThan(amount)) throw new BadRequestException('موجودی حساب کافی نیست.');
|
if (balance.lessThan(amount)) throw new BadRequestException('موجودی حساب کافی نیست.');
|
||||||
const status = paid.plus(amount).equals(row.payable) ? 'settled' : 'partial';
|
const status = payrollStatus(row.payable, paid.plus(amount));
|
||||||
await this.prisma.$transaction([
|
await this.prisma.$transaction([
|
||||||
this.prisma.financialMovement.create({
|
this.prisma.financialMovement.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -158,6 +173,123 @@ export class TreasuryOpsService {
|
|||||||
return this.listPayroll(actor);
|
return this.listPayroll(actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updatePayroll(
|
||||||
|
actor: Actor,
|
||||||
|
id: string,
|
||||||
|
input: {
|
||||||
|
userId: string;
|
||||||
|
period: string;
|
||||||
|
baseAmount: string | number;
|
||||||
|
benefits?: string | number;
|
||||||
|
benefitsReason?: string | null;
|
||||||
|
bonus?: string | number;
|
||||||
|
bonusReason?: string | null;
|
||||||
|
deductions?: string | number;
|
||||||
|
deductionsReason?: string | null;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const row = await this.prisma.financialPayroll.findFirst({
|
||||||
|
where: { id, organizationId: actor.organizationId },
|
||||||
|
include: { movements: true },
|
||||||
|
});
|
||||||
|
if (!row || row.voidedAt || row.status === 'void') {
|
||||||
|
throw new NotFoundException('رکورد حقوق برای ویرایش پیدا نشد.');
|
||||||
|
}
|
||||||
|
const base = dec(input.baseAmount);
|
||||||
|
const benefits = dec(input.benefits ?? 0);
|
||||||
|
const bonus = dec(input.bonus ?? 0);
|
||||||
|
const deductions = dec(input.deductions ?? 0);
|
||||||
|
const payable = base.plus(benefits).plus(bonus).minus(deductions);
|
||||||
|
if (!input.userId || !input.period?.trim()) {
|
||||||
|
throw new BadRequestException('پرسنل و دوره حقوق لازم است.');
|
||||||
|
}
|
||||||
|
if (!payable.greaterThan(0)) throw new BadRequestException('مبلغ قابل پرداخت باید بزرگتر از صفر باشد.');
|
||||||
|
const paid = this.paidOf(row.movements);
|
||||||
|
if (payable.lessThan(paid)) {
|
||||||
|
throw new BadRequestException('مبلغ قابل پرداخت از پرداختهای ثبتشده کمتر است.');
|
||||||
|
}
|
||||||
|
const member = await this.prisma.membership.findFirst({
|
||||||
|
where: { organizationId: actor.organizationId, userId: input.userId, isActive: true },
|
||||||
|
});
|
||||||
|
if (!member) throw new BadRequestException('این کاربر عضو محیط نیست.');
|
||||||
|
try {
|
||||||
|
await this.prisma.financialPayroll.update({
|
||||||
|
where: { id: row.id },
|
||||||
|
data: {
|
||||||
|
userId: input.userId,
|
||||||
|
period: input.period.trim(),
|
||||||
|
baseAmount: base,
|
||||||
|
benefits,
|
||||||
|
benefitsReason: benefits.greaterThan(0) ? reasonText(input.benefitsReason) : null,
|
||||||
|
bonus,
|
||||||
|
bonusReason: bonus.greaterThan(0) ? reasonText(input.bonusReason) : null,
|
||||||
|
deductions,
|
||||||
|
deductionsReason: deductions.greaterThan(0) ? reasonText(input.deductionsReason) : null,
|
||||||
|
payable,
|
||||||
|
status: payrollStatus(payable, paid),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
|
||||||
|
throw new BadRequestException('برای این پرسنل در این دوره حقوق ثبت شده است.');
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return this.listPayroll(actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePayrollPayment(
|
||||||
|
actor: Actor,
|
||||||
|
payrollId: string,
|
||||||
|
movementId: string,
|
||||||
|
input: { amount: string | number; accountId: string; occurredAt?: string },
|
||||||
|
) {
|
||||||
|
const movement = await this.prisma.financialMovement.findFirst({
|
||||||
|
where: {
|
||||||
|
id: movementId,
|
||||||
|
payrollId,
|
||||||
|
organizationId: actor.organizationId,
|
||||||
|
voidedAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const payroll = await this.prisma.financialPayroll.findFirst({
|
||||||
|
where: { id: payrollId, organizationId: actor.organizationId },
|
||||||
|
include: { movements: true },
|
||||||
|
});
|
||||||
|
if (!movement || !payroll || payroll.voidedAt || payroll.status === 'void') {
|
||||||
|
throw new NotFoundException('پرداخت حقوق برای ویرایش پیدا نشد.');
|
||||||
|
}
|
||||||
|
const amount = dec(input.amount);
|
||||||
|
const otherPaid = this.paidOf(payroll.movements.filter((item) => item.id !== movement.id));
|
||||||
|
if (!amount.greaterThan(0) || otherPaid.plus(amount).greaterThan(payroll.payable)) {
|
||||||
|
throw new BadRequestException('مبلغ پرداخت حقوق نامعتبر است.');
|
||||||
|
}
|
||||||
|
const account = await this.prisma.financialAccount.findFirst({
|
||||||
|
where: { id: input.accountId, organizationId: actor.organizationId },
|
||||||
|
});
|
||||||
|
if (!account || (!account.isActive && account.id !== movement.accountId)) {
|
||||||
|
throw new BadRequestException('حساب پرداخت پیدا نشد.');
|
||||||
|
}
|
||||||
|
const available = await this.balance(account.id);
|
||||||
|
const room = account.id === movement.accountId ? available.plus(movement.amount) : available;
|
||||||
|
if (room.lessThan(amount)) throw new BadRequestException('موجودی حساب کافی نیست.');
|
||||||
|
await this.prisma.$transaction([
|
||||||
|
this.prisma.financialMovement.update({
|
||||||
|
where: { id: movement.id },
|
||||||
|
data: {
|
||||||
|
accountId: account.id,
|
||||||
|
amount,
|
||||||
|
occurredAt: parseDate(input.occurredAt) ?? movement.occurredAt,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
this.prisma.financialPayroll.update({
|
||||||
|
where: { id: payroll.id },
|
||||||
|
data: { status: payrollStatus(payroll.payable, otherPaid.plus(amount)) },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
return this.listPayroll(actor);
|
||||||
|
}
|
||||||
|
|
||||||
async listRecurring(actor: Actor) {
|
async listRecurring(actor: Actor) {
|
||||||
const rows = await this.prisma.financialRecurringItem.findMany({
|
const rows = await this.prisma.financialRecurringItem.findMany({
|
||||||
where: { organizationId: actor.organizationId },
|
where: { organizationId: actor.organizationId },
|
||||||
@@ -495,8 +627,11 @@ export class TreasuryOpsService {
|
|||||||
period: string;
|
period: string;
|
||||||
baseAmount: Prisma.Decimal;
|
baseAmount: Prisma.Decimal;
|
||||||
benefits: Prisma.Decimal;
|
benefits: Prisma.Decimal;
|
||||||
|
benefitsReason?: string | null;
|
||||||
bonus: Prisma.Decimal;
|
bonus: Prisma.Decimal;
|
||||||
|
bonusReason?: string | null;
|
||||||
deductions: Prisma.Decimal;
|
deductions: Prisma.Decimal;
|
||||||
|
deductionsReason?: string | null;
|
||||||
payable: Prisma.Decimal;
|
payable: Prisma.Decimal;
|
||||||
dueDate: Date;
|
dueDate: Date;
|
||||||
status: string;
|
status: string;
|
||||||
@@ -504,6 +639,7 @@ export class TreasuryOpsService {
|
|||||||
id: string;
|
id: string;
|
||||||
amount: Prisma.Decimal;
|
amount: Prisma.Decimal;
|
||||||
voidedAt: Date | null;
|
voidedAt: Date | null;
|
||||||
|
accountId: string;
|
||||||
occurredAt: Date;
|
occurredAt: Date;
|
||||||
account?: { name: string } | null;
|
account?: { name: string } | null;
|
||||||
}>;
|
}>;
|
||||||
@@ -519,8 +655,11 @@ export class TreasuryOpsService {
|
|||||||
period: row.period,
|
period: row.period,
|
||||||
baseAmount: money(row.baseAmount),
|
baseAmount: money(row.baseAmount),
|
||||||
benefits: money(row.benefits),
|
benefits: money(row.benefits),
|
||||||
|
benefitsReason: row.benefitsReason?.trim() || null,
|
||||||
bonus: money(row.bonus),
|
bonus: money(row.bonus),
|
||||||
|
bonusReason: row.bonusReason?.trim() || null,
|
||||||
deductions: money(row.deductions),
|
deductions: money(row.deductions),
|
||||||
|
deductionsReason: row.deductionsReason?.trim() || null,
|
||||||
payable: money(row.payable),
|
payable: money(row.payable),
|
||||||
paidAmount: money(paid),
|
paidAmount: money(paid),
|
||||||
remaining: money(left.isNegative() ? 0 : left),
|
remaining: money(left.isNegative() ? 0 : left),
|
||||||
@@ -532,6 +671,7 @@ export class TreasuryOpsService {
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
amount: money(item.amount),
|
amount: money(item.amount),
|
||||||
occurredAt: item.occurredAt.toISOString(),
|
occurredAt: item.occurredAt.toISOString(),
|
||||||
|
accountId: item.accountId,
|
||||||
accountName: item.account?.name ?? '',
|
accountName: item.account?.name ?? '',
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -232,6 +232,25 @@ export class TreasuryController {
|
|||||||
return this.ops.createPayroll(user, body);
|
return this.ops.createPayroll(user, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch('payroll/:id')
|
||||||
|
updatePayroll(
|
||||||
|
@CurrentUser() user: Actor,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: Parameters<TreasuryOpsService['updatePayroll']>[2],
|
||||||
|
) {
|
||||||
|
return this.ops.updatePayroll(user, id, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('payroll/:id/payments/:movementId')
|
||||||
|
updatePayrollPayment(
|
||||||
|
@CurrentUser() user: Actor,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('movementId') movementId: string,
|
||||||
|
@Body() body: Parameters<TreasuryOpsService['updatePayrollPayment']>[3],
|
||||||
|
) {
|
||||||
|
return this.ops.updatePayrollPayment(user, id, movementId, body);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('payroll/:id/pay')
|
@Post('payroll/:id/pay')
|
||||||
payPayroll(
|
payPayroll(
|
||||||
@CurrentUser() user: Actor,
|
@CurrentUser() user: Actor,
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ const props = withDefaults(
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
clearable?: boolean;
|
clearable?: boolean;
|
||||||
icon?: 'calendar' | 'clock' | 'bell';
|
icon?: 'calendar' | 'clock' | 'bell';
|
||||||
|
/** اگر داده شود، متن باکس بهجای تاریخ خام همین مقدار است. */
|
||||||
|
displayText?: string;
|
||||||
|
/** نمایی که با باز شدن پنل دیده میشود. */
|
||||||
|
startView?: 'days' | 'months' | 'years';
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
mode: 'date',
|
mode: 'date',
|
||||||
@@ -37,6 +41,8 @@ const props = withDefaults(
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
clearable: true,
|
clearable: true,
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
|
displayText: '',
|
||||||
|
startView: 'days',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -87,6 +93,7 @@ const triggerIcon = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const display = computed(() => {
|
const display = computed(() => {
|
||||||
|
if (props.displayText) return props.displayText;
|
||||||
if (props.mode === 'time') {
|
if (props.mode === 'time') {
|
||||||
const match = /^(\d{1,2}):(\d{2})$/.exec(model.value);
|
const match = /^(\d{1,2}):(\d{2})$/.exec(model.value);
|
||||||
if (!match) return '';
|
if (!match) return '';
|
||||||
@@ -368,7 +375,7 @@ function onWindowChange() {
|
|||||||
async function show() {
|
async function show() {
|
||||||
if (props.disabled) return;
|
if (props.disabled) return;
|
||||||
syncDraftFrom(parsed.value);
|
syncDraftFrom(parsed.value);
|
||||||
calView.value = 'days';
|
calView.value = props.startView;
|
||||||
clockFace.value = 'hour';
|
clockFace.value = 'hour';
|
||||||
syncViewport();
|
syncViewport();
|
||||||
liftAboveOverlays();
|
liftAboveOverlays();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const props = withDefaults(
|
|||||||
/** اگر true باشد مقدار عددی emit میشود (نه رشته) */
|
/** اگر true باشد مقدار عددی emit میشود (نه رشته) */
|
||||||
asNumber?: boolean;
|
asNumber?: boolean;
|
||||||
dir?: 'ltr' | 'rtl';
|
dir?: 'ltr' | 'rtl';
|
||||||
|
addonAfter?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
required: false,
|
required: false,
|
||||||
@@ -30,6 +31,7 @@ const props = withDefaults(
|
|||||||
decimals: 2,
|
decimals: 2,
|
||||||
asNumber: false,
|
asNumber: false,
|
||||||
dir: 'ltr',
|
dir: 'ltr',
|
||||||
|
addonAfter: '',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -129,6 +131,7 @@ function normalizeSep(raw: string): string {
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:value="text"
|
:value="text"
|
||||||
|
:addon-after="addonAfter || undefined"
|
||||||
style="width: 100%; font-variant-numeric: tabular-nums"
|
style="width: 100%; font-variant-numeric: tabular-nums"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ onUnmounted(() => {
|
|||||||
<a-row :gutter="[16, 0]">
|
<a-row :gutter="[16, 0]">
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="موجودی واقعی">
|
<a-form-item label="موجودی واقعی">
|
||||||
<MoneyInput v-model="stated" :decimals="0" />
|
<MoneyInput addon-after="تومان" v-model="stated" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ onMounted(() => {
|
|||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="موجودی اولیه" name="openingBalance">
|
<a-form-item label="موجودی اولیه" name="openingBalance">
|
||||||
<MoneyInput v-model="form.openingBalance" :decimals="0" />
|
<MoneyInput addon-after="تومان" v-model="form.openingBalance" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
@@ -400,7 +400,7 @@ onMounted(() => {
|
|||||||
name="amount"
|
name="amount"
|
||||||
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
||||||
>
|
>
|
||||||
<MoneyInput v-model="transfer.amount" :decimals="0" />
|
<MoneyInput addon-after="تومان" v-model="transfer.amount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ onMounted(() => {
|
|||||||
<UiCard title="سند جدید">
|
<UiCard title="سند جدید">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<UiField label="عنوان"><UiInput v-model="form.title" /></UiField>
|
<UiField label="عنوان"><UiInput v-model="form.title" /></UiField>
|
||||||
<UiField label="مبلغ کل"><MoneyInput v-model="form.amount" :decimals="0" /></UiField>
|
<UiField label="مبلغ کل"><MoneyInput addon-after="تومان" v-model="form.amount" :decimals="0" /></UiField>
|
||||||
<UiField label="دستهبندی">
|
<UiField label="دستهبندی">
|
||||||
<select v-model="form.categoryId" class="select">
|
<select v-model="form.categoryId" class="select">
|
||||||
<option value="">بدون دسته</option>
|
<option value="">بدون دسته</option>
|
||||||
@@ -166,7 +166,7 @@ onMounted(() => {
|
|||||||
</select>
|
</select>
|
||||||
</UiField>
|
</UiField>
|
||||||
<UiField label="طرف حساب"><UiInput v-model="form.partyName" /></UiField>
|
<UiField label="طرف حساب"><UiInput v-model="form.partyName" /></UiField>
|
||||||
<UiField :label="settleLabel"><MoneyInput v-model="form.settleAmount" :decimals="0" /></UiField>
|
<UiField :label="settleLabel"><MoneyInput addon-after="تومان" v-model="form.settleAmount" :decimals="0" /></UiField>
|
||||||
<UiField label="حساب">
|
<UiField label="حساب">
|
||||||
<select v-model="form.accountId" class="select">
|
<select v-model="form.accountId" class="select">
|
||||||
<option value="">انتخاب حساب</option>
|
<option value="">انتخاب حساب</option>
|
||||||
|
|||||||
@@ -215,12 +215,12 @@ onMounted(() => {
|
|||||||
name="amount"
|
name="amount"
|
||||||
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
||||||
>
|
>
|
||||||
<MoneyInput v-model="form.amount" :decimals="0" />
|
<MoneyInput addon-after="تومان" v-model="form.amount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item :label="settleLabel" name="settleAmount">
|
<a-form-item :label="settleLabel" name="settleAmount">
|
||||||
<MoneyInput v-model="form.settleAmount" :decimals="0" :disabled="form.partyKind === 'other'" />
|
<MoneyInput addon-after="تومان" v-model="form.settleAmount" :decimals="0" :disabled="form.partyKind === 'other'" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
|
|||||||
@@ -411,20 +411,20 @@ onMounted(() => {
|
|||||||
<a-row :gutter="[12, 0]">
|
<a-row :gutter="[12, 0]">
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item
|
<a-form-item
|
||||||
label="مبلغ کل (تومان)"
|
label="مبلغ کل"
|
||||||
name="amount"
|
name="amount"
|
||||||
:rules="[{ required: true, message: 'مبلغ کل را وارد کنید' }]"
|
:rules="[{ required: true, message: 'مبلغ کل را وارد کنید' }]"
|
||||||
>
|
>
|
||||||
<MoneyInput v-model="form.amount" :decimals="0" placeholder="۰" />
|
<MoneyInput addon-after="تومان" v-model="form.amount" :decimals="0" placeholder="۰" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item
|
<a-form-item
|
||||||
:label="`${settleLabel} (تومان)`"
|
:label="settleLabel"
|
||||||
name="settleAmount"
|
name="settleAmount"
|
||||||
:rules="[{ required: true, message: 'مبلغ دریافتشده را وارد کنید' }]"
|
:rules="[{ required: true, message: 'مبلغ دریافتشده را وارد کنید' }]"
|
||||||
>
|
>
|
||||||
<MoneyInput
|
<MoneyInput addon-after="تومان"
|
||||||
v-model="form.settleAmount"
|
v-model="form.settleAmount"
|
||||||
:decimals="0"
|
:decimals="0"
|
||||||
:disabled="form.partyKind === 'other'"
|
:disabled="form.partyKind === 'other'"
|
||||||
@@ -604,12 +604,12 @@ onMounted(() => {
|
|||||||
name="amount"
|
name="amount"
|
||||||
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
||||||
>
|
>
|
||||||
<MoneyInput v-model="form.amount" :decimals="0" />
|
<MoneyInput addon-after="تومان" v-model="form.amount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item :label="settleLabel" name="settleAmount">
|
<a-form-item :label="settleLabel" name="settleAmount">
|
||||||
<MoneyInput v-model="form.settleAmount" :decimals="0" :disabled="form.partyKind === 'other'" />
|
<MoneyInput addon-after="تومان" v-model="form.settleAmount" :decimals="0" :disabled="form.partyKind === 'other'" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
|
|||||||
@@ -13,24 +13,42 @@ import {
|
|||||||
Input as AInput,
|
Input as AInput,
|
||||||
Row as ARow,
|
Row as ARow,
|
||||||
Select as ASelect,
|
Select as ASelect,
|
||||||
|
Space as ASpace,
|
||||||
Table as ATable,
|
Table as ATable,
|
||||||
|
Tooltip as ATooltip,
|
||||||
Typography,
|
Typography,
|
||||||
message,
|
message,
|
||||||
|
theme,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
import { EditOutlined, WalletOutlined } from '@ant-design/icons-vue';
|
||||||
import type { TableColumnsType } from 'ant-design-vue';
|
import type { TableColumnsType } from 'ant-design-vue';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import type { FinancialAccountDto, OrgUserDto } from '@mento/shared';
|
import type { FinancialAccountDto, OrgUserDto } from '@mento/shared';
|
||||||
import MoneyInput from '@/components/MoneyInput.vue';
|
import MoneyInput from '@/components/MoneyInput.vue';
|
||||||
import { formatJalaliNumeric } from '@/lib/jalali';
|
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 { toman } from './treasury-format';
|
||||||
import { parseMoneyNumber } from '@/lib/money';
|
import { parseMoneyNumber } from '@/lib/money';
|
||||||
|
|
||||||
const { Text: ATypographyText } = Typography;
|
const { Text: ATypographyText } = Typography;
|
||||||
|
const { useToken } = theme;
|
||||||
|
const { token } = useToken();
|
||||||
|
|
||||||
type PayrollPayment = {
|
type PayrollPayment = {
|
||||||
id: string;
|
id: string;
|
||||||
amount: string;
|
amount: string;
|
||||||
occurredAt: string;
|
occurredAt: string;
|
||||||
|
accountId?: string;
|
||||||
accountName: string;
|
accountName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,8 +59,11 @@ type PayrollRow = {
|
|||||||
period: string;
|
period: string;
|
||||||
baseAmount: string;
|
baseAmount: string;
|
||||||
benefits: string;
|
benefits: string;
|
||||||
|
benefitsReason?: string | null;
|
||||||
bonus: string;
|
bonus: string;
|
||||||
|
bonusReason?: string | null;
|
||||||
deductions: string;
|
deductions: string;
|
||||||
|
deductionsReason?: string | null;
|
||||||
payable: string;
|
payable: string;
|
||||||
paidAmount: string;
|
paidAmount: string;
|
||||||
remaining: string;
|
remaining: string;
|
||||||
@@ -63,17 +84,24 @@ const accounts = ref<FinancialAccountDto[]>([]);
|
|||||||
const error = ref('');
|
const error = ref('');
|
||||||
const ok = ref('');
|
const ok = ref('');
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
|
const todayJalali = dateToJalali(new Date());
|
||||||
|
const periodDate = ref(formatLocalDateInput(jalaliToDate(todayJalali.jy, todayJalali.jm, 1)));
|
||||||
const form = ref({
|
const form = ref({
|
||||||
userId: undefined as string | undefined,
|
userId: undefined as string | undefined,
|
||||||
period: '',
|
|
||||||
baseAmount: '',
|
baseAmount: '',
|
||||||
benefits: '',
|
benefits: '',
|
||||||
|
benefitsReason: '',
|
||||||
bonus: '',
|
bonus: '',
|
||||||
|
bonusReason: '',
|
||||||
deductions: '',
|
deductions: '',
|
||||||
|
deductionsReason: '',
|
||||||
});
|
});
|
||||||
const pay = ref({ id: '', amount: '', accountId: undefined as string | undefined });
|
const pay = ref({ id: '', amount: '', accountId: undefined as string | undefined });
|
||||||
|
const editingId = ref('');
|
||||||
|
const editingPaymentId = ref('');
|
||||||
const filterUserId = ref<string | undefined>(undefined);
|
const filterUserId = ref<string | undefined>(undefined);
|
||||||
const filterPeriod = ref('');
|
const filterYear = ref<number | undefined>(undefined);
|
||||||
|
const filterMonth = ref<number | undefined>(undefined);
|
||||||
const filterStatus = ref('all');
|
const filterStatus = ref('all');
|
||||||
const filterMin = ref('');
|
const filterMin = ref('');
|
||||||
const filterMax = ref('');
|
const filterMax = ref('');
|
||||||
@@ -90,6 +118,30 @@ const userOptions = computed(() =>
|
|||||||
users.value.map((user) => ({ value: user.id, label: user.fullName })),
|
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 payablePreview = computed(() => {
|
||||||
const base = parseMoneyNumber(String(form.value.baseAmount ?? ''), 0) ?? 0;
|
const base = parseMoneyNumber(String(form.value.baseAmount ?? ''), 0) ?? 0;
|
||||||
const benefits = parseMoneyNumber(String(form.value.benefits ?? ''), 0) ?? 0;
|
const benefits = parseMoneyNumber(String(form.value.benefits ?? ''), 0) ?? 0;
|
||||||
@@ -103,21 +155,23 @@ const openRows = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const payOptions = computed(() =>
|
const payOptions = computed(() =>
|
||||||
openRows.value.map((row) => ({
|
rows.value
|
||||||
value: row.id,
|
.filter((row) => row.status === 'open' || row.status === 'partial' || row.id === pay.value.id)
|
||||||
label: `${row.fullName} · ${row.period} · مانده ${toman(row.remaining)}`,
|
.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 selectedPay = computed(() => rows.value.find((row) => row.id === pay.value.id) ?? null);
|
||||||
|
|
||||||
const filtered = computed(() => {
|
const filtered = computed(() => {
|
||||||
const period = filterPeriod.value.trim();
|
|
||||||
const min = parseMoneyNumber(String(filterMin.value ?? ''), 0);
|
const min = parseMoneyNumber(String(filterMin.value ?? ''), 0);
|
||||||
const max = parseMoneyNumber(String(filterMax.value ?? ''), 0);
|
const max = parseMoneyNumber(String(filterMax.value ?? ''), 0);
|
||||||
return rows.value.filter((row) => {
|
return rows.value.filter((row) => {
|
||||||
if (filterUserId.value && row.userId !== filterUserId.value) return false;
|
if (filterUserId.value && row.userId !== filterUserId.value) return false;
|
||||||
if (period && !row.period.includes(period)) return false;
|
if (!periodMatches(row.period, filterYear.value, filterMonth.value)) return false;
|
||||||
if (filterStatus.value !== 'all' && row.status !== filterStatus.value) return false;
|
if (filterStatus.value !== 'all' && row.status !== filterStatus.value) return false;
|
||||||
const payable = Number(row.payable);
|
const payable = Number(row.payable);
|
||||||
if (min != null && filterMin.value !== '' && payable < min) return false;
|
if (min != null && filterMin.value !== '' && payable < min) return false;
|
||||||
@@ -143,15 +197,78 @@ const reportColumns: TableColumnsType<PayrollRow> = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const paymentColumns: TableColumnsType<PayrollPayment> = [
|
const paymentColumns: TableColumnsType<PayrollPayment> = [
|
||||||
{
|
{ title: 'تاریخ', key: 'occurredAt' },
|
||||||
title: 'تاریخ',
|
|
||||||
key: 'occurredAt',
|
|
||||||
customRender: ({ record }) => formatJalaliNumeric(new Date(record.occurredAt)),
|
|
||||||
},
|
|
||||||
{ title: 'حساب', dataIndex: 'accountName', key: 'accountName' },
|
{ title: 'حساب', dataIndex: 'accountName', key: 'accountName' },
|
||||||
{ title: 'مبلغ', key: 'amount', customRender: ({ record }) => toman(record.amount) },
|
{ 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) {
|
function apiMessage(e: unknown) {
|
||||||
if (!axios.isAxiosError(e)) return 'عملیات ناموفق بود.';
|
if (!axios.isAxiosError(e)) return 'عملیات ناموفق بود.';
|
||||||
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
||||||
@@ -173,30 +290,91 @@ async function load() {
|
|||||||
accounts.value = acc.data.filter((row) => row.isActive);
|
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() {
|
async function createRow() {
|
||||||
error.value = '';
|
error.value = '';
|
||||||
ok.value = '';
|
ok.value = '';
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
|
const id = editingId.value;
|
||||||
try {
|
try {
|
||||||
const baseAmount = parseMoneyNumber(String(form.value.baseAmount ?? ''), 0) ?? 0;
|
if (id) await api.patch(`/treasury/payroll/${id}`, payrollPayload());
|
||||||
await api.post('/treasury/payroll', {
|
else await api.post('/treasury/payroll', payrollPayload());
|
||||||
userId: form.value.userId,
|
editingId.value = '';
|
||||||
period: form.value.period,
|
blankForm();
|
||||||
baseAmount,
|
ok.value = id
|
||||||
benefits: parseMoneyNumber(String(form.value.benefits ?? ''), 0) ?? 0,
|
? 'حقوق بهروز شد.'
|
||||||
bonus: parseMoneyNumber(String(form.value.bonus ?? ''), 0) ?? 0,
|
: 'حقوق ثبت شد. تا وقتی پرداخت نکنید موجودی حساب عوض نمیشود.';
|
||||||
deductions: parseMoneyNumber(String(form.value.deductions ?? ''), 0) ?? 0,
|
|
||||||
});
|
|
||||||
form.value = {
|
|
||||||
userId: undefined,
|
|
||||||
period: '',
|
|
||||||
baseAmount: '',
|
|
||||||
benefits: '',
|
|
||||||
bonus: '',
|
|
||||||
deductions: '',
|
|
||||||
};
|
|
||||||
ok.value = 'حقوق ثبت شد. تا وقتی پرداخت نکنید موجودی حساب عوض نمیشود.';
|
|
||||||
await load();
|
await load();
|
||||||
|
if (!id) pay.value = { id: '', amount: '', accountId: undefined };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = apiMessage(e);
|
error.value = apiMessage(e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -204,17 +382,38 @@ async function createRow() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPay(id: string) {
|
function onPayPick(value: unknown) {
|
||||||
const row = rows.value.find((item) => item.id === id);
|
editingPaymentId.value = '';
|
||||||
if (!row) {
|
if (value == null || value === '') {
|
||||||
pay.value = { id: '', amount: '', accountId: pay.value.accountId };
|
pay.value = { id: '', amount: '', accountId: undefined };
|
||||||
return;
|
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 = {
|
pay.value = {
|
||||||
id: row.id,
|
id: pay.value.id,
|
||||||
amount: row.remaining,
|
amount: moneyField(payment.amount),
|
||||||
accountId: pay.value.accountId ?? accounts.value[0]?.id,
|
accountId: payment.accountId,
|
||||||
};
|
};
|
||||||
|
scrollToPayroll('payroll-pay');
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelPaymentEdit() {
|
||||||
|
editingPaymentId.value = '';
|
||||||
|
pay.value = { id: pay.value.id, amount: '', accountId: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function payRow() {
|
async function payRow() {
|
||||||
@@ -229,19 +428,25 @@ async function payRow() {
|
|||||||
error.value = 'مبلغ پرداخت معتبر نیست.';
|
error.value = 'مبلغ پرداخت معتبر نیست.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await api.post(`/treasury/payroll/${keepId}/pay`, {
|
const paymentId = editingPaymentId.value;
|
||||||
amount,
|
if (paymentId) {
|
||||||
accountId,
|
await api.patch(`/treasury/payroll/${keepId}/payments/${paymentId}`, { amount, accountId });
|
||||||
clientKey: crypto.randomUUID(),
|
} else {
|
||||||
});
|
await api.post(`/treasury/payroll/${keepId}/pay`, {
|
||||||
ok.value = 'پرداخت حقوق از حساب ثبت شد. اگر مانده مانده باشد، مرحلهٔ بعد را همینجا بزنید.';
|
amount,
|
||||||
|
accountId,
|
||||||
|
clientKey: crypto.randomUUID(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
editingPaymentId.value = '';
|
||||||
|
ok.value = paymentId
|
||||||
|
? 'پرداخت حقوق بهروز شد.'
|
||||||
|
: 'پرداخت حقوق از حساب ثبت شد. اگر مانده مانده باشد، مرحلهٔ بعد را همینجا بزنید.';
|
||||||
await load();
|
await load();
|
||||||
const still = rows.value.find(
|
const still = rows.value.find((row) => row.id === keepId && row.status !== 'void');
|
||||||
(row) => row.id === keepId && row.status !== 'settled' && row.status !== 'void',
|
|
||||||
);
|
|
||||||
pay.value = still
|
pay.value = still
|
||||||
? { id: still.id, amount: still.remaining, accountId }
|
? { id: still.id, amount: paymentId ? '' : still.status === 'settled' ? '' : still.remaining, accountId: paymentId ? undefined : accountId }
|
||||||
: { id: '', amount: '', accountId };
|
: { id: '', amount: '', accountId: undefined };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = apiMessage(e);
|
error.value = apiMessage(e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -261,13 +466,14 @@ onMounted(() => {
|
|||||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||||
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
||||||
|
|
||||||
<a-card title="تعیین حقوق" :bordered="false">
|
<a-card id="payroll-setup" title="تعیین حقوق" :bordered="false">
|
||||||
<a-form layout="vertical" :model="form" @finish="createRow" @finishFailed="onFinishFailed">
|
<a-form layout="vertical" :model="form" @finish="createRow" @finishFailed="onFinishFailed">
|
||||||
<a-row :gutter="[16, 0]">
|
<a-row :gutter="[8, 0]">
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :sm="12">
|
||||||
<a-form-item
|
<a-form-item
|
||||||
label="پرسنل"
|
label="پرسنل"
|
||||||
name="userId"
|
name="userId"
|
||||||
|
style="margin-bottom: 8px"
|
||||||
:rules="[{ required: true, message: 'پرسنل الزامی است' }]"
|
:rules="[{ required: true, message: 'پرسنل الزامی است' }]"
|
||||||
>
|
>
|
||||||
<a-select
|
<a-select
|
||||||
@@ -281,55 +487,75 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :sm="12">
|
||||||
<a-form-item
|
<a-form-item label="دوره" style="margin-bottom: 8px">
|
||||||
label="دوره"
|
<JalaliDateInput
|
||||||
name="period"
|
v-model="periodDate"
|
||||||
:rules="[{ required: true, message: 'دوره الزامی است' }]"
|
start-view="months"
|
||||||
>
|
:clearable="false"
|
||||||
<a-input v-model:value="form.period" placeholder="۱۴۰۵-۰۷" />
|
:display-text="formatPeriod(builtPeriod.key)"
|
||||||
|
placeholder="انتخاب دوره"
|
||||||
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="12" :md="6">
|
||||||
<a-form-item
|
<a-form-item
|
||||||
label="پایه"
|
label="پایه"
|
||||||
name="baseAmount"
|
name="baseAmount"
|
||||||
|
style="margin-bottom: 8px"
|
||||||
:rules="[{ required: true, message: 'مبلغ پایه الزامی است' }]"
|
:rules="[{ required: true, message: 'مبلغ پایه الزامی است' }]"
|
||||||
>
|
>
|
||||||
<MoneyInput v-model="form.baseAmount" :decimals="0" />
|
<MoneyInput v-model="form.baseAmount" addon-after="تومان" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="12" :md="6">
|
||||||
<a-form-item label="مزایا" name="benefits">
|
<a-form-item label="مزایا" name="benefits" style="margin-bottom: 8px">
|
||||||
<MoneyInput v-model="form.benefits" :decimals="0" />
|
<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-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="12" :md="6">
|
||||||
<a-form-item label="پاداش" name="bonus">
|
<a-form-item label="پاداش" name="bonus" style="margin-bottom: 8px">
|
||||||
<MoneyInput v-model="form.bonus" :decimals="0" />
|
<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-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="12" :md="6">
|
||||||
<a-form-item label="کسورات" name="deductions">
|
<a-form-item label="کسورات" name="deductions" style="margin-bottom: 8px">
|
||||||
<MoneyInput v-model="form.deductions" :decimals="0" />
|
<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-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="16">
|
<a-col :span="24">
|
||||||
<a-form-item label="قابل پرداخت">
|
<a-flex align="center" justify="space-between" wrap="wrap" :gap="8">
|
||||||
<a-typography-text strong>{{ toman(payablePreview) }}</a-typography-text>
|
<a-typography-text>
|
||||||
</a-form-item>
|
قابل پرداخت
|
||||||
|
<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-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-button type="primary" html-type="submit" :loading="saving">ثبت حقوق</a-button>
|
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<a-card title="پرداخت" :bordered="false">
|
<a-card id="payroll-pay" title="پرداخت" :bordered="false">
|
||||||
<a-flex vertical :gap="16">
|
<a-flex vertical :gap="16">
|
||||||
<a-typography-text type="secondary">
|
<a-typography-text type="secondary">
|
||||||
میتوانید کل مانده را یکجا بدهید، یا هر بار بخشی را تا تمام شدن مانده ثبت کنید.
|
میتوانید کل مانده را یکجا بدهید، یا هر بار بخشی را تا تمام شدن مانده ثبت کنید.
|
||||||
</a-typography-text>
|
</a-typography-text>
|
||||||
<a-empty v-if="!openRows.length" description="حقوق باز برای پرداخت نیست." />
|
<a-empty v-if="!openRows.length && !pay.id" description="حقوق باز برای پرداخت نیست." />
|
||||||
<a-form
|
<a-form
|
||||||
v-else
|
v-else
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
@@ -346,12 +572,13 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<a-select
|
<a-select
|
||||||
:value="pay.id || undefined"
|
:value="pay.id || undefined"
|
||||||
|
allow-clear
|
||||||
show-search
|
show-search
|
||||||
option-filter-prop="label"
|
option-filter-prop="label"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
placeholder="انتخاب پرسنل و دوره"
|
placeholder="انتخاب دوره"
|
||||||
:options="payOptions"
|
:options="payOptions"
|
||||||
@update:value="selectPay(String($event ?? ''))"
|
@change="onPayPick"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -361,7 +588,7 @@ onMounted(() => {
|
|||||||
name="amount"
|
name="amount"
|
||||||
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
||||||
>
|
>
|
||||||
<MoneyInput v-model="pay.amount" :decimals="0" />
|
<MoneyInput v-model="pay.amount" addon-after="تومان" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="6">
|
<a-col :xs="24" :md="6">
|
||||||
@@ -387,9 +614,12 @@ onMounted(() => {
|
|||||||
<a-typography-text>پرداختشده {{ toman(selectedPay.paidAmount) }}</a-typography-text>
|
<a-typography-text>پرداختشده {{ toman(selectedPay.paidAmount) }}</a-typography-text>
|
||||||
<a-typography-text strong>مانده {{ toman(selectedPay.remaining) }}</a-typography-text>
|
<a-typography-text strong>مانده {{ toman(selectedPay.remaining) }}</a-typography-text>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
<a-button type="primary" html-type="submit" :loading="saving" :disabled="!pay.id">
|
<a-space>
|
||||||
ثبت پرداخت
|
<a-button v-if="editingPaymentId" :disabled="saving" @click="cancelPaymentEdit">انصراف</a-button>
|
||||||
</a-button>
|
<a-button type="primary" html-type="submit" :loading="saving" :disabled="!pay.id">
|
||||||
|
{{ editingPaymentId ? 'ذخیره پرداخت' : 'ثبت پرداخت' }}
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<a-table
|
<a-table
|
||||||
@@ -398,7 +628,31 @@ onMounted(() => {
|
|||||||
:data-source="selectedPay.payments"
|
:data-source="selectedPay.payments"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
row-key="id"
|
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-flex>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
@@ -420,7 +674,26 @@ onMounted(() => {
|
|||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="دوره">
|
<a-form-item label="دوره">
|
||||||
<a-input v-model:value="filterPeriod" placeholder="مثلاً ۱۴۰۵-۰۷" allow-clear />
|
<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-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
@@ -430,12 +703,12 @@ onMounted(() => {
|
|||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="حداقل قابل پرداخت">
|
<a-form-item label="حداقل قابل پرداخت">
|
||||||
<MoneyInput v-model="filterMin" :decimals="0" />
|
<MoneyInput v-model="filterMin" addon-after="تومان" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="حداکثر قابل پرداخت">
|
<a-form-item label="حداکثر قابل پرداخت">
|
||||||
<MoneyInput v-model="filterMax" :decimals="0" />
|
<MoneyInput v-model="filterMax" addon-after="تومان" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
@@ -451,14 +724,43 @@ onMounted(() => {
|
|||||||
<template v-if="column.key === 'status'">
|
<template v-if="column.key === 'status'">
|
||||||
{{ statusLabel[record.status] ?? record.status }}
|
{{ statusLabel[record.status] ?? record.status }}
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="column.key === 'period'">
|
||||||
|
{{ formatPeriod(record.period) }}
|
||||||
|
</template>
|
||||||
<template v-else-if="column.key === 'action'">
|
<template v-else-if="column.key === 'action'">
|
||||||
<a-button
|
<a-space :size="4">
|
||||||
v-if="record.status === 'open' || record.status === 'partial'"
|
<a-tooltip v-if="record.status !== 'void'" title="ویرایش">
|
||||||
type="link"
|
<a-button
|
||||||
@click="selectPay(record.id)"
|
type="text"
|
||||||
>
|
aria-label="ویرایش"
|
||||||
پرداخت
|
:style="{ color: token.colorInfo }"
|
||||||
</a-button>
|
@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>
|
||||||
<template v-else-if="moneyKeys.includes(column.key as (typeof moneyKeys)[number])">
|
<template v-else-if="moneyKeys.includes(column.key as (typeof moneyKeys)[number])">
|
||||||
{{ toman(record[column.key as (typeof moneyKeys)[number]]) }}
|
{{ toman(record[column.key as (typeof moneyKeys)[number]]) }}
|
||||||
|
|||||||
@@ -234,12 +234,12 @@ onMounted(() => {
|
|||||||
name="totalAmount"
|
name="totalAmount"
|
||||||
:rules="[{ required: true, message: 'مبلغ کل الزامی است' }]"
|
:rules="[{ required: true, message: 'مبلغ کل الزامی است' }]"
|
||||||
>
|
>
|
||||||
<MoneyInput v-model="form.totalAmount" :decimals="0" />
|
<MoneyInput addon-after="تومان" v-model="form.totalAmount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="پیشپرداخت" name="prepaidAmount">
|
<a-form-item label="پیشپرداخت" name="prepaidAmount">
|
||||||
<MoneyInput v-model="form.prepaidAmount" :decimals="0" />
|
<MoneyInput addon-after="تومان" v-model="form.prepaidAmount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
@@ -285,7 +285,7 @@ onMounted(() => {
|
|||||||
<a-row v-for="(line, index) in customLines" :key="index" :gutter="[16, 0]">
|
<a-row v-for="(line, index) in customLines" :key="index" :gutter="[16, 0]">
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
<a-form-item :label="`مبلغ قسط ${index + 1}`">
|
<a-form-item :label="`مبلغ قسط ${index + 1}`">
|
||||||
<MoneyInput v-model="line.amount" :decimals="0" />
|
<MoneyInput addon-after="تومان" v-model="line.amount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
@@ -320,7 +320,7 @@ onMounted(() => {
|
|||||||
name="amount"
|
name="amount"
|
||||||
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
||||||
>
|
>
|
||||||
<MoneyInput v-model="collect.amount" :decimals="0" />
|
<MoneyInput addon-after="تومان" v-model="collect.amount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ onMounted(() => {
|
|||||||
name="amount"
|
name="amount"
|
||||||
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
||||||
>
|
>
|
||||||
<MoneyInput v-model="form.amount" :decimals="0" />
|
<MoneyInput addon-after="تومان" v-model="form.amount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
|
|||||||
Reference in New Issue
Block a user