chore: reorganize project structure and ensure default queues

This commit is contained in:
Esdras Renan 2025-10-06 22:59:35 -03:00
parent 854887f499
commit 1cccb852a5
201 changed files with 417 additions and 838 deletions

52
.gitignore vendored
View file

@ -1,9 +1,45 @@
# Root ignore for monorepo
web/node_modules/
web/.next/
web/.turbo/
web/out/
web/.env.local
web/.env*
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
Thumbs.db
*.pem
*.sqlite
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# backups locais
.archive/

View file

@ -1,3 +1,7 @@
# Roadmap de Próximos Passos
Lista priorizada de evoluções propostas para o Sistema de Chamados. Confira `agents.md` para visão geral, escopo atual e diretrizes de uso.
# 🧩 Permissões e acessos
- [x] Criar perfil **Gestor da Empresa (cliente)** com permissões específicas

69
README.md Normal file
View file

@ -0,0 +1,69 @@
## Sistema de Chamados
Aplicação Next.js 15 com Convex e Better Auth para gestão de tickets da Rever. Todo o código-fonte está organizado diretamente na raiz do repositório, conforme convenções do Next.js.
## Requisitos
- Node.js >= 20
- pnpm >= 8
- CLI do Convex (`pnpm dlx convex dev` instalará automaticamente no primeiro uso)
## Configuração rápida
1. Instale as dependências:
```bash
pnpm install
```
2. Ajuste o arquivo `.env` (ou crie a partir do exemplo) e confirme os valores de:
- `NEXT_PUBLIC_CONVEX_URL` (gerado pelo Convex Dev)
- `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`, `DATABASE_URL`
3. Aplique as migrações e gere o client Prisma:
```bash
pnpm prisma migrate deploy
pnpm prisma:generate
```
4. Popule usuários padrão do Better Auth:
```bash
pnpm auth:seed
```
5. (Opcional) Para re-sincronizar manualmente as filas padrão, execute:
```bash
pnpm queues:ensure
```
6. Em um terminal, execute o backend em tempo real do Convex:
```bash
pnpm convex:dev
```
7. Em outro terminal, suba o frontend Next.js:
```bash
pnpm dev
```
8. Com o Convex ativo, acesse `http://localhost:3000/dev/seed` uma vez para popular dados de demonstração (tickets, usuários, comentários) diretamente no banco do Convex.
> Se o CLI perguntar sobre configuração do projeto Convex, escolha criar um novo deployment local (opção padrão) e confirme. As credenciais são armazenadas em `.convex/` automaticamente.
## Scripts úteis
- `pnpm lint` — ESLint com as regras do projeto.
- `pnpm exec vitest run` — suíte de testes unitários.
- `pnpm auth:seed` — atualiza/cria contas padrão do Better Auth (credenciais em `agents.md`).
- `pnpm prisma migrate deploy` — aplica migrações ao banco SQLite local.
- `pnpm convex:dev` — roda o Convex em modo desenvolvimento, gerando tipos em `convex/_generated`.
## Estrutura principal
- `app/` dentro de `src/` — rotas e layouts do Next.js (App Router).
- `components/` — componentes reutilizáveis (UI, formulários, layouts).
- `convex/` — queries, mutations e seeds do Convex.
- `prisma/` — schema, migrações e banco SQLite (`prisma/db.sqlite`).
- `scripts/` — utilitários em Node para sincronização e seeds adicionais.
- `agents.md` — guia operacional e contexto funcional (em PT-BR).
- `PROXIMOS_PASSOS.md` — backlog de melhorias futuras.
## Credenciais de demonstração
Após executar `pnpm auth:seed`, as credenciais padrão ficam disponíveis conforme descrito em `agents.md` (seção “Credenciais padrão”). Ajuste variáveis `SEED_USER_*` se precisar sobrepor usuários ou senhas durante o seed.
## Próximos passos
Consulte `PROXIMOS_PASSOS.md` para acompanhar o backlog funcional e o progresso das iniciativas planejadas.

View file

