mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
215 lines
6.8 KiB
TypeScript
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>
|
|
);
|
|
}
|