feat: add treasury audit, attachment download, and installment SMS

Let cheque clearance pick a cash account and export income, expense, and installment lists to Excel.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-18 19:59:24 +03:30
co-authored by Cursor
parent 8981a552fc
commit 96befd3867
11 changed files with 285 additions and 21 deletions
@@ -0,0 +1,57 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { api } from '@/lib/api';
import { UiPageHeader } from '@/components/ui';
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
import { formatJalaliNumeric } from '@/lib/jalali';
type Row = {
id: string;
action: string;
entityType: string;
createdAt: string;
before: unknown;
after: unknown;
};
const rows = ref<Row[]>([]);
const error = ref('');
const actionLabel: Record<string, string> = {
settle: 'تسویه سند',
cash_post: 'نشستن وجه در حساب',
};
onMounted(async () => {
try {
const { data } = await api.get<Row[]>('/treasury/audit');
rows.value = data;
} catch {
error.value = 'سوابق بارگذاری نشد.';
}
});
</script>
<template>
<section class="page">
<UiPageHeader title="سوابق تغییرات مالی" description="تسویه سند و نشستن وجه در صندوق، با مقدار ثبت‌شده." />
<TreasuryNav />
<p v-if="error" class="err">{{ error }}</p>
<p v-if="!error && !rows.length" class="muted">سابقه‌ای نیست.</p>
<article v-for="row in rows" :key="row.id">
<header>
<strong>{{ actionLabel[row.action] ?? row.action }}</strong>
<time>{{ formatJalaliNumeric(new Date(row.createdAt)) }}</time>
</header>
<pre>{{ JSON.stringify(row.after ?? row.before, null, 2) }}</pre>
</article>
</section>
</template>
<style scoped>
.page { display: grid; gap: 0.8rem; }
article { background: #fff; border: 1px solid #e7eef2; border-radius: 14px; padding: 0.8rem 1rem; }
header { display: flex; justify-content: space-between; gap: 0.75rem; }
time, .muted { color: #64748b; }
pre { margin: 0.45rem 0 0; white-space: pre-wrap; font: inherit; color: #334155; }
.err { color: #b91c1c; font-weight: 700; }
</style>
@@ -91,6 +91,29 @@ async function createDoc() {
}
}
async function downloadExcel() {
const { data } = await api.get<Blob>('/treasury/documents/export', {
params: { kind: props.kind },
responseType: 'blob',
});
const url = URL.createObjectURL(data);
const link = document.createElement('a');
link.href = url;
link.download = props.kind === 'income' ? 'incomes.xlsx' : 'expenses.xlsx';
link.click();
URL.revokeObjectURL(url);
}
async function downloadAttachment(id: string, fileName: string) {
const { data } = await api.get<Blob>(`/treasury/attachments/${id}`, { responseType: 'blob' });
const url = URL.createObjectURL(data);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.click();
URL.revokeObjectURL(url);
}
async function upload(id: string, event: Event) {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
@@ -99,6 +122,7 @@ async function upload(id: string, event: Event) {
try {
await api.post(`/treasury/documents/${id}/attachments`, body);
ok.value = 'پیوست ذخیره شد.';
await load();
} catch (e) {
error.value = apiMessage(e);
}
@@ -161,6 +185,7 @@ onMounted(() => {
<UiField label="پیگیری"><UiInput v-model="form.reference" /></UiField>
</div>
<UiButton :loading="saving" @click="createDoc">ثبت</UiButton>
<UiButton variant="secondary" @click="downloadExcel">خروجی اکسل</UiButton>
</UiCard>
<UiField label="جستجو"><UiInput v-model="q" @change="load" /></UiField>
<div class="list">
@@ -176,6 +201,15 @@ onMounted(() => {
<div class="side">
<b>{{ toman(row.amount) }}</b>
<button v-if="row.status !== 'void'" type="button" @click="voidDoc(row.id)">ابطال</button>
<button
v-for="file in row.attachments ?? []"
:key="file.id"
type="button"
class="file"
@click="downloadAttachment(file.id, file.fileName)"
>
{{ file.fileName }}
</button>
<label v-if="row.status !== 'void'" class="file">
پیوست
<input type="file" @change="upload(row.id, $event)" />
@@ -51,6 +51,16 @@ async function load() {
accounts.value = acc.data.filter((row) => row.isActive);
}
async function downloadExcel() {
const { data } = await api.get<Blob>('/treasury/receivables/export', { responseType: 'blob' });
const url = URL.createObjectURL(data);
const link = document.createElement('a');
link.href = url;
link.download = 'installments.xlsx';
link.click();
URL.revokeObjectURL(url);
}
async function createContract() {
saving.value = true;
error.value = '';
@@ -148,6 +158,7 @@ onMounted(() => {
<UiButton variant="secondary" @click="customLines.push({ amount: '', dueDate: '' })">قسط دیگر</UiButton>
</div>
<UiButton :loading="saving" @click="createContract">ساخت اقساط</UiButton>
<UiButton variant="secondary" @click="downloadExcel">خروجی اکسل</UiButton>
</UiCard>
<article v-for="row in rows" :key="row.id" class="contract">