feat(web): migrate CRM screens to Ant Design Vue with central theme
Adopt ant-design-vue and a shared antdTheme for leads, pipeline, invoices, and shared form controls under strict single-page governance. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,12 +17,28 @@ import {
|
||||
updateDealStage,
|
||||
} from '@/lib/pipeline-board';
|
||||
import ExcelImportFlow from '@/components/ExcelImportFlow.vue';
|
||||
import UiBusyOverlay from '@/components/ui/UiBusyOverlay.vue';
|
||||
import UiButton from '@/components/ui/UiButton.vue';
|
||||
import UiInput from '@/components/ui/UiInput.vue';
|
||||
import UiModal from '@/components/ui/UiModal.vue';
|
||||
import DealFormView from '@/views/DealFormView.vue';
|
||||
import SavedFiltersBar from '@/components/SavedFiltersBar.vue';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
UploadOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import {
|
||||
Alert as AAlert,
|
||||
Button as AButton,
|
||||
Flex as AFlex,
|
||||
Form as AForm,
|
||||
FormItem as AFormItem,
|
||||
Input as AInput,
|
||||
Menu as AMenu,
|
||||
MenuItem as AMenuItem,
|
||||
Modal,
|
||||
Modal as AModal,
|
||||
Space as ASpace,
|
||||
Spin as ASpin,
|
||||
theme,
|
||||
} from 'ant-design-vue';
|
||||
import type { ImportContactsResult, PipelineBoardDto, PipelineDto } from '@mento/shared';
|
||||
import {
|
||||
DEAL_EXCEL_ALIASES,
|
||||
@@ -30,6 +46,9 @@ import {
|
||||
EXCEL_CUSTOM_FIELD_PREFIX,
|
||||
brand,
|
||||
} from '@mento/shared';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { canManageSettings } from '@/lib/roles';
|
||||
import { bindMenuDismiss, cardMenuStyle } from '@/lib/anchored-menu';
|
||||
|
||||
const dealExcelFields = DEAL_EXCEL_COLUMNS.filter((c) => c.key !== '_type').map((c) => ({
|
||||
key: c.key,
|
||||
@@ -40,9 +59,24 @@ const dealClientReadOptions = {
|
||||
rawValues: true,
|
||||
customFieldPrefix: EXCEL_CUSTOM_FIELD_PREFIX,
|
||||
};
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { canManageSettings } from '@/lib/roles';
|
||||
import { bindMenuDismiss, cardMenuStyle } from '@/lib/anchored-menu';
|
||||
|
||||
const { token } = theme.useToken();
|
||||
const GAP = 16;
|
||||
|
||||
/** پدینگ بیرونی فقط از `.content` شل = ۱۶px */
|
||||
const pageStyle = {
|
||||
boxSizing: 'border-box' as const,
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: `${GAP}px`,
|
||||
flex: '1 1 auto',
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
};
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -62,7 +96,6 @@ const amountMax = ref('');
|
||||
const showCreate = ref(false);
|
||||
const newName = ref('');
|
||||
const dealModal = ref<{ dealId?: string; stageId?: string } | null>(null);
|
||||
const deleteStageId = ref<string | null>(null);
|
||||
const stageBusy = ref(false);
|
||||
const busy = ref(false);
|
||||
const importMsg = ref('');
|
||||
@@ -383,59 +416,47 @@ async function onAddStage(name: string) {
|
||||
}
|
||||
|
||||
function onDeleteStage(stageId: string) {
|
||||
deleteStageId.value = stageId;
|
||||
const title = stages.value.find((s) => s.id === stageId)?.title ?? '';
|
||||
Modal.confirm({
|
||||
title: 'حذف ستون',
|
||||
content: `ستون «${title}» حذف شود؟ اگر معاملهای داخل آن باشد باید اول آنها را جابهجا کنید.`,
|
||||
okText: 'حذف',
|
||||
cancelText: 'انصراف',
|
||||
okButtonProps: { danger: true },
|
||||
async onOk() {
|
||||
if (!boardPipelineId.value || stageBusy.value) return;
|
||||
stageBusy.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
await api.delete(`/pipelines/${boardPipelineId.value}/stages/${stageId}`);
|
||||
await load();
|
||||
} catch (e) {
|
||||
error.value = apiMessage(e, 'حذف ستون ناموفق بود.');
|
||||
throw e;
|
||||
} finally {
|
||||
stageBusy.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const deleteStageTitle = computed(() => {
|
||||
const id = deleteStageId.value;
|
||||
return stages.value.find((s) => s.id === id)?.title ?? '';
|
||||
});
|
||||
|
||||
async function confirmDeleteStage() {
|
||||
const stageId = deleteStageId.value;
|
||||
if (!stageId || !boardPipelineId.value || stageBusy.value) return;
|
||||
stageBusy.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
await api.delete(`/pipelines/${boardPipelineId.value}/stages/${stageId}`);
|
||||
deleteStageId.value = null;
|
||||
await load();
|
||||
} catch (e) {
|
||||
error.value = apiMessage(e, 'حذف ستون ناموفق بود.');
|
||||
} finally {
|
||||
stageBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
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 ids = deleteDealIds.value;
|
||||
if (ids.length !== 1) return '';
|
||||
function dealTitleById(dealId: string) {
|
||||
for (const s of stages.value) {
|
||||
const d = s.deals.find((x) => x.id === ids[0]);
|
||||
const d = s.deals.find((x) => x.id === dealId);
|
||||
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;
|
||||
deleteDealIds.value = [dealId];
|
||||
confirmDeleteDeals([dealId]);
|
||||
}
|
||||
|
||||
function startSelect(dealId: string) {
|
||||
@@ -460,31 +481,43 @@ function clearSelect() {
|
||||
function askBulkDelete() {
|
||||
if (!selectedIds.value.size) return;
|
||||
menuOpenId.value = null;
|
||||
deleteDealIds.value = [...selectedIds.value];
|
||||
confirmDeleteDeals([...selectedIds.value]);
|
||||
}
|
||||
|
||||
async function confirmDeleteDeal() {
|
||||
const ids = deleteDealIds.value;
|
||||
function confirmDeleteDeals(ids: string[]) {
|
||||
if (!ids.length || dealDeleting.value) return;
|
||||
dealDeleting.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
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 {
|
||||
dealDeleting.value = false;
|
||||
}
|
||||
const copy =
|
||||
ids.length === 1
|
||||
? `معاملهٔ «${dealTitleById(ids[0]!)}» حذف شود؟ این کار قابل بازگشت نیست.`
|
||||
: `${toFaDigits(ids.length)} معامله حذف شود؟ این کار قابل بازگشت نیست.`;
|
||||
Modal.confirm({
|
||||
title: ids.length > 1 ? 'حذف معاملهها' : 'حذف معامله',
|
||||
content: copy,
|
||||
okText: 'حذف',
|
||||
cancelText: 'انصراف',
|
||||
okButtonProps: { danger: true },
|
||||
async onOk() {
|
||||
dealDeleting.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const results = await Promise.allSettled(ids.map((id) => api.delete(`/deals/${id}`)));
|
||||
const failed = results.filter((r) => r.status === 'rejected').length;
|
||||
clearSelect();
|
||||
await load();
|
||||
if (failed) {
|
||||
error.value =
|
||||
failed === ids.length
|
||||
? 'حذف معاملهها ناموفق بود.'
|
||||
: `${toFaDigits(failed)} مورد حذف نشد.`;
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = apiMessage(e, 'حذف معامله ناموفق بود.');
|
||||
throw e;
|
||||
} finally {
|
||||
dealDeleting.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function onDealSaved() {
|
||||
@@ -593,6 +626,19 @@ function onMenuDeal(dealId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function onDealMenuClick({ key }: { key: string | number }) {
|
||||
const id = menuOpenId.value;
|
||||
if (!id) return;
|
||||
if (key === 'select') startSelect(id);
|
||||
else if (key === 'view') onOpenDeal(id);
|
||||
else if (key === 'edit') {
|
||||
dealModal.value = { dealId: id };
|
||||
menuOpenId.value = null;
|
||||
} else if (key === 'promote') void promotePriorDeal(id);
|
||||
else if (key === 'delete') askDeleteDeal(id);
|
||||
else menuOpenId.value = null;
|
||||
}
|
||||
|
||||
const unbindMenuDismiss = bindMenuDismiss(
|
||||
() => menuOpenId.value != null,
|
||||
() => (menuOpenId.value = null),
|
||||
@@ -627,102 +673,141 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page" dir="rtl">
|
||||
<UiBusyOverlay :open="pageBusy" :message="busyMessage" />
|
||||
<PipelineToolbar
|
||||
pipeline-name="مسیر فروش"
|
||||
:pipeline-options="pipelineOptions"
|
||||
v-model:selected-pipeline-id="selectedPipelineId"
|
||||
v-model:view-mode="viewMode"
|
||||
v-model:search="search"
|
||||
v-model:amount-min="amountMin"
|
||||
v-model:amount-max="amountMax"
|
||||
:can-create="canCreatePipeline"
|
||||
@create="showCreate = true"
|
||||
>
|
||||
<template #excel>
|
||||
<button class="btn-ghost-sm" type="button" :disabled="loading || busy" @click="exportExcel">
|
||||
خروجی اکسل
|
||||
</button>
|
||||
<ExcelImportFlow
|
||||
preview-path="/deals/import/preview"
|
||||
import-path="/deals/import"
|
||||
rows-import-path="/deals/import/rows"
|
||||
:client-aliases="DEAL_EXCEL_ALIASES"
|
||||
:client-fields="dealExcelFields"
|
||||
:client-read-options="dealClientReadOptions"
|
||||
:disabled="loading || busy"
|
||||
@start="onImportStart"
|
||||
@done="onImportDone"
|
||||
@fail="onImportFail"
|
||||
@settled="onImportSettled"
|
||||
>
|
||||
<button class="btn-ghost-sm" type="button" :disabled="loading || busy">
|
||||
ورود از اکسل
|
||||
</button>
|
||||
</ExcelImportFlow>
|
||||
</template>
|
||||
</PipelineToolbar>
|
||||
|
||||
<SavedFiltersBar
|
||||
scope="deals"
|
||||
:current-filters="dealsSavedFilters"
|
||||
@apply="applyDealsSavedFilters"
|
||||
/>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<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"
|
||||
<div class="pipeline-page" dir="rtl" :style="pageStyle">
|
||||
<a-flex vertical :gap="GAP" :style="{ flexShrink: 0 }">
|
||||
<PipelineToolbar
|
||||
pipeline-name="مسیر فروش"
|
||||
:pipeline-options="pipelineOptions"
|
||||
v-model:selected-pipeline-id="selectedPipelineId"
|
||||
v-model:view-mode="viewMode"
|
||||
v-model:search="search"
|
||||
v-model:amount-min="amountMin"
|
||||
v-model:amount-max="amountMax"
|
||||
:can-create="canCreatePipeline"
|
||||
@create="showCreate = true"
|
||||
>
|
||||
حذف انتخابشدهها
|
||||
</button>
|
||||
<button type="button" class="select-cancel" @click="clearSelect">انصراف از انتخاب</button>
|
||||
<template #excel>
|
||||
<a-button :disabled="loading || busy" @click="exportExcel">
|
||||
<template #icon><DownloadOutlined /></template>
|
||||
خروجی اکسل
|
||||
</a-button>
|
||||
<ExcelImportFlow
|
||||
preview-path="/deals/import/preview"
|
||||
import-path="/deals/import"
|
||||
rows-import-path="/deals/import/rows"
|
||||
:client-aliases="DEAL_EXCEL_ALIASES"
|
||||
:client-fields="dealExcelFields"
|
||||
:client-read-options="dealClientReadOptions"
|
||||
:disabled="loading || busy"
|
||||
@start="onImportStart"
|
||||
@done="onImportDone"
|
||||
@fail="onImportFail"
|
||||
@settled="onImportSettled"
|
||||
>
|
||||
<a-button :disabled="loading || busy">
|
||||
<template #icon><UploadOutlined /></template>
|
||||
ورود از اکسل
|
||||
</a-button>
|
||||
</ExcelImportFlow>
|
||||
</template>
|
||||
<template #filters>
|
||||
<SavedFiltersBar
|
||||
compact
|
||||
scope="deals"
|
||||
:current-filters="dealsSavedFilters"
|
||||
@apply="applyDealsSavedFilters"
|
||||
/>
|
||||
</template>
|
||||
</PipelineToolbar>
|
||||
|
||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||
<a-alert v-if="importMsg" type="success" show-icon :message="importMsg" />
|
||||
<a-spin v-if="loading && !stages.length" tip="در حال بارگذاری مسیر فروش…" />
|
||||
|
||||
<a-alert
|
||||
v-if="selectMode"
|
||||
type="info"
|
||||
show-icon
|
||||
:message="`${toFaDigits(selectedCount)} معامله انتخاب شده`"
|
||||
>
|
||||
<template #action>
|
||||
<a-space>
|
||||
<a-button
|
||||
v-if="selectedCount"
|
||||
type="primary"
|
||||
danger
|
||||
@click="askBulkDelete"
|
||||
>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
حذف انتخابشدهها
|
||||
</a-button>
|
||||
<a-button @click="clearSelect">انصراف از انتخاب</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-alert>
|
||||
</a-flex>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'relative',
|
||||
flex: '1 1 auto',
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="pageBusy"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: token.colorBgMask,
|
||||
}"
|
||||
>
|
||||
<a-spin :tip="busyMessage" />
|
||||
</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
|
||||
v-else-if="viewMode === 'kanban' && stages.length"
|
||||
:stages="visibleStages"
|
||||
: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"
|
||||
@drop="onDrop"
|
||||
@add-deal="onAddDeal"
|
||||
@open-deal="onOpenDeal"
|
||||
@menu-deal="onMenuDeal"
|
||||
@toggle-select="toggleSelect"
|
||||
@bulk-delete="askBulkDelete"
|
||||
@rename-stage="onRenameStage"
|
||||
@delete-stage="onDeleteStage"
|
||||
@move-stage="onMoveStage"
|
||||
@column-drag-start="onColumnDragStart"
|
||||
@column-drag-end="onColumnDragEnd"
|
||||
@add-stage="onAddStage"
|
||||
/>
|
||||
</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
|
||||
v-else-if="viewMode === 'kanban' && stages.length"
|
||||
:stages="visibleStages"
|
||||
: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"
|
||||
@drop="onDrop"
|
||||
@add-deal="onAddDeal"
|
||||
@open-deal="onOpenDeal"
|
||||
@menu-deal="onMenuDeal"
|
||||
@toggle-select="toggleSelect"
|
||||
@bulk-delete="askBulkDelete"
|
||||
@rename-stage="onRenameStage"
|
||||
@delete-stage="onDeleteStage"
|
||||
@move-stage="onMoveStage"
|
||||
@column-drag-start="onColumnDragStart"
|
||||
@column-drag-end="onColumnDragEnd"
|
||||
@add-stage="onAddStage"
|
||||
/>
|
||||
|
||||
<Teleport to="body">
|
||||
<DealCardOverlay
|
||||
v-if="overlayDeal && draggingId"
|
||||
@@ -734,109 +819,62 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
|
||||
</Teleport>
|
||||
|
||||
<Teleport to="body">
|
||||
<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>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
@click="dealModal = { dealId: menuOpenId ?? undefined }; menuOpenId = null"
|
||||
<div
|
||||
v-if="menuOpenId"
|
||||
:style="{
|
||||
position: 'fixed',
|
||||
zIndex: 1080,
|
||||
...(menuStyle ?? {}),
|
||||
}"
|
||||
>
|
||||
<a-menu
|
||||
:style="{ minWidth: '160px', boxShadow: token.boxShadowSecondary }"
|
||||
@click="onDealMenuClick"
|
||||
>
|
||||
ویرایش
|
||||
</button>
|
||||
<button
|
||||
v-if="canPromotePrior(findDeal(menuOpenId))"
|
||||
type="button"
|
||||
role="menuitem"
|
||||
@click="promotePriorDeal(menuOpenId)"
|
||||
>
|
||||
انتقال به کار جاری
|
||||
</button>
|
||||
<button type="button" role="menuitem" class="danger" @click="askDeleteDeal(menuOpenId)">
|
||||
حذف
|
||||
</button>
|
||||
<button type="button" role="menuitem" @click="menuOpenId = null">بستن</button>
|
||||
<a-menu-item key="select">انتخاب</a-menu-item>
|
||||
<a-menu-item key="view">مشاهده معامله</a-menu-item>
|
||||
<a-menu-item key="edit">ویرایش</a-menu-item>
|
||||
<a-menu-item
|
||||
v-if="canPromotePrior(findDeal(menuOpenId))"
|
||||
key="promote"
|
||||
>
|
||||
انتقال به کار جاری
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delete" danger>حذف</a-menu-item>
|
||||
<a-menu-item key="close">بستن</a-menu-item>
|
||||
</a-menu>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<UiModal
|
||||
<a-modal
|
||||
:open="showCreate && canCreatePipeline"
|
||||
title="مسیر فروش جدید"
|
||||
@close="showCreate = false"
|
||||
:confirm-loading="saving"
|
||||
ok-text="ساخت"
|
||||
cancel-text="انصراف"
|
||||
:ok-button-props="{ disabled: newName.trim().length < 2 }"
|
||||
destroy-on-close
|
||||
@ok="createPipeline"
|
||||
@cancel="showCreate = false"
|
||||
>
|
||||
<form class="create-form" @submit.prevent="createPipeline">
|
||||
<label class="field">
|
||||
<span>نام مسیر فروش</span>
|
||||
<UiInput
|
||||
v-model="newName"
|
||||
<a-form layout="vertical" @finish="createPipeline">
|
||||
<a-form-item label="نام مسیر فروش" required>
|
||||
<a-input
|
||||
v-model:value="newName"
|
||||
placeholder="نام مسیر فروش جدید…"
|
||||
:maxlength="120"
|
||||
required
|
||||
:minlength="2"
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
<template #footer>
|
||||
<UiButton
|
||||
type="button"
|
||||
:disabled="saving || newName.trim().length < 2"
|
||||
@click="createPipeline"
|
||||
>
|
||||
{{ saving ? 'در حال ساخت…' : 'ساخت' }}
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" type="button" @click="showCreate = false">
|
||||
انصراف
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiModal>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<UiModal
|
||||
:open="Boolean(deleteStageId)"
|
||||
title="حذف ستون"
|
||||
@close="deleteStageId = null"
|
||||
>
|
||||
<p class="delete-copy">
|
||||
ستون «{{ deleteStageTitle }}» حذف شود؟ اگر معاملهای داخل آن باشد باید
|
||||
اول آنها را جابهجا کنید.
|
||||
</p>
|
||||
<template #footer>
|
||||
<UiButton
|
||||
type="button"
|
||||
:disabled="stageBusy"
|
||||
@click="confirmDeleteStage"
|
||||
>
|
||||
{{ stageBusy ? 'در حال حذف…' : 'حذف' }}
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" type="button" @click="deleteStageId = null">
|
||||
انصراف
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiModal>
|
||||
|
||||
<UiModal
|
||||
:open="deleteDealIds.length > 0"
|
||||
:title="deleteDealIds.length > 1 ? 'حذف معاملهها' : 'حذف معامله'"
|
||||
@close="deleteDealIds = []"
|
||||
>
|
||||
<p class="delete-copy">{{ deleteConfirmCopy }}</p>
|
||||
<template #footer>
|
||||
<UiButton type="button" :disabled="dealDeleting" @click="confirmDeleteDeal">
|
||||
{{ dealDeleting ? 'در حال حذف…' : 'حذف' }}
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" type="button" @click="deleteDealIds = []">
|
||||
انصراف
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiModal>
|
||||
|
||||
<UiModal
|
||||
<a-modal
|
||||
:open="Boolean(dealModal)"
|
||||
:title="dealModal?.dealId ? 'ویرایش معامله' : 'ایجاد معامله جدید'"
|
||||
@close="dealModal = null"
|
||||
:footer="null"
|
||||
destroy-on-close
|
||||
width="720px"
|
||||
@cancel="dealModal = null"
|
||||
>
|
||||
<DealFormView
|
||||
v-if="dealModal"
|
||||
@@ -847,117 +885,14 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
|
||||
@saved="onDealSaved"
|
||||
@cancel="dealModal = null"
|
||||
/>
|
||||
</UiModal>
|
||||
</section>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
container-type: inline-size;
|
||||
container-name: pipeline;
|
||||
}
|
||||
|
||||
.create-form .field {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.create-form .field span {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--danger);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ok {
|
||||
color: #0f766e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.delete-copy {
|
||||
/* پدینگ بیرونی فقط از `.content` شل = ۱۶px */
|
||||
.pipeline-page {
|
||||
margin: 0;
|
||||
line-height: 1.7;
|
||||
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;
|
||||
z-index: 80;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 0.35rem;
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.menu-pop button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0.45rem 0.85rem;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
color: var(--ink);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.menu-pop button:hover {
|
||||
background: var(--surface-2, #f1f5f9);
|
||||
border-radius: calc(var(--radius) - 4px);
|
||||
}
|
||||
|
||||
.menu-pop button.danger {
|
||||
color: var(--danger);
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user