fix: types isses

This commit is contained in:
Nathnael
2026-07-02 11:09:25 +00:00
parent 79490d5979
commit 03740ee719
12 changed files with 185 additions and 676 deletions

View File

@@ -101,6 +101,13 @@ export class CreateBulkLineDto {
@Min(0)
@Transform(({ value }) => Number(value))
hazardousQuantity?: number;
@ApiPropertyOptional({ minimum: 0 })
@IsOptional()
@IsInt()
@Min(0)
@Transform(({ value }) => Number(value))
reeferQuantity?: number;
}
/** Shipment booking created under a contract (Path A customer, Path B GL ET). */

View File

@@ -1,6 +1,6 @@
import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useMutation, useQueries } from "@tanstack/react-query";
import { useMutation, useQueries, useQuery } from "@tanstack/react-query";
import { Badge, Box, Button, Group, Modal, Stack, Text } from "@mantine/core";
import {
AlertTriangle,
@@ -15,6 +15,8 @@ import type { Freight } from "@edr/types";
import { api } from "@/services/api";
import { ModalSafeWrapper } from "@/components/customer-actions/ModalSafeWrapper";
import { paymentsService, type PaymentMethod } from "@/services/payments.service";
import { invoicesService } from "@/services/invoices.service";
import { isPayable } from "@/pages/billing/invoice-ui";
import { ContractClearancePanel } from "@/pages/contracts/ContractClearancePanel";
import { PaymentMethodModal } from "@/pages/bookings/BookingDetailPage/components/PaymentMethodModal";
import { Card } from "./Card";
@@ -118,17 +120,37 @@ export function ActionNeededSection({
[clearanceItems, baseItems],
).sort((a, b) => Number(b.urgent ?? 0) - Number(a.urgent ?? 0));
// Mirror ReadonlyBookingView: POST /payments/initiate returns the provider's
// redirect (clientAction.url); fall back to the public checkout page.
// Billing is invoice-centric — resolve the booking's currently payable
// invoice before paying it (mirrors ReadonlyBookingView).
const { data: payItemInvoices = [] } = useQuery({
queryKey: ["booking-invoices", payItem?.targetId],
queryFn: () => invoicesService.listForSource("booking", payItem!.targetId),
enabled: payItem !== null,
});
const payableInvoiceId = payItemInvoices.find((inv) =>
isPayable(inv.status),
)?.id;
const payMutation = useMutation({
mutationFn: (method: PaymentMethod) =>
api.payments.initiate.call({ bookingId: payItem!.targetId, method }),
mutationFn: (method: PaymentMethod) => {
if (!payableInvoiceId) {
throw new Error(
"No payable invoice found for this booking yet. Please refresh or contact support.",
);
}
return api.invoices.pay.call({
id: payableInvoiceId,
payload: { method, platform: "web" },
});
},
onSuccess: (data, method) => {
const bookingId = payItem!.targetId;
const url =
data?.clientAction?.type === "REDIRECT" && data.clientAction.url
? data.clientAction.url
: paymentsService.checkoutUrl({ bookingId, method });
: paymentsService.checkoutUrlForInvoice({
invoiceId: payableInvoiceId!,
method,
});
window.location.href = url;
},
});

View File

@@ -518,9 +518,12 @@ export const INVOICE_BADGE: Record<
{ label: string; bg: string; text: string }
> = {
[Freight.InvoiceStatus.Draft]: { label: "Draft", bg: "edr-slate-soft", text: "edr-slate" },
[Freight.InvoiceStatus.Issued]: { label: "Due soon", bg: "edr-amber-soft", text: "edr-amber-text" },
[Freight.InvoiceStatus.Pending]: { label: "Due soon", bg: "edr-amber-soft", text: "edr-amber-text" },
[Freight.InvoiceStatus.PartiallyPaid]: { label: "Partially paid", bg: "edr-amber-soft", text: "edr-amber-text" },
[Freight.InvoiceStatus.Paid]: { label: "Paid", bg: "edr-soft", text: "edr-green.7" },
[Freight.InvoiceStatus.Overdue]: { label: "Overdue", bg: "edr-red-soft", text: "edr-red" },
[Freight.InvoiceStatus.Cancelled]: { label: "Cancelled", bg: "edr-slate-soft", text: "edr-slate" },
[Freight.InvoiceStatus.Refunded]: { label: "Refunded", bg: "edr-blue-soft", text: "edr-blue" },
[Freight.InvoiceStatus.Expired]: { label: "Expired", bg: "edr-red-soft", text: "edr-red" },
};

View File

@@ -5,7 +5,9 @@ import type { PortalInvoice } from "@/services/invoices.service";
/** Statuses a customer can still pay. */
export const PAYABLE_STATUSES: Freight.InvoiceStatus[] = [
Freight.InvoiceStatus.Issued,
Freight.InvoiceStatus.Pending,
Freight.InvoiceStatus.PartiallyPaid,
Freight.InvoiceStatus.Overdue,
];
@@ -17,7 +19,9 @@ const STATUS_STYLE: Record<
{ label: string; bg: string; fg: string }
> = {
[Freight.InvoiceStatus.Draft]: { label: "Draft", bg: "#EEF2F6", fg: "#64748B" },
[Freight.InvoiceStatus.Issued]: { label: "Due", bg: "#FEF3E2", fg: "#B45309" },
[Freight.InvoiceStatus.Pending]: { label: "Due", bg: "#FEF3E2", fg: "#B45309" },
[Freight.InvoiceStatus.PartiallyPaid]: { label: "Partially paid", bg: "#FEF9E7", fg: "#A16207" },
[Freight.InvoiceStatus.Paid]: { label: "Paid", bg: "#E6F7EF", fg: "#0A6F4D" },
[Freight.InvoiceStatus.Overdue]: { label: "Overdue", bg: "#FDECEC", fg: "#C0392B" },
[Freight.InvoiceStatus.Cancelled]: { label: "Cancelled", bg: "#EEF2F6", fg: "#64748B" },

View File

@@ -20,7 +20,7 @@ export function BookingClearanceWorkflowBanner({
}) {
const isPhased =
booking.customsClearingEnabled &&
booking.contractKind === "GENERAL";
booking.bookingType === "GENERAL_CONTRACT";
const { view, viewer } = useFileViewer();

View File

@@ -36,6 +36,7 @@ export function BodyGrid({ left, right }: { left: ReactNode; right: ReactNode })
interface SectionCardProps extends PaperProps {
children: ReactNode;
ref?: Ref<HTMLDivElement>;
id?: string;
}
export function SectionCard({ children, ref, style, ...props }: SectionCardProps) {

View File

@@ -1,4 +1,4 @@
import { useMutation } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { api } from "@/services/api";
@@ -6,24 +6,45 @@ import {
paymentsService,
type PaymentMethod,
} from "@/services/payments.service";
import { invoicesService } from "@/services/invoices.service";
import { isPayable } from "@/pages/billing/invoice-ui";
/**
* Shared payment flow for a single booking: opens the method modal, fires
* POST /payments/initiate, and redirects the browser to the provider (or the
* fallback checkout page). Reused by the booking detail page, the booking list,
* and the home page so "Pay now" behaves identically everywhere.
* POST /billing/my-invoices/:id/pay for the booking's currently payable
* invoice, and redirects the browser to the provider (or the fallback
* checkout page). Reused by the booking detail page, the booking list, and
* the home page so "Pay now" behaves identically everywhere.
*/
export function useBookingPayment(bookingId: string) {
const [modalOpen, setModalOpen] = useState(false);
const { data: invoices = [] } = useQuery({
queryKey: ["booking-invoices", bookingId],
queryFn: () => invoicesService.listForSource("booking", bookingId),
});
const payableInvoiceId = invoices.find((inv) => isPayable(inv.status))?.id;
const mutation = useMutation({
mutationFn: (method: PaymentMethod) =>
api.payments.initiate.call({ bookingId, method }),
mutationFn: (method: PaymentMethod) => {
if (!payableInvoiceId) {
throw new Error(
"No payable invoice found for this booking yet. Please refresh or contact support.",
);
}
return api.invoices.pay.call({
id: payableInvoiceId,
payload: { method, platform: "web" },
});
},
onSuccess: (data, method) => {
const redirectUrl =
data?.clientAction?.type === "REDIRECT" && data.clientAction.url
? data.clientAction.url
: paymentsService.checkoutUrl({ bookingId, method });
: paymentsService.checkoutUrlForInvoice({
invoiceId: payableInvoiceId!,
method,
});
window.location.href = redirectUrl;
},
});

View File

@@ -17,11 +17,9 @@ import {
Clock,
Download,
Eye,
FileText,
Upload,
} from "lucide-react";
import type { Freight } from "@edr/types";
import { isViewable } from "@edr/ui-common";
import { api } from "@/services/api";
import { fileViewUrl } from "@/constants/apiConfig";

View File

@@ -13,19 +13,19 @@ import {
Textarea,
Title,
} from "@mantine/core";
import { ArrowLeft, Send } from "lucide-react";
import { ArrowLeft, CalendarDays, Send } from "lucide-react";
import toast from "react-hot-toast";
import type { Freight } from "@edr/types";
import { DatePickerInput } from "@mantine/dates";
import { contractsService } from "@/services/contracts.service";
import { OperationDatePicker } from "@edr/ui-common";
const BORDER = "#E6ECF2";
export default function NewShipmentRequestPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [scheduledDate, setScheduledDate] = useState<Date | null>(null);
const [scheduledDate, setScheduledDate] = useState("");
const [quantity, setQuantity] = useState<number | string>(1);
const [notes, setNotes] = useState("");
@@ -65,7 +65,7 @@ export default function NewShipmentRequestPage() {
const handleSubmit = () => {
const dto: Freight.CreateBookingRequestDto = {
contractRouteId: route?.id,
scheduledDate: scheduledDate?.toISOString(),
scheduledDate: scheduledDate || undefined,
notes: notes.trim() || undefined,
};
@@ -104,10 +104,15 @@ export default function NewShipmentRequestPage() {
<Paper withBorder radius="lg" p="xl" style={{ borderColor: BORDER }}>
<Stack gap="md">
<OperationDatePicker
<DatePickerInput
label="Preferred shipment date"
value={scheduledDate}
onChange={setScheduledDate}
placeholder="Pick a date"
leftSection={<CalendarDays size={16} />}
minDate={new Date().toISOString().slice(0, 10)}
value={scheduledDate || null}
onChange={(v) => setScheduledDate(v ?? "")}
radius="md"
popoverProps={{ withinPortal: true }}
/>
<NumberInput

View File

@@ -595,6 +595,7 @@ export interface CreateBulkLineDto {
cargoWeightTons?: number;
itemCount?: number;
hazardousQuantity?: number;
reeferQuantity?: number;
}
/** Shipment booking created under a contract (Path A customer, Path B GL ET). */

View File

@@ -586,6 +586,10 @@ export interface IInvoice extends BaseEntity {
companyProfileId: string;
companyProfile?: IInvoiceCompanyProfile;
totalAmount: number;
/** Cumulative amount settled so far (supports partial payment). */
paidAmount: number;
/** Outstanding balance = totalAmount - paidAmount (0 once fully paid). */
balanceAmount: number;
currency: string;
status: InvoiceStatus;
/** Originating subsystem: booking / warehouse / demurrage. */

747
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff