task-d.5.4

This commit is contained in:
phaichayon
2026-06-25 08:06:53 +07:00
parent 0c28735c90
commit 6093003b70
22 changed files with 1930 additions and 207 deletions

View File

@@ -1,13 +1,17 @@
import { auth } from '@/auth';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { notFound } from 'next/navigation';
import Link from 'next/link';
import PageContainer from '@/components/layout/page-container';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
enquiryByIdOptions,
enquiryFollowupsOptions,
enquiryProjectPartiesOptions
} from '@/features/crm/enquiries/api/queries';
import { EnquiryDetail } from '@/features/crm/enquiries/components/enquiry-detail';
import { getEnquiryReferenceData } from '@/features/crm/enquiries/server/service';
import { getEnquiryDetail, getEnquiryReferenceData } from '@/features/crm/enquiries/server/service';
import { getLeadById } from '@/features/crm/leads/server/service';
import { listEnquiryQuotationRelations } from '@/features/crm/quotations/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { getQueryClient } from '@/lib/query-client';
@@ -19,117 +23,146 @@ type PageProps = {
export default async function EnquiryDetailRoute({ params }: PageProps) {
const { id } = await params;
const session = await auth();
const canRead =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmEnquiryRead)));
const canUpdate =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmEnquiryUpdate)));
const canFollowupCreate =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmEnquiryFollowupCreate)));
const canFollowupUpdate =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmEnquiryFollowupUpdate)));
const canFollowupDelete =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmEnquiryFollowupDelete)));
const canAssign =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmEnquiryAssign)));
const canReassign =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmEnquiryReassign)));
const canReadQuotation =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmQuotationRead)));
const canViewOutcomeCommercialData =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmQuotationPricingRead)));
const canManageFollowups = {
create:
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmEnquiryFollowupCreate))),
update:
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmEnquiryFollowupUpdate))),
delete:
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmEnquiryFollowupDelete)))
};
const canMarkWon =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmEnquiryMarkWon)));
const canMarkLost =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmEnquiryMarkLost)));
const canReopen =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmEnquiryReopen)));
const queryClient = getQueryClient();
if (canRead) {
void queryClient.prefetchQuery(enquiryByIdOptions(id));
void queryClient.prefetchQuery(enquiryProjectPartiesOptions(id));
void queryClient.prefetchQuery(enquiryFollowupsOptions(id));
const canViewRelatedQuotations =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmQuotationRead)));
const canViewOutcomeCommercialData = canRead;
if (!canRead || !session?.user?.activeOrganizationId || !session.user.id) {
notFound();
}
const referenceData =
canRead && session?.user?.activeOrganizationId
? await getEnquiryReferenceData(session.user.activeOrganizationId)
: null;
const relatedQuotations =
canRead && canReadQuotation && session?.user?.activeOrganizationId
? await listEnquiryQuotationRelations(id, session.user.activeOrganizationId)
: [];
const accessContext = {
organizationId: session.user.activeOrganizationId,
userId: session.user.id,
membershipRole: session.user.activeMembershipRole ?? 'user',
businessRole: session.user.activeBusinessRole ?? 'sales_support',
businessRoles: [session.user.activeBusinessRole ?? 'sales_support'],
permissions: session.user.activePermissions ?? [],
branchScopeIds: session.user.activeBranchScopeIds ?? [],
productTypeScopeIds: session.user.activeProductTypeScopeIds ?? [],
ownershipScope: session.user.activeOwnershipScope ?? 'organization',
branchScopeMode: 'all',
productScopeMode: 'all',
approvalAuthority: null
} as const;
const queryClient = getQueryClient();
await Promise.all([
queryClient.prefetchQuery(enquiryByIdOptions(id)),
queryClient.prefetchQuery(enquiryFollowupsOptions(id)),
queryClient.prefetchQuery(enquiryProjectPartiesOptions(id))
]);
const enquiryDetail = await getEnquiryDetail(id, session.user.activeOrganizationId, accessContext);
const [referenceData, relatedQuotations, sourceLead] = await Promise.all([
getEnquiryReferenceData(session.user.activeOrganizationId),
listEnquiryQuotationRelations(id, session.user.activeOrganizationId),
enquiryDetail.leadId
? getLeadById(enquiryDetail.leadId, session.user.activeOrganizationId).catch(() => null)
: Promise.resolve(null)
]);
return (
<PageContainer
pageTitle='รายละเอียดโอกาสขาย'
pageDescription='ข้อมูลโอกาสขาย ไทม์ไลน์การติดตาม และสถานะการดูแลของฝ่ายขาย'
access={canRead}
accessFallback={
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
You do not have access to this enquiry.
</div>
}
pageDescription='พื้นที่ทำงานของฝ่ายขายสำหรับติดตาม execution ของ enquiry และเชื่อมกับ quotation'
>
{referenceData ? (
<HydrationBoundary state={dehydrate(queryClient)}>
<EnquiryDetail
enquiryId={id}
referenceData={referenceData}
relatedQuotations={relatedQuotations}
workspace='enquiry'
canUpdate={canUpdate}
canAssign={canAssign}
canReassign={canReassign}
canViewRelatedQuotations={canReadQuotation}
canViewOutcomeCommercialData={canViewOutcomeCommercialData}
canMarkWon={canMarkWon}
canMarkLost={canMarkLost}
canReopen={canReopen}
canManageFollowups={{
create: canFollowupCreate,
update: canFollowupUpdate,
delete: canFollowupDelete
}}
/>
</HydrationBoundary>
{sourceLead ? (
<Card className='mb-6'>
<CardHeader>
<CardTitle>Source Lead</CardTitle>
</CardHeader>
<CardContent className='text-sm'>
<Link href={`/dashboard/crm/leads/${sourceLead.id}`} className='font-medium hover:underline'>
{sourceLead.code}
</Link>
<div className='text-muted-foreground mt-1'>{sourceLead.projectName ?? 'No project name'}</div>
</CardContent>
</Card>
) : null}
<HydrationBoundary state={dehydrate(queryClient)}>
<EnquiryDetail
enquiryId={id}
referenceData={referenceData}
relatedQuotations={relatedQuotations}
workspace='enquiry'
canUpdate={canUpdate}
canAssign={canAssign}
canReassign={canReassign}
canViewRelatedQuotations={canViewRelatedQuotations}
canViewOutcomeCommercialData={canViewOutcomeCommercialData}
canMarkWon={canMarkWon}
canMarkLost={canMarkLost}
canReopen={canReopen}
canManageFollowups={canManageFollowups}
/>
</HydrationBoundary>
</PageContainer>
);
}

View File

@@ -1,14 +1,9 @@
import { auth } from '@/auth';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { notFound, redirect } from 'next/navigation';
import PageContainer from '@/components/layout/page-container';
import {
enquiryByIdOptions,
enquiryFollowupsOptions,
enquiryProjectPartiesOptions
} from '@/features/crm/enquiries/api/queries';
import { EnquiryDetail } from '@/features/crm/enquiries/components/enquiry-detail';
import { getEnquiryDetail, getEnquiryReferenceData } from '@/features/crm/enquiries/server/service';
import { leadByIdOptions } from '@/features/crm/leads/api/queries';
import { LeadDetail } from '@/features/crm/leads/components/lead-detail';
import { getLeadReferenceData } from '@/features/crm/leads/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { getQueryClient } from '@/lib/query-client';
@@ -19,91 +14,57 @@ type PageProps = {
export default async function LeadDetailRoute({ params }: PageProps) {
const { id } = await params;
const session = await auth();
const canRead =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmLeadRead)));
const canUpdate =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmLeadUpdate)));
const canDelete =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmLeadDelete)));
const canAssign =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmLeadAssign)));
if (!canRead || !session?.user?.activeOrganizationId) {
return (
<PageContainer access={false} pageTitle='Lead Detail'>
{null}
</PageContainer>
);
}
const queryClient = getQueryClient();
if (!session?.user?.activeOrganizationId) {
notFound();
}
const enquiry = canRead
? await getEnquiryDetail(id, session.user.activeOrganizationId, {
organizationId: session.user.activeOrganizationId,
userId: session.user.id,
membershipRole: session.user.activeMembershipRole ?? 'user',
businessRole: session.user.activeBusinessRole ?? 'sales_support',
permissions: session.user.activePermissions ?? [],
branchScopeIds: session.user.activeBranchScopeIds ?? [],
productTypeScopeIds: session.user.activeProductTypeScopeIds ?? [],
ownershipScope: session.user.activeOwnershipScope ?? 'organization',
branchScopeMode: 'all',
productScopeMode: 'all'
})
: null;
if (enquiry && enquiry.pipelineStage !== 'lead') {
redirect(`/dashboard/crm/enquiries/${id}`);
}
if (canRead) {
void queryClient.prefetchQuery(enquiryByIdOptions(id));
void queryClient.prefetchQuery(enquiryProjectPartiesOptions(id));
void queryClient.prefetchQuery(enquiryFollowupsOptions(id));
}
const referenceData =
canRead && session.user.activeOrganizationId
? await getEnquiryReferenceData(session.user.activeOrganizationId)
: null;
await queryClient.prefetchQuery(leadByIdOptions(id));
const referenceData = await getLeadReferenceData(session.user.activeOrganizationId);
return (
<PageContainer
pageTitle='รายละเอียดลีด'
pageDescription='ข้อมูลลีดสำหรับการคัดกรอง ติดตาม และส่งต่อให้ฝ่ายขาย'
access={canRead}
accessFallback={
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
You do not have access to this lead.
</div>
}
pageTitle='Lead Detail'
pageDescription='Separated lead workspace for marketing qualification and sales handoff.'
>
{referenceData ? (
<HydrationBoundary state={dehydrate(queryClient)}>
<EnquiryDetail
enquiryId={id}
referenceData={referenceData}
relatedQuotations={[]}
workspace='lead'
canUpdate={canUpdate}
canAssign={canAssign}
canReassign={false}
canViewRelatedQuotations={false}
canViewOutcomeCommercialData={false}
canMarkWon={false}
canMarkLost={false}
canReopen={false}
canManageFollowups={{
create: false,
update: false,
delete: false
}}
/>
</HydrationBoundary>
) : null}
<HydrationBoundary state={dehydrate(queryClient)}>
<LeadDetail
leadId={id}
referenceData={referenceData}
canUpdate={canUpdate}
canDelete={canDelete}
canAssign={canAssign}
/>
</HydrationBoundary>
</PageContainer>
);
}

