Compare commits

..

No commits in common. "777693c7dde151d3ba18953088d5ea7a1224e4e3" and "ad4ab40389310b4db4612849cdbdebbc0c49fdfb" have entirely different histories.

13 changed files with 130 additions and 4645 deletions

View File

@ -1,13 +1,10 @@
<!doctype html> <!doctype html>
<html lang="es"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#f97316" /> <title>frontend</title>
<meta name="description" content="MiChangarrito - Sistema POS para changarritos" />
<title>MiChangarrito</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

4454
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,6 @@
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.46.4", "typescript-eslint": "^8.46.4",
"vite": "^7.2.4", "vite": "^7.2.4"
"vite-plugin-pwa": "^1.2.0"
} }
} }

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" width="180" height="180">
<rect width="180" height="180" fill="#f97316" rx="22"/>
<text x="90" y="112" font-family="Arial, sans-serif" font-size="68" font-weight="bold" fill="white" text-anchor="middle">MCH</text>
</svg>

Before

Width:  |  Height:  |  Size: 287 B

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
<rect width="32" height="32" fill="#f97316" rx="4"/>
<text x="16" y="22" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="white" text-anchor="middle">M</text>
</svg>

Before

Width:  |  Height:  |  Size: 277 B

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192" width="192" height="192">
<rect width="192" height="192" fill="#f97316" rx="24"/>
<text x="96" y="120" font-family="Arial, sans-serif" font-size="72" font-weight="bold" fill="white" text-anchor="middle">MCH</text>
</svg>

Before

Width:  |  Height:  |  Size: 287 B

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<rect width="512" height="512" fill="#f97316" rx="64"/>
<text x="256" y="320" font-family="Arial, sans-serif" font-size="192" font-weight="bold" fill="white" text-anchor="middle">MCH</text>
</svg>

Before

Width:  |  Height:  |  Size: 289 B

View File

@ -1,7 +1,6 @@
import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom'; import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider, useAuth } from './contexts/AuthContext'; import { AuthProvider, useAuth } from './contexts/AuthContext';
import { ThemeProvider } from './contexts/ThemeContext';
import { Layout } from './components/Layout'; import { Layout } from './components/Layout';
import { Dashboard } from './pages/Dashboard'; import { Dashboard } from './pages/Dashboard';
import { Products } from './pages/Products'; import { Products } from './pages/Products';
@ -60,41 +59,39 @@ function PublicRoute() {
function App() { function App() {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ThemeProvider> <AuthProvider>
<AuthProvider> <BrowserRouter>
<BrowserRouter> <Routes>
<Routes> {/* Public routes */}
{/* Public routes */} <Route element={<PublicRoute />}>
<Route element={<PublicRoute />}> <Route path="/login" element={<Login />} />
<Route path="/login" element={<Login />} /> <Route path="/register" element={<Register />} />
<Route path="/register" element={<Register />} /> </Route>
</Route>
{/* Protected routes */} {/* Protected routes */}
<Route element={<ProtectedRoute />}> <Route element={<ProtectedRoute />}>
<Route path="/" element={<Layout />}> <Route path="/" element={<Layout />}>
<Route index element={<Navigate to="/dashboard" replace />} /> <Route index element={<Navigate to="/dashboard" replace />} />
<Route path="dashboard" element={<Dashboard />} /> <Route path="dashboard" element={<Dashboard />} />
<Route path="products" element={<Products />} /> <Route path="products" element={<Products />} />
<Route path="orders" element={<Orders />} /> <Route path="orders" element={<Orders />} />
<Route path="customers" element={<Customers />} /> <Route path="customers" element={<Customers />} />
<Route path="fiado" element={<Fiado />} /> <Route path="fiado" element={<Fiado />} />
<Route path="inventory" element={<Inventory />} /> <Route path="inventory" element={<Inventory />} />
<Route path="referrals" element={<Referrals />} /> <Route path="referrals" element={<Referrals />} />
<Route path="invoices" element={<Invoices />} /> <Route path="invoices" element={<Invoices />} />
<Route path="marketplace" element={<Marketplace />} /> <Route path="marketplace" element={<Marketplace />} />
<Route path="tokens" element={<Tokens />} /> <Route path="tokens" element={<Tokens />} />
<Route path="codi-spei" element={<CodiSpei />} /> <Route path="codi-spei" element={<CodiSpei />} />
<Route path="settings" element={<Settings />} /> <Route path="settings" element={<Settings />} />
</Route>
</Route> </Route>
</Route>
{/* Catch all */} {/* Catch all */}
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</AuthProvider> </AuthProvider>
</ThemeProvider>
</QueryClientProvider> </QueryClientProvider>
); );
} }

View File

