diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/plans/task-fixcrm.md b/plans/task-fixcrm.md new file mode 100644 index 0000000..c56ddde --- /dev/null +++ b/plans/task-fixcrm.md @@ -0,0 +1,305 @@ +# Task Fix: CRM Dashboard Opportunity Rename Compatibility + +## Objective + +Fix CRM Dashboard API/runtime errors after the force rename from Enquiry to Opportunity. + +The current issue appears when the CRM dashboard query returns `500 Internal Server Error`, causing TanStack Query hydration to reject: + +```txt +["crm-dashboard", {...}] +``` + +This task aligns dashboard, reports, query keys, imports, SQL, DTOs, and permissions with the new Opportunity domain. + +--- + +## Error Context + +Observed error: + +```txt +A query that was dehydrated as pending ended up rejecting. +[["crm-dashboard", {...}]]: Error: API error: 500 Internal Server Error +``` + +Likely root cause: + +```txt +Dashboard API still references old Enquiry domain after D.5.5.1 rename. +``` + +Examples of stale references: + +```txt +crm_enquiries +crmEnquiries +enquiryId +Enquiry +enquiry-aging +crm.enquiry.* +``` + +--- + +## Mandatory Review + +Review: + +```txt +src/features/crm/dashboard/** +src/app/api/crm/dashboard/** +src/app/dashboard/crm/** +src/app/api/crm/reports/** +src/app/dashboard/crm/reports/** +src/features/crm/reports/** +src/db/schema.ts +src/features/crm/opportunities/** +src/config/nav-config.ts +src/db/seeds/foundation.seed.ts +``` + +--- + +## Scope 1: Identify Stale References + +Run: + +```bash +rg "crmEnquir|crm_enquir|enquiry|Enquiry|enquiryId|crm\.enquiry|/crm/enquiries" src/features/crm/dashboard src/app/api/crm/dashboard src/app/dashboard/crm src/features/crm/reports src/app/api/crm/reports src/app/dashboard/crm/reports +``` + +Expected action: + +* Replace active application references with Opportunity terminology. +* Historical docs can remain unchanged. +* No active dashboard/report runtime code should depend on Enquiry naming. + +--- + +## Scope 2: Dashboard API Fix + +Update dashboard API/service to use: + +```txt +crmOpportunities +crm_opportunities +opportunityId +crm.opportunity.* +``` + +instead of: + +```txt +crmEnquiries +crm_enquiries +enquiryId +crm.enquiry.* +``` + +Ensure all dashboard metrics still return successfully. + +Required dashboard response areas to verify: + +```txt +summary cards +pipeline counts +sales ranking +follow-up due / overdue +hot projects +quotation summary +won / lost summary +project party filters +``` + +--- + +## Scope 3: SQL / Drizzle Query Fix + +Fix all raw SQL and Drizzle queries that reference old table/column names. + +Replace: + +```txt +crm_enquiries +crm_enquiry_followups +crm_enquiry_customers +enquiry_id +``` + +with: + +```txt +crm_opportunities +crm_opportunity_followups +crm_opportunity_customers +opportunity_id +``` + +If any remaining DB column intentionally keeps legacy naming, document it and map it at service boundary only. + +--- + +## Scope 4: Dashboard DTO / Mapper Fix + +Update dashboard DTOs, mappers, and labels. + +Rename: + +```txt +enquiryCount +enquiryPipeline +enquiryValue +enquiryAging +``` + +to: + +```txt +opportunityCount +opportunityPipeline +opportunityValue +opportunityAging +``` + +Rules: + +* API response should use Opportunity naming. +* UI labels should use Opportunity naming. +* Do not expose Enquiry naming in active CRM dashboard. + +--- + +## Scope 5: Report Route Compatibility + +Verify renamed routes: + +```txt +/api/crm/reports/opportunity-aging +/dashboard/crm/reports/opportunity-aging +``` + +Remove or replace active references to: + +```txt +/api/crm/reports/enquiry-aging +/dashboard/crm/reports/enquiry-aging +``` + +Update nav config if needed. + +--- + +## Scope 6: Permissions + +Replace dashboard/report permission checks: + +```txt +crm.enquiry.* +``` + +with: + +```txt +crm.opportunity.* +``` + +Verify seed/config contains the required permissions: + +```txt +crm.opportunity.read +crm.opportunity.create +crm.opportunity.update +crm.opportunity.delete +crm.opportunity.assign +crm.opportunity.followup.create +crm.opportunity.followup.update +crm.opportunity.followup.delete +crm.opportunity.lifecycle.update +crm.opportunity.lifecycle.reopen +``` + +--- + +## Scope 7: Error Handling Improvement + +Prevent dashboard SSR hydration from failing hard when API returns an error. + +Improve dashboard query behavior: + +* Server prefetch should catch API failure and return a safe empty dashboard model, or +* Client component should render an error state instead of causing hydration failure. + +Do not hide backend errors in development logs. + +Recommended: + +```txt +dashboard API logs detailed server error +dashboard UI shows recoverable error message +query fallback uses empty dashboard model only when safe +``` + +--- + +## Scope 8: Verification + +Run: + +```bash +npm exec tsc --noEmit +npm run build +``` + +Search validation: + +```bash +rg "crmEnquir|crm_enquir|enquiry|Enquiry|enquiryId|crm\.enquiry|/crm/enquiries" src +``` + +Allowed only if: + +```txt +historical docs +comments explicitly marked legacy +migration history +``` + +Manual verification: + +```txt +Open CRM Dashboard +Dashboard API returns 200 +No TanStack hydration rejection +Opportunity counts render +Quotation counts render +Won/Lost summary renders +Sales ranking renders +Follow-up widgets render +Hot project widgets render +Opportunity aging report opens +Navigation links use opportunity routes +``` + +--- + +## Definition of Done + +Task is complete when: + +* CRM Dashboard no longer returns 500 +* Dashboard no longer references active Enquiry domain +* Reports use Opportunity naming +* Permissions use `crm.opportunity.*` +* API response and UI labels use Opportunity terminology +* TanStack hydration error is gone +* `npm exec tsc --noEmit` passes +* `npm run build` passes + +Result: + +```txt +Dashboard Opportunity Rename Compatibility = Fixed +CRM Dashboard Runtime = Restored +Ready to continue Approval + Notification roadmap +``` diff --git a/src/app/api/crm/dashboard/route.ts b/src/app/api/crm/dashboard/route.ts index 881ecef..3e57d66 100644 --- a/src/app/api/crm/dashboard/route.ts +++ b/src/app/api/crm/dashboard/route.ts @@ -15,17 +15,14 @@ export async function GET(request: NextRequest) { userId: session.user.id, membership }); - const data = await getCrmDashboardData( - securityContext, - { - dateFrom: searchParams.get('dateFrom'), - dateTo: searchParams.get('dateTo'), - branch: searchParams.get('branch'), - salesman: searchParams.get('salesman'), - projectPartyRole: searchParams.get('projectPartyRole'), - productType: searchParams.get('productType') - } - ); + const data = await getCrmDashboardData(securityContext, { + dateFrom: searchParams.get('dateFrom'), + dateTo: searchParams.get('dateTo'), + branch: searchParams.get('branch'), + salesman: searchParams.get('salesman'), + projectPartyRole: searchParams.get('projectPartyRole'), + productType: searchParams.get('productType') + }); return NextResponse.json(data); } catch (error) { @@ -35,6 +32,22 @@ export async function GET(request: NextRequest) { console.error('GET /api/crm/dashboard failed', error); - return NextResponse.json({ message: 'Unable to load CRM dashboard' }, { status: 500 }); + return NextResponse.json( + { + message: 'Unable to load CRM dashboard', + ...(process.env.NODE_ENV !== 'production' && error instanceof Error + ? { + detail: error.message, + cause: + error.cause instanceof Error + ? error.cause.message + : error.cause + ? String(error.cause) + : null + } + : {}) + }, + { status: 500 } + ); } } diff --git a/src/app/dashboard/crm/page.tsx b/src/app/dashboard/crm/page.tsx index 1e3c4ee..1f3e5c2 100644 --- a/src/app/dashboard/crm/page.tsx +++ b/src/app/dashboard/crm/page.tsx @@ -1,12 +1,16 @@ import { HydrationBoundary, dehydrate } from '@tanstack/react-query'; +import type { SearchParams } from 'nuqs/server'; import { auth } from '@/auth'; import PageContainer from '@/components/layout/page-container'; import { crmDashboardQueryOptions } from '@/features/crm/dashboard/api/queries'; +import { + createEmptyCrmDashboardResponse, + type CrmDashboardFilters +} from '@/features/crm/dashboard/api/types'; import { CrmDashboard } from '@/features/crm/dashboard/components/crm-dashboard'; import { PERMISSIONS } from '@/lib/auth/rbac'; import { getQueryClient } from '@/lib/query-client'; import { searchParamsCache } from '@/lib/searchparams'; -import type { SearchParams } from 'nuqs/server'; export const metadata = { title: 'Dashboard: CRM' @@ -16,14 +20,27 @@ type PageProps = { searchParams: Promise; }; +function normalizeRequiredFilters(filters: CrmDashboardFilters): Required { + return { + dateFrom: filters.dateFrom ?? null, + dateTo: filters.dateTo ?? null, + branch: filters.branch ?? null, + salesman: filters.salesman ?? null, + projectPartyRole: filters.projectPartyRole ?? null, + productType: filters.productType ?? null + }; +} + export default async function CrmDashboardRoute(props: PageProps) { const searchParams = await props.searchParams; const session = await auth(); + const canRead = session?.user?.systemRole === 'super_admin' || (!!session?.user?.activeOrganizationId && (session.user.activeMembershipRole === 'admin' || session.user.activePermissions.includes(PERMISSIONS.crmDashboardRead))); + const canExport = session?.user?.systemRole === 'super_admin' || (!!session?.user?.activeOrganizationId && @@ -40,16 +57,29 @@ export default async function CrmDashboardRoute(props: PageProps) { projectPartyRole: searchParamsCache.get('projectPartyRole'), productType: searchParamsCache.get('productType') }; + const queryClient = getQueryClient(); + const dashboardQuery = crmDashboardQueryOptions(filters); if (canRead) { - void queryClient.prefetchQuery(crmDashboardQueryOptions(filters)); + try { + await queryClient.prefetchQuery(dashboardQuery); + } catch (error) { + console.error('CRM dashboard prefetch failed', error); + queryClient.setQueryData( + dashboardQuery.queryKey, + createEmptyCrmDashboardResponse( + normalizeRequiredFilters(filters), + 'CRM dashboard data could not be loaded.' + ) + ); + } } return ( diff --git a/src/features/crm/dashboard/api/types.ts b/src/features/crm/dashboard/api/types.ts index 85d347f..faa3be3 100644 --- a/src/features/crm/dashboard/api/types.ts +++ b/src/features/crm/dashboard/api/types.ts @@ -162,3 +162,85 @@ export interface CrmDashboardResponse { }; } +export function createEmptyCrmDashboardResponse( + filters: Required, + message = 'CRM dashboard is temporarily unavailable.' +): CrmDashboardResponse { + return { + success: false, + time: new Date().toISOString(), + message, + filters, + referenceData: { + branches: [], + salesmen: [], + productTypes: [], + projectPartyRoles: [] + }, + summary: { + lead: { + leadCount: 0, + newLeads: 0, + unassignedLeads: 0, + averageLeadAgingDays: 0 + }, + opportunity: { + opportunityCount: 0, + activeOpportunities: 0, + wonCount: 0, + lostCount: 0, + winRate: 0, + hotOpportunities: 0, + opportunityValue: 0 + }, + quotation: { + draftQuotations: 0, + pendingApproval: 0, + approvedQuotations: 0, + sentQuotations: 0 + }, + revenue: { + quotationValue: 0, + wonValue: 0, + lostValue: 0 + } + }, + funnel: [], + revenueAnalytics: { + topEndCustomers: [], + topBillingCustomers: [], + topContractors: [], + topConsultants: [] + }, + salesRanking: [], + followups: { + summary: { + dueToday: 0, + dueThisWeek: 0, + overdue: 0, + completed: 0 + }, + upcoming: [] + }, + approvals: { + summary: { + pendingApprovals: 0, + approvedToday: 0, + rejectedToday: 0, + returnedToday: 0, + averageApprovalTimeHours: 0 + }, + myPendingApprovals: [] + }, + hotProjects: [], + outcomeAnalytics: { + winRate: 0, + lostByReason: [], + lostByCompetitor: [] + }, + visibility: { + canViewCommercialData: false, + canViewApprovalData: false + } + }; +} diff --git a/src/features/crm/dashboard/components/crm-dashboard.tsx b/src/features/crm/dashboard/components/crm-dashboard.tsx index f834cb6..fca2bef 100644 --- a/src/features/crm/dashboard/components/crm-dashboard.tsx +++ b/src/features/crm/dashboard/components/crm-dashboard.tsx @@ -35,30 +35,42 @@ export function CrmDashboard({ canExport }: { canExport: boolean }) { return (
+ {!data.success ? ( +
+ {data.message} +
+ ) : null} + + + {data.visibility.canViewCommercialData ? (
) : null} + {data.visibility.canViewCommercialData ? ( ) : null} + {data.visibility.canViewCommercialData || data.visibility.canViewApprovalData ? (
{data.visibility.canViewCommercialData ? ( ) : null} + {data.visibility.canViewApprovalData ? ( ) : null}
) : null} +
); diff --git a/src/features/crm/dashboard/server/service.ts b/src/features/crm/dashboard/server/service.ts index ad6aa3d..1126937 100644 --- a/src/features/crm/dashboard/server/service.ts +++ b/src/features/crm/dashboard/server/service.ts @@ -53,7 +53,7 @@ import type { CrmDashboardSummary } from '../api/types'; -const ENQUIRY_STATUS_CATEGORY = 'crm_opportunity_status'; +const OPPORTUNITY_STATUS_CATEGORY = 'crm_opportunity_stage'; const QUOTATION_STATUS_CATEGORY = 'crm_quotation_status'; const PRODUCT_TYPE_CATEGORY = 'crm_product_type'; const PROJECT_PARTY_ROLE_CATEGORY = 'crm_project_party_role'; @@ -85,6 +85,26 @@ type StatusMaps = { quotationStatusById: Map; }; +const dashboardOpportunitySelect = { + id: crmOpportunities.id, + organizationId: crmOpportunities.organizationId, + branchId: crmOpportunities.branchId, + customerId: crmOpportunities.customerId, + code: crmOpportunities.code, + title: crmOpportunities.title, + projectName: crmOpportunities.projectName, + productType: crmOpportunities.productType, + estimatedValue: crmOpportunities.estimatedValue, + chancePercent: crmOpportunities.chancePercent, + isHotProject: crmOpportunities.isHotProject, + pipelineStage: crmOpportunities.pipelineStage, + assignedToUserId: crmOpportunities.assignedToUserId, + createdAt: crmOpportunities.createdAt, + createdBy: crmOpportunities.createdBy, + updatedAt: crmOpportunities.updatedAt, + deletedAt: crmOpportunities.deletedAt +} as const; + function normalizeDashboardFilters(filters: CrmDashboardFilters): NormalizedFilters { const now = new Date(); const monthStart = startOfDay(new Date(now.getFullYear(), now.getMonth(), 1)); @@ -133,7 +153,10 @@ function hasDashboardFullScope(context: DashboardContext) { } function canAccessDashboardOpportunity( - row: typeof crmOpportunities.$inferSelect, + row: Pick< + typeof crmOpportunities.$inferSelect, + 'branchId' | 'productType' | 'createdBy' | 'assignedToUserId' + >, context: DashboardContext ) { if (!hasBranchScopeAccess(context, row.branchId)) { @@ -180,7 +203,7 @@ function canAccessDashboardQuotation( async function getStatusMaps(organizationId: string): Promise { const [opportunityOptions, quotationOptions] = await Promise.all([ - getActiveOptionsByCategory(ENQUIRY_STATUS_CATEGORY, { organizationId }), + getActiveOptionsByCategory(OPPORTUNITY_STATUS_CATEGORY, { organizationId }), getActiveOptionsByCategory(QUOTATION_STATUS_CATEGORY, { organizationId }) ]); @@ -192,7 +215,7 @@ async function getStatusMaps(organizationId: string): Promise { async function getReferenceData(organizationId: string): Promise { const [branches, salesmen, productTypes, projectPartyRoles] = await Promise.all([ - getUserBranches(), + getUserBranches(organizationId), db .select({ id: users.id, name: users.name }) .from(memberships) @@ -220,7 +243,10 @@ async function getReferenceData(organizationId: string): Promise, filters: NormalizedFilters ) { const { from, to } = toDayBounds(filters); @@ -248,7 +274,7 @@ function matchesQuotationFilters( } function isPipelineStage( - opportunity: typeof crmOpportunities.$inferSelect, + opportunity: Pick, stage: 'lead' | 'opportunity' | 'closed_won' | 'closed_lost' ) { return opportunity.pipelineStage === stage; @@ -260,7 +286,7 @@ async function loadScopedRows( ) { const [opportunities, quotations] = await Promise.all([ db - .select() + .select(dashboardOpportunitySelect) .from(crmOpportunities) .where( and(eq(crmOpportunities.organizationId, context.organizationId), isNull(crmOpportunities.deletedAt)) @@ -284,7 +310,10 @@ async function loadScopedRows( } function buildSummary( - scopedOpportunities: Array, + scopedOpportunities: Array>, scopedQuotations: Array, statusMaps: StatusMaps, outcomeMetrics: { wonValue: number; lostValue: number; winRate: number } @@ -456,7 +485,7 @@ async function buildRevenueAnalytics( } async function buildSalesRanking( - scopedOpportunities: Array, + scopedOpportunities: Array>, scopedQuotations: Array, statusMaps: StatusMaps, wonRevenueRows: Array<{ assignedToUserId: string | null; revenue: number }> @@ -602,7 +631,12 @@ async function buildFollowups( const opportunityIds = [...new Set(opportunityFollowups.map((row) => row.opportunityId))]; const quotationIds = [...new Set(quotationFollowups.map((row) => row.quotationId))]; const [opportunities, quotations] = await Promise.all([ - opportunityIds.length ? db.select().from(crmOpportunities).where(inArray(crmOpportunities.id, opportunityIds)) : [], + opportunityIds.length + ? db + .select(dashboardOpportunitySelect) + .from(crmOpportunities) + .where(inArray(crmOpportunities.id, opportunityIds)) + : [], quotationIds.length ? db.select().from(crmQuotations).where(inArray(crmQuotations.id, quotationIds)) : [] ]); const customerIds = [...new Set([...opportunities.map((row) => row.customerId), ...quotations.map((row) => row.customerId)])]; @@ -748,8 +782,8 @@ async function buildHotProjects( statusMaps: StatusMaps ): Promise { const rows = await db - .select() - .from(crmOpportunities) + .select(dashboardOpportunitySelect) + .from(crmOpportunities) .where( and( eq(crmOpportunities.organizationId, context.organizationId), @@ -871,4 +905,3 @@ export async function getCrmDashboardData( } }; } - diff --git a/src/features/crm/reporting/server/service.ts b/src/features/crm/reporting/server/service.ts index 9969e07..45fe2a4 100644 --- a/src/features/crm/reporting/server/service.ts +++ b/src/features/crm/reporting/server/service.ts @@ -47,6 +47,17 @@ type ProjectPartyTableAvailability = { let projectPartyTableAvailability: ProjectPartyTableAvailability | null = null; +const outcomeOpportunitySelect = { + id: crmOpportunities.id, + customerId: crmOpportunities.customerId, + branchId: crmOpportunities.branchId, + productType: crmOpportunities.productType, + assignedToUserId: crmOpportunities.assignedToUserId, + poAmount: crmOpportunities.poAmount, + closedWonAt: crmOpportunities.closedWonAt, + closedLostAt: crmOpportunities.closedLostAt +} as const; + function splitFilterValue(value?: string) { return value ?.split(',') @@ -412,7 +423,7 @@ async function getOutcomeRevenueRecords( ): Promise { const whereFilters = buildOutcomeRevenueFilters(organizationId, stage, filters); const where = whereFilters.length === 1 ? whereFilters[0] : and(...whereFilters); - const opportunities = await db.select().from(crmOpportunities).where(where); + const opportunities = await db.select(outcomeOpportunitySelect).from(crmOpportunities).where(where); if (!opportunities.length) { return []; @@ -556,4 +567,3 @@ export async function getWinRate( return Math.round((wonRows.length / denominator) * 10000) / 100; } - diff --git a/src/features/crm/reports/server/datasets/pipeline-report.dataset.ts b/src/features/crm/reports/server/datasets/pipeline-report.dataset.ts index 4cf00c3..a09454e 100644 --- a/src/features/crm/reports/server/datasets/pipeline-report.dataset.ts +++ b/src/features/crm/reports/server/datasets/pipeline-report.dataset.ts @@ -91,7 +91,7 @@ export async function listPipelineReportDataset( const { start, end } = getDateRange(requiredFilters); const [branches, productTypes, leadSources, opportunityRows, quotationRows, followupRows] = await Promise.all([ - getUserBranches(), + getUserBranches(context.organizationId), getActiveOptionsByCategory('crm_product_type', { organizationId: context.organizationId }), getActiveOptionsByCategory('crm_lead_channel', { organizationId: context.organizationId }), db @@ -260,4 +260,3 @@ export async function listPipelineReportDataset( rows }; } - diff --git a/src/features/crm/shared/terminology.ts b/src/features/crm/shared/terminology.ts index 58a07e8..c04c655 100644 --- a/src/features/crm/shared/terminology.ts +++ b/src/features/crm/shared/terminology.ts @@ -14,39 +14,35 @@ export const CRM_TERMS = { contact: 'ผู้ติดต่อ', contacts: 'ผู้ติดต่อ', billingCustomer: 'ลูกค้าผู้รับใบเสนอราคา', - projectParties: 'ผู้เกี่ยวข้องในโครงการ', - crmSettings: 'ตั้งค่า CRM', - leadCount: 'จำนวนลีด', - opportunityCount: 'จำนวนโอกาสขาย', - wonCount: 'จำนวนงานที่ชนะ', - lostCount: 'จำนวนงานที่แพ้', - revenue: 'มูลค่าใบเสนอราคา', - conversionRate: 'อัตราการเปลี่ยนสถานะ' + contractor: 'ผู้รับเหมา', + consultant: 'ที่ปรึกษา', + endCustomer: 'ผู้ใช้งานปลายทาง', + projectParty: 'ผู้เกี่ยวข้องในโครงการ' } as const; -export const PIPELINE_STAGE_THAI_LABELS = { - lead: 'ลีด', - opportunity: 'โอกาสขาย', - closed_won: 'ชนะ', - closed_lost: 'แพ้' -} as const; - -export const PROJECT_PARTY_ROLE_THAI_LABELS: Record = { +const PROJECT_PARTY_ROLE_LABELS: Record = { billing_customer: 'ลูกค้าผู้รับใบเสนอราคา', customer: 'ลูกค้า', consultant: 'ที่ปรึกษา', contractor: 'ผู้รับเหมา', - end_customer: 'เจ้าของโครงการ', + end_customer: 'ผู้ใช้งานปลายทาง', other: 'อื่น ๆ' }; +const PIPELINE_STAGE_LABELS: Record = { + lead: 'ลีด', + opportunity: 'โอกาสขาย', + quotation_created: 'ออกใบเสนอราคาแล้ว', + negotiation: 'กำลังเจรจา', + closed: 'ปิดงาน', + closed_won: 'ชนะ', + closed_lost: 'แพ้' +}; + export function getProjectPartyRoleThaiLabel(roleCode: string, fallback?: string | null) { - return PROJECT_PARTY_ROLE_THAI_LABELS[roleCode] ?? fallback ?? roleCode; + return PROJECT_PARTY_ROLE_LABELS[roleCode] ?? fallback ?? roleCode; } -export function getPipelineStageThaiLabel( - stage: keyof typeof PIPELINE_STAGE_THAI_LABELS | string -) { - return PIPELINE_STAGE_THAI_LABELS[stage as keyof typeof PIPELINE_STAGE_THAI_LABELS] ?? stage; +export function getPipelineStageThaiLabel(stageCode: string, fallback?: string | null) { + return PIPELINE_STAGE_LABELS[stageCode] ?? fallback ?? stageCode; } - diff --git a/src/features/foundation/branch-scope/service.ts b/src/features/foundation/branch-scope/service.ts index 05d3130..8d6fe43 100644 --- a/src/features/foundation/branch-scope/service.ts +++ b/src/features/foundation/branch-scope/service.ts @@ -1,5 +1,5 @@ -import { AuthError } from '@/lib/auth/session'; import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service'; +import { AuthError } from '@/lib/auth/session'; import type { FoundationBranch } from './types'; function mapBranchOption( @@ -13,14 +13,17 @@ function mapBranchOption( }; } -export async function getUserBranches(): Promise { - const options = await getActiveOptionsByCategory('crm_branch'); +export async function getUserBranches(organizationId?: string): Promise { + const options = await getActiveOptionsByCategory( + 'crm_branch', + organizationId ? { organizationId } : undefined + ); return options.map(mapBranchOption); } -export async function getActiveBranch(): Promise { - const branches = await getUserBranches(); +export async function getActiveBranch(organizationId?: string): Promise { + const branches = await getUserBranches(organizationId); if (branches.length === 1) { return branches[0]; @@ -29,12 +32,12 @@ export async function getActiveBranch(): Promise { return null; } -export async function validateBranchAccess(branchId?: string | null) { +export async function validateBranchAccess(branchId?: string | null, organizationId?: string) { if (!branchId) { - return getActiveBranch(); + return getActiveBranch(organizationId); } - const branches = await getUserBranches(); + const branches = await getUserBranches(organizationId); const branch = branches.find((item) => item.id === branchId); if (!branch) {