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>
226 lines
7.0 KiB
Vue
226 lines
7.0 KiB
Vue
<script setup lang="ts">
|
|
import { computed, defineComponent, onMounted, ref } from 'vue';
|
|
import axios from 'axios';
|
|
import {
|
|
Button as AButton,
|
|
Flex as AFlex,
|
|
Select as ASelect,
|
|
Typography,
|
|
message,
|
|
} from 'ant-design-vue';
|
|
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 } },
|
|
render() {
|
|
return this.vnodes;
|
|
},
|
|
});
|
|
|
|
const ASelectOption = ASelect.Option;
|
|
const ASelectOptGroup = ASelect.OptGroup;
|
|
const { Text: ATypographyText } = Typography;
|
|
|
|
const kind = defineModel<FinancialPartyKind | undefined>('kind');
|
|
const partyId = defineModel<string | undefined>('partyId');
|
|
|
|
type Choice = { id: string; name: string };
|
|
|
|
const people = ref<Choice[]>([]);
|
|
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 selectValue = computed(() => {
|
|
if (kind.value === 'other') return 'other';
|
|
if (kind.value && partyId.value) return `${kind.value}:${partyId.value}`;
|
|
return undefined;
|
|
});
|
|
|
|
function withCurrent(list: Choice[], group: 'person' | 'company' | 'user') {
|
|
if (kind.value !== group || !partyId.value) return list;
|
|
if (list.some((row) => row.id === partyId.value)) return list;
|
|
const name = labels.value[`${group}:${partyId.value}`] || 'انتخابشده';
|
|
return [{ id: partyId.value, name }, ...list];
|
|
}
|
|
|
|
const peopleOptions = computed(() => withCurrent(people.value, 'person'));
|
|
const companyOptions = computed(() => withCurrent(companies.value, 'company'));
|
|
const userOptions = computed(() => withCurrent(users.value, 'user'));
|
|
|
|
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 || 'ثبت ناموفق بود.';
|
|
}
|
|
|
|
let timer = 0;
|
|
function onSearch(value: string) {
|
|
window.clearTimeout(timer);
|
|
timer = window.setTimeout(() => {
|
|
void load(value);
|
|
}, 250);
|
|
}
|
|
|
|
async function load(q = '') {
|
|
loading.value = true;
|
|
try {
|
|
const { data } = await api.get<TreasuryPartySearchDto>('/treasury/parties', {
|
|
params: { q: q.trim() || undefined },
|
|
});
|
|
people.value = data.people;
|
|
companies.value = data.companies;
|
|
users.value = data.users;
|
|
for (const row of data.people) labels.value[`person:${row.id}`] = row.name;
|
|
for (const row of data.companies) labels.value[`company:${row.id}`] = row.name;
|
|
for (const row of data.users) labels.value[`user:${row.id}`] = row.name;
|
|
} catch (e) {
|
|
if (!(axios.isAxiosError(e) && e.response?.status === 404)) message.error(apiMessage(e));
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function onChange(value: unknown) {
|
|
if (typeof value !== 'string' || !value) {
|
|
kind.value = undefined;
|
|
partyId.value = undefined;
|
|
return;
|
|
}
|
|
if (value === 'other') {
|
|
kind.value = 'other';
|
|
partyId.value = undefined;
|
|
return;
|
|
}
|
|
const splitAt = value.indexOf(':');
|
|
const nextKind = value.slice(0, splitAt) as FinancialPartyKind;
|
|
const id = value.slice(splitAt + 1);
|
|
if (nextKind !== 'person' && nextKind !== 'company' && nextKind !== 'user') return;
|
|
kind.value = nextKind;
|
|
partyId.value = id;
|
|
}
|
|
|
|
function onDropdown(open: boolean) {
|
|
selectOpen.value = open;
|
|
}
|
|
|
|
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(() => {
|
|
void load();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<a-flex vertical :gap="4">
|
|
<a-select
|
|
:open="selectOpen"
|
|
show-search
|
|
@dropdownVisibleChange="onDropdown"
|
|
allow-clear
|
|
:filter-option="false"
|
|
:loading="loading"
|
|
:value="selectValue"
|
|
placeholder="مشتری، شرکت، کاربر یا سایر"
|
|
style="width: 100%"
|
|
@search="onSearch"
|
|
@change="onChange"
|
|
>
|
|
<a-select-option value="other" label="سایر">سایر</a-select-option>
|
|
<a-select-opt-group v-if="peopleOptions.length" label="مشتریان">
|
|
<a-select-option
|
|
v-for="row in peopleOptions"
|
|
:key="`person:${row.id}`"
|
|
:value="`person:${row.id}`"
|
|
:label="row.name"
|
|
>
|
|
{{ row.name }}
|
|
</a-select-option>
|
|
</a-select-opt-group>
|
|
<a-select-opt-group v-if="companyOptions.length" label="شرکتها">
|
|
<a-select-option
|
|
v-for="row in companyOptions"
|
|
:key="`company:${row.id}`"
|
|
:value="`company:${row.id}`"
|
|
:label="row.name"
|
|
>
|
|
{{ row.name }}
|
|
</a-select-option>
|
|
</a-select-opt-group>
|
|
<a-select-opt-group v-if="userOptions.length" label="کاربران">
|
|
<a-select-option
|
|
v-for="row in userOptions"
|
|
:key="`user:${row.id}`"
|
|
:value="`user:${row.id}`"
|
|
:label="row.name"
|
|
>
|
|
{{ row.name }}
|
|
</a-select-option>
|
|
</a-select-opt-group>
|
|
<template #dropdownRender="{ menuNode }">
|
|
<v-nodes :vnodes="menuNode" />
|
|
<a-flex @mousedown.prevent>
|
|
<a-button type="link" @click="openPersonForm">شخص جدید</a-button>
|
|
<a-button type="link" @click="openCompanyForm">شرکت جدید</a-button>
|
|
</a-flex>
|
|
</template>
|
|
</a-select>
|
|
<a-typography-text v-if="kind === 'other'" type="secondary">
|
|
برای سایر، مبلغ دریافت یا پرداخت با مبلغ کل یکی است و مانده نمیماند.
|
|
</a-typography-text>
|
|
</a-flex>
|
|
|
|
<UiModal :open="personOpen" title="شخص جدید" @close="personOpen = false">
|
|
<PersonFormView
|
|
v-if="personOpen"
|
|
embedded
|
|
@saved="onPersonCreated"
|
|
@cancel="personOpen = false"
|
|
/>
|
|
</UiModal>
|
|
|
|
<UiModal :open="companyOpen" title="شرکت جدید" @close="companyOpen = false">
|
|
<CompanyFormView
|
|
v-if="companyOpen"
|
|
embedded
|
|
@saved="onCompanyCreated"
|
|
@cancel="companyOpen = false"
|
|
/>
|
|
</UiModal>
|
|
</template>
|