216 lines
7.0 KiB
TypeScript
216 lines
7.0 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")}
|
|
/>
|
|
<div class="mt-4">
|
|
<a
|
|
href="/tenant/payment-methods"
|
|
class="text-blue-600 hover:text-blue-500 font-medium"
|
|
>
|
|
Manage Payment Methods →
|
|
</a>
|
|
</div>
|
|
{/* 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>
|
|
);
|
|
}
|
|
|