154 lines
7.1 KiB
TypeScript
154 lines
7.1 KiB
TypeScript
|
|
import { useState, useEffect } from 'react';
|
||
|
|
import api from '../services/api';
|
||
|
|
import {
|
||
|
|
ShieldCheck, AlertCircle, CheckCircle2,
|
||
|
|
User as UserIcon, Clock
|
||
|
|
} from 'lucide-react';
|
||
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
||
|
|
import { useToast } from '../context/use-toast';
|
||
|
|
import clsx from 'clsx';
|
||
|
|
|
||
|
|
export default function TreasuryPage() {
|
||
|
|
const { showToast } = useToast();
|
||
|
|
const [pending, setPending] = useState<any[]>([]);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [selectedSession, setSelectedSession] = useState<any>(null);
|
||
|
|
const [notes, setNotes] = useState('');
|
||
|
|
|
||
|
|
const loadPending = async () => {
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
const res = await api.get('/cashsessions/pending');
|
||
|
|
setPending(res.data);
|
||
|
|
} catch (e) {
|
||
|
|
showToast("Error al cargar sesiones pendientes", "error");
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
useEffect(() => { loadPending(); }, []);
|
||
|
|
|
||
|
|
const handleValidate = async () => {
|
||
|
|
if (!selectedSession) return;
|
||
|
|
try {
|
||
|
|
await api.post(`/cashsessions/${selectedSession.id}/validate`, JSON.stringify(notes), {
|
||
|
|
headers: { 'Content-Type': 'application/json' }
|
||
|
|
});
|
||
|
|
showToast("Caja liquidada y archivada", "success");
|
||
|
|
setSelectedSession(null);
|
||
|
|
setNotes('');
|
||
|
|
loadPending();
|
||
|
|
} catch (e) {
|
||
|
|
showToast("Error al validar", "error");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (loading) return <div className="p-20 text-center uppercase font-black text-xs text-slate-400 animate-pulse">Cargando Tesorería...</div>;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="p-8 flex flex-col gap-8 bg-[#f8fafc] h-full">
|
||
|
|
<header>
|
||
|
|
<span className="text-[10px] font-black text-blue-600 uppercase tracking-[0.3em] mb-1 block">Administración Central</span>
|
||
|
|
<h2 className="text-3xl font-black text-slate-900 tracking-tight uppercase">Validación de Cajas</h2>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
|
||
|
|
|
||
|
|
{/* LISTADO DE CAJAS PENDIENTES */}
|
||
|
|
<div className="lg:col-span-7 space-y-4">
|
||
|
|
<h4 className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-2">Sesiones esperando cierre definitivo</h4>
|
||
|
|
{pending.length === 0 ? (
|
||
|
|
<div className="bg-white p-20 rounded-[2.5rem] border-2 border-dashed border-slate-200 text-center opacity-40">
|
||
|
|
<CheckCircle2 size={48} className="mx-auto mb-4 text-emerald-500" />
|
||
|
|
<p className="font-black text-xs uppercase tracking-widest">No hay cajas pendientes de validación</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
pending.map(s => (
|
||
|
|
<motion.div
|
||
|
|
key={s.id}
|
||
|
|
whileHover={{ x: 5 }}
|
||
|
|
onClick={() => setSelectedSession(s)}
|
||
|
|
className={clsx(
|
||
|
|
"p-6 bg-white rounded-[2rem] border-2 transition-all cursor-pointer flex justify-between items-center group",
|
||
|
|
selectedSession?.id === s.id ? "border-blue-600 shadow-xl shadow-blue-100" : "border-slate-100 hover:border-blue-200 shadow-sm"
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
<div className="flex items-center gap-6">
|
||
|
|
<div className="w-14 h-14 bg-slate-100 rounded-2xl flex items-center justify-center text-slate-400 group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||
|
|
<UserIcon size={24} />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<p className="font-black text-slate-900 uppercase text-sm">{s.username}</p>
|
||
|
|
<p className="text-[10px] font-bold text-slate-400 uppercase flex items-center gap-1.5 mt-1">
|
||
|
|
<Clock size={12} /> Cerrada: {new Date(s.closingDate).toLocaleTimeString()}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="text-right">
|
||
|
|
<p className="text-xl font-mono font-black text-slate-900">$ {(s.declaredCash + s.declaredCards + s.declaredTransfers).toLocaleString()}</p>
|
||
|
|
<span className="text-[9px] font-black text-blue-600 bg-blue-50 px-2 py-1 rounded-md mt-1 inline-block">PENDIENTE LIQUIDAR</span>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
))
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* PANEL DE ACCIÓN (DERECHA) */}
|
||
|
|
<div className="lg:col-span-5">
|
||
|
|
<AnimatePresence mode="wait">
|
||
|
|
{selectedSession ? (
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 10 }}
|
||
|
|
className="bg-slate-900 rounded-[2.5rem] p-8 text-white shadow-2xl relative overflow-hidden"
|
||
|
|
>
|
||
|
|
<div className="absolute top-0 right-0 p-8 opacity-5"><ShieldCheck size={120} /></div>
|
||
|
|
<h3 className="text-xl font-black uppercase mb-8 flex items-center gap-3">
|
||
|
|
<AlertCircle className="text-blue-400" /> Detalle de Liquidación
|
||
|
|
</h3>
|
||
|
|
|
||
|
|
<div className="space-y-6 relative z-10">
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<TreasuryStat label="Efectivo Declarado" value={selectedSession.declaredCash} />
|
||
|
|
<TreasuryStat label="Diferencia" value={selectedSession.totalDifference} color={selectedSession.totalDifference >= 0 ? "text-emerald-400" : "text-rose-400"} />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="bg-white/5 rounded-3xl p-6 border border-white/10 space-y-4">
|
||
|
|
<label className="text-[10px] font-black text-blue-400 uppercase tracking-widest block">Observaciones de Tesorería</label>
|
||
|
|
<textarea
|
||
|
|
value={notes}
|
||
|
|
onChange={e => setNotes(e.target.value)}
|
||
|
|
placeholder="Indique si el dinero coincide con el sobre entregado..."
|
||
|
|
className="w-full bg-transparent border-none outline-none text-sm font-medium placeholder:opacity-20 min-h-[100px] resize-none"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<button
|
||
|
|
onClick={handleValidate}
|
||
|
|
className="w-full py-5 bg-blue-600 hover:bg-blue-700 text-white rounded-2xl font-black uppercase text-xs tracking-widest shadow-xl shadow-blue-500/20 transition-all flex items-center justify-center gap-3"
|
||
|
|
>
|
||
|
|
<CheckCircle2 size={18} /> Validar y Archivar Caja
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
) : (
|
||
|
|
<div className="p-10 text-center text-slate-300 border-2 border-dashed border-slate-200 rounded-[2.5rem]">
|
||
|
|
<p className="text-xs font-black uppercase tracking-widest">Seleccione una caja para auditar</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</AnimatePresence>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function TreasuryStat({ label, value, color = "text-white" }: any) {
|
||
|
|
return (
|
||
|
|
<div className="bg-white/5 p-4 rounded-2xl border border-white/10">
|
||
|
|
<span className="text-[9px] font-black text-slate-500 uppercase block mb-1">{label}</span>
|
||
|
|
<span className={clsx("text-lg font-mono font-black", color)}>$ {value.toLocaleString()}</span>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|