club home redesign; deal multi-select delete; anchored card menus
This commit is contained in:
@@ -15,6 +15,8 @@ const props = withDefaults(
|
||||
accent: PipelineAccent;
|
||||
dragging?: boolean;
|
||||
copy?: PipelineUiCopy;
|
||||
selectMode?: boolean;
|
||||
selected?: boolean;
|
||||
}>(),
|
||||
{ copy: () => DEAL_BOARD_COPY },
|
||||
);
|
||||
@@ -24,6 +26,8 @@ const emit = defineEmits<{
|
||||
dragEnd: [];
|
||||
open: [dealId: string];
|
||||
menu: [dealId: string];
|
||||
toggleSelect: [dealId: string];
|
||||
bulkDelete: [];
|
||||
}>();
|
||||
|
||||
const showAmount = computed(
|
||||
@@ -42,30 +46,60 @@ const avatarSrc = computed(() =>
|
||||
const extraAssignees = computed(() =>
|
||||
Math.max(0, (props.deal.assigneeNames?.length ?? 0) - 1),
|
||||
);
|
||||
const showTrash = computed(() => props.selectMode && props.selected);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="deal-card"
|
||||
:class="{ dragging, 'no-amount': !showAmount }"
|
||||
:class="{
|
||||
dragging,
|
||||
'no-amount': !showAmount,
|
||||
'select-mode': selectMode,
|
||||
selected,
|
||||
}"
|
||||
:data-card-id="deal.id"
|
||||
draggable="true"
|
||||
:draggable="!selectMode"
|
||||
role="listitem"
|
||||
:aria-grabbed="dragging ? 'true' : 'false'"
|
||||
:style="{
|
||||
'--badge-bg': colors.soft,
|
||||
'--badge-fg': colors.text,
|
||||
}"
|
||||
@dragstart="emit('dragStart', deal, $event)"
|
||||
@dragstart="!selectMode && emit('dragStart', deal, $event)"
|
||||
@dragend="emit('dragEnd')"
|
||||
@dblclick="emit('open', deal.id)"
|
||||
@dblclick="!selectMode && emit('open', deal.id)"
|
||||
@click="selectMode && emit('toggleSelect', deal.id)"
|
||||
>
|
||||
<span class="badge" aria-hidden="true">{{ deal.initials }}</span>
|
||||
<button
|
||||
v-if="selectMode"
|
||||
type="button"
|
||||
class="tick"
|
||||
:class="{ on: selected }"
|
||||
:aria-label="selected ? 'لغو انتخاب' : 'انتخاب معامله'"
|
||||
:aria-pressed="selected"
|
||||
@click.stop="emit('toggleSelect', deal.id)"
|
||||
>
|
||||
<UiIcon v-if="selected" name="check" :size="14" />
|
||||
</button>
|
||||
<span v-else class="badge" aria-hidden="true">{{ deal.initials }}</span>
|
||||
<div class="titles">
|
||||
<h3 @click.stop="emit('open', deal.id)">{{ deal.title }}</h3>
|
||||
<h3 @click.stop="selectMode ? emit('toggleSelect', deal.id) : emit('open', deal.id)">
|
||||
{{ deal.title }}
|
||||
</h3>
|
||||
<p v-if="deal.description" class="desc">{{ deal.description }}</p>
|
||||
</div>
|
||||
<button
|
||||
v-if="showTrash"
|
||||
type="button"
|
||||
class="menu trash"
|
||||
aria-label="حذف انتخابشدهها"
|
||||
@click.stop="emit('bulkDelete')"
|
||||
>
|
||||
<UiIcon name="trash" :size="16" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="menu"
|
||||
:aria-label="copy.itemMenuAria"
|
||||
@@ -116,11 +150,21 @@ const extraAssignees = computed(() =>
|
||||
box-shadow 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
transform 0.18s ease,
|
||||
opacity 0.18s ease;
|
||||
opacity 0.18s ease,
|
||||
background 0.18s ease;
|
||||
text-align: right;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.deal-card.select-mode {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.deal-card.selected {
|
||||
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
|
||||
background: color-mix(in srgb, var(--accent) 6%, #fff);
|
||||
}
|
||||
|
||||
.deal-card:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 28%, var(--line));
|
||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.07);
|
||||
@@ -130,6 +174,10 @@ const extraAssignees = computed(() =>
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.deal-card.select-mode:active {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.deal-card.dragging {
|
||||
opacity: 0.95;
|
||||
transform: scale(1.02);
|
||||
@@ -147,7 +195,8 @@ const extraAssignees = computed(() =>
|
||||
}
|
||||
|
||||
.badge,
|
||||
.avatar {
|
||||
.avatar,
|
||||
.tick {
|
||||
width: var(--face);
|
||||
height: var(--face);
|
||||
max-width: var(--face);
|
||||
@@ -165,6 +214,21 @@ const extraAssignees = computed(() =>
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tick {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 2px solid #cbd5e1;
|
||||
background: #fff;
|
||||
color: #fff;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tick.on {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--line);
|
||||
@@ -239,6 +303,15 @@ const extraAssignees = computed(() =>
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.menu.trash {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.menu.trash:hover,
|
||||
.menu.trash:focus-visible {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -12,8 +12,10 @@ withDefaults(
|
||||
dropStageId: string | null;
|
||||
canManage?: boolean;
|
||||
copy?: PipelineUiCopy;
|
||||
selectMode?: boolean;
|
||||
selectedIds?: Set<string>;
|
||||
}>(),
|
||||
{ copy: () => DEAL_BOARD_COPY },
|
||||
{ copy: () => DEAL_BOARD_COPY, selectedIds: () => new Set() },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -24,6 +26,8 @@ const emit = defineEmits<{
|
||||
addDeal: [stageId: string];
|
||||
openDeal: [dealId: string];
|
||||
menuDeal: [dealId: string];
|
||||
toggleSelect: [dealId: string];
|
||||
bulkDelete: [];
|
||||
renameStage: [stageId: string, title: string];
|
||||
deleteStage: [stageId: string];
|
||||
moveStage: [stageId: string, dir: -1 | 1];
|
||||
@@ -69,6 +73,8 @@ function submitAdd() {
|
||||
:drop-active="dropStageId === stage.id"
|
||||
:can-manage="canManage"
|
||||
:copy="copy"
|
||||
:select-mode="selectMode"
|
||||
:selected-ids="selectedIds"
|
||||
:is-first="index === 0"
|
||||
:is-last="index === stages.length - 1"
|
||||
@drag-start="(d, e) => emit('dragStart', d, e)"
|
||||
@@ -78,6 +84,8 @@ function submitAdd() {
|
||||
@add-deal="emit('addDeal', $event)"
|
||||
@open-deal="emit('openDeal', $event)"
|
||||
@menu-deal="emit('menuDeal', $event)"
|
||||
@toggle-select="emit('toggleSelect', $event)"
|
||||
@bulk-delete="emit('bulkDelete')"
|
||||
@rename="(id, title) => emit('renameStage', id, title)"
|
||||
@delete="emit('deleteStage', $event)"
|
||||
@move="(id, dir) => emit('moveStage', id, dir)"
|
||||
|
||||
@@ -15,8 +15,10 @@ const props = withDefaults(
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
copy?: PipelineUiCopy;
|
||||
selectMode?: boolean;
|
||||
selectedIds?: Set<string>;
|
||||
}>(),
|
||||
{ copy: () => DEAL_BOARD_COPY },
|
||||
{ copy: () => DEAL_BOARD_COPY, selectedIds: () => new Set() },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -27,6 +29,8 @@ const emit = defineEmits<{
|
||||
addDeal: [stageId: string];
|
||||
openDeal: [dealId: string];
|
||||
menuDeal: [dealId: string];
|
||||
toggleSelect: [dealId: string];
|
||||
bulkDelete: [];
|
||||
rename: [stageId: string, title: string];
|
||||
delete: [stageId: string];
|
||||
move: [stageId: string, dir: -1 | 1];
|
||||
@@ -220,10 +224,14 @@ onBeforeUnmount(() => {
|
||||
:accent="stage.color"
|
||||
:dragging="draggingId === deal.id"
|
||||
:copy="copy"
|
||||
:select-mode="selectMode"
|
||||
:selected="selectedIds.has(deal.id)"
|
||||
@drag-start="(d, e) => emit('dragStart', d, e)"
|
||||
@drag-end="emit('dragEnd')"
|
||||
@open="emit('openDeal', $event)"
|
||||
@menu="emit('menuDeal', $event)"
|
||||
@toggle-select="emit('toggleSelect', $event)"
|
||||
@bulk-delete="emit('bulkDelete')"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="!stage.deals.length" class="empty">{{ copy.empty }}</p>
|
||||
|
||||
@@ -13,13 +13,17 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
stages: PipelineStage[];
|
||||
copy?: PipelineUiCopy;
|
||||
selectMode?: boolean;
|
||||
selectedIds?: Set<string>;
|
||||
}>(),
|
||||
{ copy: () => DEAL_BOARD_COPY },
|
||||
{ copy: () => DEAL_BOARD_COPY, selectedIds: () => new Set() },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
openDeal: [dealId: string];
|
||||
menuDeal: [dealId: string];
|
||||
toggleSelect: [dealId: string];
|
||||
bulkDelete: [];
|
||||
}>();
|
||||
|
||||
const rows = computed(() =>
|
||||
@@ -51,6 +55,7 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
<table v-table-cols="tableKey" class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-if="selectMode" class="tick-col"></th>
|
||||
<th>{{ copy.listColTitle }}</th>
|
||||
<th>مرحله</th>
|
||||
<th v-if="!hideAmount">مبلغ</th>
|
||||
@@ -59,12 +64,34 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in rows" :key="row.deal.id">
|
||||
<tr
|
||||
v-for="row in rows"
|
||||
:key="row.deal.id"
|
||||
:data-card-id="row.deal.id"
|
||||
:class="{ selected: selectedIds.has(row.deal.id) }"
|
||||
@click="selectMode && emit('toggleSelect', row.deal.id)"
|
||||
>
|
||||
<td v-if="selectMode" class="tick-col" @click.stop>
|
||||
<button
|
||||
type="button"
|
||||
class="tick"
|
||||
:class="{ on: selectedIds.has(row.deal.id) }"
|
||||
:aria-label="selectedIds.has(row.deal.id) ? 'لغو انتخاب' : 'انتخاب معامله'"
|
||||
:aria-pressed="selectedIds.has(row.deal.id)"
|
||||
@click="emit('toggleSelect', row.deal.id)"
|
||||
>
|
||||
<UiIcon v-if="selectedIds.has(row.deal.id)" name="check" :size="14" />
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="identity"
|
||||
@click="emit('openDeal', row.deal.id)"
|
||||
@click.stop="
|
||||
selectMode
|
||||
? emit('toggleSelect', row.deal.id)
|
||||
: emit('openDeal', row.deal.id)
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="badge"
|
||||
@@ -103,8 +130,18 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
{{ row.deal.date }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="menu-cell">
|
||||
<td class="menu-cell" @click.stop>
|
||||
<button
|
||||
v-if="selectMode && selectedIds.has(row.deal.id)"
|
||||
type="button"
|
||||
class="menu trash"
|
||||
aria-label="حذف انتخابشدهها"
|
||||
@click="emit('bulkDelete')"
|
||||
>
|
||||
<UiIcon name="trash" :size="18" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="menu"
|
||||
:aria-label="copy.itemMenuAria"
|
||||
@@ -123,25 +160,62 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
v-for="row in rows"
|
||||
:key="row.deal.id"
|
||||
class="card"
|
||||
@dblclick="emit('openDeal', row.deal.id)"
|
||||
:class="{ selected: selectedIds.has(row.deal.id) }"
|
||||
:data-card-id="row.deal.id"
|
||||
@dblclick="!selectMode && emit('openDeal', row.deal.id)"
|
||||
@click="selectMode && emit('toggleSelect', row.deal.id)"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="identity"
|
||||
@click="emit('openDeal', row.deal.id)"
|
||||
>
|
||||
<span
|
||||
class="badge"
|
||||
:style="{
|
||||
background: PIPELINE_ACCENT[row.accent].soft,
|
||||
color: PIPELINE_ACCENT[row.accent].text,
|
||||
}"
|
||||
>{{ row.deal.initials }}</span>
|
||||
<span class="titles">
|
||||
<strong>{{ row.deal.title }}</strong>
|
||||
<em v-if="row.deal.description">{{ row.deal.description }}</em>
|
||||
</span>
|
||||
</button>
|
||||
<div class="card-top">
|
||||
<button
|
||||
v-if="selectMode"
|
||||
type="button"
|
||||
class="tick"
|
||||
:class="{ on: selectedIds.has(row.deal.id) }"
|
||||
:aria-label="selectedIds.has(row.deal.id) ? 'لغو انتخاب' : 'انتخاب معامله'"
|
||||
@click.stop="emit('toggleSelect', row.deal.id)"
|
||||
>
|
||||
<UiIcon v-if="selectedIds.has(row.deal.id)" name="check" :size="14" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="identity"
|
||||
@click.stop="
|
||||
selectMode
|
||||
? emit('toggleSelect', row.deal.id)
|
||||
: emit('openDeal', row.deal.id)
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="badge"
|
||||
:style="{
|
||||
background: PIPELINE_ACCENT[row.accent].soft,
|
||||
color: PIPELINE_ACCENT[row.accent].text,
|
||||
}"
|
||||
>{{ row.deal.initials }}</span>
|
||||
<span class="titles">
|
||||
<strong>{{ row.deal.title }}</strong>
|
||||
<em v-if="row.deal.description">{{ row.deal.description }}</em>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="selectMode && selectedIds.has(row.deal.id)"
|
||||
type="button"
|
||||
class="menu trash"
|
||||
aria-label="حذف انتخابشدهها"
|
||||
@click.stop="emit('bulkDelete')"
|
||||
>
|
||||
<UiIcon name="trash" :size="18" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="menu"
|
||||
:aria-label="copy.itemMenuAria"
|
||||
@click.stop="emit('menuDeal', row.deal.id)"
|
||||
>
|
||||
<UiIcon name="dots-v" :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
class="stage"
|
||||
:style="{
|
||||
@@ -216,6 +290,34 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.data-table tbody tr.selected td {
|
||||
background: color-mix(in srgb, var(--accent) 7%, #fff);
|
||||
}
|
||||
|
||||
.tick-col {
|
||||
width: 2.6rem;
|
||||
padding-inline: 0.55rem !important;
|
||||
}
|
||||
|
||||
.tick {
|
||||
width: 1.45rem;
|
||||
height: 1.45rem;
|
||||
border-radius: 6px;
|
||||
border: 2px solid #cbd5e1;
|
||||
background: #fff;
|
||||
color: #fff;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.tick.on {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -329,6 +431,16 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.menu.trash {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.menu.trash:hover,
|
||||
.menu.trash:focus-visible {
|
||||
color: var(--danger);
|
||||
background: color-mix(in srgb, var(--danger) 10%, #fff);
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: none;
|
||||
gap: 0.7rem;
|
||||
@@ -344,6 +456,22 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.card.selected {
|
||||
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
|
||||
background: color-mix(in srgb, var(--accent) 6%, #fff);
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-top .identity {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-foot {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
export function cardMenuStyle(
|
||||
cardId: string,
|
||||
width = 160,
|
||||
height = 190,
|
||||
height = 230,
|
||||
): Record<string, string> | null {
|
||||
const btn = document.querySelector<HTMLElement>(
|
||||
`[data-card-id="${CSS.escape(cardId)}"] .menu`,
|
||||
@@ -21,7 +21,7 @@ export function bindMenuDismiss(isOpen: () => boolean, close: () => void): () =>
|
||||
const onPointer = (e: PointerEvent) => {
|
||||
if (!isOpen()) return;
|
||||
const t = e.target as Element | null;
|
||||
if (t?.closest('.menu-pop, .deal-card .menu')) return;
|
||||
if (t?.closest('.menu-pop, [data-card-id] .menu')) return;
|
||||
close();
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
|
||||
@@ -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