3e64514b08
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>
90 lines
3.7 KiB
TypeScript
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;
|
|
};
|