import { decryptDataWithStoredPassword, encryptDataWithStoredPassword, getEncryptionPassword, MakeNewDate, } from "../helpers.ts"; import { Obligation } from "../services/financhalServices.ts"; import { useEffect, useRef, useState } from "preact/hooks"; import { PopAddObligationDialog } from "./dialogs/PopAddObligationDialog.tsx"; interface Props { obligations: Obligation[]; userId?: string; openModal?: boolean; } export default function ObligationList( { obligations, userId, openModal = false }: Props, ) { // Theme classes are defined in styles.css and respond to theme mode + media. const [_popAddObligation, popupAddObligationDialog] = PopAddObligationDialog({ userId, defaultOpen: openModal, }); const [processedObligations, setProcessedObligations] = useState< Obligation[] >(obligations); const [_isProcessing, setIsProcessing] = useState(false); const [_error, setError] = useState(null); const [isOpeningModal, setIsOpeningModal] = useState(openModal); const processingRef = useRef(false); // Process obligations on component mount and when obligations or userId change useEffect(() => { setTimeout(() => { const lockRoute = localStorage.getItem("lock-route"); const url = new URL(globalThis.location.href); const currentRoute = url.pathname + url.search; if (lockRoute && lockRoute === currentRoute) { localStorage.removeItem("lock-route"); } }, 100); // Prevent concurrent processing if (processingRef.current) { return; } if (!userId) { setProcessedObligations(obligations); setError(null); return; } const processObligations = async () => { processingRef.current = true; setIsProcessing(true); setError(null); try { const encryptionPassword = getEncryptionPassword(userId); if (!encryptionPassword) { // No encryption key available, use obligations as-is setProcessedObligations(obligations); return; } const updatedObligations: Obligation[] = []; let hasEncryptionUpdates = false; for (const obligation of obligations) { const processedObligation = { ...obligation }; if (obligation.encrypted_data) { // Try to decrypt existing encrypted data try { const decryptedData = await decryptDataWithStoredPassword( userId, obligation.encrypted_data, ); if (decryptedData) { const parsed = JSON.parse(decryptedData); if (parsed.amount !== undefined) { processedObligation.amount = parsed.amount; } if (parsed.description !== undefined) { processedObligation.description = parsed.description; } } } catch (error) { console.error("Failed to decrypt obligation data:", error); setError("Failed to decrypt some obligation data"); } } else if ( obligation.amount !== null || obligation.description !== null ) { // No encrypted data but has plaintext data - encrypt it and clear plaintext try { const dataToEncrypt = { amount: obligation.amount, description: obligation.description, }; const encryptedData = await encryptDataWithStoredPassword( userId, JSON.stringify(dataToEncrypt), ); if (encryptedData) { // Update the obligation in the database const response = await fetch("/api/update/obligation", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ obligation_id: obligation.id, encrypted_data: encryptedData, description: null, // Clear plaintext description amount: null, // Clear plaintext amount }), }); if (response.ok) { // Update local state processedObligation.encrypted_data = encryptedData; processedObligation.description = obligation.description; // Keep for display processedObligation.amount = obligation.amount; // Keep for display hasEncryptionUpdates = true; } else { console.error("Failed to update obligation encryption"); setError("Failed to encrypt some obligation data"); } } } catch (error) { console.error("Failed to encrypt obligation data:", error); setError("Failed to encrypt some obligation data"); } } updatedObligations.push(processedObligation); } setProcessedObligations(updatedObligations); if (hasEncryptionUpdates) { console.log("Successfully encrypted plaintext obligation data"); } } catch (error) { console.error("Unexpected error during obligation processing:", error); setError("An unexpected error occurred while processing obligations"); setProcessedObligations(obligations); // Fallback to original data } finally { setIsProcessing(false); processingRef.current = false; } }; processObligations(); // Cleanup function to handle component unmount return () => { processingRef.current = false; }; }, []); async function handleExportObligations() { const exportData = processedObligations.map((ob) => ({ id: ob.id, description: ob.description ?? "", amount: ob.amount ?? 0, frequency: ob.frequency ?? "", due_date: ob.due_date ? new Date(ob.due_date).toISOString().split("T")[0] : null, })); const json = JSON.stringify(exportData, null, 2); const blob = new Blob([json], { type: "application/json" }); const suggestedName = `budget-obligations-${ new Date().toISOString().split("T")[0] }.json`; if (typeof globalThis.showSaveFilePicker === "function") { try { const handle = await globalThis.showSaveFilePicker({ suggestedName, types: [{ description: "JSON file", accept: { "application/json": [".json"] }, }], }); const writable = await handle.createWritable(); await writable.write(blob); await writable.close(); } catch (err) { if ((err as Error).name !== "AbortError") { console.error("Export failed:", err); fallbackDownload(blob, suggestedName); } } } else { fallbackDownload(blob, suggestedName); } } function fallbackDownload(blob: Blob, filename: string) { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } return ( <> {popupAddObligationDialog}

Your Obligations

Back to Financial Calendar
); }