Skill do projeto SGDTI — Sistema de Gestão do DTI da Prefeitura Municipal de Costa Marques/RO. Use esta skill SEMPRE que o usuário mencionar: SGDTI, DTI, ordem de serviço, OS, inventário de TI, almoxarifado de peças, secretarias, chamados, equipamentos patrimoniais, etiqueta QR Code, Supabase, dashboard do DTI, relatórios de atendimento, ou qualquer tarefa relacionada a este sistema. Esta skill contém o contexto completo do projeto, decisões técnicas, stack, estrutura de banco de dados e convenções de código — leia-a antes de qualquer tarefa de desenvolvimento neste projeto.
Resources
11Install
npx skillscat add dtiprefcm/sgdti Install via the SkillsCat registry.
SGDTI — Skill de Desenvolvimento
Contexto do Projeto
Sistema web para o Departamento de Tecnologia da Informação (DTI) da Prefeitura Municipal de Costa Marques/RO.
Leia sempre o STATUS.md antes de qualquer tarefa:
/STATUS.md ← estado atual, sprints, tarefas concluídas/pendentesRepositório: GitHub com GitFlow · Deploy: Vercel · Banco: Supabase (PostgreSQL)
🎨 Design — Skill Obrigatória para Componentes Visuais
Instalada em:
2026-05-24vianpx skills add https://github.com/anthropics/skills --skill frontend-design
Para qualquer tarefa que envolva criação ou edição de componentes visuais, use a skill frontend-design em conjunto com esta skill.
Isso inclui obrigatoriamente:
- Dashboard principal (cards, gráficos, feed de atividade)
- Formulários de OS (portal público e painel interno)
- Tabelas de inventário e almoxarifado
- Sidebar e header do layout base
- Páginas de listagem com filtros
- Etiquetas patrimoniais com QR Code
- Relatórios e impressão
- Qualquer componente novo em
components/
Por que: a frontend-design skill define os design tokens, paleta de cores, tipografia, espaçamentos e padrões visuais que garantem uma interface de alto nível — adequada para um sistema público municipal. Sem ela, o resultado tende a ter aparência genérica.
Como combinar as duas skills na prática:
1. Leia esta skill (SKILL.md) → stack, banco, convenções de código
2. Leia frontend-design SKILL.md → tokens de design, componentes visuais, padrões de UI
3. Leia STATUS.md → o que está em andamento
4. Implemente com as duas referências ativasStack Obrigatória
| Camada | Tecnologia | Observação |
|---|---|---|
| Framework | Next.js 14 (App Router) | NUNCA Pages Router |
| Linguagem | TypeScript estrito | strict: true no tsconfig |
| UI | Tailwind CSS + shadcn/ui | Não usar CSS externo, não usar styled-components |
| Banco | Supabase PostgreSQL | Usar @supabase/ssr (não @supabase/auth-helpers) |
| Auth | Supabase Auth | Não usar NextAuth neste projeto |
| Storage | Supabase Storage | Buckets: os-anexos, equipamento-fotos, documentos |
| Forms | React Hook Form + Zod | Validação sempre tipada |
| Gráficos | Recharts | Para o dashboard |
| QR Code | qrcode.react | Para etiquetas patrimoniais |
| Relatórios | jsPDF ou React-PDF | Geração client-side |
Estrutura de Pastas
sgdti/
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ └── layout.tsx
│ ├── (dashboard)/ ← rotas protegidas do DTI
│ │ ├── layout.tsx ← sidebar + header
│ │ ├── page.tsx ← dashboard principal
│ │ ├── os/ ← ordens de serviço
│ │ │ ├── page.tsx ← listagem
│ │ │ ├── [id]/page.tsx ← detalhe/atendimento
│ │ │ └── nova/page.tsx ← abertura interna
│ │ ├── inventario/
│ │ │ ├── page.tsx
│ │ │ └── [id]/page.tsx
│ │ ├── almoxarifado/
│ │ ├── secretarias/
│ │ └── relatorios/
│ ├── os/
│ │ └── [token]/page.tsx ← portal PÚBLICO (sem auth)
│ └── api/ ← route handlers Next.js
├── components/
│ ├── ui/ ← gerados pelo shadcn/ui (não editar)
│ ├── os/ ← componentes de OS
│ ├── inventario/
│ ├── almoxarifado/
│ ├── dashboard/
│ └── shared/ ← componentes reutilizáveis
├── lib/
│ ├── supabase/
│ │ ├── client.ts ← client-side
│ │ ├── server.ts ← server-side (SSR)
│ │ └── middleware.ts
│ ├── validations/ ← schemas Zod por módulo
│ └── utils.ts
├── types/
│ └── database.ts ← tipos gerados do Supabase
├── supabase/
│ └── migrations/ ← arquivos SQL de migração
└── STATUS.mdBanco de Dados — Tabelas e Campos Principais
secretarias
id uuid PK | nome text | sigla text | responsavel text
contato text | email text | endereco text
token_os uuid UNIQUE -- token para o link público de OS
created_at timestamptz | updated_at timestamptzusuarios
id uuid PK (= auth.users.id) | nome text | email text
perfil text CHECK ('administrador','tecnico','visualizador')
ativo boolean | created_at timestamptzordens_servico
id uuid PK | numero_protocolo text UNIQUE
secretaria_id uuid FK | solicitante_nome text | solicitante_contato text
categoria text CHECK ('rede','computador','impressora','certificado','internet','outro')
descricao text | status text CHECK ('aberta','em_andamento','aguardando_peca','aguardando_secretaria','encerrada')
prioridade text CHECK ('baixa','media','alta','critica')
tecnico_id uuid FK | data_abertura timestamptz | data_prevista timestamptz
data_encerramento timestamptz | patrimonio text | numero_serie text
diagnostico text | solucao text | vai_laboratorio boolean
memorando_url text | created_at timestamptz | updated_at timestamptzos_logs
id uuid PK | os_id uuid FK | usuario_id uuid FK
acao text | descricao text | created_at timestamptzos_anexos
id uuid PK | os_id uuid FK | tipo text ('foto_problema','foto_resolucao','memorando','outro')
url text | nome_arquivo text | tamanho_bytes int | created_at timestamptzequipamentos
id uuid PK | patrimonio text UNIQUE | numero_serie text
tipo text ('computador','notebook','impressora','switch_gerenciavel','switch_simples',
'roteador','servidor','onu','nobreak','outro')
marca text | modelo text | especificacoes jsonb
secretaria_id uuid FK | usuario_responsavel text
data_aquisicao date | nota_fiscal text | garantia_inicio date | garantia_fim date
estado text CHECK ('otimo','bom','regular','manutencao','inativo','baixado')
observacoes text | created_at timestamptz | updated_at timestamptzpecas
id uuid PK | nome text | categoria text | marca text | modelo text
quantidade_atual int | quantidade_minima int
localizacao text | observacoes text
created_at timestamptz | updated_at timestamptzemprestimos
id uuid PK | equipamento_id uuid FK | secretaria_destino_id uuid FK
responsavel text | data_saida date | data_prevista_devolucao date
data_devolucao date | observacoes text | status text ('ativo','devolvido','atrasado')
created_at timestamptzConfiguração Supabase (2026)
⚠️ Atenção: O Supabase mudou o sistema de API keys em 2026. Usar as novas chaves:
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://[projeto].supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxx # antes: ANON_KEY
SUPABASE_SECRET_KEY=sb_secret_xxx # antes: SERVICE_ROLE_KEYClient-side (lib/supabase/client.ts):
import { createBrowserClient } from '@supabase/ssr'
import type { Database } from '@/types/database'
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
)
}Server-side (lib/supabase/server.ts):
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import type { Database } from '@/types/database'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{ cookies: { getAll: () => cookieStore.getAll(),
setAll: (c) => c.forEach(({ name, value, options }) => cookieStore.set(name, value, options)) } }
)
}Convenções de Código
Nomenclatura
- Componentes:
PascalCase→OsCard.tsx,InventarioTabela.tsx - Funções/variáveis:
camelCase→buscarOrdens,totalAberto - Tabelas banco:
snake_case→ordens_servico,os_logs - Rotas/arquivos:
kebab-case→nova-os/,detalhes-equipamento/
Componente Server vs Client
// Server Component (padrão no App Router — sem 'use client')
// Use para: buscar dados do Supabase, renderizar listas, pages
// Client Component (adicionar 'use client' no topo)
// Use para: formulários, interatividade, hooks (useState, useEffect)
// Manter ao mínimo — só o que precisa de interatividadePadrão de busca de dados
// ✅ CORRETO — Server Component buscando dados
// app/(dashboard)/os/page.tsx
import { createClient } from '@/lib/supabase/server'
export default async function OsPage() {
const supabase = await createClient()
const { data: ordens, error } = await supabase
.from('ordens_servico')
.select('*, secretarias(nome), usuarios(nome)')
.order('created_at', { ascending: false })
if (error) throw error
return <OsListagem ordens={ordens} />
}Validação com Zod
// lib/validations/os.ts
import { z } from 'zod'
export const novaOsSchema = z.object({
secretaria_id: z.string().uuid('Secretaria inválida'),
solicitante_nome: z.string().min(3, 'Nome obrigatório'),
solicitante_contato: z.string().optional(),
categoria: z.enum(['rede','computador','impressora','certificado','internet','outro']),
descricao: z.string().min(10, 'Descreva o problema com pelo menos 10 caracteres'),
patrimonio: z.string().optional(),
numero_serie: z.string().optional(),
vai_laboratorio: z.boolean().default(false),
})
export type NovaOsInput = z.infer<typeof novaOsSchema>Testes — Vitest + Testing Library
Configuração vitest.config.ts:
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./tests/setup.ts'],
},
})tests/setup.ts:
import '@testing-library/jest-dom'Scripts no package.json:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "next lint",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}Onde ficam os testes
sgdti/
├── tests/
│ ├── setup.ts ← configuração global
│ ├── unit/ ← testes de funções isoladas
│ │ ├── validations/
│ │ │ ├── os.test.ts ← testa schemas Zod de OS
│ │ │ └── inventario.test.ts
│ │ ├── utils/
│ │ │ ├── sla.test.ts ← testa cálculo de prazos/alertas
│ │ │ └── protocolo.test.ts ← testa geração de número de protocolo
│ └── integration/ ← testes de componentes completos
│ ├── OsForm.test.tsx ← testa formulário de OS
│ ├── InventarioCard.test.tsx
│ └── Dashboard.test.tsxTestes obrigatórios por módulo
M1 — Ordens de Serviço (tests/unit/validations/os.test.ts)
import { describe, it, expect } from 'vitest'
import { novaOsSchema } from '@/lib/validations/os'
describe('Validação de Nova OS', () => {
it('deve rejeitar OS sem descrição', () => {
const resultado = novaOsSchema.safeParse({
secretaria_id: 'uuid-valido',
solicitante_nome: 'João',
categoria: 'computador',
descricao: '', // ← inválido
})
expect(resultado.success).toBe(false)
})
it('deve rejeitar descrição com menos de 10 caracteres', () => {
const resultado = novaOsSchema.safeParse({
secretaria_id: 'uuid-valido',
solicitante_nome: 'João',
categoria: 'computador',
descricao: 'curta', // ← menos de 10 chars
})
expect(resultado.success).toBe(false)
})
it('deve aceitar OS com todos os campos válidos', () => {
const resultado = novaOsSchema.safeParse({
secretaria_id: '550e8400-e29b-41d4-a716-446655440000',
solicitante_nome: 'João Silva',
categoria: 'computador',
descricao: 'Computador não liga após queda de energia',
vai_laboratorio: true,
})
expect(resultado.success).toBe(true)
})
it('deve rejeitar categoria inválida', () => {
const resultado = novaOsSchema.safeParse({
secretaria_id: '550e8400-e29b-41d4-a716-446655440000',
solicitante_nome: 'João Silva',
categoria: 'categoria_inexistente', // ← inválido
descricao: 'Descrição válida aqui',
})
expect(resultado.success).toBe(false)
})
})M1 — Cálculo de SLA (tests/unit/utils/sla.test.ts)
import { describe, it, expect } from 'vitest'
import { calcularStatusSla, calcularDataPrevista } from '@/lib/utils/sla'
describe('Cálculo de SLA', () => {
it('deve retornar status verde quando prazo tem mais de 50% do tempo', () => {
const abertura = new Date('2026-01-01T08:00:00')
const prevista = new Date('2026-01-03T08:00:00') // 48h → computador
const agora = new Date('2026-01-01T10:00:00') // 2h passadas
expect(calcularStatusSla(abertura, prevista, agora)).toBe('verde')
})
it('deve retornar status amarelo quando faltam menos de 12h (computador)', () => {
const abertura = new Date('2026-01-01T08:00:00')
const prevista = new Date('2026-01-03T08:00:00')
const agora = new Date('2026-01-02T22:00:00') // faltam 10h
expect(calcularStatusSla(abertura, prevista, agora)).toBe('amarelo')
})
it('deve retornar status vermelho quando prazo já venceu', () => {
const abertura = new Date('2026-01-01T08:00:00')
const prevista = new Date('2026-01-03T08:00:00')
const agora = new Date('2026-01-04T08:00:00') // 24h atrasado
expect(calcularStatusSla(abertura, prevista, agora)).toBe('vermelho')
})
it('deve calcular data prevista corretamente por categoria', () => {
const abertura = new Date('2026-01-01T08:00:00')
const prevista = calcularDataPrevista(abertura, 'rede')
// SLA rede = 4h
expect(prevista).toEqual(new Date('2026-01-01T12:00:00'))
})
})M2 — Inventário (tests/unit/validations/inventario.test.ts)
import { describe, it, expect } from 'vitest'
import { equipamentoSchema } from '@/lib/validations/inventario'
describe('Validação de Equipamento', () => {
it('deve rejeitar equipamento sem número de patrimônio', () => {
const resultado = equipamentoSchema.safeParse({ tipo: 'computador', marca: 'Dell' })
expect(resultado.success).toBe(false)
})
it('deve aceitar todos os tipos válidos de equipamento', () => {
const tipos = ['computador','notebook','impressora','switch_gerenciavel',
'switch_simples','roteador','servidor','onu','nobreak','outro']
tipos.forEach(tipo => {
const resultado = equipamentoSchema.safeParse({
patrimonio: '001234', tipo, marca: 'Genérico', modelo: 'X1'
})
expect(resultado.success).toBe(true)
})
})
})M3 — Almoxarifado (tests/unit/utils/estoque.test.ts)
import { describe, it, expect } from 'vitest'
import { verificarEstoqueMinimo, calcularSaldoAposMovimentacao } from '@/lib/utils/estoque'
describe('Controle de Estoque', () => {
it('deve alertar quando quantidade cai abaixo do mínimo', () => {
expect(verificarEstoqueMinimo(2, 5)).toBe(true) // 2 < 5 → alerta
expect(verificarEstoqueMinimo(5, 5)).toBe(false) // igual → sem alerta
expect(verificarEstoqueMinimo(8, 5)).toBe(false) // acima → ok
})
it('não deve permitir saída que deixa estoque negativo', () => {
expect(() => calcularSaldoAposMovimentacao(3, 'saida', 5)).toThrow()
})
it('deve calcular saldo corretamente para entrada', () => {
expect(calcularSaldoAposMovimentacao(10, 'entrada', 5)).toBe(15)
})
})Padrão de log de erros no código
Quando algo der errado em produção, o log deve ser claro e rastreável:
// ✅ CORRETO — log estruturado com contexto
try {
const { data, error } = await supabase.from('ordens_servico').insert(novaOs)
if (error) {
console.error('[OS:criar] Erro ao inserir OS no banco', {
erro: error.message,
codigo: error.code,
secretaria_id: novaOs.secretaria_id,
categoria: novaOs.categoria,
})
throw new Error('Não foi possível criar a ordem de serviço')
}
} catch (err) {
console.error('[OS:criar] Erro inesperado', err)
throw err
}
// ❌ ERRADO — log genérico sem contexto
catch (err) {
console.log('deu erro') // inútil para debug
}Prefixos de log por módulo:
[OS:criar] [OS:atualizar] [OS:encerrar]
[INV:criar] [INV:mover]
[ALM:entrada] [ALM:saida] [ALM:emprestimo]
[AUTH:login] [AUTH:perfil]
[UPLOAD:foto] [UPLOAD:pdf]Checklist de qualidade — por tarefa
Antes de marcar [x] no STATUS.md, confirme:
[ ] npm run lint → zero warnings, zero erros
[ ] npm run build → compilou sem erros TypeScript
[ ] npm run test → todos os testes passando
[ ] Log de erros estruturado implementado
[ ] Validação Zod presente em todos os formulários
[ ] Nenhuma chave secreta no código (só em .env.local)
[ ] Componentes Server/Client usados corretamente
[ ] RLS do Supabase testado para o perfil correto
[ ] frontend-design skill consultada (se tarefa visual) ← NOVO