@ -1,24 +1,27 @@
# Plano de Desenvolvimento — Sistema de Chamados
# Plano de Desenvolvimento — Sistema de Chamados
> **Diretriz máxima:** todas as respostas, comunicações e documentações devem ser redigidas em português brasileiro.
## Contato principal
- **Esdras Renan** — monkeyesdras@gmail.com
## Credenciais padrão (Better Auth)
- Administrador: `admin@sistema.dev` / `admin123`
- Agente Demo: `agente.demo@sistema.dev` / `agent123`
- Cliente Demo: `cliente.demo@sistema.dev` / `cliente123`
> Execute `pnpm --dir web auth:seed` após configurar `.env.local`. O script atualiza as contas acima ou cria novas conforme variáveis `SEED_USER_*`.
## Credenciais padrão (Better Auth)
- Administrador: `admin@sistema.dev` / `admin123`
- Agente Demo: `agente.demo@sistema.dev` / `agent123`
- Cliente Demo: `cliente.demo@sistema.dev` / `cliente123`
> Execute `pnpm auth:seed` após configurar `.env`. O script atualiza as contas acima ou cria novas conforme variáveis `SEED_USER_*`.
## Sincronização com Convex
- Usuários e tickets demo são garantidos via `web/convex/seed.ts`.
- Com `pnpm convex:dev` rodando, acesse `/dev/seed` uma vez para popular dados quando necessário.
## Sincronização com Convex
- Usuários e tickets demo são garantidos via `convex/seed.ts`.
- Após iniciar `pnpm convex:dev`, acesse `/dev/seed` uma vez por ambiente local para carregar dados reais de demonstração no banco do Convex.
## Setup local rápido
1. `cd web && pnpm install`
2. `cp .env.example .env.local` e ajuste `NEXT_PUBLIC_CONVEX_URL` apontando para o servidor Convex local.
3. `pnpm --dir web auth:seed`
4. `pnpm --dir web convex:dev`
5. Em outro terminal: `pnpm --dir web dev`
## Setup local rápido
1. `pnpm install`
2. Ajuste `.env` (ou crie a partir do exemplo) e confirme `NEXT_PUBLIC_CONVEX_URL` apontando para o Convex local.
3. `pnpm auth:seed`
4. (Opcional) `pnpm queues:ensure`
5. `pnpm convex:dev`
6. Em outro terminal: `pnpm dev`
## Estado atual
- Autenticação Better Auth com guardas client-side (`AuthGuard`) bloqueando rotas protegidas.
@ -52,9 +55,9 @@
3. Implementar ações rápidas (status/fila) diretamente na listagem de tickets.
4. Definir limites e monitoramento para anexos por tenant.
## Rotina antes de abrir PR
- `pnpm --dir "web" lint`
- `pnpm --dir "web" exec vitest run`
## Rotina antes de abrir PR
- `pnpm lint`
- `pnpm exec vitest run`
- Revisar toasts/labels em PT-BR e ausência de segredos no diff.
- Adicionar coautor `factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>` quando aplicável.
@ -64,12 +67,12 @@
- Reutilizar componentes shadcn existentes e seguir o estilo do arquivo editado.
- Validações client-side críticas devem sinalizar erros inline e exibir toast.
## Estrutura útil
- `web/convex/` — queries e mutations (ex.: `tickets.ts`, `users.ts`).
- `web/src/components/tickets/` — UI interna (dialog, listas, header, timeline).
- `web/src/components/portal/` — formulários e fluxos do portal do cliente.
- `web/scripts/` — seeds Better Auth e utilidades.
- `web/src/components/auth/auth-guard.tsx` — proteção de rotas client-side.
## Estrutura útil
- `convex/` — queries e mutations (ex.: `tickets.ts`, `users.ts`).
- `src/components/tickets/` — UI interna (dialog, listas, header, timeline).
- `src/components/portal/` — formulários e fluxos do portal do cliente.
- `scripts/` — seeds Better Auth e utilidades.
- `src/components/auth/auth-guard.tsx` — proteção de rotas client-side.
## Histórico resumido
- Scaffold Next.js + Turbopack configurado com Better Auth e Convex.

View file

@ -3,7 +3,7 @@
> next build
Ôû▓ Next.js 15.5.3
- Environments: .env.local
- Environments: .env
Creating an optimized production build ...
Ô£ô Compiled successfully in 3.0s

90
convex/README.md Normal file
View file

