fix(web): serve real dashboard data instead of mocks
Drop forced home/treasury/reports mock fallbacks so API and database content drive the UI.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { Spin as ASpin } from 'ant-design-vue';
|
import { Alert as AAlert, Spin as ASpin } from 'ant-design-vue';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { formatMoney } from '@/lib/money';
|
import { formatMoney } from '@/lib/money';
|
||||||
@@ -13,8 +13,6 @@ import HomeAiAssistant from '@/components/dashboard/HomeAiAssistant.vue';
|
|||||||
import HomeRecentActivities from '@/components/dashboard/HomeRecentActivities.vue';
|
import HomeRecentActivities from '@/components/dashboard/HomeRecentActivities.vue';
|
||||||
import HomeMiniPipeline from '@/components/dashboard/HomeMiniPipeline.vue';
|
import HomeMiniPipeline from '@/components/dashboard/HomeMiniPipeline.vue';
|
||||||
import HomeTopCustomers from '@/components/dashboard/HomeTopCustomers.vue';
|
import HomeTopCustomers from '@/components/dashboard/HomeTopCustomers.vue';
|
||||||
import { Alert as AAlert } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type {
|
import type {
|
||||||
ActivityDto,
|
ActivityDto,
|
||||||
@@ -24,21 +22,11 @@ import type {
|
|||||||
PaginatedResponse,
|
PaginatedResponse,
|
||||||
PipelineBoardDto,
|
PipelineBoardDto,
|
||||||
} from '@mento/shared';
|
} from '@mento/shared';
|
||||||
import {
|
|
||||||
HOME_ACTIVITIES_MOCK,
|
|
||||||
HOME_DASHBOARD_MOCK,
|
|
||||||
HOME_PIPELINE_MOCK,
|
|
||||||
isHomeDashboardEmpty,
|
|
||||||
} from './home-dashboard-mock';
|
|
||||||
|
|
||||||
/** پیشنمایش UI با دیتای ماک — برای اتصال به API واقعی بگذارید false */
|
|
||||||
const FORCE_HOME_MOCK = true;
|
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const usingMock = ref(false);
|
const loadError = ref('');
|
||||||
const showMockBanner = ref(true);
|
|
||||||
const home = ref<HomeDashboardDto | null>(null);
|
const home = ref<HomeDashboardDto | null>(null);
|
||||||
const activities = ref<ActivityDto[]>([]);
|
const activities = ref<ActivityDto[]>([]);
|
||||||
const board = ref<PipelineBoardDto | null>(null);
|
const board = ref<PipelineBoardDto | null>(null);
|
||||||
@@ -153,21 +141,10 @@ function onAskHotkey(e: KeyboardEvent) {
|
|||||||
document.getElementById('home-hero-ask')?.focus();
|
document.getElementById('home-hero-ask')?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyHomeMock() {
|
|
||||||
home.value = HOME_DASHBOARD_MOCK;
|
|
||||||
activities.value = HOME_ACTIVITIES_MOCK;
|
|
||||||
board.value = HOME_PIPELINE_MOCK;
|
|
||||||
usingMock.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
window.addEventListener('keydown', onAskHotkey);
|
window.addEventListener('keydown', onAskHotkey);
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
if (FORCE_HOME_MOCK) {
|
loadError.value = '';
|
||||||
applyHomeMock();
|
|
||||||
loading.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const [homeRes, actRes, boardRes] = await Promise.all([
|
const [homeRes, actRes, boardRes] = await Promise.all([
|
||||||
api.get<HomeDashboardDto>('/reports/home'),
|
api.get<HomeDashboardDto>('/reports/home'),
|
||||||
@@ -176,16 +153,16 @@ onMounted(async () => {
|
|||||||
}),
|
}),
|
||||||
api.get<PipelineBoardDto>('/pipelines/board'),
|
api.get<PipelineBoardDto>('/pipelines/board'),
|
||||||
]);
|
]);
|
||||||
if (isHomeDashboardEmpty(homeRes.data)) {
|
home.value = homeRes.data;
|
||||||
applyHomeMock();
|
activities.value = actRes.data.items;
|
||||||
} else {
|
board.value = boardRes.data;
|
||||||
home.value = homeRes.data;
|
} catch (e) {
|
||||||
activities.value = actRes.data.items;
|
loadError.value = axios.isAxiosError(e)
|
||||||
board.value = boardRes.data;
|
? String(
|
||||||
usingMock.value = false;
|
(e.response?.data as { message?: string })?.message ??
|
||||||
}
|
'بارگذاری داشبورد ناموفق بود.',
|
||||||
} catch {
|
)
|
||||||
applyHomeMock();
|
: 'بارگذاری داشبورد ناموفق بود.';
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -211,13 +188,11 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<a-alert
|
<a-alert
|
||||||
v-if="usingMock && showMockBanner"
|
v-if="loadError"
|
||||||
type="info"
|
type="error"
|
||||||
closable
|
show-icon
|
||||||
class="rounded-xl"
|
class="rounded-xl"
|
||||||
message="پیشنمایش با دیتای ماک"
|
:message="loadError"
|
||||||
description="برای دیدن دادهٔ واقعی، FORCE_HOME_MOCK را در HomeView روی false بگذارید."
|
|
||||||
@close="showMockBanner = false"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="loading" class="flex items-center justify-center min-h-[120px]">
|
<div v-if="loading" class="flex items-center justify-center min-h-[120px]">
|
||||||
|
|||||||
@@ -170,349 +170,6 @@ const chartFillStyle = { height: '100%', width: '100%' };
|
|||||||
type PeriodKey = 'today' | 'week' | 'month' | 'quarter' | 'year' | 'custom';
|
type PeriodKey = 'today' | 'week' | 'month' | 'quarter' | 'year' | 'custom';
|
||||||
type PageMode = 'dashboard' | 'builder';
|
type PageMode = 'dashboard' | 'builder';
|
||||||
|
|
||||||
/** Rich mock for layout review. Disable with ?mock=0 */
|
|
||||||
function buildMockReportsDashboard(): ReportsDashboardDto {
|
|
||||||
const days = Array.from({ length: 13 }, (_, i) => {
|
|
||||||
const d = i + 1;
|
|
||||||
const incoming = 8 + ((i * 3) % 7);
|
|
||||||
const won = Math.max(1, Math.round(incoming * (0.22 + (i % 5) * 0.04)));
|
|
||||||
return {
|
|
||||||
key: `1405-08-${String(d).padStart(2, '0')}`,
|
|
||||||
label: `${toFaDigits(d)} آبان`,
|
|
||||||
incomingCount: incoming,
|
|
||||||
wonCount: won,
|
|
||||||
conversionPercent: Math.round((won / incoming) * 100),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const trend = days.map((d, i) => ({
|
|
||||||
key: d.key,
|
|
||||||
label: d.label,
|
|
||||||
value: 18_000_000 + i * 4_500_000 + (i % 3) * 2_000_000,
|
|
||||||
}));
|
|
||||||
return {
|
|
||||||
from: '2026-10-23',
|
|
||||||
to: '2026-11-04',
|
|
||||||
currency: 'IRR',
|
|
||||||
kpis: [
|
|
||||||
{
|
|
||||||
key: 'new_customers',
|
|
||||||
label: 'مشتریان جدید',
|
|
||||||
value: 42,
|
|
||||||
format: 'number',
|
|
||||||
changePercent: 18,
|
|
||||||
changeLabel: 'نسبت به دوره قبل',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'invoice_count',
|
|
||||||
label: 'تعداد فاکتور',
|
|
||||||
value: 67,
|
|
||||||
format: 'number',
|
|
||||||
changePercent: 9,
|
|
||||||
changeLabel: 'نسبت به دوره قبل',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'sales_amount',
|
|
||||||
label: 'مبلغ فروش',
|
|
||||||
value: 485_000_000,
|
|
||||||
format: 'money',
|
|
||||||
changePercent: 14,
|
|
||||||
changeLabel: 'نسبت به دوره قبل',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'avg_invoice',
|
|
||||||
label: 'میانگین فاکتور',
|
|
||||||
value: 7_240_000,
|
|
||||||
format: 'money',
|
|
||||||
changePercent: -3,
|
|
||||||
changeLabel: 'نسبت به دوره قبل',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'avg_sale',
|
|
||||||
label: 'میانگین فروش',
|
|
||||||
value: 12_100_000,
|
|
||||||
format: 'money',
|
|
||||||
changePercent: 6,
|
|
||||||
changeLabel: 'نسبت به دوره قبل',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
salesTrend: trend,
|
|
||||||
monthlyComparison: [
|
|
||||||
{ key: 'm1', label: 'مرداد', value: 320_000_000 },
|
|
||||||
{ key: 'm2', label: 'شهریور', value: 410_000_000 },
|
|
||||||
{ key: 'm3', label: 'مهر', value: 455_000_000 },
|
|
||||||
{ key: 'm4', label: 'آبان', value: 485_000_000 },
|
|
||||||
],
|
|
||||||
customerGrowth: [
|
|
||||||
{ key: 'g1', label: 'مرداد', value: 18 },
|
|
||||||
{ key: 'g2', label: 'شهریور', value: 24 },
|
|
||||||
{ key: 'g3', label: 'مهر', value: 31 },
|
|
||||||
{ key: 'g4', label: 'آبان', value: 42 },
|
|
||||||
],
|
|
||||||
sources: [
|
|
||||||
{ label: 'اینستاگرام', count: 28, percent: 34, amount: 120_000_000 },
|
|
||||||
{ label: 'معرفی', count: 19, percent: 23, amount: 150_000_000 },
|
|
||||||
{ label: 'سایت', count: 16, percent: 20, amount: 95_000_000 },
|
|
||||||
{ label: 'تبلیغات', count: 12, percent: 15, amount: 70_000_000 },
|
|
||||||
{ label: 'سایر', count: 7, percent: 8, amount: 50_000_000 },
|
|
||||||
],
|
|
||||||
lostReasons: [
|
|
||||||
{ label: 'قیمت', count: 11, percent: 40 },
|
|
||||||
{ label: 'رقیب', count: 8, percent: 29 },
|
|
||||||
{ label: 'زمانبندی', count: 5, percent: 18 },
|
|
||||||
{ label: 'عدم نیاز', count: 4, percent: 13 },
|
|
||||||
],
|
|
||||||
salesByCategory: [
|
|
||||||
{ label: 'نرمافزار', count: 22, percent: 45, amount: 220_000_000 },
|
|
||||||
{ label: 'سرویس', count: 15, percent: 30, amount: 145_000_000 },
|
|
||||||
{ label: 'سختافزار', count: 12, percent: 25, amount: 120_000_000 },
|
|
||||||
],
|
|
||||||
topProducts: [
|
|
||||||
{ name: 'پلن حرفهای', quantity: 18, amount: 180_000_000 },
|
|
||||||
{ name: 'پلن پایه', quantity: 24, amount: 96_000_000 },
|
|
||||||
{ name: 'پشتیبانی سالانه', quantity: 12, amount: 72_000_000 },
|
|
||||||
{ name: 'ماژول پیامک', quantity: 9, amount: 45_000_000 },
|
|
||||||
{ name: 'آموزش', quantity: 7, amount: 28_000_000 },
|
|
||||||
],
|
|
||||||
customerStatus: [
|
|
||||||
{ label: 'فعال', count: 86, percent: 58 },
|
|
||||||
{ label: 'در حال پیگیری', count: 34, percent: 23 },
|
|
||||||
{ label: 'راکد', count: 28, percent: 19 },
|
|
||||||
],
|
|
||||||
activities: {
|
|
||||||
open: 24,
|
|
||||||
done: 58,
|
|
||||||
overdue: 7,
|
|
||||||
byType: [
|
|
||||||
{ type: 'call', label: 'تماس', count: 32 },
|
|
||||||
{ type: 'meeting', label: 'جلسه', count: 18 },
|
|
||||||
{ type: 'task', label: 'وظیفه', count: 21 },
|
|
||||||
{ type: 'email', label: 'ایمیل', count: 11 },
|
|
||||||
{ type: 'note', label: 'یادداشت', count: 7 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
team: [
|
|
||||||
{
|
|
||||||
userId: 'u1',
|
|
||||||
fullName: 'مدیر نمونه',
|
|
||||||
wonAmount: 165_000_000,
|
|
||||||
wonCount: 9,
|
|
||||||
activitiesDone: 22,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
userId: 'u2',
|
|
||||||
fullName: 'کارشناس فروش نمونه',
|
|
||||||
wonAmount: 142_000_000,
|
|
||||||
wonCount: 11,
|
|
||||||
activitiesDone: 31,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
userId: 'u3',
|
|
||||||
fullName: 'آقای بنی لوحی',
|
|
||||||
wonAmount: 98_000_000,
|
|
||||||
wonCount: 6,
|
|
||||||
activitiesDone: 14,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
userId: 'u4',
|
|
||||||
fullName: 'مدیر سازمانی نمونه',
|
|
||||||
wonAmount: 80_000_000,
|
|
||||||
wonCount: 5,
|
|
||||||
activitiesDone: 10,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
insights: [
|
|
||||||
'نرخ تبدیل اینستاگرام ۱۸٪ بالاتر از میانگین منابع است؛ بودجه تبلیغ را اینجا نگه دارید.',
|
|
||||||
'مرحله «قیمت و نهایی کردن» بیشترین ریزش را دارد؛ اسکریپت تخفیف کنترلشده پیشنهاد میشود.',
|
|
||||||
'دو فروشنده بالای میانگین تیم هستند؛ جلسات کپیروش فروش را با آنها برگزار کنید.',
|
|
||||||
],
|
|
||||||
sales: {
|
|
||||||
openCount: 38,
|
|
||||||
wonCount: 31,
|
|
||||||
lostCount: 28,
|
|
||||||
openAmount: 620_000_000,
|
|
||||||
wonAmount: 485_000_000,
|
|
||||||
lostAmount: 210_000_000,
|
|
||||||
winRate: 52,
|
|
||||||
},
|
|
||||||
funnel: [
|
|
||||||
{
|
|
||||||
stageId: 's1',
|
|
||||||
stageName: 'لید جدید',
|
|
||||||
pipelineId: 'p1',
|
|
||||||
pipelineName: 'مسیر فروش اصلی',
|
|
||||||
count: 24,
|
|
||||||
amount: 180_000_000,
|
|
||||||
conversionFromPrevPercent: null,
|
|
||||||
shareOfPipelinePercent: 35,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
stageId: 's2',
|
|
||||||
stageName: 'اولین تماس',
|
|
||||||
pipelineId: 'p1',
|
|
||||||
pipelineName: 'مسیر فروش اصلی',
|
|
||||||
count: 16,
|
|
||||||
amount: 140_000_000,
|
|
||||||
conversionFromPrevPercent: 67,
|
|
||||||
shareOfPipelinePercent: 24,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
stageId: 's3',
|
|
||||||
stageName: 'مذاکرات اولیه',
|
|
||||||
pipelineId: 'p1',
|
|
||||||
pipelineName: 'مسیر فروش اصلی',
|
|
||||||
count: 12,
|
|
||||||
amount: 160_000_000,
|
|
||||||
conversionFromPrevPercent: 75,
|
|
||||||
shareOfPipelinePercent: 18,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
stageId: 's4',
|
|
||||||
stageName: 'پیشنهاد قیمت',
|
|
||||||
pipelineId: 'p1',
|
|
||||||
pipelineName: 'مسیر فروش اصلی',
|
|
||||||
count: 9,
|
|
||||||
amount: 190_000_000,
|
|
||||||
conversionFromPrevPercent: 75,
|
|
||||||
shareOfPipelinePercent: 13,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
stageId: 's5',
|
|
||||||
stageName: 'قیمت و نهایی کردن',
|
|
||||||
pipelineId: 'p1',
|
|
||||||
pipelineName: 'مسیر فروش اصلی',
|
|
||||||
count: 7,
|
|
||||||
amount: 210_000_000,
|
|
||||||
conversionFromPrevPercent: 78,
|
|
||||||
shareOfPipelinePercent: 10,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
conversionRate: {
|
|
||||||
incomingCount: 94,
|
|
||||||
wonCount: 31,
|
|
||||||
conversionPercent: 33,
|
|
||||||
previousIncomingCount: 81,
|
|
||||||
previousWonCount: 22,
|
|
||||||
previousConversionPercent: 27,
|
|
||||||
changePercent: 6,
|
|
||||||
byTime: days,
|
|
||||||
byUser: [
|
|
||||||
{
|
|
||||||
key: 'u2',
|
|
||||||
label: 'کارشناس فروش نمونه',
|
|
||||||
incomingCount: 34,
|
|
||||||
wonCount: 14,
|
|
||||||
conversionPercent: 41,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'u1',
|
|
||||||
label: 'مدیر نمونه',
|
|
||||||
incomingCount: 28,
|
|
||||||
wonCount: 9,
|
|
||||||
conversionPercent: 32,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'u3',
|
|
||||||
label: 'آقای بنی لوحی',
|
|
||||||
incomingCount: 20,
|
|
||||||
wonCount: 5,
|
|
||||||
conversionPercent: 25,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'u4',
|
|
||||||
label: 'مدیر سازمانی نمونه',
|
|
||||||
incomingCount: 12,
|
|
||||||
wonCount: 3,
|
|
||||||
conversionPercent: 25,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
bySource: [
|
|
||||||
{
|
|
||||||
key: 'ig',
|
|
||||||
label: 'اینستاگرام',
|
|
||||||
incomingCount: 30,
|
|
||||||
wonCount: 12,
|
|
||||||
conversionPercent: 40,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'ref',
|
|
||||||
label: 'معرفی',
|
|
||||||
incomingCount: 22,
|
|
||||||
wonCount: 9,
|
|
||||||
conversionPercent: 41,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'web',
|
|
||||||
label: 'سایت',
|
|
||||||
incomingCount: 24,
|
|
||||||
wonCount: 6,
|
|
||||||
conversionPercent: 25,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'ads',
|
|
||||||
label: 'تبلیغات',
|
|
||||||
incomingCount: 18,
|
|
||||||
wonCount: 4,
|
|
||||||
conversionPercent: 22,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
periodCompare: [
|
|
||||||
{
|
|
||||||
key: 'won_amount',
|
|
||||||
label: 'مبلغ فروش موفق',
|
|
||||||
format: 'money',
|
|
||||||
current: 485_000_000,
|
|
||||||
previous: 410_000_000,
|
|
||||||
changePercent: 18,
|
|
||||||
drillKind: 'period_won',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'won_count',
|
|
||||||
label: 'تعداد فروش موفق',
|
|
||||||
format: 'number',
|
|
||||||
current: 31,
|
|
||||||
previous: 26,
|
|
||||||
changePercent: 19,
|
|
||||||
drillKind: 'period_won',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'open_amount',
|
|
||||||
label: 'معاملات باز',
|
|
||||||
format: 'money',
|
|
||||||
current: 620_000_000,
|
|
||||||
previous: 540_000_000,
|
|
||||||
changePercent: 15,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'win_rate',
|
|
||||||
label: 'نرخ برد',
|
|
||||||
format: 'percent',
|
|
||||||
current: 52,
|
|
||||||
previous: 47,
|
|
||||||
changePercent: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'new_people',
|
|
||||||
label: 'مشتریان جدید',
|
|
||||||
format: 'number',
|
|
||||||
current: 42,
|
|
||||||
previous: 35,
|
|
||||||
changePercent: 20,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
tickets: {
|
|
||||||
open: 6,
|
|
||||||
resolved: 14,
|
|
||||||
closed: 9,
|
|
||||||
slaBreached: 1,
|
|
||||||
avgCsat: 4.2,
|
|
||||||
csatCount: 11,
|
|
||||||
},
|
|
||||||
peopleCreated: 42,
|
|
||||||
invoiceCount: 67,
|
|
||||||
invoiceAmount: 485_000_000,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const PERIODS: Array<{ key: PeriodKey; label: string }> = [
|
const PERIODS: Array<{ key: PeriodKey; label: string }> = [
|
||||||
{ key: 'today', label: 'امروز' },
|
{ key: 'today', label: 'امروز' },
|
||||||
@@ -1201,14 +858,7 @@ onMounted(() => {
|
|||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
const useMock = route.query.mock !== '0';
|
|
||||||
try {
|
try {
|
||||||
if (useMock) {
|
|
||||||
const orgRes = await api.get<OrganizationDto>('/organizations/current');
|
|
||||||
org.value = orgRes.data;
|
|
||||||
data.value = buildMockReportsDashboard();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const [dash, orgRes] = await Promise.all([
|
const [dash, orgRes] = await Promise.all([
|
||||||
api.get<ReportsDashboardDto>('/reports/dashboard', {
|
api.get<ReportsDashboardDto>('/reports/dashboard', {
|
||||||
params: {
|
params: {
|
||||||
@@ -1221,11 +871,6 @@ async function load() {
|
|||||||
data.value = dash.data;
|
data.value = dash.data;
|
||||||
org.value = orgRes.data;
|
org.value = orgRes.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (useMock) {
|
|
||||||
data.value = buildMockReportsDashboard();
|
|
||||||
error.value = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
error.value = axios.isAxiosError(e)
|
error.value = axios.isAxiosError(e)
|
||||||
? String(
|
? String(
|
||||||
(e.response?.data as { message?: string })?.message ??
|
(e.response?.data as { message?: string })?.message ??
|
||||||
|
|||||||
@@ -1,358 +0,0 @@
|
|||||||
import type {
|
|
||||||
ActivityDto,
|
|
||||||
HomeDashboardDto,
|
|
||||||
PipelineBoardDto,
|
|
||||||
} from '@mento/shared';
|
|
||||||
|
|
||||||
/** پیشنمایش داشبورد خانه وقتی API خالی است یا FORCE روشن باشد */
|
|
||||||
export const HOME_DASHBOARD_MOCK: HomeDashboardDto = {
|
|
||||||
currency: 'IRR',
|
|
||||||
kpis: [
|
|
||||||
{
|
|
||||||
key: 'sales',
|
|
||||||
label: 'فروش این ماه',
|
|
||||||
value: 186_500_000,
|
|
||||||
changePercent: 18.4,
|
|
||||||
changeLabel: 'نسبت به ماه قبل',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'customers',
|
|
||||||
label: 'مشتریان جدید',
|
|
||||||
value: 42,
|
|
||||||
changePercent: 12.5,
|
|
||||||
changeLabel: 'نسبت به ماه قبل',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'open-deals',
|
|
||||||
label: 'معاملات باز',
|
|
||||||
value: 27,
|
|
||||||
changePercent: -4.2,
|
|
||||||
changeLabel: 'نسبت به هفته قبل',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'followups',
|
|
||||||
label: 'پیگیری امروز',
|
|
||||||
value: 9,
|
|
||||||
changePercent: 25,
|
|
||||||
changeLabel: 'نسبت به دیروز',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'win-rate',
|
|
||||||
label: 'نرخ برد',
|
|
||||||
value: 58.3,
|
|
||||||
changePercent: null,
|
|
||||||
changeLabel: '۱۲ ماه اخیر',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'overdue',
|
|
||||||
label: 'عقبافتاده',
|
|
||||||
value: 6,
|
|
||||||
changePercent: null,
|
|
||||||
changeLabel: 'فعالیت باز',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
alerts: [
|
|
||||||
{
|
|
||||||
key: 'stale-deal',
|
|
||||||
tone: 'urgent',
|
|
||||||
text: 'شرکت نمونه آکمه در مرحله تصمیمگیری است — امروز تماس بگیرید',
|
|
||||||
link: '/pipeline',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'stale-count',
|
|
||||||
tone: 'warning',
|
|
||||||
text: '۵ معامله بیش از ۵ روز پیگیری نشده',
|
|
||||||
link: '/pipeline',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'overdue-acts',
|
|
||||||
tone: 'warning',
|
|
||||||
text: '۶ پیگیری عقبافتاده دارید — همین امروز ببندید',
|
|
||||||
link: '/activities',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'target-close',
|
|
||||||
tone: 'info',
|
|
||||||
text: 'به تارگت ماه نزدیک شدهاید',
|
|
||||||
link: '/sales/forecast',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'big-deal',
|
|
||||||
tone: 'info',
|
|
||||||
text: 'پیشنهاد ۱۱۲ میلیون تومانی سپهر در انتظار تأیید است',
|
|
||||||
link: '/pipeline',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'win-streak',
|
|
||||||
tone: 'success',
|
|
||||||
text: 'این هفته ۳ معامله برنده بستهاید',
|
|
||||||
link: '/reports',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'meeting-today',
|
|
||||||
tone: 'urgent',
|
|
||||||
text: 'جلسه دمو با پارسنوین ساعت ۱۴ — آمادهسازی پیشنهاد',
|
|
||||||
link: '/activities',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'quote-expire',
|
|
||||||
tone: 'warning',
|
|
||||||
text: 'پیشفاکتور صنایع سپهر تا فردا منقضی میشود',
|
|
||||||
link: '/pipeline',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'new-lead',
|
|
||||||
tone: 'info',
|
|
||||||
text: '۴ لید جدید از فرم وبسایت بدون مالک ماندهاند',
|
|
||||||
link: '/contacts',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'renewal',
|
|
||||||
tone: 'info',
|
|
||||||
text: 'قرارداد تمدید سارا محمدی این ماه سررسید میشود',
|
|
||||||
link: '/pipeline',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
salesTrend: [
|
|
||||||
{ monthKey: '1403-07', monthLabel: 'مهر', amount: 64_000_000 },
|
|
||||||
{ monthKey: '1403-08', monthLabel: 'آبان', amount: 71_500_000 },
|
|
||||||
{ monthKey: '1403-09', monthLabel: 'آذر', amount: 68_200_000 },
|
|
||||||
{ monthKey: '1403-10', monthLabel: 'دی', amount: 79_800_000 },
|
|
||||||
{ monthKey: '1403-11', monthLabel: 'بهمن', amount: 88_400_000 },
|
|
||||||
{ monthKey: '1403-12', monthLabel: 'اسفند', amount: 95_000_000 },
|
|
||||||
{ monthKey: '1404-01', monthLabel: 'فروردین', amount: 82_600_000 },
|
|
||||||
{ monthKey: '1404-02', monthLabel: 'اردیبهشت', amount: 92_000_000 },
|
|
||||||
{ monthKey: '1404-03', monthLabel: 'خرداد', amount: 118_500_000 },
|
|
||||||
{ monthKey: '1404-04', monthLabel: 'تیر', amount: 141_200_000 },
|
|
||||||
{ monthKey: '1404-05', monthLabel: 'مرداد', amount: 157_800_000 },
|
|
||||||
{ monthKey: '1404-06', monthLabel: 'شهریور', amount: 186_500_000 },
|
|
||||||
],
|
|
||||||
customerGrowth: [
|
|
||||||
{ monthKey: '1403-07', monthLabel: 'مهر', value: 11 },
|
|
||||||
{ monthKey: '1403-08', monthLabel: 'آبان', value: 14 },
|
|
||||||
{ monthKey: '1403-09', monthLabel: 'آذر', value: 13 },
|
|
||||||
{ monthKey: '1403-10', monthLabel: 'دی', value: 16 },
|
|
||||||
{ monthKey: '1403-11', monthLabel: 'بهمن', value: 19 },
|
|
||||||
{ monthKey: '1403-12', monthLabel: 'اسفند', value: 22 },
|
|
||||||
{ monthKey: '1404-01', monthLabel: 'فروردین', value: 15 },
|
|
||||||
{ monthKey: '1404-02', monthLabel: 'اردیبهشت', value: 18 },
|
|
||||||
{ monthKey: '1404-03', monthLabel: 'خرداد', value: 24 },
|
|
||||||
{ monthKey: '1404-04', monthLabel: 'تیر', value: 31 },
|
|
||||||
{ monthKey: '1404-05', monthLabel: 'مرداد', value: 28 },
|
|
||||||
{ monthKey: '1404-06', monthLabel: 'شهریور', value: 42 },
|
|
||||||
],
|
|
||||||
sources: [
|
|
||||||
{ label: 'فرم لید', count: 38, percent: 32.2 },
|
|
||||||
{ label: 'معرفی دوستان', count: 29, percent: 24.6 },
|
|
||||||
{ label: 'وبسایت', count: 21, percent: 17.8 },
|
|
||||||
{ label: 'اکسل', count: 16, percent: 13.6 },
|
|
||||||
{ label: 'تلفن', count: 8, percent: 6.8 },
|
|
||||||
{ label: 'سایر', count: 6, percent: 5 },
|
|
||||||
],
|
|
||||||
topCustomers: [
|
|
||||||
{ id: 'mock-c1', name: 'شرکت آکمه', dealCount: 7, rank: 1 },
|
|
||||||
{ id: 'mock-c2', name: 'گروه پارسنوین', dealCount: 5, rank: 2 },
|
|
||||||
{ id: 'mock-c3', name: 'سارا محمدی', dealCount: 4, rank: 3 },
|
|
||||||
{ id: 'mock-c4', name: 'صنایع سپهر', dealCount: 3, rank: 4 },
|
|
||||||
{ id: 'mock-c5', name: 'رضا کریمی', dealCount: 3, rank: 5 },
|
|
||||||
],
|
|
||||||
salesSummary: {
|
|
||||||
openCount: 27,
|
|
||||||
wonCount: 35,
|
|
||||||
lostCount: 25,
|
|
||||||
openAmount: 412_000_000,
|
|
||||||
wonAmount: 696_000_000,
|
|
||||||
lostAmount: 218_000_000,
|
|
||||||
winRate: 58.3,
|
|
||||||
},
|
|
||||||
funnel: [
|
|
||||||
{ stageId: 's1', stageName: 'تماس اول', count: 11, amount: 95_000_000 },
|
|
||||||
{ stageId: 's2', stageName: 'نیازسنجی', count: 8, amount: 128_000_000 },
|
|
||||||
{ stageId: 's3', stageName: 'پیشنهاد', count: 5, amount: 112_000_000 },
|
|
||||||
{ stageId: 's4', stageName: 'مذاکره', count: 3, amount: 77_000_000 },
|
|
||||||
],
|
|
||||||
lostReasons: [
|
|
||||||
{ label: 'قیمت', count: 9, percent: 36, amount: 82_000_000 },
|
|
||||||
{ label: 'رقیب', count: 7, percent: 28, amount: 61_000_000 },
|
|
||||||
{ label: 'زمانبندی', count: 5, percent: 20, amount: 44_000_000 },
|
|
||||||
{ label: 'بدون دلیل ثبتشده', count: 4, percent: 16, amount: 31_000_000 },
|
|
||||||
],
|
|
||||||
activityByType: [
|
|
||||||
{ type: 'call', label: 'تماس', count: 14 },
|
|
||||||
{ type: 'meeting', label: 'جلسه', count: 7 },
|
|
||||||
{ type: 'task', label: 'کار', count: 11 },
|
|
||||||
{ type: 'note', label: 'یادداشت', count: 4 },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
function hoursFromNow(hours: number) {
|
|
||||||
const d = new Date();
|
|
||||||
d.setHours(d.getHours() + hours);
|
|
||||||
return d.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HOME_ACTIVITIES_MOCK = [
|
|
||||||
{
|
|
||||||
id: 'mock-act-1',
|
|
||||||
type: 'call',
|
|
||||||
status: 'open',
|
|
||||||
priority: 'high',
|
|
||||||
title: 'تماس پیگیری پیشنهاد آکمه',
|
|
||||||
description: null,
|
|
||||||
dueAt: hoursFromNow(3),
|
|
||||||
durationMinutes: 30,
|
|
||||||
reminderAt: null,
|
|
||||||
reminderNotifyApp: false,
|
|
||||||
reminderNotifySms: false,
|
|
||||||
reminderSentAt: null,
|
|
||||||
completedAt: null,
|
|
||||||
tags: [],
|
|
||||||
person: { id: 'p1', fullName: 'علی رضایی' },
|
|
||||||
company: { id: 'c1', name: 'شرکت آکمه' },
|
|
||||||
deal: null,
|
|
||||||
assignee: null,
|
|
||||||
assignees: [],
|
|
||||||
createdBy: null,
|
|
||||||
createdAt: hoursFromNow(-20),
|
|
||||||
updatedAt: hoursFromNow(-2),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mock-act-2',
|
|
||||||
type: 'meeting',
|
|
||||||
status: 'open',
|
|
||||||
priority: 'medium',
|
|
||||||
title: 'جلسه دمو با پارسنوین',
|
|
||||||
description: null,
|
|
||||||
dueAt: hoursFromNow(26),
|
|
||||||
durationMinutes: 60,
|
|
||||||
reminderAt: null,
|
|
||||||
reminderNotifyApp: false,
|
|
||||||
reminderNotifySms: false,
|
|
||||||
reminderSentAt: null,
|
|
||||||
completedAt: null,
|
|
||||||
tags: [],
|
|
||||||
person: { id: 'p2', fullName: 'مریم احمدی' },
|
|
||||||
company: { id: 'c2', name: 'گروه پارسنوین' },
|
|
||||||
deal: null,
|
|
||||||
assignee: null,
|
|
||||||
assignees: [],
|
|
||||||
createdBy: null,
|
|
||||||
createdAt: hoursFromNow(-48),
|
|
||||||
updatedAt: hoursFromNow(-10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mock-act-3',
|
|
||||||
type: 'task',
|
|
||||||
status: 'done',
|
|
||||||
priority: 'low',
|
|
||||||
title: 'ارسال پیشفاکتور سپهر',
|
|
||||||
description: null,
|
|
||||||
dueAt: hoursFromNow(-5),
|
|
||||||
durationMinutes: null,
|
|
||||||
reminderAt: null,
|
|
||||||
reminderNotifyApp: false,
|
|
||||||
reminderNotifySms: false,
|
|
||||||
reminderSentAt: null,
|
|
||||||
completedAt: hoursFromNow(-4),
|
|
||||||
tags: [],
|
|
||||||
person: null,
|
|
||||||
company: { id: 'c3', name: 'صنایع سپهر' },
|
|
||||||
deal: null,
|
|
||||||
assignee: null,
|
|
||||||
assignees: [],
|
|
||||||
createdBy: null,
|
|
||||||
createdAt: hoursFromNow(-72),
|
|
||||||
updatedAt: hoursFromNow(-4),
|
|
||||||
},
|
|
||||||
] as unknown as ActivityDto[];
|
|
||||||
|
|
||||||
export const HOME_PIPELINE_MOCK = {
|
|
||||||
pipeline: {
|
|
||||||
id: 'mock-pipe',
|
|
||||||
name: 'فروش اصلی',
|
|
||||||
isDefault: true,
|
|
||||||
createdAt: hoursFromNow(-1000),
|
|
||||||
updatedAt: hoursFromNow(-1),
|
|
||||||
},
|
|
||||||
stages: [
|
|
||||||
{
|
|
||||||
id: 's1',
|
|
||||||
name: 'تماس اول',
|
|
||||||
position: 0,
|
|
||||||
isWon: false,
|
|
||||||
isLost: false,
|
|
||||||
totalAmount: 95_000_000,
|
|
||||||
deals: [
|
|
||||||
{
|
|
||||||
id: 'd1',
|
|
||||||
title: 'لایسنس سازمانی',
|
|
||||||
amount: 45_000_000,
|
|
||||||
currency: 'IRR',
|
|
||||||
company: { id: 'c1', name: 'شرکت آکمه' },
|
|
||||||
person: null,
|
|
||||||
updatedAt: hoursFromNow(-30),
|
|
||||||
createdAt: hoursFromNow(-200),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'd2',
|
|
||||||
title: 'پکیج پشتیبانی',
|
|
||||||
amount: 22_000_000,
|
|
||||||
currency: 'IRR',
|
|
||||||
company: null,
|
|
||||||
person: { id: 'p3', fullName: 'سارا محمدی' },
|
|
||||||
updatedAt: hoursFromNow(-8),
|
|
||||||
createdAt: hoursFromNow(-90),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 's2',
|
|
||||||
name: 'نیازسنجی',
|
|
||||||
position: 1,
|
|
||||||
isWon: false,
|
|
||||||
isLost: false,
|
|
||||||
totalAmount: 128_000_000,
|
|
||||||
deals: [
|
|
||||||
{
|
|
||||||
id: 'd3',
|
|
||||||
title: 'ارتقای CRM',
|
|
||||||
amount: 68_000_000,
|
|
||||||
currency: 'IRR',
|
|
||||||
company: { id: 'c2', name: 'گروه پارسنوین' },
|
|
||||||
person: null,
|
|
||||||
updatedAt: hoursFromNow(-12),
|
|
||||||
createdAt: hoursFromNow(-140),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 's3',
|
|
||||||
name: 'پیشنهاد',
|
|
||||||
position: 2,
|
|
||||||
isWon: false,
|
|
||||||
isLost: false,
|
|
||||||
totalAmount: 112_000_000,
|
|
||||||
deals: [
|
|
||||||
{
|
|
||||||
id: 'd4',
|
|
||||||
title: 'پروژه یکپارچهسازی',
|
|
||||||
amount: 112_000_000,
|
|
||||||
currency: 'IRR',
|
|
||||||
company: { id: 'c3', name: 'صنایع سپهر' },
|
|
||||||
person: null,
|
|
||||||
updatedAt: hoursFromNow(-2),
|
|
||||||
createdAt: hoursFromNow(-60),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as unknown as PipelineBoardDto;
|
|
||||||
|
|
||||||
export function isHomeDashboardEmpty(data: HomeDashboardDto | null | undefined): boolean {
|
|
||||||
if (!data) return true;
|
|
||||||
const salesFlat = (data.salesTrend ?? []).every((p) => !p.amount);
|
|
||||||
const growthFlat = (data.customerGrowth ?? []).every((p) => !p.value);
|
|
||||||
const noSources = !(data.sources ?? []).length;
|
|
||||||
const kpisFlat = (data.kpis ?? []).every((k) => !k.value);
|
|
||||||
return (salesFlat && growthFlat && noSources) || kpisFlat;
|
|
||||||
}
|
|
||||||
@@ -45,11 +45,8 @@ import type {
|
|||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
import VChart from 'vue-echarts';
|
import VChart from 'vue-echarts';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
|
import axios from 'axios';
|
||||||
import type { TreasuryDashboardDto } from '@mento/shared';
|
import type { TreasuryDashboardDto } from '@mento/shared';
|
||||||
import {
|
|
||||||
TREASURY_DASHBOARD_MOCK,
|
|
||||||
isTreasuryDashboardEmpty,
|
|
||||||
} from './treasury-dashboard-mock';
|
|
||||||
import { toman } from './treasury-format';
|
import { toman } from './treasury-format';
|
||||||
|
|
||||||
use([CanvasRenderer, BarChart, PieChart, GridComponent, TooltipComponent, LegendComponent]);
|
use([CanvasRenderer, BarChart, PieChart, GridComponent, TooltipComponent, LegendComponent]);
|
||||||
@@ -92,7 +89,6 @@ type MetricCard = {
|
|||||||
const data = ref<TreasuryDashboardDto | null>(null);
|
const data = ref<TreasuryDashboardDto | null>(null);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const usingMock = ref(false);
|
|
||||||
|
|
||||||
const heroCards: MetricCard[] = [
|
const heroCards: MetricCard[] = [
|
||||||
{
|
{
|
||||||
@@ -288,19 +284,18 @@ const mixChartOption = computed<ChartOption>(() => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
usingMock.value = false;
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
const res = await api.get<TreasuryDashboardDto>('/treasury/dashboard');
|
const res = await api.get<TreasuryDashboardDto>('/treasury/dashboard');
|
||||||
if (isTreasuryDashboardEmpty(res.data)) {
|
data.value = res.data;
|
||||||
data.value = TREASURY_DASHBOARD_MOCK;
|
} catch (e) {
|
||||||
usingMock.value = true;
|
data.value = null;
|
||||||
} else {
|
error.value = axios.isAxiosError(e)
|
||||||
data.value = res.data;
|
? String(
|
||||||
}
|
(e.response?.data as { message?: string })?.message ??
|
||||||
} catch {
|
'بارگذاری داشبورد خزانه ناموفق بود.',
|
||||||
data.value = TREASURY_DASHBOARD_MOCK;
|
)
|
||||||
usingMock.value = true;
|
: 'بارگذاری داشبورد خزانه ناموفق بود.';
|
||||||
error.value = '';
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -311,13 +306,6 @@ onMounted(async () => {
|
|||||||
<div>
|
<div>
|
||||||
<a-flex vertical :gap="token.margin">
|
<a-flex vertical :gap="token.margin">
|
||||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||||
<a-alert
|
|
||||||
v-if="usingMock"
|
|
||||||
type="info"
|
|
||||||
show-icon
|
|
||||||
message="در حال نمایش دادهٔ نمونه (ماک) برای پیشنمایش داشبورد هستید."
|
|
||||||
description="وقتی سند و حساب واقعی ثبت شود، همین صفحه دادهٔ واقعی API را نشان میدهد."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CrmSkeleton :loading="loading">
|
<CrmSkeleton :loading="loading">
|
||||||
<a-row :gutter="[token.marginSM, token.marginSM]">
|
<a-row :gutter="[token.marginSM, token.marginSM]">
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
import type {
|
|
||||||
FinancialDocumentDto,
|
|
||||||
FinancialInstallmentDto,
|
|
||||||
TreasuryDashboardDto,
|
|
||||||
} from '@mento/shared';
|
|
||||||
|
|
||||||
function daysFromNow(days: number) {
|
|
||||||
const d = new Date();
|
|
||||||
d.setHours(12, 0, 0, 0);
|
|
||||||
d.setDate(d.getDate() + days);
|
|
||||||
return d.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function doc(
|
|
||||||
partial: Pick<
|
|
||||||
FinancialDocumentDto,
|
|
||||||
'id' | 'kind' | 'title' | 'amount' | 'settledAmount' | 'remaining' | 'status' | 'categoryName' | 'partyName'
|
|
||||||
> &
|
|
||||||
Partial<FinancialDocumentDto>,
|
|
||||||
): FinancialDocumentDto {
|
|
||||||
return {
|
|
||||||
bookedAt: daysFromNow(-3),
|
|
||||||
dueAt: daysFromNow(10),
|
|
||||||
categoryId: null,
|
|
||||||
personId: null,
|
|
||||||
accountId: null,
|
|
||||||
accountName: 'صندوق اصلی',
|
|
||||||
method: 'transfer',
|
|
||||||
reference: null,
|
|
||||||
notes: null,
|
|
||||||
createdAt: daysFromNow(-2),
|
|
||||||
...partial,
|
|
||||||
partyKind: partial.partyKind ?? null,
|
|
||||||
companyId: partial.companyId ?? null,
|
|
||||||
userId: partial.userId ?? null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function installment(
|
|
||||||
partial: FinancialInstallmentDto,
|
|
||||||
): FinancialInstallmentDto {
|
|
||||||
return partial;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** دیتای نمایشی برای پیشنمایش داشبورد وقتی API خالی است */
|
|
||||||
export const TREASURY_DASHBOARD_MOCK: TreasuryDashboardDto = {
|
|
||||||
bookedIncome: '185000000',
|
|
||||||
actualReceipts: '142500000',
|
|
||||||
bookedExpense: '98000000',
|
|
||||||
actualPayments: '87500000',
|
|
||||||
operatingProfit: '87000000',
|
|
||||||
receivables: '42500000',
|
|
||||||
payables: '18500000',
|
|
||||||
totalBalance: '256000000',
|
|
||||||
upcomingInstallments: [
|
|
||||||
installment({
|
|
||||||
id: 'mock-inst-1',
|
|
||||||
position: 2,
|
|
||||||
dueDate: daysFromNow(-2),
|
|
||||||
amount: '12000000',
|
|
||||||
paidAmount: '0',
|
|
||||||
remaining: '12000000',
|
|
||||||
status: 'overdue',
|
|
||||||
}),
|
|
||||||
installment({
|
|
||||||
id: 'mock-inst-2',
|
|
||||||
position: 3,
|
|
||||||
dueDate: daysFromNow(5),
|
|
||||||
amount: '12000000',
|
|
||||||
paidAmount: '4000000',
|
|
||||||
remaining: '8000000',
|
|
||||||
status: 'partial',
|
|
||||||
}),
|
|
||||||
installment({
|
|
||||||
id: 'mock-inst-3',
|
|
||||||
position: 4,
|
|
||||||
dueDate: daysFromNow(35),
|
|
||||||
amount: '12000000',
|
|
||||||
paidAmount: '0',
|
|
||||||
remaining: '12000000',
|
|
||||||
status: 'open',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
upcomingPayables: [
|
|
||||||
doc({
|
|
||||||
id: 'mock-pay-1',
|
|
||||||
kind: 'expense',
|
|
||||||
title: 'اجاره دفتر — مهر',
|
|
||||||
amount: '45000000',
|
|
||||||
settledAmount: '30000000',
|
|
||||||
remaining: '15000000',
|
|
||||||
status: 'partial',
|
|
||||||
categoryName: 'اجاره',
|
|
||||||
partyName: 'موجر ساختمان',
|
|
||||||
}),
|
|
||||||
doc({
|
|
||||||
id: 'mock-pay-2',
|
|
||||||
kind: 'expense',
|
|
||||||
title: 'بیمه تکمیلی کارکنان',
|
|
||||||
amount: '8500000',
|
|
||||||
settledAmount: '5000000',
|
|
||||||
remaining: '3500000',
|
|
||||||
status: 'partial',
|
|
||||||
categoryName: 'بیمه',
|
|
||||||
partyName: 'بیمه ایران',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
recent: [
|
|
||||||
doc({
|
|
||||||
id: 'mock-doc-1',
|
|
||||||
kind: 'income',
|
|
||||||
title: 'پیشپرداخت پروژه آلفا',
|
|
||||||
amount: '75000000',
|
|
||||||
settledAmount: '75000000',
|
|
||||||
remaining: '0',
|
|
||||||
status: 'settled',
|
|
||||||
categoryName: 'فروش خدمات',
|
|
||||||
partyName: 'شرکت آریا',
|
|
||||||
bookedAt: daysFromNow(-1),
|
|
||||||
}),
|
|
||||||
doc({
|
|
||||||
id: 'mock-doc-2',
|
|
||||||
kind: 'expense',
|
|
||||||
title: 'خرید تجهیزات شبکه',
|
|
||||||
amount: '18500000',
|
|
||||||
settledAmount: '18500000',
|
|
||||||
remaining: '0',
|
|
||||||
status: 'settled',
|
|
||||||
categoryName: 'تجهیزات',
|
|
||||||
partyName: 'فروشگاه سختافزار',
|
|
||||||
bookedAt: daysFromNow(-2),
|
|
||||||
}),
|
|
||||||
doc({
|
|
||||||
id: 'mock-doc-3',
|
|
||||||
kind: 'income',
|
|
||||||
title: 'قسط قرارداد بتا',
|
|
||||||
amount: '12000000',
|
|
||||||
settledAmount: '0',
|
|
||||||
remaining: '12000000',
|
|
||||||
status: 'open',
|
|
||||||
categoryName: 'فروش خدمات',
|
|
||||||
partyName: 'گروه سپهر',
|
|
||||||
bookedAt: daysFromNow(-4),
|
|
||||||
}),
|
|
||||||
doc({
|
|
||||||
id: 'mock-doc-4',
|
|
||||||
kind: 'expense',
|
|
||||||
title: 'حقوق مهر — تیم فروش',
|
|
||||||
amount: '42000000',
|
|
||||||
settledAmount: '42000000',
|
|
||||||
remaining: '0',
|
|
||||||
status: 'settled',
|
|
||||||
categoryName: 'حقوق',
|
|
||||||
partyName: null,
|
|
||||||
bookedAt: daysFromNow(-5),
|
|
||||||
}),
|
|
||||||
doc({
|
|
||||||
id: 'mock-doc-5',
|
|
||||||
kind: 'income',
|
|
||||||
title: 'فروش پکیج پشتیبانی',
|
|
||||||
amount: '28000000',
|
|
||||||
settledAmount: '10000000',
|
|
||||||
remaining: '18000000',
|
|
||||||
status: 'partial',
|
|
||||||
categoryName: 'پشتیبانی',
|
|
||||||
partyName: 'کارخانه نوین',
|
|
||||||
bookedAt: daysFromNow(-6),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isTreasuryDashboardEmpty(data: TreasuryDashboardDto | null | undefined) {
|
|
||||||
if (!data) return true;
|
|
||||||
const keys = [
|
|
||||||
data.bookedIncome,
|
|
||||||
data.actualReceipts,
|
|
||||||
data.bookedExpense,
|
|
||||||
data.actualPayments,
|
|
||||||
data.totalBalance,
|
|
||||||
data.receivables,
|
|
||||||
data.payables,
|
|
||||||
] as const;
|
|
||||||
const allZero = keys.every((v) => !Number(v));
|
|
||||||
return (
|
|
||||||
allZero &&
|
|
||||||
data.recent.length === 0 &&
|
|
||||||
data.upcomingInstallments.length === 0 &&
|
|
||||||
data.upcomingPayables.length === 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user