Files
CRM_Montzar/apps/web/src/views/treasury/TreasuryAccountView.vue
T
MahdiandCursor 8981a552fc feat: gate treasury as an org module and group money inputs
Link optional invoice and cheque settlements to cash accounts, and let superadmin enable the finance module per organization.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 19:33:09 +03:30

122 lines
4.8 KiB
Vue

<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { api } from '@/lib/api';
import type { FinancialAccountDto, FinancialMovementDto } from '@mento/shared';
import { UiButton, UiField, UiInput, UiPageHeader } from '@/components/ui';
import MoneyInput from '@/components/MoneyInput.vue';
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
import { toman } from './treasury-format';
import { formatJalaliNumeric } from '@/lib/jalali';
const route = useRoute();
const account = ref<FinancialAccountDto | null>(null);
const items = ref<FinancialMovementDto[]>([]);
const error = ref('');
const stated = ref('');
const reason = ref('');
const note = ref('');
async function reconcile() {
error.value = '';
note.value = '';
try {
const { data } = await api.post<{ difference: string; systemBalance: string }>('/treasury/reconciliations', {
accountId: route.params.id,
statedBalance: stated.value,
});
note.value = `اختلاف ${toman(data.difference)} نسبت به موجودی سیستم ${toman(data.systemBalance)}. موجودی خودکار عوض نشد.`;
} catch {
error.value = 'ثبت مغایرت ناموفق بود.';
}
}
async function adjust(direction: 'in' | 'out') {
error.value = '';
try {
await api.post('/treasury/adjustments', {
accountId: route.params.id,
amount: stated.value,
direction,
reason: reason.value,
});
window.location.reload();
} catch {
error.value = 'اصلاح موجودی ناموفق بود.';
}
}
onMounted(async () => {
try {
const { data } = await api.get<{ account: FinancialAccountDto; items: FinancialMovementDto[] }>(
`/treasury/accounts/${route.params.id}/ledger`,
);
account.value = data.account;
items.value = data.items;
} catch {
error.value = 'گردش حساب بارگذاری نشد.';
}
});
</script>
<template>
<section class="page">
<UiPageHeader :title="account?.name || 'حساب'" description="گردش قطعی این حساب" />
<TreasuryNav />
<p v-if="error" class="err">{{ error }}</p>
<div v-if="account" class="stats">
<article><span>موجودی فعلی</span><strong>{{ toman(account.balance) }}</strong></article>
<article><span>مانده افتتاحیه</span><strong>{{ toman(account.openingBalance) }}</strong></article>
<article><span>ورودی</span><strong>{{ toman(account.periodIn) }}</strong></article>
<article><span>خروجی</span><strong>{{ toman(account.periodOut) }}</strong></article>
</div>
<div v-if="account" class="reconcile">
<UiField label="موجودی واقعی"><MoneyInput v-model="stated" :decimals="0" /></UiField>
<UiField label="علت اصلاح"><UiInput v-model="reason" /></UiField>
<UiButton variant="secondary" @click="reconcile">ثبت مغایرت</UiButton>
<UiButton variant="secondary" @click="adjust('in')">اصلاح ورود</UiButton>
<UiButton variant="secondary" @click="adjust('out')">اصلاح خروج</UiButton>
<p v-if="note" class="note">{{ note }}</p>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>تاریخ</th>
<th>شرح</th>
<th>نوع</th>
<th>ورودی</th>
<th>خروجی</th>
<th>مانده</th>
</tr>
</thead>
<tbody>
<tr v-for="row in items" :key="row.id">
<td>{{ formatJalaliNumeric(new Date(row.occurredAt)) }}</td>
<td>{{ row.memo || '—' }}</td>
<td>{{ row.kindLabel }}</td>
<td>{{ row.direction === 'in' ? toman(row.amount) : '—' }}</td>
<td>{{ row.direction === 'out' ? toman(row.amount) : '—' }}</td>
<td>{{ row.balanceAfter ? toman(row.balanceAfter) : '—' }}</td>
</tr>
<tr v-if="!items.length"><td colspan="6">گردشی ثبت نشده است.</td></tr>
</tbody>
</table>
</div>
</section>
</template>
<style scoped>
.page { display: grid; gap: 1rem; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 0.7rem; }
.stats article { background: #fff; border: 1px solid #e7eef2; border-radius: 14px; padding: 0.8rem; }
.stats span { color: #64748b; display: block; }
.table-wrap { overflow: auto; background: #fff; border: 1px solid #e7eef2; border-radius: 16px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 0.7rem; text-align: right; border-bottom: 1px solid #f1f5f9; white-space: nowrap; }
.reconcile { display: grid; grid-template-columns: 1fr 1fr auto auto auto; gap: 0.6rem; align-items: end; }
.note { grid-column: 1 / -1; color: #0f766e; font-weight: 700; }
.err { color: #b91c1c; font-weight: 700; }
@media (max-width: 800px) { .reconcile { grid-template-columns: 1fr; } }
</style>