d6d2bc1e78
- Catálogo de procedimentos por especialidade com busca e filtros - Preço automático pelo convênio/plano do paciente (valoresPlanos) - Campos de região: dente (texto livre), arcada (Superior/Inferior/Ambas), face (V/L/M/D/O multi-select) conforme tipo_regiao e exige_face - Quantidade por item com +/- - Painel financeiro: desconto (R$ ou %), entrada (auto-marcada Pago), parcelamento até 48x com vencimentos mensais automáticos - Ao salvar: gera uma entrada de financeiro por parcela + entrada separada - Integrado no botão LANÇAR do menu financeiro dos pacientes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
448 lines
28 KiB
TypeScript
448 lines
28 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
||
import {
|
||
X, Search, Plus, Minus, Trash2, DollarSign, Check,
|
||
ChevronDown, Loader2, Tag, Percent, Banknote
|
||
} from 'lucide-react';
|
||
import { HybridBackend } from '../services/backend.ts';
|
||
import { Paciente, Procedimento, Especialidade, Plano } from '../types.ts';
|
||
import { useHybridBackend } from '../hooks/useHybridBackend.ts';
|
||
import { useToast } from '../contexts/ToastContext.tsx';
|
||
|
||
// ─── Tipos internos ────────────────────────────────────────────────────────────
|
||
interface LancItem {
|
||
uid: string;
|
||
proc: Procedimento;
|
||
quantidade: number;
|
||
dente: string;
|
||
face: string;
|
||
arco: string;
|
||
observacao: string;
|
||
valorUnit: number;
|
||
}
|
||
|
||
const FACES = ['V', 'L', 'M', 'D', 'O'];
|
||
const ARCOS = ['Superior', 'Inferior', 'Ambas'];
|
||
const FORMAS_PAG = ['Pix', 'Cartão', 'Dinheiro', 'Boleto'] as const;
|
||
const STATUS_LIST = ['Pendente', 'Pago', 'Atrasado'] as const;
|
||
|
||
const fmt = (v: number) => v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
|
||
|
||
function addMonths(date: string, n: number): string {
|
||
const d = new Date(date + 'T12:00:00');
|
||
d.setMonth(d.getMonth() + n);
|
||
return d.toISOString().slice(0, 10);
|
||
}
|
||
|
||
// ─── Chip de procedimento selecionado ─────────────────────────────────────────
|
||
const ItemCard: React.FC<{
|
||
item: LancItem;
|
||
onChange: (patch: Partial<LancItem>) => void;
|
||
onRemove: () => void;
|
||
}> = ({ item, onChange, onRemove }) => {
|
||
const [open, setOpen] = useState(item.proc.tipo_regiao !== 'GERAL');
|
||
const inp = "border border-gray-200 rounded-lg px-2 py-1.5 text-xs focus:ring-2 focus:ring-blue-100 outline-none w-full";
|
||
|
||
return (
|
||
<div className="border border-gray-200 rounded-xl bg-white overflow-hidden">
|
||
<div className="flex items-center gap-2 px-3 py-2.5">
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-xs font-black text-gray-800 uppercase truncate">{item.proc.nome}</p>
|
||
<div className="flex items-center gap-2 mt-0.5">
|
||
{item.proc.especialidadeNome && (
|
||
<span className="text-[9px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded-full font-bold uppercase">{item.proc.especialidadeNome}</span>
|
||
)}
|
||
<span className="text-[10px] font-bold text-green-600">{fmt(item.valorUnit * item.quantidade)}</span>
|
||
{item.quantidade > 1 && <span className="text-[9px] text-gray-400">{item.quantidade}× {fmt(item.valorUnit)}</span>}
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-1 flex-shrink-0">
|
||
<button onClick={() => onChange({ quantidade: Math.max(1, item.quantidade - 1) })} className="w-6 h-6 rounded-full border border-gray-200 flex items-center justify-center hover:bg-gray-100"><Minus size={10} /></button>
|
||
<span className="w-5 text-center text-xs font-bold">{item.quantidade}</span>
|
||
<button onClick={() => onChange({ quantidade: item.quantidade + 1 })} className="w-6 h-6 rounded-full border border-gray-200 flex items-center justify-center hover:bg-gray-100"><Plus size={10} /></button>
|
||
</div>
|
||
<button onClick={() => setOpen(o => !o)} className="text-gray-300 hover:text-gray-600 flex-shrink-0">
|
||
<ChevronDown size={14} className={`transition-transform ${open ? 'rotate-180' : ''}`} />
|
||
</button>
|
||
<button onClick={onRemove} className="text-gray-300 hover:text-red-500 flex-shrink-0"><Trash2 size={13} /></button>
|
||
</div>
|
||
|
||
{open && (
|
||
<div className="border-t border-gray-100 bg-gray-50/50 px-3 py-2.5 grid grid-cols-2 gap-2">
|
||
{item.proc.tipo_regiao === 'DENTE' && (
|
||
<div>
|
||
<label className="text-[9px] font-black text-gray-400 uppercase block mb-1">Dente(s)</label>
|
||
<input value={item.dente} onChange={e => onChange({ dente: e.target.value })} className={inp} placeholder="ex: 16, 21-22" />
|
||
</div>
|
||
)}
|
||
{item.proc.tipo_regiao === 'ARCO' && (
|
||
<div>
|
||
<label className="text-[9px] font-black text-gray-400 uppercase block mb-1">Arcada</label>
|
||
<select value={item.arco} onChange={e => onChange({ arco: e.target.value })} className={inp}>
|
||
<option value="">— Selecione —</option>
|
||
{ARCOS.map(a => <option key={a}>{a}</option>)}
|
||
</select>
|
||
</div>
|
||
)}
|
||
{item.proc.exige_face && (
|
||
<div>
|
||
<label className="text-[9px] font-black text-gray-400 uppercase block mb-1">Face(s)</label>
|
||
<div className="flex gap-1 flex-wrap">
|
||
{FACES.map(f => (
|
||
<button key={f} type="button"
|
||
onClick={() => {
|
||
const faces = item.face.split('').filter(Boolean);
|
||
const next = faces.includes(f) ? faces.filter(x => x !== f) : [...faces, f];
|
||
onChange({ face: next.join('') });
|
||
}}
|
||
className={`w-7 h-7 rounded-lg text-[10px] font-black border transition-colors ${item.face.includes(f) ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-500 border-gray-200 hover:border-blue-400'}`}>
|
||
{f}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div className={item.proc.tipo_regiao !== 'GERAL' || item.proc.exige_face ? '' : 'col-span-2'}>
|
||
<label className="text-[9px] font-black text-gray-400 uppercase block mb-1">Obs</label>
|
||
<input value={item.observacao} onChange={e => onChange({ observacao: e.target.value })} className={inp} placeholder="observação opcional" />
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ─── Modal principal ──────────────────────────────────────────────────────────
|
||
export const LancarProcedimentoModal: React.FC<{
|
||
patient: Paciente;
|
||
onClose: () => void;
|
||
onSaved: () => void;
|
||
}> = ({ patient, onClose, onSaved }) => {
|
||
const toast = useToast();
|
||
const { data: procsRaw } = useHybridBackend<Procedimento[]>(HybridBackend.getProcedimentos);
|
||
const { data: espsRaw } = useHybridBackend<Especialidade[]>(HybridBackend.getEspecialidades);
|
||
const { data: planosRaw } = useHybridBackend<Plano[]>(HybridBackend.getPlanos);
|
||
const procs = procsRaw ?? [];
|
||
const esps = espsRaw ?? [];
|
||
const planos = planosRaw ?? [];
|
||
|
||
const [busca, setBusca] = useState('');
|
||
const [espFiltro, setEspFiltro] = useState('');
|
||
const [items, setItems] = useState<LancItem[]>([]);
|
||
const [saving, setSaving] = useState(false);
|
||
|
||
// Pagamento
|
||
const [descTipo, setDescTipo] = useState<'R$' | '%'>('R$');
|
||
const [descValor, setDescValor] = useState('');
|
||
const [entrada, setEntrada] = useState('');
|
||
const [parcelas, setParcelas] = useState(1);
|
||
const [primVenc, setPrimVenc] = useState(new Date().toISOString().slice(0, 10));
|
||
const [formaPag, setFormaPag] = useState<typeof FORMAS_PAG[number]>('Pix');
|
||
const [statusPag, setStatusPag] = useState<typeof STATUS_LIST[number]>('Pendente');
|
||
|
||
// Plano vigente do paciente
|
||
const planoVigente = useMemo(() => {
|
||
if (!patient.convenio) return null;
|
||
return planos.find(p => p.nome === patient.convenio) ?? null;
|
||
}, [patient.convenio, planos]);
|
||
|
||
const getValorProc = (proc: Procedimento): number => {
|
||
if (planoVigente) {
|
||
const vp = proc.valoresPlanos?.find(v => v.planoId === planoVigente.id);
|
||
if (vp) return vp.valor;
|
||
}
|
||
return proc.valorParticular ?? 0;
|
||
};
|
||
|
||
const addProc = (proc: Procedimento) => {
|
||
if (items.some(i => i.proc.id === proc.id)) {
|
||
setItems(prev => prev.map(i => i.proc.id === proc.id ? { ...i, quantidade: i.quantidade + 1 } : i));
|
||
return;
|
||
}
|
||
setItems(prev => [...prev, {
|
||
uid: `${proc.id}_${Date.now()}`,
|
||
proc, quantidade: 1,
|
||
dente: '', face: '', arco: '', observacao: '',
|
||
valorUnit: getValorProc(proc),
|
||
}]);
|
||
};
|
||
|
||
const updateItem = (uid: string, patch: Partial<LancItem>) =>
|
||
setItems(prev => prev.map(i => i.uid === uid ? { ...i, ...patch } : i));
|
||
|
||
const removeItem = (uid: string) => setItems(prev => prev.filter(i => i.uid !== uid));
|
||
|
||
const subtotal = items.reduce((s, i) => s + i.valorUnit * i.quantidade, 0);
|
||
const descNum = parseFloat(descValor) || 0;
|
||
const descReais = descTipo === '%' ? subtotal * descNum / 100 : descNum;
|
||
const entradaNum = parseFloat(entrada) || 0;
|
||
const restante = Math.max(0, subtotal - descReais - entradaNum);
|
||
const valorParcela = parcelas > 0 ? restante / parcelas : 0;
|
||
|
||
const procsFiltrados = useMemo(() => {
|
||
return procs.filter(p => {
|
||
if (espFiltro && p.especialidadeId !== espFiltro) return false;
|
||
if (busca && !p.nome.toLowerCase().includes(busca.toLowerCase())) return false;
|
||
return true;
|
||
});
|
||
}, [procs, espFiltro, busca]);
|
||
|
||
const procsPorEsp = useMemo(() => {
|
||
const grupos: Record<string, { esp: Especialidade | null; procs: Procedimento[] }> = {};
|
||
procsFiltrados.forEach(p => {
|
||
const key = p.especialidadeId || '__geral';
|
||
if (!grupos[key]) {
|
||
grupos[key] = { esp: esps.find(e => e.id === p.especialidadeId) ?? null, procs: [] };
|
||
}
|
||
grupos[key].procs.push(p);
|
||
});
|
||
return Object.values(grupos).sort((a, b) => (a.esp?.ordem ?? 99) - (b.esp?.ordem ?? 99));
|
||
}, [procsFiltrados, esps]);
|
||
|
||
const handleSave = async () => {
|
||
if (items.length === 0) { toast.error('SELECIONE AO MENOS UM PROCEDIMENTO'); return; }
|
||
setSaving(true);
|
||
try {
|
||
const entriesToCreate: any[] = [];
|
||
const baseDesc = items.map(i => {
|
||
const region = [i.dente, i.arco, i.face].filter(Boolean).join('/');
|
||
return i.proc.nome + (region ? ` (${region})` : '') + (i.quantidade > 1 ? ` x${i.quantidade}` : '');
|
||
}).join(', ');
|
||
|
||
if (entradaNum > 0) {
|
||
entriesToCreate.push({
|
||
id: crypto.randomUUID(),
|
||
pacienteNome: patient.nome,
|
||
descricao: `ENTRADA — ${baseDesc}`,
|
||
valor: entradaNum,
|
||
dataVencimento: primVenc,
|
||
status: 'Pago',
|
||
formaPagamento: formaPag,
|
||
});
|
||
}
|
||
|
||
for (let i = 0; i < parcelas; i++) {
|
||
const label = parcelas > 1 ? ` — ${i + 1}/${parcelas}` : '';
|
||
entriesToCreate.push({
|
||
id: crypto.randomUUID(),
|
||
pacienteNome: patient.nome,
|
||
descricao: baseDesc + label,
|
||
valor: parseFloat(valorParcela.toFixed(2)),
|
||
dataVencimento: entradaNum > 0 ? addMonths(primVenc, i + 1) : addMonths(primVenc, i),
|
||
status: i === 0 && parcelas === 1 ? statusPag : (i === 0 ? statusPag : 'Pendente'),
|
||
formaPagamento: formaPag,
|
||
});
|
||
}
|
||
|
||
for (const entry of entriesToCreate) {
|
||
await HybridBackend.saveFinanceiro(entry);
|
||
}
|
||
|
||
toast.success(`${entriesToCreate.length} LANÇAMENTO(S) GERADO(S) COM SUCESSO!`);
|
||
onSaved();
|
||
onClose();
|
||
} catch { toast.error('ERRO AO SALVAR LANÇAMENTOS'); } finally { setSaving(false); }
|
||
};
|
||
|
||
const inp = "border border-gray-200 rounded-lg px-2.5 py-2 text-sm focus:ring-2 focus:ring-blue-100 outline-none w-full bg-white";
|
||
const jaAdicionado = (id: string) => items.some(i => i.proc.id === id);
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center backdrop-blur-sm p-0">
|
||
<div className="bg-white shadow-2xl w-full h-dvh md:w-[96vw] md:h-[98vh] rounded-none md:rounded-2xl flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
||
|
||
{/* Header */}
|
||
<div className="flex-shrink-0 px-5 py-4 border-b border-gray-100 flex items-center justify-between bg-gradient-to-r from-emerald-50 to-white">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-9 h-9 bg-emerald-100 rounded-xl flex items-center justify-center">
|
||
<DollarSign size={18} className="text-emerald-600" />
|
||
</div>
|
||
<div>
|
||
<h2 className="text-sm font-black text-gray-800 uppercase">Lançar Procedimento</h2>
|
||
<p className="text-[10px] text-gray-400 uppercase font-medium">
|
||
{patient.nome}
|
||
{planoVigente && <span className="ml-2 bg-emerald-100 text-emerald-700 px-1.5 py-0.5 rounded-full font-bold">{planoVigente.nome}</span>}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-1.5 hover:bg-gray-100 rounded-lg transition-colors"><X size={18} /></button>
|
||
</div>
|
||
|
||
{/* Body */}
|
||
<div className="flex flex-1 overflow-hidden">
|
||
|
||
{/* ── Catálogo (esquerda) ── */}
|
||
<div className="w-72 flex-shrink-0 border-r border-gray-100 flex flex-col bg-gray-50/30">
|
||
{/* Busca + filtro especialidade */}
|
||
<div className="p-3 space-y-2 border-b border-gray-100 flex-shrink-0">
|
||
<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 procedimento..."
|
||
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 bg-white" />
|
||
</div>
|
||
<div className="flex gap-1 flex-wrap">
|
||
<button onClick={() => setEspFiltro('')}
|
||
className={`text-[10px] font-bold px-2 py-0.5 rounded-full border transition-colors ${!espFiltro ? 'bg-gray-800 text-white border-gray-800' : 'text-gray-500 border-gray-200 hover:bg-gray-100'}`}>
|
||
Todas
|
||
</button>
|
||
{esps.map(e => (
|
||
<button key={e.id} onClick={() => setEspFiltro(f => f === e.id ? '' : e.id)}
|
||
className={`text-[10px] font-bold px-2 py-0.5 rounded-full border transition-colors ${espFiltro === e.id ? 'bg-blue-600 text-white border-blue-600' : 'text-gray-500 border-gray-200 hover:bg-gray-100'}`}>
|
||
{e.nome}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Lista procedimentos */}
|
||
<div className="flex-1 overflow-y-auto p-2 space-y-3">
|
||
{!procsRaw && <div className="flex justify-center pt-8"><Loader2 size={18} className="animate-spin text-gray-300" /></div>}
|
||
{procsPorEsp.map((grupo, gi) => (
|
||
<div key={gi}>
|
||
{grupo.esp && (
|
||
<p className="text-[9px] font-black text-gray-400 uppercase tracking-widest px-2 mb-1">{grupo.esp.nome}</p>
|
||
)}
|
||
<div className="space-y-0.5">
|
||
{grupo.procs.map(p => {
|
||
const val = getValorProc(p);
|
||
const adicionado = jaAdicionado(p.id);
|
||
return (
|
||
<button key={p.id} onClick={() => addProc(p)}
|
||
className={`w-full text-left px-2.5 py-2 rounded-lg transition-all flex items-center justify-between gap-2 group ${adicionado ? 'bg-emerald-50 border border-emerald-200' : 'border border-transparent hover:bg-white hover:border-gray-200 hover:shadow-sm'}`}>
|
||
<div className="min-w-0 flex-1">
|
||
<p className={`text-xs font-bold truncate ${adicionado ? 'text-emerald-700' : 'text-gray-700'}`}>{p.nome}</p>
|
||
{p.codigo && <p className="text-[9px] text-gray-400">{p.codigo}</p>}
|
||
</div>
|
||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||
<span className={`text-[10px] font-black ${adicionado ? 'text-emerald-600' : 'text-green-600'}`}>{fmt(val)}</span>
|
||
{adicionado
|
||
? <Check size={13} className="text-emerald-500" />
|
||
: <Plus size={13} className="text-gray-300 group-hover:text-blue-500" />}
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
))}
|
||
{procsRaw && procsFiltrados.length === 0 && (
|
||
<p className="text-xs text-gray-400 text-center pt-6">Nenhum procedimento encontrado</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── Selecionados + pagamento (direita) ── */}
|
||
<div className="flex-1 flex flex-col overflow-hidden">
|
||
{/* Items selecionados */}
|
||
<div className="flex-1 overflow-y-auto p-4 space-y-2">
|
||
{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">
|
||
<Tag size={24} className="text-gray-300" />
|
||
</div>
|
||
<p className="text-sm font-bold text-gray-400">Nenhum procedimento selecionado</p>
|
||
<p className="text-xs text-gray-300 mt-1">Clique nos procedimentos ao lado para adicionar</p>
|
||
</div>
|
||
) : (
|
||
items.map(item => (
|
||
<ItemCard key={item.uid} item={item}
|
||
onChange={patch => updateItem(item.uid, patch)}
|
||
onRemove={() => removeItem(item.uid)} />
|
||
))
|
||
)}
|
||
</div>
|
||
|
||
{/* ── Painel financeiro ── */}
|
||
<div className="flex-shrink-0 border-t border-gray-100 bg-gray-50/50 p-4 space-y-3">
|
||
{/* Subtotal */}
|
||
<div className="flex items-center justify-between text-sm">
|
||
<span className="font-bold text-gray-500 uppercase text-xs">Subtotal</span>
|
||
<span className="font-black text-gray-800 text-base">{fmt(subtotal)}</span>
|
||
</div>
|
||
|
||
{/* Desconto + entrada */}
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className="text-[9px] font-black text-gray-400 uppercase block mb-1">Desconto</label>
|
||
<div className="flex gap-1">
|
||
<button onClick={() => setDescTipo(t => t === 'R$' ? '%' : 'R$')}
|
||
className="px-2 py-2 border border-gray-200 rounded-lg text-[10px] font-black text-gray-500 hover:bg-gray-100 flex-shrink-0 bg-white">
|
||
{descTipo === 'R$' ? <Banknote size={12} /> : <Percent size={12} />}
|
||
</button>
|
||
<input type="number" step="0.01" value={descValor} onChange={e => setDescValor(e.target.value)}
|
||
placeholder="0" className={inp + " text-sm"} />
|
||
</div>
|
||
{descReais > 0 && <p className="text-[10px] text-red-500 mt-0.5">− {fmt(descReais)}</p>}
|
||
</div>
|
||
<div>
|
||
<label className="text-[9px] font-black text-gray-400 uppercase block mb-1">Entrada (R$)</label>
|
||
<input type="number" step="0.01" value={entrada} onChange={e => setEntrada(e.target.value)}
|
||
placeholder="0" className={inp} />
|
||
{entradaNum > 0 && <p className="text-[10px] text-blue-500 mt-0.5">− {fmt(entradaNum)} (marca como Pago)</p>}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Restante + parcelas */}
|
||
<div className="bg-white rounded-xl border border-gray-200 p-3 space-y-2.5">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-xs font-black text-gray-500 uppercase">Restante a parcelar</span>
|
||
<span className="text-lg font-black text-emerald-600">{fmt(restante)}</span>
|
||
</div>
|
||
<div className="grid grid-cols-3 gap-2">
|
||
<div>
|
||
<label className="text-[9px] font-black text-gray-400 uppercase block mb-1">Parcelas</label>
|
||
<select value={parcelas} onChange={e => setParcelas(Number(e.target.value))} className={inp + " text-xs"}>
|
||
{Array.from({ length: 48 }, (_, i) => i + 1).map(n => (
|
||
<option key={n} value={n}>{n}x — {fmt(restante / n)}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="text-[9px] font-black text-gray-400 uppercase block mb-1">1ª Parcela</label>
|
||
<input type="date" value={primVenc} onChange={e => setPrimVenc(e.target.value)} className={inp + " text-xs"} />
|
||
</div>
|
||
<div>
|
||
<label className="text-[9px] font-black text-gray-400 uppercase block mb-1">Forma</label>
|
||
<select value={formaPag} onChange={e => setFormaPag(e.target.value as any)} className={inp + " text-xs"}>
|
||
{FORMAS_PAG.map(f => <option key={f}>{f}</option>)}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
{parcelas > 1 && (
|
||
<div className="flex items-center justify-between bg-emerald-50 rounded-lg px-3 py-2">
|
||
<span className="text-xs font-bold text-emerald-700">{parcelas}x de</span>
|
||
<span className="text-sm font-black text-emerald-700">{fmt(valorParcela)}</span>
|
||
</div>
|
||
)}
|
||
<div className="grid grid-cols-2 gap-2">
|
||
<div>
|
||
<label className="text-[9px] font-black text-gray-400 uppercase block mb-1">Status 1ª parcela</label>
|
||
<select value={statusPag} onChange={e => setStatusPag(e.target.value as any)} className={inp + " text-xs"}>
|
||
{STATUS_LIST.map(s => <option key={s}>{s}</option>)}
|
||
</select>
|
||
</div>
|
||
<div className="flex items-end">
|
||
<div className="w-full bg-gray-50 border border-gray-100 rounded-lg p-2 text-center">
|
||
<p className="text-[9px] font-black text-gray-400 uppercase">Total geral</p>
|
||
<p className="text-sm font-black text-gray-800">{fmt(entradaNum + restante)}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Ações */}
|
||
<div className="flex gap-2 pt-1">
|
||
<button onClick={onClose} className="px-5 py-2.5 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-lg text-xs uppercase flex-shrink-0">Cancelar</button>
|
||
<button onClick={handleSave} disabled={items.length === 0 || saving}
|
||
className="flex-1 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white font-black rounded-lg text-xs uppercase disabled:opacity-40 flex items-center justify-center gap-2 transition-colors">
|
||
{saving ? <Loader2 size={14} className="animate-spin" /> : <Check size={14} />}
|
||
{saving ? 'Salvando...' : `Confirmar ${items.length > 0 ? `— ${fmt(entradaNum + restante)}` : ''}`}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|