feat(agenda): import como migracao (status importado) + confirmar/vincular paciente - Fase 2
- import entra como status 'importado' (provisorio): separa titulo NOME x PROCEDIMENTO, traz description do Google em observacoes, nao pontua - 'importado' no CHECK de status - falta bloqueada sem paciente vinculado / importado - POST /agendamentos/:id/confirmar: vincula paciente, EXIGE cpf+telefone (enriquece cadastro), vira agendado - pendencia 'a reagendar' so com paciente vinculado - frontend: modal do importado mostra 'A CONFIRMAR' + Confirmar paciente (busca com desambiguacao homonimos: cpf/tel/nascimento + criar novo) + Remover; ScoreBadge Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+47
-6
@@ -1970,7 +1970,7 @@ app.delete('/api/agendamentos/:id', authGuard, async (req, res) => {
|
||||
clinicaId: a.clinica_id, entidade: 'agendamento', entidadeId: a.id, acao: 'cancelou',
|
||||
actorId: actor, actorNome: actorNm, detalhes: { motivo: req.body?.motivo || null, start: a.start_time }
|
||||
});
|
||||
await abrirPendencia(pool, { clinicaId: a.clinica_id, pacienteId: a.pacienteid, pacienteNome: a.pacientenome, origemId: a.id, motivo: 'cancelado' });
|
||||
if (a.pacienteid) await abrirPendencia(pool, { clinicaId: a.clinica_id, pacienteId: a.pacienteid, pacienteNome: a.pacientenome, origemId: a.id, motivo: 'cancelado' });
|
||||
await notificarInteressesSlotLivre(a);
|
||||
pushGoogleEvent('delete', a); // remove a cópia/notificação do Google
|
||||
res.json({ success: true });
|
||||
@@ -1985,6 +1985,10 @@ app.post('/api/agendamentos/:id/falta', authGuard, async (req, res) => {
|
||||
const { rows: ag } = await pool.query('SELECT * FROM agendamentos WHERE id = $1', [req.params.id]);
|
||||
if (!ag.length) return res.status(404).json({ success: false, message: 'Agendamento não encontrado.' });
|
||||
const a = ag[0];
|
||||
// Falta exige paciente vinculado (importado/provisório não pontua).
|
||||
if (!a.pacienteid || String(a.status).toLowerCase() === 'importado') {
|
||||
return res.status(400).json({ success: false, message: 'Vincule um paciente (confirme o agendamento) antes de marcar falta.' });
|
||||
}
|
||||
await pool.query(`UPDATE agendamentos SET status='falta', updated_by=$2, updated_by_nome=$3, updated_at=NOW(), version=COALESCE(version,1)+1 WHERE id=$1`, [req.params.id, actor, actorNm]);
|
||||
await registrarAudit(pool, { clinicaId: a.clinica_id, entidade: 'agendamento', entidadeId: a.id, acao: 'faltou', actorId: actor, actorNome: actorNm, detalhes: { start: a.start_time } });
|
||||
await abrirPendencia(pool, { clinicaId: a.clinica_id, pacienteId: a.pacienteid, pacienteNome: a.pacientenome, origemId: a.id, motivo: 'falta' });
|
||||
@@ -2003,7 +2007,7 @@ app.post('/api/agendamentos/:id/remarcar', authGuard, async (req, res) => {
|
||||
const a = ag[0];
|
||||
await pool.query(`UPDATE agendamentos SET status='remarcar', updated_by=$2, updated_by_nome=$3, updated_at=NOW(), version=COALESCE(version,1)+1 WHERE id=$1`, [req.params.id, actor, actorNm]);
|
||||
await registrarAudit(pool, { clinicaId: a.clinica_id, entidade: 'agendamento', entidadeId: a.id, acao: 'remarcou', actorId: actor, actorNome: actorNm, detalhes: { motivo: req.body?.motivo || null, start: a.start_time } });
|
||||
await abrirPendencia(pool, { clinicaId: a.clinica_id, pacienteId: a.pacienteid, pacienteNome: a.pacientenome, origemId: a.id, motivo: 'remarcar' });
|
||||
if (a.pacienteid) await abrirPendencia(pool, { clinicaId: a.clinica_id, pacienteId: a.pacienteid, pacienteNome: a.pacientenome, origemId: a.id, motivo: 'remarcar' });
|
||||
await notificarInteressesSlotLivre(a);
|
||||
pushGoogleEvent('delete', a);
|
||||
res.json({ success: true });
|
||||
@@ -2026,6 +2030,36 @@ app.post('/api/agendamentos/:id/restaurar', authGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// CONFIRMAR um agendamento IMPORTADO: vincula o paciente (exige CPF+telefone) e vira 'agendado'.
|
||||
// Não escreve no Google (vínculo é interno). O paciente é enriquecido se faltava CPF/telefone.
|
||||
app.post('/api/agendamentos/:id/confirmar', authGuard, async (req, res) => {
|
||||
try {
|
||||
const actor = req.authUser?.userId || null;
|
||||
const actorNm = await actorNome(actor);
|
||||
const { pacienteId, cpf, telefone, procedimento } = req.body || {};
|
||||
if (!pacienteId) return res.status(400).json({ success: false, message: 'Selecione um paciente.' });
|
||||
const { rows: ag } = await pool.query('SELECT * FROM agendamentos WHERE id = $1', [req.params.id]);
|
||||
if (!ag.length) return res.status(404).json({ success: false, message: 'Agendamento não encontrado.' });
|
||||
const a = ag[0];
|
||||
const { rows: pr } = await pool.query('SELECT id, nome, cpf, telefone FROM pacientes WHERE id = $1', [pacienteId]);
|
||||
if (!pr.length) return res.status(404).json({ success: false, message: 'Paciente não encontrado.' });
|
||||
const pac = pr[0];
|
||||
const novoCpf = String(cpf ?? pac.cpf ?? '').trim();
|
||||
const novoTel = String(telefone ?? pac.telefone ?? '').trim();
|
||||
if (!novoCpf || !novoTel) return res.status(400).json({ success: false, message: 'CPF e telefone são obrigatórios para confirmar.' });
|
||||
// Enriquece o cadastro do paciente se faltava CPF/telefone.
|
||||
if (novoCpf !== String(pac.cpf || '') || novoTel !== String(pac.telefone || '')) {
|
||||
await pool.query('UPDATE pacientes SET cpf = $1, telefone = $2 WHERE id = $3', [novoCpf.slice(0, 20), novoTel.slice(0, 30), pacienteId]);
|
||||
}
|
||||
await pool.query(
|
||||
`UPDATE agendamentos SET pacienteid=$1, pacientenome=$2, procedimento=COALESCE($3, procedimento), status='agendado',
|
||||
updated_by=$4, updated_by_nome=$5, updated_at=NOW(), version=COALESCE(version,1)+1 WHERE id=$6`,
|
||||
[pacienteId, pac.nome, procedimento || null, actor, actorNm, a.id]);
|
||||
await registrarAudit(pool, { clinicaId: a.clinica_id, entidade: 'agendamento', entidadeId: a.id, acao: 'editou', actorId: actor, actorNome: actorNm, detalhes: { confirmou: true, paciente: pac.nome } });
|
||||
res.json({ success: true });
|
||||
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
||||
});
|
||||
|
||||
// Histórico (auditoria) de um agendamento.
|
||||
app.get('/api/agendamentos/:id/historico', authGuard, async (req, res) => {
|
||||
try {
|
||||
@@ -2181,11 +2215,18 @@ app.post('/api/agenda/importar-google', authGuard, async (req, res) => {
|
||||
if (ex.length) { pulados++; continue; }
|
||||
const end = it.end?.dateTime || it.end?.date || start;
|
||||
const cor = (it.colorId && GOOGLE_EVENT_COLORS[it.colorId]) || calColor || null;
|
||||
// Separa "NOME - PROCEDIMENTO"; sem separador, o título inteiro vira o nome.
|
||||
const titulo = String(it.summary || 'Importado do Google').trim();
|
||||
const sep = titulo.indexOf(' - ');
|
||||
const pacNome = sep > 0 ? titulo.slice(0, sep).trim() : titulo;
|
||||
const proc = sep > 0 ? titulo.slice(sep + 3).trim() : null;
|
||||
const obs = it.description ? String(it.description).slice(0, 2000) : null;
|
||||
const id = `ag_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
||||
// status 'importado' = PROVISÓRIO (não pontua, não conta falta até confirmar/vincular paciente).
|
||||
await pool.query(
|
||||
`INSERT INTO agendamentos (id, clinica_id, pacientenome, start_time, end_time, status, procedimento, google_event_id, cor, created_by, created_by_nome, version)
|
||||
VALUES ($1,$2,$3,$4,$5,'agendado',$6,$7,$8,$9,$10,1)`,
|
||||
[id, clinicaId, it.summary || 'Importado do Google', start, end, 'IMPORTADO (GOOGLE)', it.id, cor, actor, actorNm]);
|
||||
`INSERT INTO agendamentos (id, clinica_id, pacientenome, start_time, end_time, status, procedimento, observacoes, google_event_id, cor, created_by, created_by_nome, version)
|
||||
VALUES ($1,$2,$3,$4,$5,'importado',$6,$7,$8,$9,$10,$11,1)`,
|
||||
[id, clinicaId, pacNome, start, end, proc, obs, it.id, cor, actor, actorNm]);
|
||||
await registrarAudit(pool, { clinicaId, entidade: 'agendamento', entidadeId: id, acao: 'criou', actorId: actor, actorNome: actorNm, detalhes: { origem: 'google', summary: it.summary } });
|
||||
criados++;
|
||||
}
|
||||
@@ -3444,7 +3485,7 @@ async function runMigrations() {
|
||||
// ── Agenda Fase 1: autoria, soft-delete, faltas, auditoria, pendências ──
|
||||
// Novo vocabulário de status (o CHECK antigo barrava 'agendado'/'cancelado').
|
||||
`ALTER TABLE agendamentos DROP CONSTRAINT IF EXISTS agendamentos_status_check`,
|
||||
`ALTER TABLE agendamentos ADD CONSTRAINT agendamentos_status_check CHECK (status IS NULL OR lower(status) IN ('agendado','confirmado','reagendado','remarcar','cancelado','falta','atendido','pendente','concluido','faltou'))`,
|
||||
`ALTER TABLE agendamentos ADD CONSTRAINT agendamentos_status_check CHECK (status IS NULL OR lower(status) IN ('importado','agendado','confirmado','reagendado','remarcar','cancelado','falta','atendido','pendente','concluido','faltou'))`,
|
||||
// Slot único só vale para status ATIVOS — assim cancelar/faltar libera o slot p/ reagendar.
|
||||
`ALTER TABLE agendamentos DROP CONSTRAINT IF EXISTS unique_dentista_slot`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS unique_dentista_slot_active ON agendamentos (dentistaid, start_time) WHERE lower(status) NOT IN ('cancelado','falta','remarcar')`,
|
||||
|
||||
@@ -4,7 +4,8 @@ import { HybridBackend } from '../services/backend.ts';
|
||||
import { useToast } from '../contexts/ToastContext.tsx';
|
||||
import {
|
||||
User, Stethoscope, Clock, CalendarRange, AlertCircle, CheckCircle2,
|
||||
History, RotateCcw, CalendarClock, Ban, Users, Pencil, ClipboardCheck, ShieldAlert
|
||||
History, RotateCcw, CalendarClock, Ban, Users, Pencil, ClipboardCheck, ShieldAlert,
|
||||
Search, UserPlus, Link2, Trash2
|
||||
} from 'lucide-react';
|
||||
|
||||
interface Props {
|
||||
@@ -23,11 +24,13 @@ const _cache: Record<string, { faltas?: number; familia?: { grupo: any; membros:
|
||||
|
||||
const low = (s: any) => String(s || '').toLowerCase();
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
importado: 'A CONFIRMAR · GOOGLE',
|
||||
agendado: 'AGENDADO', confirmado: 'CONFIRMADO', reagendado: 'REAGENDADO',
|
||||
remarcar: 'A REAGENDAR', cancelado: 'CANCELADO', falta: 'FALTOU', atendido: 'ATENDIDO',
|
||||
pendente: 'PENDENTE', concluido: 'CONCLUÍDO', faltou: 'FALTOU',
|
||||
};
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
importado: 'bg-violet-50 text-violet-700',
|
||||
cancelado: 'bg-red-50 text-red-600', falta: 'bg-red-50 text-red-600', faltou: 'bg-red-50 text-red-600',
|
||||
remarcar: 'bg-amber-50 text-amber-600', confirmado: 'bg-emerald-50 text-emerald-600',
|
||||
atendido: 'bg-emerald-50 text-emerald-600', concluido: 'bg-emerald-50 text-emerald-600',
|
||||
@@ -143,11 +146,125 @@ const HistoryPage: React.FC<{ appointmentId: string }> = ({ appointmentId }) =>
|
||||
);
|
||||
};
|
||||
|
||||
// Página de CONFIRMAÇÃO de um importado: busca/desambigua paciente, exige CPF+telefone, vincula.
|
||||
const ConfirmImportadoPage: React.FC<{ ag: any; onConfirmed: () => void }> = ({ ag, onConfirmed }) => {
|
||||
const { close } = useStackModal();
|
||||
const toast = useToast();
|
||||
const [q, setQ] = useState('');
|
||||
const [results, setResults] = useState<any[]>([]);
|
||||
const [sel, setSel] = useState<any | null>(null);
|
||||
const [novo, setNovo] = useState(false);
|
||||
const [nome, setNome] = useState(ag.pacienteNome || ag.pacientenome || '');
|
||||
const [cpf, setCpf] = useState('');
|
||||
const [tel, setTel] = useState('');
|
||||
const [proc, setProc] = useState(ag.procedimento || '');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
const t = setTimeout(() => {
|
||||
HybridBackend.getPacientes(q, 'recentes').then(r => alive && setResults((r.data || []).slice(0, 8))).catch(() => { });
|
||||
}, 300);
|
||||
return () => { alive = false; clearTimeout(t); };
|
||||
}, [q]);
|
||||
|
||||
const pick = (pac: any) => { setSel(pac); setNovo(false); setCpf(pac.cpf || ''); setTel(pac.telefone || ''); };
|
||||
const iniciarNovo = () => { setNovo(true); setSel(null); setCpf(''); setTel(''); };
|
||||
|
||||
const confirmar = async () => {
|
||||
if (!cpf.trim() || !tel.trim()) { toast.error('CPF E TELEFONE SÃO OBRIGATÓRIOS.'); return; }
|
||||
setSaving(true);
|
||||
try {
|
||||
let pacienteId = sel?.id;
|
||||
if (novo) {
|
||||
if (!nome.trim()) { toast.error('INFORME O NOME.'); setSaving(false); return; }
|
||||
pacienteId = `p_${Date.now()}`;
|
||||
await HybridBackend.savePaciente({ id: pacienteId, nome, cpf, telefone: tel } as any);
|
||||
}
|
||||
if (!pacienteId) { toast.error('SELECIONE UM PACIENTE OU CRIE UM NOVO.'); setSaving(false); return; }
|
||||
const res = await HybridBackend.confirmarAgendamento(ag.id, { pacienteId, cpf, telefone: tel, procedimento: proc || undefined });
|
||||
if (!res.success) { toast.error(res.message || 'ERRO AO CONFIRMAR.'); return; }
|
||||
toast.success('AGENDAMENTO CONFIRMADO!');
|
||||
onConfirmed(); close();
|
||||
} catch { toast.error('ERRO AO CONFIRMAR.'); } finally { setSaving(false); }
|
||||
};
|
||||
|
||||
const faltaDados = !cpf.trim() || !tel.trim();
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<p className="text-[11px] text-gray-500 leading-snug">Vincule o paciente do cadastro. Se houver homônimos, use CPF/telefone/nascimento para identificar. CPF e telefone são obrigatórios.</p>
|
||||
|
||||
{!novo && (
|
||||
<>
|
||||
<div className="relative">
|
||||
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input autoFocus value={q} onChange={e => setQ(e.target.value)} placeholder="Buscar por nome, CPF ou telefone..."
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-blue-400" />
|
||||
</div>
|
||||
<div className="max-h-52 overflow-y-auto custom-scrollbar space-y-1.5">
|
||||
{results.map(pac => (
|
||||
<button key={pac.id} onClick={() => pick(pac)}
|
||||
className={`w-full text-left p-2.5 rounded-xl border transition-colors ${sel?.id === pac.id ? 'border-blue-400 bg-blue-50' : 'border-gray-100 hover:bg-gray-50'}`}>
|
||||
<p className="text-xs font-black text-gray-800 uppercase truncate">{pac.nome}</p>
|
||||
<p className="text-[10px] text-gray-400 font-bold">
|
||||
{pac.cpf ? `CPF ${pac.cpf}` : 'sem CPF'} · {pac.telefone || 'sem tel'}{pac.datanascimento ? ` · ${pac.datanascimento}` : ''}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
{q && results.length === 0 && <p className="text-[11px] text-gray-400 text-center py-3 uppercase font-bold">Nenhum paciente encontrado.</p>}
|
||||
</div>
|
||||
<button onClick={iniciarNovo} className="w-full flex items-center justify-center gap-1.5 py-2.5 rounded-xl border-2 border-dashed border-gray-200 text-[11px] font-black uppercase text-gray-500 hover:border-blue-300 hover:text-blue-600 transition-colors">
|
||||
<UserPlus size={15} /> Criar novo paciente
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{novo && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-gray-500 uppercase">Novo paciente</label>
|
||||
<input value={nome} onChange={e => setNome(e.target.value.toUpperCase())} placeholder="NOME COMPLETO"
|
||||
className="w-full px-3 py-2.5 border border-gray-200 rounded-xl text-sm font-bold focus:outline-none focus:border-blue-400" />
|
||||
<button onClick={() => setNovo(false)} className="text-[10px] font-black text-blue-600 uppercase">← voltar à busca</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(sel || novo) && (
|
||||
<div className="bg-gray-50 rounded-xl p-3 space-y-2">
|
||||
{sel && <p className="text-xs font-black text-gray-800 uppercase">{sel.nome}</p>}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="text-[9px] font-black text-gray-400 uppercase">CPF *</label>
|
||||
<input value={cpf} onChange={e => setCpf(e.target.value)} placeholder="000.000.000-00"
|
||||
className={`w-full px-2.5 py-2 border rounded-lg text-xs font-bold focus:outline-none ${!cpf.trim() ? 'border-amber-300 bg-amber-50' : 'border-gray-200 focus:border-blue-400'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[9px] font-black text-gray-400 uppercase">Telefone *</label>
|
||||
<input value={tel} onChange={e => setTel(e.target.value)} placeholder="(00) 00000-0000"
|
||||
className={`w-full px-2.5 py-2 border rounded-lg text-xs font-bold focus:outline-none ${!tel.trim() ? 'border-amber-300 bg-amber-50' : 'border-gray-200 focus:border-blue-400'}`} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[9px] font-black text-gray-400 uppercase">Procedimento</label>
|
||||
<input value={proc} onChange={e => setProc(e.target.value.toUpperCase())} placeholder="EX: AVALIAÇÃO"
|
||||
className="w-full px-2.5 py-2 border border-gray-200 rounded-lg text-xs font-bold focus:outline-none focus:border-blue-400" />
|
||||
</div>
|
||||
{faltaDados && <p className="text-[10px] text-amber-600 font-bold">Preencha CPF e telefone para confirmar.</p>}
|
||||
<button onClick={confirmar} disabled={saving || faltaDados}
|
||||
className="w-full py-2.5 bg-emerald-600 text-white rounded-xl text-[11px] font-black uppercase hover:bg-emerald-700 transition-all disabled:opacity-50 flex items-center justify-center gap-1.5">
|
||||
<Link2 size={15} /> {saving ? 'CONFIRMANDO...' : 'Confirmar e vincular'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DetailPage: React.FC<Props> = (p) => {
|
||||
const { push, close } = useStackModal();
|
||||
const toast = useToast();
|
||||
const ag = p.appointment || {};
|
||||
const status = low(ag.status);
|
||||
const provisorio = status === 'importado';
|
||||
const inativo = ['cancelado', 'falta', 'remarcar', 'faltou'].includes(status);
|
||||
const pacienteId = ag.pacienteid || ag.pacienteId || '';
|
||||
const dentNome = ag.dentistaNome || p.dentists?.find(d => d.id === (ag.dentistaId || ag.dentistaid))?.nome || '—';
|
||||
@@ -210,6 +327,28 @@ const DetailPage: React.FC<Props> = (p) => {
|
||||
{status === 'cancelado' && ag.canceled_by_nome && <p className="text-red-400">Cancelado por <b className="text-red-600">{ag.canceled_by_nome}</b></p>}
|
||||
</div>
|
||||
|
||||
{/* Importado do Google (provisório): só confirmar/vincular ou remover. Não pontua. */}
|
||||
{provisorio ? (
|
||||
<div className="space-y-2">
|
||||
<div className="bg-violet-50 border border-violet-100 rounded-xl p-3 text-[11px] text-violet-700 leading-snug">
|
||||
Importado do Google. <b>Confirme o paciente</b> para virar um agendamento do sistema (aí passa a contar faltas/remarcações).
|
||||
</div>
|
||||
<button onClick={() => push({ title: 'CONFIRMAR PACIENTE', render: () => <ConfirmImportadoPage ag={ag} onConfirmed={p.onChanged} /> })}
|
||||
className="w-full py-3 bg-emerald-600 text-white rounded-xl text-[11px] font-black uppercase hover:bg-emerald-700 transition-all flex items-center justify-center gap-1.5">
|
||||
<Link2 size={15} /> Confirmar paciente
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => push({ title: 'HISTÓRICO', render: () => <HistoryPage appointmentId={ag.id} /> })}
|
||||
className="flex-1 py-2.5 rounded-xl bg-gray-50 hover:bg-gray-100 text-xs font-black uppercase text-gray-600 flex items-center justify-center gap-1.5">
|
||||
<History size={15} /> Histórico
|
||||
</button>
|
||||
<button onClick={() => { if (confirm('REMOVER ESTE IMPORTADO DA AGENDA?')) run(() => HybridBackend.excluirAgendamento(ag.id), 'IMPORTADO REMOVIDO.'); }}
|
||||
className="flex-1 py-2.5 rounded-xl border-2 border-gray-100 text-xs font-black uppercase text-gray-500 hover:border-red-200 hover:text-red-600 transition-all flex items-center justify-center gap-1.5">
|
||||
<Trash2 size={15} /> Remover
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (<>
|
||||
{/* Ações de status */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button onClick={() => run(() => HybridBackend.atualizarAgendamento(ag.id, { status: 'confirmado' as any }), 'CONFIRMADO!')}
|
||||
@@ -259,6 +398,7 @@ const DetailPage: React.FC<Props> = (p) => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -785,6 +785,11 @@ export const HybridBackend = {
|
||||
const res = await apiFetch(`${API_URL}/agendamentos/${id}/restaurar`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||||
return await res.json().catch(() => ({ success: res.ok }));
|
||||
},
|
||||
// Confirma um agendamento IMPORTADO: vincula paciente (exige CPF+telefone) → vira 'agendado'.
|
||||
confirmarAgendamento: async (id: string, payload: { pacienteId: string; cpf?: string; telefone?: string; procedimento?: string }) => {
|
||||
const res = await apiFetch(`${API_URL}/agendamentos/${id}/confirmar`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
||||
return await res.json().catch(() => ({ success: res.ok }));
|
||||
},
|
||||
getHistoricoAgendamento: async (id: string): Promise<any[]> => {
|
||||
const res = await apiFetch(`${API_URL}/agendamentos/${id}/historico`, {});
|
||||
const j = await res.json().catch(() => ({} as any));
|
||||
|
||||
Reference in New Issue
Block a user