Spaces:
Running
Running
Upload 26 files
Browse files- app/clients/page.tsx +102 -0
- app/hr/page.tsx +105 -0
- app/inventory/page.tsx +132 -0
- app/layout.tsx +6 -17
- app/login/page.tsx +107 -0
- app/page.tsx +121 -56
- app/sales/page.tsx +114 -0
- lib/firebase.ts +19 -0
app/clients/page.tsx
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React, { useState, useEffect } from "react";
|
| 4 |
+
import { db } from "@/lib/firebase";
|
| 5 |
+
import { collection, onSnapshot, addDoc, deleteDoc, doc } from "firebase/firestore";
|
| 6 |
+
import Link from "next/link";
|
| 7 |
+
|
| 8 |
+
interface Client {
|
| 9 |
+
id: string;
|
| 10 |
+
name: string;
|
| 11 |
+
email: string;
|
| 12 |
+
phone: string;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default function ClientsPage() {
|
| 16 |
+
const [clients, setClients] = useState<Client[]>([]);
|
| 17 |
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
| 18 |
+
const [newClient, setNewClient] = useState({ name: "", email: "", phone: "" });
|
| 19 |
+
|
| 20 |
+
useEffect(() => {
|
| 21 |
+
const unsub = onSnapshot(collection(db, "clients"), (snap) => {
|
| 22 |
+
const data = snap.docs.map(doc => ({ id: doc.id, ...doc.data() } as Client));
|
| 23 |
+
setClients(data);
|
| 24 |
+
});
|
| 25 |
+
return () => unsub();
|
| 26 |
+
}, []);
|
| 27 |
+
|
| 28 |
+
const handleAdd = async (e: React.FormEvent) => {
|
| 29 |
+
e.preventDefault();
|
| 30 |
+
await addDoc(collection(db, "clients"), newClient);
|
| 31 |
+
setIsModalOpen(false);
|
| 32 |
+
setNewClient({ name: "", email: "", phone: "" });
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
return (
|
| 36 |
+
<div className="min-h-screen bg-[#0f172a] text-white p-6">
|
| 37 |
+
<header className="flex justify-between items-center mb-10">
|
| 38 |
+
<div>
|
| 39 |
+
<Link href="/" className="text-blue-400 hover:text-blue-300 transition-colors flex items-center gap-2 mb-2">
|
| 40 |
+
← Dashboard
|
| 41 |
+
</Link>
|
| 42 |
+
<h1 className="text-4xl font-black">Directorio de Clientes</h1>
|
| 43 |
+
</div>
|
| 44 |
+
<button
|
| 45 |
+
onClick={() => setIsModalOpen(true)}
|
| 46 |
+
className="px-8 py-3 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 rounded-2xl font-bold transition-all shadow-xl shadow-purple-900/20"
|
| 47 |
+
>
|
| 48 |
+
➕ Nuevo Cliente
|
| 49 |
+
</button>
|
| 50 |
+
</header>
|
| 51 |
+
|
| 52 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 53 |
+
{clients.map((c) => (
|
| 54 |
+
<div key={c.id} className="bg-white/5 border border-white/10 rounded-[32px] p-8 backdrop-blur-xl relative overflow-hidden group hover:border-white/20 transition-all">
|
| 55 |
+
<div className="absolute -right-4 -top-4 w-24 h-24 bg-purple-500/10 blur-3xl rounded-full"></div>
|
| 56 |
+
<div className="relative z-10">
|
| 57 |
+
<div className="w-14 h-14 bg-white/10 rounded-2xl flex items-center justify-center text-2xl mb-6">👤</div>
|
| 58 |
+
<h3 className="text-xl font-bold mb-2 group-hover:text-purple-400 transition-colors">{c.name}</h3>
|
| 59 |
+
<p className="text-gray-400 text-sm mb-4">{c.email}</p>
|
| 60 |
+
<div className="flex items-center gap-2 text-xs font-mono text-purple-300 bg-purple-500/10 w-fit px-3 py-1 rounded-full border border-purple-500/20">
|
| 61 |
+
📞 {c.phone}
|
| 62 |
+
</div>
|
| 63 |
+
<div className="mt-8 pt-6 border-t border-white/5 flex gap-4">
|
| 64 |
+
<button className="text-xs font-bold text-gray-500 hover:text-white transition-colors uppercase tracking-widest">Editar</button>
|
| 65 |
+
<button onClick={() => deleteDoc(doc(db, "clients", c.id))} className="text-xs font-bold text-gray-700 hover:text-red-400 transition-colors uppercase tracking-widest ml-auto">Eliminar</button>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
))}
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
{isModalOpen && (
|
| 73 |
+
<div className="fixed inset-0 bg-black/80 backdrop-blur-md flex items-center justify-center p-6 z-50">
|
| 74 |
+
<div className="bg-[#1e293b] border border-white/10 p-10 rounded-[48px] w-full max-w-md shadow-2xl animate-in fade-in slide-in-from-bottom-5">
|
| 75 |
+
<h2 className="text-3xl font-black mb-8 text-center uppercase tracking-tighter">Registrar Cliente</h2>
|
| 76 |
+
<form onSubmit={handleAdd} className="space-y-4">
|
| 77 |
+
<input
|
| 78 |
+
type="text" placeholder="Nombre completo" required
|
| 79 |
+
value={newClient.name} onChange={e => setNewClient({...newClient, name: e.target.value})}
|
| 80 |
+
className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-purple-500/50"
|
| 81 |
+
/>
|
| 82 |
+
<input
|
| 83 |
+
type="email" placeholder="Email" required
|
| 84 |
+
value={newClient.email} onChange={e => setNewClient({...newClient, email: e.target.value})}
|
| 85 |
+
className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-purple-500/50"
|
| 86 |
+
/>
|
| 87 |
+
<input
|
| 88 |
+
type="tel" placeholder="Teléfono" required
|
| 89 |
+
value={newClient.phone} onChange={e => setNewClient({...newClient, phone: e.target.value})}
|
| 90 |
+
className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-purple-500/50"
|
| 91 |
+
/>
|
| 92 |
+
<div className="flex flex-col gap-3 mt-10">
|
| 93 |
+
<button type="submit" className="w-full bg-white text-[#0f172a] py-5 rounded-2xl font-black transition-all hover:bg-gray-200">CREAR CLIENTE</button>
|
| 94 |
+
<button type="button" onClick={() => setIsModalOpen(false)} className="w-full py-4 text-gray-500 font-bold hover:text-white transition-colors">CANCELAR</button>
|
| 95 |
+
</div>
|
| 96 |
+
</form>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
)}
|
| 100 |
+
</div>
|
| 101 |
+
);
|
| 102 |
+
}
|
app/hr/page.tsx
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React, { useState, useEffect } from "react";
|
| 4 |
+
import { db } from "@/lib/firebase";
|
| 5 |
+
import { collection, onSnapshot, addDoc, deleteDoc, doc } from "firebase/firestore";
|
| 6 |
+
import Link from "next/link";
|
| 7 |
+
|
| 8 |
+
interface Employee {
|
| 9 |
+
id: string;
|
| 10 |
+
name: string;
|
| 11 |
+
position: string;
|
| 12 |
+
salary: number;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default function HRPage() {
|
| 16 |
+
const [employees, setEmployees] = useState<Employee[]>([]);
|
| 17 |
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
| 18 |
+
const [newEmp, setNewEmp] = useState({ name: "", position: "", salary: 0 });
|
| 19 |
+
|
| 20 |
+
useEffect(() => {
|
| 21 |
+
const unsub = onSnapshot(collection(db, "employees"), (snap) => {
|
| 22 |
+
const data = snap.docs.map(doc => ({ id: doc.id, ...doc.data() } as Employee));
|
| 23 |
+
setEmployees(data);
|
| 24 |
+
});
|
| 25 |
+
return () => unsub();
|
| 26 |
+
}, []);
|
| 27 |
+
|
| 28 |
+
const handleAdd = async (e: React.FormEvent) => {
|
| 29 |
+
e.preventDefault();
|
| 30 |
+
await addDoc(collection(db, "employees"), newEmp);
|
| 31 |
+
setIsModalOpen(false);
|
| 32 |
+
setNewEmp({ name: "", position: "", salary: 0 });
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
return (
|
| 36 |
+
<div className="min-h-screen bg-[#0f172a] text-white p-6">
|
| 37 |
+
<header className="flex justify-between items-center mb-10">
|
| 38 |
+
<div>
|
| 39 |
+
<Link href="/" className="text-cyan-400 hover:text-cyan-300 transition-colors flex items-center gap-2 mb-2 font-medium">
|
| 40 |
+
← Panel Principal
|
| 41 |
+
</Link>
|
| 42 |
+
<h1 className="text-4xl font-extrabold tracking-tight">Gestión Humana</h1>
|
| 43 |
+
</div>
|
| 44 |
+
<button
|
| 45 |
+
onClick={() => setIsModalOpen(true)}
|
| 46 |
+
className="px-8 py-3 bg-white/10 hover:bg-white/20 border border-white/10 rounded-full font-bold transition-all backdrop-blur-md"
|
| 47 |
+
>
|
| 48 |
+
➕ Registrar Personal
|
| 49 |
+
</button>
|
| 50 |
+
</header>
|
| 51 |
+
|
| 52 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
| 53 |
+
{employees.map((e) => (
|
| 54 |
+
<div key={e.id} className="bg-gradient-to-br from-[#1e293b] to-[#0f172a] border border-white/5 rounded-[2rem] p-8 hover:border-cyan-500/30 transition-all group shadow-2xl">
|
| 55 |
+
<div className="flex items-center gap-4 mb-6">
|
| 56 |
+
<div className="w-12 h-12 bg-cyan-500/20 text-cyan-400 rounded-2xl flex items-center justify-center text-xl font-bold">
|
| 57 |
+
{e.name.charAt(0)}
|
| 58 |
+
</div>
|
| 59 |
+
<div>
|
| 60 |
+
<h3 className="font-bold group-hover:text-cyan-400 transition-colors">{e.name}</h3>
|
| 61 |
+
<span className="text-[10px] text-gray-500 font-black uppercase tracking-widest">{e.position}</span>
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
<div className="flex justify-between items-center py-4 border-y border-white/5 mb-6">
|
| 65 |
+
<span className="text-xs text-gray-500 uppercase font-bold">Salario</span>
|
| 66 |
+
<span className="font-mono text-cyan-400 font-bold">${Number(e.salary).toLocaleString()}</span>
|
| 67 |
+
</div>
|
| 68 |
+
<button
|
| 69 |
+
onClick={() => deleteDoc(doc(db, "employees", e.id))}
|
| 70 |
+
className="w-full py-3 bg-red-500/5 hover:bg-red-500/20 text-red-500/60 hover:text-red-500 text-[10px] font-black uppercase tracking-widest rounded-xl transition-all border border-red-500/10"
|
| 71 |
+
>
|
| 72 |
+
Dar de Baja
|
| 73 |
+
</button>
|
| 74 |
+
</div>
|
| 75 |
+
))}
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
{isModalOpen && (
|
| 79 |
+
<div className="fixed inset-0 bg-[#0f172a]/95 backdrop-blur-xl flex items-center justify-center p-6 z-50">
|
| 80 |
+
<div className="w-full max-w-sm">
|
| 81 |
+
<h2 className="text-4xl font-black mb-8 text-white italic tracking-tighter">NEW STAFF /</h2>
|
| 82 |
+
<form onSubmit={handleAdd} className="space-y-6">
|
| 83 |
+
<div className="space-y-1">
|
| 84 |
+
<label className="text-[10px] font-black text-cyan-500 uppercase ml-2">Nombre completo</label>
|
| 85 |
+
<input required value={newEmp.name} onChange={v => setNewEmp({...newEmp, name: v.target.value})} className="w-full bg-white/5 border-b border-white/20 px-4 py-4 outline-none focus:border-cyan-500 transition-all font-medium text-lg"/>
|
| 86 |
+
</div>
|
| 87 |
+
<div className="space-y-1">
|
| 88 |
+
<label className="text-[10px] font-black text-cyan-500 uppercase ml-2">Cargo / Posición</label>
|
| 89 |
+
<input required value={newEmp.position} onChange={v => setNewEmp({...newEmp, position: v.target.value})} className="w-full bg-white/5 border-b border-white/20 px-4 py-4 outline-none focus:border-cyan-500 transition-all font-medium text-lg"/>
|
| 90 |
+
</div>
|
| 91 |
+
<div className="space-y-1">
|
| 92 |
+
<label className="text-[10px] font-black text-cyan-500 uppercase ml-2">Salario Mensual</label>
|
| 93 |
+
<input type="number" required value={newEmp.salary} onChange={v => setNewEmp({...newEmp, salary: Number(v.target.value)})} className="w-full bg-white/5 border-b border-white/20 px-4 py-4 outline-none focus:border-cyan-500 transition-all font-medium text-lg"/>
|
| 94 |
+
</div>
|
| 95 |
+
<div className="flex gap-4 pt-10">
|
| 96 |
+
<button type="button" onClick={() => setIsModalOpen(false)} className="px-6 py-4 text-gray-500 font-black text-xs uppercase hover:text-white transition-colors">Cancelar</button>
|
| 97 |
+
<button type="submit" className="flex-1 bg-cyan-600 hover:bg-cyan-500 py-4 rounded-full font-black text-xs uppercase tracking-widest transition-all">Contratar</button>
|
| 98 |
+
</div>
|
| 99 |
+
</form>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
)}
|
| 103 |
+
</div>
|
| 104 |
+
);
|
| 105 |
+
}
|
app/inventory/page.tsx
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React, { useState, useEffect } from "react";
|
| 4 |
+
import { db } from "@/lib/firebase";
|
| 5 |
+
import { collection, onSnapshot, addDoc, deleteDoc, doc, updateDoc } from "firebase/firestore";
|
| 6 |
+
import Link from "next/link";
|
| 7 |
+
|
| 8 |
+
interface Product {
|
| 9 |
+
id: string;
|
| 10 |
+
name: string;
|
| 11 |
+
category: string;
|
| 12 |
+
price: number;
|
| 13 |
+
stock: number;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export default function InventoryPage() {
|
| 17 |
+
const [products, setProducts] = useState<Product[]>([]);
|
| 18 |
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
| 19 |
+
const [newProduct, setNewProduct] = useState({ name: "", category: "", price: 0, stock: 0 });
|
| 20 |
+
|
| 21 |
+
useEffect(() => {
|
| 22 |
+
const unsub = onSnapshot(collection(db, "products"), (snap) => {
|
| 23 |
+
const data = snap.docs.map(doc => ({ id: doc.id, ...doc.data() } as Product));
|
| 24 |
+
setProducts(data);
|
| 25 |
+
});
|
| 26 |
+
return () => unsub();
|
| 27 |
+
}, []);
|
| 28 |
+
|
| 29 |
+
const handleAdd = async (e: React.FormEvent) => {
|
| 30 |
+
e.preventDefault();
|
| 31 |
+
await addDoc(collection(db, "products"), newProduct);
|
| 32 |
+
setIsModalOpen(false);
|
| 33 |
+
setNewProduct({ name: "", category: "", price: 0, stock: 0 });
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
const handleDelete = async (id: string) => {
|
| 37 |
+
if(confirm("¿Seguro que quieres eliminar este producto?")) {
|
| 38 |
+
await deleteDoc(doc(db, "products", id));
|
| 39 |
+
}
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
return (
|
| 43 |
+
<div className="min-h-screen bg-[#0f172a] text-white p-6">
|
| 44 |
+
<header className="flex justify-between items-center mb-8">
|
| 45 |
+
<div>
|
| 46 |
+
<Link href="/" className="text-blue-400 hover:text-blue-300 transition-colors flex items-center gap-2 mb-2">
|
| 47 |
+
← Volver al Dashboard
|
| 48 |
+
</Link>
|
| 49 |
+
<h1 className="text-3xl font-bold">Gestión de Inventario</h1>
|
| 50 |
+
</div>
|
| 51 |
+
<button
|
| 52 |
+
onClick={() => setIsModalOpen(true)}
|
| 53 |
+
className="px-6 py-3 bg-blue-600 hover:bg-blue-500 rounded-2xl font-bold transition-all shadow-lg shadow-blue-900/20"
|
| 54 |
+
>
|
| 55 |
+
➕ Añadir Producto
|
| 56 |
+
</button>
|
| 57 |
+
</header>
|
| 58 |
+
|
| 59 |
+
{/* Inventory Table */}
|
| 60 |
+
<div className="bg-white/5 border border-white/10 rounded-3xl overflow-hidden backdrop-blur-xl">
|
| 61 |
+
<table className="w-full text-left">
|
| 62 |
+
<thead className="bg-white/5 border-b border-white/10">
|
| 63 |
+
<tr>
|
| 64 |
+
<th className="px-6 py-4 font-semibold text-gray-400 uppercase text-xs tracking-wider">Nombre</th>
|
| 65 |
+
<th className="px-6 py-4 font-semibold text-gray-400 uppercase text-xs tracking-wider">Categoría</th>
|
| 66 |
+
<th className="px-6 py-4 font-semibold text-gray-400 uppercase text-xs tracking-wider">Precio</th>
|
| 67 |
+
<th className="px-6 py-4 font-semibold text-gray-400 uppercase text-xs tracking-wider">Stock</th>
|
| 68 |
+
<th className="px-6 py-4 font-semibold text-gray-400 uppercase text-xs tracking-wider">Acciones</th>
|
| 69 |
+
</tr>
|
| 70 |
+
</thead>
|
| 71 |
+
<tbody className="divide-y divide-white/5">
|
| 72 |
+
{products.map((p) => (
|
| 73 |
+
<tr key={p.id} className="hover:bg-white/5 transition-colors group">
|
| 74 |
+
<td className="px-6 py-4 font-medium text-gray-200">{p.name}</td>
|
| 75 |
+
<td className="px-6 py-4 text-gray-400">
|
| 76 |
+
<span className="px-3 py-1 bg-white/5 rounded-full text-xs border border-white/10">{p.category}</span>
|
| 77 |
+
</td>
|
| 78 |
+
<td className="px-6 py-4 font-bold text-blue-400">${Number(p.price).toLocaleString()}</td>
|
| 79 |
+
<td className="px-6 py-4">
|
| 80 |
+
<div className="flex items-center gap-2">
|
| 81 |
+
<span className={`w-2 h-2 rounded-full ${p.stock > 10 ? 'bg-green-400' : 'bg-red-400'}`}></span>
|
| 82 |
+
{p.stock} uds.
|
| 83 |
+
</div>
|
| 84 |
+
</td>
|
| 85 |
+
<td className="px-6 py-4">
|
| 86 |
+
<button onClick={() => handleDelete(p.id)} className="text-gray-600 hover:text-red-400 transition-colors">🗑️</button>
|
| 87 |
+
</td>
|
| 88 |
+
</tr>
|
| 89 |
+
))}
|
| 90 |
+
</tbody>
|
| 91 |
+
</table>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
{/* Modal Mockup */}
|
| 95 |
+
{isModalOpen && (
|
| 96 |
+
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-6 z-50">
|
| 97 |
+
<div className="bg-[#1e293b] border border-white/10 p-8 rounded-[40px] w-full max-w-lg shadow-2xl animate-in fade-in zoom-in-95">
|
| 98 |
+
<h2 className="text-2xl font-bold mb-6">Nuevo Producto</h2>
|
| 99 |
+
<form onSubmit={handleAdd} className="space-y-4">
|
| 100 |
+
<input
|
| 101 |
+
type="text" placeholder="Nombre" required
|
| 102 |
+
value={newProduct.name} onChange={e => setNewProduct({...newProduct, name: e.target.value})}
|
| 103 |
+
className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-blue-500/50"
|
| 104 |
+
/>
|
| 105 |
+
<input
|
| 106 |
+
type="text" placeholder="Categoría" required
|
| 107 |
+
value={newProduct.category} onChange={e => setNewProduct({...newProduct, category: e.target.value})}
|
| 108 |
+
className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-blue-500/50"
|
| 109 |
+
/>
|
| 110 |
+
<div className="grid grid-cols-2 gap-4">
|
| 111 |
+
<input
|
| 112 |
+
type="number" placeholder="Precio" required
|
| 113 |
+
value={newProduct.price} onChange={e => setNewProduct({...newProduct, price: Number(e.target.value)})}
|
| 114 |
+
className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-blue-500/50"
|
| 115 |
+
/>
|
| 116 |
+
<input
|
| 117 |
+
type="number" placeholder="Stock" required
|
| 118 |
+
value={newProduct.stock} onChange={e => setNewProduct({...newProduct, stock: Number(e.target.value)})}
|
| 119 |
+
className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-blue-500/50"
|
| 120 |
+
/>
|
| 121 |
+
</div>
|
| 122 |
+
<div className="flex gap-4 mt-8">
|
| 123 |
+
<button type="button" onClick={() => setIsModalOpen(false)} className="flex-1 py-4 text-gray-400 hover:text-white transition-colors">Cancelar</button>
|
| 124 |
+
<button type="submit" className="flex-1 bg-blue-600 hover:bg-blue-500 py-4 rounded-2xl font-bold transition-all shadow-lg shadow-blue-900/20">Guardar</button>
|
| 125 |
+
</div>
|
| 126 |
+
</form>
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
)}
|
| 130 |
+
</div>
|
| 131 |
+
);
|
| 132 |
+
}
|
app/layout.tsx
CHANGED
|
@@ -1,20 +1,12 @@
|
|
| 1 |
import type { Metadata } from "next";
|
| 2 |
-
import {
|
| 3 |
import "./globals.css";
|
| 4 |
|
| 5 |
-
const
|
| 6 |
-
variable: "--font-geist-sans",
|
| 7 |
-
subsets: ["latin"],
|
| 8 |
-
});
|
| 9 |
-
|
| 10 |
-
const geistMono = Geist_Mono({
|
| 11 |
-
variable: "--font-geist-mono",
|
| 12 |
-
subsets: ["latin"],
|
| 13 |
-
});
|
| 14 |
|
| 15 |
export const metadata: Metadata = {
|
| 16 |
-
title: "
|
| 17 |
-
description: "
|
| 18 |
};
|
| 19 |
|
| 20 |
export default function RootLayout({
|
|
@@ -23,11 +15,8 @@ export default function RootLayout({
|
|
| 23 |
children: React.ReactNode;
|
| 24 |
}>) {
|
| 25 |
return (
|
| 26 |
-
<html
|
| 27 |
-
|
| 28 |
-
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
| 29 |
-
>
|
| 30 |
-
<body className="min-h-full flex flex-col">{children}</body>
|
| 31 |
</html>
|
| 32 |
);
|
| 33 |
}
|
|
|
|
| 1 |
import type { Metadata } from "next";
|
| 2 |
+
import { Inter } from "next/font/google";
|
| 3 |
import "./globals.css";
|
| 4 |
|
| 5 |
+
const inter = Inter({ subsets: ["latin"] });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
export const metadata: Metadata = {
|
| 8 |
+
title: "ERP Premium | Next Generation Control Panel",
|
| 9 |
+
description: "Modern Enterprise Resource Planning system powered by Firebase",
|
| 10 |
};
|
| 11 |
|
| 12 |
export default function RootLayout({
|
|
|
|
| 15 |
children: React.ReactNode;
|
| 16 |
}>) {
|
| 17 |
return (
|
| 18 |
+
<html lang="es">
|
| 19 |
+
<body className={`${inter.className} bg-[#0f172a]`}>{children}</body>
|
|
|
|
|
|
|
|
|
|
| 20 |
</html>
|
| 21 |
);
|
| 22 |
}
|
app/login/page.tsx
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React, { useState, useEffect } from "react";
|
| 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);
|
| 25 |
+
setError("");
|
| 26 |
+
try {
|
| 27 |
+
await signInWithEmailAndPassword(auth, email, password);
|
| 28 |
+
router.push("/");
|
| 29 |
+
} catch (err: any) {
|
| 30 |
+
setError(err.message || "Error al iniciar sesión");
|
| 31 |
+
} finally {
|
| 32 |
+
setLoading(false);
|
| 33 |
+
}
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
return (
|
| 37 |
+
<div className="min-h-screen bg-[#0f172a] flex items-center justify-center p-6 relative overflow-hidden">
|
| 38 |
+
{/* Background Decor */}
|
| 39 |
+
<div className="absolute top-0 left-0 w-full h-full">
|
| 40 |
+
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-blue-600/20 blur-[120px] rounded-full"></div>
|
| 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 className="w-full max-w-md relative z-10 transition-all duration-700 animate-in fade-in slide-in-from-bottom-8">
|
| 45 |
+
<div className="bg-white/5 border border-white/10 p-10 rounded-[40px] backdrop-blur-3xl shadow-2xl">
|
| 46 |
+
<div className="text-center mb-10">
|
| 47 |
+
<h1 className="text-4xl font-black bg-gradient-to-r from-blue-400 to-indigo-500 bg-clip-text text-transparent mb-2">
|
| 48 |
+
ERP System
|
| 49 |
+
</h1>
|
| 50 |
+
<p className="text-gray-400 text-sm font-medium">Panel de control empresarial premium</p>
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
<form onSubmit={handleLogin} className="space-y-6">
|
| 54 |
+
<div>
|
| 55 |
+
<input
|
| 56 |
+
type="email"
|
| 57 |
+
placeholder="Email corporativo"
|
| 58 |
+
required
|
| 59 |
+
value={email}
|
| 60 |
+
onChange={(e) => setEmail(e.target.value)}
|
| 61 |
+
className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-blue-500/50 focus:bg-white/10 transition-all placeholder:text-gray-600"
|
| 62 |
+
/>
|
| 63 |
+
</div>
|
| 64 |
+
<div>
|
| 65 |
+
<input
|
| 66 |
+
type="password"
|
| 67 |
+
placeholder="Contraseña"
|
| 68 |
+
required
|
| 69 |
+
value={password}
|
| 70 |
+
onChange={(e) => setPassword(e.target.value)}
|
| 71 |
+
className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-blue-500/50 focus:bg-white/10 transition-all placeholder:text-gray-600"
|
| 72 |
+
/>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
{error && (
|
| 76 |
+
<div className="bg-red-500/10 border border-red-500/20 text-red-400 text-xs p-4 rounded-xl animate-shake">
|
| 77 |
+
{error}
|
| 78 |
+
</div>
|
| 79 |
+
)}
|
| 80 |
+
|
| 81 |
+
<button
|
| 82 |
+
type="submit"
|
| 83 |
+
disabled={loading}
|
| 84 |
+
className="w-full bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold py-4 rounded-2xl shadow-lg shadow-blue-900/20 hover:shadow-blue-900/40 transition-all transform active:scale-95 disabled:opacity-50"
|
| 85 |
+
>
|
| 86 |
+
{loading ? (
|
| 87 |
+
<span className="flex items-center justify-center gap-2">
|
| 88 |
+
<svg className="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
| 89 |
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
| 90 |
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
| 91 |
+
</svg>
|
| 92 |
+
Autenticando...
|
| 93 |
+
</span>
|
| 94 |
+
) : (
|
| 95 |
+
"Entrar"
|
| 96 |
+
)}
|
| 97 |
+
</button>
|
| 98 |
+
</form>
|
| 99 |
+
|
| 100 |
+
<p className="mt-8 text-center text-xs text-gray-500 font-medium">
|
| 101 |
+
Acceso restringido para administradores autorizados
|
| 102 |
+
</p>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
);
|
| 107 |
+
}
|
app/page.tsx
CHANGED
|
@@ -1,65 +1,130 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
export default function Home() {
|
| 4 |
return (
|
| 5 |
-
<div className="
|
| 6 |
-
<
|
| 7 |
-
<
|
| 8 |
-
className="
|
| 9 |
-
|
| 10 |
-
alt="Next.js logo"
|
| 11 |
-
width={100}
|
| 12 |
-
height={20}
|
| 13 |
-
priority
|
| 14 |
-
/>
|
| 15 |
-
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
| 16 |
-
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
| 17 |
-
To get started, edit the page.tsx file.
|
| 18 |
</h1>
|
| 19 |
-
<p className="
|
| 20 |
-
Looking for a starting point or more instructions? Head over to{" "}
|
| 21 |
-
<a
|
| 22 |
-
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
| 23 |
-
className="font-medium text-zinc-950 dark:text-zinc-50"
|
| 24 |
-
>
|
| 25 |
-
Templates
|
| 26 |
-
</a>{" "}
|
| 27 |
-
or the{" "}
|
| 28 |
-
<a
|
| 29 |
-
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
| 30 |
-
className="font-medium text-zinc-950 dark:text-zinc-50"
|
| 31 |
-
>
|
| 32 |
-
Learning
|
| 33 |
-
</a>{" "}
|
| 34 |
-
center.
|
| 35 |
-
</p>
|
| 36 |
</div>
|
| 37 |
-
<div className="flex
|
| 38 |
-
<
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
target="_blank"
|
| 42 |
-
rel="noopener noreferrer"
|
| 43 |
>
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
</div>
|
| 62 |
-
</
|
| 63 |
</div>
|
| 64 |
);
|
| 65 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React, { useState, useEffect } from "react";
|
| 4 |
+
import { db, auth } from "@/lib/firebase";
|
| 5 |
+
import { collection, onSnapshot, query, orderBy, limit } from "firebase/firestore";
|
| 6 |
+
import Link from "next/link";
|
| 7 |
+
|
| 8 |
+
export default function Dashboard() {
|
| 9 |
+
const [stats, setStats] = useState({
|
| 10 |
+
products: 0,
|
| 11 |
+
clients: 0,
|
| 12 |
+
salesCount: 0,
|
| 13 |
+
revenue: 0,
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
// Real-time stats from Firestore
|
| 18 |
+
const unsubProducts = onSnapshot(collection(db, "products"), (snap) => {
|
| 19 |
+
setStats((prev) => ({ ...prev, products: snap.size }));
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
const unsubClients = onSnapshot(collection(db, "clients"), (snap) => {
|
| 23 |
+
setStats((prev) => ({ ...prev, clients: snap.size }));
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
const unsubSales = onSnapshot(collection(db, "sales"), (snap) => {
|
| 27 |
+
let rev = 0;
|
| 28 |
+
snap.forEach((doc) => {
|
| 29 |
+
rev += doc.data().total || 0;
|
| 30 |
+
});
|
| 31 |
+
setStats((prev) => ({ ...prev, salesCount: snap.size, revenue: rev }));
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
return () => {
|
| 35 |
+
unsubProducts();
|
| 36 |
+
unsubClients();
|
| 37 |
+
unsubSales();
|
| 38 |
+
};
|
| 39 |
+
}, []);
|
| 40 |
|
|
|
|
| 41 |
return (
|
| 42 |
+
<div className="min-h-screen bg-[#0f172a] text-white p-6 font-sans">
|
| 43 |
+
<header className="flex justify-between items-center mb-10">
|
| 44 |
+
<div>
|
| 45 |
+
<h1 className="text-4xl font-extrabold bg-gradient-to-r from-blue-400 to-indigo-500 bg-clip-text text-transparent">
|
| 46 |
+
ERP Premium Dashboard
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
</h1>
|
| 48 |
+
<p className="text-gray-400 mt-1">Bienvenido al centro de control de tu negocio</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
</div>
|
| 50 |
+
<div className="flex gap-4">
|
| 51 |
+
<button
|
| 52 |
+
onClick={() => auth.signOut()}
|
| 53 |
+
className="px-6 py-2 bg-white/10 hover:bg-white/20 rounded-full border border-white/10 transition-all text-sm font-medium backdrop-blur-md"
|
|
|
|
|
|
|
| 54 |
>
|
| 55 |
+
Cerrar Sesión
|
| 56 |
+
</button>
|
| 57 |
+
</div>
|
| 58 |
+
</header>
|
| 59 |
+
|
| 60 |
+
{/* Stats Grid */}
|
| 61 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-10">
|
| 62 |
+
<StatCard title="Productos" value={stats.products} icon="📦" color="from-blue-500 to-cyan-400" />
|
| 63 |
+
<StatCard title="Clientes" value={stats.clients} icon="👥" color="from-purple-500 to-pink-500" />
|
| 64 |
+
<StatCard title="Ventas Totales" value={stats.salesCount} icon="🛒" color="from-orange-500 to-yellow-400" />
|
| 65 |
+
<StatCard title="Ingresos" value={`$${stats.revenue.toLocaleString()}`} icon="💰" color="from-green-500 to-emerald-400" />
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
| 69 |
+
{/* Quick Actions */}
|
| 70 |
+
<div className="lg:col-span-1 bg-white/5 border border-white/10 rounded-3xl p-8 backdrop-blur-xl">
|
| 71 |
+
<h2 className="text-xl font-bold mb-6">Acciones Rápidas</h2>
|
| 72 |
+
<div className="space-y-4">
|
| 73 |
+
<QuickAction href="/inventory" label="Nueva Existencia" icon="➕" />
|
| 74 |
+
<QuickAction href="/sales" label="Registrar Venta" icon="🏷️" />
|
| 75 |
+
<QuickAction href="/clients" label="Añadir Cliente" icon="👤" />
|
| 76 |
+
<QuickAction href="/hr" label="Gestionar Personal" icon="👔" />
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
{/* Charts Mockup / Future Widget */}
|
| 81 |
+
<div className="lg:col-span-2 bg-white/5 border border-white/10 rounded-3xl p-8 backdrop-blur-xl relative overflow-hidden group">
|
| 82 |
+
<div className="absolute inset-0 bg-gradient-to-br from-blue-600/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700"></div>
|
| 83 |
+
<div className="relative z-10">
|
| 84 |
+
<h2 className="text-xl font-bold mb-6">Rendimiento Semanal</h2>
|
| 85 |
+
<div className="h-64 flex items-end justify-between gap-2">
|
| 86 |
+
{[60, 45, 80, 55, 95, 70, 85].map((h, i) => (
|
| 87 |
+
<div key={i} className="flex-1 flex flex-col items-center">
|
| 88 |
+
<div
|
| 89 |
+
className="w-full bg-gradient-to-t from-blue-600 to-cyan-400 rounded-t-lg transition-all duration-1000 delay-150"
|
| 90 |
+
style={{ height: `${h}%` }}
|
| 91 |
+
></div>
|
| 92 |
+
<span className="text-[10px] text-gray-500 mt-2">{['L', 'M', 'X', 'J', 'V', 'S', 'D'][i]}</span>
|
| 93 |
+
</div>
|
| 94 |
+
))}
|
| 95 |
+
</div>
|
| 96 |
+
<p className="mt-8 text-sm text-gray-400 items-center flex gap-2">
|
| 97 |
+
<span className="w-2 h-2 rounded-full bg-green-400 animate-pulse"></span>
|
| 98 |
+
Actualizado en tiempo real con Firestore
|
| 99 |
+
</p>
|
| 100 |
+
</div>
|
| 101 |
</div>
|
| 102 |
+
</div>
|
| 103 |
</div>
|
| 104 |
);
|
| 105 |
}
|
| 106 |
+
|
| 107 |
+
function StatCard({ title, value, icon, color }: { title: string, value: string | number, icon: string, color: string }) {
|
| 108 |
+
return (
|
| 109 |
+
<div className="bg-white/5 border border-white/10 rounded-3xl p-6 backdrop-blur-md relative overflow-hidden group hover:scale-[1.02] transition-all">
|
| 110 |
+
<div className={`absolute -right-4 -top-4 w-24 h-24 bg-gradient-to-br ${color} opacity-20 blur-2xl rounded-full group-hover:opacity-40 transition-opacity`}></div>
|
| 111 |
+
<div className="flex items-center gap-4 relative z-10">
|
| 112 |
+
<div className="text-3xl">{icon}</div>
|
| 113 |
+
<div>
|
| 114 |
+
<p className="text-sm text-gray-400 uppercase tracking-wider font-semibold">{title}</p>
|
| 115 |
+
<p className="text-2xl font-bold">{value}</p>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
function QuickAction({ href, label, icon }: { href: string, label: string, icon: string }) {
|
| 123 |
+
return (
|
| 124 |
+
<Link href={href} className="flex items-center gap-4 p-4 rounded-2xl bg-white/5 border border-white/5 hover:bg-white/10 hover:border-white/20 transition-all group">
|
| 125 |
+
<span className="text-xl group-hover:scale-125 transition-transform">{icon}</span>
|
| 126 |
+
<span className="font-medium text-gray-200">{label}</span>
|
| 127 |
+
<span className="ml-auto opacity-0 group-hover:opacity-100 transition-opacity">→</span>
|
| 128 |
+
</Link>
|
| 129 |
+
);
|
| 130 |
+
}
|
app/sales/page.tsx
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React, { useState, useEffect } from "react";
|
| 4 |
+
import { db } from "@/lib/firebase";
|
| 5 |
+
import { collection, onSnapshot, addDoc, serverTimestamp } from "firebase/firestore";
|
| 6 |
+
import Link from "next/link";
|
| 7 |
+
|
| 8 |
+
interface Sale {
|
| 9 |
+
id: string;
|
| 10 |
+
client: string;
|
| 11 |
+
items: string;
|
| 12 |
+
total: number;
|
| 13 |
+
date: any;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export default function SalesPage() {
|
| 17 |
+
const [sales, setSales] = useState<Sale[]>([]);
|
| 18 |
+
const [products, setProducts] = useState<any[]>([]);
|
| 19 |
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
| 20 |
+
const [newSale, setNewSale] = useState({ client: "", items: "", total: 0 });
|
| 21 |
+
|
| 22 |
+
useEffect(() => {
|
| 23 |
+
const unsubSales = onSnapshot(collection(db, "sales"), (snap) => {
|
| 24 |
+
const data = snap.docs.map(doc => ({ id: doc.id, ...doc.data() } as Sale));
|
| 25 |
+
setSales(data);
|
| 26 |
+
});
|
| 27 |
+
const unsubProducts = onSnapshot(collection(db, "products"), (snap) => {
|
| 28 |
+
setProducts(snap.docs.map(doc => ({ id: doc.id, ...doc.data() })));
|
| 29 |
+
});
|
| 30 |
+
return () => { unsubSales(); unsubProducts(); };
|
| 31 |
+
}, []);
|
| 32 |
+
|
| 33 |
+
const handleAdd = async (e: React.FormEvent) => {
|
| 34 |
+
e.preventDefault();
|
| 35 |
+
await addDoc(collection(db, "sales"), {
|
| 36 |
+
...newSale,
|
| 37 |
+
date: serverTimestamp()
|
| 38 |
+
});
|
| 39 |
+
setIsModalOpen(false);
|
| 40 |
+
setNewSale({ client: "", items: "", total: 0 });
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
return (
|
| 44 |
+
<div className="min-h-screen bg-[#0f172a] text-white p-6">
|
| 45 |
+
<header className="flex justify-between items-center mb-10">
|
| 46 |
+
<div>
|
| 47 |
+
<Link href="/" className="text-orange-400 hover:text-orange-300 transition-colors flex items-center gap-2 mb-2 font-medium">
|
| 48 |
+
← Home
|
| 49 |
+
</Link>
|
| 50 |
+
<h1 className="text-4xl font-black bg-gradient-to-r from-orange-400 to-yellow-500 bg-clip-text text-transparent">Historial de Ventas</h1>
|
| 51 |
+
</div>
|
| 52 |
+
<button
|
| 53 |
+
onClick={() => setIsModalOpen(true)}
|
| 54 |
+
className="px-10 py-4 bg-orange-600 hover:bg-orange-500 rounded-full font-black text-xs uppercase tracking-widest transition-all shadow-xl shadow-orange-900/40 transform hover:scale-105 active:scale-95"
|
| 55 |
+
>
|
| 56 |
+
➕ Registrar Transacción
|
| 57 |
+
</button>
|
| 58 |
+
</header>
|
| 59 |
+
|
| 60 |
+
<div className="bg-white/5 border border-white/10 rounded-[3rem] p-10 backdrop-blur-2xl">
|
| 61 |
+
<div className="space-y-6">
|
| 62 |
+
{sales.map((s) => (
|
| 63 |
+
<div key={s.id} className="flex items-center justify-between p-6 bg-white/5 border border-white/5 rounded-3xl hover:bg-white/10 transition-all group">
|
| 64 |
+
<div className="flex items-center gap-6">
|
| 65 |
+
<div className="w-14 h-14 bg-gradient-to-br from-orange-500 to-yellow-500 rounded-2xl flex items-center justify-center text-2xl shadow-lg">🧾</div>
|
| 66 |
+
<div>
|
| 67 |
+
<h3 className="font-bold text-lg group-hover:text-orange-400 transition-colors uppercase tracking-tight">{s.client}</h3>
|
| 68 |
+
<p className="text-gray-500 text-xs font-medium mt-1">PRODUCTOS: <span className="text-gray-400">{s.items}</span></p>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
<div className="text-right">
|
| 72 |
+
<p className="text-2xl font-black text-white">${Number(s.total).toLocaleString()}</p>
|
| 73 |
+
<p className="text-[10px] text-gray-600 font-black uppercase mt-1">Completado</p>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
))}
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
{isModalOpen && (
|
| 81 |
+
<div className="fixed inset-0 bg-[#0f172a]/95 backdrop-blur-3xl flex items-center justify-center p-6 z-50">
|
| 82 |
+
<div className="bg-white/5 border border-white/10 p-12 rounded-[50px] w-full max-w-lg shadow-2xl relative overflow-hidden">
|
| 83 |
+
<div className="absolute top-0 right-0 w-32 h-32 bg-orange-500/10 blur-[80px] rounded-full"></div>
|
| 84 |
+
<h2 className="text-3xl font-black mb-10 tracking-tighter italic">NUEVA VENTA /</h2>
|
| 85 |
+
<form onSubmit={handleAdd} className="space-y-6">
|
| 86 |
+
<input
|
| 87 |
+
placeholder="Nombre del Cliente" required
|
| 88 |
+
value={newSale.client} onChange={e => setNewSale({...newSale, client: e.target.value})}
|
| 89 |
+
className="w-full bg-white/5 border-b border-white/10 py-4 text-xl outline-none focus:border-orange-500 transition-all font-bold"
|
| 90 |
+
/>
|
| 91 |
+
<input
|
| 92 |
+
placeholder="Descripción de Ítems" required
|
| 93 |
+
value={newSale.items} onChange={e => setNewSale({...newSale, items: e.target.value})}
|
| 94 |
+
className="w-full bg-white/5 border-b border-white/10 py-4 text-lg outline-none focus:border-orange-500 transition-all"
|
| 95 |
+
/>
|
| 96 |
+
<div className="relative">
|
| 97 |
+
<span className="absolute left-0 top-1/2 -translate-y-1/2 text-3xl font-black text-gray-700">$</span>
|
| 98 |
+
<input
|
| 99 |
+
type="number" placeholder="0.00" required
|
| 100 |
+
value={newSale.total} onChange={e => setNewSale({...newSale, total: Number(e.target.value)})}
|
| 101 |
+
className="w-full bg-transparent border-b border-white/10 pl-10 py-6 text-6xl outline-none focus:border-orange-500 transition-all font-black text-orange-400 placeholder:text-gray-800"
|
| 102 |
+
/>
|
| 103 |
+
</div>
|
| 104 |
+
<div className="flex gap-4 pt-12">
|
| 105 |
+
<button type="button" onClick={() => setIsModalOpen(false)} className="px-8 py-5 text-gray-500 font-black text-xs uppercase hover:text-white transition-colors tracking-widest">Descartar</button>
|
| 106 |
+
<button type="submit" className="flex-1 bg-orange-600 hover:bg-orange-500 py-5 rounded-3xl font-black text-xs uppercase tracking-[0.2em] transition-all shadow-xl shadow-orange-900/20">Finalizar Venta</button>
|
| 107 |
+
</div>
|
| 108 |
+
</form>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
)}
|
| 112 |
+
</div>
|
| 113 |
+
);
|
| 114 |
+
}
|
lib/firebase.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { initializeApp, getApps } from "firebase/app";
|
| 2 |
+
import { getAuth } from "firebase/auth";
|
| 3 |
+
import { getFirestore } from "firebase/firestore";
|
| 4 |
+
|
| 5 |
+
const firebaseConfig = {
|
| 6 |
+
apiKey: "AIzaSyD0gjMSH04oiyUIjqTiUs3zuLkW7UP1x-s",
|
| 7 |
+
authDomain: "erpjsf.firebaseapp.com",
|
| 8 |
+
projectId: "erpjsf",
|
| 9 |
+
storageBucket: "erpjsf.firebasestorage.app",
|
| 10 |
+
messagingSenderId: "996985286814",
|
| 11 |
+
appId: "1:996985286814:web:7e02e9a31da1deac638b8f"
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
// Initialize Firebase
|
| 15 |
+
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
|
| 16 |
+
const auth = getAuth(app);
|
| 17 |
+
const db = getFirestore(app);
|
| 18 |
+
|
| 19 |
+
export { app, auth, db };
|