This commit is contained in:
phaichayon
2026-06-15 16:34:41 +07:00
parent 1d2406483e
commit eae0dee1f2
40 changed files with 9982 additions and 132 deletions

View File

@@ -5,6 +5,7 @@ import { CustomerDetail } from '@/features/crm/customers/components/customer-det
import { customerByIdOptions, customerContactsOptions } from '@/features/crm/customers/api/queries';
import { getCustomerReferenceData } from '@/features/crm/customers/server/service';
import { listCustomerEnquiryRelations } from '@/features/crm/enquiries/server/service';
import { listCustomerQuotationRelations } from '@/features/crm/quotations/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { getQueryClient } from '@/lib/query-client';
@@ -55,6 +56,10 @@ export default async function CustomerDetailRoute({ params }: PageProps) {
canRead && session?.user?.activeOrganizationId
? await listCustomerEnquiryRelations(id, session.user.activeOrganizationId)
: [];
const relatedQuotations =
canRead && session?.user?.activeOrganizationId
? await listCustomerQuotationRelations(id, session.user.activeOrganizationId)
: [];
return (
<PageContainer
@@ -79,6 +84,7 @@ export default async function CustomerDetailRoute({ params }: PageProps) {
delete: canContactDelete
}}
relatedEnquiries={relatedEnquiries}
relatedQuotations={relatedQuotations}
/>
</HydrationBoundary>
) : null}

View File

@@ -4,6 +4,7 @@ import PageContainer from '@/components/layout/page-container';
import { enquiryByIdOptions, enquiryFollowupsOptions } from '@/features/crm/enquiries/api/queries';
import { EnquiryDetail } from '@/features/crm/enquiries/components/enquiry-detail';
import { getEnquiryReferenceData } from '@/features/crm/enquiries/server/service';
import { listEnquiryQuotationRelations } from '@/features/crm/quotations/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { getQueryClient } from '@/lib/query-client';
@@ -50,6 +51,10 @@ export default async function EnquiryDetailRoute({ params }: PageProps) {
canRead && session?.user?.activeOrganizationId
? await getEnquiryReferenceData(session.user.activeOrganizationId)
: null;
const relatedQuotations =
canRead && session?.user?.activeOrganizationId
? await listEnquiryQuotationRelations(id, session.user.activeOrganizationId)
: [];
return (
<PageContainer
@@ -67,6 +72,7 @@ export default async function EnquiryDetailRoute({ params }: PageProps) {
<EnquiryDetail
enquiryId={id}
referenceData={referenceData}
relatedQuotations={relatedQuotations}
canUpdate={canUpdate}
canManageFollowups={{
create: canFollowupCreate,

View File

@@ -1,5 +1,19 @@
import { auth } from '@/auth';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import PageContainer from '@/components/layout/page-container';
import { CrmProductionPlaceholder } from '@/features/foundation/components/crm-production-placeholder';
import {
quotationAttachmentsOptions,
quotationByIdOptions,
quotationCustomersOptions,
quotationFollowupsOptions,
quotationItemsOptions,
quotationRevisionsOptions,
quotationTopicsOptions
} from '@/features/crm/quotations/api/queries';
import { QuotationDetail } from '@/features/crm/quotations/components/quotation-detail';
import { getQuotationReferenceData } from '@/features/crm/quotations/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { getQueryClient } from '@/lib/query-client';
type PageProps = {
params: Promise<{ id: string }>;
@@ -7,24 +21,90 @@ type PageProps = {
export default async function QuotationDetailRoute({ 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.crmQuotationRead)));
const canUpdate =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmQuotationUpdate)));
const canManageItems =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmQuotationItemManage)));
const canManageCustomers =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmQuotationCustomerManage)));
const canManageTopics =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmQuotationTopicManage)));
const canManageFollowups =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmQuotationFollowupManage)));
const canManageAttachments =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmQuotationAttachmentManage)));
const canCreateRevision =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmQuotationRevisionCreate)));
const queryClient = getQueryClient();
if (canRead) {
void queryClient.prefetchQuery(quotationByIdOptions(id));
void queryClient.prefetchQuery(quotationItemsOptions(id));
void queryClient.prefetchQuery(quotationCustomersOptions(id));
void queryClient.prefetchQuery(quotationTopicsOptions(id));
void queryClient.prefetchQuery(quotationFollowupsOptions(id));
void queryClient.prefetchQuery(quotationAttachmentsOptions(id));
void queryClient.prefetchQuery(quotationRevisionsOptions(id));
}
const referenceData =
canRead && session?.user?.activeOrganizationId
? await getQuotationReferenceData(session.user.activeOrganizationId)
: null;
return (
<PageContainer
pageTitle='Quotation Detail'
pageDescription={`Production detail route placeholder for quotation ${id}.`}
pageDescription='Commercial detail, child records, revisions, and audit activity.'
access={canRead}
accessFallback={
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
You do not have access to this quotation.
</div>
}
>
<CrmProductionPlaceholder
title='Quotation detail pending'
summary='This production detail route is isolated from the legacy quotation preview and approval mock flow.'
foundationItems={[
'Document sequence helper',
'Branch scope validation',
'Master option lookups',
'Audit trail foundation'
]}
nextStep='Implement production quotation detail with real approval, preview, and related-entity data.'
demoHref={`/dashboard/crm-demo/quotations/${id}`}
/>
{referenceData ? (
<HydrationBoundary state={dehydrate(queryClient)}>
<QuotationDetail
quotationId={id}
referenceData={referenceData}
canUpdate={canUpdate}
canManageItems={canManageItems}
canManageCustomers={canManageCustomers}
canManageTopics={canManageTopics}
canManageFollowups={canManageFollowups}
canManageAttachments={canManageAttachments}
canCreateRevision={canCreateRevision}
/>
</HydrationBoundary>
) : null}
</PageContainer>
);
}

