mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fix: types isses
This commit is contained in:
@@ -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). */
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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" },
|
||||
};
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -20,7 +20,7 @@ export function BookingClearanceWorkflowBanner({
|
||||
}) {
|
||||
const isPhased =
|
||||
booking.customsClearingEnabled &&
|
||||
booking.contractKind === "GENERAL";
|
||||
booking.bookingType === "GENERAL_CONTRACT";
|
||||
|
||||
const { view, viewer } = useFileViewer();
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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
747
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user