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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user