Initial commit
This commit is contained in:
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
client/node_modules
|
||||||
|
data/agenda.db
|
||||||
|
npm-debug.log
|
||||||
|
.DS_Store
|
||||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
client/node_modules
|
||||||
|
dist
|
||||||
|
client/dist
|
||||||
|
data/agenda.db
|
||||||
|
.env
|
||||||
|
npm-debug.log
|
||||||
|
.DS_Store
|
||||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM node:20-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY client/package.json client/
|
||||||
|
RUN cd client && npm install
|
||||||
|
COPY client client
|
||||||
|
RUN cd client && npm run build
|
||||||
|
|
||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json .
|
||||||
|
RUN npm install
|
||||||
|
COPY server server
|
||||||
|
COPY --from=builder /app/client/dist client/dist
|
||||||
|
|
||||||
|
EXPOSE 4000
|
||||||
|
CMD ["npm", "start"]
|
||||||
99
README.md
Normal file
99
README.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# OdontoFlow Scheduler
|
||||||
|
|
||||||
|
Dashboard de agendamento para clínicas odontológicas com front-end em React e back-end Node/PostgreSQL prontos para rodar em Docker.
|
||||||
|
|
||||||
|
## Estrutura
|
||||||
|
- `client/`: app React (Vite) reproduzindo a experiência solicitada com agenda, filtros e modais.
|
||||||
|
- `server/`: API Express conectada a um banco PostgreSQL com CRUD completo de pacientes, agendamentos e prontuários.
|
||||||
|
- `docker-compose.yml`: sobe o app, o banco PostgreSQL e o Adminer para gerenciamento visual.
|
||||||
|
|
||||||
|
## Comandos de desenvolvimento
|
||||||
|
1. Instale dependências do servidor:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
2. No diretório `client`, instale o front-end:
|
||||||
|
```bash
|
||||||
|
cd client && npm install
|
||||||
|
```
|
||||||
|
3. Garanta que exista um PostgreSQL disponível (por exemplo `docker compose up postgres -d`). Configure a URL em `DATABASE_URL` ou use `postgresql://agenda:agenda@localhost:5432/agenda`.
|
||||||
|
|
||||||
|
4. Em uma janela, rode o servidor:
|
||||||
|
```bash
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
5. Em outra, inicie o front-end em modo dev:
|
||||||
|
```bash
|
||||||
|
cd client && npm run dev
|
||||||
|
```
|
||||||
|
O front-end irá consumir a API em `http://localhost:4000/api` automaticamente.
|
||||||
|
|
||||||
|
## Construindo e executando via Docker
|
||||||
|
```bash
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
A aplicação ficará disponível em `http://localhost:4000`. O PostgreSQL roda no serviço `postgres` (persistido em `postgres_data`) e o Adminer para inspeção está em `http://localhost:8080` (use `agenda` / `agenda` / banco `agenda`).
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
- Os seeds criam três médicos e alguns agendamentos iniciais na primeira execução do banco.
|
||||||
|
- O servidor serve os assets estáticos de `client/dist`; rode `npm run build` dentro de `client` antes de subir a imagem ou deixe o Docker cuidar disso.
|
||||||
|
- Utilize as rotas REST em `/api/appointments`, `/api/patients`, `/api/records` e `/api/doctors` para integrar outros sistemas.
|
||||||
|
|
||||||
|
## Integração via N8N
|
||||||
|
Há um endpoint único pensado para automações low-code. Ele concentra todas as ações necessárias para criar, consultar, alterar ou remover agendamentos e cadastrar pacientes usando o N8N (ou qualquer outro orquestrador).
|
||||||
|
|
||||||
|
### Endpoint
|
||||||
|
- **URL:** `POST /api/integrations/n8n`
|
||||||
|
- **Corpo:** JSON com as chaves:
|
||||||
|
- `action`: string informando qual operação executar (ver tabela abaixo).
|
||||||
|
- `data`: objeto com os campos necessários para a ação escolhida.
|
||||||
|
|
||||||
|
### Ações suportadas
|
||||||
|
| Ação (`action`) | Dados mínimos em `data` | O que faz |
|
||||||
|
|-----------------|-------------------------|-----------|
|
||||||
|
| `create_appointment` | `patient`, `doctorId`, `date`, `time` (opcionais: `phone`, `type`, `status`, `notes`) | Cria um agendamento e retorna o registro completo |
|
||||||
|
| `list_appointments` | (opcional) `date`, `doctorId`, `status`, `patient` | Lista agendamentos aplicando filtros |
|
||||||
|
| `get_appointment` | `id` | Busca um agendamento específico com dados do médico |
|
||||||
|
| `update_appointment` | `id` + campos a atualizar (`patient`, `doctorId`, `date`, `time`, etc.) | Atualiza todas as informações do agendamento |
|
||||||
|
| `reschedule_appointment` | `id`, `date`, `time` (opcional `doctorId`) | Atualiza apenas data/horário e, opcionalmente, o médico |
|
||||||
|
| `delete_appointment` | `id` | Remove o agendamento informado |
|
||||||
|
| `create_patient` | `name`, `phone` (opcionais: `email`, `cpf`, `cep`, `address`, `birthdate`) | Cadastra um novo paciente |
|
||||||
|
|
||||||
|
Caso `action` seja inválido, o servidor responde com HTTP 400 e mensagem listando as opções válidas.
|
||||||
|
|
||||||
|
### Exemplo de uso (cURL ou HTTP Request node no N8N)
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:4000/api/integrations/n8n \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"action": "create_appointment",
|
||||||
|
"data": {
|
||||||
|
"patient": "João Silva",
|
||||||
|
"phone": "+55 11 9999-9999",
|
||||||
|
"doctorId": "dr-ana",
|
||||||
|
"date": "2024-10-01",
|
||||||
|
"time": "09:00",
|
||||||
|
"type": "Avaliação",
|
||||||
|
"status": "pending"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
Resposta típica:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"id": 123,
|
||||||
|
"patient": "João Silva",
|
||||||
|
"phone": "+55 11 9999-9999",
|
||||||
|
"doctor_id": "dr-ana",
|
||||||
|
"date": "2024-10-01",
|
||||||
|
"time": "09:00",
|
||||||
|
"type": "Avaliação",
|
||||||
|
"status": "pending",
|
||||||
|
"notes": "",
|
||||||
|
"createdAt": "2024-09-10T12:34:56.000Z",
|
||||||
|
"doctorId": "dr-ana"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
No N8N, basta configurar um nó HTTP Request em `POST`, apontando para a URL acima e enviando o JSON conforme a ação desejada.
|
||||||
13
client/index.html
Normal file
13
client/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>OdontoFlow</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="https://lucide.dev/icons/calendar-days.svg" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
client/package.json
Normal file
20
client/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "agenda-client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.278.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
|
"vite": "^5.1.2"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
1268
client/src/App.css
Normal file
1268
client/src/App.css
Normal file
File diff suppressed because it is too large
Load Diff
1972
client/src/App.jsx
Normal file
1972
client/src/App.jsx
Normal file
File diff suppressed because it is too large
Load Diff
10
client/src/main.jsx
Normal file
10
client/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
12
client/vite.config.mjs
Normal file
12
client/vite.config.mjs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://localhost:4000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
0
data/.gitkeep
Normal file
0
data/.gitkeep
Normal file
34
docker-compose.yml
Normal file
34
docker-compose.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: agenda
|
||||||
|
POSTGRES_USER: agenda
|
||||||
|
POSTGRES_PASSWORD: agenda
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
adminer:
|
||||||
|
image: adminer:4
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- ADMINER_DEFAULT_SERVER=postgres
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
agenda:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "4000:4000"
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- PORT=4000
|
||||||
|
- DATABASE_URL=postgresql://agenda:agenda@postgres:5432/agenda
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
18
package.json
Normal file
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "agenda-dashboard",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Node + React dashboard para clínicas odontológicas com SQLite e Docker.",
|
||||||
|
"main": "server/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server/index.js",
|
||||||
|
"dev": "node server/index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"pg": "^8.11.3"
|
||||||
|
},
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
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