fix: corrige inconsistências críticas de backend e navegação
- Backend: substitui string literals com aspas duplas por aspas simples nas queries PostgreSQL (settings, GTO builder, google_tokens delete) - Backend: corrige alias SQL 'dentistanome' para 'dentistaNome' (quoted) e ajusta frontend para usar 'pacientenome' (lowercase do PG) - Backend: move app.listen() para após todas as rotas registradas - Backend: ativa generateIntelligentNotifications no startup + intervalo 5min - Backend: magic link usa process.env.APP_URL em vez de localhost:3000 hardcoded - Frontend: adiciona 'relatorios' às permissões do role 'funcionario' - Frontend: adiciona onClick ao botão Relatórios do Dashboard - Frontend: adiciona Content-Type e JWT token em salvarAgendamento/atualizarAgendamento - Frontend: MeusTratamentos passa JWT token nas chamadas a /api/guias - Frontend: ClinicasView passa JWT token no endpoint de atualização de cor - Frontend: corrige subtítulo 'MYSQL + GOOGLE' para 'POSTGRESQL + GOOGLE' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+21
-12
@@ -262,8 +262,6 @@ const createOAuth2Client = () => new google.auth.OAuth2(
|
||||
process.env.GOOGLE_REDIRECT_URI || `http://localhost:${PORT}/api/oauth2callback`
|
||||
);
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => console.log(`[SERVER] Running on http://0.0.0.0:${PORT}`));
|
||||
|
||||
// --- GOOGLE AUTH ROUTES ---
|
||||
app.get('/api/auth/google/url', (req, res) => {
|
||||
const { dentistId } = req.query;
|
||||
@@ -321,7 +319,7 @@ app.get('/api/auth/google/status', async (req, res) => {
|
||||
|
||||
app.delete('/api/auth/google/:ownerId', async (req, res) => {
|
||||
try {
|
||||
await pool.query('DELETE FROM google_tokens WHERE owner_id = ?', [req.params.ownerId]);
|
||||
await pool.query('DELETE FROM google_tokens WHERE owner_id = $1', [req.params.ownerId]);
|
||||
res.json({ success: true });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
@@ -329,7 +327,7 @@ app.delete('/api/auth/google/:ownerId', async (req, res) => {
|
||||
// --- SETTINGS ---
|
||||
app.get('/api/settings', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM settings WHERE id = "main"');
|
||||
const { rows } = await pool.query("SELECT * FROM settings WHERE id = 'main'");
|
||||
res.json(rows[0] ? JSON.parse(rows[0].data) : {});
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
@@ -337,7 +335,11 @@ app.get('/api/settings', async (req, res) => {
|
||||
app.post('/api/settings', async (req, res) => {
|
||||
try {
|
||||
const data = JSON.stringify(req.body);
|
||||
await pool.query('INSERT INTO settings (id, category, data) VALUES ("main", "general", ?) ON DUPLICATE KEY UPDATE data = ?', [data, data]);
|
||||
await pool.query(
|
||||
`INSERT INTO settings (id, category, data) VALUES ('main', 'general', $1)
|
||||
ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data`,
|
||||
[data]
|
||||
);
|
||||
res.json({ success: true });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
@@ -347,8 +349,8 @@ app.post('/api/dentistas/magic-link', async (req, res) => {
|
||||
const { clinicaId, dentistName } = req.body;
|
||||
try {
|
||||
const token = Math.random().toString(36).substring(2, 15);
|
||||
// Em um sistema real, salvaríamos esse token em uma tabela de convites
|
||||
const magicLink = `http://localhost:3000/#/cadastro-dentista?token=${token}&clinica=${clinicaId}&nome=${encodeURIComponent(dentistName)}`;
|
||||
const appUrl = process.env.APP_URL || `http://localhost:${process.env.FRONTEND_PORT || 3000}`;
|
||||
const magicLink = `${appUrl}/#/cadastro-dentista?token=${token}&clinica=${clinicaId}&nome=${encodeURIComponent(dentistName)}`;
|
||||
res.json({ success: true, link: magicLink });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
@@ -437,7 +439,7 @@ app.get('/api/google/events', async (req, res) => {
|
||||
}
|
||||
|
||||
// 2. Legacy Support: Check Settings for Public Calendars (API KEY)
|
||||
const [rows] = await pool.query('SELECT data FROM settings WHERE id = "main"');
|
||||
const [rows] = await pool.query("SELECT data FROM settings WHERE id = 'main'");
|
||||
if (rows[0]) {
|
||||
const settings = JSON.parse(rows[0].data);
|
||||
const apiKey = settings.googleApiKey;
|
||||
@@ -513,7 +515,8 @@ app.get('/api/dashboard/stats', async (req, res) => {
|
||||
|
||||
// Recent appointments with patient and dentist names
|
||||
const { rows: recent } = await pool.query(`
|
||||
SELECT a.*, d.nome as dentistanome
|
||||
SELECT a.id, a.pacientenome, a.start_time, a.status,
|
||||
d.nome as "dentistaNome"
|
||||
FROM agendamentos a
|
||||
LEFT JOIN dentistas d ON a.dentistaid = d.id
|
||||
ORDER BY a.start_time DESC
|
||||
@@ -1159,7 +1162,7 @@ app.post('/api/gto-builder', async (req, res) => {
|
||||
const { pacienteId, dentistaId, credenciadaId } = req.body;
|
||||
const id = Math.random().toString(36).substring(2, 9).toUpperCase();
|
||||
await pool.query(
|
||||
'INSERT INTO gto_builders (id, pacienteId, dentistaId, credenciadaId, status, total) VALUES (?, ?, ?, ?, "RASCUNHO", 0)',
|
||||
"INSERT INTO gto_builders (id, pacienteId, dentistaId, credenciadaId, status, total) VALUES ($1, $2, $3, $4, 'RASCUNHO', 0)",
|
||||
[id, pacienteId, dentistaId, credenciadaId]
|
||||
);
|
||||
res.json({ id });
|
||||
@@ -1204,7 +1207,7 @@ app.delete('/api/gto-builder/:id/item/:itemId', async (req, res) => {
|
||||
|
||||
app.put('/api/gto-builder/:id/finalizar', async (req, res) => {
|
||||
try {
|
||||
await pool.query('UPDATE gto_builders SET status = "FINALIZADA" WHERE id = ?', [req.params.id]);
|
||||
await pool.query("UPDATE gto_builders SET status = 'FINALIZADA' WHERE id = $1", [req.params.id]);
|
||||
|
||||
// Logic to migrate to guias_odontologicas would go here in a real app
|
||||
// For now, we just mark as finished.
|
||||
@@ -1297,5 +1300,11 @@ async function generateIntelligentNotifications() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Start server after all routes are registered
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`[SERVER] Running on http://0.0.0.0:${PORT}`);
|
||||
// Run immediately on startup, then every 5 minutes
|
||||
generateIntelligentNotifications();
|
||||
setInterval(generateIntelligentNotifications, 5 * 60 * 1000);
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ const App: React.FC = () => {
|
||||
const permissions: Record<string, ViewKey[]> = {
|
||||
paciente: ['tratamentos', 'notificacoes'],
|
||||
dentista: ['dashboard', 'pacientes', 'agenda', 'ortodontia', 'tratamentos', 'notificacoes', 'clinicas'],
|
||||
funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas'],
|
||||
funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'relatorios', 'notificacoes', 'clinicas'],
|
||||
};
|
||||
|
||||
return permissions[role]?.includes(view) || false;
|
||||
|
||||
@@ -76,7 +76,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ activeTab, setActiveTab }) =>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-lg text-gray-800 block leading-tight tracking-tight">SCOREODONTO</span>
|
||||
<span className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">MYSQL + GOOGLE</span>
|
||||
<span className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">POSTGRESQL + GOOGLE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -449,6 +449,7 @@ export const HybridBackend = {
|
||||
salvarAgendamento: async (ag: Partial<Agendamento>) => {
|
||||
const res = await apiFetch(`${API_URL}/agendamentos`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(ag)
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -461,6 +462,7 @@ export const HybridBackend = {
|
||||
atualizarAgendamento: async (id: string, data: Partial<Agendamento>) => {
|
||||
const res = await apiFetch(`${API_URL}/agendamentos/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -38,9 +38,13 @@ export const ClinicasView: React.FC = () => {
|
||||
const handleUpdateColor = async (workspaceId: string, color: string) => {
|
||||
setSavingColor(true);
|
||||
try {
|
||||
const token = localStorage.getItem('SCOREODONTO_AUTH_TOKEN');
|
||||
const res = await fetch(`/api/clinicas/${workspaceId}/color`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
},
|
||||
body: JSON.stringify({ cor: color })
|
||||
});
|
||||
|
||||
|
||||
@@ -126,9 +126,14 @@ export const Dashboard: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button className="bg-white text-gray-900 border border-gray-200 px-6 py-3 rounded-2xl text-xs font-black uppercase tracking-widest hover:bg-gray-50 transition-all flex items-center gap-2 shadow-sm">
|
||||
{(currentRole === 'admin' || currentRole === 'donoclinica' || currentRole === 'funcionario') && (
|
||||
<button
|
||||
onClick={() => window.location.hash = '#/relatorios'}
|
||||
className="bg-white text-gray-900 border border-gray-200 px-6 py-3 rounded-2xl text-xs font-black uppercase tracking-widest hover:bg-gray-50 transition-all flex items-center gap-2 shadow-sm"
|
||||
>
|
||||
<Clock size={16} /> Relatórios
|
||||
</button>
|
||||
)}
|
||||
{(currentRole === 'admin' || currentRole === 'donoclinica' || currentRole === 'funcionario') && (
|
||||
<button
|
||||
onClick={() => window.location.hash = '#/pacientes'}
|
||||
@@ -255,7 +260,7 @@ export const Dashboard: React.FC = () => {
|
||||
<Calendar size={20} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-black text-white uppercase tracking-tight truncate">{app.pacienteNome || 'Paciente'}</div>
|
||||
<div className="text-sm font-black text-white uppercase tracking-tight truncate">{app.pacientenome || 'Paciente'}</div>
|
||||
<div className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mt-0.5 truncate">
|
||||
{app.dentistaNome || 'Dr. Dentista'} • {new Date(app.start_time).toLocaleDateString('pt-BR')} {new Date(app.start_time).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
|
||||
<Database size={32} />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white uppercase tracking-wide">SCOREODONTO</h1>
|
||||
<p className="text-blue-100 text-xs font-bold uppercase mt-1">SISTEMA INTEGRADO MYSQL + GOOGLE</p>
|
||||
<p className="text-blue-100 text-xs font-bold uppercase mt-1">SISTEMA INTEGRADO POSTGRESQL + GOOGLE</p>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
|
||||
@@ -91,7 +91,10 @@ export const MeusTratamentos: React.FC = () => {
|
||||
sortOrder
|
||||
});
|
||||
|
||||
const res = await fetch(`/api/guias?${params}`);
|
||||
const token = localStorage.getItem('SCOREODONTO_AUTH_TOKEN');
|
||||
const res = await fetch(`/api/guias?${params}`, {
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
|
||||
});
|
||||
if (!res.ok) throw new Error('Falha ao carregar guias');
|
||||
return res.json();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user