326 lines
10 KiB
TypeScript
326 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { useSession } from 'next-auth/react';
|
|
import api from '@/services/api';
|
|
|
|
type DigestFrequency = 'instant' | 'daily' | 'weekly' | 'off';
|
|
|
|
interface NotificationCategory {
|
|
id: string;
|
|
label: string;
|
|
description: string;
|
|
email: boolean;
|
|
inApp: boolean;
|
|
}
|
|
|
|
interface NotificationsFormProps {
|
|
onSave?: (data: {
|
|
notifications: NotificationCategory[];
|
|
digestFrequency: DigestFrequency;
|
|
}) => void;
|
|
}
|
|
|
|
const DEFAULT_NOTIFICATIONS: NotificationCategory[] = [
|
|
{
|
|
id: 'new_lead_alerts',
|
|
label: 'New Lead Alerts',
|
|
description: 'Choose what you want to be notified about and how.',
|
|
email: true,
|
|
inApp: true,
|
|
},
|
|
{
|
|
id: 'message_updates',
|
|
label: 'Message Updates',
|
|
description: 'Notices about new messages from clients or other agents.',
|
|
email: false,
|
|
inApp: true,
|
|
},
|
|
{
|
|
id: 'urgent_requests_tours',
|
|
label: 'Urgent Requests & Tours',
|
|
description: 'Immediate notifications for tour requests and time-sensitive items.',
|
|
email: true,
|
|
inApp: true,
|
|
},
|
|
];
|
|
|
|
export function NotificationsForm({ onSave }: NotificationsFormProps) {
|
|
const { data: session } = useSession();
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [success, setSuccess] = useState<string | null>(null);
|
|
|
|
const [notifications, setNotifications] = useState<NotificationCategory[]>(DEFAULT_NOTIFICATIONS);
|
|
const [digestFrequency, setDigestFrequency] = useState<DigestFrequency>('instant');
|
|
const [savedNotifications, setSavedNotifications] = useState<NotificationCategory[]>(DEFAULT_NOTIFICATIONS);
|
|
const [savedDigestFrequency, setSavedDigestFrequency] = useState<DigestFrequency>('instant');
|
|
|
|
const fetchPreferences = useCallback(async () => {
|
|
if (!session) return;
|
|
|
|
try {
|
|
setIsLoading(true);
|
|
const role = (session?.user as any)?.role;
|
|
const endpoint = role === 'AGENT' ? '/agents/preferences/notifications' : '/users/preferences/notifications';
|
|
|
|
const response = await api.get(endpoint);
|
|
const savedPrefs = response.data?.data || response.data || {};
|
|
|
|
if (savedPrefs.notifications) {
|
|
const updated = DEFAULT_NOTIFICATIONS.map((item) => {
|
|
const saved = savedPrefs.notifications[item.id];
|
|
if (saved) {
|
|
return {
|
|
...item,
|
|
email: saved.email ?? item.email,
|
|
inApp: saved.inApp ?? item.inApp,
|
|
};
|
|
}
|
|
return item;
|
|
});
|
|
setNotifications(updated);
|
|
setSavedNotifications(updated);
|
|
}
|
|
|
|
if (savedPrefs.digestFrequency) {
|
|
setDigestFrequency(savedPrefs.digestFrequency);
|
|
setSavedDigestFrequency(savedPrefs.digestFrequency);
|
|
}
|
|
} catch {
|
|
console.log('No saved notification preferences found, using defaults');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [session]);
|
|
|
|
useEffect(() => {
|
|
fetchPreferences();
|
|
}, [fetchPreferences]);
|
|
|
|
const toggleNotification = (id: string, type: 'email' | 'inApp') => {
|
|
setNotifications((prev) =>
|
|
prev.map((item) =>
|
|
item.id === id ? { ...item, [type]: !item[type] } : item
|
|
)
|
|
);
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
setIsSaving(true);
|
|
setError(null);
|
|
setSuccess(null);
|
|
|
|
try {
|
|
const role = (session?.user as any)?.role;
|
|
const endpoint = role === 'AGENT' ? '/agents/preferences/notifications' : '/users/preferences/notifications';
|
|
|
|
const preferences = {
|
|
notifications: notifications.reduce(
|
|
(acc, item) => ({
|
|
...acc,
|
|
[item.id]: { email: item.email, inApp: item.inApp },
|
|
}),
|
|
{}
|
|
),
|
|
digestFrequency,
|
|
};
|
|
|
|
await api.put(endpoint, preferences);
|
|
|
|
setSavedNotifications([...notifications]);
|
|
setSavedDigestFrequency(digestFrequency);
|
|
setSuccess('Notification preferences saved successfully!');
|
|
|
|
if (onSave) {
|
|
onSave({ notifications, digestFrequency });
|
|
}
|
|
|
|
setTimeout(() => setSuccess(null), 3000);
|
|
} catch (err: unknown) {
|
|
const error = err as { response?: { data?: { message?: string } } };
|
|
setError(error.response?.data?.message || 'Failed to save notification preferences. Please try again.');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const hasChanges = JSON.stringify(notifications) !== JSON.stringify(savedNotifications) || digestFrequency !== savedDigestFrequency;
|
|
|
|
const handleCancel = () => {
|
|
if (!hasChanges) return;
|
|
setNotifications([...savedNotifications]);
|
|
setDigestFrequency(savedDigestFrequency);
|
|
setError(null);
|
|
setSuccess(null);
|
|
};
|
|
|
|
const Checkbox = ({
|
|
checked,
|
|
onToggle,
|
|
label,
|
|
}: {
|
|
checked: boolean;
|
|
onToggle: () => void;
|
|
label: string;
|
|
}) => (
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<button
|
|
type="button"
|
|
onClick={onToggle}
|
|
className={`w-[16px] h-[16px] rounded-[2px] border flex items-center justify-center transition-colors ${
|
|
checked
|
|
? 'bg-[#00293D] border-[#00293D]'
|
|
: 'bg-white border-[#00293D]/30'
|
|
}`}
|
|
>
|
|
{checked && (
|
|
<svg width="10" height="8" viewBox="0 0 10 8" fill="none">
|
|
<path
|
|
d="M1 4L3.5 6.5L9 1"
|
|
stroke="white"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
)}
|
|
</button>
|
|
<span className="font-serif text-[14px] text-[#00293D]">{label}</span>
|
|
</label>
|
|
);
|
|
|
|
const frequencyOptions: { value: DigestFrequency; label: string }[] = [
|
|
{ value: 'instant', label: 'Instant' },
|
|
{ value: 'daily', label: 'Daily' },
|
|
{ value: 'weekly', label: 'Weekly' },
|
|
{ value: 'off', label: 'Off' },
|
|
];
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#e58625]"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{/* Error Message */}
|
|
{error && (
|
|
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-[10px] text-red-700 text-[13px] font-serif">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Success Message */}
|
|
{success && (
|
|
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-[10px] text-green-700 text-[13px] font-serif">
|
|
{success}
|
|
</div>
|
|
)}
|
|
|
|
{/* Notification Preferences Card */}
|
|
<div className="border border-[#00293d]/10 rounded-[15px] bg-white p-6">
|
|
{/* Title */}
|
|
<h2 className="font-fractul font-bold text-[20px] text-[#00293D] mb-4">
|
|
Notification Preferences
|
|
</h2>
|
|
|
|
{/* Divider */}
|
|
<div className="border-b border-[#00293D]/10 mb-6" />
|
|
|
|
{/* Notification Categories */}
|
|
<div className="space-y-0">
|
|
{notifications.map((item, index) => (
|
|
<div key={item.id}>
|
|
<div className="flex items-start justify-between py-4">
|
|
<div className="flex-1 pr-4">
|
|
<h3 className="font-fractul font-bold text-[14px] text-[#00293D] mb-1">
|
|
{item.label}
|
|
</h3>
|
|
<p className="font-serif text-[14px] text-[#00293D]/70">
|
|
{item.description}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-8 flex-shrink-0">
|
|
<Checkbox
|
|
checked={item.email}
|
|
onToggle={() => toggleNotification(item.id, 'email')}
|
|
label="Email"
|
|
/>
|
|
<Checkbox
|
|
checked={item.inApp}
|
|
onToggle={() => toggleNotification(item.id, 'inApp')}
|
|
label="In-App"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{index < notifications.length - 1 && (
|
|
<div className="border-b border-[#00293D]/10" />
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Divider before Email Digest */}
|
|
<div className="border-b border-[#00293D]/10 mt-2 mb-6" />
|
|
|
|
{/* Email Digest Frequency */}
|
|
<div className="mb-8">
|
|
<h3 className="font-fractul font-bold text-[14px] text-[#00293D] mb-1">
|
|
Email Digest Frequency
|
|
</h3>
|
|
<p className="font-serif text-[14px] text-[#00293D]/70 mb-4">
|
|
Choose how often you receive non-urgent email summaries
|
|
</p>
|
|
|
|
{/* Frequency Tabs */}
|
|
<div className="inline-flex items-center border border-[#00293D]/10 rounded-[7px] h-[49px] px-2">
|
|
{frequencyOptions.map((option, index) => (
|
|
<div key={option.value} className="flex items-center h-full">
|
|
<button
|
|
type="button"
|
|
onClick={() => setDigestFrequency(option.value)}
|
|
className={`px-5 py-1.5 text-[14px] font-fractul transition-colors rounded-[15px] ${
|
|
digestFrequency === option.value
|
|
? 'bg-[#e58625] text-[#00293D]'
|
|
: 'text-[#00293D] hover:bg-[#00293D]/5'
|
|
}`}
|
|
>
|
|
{option.label}
|
|
</button>
|
|
{index < frequencyOptions.length - 1 && (
|
|
<div className="w-px h-full bg-[#00293D]/10 mx-1" />
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex justify-end gap-4 pt-4 border-t border-[#00293D]/10">
|
|
<button
|
|
type="button"
|
|
onClick={handleCancel}
|
|
disabled={!hasChanges || isSaving}
|
|
className="px-10 py-3 border border-[#00293D]/10 rounded-[15px] font-fractul font-bold text-[14px] text-[#00293D] hover:bg-[#00293D]/5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleSave}
|
|
disabled={isSaving}
|
|
className="px-10 py-3 bg-[#e58625] rounded-[15px] font-fractul font-bold text-[14px] text-[#00293D] hover:bg-[#d47920] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{isSaving ? 'Saving...' : 'Save Changes'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|