"use client"; export const dynamic = "force-dynamic"; import { useEffect, useState, useCallback } from "react"; import { Permission } from "../../auth-context"; import { adminApi } from "../../api"; import { PageLayout } from "../../components/PageLayout"; import { ConfirmationButton } from "../../components/ConfirmationButton"; import { useRequireAuth } from "../../hooks/useRequireAuth"; import { useMutation } from "../../hooks/useMutation"; import { useTranslation } from "../../hooks/useTranslation"; import { components } from "../../generated/api"; import { cardStyles, formStyles, buttonStyles, bannerStyles } from "../../styles/shared"; type PricingConfigUpdate = components["schemas"]["PricingConfigUpdate"]; interface FormData { premium_buy: number; premium_sell: number; small_trade_threshold_eur: number; small_trade_extra_premium: number; eur_min_buy: number; eur_max_buy: number; eur_min_sell: number; eur_max_sell: number; } interface ValidationErrors { premium_buy?: string; premium_sell?: string; small_trade_threshold_eur?: string; small_trade_extra_premium?: string; eur_min_buy?: string; eur_max_buy?: string; eur_min_sell?: string; eur_max_sell?: string; } export default function AdminPricingPage() { const t = useTranslation("admin"); const tCommon = useTranslation("common"); const { user, isLoading, isAuthorized } = useRequireAuth({ requiredPermission: Permission.MANAGE_PRICING, fallbackRedirect: "/", }); const [formData, setFormData] = useState(null); const [errors, setErrors] = useState({}); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const [isConfirming, setIsConfirming] = useState(false); const fetchConfig = useCallback(async () => { setError(null); try { const data = await adminApi.getPricingConfig(); setFormData({ premium_buy: data.premium_buy, premium_sell: data.premium_sell, small_trade_threshold_eur: data.small_trade_threshold_eur, small_trade_extra_premium: data.small_trade_extra_premium, eur_min_buy: data.eur_min_buy, eur_max_buy: data.eur_max_buy, eur_min_sell: data.eur_min_sell, eur_max_sell: data.eur_max_sell, }); } catch (err) { setError(err instanceof Error ? err.message : t("pricing.errors.loadFailed")); } }, [t]); useEffect(() => { if (user && isAuthorized) { fetchConfig(); } }, [user, isAuthorized, fetchConfig]); const validateField = (field: keyof FormData, value: number): string | undefined => { switch (field) { case "premium_buy": case "premium_sell": case "small_trade_extra_premium": if (value < -100 || value > 100) { return t("pricing.validation.premiumRange"); } break; case "eur_min_buy": case "eur_min_sell": case "eur_max_buy": case "eur_max_sell": case "small_trade_threshold_eur": if (value <= 0) { return t("pricing.validation.positive"); } break; } return undefined; }; const validateForm = (data: FormData): ValidationErrors => { const newErrors: ValidationErrors = {}; // Validate individual fields for (const [key, value] of Object.entries(data)) { const error = validateField(key as keyof FormData, value); if (error) { newErrors[key as keyof ValidationErrors] = error; } } // Validate min < max if (data.eur_min_buy >= data.eur_max_buy) { newErrors.eur_min_buy = t("pricing.validation.minMaxBuy"); newErrors.eur_max_buy = t("pricing.validation.minMaxBuy"); } if (data.eur_min_sell >= data.eur_max_sell) { newErrors.eur_min_sell = t("pricing.validation.minMaxSell"); newErrors.eur_max_sell = t("pricing.validation.minMaxSell"); } return newErrors; }; const handleFieldChange = (field: keyof FormData, value: string) => { if (!formData) return; const numValue = parseInt(value, 10); if (isNaN(numValue)) return; const newData = { ...formData, [field]: numValue }; setFormData(newData); // Clear error for this field const newErrors = { ...errors }; delete newErrors[field]; setErrors(newErrors); // Validate field const error = validateField(field, numValue); if (error) { newErrors[field] = error; } // Validate min < max if relevant if (field === "eur_min_buy" || field === "eur_max_buy") { if (newData.eur_min_buy >= newData.eur_max_buy) { newErrors.eur_min_buy = t("pricing.validation.minMaxBuy"); newErrors.eur_max_buy = t("pricing.validation.minMaxBuy"); } else { delete newErrors.eur_min_buy; delete newErrors.eur_max_buy; } } if (field === "eur_min_sell" || field === "eur_max_sell") { if (newData.eur_min_sell >= newData.eur_max_sell) { newErrors.eur_min_sell = t("pricing.validation.minMaxSell"); newErrors.eur_max_sell = t("pricing.validation.minMaxSell"); } else { delete newErrors.eur_min_sell; delete newErrors.eur_max_sell; } } setErrors(newErrors); }; const { mutate: updateConfig, isLoading: isSaving, error: saveError, } = useMutation((data: PricingConfigUpdate) => adminApi.updatePricingConfig(data), { onSuccess: () => { setSuccess(true); setError(null); fetchConfig(); setTimeout(() => setSuccess(false), 3000); }, onError: (err) => { setError(err instanceof Error ? err.message : t("pricing.errors.saveFailed")); }, }); const handleConfirmSave = () => { if (!formData) { setIsConfirming(false); return; } const validationErrors = validateForm(formData); if (Object.keys(validationErrors).length > 0) { setErrors(validationErrors); setIsConfirming(false); return; } updateConfig({ premium_buy: formData.premium_buy, premium_sell: formData.premium_sell, small_trade_threshold_eur: formData.small_trade_threshold_eur, small_trade_extra_premium: formData.small_trade_extra_premium, eur_min_buy: formData.eur_min_buy, eur_max_buy: formData.eur_max_buy, eur_min_sell: formData.eur_min_sell, eur_max_sell: formData.eur_max_sell, }); setIsConfirming(false); }; if (isLoading || !formData) { return (
); } const hasErrors = Object.keys(errors).length > 0; const displayError = error || saveError; return (

{t("pricing.title")}

{t("pricing.subtitle")}

{success &&
{t("pricing.success")}
}
{ e.preventDefault(); setIsConfirming(true); }} > {/* Premium Settings - Full Width */}

