feat: add internal finance ledger for accounts, claims, and payroll
Keep booked income and expense separate from cash balances, with installments, recurring items, and reports. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { api } from '@/lib/api';
|
||||
import type { FinancialAccountDto, OrgUserDto } from '@mento/shared';
|
||||
import { UiButton, UiCard, UiField, UiInput, UiPageHeader } from '@/components/ui';
|
||||
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
||||
import { toman } from './treasury-format';
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void load().catch(() => {
|
||||
error.value = 'بارگذاری حقوق ناموفق بود.';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<UiPageHeader title="حقوق و دستمزد" description="فقط مالک و مدیر این صفحه را میبینند." />
|
||||
<TreasuryNav />
|
||||
<p v-if="error" class="err">{{ error }}</p>
|
||||
<p v-if="ok" class="ok">{{ ok }}</p>
|
||||
<UiCard title="ثبت دوره">
|
||||
<div class="grid">
|
||||
<UiField label="پرسنل">
|
||||
<select v-model="form.userId" class="select">
|
||||
<option value="">انتخاب</option>
|
||||
<option v-for="user in users" :key="user.id" :value="user.id">{{ user.fullName }}</option>
|
||||
</select>
|
||||
</UiField>
|
||||
<UiField label="دوره"><UiInput v-model="form.period" placeholder="۱۴۰۵-۰۷" /></UiField>
|
||||
<UiField label="پایه"><UiInput v-model="form.baseAmount" /></UiField>
|
||||
<UiField label="مزایا"><UiInput v-model="form.benefits" /></UiField>
|
||||
<UiField label="پاداش"><UiInput v-model="form.bonus" /></UiField>
|
||||
<UiField label="کسورات"><UiInput v-model="form.deductions" /></UiField>
|
||||
</div>
|
||||
<UiButton @click="createRow">ثبت حقوق</UiButton>
|
||||
</UiCard>
|
||||
<article v-for="row in rows" :key="row.id" class="row">
|
||||
<div>
|
||||
<strong>{{ row.fullName }}</strong>
|
||||
<small>{{ row.period }} · مانده {{ toman(row.remaining) }} · {{ row.status }}</small>
|
||||
</div>
|
||||
<div class="side">
|
||||
<b>{{ toman(row.payable) }}</b>
|
||||
<button v-if="row.status !== 'settled' && row.status !== 'void'" type="button" @click="pay = { id: row.id, amount: row.remaining, accountId: accounts[0]?.id || '' }">پرداخت</button>
|
||||
</div>
|
||||
</article>
|
||||
<UiCard v-if="pay.id" title="پرداخت حقوق">
|
||||
<div class="grid">
|
||||
<UiField label="مبلغ"><UiInput v-model="pay.amount" /></UiField>
|
||||
<UiField label="حساب">
|
||||
<select v-model="pay.accountId" class="select">
|
||||
<option v-for="account in accounts" :key="account.id" :value="account.id">{{ account.name }}</option>
|
||||
</select>
|
||||
</UiField>
|
||||
</div>
|
||||
<UiButton @click="payRow">ثبت پرداخت</UiButton>
|
||||
</UiCard>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page { display: grid; gap: 1rem; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.7rem; margin-bottom: 0.8rem; }
|
||||
.select { width: 100%; min-height: var(--control-h); border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; }
|
||||
.row { display: flex; justify-content: space-between; gap: 0.75rem; background: #fff; border: 1px solid #e7eef2; border-radius: 14px; padding: 0.8rem 1rem; }
|
||||
.row small { display: block; color: #64748b; }
|
||||
.side { display: grid; justify-items: end; }
|
||||
.side button { border: 0; background: transparent; color: #0f766e; font: inherit; font-weight: 700; cursor: pointer; }
|
||||
.err { color: #b91c1c; font-weight: 700; }
|
||||
.ok { color: #0f766e; font-weight: 700; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user