Files
CRM_Montzar/apps/web/src/components/pipeline/PipelineBoard.vue
T
MahdiandCursor f13a1249af feat(activities): edit detail in place and tighten the board peek
Let people change activity fields and their own notes with photos from the detail page, open full detail from the card title, and toggle the side peek with a chevron.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-10 21:31:21 +03:30

191 lines
5.4 KiB
Vue

<script setup lang="ts">
import { nextTick, ref } from 'vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import {
Button as AButton,
Card as ACard,
Flex as AFlex,
Input as AInput,
Space as ASpace,
} from 'ant-design-vue';
import PipelineColumn from './PipelineColumn.vue';
import type { PipelineDeal, PipelineStage, PipelineUiCopy } from '@/lib/pipeline-board';
import { DEAL_BOARD_COPY } from '@/lib/pipeline-board';
withDefaults(
defineProps<{
stages: PipelineStage[];
draggingId: string | null;
dropStageId: string | null;
canManage?: boolean;
copy?: PipelineUiCopy;
selectMode?: boolean;
selectedIds?: Set<string>;
peekId?: string | null;
previewId?: string | null;
}>(),
{ copy: () => DEAL_BOARD_COPY, selectedIds: () => new Set(), peekId: null, previewId: null },
);
const emit = defineEmits<{
dragStart: [deal: PipelineDeal, event: DragEvent];
dragEnd: [];
dragOver: [stageId: string, event: DragEvent];
drop: [stageId: string, event: DragEvent, index?: number];
addDeal: [stageId: string];
openDeal: [dealId: string];
detailDeal: [dealId: string];
menuDeal: [dealId: string];
toggleSelect: [dealId: string];
bulkDelete: [];
renameStage: [stageId: string, title: string];
deleteStage: [stageId: string];
moveStage: [stageId: string, dir: -1 | 1];
columnDragStart: [stageId: string, event: DragEvent];
columnDragEnd: [];
addStage: [name: string];
loadMore: [stageId: string];
}>();
const adding = ref(false);
const newName = ref('');
const addInput = ref<{ focus: () => void } | null>(null);
const shellStyle = {
boxSizing: 'border-box' as const,
flex: '1 1 auto',
height: 0,
minHeight: 0,
display: 'flex',
flexDirection: 'column' as const,
padding: 0,
minWidth: 0,
position: 'relative' as const,
overflow: 'hidden' as const,
};
const trackStyle = {
boxSizing: 'border-box' as const,
flex: '1 1 auto',
height: 0,
minHeight: 0,
display: 'flex',
flexDirection: 'row' as const,
alignItems: 'stretch' as const,
gap: '16px',
width: '100%',
overflowX: 'auto' as const,
overflowY: 'hidden' as const,
};
async function startAdd() {
adding.value = true;
newName.value = '';
await nextTick();
addInput.value?.focus?.();
}
function cancelAdd() {
adding.value = false;
newName.value = '';
}
function submitAdd() {
const name = newName.value.trim();
if (!name) return;
emit('addStage', name);
adding.value = false;
newName.value = '';
}
</script>
<template>
<div class="pipeline-board-shell" dir="rtl" :style="shellStyle">
<div class="pipeline-board-track" :style="trackStyle">
<PipelineColumn
v-for="(stage, index) in stages"
:key="stage.id"
:stage="stage"
:dragging-id="draggingId"
:drop-active="dropStageId === stage.id"
:can-manage="canManage"
:copy="copy"
:select-mode="selectMode"
:selected-ids="selectedIds"
:peek-id="peekId"
:preview-id="previewId"
:is-first="index === 0"
:is-last="index === stages.length - 1"
@drag-start="(d, e) => emit('dragStart', d, e)"
@drag-end="emit('dragEnd')"
@drag-over="(id, e) => emit('dragOver', id, e)"
@drop="(id, e, index) => emit('drop', id, e, index)"
@add-deal="emit('addDeal', $event)"
@open-deal="emit('openDeal', $event)"
@detail-deal="emit('detailDeal', $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)"
@column-drag-start="(id, e) => emit('columnDragStart', id, e)"
@column-drag-end="emit('columnDragEnd')"
@load-more="emit('loadMore', $event)"
/>
<div
v-if="canManage"
:style="{
flex: '0 0 310px',
width: '310px',
alignSelf: 'stretch',
display: 'flex',
alignItems: 'flex-start',
}"
>
<a-card v-if="adding" :style="{ width: '100%' }" :body-style="{ padding: '12px' }">
<a-flex vertical :gap="12">
<a-input
ref="addInput"
v-model:value="newName"
:maxlength="80"
placeholder="نام ستون جدید…"
aria-label="نام ستون جدید"
@pressEnter="submitAdd"
@keydown.esc.prevent="cancelAdd"
/>
<a-space>
<a-button
type="primary"
:disabled="newName.trim().length < 1"
@click="submitAdd"
>
افزودن
</a-button>
<a-button @click="cancelAdd">انصراف</a-button>
</a-space>
</a-flex>
</a-card>
<a-button v-else type="dashed" block :style="{ minHeight: '52px' }" @click="startAdd">
<template #icon><PlusOutlined /></template>
افزودن ستون
</a-button>
</div>
</div>
</div>
</template>
<style scoped>
.pipeline-board-track {
scrollbar-width: thin;
}
.pipeline-board-track::-webkit-scrollbar {
height: 8px;
}
.pipeline-board-track::-webkit-scrollbar-thumb {
border-radius: 999px;
background: color-mix(in srgb, currentColor 28%, transparent);
}
</style>