{t("pricing.premiumSettings")}

handleFieldChange("premium_buy", e.target.value)} min={-100} max={100} style={{ ...formStyles.input, width: "100%", maxWidth: "180px", ...(errors.premium_buy ? formStyles.inputError : {}), }} /> {errors.premium_buy &&
{errors.premium_buy}
}
handleFieldChange("premium_sell", e.target.value)} min={-100} max={100} style={{ ...formStyles.input, width: "100%", maxWidth: "180px", ...(errors.premium_sell ? formStyles.inputError : {}), }} /> {errors.premium_sell &&
{errors.premium_sell}
}
handleFieldChange( "small_trade_threshold_eur", (parseFloat(e.target.value) * 100).toString() ) } min={1} style={{ ...formStyles.input, width: "100%", maxWidth: "180px", ...(errors.small_trade_threshold_eur ? formStyles.inputError : {}), }} /> {errors.small_trade_threshold_eur && (
{errors.small_trade_threshold_eur}
)}
handleFieldChange("small_trade_extra_premium", e.target.value)} min={-100} max={100} style={{ ...formStyles.input, width: "100%", maxWidth: "180px", ...(errors.small_trade_extra_premium ? formStyles.inputError : {}), }} /> {errors.small_trade_extra_premium && (
{errors.small_trade_extra_premium}
)}
{/* Trade Limits - Side by Side */}

{t("pricing.tradeLimitsBuy")}

handleFieldChange("eur_min_buy", (parseFloat(e.target.value) * 100).toString()) } min={1} style={{ ...formStyles.input, width: "100%", maxWidth: "180px", ...(errors.eur_min_buy ? formStyles.inputError : {}), }} /> {errors.eur_min_buy &&
{errors.eur_min_buy}
}
handleFieldChange("eur_max_buy", (parseFloat(e.target.value) * 100).toString()) } min={1} style={{ ...formStyles.input, width: "100%", maxWidth: "180px", ...(errors.eur_max_buy ? formStyles.inputError : {}), }} /> {errors.eur_max_buy &&
{errors.eur_max_buy}
}

{t("pricing.tradeLimitsSell")}

handleFieldChange("eur_min_sell", (parseFloat(e.target.value) * 100).toString()) } min={1} style={{ ...formStyles.input, width: "100%", maxWidth: "180px", ...(errors.eur_min_sell ? formStyles.inputError : {}), }} /> {errors.eur_min_sell &&
{errors.eur_min_sell}
}
handleFieldChange("eur_max_sell", (parseFloat(e.target.value) * 100).toString()) } min={1} style={{ ...formStyles.input, width: "100%", maxWidth: "180px", ...(errors.eur_max_sell ? formStyles.inputError : {}), }} /> {errors.eur_max_sell &&
{errors.eur_max_sell}
}
setIsConfirming(false)} onActionClick={() => setIsConfirming(true)} actionLabel={isSaving ? tCommon("saving") : t("pricing.save")} confirmLabel={tCommon("confirm")} cancelLabel={tCommon("cancel")} isLoading={isSaving} actionButtonStyle={{ ...buttonStyles.primary, ...(hasErrors || isSaving ? buttonStyles.disabled : {}), }} />
); }