// SnapshotEditPage — create or edit a balance snapshot at a given date.
//
// Issue #146 / Bilan #1b ships the route `/balance/snapshot` with two modes
// driven by the `?date=` query parameter:
// - `?date=` absent → 'new' mode (date picker editable, defaults to today)
// - `?date=YYYY-MM-DD` → 'edit' mode if a snapshot exists at that date,
// otherwise 'new' mode pre-selected at that date (which mirrors the
// "redirect to edit" flow when the user comes from the future
// /balance overview's "Edit" link).
//
// The page itself only orchestrates: all DB work flows through
// `useSnapshotEditor`, the editor view through `SnapshotEditor`. Per spec
// (decisions row "Bouton Pré-remplir"), priced-kind prefill is a no-op
// here (the priced editor lands in #140).
import { useEffect, useMemo, useState } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
ArrowLeft,
Trash2,
Save,
Wallet,
RotateCcw,
AlertTriangle,
} from "lucide-react";
import { useSnapshotEditor } from "../hooks/useSnapshotEditor";
import SnapshotEditor from "../components/balance/SnapshotEditor";
export default function SnapshotEditPage() {
const { t } = useTranslation();
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const dateParam = searchParams.get("date");
const editor = useSnapshotEditor({ dateParam });
const { state } = editor;
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deleteConfirmText, setDeleteConfirmText] = useState("");
// Reset the delete modal whenever the underlying snapshot changes (e.g.
// after switching ?date=).
useEffect(() => {
setShowDeleteModal(false);
setDeleteConfirmText("");
}, [state.snapshot?.id]);
const isEditMode = state.mode === "edit";
const canPrefill = !!state.previousSnapshot;
// Aggregate value across simple + priced lines (computed live as the
// user types). Priced contribution = quantity × unit_price.
const totalValue = useMemo(() => {
let total = 0;
let hasAny = false;
for (const raw of Object.values(state.values)) {
if (!raw) continue;
const trimmed = String(raw).trim().replace(",", ".");
const n = Number(trimmed);
if (Number.isFinite(n)) {
total += n;
hasAny = true;
}
}
for (const entry of Object.values(state.pricedValues)) {
if (!entry) continue;
const qty = Number(String(entry.quantity ?? "").trim().replace(",", "."));
const price = Number(
String(entry.unit_price ?? "").trim().replace(",", ".")
);
if (Number.isFinite(qty) && Number.isFinite(price)) {
total += qty * price;
hasAny = true;
}
}
return hasAny ? total : null;
}, [state.values, state.pricedValues]);
const handleSave = async () => {
try {
await editor.save();
// After a successful create, the URL should become `?date=...` so
// refreshing keeps the user in edit mode.
if (!isEditMode) {
setSearchParams(
{ date: state.snapshotDate },
{ replace: true }
);
}
} catch {
// The hook surfaced the error via state.errorCode/state.error.
}
};
const handleDelete = async () => {
try {
await editor.remove();
navigate("/balance/accounts");
} catch {
// surfaced via state.error
}
};
return (
{
const next = e.target.value;
editor.setDate(next);
// Drive the route param so reloads stay coherent and an
// existing snapshot at the chosen date flips us into 'edit'.
if (next) {
setSearchParams({ date: next }, { replace: true });
} else {
setSearchParams({}, { replace: true });
}
// WebKitGTK (Linux Tauri WebView) does not always dismiss the
// native date popup after a value commit — user has to hit
// Esc. Force-blur is a no-op on WebView2/WKWebView. See #177.
e.currentTarget.blur();
}}
className="w-full px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--card)] text-sm focus:outline-none focus:ring-1 focus:ring-[var(--primary)] disabled:opacity-60"
/>
{isEditMode && (