feat(contacts): keep Tahesab honorifics with person gender

Male and female names can store the title when requested, and Tahesab import and export use the same gender.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-29 18:46:10 +03:30
co-authored by Cursor
parent 0d5e1034e5
commit b9bb2ff9fa
11 changed files with 350 additions and 14 deletions
+38 -4
View File
@@ -1,11 +1,12 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, ref } from 'vue';
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { RouterLink, useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import {
Alert as AAlert,
Button as AButton,
Card as ACard,
Checkbox as ACheckbox,
Col as ACol,
Divider as ADivider,
Flex as AFlex,
@@ -34,7 +35,8 @@ import {
SearchOutlined,
} from '@ant-design/icons-vue';
import { api } from '@/lib/api';
import type { CompanyDto, CreatePersonRequest, PersonDto, PersonMood } from '@mento/shared';
import type { CompanyDto, CreatePersonRequest, PersonDto, PersonGender, PersonMood } from '@mento/shared';
import { PERSON_GENDER_LABELS, PERSON_GENDERS, splitPersonHonorific } from '@mento/shared';
import { PERSON_MOOD_LABELS, PERSON_MOODS } from '@/lib/person-mood';
import { formatLocalDateInput, toFaDigits } from '@/lib/jalali';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
@@ -85,6 +87,8 @@ const companies = ref<CompanyDto[]>([]);
const firstName = ref('');
const lastName = ref('');
const gender = ref<PersonGender>('none');
const showGenderInName = ref(false);
const email = ref('');
const phone = ref('');
const mobile = ref('');
@@ -125,6 +129,15 @@ const companyOptions = computed(() => [
...companies.value.map((c) => ({ value: c.id, label: c.name })),
]);
const genderOptions = PERSON_GENDERS.map((key) => ({
value: key,
label: PERSON_GENDER_LABELS[key],
}));
watch(gender, (value) => {
if (value === 'none') showGenderInName.value = false;
});
const backTo = computed(() =>
isNew.value ? { name: 'contacts' as const } : { name: 'person-detail' as const, params: { id: id.value } },
);
@@ -249,8 +262,12 @@ function pickFromVcf() {
}
function fill(p: PersonDto) {
firstName.value = p.firstName;
lastName.value = p.lastName;
const bare = splitPersonHonorific(p.firstName, p.lastName);
firstName.value = bare.firstName;
lastName.value = bare.lastName;
gender.value = p.gender && p.gender !== 'none' ? p.gender : (bare.gender ?? 'none');
showGenderInName.value =
gender.value !== 'none' && (p.showGenderInName || bare.gender != null);
email.value = p.email ?? '';
phone.value = p.phone ?? '';
mobile.value = p.mobile ?? '';
@@ -330,6 +347,8 @@ function payload(): CreatePersonRequest {
return {
firstName: firstName.value,
lastName: lastName.value,
gender: gender.value,
showGenderInName: gender.value !== 'none' && showGenderInName.value,
email: email.value || undefined,
phone: phone.value || undefined,
mobile: mobile.value || undefined,
@@ -462,6 +481,21 @@ onMounted(load);
<a-input v-model:value="lastName" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="جنسیت">
<a-select v-model:value="gender" :options="genderOptions" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item
label="نمایش در نام"
extra="مرد «جناب آقای» و زن «سرکار خانم» ابتدای نام ذخیره می‌شود و به ته‌حساب هم همین‌طور می‌رود."
>
<a-checkbox v-model:checked="showGenderInName" :disabled="gender === 'none'">
نمایش در نام
</a-checkbox>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="موبایل">
<a-space-compact block style="width: 100%">
+11
View File
@@ -47,6 +47,7 @@ import type {
QuoteDto,
TicketDto,
} from '@mento/shared';
import { PERSON_GENDER_LABELS } from '@mento/shared';
import { PERSON_MOOD_LABELS } from '@/lib/person-mood';
import { canUseBehavior } from '@/lib/behavior-access';
import DiscBadge from '@/components/behavior/DiscBadge.vue';
@@ -167,6 +168,11 @@ const registeredAt = computed(() =>
const birthDateLabel = computed(() =>
person.value?.birthDate ? formatJalaliNumeric(new Date(person.value.birthDate)) : '—',
);
const genderLabel = computed(() => {
const key = person.value?.gender;
if (key === 'male' || key === 'female' || key === 'none') return PERSON_GENDER_LABELS[key];
return '—';
});
const externalCode = computed(() => {
if (person.value?.sourceRef?.trim()) return person.value.sourceRef.trim();
const fields = person.value?.customFields ?? {};
@@ -772,6 +778,10 @@ watch(() => route.query.tab, readTab, { immediate: true });
<dt>سمت</dt>
<dd>{{ person.position || '—' }}</dd>
</div>
<div class="fact">
<dt>جنسیت</dt>
<dd>{{ genderLabel }}</dd>
</div>
<div class="fact">
<dt>تاریخ تولد</dt>
<dd>{{ birthDateLabel }}</dd>
@@ -1102,6 +1112,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
<div><dt>تلفن</dt><dd>{{ person.phone ? toFaDigits(person.phone) : '—' }}</dd></div>
<div><dt>ایمیل</dt><dd>{{ person.email || '—' }}</dd></div>
<div><dt>سمت</dt><dd>{{ person.position || '—' }}</dd></div>
<div><dt>جنسیت</dt><dd>{{ genderLabel }}</dd></div>
<div><dt>منبع ورود</dt><dd>{{ importSourceLabel }}</dd></div>
<div><dt>کد مبدأ</dt><dd dir="ltr">{{ externalCode ? toFaDigits(externalCode) : '—' }}</dd></div>
<div><dt>نام قبلی</dt><dd>{{ person.previousFullName || '—' }}</dd></div>