dtiprefcm

sgdti

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.

dtiprefcm 0 Updated 1w ago

Resources

11
GitHub

Install

npx skillscat add dtiprefcm/sgdti

Install via the SkillsCat registry.

SKILL.md

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/pendentes

Repositório: GitHub com GitFlow · Deploy: Vercel · Banco: Supabase (PostgreSQL)


🎨 Design — Skill Obrigatória para Componentes Visuais

Instalada em: 2026-05-24 via npx 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 ativas

Stack 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.md

Banco 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 timestamptz

usuarios

id uuid PK (= auth.users.id) | nome text | email text
perfil text CHECK ('administrador','tecnico','visualizador')
ativo boolean | created_at timestamptz

ordens_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 timestamptz

os_logs

id uuid PK | os_id uuid FK | usuario_id uuid FK
acao text | descricao text | created_at timestamptz

os_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 timestamptz

equipamentos

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 timestamptz

pecas

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 timestamptz

emprestimos

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 timestamptz

Configuraçã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_KEY

Client-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: PascalCaseOsCard.tsx, InventarioTabela.tsx
  • Funções/variáveis: camelCasebuscarOrdens, totalAberto
  • Tabelas banco: snake_caseordens_servico, os_logs
  • Rotas/arquivos: kebab-casenova-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 interatividade

Padrã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.tsx

Testes 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