Files
rx.scoreodonto.com/dental-server/routes/gtos.js
T
VPS 4 Deploy Agent 35248fb27b
continuous-integration/webhook Deploy concluído (VPS4)
fix(gtos): compatibilidade TransformModal com novo formato da API
A refatoração do GET /api/gtos para retornar { rows, total, pages }
quebrou o TransformModal que esperava um array direto.

Correções:
- routes/gtos.js: restaura suporte a ?patient=NOME (match exato) para
  o TransformModal continuar filtrando só as GTOs do paciente exibido,
  sem retornar resultados de outros pacientes via ILIKE.
- TransformModal.jsx: lê data.rows (novo formato) com fallback para
  array direto (compatibilidade durante transição).

As duas superfícies (TransformModal e GtosPage) agora usam a mesma
API, mesmos dados, sem lógica duplicada ou conflitante.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 16:25:37 +02:00

225 lines
8.5 KiB
JavaScript

const express = require('express');
const router = express.Router();
const db = require('../database');
// ================================================================
// 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 search = (req.query.search || '').trim();
const patient = (req.query.patient || '').trim(); // match exato (usado pelo TransformModal)
const filter = req.query.filter || 'all';
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(200, parseInt(req.query.limit) || 20);
const offset = (page - 1) * limit;
let params = [];
let where = [];
let i = 1;
// Match exato por paciente (TransformModal: mostra só as GTOs deste paciente)
if (patient) {
where.push(`g.patient_name = $${i}`);
params.push(patient);
i++;
} else if (search) {
// Busca ILIKE por nome ou número (GtosPage)
where.push(`(g.patient_name ILIKE $${i} OR g.gto_number ILIKE $${i})`);
params.push(`%${search}%`);
i++;
}
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 });
}
});
// ================================================================
// GET /api/gtos/:id — detalhe de uma GTO com imagens vinculadas
// ================================================================
router.get('/:id', async (req, res) => {
try {
const gto = await db.get('SELECT * FROM gtos WHERE id = $1', [req.params.id]);
if (!gto) return res.status(404).json({ error: 'GTO não encontrada' });
const images = await db.all(
`SELECT i.*, gi.id as link_id
FROM gto_images gi
JOIN images i ON i.id = gi.image_id
WHERE gi.gto_id = $1
ORDER BY gi.created_at DESC`,
[req.params.id]
);
res.json({ ...gto, images });
} catch (err) {
console.error('Erro ao buscar GTO:', err);
res.status(500).json({ error: err.message });
}
});
// ================================================================
// POST /api/gtos — criar nova GTO
// Body: { gto_number, patient_name, description }
// ================================================================
router.post('/', async (req, res) => {
try {
const { gto_number, patient_name, description } = req.body;
if (!gto_number || !patient_name) {
return res.status(400).json({ error: 'gto_number e patient_name são obrigatórios' });
}
const result = await db.run(
`INSERT INTO gtos (gto_number, patient_name, description) VALUES ($1, $2, $3) RETURNING id`,
[gto_number.trim(), patient_name.trim(), description || '']
);
const gto = await db.get('SELECT * FROM gtos WHERE id = $1', [result.lastID]);
res.status(201).json(gto);
} catch (err) {
console.error('Erro ao criar GTO:', err);
res.status(500).json({ error: err.message });
}
});
// ================================================================
// PUT /api/gtos/:id — atualizar GTO
// ================================================================
router.put('/:id', async (req, res) => {
try {
const { gto_number, description } = req.body;
await db.run(
`UPDATE gtos SET gto_number = $1, description = $2 WHERE id = $3`,
[gto_number, description || '', req.params.id]
);
const gto = await db.get('SELECT * FROM gtos WHERE id = $1', [req.params.id]);
res.json(gto);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ================================================================
// DELETE /api/gtos/:id — remover GTO e vínculos
// ================================================================
router.delete('/:id', async (req, res) => {
try {
await db.run('DELETE FROM gto_images WHERE gto_id = $1', [req.params.id]);
await db.run('DELETE FROM gtos WHERE id = $1', [req.params.id]);
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ================================================================
// POST /api/gtos/:id/images — vincular imagem à GTO
// Body: { image_id }
// ================================================================
router.post('/:id/images', async (req, res) => {
try {
const { image_id } = req.body;
if (!image_id) return res.status(400).json({ error: 'image_id é obrigatório' });
// Verificar se já está vinculada
const existing = await db.get(
'SELECT id FROM gto_images WHERE gto_id = $1 AND image_id = $2',
[req.params.id, image_id]
);
if (existing) return res.status(409).json({ error: 'Imagem já vinculada a esta GTO' });
await db.run(
'INSERT INTO gto_images (gto_id, image_id) VALUES ($1, $2) RETURNING id',
[req.params.id, image_id]
);
res.status(201).json({ success: true, message: 'Imagem vinculada à GTO' });
} catch (err) {
console.error('Erro ao vincular imagem:', err);
res.status(500).json({ error: err.message });
}
});
// ================================================================
// DELETE /api/gtos/:id/images/:imageId — desvincular imagem
// ================================================================
router.delete('/:id/images/:imageId', async (req, res) => {
try {
await db.run(
'DELETE FROM gto_images WHERE gto_id = $1 AND image_id = $2',
[req.params.id, req.params.imageId]
);
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ================================================================
// POST /api/gtos/:id/sent — marcar/desmarcar GTO como enviada
// Body: { sent: true|false }
// ================================================================
router.post('/:id/sent', async (req, res) => {
try {
const sent = req.body.sent !== false; // default true
await db.run(
'UPDATE gtos SET sent = $1, sent_at = CASE WHEN $1 THEN NOW() ELSE NULL END WHERE id = $2',
[sent, req.params.id]
);
const gto = await db.get('SELECT * FROM gtos WHERE id = $1', [req.params.id]);
res.json(gto);
} catch (err) {
console.error('Erro ao marcar GTO como enviada:', err);
res.status(500).json({ error: err.message });
}
});
module.exports = router;