740 lines
24 KiB
JavaScript
740 lines
24 KiB
JavaScript
const express = require('express');
|
|
const cors = require('cors');
|
|
const morgan = require('morgan');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const { Pool } = require('pg');
|
|
|
|
const PORT = process.env.PORT || 4000;
|
|
const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://agenda:agenda@postgres:5432/agenda';
|
|
|
|
const pool = new Pool({ connectionString: DATABASE_URL });
|
|
|
|
const DEFAULT_SERVICE_TYPES = [
|
|
'Avaliação',
|
|
'Limpeza',
|
|
'Restauração',
|
|
'Extração',
|
|
'Canal',
|
|
'Manutenção Ortodôntica',
|
|
'Cirurgia',
|
|
];
|
|
|
|
async function safeQuery(query) {
|
|
try {
|
|
await pool.query(query);
|
|
} catch (err) {
|
|
if (process.env.DEBUG_SQL) {
|
|
console.warn('Ignored migration statement:', err.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function setupDatabase() {
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS doctors (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
specialty TEXT NOT NULL,
|
|
color TEXT NOT NULL
|
|
);
|
|
`);
|
|
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS patients (
|
|
id SERIAL PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
phone TEXT NOT NULL,
|
|
email TEXT,
|
|
cpf TEXT,
|
|
cep TEXT,
|
|
address TEXT,
|
|
birthdate TEXT,
|
|
createdAt TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS appointments (
|
|
id SERIAL PRIMARY KEY,
|
|
patient TEXT NOT NULL,
|
|
phone TEXT,
|
|
doctor_id TEXT NOT NULL REFERENCES doctors(id),
|
|
date TEXT NOT NULL,
|
|
time TEXT NOT NULL,
|
|
type TEXT,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
notes TEXT,
|
|
createdAt TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS records (
|
|
id SERIAL PRIMARY KEY,
|
|
patient_id INTEGER NOT NULL REFERENCES patients(id),
|
|
date TEXT NOT NULL,
|
|
professional_name TEXT,
|
|
professional_registration TEXT,
|
|
clinic_info TEXT,
|
|
chief_complaint TEXT,
|
|
medical_history TEXT,
|
|
dental_history TEXT,
|
|
clinical_exam TEXT,
|
|
diagnosis TEXT,
|
|
treatment_plan TEXT,
|
|
consent_notes TEXT,
|
|
evolution TEXT,
|
|
prescriptions TEXT,
|
|
communications TEXT,
|
|
attachments TEXT,
|
|
createdAt TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS service_types (
|
|
id SERIAL PRIMARY KEY,
|
|
name TEXT UNIQUE NOT NULL
|
|
);
|
|
`);
|
|
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS clinic_settings (
|
|
id SERIAL PRIMARY KEY,
|
|
clinic_name TEXT,
|
|
phone TEXT,
|
|
email TEXT,
|
|
address TEXT,
|
|
working_hours TEXT,
|
|
notes TEXT,
|
|
updatedAt TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await safeQuery('ALTER TABLE appointments RENAME COLUMN "doctorid" TO doctor_id;');
|
|
await safeQuery('ALTER TABLE records RENAME COLUMN "patientid" TO patient_id;');
|
|
await safeQuery('ALTER TABLE records RENAME COLUMN professionalname TO professional_name;');
|
|
await safeQuery('ALTER TABLE records RENAME COLUMN professionalregistration TO professional_registration;');
|
|
await safeQuery('ALTER TABLE records RENAME COLUMN clinicinfo TO clinic_info;');
|
|
await safeQuery('ALTER TABLE records RENAME COLUMN chiefcomplaint TO chief_complaint;');
|
|
await safeQuery('ALTER TABLE records RENAME COLUMN medicalhistory TO medical_history;');
|
|
await safeQuery('ALTER TABLE records RENAME COLUMN dentalhistory TO dental_history;');
|
|
await safeQuery('ALTER TABLE records RENAME COLUMN clinicalexam TO clinical_exam;');
|
|
await safeQuery('ALTER TABLE records RENAME COLUMN treatmentplan TO treatment_plan;');
|
|
await safeQuery('ALTER TABLE records RENAME COLUMN consentnotes TO consent_notes;');
|
|
|
|
const { rows: doctorCountRows } = await pool.query('SELECT COUNT(*)::int AS total FROM doctors');
|
|
if ((doctorCountRows[0]?.total || 0) === 0) {
|
|
const doctors = [
|
|
{ id: 'dr-ana', name: 'Dra. Ana Silva', specialty: 'Ortodontia', color: '#f97316' },
|
|
{ id: 'dr-carlos', name: 'Dr. Carlos Mendes', specialty: 'Implantodontia', color: '#0ea5e9' },
|
|
{ id: 'dr-beatriz', name: 'Dra. Beatriz Costa', specialty: 'Clínica Geral', color: '#10b981' },
|
|
];
|
|
for (const doc of doctors) {
|
|
await pool.query('INSERT INTO doctors (id, name, specialty, color) VALUES ($1,$2,$3,$4)', [
|
|
doc.id,
|
|
doc.name,
|
|
doc.specialty,
|
|
doc.color,
|
|
]);
|
|
}
|
|
}
|
|
|
|
const { rows: appointmentCountRows } = await pool.query('SELECT COUNT(*)::int AS total FROM appointments');
|
|
if ((appointmentCountRows[0]?.total || 0) === 0) {
|
|
const sampleAppointments = [
|
|
['Ana Silva', '(11) 99111-2233', 'dr-ana', '2024-07-09', '09:00', 'Avaliação', 'confirmed', 'Primeira visita'],
|
|
['Pedro Oliveira', '(21) 98888-1111', 'dr-carlos', '2024-07-09', '09:30', 'Implante', 'pending', 'Pacote de implantes'],
|
|
['Marina Tavares', '(31) 99999-2222', 'dr-beatriz', '2024-07-09', '10:00', 'Limpeza', 'pending', 'Retorno rotina'],
|
|
['Rafael Gomes', '(41) 97777-3333', 'dr-ana', '2024-07-09', '10:30', 'Ortodontia', 'confirmed', ''],
|
|
['Fernanda Lima', '(51) 98888-4444', 'dr-carlos', '2024-07-09', '11:00', 'Avaliação', 'pending', 'Novo paciente'],
|
|
['Bruno Santana', '(21) 96666-5555', 'dr-beatriz', '2024-07-09', '11:30', 'Manutenção Ortodôntica', 'confirmed', ''],
|
|
];
|
|
for (const sample of sampleAppointments) {
|
|
await pool.query(
|
|
`INSERT INTO appointments (patient, phone, doctor_id, date, time, type, status, notes)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)`,
|
|
sample,
|
|
);
|
|
}
|
|
}
|
|
|
|
const { rows: serviceTypeRows } = await pool.query('SELECT COUNT(*)::int AS total FROM service_types');
|
|
if ((serviceTypeRows[0]?.total || 0) === 0) {
|
|
for (const name of DEFAULT_SERVICE_TYPES) {
|
|
await pool.query('INSERT INTO service_types (name) VALUES ($1)', [name]);
|
|
}
|
|
}
|
|
|
|
const { rows: clinicRows } = await pool.query('SELECT COUNT(*)::int AS total FROM clinic_settings');
|
|
if ((clinicRows[0]?.total || 0) === 0) {
|
|
await pool.query(
|
|
`INSERT INTO clinic_settings (clinic_name, phone, email, address, working_hours, notes)
|
|
VALUES ($1,$2,$3,$4,$5,$6)`,
|
|
['OdontoFlow', '(00) 0000-0000', 'contato@odontoflow.com', 'Rua Exemplo, 123 - Centro', 'Seg à Sex · 08h às 18h', 'Atualize os dados conforme a clínica.'],
|
|
);
|
|
}
|
|
}
|
|
|
|
const app = express();
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
app.use(morgan('tiny'));
|
|
|
|
const asyncHandler =
|
|
(fn) =>
|
|
(...args) =>
|
|
fn(...args).catch((err) => {
|
|
console.error(err);
|
|
const res = args[1];
|
|
if (!res.headersSent) {
|
|
res.status(500).json({ error: 'Erro inesperado no servidor.' });
|
|
}
|
|
});
|
|
|
|
app.get(
|
|
'/api/doctors',
|
|
asyncHandler(async (req, res) => {
|
|
const { rows } = await pool.query('SELECT * FROM doctors ORDER BY name');
|
|
res.json(rows);
|
|
}),
|
|
);
|
|
|
|
const slugify = (value) =>
|
|
value
|
|
.toLowerCase()
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-|-$/g, '');
|
|
|
|
app.post(
|
|
'/api/doctors',
|
|
asyncHandler(async (req, res) => {
|
|
const { id, name, specialty, color } = req.body;
|
|
if (!name) {
|
|
return res.status(400).json({ error: 'Nome do doutor é obrigatório.' });
|
|
}
|
|
const generatedId = id || slugify(name) || `doctor-${Date.now()}`;
|
|
await pool.query('INSERT INTO doctors (id, name, specialty, color) VALUES ($1,$2,$3,$4)', [
|
|
generatedId,
|
|
name,
|
|
specialty || '',
|
|
color || '#0ea5e9',
|
|
]);
|
|
res.status(201).json({ id: generatedId, name, specialty: specialty || '', color: color || '#0ea5e9' });
|
|
}),
|
|
);
|
|
|
|
app.delete(
|
|
'/api/doctors/:id',
|
|
asyncHandler(async (req, res) => {
|
|
const { id } = req.params;
|
|
const { rowCount } = await pool.query('DELETE FROM doctors WHERE id = $1', [id]);
|
|
if (rowCount === 0) {
|
|
return res.status(404).json({ error: 'Doutor não encontrado.' });
|
|
}
|
|
res.json({ success: true });
|
|
}),
|
|
);
|
|
|
|
app.get(
|
|
'/api/appointments',
|
|
asyncHandler(async (req, res) => {
|
|
const { date, doctorId, status } = req.query;
|
|
let query = `
|
|
SELECT
|
|
a.id,
|
|
a.patient,
|
|
a.phone,
|
|
a.doctor_id AS "doctorId",
|
|
a.date,
|
|
a.time,
|
|
a.type,
|
|
a.status,
|
|
a.notes,
|
|
a.createdat AS "createdAt",
|
|
d.name AS "doctorName",
|
|
d.specialty,
|
|
d.color
|
|
FROM appointments a
|
|
JOIN doctors d ON d.id = a.doctor_id
|
|
WHERE 1=1
|
|
`;
|
|
const params = [];
|
|
if (date) {
|
|
params.push(date);
|
|
query += ` AND a.date = $${params.length}`;
|
|
}
|
|
if (doctorId) {
|
|
params.push(doctorId);
|
|
query += ` AND a.doctor_id = $${params.length}`;
|
|
}
|
|
if (status) {
|
|
params.push(status);
|
|
query += ` AND a.status = $${params.length}`;
|
|
}
|
|
query += ' ORDER BY a.date DESC, a.time ASC';
|
|
const { rows } = await pool.query(query, params);
|
|
res.json(rows);
|
|
}),
|
|
);
|
|
|
|
app.post(
|
|
'/api/appointments',
|
|
asyncHandler(async (req, res) => {
|
|
const { patient, phone, doctorId, date, time, type, status, notes } = req.body;
|
|
if (!patient || !doctorId || !date || !time) {
|
|
return res.status(400).json({ error: 'Campos obrigatórios: patient, doctorId, date, time.' });
|
|
}
|
|
const { rows } = await pool.query(
|
|
`INSERT INTO appointments (patient, phone, doctor_id, date, time, type, status, notes)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
|
RETURNING *`,
|
|
[patient, phone || '', doctorId, date, time, type || '', status || 'pending', notes || ''],
|
|
);
|
|
res.status(201).json({ ...rows[0], doctorId });
|
|
}),
|
|
);
|
|
|
|
app.put(
|
|
'/api/appointments/:id',
|
|
asyncHandler(async (req, res) => {
|
|
const { id } = req.params;
|
|
const { patient, phone, doctorId, date, time, type, status, notes } = req.body;
|
|
const { rowCount, rows } = await pool.query(
|
|
`UPDATE appointments
|
|
SET patient=$1, phone=$2, doctor_id=$3, date=$4, time=$5, type=$6, status=$7, notes=$8
|
|
WHERE id=$9
|
|
RETURNING *`,
|
|
[patient, phone || '', doctorId, date, time, type || '', status || 'pending', notes || '', id],
|
|
);
|
|
if (rowCount === 0) {
|
|
return res.status(404).json({ error: 'Agendamento não encontrado.' });
|
|
}
|
|
res.json({ ...rows[0], doctorId });
|
|
}),
|
|
);
|
|
|
|
app.patch(
|
|
'/api/appointments/:id/status',
|
|
asyncHandler(async (req, res) => {
|
|
const { id } = req.params;
|
|
const { status } = req.body;
|
|
const valid = ['pending', 'confirmed', 'cancelled'];
|
|
if (!valid.includes(status)) {
|
|
return res.status(400).json({ error: `Status inválido: ${status}` });
|
|
}
|
|
const { rowCount } = await pool.query('UPDATE appointments SET status=$1 WHERE id=$2', [status, id]);
|
|
if (rowCount === 0) {
|
|
return res.status(404).json({ error: 'Agendamento não encontrado.' });
|
|
}
|
|
res.json({ success: true });
|
|
}),
|
|
);
|
|
|
|
app.delete(
|
|
'/api/appointments/:id',
|
|
asyncHandler(async (req, res) => {
|
|
const { id } = req.params;
|
|
const { rowCount } = await pool.query('DELETE FROM appointments WHERE id=$1', [id]);
|
|
if (rowCount === 0) {
|
|
return res.status(404).json({ error: 'Agendamento não encontrado.' });
|
|
}
|
|
res.json({ success: true });
|
|
}),
|
|
);
|
|
|
|
app.get(
|
|
'/api/patients',
|
|
asyncHandler(async (req, res) => {
|
|
const { rows } = await pool.query('SELECT * FROM patients ORDER BY createdAt DESC');
|
|
res.json(rows);
|
|
}),
|
|
);
|
|
|
|
app.post(
|
|
'/api/patients',
|
|
asyncHandler(async (req, res) => {
|
|
const { name, phone, email, cpf, cep, address, birthdate } = req.body;
|
|
if (!name || !phone) {
|
|
return res.status(400).json({ error: 'Campos obrigatórios: name, phone.' });
|
|
}
|
|
const { rows } = await pool.query(
|
|
`INSERT INTO patients (name, phone, email, cpf, cep, address, birthdate)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7)
|
|
RETURNING *`,
|
|
[name, phone, email || '', cpf || '', cep || '', address || '', birthdate || ''],
|
|
);
|
|
res.status(201).json(rows[0]);
|
|
}),
|
|
);
|
|
|
|
app.put(
|
|
'/api/patients/:id',
|
|
asyncHandler(async (req, res) => {
|
|
const { id } = req.params;
|
|
const { name, phone, email, cpf, cep, address, birthdate } = req.body;
|
|
const { rowCount, rows } = await pool.query(
|
|
`UPDATE patients
|
|
SET name=$1, phone=$2, email=$3, cpf=$4, cep=$5, address=$6, birthdate=$7
|
|
WHERE id=$8
|
|
RETURNING *`,
|
|
[name, phone, email || '', cpf || '', cep || '', address || '', birthdate || '', id],
|
|
);
|
|
if (rowCount === 0) {
|
|
return res.status(404).json({ error: 'Paciente não encontrado.' });
|
|
}
|
|
res.json(rows[0]);
|
|
}),
|
|
);
|
|
|
|
app.delete(
|
|
'/api/patients/:id',
|
|
asyncHandler(async (req, res) => {
|
|
const { id } = req.params;
|
|
const { rowCount } = await pool.query('DELETE FROM patients WHERE id=$1', [id]);
|
|
if (rowCount === 0) {
|
|
return res.status(404).json({ error: 'Paciente não encontrado.' });
|
|
}
|
|
res.json({ success: true });
|
|
}),
|
|
);
|
|
|
|
app.get(
|
|
'/api/records',
|
|
asyncHandler(async (req, res) => {
|
|
const { patientId } = req.query;
|
|
let query = `
|
|
SELECT
|
|
r.id,
|
|
r.patient_id AS "patientId",
|
|
r.date,
|
|
r.professional_name AS "professionalName",
|
|
r.professional_registration AS "professionalRegistration",
|
|
r.clinic_info AS "clinicInfo",
|
|
r.chief_complaint AS "chiefComplaint",
|
|
r.medical_history AS "medicalHistory",
|
|
r.dental_history AS "dentalHistory",
|
|
r.clinical_exam AS "clinicalExam",
|
|
r.diagnosis,
|
|
r.treatment_plan AS "treatmentPlan",
|
|
r.consent_notes AS "consentNotes",
|
|
r.evolution,
|
|
r.prescriptions,
|
|
r.communications,
|
|
r.attachments,
|
|
r.createdAt AS "createdAt",
|
|
p.name AS "patientName",
|
|
p.cpf,
|
|
p.phone
|
|
FROM records r
|
|
JOIN patients p ON p.id = r.patient_id
|
|
WHERE 1=1
|
|
`;
|
|
const params = [];
|
|
if (patientId) {
|
|
params.push(patientId);
|
|
query += ` AND r.patient_id = $${params.length}`;
|
|
}
|
|
query += ' ORDER BY r.date DESC, r.createdAt DESC';
|
|
const { rows } = await pool.query(query, params);
|
|
res.json(rows);
|
|
}),
|
|
);
|
|
|
|
app.post(
|
|
'/api/records',
|
|
asyncHandler(async (req, res) => {
|
|
const {
|
|
patientId,
|
|
date,
|
|
professionalName,
|
|
professionalRegistration,
|
|
clinicInfo,
|
|
chiefComplaint,
|
|
medicalHistory,
|
|
dentalHistory,
|
|
clinicalExam,
|
|
diagnosis,
|
|
treatmentPlan,
|
|
consentNotes,
|
|
evolution,
|
|
prescriptions,
|
|
communications,
|
|
attachments,
|
|
} = req.body;
|
|
if (!patientId || !date) {
|
|
return res.status(400).json({ error: 'Campos obrigatórios: patientId, date.' });
|
|
}
|
|
const { rows } = await pool.query(
|
|
`INSERT INTO records
|
|
(patient_id, date, professional_name, professional_registration, clinic_info, chief_complaint,
|
|
medical_history, dental_history, clinical_exam, diagnosis, treatment_plan, consent_notes, evolution,
|
|
prescriptions, communications, attachments)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
|
|
RETURNING *`,
|
|
[
|
|
patientId,
|
|
date,
|
|
professionalName || '',
|
|
professionalRegistration || '',
|
|
clinicInfo || '',
|
|
chiefComplaint || '',
|
|
medicalHistory || '',
|
|
dentalHistory || '',
|
|
clinicalExam || '',
|
|
diagnosis || '',
|
|
treatmentPlan || '',
|
|
consentNotes || '',
|
|
evolution || '',
|
|
prescriptions || '',
|
|
communications || '',
|
|
attachments || '',
|
|
],
|
|
);
|
|
const record = rows[0];
|
|
const { rows: patientRows } = await pool.query('SELECT name AS "patientName", cpf, phone FROM patients WHERE id=$1', [
|
|
patientId,
|
|
]);
|
|
res.status(201).json({ ...record, ...patientRows[0], patientId });
|
|
}),
|
|
);
|
|
|
|
app.get(
|
|
'/api/service-types',
|
|
asyncHandler(async (req, res) => {
|
|
const { rows } = await pool.query('SELECT * FROM service_types ORDER BY name');
|
|
res.json(rows);
|
|
}),
|
|
);
|
|
|
|
app.post(
|
|
'/api/service-types',
|
|
asyncHandler(async (req, res) => {
|
|
const { name } = req.body;
|
|
if (!name) {
|
|
return res.status(400).json({ error: 'Informe o nome do tipo de atendimento.' });
|
|
}
|
|
const { rows } = await pool.query('INSERT INTO service_types (name) VALUES ($1) RETURNING *', [name]);
|
|
res.status(201).json(rows[0]);
|
|
}),
|
|
);
|
|
|
|
app.delete(
|
|
'/api/service-types/:id',
|
|
asyncHandler(async (req, res) => {
|
|
const { id } = req.params;
|
|
const { rowCount } = await pool.query('DELETE FROM service_types WHERE id=$1', [id]);
|
|
if (rowCount === 0) {
|
|
return res.status(404).json({ error: 'Tipo de atendimento não encontrado.' });
|
|
}
|
|
res.json({ success: true });
|
|
}),
|
|
);
|
|
|
|
const normalizeAction = (action) => (action || '').toLowerCase();
|
|
|
|
app.post(
|
|
'/api/integrations/n8n',
|
|
asyncHandler(async (req, res) => {
|
|
const action = normalizeAction(req.body.action);
|
|
const data = req.body.data || {};
|
|
|
|
switch (action) {
|
|
case 'create_appointment': {
|
|
const { patient, phone, doctorId, date, time, type, status, notes } = data;
|
|
if (!patient || !doctorId || !date || !time) {
|
|
return res.status(400).json({ error: 'Campos obrigatórios: patient, doctorId, date, time.' });
|
|
}
|
|
const { rows } = await pool.query(
|
|
`INSERT INTO appointments (patient, phone, doctor_id, date, time, type, status, notes)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
|
RETURNING *`,
|
|
[patient, phone || '', doctorId, date, time, type || '', status || 'pending', notes || ''],
|
|
);
|
|
return res.status(201).json({ result: { ...rows[0], doctorId } });
|
|
}
|
|
case 'list_appointments': {
|
|
const { date, doctorId, status, patient } = data;
|
|
let query = `
|
|
SELECT
|
|
a.id,
|
|
a.patient,
|
|
a.phone,
|
|
a.doctor_id AS "doctorId",
|
|
a.date,
|
|
a.time,
|
|
a.type,
|
|
a.status,
|
|
a.notes,
|
|
a.createdat AS "createdAt",
|
|
d.name AS "doctorName",
|
|
d.specialty,
|
|
d.color
|
|
FROM appointments a
|
|
JOIN doctors d ON d.id = a.doctor_id
|
|
WHERE 1=1
|
|
`;
|
|
const params = [];
|
|
if (date) {
|
|
params.push(date);
|
|
query += ` AND a.date = $${params.length}`;
|
|
}
|
|
if (doctorId) {
|
|
params.push(doctorId);
|
|
query += ` AND a.doctor_id = $${params.length}`;
|
|
}
|
|
if (status) {
|
|
params.push(status);
|
|
query += ` AND a.status = $${params.length}`;
|
|
}
|
|
if (patient) {
|
|
params.push(`%${patient.toLowerCase()}%`);
|
|
query += ` AND LOWER(a.patient) LIKE $${params.length}`;
|
|
}
|
|
query += ' ORDER BY a.date DESC, a.time ASC';
|
|
const { rows } = await pool.query(query, params);
|
|
return res.json({ result: rows });
|
|
}
|
|
case 'get_appointment': {
|
|
const { id } = data;
|
|
if (!id) return res.status(400).json({ error: 'Informe o id do agendamento.' });
|
|
const { rows } = await pool.query(
|
|
`
|
|
SELECT
|
|
a.id,
|
|
a.patient,
|
|
a.phone,
|
|
a.doctor_id AS "doctorId",
|
|
a.date,
|
|
a.time,
|
|
a.type,
|
|
a.status,
|
|
a.notes,
|
|
a.createdat AS "createdAt",
|
|
d.name AS "doctorName",
|
|
d.specialty,
|
|
d.color
|
|
FROM appointments a
|
|
JOIN doctors d ON d.id = a.doctor_id
|
|
WHERE a.id = $1
|
|
`,
|
|
[id],
|
|
);
|
|
if (!rows.length) {
|
|
return res.status(404).json({ error: 'Agendamento não encontrado.' });
|
|
}
|
|
return res.json({ result: rows[0] });
|
|
}
|
|
case 'update_appointment': {
|
|
const { id, patient, phone, doctorId, date, time, type, status, notes } = data;
|
|
if (!id) return res.status(400).json({ error: 'Informe o id do agendamento.' });
|
|
const { rowCount, rows } = await pool.query(
|
|
`UPDATE appointments
|
|
SET patient=$1, phone=$2, doctor_id=$3, date=$4, time=$5, type=$6, status=$7, notes=$8
|
|
WHERE id=$9
|
|
RETURNING *`,
|
|
[patient, phone || '', doctorId, date, time, type || '', status || 'pending', notes || '', id],
|
|
);
|
|
if (rowCount === 0) {
|
|
return res.status(404).json({ error: 'Agendamento não encontrado.' });
|
|
}
|
|
return res.json({ result: { ...rows[0], doctorId } });
|
|
}
|
|
case 'reschedule_appointment': {
|
|
const { id, date, time, doctorId } = data;
|
|
if (!id || !date || !time) {
|
|
return res.status(400).json({ error: 'Campos obrigatórios: id, date, time.' });
|
|
}
|
|
const { rowCount, rows } = await pool.query(
|
|
`UPDATE appointments
|
|
SET date=$1, time=$2, doctor_id=COALESCE($3, doctor_id)
|
|
WHERE id=$4
|
|
RETURNING *`,
|
|
[date, time, doctorId || null, id],
|
|
);
|
|
if (rowCount === 0) {
|
|
return res.status(404).json({ error: 'Agendamento não encontrado.' });
|
|
}
|
|
return res.json({ result: { ...rows[0], doctorId: doctorId || rows[0].doctor_id } });
|
|
}
|
|
case 'delete_appointment': {
|
|
const { id } = data;
|
|
if (!id) return res.status(400).json({ error: 'Informe o id do agendamento.' });
|
|
const { rowCount } = await pool.query('DELETE FROM appointments WHERE id=$1', [id]);
|
|
if (rowCount === 0) {
|
|
return res.status(404).json({ error: 'Agendamento não encontrado.' });
|
|
}
|
|
return res.json({ result: { success: true } });
|
|
}
|
|
case 'create_patient': {
|
|
const { name, phone, email, cpf, cep, address, birthdate } = data;
|
|
if (!name || !phone) {
|
|
return res.status(400).json({ error: 'Campos obrigatórios: name, phone.' });
|
|
}
|
|
const { rows } = await pool.query(
|
|
`INSERT INTO patients (name, phone, email, cpf, cep, address, birthdate)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7)
|
|
RETURNING *`,
|
|
[name, phone, email || '', cpf || '', cep || '', address || '', birthdate || ''],
|
|
);
|
|
return res.status(201).json({ result: rows[0] });
|
|
}
|
|
default:
|
|
return res.status(400).json({
|
|
error: 'Ação inválida. Utilize create_appointment, list_appointments, get_appointment, update_appointment, reschedule_appointment, delete_appointment ou create_patient.',
|
|
});
|
|
}
|
|
}),
|
|
);
|
|
|
|
app.get(
|
|
'/api/settings/clinic',
|
|
asyncHandler(async (req, res) => {
|
|
const { rows } = await pool.query('SELECT * FROM clinic_settings ORDER BY id LIMIT 1');
|
|
res.json(rows[0] || {});
|
|
}),
|
|
);
|
|
|
|
app.put(
|
|
'/api/settings/clinic',
|
|
asyncHandler(async (req, res) => {
|
|
const { clinicName, phone, email, address, workingHours, notes } = req.body;
|
|
const { rows } = await pool.query(
|
|
`UPDATE clinic_settings
|
|
SET clinic_name=$1, phone=$2, email=$3, address=$4, working_hours=$5, notes=$6, updatedAt=NOW()
|
|
WHERE id = (SELECT id FROM clinic_settings ORDER BY id LIMIT 1)
|
|
RETURNING *`,
|
|
[clinicName || '', phone || '', email || '', address || '', workingHours || '', notes || ''],
|
|
);
|
|
res.json(rows[0]);
|
|
}),
|
|
);
|
|
|
|
const clientDist = path.join(__dirname, '..', 'client', 'dist');
|
|
app.use(express.static(clientDist, { maxAge: '1h' }));
|
|
|
|
app.get('*', (req, res) => {
|
|
if (req.path.startsWith('/api')) {
|
|
return res.status(404).send({ error: 'Endpoint não encontrado.' });
|
|
}
|
|
const fallback = path.join(clientDist, 'index.html');
|
|
if (fs.existsSync(fallback)) {
|
|
return res.sendFile(fallback);
|
|
}
|
|
res.send('Front-end ainda não foi buildado. Rode `npm --prefix client install && npm --prefix client run build`.');
|
|
});
|
|
|
|
setupDatabase()
|
|
.then(() => {
|
|
app.listen(PORT, () => {
|
|
console.log(`Servidor rodando na porta ${PORT}`);
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
console.error('Falha ao iniciar o banco de dados', error);
|
|
process.exit(1);
|
|
});
|