diff --git a/docs/implementation/task-d55-opportunity-sales-workspace-refinement.md b/docs/implementation/task-d55-opportunity-sales-workspace-refinement.md
new file mode 100644
index 0000000..401676b
--- /dev/null
+++ b/docs/implementation/task-d55-opportunity-sales-workspace-refinement.md
@@ -0,0 +1,92 @@
+# Task D.5.5: Opportunity / Sales Workspace Refinement
+
+## Objective
+Refine the sales-facing enquiry workspace so lead-origin enquiries are presented as opportunities without renaming the underlying `crm_enquiries` model or changing quotation, dashboard, report, or approval foundations.
+
+## Review Completed
+- `AGENTS.md`
+- `plans/task-d.5.5.md`
+- `docs/standards/project-foundations.md`
+- `docs/standards/task-catalog.md`
+- `docs/adr/0018-lead-enquiry-domain-separation.md`
+- `docs/implementation/task-d5-lead-enquiry-domain-separation.md`
+- `docs/implementation/task-d52-lead-domain-service-api-foundation.md`
+- `docs/implementation/task-d53-lead-assignment-create-enquiry-foundation.md`
+- `docs/implementation/task-d54-lead-enquiry-workspace-separation-ui-foundation.md`
+- `docs/security/crm-authorization-boundaries.md`
+- existing enquiry list/detail/form/assignment UI
+- existing quotation reference-data and creation UI
+
+## What Changed
+- Updated [enquiries page](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/dashboard/crm/enquiries/page.tsx) to use sales-facing opportunity wording in page title and description while keeping the same enquiry route.
+- Refined list-loading and table UI in:
+ - [enquiry-listing.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/components/enquiry-listing.tsx)
+ - [enquiries-table.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/components/enquiries-table.tsx)
+ - [enquiry-columns.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/components/enquiry-columns.tsx)
+- Opportunity list now emphasizes sales execution fields:
+ - opportunity code
+ - source lead indicator
+ - customer
+ - project name
+ - product type
+ - sales owner
+ - status
+ - estimated value
+ - chance percent
+ - expected close date
+ - next follow-up
+ - created date
+- Updated [opportunity detail page](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/app/dashboard/crm/enquiries/[id]/page.tsx) to load:
+ - optional source lead summary
+ - quotation reference data when quotation creation permission exists
+- Rebuilt [enquiry-detail.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/components/enquiry-detail.tsx) around clearer sales sections:
+ - Opportunity Summary
+ - Source Lead
+ - Customer / Contact
+ - Requirement
+ - Sales Qualification
+ - Follow-up Timeline
+ - Linked Quotations
+ - Activity
+ - Quotation Readiness
+- Added [source-lead-card.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/components/source-lead-card.tsx) for read-only lead provenance visibility.
+- Added [quotation-readiness-panel.tsx](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/components/quotation-readiness-panel.tsx) for non-blocking readiness checks.
+- Extended [types.ts](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/src/features/crm/enquiries/api/types.ts) with optional list-only fields for future source lead and follow-up enrichment without breaking existing API consumers.
+
+## UI Wording Decisions
+- Route kept as `/dashboard/crm/enquiries`
+- Page title changed to `CRM Opportunities`
+- Detail title changed to `Opportunity Detail`
+- Technical code and API names remain `enquiry` / `crm_enquiries`
+
+## APIs and Foundations Reused
+- Existing enquiry APIs remained unchanged:
+ - `GET /api/crm/enquiries`
+ - `GET /api/crm/enquiries/[id]`
+ - existing follow-up and assignment APIs
+- Existing quotation reference-data foundation reused for the `Create Quotation` action.
+- Lead detail service reused for source lead summary display.
+
+## Permission Mapping
+- Opportunity list/detail still rely on existing enquiry permissions:
+ - `crm.enquiry.read`
+ - `crm.enquiry.update`
+ - `crm.enquiry.assign`
+ - `crm.enquiry.reassign`
+ - `crm.enquiry.followup.create`
+ - `crm.enquiry.followup.update`
+ - `crm.enquiry.followup.delete`
+- Quotation action is shown only when `crm.quotation.create` is available.
+- Source lead visibility on the opportunity page does not widen lead permissions; it is only rendered for users who can already access the enquiry page.
+
+## Compatibility Notes
+- `crm_enquiries`, `/api/crm/enquiries`, and existing quotation flows were not renamed.
+- Records without `lead_id` continue to render as direct sales opportunities.
+- Dashboard, reports, approvals, and quotation services were not changed.
+
+## Known Limitations
+- The current `Create Quotation` action opens the existing quotation creation sheet without automatically preselecting the current opportunity.
+- Source-lead-specific list enrichment is prepared at the UI contract level, but the list still uses lightweight lead linkage signaling rather than a fully joined source-lead dataset.
+
+## Verification
+- `npm exec tsc --noEmit`
diff --git a/plans/task-d.5.5.md b/plans/task-d.5.5.md
new file mode 100644
index 0000000..2311fa4
--- /dev/null
+++ b/plans/task-d.5.5.md
@@ -0,0 +1,432 @@
+# Task D.5.5: Opportunity / Sales Workspace Refinement
+
+## Status
+
+Planned
+
+## Objective
+
+Refine the Sales workspace so enquiries created from leads are presented and operated as Sales Opportunities, while preserving existing enquiry, quotation, dashboard, report, and approval compatibility.
+
+Business boundary:
+
+```txt
+Lead = Marketing-owned source record
+Opportunity / Enquiry = Sales-owned execution workspace
+```
+
+This task does not replace `crm_enquiries`. It improves the sales-facing workspace around it.
+
+---
+
+## Mandatory Review
+
+Review before implementation:
+
+* `AGENTS.md`
+* `docs/standards/project-foundations.md`
+* `docs/standards/task-catalog.md`
+* `docs/adr/0018-lead-enquiry-domain-separation.md`
+* `docs/implementation/task-d5-lead-enquiry-domain-separation.md`
+* `docs/implementation/task-d52-lead-domain-service-api-foundation.md`
+* `docs/implementation/task-d53-lead-assignment-create-enquiry-foundation.md`
+* `docs/implementation/task-d54-lead-enquiry-workspace-separation-ui-foundation.md`
+* `docs/security/crm-authorization-boundaries.md`
+* existing enquiry API/service/UI
+* existing quotation conversion flow
+
+---
+
+## Current Foundation
+
+Already available:
+
+```txt
+crm_leads
+crm_enquiries
+crm_enquiries.lead_id
+
+Lead UI
+Enquiry UI
+Lead Assignment API
+Lead -> Enquiry creation
+Source Lead display in Enquiry detail
+```
+
+Do not recreate:
+
+```txt
+tables
+migrations
+lead assignment service
+lead APIs
+enquiry APIs
+quotation APIs
+```
+
+---
+
+## Business Rule
+
+After a Lead is assigned:
+
+```txt
+Lead
+ -> creates / reuses Enquiry
+ -> Enquiry becomes Sales Opportunity workspace
+```
+
+The DB object remains:
+
+```txt
+crm_enquiries
+```
+
+The sales-facing business wording may display as:
+
+```txt
+Opportunity
+```
+
+where appropriate.
+
+---
+
+## Scope D5.5.1 Sales Workspace Wording
+
+Refine enquiry UI labels for sales-facing pages.
+
+Recommended display naming:
+
+```txt
+Menu: Enquiries or Opportunities
+Page title: Opportunities
+Detail title: Opportunity Detail
+Source label: Source Lead
+```
+
+Rules:
+
+* Do not rename database tables
+* Do not rename API paths in this task
+* Do not break existing `/api/crm/enquiries`
+* Avoid global find/replace that may break compatibility
+* Keep technical names in code where already established unless safely isolated
+
+---
+
+## Scope D5.5.2 Opportunity List Refinement
+
+Update existing enquiry list to better support Sales workflow.
+
+Recommended columns:
+
+```txt
+Enquiry / Opportunity Code
+Source Lead
+Customer
+Project Name
+Product Type
+Sales Owner
+Status
+Estimated Value
+Chance %
+Expected Close Date
+Last Follow-up
+Next Follow-up
+Created Date
+```
+
+Recommended filters:
+
+```txt
+Sales Owner
+Product Type
+Status
+Customer
+Source Lead
+Expected Close Date
+```
+
+Rules:
+
+* Use existing enquiry API where possible
+* Add only non-breaking response fields if needed
+* Do not change dashboard/report query contracts
+* Preserve current enquiry list behavior for records without `lead_id`
+
+---
+
+## Scope D5.5.3 Opportunity Detail Refinement
+
+Update existing enquiry detail page to make sales execution clearer.
+
+Sections:
+
+```txt
+Opportunity Summary
+Source Lead
+Customer / Contact
+Requirement
+Sales Qualification
+Follow-up Timeline
+Quotation Readiness
+Linked Quotations
+```
+
+Actions:
+
+```txt
+Edit Opportunity
+Create Follow-up
+Create Quotation
+Open Source Lead
+```
+
+Rules:
+
+* `Source Lead` is read-only
+* Marketing users may not get broader enquiry access from this block
+* Quotation creation behavior must remain unchanged
+* Enquiry records without `lead_id` must still render normally
+
+---
+
+## Scope D5.5.4 Sales Qualification Fields
+
+Use existing enquiry fields:
+
+```txt
+requirement
+estimatedValue
+chancePercent
+expectedCloseDate
+competitor
+priority
+status
+```
+
+Refine UI grouping only.
+
+Rules:
+
+* Do not add new fields unless already present in schema
+* Do not create migration
+* Keep validation compatible with existing enquiry service
+* Avoid changing required fields that could break legacy creation
+
+---
+
+## Scope D5.5.5 Source Lead Read Model
+
+Enhance enquiry detail read model if needed to include minimal source lead data:
+
+```ts
+sourceLead?: {
+ id: string;
+ code: string;
+ projectName?: string | null;
+ status?: string | null;
+ awareness?: string | null;
+}
+```
+
+Rules:
+
+* Only expose source lead if current user can access the enquiry
+* Do not widen Lead permission from Enquiry page
+* Do not make extra client-side calls if server-side enrichment already exists
+
+---
+
+## Scope D5.5.6 Follow-up Continuity
+
+Keep existing enquiry follow-up architecture.
+
+Rules:
+
+* Sales follow-ups belong to enquiry/opportunity
+* Lead follow-ups remain audit-backed from D.5.2 until future persistence decision
+* Do not merge lead follow-up and enquiry follow-up timelines in this task
+* Optional: show Source Lead activity as read-only if already available and safe
+
+---
+
+## Scope D5.5.7 Quotation Readiness Panel
+
+Add a lightweight readiness panel on Opportunity detail.
+
+Suggested checks:
+
+```txt
+Customer selected
+Contact selected
+Project name present
+Product type present
+Requirement present
+Estimated value present
+```
+
+Output:
+
+```txt
+Ready to create quotation
+Missing required information
+```
+
+Rules:
+
+* Informational only
+* Do not block quotation creation unless existing service already blocks it
+* Do not change quotation service validation
+
+---
+
+## Scope D5.5.8 Permission Handling
+
+Use existing enquiry permissions and CRM access boundaries.
+
+Examples:
+
+```txt
+crm.enquiry.read
+crm.enquiry.update
+crm.enquiry.followup.create
+crm.quotation.create
+```
+
+Rules:
+
+* Do not check role strings directly
+* Hide unauthorized actions
+* Handle 403 gracefully
+* Backend remains the source of truth
+
+---
+
+## Scope D5.5.9 Documentation
+
+Create:
+
+```txt
+docs/implementation/task-d55-opportunity-sales-workspace-refinement.md
+```
+
+Document:
+
+```txt
+UI wording decisions
+routes changed
+components changed
+API fields added
+compatibility notes
+known limitations
+```
+
+---
+
+## Deliverables
+
+Updated:
+
+```txt
+src/app/**/crm/enquiries/page.tsx
+src/app/**/crm/enquiries/[id]/page.tsx
+```
+
+Updated or added components:
+
+```txt
+src/features/crm/enquiries/components/enquiry-list.tsx
+src/features/crm/enquiries/components/enquiries-table.tsx
+src/features/crm/enquiries/components/enquiry-detail.tsx
+src/features/crm/enquiries/components/source-lead-card.tsx
+src/features/crm/enquiries/components/quotation-readiness-panel.tsx
+```
+
+Optional updated API/read model:
+
+```txt
+src/features/crm/enquiries/api/types.ts
+src/features/crm/enquiries/server/service.ts
+```
+
+Documentation:
+
+```txt
+docs/implementation/task-d55-opportunity-sales-workspace-refinement.md
+```
+
+---
+
+## Explicit Non-Scope
+
+Do not:
+
+* create new tables
+* create migrations
+* rename `crm_enquiries`
+* rename `/api/crm/enquiries`
+* rewrite quotation conversion
+* change approval workflow
+* change dashboard datasets
+* change reports
+* implement won/lost synchronization
+* migrate legacy enquiry records
+* merge lead and enquiry follow-up persistence
+* remove `crm_enquiries.pipeline_stage`
+
+These belong to later phases.
+
+---
+
+## Verification
+
+Run:
+
+```txt
+npm exec tsc --noEmit
+```
+
+Optional:
+
+```txt
+npm run lint
+npm run build
+```
+
+Manual verification:
+
+```txt
+Opportunity/Enquiry list still loads
+Records with lead_id show Source Lead
+Records without lead_id still work
+Opportunity detail shows sales execution sections
+Create quotation still works
+Existing enquiry follow-ups still work
+Lead page still opens linked enquiry
+Quotation page still works
+Dashboard still works
+Reports still work
+```
+
+---
+
+## Definition of Done
+
+Task is complete when:
+
+* Sales workspace clearly represents assigned enquiries as opportunities
+* Existing enquiry API compatibility remains intact
+* Source Lead is visible from Opportunity detail
+* Quotation readiness is visible but non-blocking
+* Existing quotation, dashboard, report, and approval behavior remains unchanged
+
+Result:
+
+```txt
+Opportunity Workspace = Refined
+Sales Execution UX = Clarified
+Lead / Enquiry Boundary = Preserved
+Ready for D.5.6 Won / Lost Lifecycle Alignment
+```
diff --git a/src/app/dashboard/crm/enquiries/[id]/page.tsx b/src/app/dashboard/crm/enquiries/[id]/page.tsx
index 6fd48a9..b900ed0 100644
--- a/src/app/dashboard/crm/enquiries/[id]/page.tsx
+++ b/src/app/dashboard/crm/enquiries/[id]/page.tsx
@@ -1,9 +1,7 @@
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,
@@ -13,6 +11,7 @@ import { EnquiryDetail } from '@/features/crm/enquiries/components/enquiry-detai
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 { getQuotationReferenceData } from '@/features/crm/quotations/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { getQueryClient } from '@/lib/query-client';
@@ -90,6 +89,12 @@ export default async function EnquiryDetailRoute({ params }: PageProps) {
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.crmQuotationRead)));
+ const canCreateQuotation =
+ session?.user?.systemRole === 'super_admin' ||
+ (!!session?.user?.activeOrganizationId &&
+ (session.user.activeMembershipRole === 'admin' ||
+ session.user.activePermissions.includes(PERMISSIONS.crmQuotationCreate)));
+
const canViewOutcomeCommercialData = canRead;
if (!canRead || !session?.user?.activeOrganizationId || !session.user.id) {
@@ -119,33 +124,20 @@ export default async function EnquiryDetailRoute({ params }: PageProps) {
]);
const enquiryDetail = await getEnquiryDetail(id, session.user.activeOrganizationId, accessContext);
- const [referenceData, relatedQuotations, sourceLead] = await Promise.all([
+ const [referenceData, relatedQuotations, sourceLead, quotationReferenceData] = 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)
+ : Promise.resolve(null),
+ canCreateQuotation ? getQuotationReferenceData(session.user.activeOrganizationId) : Promise.resolve(null)
]);
return (
- {sourceLead ? (
-
-
- Source Lead
-
-
-
- {sourceLead.code}
-
- {sourceLead.projectName ?? 'No project name'}
-
-
- ) : null}
-
diff --git a/src/app/dashboard/crm/enquiries/page.tsx b/src/app/dashboard/crm/enquiries/page.tsx
index 26c50b0..2ddbd41 100644
--- a/src/app/dashboard/crm/enquiries/page.tsx
+++ b/src/app/dashboard/crm/enquiries/page.tsx
@@ -1,14 +1,13 @@
-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 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 type { SearchParams } from 'nuqs/server';
export const metadata = {
- title: "Dashboard: CRM Enquiries",
+ title: 'Dashboard: CRM Opportunities'
};
type PageProps = {
@@ -16,75 +15,69 @@ type PageProps = {
};
export default async function EnquiriesRoute(props: PageProps) {
- const searchParams = await props.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.crmEnquiryRead)));
+
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.crmEnquiryCreate)));
+
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.crmEnquiryUpdate)));
+
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.crmEnquiryDelete)));
+
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.crmEnquiryAssign)));
+
const canReassign =
- session?.user?.systemRole === "super_admin" ||
+ session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
- (session.user.activeMembershipRole === "admin" ||
- session.user.activePermissions.includes(
- PERMISSIONS.crmEnquiryReassign,
- )));
- const businessRole = session?.user?.activeBusinessRole ?? null;
+ (session.user.activeMembershipRole === 'admin' ||
+ session.user.activePermissions.includes(PERMISSIONS.crmEnquiryReassign)));
- searchParamsCache.parse(searchParams);
+ if (!canRead || !session?.user?.activeOrganizationId) {
+ return (
+
+ {null}
+
+ );
+ }
- const referenceData =
- canRead && session?.user?.activeOrganizationId
- ? await getEnquiryReferenceData(session.user.activeOrganizationId)
- : null;
+ const referenceData = await getEnquiryReferenceData(session.user.activeOrganizationId);
return (
- You do not have access to CRM enquiries.
-
- }
- pageHeaderAction={
- canCreate && referenceData ? (
-
- ) : null
- }
+ pageTitle='CRM Opportunities'
+ pageDescription='Sales execution workspace for opportunities created directly or handed off from marketing leads.'
+ pageHeaderAction={canCreate ? : null}
>
- {referenceData ? (
-
- ) : null}
+
);
}
diff --git a/src/features/crm/enquiries/api/types.ts b/src/features/crm/enquiries/api/types.ts
index f34762f..770d871 100644
--- a/src/features/crm/enquiries/api/types.ts
+++ b/src/features/crm/enquiries/api/types.ts
@@ -132,6 +132,9 @@ export interface EnquiryRecord {
export interface EnquiryListItem extends EnquiryRecord {
customerName: string;
contactName: string | null;
+ sourceLeadCode?: string | null;
+ sourceLeadProjectName?: string | null;
+ lastFollowupDate?: string | null;
followupCount: number;
quotationCount: number;
nextFollowupDate: string | null;
diff --git a/src/features/crm/enquiries/components/enquiries-table.tsx b/src/features/crm/enquiries/components/enquiries-table.tsx
index bba61fb..b0e0a18 100644
--- a/src/features/crm/enquiries/components/enquiries-table.tsx
+++ b/src/features/crm/enquiries/components/enquiries-table.tsx
@@ -1,15 +1,15 @@
-"use client";
+'use client';
-import { useMemo } from "react";
-import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
-import { useSuspenseQuery } from "@tanstack/react-query";
-import { DataTable } from "@/components/ui/table/data-table";
-import { DataTableToolbar } from "@/components/ui/table/data-table-toolbar";
-import { useDataTable } from "@/hooks/use-data-table";
-import { getSortingStateParser } from "@/lib/parsers";
-import { enquiriesQueryOptions } from "../api/queries";
-import type { EnquiryReferenceData } from "../api/types";
-import { getEnquiryColumns } from "./enquiry-columns";
+import { useMemo } from 'react';
+import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs';
+import { useSuspenseQuery } from '@tanstack/react-query';
+import { DataTable } from '@/components/ui/table/data-table';
+import { DataTableToolbar } from '@/components/ui/table/data-table-toolbar';
+import { useDataTable } from '@/hooks/use-data-table';
+import { getSortingStateParser } from '@/lib/parsers';
+import { enquiriesQueryOptions } from '../api/queries';
+import type { EnquiryReferenceData } from '../api/types';
+import { getEnquiryColumns } from './enquiry-columns';
export function EnquiriesTable({
referenceData,
@@ -17,10 +17,10 @@ export function EnquiriesTable({
canUpdate,
canDelete,
canAssign,
- canReassign,
+ canReassign
}: {
referenceData: EnquiryReferenceData;
- workspace: "lead" | "enquiry";
+ workspace: 'lead' | 'enquiry';
canUpdate: boolean;
canDelete: boolean;
canAssign: boolean;
@@ -34,13 +34,12 @@ export function EnquiriesTable({
canUpdate,
canDelete,
canAssign,
- canReassign,
+ canReassign
}),
- [referenceData, workspace, canUpdate, canDelete, canAssign, canReassign],
+ [referenceData, workspace, canUpdate, canDelete, canAssign, canReassign]
);
- const columnIds = columns
- .map((column) => column.id)
- .filter(Boolean) as string[];
+
+ const columnIds = columns.map((column) => column.id).filter(Boolean) as string[];
const [params] = useQueryStates({
page: parseAsInteger.withDefault(1),
perPage: parseAsInteger.withDefault(10),
@@ -50,7 +49,7 @@ export function EnquiriesTable({
priority: parseAsString,
branch: parseAsString,
customer: parseAsString,
- sort: getSortingStateParser(columnIds).withDefault([]),
+ sort: getSortingStateParser(columnIds).withDefault([])
});
const filters = {
@@ -63,21 +62,19 @@ export function EnquiriesTable({
...(params.priority && { priority: params.priority }),
...(params.branch && { branch: params.branch }),
...(params.customer && { customer: params.customer }),
- ...(params.sort.length > 0 && { sort: JSON.stringify(params.sort) }),
+ ...(params.sort.length > 0 && { sort: JSON.stringify(params.sort) })
};
const { data } = useSuspenseQuery(enquiriesQueryOptions(filters));
- console.log("data", data);
- const pageCount = Math.ceil(data.totalItems / params.perPage);
+ const pageCount = Math.max(1, Math.ceil((data.totalItems || 0) / params.perPage));
+
const { table } = useDataTable({
data: data.items,
columns,
pageCount,
shallow: true,
debounceMs: 500,
- initialState: {
- columnPinning: { right: ["actions"] },
- },
+ clearOnDefault: true
});
return (
diff --git a/src/features/crm/enquiries/components/enquiry-columns.tsx b/src/features/crm/enquiries/components/enquiry-columns.tsx
index 6be94b9..2b204fb 100644
--- a/src/features/crm/enquiries/components/enquiry-columns.tsx
+++ b/src/features/crm/enquiries/components/enquiry-columns.tsx
@@ -2,13 +2,21 @@
import Link from 'next/link';
import type { Column, ColumnDef } from '@tanstack/react-table';
+import { Icons } from '@/components/icons';
import { Badge } from '@/components/ui/badge';
import { DataTableColumnHeader } from '@/components/ui/table/data-table-column-header';
-import { Icons } from '@/components/icons';
import type { EnquiryListItem, EnquiryReferenceData } from '../api/types';
import { EnquiryCellAction } from './enquiry-cell-action';
import { EnquiryStatusBadge } from './enquiry-status-badge';
+type SharedColumnsConfig = {
+ referenceData: EnquiryReferenceData;
+ canUpdate: boolean;
+ canDelete: boolean;
+ canAssign: boolean;
+ canReassign: boolean;
+};
+
function getLeadColumns({
referenceData,
canUpdate,
@@ -28,10 +36,7 @@ function getLeadColumns({
),
cell: ({ row }) => (
-
+
{row.original.code}
{row.original.title}
@@ -57,68 +62,19 @@ function getLeadColumns({
id: 'customer',
accessorFn: (row) => row.customerId,
header: ({ column }: { column: Column
}) => (
-
+
),
cell: ({ row }) => {row.original.customerName},
meta: {
- label: 'Company',
- variant: 'multiSelect' as const,
+ label: 'Customer',
+ variant: 'select' as const,
options: referenceData.customers.map((item) => ({
value: item.id,
- label: item.name
+ label: `${item.name} (${item.code})`
}))
},
enableColumnFilter: true
},
- {
- id: 'contact',
- accessorFn: (row) => row.contactId ?? '',
- header: ({ column }: { column: Column }) => (
-
- ),
- cell: ({ row }) => {row.original.contactName ?? '-'}
- },
- {
- id: 'assignedSales',
- accessorFn: (row) => row.assignedToName ?? '',
- header: ({ column }: { column: Column }) => (
-
- ),
- cell: ({ row }) => {row.original.assignedToName ?? 'Unassigned'}
- },
- {
- id: 'createdBy',
- accessorFn: (row) => row.createdByName ?? row.createdBy,
- header: ({ column }: { column: Column }) => (
-
- ),
- cell: ({ row }) => {row.original.createdByName ?? row.original.createdBy}
- },
- {
- id: 'createdAt',
- accessorKey: 'createdAt',
- header: ({ column }: { column: Column }) => (
-
- ),
- cell: ({ row }) => {new Date(row.original.createdAt).toLocaleDateString()}
- },
- {
- id: 'age',
- accessorFn: (row) => row.createdAt,
- header: ({ column }: { column: Column }) => (
-
- ),
- cell: ({ row }) => {
- const ageInDays = Math.max(
- 0,
- Math.floor(
- (Date.now() - new Date(row.original.createdAt).getTime()) / (1000 * 60 * 60 * 24)
- )
- );
-
- return {ageInDays} days;
- }
- },
{
id: 'status',
accessorKey: 'status',
@@ -164,13 +120,14 @@ function getEnquiryWorkspaceColumns({
canReassign
}: SharedColumnsConfig): ColumnDef[] {
const statusMap = new Map(referenceData.statuses.map((item) => [item.id, item]));
+ const productTypeMap = new Map(referenceData.productTypes.map((item) => [item.id, item.label]));
return [
{
id: 'code',
accessorKey: 'code',
header: ({ column }: { column: Column }) => (
-
+
),
cell: ({ row }) => (
@@ -184,92 +141,76 @@ function getEnquiryWorkspaceColumns({
),
meta: {
- label: 'Code',
- placeholder: 'Search enquiries...',
+ label: 'Opportunity',
+ placeholder: 'Search opportunities...',
variant: 'text' as const,
icon: Icons.search
},
enableColumnFilter: true
},
{
- id: 'projectName',
- accessorFn: (row) => row.projectName ?? row.title,
+ id: 'sourceLead',
+ accessorFn: (row) => (row.leadId ? 'linked' : 'direct'),
header: ({ column }: { column: Column }) => (
-
+
),
- cell: ({ row }) => {row.original.projectName ?? row.original.title}
+ cell: ({ row }) =>
+ row.original.leadId ? (
+ {row.original.source ?? row.original.sourceLeadCode ?? 'Lead linked'}
+ ) : (
+ Direct sales
+ )
},
{
id: 'customer',
accessorFn: (row) => row.customerId,
header: ({ column }: { column: Column }) => (
-
+
),
cell: ({ row }) => {row.original.customerName},
meta: {
- label: 'ลูกค้าผู้รับใบเสนอราคา',
- variant: 'multiSelect' as const,
+ label: 'Customer',
+ variant: 'select' as const,
options: referenceData.customers.map((item) => ({
value: item.id,
- label: item.name
+ label: `${item.name} (${item.code})`
}))
},
enableColumnFilter: true
},
{
- id: 'assignedSales',
+ id: 'projectName',
+ accessorFn: (row) => row.projectName ?? '',
+ header: ({ column }: { column: Column }) => (
+
+ ),
+ cell: ({ row }) => {row.original.projectName ?? '-'}
+ },
+ {
+ id: 'productType',
+ accessorFn: (row) => row.productType,
+ header: ({ column }: { column: Column }) => (
+
+ ),
+ cell: ({ row }) => {productTypeMap.get(row.original.productType) ?? row.original.productType},
+ meta: {
+ label: 'Product Type',
+ variant: 'multiSelect' as const,
+ options: referenceData.productTypes.map((item) => ({
+ value: item.id,
+ label: item.label
+ }))
+ },
+ enableColumnFilter: true
+ },
+ {
+ id: 'assignedToName',
accessorFn: (row) => row.assignedToName ?? '',
header: ({ column }: { column: Column }) => (
-
+
),
cell: ({ row }) => {row.original.assignedToName ?? '-'}
},
- {
- id: 'chancePercent',
- accessorKey: 'chancePercent',
- header: ({ column }: { column: Column }) => (
-
- ),
- cell: ({ row }) => (
- {row.original.chancePercent !== null ? `${row.original.chancePercent}%` : '-'}
- )
- },
- {
- id: 'estimatedValue',
- accessorKey: 'estimatedValue',
- header: ({ column }: { column: Column }) => (
-
- ),
- cell: ({ row }) => (
-
- {row.original.estimatedValue !== null
- ? row.original.estimatedValue.toLocaleString()
- : '-'}
-
- )
- },
- {
- id: 'quotationCount',
- accessorKey: 'quotationCount',
- header: ({ column }: { column: Column }) => (
-
- ),
- cell: ({ row }) => {row.original.quotationCount}
- },
- {
- id: 'nextFollowupDate',
- accessorKey: 'nextFollowupDate',
- header: ({ column }: { column: Column }) => (
-
- ),
- cell: ({ row }) => (
-
- {row.original.nextFollowupDate
- ? new Date(row.original.nextFollowupDate).toLocaleDateString()
- : '-'}
-
- )
- },
{
id: 'status',
accessorKey: 'status',
@@ -290,6 +231,60 @@ function getEnquiryWorkspaceColumns({
},
enableColumnFilter: true
},
+ {
+ id: 'estimatedValue',
+ accessorKey: 'estimatedValue',
+ header: ({ column }: { column: Column }) => (
+
+ ),
+ cell: ({ row }) => (
+
+ {row.original.estimatedValue !== null ? row.original.estimatedValue.toLocaleString() : '-'}
+
+ )
+ },
+ {
+ id: 'chancePercent',
+ accessorKey: 'chancePercent',
+ header: ({ column }: { column: Column }) => (
+
+ ),
+ cell: ({ row }) => {row.original.chancePercent !== null ? `${row.original.chancePercent}%` : '-'}
+ },
+ {
+ id: 'expectedCloseDate',
+ accessorKey: 'expectedCloseDate',
+ header: ({ column }: { column: Column }) => (
+
+ ),
+ cell: ({ row }) => (
+
+ {row.original.expectedCloseDate
+ ? new Date(row.original.expectedCloseDate).toLocaleDateString()
+ : '-'}
+
+ )
+ },
+ {
+ id: 'nextFollowupDate',
+ accessorKey: 'nextFollowupDate',
+ header: ({ column }: { column: Column }) => (
+
+ ),
+ cell: ({ row }) => (
+
+ {row.original.nextFollowupDate ? new Date(row.original.nextFollowupDate).toLocaleDateString() : '-'}
+
+ )
+ },
+ {
+ id: 'createdAt',
+ accessorKey: 'createdAt',
+ header: ({ column }: { column: Column }) => (
+
+ ),
+ cell: ({ row }) => {new Date(row.original.createdAt).toLocaleDateString()}
+ },
{
id: 'actions',
cell: ({ row }) => (
@@ -307,14 +302,6 @@ function getEnquiryWorkspaceColumns({
];
}
-type SharedColumnsConfig = {
- referenceData: EnquiryReferenceData;
- canUpdate: boolean;
- canDelete: boolean;
- canAssign: boolean;
- canReassign: boolean;
-};
-
export function getEnquiryColumns({
referenceData,
workspace,
@@ -322,18 +309,7 @@ export function getEnquiryColumns({
canDelete,
canAssign,
canReassign
-}: SharedColumnsConfig & {
- workspace: 'lead' | 'enquiry';
-}): ColumnDef[] {
- const sharedConfig = {
- referenceData,
- canUpdate,
- canDelete,
- canAssign,
- canReassign
- };
-
- return workspace === 'lead'
- ? getLeadColumns(sharedConfig)
- : getEnquiryWorkspaceColumns(sharedConfig);
+}: SharedColumnsConfig & { workspace: 'lead' | 'enquiry' }): ColumnDef[] {
+ const sharedConfig = { referenceData, canUpdate, canDelete, canAssign, canReassign };
+ return workspace === 'lead' ? getLeadColumns(sharedConfig) : getEnquiryWorkspaceColumns(sharedConfig);
}
diff --git a/src/features/crm/enquiries/components/enquiry-detail.tsx b/src/features/crm/enquiries/components/enquiry-detail.tsx
index 3eb9aae..b448e14 100644
--- a/src/features/crm/enquiries/components/enquiry-detail.tsx
+++ b/src/features/crm/enquiries/components/enquiry-detail.tsx
@@ -7,9 +7,9 @@ import { Icons } from '@/components/icons';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
-import { Separator } from '@/components/ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import type { QuotationRelationItem } from '@/features/crm/quotations/api/types';
+import type { QuotationReferenceData, QuotationRelationItem } from '@/features/crm/quotations/api/types';
+import { QuotationFormSheet } from '@/features/crm/quotations/components/quotation-form-sheet';
import { getPipelineStageThaiLabel } from '@/features/crm/shared/terminology';
import { enquiryByIdOptions, enquiryProjectPartiesOptions } from '../api/queries';
import type { EnquiryRecord, EnquiryReferenceData } from '../api/types';
@@ -18,6 +18,8 @@ import { EnquiryFollowupsTab } from './enquiry-followups-tab';
import { EnquiryFormSheet } from './enquiry-form-sheet';
import { EnquiryOutcomeCard } from './enquiry-outcome-card';
import { EnquiryStatusBadge } from './enquiry-status-badge';
+import { QuotationReadinessPanel } from './quotation-readiness-panel';
+import { SourceLeadCard, type SourceLeadSummary } from './source-lead-card';
function FieldItem({ label, value }: { label: string; value: string | null | undefined }) {
return (
@@ -45,11 +47,14 @@ interface EnquiryDetailProps {
canMarkWon: boolean;
canMarkLost: boolean;
canReopen: boolean;
+ canCreateQuotation: boolean;
canManageFollowups: {
create: boolean;
update: boolean;
delete: boolean;
};
+ sourceLead?: SourceLeadSummary | null;
+ quotationReferenceData?: QuotationReferenceData | null;
}
export function EnquiryDetail({
@@ -65,13 +70,18 @@ export function EnquiryDetail({
canMarkWon,
canMarkLost,
canReopen,
- canManageFollowups
+ canCreateQuotation,
+ canManageFollowups,
+ sourceLead = null,
+ quotationReferenceData = null
}: EnquiryDetailProps) {
const { data } = useSuspenseQuery(enquiryByIdOptions(enquiryId));
const { data: projectPartiesData } = useSuspenseQuery(enquiryProjectPartiesOptions(enquiryId));
const [editOpen, setEditOpen] = useState(false);
const [assignmentOpen, setAssignmentOpen] = useState(false);
+ const [quotationOpen, setQuotationOpen] = useState(false);
const enquiry = data.enquiry;
+
const branchMap = useMemo(
() => new Map(referenceData.branches.map((item) => [item.id, item.name])),
[referenceData]
@@ -89,300 +99,238 @@ export function EnquiryDetail({
[referenceData]
);
const productTypeMap = useMemo(
- () => new Map(referenceData.productTypes.map((item) => [item.id, item])),
+ () => new Map(referenceData.productTypes.map((item) => [item.id, item.label])),
[referenceData]
);
const priorityMap = useMemo(
- () => new Map(referenceData.priorities.map((item) => [item.id, item])),
- [referenceData]
- );
- const leadChannelMap = useMemo(
- () => new Map(referenceData.leadChannels.map((item) => [item.id, item])),
+ () => new Map(referenceData.priorities.map((item) => [item.id, item.label])),
[referenceData]
);
+
const customer = customerMap.get(enquiry.customerId);
const contact = enquiry.contactId ? contactMap.get(enquiry.contactId) : null;
const status = statusMap.get(enquiry.status);
+ const pipelineStageLabel = getPipelineStageLabel(enquiry.pipelineStage);
const canManageAssignment = enquiry.assignedToUserId ? canReassign : canAssign;
const assignmentMode = enquiry.assignedToUserId ? 'reassign' : 'assign';
- const pipelineStageLabel = getPipelineStageLabel(enquiry.pipelineStage);
- const isLeadWorkspace = workspace === 'lead';
- const backHref = isLeadWorkspace ? '/dashboard/crm/leads' : '/dashboard/crm/enquiries';
- const recordLabel = isLeadWorkspace ? 'ลีด' : 'โอกาสขาย';
- const ownerLabel = isLeadWorkspace ? 'ฝ่ายขายที่รับผิดชอบ' : 'ผู้ดูแลโอกาสขาย';
- const infoCardTitle = isLeadWorkspace ? 'ข้อมูลลีด' : 'ข้อมูลโอกาสขาย';
- const infoCardDescription = isLeadWorkspace
- ? 'ที่มาของลีด ข้อมูลการคัดกรอง และความพร้อมในการส่งต่อให้ฝ่ายขาย'
- : 'ขอบเขตงาน ข้อมูลผู้รับผิดชอบ และรายละเอียดการดูแลของฝ่ายขาย';
- const assignmentButtonLabel =
- assignmentMode === 'assign'
- ? isLeadWorkspace
- ? 'ส่งต่อให้ฝ่ายขาย'
- : 'มอบหมายผู้ดูแลโอกาสขาย'
- : isLeadWorkspace
- ? 'เปลี่ยนฝ่ายขายที่รับผิดชอบ'
- : 'เปลี่ยนผู้ดูแลโอกาสขาย';
- const projectSectionLabel = isLeadWorkspace ? 'ขอบเขตลีด' : 'ข้อมูลโครงการ';
- const followupTabLabel = isLeadWorkspace ? 'งานติดตามลีด' : 'งานติดตาม';
- const relatedTabLabel = isLeadWorkspace ? 'ใบเสนอราคาที่เกี่ยวข้อง' : 'ใบเสนอราคา';
- const snapshotDescription = isLeadWorkspace
- ? 'ข้อมูลระบบและสถานะการส่งต่อของลีดรายการนี้'
- : 'ข้อมูลระบบและสถานะการรับผิดชอบของโอกาสขายรายการนี้';
return (
-
-
-
-
- {enquiry.code}
-
- {enquiry.isHotProject ? Hot : null}
-
-
-
{enquiry.title}
-
- {pipelineStageLabel} | {customer?.name ?? 'ไม่ทราบชื่อลูกค้า'}
- {contact ? ` | ${contact.name}` : ''}
-
+
+
+ {quotationReferenceData ? (
+
+ ) : null}
+
+
+
+
+
{enquiry.code}
+
+
+ Opportunity detail for sales execution, follow-up continuity, and quotation readiness.
+
+
+ {canCreateQuotation ? (
+
+ ) : null}
+ {sourceLead ? (
+
+ ) : null}
{canManageAssignment ? (
) : null}
{canUpdate ? (
) : null}
-
-
+
+
-
-
-
- ภาพรวม
- {followupTabLabel}
- กิจกรรม
- {canViewRelatedQuotations ? (
- {relatedTabLabel}
- ) : null}
-
-
-
-
- {infoCardTitle}
- {infoCardDescription}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {isLeadWorkspace ? 'ผู้เกี่ยวข้องของลีด' : 'ผู้เกี่ยวข้องในโครงการ'}
-
- {!projectPartiesData.items.length ? (
-
- {isLeadWorkspace
- ? 'ยังไม่มีผู้เกี่ยวข้องของลีด'
- : 'ยังไม่มีผู้เกี่ยวข้องในโครงการ'}
-
- ) : (
-
- {projectPartiesData.items.map((item) => (
-
-
{item.customerName}
-
- บทบาท: {item.roleLabel ?? item.roleCode}
-
- {item.remark ?
{item.remark}
: null}
-
- ))}
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {isLeadWorkspace ? 'กิจกรรมของลีด' : 'กิจกรรม'}
- ประวัติการเปลี่ยนแปลงของรายการนี้และงานติดตามที่เกี่ยวข้อง
-
-
- {!data.activity.length ? (
-
- ยังไม่มีกิจกรรม
-
- ) : (
- data.activity.map((item, index) => (
-
-
-
-
-
-
- {item.action}
-
-
- {item.entityType.replaceAll('_', ' ')}
-
-
- {item.actorName ?? item.userId}
-
-
-
- {new Date(item.createdAt).toLocaleString()}
-
-
-
- {index < data.activity.length - 1 ?
: null}
-
- ))
- )}
-
-
-
- {canViewRelatedQuotations ? (
-
-
-
- {relatedTabLabel}
-
- ใบเสนอราคาที่เชื่อมกับรายการนี้แล้ว
-
-
-
- {!relatedQuotations.length ? (
-
- ยังไม่มีใบเสนอราคาที่เชื่อมกับรายการนี้
-
- ) : (
-
- {relatedQuotations.map((quotation) => (
-
-
-
-
{quotation.code}
-
- {quotation.totalAmount.toLocaleString()}
-
-
-
-
-
- ))}
-
- )}
-
-
-
- ) : null}
-
+
+ Opportunity Summary
+ Core sales execution snapshot for this opportunity.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Customer / Contact
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Requirement
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sales Qualification
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Follow-up Timeline
+ Linked Quotations
+ Activity
+
+
+
+
+
+
+
+
+
+ Linked Quotations
+
+
+ {!canViewRelatedQuotations ? (
+ Quotation access is restricted.
+ ) : relatedQuotations.length === 0 ? (
+ No quotations created yet.
+ ) : (
+ relatedQuotations.map((quotation) => (
+
+
+
+ {quotation.code}
+
+ {quotation.status}
+
+
{quotation.quotationType}
+
+ ))
+ )}
+
+
+
+
+
+
+
+ Activity
+
+
+ {data.activity.length === 0 ? (
+ No activity yet.
+ ) : (
+ data.activity.map((activity) => (
+
+
{activity.action}
+
+ {activity.actorName ?? activity.userId} -{' '}
+ {new Date(activity.createdAt).toLocaleString()}
+
+
+ ))
+ )}
+
+
+
+
+
+
+
- ข้อมูลระบบ
- {snapshotDescription}
+ Quotation Context
-
-
-
-
-
-
+
+
-
-
+
-
-
-
);
}
diff --git a/src/features/crm/enquiries/components/enquiry-listing.tsx b/src/features/crm/enquiries/components/enquiry-listing.tsx
index 32dab7f..c51698f 100644
--- a/src/features/crm/enquiries/components/enquiry-listing.tsx
+++ b/src/features/crm/enquiries/components/enquiry-listing.tsx
@@ -29,6 +29,7 @@ export default function EnquiryListing({
const branch = searchParamsCache.get('branch');
const customer = searchParamsCache.get('customer');
const sort = searchParamsCache.get('sort');
+
const filters = {
page,
limit,
@@ -41,8 +42,8 @@ export default function EnquiryListing({
...(customer && { customer }),
...(sort && { sort })
};
- const queryClient = getQueryClient();
+ const queryClient = getQueryClient();
void queryClient.prefetchQuery(enquiriesQueryOptions(filters));
return (
diff --git a/src/features/crm/enquiries/components/quotation-readiness-panel.tsx b/src/features/crm/enquiries/components/quotation-readiness-panel.tsx
new file mode 100644
index 0000000..8df7d90
--- /dev/null
+++ b/src/features/crm/enquiries/components/quotation-readiness-panel.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import type { EnquiryRecord } from '../api/types';
+
+type ReadinessItem = {
+ label: string;
+ ready: boolean;
+};
+
+export function QuotationReadinessPanel({ enquiry }: { enquiry: EnquiryRecord }) {
+ const checks: ReadinessItem[] = [
+ { label: 'Customer selected', ready: Boolean(enquiry.customerId) },
+ { label: 'Contact selected', ready: Boolean(enquiry.contactId) },
+ { label: 'Project name present', ready: Boolean(enquiry.projectName?.trim()) },
+ { label: 'Product type present', ready: Boolean(enquiry.productType) },
+ { label: 'Requirement present', ready: Boolean(enquiry.requirement?.trim()) },
+ { label: 'Estimated value present', ready: enquiry.estimatedValue !== null }
+ ];
+
+ const readyCount = checks.filter((item) => item.ready).length;
+ const isReady = readyCount === checks.length;
+
+ return (
+
+
+ Quotation Readiness
+
+
+
+ {isReady ? 'Ready to create quotation' : 'Missing required information'}
+
+
+ {checks.map((item) => (
+
+ {item.label}
+
+ {item.ready ? 'Ready' : 'Missing'}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/features/crm/enquiries/components/source-lead-card.tsx b/src/features/crm/enquiries/components/source-lead-card.tsx
new file mode 100644
index 0000000..d77c631
--- /dev/null
+++ b/src/features/crm/enquiries/components/source-lead-card.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import Link from 'next/link';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+
+export type SourceLeadSummary = {
+ id: string;
+ code: string;
+ projectName: string | null;
+ status: string | null;
+ awareness: string | null;
+};
+
+function FieldItem({ label, value }: { label: string; value: string | null | undefined }) {
+ return (
+
+
{label}
+
{value || '-'}
+
+ );
+}
+
+export function SourceLeadCard({ sourceLead }: { sourceLead: SourceLeadSummary | null }) {
+ if (!sourceLead) {
+ return null;
+ }
+
+ return (
+
+
+ Source Lead
+
+
+
+
+
+
+
+
+ Open Source Lead
+
+
+
+
+ );
+}