mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-05 00:12:09 +00:00
* docs(work-orders): add pending BE contract checklist for design parity gaps * feat(work-orders): add mentions in work-order comments composer * docs(work-orders): align pr description with jira and qa residuals * refactor(workorders): split work order view page into view components * fix(work-orders): export comment assignment vendor input types for view hook * fix(work-orders): drop preferredContact from vendor update payload Align with WorkOrderVendorUpdatePayload on current dev so the mentions branch typechecks. * fix(work-orders): remove Preferred Contact from WO vendor editor (SH-180) EOF * fix(work-orders): harden comment mentions and clear draft on success * fix(work-orders): harden mention highlight boundary and submit catch [SH-217] * fix(work-orders): keep mention cursor sync on current value [SH-217] * fix(work-orders): keep mention menu open after typing @ Pass live text into syncCursor instead of a render-time valueRef. Add a composer interaction regression for @ autocomplete visibility. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Alexandre Brandizzi <alex_brandizzi@hotmail.com>
409 lines
13 KiB
TypeScript
409 lines
13 KiB
TypeScript
import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query";
|
|
import { useNavigate } from "react-router";
|
|
import { toast } from "react-toastify";
|
|
import { workOrdersApi } from "@/domain/work-orders/api/work-orders-api";
|
|
import { BoardPatchConflictError } from "@/domain/work-orders/errors/board-patch-errors";
|
|
import type { UploadableMediaCategory } from "@/domain/work-orders/mappers/work-order-board-detail-mapper";
|
|
import type { WorkOrderFormSchemaValues } from "@/domain/work-orders/schemas/work-order-schema";
|
|
import type {
|
|
DispatchComment,
|
|
DispatchSignoff,
|
|
DispatchToVendorPayload,
|
|
UpdateDispatchPayload,
|
|
WorkOrderComment,
|
|
WorkOrderDetail,
|
|
WorkOrderFormValues,
|
|
WorkOrderVendorUpdatePayload,
|
|
} from "@/domain/work-orders/types/work-order";
|
|
import type {
|
|
WorkOrderBoardCompletion,
|
|
WorkOrderBoardMediaItem,
|
|
} from "@/domain/work-orders/types/work-order-board-detail";
|
|
import { queryKeys } from "@/infra/query-key/query-key";
|
|
|
|
function invalidateWorkOrders(queryClient: ReturnType<typeof useQueryClient>) {
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.all });
|
|
}
|
|
|
|
function invalidateBoardDetail(
|
|
queryClient: ReturnType<typeof useQueryClient>,
|
|
workOrderId: string | number,
|
|
) {
|
|
void queryClient.invalidateQueries({
|
|
queryKey: queryKeys.workOrders.boardDetail(workOrderId),
|
|
});
|
|
void queryClient.invalidateQueries({
|
|
queryKey: queryKeys.workOrders.boardMedia(workOrderId),
|
|
});
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.detail(workOrderId) });
|
|
}
|
|
|
|
export interface AddWorkOrderCommentInput {
|
|
text: string;
|
|
mentions?: string[];
|
|
commentType?: "customer" | "internal";
|
|
}
|
|
|
|
export interface UpdateWorkOrderCommentInput {
|
|
commentId: string | number;
|
|
text: string;
|
|
}
|
|
|
|
export interface UploadWorkOrderMediaInput {
|
|
file: File;
|
|
category: UploadableMediaCategory;
|
|
}
|
|
|
|
export interface UploadCompletionDocInput {
|
|
file: File;
|
|
workOrderVersion: string;
|
|
signOffName?: string;
|
|
signOffSignature?: string;
|
|
}
|
|
|
|
export interface AddDispatchCommentInput {
|
|
text: string;
|
|
sendEmail: boolean;
|
|
}
|
|
|
|
export interface UpdateChecklistItemInput {
|
|
itemId: string | number;
|
|
isCompleted: boolean;
|
|
completedBy: string;
|
|
}
|
|
|
|
export interface AddDispatchSignoffInput {
|
|
signoffType: string;
|
|
name: string;
|
|
signature: string;
|
|
signatureMethod: string;
|
|
}
|
|
|
|
export interface ChangeWorkOrderAssignmentInput {
|
|
userId: string | number;
|
|
userName: string;
|
|
}
|
|
|
|
export interface UpdateWorkOrderVendorInput {
|
|
vendorId: string | number;
|
|
payload: WorkOrderVendorUpdatePayload;
|
|
}
|
|
|
|
export function useCreateWorkOrder(): UseMutationResult<
|
|
WorkOrderDetail,
|
|
Error,
|
|
WorkOrderFormSchemaValues | WorkOrderFormValues
|
|
> {
|
|
const queryClient = useQueryClient();
|
|
const navigate = useNavigate();
|
|
|
|
return useMutation({
|
|
mutationFn: (values: WorkOrderFormSchemaValues | WorkOrderFormValues) =>
|
|
workOrdersApi.create(values),
|
|
onSuccess: (data) => {
|
|
invalidateWorkOrders(queryClient);
|
|
toast.success("Work Order created successfully!");
|
|
navigate(`/workorders/${data.id}`);
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to create work order");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateWorkOrder(
|
|
id: string | number,
|
|
): UseMutationResult<void, Error, WorkOrderFormSchemaValues | WorkOrderFormValues> {
|
|
const queryClient = useQueryClient();
|
|
const navigate = useNavigate();
|
|
|
|
return useMutation({
|
|
mutationFn: (values: WorkOrderFormSchemaValues | WorkOrderFormValues) =>
|
|
workOrdersApi.update(id, values),
|
|
onSuccess: () => {
|
|
invalidateWorkOrders(queryClient);
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.detail(id) });
|
|
toast.success("Work Order updated successfully!");
|
|
navigate(`/workorders/${id}`);
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to update work order");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateWorkOrderVendor(
|
|
workOrderId: string | number,
|
|
): UseMutationResult<void, Error, UpdateWorkOrderVendorInput> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ vendorId, payload }) =>
|
|
workOrdersApi.updateVendorFromWorkOrder(vendorId, payload),
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.detail(workOrderId) });
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.all });
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.vendors.all });
|
|
toast.success("Vendor contact updated");
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to update vendor contact");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useAddWorkOrderComment(
|
|
workOrderId: string | number,
|
|
): UseMutationResult<WorkOrderComment, Error, AddWorkOrderCommentInput> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ text, mentions }: AddWorkOrderCommentInput) =>
|
|
workOrdersApi.createComment(workOrderId, text, mentions),
|
|
onSuccess: () => {
|
|
invalidateBoardDetail(queryClient, workOrderId);
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to add comment");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateWorkOrderComment(
|
|
workOrderId: string | number,
|
|
): UseMutationResult<WorkOrderComment, Error, UpdateWorkOrderCommentInput> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ commentId, text }: UpdateWorkOrderCommentInput) =>
|
|
workOrdersApi.updateComment(workOrderId, commentId, text),
|
|
onSuccess: () => {
|
|
invalidateBoardDetail(queryClient, workOrderId);
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to update comment");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUploadWorkOrderMedia(
|
|
workOrderId: string | number,
|
|
): UseMutationResult<WorkOrderBoardMediaItem, Error, UploadWorkOrderMediaInput> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ file, category }: UploadWorkOrderMediaInput) =>
|
|
workOrdersApi.uploadMedia(workOrderId, file, category),
|
|
onSuccess: () => {
|
|
invalidateBoardDetail(queryClient, workOrderId);
|
|
invalidateWorkOrders(queryClient);
|
|
toast.success("File uploaded");
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to upload file");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteWorkOrderMedia(
|
|
workOrderId: string | number,
|
|
): UseMutationResult<void, Error, string | number> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (mediaId: string | number) => workOrdersApi.deleteMedia(workOrderId, mediaId),
|
|
onSuccess: () => {
|
|
invalidateBoardDetail(queryClient, workOrderId);
|
|
invalidateWorkOrders(queryClient);
|
|
toast.success("File removed");
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to remove file");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUploadCompletionDoc(
|
|
workOrderId: string | number,
|
|
): UseMutationResult<WorkOrderBoardCompletion, Error, UploadCompletionDocInput> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (params: UploadCompletionDocInput) =>
|
|
workOrdersApi.uploadCompletionDoc(workOrderId, params),
|
|
onSuccess: () => {
|
|
invalidateBoardDetail(queryClient, workOrderId);
|
|
invalidateWorkOrders(queryClient);
|
|
toast.success("Completion document uploaded");
|
|
},
|
|
onError: (error: Error) => {
|
|
if (error instanceof BoardPatchConflictError) {
|
|
invalidateBoardDetail(queryClient, workOrderId);
|
|
toast.error("Document is out of date. Refresh and try again.");
|
|
return;
|
|
}
|
|
toast.error(error.message || "Failed to upload completion document");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useChangeWorkOrderStatus(
|
|
workOrderId: string | number,
|
|
): UseMutationResult<void, Error, string> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (status: string) => workOrdersApi.changeStatus(workOrderId, status),
|
|
onSuccess: (_data, status) => {
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.detail(workOrderId) });
|
|
invalidateWorkOrders(queryClient);
|
|
toast.success(`Status changed to ${status}`);
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to update status");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useChangeWorkOrderAssignment(
|
|
workOrderId: string | number,
|
|
): UseMutationResult<string, Error, ChangeWorkOrderAssignmentInput> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ userId, userName }: ChangeWorkOrderAssignmentInput) =>
|
|
workOrdersApi.changeAssignment(workOrderId, userId).then(() => userName),
|
|
onSuccess: (userName) => {
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.detail(workOrderId) });
|
|
invalidateWorkOrders(queryClient);
|
|
toast.success(`Assigned to ${userName || "Unassigned"}`);
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to update assignment");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDispatchToVendor(
|
|
workOrderId: string | number,
|
|
): UseMutationResult<void, Error, DispatchToVendorPayload> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: workOrdersApi.dispatchToVendor,
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.detail(workOrderId) });
|
|
invalidateWorkOrders(queryClient);
|
|
toast.success("Dispatched successfully");
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to dispatch");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateDispatch(
|
|
dispatchId: string | number,
|
|
workOrderId: string | number,
|
|
): UseMutationResult<void, Error, UpdateDispatchPayload> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: workOrdersApi.updateDispatch,
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.dispatch(dispatchId) });
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.detail(workOrderId) });
|
|
toast.success("Dispatch updated");
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to update dispatch");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useAddDispatchComment(
|
|
dispatchId: string | number,
|
|
): UseMutationResult<DispatchComment, Error, AddDispatchCommentInput> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ text, sendEmail }: AddDispatchCommentInput) =>
|
|
workOrdersApi.addDispatchComment(dispatchId, text, sendEmail),
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.dispatch(dispatchId) });
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to send message");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateChecklistItem(
|
|
dispatchId: string | number,
|
|
): UseMutationResult<void, Error, UpdateChecklistItemInput> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ itemId, isCompleted, completedBy }: UpdateChecklistItemInput) =>
|
|
workOrdersApi.updateChecklistItem(itemId, isCompleted, completedBy),
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.dispatch(dispatchId) });
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to update checklist item");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useAddChecklistItem(
|
|
dispatchId: string | number,
|
|
): UseMutationResult<void, Error, string> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (itemText: string) => workOrdersApi.addChecklistItem(dispatchId, itemText),
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.dispatch(dispatchId) });
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to add checklist item");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useAddDispatchSignoff(
|
|
dispatchId: string | number,
|
|
): UseMutationResult<DispatchSignoff, Error, AddDispatchSignoffInput> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (input: AddDispatchSignoffInput) =>
|
|
workOrdersApi.addDispatchSignoff(dispatchId, input),
|
|
onSuccess: (_data, variables) => {
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.dispatch(dispatchId) });
|
|
toast.success(`${variables.signoffType} sign-off collected`);
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to save sign-off");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useVerifyDispatch(
|
|
dispatchId: string | number,
|
|
workOrderId: string | number,
|
|
): UseMutationResult<void, Error, void> {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: () => workOrdersApi.verifyDispatch(dispatchId),
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.dispatch(dispatchId) });
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.detail(workOrderId) });
|
|
toast.success("Dispatch verified");
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error(error.message || "Failed to verify");
|
|
},
|
|
});
|
|
}
|