@ -16,13 +16,10 @@ import {
Truck, Truck,
Coins, Coins,
QrCode, QrCode,
Sun,
Moon,
} from 'lucide-react'; } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { useTheme } from '../contexts/ThemeContext';
const navigation = [ const navigation = [
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard }, { name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
@ -42,7 +39,6 @@ const navigation = [
export function Layout() { export function Layout() {
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
const { user, tenant, logout } = useAuth(); const { user, tenant, logout } = useAuth();
const { resolvedTheme, toggleTheme } = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const handleLogout = () => { const handleLogout = () => {
@ -60,7 +56,7 @@ export function Layout() {
}; };
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors duration-200"> <div className="min-h-screen bg-gray-50">
{/* Mobile sidebar */} {/* Mobile sidebar */}
<div <div
className={clsx( className={clsx(
@ -72,13 +68,13 @@ export function Layout() {
className="fixed inset-0 bg-gray-900/80" className="fixed inset-0 bg-gray-900/80"
onClick={() => setSidebarOpen(false)} onClick={() => setSidebarOpen(false)}
/> />
<div className="fixed inset-y-0 left-0 w-64 bg-white dark:bg-gray-800 shadow-xl"> <div className="fixed inset-y-0 left-0 w-64 bg-white shadow-xl">
<div className="flex h-16 items-center justify-between px-4 border-b dark:border-gray-700"> <div className="flex h-16 items-center justify-between px-4 border-b">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Store className="h-8 w-8 text-primary-500" /> <Store className="h-8 w-8 text-primary-500" />
<span className="font-bold text-lg dark:text-gray-100">MiChangarrito</span> <span className="font-bold text-lg">MiChangarrito</span>
</div> </div>
<button onClick={() => setSidebarOpen(false)} className="dark:text-gray-300"> <button onClick={() => setSidebarOpen(false)}>
<X className="h-6 w-6" /> <X className="h-6 w-6" />
</button> </button>
</div> </div>
@ -92,8 +88,8 @@ export function Layout() {
clsx( clsx(
'flex items-center gap-3 px-3 py-2 rounded-lg transition-colors', 'flex items-center gap-3 px-3 py-2 rounded-lg transition-colors',
isActive isActive
? 'bg-primary-50 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400' ? 'bg-primary-50 text-primary-600'
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700' : 'text-gray-700 hover:bg-gray-100'
) )
} }
> >
@ -107,23 +103,12 @@ export function Layout() {
{/* Desktop sidebar */} {/* Desktop sidebar */}
<div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col"> <div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col">
<div className="flex flex-col flex-grow bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transition-colors duration-200"> <div className="flex flex-col flex-grow bg-white border-r border-gray-200">
<div className="flex h-16 items-center justify-between px-4 border-b dark:border-gray-700"> <div className="flex h-16 items-center px-4 border-b">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Store className="h-8 w-8 text-primary-500" /> <Store className="h-8 w-8 text-primary-500" />
<span className="font-bold text-lg dark:text-gray-100">MiChangarrito</span> <span className="font-bold text-lg">MiChangarrito</span>
</div> </div>
<button
onClick={toggleTheme}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
title={resolvedTheme === 'dark' ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'}
>
{resolvedTheme === 'dark' ? (
<Sun className="h-5 w-5" />
) : (
<Moon className="h-5 w-5" />
)}
</button>
</div> </div>
<nav className="flex-1 p-4 space-y-1"> <nav className="flex-1 p-4 space-y-1">
{navigation.map((item) => ( {navigation.map((item) => (
@ -134,8 +119,8 @@ export function Layout() {
clsx( clsx(
'flex items-center gap-3 px-3 py-2 rounded-lg transition-colors', 'flex items-center gap-3 px-3 py-2 rounded-lg transition-colors',
isActive isActive
? 'bg-primary-50 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400' ? 'bg-primary-50 text-primary-600'
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700' : 'text-gray-700 hover:bg-gray-100'
) )
} }
> >
@ -144,22 +129,22 @@ export function Layout() {
</NavLink> </NavLink>
))} ))}
</nav> </nav>
<div className="p-4 border-t dark:border-gray-700"> <div className="p-4 border-t">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-emerald-100 dark:bg-emerald-900/50 flex items-center justify-center"> <div className="h-10 w-10 rounded-full bg-emerald-100 flex items-center justify-center">
<span className="text-emerald-600 dark:text-emerald-400 font-semibold"> <span className="text-emerald-600 font-semibold">
{tenant ? getInitials(tenant.name) : 'MC'} {tenant ? getInitials(tenant.name) : 'MC'}
</span> </span>
</div> </div>
<div> <div>
<p className="text-sm font-medium dark:text-gray-100">{tenant?.name || 'Mi Negocio'}</p> <p className="text-sm font-medium">{tenant?.name || 'Mi Negocio'}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{user?.name}</p> <p className="text-xs text-gray-500">{user?.name}</p>
</div> </div>
</div> </div>
<button <button
onClick={handleLogout} onClick={handleLogout}
className="p-2 text-gray-400 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-400 transition-colors" className="p-2 text-gray-400 hover:text-red-500 transition-colors"
title="Cerrar sesion" title="Cerrar sesion"
> >
<LogOut className="h-5 w-5" /> <LogOut className="h-5 w-5" />
@ -172,27 +157,14 @@ export function Layout() {
{/* Main content */} {/* Main content */}
<div className="lg:pl-64"> <div className="lg:pl-64">
{/* Mobile header */} {/* Mobile header */}
<div className="sticky top-0 z-40 flex h-16 items-center justify-between gap-4 bg-white dark:bg-gray-800 border-b dark:border-gray-700 px-4 lg:hidden transition-colors duration-200"> <div className="sticky top-0 z-40 flex h-16 items-center gap-4 bg-white border-b px-4 lg:hidden">
<div className="flex items-center gap-4"> <button onClick={() => setSidebarOpen(true)}>
<button onClick={() => setSidebarOpen(true)} className="dark:text-gray-300"> <Menu className="h-6 w-6" />
<Menu className="h-6 w-6" />
</button>
<div className="flex items-center gap-2">
<Store className="h-6 w-6 text-primary-500" />
<span className="font-bold dark:text-gray-100">MiChangarrito</span>
</div>
</div>
<button
onClick={toggleTheme}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
title={resolvedTheme === 'dark' ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'}
>
{resolvedTheme === 'dark' ? (
<Sun className="h-5 w-5" />
) : (
<Moon className="h-5 w-5" />
)}
</button> </button>
<div className="flex items-center gap-2">
<Store className="h-6 w-6 text-primary-500" />
<span className="font-bold">MiChangarrito</span>
</div>
</div> </div>
{/* Page content */} {/* Page content */}

View File

@ -1,103 +0,0 @@
import { createContext, useContext, useState, useEffect } from 'react';
import type { ReactNode } from 'react';
type Theme = 'light' | 'dark' | 'system';
interface ThemeContextType {
theme: Theme;
resolvedTheme: 'light' | 'dark';
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
const STORAGE_KEY = 'michangarrito-theme';
function getSystemTheme(): 'light' | 'dark' {
if (typeof window !== 'undefined' && window.matchMedia) {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return 'light';
}
function getStoredTheme(): Theme {
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === 'light' || stored === 'dark' || stored === 'system') {
return stored;
}
}
return 'system';
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(getStoredTheme);
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light');
// Resolve the actual theme (handles 'system' preference)
useEffect(() => {
const resolved = theme === 'system' ? getSystemTheme() : theme;
setResolvedTheme(resolved);
// Apply class to document
const root = document.documentElement;
if (resolved === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
}, [theme]);
// Listen for system theme changes
useEffect(() => {
if (theme !== 'system') return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
const newTheme = e.matches ? 'dark' : 'light';
setResolvedTheme(newTheme);
const root = document.documentElement;
if (newTheme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [theme]);
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem(STORAGE_KEY, newTheme);
};
const toggleTheme = () => {
const newTheme = resolvedTheme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
};
return (
<ThemeContext.Provider
value={{
theme,
resolvedTheme,
setTheme,
toggleTheme,
}}
>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

View File

@ -26,12 +26,7 @@
@layer base { @layer base {
body { body {
@apply bg-gray-50 text-gray-900 transition-colors duration-200; @apply bg-gray-50 text-gray-900;
}
.dark body,
html.dark body {
@apply bg-gray-900 text-gray-100;
} }
} }
@ -46,16 +41,13 @@
.btn-outline { .btn-outline {
@apply border border-gray-300 hover:border-gray-400 text-gray-700 font-medium py-2 px-4 rounded-lg transition-colors; @apply border border-gray-300 hover:border-gray-400 text-gray-700 font-medium py-2 px-4 rounded-lg transition-colors;
@apply dark:border-gray-600 dark:hover:border-gray-500 dark:text-gray-300;
} }
.card { .card {
@apply bg-white rounded-xl shadow-sm border border-gray-200 p-4; @apply bg-white rounded-xl shadow-sm border border-gray-200 p-4;
@apply dark:bg-gray-800 dark:border-gray-700;
} }
.input { .input {
@apply w-full rounded-lg border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500; @apply w-full rounded-lg border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500;
@apply dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100 dark:placeholder-gray-400;
} }
} }

View File

@ -1,6 +1,5 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
darkMode: 'class',
content: [ content: [
"./index.html", "./index.html",
"./src/**/*.{js,ts,jsx,tsx}", "./src/**/*.{js,ts,jsx,tsx}",

View File

@ -1,45 +1,9 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [react()],
react(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.svg', 'robots.txt', 'apple-touch-icon.svg'],
manifest: {
name: 'MiChangarrito',
short_name: 'Changarrito',
description: 'Sistema POS para changarritos',
theme_color: '#f97316',
background_color: '#ffffff',
display: 'standalone',
icons: [
{
src: 'pwa-192x192.svg',
sizes: '192x192',
type: 'image/svg+xml'
},
{
src: 'pwa-512x512.svg',
sizes: '512x512',
type: 'image/svg+xml'
},
{
src: 'pwa-512x512.svg',
sizes: '512x512',
type: 'image/svg+xml',
purpose: 'any maskable'
}
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
}
})
],
server: { server: {
port: 3140, port: 3140,
proxy: { proxy: {