Hotfix Rule: Force Fresh Data After CRUD Mutation

Task-h not implements
This commit is contained in:
phaichayon
2026-06-16 11:59:41 +07:00
parent 357414c247
commit 56683ee7b9
16 changed files with 763 additions and 186 deletions

View File

@@ -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);
}
}
});

View File

@@ -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) =>

View File

@@ -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({
<Tabs defaultValue='overview' className='gap-4'>
<TabsList>
<TabsTrigger value='overview'>Overview</TabsTrigger>
<TabsTrigger value='contacts'>Contacts</TabsTrigger>
{canReadContacts ? <TabsTrigger value='contacts'>Contacts</TabsTrigger> : null}
<TabsTrigger value='activity'>Activity</TabsTrigger>
<TabsTrigger value='related'>Related Documents</TabsTrigger>
</TabsList>
@@ -166,14 +169,16 @@ export function CustomerDetail({
</CardContent>
</Card>
</TabsContent>
<TabsContent value='contacts'>
<CustomerContactsTab
customerId={customerId}
canCreate={canManageContacts.create}
canUpdate={canManageContacts.update}
canDelete={canManageContacts.delete}
/>
</TabsContent>
{canReadContacts ? (
<TabsContent value='contacts'>
<CustomerContactsTab
customerId={customerId}
canCreate={canManageContacts.create}
canUpdate={canManageContacts.update}
canDelete={canManageContacts.delete}
/>
</TabsContent>
) : null}
<TabsContent value='activity'>
<Card>
<CardHeader>
@@ -202,7 +207,7 @@ export function CustomerDetail({
</span>
</div>
<div className='text-muted-foreground text-xs'>
{new Date(item.createdAt).toLocaleString()}
{formatDateTime(item.createdAt)}
</div>
</div>
</div>
@@ -281,8 +286,8 @@ export function CustomerDetail({
<CardContent className='space-y-4'>
<FieldItem label='Created By' value={customer.createdBy} />
<FieldItem label='Updated By' value={customer.updatedBy} />
<FieldItem label='Created At' value={new Date(customer.createdAt).toLocaleString()} />
<FieldItem label='Updated At' value={new Date(customer.updatedAt).toLocaleString()} />
<FieldItem label='Created At' value={formatDateTime(customer.createdAt)} />
<FieldItem label='Updated At' value={formatDateTime(customer.updatedAt)} />
</CardContent>
</Card>
</div>

View File

@@ -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);
}
}
});

View File

@@ -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) =>

View File

@@ -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)
]);
}
}
});

View File

@@ -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) =>

View File

@@ -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)
]);
}
}
});

View File

@@ -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) =>

View File

@@ -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<DocumentTemplateMutationPayload> }) =>
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)
]);
}
}
});

View File

@@ -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 = {}) =>