import { and, asc, desc, eq, inArray, isNull } from 'drizzle-orm'; import { endOfDay, endOfWeek, formatISO, isAfter, isBefore, isSameDay, startOfDay, startOfWeek } from 'date-fns'; import { crmApprovalRequests, crmCustomers, crmEnquiries, crmEnquiryFollowups, crmQuotationFollowups, crmQuotations, memberships, users } from '@/db/schema'; import { db } from '@/lib/db'; import { PERMISSIONS } from '@/lib/auth/rbac'; import { getUserBranches } from '@/features/foundation/branch-scope/service'; import { listApprovalRequests } from '@/features/foundation/approval/server/service'; import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service'; import { getRevenueByBillingCustomer, getRevenueByConsultant, getRevenueByContractor, getRevenueByEndCustomer } from '@/features/crm/reporting/server/service'; import type { CrmDashboardApprovalRow, CrmDashboardApprovalSummary, CrmDashboardFilters, CrmDashboardFollowupRow, CrmDashboardFollowupSummary, CrmDashboardFunnelStep, CrmDashboardHotProjectRow, CrmDashboardReferenceData, CrmDashboardResponse, CrmDashboardRevenueRow, CrmDashboardSalesRankingRow, CrmDashboardSummary } from '../api/types'; const ENQUIRY_STATUS_CATEGORY = 'crm_enquiry_status'; const QUOTATION_STATUS_CATEGORY = 'crm_quotation_status'; const PRODUCT_TYPE_CATEGORY = 'crm_product_type'; const PROJECT_PARTY_ROLE_CATEGORY = 'crm_project_party_role'; type DashboardContext = { organizationId: string; userId: string; membershipRole: string; businessRole: string; permissions: string[]; }; type NormalizedFilters = { dateFrom: string; dateTo: string; branch: string | null; salesman: string | null; projectPartyRole: string | null; productType: string | null; }; type StatusMaps = { enquiryStatusById: Map; quotationStatusById: Map; }; const DASHBOARD_FULL_ACCESS_ROLES = new Set([ 'marketing', 'sales_manager', 'department_manager', 'top_manager' ]); const DASHBOARD_OWNED_SCOPE_ROLES = new Set(['sales', 'sales_support']); function normalizeDashboardFilters(filters: CrmDashboardFilters): NormalizedFilters { const now = new Date(); const monthStart = startOfDay(new Date(now.getFullYear(), now.getMonth(), 1)); return { dateFrom: filters.dateFrom ?? formatISO(monthStart, { representation: 'date' }), dateTo: filters.dateTo ?? formatISO(now, { representation: 'date' }), branch: filters.branch ?? null, salesman: filters.salesman ?? null, projectPartyRole: filters.projectPartyRole ?? null, productType: filters.productType ?? null }; } function toDayBounds(filters: NormalizedFilters) { return { from: startOfDay(new Date(filters.dateFrom)), to: endOfDay(new Date(filters.dateTo)) }; } function round2(value: number) { return Math.round(value * 100) / 100; } function average(values: number[]) { if (values.length === 0) { return 0; } return round2(values.reduce((sum, value) => sum + value, 0) / values.length); } function canViewCommercialData(context: DashboardContext) { return ( context.membershipRole === 'admin' || context.permissions.includes(PERMISSIONS.crmQuotationRead) ); } function canViewApprovalData(context: DashboardContext) { return ( context.membershipRole === 'admin' || context.permissions.includes(PERMISSIONS.crmApprovalRead) ); } function hasDashboardFullScope(context: DashboardContext) { return ( context.membershipRole === 'admin' || DASHBOARD_FULL_ACCESS_ROLES.has(context.businessRole) ); } function canAccessDashboardEnquiry( row: typeof crmEnquiries.$inferSelect, context: DashboardContext ) { if (hasDashboardFullScope(context)) { return true; } if (DASHBOARD_OWNED_SCOPE_ROLES.has(context.businessRole)) { return row.createdBy === context.userId || row.assignedToUserId === context.userId; } return true; } function canAccessDashboardQuotation( row: typeof crmQuotations.$inferSelect, context: DashboardContext ) { if (hasDashboardFullScope(context)) { return true; } if (DASHBOARD_OWNED_SCOPE_ROLES.has(context.businessRole)) { return row.salesmanId === context.userId; } return true; } async function getStatusMaps(organizationId: string): Promise { const [enquiryOptions, quotationOptions] = await Promise.all([ getActiveOptionsByCategory(ENQUIRY_STATUS_CATEGORY, { organizationId }), getActiveOptionsByCategory(QUOTATION_STATUS_CATEGORY, { organizationId }) ]); return { enquiryStatusById: new Map(enquiryOptions.map((item) => [item.id, item.code])), quotationStatusById: new Map(quotationOptions.map((item) => [item.id, item.code])) }; } async function getReferenceData(organizationId: string): Promise { const [branches, salesmen, productTypes, projectPartyRoles] = await Promise.all([ getUserBranches(), db .select({ id: users.id, name: users.name }) .from(memberships) .innerJoin(users, eq(memberships.userId, users.id)) .where(eq(memberships.organizationId, organizationId)) .orderBy(asc(users.name)), getActiveOptionsByCategory(PRODUCT_TYPE_CATEGORY, { organizationId }), getActiveOptionsByCategory(PROJECT_PARTY_ROLE_CATEGORY, { organizationId }) ]); return { branches: branches.map((item) => ({ id: item.id, code: item.code, label: item.name })), salesmen: salesmen.map((item) => ({ id: item.id, name: item.name })), productTypes: productTypes.map((item) => ({ id: item.id, code: item.code, label: item.label })), projectPartyRoles: projectPartyRoles.map((item) => ({ id: item.id, code: item.code, label: item.label })) }; } function matchesEnquiryFilters( row: typeof crmEnquiries.$inferSelect, filters: NormalizedFilters ) { const { from, to } = toDayBounds(filters); if (filters.branch && row.branchId !== filters.branch) return false; if (filters.salesman && row.assignedToUserId !== filters.salesman) return false; if (filters.productType && row.productType !== filters.productType) return false; if (isBefore(row.createdAt, from) || isAfter(row.createdAt, to)) return false; return true; } function matchesQuotationFilters( row: typeof crmQuotations.$inferSelect, filters: NormalizedFilters ) { const { from, to } = toDayBounds(filters); if (filters.branch && row.branchId !== filters.branch) return false; if (filters.salesman && row.salesmanId !== filters.salesman) return false; if (filters.productType && row.quotationType !== filters.productType) return false; if (isBefore(row.quotationDate, from) || isAfter(row.quotationDate, to)) return false; return true; } function isPipelineStage( enquiry: typeof crmEnquiries.$inferSelect, stage: 'lead' | 'enquiry' | 'closed_won' | 'closed_lost' ) { return enquiry.pipelineStage === stage; } async function loadScopedRows( context: DashboardContext, filters: NormalizedFilters ) { const [enquiries, quotations] = await Promise.all([ db .select() .from(crmEnquiries) .where( and(eq(crmEnquiries.organizationId, context.organizationId), isNull(crmEnquiries.deletedAt)) ), db .select() .from(crmQuotations) .where( and(eq(crmQuotations.organizationId, context.organizationId), isNull(crmQuotations.deletedAt)) ) ]); return { enquiries: enquiries .filter((row) => canAccessDashboardEnquiry(row, context)) .filter((row) => matchesEnquiryFilters(row, filters)), quotations: quotations .filter((row) => canAccessDashboardQuotation(row, context)) .filter((row) => matchesQuotationFilters(row, filters)) }; } function buildSummary( scopedEnquiries: Array, scopedQuotations: Array, statusMaps: StatusMaps ): CrmDashboardSummary { const now = new Date(); const leadRows = scopedEnquiries.filter((row) => isPipelineStage(row, 'lead')); const enquiryRows = scopedEnquiries.filter((row) => isPipelineStage(row, 'enquiry')); const wonRows = scopedEnquiries.filter((row) => isPipelineStage(row, 'closed_won')); const lostRows = scopedEnquiries.filter((row) => isPipelineStage(row, 'closed_lost')); const leadAges = leadRows.map((row) => (now.getTime() - row.createdAt.getTime()) / (1000 * 60 * 60 * 24)); const quotationStatusCodes = scopedQuotations.map((row) => statusMaps.quotationStatusById.get(row.status) ?? ''); return { lead: { leadCount: leadRows.length, newLeads: leadRows.length, unassignedLeads: leadRows.filter((row) => row.assignedToUserId === null).length, averageLeadAgingDays: average(leadAges) }, enquiry: { enquiryCount: enquiryRows.length, activeEnquiries: enquiryRows.length, wonCount: wonRows.length, lostCount: lostRows.length, hotEnquiries: enquiryRows.filter((row) => row.isHotProject).length, enquiryValue: round2(enquiryRows.reduce((sum, row) => sum + (row.estimatedValue ?? 0), 0)) }, quotation: { draftQuotations: quotationStatusCodes.filter((code) => code === 'draft').length, pendingApproval: quotationStatusCodes.filter((code) => code === 'pending_approval').length, approvedQuotations: quotationStatusCodes.filter((code) => code === 'approved').length, sentQuotations: quotationStatusCodes.filter((code) => code === 'sent').length }, revenue: { quotationValue: round2( scopedQuotations .filter((row) => (statusMaps.quotationStatusById.get(row.status) ?? '') !== 'cancelled') .reduce((sum, row) => sum + row.totalAmount, 0) ), wonValue: round2( scopedQuotations .filter((row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'accepted') .reduce((sum, row) => sum + row.totalAmount, 0) ), lostValue: round2( scopedQuotations .filter((row) => ['lost', 'rejected'].includes(statusMaps.quotationStatusById.get(row.status) ?? '') ) .reduce((sum, row) => sum + row.totalAmount, 0) ) } }; } function buildFunnel( summary: CrmDashboardSummary, scopedQuotations: Array, statusMaps: StatusMaps ): CrmDashboardFunnelStep[] { const steps = [ { key: 'lead', label: 'Lead', count: summary.lead.leadCount, value: 0 }, { key: 'enquiry', label: 'Enquiry', count: summary.enquiry.enquiryCount, value: summary.enquiry.enquiryValue }, { key: 'quotation', label: 'Quotation', count: scopedQuotations.filter( (row) => (statusMaps.quotationStatusById.get(row.status) ?? '') !== 'cancelled' ).length, value: scopedQuotations .filter((row) => (statusMaps.quotationStatusById.get(row.status) ?? '') !== 'cancelled') .reduce((sum, row) => sum + row.totalAmount, 0) }, { key: 'pending_approval', label: 'Pending Approval', count: scopedQuotations.filter( (row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'pending_approval' ).length, value: scopedQuotations .filter((row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'pending_approval') .reduce((sum, row) => sum + row.totalAmount, 0) }, { key: 'approved', label: 'Approved', count: scopedQuotations.filter( (row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'approved' ).length, value: scopedQuotations .filter((row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'approved') .reduce((sum, row) => sum + row.totalAmount, 0) }, { key: 'closed_won', label: 'Closed Won', count: summary.enquiry.wonCount, value: scopedQuotations .filter((row) => (statusMaps.quotationStatusById.get(row.status) ?? '') === 'accepted') .reduce((sum, row) => sum + row.totalAmount, 0) } ]; return steps.map((step, index) => ({ ...step, value: round2(step.value), conversionRate: index === 0 || steps[index - 1].count === 0 ? null : round2((step.count / steps[index - 1].count) * 100) })); } async function buildRevenueAnalytics( organizationId: string, filters: NormalizedFilters ) { const revenueFilters = { dateFrom: filters.dateFrom, dateTo: filters.dateTo, ...(filters.branch ? { branch: filters.branch } : {}), ...(filters.salesman ? { salesman: filters.salesman } : {}), ...(filters.productType ? { quotationType: filters.productType } : {}) }; const [ endCustomers, billingCustomers, contractors, consultants, wonEnd, wonBilling, wonContractor, wonConsultant ] = await Promise.all([ getRevenueByEndCustomer(organizationId, revenueFilters), getRevenueByBillingCustomer(organizationId, revenueFilters), getRevenueByContractor(organizationId, revenueFilters), getRevenueByConsultant(organizationId, revenueFilters), getRevenueByEndCustomer(organizationId, { ...revenueFilters, status: 'accepted' }), getRevenueByBillingCustomer(organizationId, { ...revenueFilters, status: 'accepted' }), getRevenueByContractor(organizationId, { ...revenueFilters, status: 'accepted' }), getRevenueByConsultant(organizationId, { ...revenueFilters, status: 'accepted' }) ]); function withWonRevenue( rows: Awaited>, wonRows: Awaited> ): CrmDashboardRevenueRow[] { const wonMap = new Map(wonRows.map((row) => [row.customerId, row.revenue])); return rows.slice(0, 5).map((row) => ({ customerId: row.customerId, customerCode: row.customerCode, customerName: row.customerName, roleCode: row.roleCode, revenue: row.revenue, quotationCount: row.quotationCount, wonRevenue: wonMap.get(row.customerId) ?? 0 })); } return { topEndCustomers: withWonRevenue(endCustomers, wonEnd), topBillingCustomers: withWonRevenue(billingCustomers, wonBilling), topContractors: withWonRevenue(contractors, wonContractor), topConsultants: withWonRevenue(consultants, wonConsultant) }; } async function buildSalesRanking( scopedEnquiries: Array, scopedQuotations: Array, statusMaps: StatusMaps ): Promise { const userIds = [ ...new Set( [ ...scopedEnquiries.map((row) => row.assignedToUserId).filter(Boolean), ...scopedQuotations.map((row) => row.salesmanId).filter(Boolean) ] as string[] ) ]; const salesUsers = userIds.length ? await db.select({ id: users.id, name: users.name }).from(users).where(inArray(users.id, userIds)) : []; const userMap = new Map(salesUsers.map((row) => [row.id, row.name])); const rankingMap = new Map(); for (const row of scopedEnquiries) { if (!row.assignedToUserId) continue; const current = rankingMap.get(row.assignedToUserId) ?? { salesPersonId: row.assignedToUserId, salesPersonName: userMap.get(row.assignedToUserId) ?? 'Unknown', leadCount: 0, enquiryCount: 0, quotationCount: 0, approvedQuotations: 0, wonRevenue: 0, conversionRate: 0 }; if (isPipelineStage(row, 'lead')) { current.leadCount += 1; } if (isPipelineStage(row, 'enquiry')) { current.enquiryCount += 1; } rankingMap.set(row.assignedToUserId, current); } for (const row of scopedQuotations) { if (!row.salesmanId) continue; const current = rankingMap.get(row.salesmanId) ?? { salesPersonId: row.salesmanId, salesPersonName: userMap.get(row.salesmanId) ?? 'Unknown', leadCount: 0, enquiryCount: 0, quotationCount: 0, approvedQuotations: 0, wonRevenue: 0, conversionRate: 0 }; const statusCode = statusMaps.quotationStatusById.get(row.status) ?? ''; if (statusCode !== 'cancelled') { current.quotationCount += 1; } if (statusCode === 'approved') { current.approvedQuotations += 1; } if (statusCode === 'accepted') { current.wonRevenue += row.totalAmount; } rankingMap.set(row.salesmanId, current); } return [...rankingMap.values()] .map((row) => ({ ...row, wonRevenue: round2(row.wonRevenue), conversionRate: row.quotationCount > 0 ? round2((row.approvedQuotations / row.quotationCount) * 100) : 0 })) .sort((a, b) => { if (b.wonRevenue !== a.wonRevenue) return b.wonRevenue - a.wonRevenue; if (b.quotationCount !== a.quotationCount) return b.quotationCount - a.quotationCount; return a.salesPersonName.localeCompare(b.salesPersonName); }) .slice(0, 10); } async function buildFollowups( context: DashboardContext, filters: NormalizedFilters ): Promise<{ summary: CrmDashboardFollowupSummary; upcoming: CrmDashboardFollowupRow[] }> { const allowQuotationFollowups = canViewCommercialData(context); const [enquiryFollowups, quotationFollowups] = await Promise.all([ db .select() .from(crmEnquiryFollowups) .where( and( eq(crmEnquiryFollowups.organizationId, context.organizationId), isNull(crmEnquiryFollowups.deletedAt) ) ) .orderBy(asc(crmEnquiryFollowups.followupDate)), allowQuotationFollowups ? db .select() .from(crmQuotationFollowups) .where( and( eq(crmQuotationFollowups.organizationId, context.organizationId), isNull(crmQuotationFollowups.deletedAt) ) ) .orderBy(asc(crmQuotationFollowups.followupDate)) : [] ]); const { from, to } = toDayBounds(filters); const now = new Date(); const weekStart = startOfWeek(now, { weekStartsOn: 1 }); const weekEnd = endOfWeek(now, { weekStartsOn: 1 }); const enquiryIds = [...new Set(enquiryFollowups.map((row) => row.enquiryId))]; const quotationIds = [...new Set(quotationFollowups.map((row) => row.quotationId))]; const [enquiries, quotations] = await Promise.all([ enquiryIds.length ? db.select().from(crmEnquiries).where(inArray(crmEnquiries.id, enquiryIds)) : [], quotationIds.length ? db.select().from(crmQuotations).where(inArray(crmQuotations.id, quotationIds)) : [] ]); const customerIds = [...new Set([...enquiries.map((row) => row.customerId), ...quotations.map((row) => row.customerId)])]; const salesIds = [ ...new Set( [...enquiries.map((row) => row.assignedToUserId).filter(Boolean), ...quotations.map((row) => row.salesmanId).filter(Boolean)] as string[] ) ]; const [customers, salesUsers] = await Promise.all([ customerIds.length ? db.select().from(crmCustomers).where(inArray(crmCustomers.id, customerIds)) : [], salesIds.length ? db.select({ id: users.id, name: users.name }).from(users).where(inArray(users.id, salesIds)) : [] ]); const enquiryMap = new Map(enquiries.map((row) => [row.id, row])); const quotationMap = new Map(quotations.map((row) => [row.id, row])); const customerMap = new Map(customers.map((row) => [row.id, row.name])); const salesMap = new Map(salesUsers.map((row) => [row.id, row.name])); const enquiryRows = enquiryFollowups .filter((row) => { const enquiry = enquiryMap.get(row.enquiryId); return enquiry ? canAccessDashboardEnquiry(enquiry, context) : false; }) .filter((row) => !isBefore(row.followupDate, from) && !isAfter(row.followupDate, to)) .flatMap((row) => { const enquiry = enquiryMap.get(row.enquiryId); if (!enquiry) { return []; } return [ { id: row.id, sourceType: 'enquiry', followupDate: row.followupDate.toISOString(), customerName: customerMap.get(enquiry.customerId) ?? 'Unknown customer', enquiryName: enquiry.projectName ?? enquiry.title, assignedSalesName: enquiry.assignedToUserId ? (salesMap.get(enquiry.assignedToUserId) ?? null) : null, outcome: row.outcome } ]; }); const quotationRows = quotationFollowups .filter((row) => { const quotation = quotationMap.get(row.quotationId); return quotation ? canAccessDashboardQuotation(quotation, context) : false; }) .filter((row) => !isBefore(row.followupDate, from) && !isAfter(row.followupDate, to)) .flatMap((row) => { const quotation = quotationMap.get(row.quotationId); if (!quotation) { return []; } return [ { id: row.id, sourceType: 'quotation', followupDate: row.followupDate.toISOString(), customerName: customerMap.get(quotation.customerId) ?? 'Unknown customer', enquiryName: quotation.projectName ?? quotation.code, assignedSalesName: quotation.salesmanId ? (salesMap.get(quotation.salesmanId) ?? null) : null, outcome: row.outcome } ]; }); const rows: CrmDashboardFollowupRow[] = [...enquiryRows, ...quotationRows]; return { summary: { dueToday: rows.filter((row) => isSameDay(new Date(row.followupDate), now)).length, dueThisWeek: rows.filter((row) => { const date = new Date(row.followupDate); return !isBefore(date, weekStart) && !isAfter(date, weekEnd); }).length, overdue: rows.filter((row) => isBefore(new Date(row.followupDate), startOfDay(now)) && !row.outcome).length, completed: rows.filter((row) => !!row.outcome).length }, upcoming: rows .filter((row) => !isBefore(new Date(row.followupDate), startOfDay(now))) .sort((a, b) => new Date(a.followupDate).getTime() - new Date(b.followupDate).getTime()) .slice(0, 10) }; } async function buildApprovalAnalytics( context: DashboardContext ): Promise<{ summary: CrmDashboardApprovalSummary; myPendingApprovals: CrmDashboardApprovalRow[] }> { const pending = await listApprovalRequests(context.organizationId, { page: 1, limit: 50, status: 'pending' }); const todayStart = startOfDay(new Date()); const todayEnd = endOfDay(new Date()); const rows = await db .select() .from(crmApprovalRequests) .where(and(eq(crmApprovalRequests.organizationId, context.organizationId), isNull(crmApprovalRequests.deletedAt))) .orderBy(desc(crmApprovalRequests.updatedAt)); const approvedRows = rows.filter( (row) => row.status === 'approved' && row.completedAt && !isBefore(row.completedAt, todayStart) && !isAfter(row.completedAt, todayEnd) ); const rejectedRows = rows.filter( (row) => row.status === 'rejected' && row.completedAt && !isBefore(row.completedAt, todayStart) && !isAfter(row.completedAt, todayEnd) ); const returnedRows = rows.filter( (row) => row.status === 'returned' && row.completedAt && !isBefore(row.completedAt, todayStart) && !isAfter(row.completedAt, todayEnd) ); const approvedDurations = approvedRows.map((row) => (row.completedAt!.getTime() - row.requestedAt.getTime()) / (1000 * 60 * 60)); return { summary: { pendingApprovals: pending.totalItems, approvedToday: approvedRows.length, rejectedToday: rejectedRows.length, returnedToday: returnedRows.length, averageApprovalTimeHours: average(approvedDurations) }, myPendingApprovals: pending.items .filter((item) => item.currentStepRoleCode === context.businessRole || context.membershipRole === 'admin') .slice(0, 10) .map((item) => ({ id: item.id, entityCode: item.entityCode, entityTitle: item.entityTitle, workflowName: item.workflowName, currentStepRoleName: item.currentStepRoleName, requestedAt: item.requestedAt })) }; } async function buildHotProjects( context: DashboardContext, filters: NormalizedFilters, statusMaps: StatusMaps ): Promise { const rows = await db .select() .from(crmEnquiries) .where( and( eq(crmEnquiries.organizationId, context.organizationId), isNull(crmEnquiries.deletedAt), eq(crmEnquiries.isHotProject, true) ) ) .orderBy(desc(crmEnquiries.updatedAt)); const scoped = rows .filter((row) => canAccessDashboardEnquiry(row, context)) .filter((row) => matchesEnquiryFilters(row, filters)) .filter((row) => isPipelineStage(row, 'enquiry')) .slice(0, 10); const customerIds = [...new Set(scoped.map((row) => row.customerId))]; const salesIds = [...new Set(scoped.map((row) => row.assignedToUserId).filter(Boolean))] as string[]; const [customers, salesUsers] = await Promise.all([ customerIds.length ? db.select().from(crmCustomers).where(inArray(crmCustomers.id, customerIds)) : [], salesIds.length ? db.select({ id: users.id, name: users.name }).from(users).where(inArray(users.id, salesIds)) : [] ]); const customerMap = new Map(customers.map((row) => [row.id, row.name])); const salesMap = new Map(salesUsers.map((row) => [row.id, row.name])); return scoped.map((row) => ({ enquiryId: row.id, enquiryCode: row.code, projectName: row.projectName ?? row.title, customerName: customerMap.get(row.customerId) ?? 'Unknown customer', value: row.estimatedValue ?? 0, assignedSalesName: row.assignedToUserId ? (salesMap.get(row.assignedToUserId) ?? null) : null, probability: row.chancePercent })); } export async function getCrmDashboardData( context: DashboardContext, rawFilters: CrmDashboardFilters ): Promise { const filters = normalizeDashboardFilters(rawFilters); const allowCommercialData = canViewCommercialData(context); const allowApprovalData = canViewApprovalData(context); const [referenceData, statusMaps, scoped] = await Promise.all([ getReferenceData(context.organizationId), getStatusMaps(context.organizationId), loadScopedRows(context, filters) ]); const summary = buildSummary(scoped.enquiries, scoped.quotations, statusMaps); const [revenueAnalytics, salesRanking, followups, approvals, hotProjects] = await Promise.all([ allowCommercialData ? buildRevenueAnalytics(context.organizationId, filters) : Promise.resolve({ topEndCustomers: [], topBillingCustomers: [], topContractors: [], topConsultants: [] }), allowCommercialData ? buildSalesRanking(scoped.enquiries, scoped.quotations, statusMaps) : Promise.resolve([]), buildFollowups(context, filters), allowApprovalData ? buildApprovalAnalytics(context) : Promise.resolve({ summary: { pendingApprovals: 0, approvedToday: 0, rejectedToday: 0, returnedToday: 0, averageApprovalTimeHours: 0 }, myPendingApprovals: [] }), allowCommercialData ? buildHotProjects(context, filters, statusMaps) : Promise.resolve([]) ]); return { success: true, time: new Date().toISOString(), message: 'CRM dashboard loaded successfully', filters, referenceData, summary, funnel: buildFunnel(summary, scoped.quotations, statusMaps), revenueAnalytics, salesRanking, followups, approvals, hotProjects, visibility: { canViewCommercialData: allowCommercialData, canViewApprovalData: allowApprovalData } }; }