@ -0,0 +1,90 @@
# Welcome to your Convex functions directory!
Write your Convex functions here.
See https://docs.convex.dev/functions for more.
A query function that takes two arguments looks like:
```ts
// convex/myFunctions.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const myQueryFunction = query({
// Validators for arguments.
args: {
first: v.number(),
second: v.string(),
},
// Function implementation.
handler: async (ctx, args) => {
// Read the database as many times as you need here.
// See https://docs.convex.dev/database/reading-data.
const documents = await ctx.db.query("tablename").collect();
// Arguments passed from the client are properties of the args object.
console.log(args.first, args.second);
// Write arbitrary JavaScript here: filter, aggregate, build derived data,
// remove non-public properties, or create new objects.
return documents;
},
});
```
Using this query function in a React component looks like:
```ts
const data = useQuery(api.myFunctions.myQueryFunction, {
first: 10,
second: "hello",
});
```
A mutation function looks like:
```ts
// convex/myFunctions.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const myMutationFunction = mutation({
// Validators for arguments.
args: {
first: v.string(),
second: v.string(),
},
// Function implementation.
handler: async (ctx, args) => {
// Insert or modify documents in the database here.
// Mutations can also read from the database like queries.
// See https://docs.convex.dev/database/writing-data.
const message = { body: args.first, author: args.second };
const id = await ctx.db.insert("messages", message);
// Optionally, return a value from your mutation.
return await ctx.db.get(id);
},
});
```
Using this mutation function in a React component looks like:
```ts
const mutation = useMutation(api.myFunctions.myMutationFunction);
function handleButtonPress() {
// fire and forget, the most common way to use mutations
mutation({ first: "Hello!", second: "me" });
// OR
// use the result once the mutation has completed
mutation({ first: "Hello!", second: "me" }).then((result) =>
console.log(result),
);
}
```
Use the Convex CLI to push your functions to a deployment. See everything
the Convex CLI can do by running `npx convex -h` in your project root
directory. To learn more, launch the docs with `npx convex docs`.

View file

@ -13,6 +13,7 @@ const QUEUE_RENAME_LOOKUP: Record<string, string> = {
"suporte-n2": "Laboratório",
laboratorio: "Laboratório",
Laboratorio: "Laboratório",
visitas: "Visitas",
};
function renameQueueString(value: string) {
@ -201,4 +202,3 @@ export const remove = mutation({
await ctx.db.delete(queueId);
},
});

View file

