rename: کاریز فروش → مسیر فروش, کاریز پروژه → تابلوی پروژه

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-06 20:51:50 +03:30
co-authored by Cursor
parent b3784818fc
commit b4ffef9b6f
29 changed files with 131 additions and 120 deletions
@@ -0,0 +1,2 @@
-- Rename the default sales pipeline: «کاریز فروش» → «مسیر فروش»
UPDATE "Pipeline" SET "name" = 'مسیر فروش' WHERE "name" = 'کاریز فروش';
+2 -2
View File
@@ -1231,7 +1231,7 @@ model PortalOtp {
@@index([expiresAt])
}
/// برد کانبان برای کارهای داخلی / پس‌ازفروش (جدا از کاریز فروش)
/// برد کانبان برای کارهای داخلی / پس‌ازفروش (جدا از مسیر فروش)
model ProjectBoard {
id String @id @default(cuid())
organizationId String
@@ -1775,7 +1775,7 @@ model AuditLog {
@@index([organizationId, entityType, createdAt])
}
/// فیلترهای ذخیره‌شده برای لیست‌ها و کاریزها
/// فیلترهای ذخیره‌شده برای لیست‌ها و مسیرهای فروش
/// ستون‌های مخفی هر جدول برای هر کاربر (به‌ازای هر سازمان)
model UserTablePreference {
id String @id @default(cuid())
+10 -10
View File
@@ -335,7 +335,7 @@ async function main() {
pipeline = await prisma.pipeline.create({
data: {
organizationId: org.id,
name: 'کاریز فروش',
name: 'مسیر فروش',
isDefault: true,
stages: {
create: DEFAULT_STAGES.map((s, index) => ({
@@ -354,13 +354,13 @@ async function main() {
}
const servicesPipeline = await prisma.pipeline.findFirst({
where: { organizationId: org.id, name: 'کاریز خدمات' },
where: { organizationId: org.id, name: 'مسیر خدمات' },
});
if (!servicesPipeline) {
await prisma.pipeline.create({
data: {
organizationId: org.id,
name: 'کاریز خدمات',
name: 'مسیر خدمات',
isDefault: false,
stages: {
create: DEFAULT_STAGES.map((s, index) => ({
@@ -377,7 +377,7 @@ async function main() {
console.log('Services pipeline already seeded');
}
// کاریز منتظر — مطابق طراحی UI (۳ مرحله + معاملات نمونه)
// مسیر منتظر — مطابق طراحی UI (۳ مرحله + معاملات نمونه)
const WAITING_STAGES = [
{ name: 'پیگیری', isWon: false, isLost: false },
{ name: 'مذاکرات اولیه', isWon: false, isLost: false },
@@ -483,7 +483,7 @@ async function main() {
];
let waitingPipeline = await prisma.pipeline.findFirst({
where: { organizationId: org.id, name: 'کاریز منتظر' },
where: { organizationId: org.id, name: 'مسیر منتظر' },
include: { stages: { orderBy: { position: 'asc' } } },
});
@@ -491,7 +491,7 @@ async function main() {
waitingPipeline = await prisma.pipeline.create({
data: {
organizationId: org.id,
name: 'کاریز منتظر',
name: 'مسیر منتظر',
isDefault: false,
stages: {
create: WAITING_STAGES.map((s, index) => ({
@@ -504,7 +504,7 @@ async function main() {
},
include: { stages: { orderBy: { position: 'asc' } } },
});
console.log('Seeded waiting pipeline (کاریز منتظر)');
console.log('Seeded waiting pipeline (مسیر منتظر)');
} else {
console.log('Waiting pipeline already seeded');
}
@@ -534,7 +534,7 @@ async function main() {
expectedCloseAt: new Date(d.expectedCloseAt),
ownerId: owner?.id,
position: d.position,
tags: ['دمو-کاریز'],
tags: ['دمو-مسیر'],
};
}),
});
@@ -763,7 +763,7 @@ async function main() {
{
organizationId: org.id,
authorId: owner?.id,
body: 'در دمو نوا روی گزارش کاریز خیلی حساس بودند. نسخه چاپی PDF را آماده کنیم.',
body: 'در دمو نوا روی گزارش مسیر فروش خیلی حساس بودند. نسخه چاپی PDF را آماده کنیم.',
noteType: 'call_result',
color: 'sky',
tags: ['دمو'],
@@ -971,7 +971,7 @@ async function main() {
};
await ensure('deal_moved', {
title: 'جابه‌جایی معامله در کاریز',
title: 'جابه‌جایی معامله در مسیر فروش',
body: sampleDeal
? `«${sampleDeal.title}» به مرحله جدید منتقل شد`
: 'یک معامله جابه‌جا شد',
@@ -677,7 +677,7 @@ export class LeadFormsService {
}));
if (!pipeline?.stages.length) {
throw new BadRequestException('کاریز فعالی برای ثبت معامله وجود ندارد');
throw new BadRequestException('مسیر فروش فعالی برای ثبت معامله وجود ندارد');
}
const stage =
@@ -744,7 +744,7 @@ export class LeadFormsService {
where: { id, organizationId },
select: { id: true },
});
if (!found) throw new BadRequestException('کاریز انتخاب‌شده معتبر نیست');
if (!found) throw new BadRequestException('مسیر فروش انتخاب‌شده معتبر نیست');
}
private async assertLeadStage(
@@ -502,7 +502,7 @@ export class DealsExcelService {
}
/**
* مراحل پیش‌فرض سیستم (سرنخ، تماس اول، …) که خالی مانده‌اند از کاریزهای واردشده حذف می‌شوند
* مراحل پیش‌فرض سیستم (سرنخ، تماس اول، …) که خالی مانده‌اند از مسیرهای فروش واردشده حذف می‌شوند
* تا فقط مراحل خود فایل دیده شوند.
*/
private async removeUnusedDefaultStages(organizationId: string, pipelineIds: Set<string>) {
@@ -634,7 +634,8 @@ export class DealsExcelService {
status: DealStatus,
ctx: RowContext,
) {
const name = pipelineName?.trim() || 'کاریز فروش';
const rawName = pipelineName?.trim();
const name = !rawName || rawName === 'کاریز فروش' ? 'مسیر فروش' : rawName;
const cacheKey = name.toLowerCase();
let pipeline = ctx.pipelines.get(cacheKey);
if (!pipeline) {
@@ -677,7 +678,7 @@ export class DealsExcelService {
}
const added = await this.pipelines.createStage(organizationId, pipeline.id, wanted);
const stage = added.stages.find((s) => s.name === wanted) ?? added.stages.at(-1);
if (!stage) throw new Error('کاریز مرحله‌ای ندارد');
if (!stage) throw new Error('مسیر فروش مرحله‌ای ندارد');
pipeline.stages = added.stages.map((s) => ({
id: s.id,
name: s.name,
@@ -688,7 +689,7 @@ export class DealsExcelService {
}
const stage = byStatusExisting ?? pipeline.stages[0];
if (!stage) throw new Error('کاریز مرحله‌ای ندارد');
if (!stage) throw new Error('مسیر فروش مرحله‌ای ندارد');
return { pipelineId: pipeline.id, stageId: stage.id, pipelineName: pipeline.name };
}
+3 -3
View File
@@ -92,7 +92,7 @@ export class DealsService {
resolvedPipeline.stages[0];
if (!stage) {
throw new BadRequestException('کاریز مرحله‌ای ندارد');
throw new BadRequestException('مسیر فروش مرحله‌ای ندارد');
}
await this.assertLinks(organizationId, actor, dto.personId, dto.companyId);
@@ -337,7 +337,7 @@ export class DealsService {
where: { id: dto.stageId, pipelineId: deal.pipelineId },
});
if (!stage) {
throw new BadRequestException('مرحله در این کاریز معتبر نیست');
throw new BadRequestException('مرحله در این مسیر فروش معتبر نیست');
}
const status = stage.isWon ? 'won' : stage.isLost ? 'lost' : 'open';
@@ -369,7 +369,7 @@ export class DealsService {
organizationId,
userId: deal.ownerId,
type: 'deal_moved',
title: 'جابه‌جایی معامله در کاریز',
title: 'جابه‌جایی معامله در مسیر فروش',
body: `«${deal.title}» به مرحله «${stage.name}» منتقل شد`,
link: `/deals/${deal.id}`,
entityType: 'deal',
+2 -2
View File
@@ -81,7 +81,7 @@ export class PipelinesService {
const created = await this.prisma.pipeline.create({
data: {
organizationId,
name: 'کاریز فروش',
name: 'مسیر فروش',
isDefault: true,
stages: {
create: DEFAULT_STAGES.map((s, index) => ({
@@ -389,7 +389,7 @@ export class PipelinesService {
where: { id: pipelineId, organizationId },
include: { stages: { orderBy: { position: 'asc' } } },
});
if (!pipeline) throw new NotFoundException('کاریز پیدا نشد');
if (!pipeline) throw new NotFoundException('مسیر فروش پیدا نشد');
return pipeline;
}
@@ -33,8 +33,8 @@ const CASE_COLUMNS = [
{ header: 'نام خانوادگی شخص کارت', key: 'personName', width: 22 },
{ header: 'کد دیدار شخص', key: 'personCode', width: 14 },
{ header: 'موبایل شخص کارت', key: 'personMobile', width: 16 },
{ header: 'کاریز کارت', key: 'boardName', width: 20 },
{ header: 'مرحله کاریز کارت', key: 'stageName', width: 18 },
{ header: 'تابلوی پروژه کارت', key: 'boardName', width: 20 },
{ header: 'مرحله تابلوی پروژه کارت', key: 'stageName', width: 18 },
{ header: 'تاریخ انجام کارت', key: 'doneDate', width: 16 },
{ header: 'برچسب', key: 'tag', width: 16 },
];
@@ -53,6 +53,9 @@ const CASE_ALIASES: Record<string, string> = {
نامخانوادگیشخصکارت: 'personName',
کددیدارشخص: 'personCode',
موبایلشخصکارت: 'personMobile',
تابلویپروژهکارت: 'boardName',
مرحلهتابلویپروژهکارت: 'stageName',
// سرستون‌های قدیمی (قبل از تغییر نام کاریز → تابلوی پروژه)
کاریزکارت: 'boardName',
مرحلهکاریزکارت: 'stageName',
تاریخانجامکارت: 'doneDate',
@@ -49,7 +49,7 @@ const cols = computed<Col[]>(() => {
<HomePanelEmpty
v-if="!cols.length"
title="هنوز معامله‌ای در کاریز نیست"
title="هنوز معامله‌ای در مسیر فروش نیست"
description="اولین معامله را بسازید تا جریان فروش اینجا دیده شود."
action-label="معامله جدید"
:action-to="{ name: 'deal-new' }"
@@ -18,7 +18,7 @@ withDefaults(
{
canCreate: false,
icon: 'briefcase',
createOptionLabel: 'کاریز جدید…',
createOptionLabel: 'مسیر فروش جدید…',
showAmountFilters: true,
},
);
@@ -34,7 +34,7 @@ const amountMin = defineModel<string>('amountMin', { default: '' });
const amountMax = defineModel<string>('amountMax', { default: '' });
const viewOptions = [
{ value: 'kanban' as const, label: 'نمای کاریز', icon: 'grid' as const },
{ value: 'kanban' as const, label: 'نمای ستونی', icon: 'grid' as const },
{ value: 'list' as const, label: 'نمای لیست', icon: 'list' as const },
];
+6 -6
View File
@@ -108,7 +108,7 @@ export const NAV_HUBS: NavHubDef[] = [
tint: 'sky',
shortcuts: [
{ to: '/leads', title: 'فهرست لیدها', icon: 'users', module: 'leads' },
{ to: '/leads/board', title: 'کاریز لیدها', icon: 'filter', module: 'leads' },
{ to: '/leads/board', title: 'مسیر لیدها', icon: 'filter', module: 'leads' },
],
cards: [
{
@@ -121,8 +121,8 @@ export const NAV_HUBS: NavHubDef[] = [
},
{
to: '/leads/board',
title: 'کاریز لیدها',
desc: 'نمای کانبان و حرکت بین مراحل',
title: 'مسیر لیدها',
desc: 'نمای ستونی و حرکت بین مراحل',
icon: 'filter',
tint: 'violet',
module: 'leads',
@@ -130,7 +130,7 @@ export const NAV_HUBS: NavHubDef[] = [
{
to: '/settings/lead-stages',
title: 'مراحل و وضعیت‌ها',
desc: 'تعریف وضعیت‌ها و ستون‌های کاریز لید',
desc: 'تعریف وضعیت‌ها و ستون‌های مسیر لید',
icon: 'layers',
tint: 'peach',
admin: true,
@@ -160,7 +160,7 @@ export const NAV_HUBS: NavHubDef[] = [
cards: [
{
to: '/pipeline',
title: 'کاریز فروش',
title: 'مسیر فروش',
desc: 'معاملات و مراحل فروش',
icon: 'filter',
tint: 'mint',
@@ -329,7 +329,7 @@ export const NAV_HUBS: NavHubDef[] = [
cards: [
{
to: '/projects',
title: 'کاریز پروژه',
title: 'تابلوی پروژه',
desc: 'میز کار تیمی: صف، انجام، بازبینی و ارجاع کارت',
icon: 'flag',
tint: 'violet',
+1 -1
View File
@@ -125,7 +125,7 @@ export function currencyUnitLabel(currency: string): string {
return currency;
}
/** نگاشت برد API به مدل UI کاریز */
/** نگاشت برد API به مدل UI مسیر فروش */
export function mapBoardToStages(
board: {
stages: Array<{
+3 -3
View File
@@ -435,7 +435,7 @@ onMounted(load);
>
<template #lead>
<span class="crumb">
<RouterLink :to="{ name: 'pipeline' }">کاریز فروش</RouterLink>
<RouterLink :to="{ name: 'pipeline' }">مسیر فروش</RouterLink>
<span>›</span>
<strong>{{ pageTitle }}</strong>
</span>
@@ -678,13 +678,13 @@ onMounted(load);
</UiSelect>
</UiField>
<UiField label="کاریز فروش">
<UiField label="مسیر فروش">
<UiSelect :model-value="pipelineId" @update:model-value="onPipelineChange($event)">
<option v-for="p in pipelines" :key="p.id" :value="p.id">{{ p.name }}</option>
</UiSelect>
</UiField>
<UiField label="مرحله کاریز" required>
<UiField label="مرحله" required>
<UiSelect v-model="stageId" required>
<option v-for="s in stages" :key="s.id" :value="s.id">{{ s.name }}</option>
</UiSelect>
+4 -4
View File
@@ -33,7 +33,7 @@ async function load() {
error.value =
axios.isAxiosError(e) && !e.response
? 'اتصال به API برقرار نشد.'
: 'بارگذاری کاریز لیدها ناموفق بود.';
: 'بارگذاری مسیر لیدها ناموفق بود.';
} finally {
loading.value = false;
}
@@ -107,7 +107,7 @@ onMounted(load);
<template>
<section class="page">
<UiPageHeader title="کاریز لیدها" lead="کشیدن و رها کردن برای تغییر وضعیت">
<UiPageHeader title="مسیر لیدها" lead="کشیدن و رها کردن برای تغییر وضعیت">
<template #actions>
<RouterLink :to="{ name: 'leads' }" class="link-btn">
<UiButton variant="ghost" size="sm">فهرست لیدها</UiButton>
@@ -166,8 +166,8 @@ onMounted(load);
<UiEmptyState
v-else-if="!loading"
title="مرحله‌ای برای کاریز نیست."
description="در تنظیمات مراحل لید، نمایش روی کاریز را فعال کنید."
title="مرحله‌ای برای نمایش در مسیر لیدها نیست."
description="در تنظیمات مراحل لید، «نمایش روی مسیر» را فعال کنید."
/>
</section>
</template>
+1 -1
View File
@@ -594,7 +594,7 @@ onMounted(load);
<input v-model="createDeal" type="checkbox" />
</label>
</div>
<label v-if="createDeal" class="field pipeline-field">کاریز مقصد
<label v-if="createDeal" class="field pipeline-field">مسیر فروش مقصد
<select v-model="pipelineId">
<option value="">پیش‌فرض سازمان</option>
<option v-for="p in pipelines" :key="p.id" :value="p.id">{{ p.name }}</option>
@@ -170,7 +170,7 @@ onMounted(load);
<section class="crm-page">
<UiPageHeader
title="مراحل لید"
lead="تعریف وضعیت‌های کاریز لیدها و برچسب‌های تبدیل/از دست‌رفته"
lead="تعریف وضعیت‌های مسیر لیدها و برچسب‌های تبدیل/از دست‌رفته"
/>
<p v-if="okMsg" class="ok">{{ okMsg }}</p>
@@ -194,7 +194,7 @@ onMounted(load);
</UiField>
<label class="check">
<input v-model="showOnBoard" type="checkbox" />
نمایش روی کاریز
نمایش روی مسیر
</label>
<label class="check">
<input v-model="isConverted" type="checkbox" />
@@ -227,7 +227,7 @@ onMounted(load);
</UiField>
<label class="check">
<input v-model="editShowOnBoard" type="checkbox" />
نمایش روی کاریز
نمایش روی مسیر
</label>
<label class="check">
<input v-model="editConverted" type="checkbox" />
@@ -253,7 +253,7 @@ onMounted(load);
<div class="info">
<strong>{{ stage.name }}</strong>
<span class="muted">
{{ stage.showOnBoard ? 'روی کاریز' : 'مخفی از کاریز' }}
{{ stage.showOnBoard ? 'روی مسیر' : 'مخفی از مسیر' }}
<template v-if="stage.leadCount != null">
· {{ toFaDigits(stage.leadCount) }} لید
</template>
+2 -2
View File
@@ -1230,11 +1230,11 @@ onBeforeUnmount(() => {
</ul>
</div>
<TableColumnsMenu table-key="leads" align="end" />
<div class="view-toggle" title="نمای لیست / کاریز">
<div class="view-toggle" title="نمای لیست / مسیر">
<button type="button" class="view-btn on" title="لیست">
<UiIcon name="list" :size="16" />
</button>
<RouterLink class="view-btn" :to="{ name: 'leads-board' }" title="کاریز">
<RouterLink class="view-btn" :to="{ name: 'leads-board' }" title="مسیر لیدها">
<UiIcon name="grid" :size="16" />
</RouterLink>
</div>
+1 -1
View File
@@ -870,7 +870,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
<ProfileEmptyState
v-if="dealsEmpty"
title="هنوز معامله‌ای ثبت نشده!"
description="اولین معامله این مشتری را در کاریز ثبت کنید یا یک فعالیت پیگیری بسازید."
description="اولین معامله این مشتری را در مسیر فروش ثبت کنید یا یک فعالیت پیگیری بسازید."
primary-label="افزودن معامله"
:primary-to="{ name: 'deal-new', query: { personId: id } }"
secondary-label="افزودن اولین فعالیت"
+8 -8
View File
@@ -171,7 +171,7 @@ async function load() {
if (axios.isAxiosError(e) && !e.response) {
error.value = 'اتصال به API برقرار نشد.';
} else {
error.value = 'بارگذاری کاریز ناموفق بود.';
error.value = 'بارگذاری مسیر فروش ناموفق بود.';
}
} finally {
loading.value = false;
@@ -189,7 +189,7 @@ async function createPipeline() {
showCreate.value = false;
await router.replace({ name: 'pipeline', query: { pipelineId: data.id } });
} catch {
error.value = 'ساخت کاریز ناموفق بود.';
error.value = 'ساخت مسیر فروش ناموفق بود.';
} finally {
saving.value = false;
}
@@ -451,7 +451,7 @@ function onImportStart() {
async function onImportDone(data: ImportContactsResult) {
importMsg.value = `وارد شد: ${data.created} · تکراری رد شد: ${data.skipped}${
data.errors.length ? ` · خطا: ${data.errors.length}` : ''
}${data.boards?.length ? ` · کاریز: ${data.boards.join('، ')}` : ''}`;
}${data.boards?.length ? ` · مسیر فروش: ${data.boards.join('، ')}` : ''}`;
if (data.errors.length) {
error.value = data.errors
.slice(0, 3)
@@ -504,7 +504,7 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
<section class="page" dir="rtl">
<UiBusyOverlay :open="pageBusy" :message="busyMessage" />
<PipelineToolbar
pipeline-name="کاریز فروش"
pipeline-name="مسیر فروش"
:pipeline-options="pipelineOptions"
v-model:selected-pipeline-id="selectedPipelineId"
v-model:view-mode="viewMode"
@@ -546,7 +546,7 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
<p v-if="error" class="error">{{ error }}</p>
<p v-if="importMsg" class="ok">{{ importMsg }}</p>
<p v-else-if="loading && !stages.length" class="muted">در حال بارگذاری کاریز…</p>
<p v-else-if="loading && !stages.length" class="muted">در حال بارگذاری مسیر فروش…</p>
<PipelineList
v-if="viewMode === 'list' && (!loading || stages.length)"
@@ -602,15 +602,15 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
<UiModal
:open="showCreate && canCreatePipeline"
title="کاریز جدید"
title="مسیر فروش جدید"
@close="showCreate = false"
>
<form class="create-form" @submit.prevent="createPipeline">
<label class="field">
<span>نام کاریز</span>
<span>نام مسیر فروش</span>
<UiInput
v-model="newName"
placeholder="نام کاریز جدید…"
placeholder="نام مسیر فروش جدید…"
:maxlength="120"
required
:minlength="2"
+1 -1
View File
@@ -647,7 +647,7 @@ watch(
<ProfileEmptyState
v-if="dealsEmpty"
title="هنوز معامله‌ای ثبت نشده!"
description="اولین معامله را در کاریز ثبت کنید یا یک فعالیت پیگیری بسازید."
description="اولین معامله را در مسیر فروش ثبت کنید یا یک فعالیت پیگیری بسازید."
primary-label="افزودن معامله"
:primary-to="{ name: 'deal-new' }"
secondary-label="افزودن اولین فعالیت"
+9 -9
View File
@@ -146,7 +146,7 @@ async function load() {
if (axios.isAxiosError(e) && !e.response) {
error.value = 'اتصال به API برقرار نشد.';
} else {
error.value = 'بارگذاری برد پروژه ناموفق بود.';
error.value = 'بارگذاری تابلوی پروژه ناموفق بود.';
}
} finally {
loading.value = false;
@@ -164,7 +164,7 @@ async function createBoard() {
showCreate.value = false;
await router.replace({ name: 'projects', query: { boardId: data.id } });
} catch {
error.value = 'ساخت برد ناموفق بود.';
error.value = 'ساخت تابلو ناموفق بود.';
} finally {
saving.value = false;
}
@@ -417,7 +417,7 @@ function onImportStart() {
async function onImportDone(data: ImportContactsResult) {
importMsg.value = `وارد شد: ${data.created} · تکراری رد شد: ${data.skipped}${
data.errors.length ? ` · خطا: ${data.errors.length}` : ''
}${data.boards?.length ? ` · برد: ${data.boards.join('، ')}` : ''}`;
}${data.boards?.length ? ` · تابلو: ${data.boards.join('، ')}` : ''}`;
if (data.errors.length) {
error.value = data.errors
.slice(0, 3)
@@ -468,9 +468,9 @@ onBeforeUnmount(() => {
<section class="page" dir="rtl">
<UiBusyOverlay :open="pageBusy" :message="busyMessage" />
<PipelineToolbar
pipeline-name="کاریز پروژه — کارهای تیمی"
pipeline-name="تابلوی پروژه — کارهای تیمی"
icon="grid"
create-option-label="برد جدید…"
create-option-label="تابلوی جدید…"
:pipeline-options="boardOptions"
v-model:selected-pipeline-id="selectedBoardId"
v-model:view-mode="viewMode"
@@ -505,7 +505,7 @@ onBeforeUnmount(() => {
<p v-if="error" class="error">{{ error }}</p>
<p v-if="importMsg" class="ok">{{ importMsg }}</p>
<p v-else-if="loading && !stages.length" class="muted">در حال بارگذاری برد…</p>
<p v-else-if="loading && !stages.length" class="muted">در حال بارگذاری تابلو…</p>
<PipelineList
v-if="viewMode === 'list' && (!loading || stages.length)"
@@ -564,15 +564,15 @@ onBeforeUnmount(() => {
<UiModal
:open="showCreate && canCreateBoard"
title="برد جدید"
title="تابلوی جدید"
@close="showCreate = false"
>
<form class="create-form" @submit.prevent="createBoard">
<label class="field">
<span>نام برد</span>
<span>نام تابلو</span>
<UiInput
v-model="newName"
placeholder="نام برد جدید…"
placeholder="نام تابلوی جدید…"
:maxlength="120"
required
:minlength="2"
+4 -4
View File
@@ -1404,7 +1404,7 @@ const activityMax = computed(() =>
<!-- Deep analytics (BI phase 1) -->
<div class="deep-grid">
<UiCard class="chart-card" title="تبدیل مراحل کاریز">
<UiCard class="chart-card" title="تبدیل مراحل مسیر فروش">
<p class="deep-hint">
سهم هر مرحله از معاملات باز و نرخ عبور نسبت به مرحله قبل. برای دیدن پرونده‌ها روی مرحله کلیک کنید.
</p>
@@ -1435,16 +1435,16 @@ const activityMax = computed(() =>
/>
</div>
<div class="funnel-row-meta">
<small>سهم کاریز: {{ toFaDigits(s.shareOfPipelinePercent) }}٪</small>
<small>سهم از مسیر: {{ toFaDigits(s.shareOfPipelinePercent) }}٪</small>
<small v-if="s.conversionFromPrevPercent != null">
تبدیل از قبل: {{ toFaDigits(s.conversionFromPrevPercent) }}٪
</small>
<small v-else>شروع کاریز</small>
<small v-else>شروع مسیر</small>
</div>
</button>
</div>
</div>
<p v-else class="empty-msg">معامله بازی در کاریز نیست.</p>
<p v-else class="empty-msg">معامله بازی در مسیر فروش نیست.</p>
</UiCard>
<UiCard class="chart-card" title="مقایسه با دوره قبل">
+2 -2
View File
@@ -152,7 +152,7 @@ const forecastChartOption = computed<UiChartOption>(() => {
grid: { left: '3%', right: '4%', bottom: '8%', top: '8%', containLabel: true },
xAxis: {
type: 'category',
data: ['هدف دوره', 'تحقق‌یافته', 'کاریز باز', 'پیش‌بینی'],
data: ['هدف دوره', 'تحقق‌یافته', 'معاملات باز', 'پیش‌بینی'],
axisLine: { lineStyle: { color: '#cbd5e1' } },
axisLabel: { color: '#64748b', fontFamily: 'IRANSans' },
},
@@ -721,7 +721,7 @@ onMounted(() => {
<h3>خلاصه پیش‌بینی</h3>
<div class="sum-row"><span>هدف دوره</span><strong>{{ formatMoney(forecast?.targetAmount || 0) }}</strong></div>
<div class="sum-row"><span>تحقق (برنده)</span><strong>{{ formatMoney(forecast?.wonAmount || 0) }}</strong></div>
<div class="sum-row"><span>کاریز باز</span><strong>{{ formatMoney(forecast?.openPipelineAmount || 0) }}</strong></div>
<div class="sum-row"><span>معاملات باز</span><strong>{{ formatMoney(forecast?.openPipelineAmount || 0) }}</strong></div>
<div class="sum-row payable"><span>پیش‌بینی درآمد</span><strong>{{ formatMoney(forecast?.forecastAmount || 0) }}</strong></div>
<div class="sum-row"><span>مشتریان جدید</span><strong>{{ toFaDigits(forecast?.newCustomersCount || 0) }}</strong></div>
<div class="sum-row"><span>معاملات جدید</span><strong>{{ toFaDigits(forecast?.newDealsCount || 0) }}</strong></div>