From 6f3b3defad436813bcaec845b6a472f3c484157d Mon Sep 17 00:00:00 2001 From: phaichayon Date: Thu, 16 Jul 2026 08:49:50 +0700 Subject: [PATCH] commit --- .../components/quotation-detail.tsx | 1381 ++++++++++------- 1 file changed, 833 insertions(+), 548 deletions(-) diff --git a/src/features/crm/quotations/components/quotation-detail.tsx b/src/features/crm/quotations/components/quotation-detail.tsx index 9098ae3..681c170 100644 --- a/src/features/crm/quotations/components/quotation-detail.tsx +++ b/src/features/crm/quotations/components/quotation-detail.tsx @@ -1,43 +1,53 @@ -'use client'; +"use client"; -import Link from 'next/link'; -import { useMemo, useState } from 'react'; -import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; -import { toast } from 'sonner'; -import { AlertModal } from '@/components/modal/alert-modal'; -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 Link from "next/link"; +import { useMemo, useState } from "react"; +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from "@tanstack/react-query"; +import { toast } from "sonner"; +import { AlertModal } from "@/components/modal/alert-modal"; +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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, - DialogTitle -} from '@/components/ui/dialog'; -import { Input } from '@/components/ui/input'; -import { Separator } from '@/components/ui/separator'; + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; import { Select, SelectContent, SelectItem, SelectTrigger, - SelectValue -} from '@/components/ui/select'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { RecentActivitiesPanel } from '@/features/crm/activities/components/recent-activities-panel'; -import { AuditLogTab } from '@/features/crm/components/audit-log-tab'; -import { formatDate, formatDateTime } from '@/lib/date-format'; -import { formatNumber } from '@/lib/number-format'; + SelectValue, +} from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { RecentActivitiesPanel } from "@/features/crm/activities/components/recent-activities-panel"; +import { AuditLogTab } from "@/features/crm/components/audit-log-tab"; +import { formatDate, formatDateTime } from "@/lib/date-format"; +import { formatNumber } from "@/lib/number-format"; import { CrmCurrencyInput, CrmDateInput, CrmNumberInput, CrmPercentageInput, - CrmTextarea -} from '@/features/crm/components/crm-form-controls'; + CrmTextarea, +} from "@/features/crm/components/crm-form-controls"; import { createQuotationAttachmentMutation, createQuotationCustomerMutation, @@ -55,8 +65,8 @@ import { updateQuotationCustomerMutation, updateQuotationFollowupMutation, updateQuotationItemMutation, - updateQuotationTopicMutation -} from '../api/mutations'; + updateQuotationTopicMutation, +} from "../api/mutations"; import { quotationAttachmentsOptions, quotationByIdOptions, @@ -65,13 +75,13 @@ import { quotationItemsOptions, quotationKeys, quotationRevisionsOptions, - quotationTopicsOptions -} from '../api/queries'; + quotationTopicsOptions, +} from "../api/queries"; import { downloadApprovedQuotationPdf, downloadQuotationCustomerPackage, - generateApprovedQuotationPdf -} from '../api/service'; + generateApprovedQuotationPdf, +} from "../api/service"; import type { QuotationAttachmentMutationPayload, QuotationAttachmentRecord, @@ -84,33 +94,39 @@ import type { QuotationItemRecord, QuotationReferenceData, QuotationTopicMutationPayload, - QuotationTopicRecord -} from '../api/types'; -import { submitQuotationApprovalMutation } from '@/features/foundation/approval/mutations'; -import { quotationDocumentKeys } from '@/features/crm/quotations/document/queries'; + QuotationTopicRecord, +} from "../api/types"; +import { submitQuotationApprovalMutation } from "@/features/foundation/approval/mutations"; +import { quotationDocumentKeys } from "@/features/crm/quotations/document/queries"; import { getCustomerPackageRoleLabel, getCustomerPackageUiStatus, - mapCustomerPackageErrorMessage -} from '../customer-package'; -import { QuotationDocumentPreview } from './quotation-document-preview'; -import { QuotationFormSheet } from './quotation-form-sheet'; -import { QuotationApprovalTab } from './quotation-approval-tab'; -import { QuotationStatusBadge } from './quotation-status-badge'; -import { getQueryClient } from '@/lib/query-client'; + mapCustomerPackageErrorMessage, +} from "../customer-package"; +import { QuotationDocumentPreview } from "./quotation-document-preview"; +import { QuotationFormSheet } from "./quotation-form-sheet"; +import { QuotationApprovalTab } from "./quotation-approval-tab"; +import { QuotationStatusBadge } from "./quotation-status-badge"; +import { getQueryClient } from "@/lib/query-client"; -function FieldItem({ label, value }: { label: string; value: string | null | undefined }) { +function FieldItem({ + label, + value, +}: { + label: string; + value: string | null | undefined; +}) { return ( -
-
{label}
-
{value ?? '-'}
+
+
{label}
+
{value ?? "-"}
); } function EmptyState({ message }: { message: string }) { return ( -
+
{message}
); @@ -118,7 +134,7 @@ function EmptyState({ message }: { message: string }) { function downloadBlobFile(blob: Blob, fileName: string) { const url = URL.createObjectURL(blob); - const anchor = document.createElement('a'); + const anchor = document.createElement("a"); anchor.href = url; anchor.download = fileName; document.body.append(anchor); @@ -127,18 +143,28 @@ function downloadBlobFile(blob: Blob, fileName: string) { URL.revokeObjectURL(url); } -function CustomerPackageSourceItem({ item }: { item: QuotationCustomerPackageSource }) { +function CustomerPackageSourceItem({ + item, +}: { + item: QuotationCustomerPackageSource; +}) { return ( -
-
- {getCustomerPackageRoleLabel(item.role)} - {item.sourceType === 'generated' ? 'Generated' : 'Library'} +
+
+ + {getCustomerPackageRoleLabel(item.role)} + + + {item.sourceType === "generated" ? "Generated" : "Library"} +
-
{item.fileName}
-
- {item.pageCount ? `${formatNumber(item.pageCount)} page(s)` : 'Page count unavailable'} - {item.fileSize ? ` • ${formatNumber(item.fileSize)} bytes` : ''} - {item.checksum ? ` • ${item.checksum.slice(0, 16)}` : ''} +
{item.fileName}
+
+ {item.pageCount + ? `${formatNumber(item.pageCount)} page(s)` + : "Page count unavailable"} + {item.fileSize ? ` • ${formatNumber(item.fileSize)} bytes` : ""} + {item.checksum ? ` • ${item.checksum.slice(0, 16)}` : ""}
); @@ -150,7 +176,7 @@ function ItemDialog({ onSubmit, pending, referenceData, - item + item, }: { open: boolean; onOpenChange: (open: boolean) => void; @@ -160,31 +186,35 @@ function ItemDialog({ item?: QuotationItemRecord; }) { const [values, setValues] = useState({ - productType: item?.productType ?? referenceData.productTypes[0]?.id ?? '', - description: item?.description ?? '', + productType: item?.productType ?? referenceData.productTypes[0]?.id ?? "", + description: item?.description ?? "", quantity: String(item?.quantity ?? 1), - unit: item?.unit ?? '', + unit: item?.unit ?? "", unitPrice: String(item?.unitPrice ?? 0), discount: String(item?.discount ?? 0), - discountType: item?.discountType ?? '', + discountType: item?.discountType ?? "", taxRate: String(item?.taxRate ?? 0), - notes: item?.notes ?? '' + notes: item?.notes ?? "", }); return ( - {item ? 'Edit Item' : 'Add Item'} - Line items drive quotation totals from the server. + {item ? "Edit Item" : "Add Item"} + + Line items drive quotation totals from the server. + -
+
setValues((s) => ({ ...s, description: e.target.value }))} - placeholder='Description' + onChange={(e) => + setValues((s) => ({ ...s, description: e.target.value })) + } + placeholder="Description" /> -
+
setValues((s) => ({ ...s, quantity: value }))} - placeholder='Quantity' + onChange={(value) => + setValues((s) => ({ ...s, quantity: value })) + } + placeholder="Quantity" />
-
+
setValues((s) => ({ ...s, unitPrice: value }))} - placeholder='Unit price' - currencyLabel='THB' + onChange={(value) => + setValues((s) => ({ ...s, unitPrice: value })) + } + placeholder="Unit price" + currencyLabel="THB" /> setValues((s) => ({ ...s, discount: value }))} - placeholder='Discount' - currencyLabel='THB' + onChange={(value) => + setValues((s) => ({ ...s, discount: value })) + } + placeholder="Discount" + currencyLabel="THB" />
-
+
- + {referenceData.customers.map((item) => ( @@ -350,7 +394,7 @@ function CustomerDialog({ setRemark(event.target.value)} - placeholder='Remark' + placeholder="Remark" />
-
-
-
-
- - {quotation.code} - + + {quotation.code} + + {quotation.isHotProject ? Hot : null}
-

- {quotation.projectName || 'Untitled quotation'} +

+ {quotation.projectName || "Untitled quotation"}

-

- {customer?.name ?? 'Unknown customer'} - {contact ? ` - ${contact.name}` : ''} +

+ {customer?.name ?? "Unknown customer"} + {contact ? ` - ${contact.name}` : ""}

-
+
{canSubmitCurrentQuotation ? ( ) : null} {canCreateRevision && canReviseByStatus ? ( ) : null} {canUpdate ? ( - ) : null}
-
-
+
+
- + {/* */} - - - Overview - Documents - Customer Package - Items - ผู้เกี่ยวข้องในโครงการ - Topics - Follow-ups - Attachments - Audit Log - Approval + + + Overview + + ผู้เกี่ยวข้องในโครงการ + + Items + Topics + Documents + Follow-ups + Attachments + {/* Customer Package */} + {/* Audit Log */} + {/* Approval */} - + Overview @@ -1268,88 +1443,125 @@ export function QuotationDetail({ Commercial header data and quotation summary. - - - + - + + + - + - - - - -
-
-
ผู้เกี่ยวข้องในโครงการ
+ + + + +
+
+
+ ผู้เกี่ยวข้องในโครงการ +
{!customersData.items.length ? ( -
+
No project parties have been added yet.
) : ( -
+
{customersData.items.map((item) => ( -
-
{item.customerName}
-
+
+
+ {item.customerName} +
+
Role: {item.roleLabel ?? item.roleCode}
- {item.remark ?
{item.remark}
: null} + {item.remark ? ( +
{item.remark}
+ ) : null}
))}
)}
-
+
-
+
-
- +
+
- + - +
Items @@ -1363,45 +1575,48 @@ export function QuotationDetail({ setItemOpen(true); }} > - Add Item + Add Item ) : null} - + {!itemsData.items.length ? ( - + ) : ( itemsData.items.map((item) => ( -
-
-
-
{item.description}
-
- Qty {item.quantity} {item.unit || ''} x{' '} +
+
+
+
+ {item.description} +
+
+ Qty {item.quantity} {item.unit || ""} x{" "} {formatNumber(item.unitPrice)}
-
+
Total {formatNumber(item.totalPrice)}
{canManageItems ? ( -
+
) : null} @@ -1413,13 +1628,14 @@ export function QuotationDetail({ - + - +
ผู้เกี่ยวข้องในโครงการ - Related companies for this quotation, each with a visible role. + Related companies for this quotation, each with a + visible role.
{canManageCustomers ? ( @@ -1429,48 +1645,51 @@ export function QuotationDetail({ setCustomerOpen(true); }} > - Add Party + Add Party ) : null}
- + {!customersData.items.length ? ( - + ) : ( customersData.items.map((item) => (
-
{item.customerName}
-
+
+ {item.customerName} +
+
Role: {item.roleLabel ?? item.roleCode}
{item.remark ? ( -
+
{item.remark}
) : null}
{canManageCustomers ? ( -
+
) : null} @@ -1481,13 +1700,14 @@ export function QuotationDetail({ - + - +
Topics - Scope, exclusions, payment terms, and other document sections. + Scope, exclusions, payment terms, and other document + sections.
{canManageTopics ? ( @@ -1497,46 +1717,48 @@ export function QuotationDetail({ setTopicOpen(true); }} > - Add Topic + Add Topic ) : null}
- + {!topicsData.items.length ? ( - + ) : ( topicsData.items.map((topic) => ( -
-
+
+
-
{topic.title}
-
- {topicTypeMap.get(topic.topicType)?.label ?? topic.topicType} +
{topic.title}
+
+ {topicTypeMap.get(topic.topicType)?.label ?? + topic.topicType}
{canManageTopics ? ( -
+
) : null}
-
    +
      {topic.items.map((item) => (
    • {item.content}
    • ))} @@ -1548,12 +1770,14 @@ export function QuotationDetail({ - + - +
      Follow-ups - Commercial chasing and response history. + + Commercial chasing and response history. +
      {canManageFollowups ? ( ) : null}
      - + {!followupsData.items.length ? ( - + ) : ( followupsData.items.map((item) => ( -
      -
      -
      -
      - {followupTypeMap.get(item.followupType)?.label ?? - item.followupType} +
      +
      +
      +
      + {followupTypeMap.get(item.followupType) + ?.label ?? item.followupType}
      -
      +
      {formatDate(item.followupDate)} - {item.outcome ? ` - ${item.outcome}` : ''} + {item.outcome ? ` - ${item.outcome}` : ""} +
      +
      + {item.notes || "-"}
      -
      {item.notes || '-'}
      {canManageFollowups ? ( -
      +
      ) : null} @@ -1613,13 +1840,14 @@ export function QuotationDetail({ - + - +
      Attachments - Metadata only in Task E, ready for storage integration later. + Metadata only in Task E, ready for storage integration + later.
      {canManageAttachments ? ( @@ -1629,44 +1857,49 @@ export function QuotationDetail({ setAttachmentOpen(true); }} > - Add Attachment + Add Attachment ) : null}
      - + {!attachmentsData.items.length ? ( - + ) : ( attachmentsData.items.map((item) => (
      -
      {item.originalFileName}
      -
      - {item.fileType || 'Unknown type'} - {item.fileSize ? ` - ${formatNumber(item.fileSize)} bytes` : ''} +
      + {item.originalFileName} +
      +
      + {item.fileType || "Unknown type"} + {item.fileSize + ? ` - ${formatNumber(item.fileSize)} bytes` + : ""}
      {canManageAttachments ? ( -
      +
      ) : null} @@ -1677,15 +1910,15 @@ export function QuotationDetail({ - + - + - -
      + +
      Documents @@ -1708,55 +1941,66 @@ export function QuotationDetail({ Working quotation previews generated from live data. - + Quotation PDF Preview - Working preview generated from current quotation data. Approval and - customer package generation are not required. + Working preview generated from current quotation + data. Approval and customer package generation are + not required. - -
      - - - + +
      + + +
      -
      +
      {canPreviewPdf ? ( - ) : ( - )} {canPreviewPdf ? ( - ) : ( - )} {canPreviewDocument ? ( - ) : null} @@ -1767,7 +2011,10 @@ export function QuotationDetail({ {canPreviewDocument ? ( -
      +
      ) : ( @@ -1775,91 +2022,99 @@ export function QuotationDetail({ Working Document Preview - Preview permission is required to load working document data. + Preview permission is required to load working + document data. - + )}
      - + - +
      Customer Package - Operational detail for the assembled customer-deliverable package. + Operational detail for the assembled + customer-deliverable package.
      -
      +
      {canGenerateApprovedPdf && !hasOfficialDocument ? ( ) : null} {canGenerateCustomerPackage ? ( ) : null} {canPreviewCustomerPackage && customerPackage ? ( - ) : ( - )} {canDownloadCustomerPackage ? ( ) : null}
      - + {!canGenerateCustomerPackage && !canDownloadCustomerPackage && !canPreviewCustomerPackage ? ( - + ) : ( <> -
      - +
      +
      -
      +
      {customerPackageStatusDescription}
      {customerPackage?.warnings.length ? ( -
      -
      Warnings
      +
      +
      + Warnings +
      {customerPackage.warnings.map((warning) => (
      {warning.message}
      ))}
      ) : null} -
      -
      Included Documents
      +
      +
      + Included Documents +
      {!customerPackage?.includedDocuments.length ? ( - + ) : ( -
      - {customerPackage.includedDocuments.map((item) => ( - - ))} +
      + {customerPackage.includedDocuments.map( + (item) => ( + + ), + )}
      )}
      @@ -1953,7 +2216,7 @@ export function QuotationDetail({
      -
      +
      Revision Chain @@ -1961,24 +2224,24 @@ export function QuotationDetail({ Family of quotations copied from the same parent record. - + {!revisionsData.items.length ? ( - + ) : ( revisionsData.items.map((item) => ( -
      +
      -
      {item.code}
      -
      +
      {item.code}
      +
      {formatNumber(item.totalAmount)}
      - +
      )) @@ -1989,37 +2252,49 @@ export function QuotationDetail({ Record Snapshot - Operational metadata for this quotation row. + + Operational metadata for this quotation row. + - + - - + + -
      -
      Artifact Status
      -
      +
      +
      + Artifact Status +
      +
      {quotation.approvedArtifact ? ( {quotation.approvedArtifact.status} ) : ( - - + - )} {quotation.approvedArtifact?.lockedAt ? ( - - Locked {formatDateTime(quotation.approvedArtifact.lockedAt)} + + Locked{" "} + {formatDateTime(quotation.approvedArtifact.lockedAt)} ) : null}
      {quotation.hasLegacyApprovedPdf ? ( -
      -
      Legacy approved PDF detected
      -
      - This quotation still points to the old `public/generated` storage path and has - not been migrated into the artifact storage model yet. +
      +
      + Legacy approved PDF detected +
      +
      + This quotation still points to the old `public/generated` + storage path and has not been migrated into the artifact + storage model yet.
      ) : null}