dimensionalpulsar commited on
Commit
8457d97
·
verified ·
1 Parent(s): 0681063

Upload 29 files

Browse files
.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
AGENTS.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+ <!-- END:nextjs-agent-rules -->
CLAUDE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ @AGENTS.md
Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- STAGE 1: Build ---
2
+ FROM node:20-alpine AS builder
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY package.json package-lock.json ./
7
+ RUN npm ci
8
+
9
+ # Copy source and build
10
+ COPY . .
11
+ RUN npm run build
12
+
13
+ # --- STAGE 2: Runtime ---
14
+ FROM node:20-alpine AS runner
15
+ WORKDIR /app
16
+
17
+ ENV NODE_ENV=production
18
+ ENV PORT=7860
19
+
20
+ # Copy necessary files from builder
21
+ COPY --from=builder /app/next.config.ts ./
22
+ COPY --from=builder /app/public ./public
23
+ COPY --from=builder /app/.next ./.next
24
+ COPY --from=builder /app/node_modules ./node_modules
25
+ COPY --from=builder /app/package.json ./package.json
26
+
27
+ EXPOSE 7860
28
+
29
+ # Start the application
30
+ CMD ["npm", "start"]
README.md CHANGED
@@ -1,11 +1,36 @@
1
- ---
2
- title: Restoplus
3
- emoji: 🏃
4
- colorFrom: yellow
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
app/favicon.ico ADDED
app/globals.css ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #ffffff;
5
+ --foreground: #171717;
6
+ }
7
+
8
+ @theme inline {
9
+ --color-background: var(--background);
10
+ --color-foreground: var(--foreground);
11
+ --font-sans: var(--font-geist-sans);
12
+ --font-mono: var(--font-geist-mono);
13
+ }
14
+
15
+ @media (prefers-color-scheme: dark) {
16
+ :root {
17
+ --background: #0a0a0a;
18
+ --foreground: #ededed;
19
+ }
20
+ }
21
+
22
+ body {
23
+ background: var(--background);
24
+ color: var(--foreground);
25
+ font-family: Arial, Helvetica, sans-serif;
26
+ }
app/layout.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+
5
+ const geistSans = Geist({
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: "Create Next App",
17
+ description: "Generated by create next app",
18
+ };
19
+
20
+ export default function RootLayout({
21
+ children,
22
+ }: Readonly<{
23
+ children: React.ReactNode;
24
+ }>) {
25
+ return (
26
+ <html
27
+ lang="en"
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
+ }
app/page.tsx ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Image from "next/image";
2
+
3
+ export default function Home() {
4
+ return (
5
+ <div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
6
+ <main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
7
+ <Image
8
+ className="dark:invert"
9
+ src="/next.svg"
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="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
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 flex-col gap-4 text-base font-medium sm:flex-row">
38
+ <a
39
+ className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
40
+ href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
41
+ target="_blank"
42
+ rel="noopener noreferrer"
43
+ >
44
+ <Image
45
+ className="dark:invert"
46
+ src="/vercel.svg"
47
+ alt="Vercel logomark"
48
+ width={16}
49
+ height={16}
50
+ />
51
+ Deploy Now
52
+ </a>
53
+ <a
54
+ className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
55
+ href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
56
+ target="_blank"
57
+ rel="noopener noreferrer"
58
+ >
59
+ Documentation
60
+ </a>
61
+ </div>
62
+ </main>
63
+ </div>
64
+ );
65
+ }
eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
next-env.d.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/types/routes.d.ts";
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
next.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ };
6
+
7
+ export default nextConfig;
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "erp-next",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "firebase": "^12.11.0",
13
+ "next": "16.2.1",
14
+ "react": "19.2.4",
15
+ "react-dom": "19.2.4"
16
+ },
17
+ "devDependencies": {
18
+ "@tailwindcss/postcss": "^4",
19
+ "@types/node": "^20",
20
+ "@types/react": "^19",
21
+ "@types/react-dom": "^19",
22
+ "eslint": "^9",
23
+ "eslint-config-next": "16.2.1",
24
+ "tailwindcss": "^4",
25
+ "typescript": "^5"
26
+ }
27
+ }
postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
public/file.svg ADDED
public/globe.svg ADDED
public/next.svg ADDED
public/vercel.svg ADDED
public/window.svg ADDED
src/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
+ }
src/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
+ }
src/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
+ }
src/app/layout.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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({
13
+ children,
14
+ }: Readonly<{
15
+ children: React.ReactNode;
16
+ }>) {
17
+ return (
18
+ <html lang="es">
19
+ <body className={`${inter.className} bg-[#0f172a]`}>{children}</body>
20
+ </html>
21
+ );
22
+ }
src/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
+ }
src/app/page.tsx ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
src/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
+ }
src/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 };
tsconfig.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }