import { useEffect, useState } from 'react' import { useMutation } from '@tanstack/react-query' import { KeyRound, LogOut, ShieldCheck, UserCog } from 'lucide-react' import { Link } from 'react-router-dom' import { authApi } from '../lib/auth' import { useAuth } from '../lib/useAuth' import { CAP, can } from '../lib/can' import { TimeZonePanel } from './TimeZonePanel' import { InviteDefaultsPanel, MailPanel, RecordScopePanel, SettingsLoadFailure, WhoSeesWhatPanel, WrapRatePanel, AedtNoticePanel, } from './OrgSettingsPanels' import { Badge, Panel } from '../components/ui' export function Settings() { const { principal, logout } = useAuth() return (
{/* SELF SERVICE PROFILE, amendment 6 decision 4, Track U item 2. It sits FIRST and above the password form because it is the thing a person most often comes here to do, and because the defect this closes was that there was no path at all: a name was written once at invite acceptance and never again, by anyone, so correcting a typo escalated to the top of the ladder and failed there too. Every role sees this panel; editing your own name is not an act of authority, so there is no capability check around it. */} {/* Account management lives on ONE screen. Settings used to carry its own Team panel: a second user list on a different query key, whose invite sent no tool set and so created accounts entitled to nothing, and whose role changes did not invalidate the other screen. Two surfaces for the same thing disagreed about what access a person had, which is the worst possible thing for an access control screen to be ambiguous about. */} {can(principal, CAP.userInvite) && (

Create accounts, set roles, and choose which tools each person can see.

Open accounts and access
)} {/* Owner only: the server refuses these for anyone else, so offering them would be buttons that fail. The order is the order an owner sets a company up in. Mail comes first because it is the one that unblocks everything else: until a message can leave, an invitation is a link somebody has to copy into their own mail client by hand. Then who sees what, then which records they see at all, then the defaults nobody thinks about until one of them is wrong. The TIME ZONE stays where it was, in its own panel, and is deliberately not repeated here. It lives on the organization and drives the task queue, the daily report and every period rollup. A second control would let somebody set a working day that the rest of the product ignores. */} {can(principal, CAP.orgSettings) && ( <> )}

Federated SSO (Login.gov, Entra Gov, PIV CAC) and public signup are a later sub phase. Invites are delivered manually at pilot, the link is shown after you invite a teammate.

) } const fieldCls = 'w-full rounded-md border border-line/70 bg-navy-950/60 px-3 py-1.5 text-sm outline-none focus:border-blue/60' // ProfilePanel is the self service identity edit, available to every role on their OWN account. // // The server takes the subject from the session, never from this form, so there is no user id in the // request and no way to aim it at another account. On success the session is refreshed rather than the // local state patched, so the new name comes back from the server and renders everywhere the identity // renders (the shell header, the accounts list) instead of only in the box that was typed into. function ProfilePanel() { const { principal, refresh } = useAuth() const [fullName, setFullName] = useState('') const [title, setTitle] = useState('') const [phone, setPhone] = useState('') const [msg, setMsg] = useState('') // Seeded from the session once it resolves. Without this the fields render empty on first paint and an // unedited save would read as blanking a title the person already had. useEffect(() => { setFullName(principal?.full_name ?? '') setTitle(principal?.title ?? '') setPhone(principal?.phone ?? '') }, [principal?.full_name, principal?.title, principal?.phone]) const save = useMutation({ mutationFn: () => authApi.updateOwnProfile({ full_name: fullName, title, phone }), onSuccess: async () => { setMsg('Saved. Your name now shows everywhere you appear.') await refresh() }, onError: (e: Error) => setMsg(e.message || 'Could not save your profile.'), }) const dirty = fullName !== (principal?.full_name ?? '') || title !== (principal?.title ?? '') || phone !== (principal?.phone ?? '') return (
{ e.preventDefault() setMsg('') if (!fullName.trim()) { setMsg('A name is required.') return } save.mutate() }} >

Your email address is your sign in and is not changed here. Ask an administrator to change it.

{msg &&

{msg}

}
) } function AccountPanel({ onLogout, role, orgID }: { onLogout: () => void; role: string; orgID: string }) { const [oldPw, setOldPw] = useState('') const [newPw, setNewPw] = useState('') const [msg, setMsg] = useState('') const change = useMutation({ mutationFn: () => authApi.changePassword(oldPw, newPw), onSuccess: () => { setMsg('Password changed.') setOldPw('') setNewPw('') }, onError: () => setMsg('Could not change the password. Check the current one.'), }) const logoutAll = useMutation({ mutationFn: () => authApi.logoutAll() }) return (
{role || 'unknown role'} org {orgID.slice(0, 8)}
{ e.preventDefault() setMsg('') if (newPw.length < 8) { setMsg('Use at least 8 characters.') return } change.mutate() }} >
Change password
{msg &&

{msg}

}
) }