feat(web): add prior archive promote actions
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user