This commit is contained in:
phaichayon
2026-06-25 16:26:14 +07:00
parent 69279fc341
commit 5a83040077
11 changed files with 547 additions and 64 deletions

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
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.

305
plans/task-fixcrm.md Normal file
View File

@@ -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
```

View File

@@ -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 }
);
}
}

View File

@@ -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<SearchParams>;
};
function normalizeRequiredFilters(filters: CrmDashboardFilters): Required<CrmDashboardFilters> {
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 (
<PageContainer
pageTitle='CRM Dashboard'
pageDescription='Dashboard กลางสำหรับติดตาม KPI, มูลค่าใบเสนอราคา, งานอนุมัติเอกสาร, และภาพรวมการขายจากข้อมูล CRM จริง'
pageDescription='Dashboard กลางสำหรับติดตาม KPI มูลค่าใบเสนอราคา งานอนุมัติเอกสาร และภาพรวมการขายจากข้อมูล CRM จริง'
access={canRead}
accessFallback={
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>

View File

@@ -162,3 +162,85 @@ export interface CrmDashboardResponse {
};
}
export function createEmptyCrmDashboardResponse(
filters: Required<CrmDashboardFilters>,
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
}
};
}

View File

@@ -35,30 +35,42 @@ export function CrmDashboard({ canExport }: { canExport: boolean }) {
return (
<div className='space-y-6'>
{!data.success ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
{data.message}
</div>
) : null}
<DashboardFilters referenceData={data.referenceData} canExport={canExport} />
<DashboardSummaryCards
summary={data.summary}
canViewCommercialData={data.visibility.canViewCommercialData}
/>
{data.visibility.canViewCommercialData ? (
<div className='grid gap-6 2xl:grid-cols-[1.35fr_1fr]'>
<DashboardFunnel steps={data.funnel} />
<DashboardHotProjects rows={data.hotProjects} />
</div>
) : null}
{data.visibility.canViewCommercialData ? (
<DashboardRevenueCards revenueAnalytics={data.revenueAnalytics} />
) : null}
{data.visibility.canViewCommercialData || data.visibility.canViewApprovalData ? (
<div className='grid gap-6 2xl:grid-cols-[1.2fr_1fr]'>
{data.visibility.canViewCommercialData ? (
<DashboardSalesRanking rows={data.salesRanking} />
) : null}
{data.visibility.canViewApprovalData ? (
<DashboardApprovalMetrics approvals={data.approvals} />
) : null}
</div>
) : null}
<DashboardFollowups followups={data.followups} />
</div>
);

View File

@@ -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<string, string>;
};
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<StatusMaps> {
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<StatusMaps> {
async function getReferenceData(organizationId: string): Promise<CrmDashboardReferenceData> {
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<CrmDashboardRef
}
function matchesOpportunityFilters(
row: typeof crmOpportunities.$inferSelect,
row: Pick<
typeof crmOpportunities.$inferSelect,
'branchId' | 'assignedToUserId' | 'productType' | 'createdAt'
>,
filters: NormalizedFilters
) {
const { from, to } = toDayBounds(filters);
@@ -248,7 +274,7 @@ function matchesQuotationFilters(
}
function isPipelineStage(
opportunity: typeof crmOpportunities.$inferSelect,
opportunity: Pick<typeof crmOpportunities.$inferSelect, 'pipelineStage'>,
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<typeof crmOpportunities.$inferSelect>,
scopedOpportunities: Array<Pick<
typeof crmOpportunities.$inferSelect,
'assignedToUserId' | 'createdAt' | 'estimatedValue' | 'isHotProject' | 'pipelineStage'
>>,
scopedQuotations: Array<typeof crmQuotations.$inferSelect>,
statusMaps: StatusMaps,
outcomeMetrics: { wonValue: number; lostValue: number; winRate: number }
@@ -456,7 +485,7 @@ async function buildRevenueAnalytics(
}
async function buildSalesRanking(
scopedOpportunities: Array<typeof crmOpportunities.$inferSelect>,
scopedOpportunities: Array<Pick<typeof crmOpportunities.$inferSelect, 'assignedToUserId' | 'pipelineStage'>>,
scopedQuotations: Array<typeof crmQuotations.$inferSelect>,
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<CrmDashboardHotProjectRow[]> {
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(
}
};
}

View File

@@ -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<OutcomeRevenueRecord[]> {
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;
}

View File

@@ -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
};
}

View File

@@ -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<string, string> = {
const PROJECT_PARTY_ROLE_LABELS: Record<string, string> = {
billing_customer: 'ลูกค้าผู้รับใบเสนอราคา',
customer: 'ลูกค้า',
consultant: 'ที่ปรึกษา',
contractor: 'ผู้รับเหมา',
end_customer: 'เจ้าของโครงการ',
end_customer: 'ผู้ใช้งานปลายทาง',
other: 'อื่น ๆ'
};
const PIPELINE_STAGE_LABELS: Record<string, string> = {
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;
}

View File

@@ -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<FoundationBranch[]> {
const options = await getActiveOptionsByCategory('crm_branch');
export async function getUserBranches(organizationId?: string): Promise<FoundationBranch[]> {
const options = await getActiveOptionsByCategory(
'crm_branch',
organizationId ? { organizationId } : undefined
);
return options.map(mapBranchOption);
}
export async function getActiveBranch(): Promise<FoundationBranch | null> {
const branches = await getUserBranches();
export async function getActiveBranch(organizationId?: string): Promise<FoundationBranch | null> {
const branches = await getUserBranches(organizationId);
if (branches.length === 1) {
return branches[0];
@@ -29,12 +32,12 @@ export async function getActiveBranch(): Promise<FoundationBranch | null> {
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) {