2026-09-27 17:05:11 +03:30
|
|
|
|
<script setup lang="ts">
|
2026-10-13 14:45:29 +03:30
|
|
|
|
import { computed, onBeforeUnmount, onMounted, ref } 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,
|
|
|
|
|
|
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';
|
|
|
|
|
|
import { canManageSettings } from '@/lib/roles';
|
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-09-27 17:05:11 +03:30
|
|
|
|
} from '@mento/shared';
|
2026-10-11 14:12:36 +03:30
|
|
|
|
import { ACTIVITY_TYPE_LABELS } 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();
|
|
|
|
|
|
const isAdmin = computed(() => canManageSettings(auth.user?.role));
|
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: '',
|
|
|
|
|
|
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-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-04 12:01:21 +03:30
|
|
|
|
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 ?? '',
|
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,
|
|
|
|
|
|
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>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<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-10-18 19:39:59 +03:30
|
|
|
|
<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>
|
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>
|
|
|
|
|
|
|
|
|
|
|
|
<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"
|
|
|
|
|
|
>
|
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>
|