import { useState } from 'react' import { Archive, Crown, Plus, UserMinus, Users2 } from 'lucide-react' import { Panel } from '../components/ui' import { Empty, Note, Select, btnGhost, btnPrimary, fieldCls } from '../components/form' import { CAP, can } from '../lib/can' import { useAuth } from '../lib/useAuth' import { useAddTeamMember, useCreateTeam, useOrgSettings, useRemoveTeamMember, useTeams, useUpdateTeam, } from '../lib/settingsQueries' import type { Team } from '../lib/settings' // TEAMS, on the accounts screen rather than in settings. // // A team is an access grant when record scope is narrowed, so it belongs beside the roster that grants // roles, not beside the toggles. The server agrees: reading teams needs candidate.read, writing them needs // org.settings.write, the same authority as the setting that gives a team its meaning. // // Archived teams are hidden rather than listed greyed out. An archived desk grants nothing, so showing it // among the live ones invites somebody to add a person to a team that will not widen what they can see. export function TeamsPanel({ people }: { people: { id: string; full_name: string; role: string; status: string }[] }) { const { principal } = useAuth() const teams = useTeams() const settings = useOrgSettings() const create = useCreateTeam() const [newName, setNewName] = useState('') const mayWrite = can(principal, CAP.orgSettings) const all = teams.data?.teams ?? [] const active = all.filter((t) => t.status === 'active') const archivedCount = all.length - active.length // Why teams matter right now, stated from the settings actually in force rather than in the abstract. An // owner who has not narrowed anything is told that plainly, so they do not build an org chart the product // is currently ignoring. const s = settings.data?.settings const scopeNarrowed = !!s && (s.staff_record_scope === 'team' || s.manager_record_scope === 'team') const visibilityNarrowed = !!s && s.work_visibility === 'team' const sensitivityNarrowed = !!s && [s.rate_visibility, s.margin_visibility, s.contact_visibility].includes('team') const teamsMatter = scopeNarrowed || visibilityNarrowed || sensitivityNarrowed if (teams.isError) { return ( The teams could not be read. {(teams.error as Error)?.message} ) } return ( {s && !teamsMatter && (
Nothing is currently narrowed to a team, so these are an org chart rather than an access control. They start deciding what people see the moment record scope, work visibility or a sensitivity is set to a team in settings.
)} {scopeNarrowed && (
Record scope is narrowed to a team, so somebody on no team sees only their own records. Every person who works a desk needs to be on one.
)} {active.length === 0 ? ( ) : ( )} {mayWrite && (
{ e.preventDefault() const name = newName.trim() if (!name) return create.mutate({ name }, { onSuccess: () => setNewName('') }) }} > setNewName(e.target.value)} className={`${fieldCls} max-w-xs`} />
)} {create.isError && (
{(create.error as Error).message}
)} {archivedCount > 0 && (

{archivedCount === 1 ? '1 archived desk is' : `${archivedCount} archived desks are`} hidden. An archived desk grants nothing.

)}
) } function TeamRow({ team, people, mayWrite, }: { team: Team people: { id: string; full_name: string; role: string; status: string }[] mayWrite: boolean }) { const add = useAddTeamMember() const remove = useRemoveTeamMember() const update = useUpdateTeam() const [confirmArchive, setConfirmArchive] = useState(false) const memberIDs = new Set(team.members.map((m) => m.user_id)) // Only ACTIVE accounts can be added. A deactivated account cannot sign in, so putting one on a desk would // be a grant to nobody and would make the desk look larger than it is. const addable = people.filter((p) => !memberIDs.has(p.id) && p.status === 'active') // A lead has to be ON the desk. Offering somebody who is not would create a lead whose own scope does not // include the team they lead. const leadChoices = [ { value: '', label: 'No lead' }, ...team.members.map((m) => ({ value: m.user_id, label: m.name })), ] return (
  • {team.name} {team.members.length === 1 ? '1 person' : `${team.members.length} people`}
    {mayWrite && (
    ({ value: p.id, label: `${p.full_name} (${p.role})` })), ]} onChange={(v) => { if (v) add.mutate({ teamId: team.id, userId: v }) }} className="text-xs" />
    )} {(add.isError || remove.isError || update.isError) && (
    {((add.error ?? remove.error ?? update.error) as Error)?.message ?? 'That change did not apply.'}
    )}
  • ) }