Initial commit
This commit is contained in:
739
server/index.js
Normal file
739
server/index.js
Normal file
@@ -0,0 +1,739 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user