2026-10-18 18:49:49 +03:30
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { onMounted, ref } from 'vue';
|
|
|
|
|
|
import axios from 'axios';
|
2026-10-18 21:06:18 +03:30
|
|
|
|
import {
|
|
|
|
|
|
Alert as AAlert,
|
|
|
|
|
|
Button as AButton,
|
|
|
|
|
|
Card as ACard,
|
|
|
|
|
|
Col as ACol,
|
|
|
|
|
|
Divider as ADivider,
|
|
|
|
|
|
Empty as AEmpty,
|
|
|
|
|
|
Flex as AFlex,
|
|
|
|
|
|
Form as AForm,
|
|
|
|
|
|
FormItem as AFormItem,
|
|
|
|
|
|
Input as AInput,
|
|
|
|
|
|
List as AList,
|
|
|
|
|
|
Row as ARow,
|
|
|
|
|
|
Select as ASelect,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
} from 'ant-design-vue';
|
2026-10-18 18:49:49 +03:30
|
|
|
|
import { api } from '@/lib/api';
|
|
|
|
|
|
import type { FinancialAccountDto, OrgUserDto } from '@mento/shared';
|
|
|
|
|
|
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
|
|
|
|
|
import { toman } from './treasury-format';
|
|
|
|
|
|
|
2026-10-18 21:06:18 +03:30
|
|
|
|
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
|
|
|
|
|
const AListItem = AList.Item;
|
|
|
|
|
|
const AListItemMeta = AList.Item.Meta;
|
|
|
|
|
|
const ASelectOption = ASelect.Option;
|
|
|
|
|
|
|
2026-10-18 18:49:49 +03:30
|
|
|
|
type PayrollRow = {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
fullName: string;
|
|
|
|
|
|
period: string;
|
|
|
|
|
|
payable: string;
|
|
|
|
|
|
remaining: string;
|
|
|
|
|
|
status: string;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const rows = ref<PayrollRow[]>([]);
|
|
|
|
|
|
const users = ref<OrgUserDto[]>([]);
|
|
|
|
|
|
const accounts = ref<FinancialAccountDto[]>([]);
|
|
|
|
|
|
const error = ref('');
|
|
|
|
|
|
const ok = ref('');
|
|
|
|
|
|
const form = ref({ userId: '', period: '', baseAmount: '', benefits: '', bonus: '', deductions: '' });
|
|
|
|
|
|
const pay = ref({ id: '', amount: '', accountId: '' });
|
|
|
|
|
|
|
|
|
|
|
|
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 || 'عملیات ناموفق بود.';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
users.value = people.data.filter((row) => row.isActive !== false);
|
|
|
|
|
|
accounts.value = acc.data.filter((row) => row.isActive);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function createRow() {
|
|
|
|
|
|
error.value = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.post('/treasury/payroll', form.value);
|
|
|
|
|
|
ok.value = 'حقوق ثبت شد و تا پرداخت، موجودی حساب تغییر نمیکند.';
|
|
|
|
|
|
await load();
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
error.value = apiMessage(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function payRow() {
|
|
|
|
|
|
error.value = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.post(`/treasury/payroll/${pay.value.id}/pay`, {
|
|
|
|
|
|
amount: pay.value.amount,
|
|
|
|
|
|
accountId: pay.value.accountId,
|
|
|
|
|
|
clientKey: crypto.randomUUID(),
|
|
|
|
|
|
});
|
|
|
|
|
|
ok.value = 'پرداخت حقوق از حساب ثبت شد.';
|
|
|
|
|
|
pay.value = { id: '', amount: '', accountId: '' };
|
|
|
|
|
|
await load();
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
error.value = apiMessage(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 21:06:18 +03:30
|
|
|
|
function startPay(row: PayrollRow) {
|
|
|
|
|
|
pay.value = { id: row.id, amount: row.remaining, accountId: accounts.value[0]?.id || '' };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 18:49:49 +03:30
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
void load().catch(() => {
|
|
|
|
|
|
error.value = 'بارگذاری حقوق ناموفق بود.';
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-10-18 21:06:18 +03:30
|
|
|
|
<div>
|
|
|
|
|
|
<a-flex vertical :gap="16">
|
2026-10-18 18:49:49 +03:30
|
|
|
|
<div>
|
2026-10-18 21:06:18 +03:30
|
|
|
|
<a-typography-title :level="3" :style="{ margin: 0 }">حقوق و دستمزد</a-typography-title>
|
|
|
|
|
|
<a-typography-text type="secondary">فقط مالک و مدیر این صفحه را میبینند.</a-typography-text>
|
2026-10-18 18:49:49 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-18 21:06:18 +03:30
|
|
|
|
<TreasuryNav />
|
|
|
|
|
|
|
|
|
|
|
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
|
|
|
|
|
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
|
|
|
|
|
|
|
|
|
|
|
<a-card :bordered="false">
|
|
|
|
|
|
<a-divider orientation="right">ثبت دوره</a-divider>
|
|
|
|
|
|
<a-form layout="vertical" @finish="createRow">
|
|
|
|
|
|
<a-row :gutter="[16, 0]">
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="پرسنل" name="userId">
|
|
|
|
|
|
<a-select v-model:value="form.userId" style="width: 100%" placeholder="انتخاب">
|
|
|
|
|
|
<a-select-option value="">انتخاب</a-select-option>
|
|
|
|
|
|
<a-select-option v-for="user in users" :key="user.id" :value="user.id">
|
|
|
|
|
|
{{ user.fullName }}
|
|
|
|
|
|
</a-select-option>
|
|
|
|
|
|
</a-select>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="دوره" name="period">
|
|
|
|
|
|
<a-input v-model:value="form.period" placeholder="۱۴۰۵-۰۷" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="پایه" name="baseAmount">
|
|
|
|
|
|
<a-input v-model:value="form.baseAmount" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="مزایا" name="benefits">
|
|
|
|
|
|
<a-input v-model:value="form.benefits" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="پاداش" name="bonus">
|
|
|
|
|
|
<a-input v-model:value="form.bonus" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="کسورات" name="deductions">
|
|
|
|
|
|
<a-input v-model:value="form.deductions" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
<a-button type="primary" html-type="submit">ثبت حقوق</a-button>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-if="pay.id">
|
|
|
|
|
|
<a-divider orientation="right">پرداخت حقوق</a-divider>
|
|
|
|
|
|
<a-form layout="vertical" @finish="payRow">
|
|
|
|
|
|
<a-row :gutter="[16, 0]">
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="مبلغ" name="amount">
|
|
|
|
|
|
<a-input v-model:value="pay.amount" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="حساب" name="accountId">
|
|
|
|
|
|
<a-select v-model:value="pay.accountId" style="width: 100%">
|
|
|
|
|
|
<a-select-option v-for="account in accounts" :key="account.id" :value="account.id">
|
|
|
|
|
|
{{ account.name }}
|
|
|
|
|
|
</a-select-option>
|
|
|
|
|
|
</a-select>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
<a-button type="primary" html-type="submit">ثبت پرداخت</a-button>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<a-divider orientation="right">فهرست</a-divider>
|
|
|
|
|
|
<a-empty v-if="!rows.length" description="رکورد حقوقی ثبت نشده است." />
|
|
|
|
|
|
<a-list v-else item-layout="horizontal" :data-source="rows">
|
|
|
|
|
|
<template #renderItem="{ item }">
|
|
|
|
|
|
<a-list-item>
|
|
|
|
|
|
<template #actions>
|
|
|
|
|
|
<a-typography-text strong>{{ toman(item.payable) }}</a-typography-text>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
v-if="item.status !== 'settled' && item.status !== 'void'"
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
@click="startPay(item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
پرداخت
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<a-list-item-meta>
|
|
|
|
|
|
<template #title>{{ item.fullName }}</template>
|
|
|
|
|
|
<template #description>
|
|
|
|
|
|
{{ item.period }} · مانده {{ toman(item.remaining) }} · {{ item.status }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list-item-meta>
|
|
|
|
|
|
</a-list-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|