Files
scoreodonto.com/frontend/contexts/ConfirmContext.tsx
T
VPS 4 Builder 3e64514b08 feat(financeiro+salas): motor financeiro V1/V2, glosas, GTO→financeiro, contratos de convênio e redesenho de Salas
Financeiro V1 (Épicos 1–4):
- FKs de origem no lançamento (paciente/profissional/procedimento/agendamento/contrato/realizado), soft delete e cron "Atrasado"
- prontuário de procedimentos realizados (fotos antes/depois) + regra sem-comissão (garantia/reabertura do mesmo profissional)
- normalizeFinanceiro: corrige CHECK capitalizado vs enforceUppercase (lançamentos não salvavam pela UI)
- baixa de pagamento (pago_em), despesas (fornecedor/centro de custo/recorrente), competência por data de conclusão
- relatório financeiro (período, paciente, profissional, forma, convênio, glosa)
- orçamento Assinado -> contas a receber; contrato vigente -> cobrança recorrente; renovação automática de contrato

Financeiro V2 (Comissão/Repasse):
- regras de % (padrão/procedimento/override por profissional) + custo de laboratório
- base selecionável (recebido/produção), fechamento persistido, pagar (gera despesa) + comprovante + estorno

Glosas de convênio: por item de GTO, recurso/protocolo/prazo, recuperar/estornar, impacto no relatório

GTO -> Financeiro: finalizar gera RECEITA (convênio a receber); LancarGTO passa a persistir; convênios reais (planos)

Contratos: modelo "Atendimento a Convênios / Repasse (Dentista Credenciado)" + variáveis de convênio

Salas (redesenho): perfil "Dono de Sala"; locação sempre ativa; menus MINHAS/ALUGAR SALAS;
sala como workspace isolado (seletor agrupado Clínicas x Salas, tenantGuard por dono); financeiro de
locação (reserva confirmada -> recebível); Painel da Sala (KPIs, agenda visual, recebíveis, relatório por sala)

Docs: BACKLOG-FINANCEIRO-V1, AUDITORIA-FUNCIONAL-SCOREODONTO, CHECKLIST-DEPLOY-PRODUCAO

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 00:28:07 +02:00

90 lines
3.7 KiB
TypeScript

import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react';
import { AlertTriangle, HelpCircle } from 'lucide-react';
interface ConfirmOptions {
title?: string;
message: string;
confirmText?: string;
cancelText?: string;
danger?: boolean;
}
type ConfirmInput = string | ConfirmOptions;
interface ConfirmContextType {
confirm: (input: ConfirmInput) => Promise<boolean>;
}
const ConfirmContext = createContext<ConfirmContextType | undefined>(undefined);
// Detecta ações destrutivas pelo texto → estilo de alerta (vermelho).
const DANGER_RE = /EXCLUIR|REMOVER|APAGAR|PERMANENTE|LIMPAR|CANCELAR|SAIR|DELETE|DESFEIT|RESETAR/i;
export const ConfirmProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [state, setState] = useState<{ opts: ConfirmOptions; resolve: (v: boolean) => void } | null>(null);
const confirm = useCallback((input: ConfirmInput) => {
const opts: ConfirmOptions = typeof input === 'string' ? { message: input } : input;
return new Promise<boolean>((resolve) => setState({ opts, resolve }));
}, []);
const close = useCallback((v: boolean) => {
setState((s) => { s?.resolve(v); return null; });
}, []);
useEffect(() => {
if (!state) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') close(false);
if (e.key === 'Enter') close(true);
};
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [state, close]);
const o = state?.opts;
const danger = o ? (o.danger ?? DANGER_RE.test(`${o.title || ''} ${o.message}`)) : false;
return (
<ConfirmContext.Provider value={{ confirm }}>
{children}
{o && (
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-150"
onMouseDown={(e) => { if (e.target === e.currentTarget) close(false); }}
>
<div className="bg-white rounded-2xl w-full max-w-sm shadow-2xl overflow-hidden animate-in zoom-in-95 slide-in-from-bottom-2 duration-200">
<div className="p-6 text-center">
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center mx-auto mb-4 ${danger ? 'bg-red-50' : 'bg-teal-50'}`}>
{danger ? <AlertTriangle size={26} className="text-red-500" /> : <HelpCircle size={26} className="text-teal-600" />}
</div>
{o.title && <h3 className="text-base font-black uppercase tracking-tight text-gray-900 mb-1.5">{o.title}</h3>}
<p className="text-sm text-gray-600 font-medium leading-relaxed whitespace-pre-line">{o.message}</p>
</div>
<div className="px-6 pb-6 flex gap-3">
<button
onClick={() => close(false)}
className="flex-1 py-3 border border-gray-200 rounded-xl text-xs font-black uppercase tracking-widest text-gray-500 hover:bg-gray-50 transition-colors"
>
{o.cancelText || 'Cancelar'}
</button>
<button
onClick={() => close(true)}
autoFocus
className={`flex-1 py-3 rounded-xl text-xs font-black uppercase tracking-widest text-white transition-opacity hover:opacity-90 ${danger ? 'bg-red-500' : 'bg-teal-600'}`}
>
{o.confirmText || 'Confirmar'}
</button>
</div>
</div>
</div>
)}
</ConfirmContext.Provider>
);
};
export const useConfirm = (): ((input: ConfirmInput) => Promise<boolean>) => {
const ctx = useContext(ConfirmContext);
if (!ctx) throw new Error('useConfirm must be used within a ConfirmProvider');
return ctx.confirm;
};