shoc-frontend-new/src/app/v/[token]/dispatch/_components/refuse-dispatch-dialog.tsx

142 lines
4.5 KiB
TypeScript
Raw Normal View History

import { useEffect } from "react";
import { useForm, useWatch } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material";
import { Text } from "@/components/ui/text";
import { useVendorPortal } from "@/app/v/_components/vendor-portal-context";
import { vendorPortalApi } from "@/domain/vendor-portal/api/vendor-portal-api";
import {
REFUSE_REASON_MAX_LENGTH,
refuseDispatchSchema,
type RefuseDispatchValues,
} from "@/domain/vendor-portal/schemas/refuse-dispatch-schema";
import { queryKeys } from "@/infra/query-key/query-key";
2026-08-10 13:31:12 -03:00
import { guardDialogClose } from "@/lib/dialog-guard";
type RefuseDispatchDialogProps = {
open: boolean;
dispatchId: number;
onClose: () => void;
onRefused: () => void;
};
export function RefuseDispatchDialog({
open,
dispatchId,
onClose,
onRefused,
}: RefuseDispatchDialogProps) {
const { token } = useVendorPortal();
const queryClient = useQueryClient();
const {
register,
handleSubmit,
control,
reset,
formState: { errors },
} = useForm<RefuseDispatchValues>({
resolver: zodResolver(refuseDispatchSchema),
mode: "onChange",
defaultValues: { reason: "" },
});
const reasonValue = useWatch({ control, name: "reason" }) ?? "";
const isOverLimit = reasonValue.length > REFUSE_REASON_MAX_LENGTH;
const refuseMutation = useMutation({
mutationFn: (values: RefuseDispatchValues) => {
const trimmed = values.reason.trim();
return vendorPortalApi.refuse(token, dispatchId, trimmed || undefined);
},
onSuccess: async () => {
await Promise.all([
queryClient.invalidateQueries({
queryKey: queryKeys.vendorPortal.dispatch(token, dispatchId),
}),
queryClient.invalidateQueries({
queryKey: queryKeys.vendorPortal.dispatches(token),
}),
]);
onRefused();
onClose();
},
});
const { reset: resetMutation } = refuseMutation;
useEffect(() => {
if (!open) {
reset({ reason: "" });
resetMutation();
}
}, [open, reset, resetMutation]);
const submit = handleSubmit((values) => refuseMutation.mutate(values));
const errorMessage = refuseMutation.error
? "Unable to refuse this dispatch. Please try again."
: "";
return (
2026-08-10 13:31:12 -03:00
<Dialog
open={open}
onClose={guardDialogClose(refuseMutation.isPending, onClose)}
aria-labelledby="refuse-dispatch-title"
>
<DialogTitle id="refuse-dispatch-title">Refuse dispatch</DialogTitle>
<form onSubmit={(event) => void submit(event)}>
<DialogContent>
<Text variant="body" className="vp-muted" sx={{ marginBottom: 2 }}>
Refusing ends your assignment for this dispatch. A reason is optional but helps your
dispatcher reassign the work.
</Text>
<label htmlFor="refuse-reason" className="vp-muted" style={{ fontSize: 13 }}>
Reason (optional)
</label>
<textarea
id="refuse-reason"
className="vp-textarea"
placeholder="Tell the dispatcher why you are refusing…"
aria-invalid={Boolean(errors.reason)}
aria-describedby={errors.reason ? "refuse-reason-error" : "refuse-reason-counter"}
maxLength={REFUSE_REASON_MAX_LENGTH}
{...register("reason")}
style={{ marginTop: 6 }}
/>
<div
id="refuse-reason-counter"
className="vp-muted"
style={{ fontSize: 12, textAlign: "right" }}
>
{reasonValue.length}/{REFUSE_REASON_MAX_LENGTH}
</div>
<Text variant="error" when={isOverLimit} id="refuse-reason-error">
{errors.reason?.message ?? "Reason must be 500 characters or fewer."}
</Text>
<Text variant="error" when={Boolean(errorMessage)}>
{errorMessage}
</Text>
</DialogContent>
<DialogActions>
<button
type="button"
className="vp-btn vp-btn-ghost"
onClick={onClose}
disabled={refuseMutation.isPending}
>
Cancel
</button>
<button
type="submit"
className="vp-btn vp-btn-warn"
disabled={refuseMutation.isPending || isOverLimit}
>
{refuseMutation.isPending ? "Refusing…" : "Refuse dispatch"}
</button>
</DialogActions>
</form>
</Dialog>
);
}