feat(web): add prior archive promote actions

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-17 17:28:57 +03:30
co-authored by Cursor
parent 5069647f79
commit e86841fc60
5 changed files with 117 additions and 20 deletions
@@ -41,7 +41,7 @@ const emit = defineEmits<{
stagesChanged: []; stagesChanged: [];
add: [stageId: string]; add: [stageId: string];
error: [message: string]; error: [message: string];
menuDeal: [activityId: string]; menuDeal: [activity: ActivityDto];
toggleSelect: [activityId: string]; toggleSelect: [activityId: string];
bulkDelete: []; bulkDelete: [];
}>(); }>();
@@ -115,6 +115,8 @@ function toCard(item: ActivityDto, stageId: string): PipelineDeal {
date: dateText(item), date: dateText(item),
initials: initialsFromTitle(party || item.title), initials: initialsFromTitle(party || item.title),
stageId, stageId,
createdAt: item.createdAt,
promotedFromPriorAt: item.promotedFromPriorAt ?? null,
assigneeNames, assigneeNames,
ownerId: item.assignee?.id ?? null, ownerId: item.assignee?.id ?? null,
assigneeIds: (item.assignees ?? []).map((a) => a.id), 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]; 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) { function onDragStart(deal: PipelineDeal, event: DragEvent) {
draggingId.value = deal.id; draggingId.value = deal.id;
overlayDeal.value = deal; overlayDeal.value = deal;
@@ -397,7 +412,7 @@ function openActivity(id: string) {
@drop="onDrop" @drop="onDrop"
@add-deal="emit('add', $event)" @add-deal="emit('add', $event)"
@open-deal="openActivity" @open-deal="openActivity"
@menu-deal="emit('menuDeal', $event)" @menu-deal="onMenuActivity"
@toggle-select="emit('toggleSelect', $event)" @toggle-select="emit('toggleSelect', $event)"
@bulk-delete="emit('bulkDelete')" @bulk-delete="emit('bulkDelete')"
@rename-stage="onRenameStage" @rename-stage="onRenameStage"
+5
View File
@@ -13,6 +13,8 @@ export type PipelineDeal = {
/** شناسه مرحله فعلی — هنگام جابه‌جایی به‌روز می‌شود */ /** شناسه مرحله فعلی — هنگام جابه‌جایی به‌روز می‌شود */
stageId?: string; stageId?: string;
currencyUnit?: string; currencyUnit?: string;
createdAt?: string;
promotedFromPriorAt?: string | null;
/** نام مسئولان برای نمایش روی کارت */ /** نام مسئولان برای نمایش روی کارت */
assigneeNames?: string[]; assigneeNames?: string[];
ownerId?: string | null; ownerId?: string | null;
@@ -176,6 +178,7 @@ export function mapBoardToStages(
stageId: string; stageId: string;
expectedCloseAt: string | null; expectedCloseAt: string | null;
createdAt: string; createdAt: string;
promotedFromPriorAt: string | null;
person: { fullName: string } | null; person: { fullName: string } | null;
company: { name: string } | null; company: { name: string } | null;
owner?: { id: string; fullName: string } | null; owner?: { id: string; fullName: string } | null;
@@ -225,6 +228,8 @@ export function mapBoardToStages(
), ),
stageId: deal.stageId, stageId: deal.stageId,
currencyUnit: currencyUnitLabel(deal.currency), currencyUnit: currencyUnitLabel(deal.currency),
createdAt: deal.createdAt,
promotedFromPriorAt: deal.promotedFromPriorAt,
assigneeNames, assigneeNames,
ownerId: deal.owner?.id ?? null, ownerId: deal.owner?.id ?? null,
assigneeIds: (deal.assignees ?? []).map((a) => a.id), assigneeIds: (deal.assignees ?? []).map((a) => a.id),
+52 -6
View File
@@ -231,6 +231,7 @@ const activityModal = ref<{
} | null>(null); } | null>(null);
const menuId = ref<string | null>(null); const menuId = ref<string | null>(null);
const boardMenuId = ref<string | null>(null); const boardMenuId = ref<string | null>(null);
const boardMenuActivity = ref<ActivityDto | null>(null);
const boardMenuStyle = ref<Record<string, string> | null>(null); const boardMenuStyle = ref<Record<string, string> | null>(null);
const deleteActivityIds = ref<string[]>([]); const deleteActivityIds = ref<string[]>([]);
const deleteSingleTitle = ref(''); const deleteSingleTitle = ref('');
@@ -572,6 +573,7 @@ function clearFacets() {
async function complete(id: string) { async function complete(id: string) {
menuId.value = null; menuId.value = null;
boardMenuId.value = null; boardMenuId.value = null;
boardMenuActivity.value = null;
try { try {
await api.patch(`/activities/${id}/complete`); await api.patch(`/activities/${id}/complete`);
await refreshAll(); await refreshAll();
@@ -585,6 +587,27 @@ function closePopovers() {
menuId.value = null; 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 deleteConfirmCopy = computed(() => {
const n = deleteActivityIds.value.length; const n = deleteActivityIds.value.length;
if (n <= 0) return ''; if (n <= 0) return '';
@@ -604,6 +627,7 @@ function activityTitle(id: string) {
function startSelect(id: string) { function startSelect(id: string) {
menuId.value = null; menuId.value = null;
boardMenuId.value = null; boardMenuId.value = null;
boardMenuActivity.value = null;
selectMode.value = true; selectMode.value = true;
selectedIds.value = new Set([id]); selectedIds.value = new Set([id]);
} }
@@ -624,6 +648,7 @@ function clearSelect() {
function askDeleteActivity(id: string, title?: string) { function askDeleteActivity(id: string, title?: string) {
menuId.value = null; menuId.value = null;
boardMenuId.value = null; boardMenuId.value = null;
boardMenuActivity.value = null;
deleteSingleTitle.value = title?.trim() || activityTitle(id); deleteSingleTitle.value = title?.trim() || activityTitle(id);
deleteActivityIds.value = [id]; deleteActivityIds.value = [id];
} }
@@ -632,6 +657,7 @@ function askBulkDelete() {
if (!selectedIds.value.size) return; if (!selectedIds.value.size) return;
menuId.value = null; menuId.value = null;
boardMenuId.value = null; boardMenuId.value = null;
boardMenuActivity.value = null;
deleteSingleTitle.value = ''; deleteSingleTitle.value = '';
deleteActivityIds.value = [...selectedIds.value]; deleteActivityIds.value = [...selectedIds.value];
} }
@@ -659,12 +685,13 @@ async function confirmDeleteActivities() {
} }
} }
function onBoardMenu(activityId: string) { function onBoardMenu(activity: ActivityDto) {
boardMenuId.value = boardMenuId.value === activityId ? null : activityId; boardMenuId.value = boardMenuId.value === activity.id ? null : activity.id;
boardMenuActivity.value = boardMenuId.value ? activity : null;
boardMenuStyle.value = null; boardMenuStyle.value = null;
if (!boardMenuId.value) return; if (!boardMenuId.value) return;
void nextTick(() => { void nextTick(() => {
boardMenuStyle.value = cardMenuStyle(activityId); boardMenuStyle.value = cardMenuStyle(activity.id);
}); });
} }
@@ -672,6 +699,7 @@ const unbindBoardMenuDismiss = bindMenuDismiss(
() => boardMenuId.value != null, () => boardMenuId.value != null,
() => { () => {
boardMenuId.value = null; boardMenuId.value = null;
boardMenuActivity.value = null;
}, },
); );
@@ -783,6 +811,7 @@ onMounted(() => {
watch(viewMode, () => { watch(viewMode, () => {
clearSelect(); clearSelect();
boardMenuId.value = null; boardMenuId.value = null;
boardMenuActivity.value = null;
menuId.value = null; menuId.value = null;
}); });
@@ -1213,6 +1242,9 @@ onUnmounted(() => {
<RouterLink :to="{ name: 'activity-edit', params: { id: item.id } }"> <RouterLink :to="{ name: 'activity-edit', params: { id: item.id } }">
ویرایش ویرایش
</RouterLink> </RouterLink>
<button v-if="canPromotePrior(item)" type="button" @click="promotePriorActivity(item.id)">
انتقال به کار جاری
</button>
<button v-if="item.status === 'open'" type="button" @click="complete(item.id)"> <button v-if="item.status === 'open'" type="button" @click="complete(item.id)">
انجام شد انجام شد
</button> </button>
@@ -1297,6 +1329,12 @@ onUnmounted(() => {
<RouterLink :to="{ name: 'activity-detail', params: { id: item.id } }"> <RouterLink :to="{ name: 'activity-detail', params: { id: item.id } }">
مشاهده مشاهده
</RouterLink> </RouterLink>
<RouterLink :to="{ name: 'activity-edit', params: { id: item.id } }">
ویرایش
</RouterLink>
<button v-if="canPromotePrior(item)" type="button" @click="promotePriorActivity(item.id)">
انتقال به کار جاری
</button>
<button v-if="item.status === 'open'" type="button" @click="complete(item.id)"> <button v-if="item.status === 'open'" type="button" @click="complete(item.id)">
انجام شد انجام شد
</button> </button>
@@ -1348,17 +1386,25 @@ onUnmounted(() => {
<RouterLink <RouterLink
role="menuitem" role="menuitem"
:to="{ name: 'activity-detail', params: { id: boardMenuId } }" :to="{ name: 'activity-detail', params: { id: boardMenuId } }"
@click="boardMenuId = null" @click="boardMenuId = null; boardMenuActivity = null"
> >
مشاهده مشاهده
</RouterLink> </RouterLink>
<RouterLink <RouterLink
role="menuitem" role="menuitem"
:to="{ name: 'activity-edit', params: { id: boardMenuId } }" :to="{ name: 'activity-edit', params: { id: boardMenuId } }"
@click="boardMenuId = null" @click="boardMenuId = null; boardMenuActivity = null"
> >
ویرایش ویرایش
</RouterLink> </RouterLink>
<button
v-if="boardMenuActivity && canPromotePrior(boardMenuActivity)"
type="button"
role="menuitem"
@click="promotePriorActivity(boardMenuActivity.id)"
>
انتقال به کار جاری
</button>
<button <button
type="button" type="button"
role="menuitem" role="menuitem"
@@ -1367,7 +1413,7 @@ onUnmounted(() => {
> >
حذف حذف
</button> </button>
<button type="button" role="menuitem" @click="boardMenuId = null">بستن</button> <button type="button" role="menuitem" @click="boardMenuId = null; boardMenuActivity = null">بستن</button>
</div> </div>
</Teleport> </Teleport>
+8 -12
View File
@@ -186,21 +186,17 @@ const auth = useAuthStore();
const priorArchiveOn = computed(() => Boolean(auth.user?.priorArchive)); const priorArchiveOn = computed(() => Boolean(auth.user?.priorArchive));
function isPriorActivity(row: ActivityDto) { function isPriorActivity(row: ActivityDto) {
if (row.importSource !== 'didar') return false; const cutoff = auth.user?.priorArchive?.cutoffAt;
if (row.status === 'open' && row.dueAt && new Date(row.dueAt).getTime() > Date.now()) return false; if (!auth.user?.priorArchive || !cutoff) return false;
return true; if (row.promotedFromPriorAt) return false;
return new Date(row.createdAt).getTime() < new Date(cutoff).getTime();
} }
function isPriorDeal(row: DealDto) { function isPriorDeal(row: DealDto) {
if (row.importSource !== 'didar') return false; const cutoff = auth.user?.priorArchive?.cutoffAt;
if ( if (!auth.user?.priorArchive || !cutoff) return false;
row.status === 'open' && if (row.promotedFromPriorAt) return false;
row.expectedCloseAt && return new Date(row.createdAt).getTime() < new Date(cutoff).getTime();
new Date(row.expectedCloseAt).getTime() > Date.now()
) {
return false;
}
return true;
} }
const lastCallAt = computed(() => { const lastCallAt = computed(() => {
+35
View File
@@ -497,6 +497,33 @@ function onOpenDeal(dealId: string) {
void router.push({ name: 'deal-detail', params: { id: dealId } }); 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 pageBusy = ref(false);
const busyMessage = ref('لطفاً صبر کنید…'); const busyMessage = ref('لطفاً صبر کنید…');
@@ -721,6 +748,14 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
> >
ویرایش ویرایش
</button> </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 type="button" role="menuitem" class="danger" @click="askDeleteDeal(menuOpenId)">
حذف حذف
</button> </button>