mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
Merge branch 'freight_feature/profile' of github.com:Tria-plc/edr-platform into freight_feature/profile
This commit is contained in:
@@ -8,10 +8,22 @@ import {
|
||||
Button,
|
||||
Textarea,
|
||||
FileInput,
|
||||
NumberInput,
|
||||
} from "@mantine/core";
|
||||
|
||||
import type { BookingActionDef } from "@/features/bookings/booking-actions.config";
|
||||
|
||||
/** Today + `days`, formatted as a readable date for the validity preview. */
|
||||
function validUntilLabel(days: number): string {
|
||||
const until = new Date();
|
||||
until.setDate(until.getDate() + days);
|
||||
return until.toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
interface BookingConfirmDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@@ -46,8 +58,14 @@ export function BookingConfirmDialog({
|
||||
const Icon = action.icon;
|
||||
const needsTextInput = action.input === "note" || action.input === "reason";
|
||||
const needsFileInput = action.input === "file";
|
||||
const needsDaysInput = action.input === "days";
|
||||
const daysValue = Number(inputValue.trim());
|
||||
const daysValid =
|
||||
Number.isInteger(daysValue) && daysValue >= 1 && daysValue <= 365;
|
||||
const inputMissing =
|
||||
(needsTextInput && !inputValue.trim()) || (needsFileInput && !selectedFile);
|
||||
(needsTextInput && !inputValue.trim()) ||
|
||||
(needsFileInput && !selectedFile) ||
|
||||
(needsDaysInput && !daysValid);
|
||||
const isDestructive = action.variant === "destructive";
|
||||
const accent = isDestructive ? "red" : "edr-green";
|
||||
|
||||
@@ -129,6 +147,27 @@ export function BookingConfirmDialog({
|
||||
clearable
|
||||
/>
|
||||
)}
|
||||
{needsDaysInput && (
|
||||
<Stack gap={4}>
|
||||
<NumberInput
|
||||
label={action.inputLabel ?? "Contract validity (days)"}
|
||||
withAsterisk
|
||||
min={1}
|
||||
max={365}
|
||||
clampBehavior="strict"
|
||||
allowDecimal={false}
|
||||
allowNegative={false}
|
||||
placeholder={action.inputPlaceholder ?? "e.g. 30"}
|
||||
value={inputValue === "" ? "" : Number(inputValue)}
|
||||
onChange={(value) => onInputChange(value === "" ? "" : String(value))}
|
||||
/>
|
||||
<Text size="xs" c="dimmed">
|
||||
{daysValid
|
||||
? `Contract valid from today until ${validUntilLabel(daysValue)} (${daysValue} day${daysValue === 1 ? "" : "s"}).`
|
||||
: "Enter a whole number of days between 1 and 365."}
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
{extra}
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -9,6 +9,12 @@ import {
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { useBookingDetail, useBookingMutations } from "@/hooks/bookings/useBookings";
|
||||
|
||||
/** A contract validity window must be a whole number of days, 1–365. */
|
||||
function isValidValidityDays(value: string): boolean {
|
||||
const days = Number(value.trim());
|
||||
return Number.isInteger(days) && days >= 1 && days <= 365;
|
||||
}
|
||||
|
||||
export function useBookingActionDialog(
|
||||
bookingId: string,
|
||||
context: BookingActionContext,
|
||||
@@ -59,9 +65,12 @@ export function useBookingActionDialog(
|
||||
const onSuccess = () => closeDialog();
|
||||
|
||||
switch (pendingAction.id) {
|
||||
case "accept":
|
||||
mutations.staffAccept.mutate(undefined, { onSuccess });
|
||||
case "accept": {
|
||||
const days = Number(inputValue.trim());
|
||||
if (!Number.isInteger(days) || days < 1 || days > 365) return;
|
||||
mutations.staffAccept.mutate(days, { onSuccess });
|
||||
break;
|
||||
}
|
||||
case "requestChanges":
|
||||
mutations.requestChanges.mutate(inputValue.trim(), { onSuccess });
|
||||
break;
|
||||
@@ -116,7 +125,8 @@ export function useBookingActionDialog(
|
||||
!getNextPendingApprovalStep(mergedContext.approvalSteps)) ||
|
||||
(pendingAction?.input === "file" && !selectedFile) ||
|
||||
(pendingAction?.input === "reason" && !inputValue.trim()) ||
|
||||
(pendingAction?.input === "note" && !inputValue.trim());
|
||||
(pendingAction?.input === "note" && !inputValue.trim()) ||
|
||||
(pendingAction?.input === "days" && !isValidValidityDays(inputValue));
|
||||
|
||||
return {
|
||||
actions,
|
||||
|
||||
@@ -36,7 +36,7 @@ export type BookingActionId =
|
||||
| "complete"
|
||||
| "cancel";
|
||||
|
||||
export type BookingActionInputKind = "note" | "reason" | "file";
|
||||
export type BookingActionInputKind = "note" | "reason" | "file" | "days";
|
||||
|
||||
export interface BookingActionDef {
|
||||
id: BookingActionId;
|
||||
@@ -121,10 +121,13 @@ const SUBMITTED_ACTIONS: BookingActionDef[] = [
|
||||
description: "Start the formal approval chain",
|
||||
confirmTitle: "Accept submission?",
|
||||
confirmDescription:
|
||||
"The booking moves to pending approval and approval steps are created from the rule engine.",
|
||||
"Set how long the contract stays valid, then the booking moves to pending approval and approval steps are created from the rule engine.",
|
||||
variant: "default",
|
||||
icon: ShieldCheck,
|
||||
primary: true,
|
||||
input: "days",
|
||||
inputLabel: "Contract validity (days)",
|
||||
inputPlaceholder: "e.g. 30",
|
||||
},
|
||||
{
|
||||
id: "requestChanges",
|
||||
|
||||
@@ -41,7 +41,8 @@ export function useBookingMutations(bookingId: string) {
|
||||
};
|
||||
|
||||
const staffAccept = useMutation({
|
||||
mutationFn: () => api.bookings.staffAccept.call({ id: bookingId }),
|
||||
mutationFn: (validityDays: number) =>
|
||||
api.bookings.staffAccept.call({ id: bookingId, validityDays }),
|
||||
onSuccess: (data) => onSuccess(data, "Booking accepted for approval"),
|
||||
onError: () => toast.error("Failed to accept booking"),
|
||||
});
|
||||
|
||||
@@ -1817,10 +1817,10 @@ export const api = {
|
||||
({ id }) => bookingsService.remove(id),
|
||||
),
|
||||
|
||||
staffAccept: endpoint<{ id: string }, BookingDetail>(
|
||||
staffAccept: endpoint<{ id: string; validityDays: number }, BookingDetail>(
|
||||
"bookings",
|
||||
"staffAccept",
|
||||
({ id }) => bookingsService.staffAccept(id),
|
||||
({ id, validityDays }) => bookingsService.staffAccept(id, validityDays),
|
||||
),
|
||||
|
||||
requestChanges: endpoint<{ id: string; note: string }, BookingDetail>(
|
||||
|
||||
@@ -200,7 +200,8 @@ export const bookingsService = {
|
||||
finalizeClearance: (id: string) =>
|
||||
postBooking<BookingDetail>(`/bookings/${id}/clearance/finalize`),
|
||||
|
||||
staffAccept: (id: string) => postBooking<BookingDetail>(B.STAFF_ACCEPT(id)),
|
||||
staffAccept: (id: string, validityDays: number) =>
|
||||
postBooking<BookingDetail>(B.STAFF_ACCEPT(id), { validityDays }),
|
||||
|
||||
requestChanges: (id: string, note: string) =>
|
||||
postBooking<BookingDetail>(B.STAFF_REQUEST_CHANGES(id), { note }),
|
||||
|
||||
@@ -123,6 +123,10 @@ export interface BookingDetail {
|
||||
adjustedByStaffId?: string | null;
|
||||
adjustedAt?: string | null;
|
||||
adjustmentReason?: string | null;
|
||||
/** Contract validity window set by the backoffice when accepting. */
|
||||
contractValidityDays?: number | null;
|
||||
contractValidFrom?: string | null;
|
||||
contractValidUntil?: string | null;
|
||||
pricingBreakdown?: {
|
||||
currency: string;
|
||||
totalAmount: number;
|
||||
|
||||
Reference in New Issue
Block a user