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 (
);
}
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.
{canEditOwnOrganization && (
Organization profile
)}
{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
);
}