'use client'; import {useState} from 'react'; import {proportional} from '@astryxdesign/core/Table'; import type {TableColumn} from '@astryxdesign/core/Table'; import {VStack, HStack} from '@astryxdesign/core/Layout'; import {Text} from '@astryxdesign/core/Text'; import {Badge} from '@astryxdesign/core/Badge'; import {StatusDot} from '@astryxdesign/core/StatusDot'; import {Avatar} from '@astryxdesign/core/Avatar'; import {PanelCard} from '@/shared/components/patterns/PanelCard'; import {ResponsiveTable} from '@/shared/components/patterns/ResponsiveTable'; import {SkeletonRows} from '@/shared/components/patterns/LoadingState'; import {EmptyPanel} from '@/shared/components/patterns/EmptyPanel'; import {Button} from '@astryxdesign/core/Button'; import {Banner} from '@astryxdesign/core/Banner'; import {useInvitations, useTeam} from '@/features/team/hooks/useTeam'; import {teamRepository} from '@/features/team/repositories/teamRepository'; import {AddMemberDialog} from '@/features/team/components/AddMemberDialog'; import {SecretOnce} from '@/shared/components/patterns/SecretOnce'; import type {TeamMember} from '@/features/team/types/team'; /** * Who has access to this console. * * ── What this replaced ─────────────────────────────────────────────────── * Five invented people — `aravind@nearle.in`, `Vikram Seth`, `Priya Sharma` — * held in `useState`, with store names (`Indiranagar Flagship`, * `Koramangala 80ft`) that no endpoint supplies, roles (`Cashier`, * `Marketing`, `Support`) that do not exist upstream, and four controls that * changed nothing: Add Staff, Suspend, Reset password and Remove all mutated * local state and were lost on refresh. A merchant could not tell any of it * from the real thing. * * ── Why the actions are here now ───────────────────────────────────────── * This was read-only, because `PATCH /api/team/{id}`, the invitation endpoints * and `POST /api/team/members` all answered 404 on the deployed platform when * it was built. They do not any more, so the screen does what it always * described: lists who has access and changes it. * * Deactivating revokes every session that person holds IMMEDIATELY — it is not * a flag that takes effect at their next sign-in. The platform also refuses to * remove the last active owner, answering `409 last_owner`; that message is * surfaced verbatim rather than flattened into "something went wrong", because * it names a rule the merchant can act on. */ interface TeamRow extends Record { id: string; name: string; email: string; role: string; status: string; lastLogin: string; } /** * Read off the payload, never inferred. The platform sends a boolean; there is * no "invited" or "suspended" state on this endpoint, so the screen does not * invent one. */ function toRow(m: TeamMember): TeamRow { return { id: m.id, name: m.name, email: m.email, role: m.role, status: m.active ? 'Active' : 'Inactive', // "Never" is a fact worth showing: an account created and never used is // exactly the row a manager wants to notice. lastLogin: m.lastLoginAt ? new Date(m.lastLoginAt).toLocaleDateString() : 'Never', }; } const COLUMNS: TableColumn[] = [ { key: 'name', header: 'Member', width: proportional(2), renderCell: (row) => ( {row.name} {row.email} ), }, { key: 'role', header: 'Role', width: proportional(1), renderCell: (row) => , }, { key: 'status', header: 'Access', width: proportional(1), renderCell: (row) => ( {row.status} ), }, {key: 'lastLogin', header: 'Last sign-in', width: proportional(1), align: 'end'}, ]; export function TeamManagement() { const team = useTeam(); const invitations = useInvitations(); const [adding, setAdding] = useState(false); const [error, setError] = useState(null); const [reset, setReset] = useState<{label: string; value: string} | null>( null, ); function refresh() { team.refetch(); invitations.refetch(); } async function setActive(id: string, active: boolean) { setError(null); const res = await teamRepository.update(id, {active}); // `last_owner` is the one failure worth repeating word for word: it names a // rule rather than a fault, and the merchant can act on it. if (!res.ok) setError(res.message ?? 'Could not change that account.'); refresh(); } async function resetPassword(id: string, email: string) { setError(null); const res = await teamRepository.resetPassword(id); if (!res.ok || !res.data) { setError(res.message ?? 'Could not reset that password.'); return; } setReset({label: `New password for ${email}`, value: res.data.password}); } return ( } empty={ } > {(members) => ( {error ? : null} {reset ? ( ) : null} An invitation lets somebody choose their own password; a created login shows one once. Deactivating signs that person out at once.