View File

@@ -1,28 +1,68 @@
import { auth } from '@/auth';
import PageContainer from '@/components/layout/page-container';
import { CrmProductionPlaceholder } from '@/features/foundation/components/crm-production-placeholder';
import QuotationListing from '@/features/crm/quotations/components/quotation-listing';
import { QuotationFormSheetTrigger } from '@/features/crm/quotations/components/quotation-form-sheet';
import { getQuotationReferenceData } from '@/features/crm/quotations/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 Quotations'
};
export default function QuotationsRoute() {
type PageProps = {
searchParams: Promise<SearchParams>;
};
export default async function QuotationsRoute(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.crmQuotationRead)));
const canCreate =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmQuotationCreate)));
const canUpdate =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmQuotationUpdate)));
const canDelete =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmQuotationDelete)));
searchParamsCache.parse(searchParams);
const referenceData =
canRead && session?.user?.activeOrganizationId
? await getQuotationReferenceData(session.user.activeOrganizationId)
: null;
return (
<PageContainer
pageTitle='Quotations'
pageDescription='Production quotation flows will be built on foundation services, not on the legacy demo layer.'
pageDescription='Production quotation register with revisions, related parties, and server-calculated items.'
access={canRead}
accessFallback={
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
You do not have access to CRM quotations.
</div>
}
pageHeaderAction={
canCreate && referenceData ? <QuotationFormSheetTrigger referenceData={referenceData} /> : null
}
>
<CrmProductionPlaceholder
title='Quotation module pending'
summary='The mock quotation list, kanban, and detail experiences now live only under crm-demo.'
foundationItems={[
'Quotation status master options',
'Server-side document sequence generation',
'Organization and branch context',
'Audit log helper for future status changes'
]}
nextStep='Implement quotation persistence and workflow after enquiry production APIs are ready.'
demoHref='/dashboard/crm-demo/quotations'
/>
{referenceData ? (
<QuotationListing referenceData={referenceData} canUpdate={canUpdate} canDelete={canDelete} />
) : null}
</PageContainer>
);
}