feat(leads): keep gender on leads and form submissions
A lead can carry the same honorific as a person, and a form that asks gender writes it onto the lead. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Alert as AAlert,
|
||||
Button as AButton,
|
||||
Card as ACard,
|
||||
Checkbox as ACheckbox,
|
||||
Col as ACol,
|
||||
Descriptions as ADescriptions,
|
||||
DescriptionsItem as ADescriptionsItem,
|
||||
@@ -66,8 +67,9 @@ import type {
|
||||
LeadStageDto,
|
||||
OrgUserDto,
|
||||
PaginatedResponse,
|
||||
PersonGender,
|
||||
} from '@mento/shared';
|
||||
import { ACTIVITY_TYPE_LABELS } from '@mento/shared';
|
||||
import { ACTIVITY_TYPE_LABELS, PERSON_GENDERS, PERSON_GENDER_LABELS, splitPersonHonorific } from '@mento/shared';
|
||||
import { canUseBehavior } from '@/lib/behavior-access';
|
||||
import SalesCopilotPanel from '@/components/behavior/SalesCopilotPanel.vue';
|
||||
|
||||
@@ -113,6 +115,8 @@ let narrowMq: MediaQueryList | null = null;
|
||||
const editForm = ref({
|
||||
title: '',
|
||||
contactName: '',
|
||||
gender: 'none' as PersonGender,
|
||||
showGenderInName: false,
|
||||
mobile: '',
|
||||
phone: '',
|
||||
city: '',
|
||||
@@ -145,6 +149,30 @@ const answerRows = computed(() =>
|
||||
noteView.value.rows.filter((row) => !isBirthDateAnswer(row.label)),
|
||||
);
|
||||
|
||||
const genderOptions = PERSON_GENDERS.map((key) => ({
|
||||
value: key,
|
||||
label: PERSON_GENDER_LABELS[key],
|
||||
}));
|
||||
|
||||
const genderLabel = computed(() => {
|
||||
const gender = lead.value?.gender;
|
||||
const key = gender === 'male' || gender === 'female' ? gender : 'none';
|
||||
const label = PERSON_GENDER_LABELS[key];
|
||||
return lead.value?.showGenderInName && key !== 'none' ? `${label} · در نام` : label;
|
||||
});
|
||||
|
||||
function bareLeadName(value: string) {
|
||||
const split = splitPersonHonorific(value, '');
|
||||
return [split.firstName, split.lastName].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
watch(
|
||||
() => editForm.value.gender,
|
||||
(value) => {
|
||||
if (value === 'none') editForm.value.showGenderInName = false;
|
||||
},
|
||||
);
|
||||
|
||||
function apiError(e: unknown, fallback: string) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
||||
@@ -186,9 +214,13 @@ function startEdit() {
|
||||
if (!lead.value) return;
|
||||
editing.value = true;
|
||||
moreOpen.value = false;
|
||||
const gender =
|
||||
lead.value.gender === 'male' || lead.value.gender === 'female' ? lead.value.gender : 'none';
|
||||
editForm.value = {
|
||||
title: lead.value.title,
|
||||
contactName: lead.value.contactName ?? '',
|
||||
title: bareLeadName(lead.value.title),
|
||||
contactName: bareLeadName(lead.value.contactName ?? ''),
|
||||
gender,
|
||||
showGenderInName: !!lead.value.showGenderInName && gender !== 'none',
|
||||
mobile: lead.value.mobile,
|
||||
phone: lead.value.phone ?? '',
|
||||
city: lead.value.city ?? '',
|
||||
@@ -214,6 +246,9 @@ async function saveEdit() {
|
||||
await api.patch(`/leads/${id.value}`, {
|
||||
title: editForm.value.title.trim(),
|
||||
contactName: editForm.value.contactName.trim() || null,
|
||||
gender: editForm.value.gender,
|
||||
showGenderInName:
|
||||
editForm.value.gender !== 'none' && editForm.value.showGenderInName,
|
||||
mobile: editForm.value.mobile.trim(),
|
||||
phone: editForm.value.phone.trim() || null,
|
||||
city: editForm.value.city.trim() || null,
|
||||
@@ -553,6 +588,24 @@ onBeforeUnmount(() => narrowMq?.removeEventListener('change', syncNarrow));
|
||||
<a-input v-model:value="editForm.contactName" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="جنسیت">
|
||||
<a-select v-model:value="editForm.gender" :options="genderOptions" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item
|
||||
label="نمایش در نام"
|
||||
extra="مرد «جناب آقای» و زن «سرکار خانم» ابتدای نام مخاطب ذخیره میشود. اگر نام مخاطب خالی باشد روی عنوان مینشیند و با تبدیل به مشتری به تهحساب هم میرود."
|
||||
>
|
||||
<a-checkbox
|
||||
v-model:checked="editForm.showGenderInName"
|
||||
:disabled="editForm.gender === 'none'"
|
||||
>
|
||||
نمایش در نام
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="8">
|
||||
<a-form-item label="موبایل" required>
|
||||
<a-input
|
||||
@@ -666,6 +719,7 @@ onBeforeUnmount(() => narrowMq?.removeEventListener('change', syncNarrow));
|
||||
<a-descriptions-item label="مالک/مخاطب">
|
||||
{{ lead.contactName || '—' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="جنسیت">{{ genderLabel }}</a-descriptions-item>
|
||||
<a-descriptions-item label="موبایل">
|
||||
<span dir="ltr">{{ lead.mobile }}</span>
|
||||
</a-descriptions-item>
|
||||
|
||||
@@ -997,6 +997,7 @@ onMounted(load);
|
||||
<option value="lead.trade">صنف</option>
|
||||
<option value="lead.notes">یادداشت لید</option>
|
||||
<option value="lead.source">منبع لید</option>
|
||||
<option value="lead.gender">جنسیت</option>
|
||||
</optgroup>
|
||||
<optgroup label="شخص">
|
||||
<option value="person.fullName">نام کامل</option>
|
||||
@@ -1006,6 +1007,7 @@ onMounted(load);
|
||||
<option value="person.email">ایمیل</option>
|
||||
<option value="person.phone">تلفن</option>
|
||||
<option value="person.position">سمت</option>
|
||||
<option value="person.gender">جنسیت شخص</option>
|
||||
</optgroup>
|
||||
<optgroup label="معامله">
|
||||
<option value="deal.title">عنوان معامله</option>
|
||||
|
||||
@@ -77,9 +77,10 @@ import type {
|
||||
LeadStageDto,
|
||||
LeadStatsDto,
|
||||
OrgUserDto,
|
||||
PersonGender,
|
||||
PaginatedResponse,
|
||||
} from '@mento/shared';
|
||||
import { brand } from '@mento/shared';
|
||||
import { brand, PERSON_GENDERS, PERSON_GENDER_LABELS, splitPersonHonorific } from '@mento/shared';
|
||||
|
||||
const { Title: ATypographyTitle, Text: ATypographyText, Paragraph: ATypographyParagraph } =
|
||||
Typography;
|
||||
@@ -191,6 +192,8 @@ const editingLeadId = ref<string | null>(null);
|
||||
const createForm = ref({
|
||||
title: '',
|
||||
contactName: '',
|
||||
gender: 'none' as PersonGender,
|
||||
showGenderInName: false,
|
||||
mobile: '',
|
||||
city: '',
|
||||
region: '',
|
||||
@@ -656,12 +659,31 @@ const createModalTitle = computed(() =>
|
||||
editingLeadId.value ? 'ویرایش لید' : 'ثبت لید جدید',
|
||||
);
|
||||
|
||||
const genderOptions = PERSON_GENDERS.map((key) => ({
|
||||
value: key,
|
||||
label: PERSON_GENDER_LABELS[key],
|
||||
}));
|
||||
|
||||
function bareLeadName(value: string) {
|
||||
const split = splitPersonHonorific(value, '');
|
||||
return [split.firstName, split.lastName].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
watch(
|
||||
() => createForm.value.gender,
|
||||
(value) => {
|
||||
if (value === 'none') createForm.value.showGenderInName = false;
|
||||
},
|
||||
);
|
||||
|
||||
function openCreate() {
|
||||
editingLeadId.value = null;
|
||||
createError.value = '';
|
||||
createForm.value = {
|
||||
title: '',
|
||||
contactName: '',
|
||||
gender: 'none',
|
||||
showGenderInName: false,
|
||||
mobile: '',
|
||||
city: '',
|
||||
region: '',
|
||||
@@ -693,8 +715,10 @@ function openEdit(lead: LeadDto) {
|
||||
editingLeadId.value = lead.id;
|
||||
createError.value = '';
|
||||
createForm.value = {
|
||||
title: lead.title,
|
||||
contactName: lead.contactName ?? '',
|
||||
title: bareLeadName(lead.title),
|
||||
contactName: bareLeadName(lead.contactName ?? ''),
|
||||
gender: lead.gender && lead.gender !== 'none' ? lead.gender : 'none',
|
||||
showGenderInName: !!lead.showGenderInName && lead.gender !== 'none',
|
||||
mobile: lead.mobile,
|
||||
city: lead.city ?? '',
|
||||
region: lead.region ?? '',
|
||||
@@ -857,6 +881,8 @@ async function submitCreate() {
|
||||
title: company || person || mobile,
|
||||
mobile,
|
||||
contactName: person || null,
|
||||
gender: createForm.value.gender,
|
||||
showGenderInName: createForm.value.gender !== 'none' && createForm.value.showGenderInName,
|
||||
city: createForm.value.city.trim() || null,
|
||||
region: createForm.value.region.trim() || null,
|
||||
source: createForm.value.source.trim() || null,
|
||||
@@ -1629,6 +1655,24 @@ onBeforeUnmount(() => {
|
||||
<a-input v-model:value="createForm.contactName" placeholder="مهدی احمدی" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="جنسیت">
|
||||
<a-select v-model:value="createForm.gender" :options="genderOptions" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item
|
||||
label="نمایش در نام"
|
||||
extra="مرد «جناب آقای» و زن «سرکار خانم» ابتدای نام مخاطب ذخیره میشود. اگر نام مخاطب خالی باشد روی عنوان مینشیند و با تبدیل به مشتری به تهحساب هم میرود."
|
||||
>
|
||||
<a-checkbox
|
||||
v-model:checked="createForm.showGenderInName"
|
||||
:disabled="createForm.gender === 'none'"
|
||||
>
|
||||
نمایش در نام
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="موبایل">
|
||||
<template #extra>
|
||||
|
||||
Reference in New Issue
Block a user