club home redesign; deal multi-select delete; anchored card menus
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user