Spaces:
Running
Running
Upload 34 files
Browse files- app/billing/page.tsx +276 -0
- app/layout.tsx +7 -4
- app/login/page.tsx +81 -66
- components/AuthGuard.tsx +46 -0
- components/Sidebar.tsx +2 -0
app/billing/page.tsx
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React, { useState } from "react";
|
| 4 |
+
import { motion, AnimatePresence } from "framer-motion";
|
| 5 |
+
import {
|
| 6 |
+
FileText,
|
| 7 |
+
Download,
|
| 8 |
+
Eye,
|
| 9 |
+
Search,
|
| 10 |
+
Filter,
|
| 11 |
+
CreditCard,
|
| 12 |
+
CheckCircle2,
|
| 13 |
+
Clock,
|
| 14 |
+
AlertCircle,
|
| 15 |
+
TrendingUp,
|
| 16 |
+
Receipt,
|
| 17 |
+
User,
|
| 18 |
+
Calendar,
|
| 19 |
+
DollarSign
|
| 20 |
+
} from "lucide-react";
|
| 21 |
+
|
| 22 |
+
const mockInvoices = [
|
| 23 |
+
{
|
| 24 |
+
id: "INV-2023-001",
|
| 25 |
+
customer: "Nexus Corp",
|
| 26 |
+
date: "2023-11-15",
|
| 27 |
+
amount: 15420.50,
|
| 28 |
+
status: "paid",
|
| 29 |
+
items: [
|
| 30 |
+
{ description: "Soporte Técnico Enterprise", qty: 1, price: 5000 },
|
| 31 |
+
{ description: "Licencia Anual ERP Nexus", qty: 1, price: 8000 },
|
| 32 |
+
{ description: "Implementación Cloud", qty: 1, price: 2420.50 }
|
| 33 |
+
]
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
id: "INV-2023-002",
|
| 37 |
+
customer: "Global Tech Solutions",
|
| 38 |
+
date: "2023-11-18",
|
| 39 |
+
amount: 4200.00,
|
| 40 |
+
status: "pending",
|
| 41 |
+
items: [
|
| 42 |
+
{ description: "Consultoría IT", qty: 10, price: 420 }
|
| 43 |
+
]
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
id: "INV-2023-003",
|
| 47 |
+
customer: "Alina Systems",
|
| 48 |
+
date: "2023-11-20",
|
| 49 |
+
amount: 1250.75,
|
| 50 |
+
status: "overdue",
|
| 51 |
+
items: [
|
| 52 |
+
{ description: "Mantenimiento Preventivo", qty: 5, price: 250.15 }
|
| 53 |
+
]
|
| 54 |
+
},
|
| 55 |
+
];
|
| 56 |
+
|
| 57 |
+
export default function BillingPage() {
|
| 58 |
+
const [selectedInvoice, setSelectedInvoice] = useState<any>(null);
|
| 59 |
+
|
| 60 |
+
return (
|
| 61 |
+
<div className="p-10 bg-[#0f172a] min-h-screen text-white">
|
| 62 |
+
<header className="mb-12 flex justify-between items-end">
|
| 63 |
+
<div>
|
| 64 |
+
<h1 className="text-5xl font-black tracking-tighter italic bg-gradient-to-r from-white to-gray-500 bg-clip-text text-transparent">
|
| 65 |
+
FACTURACIÓN <span className="text-blue-500">/ BILLING</span>
|
| 66 |
+
</h1>
|
| 67 |
+
<p className="text-gray-500 font-bold mt-2 uppercase text-xs tracking-[0.3em]">Gestión de Ingresos y Comprobantes Fiscales</p>
|
| 68 |
+
</div>
|
| 69 |
+
<div className="flex gap-4">
|
| 70 |
+
<button className="bg-white/5 border border-white/10 px-6 py-3 rounded-2xl text-xs font-black uppercase tracking-widest hover:bg-white/10 transition-all flex items-center gap-2">
|
| 71 |
+
<Download size={16} /> Exportar CSV
|
| 72 |
+
</button>
|
| 73 |
+
<button className="bg-blue-600 px-6 py-3 rounded-2xl text-xs font-black uppercase tracking-widest hover:bg-blue-500 transition-all shadow-xl shadow-blue-900/40">
|
| 74 |
+
Nueva Factura
|
| 75 |
+
</button>
|
| 76 |
+
</div>
|
| 77 |
+
</header>
|
| 78 |
+
|
| 79 |
+
{/* Stats Quick View */}
|
| 80 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
| 81 |
+
<StatWidget label="Recaudado Mes" value="$20,871.25" icon={<TrendingUp className="text-emerald-400" />} />
|
| 82 |
+
<StatWidget label="Pendiente Cobro" value="$4,200.00" icon={<Clock className="text-orange-400" />} />
|
| 83 |
+
<StatWidget label="Facturas Activas" value="28" icon={<Receipt className="text-blue-400" />} />
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
| 87 |
+
{/* Invoice List */}
|
| 88 |
+
<div className="lg:col-span-2 space-y-4">
|
| 89 |
+
<div className="bg-white/5 border border-white/10 rounded-[2.5rem] p-8">
|
| 90 |
+
<div className="flex justify-between items-center mb-8">
|
| 91 |
+
<h2 className="text-lg font-black italic uppercase tracking-widest text-gray-400">Historial de <span className="text-white">Facturas</span></h2>
|
| 92 |
+
<div className="flex gap-2">
|
| 93 |
+
<div className="relative">
|
| 94 |
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={14} />
|
| 95 |
+
<input className="bg-white/5 border border-white/10 rounded-xl py-2 pl-9 pr-4 text-xs focus:ring-1 focus:ring-blue-500 outline-none w-48" placeholder="Buscar..." />
|
| 96 |
+
</div>
|
| 97 |
+
<button className="bg-white/5 border border-white/10 p-2 rounded-xl text-gray-400 hover:text-white"><Filter size={16} /></button>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<div className="space-y-3">
|
| 102 |
+
{mockInvoices.map((inv) => (
|
| 103 |
+
<motion.div
|
| 104 |
+
key={inv.id}
|
| 105 |
+
whileHover={{ x: 10, backgroundColor: "rgba(255,255,255,0.05)" }}
|
| 106 |
+
onClick={() => setSelectedInvoice(inv)}
|
| 107 |
+
className={`flex items-center justify-between p-4 rounded-2xl border border-white/5 cursor-pointer transition-all ${selectedInvoice?.id === inv.id ? 'bg-white/10 border-blue-500/50 shadow-lg' : 'hover:border-white/10'}`}
|
| 108 |
+
>
|
| 109 |
+
<div className="flex items-center gap-4">
|
| 110 |
+
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${
|
| 111 |
+
inv.status === 'paid' ? 'bg-emerald-500/10 text-emerald-400' :
|
| 112 |
+
inv.status === 'pending' ? 'bg-orange-500/10 text-orange-400' : 'bg-red-500/10 text-red-400'
|
| 113 |
+
}`}>
|
| 114 |
+
<FileText size={18} />
|
| 115 |
+
</div>
|
| 116 |
+
<div>
|
| 117 |
+
<p className="text-sm font-black italic">{inv.id}</p>
|
| 118 |
+
<p className="text-[10px] text-gray-500 font-bold uppercase">{inv.customer}</p>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
<div className="text-right">
|
| 122 |
+
<p className="text-sm font-black tracking-tighter">${inv.amount.toLocaleString()}</p>
|
| 123 |
+
<p className="text-[9px] text-gray-500 font-medium">{inv.date}</p>
|
| 124 |
+
</div>
|
| 125 |
+
<div className="flex items-center gap-2">
|
| 126 |
+
<StatusBadge status={inv.status} />
|
| 127 |
+
<button className="p-2 text-gray-500 hover:text-white transition-colors">
|
| 128 |
+
<Eye size={16} />
|
| 129 |
+
</button>
|
| 130 |
+
</div>
|
| 131 |
+
</motion.div>
|
| 132 |
+
))}
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
{/* Invoice Detail Breakdown */}
|
| 138 |
+
<div className="lg:col-span-1">
|
| 139 |
+
<AnimatePresence mode="wait">
|
| 140 |
+
{selectedInvoice ? (
|
| 141 |
+
<motion.div
|
| 142 |
+
key={selectedInvoice.id}
|
| 143 |
+
initial={{ opacity: 0, x: 20 }}
|
| 144 |
+
animate={{ opacity: 1, x: 0 }}
|
| 145 |
+
exit={{ opacity: 0, x: 20 }}
|
| 146 |
+
className="bg-white text-slate-900 rounded-[3rem] p-8 shadow-2xl relative overflow-hidden h-full flex flex-col"
|
| 147 |
+
>
|
| 148 |
+
{/* Decorative Elements */}
|
| 149 |
+
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-50 rounded-bl-[100%] z-0"></div>
|
| 150 |
+
|
| 151 |
+
<div className="relative z-10 flex-1">
|
| 152 |
+
<div className="flex justify-between items-start mb-10">
|
| 153 |
+
<div>
|
| 154 |
+
<h3 className="text-2xl font-black tracking-tighter italic uppercase text-blue-600">INVOICE</h3>
|
| 155 |
+
<p className="text-[10px] font-black uppercase text-slate-400">{selectedInvoice.id}</p>
|
| 156 |
+
</div>
|
| 157 |
+
<div className="w-12 h-12 bg-blue-600 rounded-2xl flex items-center justify-center text-white font-black italic text-xl">N</div>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<div className="grid grid-cols-2 gap-6 mb-10">
|
| 161 |
+
<div className="space-y-1">
|
| 162 |
+
<p className="text-[9px] font-black uppercase text-slate-400 tracking-widest flex items-center gap-1"><User size={10} /> Cliente</p>
|
| 163 |
+
<p className="text-xs font-bold text-slate-700">{selectedInvoice.customer}</p>
|
| 164 |
+
</div>
|
| 165 |
+
<div className="space-y-1">
|
| 166 |
+
<p className="text-[9px] font-black uppercase text-slate-400 tracking-widest flex items-center gap-1"><Calendar size={10} /> Fecha</p>
|
| 167 |
+
<p className="text-xs font-bold text-slate-700">{selectedInvoice.date}</p>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
{/* Items Table */}
|
| 172 |
+
<div className="border-t border-slate-100 pt-6 mb-10">
|
| 173 |
+
<p className="text-[9px] font-black uppercase text-slate-400 tracking-widest mb-4">Desglose de Servicios</p>
|
| 174 |
+
<div className="space-y-4">
|
| 175 |
+
{selectedInvoice.items.map((item: any, idx: number) => (
|
| 176 |
+
<div key={idx} className="flex justify-between items-center text-sm border-b border-slate-50 pb-2 last:border-0">
|
| 177 |
+
<div className="flex flex-col">
|
| 178 |
+
<span className="font-bold text-slate-700">{item.description}</span>
|
| 179 |
+
<span className="text-[10px] text-slate-400 font-medium">Cant: {item.qty} x ${item.price.toLocaleString()}</span>
|
| 180 |
+
</div>
|
| 181 |
+
<span className="font-black text-slate-800">${(item.qty * item.price).toLocaleString()}</span>
|
| 182 |
+
</div>
|
| 183 |
+
))}
|
| 184 |
+
</div>
|
| 185 |
+
</div>
|
| 186 |
+
|
| 187 |
+
{/* Calculations */}
|
| 188 |
+
<div className="space-y-2 mt-auto">
|
| 189 |
+
<div className="flex justify-between text-xs font-medium text-slate-400">
|
| 190 |
+
<span>Subtotal</span>
|
| 191 |
+
<span>${(selectedInvoice.amount * 0.84).toLocaleString()}</span>
|
| 192 |
+
</div>
|
| 193 |
+
<div className="flex justify-between text-xs font-medium text-slate-400 border-b border-slate-100 pb-2">
|
| 194 |
+
<span>IVA (16%)</span>
|
| 195 |
+
<span>${(selectedInvoice.amount * 0.16).toLocaleString()}</span>
|
| 196 |
+
</div>
|
| 197 |
+
<div className="flex justify-between items-center pt-2">
|
| 198 |
+
<span className="text-sm font-black uppercase tracking-widest text-slate-800">Total Neto</span>
|
| 199 |
+
<span className="text-2xl font-black italic tracking-tighter text-blue-600">${selectedInvoice.amount.toLocaleString()}</span>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
|
| 204 |
+
<button className="mt-10 w-full bg-slate-900 text-white rounded-2xl py-4 font-black uppercase tracking-widest text-xs flex items-center justify-center gap-2 hover:bg-slate-800 active:scale-95 transition-all">
|
| 205 |
+
<Download size={16} /> Descargar PDF
|
| 206 |
+
</button>
|
| 207 |
+
</motion.div>
|
| 208 |
+
) : (
|
| 209 |
+
<div className="bg-white/5 border border-white/10 border-dashed rounded-[3rem] p-8 flex flex-col items-center justify-center text-center h-[500px]">
|
| 210 |
+
<div className="w-16 h-16 bg-white/5 rounded-full flex items-center justify-center mb-6">
|
| 211 |
+
<AlertCircle className="text-gray-500" size={32} />
|
| 212 |
+
</div>
|
| 213 |
+
<h3 className="font-black italic uppercase tracking-widest text-gray-400 mb-2">Seleccione una factura</h3>
|
| 214 |
+
<p className="text-xs text-gray-600 font-medium max-w-[200px]">Haga clic en una factura de la lista para ver el desglose detallado para el cliente.</p>
|
| 215 |
+
</div>
|
| 216 |
+
)}
|
| 217 |
+
</AnimatePresence>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
|
| 221 |
+
{/* Payment Methods Footer */}
|
| 222 |
+
<div className="mt-12 bg-white/5 border border-white/10 rounded-[2.5rem] p-8 flex items-center justify-between">
|
| 223 |
+
<div className="flex items-center gap-6">
|
| 224 |
+
<div className="flex items-center gap-2">
|
| 225 |
+
<div className="w-2 h-2 rounded-full bg-emerald-500 drop-shadow-[0_0_8px_rgba(16,185,129,0.5)]"></div>
|
| 226 |
+
<span className="text-[10px] font-black uppercase tracking-widest text-gray-400">Gateways Activos</span>
|
| 227 |
+
</div>
|
| 228 |
+
<div className="flex gap-4">
|
| 229 |
+
<div className="h-4 w-12 bg-white/10 rounded"></div>
|
| 230 |
+
<div className="h-4 w-12 bg-white/10 rounded"></div>
|
| 231 |
+
<div className="h-4 w-12 bg-white/10 rounded"></div>
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
<div className="flex items-center gap-4 text-[10px] font-black uppercase tracking-widest text-gray-500">
|
| 235 |
+
<span className="flex items-center gap-1"><CreditCard size={12} /> Pagos Automáticos</span>
|
| 236 |
+
<span className="w-1 h-1 bg-white/10 rounded-full"></span>
|
| 237 |
+
<span className="flex items-center gap-1"><DollarSign size={12} /> Liquidación Instantánea</span>
|
| 238 |
+
</div>
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
);
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
function StatWidget({ label, value, icon }: any) {
|
| 245 |
+
return (
|
| 246 |
+
<div className="bg-white/5 border border-white/10 rounded-[2rem] p-6 relative group overflow-hidden">
|
| 247 |
+
<div className="absolute top-0 left-0 w-1 h-full bg-blue-500/20 group-hover:bg-blue-500 transition-colors"></div>
|
| 248 |
+
<div className="flex justify-between items-start">
|
| 249 |
+
<div className="space-y-1">
|
| 250 |
+
<p className="text-[10px] font-black uppercase tracking-widest text-gray-500">{label}</p>
|
| 251 |
+
<p className="text-3xl font-black italic tracking-tighter">{value}</p>
|
| 252 |
+
</div>
|
| 253 |
+
<div className="w-10 h-10 bg-white/5 rounded-xl flex items-center justify-center">
|
| 254 |
+
{icon}
|
| 255 |
+
</div>
|
| 256 |
+
</div>
|
| 257 |
+
</div>
|
| 258 |
+
);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
function StatusBadge({ status }: { status: "paid" | "pending" | "overdue" | string }) {
|
| 262 |
+
const styles: Record<string, { bg: string; text: string; label: string; icon: React.ReactNode }> = {
|
| 263 |
+
paid: { bg: "bg-emerald-500/10", text: "text-emerald-400", label: "Pagado", icon: <CheckCircle2 size={10} /> },
|
| 264 |
+
pending: { bg: "bg-orange-500/10", text: "text-orange-400", label: "Pendiente", icon: <Clock size={10} /> },
|
| 265 |
+
overdue: { bg: "bg-red-500/10", text: "text-red-400", label: "Vencido", icon: <AlertCircle size={10} /> },
|
| 266 |
+
};
|
| 267 |
+
|
| 268 |
+
const style = styles[status] || { bg: "bg-gray-500/10", text: "text-gray-400", label: "Desconocido", icon: null };
|
| 269 |
+
|
| 270 |
+
return (
|
| 271 |
+
<div className={`${style.bg} ${style.text} px-3 py-1.5 rounded-full text-[9px] font-black uppercase flex items-center gap-1 tracking-wider border border-white/5`}>
|
| 272 |
+
{style.icon}
|
| 273 |
+
{style.label}
|
| 274 |
+
</div>
|
| 275 |
+
);
|
| 276 |
+
}
|
app/layout.tsx
CHANGED
|
@@ -10,6 +10,7 @@ export const metadata: Metadata = {
|
|
| 10 |
};
|
| 11 |
|
| 12 |
import Sidebar from "@/components/Sidebar";
|
|
|
|
| 13 |
|
| 14 |
export default function RootLayout({
|
| 15 |
children,
|
|
@@ -19,10 +20,12 @@ export default function RootLayout({
|
|
| 19 |
return (
|
| 20 |
<html lang="es">
|
| 21 |
<body className={`${inter.className} bg-[#0f172a] text-white selection:bg-blue-500/30`}>
|
| 22 |
-
<
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
| 26 |
</body>
|
| 27 |
</html>
|
| 28 |
);
|
|
|
|
| 10 |
};
|
| 11 |
|
| 12 |
import Sidebar from "@/components/Sidebar";
|
| 13 |
+
import AuthGuard from "@/components/AuthGuard";
|
| 14 |
|
| 15 |
export default function RootLayout({
|
| 16 |
children,
|
|
|
|
| 20 |
return (
|
| 21 |
<html lang="es">
|
| 22 |
<body className={`${inter.className} bg-[#0f172a] text-white selection:bg-blue-500/30`}>
|
| 23 |
+
<AuthGuard>
|
| 24 |
+
<Sidebar />
|
| 25 |
+
<main className="ml-72 min-h-screen">
|
| 26 |
+
{children}
|
| 27 |
+
</main>
|
| 28 |
+
</AuthGuard>
|
| 29 |
</body>
|
| 30 |
</html>
|
| 31 |
);
|
app/login/page.tsx
CHANGED
|
@@ -1,24 +1,19 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
-
import React, { useState
|
|
|
|
| 4 |
import { auth } from "@/lib/firebase";
|
| 5 |
-
import { signInWithEmailAndPassword, onAuthStateChanged } from "firebase/auth";
|
| 6 |
import { useRouter } from "next/navigation";
|
|
|
|
|
|
|
| 7 |
|
| 8 |
export default function LoginPage() {
|
| 9 |
-
const [email, setEmail] = useState("");
|
| 10 |
-
const [password, setPassword] = useState("");
|
| 11 |
const [error, setError] = useState("");
|
| 12 |
const [loading, setLoading] = useState(false);
|
| 13 |
const router = useRouter();
|
| 14 |
|
| 15 |
-
useEffect(() => {
|
| 16 |
-
const unsub = onAuthStateChanged(auth, (user) => {
|
| 17 |
-
if (user) router.push("/");
|
| 18 |
-
});
|
| 19 |
-
return () => unsub();
|
| 20 |
-
}, [router]);
|
| 21 |
-
|
| 22 |
const handleLogin = async (e: React.FormEvent) => {
|
| 23 |
e.preventDefault();
|
| 24 |
setLoading(true);
|
|
@@ -27,81 +22,101 @@ export default function LoginPage() {
|
|
| 27 |
await signInWithEmailAndPassword(auth, email, password);
|
| 28 |
router.push("/");
|
| 29 |
} catch (err: any) {
|
| 30 |
-
setError(
|
| 31 |
-
} finally {
|
| 32 |
setLoading(false);
|
| 33 |
}
|
| 34 |
};
|
| 35 |
|
| 36 |
return (
|
| 37 |
-
<div className="
|
| 38 |
-
{/* Background
|
| 39 |
-
<div className="absolute top-
|
| 40 |
-
|
| 41 |
-
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-purple-600/20 blur-[120px] rounded-full"></div>
|
| 42 |
-
</div>
|
| 43 |
|
| 44 |
-
<div
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
| 51 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
| 59 |
value={email}
|
| 60 |
onChange={(e) => setEmail(e.target.value)}
|
| 61 |
-
className="w-full bg-white/5 border border-white/10 rounded-2xl
|
|
|
|
|
|
|
| 62 |
/>
|
| 63 |
</div>
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
| 69 |
value={password}
|
| 70 |
onChange={(e) => setPassword(e.target.value)}
|
| 71 |
-
className="w-full bg-white/5 border border-white/10 rounded-2xl
|
|
|
|
|
|
|
| 72 |
/>
|
| 73 |
</div>
|
|
|
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
<
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
>
|
| 95 |
-
Autocompletar Demo Admin
|
| 96 |
-
</button>
|
| 97 |
-
</div>
|
| 98 |
-
</form>
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
</div>
|
| 104 |
-
</div>
|
| 105 |
</div>
|
| 106 |
);
|
| 107 |
}
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
+
import React, { useState } from "react";
|
| 4 |
+
import { signInWithEmailAndPassword } from "firebase/auth";
|
| 5 |
import { auth } from "@/lib/firebase";
|
|
|
|
| 6 |
import { useRouter } from "next/navigation";
|
| 7 |
+
import { motion } from "framer-motion";
|
| 8 |
+
import { Lock, User, ArrowRight, ShieldCheck } from "lucide-react";
|
| 9 |
|
| 10 |
export default function LoginPage() {
|
| 11 |
+
const [email, setEmail] = useState("admin@erp.com");
|
| 12 |
+
const [password, setPassword] = useState("admin123");
|
| 13 |
const [error, setError] = useState("");
|
| 14 |
const [loading, setLoading] = useState(false);
|
| 15 |
const router = useRouter();
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
const handleLogin = async (e: React.FormEvent) => {
|
| 18 |
e.preventDefault();
|
| 19 |
setLoading(true);
|
|
|
|
| 22 |
await signInWithEmailAndPassword(auth, email, password);
|
| 23 |
router.push("/");
|
| 24 |
} catch (err: any) {
|
| 25 |
+
setError("Credenciales inválidas. Intente de nuevo.");
|
|
|
|
| 26 |
setLoading(false);
|
| 27 |
}
|
| 28 |
};
|
| 29 |
|
| 30 |
return (
|
| 31 |
+
<div className="fixed inset-0 bg-[#020617] flex items-center justify-center p-6 z-[100]">
|
| 32 |
+
{/* Background Decorative Elements */}
|
| 33 |
+
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-blue-600/20 rounded-full blur-[120px] animate-pulse"></div>
|
| 34 |
+
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-indigo-600/20 rounded-full blur-[120px] animate-pulse delay-1000"></div>
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
<motion.div
|
| 37 |
+
initial={{ opacity: 0, y: 20 }}
|
| 38 |
+
animate={{ opacity: 1, y: 0 }}
|
| 39 |
+
className="w-full max-w-md bg-white/5 border border-white/10 rounded-[2.5rem] p-10 backdrop-blur-3xl shadow-2xl relative overflow-hidden"
|
| 40 |
+
>
|
| 41 |
+
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500"></div>
|
| 42 |
+
|
| 43 |
+
<div className="flex flex-col items-center mb-10">
|
| 44 |
+
<div className="w-16 h-16 bg-gradient-to-br from-blue-600 to-indigo-600 rounded-[1.5rem] flex items-center justify-center shadow-2xl shadow-blue-500/20 mb-6">
|
| 45 |
+
<ShieldCheck size={32} className="text-white" />
|
| 46 |
</div>
|
| 47 |
+
<h1 className="text-3xl font-black tracking-tighter italic uppercase text-white">
|
| 48 |
+
NEXUS <span className="text-blue-500 italic">ERP</span>
|
| 49 |
+
</h1>
|
| 50 |
+
<p className="text-gray-500 font-bold mt-2 uppercase text-[10px] tracking-[0.3em]">Acceso Seguro al Panel de Control</p>
|
| 51 |
+
</div>
|
| 52 |
|
| 53 |
+
<form onSubmit={handleLogin} className="space-y-6">
|
| 54 |
+
<div className="space-y-2">
|
| 55 |
+
<label className="text-[10px] font-black uppercase tracking-widest text-gray-400 ml-4">Usuario / Email</label>
|
| 56 |
+
<div className="relative group">
|
| 57 |
+
<User className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500 group-focus-within:text-blue-400 transition-colors" size={18} />
|
| 58 |
+
<input
|
| 59 |
+
type="email"
|
| 60 |
value={email}
|
| 61 |
onChange={(e) => setEmail(e.target.value)}
|
| 62 |
+
className="w-full bg-white/5 border border-white/10 rounded-2xl py-4 pl-12 pr-4 text-sm font-medium focus:ring-2 focus:ring-blue-500/50 outline-none transition-all focus:bg-white/10"
|
| 63 |
+
placeholder="usuario@empresa.com"
|
| 64 |
+
required
|
| 65 |
/>
|
| 66 |
</div>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<div className="space-y-2">
|
| 70 |
+
<label className="text-[10px] font-black uppercase tracking-widest text-gray-400 ml-4">Contraseña</label>
|
| 71 |
+
<div className="relative group">
|
| 72 |
+
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500 group-focus-within:text-blue-400 transition-colors" size={18} />
|
| 73 |
+
<input
|
| 74 |
+
type="password"
|
| 75 |
value={password}
|
| 76 |
onChange={(e) => setPassword(e.target.value)}
|
| 77 |
+
className="w-full bg-white/5 border border-white/10 rounded-2xl py-4 pl-12 pr-4 text-sm font-medium focus:ring-2 focus:ring-blue-500/50 outline-none transition-all focus:bg-white/10"
|
| 78 |
+
placeholder="••••••••"
|
| 79 |
+
required
|
| 80 |
/>
|
| 81 |
</div>
|
| 82 |
+
</div>
|
| 83 |
|
| 84 |
+
{error && (
|
| 85 |
+
<motion.p
|
| 86 |
+
initial={{ opacity: 0, x: -10 }}
|
| 87 |
+
animate={{ opacity: 1, x: 0 }}
|
| 88 |
+
className="text-red-400 text-xs font-bold text-center bg-red-500/10 py-3 rounded-xl border border-red-500/20"
|
| 89 |
+
>
|
| 90 |
+
{error}
|
| 91 |
+
</motion.p>
|
| 92 |
+
)}
|
| 93 |
|
| 94 |
+
<button
|
| 95 |
+
type="submit"
|
| 96 |
+
disabled={loading}
|
| 97 |
+
className="w-full bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-black py-4 rounded-2xl shadow-xl shadow-blue-900/40 flex items-center justify-center gap-2 group transition-all active:scale-[0.98] disabled:opacity-50"
|
| 98 |
+
>
|
| 99 |
+
{loading ? "Iniciando Sesión..." : (
|
| 100 |
+
<>
|
| 101 |
+
Entrar al Sistema
|
| 102 |
+
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
|
| 103 |
+
</>
|
| 104 |
+
)}
|
| 105 |
+
</button>
|
| 106 |
+
</form>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
+
<div className="mt-8 pt-8 border-t border-white/5">
|
| 109 |
+
<div className="bg-blue-500/5 border border-blue-500/10 rounded-2xl p-4 flex items-center gap-4">
|
| 110 |
+
<div className="w-8 h-8 rounded-full bg-blue-500/10 flex items-center justify-center">
|
| 111 |
+
<ShieldCheck size={14} className="text-blue-400" />
|
| 112 |
+
</div>
|
| 113 |
+
<div>
|
| 114 |
+
<p className="text-[9px] font-black uppercase text-blue-400 tracking-wider">Modo Demo Activado</p>
|
| 115 |
+
<p className="text-[10px] text-gray-500 font-medium">Haga clic en 'Entrar' para acceder rápidamente.</p>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
</div>
|
| 119 |
+
</motion.div>
|
| 120 |
</div>
|
| 121 |
);
|
| 122 |
}
|
components/AuthGuard.tsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from "react";
|
| 4 |
+
import { auth } from "@/lib/firebase";
|
| 5 |
+
import { onAuthStateChanged } from "firebase/auth";
|
| 6 |
+
import { useRouter, usePathname } from "next/navigation";
|
| 7 |
+
import { motion, AnimatePresence } from "framer-motion";
|
| 8 |
+
|
| 9 |
+
export default function AuthGuard({ children }: { children: React.ReactNode }) {
|
| 10 |
+
const [loading, setLoading] = useState(true);
|
| 11 |
+
const [user, setUser] = useState<any>(null);
|
| 12 |
+
const router = useRouter();
|
| 13 |
+
const pathname = usePathname();
|
| 14 |
+
|
| 15 |
+
useEffect(() => {
|
| 16 |
+
const unsubscribe = onAuthStateChanged(auth, (user) => {
|
| 17 |
+
setUser(user);
|
| 18 |
+
setLoading(false);
|
| 19 |
+
|
| 20 |
+
if (!user && pathname !== "/login") {
|
| 21 |
+
router.push("/login");
|
| 22 |
+
}
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
return () => unsubscribe();
|
| 26 |
+
}, [pathname, router]);
|
| 27 |
+
|
| 28 |
+
if (loading) {
|
| 29 |
+
return (
|
| 30 |
+
<div className="fixed inset-0 bg-[#0f172a] flex items-center justify-center z-[200]">
|
| 31 |
+
<motion.div
|
| 32 |
+
animate={{ rotate: 360 }}
|
| 33 |
+
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
| 34 |
+
className="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full shadow-lg shadow-blue-500/20"
|
| 35 |
+
/>
|
| 36 |
+
</div>
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// If not logged in and not on login page, don't show children (redirecting)
|
| 41 |
+
if (!user && pathname !== "/login") {
|
| 42 |
+
return null;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
return <>{children}</>;
|
| 46 |
+
}
|
components/Sidebar.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
| 13 |
Network,
|
| 14 |
MessageSquare,
|
| 15 |
Map as MapIcon,
|
|
|
|
| 16 |
LogOut
|
| 17 |
} from "lucide-react";
|
| 18 |
import { motion } from "framer-motion";
|
|
@@ -25,6 +26,7 @@ const menuItems = [
|
|
| 25 |
{ icon: Users, label: "Clientes", href: "/clients" },
|
| 26 |
{ icon: UserSquare2, label: "Personal / RRHH", href: "/hr" },
|
| 27 |
{ icon: CheckSquare, label: "Tareas", href: "/tasks" },
|
|
|
|
| 28 |
{ icon: Network, label: "Organigrama", href: "/org-chart" },
|
| 29 |
{ icon: MessageSquare, label: "Intranet", href: "/intranet" },
|
| 30 |
{ icon: MapIcon, label: "Mapa Mental", href: "/mind-map" },
|
|
|
|
| 13 |
Network,
|
| 14 |
MessageSquare,
|
| 15 |
Map as MapIcon,
|
| 16 |
+
FileText,
|
| 17 |
LogOut
|
| 18 |
} from "lucide-react";
|
| 19 |
import { motion } from "framer-motion";
|
|
|
|
| 26 |
{ icon: Users, label: "Clientes", href: "/clients" },
|
| 27 |
{ icon: UserSquare2, label: "Personal / RRHH", href: "/hr" },
|
| 28 |
{ icon: CheckSquare, label: "Tareas", href: "/tasks" },
|
| 29 |
+
{ icon: FileText, label: "Facturación", href: "/billing" },
|
| 30 |
{ icon: Network, label: "Organigrama", href: "/org-chart" },
|
| 31 |
{ icon: MessageSquare, label: "Intranet", href: "/intranet" },
|
| 32 |
{ icon: MapIcon, label: "Mapa Mental", href: "/mind-map" },
|