Files
edr-platform/apps/edr-freight-web/backoffice/src/components/bookings/BookingPricingSummary.tsx

215 lines
6.8 KiB
TypeScript

import { useState } from "react";
import { Banknote, Pencil, Receipt } from "lucide-react";
import {
Button,
Divider,
Group,
NumberInput,
Paper,
Stack,
Text,
Textarea,
} from "@mantine/core";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import toast from "react-hot-toast";
import type { BookingDetail } from "@/types/booking";
import { bookingsService } from "@/services/bookings.service";
import { SectionCard } from "./detail/SectionCard";
import { detailStyles } from "./detail/booking-detail.styles";
export function BookingPricingSummary({ booking }: { booking: BookingDetail }) {
const qc = useQueryClient();
const computed = Number(booking.totalAmount);
const isAdjusted =
booking.adjustedTotalAmount !== null &&
booking.adjustedTotalAmount !== undefined;
const effective = isAdjusted ? Number(booking.adjustedTotalAmount) : computed;
const lineItems = booking.pricingBreakdown?.lineItems ?? [];
const [editing, setEditing] = useState(false);
const [amount, setAmount] = useState<number | "">(effective);
const [reason, setReason] = useState("");
const adjustMutation = useMutation({
mutationFn: (payload: { amount: number | null; reason?: string }) =>
bookingsService.adjustPrice(booking.id, payload.amount, payload.reason),
onSuccess: () => {
toast.success("Price updated");
setEditing(false);
qc.invalidateQueries({ queryKey: ["bookings"] });
},
onError: () => toast.error("Could not update price"),
});
const fmt = (n: number) =>
`${booking.paymentCurrency} ${n.toLocaleString(undefined, { minimumFractionDigits: 2 })}`;
return (
<SectionCard icon={Banknote} title="Pricing & payment">
<Stack gap="md">
<Paper radius="md" withBorder p="md" style={detailStyles.highlightCard}>
<Group justify="space-between" align="flex-start">
<div>
<Text size="xs" c="dimmed" fw={500} tt="uppercase" lts="0.04em">
{isAdjusted ? "Adjusted total" : "Total amount"}
</Text>
<Text
size="xl"
fw={700}
c="edr-green.9"
mt={4}
style={{ fontVariantNumeric: "tabular-nums", letterSpacing: "-0.5px" }}
>
{fmt(effective)}
</Text>
{isAdjusted && (
<Text size="xs" c="dimmed" mt={2}>
Computed: {fmt(computed)}
{booking.adjustmentReason ? ` · ${booking.adjustmentReason}` : ""}
</Text>
)}
</div>
{!editing && (
<Button
size="compact-xs"
variant="light"
leftSection={<Pencil size={13} />}
onClick={() => {
setAmount(effective);
setEditing(true);
}}
>
Adjust
</Button>
)}
</Group>
{editing && (
<Stack gap="xs" mt="md">
<NumberInput
label="New total"
value={amount}
onChange={(v) => setAmount(v === "" ? "" : Number(v))}
min={0}
radius="md"
prefix={`${booking.paymentCurrency} `}
thousandSeparator=","
/>
<Textarea
label="Reason (optional)"
value={reason}
onChange={(e) => setReason(e.currentTarget.value)}
autosize
minRows={2}
radius="md"
/>
<Group justify="space-between" mt={4}>
{isAdjusted ? (
<Button
size="compact-sm"
variant="subtle"
color="red"
loading={adjustMutation.isPending}
onClick={() =>
adjustMutation.mutate({ amount: null })
}
>
Clear adjustment
</Button>
) : (
<span />
)}
<Group gap="xs">
<Button
size="compact-sm"
variant="default"
onClick={() => setEditing(false)}
>
Cancel
</Button>
<Button
size="compact-sm"
color="edr-green"
loading={adjustMutation.isPending}
disabled={amount === ""}
onClick={() =>
adjustMutation.mutate({
amount: Number(amount),
reason: reason.trim() || undefined,
})
}
>
Save
</Button>
</Group>
</Group>
</Stack>
)}
</Paper>
<Row label="Payment status" value={booking.paymentStatus} />
{booking.pnrCode && <Row label="PNR code" value={booking.pnrCode} mono />}
{lineItems.length > 0 && (
<>
<Divider color="var(--mantine-color-gray-2)" />
<Group gap={6}>
<Receipt size={13} color="var(--mantine-color-gray-5)" />
<Text size="xs" c="dimmed" fw={500} tt="uppercase" lts="0.04em">
Price breakdown
</Text>
</Group>
<Stack gap="xs">
{lineItems.map((li, i) => (
<Group
key={`${li.code}-${i}`}
justify="space-between"
px="sm"
py={6}
style={{
borderRadius: 8,
border: "1px solid var(--mantine-color-gray-2)",
background: "var(--mantine-color-gray-0)",
}}
>
<Text size="sm" c="dimmed">
{li.description}
</Text>
<Text size="sm" fw={600} style={{ fontVariantNumeric: "tabular-nums" }}>
{Number(li.amount).toLocaleString()} {li.currency}
</Text>
</Group>
))}
</Stack>
</>
)}
</Stack>
</SectionCard>
);
}
function Row({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
return (
<Group
justify="space-between"
px="sm"
py="xs"
style={{
borderRadius: 8,
border: "1px solid var(--mantine-color-gray-2)",
background: "var(--mantine-color-gray-0)",
}}
>
<Text size="sm" c="dimmed">
{label}
</Text>
<Text size="sm" fw={600} ff={mono ? "monospace" : undefined}>
{value}
</Text>
</Group>
);
}