diff --git a/apps/web/src/components/StagePathStrip.vue b/apps/web/src/components/StagePathStrip.vue
new file mode 100644
index 0000000..413616c
--- /dev/null
+++ b/apps/web/src/components/StagePathStrip.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+ {{ stage.name }}
+
+
+
+
+
diff --git a/apps/web/src/views/ActivityDetailView.vue b/apps/web/src/views/ActivityDetailView.vue
index 43ac172..83ff6a4 100644
--- a/apps/web/src/views/ActivityDetailView.vue
+++ b/apps/web/src/views/ActivityDetailView.vue
@@ -17,11 +17,14 @@ import JalaliDateInput from '@/components/JalaliDateInput.vue';
import ActivityCommentSheet from '@/components/activities/ActivityCommentSheet.vue';
import ActivityPostponeSheet from '@/components/activities/ActivityPostponeSheet.vue';
import ActivityFieldsView from '@/components/activities/ActivityFieldsView.vue';
+import StagePathStrip from '@/components/StagePathStrip.vue';
+import { message, Modal } from 'ant-design-vue';
import type {
ActivityChecklistItem,
ActivityDto,
ActivityImageDto,
ActivityPriority,
+ ActivityStageDto,
OrgUserDto,
TeamDto,
} from '@mento/shared';
@@ -79,6 +82,8 @@ const loading = ref(true);
const saving = ref(false);
const error = ref('');
const activity = ref(null);
+const boardStages = ref([]);
+const stageMoving = ref(false);
type NoteMedia = {
id: string;
name: string;
@@ -687,7 +692,7 @@ async function load() {
activity.value = data;
hydrateDraft(data);
shareOpen.value = {};
- await loadActivityImages(data);
+ await Promise.all([loadActivityImages(data), loadBoardStages(data)]);
assignUserIds.value = data.assignees?.length
? data.assignees.map((u) => u.id)
: data.assignee?.id
@@ -696,11 +701,61 @@ async function load() {
} catch {
error.value = 'بارگذاری جزئیات فعالیت ناموفق بود.';
activity.value = null;
+ boardStages.value = [];
} finally {
loading.value = false;
}
}
+async function loadBoardStages(row: ActivityDto) {
+ const boardId =
+ row.boardId ||
+ (typeof route.query.board === 'string' && route.query.board ? route.query.board : undefined);
+ try {
+ const { data } = await api.get('/activities/stages', {
+ params: boardId ? { boardId } : undefined,
+ });
+ boardStages.value = Array.isArray(data) ? data : [];
+ } catch {
+ boardStages.value = [];
+ }
+}
+
+function moveToStage(stageId: string) {
+ if (!id.value || !activity.value || stageMoving.value || saving.value) return;
+ if (activity.value.stageId === stageId) return;
+ const target = boardStages.value.find((stage) => stage.id === stageId);
+ if (!target) return;
+ Modal.confirm({
+ title: 'جابهجایی مرحله',
+ content: `به ستون «${target.name}» منتقل میشود. انجام شود؟`,
+ okText: 'انجام بده',
+ cancelText: 'انجام نده',
+ centered: true,
+ async onOk() {
+ stageMoving.value = true;
+ error.value = '';
+ try {
+ const { data } = await api.patch(`/activities/${id.value}/stage`, { stageId });
+ if (data.status !== target.kind) {
+ message.info(
+ 'سهم شما ثبت شد. فعالیت وقتی انجامشده میشود که همهٔ مسئولها سهم خود را بزنند.',
+ );
+ await load();
+ return;
+ }
+ activity.value = data;
+ hydrateDraft(data);
+ } catch {
+ error.value = 'جابهجایی مرحله ناموفق بود.';
+ throw new Error('stage-move-failed');
+ } finally {
+ stageMoving.value = false;
+ }
+ },
+ });
+}
+
function clearImageUrls() {
for (const image of imageUrls.value) URL.revokeObjectURL(image.url);
imageUrls.value = [];
@@ -1024,6 +1079,17 @@ onUnmounted(() => {
+
+
+
+
-
@@ -1718,6 +1784,12 @@ onUnmounted(() => {
min-width: 0;
}
+.stage-path {
+ margin-top: 0.35rem;
+ padding-top: 0.75rem;
+ border-top: 1px solid #f1f5f9;
+}
+
.hero h2 {
margin: 0;
font-size: 1.05rem;
diff --git a/apps/web/src/views/DealFormView.vue b/apps/web/src/views/DealFormView.vue
index 949067d..fde84b4 100644
--- a/apps/web/src/views/DealFormView.vue
+++ b/apps/web/src/views/DealFormView.vue
@@ -18,6 +18,7 @@ import {
FormItem as AFormItem,
Input as AInput,
InputNumber as AInputNumber,
+ Modal,
Modal as AModal,
Row as ARow,
Segmented as ASegmented,
@@ -47,6 +48,7 @@ import EntityTimeline from '@/components/EntityTimeline.vue';
import RelatedTasksPanel from '@/components/routines/RelatedTasksPanel.vue';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import MoneyInput from '@/components/MoneyInput.vue';
+import StagePathStrip from '@/components/StagePathStrip.vue';
import UserMultiSelect from '@/components/UserMultiSelect.vue';
import PersonFormView from '@/views/PersonFormView.vue';
import type {
@@ -104,6 +106,7 @@ const pageTitle = computed(() => (isNew.value ? 'ایجاد معامله جدی
const loading = ref(false);
const saving = ref(false);
+const stageMoving = ref(false);
const error = ref('');
const showOthers = ref(true);
const othersKeys = ref(['others']);
@@ -197,6 +200,43 @@ const pipelineSelectOptions = computed(() =>
const stageSelectOptions = computed(() =>
stages.value.map((s) => ({ value: s.id, label: s.name })),
);
+const stagePathItems = computed(() => stages.value.map((s) => ({ id: s.id, name: s.name })));
+
+function onStageSelect(nextId: string) {
+ if (!nextId || nextId === stageId.value || stageMoving.value || saving.value) return;
+ const target = stages.value.find((stage) => stage.id === nextId);
+ if (!target) return;
+ const prev = stageId.value;
+ Modal.confirm({
+ title: 'جابهجایی مرحله',
+ content: `به مرحله «${target.name}» منتقل میشود. انجام شود؟`,
+ okText: 'انجام بده',
+ cancelText: 'انجام نده',
+ centered: true,
+ async onOk() {
+ stageId.value = nextId;
+ if (isNew.value || !id.value) return;
+ stageMoving.value = true;
+ error.value = '';
+ try {
+ await api.patch(`/deals/${id.value}/move`, { stageId: nextId });
+ } catch (e) {
+ stageId.value = prev;
+ if (axios.isAxiosError(e)) {
+ const msg = (e.response?.data as { message?: string | string[] })?.message;
+ error.value = Array.isArray(msg)
+ ? msg.join('، ')
+ : (msg ?? 'جابهجایی مرحله ناموفق بود.');
+ } else {
+ error.value = 'جابهجایی مرحله ناموفق بود.';
+ }
+ throw e;
+ } finally {
+ stageMoving.value = false;
+ }
+ },
+ });
+}
const productSelectOptions = computed(() =>
products.value.map((p) => ({ value: p.id, label: p.name })),
);
@@ -665,6 +705,20 @@ onMounted(load);
+
+
+
+