integratedhub/islands/TenantSettings.tsx
2026-01-20 09:02:26 -06:00

207 lines
6.7 KiB
TypeScript

// islands/TenantSettings.tsx
import { useState } from "preact/hooks";
import { User } from "../db/interfaces.ts";
// Define the structure for the preference objects
interface NotificationPreferences {
email_messages: boolean;
email_reminders: boolean;
email_payments: boolean;
}
interface PaymentPreferences {
payment_reminders: boolean;
// Note: default_auto_pay is typically lease-specific, this is for general preference
}
interface TenantSettingsProps {
user: User;
}
function parsePreferences<T extends Record<string, boolean>>(
raw: unknown,
fallback: T,
): T {
if (!raw) return fallback;
if (typeof raw === "string") {
try {
const parsed = JSON.parse(raw) as Partial<T>;
return { ...fallback, ...parsed };
} catch {
return fallback;
}
}
if (typeof raw === "object") {
return { ...fallback, ...(raw as Partial<T>) };
}
return fallback;
}
export default function TenantSettings({ user }: TenantSettingsProps) {
const initialNotifPrefs: NotificationPreferences = parsePreferences(
user.notification_preferences_json as unknown,
{
email_messages: true,
email_reminders: true,
email_payments: true,
},
);
const initialPaymentPrefs: PaymentPreferences = parsePreferences(
user.payment_preferences_json as unknown,
{
payment_reminders: true,
},
);
const [notificationPrefs, setNotificationPrefs] = useState(initialNotifPrefs);
const [paymentPrefs, setPaymentPrefs] = useState(initialPaymentPrefs);
const [status, setStatus] = useState<"idle" | "saving" | "success" | "error">(
"idle",
);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const handleNotificationChange = (key: keyof NotificationPreferences) => {
setNotificationPrefs((prev) => ({ ...prev, [key]: !prev[key] }));
};
const handlePaymentChange = (key: keyof PaymentPreferences) => {
setPaymentPrefs((prev) => ({ ...prev, [key]: !prev[key] }));
};
const handleSubmit = async (e: Event) => {
e.preventDefault();
setStatus("saving");
setErrorMessage(null);
try {
const response = await fetch("/api/user/preferences", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
notification_preferences: notificationPrefs,
payment_preferences: paymentPrefs,
}),
});
if (response.ok) {
setStatus("success");
setTimeout(() => setStatus("idle"), 3000);
} else {
const errorData = await response.json();
setErrorMessage(errorData.error || "Failed to save preferences.");
setStatus("error");
}
} catch (e) {
console.error("Submission error:", e);
setErrorMessage("An unexpected error occurred.");
setStatus("error");
}
};
const StatusMessage = () => {
switch (status) {
case "saving":
return <div class="text-blue-600 text-sm">Saving...</div>;
case "success":
return (
<div class="text-green-600 text-sm">
Preferences saved successfully!
</div>
);
case "error":
return <div class="text-red-600 text-sm">Error: {errorMessage}</div>;
default:
return null;
}
};
const PreferenceToggle = ({ label, checked, onChange }: {
label: string;
checked: boolean;
onChange: () => void;
}) => (
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
<span class="text-sm font-medium text-gray-700">{label}</span>
<label class="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={checked}
onChange={onChange}
class="sr-only peer"
disabled={status === "saving"}
/>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600">
</div>
</label>
</div>
);
return (
<form onSubmit={handleSubmit} class="space-y-8">
{/* Notification Preferences */}
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h2 class="text-lg leading-6 font-medium text-gray-900">
Notification Preferences
</h2>
<p class="mt-1 text-sm text-gray-500">
Control how you receive alerts about your tenancy.
</p>
<div class="mt-6 space-y-4">
<PreferenceToggle
label="Email for New Messages"
checked={notificationPrefs.email_messages}
onChange={() => handleNotificationChange("email_messages")}
/>
<PreferenceToggle
label="Email for Reminders (Rent, Tasks)"
checked={notificationPrefs.email_reminders}
onChange={() => handleNotificationChange("email_reminders")}
/>
<PreferenceToggle
label="Email for Payment Confirmations"
checked={notificationPrefs.email_payments}
onChange={() => handleNotificationChange("email_payments")}
/>
</div>
</div>
</div>
{/* Payment Preferences */}
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h2 class="text-lg leading-6 font-medium text-gray-900">
Payment Preferences
</h2>
<p class="mt-1 text-sm text-gray-500">
Manage settings related to your rent payments.
</p>
<div class="mt-6 space-y-4">
<PreferenceToggle
label="Receive Payment Due Reminders"
checked={paymentPrefs.payment_reminders}
onChange={() => handlePaymentChange("payment_reminders")}
/>
{/* Auto-pay setup is handled on the Payment Methods page, not here */}
</div>
</div>
</div>
{/* Save Button and Status */}
<div class="flex justify-end items-center space-x-4">
<StatusMessage />
<button
type="submit"
disabled={status === "saving"}
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{status === "saving" ? "Saving..." : "Save Changes"}
</button>
</div>
</form>
);
}