@ -46,6 +46,7 @@ const QUEUE_RENAME_LOOKUP: Record<string, string> = {
"suporte-n2": "Laboratório",
laboratorio: "Laboratório",
Laboratorio: "Laboratório",
visitas: "Visitas",
};
function renameQueueString(value?: string | null): string | null {
@ -59,14 +60,14 @@ function renameQueueString(value?: string | null): string | null {
function normalizeQueueName(queue?: Doc<"queues"> | null): string | null {
if (!queue) return null;
const normalized = renameQueueString(queue.name);
if (normalized && normalized !== queue.name) {
if (normalized) {
return normalized;
}
if (queue.slug) {
const fromSlug = renameQueueString(queue.slug);
if (fromSlug) return fromSlug;
}
return normalized ?? queue.name;
return queue.name;
}
function normalizeTeams(teams?: string[] | null): string[] {
@ -1175,7 +1176,7 @@ export const playNext = mutation({
agentId: v.id("users"),
},
handler: async (ctx, { tenantId, queueId, agentId }) => {
const agent = await requireStaff(ctx, agentId, tenantId)
const { user: agent } = await requireStaff(ctx, agentId, tenantId)
// Find eligible tickets: not resolved/closed and not assigned
let candidates: Doc<"tickets">[] = []
if (queueId) {
@ -1211,7 +1212,7 @@ export const playNext = mutation({
await ctx.db.insert("ticketEvents", {
ticketId: chosen._id,
type: "ASSIGNEE_CHANGED",
payload: { assigneeId: agentId, assigneeName: agent?.name },
payload: { assigneeId: agentId, assigneeName: agent.name },
createdAt: now,
});

25
convex/tsconfig.json Normal file
View file

@ -0,0 +1,25 @@
{
/* This TypeScript project config describes the environment that
* Convex functions run in and is used to typecheck them.
* You can modify it, but some settings are required to use Convex.
*/
"compilerOptions": {
/* These settings are not required by Convex and can be modified. */
"allowJs": true,
"strict": true,
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
/* These compiler options are required by Convex */
"target": "ESNext",
"lib": ["ES2021", "dom"],
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"isolatedModules": true,
"noEmit": true
},
"include": ["./**/*"],
"exclude": ["./_generated"]
}

View file

@ -1,683 +0,0 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--animate-scroll: scroll var(--animation-duration, 40s) var(--animation-direction, forwards) linear infinite;
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
@keyframes moveDot {
0%, 100% {
top: 10%;
right: 10%;
}
25% {
top: 10%;
right: calc(100% - 35px);
}
50% {
top: calc(100% - 30px);
right: calc(100% - 35px);
}
75% {
top: calc(100% - 30px);
right: 10%;
}
}
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--chart-green: 142.1 76.2% 36.3%;
--chart-orange: 28.3 92.5% 58.4%;
--chart-red: 0 84.2% 60.2%;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--chart-green: 142.1 76.2% 36.3%;
--chart-orange: 28.3 92.5% 58.4%;
--chart-red: 0 84.2% 60.2%;
}
@keyframes scroll {
to {
transform: translate(calc(-50% - 0.5rem));
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
@layer utilities {
.animate-ripple {
animation: ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite;
}
}
@keyframes ripple {
0%, 100% {
transform: translate(-50%, -50%) scale(1);
}
50% {
transform: translate(-50%, -50%) scale(0.9);
}
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: translateY(30px) scale(0.8);
}
50% {
opacity: 1;
transform: translateY(-3px) scale(1.02);
}
70% {
transform: translateY(1px) scale(0.99);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Previne flash branco em navegação durante scroll */
.rever-nav [data-slot="navigation-menu-viewport"] {
transition: opacity 150ms ease-in-out;
}
/* Estabiliza hovers durante scroll suave */
.smooth-scrolling [data-slot="navigation-menu-trigger"] {
pointer-events: none;
}
/* Otimizações de performance para scroll suave - excluindo marquee */
[style*="transform"]:not(.transform-3d):not(.transform-3d *) {
will-change: transform;
transform-style: preserve-3d;
}
/* Otimização específica para elementos animados - excluindo marquee */
.motion-div:not(.transform-3d *),
[data-framer-name]:not(.transform-3d *),
[style*="translateX"]:not(.transform-3d *),
[style*="translateY"]:not(.transform-3d *),
[style*="scale"]:not(.transform-3d *) {
will-change: transform;
contain: layout style paint;
backface-visibility: hidden;
}
/* Container de animações 3D - sem otimizações que interferem no hover */
.transform-3d {
transform-style: preserve-3d;
}
/* Garante que shadow-2xl funcione corretamente no 3D marquee */
.transform-3d img.hover\:shadow-2xl:hover {
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 25%);
}
/* Otimização para imagens em movimento - excluindo 3D marquee */
img[style*="transform"]:not(.transform-3d img) {
will-change: transform;
image-rendering: optimizeSpeed;
}
/* Previne layout shift nos logos da Rever */
img[alt="Rever Logo"], [alt="Rever Logo"] {
transition: none; /* Remove transições que podem causar flash */
}
/* Remove scroll bar visível em todos os dispositivos móveis */
.no-visible-scrollbar::-webkit-scrollbar {
display: none;
}
/* Estilos específicos do blog - força texto branco */
.blog-content {
color: white !important;
}
.blog-content * {
color: white !important;
}
.blog-content p {
color: white !important;
}
.blog-content h1,
.blog-content h2,
.blog-content h3,
.blog-content h4,
.blog-content h5,
.blog-content h6 {
color: white !important;
}
.blog-content ul,
.blog-content ol,
.blog-content li {
color: white !important;
}
.blog-content blockquote {
color: white !important;
}
.blog-content div,
.blog-content span {
color: white !important;
}
/* Mantém cores específicas para links */
.blog-content a {
color: #00e8ff !important;
}
.blog-content a:hover {
color: #00d4e6 !important;
}
/* Mantém cores específicas para código */
.blog-content code {
color: #00e8ff !important;
}
/* Estilos para iframes em conteúdo de blog */
.blog-content iframe {
max-width: 100% !important;
margin: 2rem auto !important;
display: block !important;
border-radius: 12px !important;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8) !important;
}
/* Container responsivo para vídeos embarcados */
.video-embed-container {
position: relative;
width: 100%;
height: 0;
padding-bottom: 56.25%; /* Aspect ratio 16:9 */
margin: 2rem auto;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8);
}
.video-embed-container iframe {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
border: 0 !important;
margin: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
}
.no-visible-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Para CategoryFilter - esconde scrollbar em modo horizontal */
.scrollbar-hide {
-ms-overflow-style: none; /* IE e Edge */
scrollbar-width: none; /* Firefox */
}
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Chrome, Safari e Opera */
}
/* CSS para placeholder do TipTap Editor */
.ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: #adb5bd;
pointer-events: none;
height: 0;
font-size: 0.875rem; /* Equivalente a text-sm */
}
.ProseMirror .is-empty::before {
content: attr(data-placeholder);
float: left;
color: #9ca3af !important;
pointer-events: none;
height: 0;
font-style: italic;
font-weight: normal;
}
/* Alternativa mais específica */
.prose .ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
color: #9ca3af !important;
pointer-events: none;
height: 0;
font-style: italic;
font-weight: normal;
position: absolute;
}
/* Estilos de espaçamento do TipTap Editor */
.ProseMirror p {
margin-bottom: 1.5rem; /* Equivalente a mb-6 */
}
.ProseMirror h1 {
margin-top: 3rem; /* mt-12 */
margin-bottom: 1.5rem; /* mb-6 */
font-size: 2.25rem; /* text-4xl */
font-weight: 700;
line-height: 1.2;
}
.ProseMirror h2 {
margin-top: 2.5rem; /* mt-10 */
margin-bottom: 1.25rem; /* mb-5 */
font-size: 1.875rem; /* text-3xl */
font-weight: 700;
line-height: 1.25;
}
.ProseMirror h3 {
margin-top: 2rem; /* mt-8 */
margin-bottom: 1rem; /* mb-4 */
font-size: 1.5rem; /* text-2xl */
font-weight: 700;
line-height: 1.375;
}
.ProseMirror h4 {
margin-top: 1.5rem; /* mt-6 */
margin-bottom: 0.75rem; /* mb-3 */
font-size: 1.25rem; /* text-xl */
font-weight: 700;
}
.ProseMirror ul,
.ProseMirror ol {
margin-top: 1.5rem; /* my-6 */
margin-bottom: 1.5rem;
padding-left: 1.5rem;
}
.ProseMirror li {
margin-bottom: 0.75rem; /* space-y-3 */
}
.ProseMirror blockquote {
margin-top: 2rem; /* my-8 */
margin-bottom: 2rem;
padding-left: 1.5rem;
border-left: 4px solid #00e8ff;
font-style: italic;
color: #6b7280;
}
.ProseMirror img,
.ProseMirror [data-type="resizableImage"] {
margin-top: 2rem; /* my-8 */
margin-bottom: 2rem;
border-radius: 0.75rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.ProseMirror pre {
margin-top: 2rem; /* my-8 */
margin-bottom: 2rem;
padding: 1rem;
background-color: #111827;
border: 1px solid #374151;
border-radius: 0.5rem;
overflow-x: auto;
}
.ProseMirror code {
background-color: #1f2937;
color: #00e8ff;
padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-family: monospace;
}
.ProseMirror hr {
margin-top: 2rem;
margin-bottom: 2rem;
border-color: #e5e7eb;
}
/* Remover margens do primeiro e último elemento */
.ProseMirror > *:first-child {
margin-top: 0;
}
.ProseMirror > *:last-child {
margin-bottom: 0;
}
/* Espaçamento entre imagem e texto */
.ProseMirror p + [data-type="resizableImage"],
.ProseMirror [data-type="resizableImage"] + p,
.ProseMirror p + img,
.ProseMirror img + p {
margin-top: 2rem;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--chart-1: hsl(188 100% 50%); /* #00e8ff */
--chart-2: hsl(220 13% 69%); /* light gray */
--chart-3: hsl(220 14% 96%); /* lighter gray */
--chart-4: hsl(221 83% 53%); /* blue */
--chart-5: hsl(215.4 16.3% 46.9%); /* muted text */
--chart-green: 142.1 76.2% 36.3%;
--chart-orange: 28.3 92.5% 58.4%;
--chart-red: 0 84.2% 60.2%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: hsl(188 100% 50%); /* #00e8ff */
--chart-2: hsl(217.2 32.6% 17.5%);
--chart-3: hsl(217.2 32.6% 17.5%);
--chart-4: hsl(210 40% 98%);
--chart-5: 12.1 83.7% 60%;
--chart-green: 142.1 76.2% 36.3%;
--chart-orange: 28.3 92.5% 58.4%;
--chart-red: 0 84.2% 60.2%;
}
}
/* Estilos específicos para o TipTap Editor */
.ProseMirror ul {
list-style-type: disc;
list-style-position: outside;
margin-left: 1.5rem;
margin-top: 2rem;
margin-bottom: 2rem;
}
.ProseMirror ol {
list-style-type: decimal;
list-style-position: outside;
margin-left: 1.5rem;
margin-top: 2rem;
margin-bottom: 2rem;
}
.ProseMirror li {
margin-top: 0;
margin-bottom: -0.25rem;
padding-left: 0.5rem;
line-height: 1.25;
}
.ProseMirror p {
margin-bottom: 2rem;
line-height: 1.625;
}
.ProseMirror hr {
margin-top: 2rem;
margin-bottom: 2rem;
border: none;
border-top: 1px solid #ccc;
}
/* Garantir espaçamento adequado entre elementos */
.ProseMirror > * + * {
margin-top: 1rem;
}
.ProseMirror > ul + *,
.ProseMirror > ol + *,
.ProseMirror > * + ul,
.ProseMirror > * + ol {
margin-top: 2rem;
}
@layer base {
.shader-surface {
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--background: oklch(0 0 0);
--foreground: oklch(1 0 0);
--card: oklch(0.1 0 0 / 0.1);
--card-foreground: oklch(1 0 0);
--popover: oklch(0 0 0 / 0.8);
--popover-foreground: oklch(1 0 0);
--primary: oklch(1 0 0);
--primary-foreground: oklch(0 0 0);
--secondary: oklch(0.65 0.25 25);
--secondary-foreground: oklch(1 0 0);
--muted: oklch(1 0 0 / 0.5);
--muted-foreground: oklch(0.7 0 0);
--accent: oklch(0.65 0.25 25);
--accent-foreground: oklch(1 0 0);
--destructive: oklch(0.6 0.25 15);
--destructive-foreground: oklch(1 0 0);
--border: oklch(1 0 0 / 0.2);
--input: oklch(1 0 0 / 0.1);
--ring: oklch(1 0 0 / 0.3);
--chart-1: oklch(0.65 0.25 25);
--chart-2: oklch(0.8 0.15 85);
--chart-3: oklch(0.7 0.2 140);
--chart-4: oklch(0.7 0.2 240);
--chart-5: oklch(0.6 0.25 300);
--radius: 0.5rem;
--sidebar: oklch(0 0 0 / 0.9);
--sidebar-foreground: oklch(1 0 0);
--sidebar-primary: oklch(0.65 0.25 25);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.65 0.25 25);
--sidebar-accent-foreground: oklch(1 0 0);
--sidebar-border: oklch(1 0 0 / 0.2);
--sidebar-ring: oklch(1 0 0 / 0.3);
}
.shader-surface.dark {
--background: oklch(0 0 0);
--foreground: oklch(1 0 0);
--card: oklch(0.1 0 0 / 0.1);
--card-foreground: oklch(1 0 0);
--popover: oklch(0 0 0 / 0.8);
--popover-foreground: oklch(1 0 0);
--primary: oklch(1 0 0);
--primary-foreground: oklch(0 0 0);
--secondary: oklch(0.65 0.25 25);
--secondary-foreground: oklch(1 0 0);
--muted: oklch(1 0 0 / 0.5);
--muted-foreground: oklch(0.7 0 0);
--accent: oklch(0.65 0.25 25);
--accent-foreground: oklch(1 0 0);
--destructive: oklch(0.6 0.25 15);
--destructive-foreground: oklch(1 0 0);
--border: oklch(1 0 0 / 0.2);
--input: oklch(1 0 0 / 0.1);
--ring: oklch(1 0 0 / 0.3);
--chart-1: oklch(0.65 0.25 25);
--chart-2: oklch(0.8 0.15 85);
--chart-3: oklch(0.7 0.2 140);
--chart-4: oklch(0.7 0.2 240);
--chart-5: oklch(0.6 0.25 300);
--sidebar: oklch(0 0 0 / 0.9);
--sidebar-foreground: oklch(1 0 0);
--sidebar-primary: oklch(0.65 0.25 25);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.65 0.25 25);
--sidebar-accent-foreground: oklch(1 0 0);
--sidebar-border: oklch(1 0 0 / 0.2);
--sidebar-ring: oklch(1 0 0 / 0.3);
}
}

View file

@ -10,7 +10,8 @@
"prisma:generate": "prisma generate",
"convex:dev": "convex dev",
"test": "vitest",
"auth:seed": "node --env-file=.env.local scripts/seed-auth.mjs"
"auth:seed": "node scripts/seed-auth.mjs",
"queues:ensure": "node scripts/ensure-default-queues.mjs"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
@ -43,6 +44,7 @@
"better-auth": "^1.3.26",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^16.4.5",
"convex": "^1.27.3",
"date-fns": "^4.1.0",
"lucide-react": "^0.544.0",

View file

@ -104,6 +104,9 @@ importers:
date-fns:
specifier: ^4.1.0
version: 4.1.0
dotenv:
specifier: ^16.4.5
version: 16.6.1
lucide-react:
specifier: ^0.544.0
version: 0.544.0(react@19.2.0)

View file

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 391 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 128 B

After

Width:  |  Height:  |  Size: 128 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 385 B

After

Width:  |  Height:  |  Size: 385 B

Before After
Before After

View file

@ -1,3 +1,4 @@
import "dotenv/config"
import { ConvexHttpClient } from "convex/browser";
const url = process.env.NEXT_PUBLIC_CONVEX_URL;

View file

@ -0,0 +1,73 @@
import "dotenv/config"
import { ConvexHttpClient } from "convex/browser"
const tenantId = process.env.SYNC_TENANT_ID || "tenant-atlas"
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL
if (!convexUrl) {
console.error("NEXT_PUBLIC_CONVEX_URL não configurado. Ajuste o .env antes de executar o script.")
process.exit(1)
}
const DEFAULT_QUEUES = [
{ name: "Chamados" },
{ name: "Laboratório" },
{ name: "Visitas" },
]
function slugify(value) {
return value
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^\w\s-]/g, "")
.trim()
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.toLowerCase()
}
async function main() {
const client = new ConvexHttpClient(convexUrl)
const agents = await client.query("users:listAgents", { tenantId })
const admin =
agents.find((user) => (user.role ?? "").toUpperCase() === "ADMIN") ??
agents[0]
if (!admin?._id) {
console.error("Nenhum usuário ADMIN encontrado no Convex para criar filas padrão.")
process.exit(1)
}
const existing = await client.query("queues:list", {
tenantId,
viewerId: admin._id,
})
const existingSlugs = new Set(existing.map((queue) => queue.slug))
const created = []
for (const def of DEFAULT_QUEUES) {
const slug = slugify(def.name)
if (existingSlugs.has(slug)) {
continue
}
await client.mutation("queues:create", {
tenantId,
actorId: admin._id,
name: def.name,
})
created.push(def.name)
}
if (created.length === 0) {
console.log("Nenhuma fila criada. As filas padrão já existem.")
} else {
console.log(`Filas criadas: ${created.join(", ")}`)
}
}
main().catch((error) => {
console.error("Falha ao garantir filas padrão", error)
process.exit(1)
})

View file

@ -1,3 +1,4 @@
import "dotenv/config"
import { PrismaClient } from "@prisma/client"
import { ConvexHttpClient } from "convex/browser"

View file

@ -1,3 +1,4 @@
import "dotenv/config"
import { ConvexHttpClient } from "convex/browser"
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL

View file

@ -1,3 +1,4 @@
import "dotenv/config"
import pkg from "@prisma/client"
import { hashPassword } from "better-auth/crypto"
import { ConvexHttpClient } from "convex/browser"

View file

@ -1,3 +1,4 @@
import "dotenv/config"
import pkg from "@prisma/client"
import { hashPassword } from "better-auth/crypto"
@ -131,7 +132,7 @@ const defaultUsers = singleUserFromEnv ?? [
},
]
async function upsertAuthUser({ email, password, name, role, tenantId: userTenant }: (typeof defaultUsers)[number]) {
async function upsertAuthUser({ email, password, name, role, tenantId: userTenant }) {
const hashedPassword = await hashPassword(password)
const user = await prisma.authUser.upsert({

View file

@ -1,3 +1,4 @@
import "dotenv/config"
import { PrismaClient } from "@prisma/client"
import { ConvexHttpClient } from "convex/browser"

View file

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

View file

@ -17,6 +17,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Badge } from "@/components/ui/badge"
import { Skeleton } from "@/components/ui/skeleton"
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { useDefaultQueues } from "@/hooks/use-default-queues"
type Queue = {
id: string
@ -33,6 +34,7 @@ type TeamOption = {
export function QueuesManager() {
const { session, convexUserId } = useAuth()
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
useDefaultQueues(tenantId)
const NO_TEAM_VALUE = "__none__"

Some files were not shown because too many files have changed in this diff Show more