Unify the web UI on shared controls and let pipeline columns be managed on the board.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-10 01:22:24 +03:30
co-authored by Cursor
parent f1aba1b386
commit b8ad4037b6
40 changed files with 2334 additions and 788 deletions
+61 -13
View File
@@ -37,11 +37,29 @@ type MenuKey = 'person' | 'company' | 'owner' | 'currency' | '';
type ContactKind = 'person' | 'company';
type DealHealth = 'normal' | 'stale' | 'rotten';
const props = withDefaults(
defineProps<{
embedded?: boolean;
dealId?: string;
initialPipelineId?: string;
initialStageId?: string;
}>(),
{ embedded: false },
);
const emit = defineEmits<{
saved: [];
cancel: [];
}>();
const route = useRoute();
const router = useRouter();
const auth = useAuthStore();
const id = computed(() => (typeof route.params.id === 'string' ? route.params.id : ''));
const isNew = computed(() => route.name === 'deal-new');
const id = computed(() => {
if (props.embedded) return props.dealId ?? '';
return typeof route.params.id === 'string' ? route.params.id : '';
});
const isNew = computed(() => (props.embedded ? !props.dealId : route.name === 'deal-new'));
const pageTitle = computed(() => (isNew.value ? 'ایجاد معامله جدید' : 'ویرایش معامله'));
const loading = ref(false);
@@ -188,7 +206,8 @@ async function load() {
error.value = '';
try {
const queryPipelineId =
typeof route.query.pipelineId === 'string' ? route.query.pipelineId : undefined;
props.initialPipelineId ||
(typeof route.query.pipelineId === 'string' ? route.query.pipelineId : undefined);
const [peopleRes, companiesRes, pipelinesRes, productsRes, orgRes, usersRes] =
await Promise.all([
api.get<{ items: PersonDto[] }>('/people', { params: { limit: 100 } }),
@@ -254,12 +273,10 @@ async function load() {
companyId.value = route.query.companyId;
if (!personId.value) contactKind.value = 'company';
}
if (
typeof route.query.stageId === 'string' &&
route.query.stageId &&
stages.value.some((s) => s.id === route.query.stageId)
) {
stageId.value = route.query.stageId;
const incomingStage = props.initialStageId ||
(typeof route.query.stageId === 'string' ? route.query.stageId : '');
if (incomingStage && stages.value.some((s) => s.id === incomingStage)) {
stageId.value = incomingStage;
}
}
} catch {
@@ -318,6 +335,10 @@ function validate() {
}
function cancel() {
if (props.embedded) {
emit('cancel');
return;
}
void router.push({ name: 'pipeline' });
}
@@ -338,6 +359,10 @@ async function submit() {
} catch {
/* deal saved; follow-up is optional */
}
if (props.embedded) {
emit('saved');
return;
}
await router.replace({ name: 'deal-detail', params: { id: data.id } });
} else {
await api.patch(`/deals/${id.value}`, {
@@ -347,6 +372,10 @@ async function submit() {
if (stageId.value) {
await api.patch(`/deals/${id.value}/move`, { stageId: stageId.value });
}
if (props.embedded) {
emit('saved');
return;
}
await router.push({ name: 'pipeline' });
}
} catch (e) {
@@ -374,8 +403,17 @@ onMounted(load);
</script>
<template>
<section class="page" @click="closeMenus">
<header class="desktop-head">
<section class="page" :class="{ embedded }" @click="closeMenus">
<div v-if="embedded" class="embed-actions">
<button class="btn primary" type="button" :disabled="saving" @click="submit">
{{ saving ? 'در حال ذخیره…' : 'ذخیره معامله' }}
</button>
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">
انصراف
</button>
</div>
<header v-if="!embedded" class="desktop-head">
<div class="title-block">
<span class="doc-ico">
<UiIcon name="document" :size="22" />
@@ -414,7 +452,7 @@ onMounted(load);
</div>
</header>
<header class="mobile-head">
<header v-if="!embedded" class="mobile-head">
<button class="icon-btn" type="button" aria-label="ذخیره" :disabled="saving" @click="submit">
<UiIcon name="check-plain" :size="22" />
</button>
@@ -748,7 +786,7 @@ onMounted(load);
</p>
</form>
<footer class="mobile-save">
<footer v-if="!embedded" class="mobile-save">
<button class="btn outline" type="button" :disabled="saving" @click="cancel">انصراف</button>
<button class="btn primary" type="button" :disabled="saving || loading" @click="submit">
{{ saving ? 'در حال ذخیره…' : 'ذخیره معامله' }}
@@ -765,6 +803,16 @@ onMounted(load);
min-width: 0;
}
.page.embedded {
padding-bottom: 0;
}
.embed-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.desktop-head {
display: none;
justify-content: space-between;