View File

@@ -1,14 +1,14 @@
import { auth } from '@/auth';
import PageContainer from '@/components/layout/page-container';
import EnquiryListing from '@/features/crm/enquiries/components/enquiry-listing';
import { EnquiryFormSheetTrigger } from '@/features/crm/enquiries/components/enquiry-form-sheet';
import { getEnquiryReferenceData } from '@/features/crm/enquiries/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { searchParamsCache } from '@/lib/searchparams';
import type { SearchParams } from 'nuqs/server';
import { auth } from "@/auth";
import PageContainer from "@/components/layout/page-container";
import { LeadFormTrigger } from "@/features/crm/leads/components/lead-form";
import { LeadList } from "@/features/crm/leads/components/lead-list";
import { getLeadReferenceData } from "@/features/crm/leads/server/service";
import { PERMISSIONS } from "@/lib/auth/rbac";
import { searchParamsCache } from "@/lib/searchparams";
import type { SearchParams } from "nuqs/server";
export const metadata = {
title: 'Dashboard: CRM Leads'
title: "Dashboard: CRM Leads",
};
type PageProps = {
@@ -18,63 +18,63 @@ type PageProps = {
export default async function LeadsRoute(props: PageProps) {
const searchParams = await props.searchParams;
const session = await auth();
const canRead =
session?.user?.systemRole === 'super_admin' ||
session?.user?.systemRole === "super_admin" ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
(session.user.activeMembershipRole === "admin" ||
session.user.activePermissions.includes(PERMISSIONS.crmLeadRead)));
const canCreate =
session?.user?.systemRole === 'super_admin' ||
session?.user?.systemRole === "super_admin" ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
(session.user.activeMembershipRole === "admin" ||
session.user.activePermissions.includes(PERMISSIONS.crmLeadCreate)));
const canUpdate =
session?.user?.systemRole === 'super_admin' ||
session?.user?.systemRole === "super_admin" ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
(session.user.activeMembershipRole === "admin" ||
session.user.activePermissions.includes(PERMISSIONS.crmLeadUpdate)));
const canDelete =
session?.user?.systemRole === 'super_admin' ||
session?.user?.systemRole === "super_admin" ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
(session.user.activeMembershipRole === "admin" ||
session.user.activePermissions.includes(PERMISSIONS.crmLeadDelete)));
const canAssign =
session?.user?.systemRole === 'super_admin' ||
session?.user?.systemRole === "super_admin" ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
(session.user.activeMembershipRole === "admin" ||
session.user.activePermissions.includes(PERMISSIONS.crmLeadAssign)));
if (!canRead || !session?.user?.activeOrganizationId) {
return (
<PageContainer access={false} pageTitle="CRM Leads">
{null}
</PageContainer>
);
}
searchParamsCache.parse(searchParams);
const referenceData =
canRead && session?.user?.activeOrganizationId
? await getEnquiryReferenceData(session.user.activeOrganizationId)
: null;
const referenceData = await getLeadReferenceData(
session.user.activeOrganizationId,
);
return (
<PageContainer
pageTitle='ลีด'
pageDescription='พื้นที่ทำงานของการตลาดสำหรับเก็บลีด คัดกรองความพร้อม และส่งต่อให้ฝ่ายขาย'
access={canRead}
accessFallback={
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
You do not have access to CRM leads.
</div>
}
pageTitle="CRM Leads"
pageDescription="Marketing-qualified lead workspace before handoff into sales enquiries."
pageHeaderAction={
canCreate && referenceData ? (
<EnquiryFormSheetTrigger referenceData={referenceData} workspace='lead' />
) : null
canCreate ? <LeadFormTrigger referenceData={referenceData} /> : null
}
>
{referenceData ? (
<EnquiryListing
<LeadList
referenceData={referenceData}
workspace='lead'
canUpdate={canUpdate}
canDelete={canDelete}
canAssign={canAssign}
canReassign={false}
/>
) : null}
</PageContainer>