diff --git a/README.md b/README.md
index ae9a74f..590ec18 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ SaaS de inteligência logística. Cada operação simula frete, compara transpor
- Chat interno (admin da plataforma ↔ empresa)
- Arquivos de operação (CSV/XLSX) por empresa
-Dois produtos no mesmo sistema: o **admin da Sinka** (`/admin`) cria e suspende empresas; cada empresa entra pelo slug na URL (`/demo`, `/outra-empresa`).
+Dois produtos no mesmo sistema: o **admin da Sinka** (`/admin`) cria e suspende empresas; cada empresa entra pelo slug na URL (`/cliente-1`, `/cliente-2`).
## Isolamento
@@ -68,9 +68,10 @@ docker compose --profile full up -d --build
| Área | URL | E-mail | Senha |
| --- | --- | --- | --- |
| Plataforma | `/admin/login` | tina.r@example.net | SinkaPlatform!1 |
-| Empresa (admin) | `/demo/login` | xena.w@example.org | SinkaAdmin!1 |
-| Empresa (manager) | `/demo/login` | james.b@example.com | SinkaManager!1 |
-| Empresa (operador) | `/demo/login` | ursula.b@example.com | SinkaOperador!1 |
+| Cliente 1 (admin) | `/cliente-1/login` | xena.w@example.org | SinkaAdmin!1 |
+| Cliente 1 (manager) | `/cliente-1/login` | james.b@example.com | SinkaManager!1 |
+| Cliente 1 (operador) | `/cliente-1/login` | ursula.b@example.com | SinkaOperador!1 |
+| Cliente 2 (admin) | `/cliente-2/login` | ivan.p@example.net | Cliente2Admin!1 |
## Testes
diff --git a/api/prisma/seed.ts b/api/prisma/seed.ts
index 410e648..98e4bd7 100644
--- a/api/prisma/seed.ts
+++ b/api/prisma/seed.ts
@@ -24,10 +24,70 @@ async function upsertUser(
});
}
+async function ensureTenant(
+ platform: PlatformPrisma,
+ name: string,
+ slug: string,
+ aliases: string[] = [],
+) {
+ let tenant = await platform.tenant.findUnique({ where: { slug } });
+ if (!tenant) {
+ for (const alias of aliases) {
+ const old = await platform.tenant.findUnique({ where: { slug: alias } });
+ if (old) {
+ tenant = await platform.tenant.update({
+ where: { id: old.id },
+ data: { name, slug },
+ });
+ break;
+ }
+ }
+ }
+ if (!tenant) {
+ const database = databaseForSlug(slug);
+ await createMysqlDatabase(database);
+ await pushTenantSchema(database);
+ tenant = await platform.tenant.create({ data: { name, slug, database } });
+ } else {
+ await pushTenantSchema(tenant.database);
+ if (tenant.name !== name) {
+ tenant = await platform.tenant.update({ where: { id: tenant.id }, data: { name } });
+ }
+ }
+ return tenant;
+}
+
+async function seedCompany(
+ tenant: { database: string },
+ users: { email: string; name: string; password: string; role: 'ADMIN' | 'MANAGER' | 'OPERATOR' }[],
+ customers: { name: string; document: string; city: string; state: string }[],
+ carriers: { name: string; baseFee: number; pricePerKg: number; pricePerKm: number; adValoremRate: number }[],
+) {
+ const db = new TenantPrisma({
+ datasources: { db: { url: urlForDatabase(tenant.database) } },
+ });
+ for (const user of users) {
+ await upsertUser(db, user.email, user.name, user.password, user.role);
+ }
+ for (const customer of customers) {
+ const existing = await db.customer.findFirst({ where: { name: customer.name } });
+ if (!existing) {
+ await db.customer.create({ data: customer });
+ }
+ }
+ for (const carrier of carriers) {
+ const existing = await db.carrier.findFirst({ where: { name: carrier.name } });
+ if (existing) {
+ await db.carrier.update({ where: { id: existing.id }, data: carrier });
+ } else {
+ await db.carrier.create({ data: carrier });
+ }
+ }
+ await db.$disconnect();
+}
+
async function main() {
const platform = new PlatformPrisma();
- const slug = 'demo';
- const database = databaseForSlug(slug);
const platformEmail = process.env.PLATFORM_ADMIN_EMAIL ?? 'tina.r@example.net';
const platformPassword = process.env.PLATFORM_ADMIN_PASSWORD ?? 'SinkaPlatform!1';
@@ -42,75 +102,68 @@ async function main() {
},
});
- let tenant = await platform.tenant.findUnique({ where: { slug } });
- if (!tenant) {
- await createMysqlDatabase(database);
- await pushTenantSchema(database);
- tenant = await platform.tenant.create({
- data: { name: 'Sinka Demo', slug, database },
- });
- } else {
- await pushTenantSchema(tenant.database);
- }
+ const cliente1 = await ensureTenant(platform, 'Cliente 1', 'cliente-1', ['demo']);
+ const cliente2 = await ensureTenant(platform, 'Cliente 2', 'cliente-2');
- const others = await platform.tenant.findMany({ where: { slug: { not: slug } } });
+ const others = await platform.tenant.findMany({
+ where: { slug: { notIn: ['cliente-1', 'cliente-2'] } },
+ });
for (const item of others) {
await pushTenantSchema(item.database);
}
- const db = new TenantPrisma({
- datasources: { db: { url: urlForDatabase(tenant.database) } },
- });
-
- await upsertUser(
- db,
- process.env.DEMO_ADMIN_EMAIL ?? 'xena.w@example.org',
- 'Ana Admin',
- process.env.DEMO_ADMIN_PASSWORD ?? 'SinkaAdmin!1',
- 'ADMIN',
- );
- await upsertUser(
- db,
- process.env.DEMO_MANAGER_EMAIL ?? 'james.b@example.com',
- 'Marcos Manager',
- process.env.DEMO_MANAGER_PASSWORD ?? 'SinkaManager!1',
- 'MANAGER',
- );
- await upsertUser(
- db,
- process.env.DEMO_OPERATOR_EMAIL ?? 'ursula.b@example.com',
- 'Olga Operador',
- process.env.DEMO_OPERATOR_PASSWORD ?? 'SinkaOperador!1',
- 'OPERATOR',
+ await seedCompany(
+ cliente1,
+ [
+ {
+ email: process.env.DEMO_ADMIN_EMAIL ?? 'xena.w@example.org',
+ name: 'Ana Admin',
+ password: process.env.DEMO_ADMIN_PASSWORD ?? 'SinkaAdmin!1',
+ role: 'ADMIN',
+ },
+ {
+ email: process.env.DEMO_MANAGER_EMAIL ?? 'james.b@example.com',
+ name: 'Marcos Manager',
+ password: process.env.DEMO_MANAGER_PASSWORD ?? 'SinkaManager!1',
+ role: 'MANAGER',
+ },
+ {
+ email: process.env.DEMO_OPERATOR_EMAIL ?? 'ursula.b@example.com',
+ name: 'Olga Operador',
+ password: process.env.DEMO_OPERATOR_PASSWORD ?? 'SinkaOperador!1',
+ role: 'OPERATOR',
+ },
+ ],
+ [
+ { name: 'Mercado Aurora', document: '12.345.678/0001-90', city: 'São Paulo', state: 'SP' },
+ { name: 'Café do Litoral', document: '98.765.432/0001-10', city: 'Santos', state: 'SP' },
+ ],
+ [
+ { name: 'Litoral Mix', baseFee: 45, pricePerKg: 1.8, pricePerKm: 0.85, adValoremRate: 0.003 },
+ { name: 'Rota Sul', baseFee: 32, pricePerKg: 1.4, pricePerKm: 0.72, adValoremRate: 0.004 },
+ { name: 'Express RJ', baseFee: 60, pricePerKg: 2.2, pricePerKm: 1.1, adValoremRate: 0.002 },
+ ],
);
- for (const customer of [
- { name: 'Mercado Aurora', document: '12.345.678/0001-90', city: 'São Paulo', state: 'SP' },
- { name: 'Café do Litoral', document: '98.765.432/0001-10', city: 'Santos', state: 'SP' },
- ]) {
- const existing = await db.customer.findFirst({ where: { name: customer.name } });
- if (!existing) {
- await db.customer.create({ data: customer });
- }
- }
+ await seedCompany(
+ cliente2,
+ [
+ {
+ email: 'ivan.p@example.net',
+ name: 'Bruno Admin',
+ password: 'Cliente2Admin!1',
+ role: 'ADMIN',
+ },
+ ],
+ [
+ { name: 'Padaria Centro', document: '11.222.333/0001-44', city: 'Curitiba', state: 'PR' },
+ { name: 'Loja Norte', document: '55.666.777/0001-88', city: 'Joinville', state: 'SC' },
+ ],
+ [{ name: 'Carga Paraná', baseFee: 38, pricePerKg: 1.5, pricePerKm: 0.7, adValoremRate: 0.0035 }],
+ );
- const carriers = [
- { name: 'Litoral Mix', baseFee: 45, pricePerKg: 1.8, pricePerKm: 0.85, adValoremRate: 0.003 },
- { name: 'Rota Sul', baseFee: 32, pricePerKg: 1.4, pricePerKm: 0.72, adValoremRate: 0.004 },
- { name: 'Express RJ', baseFee: 60, pricePerKg: 2.2, pricePerKm: 1.1, adValoremRate: 0.002 },
- ];
- for (const carrier of carriers) {
- const existing = await db.carrier.findFirst({ where: { name: carrier.name } });
- if (existing) {
- await db.carrier.update({ where: { id: existing.id }, data: carrier });
- } else {
- await db.carrier.create({ data: carrier });
- }
- }
-
- await db.$disconnect();
await platform.$disconnect();
- console.log('Seed ok: platform + tenant demo (admin, manager, operador).');
+ console.log('Seed ok: platform + Cliente 1 + Cliente 2.');
}
main().catch(async (error) => {
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 720ea4e..96e4488 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -14,4 +14,4 @@
Document root de produto: o frontend Next.js. A API não serve HTML.
-Login de empresa exige o slug **na URL** (`/demo`, `/filial-sul`). O JWT carrega `tenantId` + papel; as queries usam o PrismaClient daquele database.
+Login de empresa exige o slug **na URL** (`/cliente-1`, `/cliente-2`). O JWT carrega `tenantId` + papel; as queries usam o PrismaClient daquele database.
diff --git a/web/next.config.ts b/web/next.config.ts
index 27ed0fe..09feabd 100644
--- a/web/next.config.ts
+++ b/web/next.config.ts
@@ -13,9 +13,9 @@ const nextConfig = {
},
async redirects() {
return [
- { source: "/login", destination: "/demo/login", permanent: false },
- { source: "/app", destination: "/demo", permanent: false },
- { source: "/app/:path*", destination: "/demo/:path*", permanent: false },
+ { source: "/login", destination: "/cliente-1/login", permanent: false },
+ { source: "/app", destination: "/cliente-1", permanent: false },
+ { source: "/app/:path*", destination: "/cliente-1/:path*", permanent: false },
{ source: "/platform", destination: "/admin", permanent: false },
{ source: "/platform/:path*", destination: "/admin/:path*", permanent: false },
];
diff --git a/web/src/app/admin/(panel)/page.tsx b/web/src/app/admin/(panel)/page.tsx
index 05f1b19..85d2a3a 100644
--- a/web/src/app/admin/(panel)/page.tsx
+++ b/web/src/app/admin/(panel)/page.tsx
@@ -225,7 +225,7 @@ export default function PlatformPage() {
/>
setSlug(event.target.value.toLowerCase())}
required
diff --git a/web/src/components/landing/sinka-landing.tsx b/web/src/components/landing/sinka-landing.tsx
index 8713755..5354ac1 100644
--- a/web/src/components/landing/sinka-landing.tsx
+++ b/web/src/components/landing/sinka-landing.tsx
@@ -49,7 +49,7 @@ export function SinkaLanding() {
Conhecer a plataforma
-
+
Começar agora
@@ -175,7 +175,7 @@ export function SinkaLanding() {
Conhecer a Sinka
-
+
Começar agora
diff --git a/web/src/components/landing/site-header.tsx b/web/src/components/landing/site-header.tsx
index 9ad4c1c..94b1eeb 100644
--- a/web/src/components/landing/site-header.tsx
+++ b/web/src/components/landing/site-header.tsx
@@ -43,7 +43,7 @@ export function SiteHeader() {
Conhecer a plataforma
Entrar