From 56683ee7b9dce2daa0d52f1d14725ccdef2bc6e7 Mon Sep 17 00:00:00 2001 From: phaichayon Date: Tue, 16 Jun 2026 11:59:41 +0700 Subject: [PATCH] Hotfix Rule: Force Fresh Data After CRUD Mutation Task-h not implements --- AGENTS.md | 30 ++ docs/implementation/technical-debt.md | 18 ++ plans/task-h.md | 248 ++++++++++++++++ src/app/dashboard/crm/customers/[id]/page.tsx | 9 + src/features/crm/customers/api/mutations.ts | 66 +++-- src/features/crm/customers/api/queries.ts | 9 +- .../customers/components/customer-detail.tsx | 29 +- src/features/crm/enquiries/api/mutations.ts | 66 +++-- src/features/crm/enquiries/api/queries.ts | 9 +- src/features/crm/quotations/api/mutations.ts | 268 ++++++++++++------ src/features/crm/quotations/api/queries.ts | 24 +- src/features/foundation/approval/mutations.ts | 87 ++++-- src/features/foundation/approval/queries.ts | 6 +- .../foundation/document-template/mutations.ts | 48 +++- .../foundation/document-template/queries.ts | 9 +- src/lib/format.ts | 23 ++ 16 files changed, 763 insertions(+), 186 deletions(-) create mode 100644 plans/task-h.md diff --git a/AGENTS.md b/AGENTS.md index 99a2adb..79eaa02 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -375,6 +375,31 @@ Preferred pattern for new pages: 3. hydrate with `HydrationBoundary` 4. read with `useSuspenseQuery()` +Required pattern for CRUD mutations: + +1. every feature must define centralized query key factories in `queries.ts` +2. use grouped keys such as `all`, `lists()`, `list(filters)`, `details()`, `detail(id)`, plus child-resource keys like `contacts(id)` or `followups(id)` when needed +3. after create, explicitly invalidate the list-level key +4. after update, explicitly invalidate both list and detail keys +5. after delete, explicitly invalidate the list key and remove stale detail queries with `removeQueries` +6. invalidate related queries affected by the mutation, such as counts, child tabs, document previews, approval panels, and cross-feature related lists +7. do not rely on `staleTime` alone to refresh production CRUD data +8. do not rely on `router.refresh()` alone for CRUD freshness; React Query invalidation is required first, and `router.refresh()` is optional only as a supplement + +Mutation callback safety rule: + +- if a shared mutation object from `api/mutations.ts` is spread into `useMutation({ ...sharedMutation, ... })`, do not override `onSuccess` or `onSettled` in a way that drops cache invalidation +- prefer keeping cache invalidation inside the shared mutation definition +- if component-level success behavior is needed, either: + - put invalidation in shared `onSettled` and keep UI concerns like toast and closing dialogs in component `onSuccess`, or + - call the shared invalidate helper explicitly from the component before closing the sheet/dialog + +UI completion rule after successful mutation: + +- close sheet/dialog only after the mutation promise resolves successfully +- table/detail views must show fresh data immediately without a full browser refresh +- related tabs and counts must refresh in the same interaction when their backing data changed + ### URL State Use `nuqs` for table search params and list filters. @@ -486,3 +511,8 @@ Ensure these are set: 12. Treat legacy mock-backed modules as migration seams. 13. Follow the migrated `products` and `users` feature patterns when building new CRUD modules. 14. Do not re-enable self-service sign-up unless the product requirement changes explicitly. +15. For every CRUD mutation, explicitly invalidate React Query caches for list/detail/related data; never rely on passive staleness recovery. +16. Centralize query keys in each feature's `api/queries.ts`; do not scatter manual string query keys in components. +17. When deleting entities, remove stale detail queries with `removeQueries` in addition to invalidating lists. +18. If a component overrides `useMutation` callbacks on top of a shared mutation config, preserve or explicitly call the shared invalidation behavior before closing UI. +19. After successful create/update/delete, sheets, dialogs, and destructive modals must close only after cache invalidation has been triggered and the UI can refresh from fresh data. diff --git a/docs/implementation/technical-debt.md b/docs/implementation/technical-debt.md index 23827fa..fc24bfe 100644 --- a/docs/implementation/technical-debt.md +++ b/docs/implementation/technical-debt.md @@ -190,3 +190,21 @@ Future: - approved PDF generator - storage abstraction - immutable approved artifact URL on quotation + +## After Task G + +### Template Mapping Editor +Mapping CRUD UI ยังไม่มี +Future: ทำ mapping editor สำหรับ admin + +### Canvas-Accurate PDF Preview +Preview ปัจจุบันเป็น native app preview ไม่ใช่ pdfme canvas rendering +Future: เพิ่ม pdfme renderer preview + +### Product-Specific Template Rule +ตอนนี้ใช้ productType = default +Future: define rule สำหรับ crane/dockdoor/solarcell + +### PDF Binary Generation +ยังไม่มี generate/download/store PDF +Future: Task H \ No newline at end of file diff --git a/plans/task-h.md b/plans/task-h.md new file mode 100644 index 0000000..61c6016 --- /dev/null +++ b/plans/task-h.md @@ -0,0 +1,248 @@ +# Task H: PDF Generation + Approved Document Persistence + +## ALLA OS CRM vNext + +คุณคือ Senior Full-stack Engineer + +ให้ implement PDF generation สำหรับ Quotation โดยต่อยอดจาก Task G + +## ต้องอ่านก่อน + +```txt +docs/implementation/task-a-template-audit.md +docs/implementation/technical-debt.md +docs/adr/0008-attachment-storage-strategy.md + +src/features/foundation/document-template/** +src/features/crm/quotations/document/** +src/features/crm/quotations/** +src/features/foundation/approval/** +src/db/schema.ts +``` + +## Goal + +สร้าง production foundation สำหรับ: + +```txt +Generate PDF from pdfme template +Preview/download PDF +Persist approved PDF snapshot +Attach generated PDF metadata to quotation +``` + +## Scope H1: PDF Generation Service + +สร้าง service: + +```txt +src/features/foundation/pdf-generator/** +``` + +Functions: + +```ts +generatePdfFromTemplate(input) +generateQuotationPdf(quotationId) +generateQuotationPreviewPdf(quotationId) +generateApprovedQuotationPdf(quotationId) +``` + +Rules: + +* ใช้ templateVersion + schemaJson จาก Task G +* ใช้ `buildQuotationDocumentData` +* ใช้ `mapDocumentDataToTemplateInput` +* ห้าม generate จาก client +* validate organizationId ทุกครั้ง + +## Scope H2: PDF Download APIs + +เพิ่ม routes: + +```txt +src/app/api/crm/quotations/[id]/pdf-preview/route.ts +src/app/api/crm/quotations/[id]/pdf-download/route.ts +src/app/api/crm/quotations/[id]/approved-pdf/route.ts +``` + +Behavior: + +```txt +pdf-preview = generate PDF ชั่วคราวจาก current data +pdf-download = download quotation PDF +approved-pdf = generate/store approved PDF +``` + +Rules: + +* preview/download ต้อง check permission +* approved-pdf ต้องอนุญาตเฉพาะ approved quotation +* response ต้องเป็น application/pdf +* filename ใช้ quotation code + +## Scope H3: Approved Snapshot Persistence + +เมื่อ quotation approved แล้ว หรือเมื่อกด generate approved PDF: + +ให้ persist: + +```txt +approvedSnapshot +approvedPdfUrl +approvedAt +templateVersionId +``` + +ถ้า schema ยังไม่มี field บางตัว ให้เพิ่มอย่างระวัง + +Snapshot ต้องเก็บ: + +```txt +quotation data +items +customers +topics +approval timeline +templateVersionId +templateInput +generatedAt +generatedBy +``` + +## Scope H4: Attachment Metadata Integration + +หลัง generate approved PDF ให้สร้าง attachment metadata: + +```txt +fileName +originalFileName +filePath +fileSize +fileType = application/pdf +description = Approved quotation PDF +uploadedBy +uploadedAt +``` + +ถ้ายังไม่มี storage provider จริง: + +* ใช้ local/generated path placeholder ได้ +* ระบุ technical debt ชัดเจน + +## Scope H5: Quotation UI Integration + +ใน quotation detail: + +เพิ่ม actions: + +```txt +Preview PDF +Download PDF +Generate Approved PDF +View Approved PDF +``` + +Rules: + +* Preview PDF ใช้ได้กับ draft/pending/approved +* Generate Approved PDF ใช้ได้เฉพาะ approved +* ถ้ามี approvedPdfUrl แล้ว แสดง View Approved PDF +* แสดง template version ที่ใช้ + +## Scope H6: Approval Integration + +หลัง final approval สำเร็จ: + +```txt +quotation.status = approved +``` + +ให้เตรียม hook หรือ optional call: + +```ts +prepareApprovedQuotationSnapshot() +``` + +ถ้า generate PDF อัตโนมัติยังเสี่ยง ให้ทำ manual action ก่อน + +Recommendation: + +```txt +Task H: manual generate approved PDF +Future: auto-generate after final approval +``` + +## Permissions + +เพิ่มถ้ายังไม่มี: + +```txt +crm.quotation.pdf.preview +crm.quotation.pdf.download +crm.quotation.pdf.generate_approved +``` + +## Audit + +Audit actions: + +```txt +crm_quotation_pdf_preview optional +crm_quotation_pdf_download optional +crm_quotation_pdf_generate_approved required +``` + +Preview/download audit ไม่บังคับ +Generate approved PDF ต้อง audit + +## ห้ามทำ + +```txt +Template drag/drop designer +Notification +Dashboard KPI +Report +External object storage production +Email sending +``` + +## Output + +สรุป: + +1. Files Added +2. Files Modified +3. PDF Generator Added +4. API Routes Added +5. UI Actions Added +6. Approved Snapshot Persistence +7. Attachment Metadata Integration +8. Permission Added +9. Audit Integration +10. Remaining Risks +11. Task I Readiness + +## Definition of Done + +Task H ผ่านเมื่อ: + +✔ generate PDF จาก pdfme template ได้ + +✔ preview/download PDF ได้ + +✔ approved quotation generate approved PDF ได้ + +✔ approved snapshot ถูก persist + +✔ attachment metadata ถูกสร้าง + +✔ UI มี PDF actions + +✔ permission guard ครบ + +✔ audit generate approved PDF + +✔ ยังไม่ทำ template designer + +✔ ยังไม่ทำ notification/report diff --git a/src/app/dashboard/crm/customers/[id]/page.tsx b/src/app/dashboard/crm/customers/[id]/page.tsx index 8f6c660..0339bec 100644 --- a/src/app/dashboard/crm/customers/[id]/page.tsx +++ b/src/app/dashboard/crm/customers/[id]/page.tsx @@ -31,6 +31,11 @@ export default async function CustomerDetailRoute({ params }: PageProps) { (!!session?.user?.activeOrganizationId && (session.user.activeMembershipRole === 'admin' || session.user.activePermissions.includes(PERMISSIONS.crmContactCreate))); + const canContactRead = + session?.user?.systemRole === 'super_admin' || + (!!session?.user?.activeOrganizationId && + (session.user.activeMembershipRole === 'admin' || + session.user.activePermissions.includes(PERMISSIONS.crmContactRead))); const canContactUpdate = session?.user?.systemRole === 'super_admin' || (!!session?.user?.activeOrganizationId && @@ -45,6 +50,9 @@ export default async function CustomerDetailRoute({ params }: PageProps) { if (canRead) { void queryClient.prefetchQuery(customerByIdOptions(id)); + } + + if (canContactRead) { void queryClient.prefetchQuery(customerContactsOptions(id)); } @@ -78,6 +86,7 @@ export default async function CustomerDetailRoute({ params }: PageProps) { customerId={id} referenceData={referenceData} canUpdate={canUpdate} + canReadContacts={canContactRead} canManageContacts={{ create: canContactCreate, update: canContactUpdate, diff --git a/src/features/crm/customers/api/mutations.ts b/src/features/crm/customers/api/mutations.ts index ba3ca36..689e328 100644 --- a/src/features/crm/customers/api/mutations.ts +++ b/src/features/crm/customers/api/mutations.ts @@ -11,26 +11,57 @@ import { import { customerKeys } from './queries'; import type { CustomerContactMutationPayload, CustomerMutationPayload } from './types'; +async function invalidateCustomerLists() { + await getQueryClient().invalidateQueries({ queryKey: customerKeys.lists() }); +} + +async function invalidateCustomerDetail(id: string) { + await getQueryClient().invalidateQueries({ queryKey: customerKeys.detail(id) }); +} + +async function invalidateCustomerContacts(id: string) { + await getQueryClient().invalidateQueries({ queryKey: customerKeys.contacts(id) }); +} + +export async function invalidateCustomerMutationQueries(id: string) { + await Promise.all([invalidateCustomerLists(), invalidateCustomerDetail(id)]); +} + +export async function invalidateCustomerContactMutationQueries(customerId: string) { + await Promise.all([ + invalidateCustomerLists(), + invalidateCustomerDetail(customerId), + invalidateCustomerContacts(customerId) + ]); +} + export const createCustomerMutation = mutationOptions({ mutationFn: (data: CustomerMutationPayload) => createCustomer(data), - onSuccess: () => { - getQueryClient().invalidateQueries({ queryKey: customerKeys.all }); + onSettled: async (_data, error) => { + if (!error) { + await invalidateCustomerLists(); + } } }); export const updateCustomerMutation = mutationOptions({ mutationFn: ({ id, values }: { id: string; values: CustomerMutationPayload }) => updateCustomer(id, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: customerKeys.all }); - getQueryClient().invalidateQueries({ queryKey: customerKeys.detail(variables.id) }); + onSettled: async (_data, error, variables) => { + if (!error) { + await invalidateCustomerMutationQueries(variables.id); + } } }); export const deleteCustomerMutation = mutationOptions({ mutationFn: (id: string) => deleteCustomer(id), - onSuccess: () => { - getQueryClient().invalidateQueries({ queryKey: customerKeys.all }); + onSettled: async (_data, error, id) => { + if (!error) { + getQueryClient().removeQueries({ queryKey: customerKeys.detail(id) }); + getQueryClient().removeQueries({ queryKey: customerKeys.contacts(id) }); + await invalidateCustomerLists(); + } } }); @@ -42,9 +73,10 @@ export const createCustomerContactMutation = mutationOptions({ customerId: string; values: CustomerContactMutationPayload; }) => createCustomerContact(customerId, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: customerKeys.contacts(variables.customerId) }); - getQueryClient().invalidateQueries({ queryKey: customerKeys.detail(variables.customerId) }); + onSettled: async (_data, error, variables) => { + if (!error) { + await invalidateCustomerContactMutationQueries(variables.customerId); + } } }); @@ -58,17 +90,19 @@ export const updateCustomerContactMutation = mutationOptions({ contactId: string; values: CustomerContactMutationPayload; }) => updateCustomerContact(customerId, contactId, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: customerKeys.contacts(variables.customerId) }); - getQueryClient().invalidateQueries({ queryKey: customerKeys.detail(variables.customerId) }); + onSettled: async (_data, error, variables) => { + if (!error) { + await invalidateCustomerContactMutationQueries(variables.customerId); + } } }); export const deleteCustomerContactMutation = mutationOptions({ mutationFn: ({ customerId, contactId }: { customerId: string; contactId: string }) => deleteCustomerContact(customerId, contactId), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: customerKeys.contacts(variables.customerId) }); - getQueryClient().invalidateQueries({ queryKey: customerKeys.detail(variables.customerId) }); + onSettled: async (_data, error, variables) => { + if (!error) { + await invalidateCustomerContactMutationQueries(variables.customerId); + } } }); diff --git a/src/features/crm/customers/api/queries.ts b/src/features/crm/customers/api/queries.ts index 3fe9b2d..7e9d0bd 100644 --- a/src/features/crm/customers/api/queries.ts +++ b/src/features/crm/customers/api/queries.ts @@ -4,9 +4,12 @@ import type { CustomerFilters } from './types'; export const customerKeys = { all: ['crm-customers'] as const, - list: (filters: CustomerFilters) => [...customerKeys.all, 'list', filters] as const, - detail: (id: string) => [...customerKeys.all, 'detail', id] as const, - contacts: (id: string) => [...customerKeys.all, 'contacts', id] as const + lists: () => [...customerKeys.all, 'list'] as const, + list: (filters: CustomerFilters) => [...customerKeys.lists(), filters] as const, + details: () => [...customerKeys.all, 'detail'] as const, + detail: (id: string) => [...customerKeys.details(), id] as const, + contactsRoot: () => [...customerKeys.all, 'contacts'] as const, + contacts: (id: string) => [...customerKeys.contactsRoot(), id] as const }; export const customersQueryOptions = (filters: CustomerFilters) => diff --git a/src/features/crm/customers/components/customer-detail.tsx b/src/features/crm/customers/components/customer-detail.tsx index 2487486..2a43acf 100644 --- a/src/features/crm/customers/components/customer-detail.tsx +++ b/src/features/crm/customers/components/customer-detail.tsx @@ -9,6 +9,7 @@ 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 { formatDateTime } from '@/lib/format'; import { customerByIdOptions } from '../api/queries'; import type { CustomerReferenceData } from '../api/types'; import { CustomerFormSheet } from './customer-form-sheet'; @@ -30,6 +31,7 @@ export function CustomerDetail({ customerId, referenceData, canUpdate, + canReadContacts, canManageContacts, relatedEnquiries, relatedQuotations @@ -37,6 +39,7 @@ export function CustomerDetail({ customerId: string; referenceData: CustomerReferenceData; canUpdate: boolean; + canReadContacts: boolean; canManageContacts: { create: boolean; update: boolean; @@ -113,7 +116,7 @@ export function CustomerDetail({ Overview - Contacts + {canReadContacts ? Contacts : null} Activity Related Documents @@ -166,14 +169,16 @@ export function CustomerDetail({ - - - + {canReadContacts ? ( + + + + ) : null} @@ -202,7 +207,7 @@ export function CustomerDetail({
- {new Date(item.createdAt).toLocaleString()} + {formatDateTime(item.createdAt)}
@@ -281,8 +286,8 @@ export function CustomerDetail({ - - + +
diff --git a/src/features/crm/enquiries/api/mutations.ts b/src/features/crm/enquiries/api/mutations.ts index 5f64a5b..578642b 100644 --- a/src/features/crm/enquiries/api/mutations.ts +++ b/src/features/crm/enquiries/api/mutations.ts @@ -11,26 +11,57 @@ import { import { enquiryKeys } from './queries'; import type { EnquiryFollowupMutationPayload, EnquiryMutationPayload } from './types'; +async function invalidateEnquiryLists() { + await getQueryClient().invalidateQueries({ queryKey: enquiryKeys.lists() }); +} + +async function invalidateEnquiryDetail(id: string) { + await getQueryClient().invalidateQueries({ queryKey: enquiryKeys.detail(id) }); +} + +async function invalidateEnquiryFollowups(id: string) { + await getQueryClient().invalidateQueries({ queryKey: enquiryKeys.followups(id) }); +} + +export async function invalidateEnquiryMutationQueries(id: string) { + await Promise.all([invalidateEnquiryLists(), invalidateEnquiryDetail(id)]); +} + +export async function invalidateEnquiryFollowupMutationQueries(enquiryId: string) { + await Promise.all([ + invalidateEnquiryLists(), + invalidateEnquiryDetail(enquiryId), + invalidateEnquiryFollowups(enquiryId) + ]); +} + export const createEnquiryMutation = mutationOptions({ mutationFn: (data: EnquiryMutationPayload) => createEnquiry(data), - onSuccess: () => { - getQueryClient().invalidateQueries({ queryKey: enquiryKeys.all }); + onSettled: async (_data, error) => { + if (!error) { + await invalidateEnquiryLists(); + } } }); export const updateEnquiryMutation = mutationOptions({ mutationFn: ({ id, values }: { id: string; values: EnquiryMutationPayload }) => updateEnquiry(id, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: enquiryKeys.all }); - getQueryClient().invalidateQueries({ queryKey: enquiryKeys.detail(variables.id) }); + onSettled: async (_data, error, variables) => { + if (!error) { + await invalidateEnquiryMutationQueries(variables.id); + } } }); export const deleteEnquiryMutation = mutationOptions({ mutationFn: (id: string) => deleteEnquiry(id), - onSuccess: () => { - getQueryClient().invalidateQueries({ queryKey: enquiryKeys.all }); + onSettled: async (_data, error, id) => { + if (!error) { + getQueryClient().removeQueries({ queryKey: enquiryKeys.detail(id) }); + getQueryClient().removeQueries({ queryKey: enquiryKeys.followups(id) }); + await invalidateEnquiryLists(); + } } }); @@ -42,9 +73,10 @@ export const createEnquiryFollowupMutation = mutationOptions({ enquiryId: string; values: EnquiryFollowupMutationPayload; }) => createEnquiryFollowup(enquiryId, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: enquiryKeys.followups(variables.enquiryId) }); - getQueryClient().invalidateQueries({ queryKey: enquiryKeys.detail(variables.enquiryId) }); + onSettled: async (_data, error, variables) => { + if (!error) { + await invalidateEnquiryFollowupMutationQueries(variables.enquiryId); + } } }); @@ -58,17 +90,19 @@ export const updateEnquiryFollowupMutation = mutationOptions({ followupId: string; values: EnquiryFollowupMutationPayload; }) => updateEnquiryFollowup(enquiryId, followupId, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: enquiryKeys.followups(variables.enquiryId) }); - getQueryClient().invalidateQueries({ queryKey: enquiryKeys.detail(variables.enquiryId) }); + onSettled: async (_data, error, variables) => { + if (!error) { + await invalidateEnquiryFollowupMutationQueries(variables.enquiryId); + } } }); export const deleteEnquiryFollowupMutation = mutationOptions({ mutationFn: ({ enquiryId, followupId }: { enquiryId: string; followupId: string }) => deleteEnquiryFollowup(enquiryId, followupId), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: enquiryKeys.followups(variables.enquiryId) }); - getQueryClient().invalidateQueries({ queryKey: enquiryKeys.detail(variables.enquiryId) }); + onSettled: async (_data, error, variables) => { + if (!error) { + await invalidateEnquiryFollowupMutationQueries(variables.enquiryId); + } } }); diff --git a/src/features/crm/enquiries/api/queries.ts b/src/features/crm/enquiries/api/queries.ts index 47c9e5d..5d99391 100644 --- a/src/features/crm/enquiries/api/queries.ts +++ b/src/features/crm/enquiries/api/queries.ts @@ -4,9 +4,12 @@ import type { EnquiryFilters } from './types'; export const enquiryKeys = { all: ['crm-enquiries'] as const, - list: (filters: EnquiryFilters) => [...enquiryKeys.all, 'list', filters] as const, - detail: (id: string) => [...enquiryKeys.all, 'detail', id] as const, - followups: (id: string) => [...enquiryKeys.all, 'followups', id] as const + lists: () => [...enquiryKeys.all, 'list'] as const, + list: (filters: EnquiryFilters) => [...enquiryKeys.lists(), filters] as const, + details: () => [...enquiryKeys.all, 'detail'] as const, + detail: (id: string) => [...enquiryKeys.details(), id] as const, + followupsRoot: () => [...enquiryKeys.all, 'followups'] as const, + followups: (id: string) => [...enquiryKeys.followupsRoot(), id] as const }; export const enquiriesQueryOptions = (filters: EnquiryFilters) => diff --git a/src/features/crm/quotations/api/mutations.ts b/src/features/crm/quotations/api/mutations.ts index 79966d2..26d0c04 100644 --- a/src/features/crm/quotations/api/mutations.ts +++ b/src/features/crm/quotations/api/mutations.ts @@ -32,43 +32,104 @@ import type { QuotationTopicMutationPayload } from './types'; -function invalidateQuotationDocumentQueries(quotationId: string) { - getQueryClient().invalidateQueries({ queryKey: quotationDocumentKeys.data(quotationId) }); - getQueryClient().invalidateQueries({ queryKey: quotationDocumentKeys.preview(quotationId) }); +async function invalidateQuotationLists() { + await getQueryClient().invalidateQueries({ queryKey: quotationKeys.lists() }); +} + +async function invalidateQuotationDetail(quotationId: string) { + await getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(quotationId) }); +} + +async function invalidateQuotationDocumentQueries(quotationId: string) { + await Promise.all([ + getQueryClient().invalidateQueries({ queryKey: quotationDocumentKeys.data(quotationId) }), + getQueryClient().invalidateQueries({ queryKey: quotationDocumentKeys.preview(quotationId) }) + ]); +} + +async function invalidateQuotationHeaderQueries(quotationId: string) { + await Promise.all([ + invalidateQuotationLists(), + invalidateQuotationDetail(quotationId), + invalidateQuotationDocumentQueries(quotationId) + ]); +} + +async function invalidateQuotationItems(quotationId: string) { + await getQueryClient().invalidateQueries({ queryKey: quotationKeys.items(quotationId) }); +} + +async function invalidateQuotationCustomers(quotationId: string) { + await getQueryClient().invalidateQueries({ queryKey: quotationKeys.customers(quotationId) }); +} + +async function invalidateQuotationTopics(quotationId: string) { + await getQueryClient().invalidateQueries({ queryKey: quotationKeys.topics(quotationId) }); +} + +async function invalidateQuotationFollowups(quotationId: string) { + await getQueryClient().invalidateQueries({ queryKey: quotationKeys.followups(quotationId) }); +} + +async function invalidateQuotationAttachments(quotationId: string) { + await getQueryClient().invalidateQueries({ queryKey: quotationKeys.attachments(quotationId) }); +} + +async function invalidateQuotationRevisions(quotationId: string) { + await getQueryClient().invalidateQueries({ queryKey: quotationKeys.revisions(quotationId) }); +} + +async function removeQuotationDetailQueries(quotationId: string) { + getQueryClient().removeQueries({ queryKey: quotationKeys.detail(quotationId) }); + getQueryClient().removeQueries({ queryKey: quotationKeys.items(quotationId) }); + getQueryClient().removeQueries({ queryKey: quotationKeys.customers(quotationId) }); + getQueryClient().removeQueries({ queryKey: quotationKeys.topics(quotationId) }); + getQueryClient().removeQueries({ queryKey: quotationKeys.followups(quotationId) }); + getQueryClient().removeQueries({ queryKey: quotationKeys.attachments(quotationId) }); + getQueryClient().removeQueries({ queryKey: quotationKeys.revisions(quotationId) }); + getQueryClient().removeQueries({ queryKey: quotationDocumentKeys.data(quotationId) }); + getQueryClient().removeQueries({ queryKey: quotationDocumentKeys.preview(quotationId) }); } export const createQuotationMutation = mutationOptions({ mutationFn: (data: QuotationMutationPayload) => createQuotation(data), - onSuccess: () => { - getQueryClient().invalidateQueries({ queryKey: quotationKeys.all }); + onSettled: async (_data, error) => { + if (!error) { + await invalidateQuotationLists(); + } } }); export const updateQuotationMutation = mutationOptions({ mutationFn: ({ id, values }: { id: string; values: QuotationMutationPayload }) => updateQuotation(id, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: quotationKeys.all }); - getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.id) }); - invalidateQuotationDocumentQueries(variables.id); + onSettled: async (_data, error, variables) => { + if (!error) { + await invalidateQuotationHeaderQueries(variables.id); + } } }); export const deleteQuotationMutation = mutationOptions({ mutationFn: (id: string) => deleteQuotation(id), - onSuccess: () => { - getQueryClient().invalidateQueries({ queryKey: quotationKeys.all }); + onSettled: async (_data, error, id) => { + if (!error) { + await removeQuotationDetailQueries(id); + await invalidateQuotationLists(); + } } }); export const createQuotationItemMutation = mutationOptions({ mutationFn: ({ quotationId, values }: { quotationId: string; values: QuotationItemMutationPayload }) => createQuotationItem(quotationId, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: quotationKeys.items(variables.quotationId) }); - getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) }); - getQueryClient().invalidateQueries({ queryKey: quotationKeys.all }); - invalidateQuotationDocumentQueries(variables.quotationId); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationItems(variables.quotationId), + invalidateQuotationHeaderQueries(variables.quotationId) + ]); + } } }); @@ -82,22 +143,26 @@ export const updateQuotationItemMutation = mutationOptions({ itemId: string; values: QuotationItemMutationPayload; }) => updateQuotationItem(quotationId, itemId, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: quotationKeys.items(variables.quotationId) }); - getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) }); - getQueryClient().invalidateQueries({ queryKey: quotationKeys.all }); - invalidateQuotationDocumentQueries(variables.quotationId); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationItems(variables.quotationId), + invalidateQuotationHeaderQueries(variables.quotationId) + ]); + } } }); export const deleteQuotationItemMutation = mutationOptions({ mutationFn: ({ quotationId, itemId }: { quotationId: string; itemId: string }) => deleteQuotationItem(quotationId, itemId), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: quotationKeys.items(variables.quotationId) }); - getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) }); - getQueryClient().invalidateQueries({ queryKey: quotationKeys.all }); - invalidateQuotationDocumentQueries(variables.quotationId); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationItems(variables.quotationId), + invalidateQuotationHeaderQueries(variables.quotationId) + ]); + } } }); @@ -109,12 +174,13 @@ export const createQuotationCustomerMutation = mutationOptions({ quotationId: string; values: QuotationCustomerMutationPayload; }) => createQuotationCustomer(quotationId, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ - queryKey: quotationKeys.customers(variables.quotationId) - }); - getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) }); - invalidateQuotationDocumentQueries(variables.quotationId); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationCustomers(variables.quotationId), + invalidateQuotationHeaderQueries(variables.quotationId) + ]); + } } }); @@ -128,51 +194,68 @@ export const updateQuotationCustomerMutation = mutationOptions({ relationId: string; values: QuotationCustomerMutationPayload; }) => updateQuotationCustomer(quotationId, relationId, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ - queryKey: quotationKeys.customers(variables.quotationId) - }); - getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) }); - invalidateQuotationDocumentQueries(variables.quotationId); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationCustomers(variables.quotationId), + invalidateQuotationHeaderQueries(variables.quotationId) + ]); + } } }); export const deleteQuotationCustomerMutation = mutationOptions({ mutationFn: ({ quotationId, relationId }: { quotationId: string; relationId: string }) => deleteQuotationCustomer(quotationId, relationId), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ - queryKey: quotationKeys.customers(variables.quotationId) - }); - getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) }); - invalidateQuotationDocumentQueries(variables.quotationId); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationCustomers(variables.quotationId), + invalidateQuotationHeaderQueries(variables.quotationId) + ]); + } } }); export const createQuotationTopicMutation = mutationOptions({ mutationFn: ({ quotationId, values }: { quotationId: string; values: QuotationTopicMutationPayload }) => createQuotationTopic(quotationId, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: quotationKeys.topics(variables.quotationId) }); - invalidateQuotationDocumentQueries(variables.quotationId); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationTopics(variables.quotationId), + invalidateQuotationDetail(variables.quotationId), + invalidateQuotationDocumentQueries(variables.quotationId) + ]); + } } }); export const updateQuotationTopicMutation = mutationOptions({ mutationFn: ({ quotationId, values }: { quotationId: string; values: QuotationTopicMutationPayload }) => updateQuotationTopic(quotationId, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: quotationKeys.topics(variables.quotationId) }); - invalidateQuotationDocumentQueries(variables.quotationId); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationTopics(variables.quotationId), + invalidateQuotationDetail(variables.quotationId), + invalidateQuotationDocumentQueries(variables.quotationId) + ]); + } } }); export const deleteQuotationTopicMutation = mutationOptions({ mutationFn: ({ quotationId, topicId }: { quotationId: string; topicId: string }) => deleteQuotationTopic(quotationId, topicId), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: quotationKeys.topics(variables.quotationId) }); - invalidateQuotationDocumentQueries(variables.quotationId); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationTopics(variables.quotationId), + invalidateQuotationDetail(variables.quotationId), + invalidateQuotationDocumentQueries(variables.quotationId) + ]); + } } }); @@ -184,11 +267,13 @@ export const createQuotationFollowupMutation = mutationOptions({ quotationId: string; values: QuotationFollowupMutationPayload; }) => createQuotationFollowup(quotationId, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ - queryKey: quotationKeys.followups(variables.quotationId) - }); - getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) }); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationFollowups(variables.quotationId), + invalidateQuotationDetail(variables.quotationId) + ]); + } } }); @@ -200,22 +285,26 @@ export const updateQuotationFollowupMutation = mutationOptions({ quotationId: string; values: QuotationFollowupMutationPayload; }) => updateQuotationFollowup(quotationId, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ - queryKey: quotationKeys.followups(variables.quotationId) - }); - getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) }); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationFollowups(variables.quotationId), + invalidateQuotationDetail(variables.quotationId) + ]); + } } }); export const deleteQuotationFollowupMutation = mutationOptions({ mutationFn: ({ quotationId, followupId }: { quotationId: string; followupId: string }) => deleteQuotationFollowup(quotationId, followupId), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ - queryKey: quotationKeys.followups(variables.quotationId) - }); - getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) }); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationFollowups(variables.quotationId), + invalidateQuotationDetail(variables.quotationId) + ]); + } } }); @@ -227,10 +316,13 @@ export const createQuotationAttachmentMutation = mutationOptions({ quotationId: string; values: QuotationAttachmentMutationPayload; }) => createQuotationAttachment(quotationId, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ - queryKey: quotationKeys.attachments(variables.quotationId) - }); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationAttachments(variables.quotationId), + invalidateQuotationDetail(variables.quotationId) + ]); + } } }); @@ -242,20 +334,26 @@ export const updateQuotationAttachmentMutation = mutationOptions({ quotationId: string; values: QuotationAttachmentMutationPayload; }) => updateQuotationAttachment(quotationId, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ - queryKey: quotationKeys.attachments(variables.quotationId) - }); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationAttachments(variables.quotationId), + invalidateQuotationDetail(variables.quotationId) + ]); + } } }); export const deleteQuotationAttachmentMutation = mutationOptions({ mutationFn: ({ quotationId, attachmentId }: { quotationId: string; attachmentId: string }) => deleteQuotationAttachment(quotationId, attachmentId), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ - queryKey: quotationKeys.attachments(variables.quotationId) - }); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationAttachments(variables.quotationId), + invalidateQuotationDetail(variables.quotationId) + ]); + } } }); @@ -267,8 +365,12 @@ export const createQuotationRevisionMutation = mutationOptions({ quotationId: string; revisionRemark?: string; }) => createQuotationRevision(quotationId, revisionRemark), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: quotationKeys.revisions(variables.quotationId) }); - getQueryClient().invalidateQueries({ queryKey: quotationKeys.all }); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateQuotationRevisions(variables.quotationId), + invalidateQuotationHeaderQueries(variables.quotationId) + ]); + } } }); diff --git a/src/features/crm/quotations/api/queries.ts b/src/features/crm/quotations/api/queries.ts index bff87db..eb329d6 100644 --- a/src/features/crm/quotations/api/queries.ts +++ b/src/features/crm/quotations/api/queries.ts @@ -13,14 +13,22 @@ import type { QuotationFilters } from './types'; export const quotationKeys = { all: ['crm-quotations'] as const, - list: (filters: QuotationFilters) => [...quotationKeys.all, 'list', filters] as const, - detail: (id: string) => [...quotationKeys.all, 'detail', id] as const, - items: (id: string) => [...quotationKeys.all, 'items', id] as const, - customers: (id: string) => [...quotationKeys.all, 'customers', id] as const, - topics: (id: string) => [...quotationKeys.all, 'topics', id] as const, - followups: (id: string) => [...quotationKeys.all, 'followups', id] as const, - attachments: (id: string) => [...quotationKeys.all, 'attachments', id] as const, - revisions: (id: string) => [...quotationKeys.all, 'revisions', id] as const + lists: () => [...quotationKeys.all, 'list'] as const, + list: (filters: QuotationFilters) => [...quotationKeys.lists(), filters] as const, + details: () => [...quotationKeys.all, 'detail'] as const, + detail: (id: string) => [...quotationKeys.details(), id] as const, + itemsRoot: () => [...quotationKeys.all, 'items'] as const, + items: (id: string) => [...quotationKeys.itemsRoot(), id] as const, + customersRoot: () => [...quotationKeys.all, 'customers'] as const, + customers: (id: string) => [...quotationKeys.customersRoot(), id] as const, + topicsRoot: () => [...quotationKeys.all, 'topics'] as const, + topics: (id: string) => [...quotationKeys.topicsRoot(), id] as const, + followupsRoot: () => [...quotationKeys.all, 'followups'] as const, + followups: (id: string) => [...quotationKeys.followupsRoot(), id] as const, + attachmentsRoot: () => [...quotationKeys.all, 'attachments'] as const, + attachments: (id: string) => [...quotationKeys.attachmentsRoot(), id] as const, + revisionsRoot: () => [...quotationKeys.all, 'revisions'] as const, + revisions: (id: string) => [...quotationKeys.revisionsRoot(), id] as const }; export const quotationsQueryOptions = (filters: QuotationFilters) => diff --git a/src/features/foundation/approval/mutations.ts b/src/features/foundation/approval/mutations.ts index ef10c05..194ba01 100644 --- a/src/features/foundation/approval/mutations.ts +++ b/src/features/foundation/approval/mutations.ts @@ -13,75 +13,104 @@ import { approvalKeys } from './queries'; import { quotationKeys } from '@/features/crm/quotations/api/queries'; import type { ApprovalActionPayload, SubmitApprovalPayload } from './types'; -function invalidateQuotationQueries(quotationId: string) { - const queryClient = getQueryClient(); - queryClient.invalidateQueries({ queryKey: quotationKeys.all }); - queryClient.invalidateQueries({ queryKey: quotationKeys.detail(quotationId) }); - queryClient.invalidateQueries({ queryKey: quotationDocumentKeys.data(quotationId) }); - queryClient.invalidateQueries({ queryKey: quotationDocumentKeys.preview(quotationId) }); +async function invalidateApprovalLists() { + await getQueryClient().invalidateQueries({ queryKey: approvalKeys.lists() }); } -function invalidateRelatedEntityQueries(approvalId: string) { +async function invalidateApprovalDetail(id: string) { + await getQueryClient().invalidateQueries({ queryKey: approvalKeys.detail(id) }); +} + +async function invalidateQuotationQueries(quotationId: string) { + const queryClient = getQueryClient(); + await Promise.all([ + queryClient.invalidateQueries({ queryKey: quotationKeys.lists() }), + queryClient.invalidateQueries({ queryKey: quotationKeys.detail(quotationId) }), + queryClient.invalidateQueries({ queryKey: quotationDocumentKeys.data(quotationId) }), + queryClient.invalidateQueries({ queryKey: quotationDocumentKeys.preview(quotationId) }) + ]); +} + +async function invalidateRelatedEntityQueries(approvalId: string) { const queryClient = getQueryClient(); const cachedDetail = queryClient.getQueryData<{ approval: { request: { entityType: string; entityId: string } } }>( approvalKeys.detail(approvalId) ); if (cachedDetail?.approval.request.entityType === 'quotation') { - invalidateQuotationQueries(cachedDetail.approval.request.entityId); + await invalidateQuotationQueries(cachedDetail.approval.request.entityId); } } export const submitApprovalMutation = mutationOptions({ mutationFn: (data: SubmitApprovalPayload) => submitApproval(data), - onSuccess: () => { - getQueryClient().invalidateQueries({ queryKey: approvalKeys.all }); + onSettled: async (_data, error) => { + if (!error) { + await invalidateApprovalLists(); + } } }); export const submitQuotationApprovalMutation = mutationOptions({ mutationFn: ({ id, remark }: { id: string; remark?: string }) => submitQuotationForApproval(id, remark), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: approvalKeys.all }); - invalidateQuotationQueries(variables.id); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([invalidateApprovalLists(), invalidateQuotationQueries(variables.id)]); + } } }); export const cancelApprovalMutation = mutationOptions({ mutationFn: (id: string) => cancelApproval(id), - onSuccess: (_data, id) => { - getQueryClient().invalidateQueries({ queryKey: approvalKeys.all }); - getQueryClient().invalidateQueries({ queryKey: approvalKeys.detail(id) }); - invalidateRelatedEntityQueries(id); + onSettled: async (_data, error, id) => { + if (!error) { + await Promise.all([ + invalidateApprovalLists(), + invalidateApprovalDetail(id), + invalidateRelatedEntityQueries(id) + ]); + } } }); export const approveApprovalMutation = mutationOptions({ mutationFn: ({ id, values }: { id: string; values: ApprovalActionPayload }) => approveApproval(id, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: approvalKeys.all }); - getQueryClient().invalidateQueries({ queryKey: approvalKeys.detail(variables.id) }); - invalidateRelatedEntityQueries(variables.id); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateApprovalLists(), + invalidateApprovalDetail(variables.id), + invalidateRelatedEntityQueries(variables.id) + ]); + } } }); export const rejectApprovalMutation = mutationOptions({ mutationFn: ({ id, values }: { id: string; values: ApprovalActionPayload }) => rejectApproval(id, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: approvalKeys.all }); - getQueryClient().invalidateQueries({ queryKey: approvalKeys.detail(variables.id) }); - invalidateRelatedEntityQueries(variables.id); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateApprovalLists(), + invalidateApprovalDetail(variables.id), + invalidateRelatedEntityQueries(variables.id) + ]); + } } }); export const returnApprovalMutation = mutationOptions({ mutationFn: ({ id, values }: { id: string; values: ApprovalActionPayload }) => returnApproval(id, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: approvalKeys.all }); - getQueryClient().invalidateQueries({ queryKey: approvalKeys.detail(variables.id) }); - invalidateRelatedEntityQueries(variables.id); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateApprovalLists(), + invalidateApprovalDetail(variables.id), + invalidateRelatedEntityQueries(variables.id) + ]); + } } }); diff --git a/src/features/foundation/approval/queries.ts b/src/features/foundation/approval/queries.ts index daac569..54ac1cd 100644 --- a/src/features/foundation/approval/queries.ts +++ b/src/features/foundation/approval/queries.ts @@ -4,8 +4,10 @@ import type { ApprovalFilters } from './types'; export const approvalKeys = { all: ['crm-approvals'] as const, - list: (filters: ApprovalFilters) => [...approvalKeys.all, 'list', filters] as const, - detail: (id: string) => [...approvalKeys.all, 'detail', id] as const + lists: () => [...approvalKeys.all, 'list'] as const, + list: (filters: ApprovalFilters) => [...approvalKeys.lists(), filters] as const, + details: () => [...approvalKeys.all, 'detail'] as const, + detail: (id: string) => [...approvalKeys.details(), id] as const }; export const approvalsQueryOptions = (filters: ApprovalFilters) => diff --git a/src/features/foundation/document-template/mutations.ts b/src/features/foundation/document-template/mutations.ts index cf79e93..a80cf73 100644 --- a/src/features/foundation/document-template/mutations.ts +++ b/src/features/foundation/document-template/mutations.ts @@ -9,35 +9,61 @@ import { import { documentTemplateKeys } from './queries'; import type { DocumentTemplateMutationPayload, DocumentTemplateVersionMutationPayload } from './types'; +async function invalidateDocumentTemplateLists() { + await getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.lists() }); +} + +async function invalidateDocumentTemplateDetail(id: string) { + await getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.detail(id) }); +} + +async function invalidateDocumentTemplateVersions(id: string) { + await getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.versions(id) }); +} + export const createDocumentTemplateMutation = mutationOptions({ mutationFn: (data: DocumentTemplateMutationPayload) => createDocumentTemplate(data), - onSuccess: () => { - getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.all }); + onSettled: async (_data, error) => { + if (!error) { + await invalidateDocumentTemplateLists(); + } } }); export const updateDocumentTemplateMutation = mutationOptions({ mutationFn: ({ id, values }: { id: string; values: Partial }) => updateDocumentTemplate(id, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.all }); - getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.detail(variables.id) }); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateDocumentTemplateLists(), + invalidateDocumentTemplateDetail(variables.id) + ]); + } } }); export const deleteDocumentTemplateMutation = mutationOptions({ mutationFn: (id: string) => deleteDocumentTemplate(id), - onSuccess: () => { - getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.all }); + onSettled: async (_data, error, id) => { + if (!error) { + getQueryClient().removeQueries({ queryKey: documentTemplateKeys.detail(id) }); + getQueryClient().removeQueries({ queryKey: documentTemplateKeys.versions(id) }); + await invalidateDocumentTemplateLists(); + } } }); export const createDocumentTemplateVersionMutation = mutationOptions({ mutationFn: ({ id, values }: { id: string; values: DocumentTemplateVersionMutationPayload }) => createDocumentTemplateVersion(id, values), - onSuccess: (_data, variables) => { - getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.all }); - getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.detail(variables.id) }); - getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.versions(variables.id) }); + onSettled: async (_data, error, variables) => { + if (!error) { + await Promise.all([ + invalidateDocumentTemplateLists(), + invalidateDocumentTemplateDetail(variables.id), + invalidateDocumentTemplateVersions(variables.id) + ]); + } } }); diff --git a/src/features/foundation/document-template/queries.ts b/src/features/foundation/document-template/queries.ts index 26f5c96..b288c33 100644 --- a/src/features/foundation/document-template/queries.ts +++ b/src/features/foundation/document-template/queries.ts @@ -4,10 +4,13 @@ import type { DocumentTemplateFilters } from './types'; export const documentTemplateKeys = { all: ['crm-document-templates'] as const, + lists: () => [...documentTemplateKeys.all, 'list'] as const, list: (filters: DocumentTemplateFilters) => - [...documentTemplateKeys.all, 'list', filters] as const, - detail: (id: string) => [...documentTemplateKeys.all, 'detail', id] as const, - versions: (id: string) => [...documentTemplateKeys.all, 'versions', id] as const + [...documentTemplateKeys.lists(), filters] as const, + details: () => [...documentTemplateKeys.all, 'detail'] as const, + detail: (id: string) => [...documentTemplateKeys.details(), id] as const, + versionsRoot: () => [...documentTemplateKeys.all, 'versions'] as const, + versions: (id: string) => [...documentTemplateKeys.versionsRoot(), id] as const }; export const documentTemplatesQueryOptions = (filters: DocumentTemplateFilters = {}) => diff --git a/src/lib/format.ts b/src/lib/format.ts index 64c36c2..39a411f 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -16,6 +16,29 @@ export function formatDate( } } +export function formatDateTime( + date: Date | string | number | undefined, + opts: Intl.DateTimeFormatOptions = {} +) { + if (!date) return ''; + + try { + return new Intl.DateTimeFormat('th-TH-u-ca-gregory-nu-latn', { + day: opts.day ?? '2-digit', + month: opts.month ?? '2-digit', + year: opts.year ?? 'numeric', + hour: opts.hour ?? '2-digit', + minute: opts.minute ?? '2-digit', + second: opts.second ?? '2-digit', + hour12: opts.hour12 ?? false, + timeZone: opts.timeZone ?? 'Asia/Bangkok', + ...opts + }).format(new Date(date)); + } catch { + return ''; + } +} + /** * Formats a date to YYYY-MM-DD format (ISO date). * This is used to prevent hydration mismatches between server and client.