feat: add toggle to DJF

This commit is contained in:
ghost2023
2026-09-06 16:35:14 +03:00
parent aa96bb4086
commit eab36a2526
21 changed files with 337 additions and 41 deletions

View File

@@ -65,6 +65,7 @@ import {
type ConsolidationCandidate,
} from "@/services/contracts.service";
import { bookingsService } from "@/services/bookings.service";
import { usePaymentCurrenciesQuery } from "@/hooks/useManualPaymentSettings";
import {
useContractCapacity,
useContractDetail,
@@ -346,6 +347,9 @@ export default function GlCreateBookingForm() {
// IMPORT bookings pick ETB or USD — starts empty so the choice is
// deliberate (required before pricing). Everything else is forced to ETB.
const [paymentCurrency, setPaymentCurrency] = useState<"USD" | "ETB" | "DJF" | "">("");
const { data: offeredCurrencies } = usePaymentCurrenciesQuery();
// Undefined while the read is in flight — assume on, the switch is rarely off.
const djfOffered = offeredCurrencies ? offeredCurrencies.includes("DJF") : true;
// What the containers carry — captured per booking (moved off the contract).
const [cargoDescription, setCargoDescription] = useState("");
const [containerLines, setContainerLines] = useState<ContainerLineDraft[]>([]);
@@ -2359,7 +2363,9 @@ export default function GlCreateBookingForm() {
onChange={setPaymentCurrency}
disabled={!isImport || requestCurrencyLocked}
allowUsd={isImport}
allowDjf={isImport}
// A currency switched off in Configuration → Payments is not
// offered, but one the customer already chose stays visible.
allowDjf={isImport && (djfOffered || paymentCurrency === "DJF")}
error={currencyError}
/>
</Box>

View File

@@ -612,7 +612,7 @@ export const buildSidebarSections = (
permission: FREIGHT_PERMS.settings.operationsStandards.view,
},
{
label: "Manual payments",
label: "Payments",
href: "/dashboard/configuration/manual-payments",
permission: FREIGHT_PERMS.settings.manualPayment.view,
},

View File

@@ -63,6 +63,7 @@ export const URL_CONSTANTS = {
MANUAL_PAYMENT_SETTINGS: {
BASE: "/payment-settings/manual",
CURRENCIES: "/payment-settings/manual/currencies",
},
AUDIT_LOGS: {

View File

@@ -9,6 +9,18 @@ import {
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
export const MANUAL_PAYMENT_SETTINGS_KEY = ["manualPaymentSettings"];
export const PAYMENT_CURRENCIES_KEY = ["paymentCurrencies"];
/**
* Currencies a booking may be billed in right now. Open read, so forms can
* hide a switched-off currency without needing the settings permission.
*/
export const usePaymentCurrenciesQuery = () =>
useQuery({
queryKey: PAYMENT_CURRENCIES_KEY,
queryFn: () => manualPaymentSettingsService.currencies(),
staleTime: 60_000,
});
export const useManualPaymentSettingsQuery = () =>
useQuery({
@@ -25,13 +37,18 @@ export const useUpdateManualPaymentSettings = () => {
return useMutation({
mutationFn: (
patch: Partial<
Pick<ManualPaymentSettings, "etbEnabled" | "usdEnabled" | "djfEnabled">
Pick<
ManualPaymentSettings,
"etbEnabled" | "usdEnabled" | "djfEnabled" | "djfPaymentsEnabled"
>
>,
) => manualPaymentSettingsService.update(patch),
onSuccess: (data) => {
queryClient.setQueryData(MANUAL_PAYMENT_SETTINGS_KEY, data);
// The Manual Payments worklist only lists enabled currencies.
// The Manual Payments worklist only lists enabled currencies, and the
// booking forms only offer currencies that are switched on.
queryClient.invalidateQueries({ queryKey: ["invoices"] });
queryClient.invalidateQueries({ queryKey: PAYMENT_CURRENCIES_KEY });
toast.success(
t("manualPaymentSettings.updated", "Manual payment settings updated"),
);

View File

@@ -291,8 +291,8 @@ export default function UsdPaymentsPanel({
const { user } = useAuth();
const canConfirm = hasPermission(user, FREIGHT_PERMS.invoices.confirmOffline);
// Manual settlement is switched on per currency in Configuration → Manual
// payments. FinanceHubPage hides the tab for a disabled currency; this is
// Manual settlement is switched on per currency in Configuration →
// Payments. FinanceHubPage hides the tab for a disabled currency; this is
// the fallback for a direct `?tab=` link, and the API refuses regardless.
const { data: manualSettings } = useManualPaymentSettingsQuery();
const currencyEnabled = manualSettings
@@ -549,7 +549,7 @@ export default function UsdPaymentsPanel({
onRowClick={(row) => navigate(`/dashboard/invoices/${row.id}`)}
emptyMessage={
!currencyEnabled
? `Manual payment is switched off for ${currency} invoices. Enable it in Configuration → Manual payments.`
? `Manual payment is switched off for ${currency} invoices. Enable it in Configuration → Payments.`
: controls.activeCount > 0
? "No invoices match these filters."
: `No ${currency} invoices awaiting manual payment confirmation.`

View File

@@ -8,7 +8,7 @@ import {
import { Badge } from "@/shared/common/ui/badge";
import { Switch } from "@/shared/common/ui/switch";
import { Skeleton } from "@/shared/common/ui/skeleton";
import { AlertTriangle, Banknote, Landmark } from "lucide-react";
import { AlertTriangle, Banknote, Coins, Landmark } from "lucide-react";
import { useAuth } from "@/auth/useAuth";
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
@@ -53,7 +53,8 @@ const CURRENCIES: {
];
/**
* Switches the manual (offline) payment channel on or off per currency.
* Two levels of switch: whether DJF is accepted as a payment currency at all,
* and whether the manual (offline) channel is open, per currency.
*
* Off means gone, not greyed out: the Manual Payments worklist lists only
* enabled currencies, and the API refuses a confirmation in a disabled one —
@@ -75,11 +76,12 @@ export default function ManualPaymentSettingsCard() {
return (
<Card className="shadow-lg border-gray-200 dark:border-gray-700">
<CardHeader>
<CardTitle>Manual payments</CardTitle>
<CardTitle>Payments</CardTitle>
<CardDescription>
Whether Finance staff may mark invoices as paid by hand, from
Invoices Manual Payments. Each currency is switched separately.
Confirming still requires the payment slip and the booking&apos;s pay
Which currencies customers may be billed in, and whether Finance
staff may mark invoices as paid by hand from Invoices Manual
Payments. Each currency is switched separately. Confirming a manual
payment still requires the payment slip and the booking&apos;s pay
window to be open.
</CardDescription>
</CardHeader>
@@ -95,6 +97,38 @@ export default function ManualPaymentSettingsCard() {
</div>
)}
{isLoading || !data ? (
<Skeleton className="h-[86px] w-full rounded-md" />
) : (
<div className="flex items-start justify-between gap-4 rounded-md border p-4 dark:border-gray-700">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Coins className="h-4 w-4 text-muted-foreground" />
<p className="font-medium">Accept Djibouti Franc (DJF)</p>
<Badge variant={data.djfPaymentsEnabled ? "default" : "secondary"}>
{data.djfPaymentsEnabled ? "Enabled" : "Disabled"}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
Whether DJF is offered as a billing currency on new bookings and
shipment requests, and whether DJF invoices can be paid online
(Waafi / CAC Bank). Switching this off stops new DJF business
invoices already in DJF stay settleable by hand below.
</p>
</div>
<Switch
checked={data.djfPaymentsEnabled}
disabled={!canManage || update.isPending}
aria-label="Accept DJF as a payment currency"
onCheckedChange={(checked) =>
update.mutate({ djfPaymentsEnabled: checked })
}
/>
</div>
)}
<p className="pt-2 text-sm font-medium">Manual (offline) settlement</p>
{isLoading || !data
? CURRENCIES.map((c) => (
<Skeleton key={c.code} className="h-[86px] w-full rounded-md" />

View File

@@ -14,6 +14,12 @@ export interface ManualPaymentSettings {
etbEnabled: boolean;
usdEnabled: boolean;
djfEnabled: boolean;
/**
* Wider than `djfEnabled`: whether DJF is accepted as a payment currency at
* all — offered on the booking forms and payable online. Off leaves existing
* DJF invoices settleable by hand.
*/
djfPaymentsEnabled: boolean;
updatedById: string | null;
updatedAt?: string;
}
@@ -24,10 +30,21 @@ export const manualPaymentSettingsService = {
return unwrap(response.data);
},
/** Currencies customers may be billed and pay in — open read, no permission. */
currencies: async (): Promise<string[]> => {
const response = await client.get<ApiResponse<string[]>>(
URL_CONSTANTS.MANUAL_PAYMENT_SETTINGS.CURRENCIES,
);
return unwrap(response.data);
},
/** Partial: an omitted currency keeps its current setting. */
update: async (
patch: Partial<
Pick<ManualPaymentSettings, "etbEnabled" | "usdEnabled" | "djfEnabled">
Pick<
ManualPaymentSettings,
"etbEnabled" | "usdEnabled" | "djfEnabled" | "djfPaymentsEnabled"
>
>,
): Promise<ManualPaymentSettings> => {
const response = await client.patch<ApiResponse<ManualPaymentSettings>>(