Implement company provisioning codes and session tweaks
This commit is contained in:
parent
0fb9bf59b2
commit
2cba553efa
28 changed files with 1407 additions and 534 deletions
|
|
@ -55,13 +55,6 @@ type MachineRegisterResponse = {
|
|||
} | null
|
||||
}
|
||||
|
||||
type CompanyOption = {
|
||||
id: string
|
||||
tenantId: string
|
||||
name: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
type AgentConfig = {
|
||||
machineId: string
|
||||
tenantId?: string | null
|
||||
|
|
@ -92,7 +85,6 @@ declare global {
|
|||
|
||||
const STORE_FILENAME = "machine-agent.json"
|
||||
const DEFAULT_APP_URL = import.meta.env.MODE === "production" ? "https://tickets.esdrasrenan.com.br" : "http://localhost:3000"
|
||||
const DEFAULT_TENANT_ID = "tenant-atlas"
|
||||
|
||||
function normalizeUrl(value?: string | null, fallback = DEFAULT_APP_URL) {
|
||||
const trimmed = (value ?? fallback).trim()
|
||||
|
|
@ -156,15 +148,13 @@ function App() {
|
|||
const [showSecret, setShowSecret] = useState(false)
|
||||
const [isLaunchingSystem, setIsLaunchingSystem] = useState(false)
|
||||
|
||||
const [provisioningSecret, setProvisioningSecret] = useState("")
|
||||
const [tenantId, setTenantId] = useState("")
|
||||
const [company, setCompany] = useState("")
|
||||
const [provisioningCode, setProvisioningCode] = useState("")
|
||||
const [validatedCompany, setValidatedCompany] = useState<{ id: string; name: string; slug: string; tenantId: string } | null>(null)
|
||||
const [companyName, setCompanyName] = useState("")
|
||||
const [isValidatingCode, setIsValidatingCode] = useState(false)
|
||||
const [codeStatus, setCodeStatus] = useState<{ tone: "success" | "error"; message: string } | null>(null)
|
||||
const [collabEmail, setCollabEmail] = useState("")
|
||||
const [collabName, setCollabName] = useState("")
|
||||
const [accessRole, setAccessRole] = useState<"collaborator" | "manager">("collaborator")
|
||||
const [companyOptions, setCompanyOptions] = useState<CompanyOption[]>([])
|
||||
const [selectedCompany, setSelectedCompany] = useState<CompanyOption | null>(null)
|
||||
const [companyLookupPending, setCompanyLookupPending] = useState(false)
|
||||
const [updating, setUpdating] = useState(false)
|
||||
const [updateInfo, setUpdateInfo] = useState<{ message: string; tone: "info" | "success" | "error" } | null>({
|
||||
message: "Atualizações automáticas são verificadas a cada inicialização.",
|
||||
|
|
@ -182,11 +172,9 @@ function App() {
|
|||
setToken(t)
|
||||
const cfg = await readConfig(s)
|
||||
setConfig(cfg)
|
||||
setAccessRole(cfg?.accessRole ?? "collaborator")
|
||||
if (cfg?.collaboratorEmail) setCollabEmail(cfg.collaboratorEmail)
|
||||
if (cfg?.collaboratorName) setCollabName(cfg.collaboratorName)
|
||||
if (cfg?.companyName) setCompany(cfg.companyName)
|
||||
if (cfg?.tenantId) setTenantId(cfg.tenantId)
|
||||
if (cfg?.companyName) setCompanyName(cfg.companyName)
|
||||
if (!t) {
|
||||
const p = await invoke<MachineProfile>("collect_machine_profile")
|
||||
setProfile(p)
|
||||
|
|
@ -198,6 +186,32 @@ function App() {
|
|||
})()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!import.meta.env.DEV) return
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
const key = (event.key || "").toLowerCase()
|
||||
if (key === "f12" || (event.ctrlKey && event.shiftKey && key === "i")) {
|
||||
invoke("open_devtools").catch(() => {})
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
function onContextMenu(event: MouseEvent) {
|
||||
if (event.ctrlKey || event.shiftKey) {
|
||||
invoke("open_devtools").catch(() => {})
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown)
|
||||
window.addEventListener("contextmenu", onContextMenu)
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown)
|
||||
window.removeEventListener("contextmenu", onContextMenu)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!store || !config) return
|
||||
const email = collabEmail.trim()
|
||||
|
|
@ -206,8 +220,7 @@ function App() {
|
|||
const normalizedName = name.length > 0 ? name : null
|
||||
if (
|
||||
config.collaboratorEmail === normalizedEmail &&
|
||||
config.collaboratorName === normalizedName &&
|
||||
config.accessRole === accessRole
|
||||
config.collaboratorName === normalizedName
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
|
@ -215,11 +228,10 @@ function App() {
|
|||
...config,
|
||||
collaboratorEmail: normalizedEmail,
|
||||
collaboratorName: normalizedName,
|
||||
accessRole,
|
||||
}
|
||||
setConfig(nextConfig)
|
||||
writeConfig(store, nextConfig).catch((err) => console.error("Falha ao atualizar colaborador", err))
|
||||
}, [store, config, config?.collaboratorEmail, config?.collaboratorName, config?.accessRole, collabEmail, collabName, accessRole])
|
||||
}, [store, config, config?.collaboratorEmail, config?.collaboratorName, collabEmail, collabName])
|
||||
|
||||
useEffect(() => {
|
||||
if (!store || !config) return
|
||||
|
|
@ -235,6 +247,59 @@ function App() {
|
|||
}
|
||||
}, [store, config])
|
||||
|
||||
useEffect(() => {
|
||||
const trimmed = provisioningCode.trim()
|
||||
if (trimmed.length < 32) {
|
||||
setValidatedCompany(null)
|
||||
setCodeStatus(null)
|
||||
setCompanyName("")
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(async () => {
|
||||
setIsValidatingCode(true)
|
||||
try {
|
||||
const res = await fetch(`${apiBaseUrl}/api/machines/provisioning`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ provisioningCode: trimmed }),
|
||||
signal: controller.signal,
|
||||
})
|
||||
if (!res.ok) {
|
||||
const message = res.status === 404 ? "Código não encontrado" : "Falha ao validar código"
|
||||
if (!cancelled) {
|
||||
setValidatedCompany(null)
|
||||
setCompanyName("")
|
||||
setCodeStatus({ tone: "error", message })
|
||||
}
|
||||
return
|
||||
}
|
||||
const data = (await res.json()) as { company: { id: string; name: string; slug: string; tenantId: string } }
|
||||
if (!cancelled) {
|
||||
setValidatedCompany(data.company)
|
||||
setCompanyName(data.company.name)
|
||||
setCodeStatus({ tone: "success", message: `Empresa encontrada: ${data.company.name}` })
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
console.error("Falha ao validar código de provisionamento", error)
|
||||
setValidatedCompany(null)
|
||||
setCompanyName("")
|
||||
setCodeStatus({ tone: "error", message: "Não foi possível validar o código agora" })
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setIsValidatingCode(false)
|
||||
}
|
||||
}, 400)
|
||||
return () => {
|
||||
cancelled = true
|
||||
clearTimeout(timeout)
|
||||
controller.abort()
|
||||
setIsValidatingCode(false)
|
||||
}
|
||||
}, [provisioningCode])
|
||||
|
||||
const resolvedAppUrl = useMemo(() => {
|
||||
if (!config?.appUrl) return appUrl
|
||||
const normalized = normalizeUrl(config.appUrl, appUrl)
|
||||
|
|
@ -244,184 +309,97 @@ function App() {
|
|||
return normalized
|
||||
}, [config?.appUrl])
|
||||
|
||||
useEffect(() => {
|
||||
const trimmedSecret = provisioningSecret.trim()
|
||||
const query = company.trim()
|
||||
if (!trimmedSecret || query.length < 2) {
|
||||
setCompanyOptions([])
|
||||
setCompanyLookupPending(false)
|
||||
return
|
||||
}
|
||||
if (selectedCompany && selectedCompany.name.toLowerCase() === query.toLowerCase()) {
|
||||
setCompanyOptions([])
|
||||
setCompanyLookupPending(false)
|
||||
return
|
||||
}
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(async () => {
|
||||
setCompanyLookupPending(true)
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
params.set("search", query)
|
||||
const tenant = tenantId.trim()
|
||||
if (tenant) params.set("tenantId", tenant)
|
||||
const res = await fetch(`${apiBaseUrl}/api/machines/companies?${params.toString()}`, {
|
||||
headers: {
|
||||
"X-Machine-Secret": trimmedSecret,
|
||||
},
|
||||
signal: controller.signal,
|
||||
})
|
||||
if (!res.ok) {
|
||||
setCompanyOptions([])
|
||||
return
|
||||
}
|
||||
const data = (await res.json()) as { companies?: CompanyOption[] }
|
||||
setCompanyOptions(data.companies ?? [])
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === "AbortError") return
|
||||
if (typeof error === "object" && error && "name" in error && (error as { name?: string }).name === "AbortError") return
|
||||
console.error("Falha ao buscar empresas", error)
|
||||
} finally {
|
||||
setCompanyLookupPending(false)
|
||||
}
|
||||
}, 200)
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
controller.abort()
|
||||
setCompanyLookupPending(false)
|
||||
}
|
||||
}, [company, tenantId, provisioningSecret, selectedCompany])
|
||||
|
||||
function handleSelectCompany(option: CompanyOption) {
|
||||
setCompany(option.name)
|
||||
setSelectedCompany(option)
|
||||
setCompanyOptions([])
|
||||
if (!tenantId.trim()) {
|
||||
setTenantId(option.tenantId)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCompanyInputChange(value: string) {
|
||||
setCompany(value)
|
||||
setSelectedCompany((prev) => {
|
||||
if (!prev) return null
|
||||
return prev.name.toLowerCase() === value.trim().toLowerCase() ? prev : null
|
||||
})
|
||||
}
|
||||
|
||||
async function register() {
|
||||
if (!profile) return
|
||||
if (!provisioningSecret.trim()) { setError("Informe o código de provisionamento."); return }
|
||||
const trimmedCode = provisioningCode.trim().toLowerCase()
|
||||
if (trimmedCode.length < 32) {
|
||||
setError("Informe o código de provisionamento fornecido pela equipe.")
|
||||
return
|
||||
}
|
||||
if (!validatedCompany) {
|
||||
setError("Valide o código de provisionamento antes de registrar a máquina.")
|
||||
return
|
||||
}
|
||||
const normalizedEmail = collabEmail.trim().toLowerCase()
|
||||
if (!normalizedEmail) {
|
||||
setError("Informe o e-mail do colaborador ou gestor para vincular esta máquina.")
|
||||
setError("Informe o e-mail do colaborador vinculado a esta máquina.")
|
||||
return
|
||||
}
|
||||
const normalizedName = collabName.trim()
|
||||
if (!normalizedName) {
|
||||
setError("Informe o nome completo do colaborador ou gestor.")
|
||||
setError("Informe o nome completo do colaborador.")
|
||||
return
|
||||
}
|
||||
setBusy(true); setError(null)
|
||||
try {
|
||||
const trimmedTenantId = tenantId.trim()
|
||||
const trimmedCompanyName = company.trim()
|
||||
let ensuredCompany: { name: string; slug: string } | null = null
|
||||
if (trimmedCompanyName) {
|
||||
if (selectedCompany && selectedCompany.name.toLowerCase() === trimmedCompanyName.toLowerCase()) {
|
||||
ensuredCompany = { name: selectedCompany.name, slug: selectedCompany.slug }
|
||||
} else {
|
||||
const ensureRes = await fetch(`${apiBaseUrl}/api/machines/companies`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Machine-Secret": provisioningSecret.trim(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: trimmedTenantId || undefined,
|
||||
name: trimmedCompanyName,
|
||||
}),
|
||||
})
|
||||
if (!ensureRes.ok) {
|
||||
const text = await ensureRes.text()
|
||||
throw new Error(`Falha ao preparar empresa (${ensureRes.status}): ${text.slice(0, 300)}`)
|
||||
}
|
||||
const ensureData = (await ensureRes.json()) as { company?: { name: string; slug: string; tenantId?: string } }
|
||||
if (!ensureData.company?.slug) {
|
||||
throw new Error("Resposta inválida ao preparar empresa.")
|
||||
}
|
||||
ensuredCompany = { name: ensureData.company.name, slug: ensureData.company.slug }
|
||||
setSelectedCompany({
|
||||
id: ensureData.company.slug,
|
||||
name: ensureData.company.name,
|
||||
slug: ensureData.company.slug,
|
||||
tenantId: ensureData.company.tenantId ?? (trimmedTenantId || DEFAULT_TENANT_ID),
|
||||
})
|
||||
if (!trimmedTenantId && ensureData.company.tenantId) {
|
||||
setTenantId(ensureData.company.tenantId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setBusy(true)
|
||||
setError(null)
|
||||
try {
|
||||
const collaboratorPayload = {
|
||||
email: normalizedEmail,
|
||||
name: normalizedName,
|
||||
}
|
||||
const collaboratorMetadata = collaboratorPayload
|
||||
? { ...collaboratorPayload, role: accessRole }
|
||||
: undefined
|
||||
const metadataPayload: Record<string, unknown> = {
|
||||
inventory: profile.inventory,
|
||||
metrics: profile.metrics,
|
||||
collaborator: { email: normalizedEmail, name: normalizedName, role: "collaborator" },
|
||||
}
|
||||
if (collaboratorMetadata) {
|
||||
metadataPayload.collaborator = collaboratorMetadata
|
||||
}
|
||||
|
||||
const payload = {
|
||||
provisioningSecret: provisioningSecret.trim(),
|
||||
tenantId: trimmedTenantId || undefined,
|
||||
companySlug: ensuredCompany?.slug,
|
||||
provisioningCode: trimmedCode,
|
||||
hostname: profile.hostname,
|
||||
os: profile.os,
|
||||
macAddresses: profile.macAddresses,
|
||||
serialNumbers: profile.serialNumbers,
|
||||
metadata: metadataPayload,
|
||||
accessRole,
|
||||
collaborator: collaboratorPayload,
|
||||
registeredBy: "desktop-agent",
|
||||
}
|
||||
const res = await fetch(`${apiBaseUrl}/api/machines/register`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) })
|
||||
|
||||
const res = await fetch(`${apiBaseUrl}/api/machines/register`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
throw new Error(`Falha no registro (${res.status}): ${text.slice(0,300)}`)
|
||||
throw new Error(`Falha no registro (${res.status}): ${text.slice(0, 300)}`)
|
||||
}
|
||||
|
||||
const data = (await res.json()) as MachineRegisterResponse
|
||||
if (!store) throw new Error("Store ausente")
|
||||
|
||||
await writeToken(store, data.machineToken)
|
||||
|
||||
const cfg: AgentConfig = {
|
||||
machineId: data.machineId,
|
||||
tenantId: data.tenantId ?? null,
|
||||
companySlug: data.companySlug ?? ensuredCompany?.slug ?? null,
|
||||
companyName: ensuredCompany?.name ?? (trimmedCompanyName || null),
|
||||
tenantId: data.tenantId ?? validatedCompany.tenantId ?? null,
|
||||
companySlug: data.companySlug ?? validatedCompany.slug ?? null,
|
||||
companyName: validatedCompany.name,
|
||||
machineEmail: data.machineEmail ?? null,
|
||||
collaboratorEmail: collaboratorPayload?.email ?? null,
|
||||
collaboratorName: collaboratorPayload?.name ?? null,
|
||||
accessRole,
|
||||
collaboratorEmail: collaboratorPayload.email,
|
||||
collaboratorName: collaboratorPayload.name,
|
||||
accessRole: "collaborator",
|
||||
assignedUserId: data.assignedUserId ?? null,
|
||||
assignedUserEmail: data.collaborator?.email ?? collaboratorPayload?.email ?? null,
|
||||
assignedUserName: data.collaborator?.name ?? collaboratorPayload?.name ?? null,
|
||||
assignedUserEmail: data.collaborator?.email ?? collaboratorPayload.email,
|
||||
assignedUserName: data.collaborator?.name ?? collaboratorPayload.name,
|
||||
apiBaseUrl,
|
||||
appUrl,
|
||||
createdAt: Date.now(),
|
||||
lastSyncedAt: Date.now(),
|
||||
expiresAt: data.expiresAt ?? null,
|
||||
}
|
||||
|
||||
await writeConfig(store, cfg)
|
||||
setConfig(cfg); setToken(data.machineToken)
|
||||
if (ensuredCompany?.name) {
|
||||
setCompany(ensuredCompany.name)
|
||||
}
|
||||
await invoke("start_machine_agent", { baseUrl: apiBaseUrl, token: data.machineToken, status: "online", intervalSeconds: 300 })
|
||||
setConfig(cfg)
|
||||
setToken(data.machineToken)
|
||||
setCompanyName(validatedCompany.name)
|
||||
|
||||
await invoke("start_machine_agent", {
|
||||
baseUrl: apiBaseUrl,
|
||||
token: data.machineToken,
|
||||
status: "online",
|
||||
intervalSeconds: 300,
|
||||
})
|
||||
setStatus("online")
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err))
|
||||
|
|
@ -446,19 +424,22 @@ function App() {
|
|||
} catch {
|
||||
// ignoramos e seguimos para o handshake
|
||||
}
|
||||
const persona = (config?.accessRole ?? accessRole) === "manager" ? "manager" : "collaborator"
|
||||
// Envia para a página de diagnóstico para validar cookies/sessão
|
||||
const redirectTarget = persona === "manager" ? "/dashboard" : "/portal/debug"
|
||||
const persona = (config?.accessRole ?? "collaborator") === "manager" ? "manager" : "collaborator"
|
||||
// Envia para a página inicial apropriada após autenticar cookies/sessão
|
||||
const redirectTarget = persona === "manager" ? "/dashboard" : "/portal/tickets"
|
||||
const url = `${resolvedAppUrl}/machines/handshake?token=${encodeURIComponent(token)}&redirect=${encodeURIComponent(redirectTarget)}`
|
||||
window.location.href = url
|
||||
}, [token, config?.accessRole, accessRole, resolvedAppUrl, apiBaseUrl])
|
||||
}, [token, config?.accessRole, resolvedAppUrl])
|
||||
|
||||
async function reprovision() {
|
||||
if (!store) return
|
||||
await store.delete("token"); await store.delete("config"); await store.save()
|
||||
autoLaunchRef.current = false
|
||||
setToken(null); setConfig(null); setStatus(null); setAccessRole("collaborator")
|
||||
setCompany(""); setSelectedCompany(null); setCompanyOptions([])
|
||||
setToken(null); setConfig(null); setStatus(null)
|
||||
setProvisioningCode("")
|
||||
setValidatedCompany(null)
|
||||
setCodeStatus(null)
|
||||
setCompanyName("")
|
||||
setIsLaunchingSystem(false)
|
||||
const p = await invoke<MachineProfile>("collect_machine_profile")
|
||||
setProfile(p)
|
||||
|
|
@ -472,7 +453,7 @@ function App() {
|
|||
? { email: collabEmail.trim(), name: collabName.trim() || undefined }
|
||||
: undefined
|
||||
const collaboratorInventory = collaboratorPayload
|
||||
? { ...collaboratorPayload, role: accessRole }
|
||||
? { ...collaboratorPayload, role: "collaborator" as const }
|
||||
: undefined
|
||||
const inventoryPayload: Record<string, unknown> = { ...profile.inventory }
|
||||
if (collaboratorInventory) {
|
||||
|
|
@ -590,69 +571,69 @@ function App() {
|
|||
<div className="grid gap-2">
|
||||
<label className="text-sm font-medium">Código de provisionamento</label>
|
||||
<div className="relative">
|
||||
<input className="w-full rounded-lg border border-slate-300 px-3 py-2 pr-9 text-sm" type={showSecret?"text":"password"} value={provisioningSecret} onChange={(e)=>setProvisioningSecret(e.target.value)} />
|
||||
<button className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-slate-600" onClick={()=>setShowSecret((v)=>!v)} aria-label="Mostrar/ocultar">
|
||||
{showSecret ? <EyeOff className="size-4"/> : <Eye className="size-4"/>}
|
||||
<input
|
||||
className="w-full rounded-lg border border-slate-300 px-3 py-2 pr-9 text-sm"
|
||||
type={showSecret ? "text" : "password"}
|
||||
value={provisioningCode}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
setProvisioningCode(value)
|
||||
setValidatedCompany(null)
|
||||
setCodeStatus(null)
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-slate-600"
|
||||
onClick={() => setShowSecret((v) => !v)}
|
||||
aria-label="Mostrar/ocultar"
|
||||
>
|
||||
{showSecret ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm font-medium">Empresa (nome ou slug, opcional)</label>
|
||||
<input
|
||||
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm"
|
||||
placeholder="ex.: Paulicon Contábil"
|
||||
value={company}
|
||||
onChange={(e) => handleCompanyInputChange(e.target.value)}
|
||||
/>
|
||||
{companyLookupPending ? (
|
||||
<p className="text-xs text-slate-500">Buscando empresas...</p>
|
||||
) : companyOptions.length > 0 ? (
|
||||
<div className="max-h-48 w-full overflow-y-auto rounded-md border border-slate-200 bg-white text-sm shadow-sm">
|
||||
{companyOptions.map((option) => (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
className="flex w-full flex-col gap-0.5 px-3 py-2 text-left hover:bg-slate-100"
|
||||
onClick={() => handleSelectCompany(option)}
|
||||
>
|
||||
<span className="font-medium text-slate-800">{option.name}</span>
|
||||
<span className="text-xs text-slate-500">{option.slug}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : company.trim().length >= 2 ? (
|
||||
<p className="text-xs text-slate-500">Empresa não encontrada — criaremos automaticamente ao registrar.</p>
|
||||
{isValidatingCode ? (
|
||||
<p className="text-xs text-slate-500">Validando código...</p>
|
||||
) : codeStatus ? (
|
||||
<p
|
||||
className={`text-xs font-medium ${
|
||||
codeStatus.tone === "success" ? "text-emerald-600" : "text-rose-600"
|
||||
}`}
|
||||
>
|
||||
{codeStatus.message}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-slate-500">Pode informar o nome completo que transformamos no slug registrado.</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
Informe o código único fornecido pela equipe para vincular esta máquina a uma empresa.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm font-medium">Perfil de acesso</label>
|
||||
<select
|
||||
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm"
|
||||
value={accessRole}
|
||||
onChange={(e) => setAccessRole((e.target.value as "collaborator" | "manager") ?? "collaborator")}
|
||||
>
|
||||
<option value="collaborator">Colaborador (portal)</option>
|
||||
<option value="manager">Gestor (painel completo)</option>
|
||||
</select>
|
||||
<p className="text-xs text-slate-500">
|
||||
Colaboradores veem apenas seus chamados. Gestores acompanham todos os tickets da empresa.
|
||||
</p>
|
||||
</div>
|
||||
{validatedCompany ? (
|
||||
<div className="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs text-emerald-700">
|
||||
<div className="text-sm font-semibold text-emerald-800">{validatedCompany.name}</div>
|
||||
<div>Tenant: <span className="font-mono text-emerald-800">{validatedCompany.tenantId}</span></div>
|
||||
<div>Slug: <span className="font-mono text-emerald-800">{validatedCompany.slug}</span></div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm font-medium">
|
||||
Colaborador (e-mail) <span className="text-rose-500">*</span>
|
||||
</label>
|
||||
<input className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm" placeholder="colaborador@empresa.com" value={collabEmail} onChange={(e)=>setCollabEmail(e.target.value)} />
|
||||
<input
|
||||
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm"
|
||||
placeholder="colaborador@empresa.com"
|
||||
value={collabEmail}
|
||||
onChange={(e) => setCollabEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm font-medium">Nome do colaborador <span className="text-rose-500">*</span></label>
|
||||
<input className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm" placeholder="Nome completo" value={collabName} onChange={(e)=>setCollabName(e.target.value)} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm font-medium">Tenant (opcional)</label>
|
||||
<input className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm" placeholder="tenant-atlas" value={tenantId} onChange={(e)=>setTenantId(e.target.value)} />
|
||||
<label className="text-sm font-medium">
|
||||
Nome do colaborador <span className="text-rose-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm"
|
||||
placeholder="Nome completo"
|
||||
value={collabName}
|
||||
onChange={(e) => setCollabName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{profile ? (
|
||||
<div className="mt-2 grid grid-cols-2 gap-2 rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs">
|
||||
|
|
@ -675,7 +656,7 @@ function App() {
|
|||
</div>
|
||||
) : null}
|
||||
<div className="mt-2 flex gap-2">
|
||||
<button disabled={busy} onClick={register} className="rounded-lg border border-black bg-black px-3 py-2 text-sm font-semibold text-white hover:bg-black/90 disabled:opacity-60">Registrar máquina</button>
|
||||
<button disabled={busy || !validatedCompany} onClick={register} className="rounded-lg border border-black bg-black px-3 py-2 text-sm font-semibold text-white hover:bg-black/90 disabled:opacity-60">Registrar máquina</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -684,10 +665,19 @@ function App() {
|
|||
<TabsList className="h-10">
|
||||
<TabsTrigger value="resumo" className="rounded-lg px-3">Resumo</TabsTrigger>
|
||||
<TabsTrigger value="inventario" className="rounded-lg px-3">Inventário</TabsTrigger>
|
||||
<TabsTrigger value="diagnostico" className="rounded-lg px-3">Diagnóstico</TabsTrigger>
|
||||
<TabsTrigger value="config" className="rounded-lg px-3">Configurações</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="resumo" className="mt-4">
|
||||
{companyName ? (
|
||||
<div className="mb-3 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-neutral-700 shadow-sm">
|
||||
<div className="text-sm font-semibold text-neutral-900">{companyName}</div>
|
||||
{config?.collaboratorEmail ? (
|
||||
<div className="text-xs text-neutral-500">
|
||||
Vinculado a {config.collaboratorEmail}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="stat-card">
|
||||
<div className="text-xs text-slate-500">CPU</div>
|
||||
|
|
@ -722,12 +712,6 @@ function App() {
|
|||
<button onClick={openSystem} className="btn btn-outline">Ver no sistema</button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="diagnostico" className="mt-4 space-y-2">
|
||||
<div className="card">
|
||||
<p className="text-sm text-slate-700">Token armazenado: <span className="font-mono break-all text-xs">{token?.slice(0, 6)}…{token?.slice(-6)}</span></p>
|
||||
<p className="text-sm text-slate-700">Base URL: <span className="font-mono text-xs">{apiBaseUrl}</span></p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="config" className="mt-4 space-y-3">
|
||||
<div className="grid gap-2">
|
||||
<label className="label">
|
||||
|
|
@ -792,26 +776,3 @@ function StatusBadge({ status }: { status: string | null }) {
|
|||
|
||||
const root = document.getElementById("root") || (() => { const el = document.createElement("div"); el.id = "root"; document.body.appendChild(el); return el })()
|
||||
createRoot(root).render(<App />)
|
||||
// DevTools shortcut (F12 / Ctrl+Shift+I) and context menu with modifier
|
||||
useEffect(() => {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const key = (e.key || '').toLowerCase()
|
||||
if (key === 'f12' || (e.ctrlKey && e.shiftKey && key === 'i')) {
|
||||
invoke('open_devtools').catch(() => {})
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
function onContextMenu(e: MouseEvent) {
|
||||
// Evita abrir sempre: use Ctrl ou Shift + botão direito para abrir DevTools
|
||||
if (e.ctrlKey || e.shiftKey) {
|
||||
invoke('open_devtools').catch(() => {})
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
window.addEventListener('contextmenu', onContextMenu)
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
window.removeEventListener('contextmenu', onContextMenu)
|
||||
}
|
||||
}, [])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue