club home redesign; deal multi-select delete; anchored card menus

This commit is contained in:
2026-10-12 17:59:07 +03:30
parent a2b01f787e
commit faac26d07d
6 changed files with 373 additions and 50 deletions
+122 -16
View File
@@ -3,7 +3,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { api } from '@/lib/api';
import { formatJalaliNumeric } from '@/lib/jalali';
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
import PipelineToolbar from '@/components/pipeline/PipelineToolbar.vue';
import type { PipelineViewMode } from '@/components/pipeline/PipelineToolbar.vue';
import PipelineBoard from '@/components/pipeline/PipelineBoard.vue';
@@ -407,32 +407,79 @@ async function confirmDeleteStage() {
}
}
const deleteDealId = ref<string | null>(null);
const deleteDealIds = ref<string[]>([]);
const dealDeleting = ref(false);
const selectMode = ref(false);
const selectedIds = ref<Set<string>>(new Set());
const selectedCount = computed(() => selectedIds.value.size);
const deleteDealTitle = computed(() => {
const id = deleteDealId.value;
const ids = deleteDealIds.value;
if (ids.length !== 1) return '';
for (const s of stages.value) {
const d = s.deals.find((x) => x.id === id);
const d = s.deals.find((x) => x.id === ids[0]);
if (d) return d.title;
}
return '';
});
const deleteConfirmCopy = computed(() => {
const n = deleteDealIds.value.length;
if (n <= 0) return '';
if (n === 1) {
return `معاملهٔ «${deleteDealTitle.value}» حذف شود؟ این کار قابل بازگشت نیست.`;
}
return `${toFaDigits(n)} معامله حذف شود؟ این کار قابل بازگشت نیست.`;
});
function askDeleteDeal(dealId: string) {
menuOpenId.value = null;
deleteDealId.value = dealId;
deleteDealIds.value = [dealId];
}
function startSelect(dealId: string) {
menuOpenId.value = null;
selectMode.value = true;
selectedIds.value = new Set([dealId]);
}
function toggleSelect(dealId: string) {
const next = new Set(selectedIds.value);
if (next.has(dealId)) next.delete(dealId);
else next.add(dealId);
selectedIds.value = next;
if (!next.size) selectMode.value = false;
}
function clearSelect() {
selectMode.value = false;
selectedIds.value = new Set();
}
function askBulkDelete() {
if (!selectedIds.value.size) return;
menuOpenId.value = null;
deleteDealIds.value = [...selectedIds.value];
}
async function confirmDeleteDeal() {
const id = deleteDealId.value;
if (!id || dealDeleting.value) return;
const ids = deleteDealIds.value;
if (!ids.length || dealDeleting.value) return;
dealDeleting.value = true;
error.value = '';
try {
await api.delete(`/deals/${id}`);
deleteDealId.value = null;
const results = await Promise.allSettled(ids.map((id) => api.delete(`/deals/${id}`)));
const failed = results.filter((r) => r.status === 'rejected').length;
deleteDealIds.value = [];
clearSelect();
await load();
if (failed) {
error.value =
failed === ids.length
? 'حذف معامله‌ها ناموفق بود.'
: `${toFaDigits(failed)} مورد حذف نشد.`;
}
} catch (e) {
error.value = apiMessage(e, 'حذف معامله ناموفق بود.');
} finally {
@@ -591,11 +638,28 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
<p v-if="importMsg" class="ok">{{ importMsg }}</p>
<p v-else-if="loading && !stages.length" class="muted">در حال بارگذاری مسیر فروش…</p>
<div v-if="selectMode" class="select-bar">
<span>{{ toFaDigits(selectedCount) }} معامله انتخاب شده</span>
<button
v-if="selectedCount"
type="button"
class="select-del"
@click="askBulkDelete"
>
حذف انتخاب‌شده‌ها
</button>
<button type="button" class="select-cancel" @click="clearSelect">انصراف از انتخاب</button>
</div>
<PipelineList
v-if="viewMode === 'list' && (!loading || stages.length)"
:stages="visibleStages"
:select-mode="selectMode"
:selected-ids="selectedIds"
@open-deal="onOpenDeal"
@menu-deal="onMenuDeal"
@toggle-select="toggleSelect"
@bulk-delete="askBulkDelete"
/>
<PipelineBoard
@@ -604,6 +668,8 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
:dragging-id="draggingId"
:drop-stage-id="dropStageId"
:can-manage="canCreatePipeline"
:select-mode="selectMode"
:selected-ids="selectedIds"
@drag-start="onDragStart"
@drag-end="onDragEnd"
@drag-over="onDragOver"
@@ -611,6 +677,8 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
@add-deal="onAddDeal"
@open-deal="onOpenDeal"
@menu-deal="onMenuDeal"
@toggle-select="toggleSelect"
@bulk-delete="askBulkDelete"
@rename-stage="onRenameStage"
@delete-stage="onDeleteStage"
@move-stage="onMoveStage"
@@ -630,6 +698,9 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
</Teleport>
<div v-if="menuOpenId" class="menu-pop" role="menu" :style="menuStyle ?? undefined">
<button type="button" role="menuitem" @click="startSelect(menuOpenId)">
انتخاب
</button>
<button type="button" role="menuitem" @click="onOpenDeal(menuOpenId)">
مشاهده معامله
</button>
@@ -701,18 +772,16 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
</UiModal>
<UiModal
:open="Boolean(deleteDealId)"
title="حذف معامله"
@close="deleteDealId = null"
:open="deleteDealIds.length > 0"
:title="deleteDealIds.length > 1 ? 'حذف معامله‌ها' : 'حذف معامله'"
@close="deleteDealIds = []"
>
<p class="delete-copy">
معاملهٔ «{{ deleteDealTitle }}» حذف شود؟ این کار قابل بازگشت نیست.
</p>
<p class="delete-copy">{{ deleteConfirmCopy }}</p>
<template #footer>
<UiButton type="button" :disabled="dealDeleting" @click="confirmDeleteDeal">
{{ dealDeleting ? 'در حال حذف…' : 'حذف' }}
</UiButton>
<UiButton variant="ghost" type="button" @click="deleteDealId = null">
<UiButton variant="ghost" type="button" @click="deleteDealIds = []">
انصراف
</UiButton>
</template>
@@ -778,6 +847,43 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
color: var(--ink);
}
.select-bar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.65rem 1rem;
padding: 0.55rem 0.85rem;
border-radius: var(--radius);
background: color-mix(in srgb, var(--accent) 10%, #fff);
border: 1px solid color-mix(in srgb, var(--accent) 25%, var(--line));
font-size: 0.86rem;
font-weight: 700;
color: var(--ink);
}
.select-bar span {
flex: 1;
min-width: 8rem;
}
.select-bar button {
border: 0;
background: none;
padding: 0;
font: inherit;
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
}
.select-bar .select-del {
color: var(--danger);
}
.select-bar .select-cancel {
color: var(--accent);
}
.menu-pop {
position: fixed;
inset-inline-start: 1rem;