feat(treasury): number documents and manage categories on their tab

Save a system tracking number on each income and expense, reuse the full contact dialogs for a new party, and create, rename, or delete categories only from the categories tab.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-04 13:11:09 +03:30
co-authored by Cursor
parent d71c2dbef4
commit 08a278e1c8
10 changed files with 361 additions and 256 deletions
@@ -22,7 +22,7 @@ const links = computed(() => {
{ value: '/finance/recurring', label: 'تکرارشونده', section: 'treasury_recurring' },
{ value: '/finance/reports', label: 'گزارش', section: 'treasury_reports' },
{ value: '/finance/audit', label: 'سوابق', section: 'treasury_audit' },
{ value: '/finance/categories', label: 'دسته‌بندی‌ها', section: 'treasury_categories' },
{ value: '/finance/categories', label: 'دسته‌بندی', section: 'treasury_categories' },
];
return all.filter((link) => treasurySectionAllowed(role, permissions, link.section));
});
@@ -4,16 +4,15 @@ import axios from 'axios';
import {
Button as AButton,
Flex as AFlex,
Form as AForm,
FormItem as AFormItem,
Input as AInput,
Modal as AModal,
Select as ASelect,
Typography,
message,
} from 'ant-design-vue';
import type { FinancialPartyKind, TreasuryPartySearchDto } from '@mento/shared';
import type { CompanyDto, FinancialPartyKind, PersonDto, TreasuryPartySearchDto } from '@mento/shared';
import { UiModal } from '@/components/ui';
import { api } from '@/lib/api';
import CompanyFormView from '@/views/CompanyFormView.vue';
import PersonFormView from '@/views/PersonFormView.vue';
const VNodes = defineComponent({
props: { vnodes: { type: Object, required: true } },
@@ -36,12 +35,9 @@ const companies = ref<Choice[]>([]);
const users = ref<Choice[]>([]);
const labels = ref<Record<string, string>>({});
const loading = ref(false);
const selectOpen = ref(false);
const personOpen = ref(false);
const companyOpen = ref(false);
const personSaving = ref(false);
const companySaving = ref(false);
const personForm = ref({ firstName: '', lastName: '' });
const companyName = ref('');
const selectValue = computed(() => {
if (kind.value === 'other') return 'other';
@@ -112,57 +108,37 @@ function onChange(value: unknown) {
partyId.value = id;
}
function savePerson() {
const firstName = personForm.value.firstName.trim();
const lastName = personForm.value.lastName.trim();
if (`${firstName} ${lastName}`.trim().length < 2) {
message.warning('نام شخص را وارد کنید.');
return Promise.reject(new Error('invalid'));
}
personSaving.value = true;
return api
.post<{ id: string; name: string }>('/treasury/parties/person', { firstName, lastName })
.then(({ data }) => {
people.value = [{ id: data.id, name: data.name }, ...people.value.filter((row) => row.id !== data.id)];
labels.value[`person:${data.id}`] = data.name;
kind.value = 'person';
partyId.value = data.id;
personForm.value = { firstName: '', lastName: '' };
message.success('شخص در مشتریان ثبت شد.');
})
.catch((e: unknown) => {
if (axios.isAxiosError(e)) message.error(apiMessage(e));
return Promise.reject(e);
})
.finally(() => {
personSaving.value = false;
});
function onDropdown(open: boolean) {
selectOpen.value = open;
}
function saveCompany() {
const name = companyName.value.trim();
if (name.length < 2) {
message.warning('نام شرکت را وارد کنید.');
return Promise.reject(new Error('invalid'));
}
companySaving.value = true;
return api
.post<{ id: string; name: string }>('/treasury/parties/company', { name })
.then(({ data }) => {
companies.value = [{ id: data.id, name: data.name }, ...companies.value.filter((row) => row.id !== data.id)];
labels.value[`company:${data.id}`] = data.name;
kind.value = 'company';
partyId.value = data.id;
companyName.value = '';
message.success('شرکت در شرکت‌ها ثبت شد.');
})
.catch((e: unknown) => {
if (axios.isAxiosError(e)) message.error(apiMessage(e));
return Promise.reject(e);
})
.finally(() => {
companySaving.value = false;
});
function openPersonForm() {
selectOpen.value = false;
personOpen.value = true;
}
function openCompanyForm() {
selectOpen.value = false;
companyOpen.value = true;
}
function onPersonCreated(person?: PersonDto) {
personOpen.value = false;
if (!person?.id) return;
const name = person.fullName?.trim() || `${person.firstName} ${person.lastName}`.trim();
people.value = [{ id: person.id, name }, ...people.value.filter((row) => row.id !== person.id)];
labels.value[`person:${person.id}`] = name;
kind.value = 'person';
partyId.value = person.id;
}
function onCompanyCreated(company?: CompanyDto) {
companyOpen.value = false;
if (!company?.id) return;
companies.value = [{ id: company.id, name: company.name }, ...companies.value.filter((row) => row.id !== company.id)];
labels.value[`company:${company.id}`] = company.name;
kind.value = 'company';
partyId.value = company.id;
}
onMounted(() => {
@@ -173,7 +149,9 @@ onMounted(() => {
<template>
<a-flex vertical :gap="4">
<a-select
:open="selectOpen"
show-search
@dropdownVisibleChange="onDropdown"
allow-clear
:filter-option="false"
:loading="loading"
@@ -217,8 +195,8 @@ onMounted(() => {
<template #dropdownRender="{ menuNode }">
<v-nodes :vnodes="menuNode" />
<a-flex @mousedown.prevent>
<a-button type="link" @click="personOpen = true">شخص جدید</a-button>
<a-button type="link" @click="companyOpen = true">شرکت جدید</a-button>
<a-button type="link" @click="openPersonForm">شخص جدید</a-button>
<a-button type="link" @click="openCompanyForm">شرکت جدید</a-button>
</a-flex>
</template>
</a-select>
@@ -227,36 +205,21 @@ onMounted(() => {
</a-typography-text>
</a-flex>
<a-modal
v-model:open="personOpen"
title="شخص جدید"
ok-text="ثبت در مشتریان"
cancel-text="انصراف"
:confirm-loading="personSaving"
@ok="savePerson"
>
<a-form layout="vertical">
<a-form-item label="نام" required>
<a-input v-model:value="personForm.firstName" />
</a-form-item>
<a-form-item label="نام خانوادگی">
<a-input v-model:value="personForm.lastName" />
</a-form-item>
</a-form>
</a-modal>
<UiModal :open="personOpen" title="شخص جدید" @close="personOpen = false">
<PersonFormView
v-if="personOpen"
embedded
@saved="onPersonCreated"
@cancel="personOpen = false"
/>
</UiModal>
<a-modal
v-model:open="companyOpen"
title="شرکت جدید"
ok-text="ثبت در شرکت‌ها"
cancel-text="انصراف"
:confirm-loading="companySaving"
@ok="saveCompany"
>
<a-form layout="vertical">
<a-form-item label="نام شرکت" required>
<a-input v-model:value="companyName" />
</a-form-item>
</a-form>
</a-modal>
<UiModal :open="companyOpen" title="شرکت جدید" @close="companyOpen = false">
<CompanyFormView
v-if="companyOpen"
embedded
@saved="onCompanyCreated"
@cancel="companyOpen = false"
/>
</UiModal>
</template>