import { useState } from 'react' import { api } from '../api.js' import { usePolled } from '../hooks.js' import { ago, until, Loading, Problem } from './Sites.jsx' // The people who work here, and how somebody new gets an account. // // Registration is by invitation, never open signup — the same line the platform // draws around creating a company. What was missing was not openness: it was // that a shop could not add a SECOND person at all without somebody running a // command on the server, so five members of staff shared one password and a // phone app for the shop floor could not exist. // // A manager mints a code and hands it over; the holder chooses their own // password. The code carries the address and the role, so passing it on cannot // turn a staff invitation into an owner account for whoever received it. export default function Team({ user }) { const team = usePolled(() => api.team(), 0, []) const invites = usePolled(() => api.invitations(), 0, []) const [inviting, setInviting] = useState(false) const [minted, setMinted] = useState(null) const [busy, setBusy] = useState('') const [error, setError] = useState('') const canManage = user.role === 'owner' || user.role === 'manager' const members = team.data || [] const pending = invites.data || [] const change = async (id, changes) => { setBusy(id); setError('') try { await api.updateMember(id, changes) team.reload() } catch (err) { setError(err.message) } finally { setBusy('') } } return ( <>

Team

{canManage && ( )}
{minted && setMinted(null)} />} {error &&

{error}

} {team.loading && !team.data ? : team.error ? : (
{canManage && {members.map(m => ( {canManage && ( )} ))}
NameEmailRoleLast signed in}
{m.full_name || '—'} {!m.active && No access} {m.email} {canManage && m.id !== user.id ? ( ) : {m.role}} {m.last_login_at ? ago(m.last_login_at) : 'Never'} {m.id === user.id ? null : m.active ? ( ) : ( )}
)} {canManage && pending.length > 0 && (

Waiting to join

    {pending.map(i => (
  • {i.email} invited as {i.role} {i.invited_by ? ` by ${i.invited_by}` : ''} · expires {until(i.expires_at)}
  • ))}
)} {inviting && ( setInviting(false)} onDone={(inv) => { setInviting(false); setMinted(inv); invites.reload() }} /> )} ) } function InviteForm({ canMintOwner, onClose, onDone }) { const [form, setForm] = useState({ email: '', full_name: '', role: 'staff' }) const [busy, setBusy] = useState(false) const [error, setError] = useState('') const set = (k) => (e) => setForm({ ...form, [k]: e.target.value }) const submit = async (e) => { e.preventDefault() setBusy(true); setError('') try { onDone(await api.invite(form)) } catch (err) { setError(err.message) setBusy(false) } } return (
) } // Shown once, and it says so. Only a hash is stored, so this cannot be read // back later — the same rule every other secret in this product follows, and // the reason is the same: a code support can look up is a code anybody with // support access can redeem. function InviteCode({ invite, onDismiss }) { return (
Invitation for {invite.email}. Give them this code. It is shown once, works once, and cannot be recovered.
Code
{invite.code}
Role
{invite.role}

They open the app, choose “I have an invitation code”, and pick their own password. Nobody else ever sees it.

) }