Lighten the in-app workspace and put Clientes right after Dashboard.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c1addaaf8e
commit
7e929a5b6a
@ -64,7 +64,7 @@ export default function ArquivosPage() {
|
|||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1 className="font-display text-4xl font-extrabold uppercase">Anexos da operação</h1>
|
<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
|
<RegistryTable
|
||||||
countLabel={`${rows.length} arquivo${rows.length === 1 ? "" : "s"}`}
|
countLabel={`${rows.length} arquivo${rows.length === 1 ? "" : "s"}`}
|
||||||
@ -78,11 +78,11 @@ export default function ArquivosPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{rows.map((row) => (
|
{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 font-medium">{row.name}</td>
|
||||||
<td className="px-5 py-3 text-[#E8F4E5]/70">{row.type}</td>
|
<td className="px-5 py-3 text-[#66756C]">{row.type}</td>
|
||||||
<td className="px-5 py-3 text-[#E8F4E5]/70">{formatSize(row.sizeBytes)}</td>
|
<td className="px-5 py-3 text-[#66756C]">{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]">{new Date(row.createdAt).toLocaleString("pt-BR")}</td>
|
||||||
<td className="px-5 py-3 text-right">
|
<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]">
|
<a href={`/api/files/${row.id}/download`} className="btn-ghost cursor-pointer px-3 py-1.5 text-[11px]">
|
||||||
Baixar
|
Baixar
|
||||||
@ -96,7 +96,7 @@ export default function ArquivosPage() {
|
|||||||
<Modal onClose={closeModal}>
|
<Modal onClose={closeModal}>
|
||||||
<form className="flex flex-col gap-3" onSubmit={onSubmit}>
|
<form className="flex flex-col gap-3" onSubmit={onSubmit}>
|
||||||
<h2 className="font-display text-2xl font-bold uppercase">Anexar arquivo</h2>
|
<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
|
<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"
|
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"
|
type="file"
|
||||||
|
|||||||
@ -108,7 +108,7 @@ export default function ClientesPage() {
|
|||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1 className="font-display text-4xl font-extrabold uppercase">Quem opera com você</h1>
|
<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
|
<RegistryTable
|
||||||
countLabel={`${rows.length} cliente${rows.length === 1 ? "" : "s"}`}
|
countLabel={`${rows.length} cliente${rows.length === 1 ? "" : "s"}`}
|
||||||
@ -124,17 +124,17 @@ export default function ClientesPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{rows.map((row) => (
|
{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 font-medium">{row.name}</td>
|
||||||
<td className="px-5 py-3 text-[#E8F4E5]/70">{row.document || "-"}</td>
|
<td className="px-5 py-3 text-[#66756C]">{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-[#66756C]">{[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.email || row.phone || "-"}</td>
|
||||||
{canWrite ? (
|
{canWrite ? (
|
||||||
<td className="px-5 py-3">
|
<td className="px-5 py-3">
|
||||||
<button className="text-[#82C85C]" type="button" onClick={() => openEdit(row)}>
|
<button className="text-[#82C85C]" type="button" onClick={() => openEdit(row)}>
|
||||||
Editar
|
Editar
|
||||||
</button>
|
</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
|
Remover
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@ -147,7 +147,7 @@ export default function ClientesPage() {
|
|||||||
<Modal onClose={closeModal}>
|
<Modal onClose={closeModal}>
|
||||||
<form className="grid gap-3" onSubmit={onSubmit}>
|
<form className="grid gap-3" onSubmit={onSubmit}>
|
||||||
<h2 className="font-display text-2xl font-bold uppercase">{editing ? "Editar cliente" : "Novo cliente"}</h2>
|
<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="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="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 })} />
|
<input className={fieldClass} placeholder="E-mail" type="email" value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} />
|
||||||
|
|||||||
@ -86,13 +86,13 @@ export default function EquipePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (me.role === "OPERATOR") {
|
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 (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1 className="font-display text-4xl font-extrabold uppercase">Pessoas da operação</h1>
|
<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
|
<RegistryTable
|
||||||
countLabel={`${members.length} pessoa${members.length === 1 ? "" : "s"}`}
|
countLabel={`${members.length} pessoa${members.length === 1 ? "" : "s"}`}
|
||||||
@ -108,13 +108,13 @@ export default function EquipePage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{members.map((member) => (
|
{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 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">
|
<td className="px-5 py-3">
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<select
|
<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}
|
value={member.role}
|
||||||
onChange={(event) => void onRole(member.id, event.target.value as 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>
|
<span className="text-[#82C85C]">{roles.find((item) => item.id === member.role)?.label}</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</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"}
|
{member.lastLoginAt ? new Date(member.lastLoginAt).toLocaleString("pt-BR") : "Nunca"}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -139,11 +139,11 @@ export default function EquipePage() {
|
|||||||
<Modal onClose={closeModal}>
|
<Modal onClose={closeModal}>
|
||||||
<form className="grid gap-3" onSubmit={onCreate}>
|
<form className="grid gap-3" onSubmit={onCreate}>
|
||||||
<h2 className="font-display text-2xl font-bold uppercase">Novo usuário</h2>
|
<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="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="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} />
|
<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) => (
|
{roles.map((item) => (
|
||||||
<option key={item.id} value={item.id}>
|
<option key={item.id} value={item.id}>
|
||||||
{item.label}
|
{item.label}
|
||||||
|
|||||||
@ -29,18 +29,18 @@ export default function HistoricoPage() {
|
|||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1 className="font-display text-4xl font-extrabold uppercase">Simulações</h1>
|
<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 ? (
|
{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) => (
|
rows.map((row) => (
|
||||||
<li key={row.id} className="px-5 py-4">
|
<li key={row.id} className="px-5 py-4">
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{row.originLabel} → {row.destinationLabel}
|
{row.originLabel} → {row.destinationLabel}
|
||||||
</p>
|
</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.bestCarrierName} · {row.quotedPrice !== null ? money(row.quotedPrice) : "-"} · {row.distanceKm} km
|
||||||
{row.customer ? ` · ${row.customer.name}` : ""} ·{" "}
|
{row.customer ? ` · ${row.customer.name}` : ""} ·{" "}
|
||||||
{new Date(row.createdAt).toLocaleString("pt-BR")}
|
{new Date(row.createdAt).toLocaleString("pt-BR")}
|
||||||
|
|||||||
@ -39,11 +39,11 @@ export default function DashboardPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return <p className="text-[#E8F4E5]/50">Carregando indicadores…</p>;
|
return <p className="text-[#7A877F]">Carregando indicadores…</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !data) {
|
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 = [
|
const cards = [
|
||||||
@ -76,19 +76,19 @@ export default function DashboardPage() {
|
|||||||
<section>
|
<section>
|
||||||
<ul className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
<ul className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
{cards.map((card) => (
|
{cards.map((card) => (
|
||||||
<li key={card.label} className="rounded-2xl border border-white/10 px-5 py-5">
|
<li key={card.label} className="rounded-2xl border border-[#E4EBE6] bg-white px-5 py-5">
|
||||||
<p className="text-xs uppercase tracking-wider text-[#E8F4E5]/45">{card.label}</p>
|
<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="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>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</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>
|
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-[#82C85C]">Leitura</p>
|
||||||
<ol className="mt-4 grid gap-3">
|
<ol className="mt-4 grid gap-3">
|
||||||
{data.insights.map((insight) => (
|
{data.insights.map((insight) => (
|
||||||
<li key={insight} className="text-sm text-[#E8F4E5]/80">
|
<li key={insight} className="text-sm text-[#3A463F]">
|
||||||
{insight}
|
{insight}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@ -96,13 +96,13 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 grid gap-6 xl:grid-cols-2">
|
<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">
|
<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>
|
<h2 className="font-display mt-1 text-xl font-bold uppercase">Transportadoras</h2>
|
||||||
</div>
|
</div>
|
||||||
<table className="w-full text-left text-sm">
|
<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>
|
<tr>
|
||||||
<th className="px-5 py-3 font-medium">Nome</th>
|
<th className="px-5 py-3 font-medium">Nome</th>
|
||||||
<th className="px-5 py-3 font-medium">Vitórias</th>
|
<th className="px-5 py-3 font-medium">Vitórias</th>
|
||||||
@ -111,19 +111,19 @@ export default function DashboardPage() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.carriers.length === 0 ? (
|
{data.carriers.length === 0 ? (
|
||||||
<tr className="border-t border-white/10">
|
<tr className="border-t border-[#E4EBE6]">
|
||||||
<td className="px-5 py-6 text-[#E8F4E5]/45" colSpan={3}>
|
<td className="px-5 py-6 text-[#7A877F]" colSpan={3}>
|
||||||
Sem cotações no período.
|
Sem cotações no período.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
data.carriers.map((carrier) => (
|
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">{carrier.name}</td>
|
||||||
<td className="px-5 py-3 text-[#E8F4E5]/70">
|
<td className="px-5 py-3 text-[#66756C]">
|
||||||
{carrier.wins} · {carrier.sharePct}%
|
{carrier.wins} · {carrier.sharePct}%
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@ -131,13 +131,13 @@ export default function DashboardPage() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</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">
|
<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>
|
<h2 className="font-display mt-1 text-xl font-bold uppercase">Rotas mais caras</h2>
|
||||||
</div>
|
</div>
|
||||||
<table className="w-full text-left text-sm">
|
<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>
|
<tr>
|
||||||
<th className="px-5 py-3 font-medium">Rota</th>
|
<th className="px-5 py-3 font-medium">Rota</th>
|
||||||
<th className="px-5 py-3 font-medium">Vezes</th>
|
<th className="px-5 py-3 font-medium">Vezes</th>
|
||||||
@ -146,17 +146,17 @@ export default function DashboardPage() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.routes.length === 0 ? (
|
{data.routes.length === 0 ? (
|
||||||
<tr className="border-t border-white/10">
|
<tr className="border-t border-[#E4EBE6]">
|
||||||
<td className="px-5 py-6 text-[#E8F4E5]/45" colSpan={3}>
|
<td className="px-5 py-6 text-[#7A877F]" colSpan={3}>
|
||||||
Sem rotas no período.
|
Sem rotas no período.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
data.routes.map((route) => (
|
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">{route.label}</td>
|
||||||
<td className="px-5 py-3 text-[#E8F4E5]/70">{route.count}</td>
|
<td className="px-5 py-3 text-[#66756C]">{route.count}</td>
|
||||||
<td className="px-5 py-3 text-[#E8F4E5]/70">{money(route.averageCost)}</td>
|
<td className="px-5 py-3 text-[#66756C]">{money(route.averageCost)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ type Simulation = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const money = (value: number) => value.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
|
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() {
|
export default function SimularPage() {
|
||||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
const [customers, setCustomers] = useState<Customer[]>([]);
|
||||||
@ -78,15 +78,15 @@ export default function SimularPage() {
|
|||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1 className="font-display text-4xl font-extrabold uppercase">Calcular frete</h1>
|
<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.
|
Informe origem, destino e a carga. A Sinka compara as transportadoras e mostra o melhor custo.
|
||||||
</p>
|
</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"}`}>
|
<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}>
|
<form className="grid gap-3" onSubmit={onSubmit}>
|
||||||
<select
|
<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}
|
value={form.customerId}
|
||||||
onChange={(event) => setForm({ ...form, customerId: event.target.value })}
|
onChange={(event) => setForm({ ...form, customerId: event.target.value })}
|
||||||
>
|
>
|
||||||
@ -118,21 +118,21 @@ export default function SimularPage() {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<aside className="flex min-h-[18rem] flex-col items-center justify-center px-6 py-8">
|
<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" />
|
<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>
|
</aside>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{result ? (
|
{result ? (
|
||||||
<div className="mt-10">
|
<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.originLabel} → {result.destinationLabel} · {result.distanceKm} km
|
||||||
{result.distanceSource === "fallback" ? " (distância estimada)" : ""}
|
{result.distanceSource === "fallback" ? " (distância estimada)" : ""}
|
||||||
</p>
|
</p>
|
||||||
<h2 className="font-display mt-2 text-2xl font-bold uppercase">
|
<h2 className="font-display mt-2 text-2xl font-bold uppercase">
|
||||||
Melhor opção: {result.bestCarrierName} · {result.quotedPrice !== null ? money(result.quotedPrice) : "-"}
|
Melhor opção: {result.bestCarrierName} · {result.quotedPrice !== null ? money(result.quotedPrice) : "-"}
|
||||||
</h2>
|
</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) => (
|
{result.quotes.map((quote, index) => (
|
||||||
<li key={quote.carrierId} className="flex items-center justify-between px-5 py-4">
|
<li key={quote.carrierId} className="flex items-center justify-between px-5 py-4">
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@ -120,13 +120,13 @@ export default function TransportadorasPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (me && !me.permissions.includes("carriers:write")) {
|
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 (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1 className="font-display text-4xl font-extrabold uppercase">Tabelas de frete</h1>
|
<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
|
<RegistryTable
|
||||||
countLabel={`${rows.length} transportadora${rows.length === 1 ? "" : "s"}`}
|
countLabel={`${rows.length} transportadora${rows.length === 1 ? "" : "s"}`}
|
||||||
@ -140,18 +140,18 @@ export default function TransportadorasPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{rows.map((row) => (
|
{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 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-[#66756C]">{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-[#66756C]">{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-[#66756C]">{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-[#66756C]">{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]">{(Number(row.adValoremRate) * 100).toFixed(2)}%</td>
|
||||||
<td className="px-5 py-3">
|
<td className="px-5 py-3">
|
||||||
<button className="text-[#82C85C]" type="button" onClick={() => openEdit(row)}>
|
<button className="text-[#82C85C]" type="button" onClick={() => openEdit(row)}>
|
||||||
Editar
|
Editar
|
||||||
</button>
|
</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
|
Remover
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@ -163,7 +163,7 @@ export default function TransportadorasPage() {
|
|||||||
<Modal onClose={closeModal}>
|
<Modal onClose={closeModal}>
|
||||||
<form className="grid gap-3" onSubmit={onSubmit}>
|
<form className="grid gap-3" onSubmit={onSubmit}>
|
||||||
<h2 className="font-display text-2xl font-bold uppercase">{editing ? "Editar" : "Nova transportadora"}</h2>
|
<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="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="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 })} />
|
<input className={fieldClass} placeholder="Telefone" value={form.phone} onChange={(event) => setForm({ ...form, phone: event.target.value })} />
|
||||||
|
|||||||
@ -149,12 +149,12 @@ function TenantRowActions({
|
|||||||
</button>
|
</button>
|
||||||
{menu ? (
|
{menu ? (
|
||||||
<div
|
<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 }}
|
style={{ top: menu.top, right: menu.right }}
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
<button
|
<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"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMenu(null);
|
setMenu(null);
|
||||||
@ -165,7 +165,7 @@ function TenantRowActions({
|
|||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
href={`/admin/chat/${tenant.slug}`}
|
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
|
Chat
|
||||||
{chatCount ? (
|
{chatCount ? (
|
||||||
@ -176,7 +176,7 @@ function TenantRowActions({
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={`/${tenant.slug}/login`}
|
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"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
@ -298,7 +298,7 @@ export default function PlatformClientesPage() {
|
|||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1 className="font-display text-4xl font-extrabold uppercase">Clientes</h1>
|
<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
|
<RegistryTable
|
||||||
scroll={false}
|
scroll={false}
|
||||||
@ -313,10 +313,10 @@ export default function PlatformClientesPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{tenants.map((tenant) => (
|
{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 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-[#66756C]">/{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.status === "ACTIVE" ? "Ativo" : "Suspenso"}</td>
|
||||||
<td className="overflow-hidden px-5 py-3">
|
<td className="overflow-hidden px-5 py-3">
|
||||||
<TenantRowActions
|
<TenantRowActions
|
||||||
tenant={tenant}
|
tenant={tenant}
|
||||||
@ -333,7 +333,7 @@ export default function PlatformClientesPage() {
|
|||||||
{created ? (
|
{created ? (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-display text-2xl font-bold uppercase">Cliente criado</h2>
|
<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">
|
<p className="mt-4 text-sm">
|
||||||
Endereço:{" "}
|
Endereço:{" "}
|
||||||
<a className="text-[#82C85C]" href={created.access.path} target="_blank" rel="noopener noreferrer">
|
<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}>
|
<form className="grid gap-3" onSubmit={onCreate}>
|
||||||
<h2 className="font-display text-2xl font-bold uppercase">Novo cliente</h2>
|
<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
|
<input
|
||||||
className={fieldClass}
|
className={fieldClass}
|
||||||
placeholder="Nome"
|
placeholder="Nome"
|
||||||
@ -383,13 +383,13 @@ export default function PlatformClientesPage() {
|
|||||||
{usersOf ? (
|
{usersOf ? (
|
||||||
<Modal wide onClose={closeUsers}>
|
<Modal wide onClose={closeUsers}>
|
||||||
<h2 className="font-display text-2xl font-bold uppercase">Usuários · {usersOf.name}</h2>
|
<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.
|
Contas de {usersOf.name}. Se a senha estiver vazia, gere uma nova.
|
||||||
</p>
|
</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">
|
<div className="mt-5 overflow-x-auto">
|
||||||
<table className="w-full text-left text-sm">
|
<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>
|
<tr>
|
||||||
<th className="pb-3 pr-6 font-medium">Nome</th>
|
<th className="pb-3 pr-6 font-medium">Nome</th>
|
||||||
<th className="pb-3 pr-6 font-medium">Usuário</th>
|
<th className="pb-3 pr-6 font-medium">Usuário</th>
|
||||||
@ -401,20 +401,20 @@ export default function PlatformClientesPage() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{users.length === 0 ? (
|
{users.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="py-6 text-[#E8F4E5]/50" colSpan={5}>
|
<td className="py-6 text-[#7A877F]" colSpan={5}>
|
||||||
Nenhum usuário neste cliente.
|
Nenhum usuário neste cliente.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
users.map((user) => (
|
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 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 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">
|
<td className="py-3 whitespace-nowrap">
|
||||||
<button
|
<button
|
||||||
className="text-xs text-[#E8F4E5]/70"
|
className="text-xs text-[#66756C]"
|
||||||
type="button"
|
type="button"
|
||||||
disabled={issuing === user.id}
|
disabled={issuing === user.id}
|
||||||
onClick={() => void issuePassword(user.id)}
|
onClick={() => void issuePassword(user.id)}
|
||||||
|
|||||||
@ -25,25 +25,25 @@ export default function PlatformIntegracoesPage() {
|
|||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1 className="font-display text-4xl font-extrabold uppercase">Integrações</h1>
|
<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}
|
{error ? <p className="mt-4 text-sm text-red-600">{error}</p> : null}
|
||||||
{loading ? <p className="mt-4 text-sm text-[#E8F4E5]/50">Verificando integrações…</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">
|
<ul className="mt-8 grid gap-3 md:grid-cols-2">
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const active = item.status === "active";
|
const active = item.status === "active";
|
||||||
return (
|
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">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<h2 className="text-lg font-semibold">{item.name}</h2>
|
<h2 className="text-lg font-semibold">{item.name}</h2>
|
||||||
<span
|
<span
|
||||||
className={`shrink-0 rounded-full px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wider ${
|
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"}
|
{active ? "Ativo" : "Inativo"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export default function PlatformPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<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">
|
<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",
|
hint: "no chat de suporte",
|
||||||
},
|
},
|
||||||
].map((card) => (
|
].map((card) => (
|
||||||
<li key={card.label} className="rounded-2xl border border-white/10 px-5 py-5">
|
<li key={card.label} className="rounded-2xl border border-[#E4EBE6] bg-white px-5 py-5">
|
||||||
<p className="text-xs uppercase tracking-wider text-[#E8F4E5]/45">{card.label}</p>
|
<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="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>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -32,6 +32,12 @@ body {
|
|||||||
font-family: var(--font-sans-face), system-ui, sans-serif;
|
font-family: var(--font-sans-face), system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html:has(.sinka-app),
|
||||||
|
body:has(.sinka-app) {
|
||||||
|
background: #f4f7f5;
|
||||||
|
color: #1b241e;
|
||||||
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
@ -165,6 +171,16 @@ h3,
|
|||||||
color: #e8f4e5;
|
color: #e8f4e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sinka-app .btn-ghost {
|
||||||
|
border-color: #c5d9c0;
|
||||||
|
color: #1b241e;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sinka-app .btn-ghost:hover {
|
||||||
|
background: #f3f6f4;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-google {
|
.btn-google {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -18,14 +18,14 @@ export function RegistryTable({
|
|||||||
scroll?: boolean;
|
scroll?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
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">
|
<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 />}
|
{action ?? <span />}
|
||||||
</div>
|
</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]" : ""}`}>
|
<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>
|
<tr>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<th key={column} className="px-5 py-3 font-medium">
|
<th key={column} className="px-5 py-3 font-medium">
|
||||||
@ -37,7 +37,7 @@ export function RegistryTable({
|
|||||||
<tbody>
|
<tbody>
|
||||||
{isEmpty ? (
|
{isEmpty ? (
|
||||||
<tr>
|
<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."}
|
{empty ?? "Nenhum registro."}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -61,9 +61,9 @@ export function Modal({
|
|||||||
wide?: boolean;
|
wide?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
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
|
<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"
|
wide ? "max-w-5xl" : "max-w-md"
|
||||||
}`}
|
}`}
|
||||||
onClick={(event) => event.stopPropagation()}
|
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]";
|
||||||
|
|||||||
@ -6,6 +6,26 @@ import { usePathname, useRouter } from "next/navigation";
|
|||||||
import { useEffect, useState, type ReactNode } from "react";
|
import { useEffect, useState, type ReactNode } from "react";
|
||||||
import { api, type SessionUser } from "@/lib/api";
|
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({
|
export function RestrictedShell({
|
||||||
kind,
|
kind,
|
||||||
basePath,
|
basePath,
|
||||||
@ -20,6 +40,7 @@ export function RestrictedShell({
|
|||||||
const [user, setUser] = useState<SessionUser | null>(null);
|
const [user, setUser] = useState<SessionUser | null>(null);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
const [navigating, setNavigating] = useState(false);
|
||||||
const loginPath = kind === "platform" ? "/admin/login" : `${basePath}/login`;
|
const loginPath = kind === "platform" ? "/admin/login" : `${basePath}/login`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -49,18 +70,27 @@ export function RestrictedShell({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
|
setNavigating(false);
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!navigating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timeout = window.setTimeout(() => setNavigating(false), 8000);
|
||||||
|
return () => window.clearTimeout(timeout);
|
||||||
|
}, [navigating]);
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await api("/api/auth/logout", { method: "POST" }).catch(() => undefined);
|
await api("/api/auth/logout", { method: "POST" }).catch(() => undefined);
|
||||||
router.replace(loginPath);
|
router.replace(loginPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return error ? (
|
||||||
<main className="flex min-h-dvh items-center justify-center bg-black text-[#E8F4E5]/60">
|
<main className="flex min-h-dvh items-center justify-center bg-[#F4F7F5] text-[#66756C]">{error}</main>
|
||||||
{error || "Carregando…"}
|
) : (
|
||||||
</main>
|
<BrandLoading />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,17 +103,17 @@ export function RestrictedShell({
|
|||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{ href: basePath, label: "Dashboard" },
|
{ href: basePath, label: "Dashboard" },
|
||||||
|
{ href: `${basePath}/clientes`, label: "Clientes" },
|
||||||
{ href: `${basePath}/simular`, label: "Simular" },
|
{ href: `${basePath}/simular`, label: "Simular" },
|
||||||
{ href: `${basePath}/historico`, label: "Histórico" },
|
{ 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")
|
...(user.permissions.includes("carriers:write")
|
||||||
? [{ href: `${basePath}/transportadoras`, label: "Transportadoras" }]
|
? [{ href: `${basePath}/transportadoras`, label: "Transportadoras" }]
|
||||||
: []),
|
: []),
|
||||||
...(user.permissions.includes("users:manage") || user.role === "MANAGER"
|
...(user.permissions.includes("users:manage") || user.role === "MANAGER"
|
||||||
? [{ href: `${basePath}/equipe`, label: "Equipe" }]
|
? [{ href: `${basePath}/equipe`, label: "Equipe" }]
|
||||||
: []),
|
: []),
|
||||||
|
{ href: `${basePath}/arquivos`, label: "Arquivos" },
|
||||||
|
{ href: `${basePath}/chat`, label: "Chat" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function isActive(href: string) {
|
function isActive(href: string) {
|
||||||
@ -96,10 +126,19 @@ export function RestrictedShell({
|
|||||||
return href === basePath ? pathname === basePath : pathname.startsWith(href);
|
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 = (
|
const nav = (
|
||||||
<>
|
<>
|
||||||
<div className="px-5 pb-6 pt-7">
|
<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" />
|
<Image src="/brand/sinka-logo.webp" alt="Sinka" width={140} height={46} className="h-6 w-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
<p className="mt-5 text-[11px] font-semibold uppercase tracking-[0.22em] text-[#82C85C]">
|
<p className="mt-5 text-[11px] font-semibold uppercase tracking-[0.22em] text-[#82C85C]">
|
||||||
@ -111,10 +150,11 @@ export function RestrictedShell({
|
|||||||
<Link
|
<Link
|
||||||
key={link.href}
|
key={link.href}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
|
onClick={() => startNavigation(link.href)}
|
||||||
className={`rounded-lg px-3 py-3 text-base ${
|
className={`rounded-lg px-3 py-3 text-base ${
|
||||||
isActive(link.href)
|
isActive(link.href)
|
||||||
? "bg-[#82C85C]/12 font-medium text-[#82C85C]"
|
? "bg-[#82C85C]/18 font-medium text-[#82C85C]"
|
||||||
: "text-[#E8F4E5]/70 hover:bg-white/5 hover:text-[#E8F4E5]"
|
: "text-white/70 hover:bg-white/10 hover:text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
@ -122,10 +162,10 @@ export function RestrictedShell({
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
<div className="mt-auto border-t border-white/10 px-5 py-5">
|
<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>
|
<p className="mt-0.5 text-xs text-[#82C85C]">{user.roleLabel}</p>
|
||||||
<button
|
<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"
|
type="button"
|
||||||
onClick={() => void logout()}
|
onClick={() => void logout()}
|
||||||
>
|
>
|
||||||
@ -136,13 +176,13 @@ export function RestrictedShell({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-dvh bg-black text-[#E8F4E5] lg:flex">
|
<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 px-4 py-3 lg:hidden">
|
<header className="flex items-center justify-between border-b border-white/10 bg-[#163027] px-4 py-3 lg:hidden">
|
||||||
<Link href={basePath}>
|
<Link href={basePath} onClick={() => startNavigation(basePath)}>
|
||||||
<Image src="/brand/sinka-logo.webp" alt="Sinka" width={120} height={40} className="h-6 w-auto" />
|
<Image src="/brand/sinka-logo.webp" alt="Sinka" width={120} height={40} className="h-6 w-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<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"
|
type="button"
|
||||||
onClick={() => setMenuOpen((open) => !open)}
|
onClick={() => setMenuOpen((open) => !open)}
|
||||||
>
|
>
|
||||||
@ -152,7 +192,7 @@ export function RestrictedShell({
|
|||||||
|
|
||||||
{menuOpen ? (
|
{menuOpen ? (
|
||||||
<button
|
<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"
|
type="button"
|
||||||
aria-label="Fechar menu"
|
aria-label="Fechar menu"
|
||||||
onClick={() => setMenuOpen(false)}
|
onClick={() => setMenuOpen(false)}
|
||||||
@ -160,7 +200,7 @@ export function RestrictedShell({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<aside
|
<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"
|
menuOpen ? "translate-x-0" : "-translate-x-full"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -168,6 +208,7 @@ export function RestrictedShell({
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main className="min-w-0 flex-1 px-5 py-8 lg:px-10 lg:py-10">{children}</main>
|
<main className="min-w-0 flex-1 px-5 py-8 lg:px-10 lg:py-10">{children}</main>
|
||||||
|
{navigating ? <BrandLoading /> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,12 +99,12 @@ export function SupportChat({
|
|||||||
</a>
|
</a>
|
||||||
) : null}
|
) : null}
|
||||||
<h1 className="font-display text-4xl font-extrabold uppercase">{heading}</h1>
|
<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">
|
<div className="flex-1 space-y-3 overflow-y-auto px-5 py-4">
|
||||||
{messages.length === 0 ? (
|
{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) => (
|
messages.map((message) => (
|
||||||
<article
|
<article
|
||||||
@ -112,7 +112,7 @@ export function SupportChat({
|
|||||||
className={`max-w-[80%] rounded-2xl px-4 py-3 text-sm ${
|
className={`max-w-[80%] rounded-2xl px-4 py-3 text-sm ${
|
||||||
message.authorKind === "platform"
|
message.authorKind === "platform"
|
||||||
? "ml-auto bg-[#82C85C] text-black"
|
? "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>
|
<p className="text-[11px] uppercase tracking-wider opacity-70">{message.authorName}</p>
|
||||||
@ -122,9 +122,9 @@ export function SupportChat({
|
|||||||
)}
|
)}
|
||||||
<div ref={bottomRef} />
|
<div ref={bottomRef} />
|
||||||
</div>
|
</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
|
<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"
|
placeholder="Escrever mensagem"
|
||||||
value={text}
|
value={text}
|
||||||
onChange={(event) => setText(event.target.value)}
|
onChange={(event) => setText(event.target.value)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user