import type { FormEvent } from 'react'; import { useMemo, useState } from 'react'; import { ClipboardList, FileCheck, KeyRound, Loader2, UserCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { ModuleHeader } from '@/components/ui/module-header'; import { NativeSelect } from '@/components/ui/native-select'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { useAuth } from '@/contexts/useAuth'; import { useIamCapabilities } from '@/business/iam-capabilities/hooks'; import { getAuthRoleLabel } from '@/business/auth/selectors'; import { hasPermission } from '@/business/auth/permissions'; import { changePassword, updateOwnProfile, updateOrganization } from '@/business/profile/api'; import { buildProfileDocumentAcknowledgmentRows, buildProfileQuizResultRows, } from '@/business/profile/selectors'; import { getErrorMessage } from '@/shared/errors/errorMessages'; import { USER_NAME_PREFIX_OPTIONS } from '@/shared/constants/users'; import { ImageUpload } from '@/components/common/ImageUpload'; import { useCurrentPersonalityResultHistory } from '@/business/personality/queryHooks'; import { useMySafetyQuizStatus } from '@/business/safety-quiz/hooks'; import { useTodayZoneCheckIn } from '@/business/zone-checkin/hooks'; import { canZoneCheckIn } from '@/business/zone-checkin/selectors'; import { usePolicies, usePolicyAcknowledgments } from '@/business/policies/hooks'; import { useSafetyProtocols } from '@/business/safety-protocols/hooks'; interface StatusMessage { readonly type: 'success' | 'error'; readonly text: string; } const formControlClassName = 'border-slate-600 bg-slate-950/80 text-slate-100 placeholder:text-slate-500 focus-visible:ring-sky-400 focus-visible:ring-offset-slate-950'; const profileCardClassName = 'border-slate-600/70 bg-slate-900/80 shadow-lg shadow-black/20'; const formPanelClassName = 'rounded-lg border border-slate-600/80 bg-slate-950/45 p-4'; function StatusBanner({ status }: { status: StatusMessage | null }) { if (!status) { return null; } const className = status.type === 'success' ? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-200' : 'border-red-500/30 bg-red-500/10 text-red-200'; return (
{status.text}
); } function ReadOnlyField({ label, value }: { label: string; value: string }) { return (

{label}

{value || '—'}

); } export default function ProfilePage() { const { user, profile, refreshUser } = useAuth(); const capabilitiesQuery = useIamCapabilities(); const isExternalProfileUser = user?.app_role?.name === 'student' || user?.app_role?.name === 'guardian'; const canShowQuizResults = Boolean(user) && !isExternalProfileUser; const safetyQuizStatus = useMySafetyQuizStatus(undefined, canShowQuizResults); const personalityHistoryStatus = useCurrentPersonalityResultHistory(canShowQuizResults); const canUseZoneCheckin = canZoneCheckIn(user); const zoneCheckinStatus = useTodayZoneCheckIn({ enabled: canShowQuizResults && canUseZoneCheckin }); const canReadAcknowledgedDocuments = hasPermission(user, 'ACK_POLICY'); const policyAcknowledgmentsStatus = usePolicyAcknowledgments(canReadAcknowledgedDocuments); const handbookPoliciesStatus = usePolicies(canReadAcknowledgedDocuments); const safetyProtocolsStatus = useSafetyProtocols(canReadAcknowledgedDocuments); const [namePrefix, setNamePrefix] = useState(user?.name_prefix ?? ''); const [firstName, setFirstName] = useState(user?.firstName ?? ''); const [lastName, setLastName] = useState(user?.lastName ?? ''); const [phoneNumber, setPhoneNumber] = useState(user?.phoneNumber ?? ''); const [email, setEmail] = useState(user?.email ?? ''); const [profileSaving, setProfileSaving] = useState(false); const [profileStatus, setProfileStatus] = useState(null); const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [passwordSaving, setPasswordSaving] = useState(false); const [passwordStatus, setPasswordStatus] = useState(null); const [avatar, setAvatar] = useState(user?.avatar ?? null); const [avatarStatus, setAvatarStatus] = useState(null); const [organizationName, setOrganizationName] = useState(user?.organizations?.name ?? ''); const [organizationLogo, setOrganizationLogo] = useState( user?.organizations?.logo ?? null, ); const [organizationSaving, setOrganizationSaving] = useState(false); const [organizationStatus, setOrganizationStatus] = useState(null); const roleLabel = profile ? getAuthRoleLabel(profile.role) : '—'; const canEditOwnOrganization = capabilitiesQuery.data?.canEditOwnOrganization === true && Boolean(user?.organizations?.id); const tenantChain = useMemo(() => { const parts = [ user?.organizations?.name, user?.school?.name, user?.campus?.name ?? user?.campus?.code, user?.classRoom?.name, ].filter((part): part is string => Boolean(part)); return parts.length > 0 ? parts.join(' › ') : 'Platform'; }, [user]); const quizResultRows = useMemo( () => buildProfileQuizResultRows( safetyQuizStatus.data?.result ?? null, personalityHistoryStatus.data ?? [], zoneCheckinStatus.isCheckedInToday && zoneCheckinStatus.todayZone ? { date: zoneCheckinStatus.todayDate ?? new Date().toISOString().slice(0, 10), zone: zoneCheckinStatus.todayZone, isCheckedInToday: true, } : null, canUseZoneCheckin, ), [ personalityHistoryStatus.data, safetyQuizStatus.data?.result, zoneCheckinStatus.isCheckedInToday, zoneCheckinStatus.todayDate, zoneCheckinStatus.todayZone, canUseZoneCheckin, ], ); const documentAcknowledgmentRows = useMemo( () => buildProfileDocumentAcknowledgmentRows( handbookPoliciesStatus.data ?? [], safetyProtocolsStatus.data ?? [], policyAcknowledgmentsStatus.data ?? [], ), [ handbookPoliciesStatus.data, policyAcknowledgmentsStatus.data, safetyProtocolsStatus.data, ], ); if (!user) { return null; } async function handleProfileSubmit(event: FormEvent) { event.preventDefault(); setProfileStatus(null); setProfileSaving(true); try { await updateOwnProfile({ name_prefix: namePrefix === '' ? null : namePrefix, firstName: firstName.trim(), lastName: lastName.trim(), phoneNumber: phoneNumber.trim() || null, email: email.trim(), }); await refreshUser(); setProfileStatus({ type: 'success', text: 'Profile updated.' }); } catch (error) { setProfileStatus({ type: 'error', text: getErrorMessage(error, 'Could not update profile') }); } finally { setProfileSaving(false); } } async function handleAvatarChange(privateUrl: string | null) { setAvatar(privateUrl); setAvatarStatus(null); try { await updateOwnProfile({ avatar: privateUrl }); await refreshUser(); setAvatarStatus({ type: 'success', text: privateUrl ? 'Avatar updated.' : 'Avatar removed.', }); } catch (error) { setAvatarStatus({ type: 'error', text: getErrorMessage(error, 'Could not update avatar') }); } } async function handlePasswordSubmit(event: FormEvent) { event.preventDefault(); setPasswordStatus(null); if (newPassword !== confirmPassword) { setPasswordStatus({ type: 'error', text: 'New password and confirmation do not match.' }); return; } setPasswordSaving(true); try { await changePassword(currentPassword, newPassword); setCurrentPassword(''); setNewPassword(''); setConfirmPassword(''); setPasswordStatus({ type: 'success', text: 'Password updated.' }); } catch (error) { setPasswordStatus({ type: 'error', text: getErrorMessage(error, 'Could not update password') }); } finally { setPasswordSaving(false); } } async function handleOrganizationSubmit(event: FormEvent) { event.preventDefault(); const organizationId = user?.organizations?.id; if (!organizationId) return; setOrganizationStatus(null); setOrganizationSaving(true); try { await updateOrganization(organizationId, { name: organizationName.trim(), logo: organizationLogo ?? undefined, }); await refreshUser(); setOrganizationStatus({ type: 'success', text: 'Organization profile updated.' }); } catch (error) { setOrganizationStatus({ type: 'error', text: getErrorMessage(error, 'Could not update organization profile'), }); } finally { setOrganizationSaving(false); } } return (
Account details

Profile identity

Your visible name, contact details, and avatar.

setNamePrefix(event.target.value)} > {USER_NAME_PREFIX_OPTIONS.map((option) => ( ))}
setFirstName(event.target.value)} />
setLastName(event.target.value)} />
setPhoneNumber(event.target.value)} />
setEmail(event.target.value)} />
{canEditOwnOrganization && ( Organization profile

Organization identity

The name and logo shown for your organization.

setOrganizationName(event.target.value)} required />
)}
{canShowQuizResults && ( Quiz results
{safetyQuizStatus.isLoading || personalityHistoryStatus.isLoading || zoneCheckinStatus.isLoading ? (

Loading quiz results...

) : (
Quiz Category Result Completed {quizResultRows.map((result) => (

{result.quiz}

{result.category} {result.result} {result.completed}
))}
)}
)} {canReadAcknowledgedDocuments && ( Document acknowledgments
{( policyAcknowledgmentsStatus.isLoading || handbookPoliciesStatus.isLoading || safetyProtocolsStatus.isLoading ) ? (

Loading document acknowledgments...

) : documentAcknowledgmentRows.length === 0 ? (

No current documents are assigned yet.

) : (
Document Category Version Acknowledged on {documentAcknowledgmentRows.map((document) => (

{document.title}

{document.status === 'acknowledged' ? 'Acknowledged' : 'Not acknowledged'}
{document.category} {document.version} {document.acknowledgedAt ? new Date(document.acknowledgedAt).toLocaleDateString() : '—'}
))}
)}
)} Change password

Password credentials

Use your current password before setting a new one.

setCurrentPassword(event.target.value)} required />
setNewPassword(event.target.value)} required />
setConfirmPassword(event.target.value)} required />
); }