"use client"; import { useCallback, useState } from "react"; import { SectionLabel } from "@/components/ui/SectionLabel"; import { POI_CATEGORIES, POI_DEFAULT_WEIGHTS, POI_LABELS, POI_WEIGHT_MAX, POI_WEIGHT_MIN, useCreateProfile, useWeightProfiles, type PoiCategoryKey, type WeightProfile, } from "@/lib/api/weightProfiles"; // ── Props ───────────────────────────────────────────────────────────────────── interface Props { /** Current effective weights for the analyze call. */ currentWeights: Record; /** * Called when user clicks "Применить". * @param weights - New weights. * @param profileId - The saved profile id if a named profile is active; null for custom/system. */ onWeightsChange: ( weights: Record, profileId: number | null, ) => void; /** * If provided, enables save/load from DB. * Must be non-empty for CRUD functionality. */ userId?: string; /** Admin token for CRUD API calls. */ adminToken?: string; } // ── Helpers ─────────────────────────────────────────────────────────────────── function clamp(v: number): number { return Math.max(POI_WEIGHT_MIN, Math.min(POI_WEIGHT_MAX, v)); } function weightsFromProfile( profile: WeightProfile, ): Record { const result = { ...POI_DEFAULT_WEIGHTS }; for (const cat of POI_CATEGORIES) { const v = profile.weights[cat]; if (typeof v === "number") { result[cat] = clamp(v); } } return result; } function weightsEqual( a: Record, b: Record, ): boolean { return POI_CATEGORIES.every((c) => a[c] === b[c]); } // ── Component ───────────────────────────────────────────────────────────────── export function WeightProfilePanel({ currentWeights, onWeightsChange, userId, adminToken, }: Props) { const [open, setOpen] = useState(false); // Local draft weights — editable before "Применить" const [draft, setDraft] = useState>(() => ({ ...currentWeights, })); // Selected profile id (null = system defaults) const [selectedProfileId, setSelectedProfileId] = useState( null, ); // Save-dialog state const [showSaveDialog, setShowSaveDialog] = useState(false); const [saveName, setSaveName] = useState(""); const [saveDefault, setSaveDefault] = useState(false); // Profiles query (only when userId + adminToken provided) const canUseCrud = !!userId && !!adminToken; const profilesQuery = useWeightProfiles(userId ?? "", adminToken ?? ""); const createMutation = useCreateProfile(adminToken ?? ""); // ── Handlers ──────────────────────────────────────────────────────────────── function handleSliderChange(cat: PoiCategoryKey, raw: string) { const value = clamp(parseFloat(raw)); setDraft((prev) => ({ ...prev, [cat]: value })); } function handleReset() { setDraft({ ...POI_DEFAULT_WEIGHTS }); setSelectedProfileId(null); } function handleLoadProfile(profile: WeightProfile) { setDraft(weightsFromProfile(profile)); setSelectedProfileId(profile.id); } function handleApply() { onWeightsChange({ ...draft }, selectedProfileId); } const handleSaveProfile = useCallback(async () => { if (!canUseCrud || !userId || !adminToken) { setShowSaveDialog(false); return; } const name = saveName.trim(); if (!name) return; try { await createMutation.mutateAsync({ user_id: userId, profile_name: name, weights: { ...draft }, is_default: saveDefault, }); setShowSaveDialog(false); setSaveName(""); setSaveDefault(false); } catch { // Error visible through createMutation.error } }, [ canUseCrud, userId, adminToken, saveName, draft, saveDefault, createMutation, ]); // ── Derived ───────────────────────────────────────────────────────────────── const profiles: WeightProfile[] = profilesQuery.data ?? []; const isDirty = !weightsEqual(draft, currentWeights); const isDraftDifferentFromDefaults = !weightsEqual( draft, POI_DEFAULT_WEIGHTS, ); // ── Styles ─────────────────────────────────────────────────────────────────── const panelStyle: React.CSSProperties = { background: "#fff", border: "1px solid #e5e7eb", borderRadius: 10, overflow: "hidden", }; const headerStyle: React.CSSProperties = { display: "flex", alignItems: "center", justifyContent: "space-between", padding: "10px 14px", cursor: "pointer", userSelect: "none", background: open ? "#f9fafb" : "#fff", borderBottom: open ? "1px solid #e5e7eb" : "none", }; const bodyStyle: React.CSSProperties = { padding: "12px 14px", display: "flex", flexDirection: "column", gap: 10, }; const sliderRowStyle: React.CSSProperties = { display: "grid", gridTemplateColumns: "130px 1fr 52px", alignItems: "center", gap: 8, }; const valueChipStyle = (v: number): React.CSSProperties => ({ fontSize: 12, fontWeight: 600, textAlign: "right", color: v < 0 ? "#ef4444" : v === 0 ? "#9ca3af" : "#16a34a", fontVariantNumeric: "tabular-nums", }); const btnStyle = ( variant: "primary" | "secondary" | "danger", ): React.CSSProperties => ({ padding: "6px 12px", fontSize: 12, fontWeight: 600, borderRadius: 6, border: "none", cursor: "pointer", background: variant === "primary" ? "#1d4ed8" : variant === "danger" ? "#dc2626" : "#f3f4f6", color: variant === "secondary" ? "#374151" : "#fff", }); // ── Render ────────────────────────────────────────────────────────────────── return (
{/* Header / toggle */}
setOpen((o) => !o)} role="button"> POI Веса
{open && (
{/* Profile selector */} {canUseCrud && (
{profilesQuery.isLoading && ( )}
)} {/* Hint when no crud */} {!canUseCrud && (

Укажите User ID и Admin Token для сохранения профилей.

)} {/* Sliders */}
{POI_CATEGORIES.map((cat) => { const value = draft[cat]; return (
{POI_LABELS[cat]} handleSliderChange(cat, e.target.value)} /> {value.toFixed(1)}
); })}
{/* Action buttons */}
{canUseCrud && isDraftDifferentFromDefaults && ( )}
{/* Info: weights changed but not applied */} {isDirty && (

Изменения не применены к анализу — нажмите «Применить» перед запуском.

)} {/* Save dialog */} {showSaveDialog && (
Новый профиль setSaveName(e.target.value)} style={{ padding: "6px 10px", fontSize: 13, border: "1px solid #d1d5db", borderRadius: 6, }} onKeyDown={(e) => { if (e.key === "Enter") void handleSaveProfile(); if (e.key === "Escape") setShowSaveDialog(false); }} />
{createMutation.error && (

{createMutation.error.message}

)}
)}
)}
); }