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 { canManageSettings } from '@/lib/roles';
import { import {
UiButton, UiButton,
UiDropdownMenu,
UiEmptyState, UiEmptyState,
UiModal, UiModal,
UiPageHeader, UiPageHeader,
@@ -69,6 +70,7 @@ const importJob = ref<LeadImportJobDto | null>(null);
const createOpen = ref(false); const createOpen = ref(false);
const createBusy = ref(false); const createBusy = ref(false);
const createError = ref(''); const createError = ref('');
const editingLeadId = ref<string | null>(null);
const createForm = ref({ const createForm = ref({
title: '', title: '',
contactName: '', contactName: '',
@@ -359,7 +361,12 @@ async function load() {
} }
} }
const createModalTitle = computed(() =>
editingLeadId.value ? 'ویرایش لید' : 'ثبت لید جدید',
);
function openCreate() { function openCreate() {
editingLeadId.value = null;
createError.value = ''; createError.value = '';
createForm.value = { createForm.value = {
title: '', title: '',
@@ -376,6 +383,42 @@ function openCreate() {
createOpen.value = true; 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) { function applyContactDraft(draft: PhoneContactDraft) {
if (draft.mobile) { if (draft.mobile) {
createForm.value.mobile = draft.mobile; createForm.value.mobile = draft.mobile;
@@ -490,26 +533,36 @@ async function submitCreate() {
return; return;
} }
createBusy.value = true; 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 { try {
const { data: created } = await api.post<LeadDto>('/leads', { if (editingLeadId.value) {
title: createForm.value.title.trim(), await api.patch(`/leads/${editingLeadId.value}`, payload);
mobile: createForm.value.mobile.trim(), createOpen.value = false;
contactName: createForm.value.contactName.trim() || null, okMsg.value = 'لید به‌روز شد.';
city: createForm.value.city.trim() || null, await load();
region: createForm.value.region.trim() || null, } else {
source: createForm.value.source.trim() || null, const { data: created } = await api.post<LeadDto>('/leads', payload);
stageId: createForm.value.stageId || undefined, createOpen.value = false;
assigneeId: createForm.value.assigneeId || null, okMsg.value = 'لید جدید ثبت شد.';
}); await load();
createOpen.value = false; void router.push({ name: 'lead-detail', params: { id: created.id } });
okMsg.value = 'لید جدید ثبت شد.'; }
await load();
void router.push({ name: 'lead-detail', params: { id: created.id } });
} catch (e) { } catch (e) {
createError.value = createError.value =
axios.isAxiosError(e) && (e.response?.data as { message?: string })?.message axios.isAxiosError(e) && (e.response?.data as { message?: string })?.message
? String((e.response?.data as { message?: string }).message) ? String((e.response?.data as { message?: string }).message)
: 'ثبت لید ناموفق بود.'; : editingLeadId.value
? 'ویرایش لید ناموفق بود.'
: 'ثبت لید ناموفق بود.';
} finally { } finally {
createBusy.value = false; createBusy.value = false;
} }
@@ -977,24 +1030,28 @@ onBeforeUnmount(stopImportPoll);
</td> </td>
<td class="actions-col" @click.stop> <td class="actions-col" @click.stop>
<div class="row-actions"> <div class="row-actions">
<div class="pop"> <UiDropdownMenu
<button :open="rowMenuId === lead.id"
type="button" align="end"
class="act-btn" @update:open="(v) => (rowMenuId = v ? lead.id : null)"
title="بیشتر" >
@click="rowMenuId = rowMenuId === lead.id ? null : lead.id" <template #trigger="{ toggle }">
> <button
<UiIcon name="dots-v" :size="16" /> 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> </button>
<ul v-if="rowMenuId === lead.id" class="menu row-menu"> </UiDropdownMenu>
<li>
<button type="button" @click="goDetail(lead.id)">مشاهده</button>
</li>
<li>
<a :href="`tel:${lead.mobile}`">تماس</a>
</li>
</ul>
</div>
<button <button
type="button" type="button"
class="act-btn call" class="act-btn call"
@@ -1076,7 +1133,7 @@ onBeforeUnmount(stopImportPoll);
</template> </template>
</UiEmptyState> </UiEmptyState>
<UiModal :open="createOpen" title="ثبت لید جدید" @close="createOpen = false"> <UiModal :open="createOpen" :title="createModalTitle" @close="createOpen = false">
<form class="create-form" @submit.prevent="submitCreate"> <form class="create-form" @submit.prevent="submitCreate">
<label> <label>
نام / شرکت نام / شرکت
@@ -1201,7 +1258,15 @@ onBeforeUnmount(stopImportPoll);
انصراف انصراف
</UiButton> </UiButton>
<UiButton variant="primary" :disabled="createBusy" @click="submitCreate"> <UiButton variant="primary" :disabled="createBusy" @click="submitCreate">
{{ createBusy ? 'در حال ثبت…' : 'ثبت لید' }} {{
createBusy
? editingLeadId
? 'در حال ذخیره…'
: 'در حال ثبت…'
: editingLeadId
? 'ذخیره تغییرات'
: 'ثبت لید'
}}
</UiButton> </UiButton>
</template> </template>
</UiModal> </UiModal>
@@ -1766,6 +1831,11 @@ onBeforeUnmount(stopImportPoll);
background: #d1fae5; background: #d1fae5;
} }
:deep(.ui-dd-panel) .danger-item,
.danger-item {
color: #b91c1c !important;
}
.pager { .pager {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;