Files
CRM_Montzar/apps/web/src/views/LeadDetailView.vue
T

1078 lines
38 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { RouterLink, useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import {
Affix as AAffix,
Alert as AAlert,
Button as AButton,
Card as ACard,
Col as ACol,
Descriptions as ADescriptions,
DescriptionsItem as ADescriptionsItem,
Divider as ADivider,
Drawer as ADrawer,
Empty as AEmpty,
Flex as AFlex,
Form as AForm,
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';
import { api } from '@/lib/api';
import {
formatClockFa,
formatJalaliNumeric,
formatLocalDateInput,
parseLocalInput,
toFaDigits,
} from '@/lib/jalali';
import { useAuthStore } from '@/stores/auth';
import { canManageSettings } from '@/lib/roles';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import RelatedTasksPanel from '@/components/routines/RelatedTasksPanel.vue';
import SmsComposeModal from '@/components/SmsComposeModal.vue';
import type {
ActivityDto,
ConvertLeadResult,
LeadDto,
LeadEventDto,
LeadStageDto,
OrgUserDto,
PaginatedResponse,
} from '@mento/shared';
import { ACTIVITY_TYPE_LABELS } from '@mento/shared';
import { canUseBehavior } from '@/lib/behavior-access';
import SalesCopilotPanel from '@/components/behavior/SalesCopilotPanel.vue';
const { Title: ATypographyTitle, Text: ATypographyText, Paragraph: ATypographyParagraph } =
Typography;
const { token } = theme.useToken();
type LeadDetail = LeadDto & { events: LeadEventDto[] };
const route = useRoute();
const router = useRouter();
const auth = useAuthStore();
const isAdmin = computed(() => canManageSettings(auth.user?.role));
const canBehavior = computed(() => canUseBehavior(auth.user));
const copilotOpen = ref(false);
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[]>([]);
const activities = ref<ActivityDto[]>([]);
const stageId = ref('');
const assigneeId = ref('');
const callOutcome = ref('');
const callBody = ref('');
const callFollowUp = ref('');
const noteBody = ref('');
const followUpAt = ref('');
const followUpBody = ref('');
const editing = ref(false);
const moreOpen = ref(false);
const smsOpen = ref(false);
const copied = ref(false);
const narrow = ref(
typeof window !== 'undefined' && window.matchMedia('(max-width: 900px)').matches,
);
let narrowMq: MediaQueryList | null = null;
const editForm = ref({
title: '',
contactName: '',
mobile: '',
phone: '',
city: '',
region: '',
source: '',
instagram: '',
birthDate: '',
trade: '',
notes: '',
});
function formatDate(value: string | null | undefined) {
if (!value) return '—';
try {
return formatJalaliNumeric(new Date(value));
} catch {
return value;
}
}
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 {
const [leadRes, stagesRes, usersRes, actRes] = await Promise.all([
api.get<LeadDetail>(`/leads/${id.value}`),
api.get<LeadStageDto[]>('/leads/stages'),
api.get<OrgUserDto[]>('/users').catch(() => ({ data: [] as OrgUserDto[] })),
api
.get<PaginatedResponse<ActivityDto>>('/activities', {
params: { filter: 'all', leadId: id.value, limit: 30 },
})
.catch(() => null),
]);
lead.value = leadRes.data;
activities.value = actRes?.data.items ?? [];
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;
}
}
function startEdit() {
if (!lead.value) return;
editing.value = true;
moreOpen.value = false;
editForm.value = {
title: lead.value.title,
contactName: lead.value.contactName ?? '',
mobile: lead.value.mobile,
phone: lead.value.phone ?? '',
city: lead.value.city ?? '',
region: lead.value.region ?? '',
source: lead.value.source ?? '',
instagram: lead.value.instagram ?? '',
birthDate: lead.value.birthDate
? formatLocalDateInput(new Date(lead.value.birthDate))
: '',
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,
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,
birthDate: editForm.value.birthDate || null,
trade: editForm.value.trade.trim() || null,
notes: editForm.value.notes.trim() || null,
});
okMsg.value = 'لید ویرایش شد.';
message.success(okMsg.value);
editing.value = false;
await load();
} catch (e) {
error.value = apiError(e, 'ویرایش لید ناموفق بود.');
} finally {
busy.value = false;
}
}
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 = 'وضعیت به‌روز شد.';
message.success(okMsg.value);
moreOpen.value = false;
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 = 'کارشناس تخصیص داده شد.';
message.success(okMsg.value);
moreOpen.value = false;
await load();
} catch (e) {
error.value = apiError(e, 'تخصیص ناموفق بود.');
} finally {
busy.value = false;
}
}
function localToIso(value: string): string | undefined {
const parsed = parseLocalInput(value);
if (!parsed || Number.isNaN(parsed.getTime())) return undefined;
return parsed.toISOString();
}
async function logCall() {
busy.value = true;
error.value = '';
try {
await api.post(`/leads/${id.value}/call`, {
outcome: callOutcome.value || undefined,
body: callBody.value || undefined,
nextFollowUpAt: callFollowUp.value ? localToIso(callFollowUp.value) : undefined,
});
callOutcome.value = '';
callBody.value = '';
callFollowUp.value = '';
okMsg.value = 'تماس ثبت شد.';
message.success(okMsg.value);
moreOpen.value = false;
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 = 'یادداشت ثبت شد.';
message.success(okMsg.value);
moreOpen.value = false;
await load();
} catch (e) {
error.value = apiError(e, 'ثبت یادداشت ناموفق بود.');
} finally {
busy.value = false;
}
}
async function setFollowUp() {
const nextFollowUpAt = localToIso(followUpAt.value);
if (!nextFollowUpAt) return;
busy.value = true;
error.value = '';
try {
await api.post(`/leads/${id.value}/follow-up`, {
nextFollowUpAt,
body: followUpBody.value || undefined,
});
followUpAt.value = '';
followUpBody.value = '';
okMsg.value = 'پیگیری تنظیم شد.';
message.success(okMsg.value);
moreOpen.value = false;
await load();
} catch (e) {
error.value = apiError(e, 'تنظیم پیگیری ناموفق بود.');
} finally {
busy.value = false;
}
}
function convertLead() {
Modal.confirm({
title: 'این لید به مشتری تبدیل شود؟',
okText: 'تبدیل',
cancelText: 'انصراف',
async onOk() {
busy.value = true;
error.value = '';
try {
const { data } = await api.post<ConvertLeadResult>(`/leads/${id.value}/convert`, {
createIfMissing: true,
});
okMsg.value = data.createdPerson
? 'لید تبدیل و شخص جدید ساخته شد.'
: 'لید به مشتری موجود متصل شد.';
message.success(okMsg.value);
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;
}
},
});
}
function archiveLead() {
Modal.confirm({
title: 'این لید بایگانی شود؟',
okText: 'بایگانی',
cancelText: 'انصراف',
okButtonProps: { danger: true },
async onOk() {
busy.value = true;
error.value = '';
try {
await api.post('/leads/archive', { leadIds: [id.value] });
okMsg.value = 'لید بایگانی شد.';
message.success(okMsg.value);
void router.push({ name: 'leads' });
} catch (e) {
error.value = apiError(e, 'بایگانی ناموفق بود.');
} finally {
busy.value = false;
}
},
});
}
const telHref = computed(() => (lead.value?.mobile ? `tel:${lead.value.mobile}` : undefined));
function openTel() {
if (telHref.value) globalThis.location.href = telHref.value;
}
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;
message.success('کپی شد');
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));
</script>
<template>
<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">
<RouterLink :to="{ name: 'leads' }" custom v-slot="{ navigate }">
<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"
:disabled="busy || !lead.mobile"
@click="smsOpen = true"
>
<template #icon><MessageOutlined /></template>
پیامک
</a-button>
<a-button v-if="!editing && canBehavior" @click="copilotOpen = !copilotOpen">
کمک برای ادامه مذاکره
</a-button>
<a-button v-if="!editing" :disabled="busy" @click="startEdit">
<template #icon><EditOutlined /></template>
ویرایش
</a-button>
<a-button
v-if="!lead.convertedPersonId"
type="primary"
:disabled="busy"
@click="convertLead"
>
<template #icon><UserAddOutlined /></template>
تبدیل به مشتری
</a-button>
<a-button
v-if="!lead.archivedAt"
danger
:disabled="busy"
@click="archiveLead"
>
بایگانی
</a-button>
<a-button v-if="narrow" @click="moreOpen = true">
<template #icon><MoreOutlined /></template>
بیشتر
</a-button>
</a-space>
</a-flex>
</a-col>
<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>
<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>
<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>
<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>
<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>
<span dir="ltr">{{ toFaDigits(lead.mobile) }}</span>
<template v-if="copied"> · کپی شد</template>
</a-button>
</a-space>
<a-space :size="8">
<a-button
v-if="telHref"
@click="openTel"
>
<template #icon><PhoneOutlined /></template>
تماس
</a-button>
<a-button @click="smsOpen = true">
<template #icon><MessageOutlined /></template>
پیامک
</a-button>
</a-space>
</a-flex>
<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>
<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="تاریخ تولد">
{{ formatDate(lead.birthDate) }}
</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>
<a-descriptions-item v-if="lead.notes" label="یادداشت" :span="2">
{{ lead.notes }}
</a-descriptions-item>
</a-descriptions>
</template>
<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>
{{ item.typeDef?.name || ACTIVITY_TYPE_LABELS[item.type as keyof typeof ACTIVITY_TYPE_LABELS] }}
· {{ 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>
<RelatedTasksPanel
v-if="!editing"
:entity="{ type: 'lead', id: lead.id, label: lead.title }"
empty-title="هنوز کاری برای این لید ثبت نشده."
empty-hint="برای پیگیری بهتر، یک تسک ایجاد کنید."
/>
</a-col>
<a-col :xs="24" :lg="7" :xl="6">
<a-affix v-if="!narrow" :offset-top="24">
<a-card title="اقدامات سریع" :bordered="false">
<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>
<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>
<template v-if="isAdmin">
<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>
<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>
<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>
<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"
>
ارسال پیامک
</a-button>
<a-button
v-if="canBehavior"
block
@click="copilotOpen = true; moreOpen = false"
>
کمک برای ادامه مذاکره
</a-button>
<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>
<template v-if="isAdmin">
<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"
>
تخصیص
</a-button>
</template>
<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>
<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"
>
ثبت یادداشت
</a-button>
<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"
>
ذخیره پیگیری
</a-button>
</a-form>
</a-drawer>
<SmsComposeModal
:open="smsOpen"
title="ارسال پیامک"
:endpoint="`/leads/${lead.id}/sms`"
:history-endpoint="`/leads/${lead.id}/sms-history`"
:name="lead.contactName || lead.title"
:phone="lead.mobile || lead.phone || ''"
:record-title="lead.title"
:company="lead.contactName ? lead.title : ''"
@close="smsOpen = false"
/>
</template>
</a-spin>
</a-col>
</a-row>
</template>