always-on calendar, currency cards

This commit is contained in:
Marshal
2026-08-05 07:03:39 +00:00
parent 5d68a3f7b7
commit 2f576fd1de
6 changed files with 229 additions and 115 deletions

View File

@@ -20,7 +20,6 @@ import {
Loader,
Modal,
Paper,
SegmentedControl,
Select,
Stack,
Switch,
@@ -49,7 +48,11 @@ import {
X,
} from "lucide-react";
import type { Freight } from "@edr/types";
import { ExportTrainPicker, OperationDatePicker } from "@edr/ui-common";
import {
CurrencySelector,
ExportTrainPicker,
OperationDatePicker,
} from "@edr/ui-common";
import { api } from "@/services/api";
import { PageContainer } from "@/components/page";
@@ -1713,24 +1716,20 @@ export default function GlCreateBookingForm() {
title="Schedule"
description="Pick the binding shipment day. Only days with an open train that has enough matching wagons for the cargo can be selected."
/>
{cargoQuery === null ? (
<Alert
color="yellow"
variant="light"
radius="md"
icon={<AlertCircle size={16} />}
>
Enter your cargo details first available shipment days depend
on the wagons your cargo needs.
</Alert>
) : (
<Box>
<StepLabel>Shipment day *</StepLabel>
<Box mt={10} w="100%">
{/* Calendar stays visible before cargo is entered — all days
disabled with a hint, since availability depends on cargo. */}
<OperationDatePicker
fullWidth
availableDays={availableDays ?? []}
isLoading={daysLoading}
availableDays={cargoQuery === null ? [] : (availableDays ?? [])}
isLoading={cargoQuery !== null && daysLoading}
emptyMessage={
cargoQuery === null
? "Enter the cargo details first — available shipment days depend on the wagons the cargo needs."
: undefined
}
value={scheduledDate}
onChange={(d) => {
setScheduledDate(d);
@@ -1753,7 +1752,6 @@ export default function GlCreateBookingForm() {
/>
) : null}
</Box>
)}
</StepCard>
)}
@@ -1769,16 +1767,10 @@ export default function GlCreateBookingForm() {
? "Requested by the customer on their shipment request."
: "The contract is quoted in USD — pick the currency this shipment is invoiced in."}
</Text>
<SegmentedControl
<CurrencySelector
value={isIntercity ? "ETB" : paymentCurrency}
onChange={(v) => setPaymentCurrency(v as "USD" | "ETB")}
onChange={setPaymentCurrency}
disabled={isIntercity}
data={[
{ label: "USD", value: "USD" },
{ label: "ETB", value: "ETB" },
]}
color="edr-green"
radius={10}
/>
</Box>

View File

@@ -22,7 +22,6 @@ import {
Loader,
Modal,
Paper,
SegmentedControl,
Stack,
Switch,
Text,
@@ -50,7 +49,11 @@ import {
} from "lucide-react";
import type { Freight } from "@edr/types";
import { ExportTrainPicker, OperationDatePicker } from "@edr/ui-common";
import {
CurrencySelector,
ExportTrainPicker,
OperationDatePicker,
} from "@edr/ui-common";
import { api } from "@/services/api";
import {
contractsService,
@@ -1144,39 +1147,14 @@ function ScheduleStep({
Your contract is quoted in USD. Pick the currency this shipment is
invoiced in the total is converted for you.
</Text>
{/* Rendered unselected until the customer chooses: SegmentedControl
highlights whatever value it is given, so passing a fallback
here would look like a made choice. */}
<SegmentedControl
<CurrencySelector
value={field.value || ""}
onChange={(v) => field.onChange(v)}
data={[
{ label: "Select…", value: "", disabled: true },
{ label: "USD", value: "USD" },
{ label: "ETB", value: "ETB" },
]}
color="edr-green"
radius={10}
error={fieldState.error?.message}
/>
{fieldState.error && (
<Text fz={12.5} c="red.6" mt={6}>
{fieldState.error.message}
</Text>
)}
</Box>
)}
/>
{cargoQuery === null ? (
<Alert
color="yellow"
variant="light"
radius="md"
icon={<AlertCircle size={16} />}
>
Enter your cargo details first available shipment days depend on the
wagons your cargo needs.
</Alert>
) : (
<Controller
name="scheduledDate"
control={form.control}
@@ -1184,10 +1162,17 @@ function ScheduleStep({
<Box>
<StepLabel>Shipment day *</StepLabel>
<Box mt={10} w="100%">
{/* Calendar stays visible before cargo is entered — all days
disabled with a hint, since availability depends on cargo. */}
<OperationDatePicker
fullWidth
availableDays={availableDays ?? []}
isLoading={isLoading}
availableDays={cargoQuery === null ? [] : (availableDays ?? [])}
isLoading={cargoQuery !== null && isLoading}
emptyMessage={
cargoQuery === null
? "Enter your cargo details first — available shipment days depend on the wagons your cargo needs."
: undefined
}
value={field.value ?? ""}
onChange={(d) => {
field.onChange(d);
@@ -1212,7 +1197,6 @@ function ScheduleStep({
</Box>
)}
/>
)}
</StepCard>
);
}

View File

@@ -0,0 +1,131 @@
import { Box, Text } from "@mantine/core";
import { Check } from "lucide-react";
export interface CurrencySelectorProps {
/** Selected currency code, or "" when none picked yet. */
value: string;
onChange: (currency: "USD" | "ETB") => void;
disabled?: boolean;
/** Validation error shown under the cards. */
error?: string;
}
const OPTIONS = [
{
code: "USD",
symbol: "$",
name: "US Dollar",
hint: "As quoted on the contract",
},
{
code: "ETB",
symbol: "Br",
name: "Ethiopian Birr",
hint: "Converted from the USD total",
},
] as const;
/**
* Card-style USD/ETB billing-currency picker. Renders unselected when `value`
* is "" so a required choice never looks pre-made.
*/
export function CurrencySelector({
value,
onChange,
disabled = false,
error,
}: CurrencySelectorProps) {
return (
<Box>
<Box
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
gap: 10,
}}
>
{OPTIONS.map((o) => {
const selected = value === o.code;
return (
<button
key={o.code}
type="button"
disabled={disabled}
aria-pressed={selected}
onClick={() => onChange(o.code)}
style={{
display: "flex",
alignItems: "center",
gap: 12,
textAlign: "left",
padding: "12px 14px",
borderRadius: 12,
border: selected
? "1.5px solid #12B981"
: error
? "1px solid #FCA5A5"
: "1px solid #E6ECF2",
background: selected ? "#F6FBF8" : "#fff",
cursor: disabled ? "default" : "pointer",
opacity: disabled && !selected ? 0.55 : 1,
transition: "border-color 120ms ease, background 120ms ease",
}}
>
<Box
style={{
width: 38,
height: 38,
flexShrink: 0,
borderRadius: 11,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 15,
fontWeight: 800,
background: selected ? "#ECF6F1" : "#F1F4F7",
color: selected ? "#0A6F4D" : "#6B7C8E",
}}
>
{o.symbol}
</Box>
<Box style={{ flex: 1, minWidth: 0 }}>
<Text fz={14} fw={700} c="#10202F" lh={1.25}>
{o.code}
<Text component="span" fz={12.5} fw={500} c="dimmed">
{" "}
· {o.name}
</Text>
</Text>
<Text fz={11.5} c="dimmed" lh={1.35}>
{o.hint}
</Text>
</Box>
<Box
style={{
width: 20,
height: 20,
flexShrink: 0,
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
border: selected ? "none" : "1.5px solid #D4DDE5",
background: selected ? "#12B981" : "transparent",
}}
>
{selected && <Check size={12} color="#fff" strokeWidth={3.5} />}
</Box>
</button>
);
})}
</Box>
{error && (
<Text fz={12.5} c="red.6" mt={6}>
{error}
</Text>
)}
</Box>
);
}
export default CurrencySelector;

View File

@@ -0,0 +1,2 @@
export { CurrencySelector, default } from "./CurrencySelector";
export type { CurrencySelectorProps } from "./CurrencySelector";

View File

@@ -18,6 +18,8 @@ export interface OperationDatePickerProps {
onChange: (date: string) => void;
/** Stretch to the full width of the parent container. */
fullWidth?: boolean;
/** Text shown under the grid when no days are available. */
emptyMessage?: string;
}
/** `yyyy-MM-dd` for a local date. */
@@ -58,6 +60,7 @@ export function OperationDatePicker({
value,
onChange,
fullWidth = false,
emptyMessage = "No scheduled departures found for this route yet.",
}: OperationDatePickerProps) {
const [month, setMonth] = useState(() => {
const now = new Date();
@@ -270,7 +273,7 @@ export function OperationDatePicker({
)}
{departureDays.size === 0 && (
<Text fz="12px" c="orange.7" mt="sm" ta="center">
No scheduled departures found for this route yet.
{emptyMessage}
</Text>
)}
</>

View File

@@ -26,6 +26,8 @@ export { OperationDatePicker } from "./components/OperationDatePicker";
export type { OperationDatePickerProps } from "./components/OperationDatePicker";
export { ExportTrainPicker } from "./components/ExportTrainPicker";
export type { ExportTrainPickerProps } from "./components/ExportTrainPicker";
export { CurrencySelector } from "./components/CurrencySelector";
export type { CurrencySelectorProps } from "./components/CurrencySelector";
export { CountdownTimer } from "./components/CountdownTimer";
export type { CountdownTimerProps } from "./components/CountdownTimer";