2026-09-27 17:05:11 +03:30
< script setup lang = "ts" >
2026-10-31 12:49:42 +03:30
import { computed , onBeforeUnmount , onMounted , ref , watch } from 'vue' ;
2026-10-18 19:39:59 +03:30
import { RouterLink , useRoute , useRouter } from 'vue-router' ;
2026-09-27 17:05:11 +03:30
import axios from 'axios' ;
2026-10-18 19:39:59 +03:30
import {
Affix as AAffix ,
Alert as AAlert ,
Button as AButton ,
Card as ACard ,
2026-10-31 12:49:42 +03:30
Checkbox as ACheckbox ,
2026-10-18 19:39:59 +03:30
Col as ACol ,
Descriptions as ADescriptions ,
DescriptionsItem as ADescriptionsItem ,
Divider as ADivider ,
Drawer as ADrawer ,
Empty as AEmpty ,
Flex as AFlex ,
Form as AForm ,
2026-10-25 20:21:33 +03:30
Image as AImage ,
2026-10-18 19:39:59 +03:30
FormItem as AFormItem ,
Input as AInput ,
List as AList ,
ListItem as AListItem ,
ListItemMeta as AListItemMeta ,
Modal ,
Row as ARow ,
Select as ASelect ,
Space as ASpace ,
Spin as ASpin ,
Tag as ATag ,
Textarea as ATextarea ,
Timeline as ATimeline ,
TimelineItem as ATimelineItem ,
Typography ,
message ,
theme ,
} from 'ant-design-vue' ;
import {
ArrowLeftOutlined ,
CopyOutlined ,
EditOutlined ,
MessageOutlined ,
MoreOutlined ,
PhoneOutlined ,
PlusOutlined ,
UserAddOutlined ,
} from '@ant-design/icons-vue' ;
2026-09-27 17:05:11 +03:30
import { api } from '@/lib/api' ;
2026-10-25 20:21:33 +03:30
import { isBirthDateAnswer , leadNoteFileIsImage , parseLeadFormNotes } from '@/lib/lead-form-notes' ;
2026-10-18 19:39:59 +03:30
import {
formatClockFa ,
formatJalaliNumeric ,
formatLocalDateInput ,
parseLocalInput ,
toFaDigits ,
} from '@/lib/jalali' ;
2026-09-27 17:05:11 +03:30
import { useAuthStore } from '@/stores/auth' ;
2026-11-03 13:15:21 +03:30
import { canAssignToOthers } from '@/lib/assign-access' ;
2026-10-13 14:45:29 +03:30
import JalaliDateInput from '@/components/JalaliDateInput.vue' ;
2026-10-06 19:54:38 +03:30
import RelatedTasksPanel from '@/components/routines/RelatedTasksPanel.vue' ;
2026-10-13 21:05:17 +03:30
import SmsComposeModal from '@/components/SmsComposeModal.vue' ;
2026-09-27 17:05:11 +03:30
import type {
2026-10-11 14:12:36 +03:30
ActivityDto ,
2026-09-27 17:05:11 +03:30
ConvertLeadResult ,
LeadDto ,
LeadEventDto ,
LeadStageDto ,
OrgUserDto ,
2026-10-11 14:12:36 +03:30
PaginatedResponse ,
2026-10-31 12:49:42 +03:30
PersonGender ,
2026-09-27 17:05:11 +03:30
} from '@mento/shared' ;
2026-10-31 12:49:42 +03:30
import { ACTIVITY_TYPE_LABELS , PERSON_GENDERS , PERSON_GENDER_LABELS , splitPersonHonorific } from '@mento/shared' ;
2026-10-18 21:04:26 +03:30
import { canUseBehavior } from '@/lib/behavior-access' ;
import SalesCopilotPanel from '@/components/behavior/SalesCopilotPanel.vue' ;
2026-09-27 17:05:11 +03:30
2026-10-18 19:39:59 +03:30
const { Title : ATypographyTitle , Text : ATypographyText , Paragraph : ATypographyParagraph } =
Typography ;
const { token } = theme . useToken ();
2026-09-27 17:05:11 +03:30
type LeadDetail = LeadDto & { events : LeadEventDto [] };
const route = useRoute ();
const router = useRouter ();
const auth = useAuthStore ();
2026-11-03 13:15:21 +03:30
const canAssignOthers = computed (() => canAssignToOthers ( auth . user , 'leads' ));
2026-10-18 21:04:26 +03:30
const canBehavior = computed (() => canUseBehavior ( auth . user ));
const copilotOpen = ref ( false );
2026-09-27 17:05:11 +03:30
const id = computed (() => String ( route . params . id ));
const loading = ref ( false );
const busy = ref ( false );
const error = ref ( '' );
const okMsg = ref ( '' );
const lead = ref < LeadDetail | null >( null );
const stages = ref < LeadStageDto [] >([]);
const users = ref < OrgUserDto [] >([]);
2026-10-11 14:12:36 +03:30
const activities = ref < ActivityDto [] >([]);
2026-09-27 17:05:11 +03:30
const stageId = ref ( '' );
const assigneeId = ref ( '' );
const callOutcome = ref ( '' );
const callBody = ref ( '' );
const callFollowUp = ref ( '' );
const noteBody = ref ( '' );
const followUpAt = ref ( '' );
const followUpBody = ref ( '' );
2026-10-04 12:01:21 +03:30
const editing = ref ( false );
2026-10-13 14:45:29 +03:30
const moreOpen = ref ( false );
2026-10-13 21:05:17 +03:30
const smsOpen = ref ( false );
2026-10-13 14:45:29 +03:30
const copied = ref ( false );
2026-10-18 19:39:59 +03:30
const narrow = ref (
typeof window !== 'undefined' && window . matchMedia ( '(max-width: 900px)' ). matches ,
);
2026-10-13 14:45:29 +03:30
let narrowMq : MediaQueryList | null = null ;
2026-10-04 12:01:21 +03:30
const editForm = ref ({
title : '' ,
contactName : '' ,
2026-10-31 12:49:42 +03:30
gender : 'none' as PersonGender ,
showGenderInName : false ,
2026-10-04 12:01:21 +03:30
mobile : '' ,
phone : '' ,
city : '' ,
region : '' ,
source : '' ,
instagram : '' ,
2026-10-13 18:04:53 +03:30
birthDate : '' ,
2026-10-04 12:01:21 +03:30
trade : '' ,
notes : '' ,
});
2026-09-27 17:05:11 +03:30
function formatDate ( value : string | null | undefined ) {
if ( ! value ) return '—' ;
try {
return formatJalaliNumeric ( new Date ( value ));
} catch {
return value ;
}
}
2026-10-25 20:21:33 +03:30
const noteView = computed (() => parseLeadFormNotes ( lead . value ? . notes ));
const formBirthDate = computed (
() => noteView . value . rows . find (( row ) => isBirthDateAnswer ( row . label ) && ! row . file ) ? . text ?? '' ,
);
const shownBirthDate = computed (() => {
if ( lead . value ? . birthDate ) return formatDate ( lead . value . birthDate );
return formBirthDate . value || '—' ;
});
const answerRows = computed (() =>
noteView . value . rows . filter (( row ) => ! isBirthDateAnswer ( row . label )),
);
2026-10-31 12:49:42 +03:30
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 ;
},
);
2026-09-27 17:05:11 +03:30
function apiError ( e : unknown , fallback : string ) {
if ( axios . isAxiosError ( e )) {
const msg = ( e . response ? . data as { message ?: string | string [] }) ? . message ;
if ( Array . isArray ( msg )) return msg . join ( '، ' );
if ( msg ) return msg ;
if ( ! e . response ) return 'اتصال به API برقرار نشد.' ;
}
return fallback ;
}
async function load () {
loading . value = true ;
error . value = '' ;
try {
2026-10-11 14:12:36 +03:30
const [ leadRes , stagesRes , usersRes , actRes ] = await Promise . all ([
2026-09-27 17:05:11 +03:30
api . get < LeadDetail >( `/leads/ ${ id . value } ` ),
api . get < LeadStageDto [] >( '/leads/stages' ),
api . get < OrgUserDto [] >( '/users' ). catch (() => ({ data : [] as OrgUserDto [] })),
2026-10-11 14:12:36 +03:30
api
. get < PaginatedResponse < ActivityDto > > ( '/activities' , {
params : { filter : 'all' , leadId : id . value , limit : 30 },
})
. catch (() => null ),
2026-09-27 17:05:11 +03:30
]);
lead . value = leadRes . data ;
2026-10-11 14:12:36 +03:30
activities . value = actRes ? . data . items ?? [];
2026-09-27 17:05:11 +03:30
stages . value = stagesRes . data ;
users . value = usersRes . data . filter (( u ) => u . isActive !== false );
stageId . value = leadRes . data . stageId ;
assigneeId . value = leadRes . data . assigneeId ?? '' ;
} catch ( e ) {
error . value = apiError ( e , 'بارگذاری لید ناموفق بود.' );
} finally {
loading . value = false ;
}
}
2026-10-04 12:01:21 +03:30
function startEdit () {
if ( ! lead . value ) return ;
editing . value = true ;
2026-10-18 19:39:59 +03:30
moreOpen . value = false ;
2026-10-31 12:49:42 +03:30
const gender =
lead . value . gender === 'male' || lead . value . gender === 'female' ? lead . value . gender : 'none' ;
2026-10-04 12:01:21 +03:30
editForm . value = {
2026-10-31 12:49:42 +03:30
title : bareLeadName ( lead . value . title ),
contactName : bareLeadName ( lead . value . contactName ?? '' ),
gender ,
showGenderInName : !! lead . value . showGenderInName && gender !== 'none' ,
2026-10-04 12:01:21 +03:30
mobile : lead . value . mobile ,
phone : lead . value . phone ?? '' ,
city : lead . value . city ?? '' ,
region : lead . value . region ?? '' ,
source : lead . value . source ?? '' ,
instagram : lead . value . instagram ?? '' ,
2026-10-18 19:39:59 +03:30
birthDate : lead . value . birthDate
? formatLocalDateInput ( new Date ( lead . value . birthDate ))
: '' ,
2026-10-04 12:01:21 +03:30
trade : lead . value . trade ?? '' ,
notes : lead . value . notes ?? '' ,
};
}
async function saveEdit () {
if ( ! editForm . value . title . trim () || ! editForm . value . mobile . trim ()) {
error . value = 'عنوان و موبایل الزامی است.' ;
return ;
}
busy . value = true ;
error . value = '' ;
try {
await api . patch ( `/leads/ ${ id . value } ` , {
title : editForm . value . title . trim (),
contactName : editForm . value . contactName . trim () || null ,
2026-10-31 12:49:42 +03:30
gender : editForm . value . gender ,
showGenderInName :
editForm . value . gender !== 'none' && editForm . value . showGenderInName ,
2026-10-04 12:01:21 +03:30
mobile : editForm . value . mobile . trim (),
phone : editForm . value . phone . trim () || null ,
city : editForm . value . city . trim () || null ,
region : editForm . value . region . trim () || null ,
source : editForm . value . source . trim () || null ,
instagram : editForm . value . instagram . trim () || null ,
2026-10-20 15:20:47 +03:30
birthDate : editForm . value . birthDate || null ,
2026-10-04 12:01:21 +03:30
trade : editForm . value . trade . trim () || null ,
notes : editForm . value . notes . trim () || null ,
});
okMsg . value = 'لید ویرایش شد.' ;
2026-10-18 19:39:59 +03:30
message . success ( okMsg . value );
2026-10-04 12:01:21 +03:30
editing . value = false ;
await load ();
} catch ( e ) {
error . value = apiError ( e , 'ویرایش لید ناموفق بود.' );
} finally {
busy . value = false ;
}
}
2026-09-27 17:05:11 +03:30
async function changeStage () {
if ( ! stageId . value || stageId . value === lead . value ? . stageId ) return ;
busy . value = true ;
error . value = '' ;
try {
await api . patch ( `/leads/ ${ id . value } /move` , { stageId : stageId . value });
okMsg . value = 'وضعیت بهروز شد.' ;
2026-10-18 19:39:59 +03:30
message . success ( okMsg . value );
moreOpen . value = false ;
2026-09-27 17:05:11 +03:30
await load ();
} catch ( e ) {
error . value = apiError ( e , 'تغییر وضعیت ناموفق بود.' );
} finally {
busy . value = false ;
}
}
async function assignLead () {
if ( ! assigneeId . value ) return ;
busy . value = true ;
error . value = '' ;
try {
await api . post ( '/leads/assign' , {
leadIds : [ id . value ],
assigneeId : assigneeId . value ,
});
okMsg . value = 'کارشناس تخصیص داده شد.' ;
2026-10-18 19:39:59 +03:30
message . success ( okMsg . value );
moreOpen . value = false ;
2026-09-27 17:05:11 +03:30
await load ();
} catch ( e ) {
error . value = apiError ( e , 'تخصیص ناموفق بود.' );
} finally {
busy . value = false ;
}
}
2026-10-13 14:45:29 +03:30
function localToIso ( value : string ) : string | undefined {
const parsed = parseLocalInput ( value );
if ( ! parsed || Number . isNaN ( parsed . getTime ())) return undefined ;
return parsed . toISOString ();
}
2026-09-27 17:05:11 +03:30
async function logCall () {
busy . value = true ;
error . value = '' ;
try {
await api . post ( `/leads/ ${ id . value } /call` , {
outcome : callOutcome . value || undefined ,
body : callBody . value || undefined ,
2026-10-13 14:45:29 +03:30
nextFollowUpAt : callFollowUp . value ? localToIso ( callFollowUp . value ) : undefined ,
2026-09-27 17:05:11 +03:30
});
callOutcome . value = '' ;
callBody . value = '' ;
callFollowUp . value = '' ;
okMsg . value = 'تماس ثبت شد.' ;
2026-10-18 19:39:59 +03:30
message . success ( okMsg . value );
moreOpen . value = false ;
2026-09-27 17:05:11 +03:30
await load ();
} catch ( e ) {
error . value = apiError ( e , 'ثبت تماس ناموفق بود.' );
} finally {
busy . value = false ;
}
}
async function addNote () {
if ( ! noteBody . value . trim ()) return ;
busy . value = true ;
error . value = '' ;
try {
await api . post ( `/leads/ ${ id . value } /notes` , { body : noteBody . value . trim () });
noteBody . value = '' ;
okMsg . value = 'یادداشت ثبت شد.' ;
2026-10-18 19:39:59 +03:30
message . success ( okMsg . value );
moreOpen . value = false ;
2026-09-27 17:05:11 +03:30
await load ();
} catch ( e ) {
error . value = apiError ( e , 'ثبت یادداشت ناموفق بود.' );
} finally {
busy . value = false ;
}
}
async function setFollowUp () {
2026-10-13 14:45:29 +03:30
const nextFollowUpAt = localToIso ( followUpAt . value );
if ( ! nextFollowUpAt ) return ;
2026-09-27 17:05:11 +03:30
busy . value = true ;
error . value = '' ;
try {
await api . post ( `/leads/ ${ id . value } /follow-up` , {
2026-10-13 14:45:29 +03:30
nextFollowUpAt ,
2026-09-27 17:05:11 +03:30
body : followUpBody . value || undefined ,
});
followUpAt . value = '' ;
followUpBody . value = '' ;
okMsg . value = 'پیگیری تنظیم شد.' ;
2026-10-18 19:39:59 +03:30
message . success ( okMsg . value );
moreOpen . value = false ;
2026-09-27 17:05:11 +03:30
await load ();
} catch ( e ) {
error . value = apiError ( e , 'تنظیم پیگیری ناموفق بود.' );
} finally {
busy . value = false ;
}
}
2026-10-18 19:39:59 +03:30
function convertLead () {
Modal . confirm ({
title : 'این لید به مشتری تبدیل شود؟' ,
okText : 'تبدیل' ,
cancelText : 'انصراف' ,
async onOk () {
2026-09-27 17:05:11 +03:30
busy . value = true ;
error . value = '' ;
try {
const { data } = await api . post < ConvertLeadResult >( `/leads/ ${ id . value } /convert` , {
createIfMissing : true ,
});
okMsg . value = data . createdPerson
? 'لید تبدیل و شخص جدید ساخته شد.'
: 'لید به مشتری موجود متصل شد.' ;
2026-10-18 19:39:59 +03:30
message . success ( okMsg . value );
2026-09-27 17:05:11 +03:30
await load ();
if ( data . personId ) {
void router . push ({ name : 'person-detail' , params : { id : data . personId } });
}
} catch ( e ) {
error . value = apiError ( e , 'تبدیل به مشتری ناموفق بود.' );
} finally {
busy . value = false ;
}
2026-10-18 19:39:59 +03:30
},
});
2026-09-27 17:05:11 +03:30
}
2026-10-18 19:39:59 +03:30
function archiveLead () {
Modal . confirm ({
title : 'این لید بایگانی شود؟' ,
okText : 'بایگانی' ,
cancelText : 'انصراف' ,
okButtonProps : { danger : true },
async onOk () {
2026-09-27 17:05:11 +03:30
busy . value = true ;
error . value = '' ;
try {
await api . post ( '/leads/archive' , { leadIds : [ id . value ] });
okMsg . value = 'لید بایگانی شد.' ;
2026-10-18 19:39:59 +03:30
message . success ( okMsg . value );
2026-09-27 17:05:11 +03:30
void router . push ({ name : 'leads' });
} catch ( e ) {
error . value = apiError ( e , 'بایگانی ناموفق بود.' );
} finally {
busy . value = false ;
}
2026-10-18 19:39:59 +03:30
},
});
2026-09-27 17:05:11 +03:30
}
2026-10-13 14:45:29 +03:30
const telHref = computed (() => ( lead . value ? . mobile ? `tel: ${ lead . value . mobile } ` : undefined ));
2026-10-18 19:48:16 +03:30
function openTel () {
if ( telHref . value ) globalThis . location . href = telHref . value ;
}
2026-10-13 14:45:29 +03:30
const feed = computed (() => {
const rows : Array < { key : string ; title : string ; by : string ; at : string } > = [];
for ( const ev of lead . value ? . events ?? []) {
rows . push ({
key : `ev- ${ ev . id } ` ,
title : ev . title ,
by : ev . body ? . trim () || 'توسط سیستم' ,
at : ev . createdAt ,
});
}
for ( const a of activities . value ) {
const who = a . createdBy ? . fullName || a . assignee ? . fullName ;
rows . push ({
key : `act- ${ a . id } ` ,
title : a . title ,
by : who ? `توسط ${ who } ` : 'توسط سیستم' ,
at : a . createdAt ,
});
}
return rows . sort (( a , b ) => + new Date ( b . at ) - + new Date ( a . at )). slice ( 0 , 4 );
});
function feedParts ( iso : string ) {
const d = new Date ( iso );
if ( Number . isNaN ( d . getTime ())) return { day : '—' , time : '' };
return { day : formatJalaliNumeric ( d ), time : formatClockFa ( d ) };
}
async function copyMobile () {
const value = lead . value ? . mobile ;
if ( ! value ) return ;
try {
await navigator . clipboard . writeText ( value );
copied . value = true ;
2026-10-18 19:39:59 +03:30
message . success ( 'کپی شد' );
2026-10-13 14:45:29 +03:30
window . setTimeout (() => {
copied . value = false ;
}, 1400 );
} catch {
copied . value = false ;
}
}
function syncNarrow () {
narrow . value = window . matchMedia ( '(max-width: 900px)' ). matches ;
if ( ! narrow . value ) moreOpen . value = false ;
}
onMounted (() => {
narrowMq = window . matchMedia ( '(max-width: 900px)' );
narrowMq . addEventListener ( 'change' , syncNarrow );
void load ();
});
onBeforeUnmount (() => narrowMq ? . removeEventListener ( 'change' , syncNarrow ));
2026-09-27 17:05:11 +03:30
</ script >
< template >
2026-10-18 19:39:59 +03:30
< a-row : gutter = "[24, 24]" >
< a-col :span = "24" >
< a-flex justify = "space-between" align = "center" wrap = "wrap" :gap = "token.marginSM" >
< a-space :size = "12" align = "start" >
2026-10-25 19:22:30 +03:30
< RouterLink : to = "{ name: 'leads-board' }" custom v-slot = "{ navigate }" >
2026-10-18 19:39:59 +03:30
< a-button @click ="navigate" >
< template # icon >< ArrowLeftOutlined /></ template >
</ a-button >
</ RouterLink >
< div >
< a-typography-title :level = "3" : style = "{ margin: 0 }" >
{{ lead ? . title || 'جزئیات لید' }}
</ a-typography-title >
< a-typography-text type = "secondary" >
{{ lead ? `موبایل ${ lead . mobile } ` : 'در حال بارگذاری…' }}
</ a-typography-text >
</ div >
</ a-space >
< a-space v-if = "lead" :size="8" wrap >
< a-button
v-if = "!editing"
2026-10-13 21:05:17 +03:30
: disabled = "busy || !lead.mobile"
@click ="smsOpen = true"
>
2026-10-18 19:39:59 +03:30
< template # icon >< MessageOutlined /></ template >
2026-10-13 21:05:17 +03:30
پیامک
2026-10-18 19:39:59 +03:30
</ a-button >
2026-10-18 21:33:56 +03:30
< a-button v-if = "!editing && canBehavior" @click="copilotOpen = !copilotOpen" >
کمک برای ادامه مذاکره
</ a -button >
2026-10-18 19:39:59 +03:30
< a-button v-if = "!editing" :disabled="busy" @click="startEdit" >
< template # icon >< EditOutlined /></ template >
2026-10-04 12:01:21 +03:30
ویرایش
2026-10-18 19:39:59 +03:30
</ a-button >
< a-button
v-if = "!lead.convertedPersonId"
type = "primary"
2026-09-27 17:05:11 +03:30
:disabled = "busy"
@click ="convertLead"
>
2026-10-18 19:39:59 +03:30
< template # icon >< UserAddOutlined /></ template >
2026-09-27 17:05:11 +03:30
تبدیل به مشتری
2026-10-18 19:39:59 +03:30
</ a-button >
< a-button
v-if = "!lead.archivedAt"
danger
2026-09-27 17:05:11 +03:30
:disabled = "busy"
@click ="archiveLead"
>
بایگانی
2026-10-18 19:39:59 +03:30
</ a-button >
< a-button v-if = "narrow" @click="moreOpen = true" >
< template # icon >< MoreOutlined /></ template >
بیشتر
</ a-button >
</ a-space >
</ a-flex >
</ a-col >
2026-10-13 14:45:29 +03:30
2026-10-18 21:33:56 +03:30
< a-col v-if = "lead && copilotOpen && canBehavior" :span="24" >
< SalesCopilotPanel
:person-id = "lead.convertedPersonId"
:lead-id = "lead.id"
: customer -name =" lead.contactName || lead.title "
:stage-name = "lead.stageName"
:phone = "lead.mobile"
:sms-endpoint = "`/leads/${lead.id}/sms`"
:sms-history-endpoint = "`/leads/${lead.id}/sms-history`"
/>
</ a-col >
2026-09-27 17:05:11 +03:30
2026-10-18 19:39:59 +03:30
< a-col v-if = "okMsg" :span="24" >
< a-alert type = "success" show -icon :message = "okMsg" closable @close ="okMsg = ''" />
</ a-col >
< a-col v-if = "error" :span="24" >
< a-alert type = "error" show -icon :message = "error" closable @close ="error = ''" />
</ a-col >
2026-09-27 17:05:11 +03:30
2026-10-18 19:39:59 +03:30
< a-col :span = "24" >
< a-spin :spinning = "loading" >
< template v-if = "lead" >
< a-row : gutter = "[24, 24]" >
< a-col :xs = "24" :lg = "17" :xl = "18" >
< a-card :bordered = "false" title = "اطلاعات لید" >
< a-form v-if = "editing" layout="vertical" :model="editForm" >
< a-row : gutter = "[16, 0]" >
< a-col :xs = "24" :md = "12" >
< a-form-item label = "عنوان" required >
< a-input v -model:value = "editForm.title" style = "width: 100%" />
</ a-form-item >
</ a-col >
< a-col :xs = "24" :md = "12" >
< a-form-item label = "مالک/مخاطب" >
< a-input v -model:value = "editForm.contactName" style = "width: 100%" />
</ a-form-item >
</ a-col >
2026-10-31 12:49:42 +03:30
< 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 >
2026-10-18 19:39:59 +03:30
< a-col :xs = "24" :sm = "12" :md = "8" >
< a-form-item label = "موبایل" required >
< a-input
v -model:value = "editForm.mobile"
dir = "ltr"
style = "width: 100%"
/>
</ a-form-item >
</ a-col >
< a-col :xs = "24" :sm = "12" :md = "8" >
< a-form-item label = "تلفن" >
< a-input
v -model:value = "editForm.phone"
dir = "ltr"
style = "width: 100%"
/>
</ a-form-item >
</ a-col >
< a-col :xs = "24" :sm = "12" :md = "8" >
< a-form-item label = "شهر" >
< a-input v -model:value = "editForm.city" style = "width: 100%" />
</ a-form-item >
</ a-col >
< a-col :xs = "24" :sm = "12" :md = "8" >
< a-form-item label = "منطقه" >
< a-input v -model:value = "editForm.region" style = "width: 100%" />
</ a-form-item >
</ a-col >
< a-col :xs = "24" :sm = "12" :md = "8" >
< a-form-item label = "منبع" >
< a-input v -model:value = "editForm.source" style = "width: 100%" />
</ a-form-item >
</ a-col >
< a-col :xs = "24" :sm = "12" :md = "8" >
< a-form-item label = "اینستاگرام" >
< a-input
v -model:value = "editForm.instagram"
dir = "ltr"
style = "width: 100%"
/>
</ a-form-item >
</ a-col >
< a-col :xs = "24" :sm = "12" :md = "8" >
< a-form-item label = "تاریخ تولد" >
< JalaliDateInput
v-model = "editForm.birthDate"
mode = "date"
style = "width: 100%"
/>
</ a-form-item >
</ a-col >
< a-col :xs = "24" :sm = "12" :md = "8" >
< a-form-item label = "صنف" >
< a-input v -model:value = "editForm.trade" style = "width: 100%" />
</ a-form-item >
</ a-col >
< a-col :span = "24" >
< a-form-item label = "یادداشت" >
< a-textarea
v -model:value = "editForm.notes"
:rows = "3"
style = "width: 100%"
/>
</ a-form-item >
</ a-col >
< a-col :span = "24" >
< a-space :size = "8" >
< a-button type = "primary" :loading = "busy" @click ="saveEdit" >
ذخیره
</ a -button >
< a-button :disabled = "busy" @click ="editing = false" > انصراف </ a -button >
</ a-space >
</ a-col >
</ a-row >
</ a-form >
2026-09-27 17:05:11 +03:30
2026-10-18 19:39:59 +03:30
< template v-else >
< a-flex
v-if = "narrow"
justify = "space-between"
align = "center"
wrap = "wrap"
:gap = "8"
: style = "{ marginBottom: `${token.marginMD}px` }"
>
< a-space :size = "8" wrap >
< a-tag color = "processing" >{{ lead . stageName }}</ a-tag >
< a-button type = "link" @click ="copyMobile" >
< template # icon >< CopyOutlined /></ template >
2026-10-13 14:45:29 +03:30
< span dir = "ltr" >{{ toFaDigits ( lead . mobile ) }}</ span >
2026-10-18 19:39:59 +03:30
< template v-if = "copied" > · کپی شد </ template >
</ a -button >
</ a-space >
< a-space :size = "8" >
< a-button
v-if = "telHref"
2026-10-18 19:48:16 +03:30
@click ="openTel"
2026-10-18 19:39:59 +03:30
>
< template # icon >< PhoneOutlined /></ template >
تماس
</ a-button >
< a-button @click ="smsOpen = true" >
< template # icon >< MessageOutlined /></ template >
2026-10-13 21:05:17 +03:30
پیامک
2026-10-18 19:39:59 +03:30
</ a-button >
</ a-space >
</ a-flex >
2026-10-13 14:45:29 +03:30
2026-10-18 19:39:59 +03:30
< a-descriptions : column = "{ xs: 1, sm: 2, md: 2 }" bordered size = "small" >
< a-descriptions-item label = "نام" >{{ lead . title }}</ a-descriptions-item >
< a-descriptions-item label = "مالک/مخاطب" >
{{ lead . contactName || '—' }}
</ a-descriptions-item >
2026-10-31 12:49:42 +03:30
< a-descriptions-item label = "جنسیت" >{{ genderLabel }}</ a-descriptions-item >
2026-10-18 19:39:59 +03:30
< a-descriptions-item label = "موبایل" >
< span dir = "ltr" >{{ lead . mobile }}</ span >
</ a-descriptions-item >
< a-descriptions-item label = "تلفن" >
< span dir = "ltr" >{{ lead . phone || '—' }}</ span >
</ a-descriptions-item >
< a-descriptions-item label = "شهر" >{{ lead . city || '—' }}</ a-descriptions-item >
< a-descriptions-item label = "منطقه" >
{{ lead . region || '—' }}
</ a-descriptions-item >
< a-descriptions-item label = "اینستاگرام" >
< span dir = "ltr" >{{ lead . instagram || '—' }}</ span >
</ a-descriptions-item >
< a-descriptions-item label = "صنف" >{{ lead . trade || '—' }}</ a-descriptions-item >
< a-descriptions-item label = "منبع" >
{{ lead . source || '—' }}
</ a-descriptions-item >
< a-descriptions-item label = "وضعیت" >{{ lead . stageName }}</ a-descriptions-item >
< a-descriptions-item label = "کارشناس" >
{{ lead . assigneeName || '—' }}
</ a-descriptions-item >
< a-descriptions-item label = "تاریخ تولد" >
2026-10-25 20:21:33 +03:30
{{ shownBirthDate }}
2026-10-18 19:39:59 +03:30
</ a-descriptions-item >
< a-descriptions-item label = "تاریخ ثبت" >
{{ formatDate ( lead . createdAt ) }}
</ a-descriptions-item >
< a-descriptions-item label = "آخرین تماس" >
{{ formatDate ( lead . lastContactAt ) }}
</ a-descriptions-item >
< a-descriptions-item label = "پیگیری بعدی" >
{{ formatDate ( lead . nextFollowUpAt ) }}
</ a-descriptions-item >
< a-descriptions-item label = "روز در مرحله" >
{{ toFaDigits ( lead . daysInStage ) }}
</ a-descriptions-item >
< a-descriptions-item
v-if = "lead.addresses?.length"
label = "آدرسها"
:span = "2"
>
< div v-for = "(addr, i) in lead.addresses" :key="i" >{{ addr }}</ div >
</ a -descriptions-item >
2026-10-25 20:21:33 +03:30
< template v-if = "noteView.mode === 'answers'" >
< a-descriptions-item
v-for = "(row, index) in answerRows"
:key = "`${row.label}-${index}`"
:label = "row.label"
:span = "2"
: label -style =" { whiteSpace : ' normal ' } "
: content -style =" { whiteSpace : ' normal ' } "
>
< a-space v-if = "row.file" direction="vertical" :size="4" >
< a-image
v-if = "row.file.dataUrl && leadNoteFileIsImage(row.file)"
:src = "row.file.dataUrl"
:alt = "row.file.name"
:width = "160"
/>
< a-typography-text >{{ row . file . name }}</ a-typography-text >
</ a-space >
< a-typography-paragraph
v-else
: ellipsis = "{ rows: 4, expandable: true, symbol: 'بیشتر' }"
:content = "row.text"
style = "margin-bottom: 0"
/>
</ a-descriptions-item >
< a-descriptions-item
v-if = "noteView.prose"
label = "یادداشت"
:span = "2"
>
< a-typography-paragraph
: ellipsis = "{ rows: 4, expandable: true, symbol: 'بیشتر' }"
:content = "noteView.prose"
style = "margin-bottom: 0"
/>
</ a-descriptions-item >
</ template >
< a-descriptions-item v-else-if = "lead.notes" label="یادداشت" :span="2" >
2026-10-25 19:22:30 +03:30
< a-typography-paragraph
: ellipsis = "{ rows: 4, expandable: true, symbol: 'بیشتر' }"
:content = "lead.notes"
style = "margin-bottom: 0"
/>
2026-10-18 19:39:59 +03:30
</ a-descriptions-item >
</ a-descriptions >
</ template >
2026-10-13 14:45:29 +03:30
2026-10-18 19:39:59 +03:30
< template v-if = "!editing" >
< a-divider orientation = "right" > فعالیت های مرتبط </ a-divider >
< a-list
v-if = "activities.length"
:data-source = "activities"
item -layout =" horizontal "
>
< template # renderItem = "{ item }" >
< a-list-item >
< a-list-item-meta >
< template # title >
< RouterLink
: to = "{ name: 'activity-detail', params: { id: item.id } }"
>
{{ item . title }}
</ RouterLink >
</ template >
< template # description >
2026-10-18 19:48:16 +03:30
{{ item . typeDef ? . name || ACTIVITY_TYPE_LABELS [ item . type as keyof typeof ACTIVITY_TYPE_LABELS ] }}
2026-10-18 19:39:59 +03:30
· {{ item . assignee ? . fullName || 'بدون مسئول' }}
· {{ formatDate ( item . dueAt ) }}
·
{{
item . status === 'done'
? 'انجام شد'
: item . status === 'canceled'
? 'لغو شده'
: 'باز'
}}
</ template >
</ a-list-item-meta >
</ a-list-item >
</ template >
</ a-list >
< a-empty v-else description = "فعالیت مرتبطی نیست." />
< a-divider orientation = "right" > جدول زمانی </ a-divider >
< a-timeline v-if = "lead.events.length" >
< a-timeline-item v-for = "ev in lead.events" :key="ev.id" >
< a-typography-text strong >{{ ev . title }}</ a-typography-text >
< div >
< a-typography-text type = "secondary" >
{{ formatDate ( ev . createdAt ) }}
</ a-typography-text >
</ div >
< a-typography-paragraph v-if = "ev.body" type="secondary" >
{{ ev.body }}
</ a -typography-paragraph >
</ a-timeline-item >
</ a-timeline >
< a-empty
v-else
description = "رویدادی ثبت نشده. تماس، یادداشت یا پیگیری اضافه کنید."
/>
< a-divider v-if = "narrow" orientation="right" > خلاصه فعالیت </ a -divider >
< a-list v-if = "narrow && feed.length" :data-source="feed" >
< template # renderItem = "{ item }" >
< a-list-item >
< a-list-item-meta :title = "item.title" :description = "item.by" />
< template # actions >
< a-typography-text type = "secondary" >
{{ feedParts ( item . at ). day }}
{{ feedParts ( item . at ). time }}
</ a-typography-text >
</ template >
</ a-list-item >
</ template >
</ a-list >
< RouterLink
v-if = "narrow"
: to = "{ name: 'activities' }"
custom
v-slot = "{ navigate }"
>
< a-button type = "link" @click ="navigate" >
مشاهده همه فعالیتها
</ a -button >
</ RouterLink >
< RouterLink
v-if = "narrow"
: to = "{ name: 'activity-new' }"
custom
v-slot = "{ navigate }"
>
< a-button type = "dashed" block @click ="navigate" >
< template # icon >< PlusOutlined /></ template >
ایجاد فعالیت
</ a-button >
</ RouterLink >
</ template >
</ a-card >
2026-10-13 14:45:29 +03:30
< RelatedTasksPanel
v-if = "!editing"
: entity = "{ type: 'lead', id: lead.id, label: lead.title }"
empty -title =" هنوز کاری برای این لید ثبت نشده. "
empty -hint =" برای پیگیری بهتر ، یک تسک ایجاد کنید. "
/>
2026-10-18 19:39:59 +03:30
</ a-col >
2026-10-13 14:45:29 +03:30
2026-10-18 19:39:59 +03:30
< a-col :xs = "24" :lg = "7" :xl = "6" >
< a-affix v-if = "!narrow" :offset-top="24" >
2026-10-25 19:22:30 +03:30
< a-card
title = "اقدامات سریع"
:bordered = "false"
: body -style =" { maxHeight : ' calc ( 100vh - 140px )', overflowY : ' auto ' } "
>
2026-10-18 19:39:59 +03:30
< a-form layout = "vertical" >
< a-divider orientation = "right" > پیامک </ a-divider >
< a-button
block
:disabled = "!lead.mobile"
@click ="smsOpen = true"
>
< template # icon >< MessageOutlined /></ template >
ارسال پیامک
</ a-button >
2026-10-13 14:45:29 +03:30
2026-10-18 19:39:59 +03:30
< a-divider orientation = "right" > تغییر وضعیت </ a-divider >
< a-form-item label = "وضعیت" >
< a-select
v -model:value = "stageId"
style = "width: 100%"
: options = "stages.map((s) => ({ value: s.id, label: s.name }))"
/>
</ a-form-item >
< a-button block :disabled = "busy" :loading = "busy" @click ="changeStage" >
اعمال وضعیت
</ a -button >
2026-09-27 17:05:11 +03:30
2026-11-03 13:15:21 +03:30
< template v-if = "canAssignOthers" >
2026-10-18 19:39:59 +03:30
< a-divider orientation = "right" > تخصیص کارشناس </ a-divider >
< a-form-item label = "کارشناس" >
< a-select
v -model:value = "assigneeId"
allow -clear
placeholder = "انتخاب…"
style = "width: 100%"
: options = "users.map((u) => ({ value: u.id, label: u.fullName }))"
/>
</ a-form-item >
< a-button
block
: disabled = "busy || !assigneeId"
:loading = "busy"
@click ="assignLead"
>
تخصیص
</ a-button >
</ template >
2026-10-06 19:54:38 +03:30
2026-10-18 19:39:59 +03:30
< a-divider orientation = "right" > ثبت تماس </ a-divider >
< a-form-item label = "نتیجه" >
< a-input
v -model:value = "callOutcome"
placeholder = "مثلاً پاسخ داد / اشغال"
style = "width: 100%"
/>
</ a-form-item >
< a-form-item label = "توضیح" >
< a-textarea
v -model:value = "callBody"
:rows = "4"
placeholder = "خلاصه مکالمه"
style = "width: 100%"
/>
</ a-form-item >
< a-form-item label = "پیگیری بعدی" >
< JalaliDateInput
v-model = "callFollowUp"
mode = "datetime"
style = "width: 100%"
/>
</ a-form-item >
< a-button block :disabled = "busy" :loading = "busy" @click ="logCall" >
ثبت تماس
</ a -button >
2026-10-11 14:12:36 +03:30
2026-10-18 19:39:59 +03:30
< a-divider orientation = "right" > یادداشت </ a-divider >
< a-form-item label = "متن" >
< a-input
v -model:value = "noteBody"
placeholder = "یادداشت…"
style = "width: 100%"
/>
</ a-form-item >
< a-button
block
: disabled = "busy || !noteBody.trim()"
:loading = "busy"
@click ="addNote"
>
ثبت یادداشت
</ a-button >
2026-09-27 17:05:11 +03:30
2026-10-18 19:39:59 +03:30
< a-divider orientation = "right" > پیگیری بعدی </ a-divider >
< a-form-item label = "زمان" >
< JalaliDateInput
v-model = "followUpAt"
mode = "datetime"
style = "width: 100%"
/>
</ a-form-item >
< a-form-item label = "توضیح" >
< a-input
v -model:value = "followUpBody"
placeholder = "اختیاری"
style = "width: 100%"
/>
</ a-form-item >
< a-button
block
type = "primary"
: disabled = "busy || !followUpAt"
:loading = "busy"
@click ="setFollowUp"
>
ذخیره پیگیری
</ a-button >
</ a-form >
< a-divider orientation = "right" > خلاصه </ a-divider >
< a-descriptions :column = "1" size = "small" bordered >
< a-descriptions-item label = "وضعیت" >
{{ lead . stageName }}
</ a-descriptions-item >
< a-descriptions-item label = "کارشناس" >
{{ lead . assigneeName || '—' }}
</ a-descriptions-item >
< a-descriptions-item label = "پیگیری بعدی" >
{{ formatDate ( lead . nextFollowUpAt ) }}
</ a-descriptions-item >
< a-descriptions-item label = "روز در مرحله" >
{{ toFaDigits ( lead . daysInStage ) }}
</ a-descriptions-item >
</ a-descriptions >
</ a-card >
</ a-affix >
</ a-col >
</ a-row >
< a-drawer
v -model:open = "moreOpen"
title = "اقدامات سریع"
placement = "bottom"
:height = "'85vh'"
>
< a-form layout = "vertical" >
< a-divider orientation = "right" > پیامک </ a-divider >
< a-button
block
:disabled = "!lead.mobile"
@click ="smsOpen = true; moreOpen = false"
>
2026-10-13 21:05:17 +03:30
ارسال پیامک
2026-10-18 19:39:59 +03:30
</ a-button >
2026-10-18 21:33:56 +03:30
< a-button
v-if = "canBehavior"
block
@click ="copilotOpen = true; moreOpen = false"
>
کمک برای ادامه مذاکره
</ a-button >
2026-09-27 17:05:11 +03:30
2026-10-18 19:39:59 +03:30
< a-divider orientation = "right" > تغییر وضعیت </ a-divider >
< a-form-item label = "وضعیت" >
< a-select
v -model:value = "stageId"
style = "width: 100%"
: options = "stages.map((s) => ({ value: s.id, label: s.name }))"
/>
</ a-form-item >
< a-button block :disabled = "busy" @click ="changeStage" > اعمال وضعیت </ a -button >
2026-11-03 13:15:21 +03:30
< template v-if = "canAssignOthers" >
2026-10-18 19:39:59 +03:30
< a-divider orientation = "right" > تخصیص کارشناس </ a-divider >
< a-form-item label = "کارشناس" >
< a-select
v -model:value = "assigneeId"
allow -clear
placeholder = "انتخاب…"
style = "width: 100%"
: options = "users.map((u) => ({ value: u.id, label: u.fullName }))"
/>
</ a-form-item >
< a-button
block
: disabled = "busy || !assigneeId"
@click ="assignLead"
>
2026-09-27 17:05:11 +03:30
تخصیص
2026-10-18 19:39:59 +03:30
</ a-button >
</ template >
2026-09-27 17:05:11 +03:30
2026-10-18 19:39:59 +03:30
< a-divider orientation = "right" > ثبت تماس </ a-divider >
< a-form-item label = "نتیجه" >
< a-input
v -model:value = "callOutcome"
placeholder = "مثلاً پاسخ داد / اشغال"
style = "width: 100%"
/>
</ a-form-item >
< a-form-item label = "توضیح" >
< a-textarea
v -model:value = "callBody"
:rows = "4"
placeholder = "خلاصه مکالمه"
style = "width: 100%"
/>
</ a-form-item >
< a-form-item label = "پیگیری بعدی" >
< JalaliDateInput
v-model = "callFollowUp"
mode = "datetime"
style = "width: 100%"
/>
</ a-form-item >
< a-button block :disabled = "busy" @click ="logCall" > ثبت تماس </ a -button >
2026-09-27 17:05:11 +03:30
2026-10-18 19:39:59 +03:30
< a-divider orientation = "right" > یادداشت </ a-divider >
< a-form-item label = "متن" >
< a-input
v -model:value = "noteBody"
placeholder = "یادداشت…"
style = "width: 100%"
/>
</ a-form-item >
< a-button
block
: disabled = "busy || !noteBody.trim()"
@click ="addNote"
>
2026-09-27 17:05:11 +03:30
ثبت یادداشت
2026-10-18 19:39:59 +03:30
</ a-button >
2026-09-27 17:05:11 +03:30
2026-10-18 19:39:59 +03:30
< a-divider orientation = "right" > پیگیری بعدی </ a-divider >
< a-form-item label = "زمان" >
< JalaliDateInput
v-model = "followUpAt"
mode = "datetime"
style = "width: 100%"
/>
</ a-form-item >
< a-form-item label = "توضیح" >
< a-input
v -model:value = "followUpBody"
placeholder = "اختیاری"
style = "width: 100%"
/>
</ a-form-item >
< a-button
block
type = "primary"
: disabled = "busy || !followUpAt"
@click ="setFollowUp"
>
2026-09-27 17:05:11 +03:30
ذخیره پیگیری
2026-10-18 19:39:59 +03:30
</ a-button >
</ a-form >
</ a-drawer >
2026-10-13 21:05:17 +03:30
< SmsComposeModal
:open = "smsOpen"
title = "ارسال پیامک"
:endpoint = "`/leads/${lead.id}/sms`"
2026-10-17 21:23:07 +03:30
:history-endpoint = "`/leads/${lead.id}/sms-history`"
2026-10-13 21:05:17 +03:30
: name = "lead.contactName || lead.title"
: phone = "lead.mobile || lead.phone || ''"
:record-title = "lead.title"
: company = "lead.contactName ? lead.title : ''"
@close ="smsOpen = false"
/>
2026-09-27 17:05:11 +03:30
</ template >
2026-10-18 19:39:59 +03:30
</ a-spin >
</ a-col >
</ a-row >
2026-09-27 17:05:11 +03:30
</ template >