diff --git a/src/App.js b/src/App.js index bd2cf39..181053c 100644 --- a/src/App.js +++ b/src/App.js @@ -17,12 +17,13 @@ import BecomeMember from './pages/BecomeMember'; import PaymentSuccess from './pages/PaymentSuccess'; import PaymentCancel from './pages/PaymentCancel'; import AdminDashboard from './pages/admin/AdminDashboard'; -import AdminUsers from './pages/admin/AdminUsers'; import AdminUserView from './pages/admin/AdminUserView'; import AdminStaff from './pages/admin/AdminStaff'; import AdminMembers from './pages/admin/AdminMembers'; +import AdminPermissions from './pages/admin/AdminPermissions'; +import AdminRoles from './pages/admin/AdminRoles'; import AdminEvents from './pages/admin/AdminEvents'; -import AdminApprovals from './pages/admin/AdminApprovals'; +import AdminValidations from './pages/admin/AdminValidations'; import AdminPlans from './pages/admin/AdminPlans'; import AdminSubscriptions from './pages/admin/AdminSubscriptions'; import AdminLayout from './layouts/AdminLayout'; @@ -46,6 +47,8 @@ import Donate from './pages/Donate'; import DonationSuccess from './pages/DonationSuccess'; import Resources from './pages/Resources'; import ContactUs from './pages/ContactUs'; +import TermsOfService from './pages/TermsOfService'; +import PrivacyPolicy from './pages/PrivacyPolicy'; const PrivateRoute = ({ children, adminOnly = false }) => { const { user, loading } = useAuth(); @@ -58,7 +61,7 @@ const PrivateRoute = ({ children, adminOnly = false }) => { return ; } - if (adminOnly && user.role !== 'admin') { + if (adminOnly && !['admin', 'superadmin'].includes(user.role)) { return ; } @@ -105,6 +108,10 @@ function App() { } /> } /> + {/* Legal Pages - Public Access */} + } /> + } /> + @@ -189,13 +196,6 @@ function App() { } /> - - - - - - } /> @@ -210,10 +210,10 @@ function App() { } /> - - + } /> @@ -259,6 +259,13 @@ function App() { } /> + + + + + + } /> diff --git a/src/components/AdminSidebar.js b/src/components/AdminSidebar.js index 622579a..6c0578c 100644 --- a/src/components/AdminSidebar.js +++ b/src/components/AdminSidebar.js @@ -39,7 +39,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => { try { const response = await api.get('/admin/users'); const pending = response.data.filter(u => - ['pending_approval', 'pre_approved'].includes(u.status) + ['pending_validation', 'pre_validated'].includes(u.status) ); setPendingCount(pending.length); } catch (error) { @@ -105,9 +105,9 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => { disabled: false }, { - name: 'Approvals', + name: 'Validations', icon: CheckCircle, - path: '/admin/approvals', + path: '/admin/validations', disabled: false, badge: pendingCount }, @@ -154,9 +154,9 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => { disabled: false }, { - name: 'Roles', + name: 'Permissions', icon: Shield, - path: '/admin/roles', + path: '/admin/permissions', disabled: false, superadminOnly: true } diff --git a/src/components/ConfirmationDialog.js b/src/components/ConfirmationDialog.js new file mode 100644 index 0000000..4b9fc2f --- /dev/null +++ b/src/components/ConfirmationDialog.js @@ -0,0 +1,111 @@ +import React from 'react'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from './ui/alert-dialog'; +import { AlertTriangle, Info, CheckCircle } from 'lucide-react'; + +/** + * Reusable confirmation dialog component following the design system + * + * @param {boolean} open - Controls dialog visibility + * @param {function} onOpenChange - Callback when dialog open state changes + * @param {function} onConfirm - Callback when user confirms + * @param {string} title - Dialog title + * @param {string} description - Dialog description/message + * @param {string} confirmText - Confirm button text (default: "Confirm") + * @param {string} cancelText - Cancel button text (default: "Cancel") + * @param {string} variant - Visual variant: 'warning', 'danger', 'info', 'success' (default: 'warning') + * @param {boolean} loading - Show loading state on confirm button + */ +const ConfirmationDialog = ({ + open, + onOpenChange, + onConfirm, + title, + description, + confirmText = 'Confirm', + cancelText = 'Cancel', + variant = 'warning', + loading = false, +}) => { + const variants = { + warning: { + icon: AlertTriangle, + iconColor: 'text-[#ff9e77]', + confirmButtonClass: 'bg-[#ff9e77] text-white hover:bg-[#e88d66] rounded-full px-6', + }, + danger: { + icon: AlertTriangle, + iconColor: 'text-red-600', + confirmButtonClass: 'bg-red-600 text-white hover:bg-red-700 rounded-full px-6', + }, + info: { + icon: Info, + iconColor: 'text-[#664fa3]', + confirmButtonClass: 'bg-[#664fa3] text-white hover:bg-[#553d8a] rounded-full px-6', + }, + success: { + icon: CheckCircle, + iconColor: 'text-[#81B29A]', + confirmButtonClass: 'bg-[#81B29A] text-white hover:bg-[#6fa188] rounded-full px-6', + }, + }; + + const config = variants[variant] || variants.warning; + const Icon = config.icon; + + return ( + + + +
+
+ +
+
+ + {title} + + + {description} + +
+
+
+ + + {cancelText} + + { + e.preventDefault(); + onConfirm(); + }} + className={config.confirmButtonClass} + disabled={loading} + > + {loading ? 'Processing...' : confirmText} + + +
+
+ ); +}; + +export default ConfirmationDialog; diff --git a/src/components/CreateMemberDialog.js b/src/components/CreateMemberDialog.js new file mode 100644 index 0000000..a2d11e5 --- /dev/null +++ b/src/components/CreateMemberDialog.js @@ -0,0 +1,336 @@ +import React, { useState } from 'react'; +import api from '../utils/api'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from './ui/dialog'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; +import { Label } from './ui/label'; +import { toast } from 'sonner'; +import { Loader2, UserPlus } from 'lucide-react'; + +const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => { + const [formData, setFormData] = useState({ + email: '', + password: '', + first_name: '', + last_name: '', + phone: '', + address: '', + city: '', + state: '', + zipcode: '', + date_of_birth: '', + member_since: '', + role: 'member' + }); + const [loading, setLoading] = useState(false); + const [errors, setErrors] = useState({}); + + const handleChange = (field, value) => { + setFormData(prev => ({ ...prev, [field]: value })); + // Clear error when user starts typing + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: null })); + } + }; + + const validate = () => { + const newErrors = {}; + + if (!formData.email) { + newErrors.email = 'Email is required'; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + newErrors.email = 'Invalid email format'; + } + + if (!formData.password || formData.password.length < 8) { + newErrors.password = 'Password must be at least 8 characters'; + } + + if (!formData.first_name) { + newErrors.first_name = 'First name is required'; + } + + if (!formData.last_name) { + newErrors.last_name = 'Last name is required'; + } + + if (!formData.phone) { + newErrors.phone = 'Phone is required'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!validate()) { + return; + } + + setLoading(true); + + try { + // Format dates for backend + const payload = { ...formData }; + if (payload.date_of_birth === '') { + delete payload.date_of_birth; + } + if (payload.member_since === '') { + delete payload.member_since; + } + + await api.post('/admin/users/create', payload); + toast.success('Member created successfully'); + + // Reset form + setFormData({ + email: '', + password: '', + first_name: '', + last_name: '', + phone: '', + address: '', + city: '', + state: '', + zipcode: '', + date_of_birth: '', + member_since: '', + role: 'member' + }); + + onOpenChange(false); + if (onSuccess) onSuccess(); + } catch (error) { + const errorMessage = error.response?.data?.detail || 'Failed to create member'; + toast.error(errorMessage); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + Create Member + + + Create a new member account with direct login access. Member will be created immediately. + + + +
+
+ {/* Email & Password Row */} +
+
+ + handleChange('email', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="member@example.com" + /> + {errors.email && ( +

{errors.email}

+ )} +
+ +
+ + handleChange('password', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="Minimum 8 characters" + /> + {errors.password && ( +

{errors.password}

+ )} +
+
+ + {/* Name Row */} +
+
+ + handleChange('first_name', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="John" + /> + {errors.first_name && ( +

{errors.first_name}

+ )} +
+ +
+ + handleChange('last_name', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="Doe" + /> + {errors.last_name && ( +

{errors.last_name}

+ )} +
+
+ + {/* Phone */} +
+ + handleChange('phone', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="(555) 123-4567" + /> + {errors.phone && ( +

{errors.phone}

+ )} +
+ + {/* Address */} +
+ + handleChange('address', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="123 Main St" + /> +
+ + {/* City, State, Zipcode Row */} +
+
+ + handleChange('city', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="San Francisco" + /> +
+ +
+ + handleChange('state', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="CA" + maxLength={2} + /> +
+ +
+ + handleChange('zipcode', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="94102" + /> +
+
+ + {/* Dates Row */} +
+
+ + handleChange('date_of_birth', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + /> +
+ +
+ + handleChange('member_since', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + /> +
+
+
+ + + + + +
+
+
+ ); +}; + +export default CreateMemberDialog; diff --git a/src/components/CreateStaffDialog.js b/src/components/CreateStaffDialog.js new file mode 100644 index 0000000..314aa70 --- /dev/null +++ b/src/components/CreateStaffDialog.js @@ -0,0 +1,254 @@ +import React, { useState } from 'react'; +import api from '../utils/api'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from './ui/dialog'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; +import { Label } from './ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; +import { toast } from 'sonner'; +import { Loader2, UserPlus } from 'lucide-react'; + +const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => { + const [formData, setFormData] = useState({ + email: '', + password: '', + first_name: '', + last_name: '', + phone: '', + role: 'admin' + }); + const [loading, setLoading] = useState(false); + const [errors, setErrors] = useState({}); + + const handleChange = (field, value) => { + setFormData(prev => ({ ...prev, [field]: value })); + // Clear error when user starts typing + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: null })); + } + }; + + const validate = () => { + const newErrors = {}; + + if (!formData.email) { + newErrors.email = 'Email is required'; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + newErrors.email = 'Invalid email format'; + } + + if (!formData.password || formData.password.length < 8) { + newErrors.password = 'Password must be at least 8 characters'; + } + + if (!formData.first_name) { + newErrors.first_name = 'First name is required'; + } + + if (!formData.last_name) { + newErrors.last_name = 'Last name is required'; + } + + if (!formData.phone) { + newErrors.phone = 'Phone is required'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!validate()) { + return; + } + + setLoading(true); + + try { + await api.post('/admin/users/create', formData); + toast.success('Staff member created successfully'); + + // Reset form + setFormData({ + email: '', + password: '', + first_name: '', + last_name: '', + phone: '', + role: 'admin' + }); + + onOpenChange(false); + if (onSuccess) onSuccess(); + } catch (error) { + const errorMessage = error.response?.data?.detail || 'Failed to create staff member'; + toast.error(errorMessage); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + Create Staff Member + + + Create a new staff account with direct login access. User will be created immediately. + + + +
+
+ {/* Email */} +
+ + handleChange('email', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="staff@example.com" + /> + {errors.email && ( +

{errors.email}

+ )} +
+ + {/* Password */} +
+ + handleChange('password', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="Minimum 8 characters" + /> + {errors.password && ( +

{errors.password}

+ )} +
+ + {/* First Name */} +
+ + handleChange('first_name', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="John" + /> + {errors.first_name && ( +

{errors.first_name}

+ )} +
+ + {/* Last Name */} +
+ + handleChange('last_name', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="Doe" + /> + {errors.last_name && ( +

{errors.last_name}

+ )} +
+ + {/* Phone */} +
+ + handleChange('phone', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="(555) 123-4567" + /> + {errors.phone && ( +

{errors.phone}

+ )} +
+ + {/* Role */} +
+ + +
+
+ + + + + +
+
+
+ ); +}; + +export default CreateStaffDialog; diff --git a/src/components/ImportMembersDialog.js b/src/components/ImportMembersDialog.js new file mode 100644 index 0000000..5cca9de --- /dev/null +++ b/src/components/ImportMembersDialog.js @@ -0,0 +1,335 @@ +import React, { useState } from 'react'; +import api from '../utils/api'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from './ui/dialog'; +import { Button } from './ui/button'; +import { Label } from './ui/label'; +import { Checkbox } from './ui/checkbox'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from './ui/table'; +import { Alert, AlertDescription } from './ui/alert'; +import { Badge } from './ui/badge'; +import { toast } from 'sonner'; +import { Loader2, Upload, FileUp, CheckCircle, XCircle, AlertCircle } from 'lucide-react'; + +const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => { + const [file, setFile] = useState(null); + const [updateExisting, setUpdateExisting] = useState(false); + const [loading, setLoading] = useState(false); + const [dragActive, setDragActive] = useState(false); + const [importResult, setImportResult] = useState(null); + + const handleDrag = (e) => { + e.preventDefault(); + e.stopPropagation(); + if (e.type === "dragenter" || e.type === "dragover") { + setDragActive(true); + } else if (e.type === "dragleave") { + setDragActive(false); + } + }; + + const handleDrop = (e) => { + e.preventDefault(); + e.stopPropagation(); + setDragActive(false); + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + handleFileSelect(e.dataTransfer.files[0]); + } + }; + + const handleFileSelect = (selectedFile) => { + if (!selectedFile.name.endsWith('.csv')) { + toast.error('Please select a CSV file'); + return; + } + setFile(selectedFile); + setImportResult(null); + }; + + const handleFileInput = (e) => { + if (e.target.files && e.target.files[0]) { + handleFileSelect(e.target.files[0]); + } + }; + + const handleSubmit = async () => { + if (!file) { + toast.error('Please select a file'); + return; + } + + setLoading(true); + + try { + const formData = new FormData(); + formData.append('file', file); + formData.append('update_existing', updateExisting); + + const response = await api.post('/admin/users/import', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + setImportResult(response.data); + + if (response.data.status === 'completed') { + toast.success('All members imported successfully!'); + } else if (response.data.status === 'partial') { + toast.warning('Import partially successful. Check errors below.'); + } else { + toast.error('Import failed. Check errors below.'); + } + + if (onSuccess) onSuccess(); + } catch (error) { + const errorMessage = error.response?.data?.detail || 'Failed to import members'; + toast.error(errorMessage); + } finally { + setLoading(false); + } + }; + + const handleClose = () => { + setFile(null); + setUpdateExisting(false); + setImportResult(null); + onOpenChange(false); + }; + + const getStatusIcon = (status) => { + if (status === 'completed') { + return ; + } else if (status === 'partial') { + return ; + } else { + return ; + } + }; + + const getStatusBadge = (status) => { + const config = { + completed: { label: 'Completed', className: 'bg-green-500 text-white' }, + partial: { label: 'Partial Success', className: 'bg-orange-500 text-white' }, + failed: { label: 'Failed', className: 'bg-red-500 text-white' }, + }; + + const statusConfig = config[status] || config.failed; + return ( + + {statusConfig.label} + + ); + }; + + return ( + + + + + + {importResult ? 'Import Results' : 'Import Members from CSV'} + + + {importResult + ? 'Review the import results below' + : 'Upload a CSV file to bulk import members. Ensure the CSV has the required columns.'} + + + + {!importResult ? ( + // Upload Form +
+ {/* CSV Format Instructions */} + + + Required columns: Email, First Name, Last Name, Phone, Role +
+ Optional columns: Status, Address, City, State, Zipcode, Date of Birth, Member Since +
+ Date format: YYYY-MM-DD (e.g., 2024-01-15) +
+
+ + {/* File Upload Area */} +
+ {file ? ( +
+ +
+

+ {file.name} +

+

+ {(file.size / 1024).toFixed(2)} KB +

+
+ +
+ ) : ( +
+ +
+

+ Drag and drop your CSV file here +

+

or

+ + +
+
+ )} +
+ + {/* Options */} +
+ + +
+
+ ) : ( + // Import Results +
+ {/* Summary Cards */} +
+
+

Total Rows

+

{importResult.total_rows}

+
+
+

Successful

+

{importResult.successful_rows}

+
+
+

Failed

+

{importResult.failed_rows}

+
+
+ {getStatusIcon(importResult.status)} + {getStatusBadge(importResult.status)} +
+
+ + {/* Errors Table */} + {importResult.errors && importResult.errors.length > 0 && ( +
+

+ Errors ({importResult.errors.length} {importResult.errors.length === 10 ? '- showing first 10' : ''}) +

+
+ + + + Row + Email + Error + + + + {importResult.errors.map((error, idx) => ( + + {error.row} + {error.email} + {error.error} + + ))} + +
+
+
+ )} +
+ )} + + + {!importResult ? ( + <> + + + + ) : ( + + )} + +
+
+ ); +}; + +export default ImportMembersDialog; diff --git a/src/components/InviteStaffDialog.js b/src/components/InviteStaffDialog.js new file mode 100644 index 0000000..b15d4ae --- /dev/null +++ b/src/components/InviteStaffDialog.js @@ -0,0 +1,313 @@ +import React, { useState, useEffect } from 'react'; +import api from '../utils/api'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from './ui/dialog'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; +import { Label } from './ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; +import { toast } from 'sonner'; +import { Loader2, Mail, Copy, Check } from 'lucide-react'; + +const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => { + const [formData, setFormData] = useState({ + email: '', + first_name: '', + last_name: '', + phone: '', + role: 'admin' + }); + const [loading, setLoading] = useState(false); + const [errors, setErrors] = useState({}); + const [invitationUrl, setInvitationUrl] = useState(null); + const [copied, setCopied] = useState(false); + const [roles, setRoles] = useState([]); + const [loadingRoles, setLoadingRoles] = useState(false); + + // Fetch roles when dialog opens + useEffect(() => { + if (open) { + fetchRoles(); + } + }, [open]); + + const fetchRoles = async () => { + setLoadingRoles(true); + try { + const response = await api.get('/admin/roles'); + // Filter to show only admin-type roles (not guest or member) + const staffRoles = response.data.filter(role => + ['admin', 'superadmin', 'finance'].includes(role.code) || !role.is_system_role + ); + setRoles(staffRoles); + } catch (error) { + console.error('Failed to fetch roles:', error); + toast.error('Failed to load roles'); + } finally { + setLoadingRoles(false); + } + }; + + const handleChange = (field, value) => { + setFormData(prev => ({ ...prev, [field]: value })); + // Clear error when user starts typing + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: null })); + } + }; + + const validate = () => { + const newErrors = {}; + + if (!formData.email) { + newErrors.email = 'Email is required'; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + newErrors.email = 'Invalid email format'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!validate()) { + return; + } + + setLoading(true); + + try { + const response = await api.post('/admin/users/invite', formData); + toast.success('Invitation sent successfully'); + + // Show invitation URL + setInvitationUrl(response.data.invitation_url); + + // Don't close dialog yet - show invitation URL first + if (onSuccess) onSuccess(); + } catch (error) { + const errorMessage = error.response?.data?.detail || 'Failed to send invitation'; + toast.error(errorMessage); + } finally { + setLoading(false); + } + }; + + const copyToClipboard = () => { + navigator.clipboard.writeText(invitationUrl); + setCopied(true); + toast.success('Invitation link copied to clipboard'); + setTimeout(() => setCopied(false), 2000); + }; + + const handleClose = () => { + // Reset form + setFormData({ + email: '', + first_name: '', + last_name: '', + phone: '', + role: 'admin' + }); + setInvitationUrl(null); + setCopied(false); + onOpenChange(false); + }; + + return ( + + + + + + {invitationUrl ? 'Invitation Sent' : 'Invite Staff Member'} + + + {invitationUrl + ? 'The invitation has been sent via email. You can also copy the link below.' + : 'Send an email invitation to join as staff. They will set their own password.'} + + + + {invitationUrl ? ( + // Show invitation URL after successful send +
+ +
+ + +
+
+ ) : ( + // Show invitation form +
+
+ {/* Email */} +
+ + handleChange('email', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="staff@example.com" + /> + {errors.email && ( +

{errors.email}

+ )} +
+ + {/* First Name (Optional) */} +
+ + handleChange('first_name', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="John" + /> +
+ + {/* Last Name (Optional) */} +
+ + handleChange('last_name', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="Doe" + /> +
+ + {/* Phone (Optional) */} +
+ + handleChange('phone', e.target.value)} + className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" + placeholder="(555) 123-4567" + /> +
+ + {/* Role */} +
+ + + {roles.length > 0 && ( +

+ {roles.find(r => r.code === formData.role)?.description || ''} +

+ )} +
+
+ + + + + +
+ )} + + {invitationUrl && ( + + + + )} +
+
+ ); +}; + +export default InviteStaffDialog; diff --git a/src/components/Navbar.js b/src/components/Navbar.js index 4b700fc..95e50c4 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -32,7 +32,7 @@ const Navbar = () => { Welcome, {user.first_name} )} - {user?.role === 'admin' && ( + {(user?.role === 'admin' || user?.role === 'superadmin') && ( -
-

- LOAF is supported by
the Hollyfield Foundation -

-
+

+ LOAF is supported by
the Hollyfield Foundation +

{/* Bottom Footer */} -