diff --git a/apps/web/src/components/activities/ActivitiesKanban.vue b/apps/web/src/components/activities/ActivitiesKanban.vue index 39d0d68..2129008 100644 --- a/apps/web/src/components/activities/ActivitiesKanban.vue +++ b/apps/web/src/components/activities/ActivitiesKanban.vue @@ -41,7 +41,7 @@ const emit = defineEmits<{ stagesChanged: []; add: [stageId: string]; error: [message: string]; - menuDeal: [activityId: string]; + menuDeal: [activity: ActivityDto]; toggleSelect: [activityId: string]; bulkDelete: []; }>(); @@ -115,6 +115,8 @@ function toCard(item: ActivityDto, stageId: string): PipelineDeal { date: dateText(item), initials: initialsFromTitle(party || item.title), stageId, + createdAt: item.createdAt, + promotedFromPriorAt: item.promotedFromPriorAt ?? null, assigneeNames, ownerId: item.assignee?.id ?? null, assigneeIds: (item.assignees ?? []).map((a) => a.id), @@ -205,6 +207,19 @@ function findStageOf(id: string) { return Object.entries(columns.value).find(([, col]) => col.items.some((i) => i.id === id))?.[0]; } +function findActivity(id: string) { + for (const col of Object.values(columns.value)) { + const item = col.items.find((i) => i.id === id); + if (item) return item; + } + return null; +} + +function onMenuActivity(id: string) { + const item = findActivity(id); + if (item) emit('menuDeal', item); +} + function onDragStart(deal: PipelineDeal, event: DragEvent) { draggingId.value = deal.id; overlayDeal.value = deal; @@ -397,7 +412,7 @@ function openActivity(id: string) { @drop="onDrop" @add-deal="emit('add', $event)" @open-deal="openActivity" - @menu-deal="emit('menuDeal', $event)" + @menu-deal="onMenuActivity" @toggle-select="emit('toggleSelect', $event)" @bulk-delete="emit('bulkDelete')" @rename-stage="onRenameStage" diff --git a/apps/web/src/lib/pipeline-board.ts b/apps/web/src/lib/pipeline-board.ts index c6e3297..93913a1 100644 --- a/apps/web/src/lib/pipeline-board.ts +++ b/apps/web/src/lib/pipeline-board.ts @@ -13,6 +13,8 @@ export type PipelineDeal = { /** شناسه مرحله فعلی — هنگام جابه‌جایی به‌روز می‌شود */ stageId?: string; currencyUnit?: string; + createdAt?: string; + promotedFromPriorAt?: string | null; /** نام مسئولان برای نمایش روی کارت */ assigneeNames?: string[]; ownerId?: string | null; @@ -176,6 +178,7 @@ export function mapBoardToStages( stageId: string; expectedCloseAt: string | null; createdAt: string; + promotedFromPriorAt: string | null; person: { fullName: string } | null; company: { name: string } | null; owner?: { id: string; fullName: string } | null; @@ -225,6 +228,8 @@ export function mapBoardToStages( ), stageId: deal.stageId, currencyUnit: currencyUnitLabel(deal.currency), + createdAt: deal.createdAt, + promotedFromPriorAt: deal.promotedFromPriorAt, assigneeNames, ownerId: deal.owner?.id ?? null, assigneeIds: (deal.assignees ?? []).map((a) => a.id), diff --git a/apps/web/src/views/ActivitiesView.vue b/apps/web/src/views/ActivitiesView.vue index dad8ca4..99e5f57 100644 --- a/apps/web/src/views/ActivitiesView.vue +++ b/apps/web/src/views/ActivitiesView.vue @@ -231,6 +231,7 @@ const activityModal = ref<{ } | null>(null); const menuId = ref(null); const boardMenuId = ref(null); +const boardMenuActivity = ref(null); const boardMenuStyle = ref | null>(null); const deleteActivityIds = ref([]); const deleteSingleTitle = ref(''); @@ -572,6 +573,7 @@ function clearFacets() { async function complete(id: string) { menuId.value = null; boardMenuId.value = null; + boardMenuActivity.value = null; try { await api.patch(`/activities/${id}/complete`); await refreshAll(); @@ -585,6 +587,27 @@ function closePopovers() { menuId.value = null; } +function canPromotePrior(item: ActivityDto) { + const pa = auth.user?.priorArchive; + if (!pa || pa.lens !== 'archive' || !pa.cutoffAt) return false; + if (item.promotedFromPriorAt) return false; + return new Date(item.createdAt).getTime() < new Date(pa.cutoffAt).getTime(); +} + +async function promotePriorActivity(id: string) { + menuId.value = null; + boardMenuId.value = null; + boardMenuActivity.value = null; + error.value = ''; + try { + await api.post(`/activities/${id}/promote-from-prior`); + kanbanReload.value += 1; + await refreshAll(); + } catch { + error.value = 'انتقال فعالیت به کار جاری ناموفق بود.'; + } +} + const deleteConfirmCopy = computed(() => { const n = deleteActivityIds.value.length; if (n <= 0) return ''; @@ -604,6 +627,7 @@ function activityTitle(id: string) { function startSelect(id: string) { menuId.value = null; boardMenuId.value = null; + boardMenuActivity.value = null; selectMode.value = true; selectedIds.value = new Set([id]); } @@ -624,6 +648,7 @@ function clearSelect() { function askDeleteActivity(id: string, title?: string) { menuId.value = null; boardMenuId.value = null; + boardMenuActivity.value = null; deleteSingleTitle.value = title?.trim() || activityTitle(id); deleteActivityIds.value = [id]; } @@ -632,6 +657,7 @@ function askBulkDelete() { if (!selectedIds.value.size) return; menuId.value = null; boardMenuId.value = null; + boardMenuActivity.value = null; deleteSingleTitle.value = ''; deleteActivityIds.value = [...selectedIds.value]; } @@ -659,12 +685,13 @@ async function confirmDeleteActivities() { } } -function onBoardMenu(activityId: string) { - boardMenuId.value = boardMenuId.value === activityId ? null : activityId; +function onBoardMenu(activity: ActivityDto) { + boardMenuId.value = boardMenuId.value === activity.id ? null : activity.id; + boardMenuActivity.value = boardMenuId.value ? activity : null; boardMenuStyle.value = null; if (!boardMenuId.value) return; void nextTick(() => { - boardMenuStyle.value = cardMenuStyle(activityId); + boardMenuStyle.value = cardMenuStyle(activity.id); }); } @@ -672,6 +699,7 @@ const unbindBoardMenuDismiss = bindMenuDismiss( () => boardMenuId.value != null, () => { boardMenuId.value = null; + boardMenuActivity.value = null; }, ); @@ -783,6 +811,7 @@ onMounted(() => { watch(viewMode, () => { clearSelect(); boardMenuId.value = null; + boardMenuActivity.value = null; menuId.value = null; }); @@ -1213,6 +1242,9 @@ onUnmounted(() => { ویرایش + @@ -1297,6 +1329,12 @@ onUnmounted(() => { مشاهده + + ویرایش + + @@ -1348,17 +1386,25 @@ onUnmounted(() => { مشاهده ویرایش + - + diff --git a/apps/web/src/views/PersonProfileView.vue b/apps/web/src/views/PersonProfileView.vue index 2218c44..32b9b80 100644 --- a/apps/web/src/views/PersonProfileView.vue +++ b/apps/web/src/views/PersonProfileView.vue @@ -186,21 +186,17 @@ const auth = useAuthStore(); const priorArchiveOn = computed(() => Boolean(auth.user?.priorArchive)); function isPriorActivity(row: ActivityDto) { - if (row.importSource !== 'didar') return false; - if (row.status === 'open' && row.dueAt && new Date(row.dueAt).getTime() > Date.now()) return false; - return true; + const cutoff = auth.user?.priorArchive?.cutoffAt; + if (!auth.user?.priorArchive || !cutoff) return false; + if (row.promotedFromPriorAt) return false; + return new Date(row.createdAt).getTime() < new Date(cutoff).getTime(); } function isPriorDeal(row: DealDto) { - if (row.importSource !== 'didar') return false; - if ( - row.status === 'open' && - row.expectedCloseAt && - new Date(row.expectedCloseAt).getTime() > Date.now() - ) { - return false; - } - return true; + const cutoff = auth.user?.priorArchive?.cutoffAt; + if (!auth.user?.priorArchive || !cutoff) return false; + if (row.promotedFromPriorAt) return false; + return new Date(row.createdAt).getTime() < new Date(cutoff).getTime(); } const lastCallAt = computed(() => { diff --git a/apps/web/src/views/PipelineBoardView.vue b/apps/web/src/views/PipelineBoardView.vue index b3138a4..fea9e31 100644 --- a/apps/web/src/views/PipelineBoardView.vue +++ b/apps/web/src/views/PipelineBoardView.vue @@ -497,6 +497,33 @@ function onOpenDeal(dealId: string) { void router.push({ name: 'deal-detail', params: { id: dealId } }); } +function findDeal(dealId: string | null) { + if (!dealId) return null; + for (const stage of stages.value) { + const deal = stage.deals.find((d) => d.id === dealId); + if (deal) return deal; + } + return null; +} + +function canPromotePrior(deal: PipelineDeal | null) { + const pa = auth.user?.priorArchive; + if (!deal || !pa || pa.lens !== 'archive' || !pa.cutoffAt || !deal.createdAt) return false; + if (deal.promotedFromPriorAt) return false; + return new Date(deal.createdAt).getTime() < new Date(pa.cutoffAt).getTime(); +} + +async function promotePriorDeal(dealId: string) { + menuOpenId.value = null; + error.value = ''; + try { + await api.post(`/deals/${dealId}/promote-from-prior`); + await load(); + } catch (e) { + error.value = apiMessage(e, 'انتقال معامله به کار جاری ناموفق بود.'); + } +} + const pageBusy = ref(false); const busyMessage = ref('لطفاً صبر کنید…'); @@ -721,6 +748,14 @@ defineExpose({ updateDealStage: applyUpdateDealStage }); > ویرایش +