540 lines
29 KiB
JavaScript
540 lines
29 KiB
JavaScript
import React, { useEffect, useRef, useState } from 'react';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from './ui/dialog';
|
|
import { Button } from './ui/button';
|
|
import { Card } from './ui/card';
|
|
import { Checkbox } from './ui/checkbox';
|
|
import { Input } from './ui/input';
|
|
import { Label } from './ui/label';
|
|
import { Textarea } from './ui/textarea';
|
|
import { User, Mail, Phone, Calendar, UserCheck, Clock, FileText } from 'lucide-react';
|
|
import StatusBadge from './StatusBadge';
|
|
import api from '../utils/api';
|
|
import { toast } from 'sonner';
|
|
|
|
const ViewRegistrationDialog = ({ open, onOpenChange, user }) => {
|
|
if (!user) return null;
|
|
|
|
const [formData, setFormData] = useState(null);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
const autoSaveTimeoutRef = useRef(null);
|
|
const pendingSaveRef = useRef(false);
|
|
|
|
const leadSourceOptions = [
|
|
'Current member',
|
|
'Friend',
|
|
'OutSmart Magazine',
|
|
'Search engine (Google etc.)',
|
|
"I've known about LOAF for a long time",
|
|
'Other'
|
|
];
|
|
|
|
const volunteerOptions = [
|
|
'Welcoming new members at events',
|
|
'Sending out birthday cards',
|
|
'Care Team Calls',
|
|
'Sharing ideas for events',
|
|
'Researching grants',
|
|
'Applying for grants',
|
|
'Assisting with TeatherLOAFers',
|
|
'Assisting with ActiveLOAFers',
|
|
'Assisting with weekday Lunch Bunch',
|
|
'Uploading Photos to the Website',
|
|
'Assisting with eNewsletter',
|
|
'Other administrative task'
|
|
];
|
|
|
|
useEffect(() => {
|
|
if (!open || !user) return;
|
|
const nextFormData = {
|
|
lead_sources: Array.isArray(user.lead_sources) ? user.lead_sources : [],
|
|
partner_first_name: user.partner_first_name || '',
|
|
partner_last_name: user.partner_last_name || '',
|
|
partner_is_member: Boolean(user.partner_is_member),
|
|
partner_plan_to_become_member: Boolean(user.partner_plan_to_become_member),
|
|
newsletter_publish_name: Boolean(user.newsletter_publish_name),
|
|
newsletter_publish_photo: Boolean(user.newsletter_publish_photo),
|
|
newsletter_publish_birthday: Boolean(user.newsletter_publish_birthday),
|
|
newsletter_publish_none: Boolean(user.newsletter_publish_none),
|
|
referred_by_member_name: user.referred_by_member_name || '',
|
|
volunteer_interests: Array.isArray(user.volunteer_interests) ? user.volunteer_interests : [],
|
|
scholarship_requested: Boolean(user.scholarship_requested),
|
|
scholarship_reason: user.scholarship_reason || ''
|
|
};
|
|
setFormData(nextFormData);
|
|
setHasUnsavedChanges(false);
|
|
}, [open, user]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (autoSaveTimeoutRef.current) {
|
|
clearTimeout(autoSaveTimeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return '—';
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
};
|
|
|
|
const formatDateTime = (dateString) => {
|
|
if (!dateString) return '—';
|
|
return new Date(dateString).toLocaleString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
};
|
|
|
|
const formatPhoneNumber = (phone) => {
|
|
if (!phone) return '—';
|
|
const cleaned = phone.replace(/\D/g, '');
|
|
if (cleaned.length === 10) {
|
|
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`;
|
|
}
|
|
return phone;
|
|
};
|
|
|
|
const saveProfile = async (showToast = true) => {
|
|
if (!formData) return;
|
|
if (isSaving) {
|
|
pendingSaveRef.current = true;
|
|
return;
|
|
}
|
|
|
|
setIsSaving(true);
|
|
try {
|
|
await api.put('/users/profile', {
|
|
lead_sources: formData.lead_sources,
|
|
partner_first_name: formData.partner_first_name,
|
|
partner_last_name: formData.partner_last_name,
|
|
partner_is_member: formData.partner_is_member,
|
|
partner_plan_to_become_member: formData.partner_plan_to_become_member,
|
|
newsletter_publish_name: formData.newsletter_publish_name,
|
|
newsletter_publish_photo: formData.newsletter_publish_photo,
|
|
newsletter_publish_birthday: formData.newsletter_publish_birthday,
|
|
newsletter_publish_none: formData.newsletter_publish_none,
|
|
referred_by_member_name: formData.referred_by_member_name,
|
|
volunteer_interests: formData.volunteer_interests,
|
|
scholarship_requested: formData.scholarship_requested,
|
|
scholarship_reason: formData.scholarship_reason
|
|
});
|
|
setHasUnsavedChanges(false);
|
|
if (showToast) {
|
|
toast.success('Registration details saved');
|
|
}
|
|
} catch (error) {
|
|
if (showToast) {
|
|
toast.error(error.response?.data?.detail || 'Failed to save registration details');
|
|
}
|
|
} finally {
|
|
setIsSaving(false);
|
|
if (pendingSaveRef.current) {
|
|
pendingSaveRef.current = false;
|
|
saveProfile(showToast);
|
|
}
|
|
}
|
|
};
|
|
|
|
const scheduleAutoSave = () => {
|
|
setHasUnsavedChanges(true);
|
|
if (autoSaveTimeoutRef.current) {
|
|
clearTimeout(autoSaveTimeoutRef.current);
|
|
}
|
|
autoSaveTimeoutRef.current = setTimeout(() => {
|
|
saveProfile(false);
|
|
}, 800);
|
|
};
|
|
|
|
const handleInputChange = (e) => {
|
|
const { name, value } = e.target;
|
|
setFormData(prev => {
|
|
const next = { ...prev, [name]: value };
|
|
return next;
|
|
});
|
|
scheduleAutoSave();
|
|
};
|
|
|
|
const handleCheckboxChange = (name, checked) => {
|
|
setFormData(prev => ({ ...prev, [name]: checked }));
|
|
scheduleAutoSave();
|
|
};
|
|
|
|
const handleLeadSourceChange = (source) => {
|
|
setFormData(prev => {
|
|
const sources = prev.lead_sources.includes(source)
|
|
? prev.lead_sources.filter((item) => item !== source)
|
|
: [...prev.lead_sources, source];
|
|
return { ...prev, lead_sources: sources };
|
|
});
|
|
scheduleAutoSave();
|
|
};
|
|
|
|
const handleVolunteerChange = (option) => {
|
|
setFormData(prev => {
|
|
const interests = prev.volunteer_interests.includes(option)
|
|
? prev.volunteer_interests.filter((item) => item !== option)
|
|
: [...prev.volunteer_interests, option];
|
|
return { ...prev, volunteer_interests: interests };
|
|
});
|
|
scheduleAutoSave();
|
|
};
|
|
|
|
const InfoRow = ({ icon: Icon, label, value }) => (
|
|
<div className="flex items-start gap-3 py-3 border-b border-[var(--neutral-800)] last:border-b-0">
|
|
<div className="h-10 w-10 rounded-lg bg-[var(--lavender-400)] flex items-center justify-center flex-shrink-0">
|
|
<Icon className="h-5 w-5 text-brand-purple" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{label}
|
|
</p>
|
|
<p className="font-medium text-[var(--purple-ink)] break-words" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
{value || '—'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-[600px] rounded-2xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
<FileText className="h-6 w-6" />
|
|
Registration Details
|
|
</DialogTitle>
|
|
<DialogDescription className="text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
View the registration information for this member application.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="py-4 space-y-4">
|
|
{/* User Header Card */}
|
|
<Card className="p-4 bg-[var(--lavender-400)] border-2 border-[var(--neutral-800)] rounded-xl">
|
|
<div className="flex items-center gap-4">
|
|
<div className="h-16 w-16 rounded-full bg-[var(--neutral-800)]/20 flex items-center justify-center">
|
|
<User className="h-8 w-8 text-brand-purple" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
{user.first_name} {user.last_name}
|
|
</p>
|
|
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{user.email}
|
|
</p>
|
|
<div className="mt-2">
|
|
<StatusBadge status={user.status} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Contact Information */}
|
|
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
Contact Information
|
|
</h3>
|
|
<InfoRow icon={Mail} label="Email Address" value={user.email} />
|
|
<InfoRow icon={Phone} label="Phone Number" value={formatPhoneNumber(user.phone)} />
|
|
</Card>
|
|
|
|
{/* Registration Details */}
|
|
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
Registration Details
|
|
</h3>
|
|
<InfoRow icon={Calendar} label="Registration Date" value={formatDate(user.created_at)} />
|
|
<InfoRow icon={UserCheck} label="Referred By" value={formData?.referred_by_member_name} />
|
|
<InfoRow icon={Clock} label="Email Verification Expires" value={formatDateTime(user.email_verification_expires_at)} />
|
|
</Card>
|
|
|
|
{formData && (
|
|
<>
|
|
{/* How Did You Hear About Us */}
|
|
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
How Did You Hear About Us? *
|
|
</h3>
|
|
<div className="space-y-3">
|
|
{leadSourceOptions.map((source) => (
|
|
<div key={source} className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id={`lead_${source}`}
|
|
checked={formData.lead_sources.includes(source)}
|
|
onCheckedChange={() => handleLeadSourceChange(source)}
|
|
/>
|
|
<Label htmlFor={`lead_${source}`} className="text-base cursor-pointer">
|
|
{source}
|
|
</Label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Partner Information */}
|
|
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
Partner Information (Optional)
|
|
</h3>
|
|
<div className="grid md:grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<Label htmlFor="partner_first_name">Partner First Name</Label>
|
|
<Input
|
|
id="partner_first_name"
|
|
name="partner_first_name"
|
|
value={formData.partner_first_name}
|
|
onChange={handleInputChange}
|
|
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="partner_last_name">Partner Last Name</Label>
|
|
<Input
|
|
id="partner_last_name"
|
|
name="partner_last_name"
|
|
value={formData.partner_last_name}
|
|
onChange={handleInputChange}
|
|
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="partner_is_member"
|
|
checked={formData.partner_is_member}
|
|
onCheckedChange={(checked) => handleCheckboxChange('partner_is_member', checked)}
|
|
/>
|
|
<Label htmlFor="partner_is_member" className="text-base cursor-pointer">
|
|
Is your partner already a member?
|
|
</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="partner_plan_to_become_member"
|
|
checked={formData.partner_plan_to_become_member}
|
|
onCheckedChange={(checked) => handleCheckboxChange('partner_plan_to_become_member', checked)}
|
|
/>
|
|
<Label htmlFor="partner_plan_to_become_member" className="text-base cursor-pointer">
|
|
Does your partner plan to become a member?
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Newsletter Preferences */}
|
|
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
Newsletter Publication Preferences *
|
|
</h3>
|
|
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
Please check what information may be published in LOAF Newsletter
|
|
</p>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="newsletter_publish_name"
|
|
checked={formData.newsletter_publish_name}
|
|
onCheckedChange={(checked) => handleCheckboxChange('newsletter_publish_name', checked)}
|
|
/>
|
|
<Label htmlFor="newsletter_publish_name" className="text-base cursor-pointer">
|
|
Name
|
|
</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="newsletter_publish_photo"
|
|
checked={formData.newsletter_publish_photo}
|
|
onCheckedChange={(checked) => handleCheckboxChange('newsletter_publish_photo', checked)}
|
|
/>
|
|
<Label htmlFor="newsletter_publish_photo" className="text-base cursor-pointer">
|
|
Photo (added later in profile)
|
|
</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="newsletter_publish_birthday"
|
|
checked={formData.newsletter_publish_birthday}
|
|
onCheckedChange={(checked) => handleCheckboxChange('newsletter_publish_birthday', checked)}
|
|
/>
|
|
<Label htmlFor="newsletter_publish_birthday" className="text-base cursor-pointer">
|
|
Birthday
|
|
</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="newsletter_publish_none"
|
|
checked={formData.newsletter_publish_none}
|
|
onCheckedChange={(checked) => handleCheckboxChange('newsletter_publish_none', checked)}
|
|
/>
|
|
<Label htmlFor="newsletter_publish_none" className="text-base cursor-pointer">
|
|
Do not publish any of my information
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Referral */}
|
|
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
Referral
|
|
</h3>
|
|
<div>
|
|
<Label htmlFor="referred_by_member_name">Name of a LOAF Member who already knows you</Label>
|
|
<Input
|
|
id="referred_by_member_name"
|
|
name="referred_by_member_name"
|
|
value={formData.referred_by_member_name}
|
|
onChange={handleInputChange}
|
|
placeholder="Enter member name or email"
|
|
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
|
/>
|
|
<p className="text-sm text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
If referred by a current member, you may skip the event attendance requirement.
|
|
</p>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Volunteer Interests */}
|
|
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
Volunteer Interests (Optional)
|
|
</h3>
|
|
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
I may at some time be interested in volunteering with LOAF in the following ways (training is provided)
|
|
</p>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
{volunteerOptions.map((option) => (
|
|
<div key={option} className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id={`volunteer_${option}`}
|
|
checked={formData.volunteer_interests.includes(option)}
|
|
onCheckedChange={() => handleVolunteerChange(option)}
|
|
/>
|
|
<Label htmlFor={`volunteer_${option}`} className="text-base cursor-pointer">
|
|
{option}
|
|
</Label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Scholarship Request */}
|
|
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="scholarship_requested"
|
|
checked={formData.scholarship_requested}
|
|
onCheckedChange={(checked) => handleCheckboxChange('scholarship_requested', checked)}
|
|
/>
|
|
<Label htmlFor="scholarship_requested" className="text-base cursor-pointer font-semibold">
|
|
I am requesting for scholarship
|
|
</Label>
|
|
</div>
|
|
<p className="text-sm text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
Scholarship information is kept confidential
|
|
</p>
|
|
{formData.scholarship_requested && (
|
|
<div className="mt-4">
|
|
<Label htmlFor="scholarship_reason">Please explain your situation *</Label>
|
|
<Textarea
|
|
id="scholarship_reason"
|
|
name="scholarship_reason"
|
|
value={formData.scholarship_reason}
|
|
onChange={handleInputChange}
|
|
placeholder="Tell us why you're requesting a scholarship..."
|
|
rows={4}
|
|
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
|
/>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</>
|
|
)}
|
|
|
|
{/* Additional Information (if available) */}
|
|
{(user.address || user.city || user.state || user.zip_code) && (
|
|
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
Address
|
|
</h3>
|
|
<div className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{user.address && <p>{user.address}</p>}
|
|
{(user.city || user.state || user.zip_code) && (
|
|
<p>
|
|
{[user.city, user.state, user.zip_code].filter(Boolean).join(', ')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Notes (if available) */}
|
|
{user.notes && (
|
|
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
Notes
|
|
</h3>
|
|
<p className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{user.notes}
|
|
</p>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Rejection Reason (if rejected) */}
|
|
{user.status === 'rejected' && user.rejection_reason && (
|
|
<Card className="p-4 border border-red-300 bg-red-50 dark:bg-red-500/10 rounded-xl">
|
|
<h3 className="text-lg font-semibold text-red-600 mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
Rejection Reason
|
|
</h3>
|
|
<p className="text-red-600" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{user.rejection_reason}
|
|
</p>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<div className="flex-1 text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
{isSaving && 'Saving changes...'}
|
|
{!isSaving && hasUnsavedChanges && 'Unsaved changes'}
|
|
{!isSaving && !hasUnsavedChanges && 'All changes saved'}
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
onClick={() => saveProfile(true)}
|
|
disabled={!hasUnsavedChanges || isSaving}
|
|
className="rounded-xl border-2 border-[var(--neutral-800)] bg-white text-[var(--purple-ink)] hover:bg-[var(--lavender-300)]"
|
|
>
|
|
Save All
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
onClick={() => onOpenChange(false)}
|
|
className="rounded-xl bg-[var(--purple-ink)] hover:bg-[var(--purple-ink)]/90 text-white"
|
|
>
|
|
Close
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default ViewRegistrationDialog;
|