Files
CRM_Montzar/apps/web/src/components/treasury/TreasuryPartySelect.vue
T

263 lines
8.3 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed, defineComponent, onMounted, ref } from 'vue';
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 { api } from '@/lib/api';
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 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';
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 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 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;
});
}
onMounted(() => {
void load();
});
</script>
<template>
<a-flex vertical :gap="4">
<a-select
show-search
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="personOpen = true">شخص جدید</a-button>
<a-button type="link" @click="companyOpen = true">شرکت جدید</a-button>
</a-flex>
</template>
</a-select>
<a-typography-text v-if="kind === 'other'" type="secondary">
برای سایر، مبلغ دریافت یا پرداخت با مبلغ کل یکی است و مانده نمی‌ماند.
</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>
<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>
</template>