Hotfix Rule: Force Fresh Data After CRUD Mutation
Task-h not implements
This commit is contained in:
30
AGENTS.md
30
AGENTS.md
@@ -375,6 +375,31 @@ Preferred pattern for new pages:
|
|||||||
3. hydrate with `HydrationBoundary`
|
3. hydrate with `HydrationBoundary`
|
||||||
4. read with `useSuspenseQuery()`
|
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
|
### URL State
|
||||||
|
|
||||||
Use `nuqs` for table search params and list filters.
|
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.
|
12. Treat legacy mock-backed modules as migration seams.
|
||||||
13. Follow the migrated `products` and `users` feature patterns when building new CRUD modules.
|
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.
|
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.
|
||||||
|
|||||||
@@ -190,3 +190,21 @@ Future:
|
|||||||
- approved PDF generator
|
- approved PDF generator
|
||||||
- storage abstraction
|
- storage abstraction
|
||||||
- immutable approved artifact URL on quotation
|
- 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
|
||||||
248
plans/task-h.md
Normal file
248
plans/task-h.md
Normal file
@@ -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
|
||||||
@@ -31,6 +31,11 @@ export default async function CustomerDetailRoute({ params }: PageProps) {
|
|||||||
(!!session?.user?.activeOrganizationId &&
|
(!!session?.user?.activeOrganizationId &&
|
||||||
(session.user.activeMembershipRole === 'admin' ||
|
(session.user.activeMembershipRole === 'admin' ||
|
||||||
session.user.activePermissions.includes(PERMISSIONS.crmContactCreate)));
|
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 =
|
const canContactUpdate =
|
||||||
session?.user?.systemRole === 'super_admin' ||
|
session?.user?.systemRole === 'super_admin' ||
|
||||||
(!!session?.user?.activeOrganizationId &&
|
(!!session?.user?.activeOrganizationId &&
|
||||||
@@ -45,6 +50,9 @@ export default async function CustomerDetailRoute({ params }: PageProps) {
|
|||||||
|
|
||||||
if (canRead) {
|
if (canRead) {
|
||||||
void queryClient.prefetchQuery(customerByIdOptions(id));
|
void queryClient.prefetchQuery(customerByIdOptions(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canContactRead) {
|
||||||
void queryClient.prefetchQuery(customerContactsOptions(id));
|
void queryClient.prefetchQuery(customerContactsOptions(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +86,7 @@ export default async function CustomerDetailRoute({ params }: PageProps) {
|
|||||||
customerId={id}
|
customerId={id}
|
||||||
referenceData={referenceData}
|
referenceData={referenceData}
|
||||||
canUpdate={canUpdate}
|
canUpdate={canUpdate}
|
||||||
|
canReadContacts={canContactRead}
|
||||||
canManageContacts={{
|
canManageContacts={{
|
||||||
create: canContactCreate,
|
create: canContactCreate,
|
||||||
update: canContactUpdate,
|
update: canContactUpdate,
|
||||||
|
|||||||
@@ -11,26 +11,57 @@ import {
|
|||||||
import { customerKeys } from './queries';
|
import { customerKeys } from './queries';
|
||||||
import type { CustomerContactMutationPayload, CustomerMutationPayload } from './types';
|
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({
|
export const createCustomerMutation = mutationOptions({
|
||||||
mutationFn: (data: CustomerMutationPayload) => createCustomer(data),
|
mutationFn: (data: CustomerMutationPayload) => createCustomer(data),
|
||||||
onSuccess: () => {
|
onSettled: async (_data, error) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: customerKeys.all });
|
if (!error) {
|
||||||
|
await invalidateCustomerLists();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateCustomerMutation = mutationOptions({
|
export const updateCustomerMutation = mutationOptions({
|
||||||
mutationFn: ({ id, values }: { id: string; values: CustomerMutationPayload }) =>
|
mutationFn: ({ id, values }: { id: string; values: CustomerMutationPayload }) =>
|
||||||
updateCustomer(id, values),
|
updateCustomer(id, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: customerKeys.all });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: customerKeys.detail(variables.id) });
|
await invalidateCustomerMutationQueries(variables.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteCustomerMutation = mutationOptions({
|
export const deleteCustomerMutation = mutationOptions({
|
||||||
mutationFn: (id: string) => deleteCustomer(id),
|
mutationFn: (id: string) => deleteCustomer(id),
|
||||||
onSuccess: () => {
|
onSettled: async (_data, error, id) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: customerKeys.all });
|
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;
|
customerId: string;
|
||||||
values: CustomerContactMutationPayload;
|
values: CustomerContactMutationPayload;
|
||||||
}) => createCustomerContact(customerId, values),
|
}) => createCustomerContact(customerId, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: customerKeys.contacts(variables.customerId) });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: customerKeys.detail(variables.customerId) });
|
await invalidateCustomerContactMutationQueries(variables.customerId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,17 +90,19 @@ export const updateCustomerContactMutation = mutationOptions({
|
|||||||
contactId: string;
|
contactId: string;
|
||||||
values: CustomerContactMutationPayload;
|
values: CustomerContactMutationPayload;
|
||||||
}) => updateCustomerContact(customerId, contactId, values),
|
}) => updateCustomerContact(customerId, contactId, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: customerKeys.contacts(variables.customerId) });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: customerKeys.detail(variables.customerId) });
|
await invalidateCustomerContactMutationQueries(variables.customerId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteCustomerContactMutation = mutationOptions({
|
export const deleteCustomerContactMutation = mutationOptions({
|
||||||
mutationFn: ({ customerId, contactId }: { customerId: string; contactId: string }) =>
|
mutationFn: ({ customerId, contactId }: { customerId: string; contactId: string }) =>
|
||||||
deleteCustomerContact(customerId, contactId),
|
deleteCustomerContact(customerId, contactId),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: customerKeys.contacts(variables.customerId) });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: customerKeys.detail(variables.customerId) });
|
await invalidateCustomerContactMutationQueries(variables.customerId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import type { CustomerFilters } from './types';
|
|||||||
|
|
||||||
export const customerKeys = {
|
export const customerKeys = {
|
||||||
all: ['crm-customers'] as const,
|
all: ['crm-customers'] as const,
|
||||||
list: (filters: CustomerFilters) => [...customerKeys.all, 'list', filters] as const,
|
lists: () => [...customerKeys.all, 'list'] as const,
|
||||||
detail: (id: string) => [...customerKeys.all, 'detail', id] as const,
|
list: (filters: CustomerFilters) => [...customerKeys.lists(), filters] as const,
|
||||||
contacts: (id: string) => [...customerKeys.all, 'contacts', id] 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) =>
|
export const customersQueryOptions = (filters: CustomerFilters) =>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { formatDateTime } from '@/lib/format';
|
||||||
import { customerByIdOptions } from '../api/queries';
|
import { customerByIdOptions } from '../api/queries';
|
||||||
import type { CustomerReferenceData } from '../api/types';
|
import type { CustomerReferenceData } from '../api/types';
|
||||||
import { CustomerFormSheet } from './customer-form-sheet';
|
import { CustomerFormSheet } from './customer-form-sheet';
|
||||||
@@ -30,6 +31,7 @@ export function CustomerDetail({
|
|||||||
customerId,
|
customerId,
|
||||||
referenceData,
|
referenceData,
|
||||||
canUpdate,
|
canUpdate,
|
||||||
|
canReadContacts,
|
||||||
canManageContacts,
|
canManageContacts,
|
||||||
relatedEnquiries,
|
relatedEnquiries,
|
||||||
relatedQuotations
|
relatedQuotations
|
||||||
@@ -37,6 +39,7 @@ export function CustomerDetail({
|
|||||||
customerId: string;
|
customerId: string;
|
||||||
referenceData: CustomerReferenceData;
|
referenceData: CustomerReferenceData;
|
||||||
canUpdate: boolean;
|
canUpdate: boolean;
|
||||||
|
canReadContacts: boolean;
|
||||||
canManageContacts: {
|
canManageContacts: {
|
||||||
create: boolean;
|
create: boolean;
|
||||||
update: boolean;
|
update: boolean;
|
||||||
@@ -113,7 +116,7 @@ export function CustomerDetail({
|
|||||||
<Tabs defaultValue='overview' className='gap-4'>
|
<Tabs defaultValue='overview' className='gap-4'>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value='overview'>Overview</TabsTrigger>
|
<TabsTrigger value='overview'>Overview</TabsTrigger>
|
||||||
<TabsTrigger value='contacts'>Contacts</TabsTrigger>
|
{canReadContacts ? <TabsTrigger value='contacts'>Contacts</TabsTrigger> : null}
|
||||||
<TabsTrigger value='activity'>Activity</TabsTrigger>
|
<TabsTrigger value='activity'>Activity</TabsTrigger>
|
||||||
<TabsTrigger value='related'>Related Documents</TabsTrigger>
|
<TabsTrigger value='related'>Related Documents</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -166,6 +169,7 @@ export function CustomerDetail({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
{canReadContacts ? (
|
||||||
<TabsContent value='contacts'>
|
<TabsContent value='contacts'>
|
||||||
<CustomerContactsTab
|
<CustomerContactsTab
|
||||||
customerId={customerId}
|
customerId={customerId}
|
||||||
@@ -174,6 +178,7 @@ export function CustomerDetail({
|
|||||||
canDelete={canManageContacts.delete}
|
canDelete={canManageContacts.delete}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
) : null}
|
||||||
<TabsContent value='activity'>
|
<TabsContent value='activity'>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -202,7 +207,7 @@ export function CustomerDetail({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground text-xs'>
|
<div className='text-muted-foreground text-xs'>
|
||||||
{new Date(item.createdAt).toLocaleString()}
|
{formatDateTime(item.createdAt)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -281,8 +286,8 @@ export function CustomerDetail({
|
|||||||
<CardContent className='space-y-4'>
|
<CardContent className='space-y-4'>
|
||||||
<FieldItem label='Created By' value={customer.createdBy} />
|
<FieldItem label='Created By' value={customer.createdBy} />
|
||||||
<FieldItem label='Updated By' value={customer.updatedBy} />
|
<FieldItem label='Updated By' value={customer.updatedBy} />
|
||||||
<FieldItem label='Created At' value={new Date(customer.createdAt).toLocaleString()} />
|
<FieldItem label='Created At' value={formatDateTime(customer.createdAt)} />
|
||||||
<FieldItem label='Updated At' value={new Date(customer.updatedAt).toLocaleString()} />
|
<FieldItem label='Updated At' value={formatDateTime(customer.updatedAt)} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,26 +11,57 @@ import {
|
|||||||
import { enquiryKeys } from './queries';
|
import { enquiryKeys } from './queries';
|
||||||
import type { EnquiryFollowupMutationPayload, EnquiryMutationPayload } from './types';
|
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({
|
export const createEnquiryMutation = mutationOptions({
|
||||||
mutationFn: (data: EnquiryMutationPayload) => createEnquiry(data),
|
mutationFn: (data: EnquiryMutationPayload) => createEnquiry(data),
|
||||||
onSuccess: () => {
|
onSettled: async (_data, error) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: enquiryKeys.all });
|
if (!error) {
|
||||||
|
await invalidateEnquiryLists();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateEnquiryMutation = mutationOptions({
|
export const updateEnquiryMutation = mutationOptions({
|
||||||
mutationFn: ({ id, values }: { id: string; values: EnquiryMutationPayload }) =>
|
mutationFn: ({ id, values }: { id: string; values: EnquiryMutationPayload }) =>
|
||||||
updateEnquiry(id, values),
|
updateEnquiry(id, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: enquiryKeys.all });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: enquiryKeys.detail(variables.id) });
|
await invalidateEnquiryMutationQueries(variables.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteEnquiryMutation = mutationOptions({
|
export const deleteEnquiryMutation = mutationOptions({
|
||||||
mutationFn: (id: string) => deleteEnquiry(id),
|
mutationFn: (id: string) => deleteEnquiry(id),
|
||||||
onSuccess: () => {
|
onSettled: async (_data, error, id) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: enquiryKeys.all });
|
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;
|
enquiryId: string;
|
||||||
values: EnquiryFollowupMutationPayload;
|
values: EnquiryFollowupMutationPayload;
|
||||||
}) => createEnquiryFollowup(enquiryId, values),
|
}) => createEnquiryFollowup(enquiryId, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: enquiryKeys.followups(variables.enquiryId) });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: enquiryKeys.detail(variables.enquiryId) });
|
await invalidateEnquiryFollowupMutationQueries(variables.enquiryId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,17 +90,19 @@ export const updateEnquiryFollowupMutation = mutationOptions({
|
|||||||
followupId: string;
|
followupId: string;
|
||||||
values: EnquiryFollowupMutationPayload;
|
values: EnquiryFollowupMutationPayload;
|
||||||
}) => updateEnquiryFollowup(enquiryId, followupId, values),
|
}) => updateEnquiryFollowup(enquiryId, followupId, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: enquiryKeys.followups(variables.enquiryId) });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: enquiryKeys.detail(variables.enquiryId) });
|
await invalidateEnquiryFollowupMutationQueries(variables.enquiryId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteEnquiryFollowupMutation = mutationOptions({
|
export const deleteEnquiryFollowupMutation = mutationOptions({
|
||||||
mutationFn: ({ enquiryId, followupId }: { enquiryId: string; followupId: string }) =>
|
mutationFn: ({ enquiryId, followupId }: { enquiryId: string; followupId: string }) =>
|
||||||
deleteEnquiryFollowup(enquiryId, followupId),
|
deleteEnquiryFollowup(enquiryId, followupId),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: enquiryKeys.followups(variables.enquiryId) });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: enquiryKeys.detail(variables.enquiryId) });
|
await invalidateEnquiryFollowupMutationQueries(variables.enquiryId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import type { EnquiryFilters } from './types';
|
|||||||
|
|
||||||
export const enquiryKeys = {
|
export const enquiryKeys = {
|
||||||
all: ['crm-enquiries'] as const,
|
all: ['crm-enquiries'] as const,
|
||||||
list: (filters: EnquiryFilters) => [...enquiryKeys.all, 'list', filters] as const,
|
lists: () => [...enquiryKeys.all, 'list'] as const,
|
||||||
detail: (id: string) => [...enquiryKeys.all, 'detail', id] as const,
|
list: (filters: EnquiryFilters) => [...enquiryKeys.lists(), filters] as const,
|
||||||
followups: (id: string) => [...enquiryKeys.all, 'followups', id] 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) =>
|
export const enquiriesQueryOptions = (filters: EnquiryFilters) =>
|
||||||
|
|||||||
@@ -32,43 +32,104 @@ import type {
|
|||||||
QuotationTopicMutationPayload
|
QuotationTopicMutationPayload
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
function invalidateQuotationDocumentQueries(quotationId: string) {
|
async function invalidateQuotationLists() {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationDocumentKeys.data(quotationId) });
|
await getQueryClient().invalidateQueries({ queryKey: quotationKeys.lists() });
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationDocumentKeys.preview(quotationId) });
|
}
|
||||||
|
|
||||||
|
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({
|
export const createQuotationMutation = mutationOptions({
|
||||||
mutationFn: (data: QuotationMutationPayload) => createQuotation(data),
|
mutationFn: (data: QuotationMutationPayload) => createQuotation(data),
|
||||||
onSuccess: () => {
|
onSettled: async (_data, error) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.all });
|
if (!error) {
|
||||||
|
await invalidateQuotationLists();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateQuotationMutation = mutationOptions({
|
export const updateQuotationMutation = mutationOptions({
|
||||||
mutationFn: ({ id, values }: { id: string; values: QuotationMutationPayload }) =>
|
mutationFn: ({ id, values }: { id: string; values: QuotationMutationPayload }) =>
|
||||||
updateQuotation(id, values),
|
updateQuotation(id, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.all });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.id) });
|
await invalidateQuotationHeaderQueries(variables.id);
|
||||||
invalidateQuotationDocumentQueries(variables.id);
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteQuotationMutation = mutationOptions({
|
export const deleteQuotationMutation = mutationOptions({
|
||||||
mutationFn: (id: string) => deleteQuotation(id),
|
mutationFn: (id: string) => deleteQuotation(id),
|
||||||
onSuccess: () => {
|
onSettled: async (_data, error, id) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.all });
|
if (!error) {
|
||||||
|
await removeQuotationDetailQueries(id);
|
||||||
|
await invalidateQuotationLists();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createQuotationItemMutation = mutationOptions({
|
export const createQuotationItemMutation = mutationOptions({
|
||||||
mutationFn: ({ quotationId, values }: { quotationId: string; values: QuotationItemMutationPayload }) =>
|
mutationFn: ({ quotationId, values }: { quotationId: string; values: QuotationItemMutationPayload }) =>
|
||||||
createQuotationItem(quotationId, values),
|
createQuotationItem(quotationId, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.items(variables.quotationId) });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) });
|
await Promise.all([
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.all });
|
invalidateQuotationItems(variables.quotationId),
|
||||||
invalidateQuotationDocumentQueries(variables.quotationId);
|
invalidateQuotationHeaderQueries(variables.quotationId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -82,22 +143,26 @@ export const updateQuotationItemMutation = mutationOptions({
|
|||||||
itemId: string;
|
itemId: string;
|
||||||
values: QuotationItemMutationPayload;
|
values: QuotationItemMutationPayload;
|
||||||
}) => updateQuotationItem(quotationId, itemId, values),
|
}) => updateQuotationItem(quotationId, itemId, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.items(variables.quotationId) });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) });
|
await Promise.all([
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.all });
|
invalidateQuotationItems(variables.quotationId),
|
||||||
invalidateQuotationDocumentQueries(variables.quotationId);
|
invalidateQuotationHeaderQueries(variables.quotationId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteQuotationItemMutation = mutationOptions({
|
export const deleteQuotationItemMutation = mutationOptions({
|
||||||
mutationFn: ({ quotationId, itemId }: { quotationId: string; itemId: string }) =>
|
mutationFn: ({ quotationId, itemId }: { quotationId: string; itemId: string }) =>
|
||||||
deleteQuotationItem(quotationId, itemId),
|
deleteQuotationItem(quotationId, itemId),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.items(variables.quotationId) });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) });
|
await Promise.all([
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.all });
|
invalidateQuotationItems(variables.quotationId),
|
||||||
invalidateQuotationDocumentQueries(variables.quotationId);
|
invalidateQuotationHeaderQueries(variables.quotationId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,12 +174,13 @@ export const createQuotationCustomerMutation = mutationOptions({
|
|||||||
quotationId: string;
|
quotationId: string;
|
||||||
values: QuotationCustomerMutationPayload;
|
values: QuotationCustomerMutationPayload;
|
||||||
}) => createQuotationCustomer(quotationId, values),
|
}) => createQuotationCustomer(quotationId, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({
|
if (!error) {
|
||||||
queryKey: quotationKeys.customers(variables.quotationId)
|
await Promise.all([
|
||||||
});
|
invalidateQuotationCustomers(variables.quotationId),
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) });
|
invalidateQuotationHeaderQueries(variables.quotationId)
|
||||||
invalidateQuotationDocumentQueries(variables.quotationId);
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,51 +194,68 @@ export const updateQuotationCustomerMutation = mutationOptions({
|
|||||||
relationId: string;
|
relationId: string;
|
||||||
values: QuotationCustomerMutationPayload;
|
values: QuotationCustomerMutationPayload;
|
||||||
}) => updateQuotationCustomer(quotationId, relationId, values),
|
}) => updateQuotationCustomer(quotationId, relationId, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({
|
if (!error) {
|
||||||
queryKey: quotationKeys.customers(variables.quotationId)
|
await Promise.all([
|
||||||
});
|
invalidateQuotationCustomers(variables.quotationId),
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) });
|
invalidateQuotationHeaderQueries(variables.quotationId)
|
||||||
invalidateQuotationDocumentQueries(variables.quotationId);
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteQuotationCustomerMutation = mutationOptions({
|
export const deleteQuotationCustomerMutation = mutationOptions({
|
||||||
mutationFn: ({ quotationId, relationId }: { quotationId: string; relationId: string }) =>
|
mutationFn: ({ quotationId, relationId }: { quotationId: string; relationId: string }) =>
|
||||||
deleteQuotationCustomer(quotationId, relationId),
|
deleteQuotationCustomer(quotationId, relationId),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({
|
if (!error) {
|
||||||
queryKey: quotationKeys.customers(variables.quotationId)
|
await Promise.all([
|
||||||
});
|
invalidateQuotationCustomers(variables.quotationId),
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) });
|
invalidateQuotationHeaderQueries(variables.quotationId)
|
||||||
invalidateQuotationDocumentQueries(variables.quotationId);
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createQuotationTopicMutation = mutationOptions({
|
export const createQuotationTopicMutation = mutationOptions({
|
||||||
mutationFn: ({ quotationId, values }: { quotationId: string; values: QuotationTopicMutationPayload }) =>
|
mutationFn: ({ quotationId, values }: { quotationId: string; values: QuotationTopicMutationPayload }) =>
|
||||||
createQuotationTopic(quotationId, values),
|
createQuotationTopic(quotationId, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.topics(variables.quotationId) });
|
if (!error) {
|
||||||
invalidateQuotationDocumentQueries(variables.quotationId);
|
await Promise.all([
|
||||||
|
invalidateQuotationTopics(variables.quotationId),
|
||||||
|
invalidateQuotationDetail(variables.quotationId),
|
||||||
|
invalidateQuotationDocumentQueries(variables.quotationId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateQuotationTopicMutation = mutationOptions({
|
export const updateQuotationTopicMutation = mutationOptions({
|
||||||
mutationFn: ({ quotationId, values }: { quotationId: string; values: QuotationTopicMutationPayload }) =>
|
mutationFn: ({ quotationId, values }: { quotationId: string; values: QuotationTopicMutationPayload }) =>
|
||||||
updateQuotationTopic(quotationId, values),
|
updateQuotationTopic(quotationId, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.topics(variables.quotationId) });
|
if (!error) {
|
||||||
invalidateQuotationDocumentQueries(variables.quotationId);
|
await Promise.all([
|
||||||
|
invalidateQuotationTopics(variables.quotationId),
|
||||||
|
invalidateQuotationDetail(variables.quotationId),
|
||||||
|
invalidateQuotationDocumentQueries(variables.quotationId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteQuotationTopicMutation = mutationOptions({
|
export const deleteQuotationTopicMutation = mutationOptions({
|
||||||
mutationFn: ({ quotationId, topicId }: { quotationId: string; topicId: string }) =>
|
mutationFn: ({ quotationId, topicId }: { quotationId: string; topicId: string }) =>
|
||||||
deleteQuotationTopic(quotationId, topicId),
|
deleteQuotationTopic(quotationId, topicId),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.topics(variables.quotationId) });
|
if (!error) {
|
||||||
invalidateQuotationDocumentQueries(variables.quotationId);
|
await Promise.all([
|
||||||
|
invalidateQuotationTopics(variables.quotationId),
|
||||||
|
invalidateQuotationDetail(variables.quotationId),
|
||||||
|
invalidateQuotationDocumentQueries(variables.quotationId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -184,11 +267,13 @@ export const createQuotationFollowupMutation = mutationOptions({
|
|||||||
quotationId: string;
|
quotationId: string;
|
||||||
values: QuotationFollowupMutationPayload;
|
values: QuotationFollowupMutationPayload;
|
||||||
}) => createQuotationFollowup(quotationId, values),
|
}) => createQuotationFollowup(quotationId, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({
|
if (!error) {
|
||||||
queryKey: quotationKeys.followups(variables.quotationId)
|
await Promise.all([
|
||||||
});
|
invalidateQuotationFollowups(variables.quotationId),
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) });
|
invalidateQuotationDetail(variables.quotationId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -200,22 +285,26 @@ export const updateQuotationFollowupMutation = mutationOptions({
|
|||||||
quotationId: string;
|
quotationId: string;
|
||||||
values: QuotationFollowupMutationPayload;
|
values: QuotationFollowupMutationPayload;
|
||||||
}) => updateQuotationFollowup(quotationId, values),
|
}) => updateQuotationFollowup(quotationId, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({
|
if (!error) {
|
||||||
queryKey: quotationKeys.followups(variables.quotationId)
|
await Promise.all([
|
||||||
});
|
invalidateQuotationFollowups(variables.quotationId),
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) });
|
invalidateQuotationDetail(variables.quotationId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteQuotationFollowupMutation = mutationOptions({
|
export const deleteQuotationFollowupMutation = mutationOptions({
|
||||||
mutationFn: ({ quotationId, followupId }: { quotationId: string; followupId: string }) =>
|
mutationFn: ({ quotationId, followupId }: { quotationId: string; followupId: string }) =>
|
||||||
deleteQuotationFollowup(quotationId, followupId),
|
deleteQuotationFollowup(quotationId, followupId),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({
|
if (!error) {
|
||||||
queryKey: quotationKeys.followups(variables.quotationId)
|
await Promise.all([
|
||||||
});
|
invalidateQuotationFollowups(variables.quotationId),
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.detail(variables.quotationId) });
|
invalidateQuotationDetail(variables.quotationId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -227,10 +316,13 @@ export const createQuotationAttachmentMutation = mutationOptions({
|
|||||||
quotationId: string;
|
quotationId: string;
|
||||||
values: QuotationAttachmentMutationPayload;
|
values: QuotationAttachmentMutationPayload;
|
||||||
}) => createQuotationAttachment(quotationId, values),
|
}) => createQuotationAttachment(quotationId, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({
|
if (!error) {
|
||||||
queryKey: quotationKeys.attachments(variables.quotationId)
|
await Promise.all([
|
||||||
});
|
invalidateQuotationAttachments(variables.quotationId),
|
||||||
|
invalidateQuotationDetail(variables.quotationId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -242,20 +334,26 @@ export const updateQuotationAttachmentMutation = mutationOptions({
|
|||||||
quotationId: string;
|
quotationId: string;
|
||||||
values: QuotationAttachmentMutationPayload;
|
values: QuotationAttachmentMutationPayload;
|
||||||
}) => updateQuotationAttachment(quotationId, values),
|
}) => updateQuotationAttachment(quotationId, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({
|
if (!error) {
|
||||||
queryKey: quotationKeys.attachments(variables.quotationId)
|
await Promise.all([
|
||||||
});
|
invalidateQuotationAttachments(variables.quotationId),
|
||||||
|
invalidateQuotationDetail(variables.quotationId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteQuotationAttachmentMutation = mutationOptions({
|
export const deleteQuotationAttachmentMutation = mutationOptions({
|
||||||
mutationFn: ({ quotationId, attachmentId }: { quotationId: string; attachmentId: string }) =>
|
mutationFn: ({ quotationId, attachmentId }: { quotationId: string; attachmentId: string }) =>
|
||||||
deleteQuotationAttachment(quotationId, attachmentId),
|
deleteQuotationAttachment(quotationId, attachmentId),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({
|
if (!error) {
|
||||||
queryKey: quotationKeys.attachments(variables.quotationId)
|
await Promise.all([
|
||||||
});
|
invalidateQuotationAttachments(variables.quotationId),
|
||||||
|
invalidateQuotationDetail(variables.quotationId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -267,8 +365,12 @@ export const createQuotationRevisionMutation = mutationOptions({
|
|||||||
quotationId: string;
|
quotationId: string;
|
||||||
revisionRemark?: string;
|
revisionRemark?: string;
|
||||||
}) => createQuotationRevision(quotationId, revisionRemark),
|
}) => createQuotationRevision(quotationId, revisionRemark),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.revisions(variables.quotationId) });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: quotationKeys.all });
|
await Promise.all([
|
||||||
|
invalidateQuotationRevisions(variables.quotationId),
|
||||||
|
invalidateQuotationHeaderQueries(variables.quotationId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,14 +13,22 @@ import type { QuotationFilters } from './types';
|
|||||||
|
|
||||||
export const quotationKeys = {
|
export const quotationKeys = {
|
||||||
all: ['crm-quotations'] as const,
|
all: ['crm-quotations'] as const,
|
||||||
list: (filters: QuotationFilters) => [...quotationKeys.all, 'list', filters] as const,
|
lists: () => [...quotationKeys.all, 'list'] as const,
|
||||||
detail: (id: string) => [...quotationKeys.all, 'detail', id] as const,
|
list: (filters: QuotationFilters) => [...quotationKeys.lists(), filters] as const,
|
||||||
items: (id: string) => [...quotationKeys.all, 'items', id] as const,
|
details: () => [...quotationKeys.all, 'detail'] as const,
|
||||||
customers: (id: string) => [...quotationKeys.all, 'customers', id] as const,
|
detail: (id: string) => [...quotationKeys.details(), id] as const,
|
||||||
topics: (id: string) => [...quotationKeys.all, 'topics', id] as const,
|
itemsRoot: () => [...quotationKeys.all, 'items'] as const,
|
||||||
followups: (id: string) => [...quotationKeys.all, 'followups', id] as const,
|
items: (id: string) => [...quotationKeys.itemsRoot(), id] as const,
|
||||||
attachments: (id: string) => [...quotationKeys.all, 'attachments', id] as const,
|
customersRoot: () => [...quotationKeys.all, 'customers'] as const,
|
||||||
revisions: (id: string) => [...quotationKeys.all, 'revisions', id] 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) =>
|
export const quotationsQueryOptions = (filters: QuotationFilters) =>
|
||||||
|
|||||||
@@ -13,75 +13,104 @@ import { approvalKeys } from './queries';
|
|||||||
import { quotationKeys } from '@/features/crm/quotations/api/queries';
|
import { quotationKeys } from '@/features/crm/quotations/api/queries';
|
||||||
import type { ApprovalActionPayload, SubmitApprovalPayload } from './types';
|
import type { ApprovalActionPayload, SubmitApprovalPayload } from './types';
|
||||||
|
|
||||||
function invalidateQuotationQueries(quotationId: string) {
|
async function invalidateApprovalLists() {
|
||||||
const queryClient = getQueryClient();
|
await getQueryClient().invalidateQueries({ queryKey: approvalKeys.lists() });
|
||||||
queryClient.invalidateQueries({ queryKey: quotationKeys.all });
|
|
||||||
queryClient.invalidateQueries({ queryKey: quotationKeys.detail(quotationId) });
|
|
||||||
queryClient.invalidateQueries({ queryKey: quotationDocumentKeys.data(quotationId) });
|
|
||||||
queryClient.invalidateQueries({ queryKey: quotationDocumentKeys.preview(quotationId) });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 queryClient = getQueryClient();
|
||||||
const cachedDetail = queryClient.getQueryData<{ approval: { request: { entityType: string; entityId: string } } }>(
|
const cachedDetail = queryClient.getQueryData<{ approval: { request: { entityType: string; entityId: string } } }>(
|
||||||
approvalKeys.detail(approvalId)
|
approvalKeys.detail(approvalId)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cachedDetail?.approval.request.entityType === 'quotation') {
|
if (cachedDetail?.approval.request.entityType === 'quotation') {
|
||||||
invalidateQuotationQueries(cachedDetail.approval.request.entityId);
|
await invalidateQuotationQueries(cachedDetail.approval.request.entityId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const submitApprovalMutation = mutationOptions({
|
export const submitApprovalMutation = mutationOptions({
|
||||||
mutationFn: (data: SubmitApprovalPayload) => submitApproval(data),
|
mutationFn: (data: SubmitApprovalPayload) => submitApproval(data),
|
||||||
onSuccess: () => {
|
onSettled: async (_data, error) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: approvalKeys.all });
|
if (!error) {
|
||||||
|
await invalidateApprovalLists();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const submitQuotationApprovalMutation = mutationOptions({
|
export const submitQuotationApprovalMutation = mutationOptions({
|
||||||
mutationFn: ({ id, remark }: { id: string; remark?: string }) => submitQuotationForApproval(id, remark),
|
mutationFn: ({ id, remark }: { id: string; remark?: string }) => submitQuotationForApproval(id, remark),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: approvalKeys.all });
|
if (!error) {
|
||||||
invalidateQuotationQueries(variables.id);
|
await Promise.all([invalidateApprovalLists(), invalidateQuotationQueries(variables.id)]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const cancelApprovalMutation = mutationOptions({
|
export const cancelApprovalMutation = mutationOptions({
|
||||||
mutationFn: (id: string) => cancelApproval(id),
|
mutationFn: (id: string) => cancelApproval(id),
|
||||||
onSuccess: (_data, id) => {
|
onSettled: async (_data, error, id) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: approvalKeys.all });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: approvalKeys.detail(id) });
|
await Promise.all([
|
||||||
invalidateRelatedEntityQueries(id);
|
invalidateApprovalLists(),
|
||||||
|
invalidateApprovalDetail(id),
|
||||||
|
invalidateRelatedEntityQueries(id)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const approveApprovalMutation = mutationOptions({
|
export const approveApprovalMutation = mutationOptions({
|
||||||
mutationFn: ({ id, values }: { id: string; values: ApprovalActionPayload }) =>
|
mutationFn: ({ id, values }: { id: string; values: ApprovalActionPayload }) =>
|
||||||
approveApproval(id, values),
|
approveApproval(id, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: approvalKeys.all });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: approvalKeys.detail(variables.id) });
|
await Promise.all([
|
||||||
invalidateRelatedEntityQueries(variables.id);
|
invalidateApprovalLists(),
|
||||||
|
invalidateApprovalDetail(variables.id),
|
||||||
|
invalidateRelatedEntityQueries(variables.id)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const rejectApprovalMutation = mutationOptions({
|
export const rejectApprovalMutation = mutationOptions({
|
||||||
mutationFn: ({ id, values }: { id: string; values: ApprovalActionPayload }) =>
|
mutationFn: ({ id, values }: { id: string; values: ApprovalActionPayload }) =>
|
||||||
rejectApproval(id, values),
|
rejectApproval(id, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: approvalKeys.all });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: approvalKeys.detail(variables.id) });
|
await Promise.all([
|
||||||
invalidateRelatedEntityQueries(variables.id);
|
invalidateApprovalLists(),
|
||||||
|
invalidateApprovalDetail(variables.id),
|
||||||
|
invalidateRelatedEntityQueries(variables.id)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const returnApprovalMutation = mutationOptions({
|
export const returnApprovalMutation = mutationOptions({
|
||||||
mutationFn: ({ id, values }: { id: string; values: ApprovalActionPayload }) =>
|
mutationFn: ({ id, values }: { id: string; values: ApprovalActionPayload }) =>
|
||||||
returnApproval(id, values),
|
returnApproval(id, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: approvalKeys.all });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: approvalKeys.detail(variables.id) });
|
await Promise.all([
|
||||||
invalidateRelatedEntityQueries(variables.id);
|
invalidateApprovalLists(),
|
||||||
|
invalidateApprovalDetail(variables.id),
|
||||||
|
invalidateRelatedEntityQueries(variables.id)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import type { ApprovalFilters } from './types';
|
|||||||
|
|
||||||
export const approvalKeys = {
|
export const approvalKeys = {
|
||||||
all: ['crm-approvals'] as const,
|
all: ['crm-approvals'] as const,
|
||||||
list: (filters: ApprovalFilters) => [...approvalKeys.all, 'list', filters] as const,
|
lists: () => [...approvalKeys.all, 'list'] as const,
|
||||||
detail: (id: string) => [...approvalKeys.all, 'detail', id] 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) =>
|
export const approvalsQueryOptions = (filters: ApprovalFilters) =>
|
||||||
|
|||||||
@@ -9,35 +9,61 @@ import {
|
|||||||
import { documentTemplateKeys } from './queries';
|
import { documentTemplateKeys } from './queries';
|
||||||
import type { DocumentTemplateMutationPayload, DocumentTemplateVersionMutationPayload } from './types';
|
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({
|
export const createDocumentTemplateMutation = mutationOptions({
|
||||||
mutationFn: (data: DocumentTemplateMutationPayload) => createDocumentTemplate(data),
|
mutationFn: (data: DocumentTemplateMutationPayload) => createDocumentTemplate(data),
|
||||||
onSuccess: () => {
|
onSettled: async (_data, error) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.all });
|
if (!error) {
|
||||||
|
await invalidateDocumentTemplateLists();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateDocumentTemplateMutation = mutationOptions({
|
export const updateDocumentTemplateMutation = mutationOptions({
|
||||||
mutationFn: ({ id, values }: { id: string; values: Partial<DocumentTemplateMutationPayload> }) =>
|
mutationFn: ({ id, values }: { id: string; values: Partial<DocumentTemplateMutationPayload> }) =>
|
||||||
updateDocumentTemplate(id, values),
|
updateDocumentTemplate(id, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.all });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.detail(variables.id) });
|
await Promise.all([
|
||||||
|
invalidateDocumentTemplateLists(),
|
||||||
|
invalidateDocumentTemplateDetail(variables.id)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteDocumentTemplateMutation = mutationOptions({
|
export const deleteDocumentTemplateMutation = mutationOptions({
|
||||||
mutationFn: (id: string) => deleteDocumentTemplate(id),
|
mutationFn: (id: string) => deleteDocumentTemplate(id),
|
||||||
onSuccess: () => {
|
onSettled: async (_data, error, id) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.all });
|
if (!error) {
|
||||||
|
getQueryClient().removeQueries({ queryKey: documentTemplateKeys.detail(id) });
|
||||||
|
getQueryClient().removeQueries({ queryKey: documentTemplateKeys.versions(id) });
|
||||||
|
await invalidateDocumentTemplateLists();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createDocumentTemplateVersionMutation = mutationOptions({
|
export const createDocumentTemplateVersionMutation = mutationOptions({
|
||||||
mutationFn: ({ id, values }: { id: string; values: DocumentTemplateVersionMutationPayload }) =>
|
mutationFn: ({ id, values }: { id: string; values: DocumentTemplateVersionMutationPayload }) =>
|
||||||
createDocumentTemplateVersion(id, values),
|
createDocumentTemplateVersion(id, values),
|
||||||
onSuccess: (_data, variables) => {
|
onSettled: async (_data, error, variables) => {
|
||||||
getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.all });
|
if (!error) {
|
||||||
getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.detail(variables.id) });
|
await Promise.all([
|
||||||
getQueryClient().invalidateQueries({ queryKey: documentTemplateKeys.versions(variables.id) });
|
invalidateDocumentTemplateLists(),
|
||||||
|
invalidateDocumentTemplateDetail(variables.id),
|
||||||
|
invalidateDocumentTemplateVersions(variables.id)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import type { DocumentTemplateFilters } from './types';
|
|||||||
|
|
||||||
export const documentTemplateKeys = {
|
export const documentTemplateKeys = {
|
||||||
all: ['crm-document-templates'] as const,
|
all: ['crm-document-templates'] as const,
|
||||||
|
lists: () => [...documentTemplateKeys.all, 'list'] as const,
|
||||||
list: (filters: DocumentTemplateFilters) =>
|
list: (filters: DocumentTemplateFilters) =>
|
||||||
[...documentTemplateKeys.all, 'list', filters] as const,
|
[...documentTemplateKeys.lists(), filters] as const,
|
||||||
detail: (id: string) => [...documentTemplateKeys.all, 'detail', id] as const,
|
details: () => [...documentTemplateKeys.all, 'detail'] as const,
|
||||||
versions: (id: string) => [...documentTemplateKeys.all, 'versions', id] 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 = {}) =>
|
export const documentTemplatesQueryOptions = (filters: DocumentTemplateFilters = {}) =>
|
||||||
|
|||||||
@@ -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).
|
* Formats a date to YYYY-MM-DD format (ISO date).
|
||||||
* This is used to prevent hydration mismatches between server and client.
|
* This is used to prevent hydration mismatches between server and client.
|
||||||
|
|||||||
Reference in New Issue
Block a user