feat(gtos): página de gerenciamento de GTOs com filtros e paginação
continuous-integration/webhook Deploy concluído (VPS4)

Backend:
- routes/gtos.js: GET /api/gtos reescrito com filtros (all/vinculada/enviada),
  busca ILIKE por patient_name e gto_number, paginação (page/limit),
  retorna { rows, total, pages } + thumb_url da primeira imagem vinculada.

Frontend:
- GtosPage.jsx (/gtos): cards de stats clicáveis (Todas/Vinculadas/Enviadas)
  com contadores em tempo real; tabela com colunas: thumb, número, paciente,
  qtd imagens (badge colorido), descrição, data, status, ações; paginação
  numérica; busca com debounce 350ms.
- StatusBadge: Pendente / Vinculada / Enviada com cores distintas.
- GtoDetailModal: info completa, grid de imagens vinculadas com thumb +
  botão desvincular, toggle enviada/pendente com feedback visual.
- CreateGtoModal: formulário com número, paciente, descrição.
- Sidebar: item GTOs (fa-file-medical) entre Pacientes e Novo Paciente.
- App.jsx: rota /gtos (ProtectedRoute).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-05-31 16:06:42 +02:00
parent 98dcb729a5
commit f027842c76
4 changed files with 625 additions and 24 deletions
+65 -24
View File
@@ -3,34 +3,75 @@ const router = express.Router();
const db = require('../database');
// ================================================================
// GET /api/gtos?patient=nome — listar GTOs de um paciente
// GET /api/gtos — listar todas as GTOs
// GET /api/gtos — listar GTOs com filtros e paginação
// Query params:
// search — busca por patient_name ou gto_number (ILIKE)
// filter — 'all' | 'vinculada' | 'enviada'
// page — número da página (default 1)
// limit — itens por página (default 20)
// ================================================================
router.get('/', async (req, res) => {
try {
const { patient } = req.query;
let rows;
if (patient) {
rows = await db.all(
`SELECT g.*, COUNT(gi.id) as image_count
FROM gtos g
LEFT JOIN gto_images gi ON gi.gto_id = g.id
WHERE g.patient_name = $1
GROUP BY g.id
ORDER BY g.created_at DESC`,
[patient]
);
} else {
rows = await db.all(
`SELECT g.*, COUNT(gi.id) as image_count
FROM gtos g
LEFT JOIN gto_images gi ON gi.gto_id = g.id
GROUP BY g.id
ORDER BY g.created_at DESC
LIMIT 200`
);
const search = (req.query.search || '').trim();
const filter = req.query.filter || 'all';
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(100, parseInt(req.query.limit) || 20);
const offset = (page - 1) * limit;
let params = [];
let where = [];
let i = 1;
if (search) {
where.push(`(g.patient_name ILIKE $${i} OR g.gto_number ILIKE $${i})`);
params.push(`%${search}%`);
i++;
}
res.json(rows);
if (filter === 'vinculada') {
where.push(`EXISTS (SELECT 1 FROM gto_images gi2 WHERE gi2.gto_id = g.id)`);
} else if (filter === 'enviada') {
where.push(`g.sent = TRUE`);
}
const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
// Total para paginação
const countRow = await db.get(
`SELECT COUNT(*) as total FROM gtos g ${whereClause}`,
params
);
const total = parseInt(countRow.total);
const rows = await db.all(
`SELECT g.*,
COUNT(gi.id)::int AS image_count
FROM gtos g
LEFT JOIN gto_images gi ON gi.gto_id = g.id
${whereClause}
GROUP BY g.id
ORDER BY g.created_at DESC
LIMIT ${limit} OFFSET ${offset}`,
params
);
// Adiciona thumb_url da primeira imagem vinculada para preview
for (const row of rows) {
if (parseInt(row.image_count) > 0) {
const firstImg = await db.get(
`SELECT i.id FROM gto_images gi
JOIN images i ON i.id = gi.image_id
WHERE gi.gto_id = $1
ORDER BY gi.created_at ASC LIMIT 1`,
[row.id]
);
row.thumb_url = firstImg ? `/api/images/${firstImg.id}/thumbnail` : null;
} else {
row.thumb_url = null;
}
}
res.json({ rows, total, page, limit, pages: Math.ceil(total / limit) });
} catch (err) {
console.error('Erro ao listar GTOs:', err);
res.status(500).json({ error: err.message });