231 lines
6.9 KiB
TypeScript
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 and respond to theme mode + media.
|
|
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>
|
|
);
|
|
}
|