feat(pacientes): modais de receita, pedido de exame e edição em 4 páginas

- ReceitaModal: gerenciamento de modelos de receita por especialidade com
  drag-and-drop para reordenar medicamentos e preview de impressão
- PedidoExameModal: pedido de exames por catálogo (Raio-X / Tomografia /
  Outros) com DnD para ordenar, campos de região/observação por item e
  preview de impressão
- EditarPacienteModal: reformulado em 4 páginas (Dados / Família /
  Endereço / Histórico) com navegação anterior/próximo e modal 2xl
- Backend: tabelas modelos_receita, items_receita, pedidos_exame,
  items_pedido_exame criadas na migração; colunas de endereço adicionadas
  em pacientes; endpoints CRUD para receitas e pedidos de exame
- Integração: botões Receita e Raio-X no menu clínico agora abrem os
  novos modais reais em vez do placeholder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-05-14 16:53:28 +02:00
parent 3d287a4579
commit 15a8527f1c
7 changed files with 1337 additions and 108 deletions
+150 -1
View File
@@ -1665,16 +1665,165 @@ async function generateIntelligentNotifications() {
}
}
// --- MODELOS DE RECEITA ---
app.get('/api/modelos-receita', async (req, res) => {
try {
const { especialidadeId } = req.query;
let q = `SELECT m.*, COALESCE(json_agg(i ORDER BY i.ordem) FILTER (WHERE i.id IS NOT NULL), '[]') AS items
FROM modelos_receita m LEFT JOIN items_receita i ON i.modelo_id = m.id`;
const params = [];
if (especialidadeId) { q += ` WHERE m.especialidade_id = $1`; params.push(especialidadeId); }
q += ` GROUP BY m.id ORDER BY m.nome`;
const { rows } = await pool.query(q, params);
res.json(rows.map(r => ({
id: r.id, nome: r.nome,
especialidadeId: r.especialidade_id, especialidadeNome: r.especialidade_nome,
instrucoes: r.instrucoes, items: r.items || []
})));
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/modelos-receita', async (req, res) => {
try {
const { nome, especialidadeId, especialidadeNome, instrucoes, items = [] } = req.body;
const id = `mr_${Date.now()}`;
await pool.query(
`INSERT INTO modelos_receita (id, nome, especialidade_id, especialidade_nome, instrucoes) VALUES ($1,$2,$3,$4,$5)`,
[id, nome, especialidadeId || null, especialidadeNome || null, instrucoes || null]
);
for (let i = 0; i < items.length; i++) {
const it = items[i];
await pool.query(
`INSERT INTO items_receita (id, modelo_id, medicamento, dose, via, frequencia, duracao, instrucoes, ordem) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
[`ir_${Date.now()}_${i}`, id, it.medicamento, it.dose || '', it.via || 'VO', it.frequencia || '', it.duracao || '', it.instrucoes || null, i]
);
}
res.json({ success: true, id });
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.put('/api/modelos-receita/:id', async (req, res) => {
try {
const { nome, especialidadeId, especialidadeNome, instrucoes, items = [] } = req.body;
await pool.query(
`UPDATE modelos_receita SET nome=$1, especialidade_id=$2, especialidade_nome=$3, instrucoes=$4 WHERE id=$5`,
[nome, especialidadeId || null, especialidadeNome || null, instrucoes || null, req.params.id]
);
await pool.query(`DELETE FROM items_receita WHERE modelo_id = $1`, [req.params.id]);
for (let i = 0; i < items.length; i++) {
const it = items[i];
await pool.query(
`INSERT INTO items_receita (id, modelo_id, medicamento, dose, via, frequencia, duracao, instrucoes, ordem) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
[`ir_${Date.now()}_${i}`, req.params.id, it.medicamento, it.dose || '', it.via || 'VO', it.frequencia || '', it.duracao || '', it.instrucoes || null, i]
);
}
res.json({ success: true });
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.delete('/api/modelos-receita/:id', async (req, res) => {
try {
await pool.query(`DELETE FROM items_receita WHERE modelo_id = $1`, [req.params.id]);
await pool.query(`DELETE FROM modelos_receita WHERE id = $1`, [req.params.id]);
res.json({ success: true });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// --- PEDIDOS DE EXAME ---
app.get('/api/pedidos-exame', async (req, res) => {
try {
const { pacienteId } = req.query;
let q = `SELECT p.*, COALESCE(json_agg(i ORDER BY i.ordem) FILTER (WHERE i.id IS NOT NULL), '[]') AS items
FROM pedidos_exame p LEFT JOIN items_pedido_exame i ON i.pedido_id = p.id`;
const params = [];
if (pacienteId) { q += ` WHERE p.paciente_id = $1`; params.push(pacienteId); }
q += ` GROUP BY p.id ORDER BY p.created_at DESC LIMIT 50`;
const { rows } = await pool.query(q, params);
res.json(rows.map(r => ({
id: r.id, pacienteId: r.paciente_id, pacienteNome: r.paciente_nome,
profissionalId: r.profissional_id, profissionalNome: r.profissional_nome,
data: r.data, observacoes: r.observacoes, items: r.items || []
})));
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/pedidos-exame', async (req, res) => {
try {
const { pacienteId, pacienteNome, profissionalId, profissionalNome, data, observacoes, items = [] } = req.body;
const id = `pe_${Date.now()}`;
await pool.query(
`INSERT INTO pedidos_exame (id, paciente_id, paciente_nome, profissional_id, profissional_nome, data, observacoes) VALUES ($1,$2,$3,$4,$5,$6,$7)`,
[id, pacienteId || null, pacienteNome, profissionalId || null, profissionalNome || null, data, observacoes || null]
);
for (let i = 0; i < items.length; i++) {
const it = items[i];
await pool.query(
`INSERT INTO items_pedido_exame (id, pedido_id, nome, categoria, regiao, observacao, ordem) VALUES ($1,$2,$3,$4,$5,$6,$7)`,
[`ie_${Date.now()}_${i}`, id, it.nome, it.categoria, it.regiao || null, it.observacao || null, i]
);
}
res.json({ success: true, id });
} catch (err) { res.status(500).json({ error: err.message }); }
});
async function runMigrations() {
const migrations = [
`ALTER TABLE pacientes ADD COLUMN IF NOT EXISTS grupofamiliarid TEXT`,
`ALTER TABLE pacientes ADD COLUMN IF NOT EXISTS grupofamiliarrelacao TEXT`,
`ALTER TABLE pacientes ADD COLUMN IF NOT EXISTS indicadorporid TEXT`,
`ALTER TABLE pacientes ADD COLUMN IF NOT EXISTS observacoes TEXT`,
`ALTER TABLE pacientes ADD COLUMN IF NOT EXISTS logradouro TEXT`,
`ALTER TABLE pacientes ADD COLUMN IF NOT EXISTS numero TEXT`,
`ALTER TABLE pacientes ADD COLUMN IF NOT EXISTS complemento TEXT`,
`ALTER TABLE pacientes ADD COLUMN IF NOT EXISTS bairro TEXT`,
`ALTER TABLE pacientes ADD COLUMN IF NOT EXISTS cidade TEXT`,
`ALTER TABLE pacientes ADD COLUMN IF NOT EXISTS estado TEXT`,
`ALTER TABLE pacientes ADD COLUMN IF NOT EXISTS cep TEXT`,
`CREATE TABLE IF NOT EXISTS modelos_receita (
id TEXT PRIMARY KEY,
nome TEXT NOT NULL,
especialidade_id TEXT,
especialidade_nome TEXT,
instrucoes TEXT,
clinica_id TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
)`,
`CREATE TABLE IF NOT EXISTS items_receita (
id TEXT PRIMARY KEY,
modelo_id TEXT NOT NULL,
medicamento TEXT NOT NULL,
dose TEXT NOT NULL DEFAULT '',
via TEXT NOT NULL DEFAULT 'VO',
frequencia TEXT NOT NULL DEFAULT '',
duracao TEXT NOT NULL DEFAULT '',
instrucoes TEXT,
ordem INTEGER DEFAULT 0
)`,
`CREATE TABLE IF NOT EXISTS pedidos_exame (
id TEXT PRIMARY KEY,
paciente_id TEXT,
paciente_nome TEXT,
profissional_id TEXT,
profissional_nome TEXT,
data TEXT NOT NULL,
observacoes TEXT,
clinica_id TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
)`,
`CREATE TABLE IF NOT EXISTS items_pedido_exame (
id TEXT PRIMARY KEY,
pedido_id TEXT NOT NULL,
nome TEXT NOT NULL,
categoria TEXT NOT NULL,
regiao TEXT,
observacao TEXT,
ordem INTEGER DEFAULT 0
)`,
];
for (const sql of migrations) {
try { await pool.query(sql); } catch (e) { console.error('[MIGRATION]', e.message); }
}
console.log('[MIGRATION] pacientes columns OK');
console.log('[MIGRATION] all migrations OK');
}
// Start server after all routes are registered
+1 -1
View File
@@ -1 +1 @@
export const APP_VERSION = 'V1.0.4';
export const APP_VERSION = 'V1.0.5';
+39
View File
@@ -260,6 +260,45 @@ export const HybridBackend = {
return res.json();
},
getModelosReceita: async (especialidadeId?: string): Promise<import('../types.ts').ModeloReceita[]> => {
const url = especialidadeId ? `${API_URL}/modelos-receita?especialidadeId=${especialidadeId}` : `${API_URL}/modelos-receita`;
const res = await apiFetch(url);
const data = await res.json();
return Array.isArray(data) ? data : [];
},
saveModeloReceita: async (modelo: Omit<import('../types.ts').ModeloReceita, 'id'>) => {
const res = await apiFetch(`${API_URL}/modelos-receita`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(modelo)
});
return res.json();
},
updateModeloReceita: async (modelo: import('../types.ts').ModeloReceita) => {
const res = await apiFetch(`${API_URL}/modelos-receita/${modelo.id}`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(modelo)
});
return res.json();
},
deleteModeloReceita: async (id: string) => {
await apiFetch(`${API_URL}/modelos-receita/${id}`, { method: 'DELETE' });
},
savePedidoExame: async (pedido: Omit<import('../types.ts').PedidoExame, 'id'>) => {
const res = await apiFetch(`${API_URL}/pedidos-exame`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(pedido)
});
return res.json();
},
getPedidosExame: async (pacienteId?: string): Promise<import('../types.ts').PedidoExame[]> => {
const url = pacienteId ? `${API_URL}/pedidos-exame?pacienteId=${pacienteId}` : `${API_URL}/pedidos-exame`;
const res = await apiFetch(url);
const data = await res.json();
return Array.isArray(data) ? data : [];
},
getDentistas: async (clinicaId?: string): Promise<Dentista[]> => {
const url = clinicaId ? `${API_URL}/dentistas?clinicaId=${clinicaId}` : `${API_URL}/dentistas`;
const res = await apiFetch(url);
+50
View File
@@ -20,6 +20,56 @@ export interface Paciente {
grupoFamiliarRelacao?: string;
indicadoPorId?: string;
indicadoPorNome?: string;
// endereço
logradouro?: string;
numero?: string;
complemento?: string;
bairro?: string;
cidade?: string;
estado?: string;
cep?: string;
// clínico
observacoes?: string;
}
export interface ItemReceita {
id: string;
medicamento: string;
dose: string;
via: string;
frequencia: string;
duracao: string;
instrucoes?: string;
ordem: number;
}
export interface ModeloReceita {
id: string;
nome: string;
especialidadeId?: string;
especialidadeNome?: string;
instrucoes?: string;
items: ItemReceita[];
}
export interface ItemExame {
id: string;
nome: string;
categoria: string;
regiao?: string;
observacao?: string;
ordem: number;
}
export interface PedidoExame {
id: string;
pacienteId?: string;
pacienteNome: string;
profissionalId?: string;
profissionalNome?: string;
data: string;
observacoes?: string;
items: ItemExame[];
}
export interface Dentista {
+249 -106
View File
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Search, Plus, DollarSign, X, Printer, CreditCard, Rocket, Loader2, FolderOpen, CalendarDays, RadioTower, Pencil, Wallet, PlusCircle, Banknote, History, FileText, Calendar, Copy, CheckCircle2, Users, UserCheck } from 'lucide-react';
import { Search, Plus, DollarSign, X, Printer, CreditCard, Rocket, Loader2, FolderOpen, CalendarDays, RadioTower, Pencil, Wallet, PlusCircle, Banknote, History, FileText, Calendar, Copy, CheckCircle2, Users, UserCheck, ChevronLeft, ChevronRight, Save } from 'lucide-react';
import { HybridBackend } from '../services/backend.ts';
import { Paciente, Dentista, Agendamento, Plano } from '../types.ts';
import { useHybridBackend } from '../hooks/useHybridBackend.ts';
@@ -464,10 +464,15 @@ const HistoricoModal: React.FC<{ patient: Paciente; onClose: () => void }> = ({
);
};
// ----------- EDITAR PACIENTE MODAL -----------
// ----------- EDITAR PACIENTE MODAL (4 páginas) -----------
const ESTADOS_BR = ['AC','AL','AP','AM','BA','CE','DF','ES','GO','MA','MT','MS','MG','PA','PB','PR','PE','PI','RJ','RN','RS','RO','RR','SC','SP','SE','TO'];
const EDIT_STEPS = ['Dados', 'Família', 'Endereço', 'Histórico'];
const EditarPacienteModal: React.FC<{ patient: Paciente; onClose: () => void; onSaved: () => void }> = ({ patient, onClose, onSaved }) => {
const toast = useToast();
const { data: planos } = useHybridBackend<Plano[]>(HybridBackend.getPlanos);
const { data: agendamentos } = useHybridBackend<Agendamento[]>(HybridBackend.getAgendamentos);
const [page, setPage] = useState(0);
const [form, setForm] = useState({ ...patient, dataNascimento: toInputDate(patient.dataNascimento) });
const [familiar, setFamiliar] = useState<{ id: string; nome: string } | null>(
patient.grupoFamiliarId ? { id: patient.grupoFamiliarId, nome: patient.grupoFamiliarNome || '' } : null
@@ -477,14 +482,15 @@ const EditarPacienteModal: React.FC<{ patient: Paciente; onClose: () => void; on
patient.indicadoPorId ? { id: patient.indicadoPorId, nome: patient.indicadoPorNome || '' } : null
);
const meusAgendamentos = (agendamentos ?? []).filter(a => a.pacienteNome === patient.nome);
useEffect(() => {
const h = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', h);
return () => document.removeEventListener('keydown', h);
}, [onClose]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const handleSubmit = async () => {
try {
const { grupoFamiliarNome: _gn, indicadoPorNome: _in, ...formData } = form;
await HybridBackend.updatePaciente({
@@ -498,106 +504,252 @@ const EditarPacienteModal: React.FC<{ patient: Paciente; onClose: () => void; on
} catch { toast.error('ERRO AO SALVAR.'); }
};
const input = "w-full border border-gray-200 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-100 focus:border-blue-300 outline-none";
const inp = "w-full border border-gray-200 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-100 focus:border-blue-300 outline-none";
return (
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center backdrop-blur-sm p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg animate-in fade-in zoom-in-50 duration-200">
<div className="p-5 border-b border-gray-100 flex justify-between items-center">
<h2 className="text-sm font-black text-gray-800 uppercase flex items-center gap-2">
<Pencil size={15} className="text-blue-500" /> Editar Paciente
</h2>
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl flex flex-col max-h-[90vh] animate-in fade-in zoom-in-50 duration-200">
{/* Header */}
<div className="p-5 border-b border-gray-100 flex justify-between items-center flex-shrink-0">
<div className="flex items-center gap-2">
<Pencil size={15} className="text-blue-500" />
<div>
<h2 className="text-sm font-black text-gray-800 uppercase">Editar Paciente</h2>
<p className="text-[10px] text-gray-400 font-medium uppercase">{patient.nome}</p>
</div>
</div>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={18} /></button>
</div>
<form onSubmit={handleSubmit} className="p-5 space-y-4 max-h-[80vh] overflow-y-auto">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Nome Completo</label>
<input required value={form.nome} onChange={e => setForm(f => ({ ...f, nome: e.target.value.toUpperCase() }))} className={input} />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">CPF</label>
<input value={form.cpf} onChange={e => setForm(f => ({ ...f, cpf: e.target.value }))} className={input} />
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Telefone</label>
<input value={form.telefone} onChange={e => setForm(f => ({ ...f, telefone: e.target.value }))} className={input} />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Nascimento</label>
<input type="date" value={form.dataNascimento} onChange={e => setForm(f => ({ ...f, dataNascimento: e.target.value }))} className={input} />
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Convênio</label>
<select value={form.convenio || ''} onChange={e => setForm(f => ({ ...f, convenio: e.target.value }))} className={input}>
<option value=""> Particular </option>
{planos?.map(p => <option key={p.id} value={p.nome}>{p.nome}</option>)}
</select>
</div>
</div>
<div className="border-t border-gray-100 pt-3 space-y-3">
{/* Familiar */}
<AutocompleteField<Paciente>
label="Familiar (vínculo)"
placeholder="Buscar paciente familiar..."
value={familiar}
onSelect={p => { setFamiliar({ id: p.id, nome: p.nome }); }}
onClear={() => { setFamiliar(null); setFamiliarRelacao(''); }}
fetchFn={q => HybridBackend.getPacientes(q).then(r => r.data)}
renderOption={p => (
<span className="flex items-center gap-2 w-full">
<Users size={12} className="text-indigo-400 flex-shrink-0" />
<span className="font-bold flex-1">{p.nome}</span>
<span className="text-[10px] text-gray-400">{p.cpf}</span>
</span>
)}
/>
{familiar && (
{/* Step indicator */}
<div className="px-6 pt-4 pb-0 flex-shrink-0">
<div className="flex items-center">
{EDIT_STEPS.map((s, i) => (
<React.Fragment key={i}>
<button onClick={() => setPage(i)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] font-black uppercase transition-all whitespace-nowrap ${
page === i ? 'bg-blue-600 text-white' : i < page ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-400'
}`}>
<span className="w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-black">{i < page ? '✓' : i + 1}</span>
{s}
</button>
{i < EDIT_STEPS.length - 1 && (
<div className={`flex-1 h-0.5 mx-1 ${i < page ? 'bg-green-300' : 'bg-gray-200'}`} />
)}
</React.Fragment>
))}
</div>
</div>
{/* Page content */}
<div className="flex-1 overflow-y-auto px-6 py-5 space-y-4">
{/* Página 1: Dados básicos */}
{page === 0 && (<>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Nome Completo</label>
<input required value={form.nome} onChange={e => setForm(f => ({ ...f, nome: e.target.value.toUpperCase() }))} className={inp} />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Parentesco</label>
<input
list="relacoes-list-editar"
value={familiarRelacao}
onChange={e => setFamiliarRelacao(e.target.value.toUpperCase())}
placeholder="EX: MÃE, PAI, IRMÃO..."
className={input}
/>
<datalist id="relacoes-list-editar">
{RELACOES_COMUNS.map(r => <option key={r} value={r} />)}
</datalist>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">CPF</label>
<input value={form.cpf} onChange={e => setForm(f => ({ ...f, cpf: e.target.value }))} className={inp} />
</div>
)}
<AutocompleteField<Paciente>
label="Indicado por"
placeholder="Buscar paciente que indicou..."
value={indicadoPor}
onSelect={p => setIndicadoPor({ id: p.id, nome: p.nome })}
onClear={() => setIndicadoPor(null)}
fetchFn={q => HybridBackend.getPacientes(q).then(r => r.data)}
renderOption={p => (
<span className="flex items-center gap-2 w-full">
<UserCheck size={12} className="text-green-400 flex-shrink-0" />
<span className="font-bold flex-1">{p.nome}</span>
<span className="text-[10px] text-gray-400">{p.cpf}</span>
</span>
)}
/>
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Telefone</label>
<input value={form.telefone} onChange={e => setForm(f => ({ ...f, telefone: e.target.value }))} className={inp} />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Nascimento</label>
<input type="date" value={form.dataNascimento} onChange={e => setForm(f => ({ ...f, dataNascimento: e.target.value }))} className={inp} />
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Convênio</label>
<select value={form.convenio || ''} onChange={e => setForm(f => ({ ...f, convenio: e.target.value }))} className={inp}>
<option value=""> Particular </option>
{planos?.map(p => <option key={p.id} value={p.nome}>{p.nome}</option>)}
</select>
</div>
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">E-mail</label>
<input type="email" value={form.email || ''} onChange={e => setForm(f => ({ ...f, email: e.target.value }))} className={inp} />
</div>
</>)}
<div className="flex justify-end gap-2 pt-2">
<button type="button" onClick={onClose} className="px-5 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-lg text-xs uppercase">Cancelar</button>
<button type="submit" className="px-5 py-2 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-lg text-xs uppercase">Salvar</button>
</div>
</form>
{/* Página 2: Família e indicação */}
{page === 1 && (
<div className="space-y-4">
<AutocompleteField<Paciente>
label="Familiar (vínculo)"
placeholder="Buscar paciente familiar..."
value={familiar}
onSelect={p => { setFamiliar({ id: p.id, nome: p.nome }); }}
onClear={() => { setFamiliar(null); setFamiliarRelacao(''); }}
fetchFn={q => HybridBackend.getPacientes(q).then(r => r.data)}
renderOption={p => (
<span className="flex items-center gap-2 w-full">
<Users size={12} className="text-indigo-400 flex-shrink-0" />
<span className="font-bold flex-1">{p.nome}</span>
<span className="text-[10px] text-gray-400">{p.cpf}</span>
</span>
)}
/>
{familiar && (
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Parentesco</label>
<input
list="relacoes-list-editar"
value={familiarRelacao}
onChange={e => setFamiliarRelacao(e.target.value.toUpperCase())}
placeholder="EX: MÃE, PAI, IRMÃO..."
className={inp}
/>
<datalist id="relacoes-list-editar">
{RELACOES_COMUNS.map(r => <option key={r} value={r} />)}
</datalist>
</div>
)}
<AutocompleteField<Paciente>
label="Indicado por"
placeholder="Buscar paciente que indicou..."
value={indicadoPor}
onSelect={p => setIndicadoPor({ id: p.id, nome: p.nome })}
onClear={() => setIndicadoPor(null)}
fetchFn={q => HybridBackend.getPacientes(q).then(r => r.data)}
renderOption={p => (
<span className="flex items-center gap-2 w-full">
<UserCheck size={12} className="text-green-400 flex-shrink-0" />
<span className="font-bold flex-1">{p.nome}</span>
<span className="text-[10px] text-gray-400">{p.cpf}</span>
</span>
)}
/>
</div>
)}
{/* Página 3: Endereço */}
{page === 2 && (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">CEP</label>
<input value={form.cep || ''} onChange={e => setForm(f => ({ ...f, cep: e.target.value }))} className={inp} placeholder="00000-000" maxLength={9} />
</div>
<div className="col-span-2">
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Logradouro</label>
<input value={form.logradouro || ''} onChange={e => setForm(f => ({ ...f, logradouro: e.target.value.toUpperCase() }))} className={inp} placeholder="RUA, AV, TRAVESSA..." />
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Número</label>
<input value={form.numero || ''} onChange={e => setForm(f => ({ ...f, numero: e.target.value }))} className={inp} placeholder="123" />
</div>
<div className="col-span-2">
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Complemento</label>
<input value={form.complemento || ''} onChange={e => setForm(f => ({ ...f, complemento: e.target.value.toUpperCase() }))} className={inp} placeholder="APTO, BLOCO..." />
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="col-span-2">
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Bairro</label>
<input value={form.bairro || ''} onChange={e => setForm(f => ({ ...f, bairro: e.target.value.toUpperCase() }))} className={inp} placeholder="BAIRRO" />
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">UF</label>
<select value={form.estado || ''} onChange={e => setForm(f => ({ ...f, estado: e.target.value }))} className={inp}>
<option value=""></option>
{ESTADOS_BR.map(uf => <option key={uf}>{uf}</option>)}
</select>
</div>
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Cidade</label>
<input value={form.cidade || ''} onChange={e => setForm(f => ({ ...f, cidade: e.target.value.toUpperCase() }))} className={inp} placeholder="CIDADE" />
</div>
</div>
)}
{/* Página 4: Histórico e observações */}
{page === 3 && (
<div className="space-y-4">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Observações Clínicas</label>
<textarea
rows={4}
value={form.observacoes || ''}
onChange={e => setForm(f => ({ ...f, observacoes: e.target.value }))}
className={inp + " resize-none"}
placeholder="ALERGIAS, HISTÓRICO MÉDICO, OBSERVAÇÕES..."
/>
</div>
<div>
<p className="text-[10px] font-black text-gray-500 uppercase tracking-wider mb-2">
Histórico de Atendimentos <span className="text-blue-600">({meusAgendamentos.length})</span>
</p>
{meusAgendamentos.length === 0 && (
<p className="text-xs text-gray-400 text-center py-4 border-2 border-dashed border-gray-100 rounded-xl">Nenhum atendimento registrado.</p>
)}
<div className="space-y-1.5 max-h-52 overflow-y-auto">
{meusAgendamentos.map(a => (
<div key={a.id} className="flex items-center justify-between border border-gray-100 rounded-lg p-2.5 bg-gray-50/70">
<div>
<p className="font-bold text-gray-800 text-xs uppercase">{a.procedimento}</p>
<p className="text-[10px] text-gray-400">{new Date(a.start).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' })}</p>
</div>
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase ${
a.status === 'Confirmado' ? 'bg-green-100 text-green-700' :
a.status === 'Pendente' ? 'bg-amber-100 text-amber-700' :
a.status === 'Faltou' ? 'bg-red-100 text-red-600' :
'bg-gray-100 text-gray-600'
}`}>{a.status}</span>
</div>
))}
</div>
</div>
</div>
)}
</div>
{/* Footer navigation */}
<div className="p-4 border-t border-gray-100 flex justify-between items-center flex-shrink-0">
<button
type="button"
onClick={() => page > 0 ? setPage(p => p - 1) : onClose()}
className="px-5 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-lg text-xs uppercase flex items-center gap-1.5"
>
<ChevronLeft size={14} /> {page === 0 ? 'Cancelar' : 'Voltar'}
</button>
{page < EDIT_STEPS.length - 1 ? (
<button
type="button"
onClick={() => setPage(p => p + 1)}
className="px-5 py-2 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-lg text-xs uppercase flex items-center gap-1.5"
>
Próximo <ChevronRight size={14} />
</button>
) : (
<button
type="button"
onClick={handleSubmit}
className="px-5 py-2 bg-green-600 hover:bg-green-700 text-white font-bold rounded-lg text-xs uppercase flex items-center gap-1.5"
>
<Save size={14} /> Salvar Tudo
</button>
)}
</div>
</div>
</div>
);
};
import { PageHeader } from '../components/PageHeader.tsx';
import { ReceitaModal } from './ReceitaModal.tsx';
import { PedidoExameModal } from './PedidoExameModal.tsx';
const CopyField: React.FC<{ label: string; value?: string | null }> = ({ label, value }) => {
const [copied, setCopied] = useState(false);
@@ -679,11 +831,6 @@ export const PatientsView: React.FC = () => {
const saldoAParcelar = valorTotalNum - descontoNum - entradaNum;
const valorParcela = saldoAParcelar > 0 && asaasForm.parcelas > 0 ? saldoAParcelar / Number(asaasForm.parcelas) : 0;
const handleDocGeneration = () => {
toast.success(`DOCUMENTO GERADO E SALVO NO DRIVE PARA ${modal.patient?.nome}!`);
closeModal();
};
const handleAsaasSubmit = (e: React.FormEvent) => {
e.preventDefault();
toast.success(`COBRANÇA GERADA NO ASAAS PARA ${modal.patient?.nome}!`);
@@ -820,18 +967,14 @@ export const PatientsView: React.FC = () => {
<AgendamentoModal patient={modal.patient} onClose={closeModal} />
)}
{/* MODAL DOCUMENTOS */}
{(modal.type === 'doc-receita' || modal.type === 'doc-raiox') && (
<div className="fixed inset-0 bg-black bg-opacity-40 z-50 flex items-center justify-center backdrop-blur-sm">
<div className="bg-white rounded-xl shadow-2xl p-6 max-w-md w-full animate-in fade-in zoom-in-50 duration-200">
<h3 className="text-xl font-bold mb-4 uppercase">GERAR {modal.type === 'doc-receita' ? 'RECEITA' : 'PEDIDO DE RAIO-X'}</h3>
<p className="text-gray-600 mb-4 text-sm font-bold uppercase">O SISTEMA IRÁ GERAR UM DOCUMENTO NO GOOGLE DOCS USANDO O MODELO PADRÃO.</p>
<div className="flex justify-end gap-3">
<button onClick={closeModal} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg text-xs font-bold uppercase">CANCELAR</button>
<button onClick={handleDocGeneration} className="px-4 py-2 bg-gray-800 text-white rounded-lg flex items-center gap-2 text-xs font-bold uppercase"><Printer size={16} /> GERAR PDF</button>
</div>
</div>
</div>
{/* RECEITA MODAL */}
{modal.type === 'doc-receita' && modal.patient && (
<ReceitaModal paciente={modal.patient} onClose={closeModal} />
)}
{/* PEDIDO DE EXAME MODAL */}
{modal.type === 'doc-raiox' && modal.patient && (
<PedidoExameModal paciente={modal.patient} onClose={closeModal} />
)}
{/* MODAL MENU FINANCEIRO */}
+411
View File
@@ -0,0 +1,411 @@
import React, { useState, useEffect } from 'react';
import { X, Plus, GripVertical, Trash2, Printer, ChevronLeft, Search, Microscope, Activity, Image, FileX } from 'lucide-react';
import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd';
import { HybridBackend } from '../services/backend.ts';
import { PedidoExame, ItemExame, Paciente, Dentista } from '../types.ts';
import { useHybridBackend } from '../hooks/useHybridBackend.ts';
import { useToast } from '../contexts/ToastContext.tsx';
const CATALOG: { categoria: string; icon: React.ReactNode; cor: string; items: { nome: string; regiao?: string }[] }[] = [
{
categoria: 'RAIO-X',
icon: <Image size={14} />,
cor: 'blue',
items: [
{ nome: 'Periapical' },
{ nome: 'Interproximal (Bite-Wing)' },
{ nome: 'Panorâmica' },
{ nome: 'Oclusal Superior' },
{ nome: 'Oclusal Inferior' },
{ nome: 'Cefalométrica Lateral' },
{ nome: 'P.A. de Face' },
],
},
{
categoria: 'TOMOGRAFIA',
icon: <Activity size={14} />,
cor: 'purple',
items: [
{ nome: 'Cone Beam — Dente Único', regiao: '' },
{ nome: 'Cone Beam — Terço Anterior' },
{ nome: 'Cone Beam — Arcada Completa Superior' },
{ nome: 'Cone Beam — Arcada Completa Inferior' },
{ nome: 'Cone Beam — Ambas Arcadas' },
{ nome: 'ATM (Articulação Temporomandibular)' },
],
},
{
categoria: 'OUTROS',
icon: <Microscope size={14} />,
cor: 'green',
items: [
{ nome: 'Cefalometria Analítica' },
{ nome: 'Fotografia Clínica Extraoral' },
{ nome: 'Fotografia Clínica Intraoral' },
{ nome: 'Modelos de Gesso / Escaneamento' },
{ nome: 'Biópsia' },
{ nome: 'Exame Laboratorial' },
],
},
];
const COR: Record<string, { bg: string; text: string; border: string; chip: string }> = {
blue: { bg: 'bg-blue-50', text: 'text-blue-700', border: 'border-blue-200', chip: 'bg-blue-100 text-blue-700' },
purple: { bg: 'bg-purple-50', text: 'text-purple-700', border: 'border-purple-200', chip: 'bg-purple-100 text-purple-700' },
green: { bg: 'bg-green-50', text: 'text-green-700', border: 'border-green-200', chip: 'bg-green-100 text-green-700' },
};
// --------------- Print ---------------
const PrintView: React.FC<{ pedido: PedidoExame; dentista?: Dentista; onClose: () => void }> = ({ pedido, dentista, onClose }) => {
const hoje = new Date().toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' });
const grupos = CATALOG.map(c => ({ ...c, selecionados: pedido.items.filter(it => it.categoria === c.categoria) })).filter(c => c.selecionados.length > 0);
return (
<div className="flex flex-col h-full">
<div className="p-4 border-b border-gray-100 flex items-center justify-between print:hidden">
<div className="flex items-center gap-2">
<button onClick={onClose} className="text-gray-400 hover:text-gray-700"><ChevronLeft size={18} /></button>
<h3 className="text-sm font-black text-gray-800 uppercase">Pré-visualização</h3>
</div>
<button onClick={() => window.print()} className="flex items-center gap-2 px-4 py-2 bg-gray-900 text-white rounded-lg text-xs font-bold hover:bg-black">
<Printer size={14} /> Imprimir / Salvar PDF
</button>
</div>
<div className="flex-1 overflow-y-auto bg-gray-100 p-6 print:p-0 print:bg-white">
<div className="max-w-xl mx-auto bg-white rounded-2xl shadow-sm border border-gray-200 p-8 print:shadow-none print:rounded-none print:border-0 print:max-w-full">
<div className="text-center border-b-2 border-gray-800 pb-4 mb-6">
<h1 className="text-lg font-black text-gray-900 uppercase">PEDIDO DE EXAMES</h1>
{dentista && <p className="text-sm font-bold text-gray-700 mt-1">{dentista.nome} CRO: {dentista.cro || '—'}/{dentista.cro_uf || '—'}</p>}
</div>
<div className="mb-6 grid grid-cols-2 gap-3 text-sm">
<div><span className="text-gray-500 text-xs uppercase font-bold">Paciente</span><p className="font-bold text-gray-900 uppercase">{pedido.pacienteNome}</p></div>
<div><span className="text-gray-500 text-xs uppercase font-bold">Data</span><p className="font-bold text-gray-900">{hoje}</p></div>
</div>
<div className="space-y-5">
{grupos.map(g => (
<div key={g.categoria}>
<h3 className="text-xs font-black text-gray-500 uppercase tracking-widest mb-2 border-b border-gray-100 pb-1">{g.categoria}</h3>
<ul className="space-y-1.5">
{g.selecionados.map((it, i) => (
<li key={it.id} className="flex items-start gap-2 text-sm text-gray-800">
<span className="font-black text-gray-400 w-5 flex-shrink-0">{i + 1}.</span>
<div>
<span className="font-bold">{it.nome}</span>
{it.regiao && <span className="text-gray-500"> Região: {it.regiao}</span>}
{it.observacao && <p className="text-xs text-gray-500 italic mt-0.5">{it.observacao}</p>}
</div>
</li>
))}
</ul>
</div>
))}
</div>
{pedido.observacoes && (
<div className="mt-5 p-3 bg-amber-50 border border-amber-200 rounded-lg text-xs text-amber-800 italic">
<span className="font-black not-italic uppercase">Obs: </span>{pedido.observacoes}
</div>
)}
<div className="mt-12 pt-6 border-t-2 border-gray-800 flex justify-between items-end">
<div><p className="text-xs text-gray-500">Assinatura e Carimbo</p><div className="h-10" /></div>
<p className="text-xs text-gray-400">{hoje}</p>
</div>
</div>
</div>
</div>
);
};
// --------------- Detail chip for exam item ---------------
const ItemChip: React.FC<{
item: ItemExame;
onChange: (v: Partial<ItemExame>) => void;
onRemove: () => void;
dragHandle: React.ReactNode;
isDragging: boolean;
}> = ({ item, onChange, onRemove, dragHandle, isDragging }) => {
const [expanded, setExpanded] = useState(false);
const cat = CATALOG.find(c => c.categoria === item.categoria);
const cor = COR[cat?.cor || 'blue'];
return (
<div className={`rounded-xl border transition-all ${isDragging ? 'shadow-lg' : ''} ${cor.border} bg-white`}>
<div className="flex items-center gap-2 px-3 py-2.5">
<span className="text-gray-300 hover:text-gray-500 cursor-grab flex-shrink-0">{dragHandle}</span>
<span className={`text-[9px] font-black px-1.5 py-0.5 rounded-full flex-shrink-0 ${cor.chip}`}>{item.categoria}</span>
<p className="flex-1 text-xs font-bold text-gray-800 truncate">{item.nome}</p>
<button onClick={() => setExpanded(e => !e)} className={`text-[9px] font-bold px-2 py-0.5 rounded-full border flex-shrink-0 ${expanded ? 'bg-gray-100 text-gray-600 border-gray-200' : 'text-gray-400 border-gray-200 hover:bg-gray-50'}`}>
{expanded ? 'fechar' : 'detalhe'}
</button>
<button onClick={onRemove} className="text-gray-300 hover:text-red-500 flex-shrink-0"><Trash2 size={13} /></button>
</div>
{expanded && (
<div className="px-3 pb-3 pt-0 grid grid-cols-2 gap-2">
<div>
<label className="text-[9px] font-black text-gray-400 uppercase">Região / Dente</label>
<input value={item.regiao || ''} onChange={e => onChange({ regiao: e.target.value })}
placeholder="ex: dente 16, arcada inf."
className="w-full border border-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:ring-2 focus:ring-blue-100 outline-none" />
</div>
<div>
<label className="text-[9px] font-black text-gray-400 uppercase">Observação</label>
<input value={item.observacao || ''} onChange={e => onChange({ observacao: e.target.value })}
placeholder="ex: urgente, pós-cirúrgico"
className="w-full border border-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:ring-2 focus:ring-blue-100 outline-none" />
</div>
</div>
)}
</div>
);
};
// --------------- Modal principal ---------------
export const PedidoExameModal: React.FC<{ paciente: Paciente; onClose: () => void }> = ({ paciente, onClose }) => {
const toast = useToast();
const { data: dentistas = [] } = useHybridBackend<Dentista[]>(HybridBackend.getDentistas);
const [items, setItems] = useState<ItemExame[]>([]);
const [observacoes, setObservacoes] = useState('');
const [busca, setBusca] = useState('');
const [catAtiva, setCatAtiva] = useState<string | null>(null);
const [view, setView] = useState<'editor' | 'print'>('editor');
const [saving, setSaving] = useState(false);
const dentistaPrincipal = dentistas[0];
useEffect(() => {
const h = (e: KeyboardEvent) => { if (e.key === 'Escape') { if (view !== 'editor') setView('editor'); else onClose(); } };
document.addEventListener('keydown', h);
return () => document.removeEventListener('keydown', h);
}, [view, onClose]);
const catalogFiltrado = CATALOG.map(c => ({
...c,
items: c.items.filter(it => {
if (catAtiva && c.categoria !== catAtiva) return false;
if (busca && !it.nome.toLowerCase().includes(busca.toLowerCase())) return false;
return true;
}),
})).filter(c => c.items.length > 0);
const addItem = (catNome: string, itemNome: string, regiao?: string) => {
if (items.some(it => it.nome === itemNome && it.categoria === catNome)) {
toast.error('EXAME JÁ ADICIONADO');
return;
}
setItems(prev => [...prev, {
id: `ex_${Date.now()}_${Math.random()}`,
nome: itemNome,
categoria: catNome,
regiao: regiao || '',
observacao: '',
ordem: prev.length,
}]);
};
const updateItem = (id: string, patch: Partial<ItemExame>) => {
setItems(prev => prev.map(it => it.id === id ? { ...it, ...patch } : it));
};
const removeItem = (id: string) => setItems(prev => prev.filter(it => it.id !== id));
const onDragEnd = (result: DropResult) => {
if (!result.destination) return;
const list = [...items];
const [moved] = list.splice(result.source.index, 1);
list.splice(result.destination.index, 0, moved);
setItems(list.map((it, i) => ({ ...it, ordem: i })));
};
const handleGerarPedido = async () => {
if (items.length === 0) { toast.error('ADICIONE AO MENOS UM EXAME'); return; }
setSaving(true);
try {
await HybridBackend.savePedidoExame({
pacienteId: paciente.id,
pacienteNome: paciente.nome,
profissionalId: dentistaPrincipal?.id,
profissionalNome: dentistaPrincipal?.nome,
data: new Date().toISOString().slice(0, 10),
observacoes: observacoes || undefined,
items,
});
setView('print');
} catch { toast.error('ERRO AO SALVAR PEDIDO'); } finally { setSaving(false); }
};
const pedidoParaImprimir: PedidoExame = {
id: 'preview',
pacienteId: paciente.id,
pacienteNome: paciente.nome,
profissionalId: dentistaPrincipal?.id,
profissionalNome: dentistaPrincipal?.nome,
data: new Date().toISOString().slice(0, 10),
observacoes,
items,
};
if (view === 'print') {
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center backdrop-blur-sm p-3">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl h-[85vh] flex flex-col overflow-hidden">
<PrintView pedido={pedidoParaImprimir} dentista={dentistaPrincipal} onClose={() => setView('editor')} />
</div>
</div>
);
}
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center backdrop-blur-sm p-3">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-4xl h-[90vh] flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-200">
{/* Header */}
<div className="p-5 border-b border-gray-100 flex items-center justify-between flex-shrink-0">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-indigo-100 rounded-lg flex items-center justify-center"><FileX size={15} className="text-indigo-600" /></div>
<div>
<h2 className="text-sm font-black text-gray-800 uppercase">Pedido de Exames</h2>
<p className="text-[10px] text-gray-400 uppercase">{paciente.nome}</p>
</div>
</div>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={18} /></button>
</div>
{/* Body: catálogo + selecionados */}
<div className="flex flex-1 overflow-hidden">
{/* Catálogo (esquerda) */}
<div className="w-80 flex-shrink-0 border-r border-gray-100 flex flex-col">
<div className="p-3 border-b border-gray-100 space-y-2">
<div className="relative">
<Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400" />
<input value={busca} onChange={e => setBusca(e.target.value)} placeholder="Buscar exame..."
className="w-full pl-8 pr-3 py-2 border border-gray-200 rounded-lg text-xs focus:ring-2 focus:ring-blue-100 outline-none" />
</div>
<div className="flex gap-1 flex-wrap">
<button onClick={() => setCatAtiva(null)}
className={`text-[10px] font-bold px-2 py-0.5 rounded-full border transition-colors ${!catAtiva ? 'bg-gray-800 text-white border-gray-800' : 'text-gray-500 border-gray-200 hover:bg-gray-50'}`}>
Todos
</button>
{CATALOG.map(c => {
const cor = COR[c.cor];
return (
<button key={c.categoria} onClick={() => setCatAtiva(a => a === c.categoria ? null : c.categoria)}
className={`text-[10px] font-bold px-2 py-0.5 rounded-full border transition-colors ${catAtiva === c.categoria ? cor.chip + ' border-transparent' : 'text-gray-500 border-gray-200 hover:bg-gray-50'}`}>
{c.categoria}
</button>
);
})}
</div>
</div>
<div className="flex-1 overflow-y-auto p-3 space-y-4">
{catalogFiltrado.map(cat => {
const cor = COR[cat.cor];
return (
<div key={cat.categoria}>
<div className={`flex items-center gap-1.5 mb-2 ${cor.text}`}>
{cat.icon}
<span className="text-[10px] font-black uppercase tracking-wider">{cat.categoria}</span>
</div>
<div className="space-y-1">
{cat.items.map(it => {
const jaSelecionado = items.some(s => s.nome === it.nome && s.categoria === cat.categoria);
return (
<button key={it.nome}
onClick={() => addItem(cat.categoria, it.nome, it.regiao)}
disabled={jaSelecionado}
className={`w-full text-left px-3 py-2 rounded-lg text-xs transition-all flex items-center justify-between group ${
jaSelecionado
? `${cor.bg} ${cor.text} font-bold opacity-60 cursor-default`
: `border border-gray-100 hover:${cor.bg} hover:${cor.border} hover:${cor.text} text-gray-600 font-medium`
}`}>
<span>{it.nome}</span>
{jaSelecionado
? <span className="text-[9px] font-black opacity-70"></span>
: <Plus size={12} className="opacity-0 group-hover:opacity-100 flex-shrink-0" />}
</button>
);
})}
</div>
</div>
);
})}
{catalogFiltrado.length === 0 && (
<p className="text-xs text-gray-400 text-center py-6">Nenhum exame encontrado</p>
)}
</div>
</div>
{/* Selecionados (direita) */}
<div className="flex-1 flex flex-col overflow-hidden">
<div className="p-3 border-b border-gray-100 flex items-center justify-between">
<p className="text-[10px] font-black text-gray-500 uppercase tracking-wider">
Exames Selecionados {items.length > 0 && <span className="text-blue-600 ml-1">({items.length})</span>}
</p>
{items.length > 0 && (
<button onClick={() => setItems([])} className="text-[10px] text-red-500 hover:text-red-700 font-bold">Limpar tudo</button>
)}
</div>
<div className="flex-1 overflow-y-auto p-3">
{items.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center">
<div className="w-16 h-16 bg-gray-100 rounded-2xl flex items-center justify-center mb-3">
<FileX size={24} className="text-gray-300" />
</div>
<p className="text-sm font-bold text-gray-400">Nenhum exame selecionado</p>
<p className="text-xs text-gray-300 mt-1">Clique nos exames ao lado para adicionar</p>
</div>
) : (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="exames-selecionados">
{provided => (
<div ref={provided.innerRef} {...provided.droppableProps} className="space-y-2">
{items.map((it, idx) => (
<Draggable key={it.id} draggableId={it.id} index={idx}>
{(prov, snap) => (
<div ref={prov.innerRef} {...prov.draggableProps}>
<ItemChip
item={it}
isDragging={snap.isDragging}
dragHandle={<span {...prov.dragHandleProps}><GripVertical size={15} /></span>}
onChange={patch => updateItem(it.id, patch)}
onRemove={() => removeItem(it.id)}
/>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
)}
</div>
{/* Observações + ação */}
<div className="p-3 border-t border-gray-100 space-y-3 flex-shrink-0">
<div>
<label className="block text-[9px] font-black text-gray-400 uppercase tracking-wider mb-1">Observações (opcional)</label>
<input value={observacoes} onChange={e => setObservacoes(e.target.value)}
placeholder="ex: urgente, pós-cirúrgico, encaminhar para..."
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-xs focus:ring-2 focus:ring-blue-100 outline-none" />
</div>
<div className="flex gap-2 justify-end">
<button onClick={onClose} className="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-lg text-xs uppercase">Cancelar</button>
<button
onClick={handleGerarPedido}
disabled={items.length === 0 || saving}
className="flex items-center gap-2 px-5 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-bold rounded-lg text-xs uppercase disabled:opacity-40">
<Printer size={14} /> {saving ? 'Salvando...' : 'Gerar Pedido'}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
+437
View File
@@ -0,0 +1,437 @@
import React, { useState, useEffect, useCallback } from 'react';
import { X, Plus, GripVertical, Trash2, Pencil, FileText, ChevronLeft, Printer, Save, Search, Tag, ClipboardList } from 'lucide-react';
import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd';
import { HybridBackend } from '../services/backend.ts';
import { ModeloReceita, ItemReceita, Especialidade, Paciente, Dentista } from '../types.ts';
import { useHybridBackend } from '../hooks/useHybridBackend.ts';
import { useToast } from '../contexts/ToastContext.tsx';
const VIAS = ['VO', 'SL', 'IV', 'IM', 'SC', 'Tópico', 'Inalatória', 'Colírio', 'Nasal'];
const FREQUENCIAS = ['1x/dia', '2x/dia', '3x/dia', '4x/dia', '6/6h', '8/8h', '12/12h', '24/24h', 'Dose única', 'Se necessário'];
const DURACOES = ['1 dia', '2 dias', '3 dias', '5 dias', '7 dias', '10 dias', '14 dias', '21 dias', '30 dias', 'Contínuo'];
const ITEM_VAZIO: Omit<ItemReceita, 'id' | 'ordem'> = { medicamento: '', dose: '', via: 'VO', frequencia: '8/8h', duracao: '7 dias', instrucoes: '' };
// --------------- Editor de item de medicamento ---------------
const ItemEditor: React.FC<{
item: Omit<ItemReceita, 'id' | 'ordem'>;
onChange: (v: Omit<ItemReceita, 'id' | 'ordem'>) => void;
onSave: () => void;
onCancel: () => void;
}> = ({ item, onChange, onSave, onCancel }) => {
const inp = "w-full border border-gray-200 rounded-lg px-3 py-2 text-xs focus:ring-2 focus:ring-blue-100 focus:border-blue-300 outline-none bg-white";
const sel = inp + " appearance-none";
return (
<div className="bg-blue-50 border border-blue-100 rounded-xl p-3 space-y-2">
<div>
<label className="text-[9px] font-black text-gray-500 uppercase tracking-wider">Medicamento</label>
<input autoFocus value={item.medicamento} onChange={e => onChange({ ...item, medicamento: e.target.value.toUpperCase() })}
placeholder="EX: AMOXICILINA 500MG" className={inp} />
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-[9px] font-black text-gray-500 uppercase tracking-wider">Dose</label>
<input value={item.dose} onChange={e => onChange({ ...item, dose: e.target.value })} placeholder="EX: 500mg" className={inp} />
</div>
<div>
<label className="text-[9px] font-black text-gray-500 uppercase tracking-wider">Via</label>
<select value={item.via} onChange={e => onChange({ ...item, via: e.target.value })} className={sel}>
{VIAS.map(v => <option key={v}>{v}</option>)}
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-[9px] font-black text-gray-500 uppercase tracking-wider">Frequência</label>
<select value={item.frequencia} onChange={e => onChange({ ...item, frequencia: e.target.value })} className={sel}>
{FREQUENCIAS.map(f => <option key={f}>{f}</option>)}
</select>
</div>
<div>
<label className="text-[9px] font-black text-gray-500 uppercase tracking-wider">Duração</label>
<select value={item.duracao} onChange={e => onChange({ ...item, duracao: e.target.value })} className={sel}>
{DURACOES.map(d => <option key={d}>{d}</option>)}
</select>
</div>
</div>
<div>
<label className="text-[9px] font-black text-gray-500 uppercase tracking-wider">Instruções extras (opcional)</label>
<input value={item.instrucoes || ''} onChange={e => onChange({ ...item, instrucoes: e.target.value })}
placeholder="EX: TOMAR APÓS AS REFEIÇÕES" className={inp} />
</div>
<div className="flex justify-end gap-2 pt-1">
<button onClick={onCancel} className="px-3 py-1.5 bg-white border border-gray-200 text-gray-600 rounded-lg text-xs font-bold hover:bg-gray-50">Cancelar</button>
<button onClick={onSave} disabled={!item.medicamento.trim()} className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-xs font-bold hover:bg-blue-700 disabled:opacity-40 flex items-center gap-1">
<Save size={12} /> Confirmar
</button>
</div>
</div>
);
};
// --------------- Editor de modelo ---------------
const ModeloEditor: React.FC<{
modelo: ModeloReceita | null;
especialidades: Especialidade[];
onSave: (m: ModeloReceita) => void;
onBack: () => void;
}> = ({ modelo, especialidades, onSave, onBack }) => {
const toast = useToast();
const [nome, setNome] = useState(modelo?.nome || '');
const [espId, setEspId] = useState(modelo?.especialidadeId || '');
const [instrucoes, setInstrucoes] = useState(modelo?.instrucoes || '');
const [items, setItems] = useState<ItemReceita[]>(modelo?.items || []);
const [editingIdx, setEditingIdx] = useState<number | null>(null);
const [newItem, setNewItem] = useState<Omit<ItemReceita, 'id' | 'ordem'>>(ITEM_VAZIO);
const [adding, setAdding] = useState(false);
const [saving, setSaving] = useState(false);
const espNome = especialidades.find(e => e.id === espId)?.nome || '';
const onDragEnd = (result: DropResult) => {
if (!result.destination) return;
const list = [...items];
const [moved] = list.splice(result.source.index, 1);
list.splice(result.destination.index, 0, moved);
setItems(list.map((it, i) => ({ ...it, ordem: i })));
};
const addItem = () => {
if (!newItem.medicamento.trim()) return;
setItems(prev => [...prev, { ...newItem, id: `new_${Date.now()}`, ordem: prev.length }]);
setNewItem(ITEM_VAZIO);
setAdding(false);
};
const updateItem = (idx: number, val: Omit<ItemReceita, 'id' | 'ordem'>) => {
setItems(prev => prev.map((it, i) => i === idx ? { ...it, ...val } : it));
};
const removeItem = (idx: number) => setItems(prev => prev.filter((_, i) => i !== idx));
const handleSave = async () => {
if (!nome.trim()) { toast.error('NOME DO MODELO OBRIGATÓRIO'); return; }
setSaving(true);
try {
const payload = { nome: nome.toUpperCase(), especialidadeId: espId || undefined, especialidadeNome: espNome || undefined, instrucoes: instrucoes || undefined, items };
if (modelo?.id) {
await HybridBackend.updateModeloReceita({ ...payload, id: modelo.id });
} else {
const res = await HybridBackend.saveModeloReceita(payload);
onSave({ ...payload, id: res.id, items });
return;
}
onSave({ ...payload, id: modelo.id, items });
} catch { toast.error('ERRO AO SALVAR MODELO'); } finally { setSaving(false); }
};
const inp = "w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm focus:ring-2 focus:ring-blue-100 focus:border-blue-300 outline-none bg-white";
return (
<div className="flex flex-col h-full">
<div className="p-5 border-b border-gray-100 flex items-center gap-3">
<button onClick={onBack} className="text-gray-400 hover:text-gray-700 flex-shrink-0"><ChevronLeft size={18} /></button>
<h3 className="text-sm font-black text-gray-800 uppercase">{modelo ? 'Editar Modelo' : 'Novo Modelo'}</h3>
</div>
<div className="flex-1 overflow-y-auto p-5 space-y-4">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Nome do Modelo *</label>
<input value={nome} onChange={e => setNome(e.target.value.toUpperCase())} className={inp} placeholder="EX: ANTIBIÓTICO PÓS-CIRÚRGICO" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Especialidade</label>
<select value={espId} onChange={e => setEspId(e.target.value)} className={inp + " text-sm"}>
<option value=""> Geral </option>
{especialidades.map(e => <option key={e.id} value={e.id}>{e.nome}</option>)}
</select>
</div>
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Instrução Geral (opcional)</label>
<input value={instrucoes} onChange={e => setInstrucoes(e.target.value)} className={inp} placeholder="EX: USO EM CASO DE INFECÇÃO ODONTOGÊNICA" />
</div>
{/* Lista de medicamentos */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-[10px] font-black text-gray-500 uppercase tracking-wider">Medicamentos ({items.length})</label>
{!adding && <button onClick={() => setAdding(true)} className="flex items-center gap-1 text-xs font-bold text-blue-600 hover:text-blue-800">
<Plus size={13} /> Adicionar
</button>}
</div>
{adding && (
<div className="mb-2">
<ItemEditor item={newItem} onChange={setNewItem} onSave={addItem} onCancel={() => { setAdding(false); setNewItem(ITEM_VAZIO); }} />
</div>
)}
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="items-receita">
{provided => (
<div ref={provided.innerRef} {...provided.droppableProps} className="space-y-1.5">
{items.map((it, idx) => (
<Draggable key={it.id} draggableId={it.id} index={idx}>
{(prov, snap) => (
<div ref={prov.innerRef} {...prov.draggableProps}
className={`rounded-xl border transition-shadow ${snap.isDragging ? 'shadow-lg border-blue-300 bg-blue-50' : 'border-gray-200 bg-white'}`}>
{editingIdx === idx ? (
<div className="p-2">
<ItemEditor
item={{ medicamento: it.medicamento, dose: it.dose, via: it.via, frequencia: it.frequencia, duracao: it.duracao, instrucoes: it.instrucoes }}
onChange={v => updateItem(idx, v)}
onSave={() => setEditingIdx(null)}
onCancel={() => setEditingIdx(null)}
/>
</div>
) : (
<div className="flex items-center gap-2 px-3 py-2.5">
<span {...prov.dragHandleProps} className="text-gray-300 hover:text-gray-500 cursor-grab flex-shrink-0"><GripVertical size={15} /></span>
<div className="flex-1 min-w-0">
<p className="text-xs font-bold text-gray-800 uppercase truncate">{it.medicamento}</p>
<p className="text-[10px] text-gray-400">{[it.dose, it.via, it.frequencia, it.duracao].filter(Boolean).join(' · ')}</p>
{it.instrucoes && <p className="text-[10px] text-blue-500 italic">{it.instrucoes}</p>}
</div>
<button onClick={() => setEditingIdx(idx)} className="text-gray-300 hover:text-blue-500 flex-shrink-0"><Pencil size={13} /></button>
<button onClick={() => removeItem(idx)} className="text-gray-300 hover:text-red-500 flex-shrink-0"><Trash2 size={13} /></button>
</div>
)}
</div>
)}
</Draggable>
))}
{provided.placeholder}
{items.length === 0 && !adding && (
<div className="text-center py-6 text-xs text-gray-400 border-2 border-dashed border-gray-200 rounded-xl">
Nenhum medicamento adicionado ainda
</div>
)}
</div>
)}
</Droppable>
</DragDropContext>
</div>
</div>
<div className="p-4 border-t border-gray-100 flex justify-end gap-2">
<button onClick={onBack} className="px-5 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-lg text-xs uppercase">Cancelar</button>
<button onClick={handleSave} disabled={saving || !nome.trim()} className="px-5 py-2 bg-green-600 hover:bg-green-700 text-white font-bold rounded-lg text-xs uppercase flex items-center gap-1.5 disabled:opacity-40">
<Save size={14} /> {saving ? 'Salvando...' : 'Salvar Modelo'}
</button>
</div>
</div>
);
};
// --------------- Preview de impressão ---------------
const PrintPreview: React.FC<{
modelo: ModeloReceita;
paciente: Paciente;
dentista?: Dentista;
onClose: () => void;
}> = ({ modelo, paciente, dentista, onClose }) => {
const hoje = new Date().toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' });
return (
<div className="flex flex-col h-full">
<div className="p-4 border-b border-gray-100 flex items-center justify-between print:hidden">
<div className="flex items-center gap-2">
<button onClick={onClose} className="text-gray-400 hover:text-gray-700"><ChevronLeft size={18} /></button>
<h3 className="text-sm font-black text-gray-800 uppercase">Pré-visualização</h3>
</div>
<button onClick={() => window.print()} className="flex items-center gap-2 px-4 py-2 bg-gray-900 text-white rounded-lg text-xs font-bold hover:bg-black">
<Printer size={14} /> Imprimir / Salvar PDF
</button>
</div>
<div className="flex-1 overflow-y-auto bg-gray-100 p-6 print:p-0 print:bg-white">
<div id="receita-print" className="max-w-xl mx-auto bg-white rounded-2xl shadow-sm border border-gray-200 p-8 print:shadow-none print:rounded-none print:border-0 print:max-w-full">
<div className="text-center border-b-2 border-gray-800 pb-4 mb-6">
<h1 className="text-lg font-black text-gray-900 uppercase">RECEITUÁRIO</h1>
{dentista && <p className="text-sm font-bold text-gray-700 mt-1">{dentista.nome} CRO: {dentista.cro || '—'}/{dentista.cro_uf || '—'}</p>}
</div>
<div className="mb-6 grid grid-cols-2 gap-3 text-sm">
<div><span className="text-gray-500 text-xs uppercase font-bold">Paciente</span><p className="font-bold text-gray-900 uppercase">{paciente.nome}</p></div>
<div><span className="text-gray-500 text-xs uppercase font-bold">Data</span><p className="font-bold text-gray-900">{hoje}</p></div>
</div>
{modelo.instrucoes && <div className="mb-5 p-3 bg-amber-50 border border-amber-200 rounded-lg text-xs text-amber-800 font-medium italic">{modelo.instrucoes}</div>}
<div className="space-y-4">
{modelo.items.map((it, i) => (
<div key={it.id} className="border-b border-gray-100 pb-3 last:border-0 last:pb-0">
<p className="font-black text-gray-900 text-sm">{i + 1}. {it.medicamento}</p>
<p className="text-gray-700 text-sm ml-4">
{[it.dose, it.via, it.frequencia, `por ${it.duracao}`].filter(Boolean).join(' — ')}
</p>
{it.instrucoes && <p className="text-gray-500 text-xs ml-4 italic mt-0.5">{it.instrucoes}</p>}
</div>
))}
</div>
<div className="mt-12 pt-6 border-t-2 border-gray-800 flex justify-between items-end">
<div><p className="text-xs text-gray-500">Assinatura e Carimbo</p><div className="h-10" /></div>
<p className="text-xs text-gray-400">{hoje}</p>
</div>
</div>
</div>
</div>
);
};
// --------------- Modal principal ---------------
export const ReceitaModal: React.FC<{ paciente: Paciente; onClose: () => void }> = ({ paciente, onClose }) => {
const toast = useToast();
const { data: especialidades = [] } = useHybridBackend<Especialidade[]>(HybridBackend.getEspecialidades);
const { data: dentistas = [] } = useHybridBackend<Dentista[]>(HybridBackend.getDentistas);
const [modelos, setModelos] = useState<ModeloReceita[]>([]);
const [loading, setLoading] = useState(true);
const [filtroEsp, setFiltroEsp] = useState('');
const [busca, setBusca] = useState('');
const [view, setView] = useState<'lista' | 'editor' | 'print'>('lista');
const [modeloAtivo, setModeloAtivo] = useState<ModeloReceita | null>(null);
const [printModelo, setPrintModelo] = useState<ModeloReceita | null>(null);
const dentistaPrincipal = dentistas[0];
const carregar = useCallback(async () => {
setLoading(true);
try { setModelos(await HybridBackend.getModelosReceita()); }
catch { toast.error('Erro ao carregar modelos'); }
finally { setLoading(false); }
}, []);
useEffect(() => { carregar(); }, [carregar]);
useEffect(() => {
const h = (e: KeyboardEvent) => { if (e.key === 'Escape') { if (view !== 'lista') setView('lista'); else onClose(); } };
document.addEventListener('keydown', h);
return () => document.removeEventListener('keydown', h);
}, [view, onClose]);
const handleDelete = async (id: string) => {
if (!confirm('Excluir este modelo?')) return;
await HybridBackend.deleteModeloReceita(id);
setModelos(prev => prev.filter(m => m.id !== id));
};
const filtrados = modelos.filter(m => {
const matchEsp = !filtroEsp || m.especialidadeId === filtroEsp;
const matchBusca = !busca || m.nome.toLowerCase().includes(busca.toLowerCase());
return matchEsp && matchBusca;
});
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center backdrop-blur-sm p-3">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl h-[85vh] flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-200">
{view === 'lista' && (
<>
<div className="p-5 border-b border-gray-100 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-red-100 rounded-lg flex items-center justify-center"><FileText size={15} className="text-red-600" /></div>
<div>
<h2 className="text-sm font-black text-gray-800 uppercase">Modelos de Receita</h2>
<p className="text-[10px] text-gray-400 uppercase">{paciente.nome}</p>
</div>
</div>
<div className="flex items-center gap-2">
<button onClick={() => { setModeloAtivo(null); setView('editor'); }}
className="flex items-center gap-1.5 px-3 py-2 bg-green-600 hover:bg-green-700 text-white text-xs font-bold rounded-lg">
<Plus size={13} /> Novo Modelo
</button>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={18} /></button>
</div>
</div>
<div className="flex flex-1 overflow-hidden">
{/* Sidebar especialidades */}
<div className="w-44 border-r border-gray-100 flex-shrink-0 overflow-y-auto bg-gray-50 p-2">
<p className="text-[9px] font-black text-gray-400 uppercase px-2 mb-2 tracking-wider">Especialidade</p>
<button onClick={() => setFiltroEsp('')}
className={`w-full text-left px-2.5 py-1.5 rounded-lg text-xs font-bold mb-0.5 transition-colors ${!filtroEsp ? 'bg-blue-600 text-white' : 'text-gray-600 hover:bg-gray-100'}`}>
Todas
</button>
{especialidades.map(e => (
<button key={e.id} onClick={() => setFiltroEsp(e.id)}
className={`w-full text-left px-2.5 py-1.5 rounded-lg text-xs font-bold mb-0.5 transition-colors truncate ${filtroEsp === e.id ? 'bg-blue-600 text-white' : 'text-gray-600 hover:bg-gray-100'}`}>
{e.nome}
</button>
))}
</div>
{/* Lista de modelos */}
<div className="flex-1 flex flex-col overflow-hidden">
<div className="p-3 border-b border-gray-100">
<div className="relative">
<Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400" />
<input value={busca} onChange={e => setBusca(e.target.value)} placeholder="Buscar modelo..."
className="w-full pl-8 pr-3 py-2 border border-gray-200 rounded-lg text-xs focus:ring-2 focus:ring-blue-100 outline-none" />
</div>
</div>
<div className="flex-1 overflow-y-auto p-3 space-y-2">
{loading && <div className="text-center py-8 text-xs text-gray-400">Carregando...</div>}
{!loading && filtrados.length === 0 && (
<div className="text-center py-10">
<ClipboardList size={28} className="text-gray-200 mx-auto mb-2" />
<p className="text-xs text-gray-400">Nenhum modelo encontrado</p>
<button onClick={() => { setModeloAtivo(null); setView('editor'); }}
className="mt-3 text-xs font-bold text-blue-600 hover:underline">+ Criar primeiro modelo</button>
</div>
)}
{filtrados.map(m => (
<div key={m.id} className="bg-white border border-gray-200 rounded-xl p-3 hover:border-blue-200 hover:shadow-sm transition-all group">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<p className="text-xs font-black text-gray-800 uppercase truncate">{m.nome}</p>
<div className="flex items-center gap-2 mt-0.5">
{m.especialidadeNome && <span className="text-[9px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded-full font-bold uppercase">{m.especialidadeNome}</span>}
<span className="text-[9px] text-gray-400">{m.items.length} medicamento{m.items.length !== 1 ? 's' : ''}</span>
</div>
{m.instrucoes && <p className="text-[10px] text-gray-400 italic mt-1 truncate">{m.instrucoes}</p>}
</div>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
<button onClick={() => { setPrintModelo(m); setView('print'); }}
title="Prescrever para este paciente"
className="p-1.5 text-gray-400 hover:text-green-600 hover:bg-green-50 rounded-lg"><Printer size={13} /></button>
<button onClick={() => { setModeloAtivo(m); setView('editor'); }}
className="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg"><Pencil size={13} /></button>
<button onClick={() => handleDelete(m.id)}
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg"><Trash2 size={13} /></button>
</div>
</div>
{m.items.length > 0 && (
<div className="mt-2 pt-2 border-t border-gray-50 space-y-0.5">
{m.items.slice(0, 3).map(it => (
<p key={it.id} className="text-[10px] text-gray-500 truncate">
{it.medicamento} {it.dose} {it.frequencia}
</p>
))}
{m.items.length > 3 && <p className="text-[10px] text-gray-400">+{m.items.length - 3} mais...</p>}
</div>
)}
</div>
))}
</div>
</div>
</div>
</>
)}
{view === 'editor' && (
<ModeloEditor
modelo={modeloAtivo}
especialidades={especialidades}
onSave={m => { carregar(); setView('lista'); toast.success('MODELO SALVO!'); }}
onBack={() => setView('lista')}
/>
)}
{view === 'print' && printModelo && (
<PrintPreview
modelo={printModelo}
paciente={paciente}
dentista={dentistaPrincipal}
onClose={() => setView('lista')}
/>
)}
</div>
</div>
);
};