docs: registrar fluxo do updater e atualizar chaves
This commit is contained in:
parent
206d00700e
commit
b5fd920efd
50 changed files with 980 additions and 93 deletions
|
|
@ -119,7 +119,7 @@ type MachineInventory = {
|
|||
disks?: Array<{ name?: string; mountPoint?: string; fs?: string; interface?: string | null; serial?: string | null; totalBytes?: number; availableBytes?: number }>
|
||||
extended?: { linux?: LinuxExtended; windows?: WindowsExtended; macos?: MacExtended }
|
||||
services?: Array<{ name?: string; status?: string; displayName?: string }>
|
||||
collaborator?: { email?: string; name?: string }
|
||||
collaborator?: { email?: string; name?: string; role?: string }
|
||||
}
|
||||
|
||||
export type MachinesQueryItem = {
|
||||
|
|
@ -135,6 +135,11 @@ export type MachinesQueryItem = {
|
|||
serialNumbers: string[]
|
||||
authUserId: string | null
|
||||
authEmail: string | null
|
||||
persona: string | null
|
||||
assignedUserId: string | null
|
||||
assignedUserEmail: string | null
|
||||
assignedUserName: string | null
|
||||
assignedUserRole: string | null
|
||||
status: string | null
|
||||
lastHeartbeatAt: number | null
|
||||
heartbeatAgeMs: number | null
|
||||
|
|
@ -209,12 +214,6 @@ function formatPercent(value?: number | null) {
|
|||
return `${normalized.toFixed(0)}%`
|
||||
}
|
||||
|
||||
function fmtBool(value: unknown) {
|
||||
if (value === true) return "Sim"
|
||||
if (value === false) return "Não"
|
||||
return "—"
|
||||
}
|
||||
|
||||
function readBool(source: unknown, key: string): boolean | undefined {
|
||||
if (!source || typeof source !== "object") return undefined
|
||||
const value = (source as Record<string, unknown>)[key]
|
||||
|
|
@ -490,15 +489,31 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
}
|
||||
}
|
||||
|
||||
// collaborator (from inventory metadata, when provided by onboarding)
|
||||
type Collaborator = { email?: string; name?: string }
|
||||
const collaborator: Collaborator | null = (() => {
|
||||
// collaborator (from machine assignment or metadata)
|
||||
type Collaborator = { email?: string; name?: string; role?: string }
|
||||
const collaborator: Collaborator | null = useMemo(() => {
|
||||
if (machine?.assignedUserEmail) {
|
||||
return {
|
||||
email: machine.assignedUserEmail ?? undefined,
|
||||
name: machine.assignedUserName ?? undefined,
|
||||
role: machine.persona ?? machine.assignedUserRole ?? undefined,
|
||||
}
|
||||
}
|
||||
if (!metadata || typeof metadata !== "object") return null
|
||||
const inv = metadata as Record<string, unknown>
|
||||
const c = inv["collaborator"]
|
||||
if (c && typeof c === "object") return c as Collaborator
|
||||
if (c && typeof c === "object") {
|
||||
const base = c as Record<string, unknown>
|
||||
return {
|
||||
email: typeof base.email === "string" ? base.email : undefined,
|
||||
name: typeof base.name === "string" ? base.name : undefined,
|
||||
role: typeof base.role === "string" ? (base.role as string) : undefined,
|
||||
}
|
||||
}
|
||||
return null
|
||||
})()
|
||||
}, [machine?.assignedUserEmail, machine?.assignedUserName, machine?.persona, machine?.assignedUserRole, metadata])
|
||||
|
||||
const personaLabel = collaborator?.role === "manager" ? "Gestor" : "Colaborador"
|
||||
|
||||
const companyName = (() => {
|
||||
if (!companies || !machine?.companySlug) return machine?.companySlug ?? null
|
||||
|
|
@ -513,6 +528,13 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
const [dialogQuery, setDialogQuery] = useState("")
|
||||
const [deleteDialog, setDeleteDialog] = useState(false)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const [accessDialog, setAccessDialog] = useState(false)
|
||||
const [accessEmail, setAccessEmail] = useState<string>(collaborator?.email ?? "")
|
||||
const [accessName, setAccessName] = useState<string>(collaborator?.name ?? "")
|
||||
const [accessRole, setAccessRole] = useState<"collaborator" | "manager">(
|
||||
(machine?.persona === "manager" || collaborator?.role === "manager") ? "manager" : "collaborator"
|
||||
)
|
||||
const [savingAccess, setSavingAccess] = useState(false)
|
||||
const jsonText = useMemo(() => {
|
||||
const payload = {
|
||||
id: machine?.id,
|
||||
|
|
@ -535,6 +557,42 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
}, [jsonText, dialogQuery])
|
||||
|
||||
// removed copy/export inventory JSON buttons as requested
|
||||
useEffect(() => {
|
||||
setAccessEmail(collaborator?.email ?? "")
|
||||
setAccessName(collaborator?.name ?? "")
|
||||
setAccessRole((machine?.persona === "manager" || collaborator?.role === "manager") ? "manager" : "collaborator")
|
||||
}, [machine?.id, machine?.persona, collaborator?.email, collaborator?.name, collaborator?.role])
|
||||
|
||||
const handleSaveAccess = async () => {
|
||||
if (!machine) return
|
||||
if (!accessEmail.trim()) {
|
||||
toast.error("Informe o e-mail do colaborador ou gestor.")
|
||||
return
|
||||
}
|
||||
setSavingAccess(true)
|
||||
try {
|
||||
const response = await fetch("/api/admin/machines/access", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
machineId: machine.id,
|
||||
persona: accessRole,
|
||||
email: accessEmail.trim(),
|
||||
name: accessName.trim() || undefined,
|
||||
}),
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text())
|
||||
}
|
||||
toast.success("Perfil de acesso atualizado.")
|
||||
setAccessDialog(false)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
toast.error("Falha ao atualizar acesso da máquina.")
|
||||
} finally {
|
||||
setSavingAccess(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-slate-200">
|
||||
|
|
@ -594,17 +652,21 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
) : null}
|
||||
{collaborator?.email ? (
|
||||
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||
Colaborador: {collaborator?.name ? `${collaborator.name} · ` : ""}{collaborator.email}
|
||||
{personaLabel}: {collaborator?.name ? `${collaborator.name} · ` : ""}{collaborator.email}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{machine.authEmail ? (
|
||||
<Button size="sm" variant="outline" onClick={copyEmail} className="gap-2 border-dashed">
|
||||
<ClipboardCopy className="size-4" />
|
||||
Copiar e-mail
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{machine.authEmail ? (
|
||||
<Button size="sm" variant="outline" onClick={copyEmail} className="gap-2 border-dashed">
|
||||
<ClipboardCopy className="size-4" />
|
||||
Copiar e-mail
|
||||
</Button>
|
||||
) : null}
|
||||
<Button size="sm" variant="outline" className="gap-2 border-dashed" onClick={() => { setAccessDialog(true) }}>
|
||||
<ShieldCheck className="size-4" />
|
||||
Ajustar acesso
|
||||
</Button>
|
||||
{machine.registeredBy ? (
|
||||
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||
Registrada via {machine.registeredBy}
|
||||
|
|
@ -653,6 +715,42 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={accessDialog} onOpenChange={setAccessDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Ajustar acesso da máquina</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-3 py-2">
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm font-medium">Perfil</label>
|
||||
<Select value={accessRole} onValueChange={(value) => setAccessRole((value as "collaborator" | "manager") ?? "collaborator")}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione o perfil" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="collaborator">Colaborador (portal)</SelectItem>
|
||||
<SelectItem value="manager">Gestor (painel completo)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm font-medium">E-mail</label>
|
||||
<Input type="email" value={accessEmail} onChange={(e) => setAccessEmail(e.target.value)} placeholder="colaborador@empresa.com" />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm font-medium">Nome (opcional)</label>
|
||||
<Input value={accessName} onChange={(e) => setAccessName(e.target.value)} placeholder="Nome completo" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setAccessDialog(false)} disabled={savingAccess}>Cancelar</Button>
|
||||
<Button onClick={handleSaveAccess} disabled={savingAccess || !accessEmail.trim()}>
|
||||
{savingAccess ? "Salvando..." : "Salvar"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<section className="space-y-2">
|
||||
<h4 className="text-sm font-semibold">Sincronização</h4>
|
||||
<div className="grid gap-2 text-sm text-muted-foreground">
|
||||
|
|
@ -1372,16 +1470,27 @@ function MachineCard({ machine, companyName }: { machine: MachinesQueryItem; com
|
|||
const memPct = mm?.memoryUsedPercent ?? (Number.isFinite(memUsed) && Number.isFinite(memTotal) ? (Number(memUsed) / Number(memTotal)) * 100 : NaN)
|
||||
const cpuPct = mm?.cpuUsagePercent ?? NaN
|
||||
const collaborator = (() => {
|
||||
if (machine.assignedUserEmail) {
|
||||
return {
|
||||
email: machine.assignedUserEmail ?? undefined,
|
||||
name: machine.assignedUserName ?? undefined,
|
||||
role: machine.persona ?? machine.assignedUserRole ?? undefined,
|
||||
}
|
||||
}
|
||||
const inv = machine.inventory as unknown
|
||||
if (!inv || typeof inv !== "object") return null
|
||||
const raw = (inv as Record<string, unknown>).collaborator
|
||||
if (!raw || typeof raw !== "object") return null
|
||||
const obj = raw as Record<string, unknown>
|
||||
const email = typeof obj.email === "string" ? obj.email : undefined
|
||||
const name = typeof obj.name === "string" ? obj.name : undefined
|
||||
if (!email) return null
|
||||
return { email, name }
|
||||
return {
|
||||
email,
|
||||
name: typeof obj.name === "string" ? obj.name : undefined,
|
||||
role: typeof obj.role === "string" ? (obj.role as string) : undefined,
|
||||
}
|
||||
})()
|
||||
const persona = collaborator?.role === "manager" ? "Gestor" : "Colaborador"
|
||||
const companyLabel = companyName ?? machine.companySlug ?? null
|
||||
|
||||
return (
|
||||
|
|
@ -1427,7 +1536,7 @@ function MachineCard({ machine, companyName }: { machine: MachinesQueryItem; com
|
|||
</div>
|
||||
{collaborator?.email ? (
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{collaborator.name ? `${collaborator.name} · ` : ""}
|
||||
{persona}: {collaborator.name ? `${collaborator.name} · ` : ""}
|
||||
{collaborator.email}
|
||||
</p>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -23,17 +23,21 @@ const navItems = [
|
|||
export function PortalShell({ children }: PortalShellProps) {
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const { session } = useAuth()
|
||||
const { session, machineContext } = useAuth()
|
||||
const [isSigningOut, setIsSigningOut] = useState(false)
|
||||
|
||||
const displayName = machineContext?.assignedUserName ?? session?.user.name ?? session?.user.email ?? "Cliente"
|
||||
const displayEmail = machineContext?.assignedUserEmail ?? session?.user.email ?? ""
|
||||
const personaLabel = machineContext?.persona === "manager" ? "Gestor" : "Colaborador"
|
||||
|
||||
const initials = useMemo(() => {
|
||||
const name = session?.user.name || session?.user.email || "Cliente"
|
||||
const name = displayName || displayEmail || "Cliente"
|
||||
return name
|
||||
.split(" ")
|
||||
.slice(0, 2)
|
||||
.map((part) => part.charAt(0).toUpperCase())
|
||||
.join("")
|
||||
}, [session?.user.name, session?.user.email])
|
||||
}, [displayName, displayEmail])
|
||||
|
||||
async function handleSignOut() {
|
||||
if (isSigningOut) return
|
||||
|
|
@ -85,12 +89,15 @@ export function PortalShell({ children }: PortalShellProps) {
|
|||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Avatar className="size-9 border border-slate-200">
|
||||
<AvatarImage src={session?.user.avatarUrl ?? undefined} alt={session?.user.name ?? ""} />
|
||||
<AvatarImage src={session?.user.avatarUrl ?? undefined} alt={displayName ?? ""} />
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col leading-tight">
|
||||
<span className="font-semibold text-neutral-900">{session?.user.name ?? "Cliente"}</span>
|
||||
<span className="text-xs text-neutral-500">{session?.user.email ?? ""}</span>
|
||||
<span className="font-semibold text-neutral-900">{displayName}</span>
|
||||
<span className="text-xs text-neutral-500">{displayEmail}</span>
|
||||
{machineContext ? (
|
||||
<span className="text-[10px] uppercase tracking-wide text-neutral-400">{personaLabel}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue