Spaces:
Sleeping
Sleeping
Upload 29 files
Browse files- .gitignore +41 -0
- AGENTS.md +5 -0
- CLAUDE.md +1 -0
- Dockerfile +30 -0
- README.md +36 -11
- app/favicon.ico +0 -0
- app/globals.css +26 -0
- app/layout.tsx +33 -0
- app/page.tsx +65 -0
- eslint.config.mjs +18 -0
- next-env.d.ts +6 -0
- next.config.ts +7 -0
- package-lock.json +0 -0
- package.json +27 -0
- postcss.config.mjs +7 -0
- public/file.svg +1 -0
- public/globe.svg +1 -0
- public/next.svg +1 -0
- public/vercel.svg +1 -0
- public/window.svg +1 -0
- src/app/clients/page.tsx +102 -0
- src/app/hr/page.tsx +105 -0
- src/app/inventory/page.tsx +132 -0
- src/app/layout.tsx +22 -0
- src/app/login/page.tsx +107 -0
- src/app/page.tsx +130 -0
- src/app/sales/page.tsx +114 -0
- src/lib/firebase.ts +19 -0
- tsconfig.json +34 -0
.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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|