Lighten the in-app workspace and put Clientes right after Dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
armando 2026-09-16 04:59:16 -03:00
parent c1addaaf8e
commit 7e929a5b6a
14 changed files with 181 additions and 124 deletions

View File

@ -64,7 +64,7 @@ export default function ArquivosPage() {
return (
<section>
<h1 className="font-display text-4xl font-extrabold uppercase">Anexos da operação</h1>
{error && !open ? <p className="mt-4 text-sm text-red-300">{error}</p> : null}
{error && !open ? <p className="mt-4 text-sm text-red-600">{error}</p> : null}
<RegistryTable
countLabel={`${rows.length} arquivo${rows.length === 1 ? "" : "s"}`}
@ -78,11 +78,11 @@ export default function ArquivosPage() {
}
>
{rows.map((row) => (
<tr key={row.id} className="border-t border-white/10">
<tr key={row.id} className="border-t border-[#E4EBE6]">
<td className="px-5 py-3 font-medium">{row.name}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{row.type}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{formatSize(row.sizeBytes)}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{new Date(row.createdAt).toLocaleString("pt-BR")}</td>
<td className="px-5 py-3 text-[#66756C]">{row.type}</td>
<td className="px-5 py-3 text-[#66756C]">{formatSize(row.sizeBytes)}</td>
<td className="px-5 py-3 text-[#66756C]">{new Date(row.createdAt).toLocaleString("pt-BR")}</td>
<td className="px-5 py-3 text-right">
<a href={`/api/files/${row.id}/download`} className="btn-ghost cursor-pointer px-3 py-1.5 text-[11px]">
Baixar
@ -96,7 +96,7 @@ export default function ArquivosPage() {
<Modal onClose={closeModal}>
<form className="flex flex-col gap-3" onSubmit={onSubmit}>
<h2 className="font-display text-2xl font-bold uppercase">Anexar arquivo</h2>
{error ? <p className="text-sm text-red-300">{error}</p> : null}
{error ? <p className="text-sm text-red-600">{error}</p> : null}
<input
className="text-sm file:mr-3 file:rounded-lg file:border-0 file:bg-[#82C85C] file:px-3 file:py-2 file:text-xs file:font-semibold file:text-black"
type="file"

View File

@ -108,7 +108,7 @@ export default function ClientesPage() {
return (
<section>
<h1 className="font-display text-4xl font-extrabold uppercase">Quem opera com você</h1>
{error && !open ? <p className="mt-4 text-sm text-red-300">{error}</p> : null}
{error && !open ? <p className="mt-4 text-sm text-red-600">{error}</p> : null}
<RegistryTable
countLabel={`${rows.length} cliente${rows.length === 1 ? "" : "s"}`}
@ -124,17 +124,17 @@ export default function ClientesPage() {
}
>
{rows.map((row) => (
<tr key={row.id} className="border-t border-white/10">
<tr key={row.id} className="border-t border-[#E4EBE6]">
<td className="px-5 py-3 font-medium">{row.name}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{row.document || "-"}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{[row.city, row.state].filter(Boolean).join(" / ") || "-"}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{row.email || row.phone || "-"}</td>
<td className="px-5 py-3 text-[#66756C]">{row.document || "-"}</td>
<td className="px-5 py-3 text-[#66756C]">{[row.city, row.state].filter(Boolean).join(" / ") || "-"}</td>
<td className="px-5 py-3 text-[#66756C]">{row.email || row.phone || "-"}</td>
{canWrite ? (
<td className="px-5 py-3">
<button className="text-[#82C85C]" type="button" onClick={() => openEdit(row)}>
Editar
</button>
<button className="ml-3 text-[#E8F4E5]/50" type="button" onClick={() => void onDelete(row.id)}>
<button className="ml-3 text-[#7A877F]" type="button" onClick={() => void onDelete(row.id)}>
Remover
</button>
</td>
@ -147,7 +147,7 @@ export default function ClientesPage() {
<Modal onClose={closeModal}>
<form className="grid gap-3" onSubmit={onSubmit}>
<h2 className="font-display text-2xl font-bold uppercase">{editing ? "Editar cliente" : "Novo cliente"}</h2>
{error ? <p className="text-sm text-red-300">{error}</p> : null}
{error ? <p className="text-sm text-red-600">{error}</p> : null}
<input className={fieldClass} placeholder="Nome" value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} required />
<input className={fieldClass} placeholder="CNPJ" value={form.document} onChange={(event) => setForm({ ...form, document: event.target.value })} />
<input className={fieldClass} placeholder="E-mail" type="email" value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} />

View File

@ -86,13 +86,13 @@ export default function EquipePage() {
}
if (me.role === "OPERATOR") {
return <p className="text-[#E8F4E5]/60">Seu perfil de Operador não gerencia a equipe.</p>;
return <p className="text-[#66756C]">Seu perfil de Operador não gerencia a equipe.</p>;
}
return (
<section>
<h1 className="font-display text-4xl font-extrabold uppercase">Pessoas da operação</h1>
{error && !open ? <p className="mt-4 text-sm text-red-300">{error}</p> : null}
{error && !open ? <p className="mt-4 text-sm text-red-600">{error}</p> : null}
<RegistryTable
countLabel={`${members.length} pessoa${members.length === 1 ? "" : "s"}`}
@ -108,13 +108,13 @@ export default function EquipePage() {
}
>
{members.map((member) => (
<tr key={member.id} className="border-t border-white/10">
<tr key={member.id} className="border-t border-[#E4EBE6]">
<td className="px-5 py-3 font-medium">{member.name}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{member.email}</td>
<td className="px-5 py-3 text-[#66756C]">{member.email}</td>
<td className="px-5 py-3">
{canManage ? (
<select
className="rounded-full border border-white/10 bg-black px-3 py-1.5 text-sm"
className="rounded-full border border-[#E4EBE6] bg-white px-3 py-1.5 text-sm"
value={member.role}
onChange={(event) => void onRole(member.id, event.target.value as Member["role"])}
>
@ -128,7 +128,7 @@ export default function EquipePage() {
<span className="text-[#82C85C]">{roles.find((item) => item.id === member.role)?.label}</span>
)}
</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">
<td className="px-5 py-3 text-[#66756C]">
{member.lastLoginAt ? new Date(member.lastLoginAt).toLocaleString("pt-BR") : "Nunca"}
</td>
</tr>
@ -139,11 +139,11 @@ export default function EquipePage() {
<Modal onClose={closeModal}>
<form className="grid gap-3" onSubmit={onCreate}>
<h2 className="font-display text-2xl font-bold uppercase">Novo usuário</h2>
{error ? <p className="text-sm text-red-300">{error}</p> : null}
{error ? <p className="text-sm text-red-600">{error}</p> : null}
<input className={fieldClass} placeholder="Nome" value={name} onChange={(event) => setName(event.target.value)} required />
<input className={fieldClass} placeholder="E-mail" type="email" value={email} onChange={(event) => setEmail(event.target.value)} required />
<input className={fieldClass} placeholder="Senha" type="password" value={password} onChange={(event) => setPassword(event.target.value)} required minLength={8} />
<select className="rounded-xl border border-white/10 bg-black px-4 py-3" value={role} onChange={(event) => setRole(event.target.value as typeof role)}>
<select className="rounded-xl border border-[#E4EBE6] bg-white px-4 py-3" value={role} onChange={(event) => setRole(event.target.value as typeof role)}>
{roles.map((item) => (
<option key={item.id} value={item.id}>
{item.label}

View File

@ -29,18 +29,18 @@ export default function HistoricoPage() {
return (
<section>
<h1 className="font-display text-4xl font-extrabold uppercase">Simulações</h1>
{error ? <p className="mt-4 text-sm text-red-300">{error}</p> : null}
{error ? <p className="mt-4 text-sm text-red-600">{error}</p> : null}
<ul className="mt-8 divide-y divide-white/10 rounded-2xl border border-white/10">
<ul className="mt-8 divide-y divide-[#E4EBE6] rounded-2xl border border-[#E4EBE6] bg-white">
{rows.length === 0 ? (
<li className="px-5 py-8 text-[#E8F4E5]/50">Nenhuma simulação ainda.</li>
<li className="px-5 py-8 text-[#7A877F]">Nenhuma simulação ainda.</li>
) : (
rows.map((row) => (
<li key={row.id} className="px-5 py-4">
<p className="font-medium">
{row.originLabel} {row.destinationLabel}
</p>
<p className="text-sm text-[#E8F4E5]/55">
<p className="text-sm text-[#66756C]">
{row.bestCarrierName} · {row.quotedPrice !== null ? money(row.quotedPrice) : "-"} · {row.distanceKm} km
{row.customer ? ` · ${row.customer.name}` : ""} ·{" "}
{new Date(row.createdAt).toLocaleString("pt-BR")}

View File

@ -39,11 +39,11 @@ export default function DashboardPage() {
}, []);
if (!data && !error) {
return <p className="text-[#E8F4E5]/50">Carregando indicadores</p>;
return <p className="text-[#7A877F]">Carregando indicadores</p>;
}
if (error || !data) {
return <p className="text-sm text-red-300">{error || "Não foi possível carregar o painel."}</p>;
return <p className="text-sm text-red-600">{error || "Não foi possível carregar o painel."}</p>;
}
const cards = [
@ -76,19 +76,19 @@ export default function DashboardPage() {
<section>
<ul className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{cards.map((card) => (
<li key={card.label} className="rounded-2xl border border-white/10 px-5 py-5">
<p className="text-xs uppercase tracking-wider text-[#E8F4E5]/45">{card.label}</p>
<li key={card.label} className="rounded-2xl border border-[#E4EBE6] bg-white px-5 py-5">
<p className="text-xs uppercase tracking-wider text-[#7A877F]">{card.label}</p>
<p className="font-display mt-3 text-3xl font-extrabold">{card.value}</p>
<p className="mt-2 text-xs text-[#E8F4E5]/50">{card.hint}</p>
<p className="mt-2 text-xs text-[#7A877F]">{card.hint}</p>
</li>
))}
</ul>
<div className="mt-8 rounded-2xl border border-white/10 px-5 py-5">
<div className="mt-8 rounded-2xl border border-[#E4EBE6] bg-white px-5 py-5">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-[#82C85C]">Leitura</p>
<ol className="mt-4 grid gap-3">
{data.insights.map((insight) => (
<li key={insight} className="text-sm text-[#E8F4E5]/80">
<li key={insight} className="text-sm text-[#3A463F]">
{insight}
</li>
))}
@ -96,13 +96,13 @@ export default function DashboardPage() {
</div>
<div className="mt-8 grid gap-6 xl:grid-cols-2">
<div className="overflow-hidden rounded-2xl border border-white/10">
<div className="overflow-hidden rounded-2xl border border-[#E4EBE6] bg-white">
<div className="px-5 py-4">
<p className="text-xs uppercase tracking-wider text-[#E8F4E5]/45">Quem vence a cotação</p>
<p className="text-xs uppercase tracking-wider text-[#7A877F]">Quem vence a cotação</p>
<h2 className="font-display mt-1 text-xl font-bold uppercase">Transportadoras</h2>
</div>
<table className="w-full text-left text-sm">
<thead className="border-t border-white/10 bg-white/5 text-xs uppercase tracking-wider text-[#E8F4E5]/45">
<thead className="border-t border-[#E4EBE6] bg-[#F3F6F4] text-xs uppercase tracking-wider text-[#7A877F]">
<tr>
<th className="px-5 py-3 font-medium">Nome</th>
<th className="px-5 py-3 font-medium">Vitórias</th>
@ -111,19 +111,19 @@ export default function DashboardPage() {
</thead>
<tbody>
{data.carriers.length === 0 ? (
<tr className="border-t border-white/10">
<td className="px-5 py-6 text-[#E8F4E5]/45" colSpan={3}>
<tr className="border-t border-[#E4EBE6]">
<td className="px-5 py-6 text-[#7A877F]" colSpan={3}>
Sem cotações no período.
</td>
</tr>
) : (
data.carriers.map((carrier) => (
<tr key={carrier.name} className="border-t border-white/10">
<tr key={carrier.name} className="border-t border-[#E4EBE6]">
<td className="px-5 py-3">{carrier.name}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">
<td className="px-5 py-3 text-[#66756C]">
{carrier.wins} · {carrier.sharePct}%
</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{money(carrier.averageWin)}</td>
<td className="px-5 py-3 text-[#66756C]">{money(carrier.averageWin)}</td>
</tr>
))
)}
@ -131,13 +131,13 @@ export default function DashboardPage() {
</table>
</div>
<div className="overflow-hidden rounded-2xl border border-white/10">
<div className="overflow-hidden rounded-2xl border border-[#E4EBE6] bg-white">
<div className="px-5 py-4">
<p className="text-xs uppercase tracking-wider text-[#E8F4E5]/45">Onde o frete pesa</p>
<p className="text-xs uppercase tracking-wider text-[#7A877F]">Onde o frete pesa</p>
<h2 className="font-display mt-1 text-xl font-bold uppercase">Rotas mais caras</h2>
</div>
<table className="w-full text-left text-sm">
<thead className="border-t border-white/10 bg-white/5 text-xs uppercase tracking-wider text-[#E8F4E5]/45">
<thead className="border-t border-[#E4EBE6] bg-[#F3F6F4] text-xs uppercase tracking-wider text-[#7A877F]">
<tr>
<th className="px-5 py-3 font-medium">Rota</th>
<th className="px-5 py-3 font-medium">Vezes</th>
@ -146,17 +146,17 @@ export default function DashboardPage() {
</thead>
<tbody>
{data.routes.length === 0 ? (
<tr className="border-t border-white/10">
<td className="px-5 py-6 text-[#E8F4E5]/45" colSpan={3}>
<tr className="border-t border-[#E4EBE6]">
<td className="px-5 py-6 text-[#7A877F]" colSpan={3}>
Sem rotas no período.
</td>
</tr>
) : (
data.routes.map((route) => (
<tr key={route.label} className="border-t border-white/10">
<tr key={route.label} className="border-t border-[#E4EBE6]">
<td className="px-5 py-3">{route.label}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{route.count}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{money(route.averageCost)}</td>
<td className="px-5 py-3 text-[#66756C]">{route.count}</td>
<td className="px-5 py-3 text-[#66756C]">{money(route.averageCost)}</td>
</tr>
))
)}

View File

@ -21,7 +21,7 @@ type Simulation = {
};
const money = (value: number) => value.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
const fieldClass = "rounded-xl border border-white/10 bg-white/5 px-4 py-3";
const fieldClass = "rounded-xl border border-[#E4EBE6] bg-white px-4 py-3";
export default function SimularPage() {
const [customers, setCustomers] = useState<Customer[]>([]);
@ -78,15 +78,15 @@ export default function SimularPage() {
return (
<section>
<h1 className="font-display text-4xl font-extrabold uppercase">Calcular frete</h1>
<p className="mt-4 max-w-xl text-[#E8F4E5]/65">
<p className="mt-4 max-w-xl text-[#66756C]">
Informe origem, destino e a carga. A Sinka compara as transportadoras e mostra o melhor custo.
</p>
{error ? <p className="mt-4 text-sm text-red-300">{error}</p> : null}
{error ? <p className="mt-4 text-sm text-red-600">{error}</p> : null}
<div className={`mt-8 grid items-start gap-6 ${loading ? "lg:grid-cols-[minmax(0,28rem)_minmax(0,1fr)]" : "max-w-xl"}`}>
<form className="grid gap-3" onSubmit={onSubmit}>
<select
className="rounded-xl border border-white/10 bg-black px-4 py-3"
className="rounded-xl border border-[#E4EBE6] bg-white px-4 py-3"
value={form.customerId}
onChange={(event) => setForm({ ...form, customerId: event.target.value })}
>
@ -118,21 +118,21 @@ export default function SimularPage() {
{loading ? (
<aside className="flex min-h-[18rem] flex-col items-center justify-center px-6 py-8">
<img src="/brand/freight-truck.webp" alt="" className="h-44 w-auto max-w-full object-contain" />
<p className="mt-6 text-sm text-[#E8F4E5]/70">Calculando a melhor rota</p>
<p className="mt-6 text-sm text-[#66756C]">Calculando a melhor rota</p>
</aside>
) : null}
</div>
{result ? (
<div className="mt-10">
<p className="text-sm text-[#E8F4E5]/60">
<p className="text-sm text-[#66756C]">
{result.originLabel} {result.destinationLabel} · {result.distanceKm} km
{result.distanceSource === "fallback" ? " (distância estimada)" : ""}
</p>
<h2 className="font-display mt-2 text-2xl font-bold uppercase">
Melhor opção: {result.bestCarrierName} · {result.quotedPrice !== null ? money(result.quotedPrice) : "-"}
</h2>
<ul className="mt-6 divide-y divide-white/10 rounded-2xl border border-white/10">
<ul className="mt-6 divide-y divide-[#E4EBE6] rounded-2xl border border-[#E4EBE6] bg-white">
{result.quotes.map((quote, index) => (
<li key={quote.carrierId} className="flex items-center justify-between px-5 py-4">
<span>

View File

@ -120,13 +120,13 @@ export default function TransportadorasPage() {
}
if (me && !me.permissions.includes("carriers:write")) {
return <p className="text-[#E8F4E5]/60">Seu perfil não gerencia transportadoras.</p>;
return <p className="text-[#66756C]">Seu perfil não gerencia transportadoras.</p>;
}
return (
<section>
<h1 className="font-display text-4xl font-extrabold uppercase">Tabelas de frete</h1>
{error && !open ? <p className="mt-4 text-sm text-red-300">{error}</p> : null}
{error && !open ? <p className="mt-4 text-sm text-red-600">{error}</p> : null}
<RegistryTable
countLabel={`${rows.length} transportadora${rows.length === 1 ? "" : "s"}`}
@ -140,18 +140,18 @@ export default function TransportadorasPage() {
}
>
{rows.map((row) => (
<tr key={row.id} className="border-t border-white/10">
<tr key={row.id} className="border-t border-[#E4EBE6]">
<td className="px-5 py-3 font-medium">{row.name}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{row.active ? "Ativa" : "Inativa"}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{money(Number(row.baseFee))}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{money(Number(row.pricePerKg))}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{money(Number(row.pricePerKm))}</td>
<td className="px-5 py-3 text-[#E8F4E5]/70">{(Number(row.adValoremRate) * 100).toFixed(2)}%</td>
<td className="px-5 py-3 text-[#66756C]">{row.active ? "Ativa" : "Inativa"}</td>
<td className="px-5 py-3 text-[#66756C]">{money(Number(row.baseFee))}</td>
<td className="px-5 py-3 text-[#66756C]">{money(Number(row.pricePerKg))}</td>
<td className="px-5 py-3 text-[#66756C]">{money(Number(row.pricePerKm))}</td>
<td className="px-5 py-3 text-[#66756C]">{(Number(row.adValoremRate) * 100).toFixed(2)}%</td>
<td className="px-5 py-3">
<button className="text-[#82C85C]" type="button" onClick={() => openEdit(row)}>
Editar
</button>
<button className="ml-3 text-[#E8F4E5]/50" type="button" onClick={() => void onDelete(row.id)}>
<button className="ml-3 text-[#7A877F]" type="button" onClick={() => void onDelete(row.id)}>
Remover
</button>
</td>
@ -163,7 +163,7 @@ export default function TransportadorasPage() {
<Modal onClose={closeModal}>
<form className="grid gap-3" onSubmit={onSubmit}>
<h2 className="font-display text-2xl font-bold uppercase">{editing ? "Editar" : "Nova transportadora"}</h2>
{error ? <p className="text-sm text-red-300">{error}</p> : null}
{error ? <p className="text-sm text-red-600">{error}</p> : null}
<input className={fieldClass} placeholder="Nome" value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} required />
<input className={fieldClass} placeholder="CNPJ" value={form.document} onChange={(event) => setForm({ ...form, document: event.target.value })} />
<input className={fieldClass} placeholder="Telefone" value={form.phone} onChange={(event) => setForm({ ...form, phone: event.target.value })} />

View File

@ -149,12 +149,12 @@ function TenantRowActions({
</button>
{menu ? (
<div
className="fixed z-50 min-w-40 overflow-hidden rounded-xl border border-white/10 bg-black py-1 shadow-xl"
className="fixed z-50 min-w-40 overflow-hidden rounded-xl border border-[#E4EBE6] bg-white py-1 shadow-xl"
style={{ top: menu.top, right: menu.right }}
onClick={(event) => event.stopPropagation()}
>
<button
className="block w-full px-4 py-2 text-left text-sm text-[#E8F4E5] hover:bg-white/5"
className="block w-full px-4 py-2 text-left text-sm text-[#1B241E] hover:bg-[#F3F6F4]"
type="button"
onClick={() => {
setMenu(null);
@ -165,7 +165,7 @@ function TenantRowActions({
</button>
<a
href={`/admin/chat/${tenant.slug}`}
className="flex items-center justify-between px-4 py-2 text-sm text-[#E8F4E5] hover:bg-white/5"
className="flex items-center justify-between px-4 py-2 text-sm text-[#1B241E] hover:bg-[#F3F6F4]"
>
Chat
{chatCount ? (
@ -176,7 +176,7 @@ function TenantRowActions({
</a>
<a
href={`/${tenant.slug}/login`}
className="block px-4 py-2 text-sm text-[#E8F4E5] hover:bg-white/5"
className="block px-4 py-2 text-sm text-[#1B241E] hover:bg-[#F3F6F4]"
target="_blank"
rel="noopener noreferrer"
>
@ -298,7 +298,7 @@ export default function PlatformClientesPage() {
return (
<section>
<h1 className="font-display text-4xl font-extrabold uppercase">Clientes</h1>
{error && !open ? <p className="mt-4 text-sm text-red-300">{error}</p> : null}
{error && !open ? <p className="mt-4 text-sm text-red-600">{error}</p> : null}
<RegistryTable
scroll={false}
@ -313,10 +313,10 @@ export default function PlatformClientesPage() {
}
>
{tenants.map((tenant) => (
<tr key={tenant.id} className="border-t border-white/10">
<tr key={tenant.id} className="border-t border-[#E4EBE6]">
<td className="truncate px-5 py-3 font-medium">{tenant.name}</td>
<td className="truncate px-5 py-3 text-[#E8F4E5]/70">/{tenant.slug}</td>
<td className="truncate px-5 py-3 text-[#E8F4E5]/70">{tenant.status === "ACTIVE" ? "Ativo" : "Suspenso"}</td>
<td className="truncate px-5 py-3 text-[#66756C]">/{tenant.slug}</td>
<td className="truncate px-5 py-3 text-[#66756C]">{tenant.status === "ACTIVE" ? "Ativo" : "Suspenso"}</td>
<td className="overflow-hidden px-5 py-3">
<TenantRowActions
tenant={tenant}
@ -333,7 +333,7 @@ export default function PlatformClientesPage() {
{created ? (
<div>
<h2 className="font-display text-2xl font-bold uppercase">Cliente criado</h2>
<p className="mt-3 text-sm text-[#E8F4E5]/65">Guarde o acesso do administrador:</p>
<p className="mt-3 text-sm text-[#66756C]">Guarde o acesso do administrador:</p>
<p className="mt-4 text-sm">
Endereço:{" "}
<a className="text-[#82C85C]" href={created.access.path} target="_blank" rel="noopener noreferrer">
@ -349,7 +349,7 @@ export default function PlatformClientesPage() {
) : (
<form className="grid gap-3" onSubmit={onCreate}>
<h2 className="font-display text-2xl font-bold uppercase">Novo cliente</h2>
{error ? <p className="text-sm text-red-300">{error}</p> : null}
{error ? <p className="text-sm text-red-600">{error}</p> : null}
<input
className={fieldClass}
placeholder="Nome"
@ -383,13 +383,13 @@ export default function PlatformClientesPage() {
{usersOf ? (
<Modal wide onClose={closeUsers}>
<h2 className="font-display text-2xl font-bold uppercase">Usuários · {usersOf.name}</h2>
<p className="mt-2 text-sm text-[#E8F4E5]/60">
<p className="mt-2 text-sm text-[#66756C]">
Contas de {usersOf.name}. Se a senha estiver vazia, gere uma nova.
</p>
{usersError ? <p className="mt-3 text-sm text-red-300">{usersError}</p> : null}
{usersError ? <p className="mt-3 text-sm text-red-600">{usersError}</p> : null}
<div className="mt-5 overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="text-xs uppercase tracking-wider text-[#E8F4E5]/50">
<thead className="text-xs uppercase tracking-wider text-[#7A877F]">
<tr>
<th className="pb-3 pr-6 font-medium">Nome</th>
<th className="pb-3 pr-6 font-medium">Usuário</th>
@ -401,20 +401,20 @@ export default function PlatformClientesPage() {
<tbody>
{users.length === 0 ? (
<tr>
<td className="py-6 text-[#E8F4E5]/50" colSpan={5}>
<td className="py-6 text-[#7A877F]" colSpan={5}>
Nenhum usuário neste cliente.
</td>
</tr>
) : (
users.map((user) => (
<tr key={user.id} className="border-t border-white/10">
<tr key={user.id} className="border-t border-[#E4EBE6]">
<td className="py-3 pr-6 font-medium whitespace-nowrap">{user.name}</td>
<td className="py-3 pr-6 text-[#E8F4E5]/80 whitespace-nowrap">{user.email}</td>
<td className="py-3 pr-6 text-[#3A463F] whitespace-nowrap">{user.email}</td>
<td className="py-3 pr-6 font-mono whitespace-nowrap text-[#82C85C]">{user.password || "-"}</td>
<td className="py-3 pr-6 whitespace-nowrap text-[#E8F4E5]/70">{roleLabel[user.role]}</td>
<td className="py-3 pr-6 whitespace-nowrap text-[#66756C]">{roleLabel[user.role]}</td>
<td className="py-3 whitespace-nowrap">
<button
className="text-xs text-[#E8F4E5]/70"
className="text-xs text-[#66756C]"
type="button"
disabled={issuing === user.id}
onClick={() => void issuePassword(user.id)}

View File

@ -25,25 +25,25 @@ export default function PlatformIntegracoesPage() {
return (
<section>
<h1 className="font-display text-4xl font-extrabold uppercase">Integrações</h1>
{error ? <p className="mt-4 text-sm text-red-300">{error}</p> : null}
{loading ? <p className="mt-4 text-sm text-[#E8F4E5]/50">Verificando integrações</p> : null}
{error ? <p className="mt-4 text-sm text-red-600">{error}</p> : null}
{loading ? <p className="mt-4 text-sm text-[#7A877F]">Verificando integrações</p> : null}
<ul className="mt-8 grid gap-3 md:grid-cols-2">
{items.map((item) => {
const active = item.status === "active";
return (
<li key={item.id} className="rounded-2xl border border-white/10 px-5 py-5">
<li key={item.id} className="rounded-2xl border border-[#E4EBE6] bg-white px-5 py-5">
<div className="flex items-start justify-between gap-3">
<h2 className="text-lg font-semibold">{item.name}</h2>
<span
className={`shrink-0 rounded-full px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wider ${
active ? "bg-[#82C85C]/15 text-[#82C85C]" : "bg-red-400/15 text-red-300"
active ? "bg-[#82C85C]/15 text-[#82C85C]" : "bg-red-400/15 text-red-600"
}`}
>
{active ? "Ativo" : "Inativo"}
</span>
</div>
<p className="mt-2 text-sm text-[#E8F4E5]/60">{item.description}</p>
<p className="mt-2 text-sm text-[#66756C]">{item.description}</p>
</li>
);
})}

View File

@ -28,7 +28,7 @@ export default function PlatformPage() {
return (
<section>
{error ? <p className="mb-4 text-sm text-red-300">{error}</p> : null}
{error ? <p className="mb-4 text-sm text-red-600">{error}</p> : null}
<ul className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{[
@ -53,10 +53,10 @@ export default function PlatformPage() {
hint: "no chat de suporte",
},
].map((card) => (
<li key={card.label} className="rounded-2xl border border-white/10 px-5 py-5">
<p className="text-xs uppercase tracking-wider text-[#E8F4E5]/45">{card.label}</p>
<li key={card.label} className="rounded-2xl border border-[#E4EBE6] bg-white px-5 py-5">
<p className="text-xs uppercase tracking-wider text-[#7A877F]">{card.label}</p>
<p className="font-display mt-3 text-3xl font-extrabold">{card.value}</p>
<p className="mt-2 text-xs text-[#E8F4E5]/50">{card.hint}</p>
<p className="mt-2 text-xs text-[#7A877F]">{card.hint}</p>
</li>
))}
</ul>

View File

@ -32,6 +32,12 @@ body {
font-family: var(--font-sans-face), system-ui, sans-serif;
}
html:has(.sinka-app),
body:has(.sinka-app) {
background: #f4f7f5;
color: #1b241e;
}
h1,
h2,
h3,
@ -165,6 +171,16 @@ h3,
color: #e8f4e5;
}
.sinka-app .btn-ghost {
border-color: #c5d9c0;
color: #1b241e;
background: #fff;
}
.sinka-app .btn-ghost:hover {
background: #f3f6f4;
}
.btn-google {
display: inline-flex;
align-items: center;

View File

@ -18,14 +18,14 @@ export function RegistryTable({
scroll?: boolean;
}) {
return (
<div className="mt-8 overflow-hidden rounded-2xl border border-white/10">
<div className="mt-8 overflow-hidden rounded-2xl border border-[#E4EBE6] bg-white">
<div className="flex items-center justify-between gap-3 px-5 py-3">
<p className="text-sm text-[#E8F4E5]/60">{countLabel}</p>
<p className="text-sm text-[#66756C]">{countLabel}</p>
{action ?? <span />}
</div>
<div className={`border-t border-white/10 ${scroll ? "overflow-x-auto" : "overflow-hidden"}`}>
<div className={`border-t border-[#E4EBE6] ${scroll ? "overflow-x-auto" : "overflow-hidden"}`}>
<table className={`w-full table-fixed text-left text-sm ${scroll ? "min-w-[36rem]" : ""}`}>
<thead className="bg-white/5 text-xs uppercase tracking-wider text-[#E8F4E5]/50">
<thead className="bg-[#F3F6F4] text-xs uppercase tracking-wider text-[#7A877F]">
<tr>
{columns.map((column) => (
<th key={column} className="px-5 py-3 font-medium">
@ -37,7 +37,7 @@ export function RegistryTable({
<tbody>
{isEmpty ? (
<tr>
<td className="px-5 py-8 text-[#E8F4E5]/50" colSpan={columns.length}>
<td className="px-5 py-8 text-[#7A877F]" colSpan={columns.length}>
{empty ?? "Nenhum registro."}
</td>
</tr>
@ -61,9 +61,9 @@ export function Modal({
wide?: boolean;
}) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 px-4" onClick={onClose}>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4" onClick={onClose}>
<div
className={`max-h-[90dvh] w-full overflow-y-auto rounded-2xl border border-white/10 bg-black p-6 ${
className={`max-h-[90dvh] w-full overflow-y-auto rounded-2xl border border-[#E4EBE6] bg-white p-6 ${
wide ? "max-w-5xl" : "max-w-md"
}`}
onClick={(event) => event.stopPropagation()}
@ -74,4 +74,4 @@ export function Modal({
);
}
export const fieldClass = "rounded-xl border border-white/10 bg-white/5 px-4 py-3";
export const fieldClass = "rounded-xl border border-[#E4EBE6] bg-white px-4 py-3 outline-none focus:border-[#82C85C]";

View File

@ -6,6 +6,26 @@ import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState, type ReactNode } from "react";
import { api, type SessionUser } from "@/lib/api";
function BrandLoading() {
return (
<div
className="fixed inset-0 z-[60] flex items-center justify-center bg-[#163027]"
role="status"
aria-live="polite"
aria-label="Carregando"
>
<Image
src="/brand/sinka-logo.webp"
alt="Sinka"
width={180}
height={58}
className="h-8 w-auto animate-pulse"
priority
/>
</div>
);
}
export function RestrictedShell({
kind,
basePath,
@ -20,6 +40,7 @@ export function RestrictedShell({
const [user, setUser] = useState<SessionUser | null>(null);
const [error, setError] = useState("");
const [menuOpen, setMenuOpen] = useState(false);
const [navigating, setNavigating] = useState(false);
const loginPath = kind === "platform" ? "/admin/login" : `${basePath}/login`;
useEffect(() => {
@ -49,18 +70,27 @@ export function RestrictedShell({
useEffect(() => {
setMenuOpen(false);
setNavigating(false);
}, [pathname]);
useEffect(() => {
if (!navigating) {
return;
}
const timeout = window.setTimeout(() => setNavigating(false), 8000);
return () => window.clearTimeout(timeout);
}, [navigating]);
async function logout() {
await api("/api/auth/logout", { method: "POST" }).catch(() => undefined);
router.replace(loginPath);
}
if (!user) {
return (
<main className="flex min-h-dvh items-center justify-center bg-black text-[#E8F4E5]/60">
{error || "Carregando…"}
</main>
return error ? (
<main className="flex min-h-dvh items-center justify-center bg-[#F4F7F5] text-[#66756C]">{error}</main>
) : (
<BrandLoading />
);
}
@ -73,17 +103,17 @@ export function RestrictedShell({
]
: [
{ href: basePath, label: "Dashboard" },
{ href: `${basePath}/clientes`, label: "Clientes" },
{ href: `${basePath}/simular`, label: "Simular" },
{ href: `${basePath}/historico`, label: "Histórico" },
{ href: `${basePath}/chat`, label: "Chat" },
{ href: `${basePath}/arquivos`, label: "Arquivos" },
{ href: `${basePath}/clientes`, label: "Clientes" },
...(user.permissions.includes("carriers:write")
? [{ href: `${basePath}/transportadoras`, label: "Transportadoras" }]
: []),
...(user.permissions.includes("users:manage") || user.role === "MANAGER"
? [{ href: `${basePath}/equipe`, label: "Equipe" }]
: []),
{ href: `${basePath}/arquivos`, label: "Arquivos" },
{ href: `${basePath}/chat`, label: "Chat" },
];
function isActive(href: string) {
@ -96,10 +126,19 @@ export function RestrictedShell({
return href === basePath ? pathname === basePath : pathname.startsWith(href);
}
function startNavigation(href: string) {
const current = pathname.replace(/\/$/, "") || "/";
const target = href.replace(/\/$/, "") || "/";
if (current === target) {
return;
}
setNavigating(true);
}
const nav = (
<>
<div className="px-5 pb-6 pt-7">
<Link href={basePath} className="inline-block">
<Link href={basePath} className="inline-block" onClick={() => startNavigation(basePath)}>
<Image src="/brand/sinka-logo.webp" alt="Sinka" width={140} height={46} className="h-6 w-auto" />
</Link>
<p className="mt-5 text-[11px] font-semibold uppercase tracking-[0.22em] text-[#82C85C]">
@ -111,10 +150,11 @@ export function RestrictedShell({
<Link
key={link.href}
href={link.href}
onClick={() => startNavigation(link.href)}
className={`rounded-lg px-3 py-3 text-base ${
isActive(link.href)
? "bg-[#82C85C]/12 font-medium text-[#82C85C]"
: "text-[#E8F4E5]/70 hover:bg-white/5 hover:text-[#E8F4E5]"
? "bg-[#82C85C]/18 font-medium text-[#82C85C]"
: "text-white/70 hover:bg-white/10 hover:text-white"
}`}
>
{link.label}
@ -122,10 +162,10 @@ export function RestrictedShell({
))}
</nav>
<div className="mt-auto border-t border-white/10 px-5 py-5">
<p className="truncate text-sm font-medium">{user.name}</p>
<p className="truncate text-sm font-medium text-white">{user.name}</p>
<p className="mt-0.5 text-xs text-[#82C85C]">{user.roleLabel}</p>
<button
className="btn-ghost mt-4 w-full justify-center px-3 py-2 text-xs"
className="btn-ghost mt-4 w-full justify-center px-3 py-2 text-xs !border-white/25 !bg-transparent !text-white hover:!bg-white/10"
type="button"
onClick={() => void logout()}
>
@ -136,13 +176,13 @@ export function RestrictedShell({
);
return (
<div className="min-h-dvh bg-black text-[#E8F4E5] lg:flex">
<header className="flex items-center justify-between border-b border-white/10 px-4 py-3 lg:hidden">
<Link href={basePath}>
<div className="sinka-app min-h-dvh bg-[#F4F7F5] text-[#1B241E] lg:flex">
<header className="flex items-center justify-between border-b border-white/10 bg-[#163027] px-4 py-3 lg:hidden">
<Link href={basePath} onClick={() => startNavigation(basePath)}>
<Image src="/brand/sinka-logo.webp" alt="Sinka" width={120} height={40} className="h-6 w-auto" />
</Link>
<button
className="rounded-lg border border-white/15 px-3 py-1.5 text-xs uppercase tracking-wider text-[#E8F4E5]/80"
className="rounded-lg border border-white/20 px-3 py-1.5 text-xs uppercase tracking-wider text-white/80"
type="button"
onClick={() => setMenuOpen((open) => !open)}
>
@ -152,7 +192,7 @@ export function RestrictedShell({
{menuOpen ? (
<button
className="fixed inset-0 z-30 bg-black/60 lg:hidden"
className="fixed inset-0 z-30 bg-black/40 lg:hidden"
type="button"
aria-label="Fechar menu"
onClick={() => setMenuOpen(false)}
@ -160,7 +200,7 @@ export function RestrictedShell({
) : null}
<aside
className={`fixed inset-y-0 left-0 z-40 flex w-72 flex-col border-r border-white/10 bg-black transition-transform lg:static lg:translate-x-0 ${
className={`fixed inset-y-0 left-0 z-40 flex w-72 flex-col bg-[#163027] text-white transition-transform lg:static lg:translate-x-0 ${
menuOpen ? "translate-x-0" : "-translate-x-full"
}`}
>
@ -168,6 +208,7 @@ export function RestrictedShell({
</aside>
<main className="min-w-0 flex-1 px-5 py-8 lg:px-10 lg:py-10">{children}</main>
{navigating ? <BrandLoading /> : null}
</div>
);
}

View File

@ -99,12 +99,12 @@ export function SupportChat({
</a>
) : null}
<h1 className="font-display text-4xl font-extrabold uppercase">{heading}</h1>
{error ? <p className="mt-4 text-sm text-red-300">{error}</p> : null}
{error ? <p className="mt-4 text-sm text-red-600">{error}</p> : null}
<div className="mt-6 flex min-h-[22rem] flex-1 flex-col overflow-hidden rounded-2xl border border-white/10">
<div className="mt-6 flex min-h-[22rem] flex-1 flex-col overflow-hidden rounded-2xl border border-[#E4EBE6] bg-white">
<div className="flex-1 space-y-3 overflow-y-auto px-5 py-4">
{messages.length === 0 ? (
<p className="text-sm text-[#E8F4E5]/45">{ready ? "Nenhuma mensagem ainda." : "Conectando…"}</p>
<p className="text-sm text-[#7A877F]">{ready ? "Nenhuma mensagem ainda." : "Conectando…"}</p>
) : (
messages.map((message) => (
<article
@ -112,7 +112,7 @@ export function SupportChat({
className={`max-w-[80%] rounded-2xl px-4 py-3 text-sm ${
message.authorKind === "platform"
? "ml-auto bg-[#82C85C] text-black"
: "bg-white/8 text-[#E8F4E5]"
: "bg-[#F3F6F4] text-[#1B241E]"
}`}
>
<p className="text-[11px] uppercase tracking-wider opacity-70">{message.authorName}</p>
@ -122,9 +122,9 @@ export function SupportChat({
)}
<div ref={bottomRef} />
</div>
<form className="flex gap-2 border-t border-white/10 p-3" onSubmit={onSubmit}>
<form className="flex gap-2 border-t border-[#E4EBE6] p-3" onSubmit={onSubmit}>
<input
className="flex-1 rounded-xl border border-white/10 bg-white/5 px-4 py-3"
className="flex-1 rounded-xl border border-[#E4EBE6] bg-white px-4 py-3"
placeholder="Escrever mensagem"
value={text}
onChange={(event) => setText(event.target.value)}