feat: Add Billings & Payments and Testimonials settings pages for agent and user roles, and refactor notification settings form.
This commit is contained in:
@@ -4,72 +4,44 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import api from '@/services/api';
|
||||
|
||||
interface NotificationSetting {
|
||||
type DigestFrequency = 'instant' | 'daily' | 'weekly' | 'off';
|
||||
|
||||
interface NotificationCategory {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
email: boolean;
|
||||
inApp: boolean;
|
||||
}
|
||||
|
||||
interface NotificationsFormProps {
|
||||
onSave?: (data: {
|
||||
emailNotifications: NotificationSetting[];
|
||||
pushNotifications: NotificationSetting[];
|
||||
notifications: NotificationCategory[];
|
||||
digestFrequency: DigestFrequency;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
// Default notification settings (all disabled by default)
|
||||
const DEFAULT_EMAIL_NOTIFICATIONS: NotificationSetting[] = [
|
||||
const DEFAULT_NOTIFICATIONS: NotificationCategory[] = [
|
||||
{
|
||||
id: 'new_leads',
|
||||
label: 'New Leads',
|
||||
description: 'Get notified when you receive new leads',
|
||||
enabled: false,
|
||||
id: 'new_lead_alerts',
|
||||
label: 'New Lead Alerts',
|
||||
description: 'Choose what you want to be notified about and how.',
|
||||
email: true,
|
||||
inApp: true,
|
||||
},
|
||||
{
|
||||
id: 'messages',
|
||||
label: 'Messages',
|
||||
description: 'Get notified when you receive new messages',
|
||||
enabled: false,
|
||||
id: 'message_updates',
|
||||
label: 'Message Updates',
|
||||
description: 'Notices about new messages from clients or other agents.',
|
||||
email: false,
|
||||
inApp: true,
|
||||
},
|
||||
{
|
||||
id: 'connection_requests',
|
||||
label: 'Connection Requests',
|
||||
description: 'Get notified about new connection requests',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: 'property_updates',
|
||||
label: 'Property Updates',
|
||||
description: 'Get notified about property status changes',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: 'marketing',
|
||||
label: 'Marketing & Promotions',
|
||||
description: 'Receive marketing emails and promotions',
|
||||
enabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
const DEFAULT_PUSH_NOTIFICATIONS: NotificationSetting[] = [
|
||||
{
|
||||
id: 'push_messages',
|
||||
label: 'Messages',
|
||||
description: 'Push notifications for new messages',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: 'push_leads',
|
||||
label: 'New Leads',
|
||||
description: 'Push notifications for new leads',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: 'push_reminders',
|
||||
label: 'Reminders',
|
||||
description: 'Push notifications for scheduled reminders',
|
||||
enabled: false,
|
||||
id: 'urgent_requests_tours',
|
||||
label: 'Urgent Requests & Tours',
|
||||
description: 'Immediate notifications for tour requests and time-sensitive items.',
|
||||
email: true,
|
||||
inApp: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -80,10 +52,9 @@ export function NotificationsForm({ onSave }: NotificationsFormProps) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
const [emailNotifications, setEmailNotifications] = useState<NotificationSetting[]>(DEFAULT_EMAIL_NOTIFICATIONS);
|
||||
const [pushNotifications, setPushNotifications] = useState<NotificationSetting[]>(DEFAULT_PUSH_NOTIFICATIONS);
|
||||
const [notifications, setNotifications] = useState<NotificationCategory[]>(DEFAULT_NOTIFICATIONS);
|
||||
const [digestFrequency, setDigestFrequency] = useState<DigestFrequency>('instant');
|
||||
|
||||
// Fetch saved preferences on mount
|
||||
const fetchPreferences = useCallback(async () => {
|
||||
if (!session) return;
|
||||
|
||||
@@ -95,26 +66,26 @@ export function NotificationsForm({ onSave }: NotificationsFormProps) {
|
||||
const response = await api.get(endpoint);
|
||||
const savedPrefs = response.data?.data || response.data || {};
|
||||
|
||||
// Merge saved preferences with defaults
|
||||
if (savedPrefs.email) {
|
||||
setEmailNotifications((prev) =>
|
||||
prev.map((item) => ({
|
||||
...item,
|
||||
enabled: savedPrefs.email[item.id] ?? item.enabled,
|
||||
}))
|
||||
if (savedPrefs.notifications) {
|
||||
setNotifications((prev) =>
|
||||
prev.map((item) => {
|
||||
const saved = savedPrefs.notifications[item.id];
|
||||
if (saved) {
|
||||
return {
|
||||
...item,
|
||||
email: saved.email ?? item.email,
|
||||
inApp: saved.inApp ?? item.inApp,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (savedPrefs.push) {
|
||||
setPushNotifications((prev) =>
|
||||
prev.map((item) => ({
|
||||
...item,
|
||||
enabled: savedPrefs.push[item.id] ?? item.enabled,
|
||||
}))
|
||||
);
|
||||
if (savedPrefs.digestFrequency) {
|
||||
setDigestFrequency(savedPrefs.digestFrequency);
|
||||
}
|
||||
} catch (err) {
|
||||
// If no saved preferences, use defaults (all disabled)
|
||||
} catch {
|
||||
console.log('No saved notification preferences found, using defaults');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -125,18 +96,10 @@ export function NotificationsForm({ onSave }: NotificationsFormProps) {
|
||||
fetchPreferences();
|
||||
}, [fetchPreferences]);
|
||||
|
||||
const toggleEmailNotification = (id: string) => {
|
||||
setEmailNotifications((prev) =>
|
||||
const toggleNotification = (id: string, type: 'email' | 'inApp') => {
|
||||
setNotifications((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === id ? { ...item, enabled: !item.enabled } : item
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const togglePushNotification = (id: string) => {
|
||||
setPushNotifications((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === id ? { ...item, enabled: !item.enabled } : item
|
||||
item.id === id ? { ...item, [type]: !item[type] } : item
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -150,10 +113,15 @@ export function NotificationsForm({ onSave }: NotificationsFormProps) {
|
||||
const role = (session?.user as any)?.role;
|
||||
const endpoint = role === 'AGENT' ? '/agents/preferences/notifications' : '/users/preferences/notifications';
|
||||
|
||||
// Transform settings to API format
|
||||
const preferences = {
|
||||
email: emailNotifications.reduce((acc, item) => ({ ...acc, [item.id]: item.enabled }), {}),
|
||||
push: pushNotifications.reduce((acc, item) => ({ ...acc, [item.id]: item.enabled }), {}),
|
||||
notifications: notifications.reduce(
|
||||
(acc, item) => ({
|
||||
...acc,
|
||||
[item.id]: { email: item.email, inApp: item.inApp },
|
||||
}),
|
||||
{}
|
||||
),
|
||||
digestFrequency,
|
||||
};
|
||||
|
||||
await api.put(endpoint, preferences);
|
||||
@@ -161,10 +129,9 @@ export function NotificationsForm({ onSave }: NotificationsFormProps) {
|
||||
setSuccess('Notification preferences saved successfully!');
|
||||
|
||||
if (onSave) {
|
||||
onSave({ emailNotifications, pushNotifications });
|
||||
onSave({ notifications, digestFrequency });
|
||||
}
|
||||
|
||||
// Clear success message after 3 seconds
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { message?: string } } };
|
||||
@@ -174,27 +141,55 @@ export function NotificationsForm({ onSave }: NotificationsFormProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const ToggleSwitch = ({
|
||||
enabled,
|
||||
const handleCancel = () => {
|
||||
setNotifications(DEFAULT_NOTIFICATIONS);
|
||||
setDigestFrequency('instant');
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
};
|
||||
|
||||
const Checkbox = ({
|
||||
checked,
|
||||
onToggle,
|
||||
label,
|
||||
}: {
|
||||
enabled: boolean;
|
||||
checked: boolean;
|
||||
onToggle: () => void;
|
||||
label: string;
|
||||
}) => (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`relative w-[44px] h-[24px] rounded-full transition-colors cursor-pointer ${
|
||||
enabled ? 'bg-[#e58625]' : 'bg-[#00293D]/20'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute top-[2px] w-[20px] h-[20px] rounded-full bg-white shadow transition-transform ${
|
||||
enabled ? 'translate-x-[22px]' : 'translate-x-[2px]'
|
||||
<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'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
>
|
||||
{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">
|
||||
@@ -219,84 +214,103 @@ export function NotificationsForm({ onSave }: NotificationsFormProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email Notifications */}
|
||||
<div className="border border-[#00293d]/10 rounded-[15px] bg-white p-6 mb-4">
|
||||
<div className="mb-6">
|
||||
<h2 className="font-fractul font-bold text-[16px] text-[#00293D] mb-2">
|
||||
Email Notifications
|
||||
</h2>
|
||||
<p className="font-serif text-[12px] text-[#00293D]/60">
|
||||
Manage your email notification preferences
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<div className="space-y-4">
|
||||
{emailNotifications.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center justify-between py-3 border-b border-[#00293d]/10 last:border-0"
|
||||
>
|
||||
<div>
|
||||
<h3 className="font-serif font-medium text-[14px] text-[#00293D]">
|
||||
{item.label}
|
||||
</h3>
|
||||
<p className="font-serif text-[12px] text-[#00293D]/60">
|
||||
{item.description}
|
||||
</p>
|
||||
{/* 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>
|
||||
<ToggleSwitch
|
||||
enabled={item.enabled}
|
||||
onToggle={() => toggleEmailNotification(item.id)}
|
||||
/>
|
||||
{index < notifications.length - 1 && (
|
||||
<div className="border-b border-[#00293D]/10" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Push Notifications */}
|
||||
<div className="border border-[#00293d]/10 rounded-[15px] bg-white p-6 mb-4">
|
||||
<div className="mb-6">
|
||||
<h2 className="font-fractul font-bold text-[16px] text-[#00293D] mb-2">
|
||||
Push Notifications
|
||||
</h2>
|
||||
<p className="font-serif text-[12px] text-[#00293D]/60">
|
||||
Manage your push notification preferences
|
||||
{/* 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 border border-[#00293D]/10 rounded-[7px] overflow-hidden">
|
||||
{frequencyOptions.map((option, index) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => setDigestFrequency(option.value)}
|
||||
className={`px-5 py-2 text-[14px] font-fractul transition-colors ${
|
||||
digestFrequency === option.value
|
||||
? 'bg-[#e58625] text-[#00293D] font-medium'
|
||||
: 'bg-white text-[#00293D] hover:bg-[#00293D]/5'
|
||||
} ${
|
||||
index < frequencyOptions.length - 1
|
||||
? 'border-r border-[#00293D]/10'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{pushNotifications.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center justify-between py-3 border-b border-[#00293d]/10 last:border-0"
|
||||
>
|
||||
<div>
|
||||
<h3 className="font-serif font-medium text-[14px] text-[#00293D]">
|
||||
{item.label}
|
||||
</h3>
|
||||
<p className="font-serif text-[12px] text-[#00293D]/60">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
enabled={item.enabled}
|
||||
onToggle={() => togglePushNotification(item.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end gap-4 pt-4 border-t border-[#00293D]/10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
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"
|
||||
>
|
||||
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>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="px-8 py-3 bg-[#e58625] text-white rounded-[15px] font-fractul font-medium text-[14px] hover:bg-[#d47920] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user