task-fix
This commit is contained in:
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user