diff --git a/dental-server/dental-client/src/App.jsx b/dental-server/dental-client/src/App.jsx index 48b6c06..c3db12d 100644 --- a/dental-server/dental-client/src/App.jsx +++ b/dental-server/dental-client/src/App.jsx @@ -13,6 +13,7 @@ import ResetPage from './pages/ResetPage'; import DownloadPage from './pages/DownloadPage'; import PatientsPage from './pages/PatientsPage'; import SettingsPage from './pages/SettingsPage'; +import GtosPage from './pages/GtosPage'; export default function App() { return ( @@ -69,6 +70,12 @@ export default function App() { } /> + + + + } /> + {/* Catch-all: redireciona para home */} } /> diff --git a/dental-server/dental-client/src/components/Sidebar.jsx b/dental-server/dental-client/src/components/Sidebar.jsx index 5a9b834..e36cb84 100644 --- a/dental-server/dental-client/src/components/Sidebar.jsx +++ b/dental-server/dental-client/src/components/Sidebar.jsx @@ -45,6 +45,13 @@ export default function Sidebar({ version, onNewPatient, onUpload, onSettings, o Pacientes + `nav-item${isActive ? ' active' : ''}`} + > + GTOs + + diff --git a/dental-server/dental-client/src/pages/GtosPage.jsx b/dental-server/dental-client/src/pages/GtosPage.jsx new file mode 100644 index 0000000..4606898 --- /dev/null +++ b/dental-server/dental-client/src/pages/GtosPage.jsx @@ -0,0 +1,546 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import api from '../api/client'; +import { useToast } from '../contexts/ToastContext'; +import { useAuth } from '../contexts/AuthContext'; +import Sidebar from '../components/Sidebar'; +import ThumbImg from '../components/ThumbImg'; +import Modal from '../components/Modal'; +import CreatePatientModal from '../components/CreatePatientModal'; +import SettingsModal from '../components/SettingsModal'; +import UserManagementModal from '../components/UserManagementModal'; +import PluginsModal from '../components/PluginsModal'; +import SyncModal from '../components/SyncModal'; + +/* ── helpers ─────────────────────────────────────────────────── */ +function fmtDate(d) { + if (!d) return '—'; + return new Date(d).toLocaleString('pt-BR', { + day: '2-digit', month: '2-digit', year: 'numeric', + hour: '2-digit', minute: '2-digit', + }); +} + +function StatusBadge({ sent, imageCount }) { + if (sent) return ( + + ✈️ Enviada + + ); + if (imageCount > 0) return ( + + 🔗 Vinculada + + ); + return ( + + 📋 Pendente + + ); +} + +/* ── Modal de detalhes/edição de GTO ─────────────────────────── */ +function GtoDetailModal({ isOpen, onClose, gto, onUpdated }) { + const { showToast } = useToast(); + const [detail, setDetail] = useState(null); + const [loading, setLoading] = useState(false); + const [marking, setMarking] = useState(false); + + useEffect(() => { + if (isOpen && gto?.id) loadDetail(); + }, [isOpen, gto?.id]); + + async function loadDetail() { + setLoading(true); + try { + const { data } = await api.get(`/gtos/${gto.id}`); + setDetail(data); + } catch { showToast('Erro ao carregar detalhe da GTO', 'error'); } + finally { setLoading(false); } + } + + async function toggleSent() { + setMarking(true); + try { + await api.post(`/gtos/${gto.id}/sent`, { sent: !detail.sent }); + showToast(detail.sent ? 'GTO marcada como pendente.' : 'GTO marcada como enviada!', 'success'); + onUpdated?.(); + loadDetail(); + } catch { showToast('Erro ao alterar status', 'error'); } + finally { setMarking(false); } + } + + async function unlinkImage(imageId) { + try { + await api.delete(`/gtos/${gto.id}/images/${imageId}`); + showToast('Imagem desvinculada.', 'success'); + onUpdated?.(); + loadDetail(); + } catch { showToast('Erro ao desvincular imagem', 'error'); } + } + + if (!gto) return null; + + return ( + + {loading ? ( +
+ ) : detail ? ( +
+ + {/* Info */} +
+ {[ + { label:'Número GTO', value: detail.gto_number }, + { label:'Paciente', value: detail.patient_name }, + { label:'Criado em', value: fmtDate(detail.created_at) }, + { label:'Enviado em', value: detail.sent_at ? fmtDate(detail.sent_at) : '—' }, + ].map(({ label, value }) => ( +
+
{label}
+
{value}
+
+ ))} +
+ + {detail.description && ( +
+
Descrição
+
{detail.description}
+
+ )} + + {/* Status + ação */} +
+
+ + {detail.sent && Enviada em {fmtDate(detail.sent_at)}} +
+ +
+ + {/* Imagens vinculadas */} +
+
+ Imagens vinculadas ({detail.images?.length || 0}) +
+ {(!detail.images || detail.images.length === 0) ? ( +
+ Nenhuma imagem vinculada a esta GTO. +
+ ) : ( +
+ {detail.images.map(img => ( +
+ +
+ {img.patient_name || img.filename} +
+ +
+ ))} +
+ )} +
+ +
+ ) : null} + + ); +} + +/* ── Modal de criação de GTO ─────────────────────────────────── */ +function CreateGtoModal({ isOpen, onClose, onCreated }) { + const { showToast } = useToast(); + const [form, setForm] = useState({ gto_number:'', patient_name:'', description:'' }); + const [saving, setSaving] = useState(false); + const set = k => e => setForm(p => ({ ...p, [k]: e.target.value })); + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!form.gto_number.trim() || !form.patient_name.trim()) { + return showToast('Número e paciente são obrigatórios.', 'error'); + } + setSaving(true); + try { + await api.post('/gtos', form); + showToast('GTO criada com sucesso!', 'success'); + setForm({ gto_number:'', patient_name:'', description:'' }); + onCreated?.(); + onClose(); + } catch (err) { + showToast(err.response?.data?.error || 'Erro ao criar GTO.', 'error'); + } finally { setSaving(false); } + }; + + return ( + +
+
+ + +
+
+ + +
+
+ +