integratedhub/islands/dialogs/AddObligationDialog.tsx
2026-02-14 02:20:19 +00:00

231 lines
6.9 KiB
TypeScript

import { useEffect, useRef, useState } from "preact/hooks";
import {
encryptDataWithStoredPassword,
getEncryptionPassword,
} from "../../helpers.ts";
interface AddObligationDialogProps {
onClose: () => void;
userId?: string;
}
type FieldErrorState = {
description: boolean;
amount: boolean;
};
export default function AddObligationDialog(
{ onClose, userId }: AddObligationDialogProps,
) {
// Theme classes are defined in styles.css with @media (prefers-color-scheme: dark)
const formRef = useRef<HTMLFormElement>(null);
const firstInputRef = useRef<HTMLInputElement>(null);
const [loading, setLoading] = useState(false);
const [fieldErrors, setFieldErrors] = useState<FieldErrorState>({
description: false,
amount: false,
});
const today = new Date().toISOString().split("T")[0];
const addNewObligation = async () => {
if (!formRef.current) return;
// Collect form data
const formData = new FormData(formRef.current);
const description = formData.get("description")?.toString().trim() || "";
const amount = formData.get("amount")?.toString().trim() || "";
const frequency = formData.get("frequency")?.toString() || "Monthly";
const dueDate = formData.get("due_date")?.toString() || today;
const sender = typeof location !== "undefined" ? location.pathname : "/";
const validation = {
description: !description,
amount: !amount,
};
setFieldErrors(validation);
if (validation.description || validation.amount) {
return;
}
setLoading(true);
localStorage.setItem(
"lock-route",
"/calendar/budget-obligations?modal=off",
);
// Build submission data
const submitData = new FormData();
submitData.set("sender", sender);
submitData.set("frequency", frequency);
submitData.set("due_date", dueDate);
// Encrypt if password is available
const encryptionPassword = userId ? getEncryptionPassword(userId) : null;
if (encryptionPassword && (description || amount)) {
try {
const dataToEncrypt = {
description,
amount: parseFloat(amount) || 0,
};
const encryptedData = await encryptDataWithStoredPassword(
userId!,
JSON.stringify(dataToEncrypt),
);
if (encryptedData) {
submitData.set("encrypted_data", encryptedData);
// Send empty plaintext when encrypted
submitData.set("description", "");
submitData.set("amount", "0");
} else {
throw new Error("Encryption returned empty result");
}
} catch (_err) {
setLoading(false);
return;
}
} else {
submitData.set("description", description);
submitData.set("amount", amount);
submitData.set("encrypted_data", "");
}
// Submit via fetch - let redirects follow automatically
try {
const response = await fetch("/api/create/obligation", {
method: "POST",
body: submitData,
credentials: "same-origin",
});
// Fetch followed the redirect, now navigate to final URL or sender
if (response.ok) {
globalThis.location.href = "/calendar/budget-obligations?modal=off";
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || "Failed to create obligation");
}
} catch (_err) {
setLoading(false);
}
};
const clearFieldError = (field: keyof FieldErrorState) => {
setFieldErrors((prev) => prev[field] ? { ...prev, [field]: false } : prev);
};
const getInputErrorClass = (hasError: boolean) =>
hasError ? "border-red-500 ring-1 ring-red-300" : "";
useEffect(() => {
firstInputRef.current?.focus();
}, []);
return (
<div class="theme-dialog-overlay dialog-offset">
<div class="theme-dialog-container flex flex-col gap-2">
<form ref={formRef}>
<h2 class="theme-dialog-title">Add Obligation</h2>
<div class="flex flex-col gap-1 my-4">
<input
ref={firstInputRef}
class={`theme-dialog-input ${
getInputErrorClass(fieldErrors.description)
}`}
name="description"
placeholder="Description"
required
onInput={() => clearFieldError("description")}
/>
<div
class={`text-red-500 text-xs min-h-4 ${
fieldErrors.description ? "visible" : "invisible"
}`}
aria-live="polite"
>
Description is required.
</div>
</div>
<div class="flex flex-col gap-1 my-4">
<input
class={`theme-dialog-input ${
getInputErrorClass(fieldErrors.amount)
}`}
name="amount"
placeholder="Amount"
type="number"
step="0.01"
required
onInput={() => clearFieldError("amount")}
/>
<div
class={`text-red-500 text-xs min-h-4 ${
fieldErrors.amount ? "visible" : "invisible"
}`}
aria-live="polite"
>
Amount is required.
</div>
</div>
<div class="flex flex-col gap-1 my-4">
<select
class="theme-dialog-select"
name="frequency"
defaultValue="Monthly"
>
<option value="One-time">One-time</option>
<option value="Biweekly">Biweekly</option>
<option value="Weekly">Weekly</option>
<option value="Monthly">Monthly</option>
<option value="Quarterly">Quarterly</option>
<option value="Yearly">Yearly</option>
</select>
</div>
<div class="flex flex-col gap-1 my-4">
<input
class="theme-dialog-input"
name="due_date"
type="date"
defaultValue={today}
required
/>
</div>
</form>
<div class="flex justify-end gap-2 mt-2">
<button
type="button"
class={`theme-dialog-cancel-button w-40 ${
loading ? "opacity-70" : ""
}`}
onClick={() => {
localStorage.setItem(
"lock-route",
"/calendar/budget-obligations?modal=off",
);
setLoading(true);
onClose();
}}
disabled={loading}
>
Cancel
</button>
<button
type="button"
class={`theme-dialog-submit-button w-40 ${
loading ? "opacity-70" : ""
}`}
disabled={loading}
onClick={addNewObligation}
>
{loading ? "Adding..." : "Add"}
</button>
</div>
</div>
</div>
);
}