Fix Leads row menu overflow and add edit/delete actions.

Use a teleported dropdown so the menu stays above the table, with edit modal and archive delete.
This commit is contained in:
2026-09-27 20:18:44 +03:30
parent 9edbb92b13
commit 3833788aa8
+104 -34
View File
@@ -11,6 +11,7 @@ import { useAuthStore } from '@/stores/auth';
import { canManageSettings } from '@/lib/roles';
import {
UiButton,
UiDropdownMenu,
UiEmptyState,
UiModal,
UiPageHeader,
@@ -69,6 +70,7 @@ const importJob = ref<LeadImportJobDto | null>(null);
const createOpen = ref(false);
const createBusy = ref(false);
const createError = ref('');
const editingLeadId = ref<string | null>(null);
const createForm = ref({
title: '',
contactName: '',
@@ -359,7 +361,12 @@ async function load() {
}
}
const createModalTitle = computed(() =>
editingLeadId.value ? 'ویرایش لید' : 'ثبت لید جدید',
);
function openCreate() {
editingLeadId.value = null;
createError.value = '';
createForm.value = {
title: '',
@@ -376,6 +383,42 @@ function openCreate() {
createOpen.value = true;
}
function openEdit(lead: LeadDto) {
rowMenuId.value = null;
editingLeadId.value = lead.id;
createError.value = '';
createForm.value = {
title: lead.title,
contactName: lead.contactName ?? '',
mobile: lead.mobile,
city: lead.city ?? '',
region: lead.region ?? '',
source: lead.source ?? '',
stageId: lead.stageId,
assigneeId: lead.assigneeId ?? '',
};
closePick();
closeSource();
createOpen.value = true;
}
async function deleteLead(lead: LeadDto) {
rowMenuId.value = null;
if (!confirm(`لید «${lead.title}» حذف (بایگانی) شود؟`)) return;
busy.value = true;
error.value = '';
try {
await api.post('/leads/archive', { leadIds: [lead.id] });
okMsg.value = 'لید حذف شد.';
selected.value = new Set([...selected.value].filter((id) => id !== lead.id));
await load();
} catch {
error.value = 'حذف لید ناموفق بود.';
} finally {
busy.value = false;
}
}
function applyContactDraft(draft: PhoneContactDraft) {
if (draft.mobile) {
createForm.value.mobile = draft.mobile;
@@ -490,26 +533,36 @@ async function submitCreate() {
return;
}
createBusy.value = true;
const payload = {
title: createForm.value.title.trim(),
mobile: createForm.value.mobile.trim(),
contactName: createForm.value.contactName.trim() || null,
city: createForm.value.city.trim() || null,
region: createForm.value.region.trim() || null,
source: createForm.value.source.trim() || null,
stageId: createForm.value.stageId || undefined,
assigneeId: createForm.value.assigneeId || null,
};
try {
const { data: created } = await api.post<LeadDto>('/leads', {
title: createForm.value.title.trim(),
mobile: createForm.value.mobile.trim(),
contactName: createForm.value.contactName.trim() || null,
city: createForm.value.city.trim() || null,
region: createForm.value.region.trim() || null,
source: createForm.value.source.trim() || null,
stageId: createForm.value.stageId || undefined,
assigneeId: createForm.value.assigneeId || null,
});
createOpen.value = false;
okMsg.value = 'لید جدید ثبت شد.';
await load();
void router.push({ name: 'lead-detail', params: { id: created.id } });
if (editingLeadId.value) {
await api.patch(`/leads/${editingLeadId.value}`, payload);
createOpen.value = false;
okMsg.value = 'لید به‌روز شد.';
await load();
} else {
const { data: created } = await api.post<LeadDto>('/leads', payload);
createOpen.value = false;
okMsg.value = 'لید جدید ثبت شد.';
await load();
void router.push({ name: 'lead-detail', params: { id: created.id } });
}
} catch (e) {
createError.value =
axios.isAxiosError(e) && (e.response?.data as { message?: string })?.message
? String((e.response?.data as { message?: string }).message)
: 'ثبت لید ناموفق بود.';
: editingLeadId.value
? 'ویرایش لید ناموفق بود.'
: 'ثبت لید ناموفق بود.';
} finally {
createBusy.value = false;
}
@@ -977,24 +1030,28 @@ onBeforeUnmount(stopImportPoll);
</td>
<td class="actions-col" @click.stop>
<div class="row-actions">
<div class="pop">
<button
type="button"
class="act-btn"
title="بیشتر"
@click="rowMenuId = rowMenuId === lead.id ? null : lead.id"
>
<UiIcon name="dots-v" :size="16" />
<UiDropdownMenu
:open="rowMenuId === lead.id"
align="end"
@update:open="(v) => (rowMenuId = v ? lead.id : null)"
>
<template #trigger="{ toggle }">
<button
type="button"
class="act-btn"
title="بیشتر"
@click.stop="toggle"
>
<UiIcon name="dots-v" :size="16" />
</button>
</template>
<button type="button" @click="goDetail(lead.id)">مشاهده</button>
<button type="button" @click="openEdit(lead)">ویرایش</button>
<a :href="`tel:${lead.mobile}`">تماس</a>
<button type="button" class="danger-item" @click="deleteLead(lead)">
حذف
</button>
<ul v-if="rowMenuId === lead.id" class="menu row-menu">
<li>
<button type="button" @click="goDetail(lead.id)">مشاهده</button>
</li>
<li>
<a :href="`tel:${lead.mobile}`">تماس</a>
</li>
</ul>
</div>
</UiDropdownMenu>
<button
type="button"
class="act-btn call"
@@ -1076,7 +1133,7 @@ onBeforeUnmount(stopImportPoll);
</template>
</UiEmptyState>
<UiModal :open="createOpen" title="ثبت لید جدید" @close="createOpen = false">
<UiModal :open="createOpen" :title="createModalTitle" @close="createOpen = false">
<form class="create-form" @submit.prevent="submitCreate">
<label>
نام / شرکت
@@ -1201,7 +1258,15 @@ onBeforeUnmount(stopImportPoll);
انصراف
</UiButton>
<UiButton variant="primary" :disabled="createBusy" @click="submitCreate">
{{ createBusy ? 'در حال ثبت…' : 'ثبت لید' }}
{{
createBusy
? editingLeadId
? 'در حال ذخیره…'
: 'در حال ثبت…'
: editingLeadId
? 'ذخیره تغییرات'
: 'ثبت لید'
}}
</UiButton>
</template>
</UiModal>
@@ -1766,6 +1831,11 @@ onBeforeUnmount(stopImportPoll);
background: #d1fae5;
}
:deep(.ui-dd-panel) .danger-item,
.danger-item {
color: #b91c1c !important;
}
.pager {
display: flex;
flex-wrap: wrap;