Initial deploy commit
This commit is contained in:
commit
3f91de3672
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
152
README.md
Normal file
152
README.md
Normal file
@ -0,0 +1,152 @@
|
||||
# Frontend - ERP Mecanicas Diesel
|
||||
|
||||
## Stack Tecnologico
|
||||
|
||||
| Tecnologia | Version | Proposito |
|
||||
|------------|---------|-----------|
|
||||
| React | 18.x | Framework UI |
|
||||
| Vite | 6.x | Build tool |
|
||||
| TypeScript | 5.x | Lenguaje |
|
||||
| React Router | 6.x | Routing |
|
||||
| Zustand | 5.x | State management |
|
||||
| React Query | 5.x | Data fetching/caching |
|
||||
| Tailwind CSS | 3.x | Styling |
|
||||
| React Hook Form | 7.x | Formularios |
|
||||
| Zod | 3.x | Validacion |
|
||||
| Axios | 1.x | HTTP client |
|
||||
| Lucide React | - | Iconos |
|
||||
|
||||
## Estructura del Proyecto
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── common/ # Componentes reutilizables (Button, Input, etc)
|
||||
│ ├── layout/ # Layout principal (Sidebar, Header, MainLayout)
|
||||
│ └── features/ # Componentes especificos por modulo
|
||||
├── features/ # Logica por modulo/epic
|
||||
│ ├── auth/ # Autenticacion
|
||||
│ ├── service-orders/ # MMD-002: Ordenes de servicio
|
||||
│ ├── diagnostics/ # MMD-003: Diagnosticos
|
||||
│ ├── inventory/ # MMD-004: Inventario
|
||||
│ ├── vehicles/ # MMD-005: Vehiculos
|
||||
│ ├── quotes/ # MMD-006: Cotizaciones
|
||||
│ └── settings/ # MMD-001: Configuracion
|
||||
├── store/ # Zustand stores
|
||||
│ ├── authStore.ts # Estado de autenticacion
|
||||
│ └── tallerStore.ts # Estado del taller
|
||||
├── services/
|
||||
│ └── api/ # Clientes API
|
||||
│ ├── client.ts # Axios instance con interceptors
|
||||
│ ├── auth.ts # Endpoints de auth
|
||||
│ └── serviceOrders.ts # Endpoints de ordenes
|
||||
├── pages/ # Paginas/Vistas
|
||||
│ ├── Login.tsx
|
||||
│ └── Dashboard.tsx
|
||||
├── hooks/ # Custom React hooks
|
||||
├── types/ # TypeScript types
|
||||
│ └── index.ts # Tipos base
|
||||
├── utils/ # Utilidades
|
||||
├── App.tsx # Router principal
|
||||
├── main.tsx # Entry point
|
||||
└── index.css # Tailwind imports
|
||||
```
|
||||
|
||||
## Comandos
|
||||
|
||||
```bash
|
||||
# Instalar dependencias
|
||||
npm install
|
||||
|
||||
# Desarrollo
|
||||
npm run dev
|
||||
|
||||
# Build produccion
|
||||
npm run build
|
||||
|
||||
# Preview build
|
||||
npm run preview
|
||||
|
||||
# Lint
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## Variables de Entorno
|
||||
|
||||
Crear archivo `.env.local`:
|
||||
|
||||
```env
|
||||
VITE_API_URL=http://localhost:3041/api/v1
|
||||
```
|
||||
|
||||
## Modulos por Implementar
|
||||
|
||||
### MMD-001: Fundamentos (Sprint 1-2)
|
||||
- [ ] Configuracion de taller (wizard)
|
||||
- [ ] Gestion de roles
|
||||
- [ ] Catalogo de servicios
|
||||
- [ ] Gestion de bahias
|
||||
|
||||
### MMD-002: Ordenes de Servicio (Sprint 2-5)
|
||||
- [ ] Lista de ordenes con filtros
|
||||
- [ ] Detalle de orden
|
||||
- [ ] Crear orden (wizard 4 pasos)
|
||||
- [ ] Tablero Kanban
|
||||
- [ ] Registro de trabajos
|
||||
|
||||
### MMD-003: Diagnosticos (Sprint 2-4)
|
||||
- [ ] Lista de diagnosticos
|
||||
- [ ] Scanner OBD (DTC codes)
|
||||
- [ ] Pruebas de banco
|
||||
- [ ] Galeria de fotos
|
||||
|
||||
### MMD-004: Inventario (Sprint 4-6)
|
||||
- [ ] Catalogo de refacciones
|
||||
- [ ] Kardex de movimientos
|
||||
- [ ] Alertas de stock
|
||||
- [ ] Recepcion de mercancia
|
||||
|
||||
### MMD-005: Vehiculos (Sprint 4-6)
|
||||
- [ ] Lista de vehiculos
|
||||
- [ ] Ficha tecnica
|
||||
- [ ] Especificaciones de motor
|
||||
- [ ] Historial de servicios
|
||||
|
||||
### MMD-006: Cotizaciones (Sprint 6)
|
||||
- [ ] Lista de cotizaciones
|
||||
- [ ] Crear cotizacion
|
||||
- [ ] Generar PDF
|
||||
- [ ] Envio por email/WhatsApp
|
||||
|
||||
## Convenciones
|
||||
|
||||
### Nombres de Archivos
|
||||
- Componentes: `PascalCase.tsx`
|
||||
- Hooks: `useCamelCase.ts`
|
||||
- Stores: `camelCaseStore.ts`
|
||||
- Types: `camelCase.types.ts`
|
||||
- Services: `camelCase.ts`
|
||||
|
||||
### Estructura de Feature
|
||||
```
|
||||
features/{feature}/
|
||||
├── components/ # Componentes UI
|
||||
├── hooks/ # Custom hooks
|
||||
├── types/ # TypeScript types
|
||||
└── index.ts # Exports publicos
|
||||
```
|
||||
|
||||
## Dependencias del Backend
|
||||
|
||||
Este frontend requiere el backend de mecanicas-diesel corriendo en el puerto 3041.
|
||||
|
||||
```bash
|
||||
# Desde la raiz del proyecto
|
||||
cd ../backend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*ERP Mecanicas Diesel - Sistema NEXUS*
|
||||
*Creado: 2025-12-08*
|
||||
23
eslint.config.js
Normal file
23
eslint.config.js
Normal file
@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3731
package-lock.json
generated
Normal file
3731
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file
41
package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"axios": "^1.13.2",
|
||||
"lucide-react": "^0.556.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.68.0",
|
||||
"react-router-dom": "^6.30.2",
|
||||
"zod": "^4.1.13",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^24.10.2",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.4"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
134
src/App.tsx
Normal file
134
src/App.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { MainLayout } from './components/layout';
|
||||
import { Login } from './pages/Login';
|
||||
import { Dashboard } from './pages/Dashboard';
|
||||
import { useAuthStore } from './store/authStore';
|
||||
|
||||
// Create React Query client
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Protected route wrapper
|
||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// Placeholder pages
|
||||
function ServiceOrdersPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Ordenes de Servicio</h1>
|
||||
<p className="text-gray-500">Modulo en desarrollo...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiagnosticsPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Diagnosticos</h1>
|
||||
<p className="text-gray-500">Modulo en desarrollo...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InventoryPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Inventario</h1>
|
||||
<p className="text-gray-500">Modulo en desarrollo...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VehiclesPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Vehiculos</h1>
|
||||
<p className="text-gray-500">Modulo en desarrollo...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function QuotesPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Cotizaciones</h1>
|
||||
<p className="text-gray-500">Modulo en desarrollo...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Configuracion</h1>
|
||||
<p className="text-gray-500">Modulo en desarrollo...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/* Public routes */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
{/* Protected routes */}
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<MainLayout />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
>
|
||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="orders" element={<ServiceOrdersPage />} />
|
||||
<Route path="orders/:id" element={<ServiceOrdersPage />} />
|
||||
<Route path="diagnostics" element={<DiagnosticsPage />} />
|
||||
<Route path="diagnostics/:id" element={<DiagnosticsPage />} />
|
||||
<Route path="inventory" element={<InventoryPage />} />
|
||||
<Route path="inventory/:id" element={<InventoryPage />} />
|
||||
<Route path="vehicles" element={<VehiclesPage />} />
|
||||
<Route path="vehicles/:id" element={<VehiclesPage />} />
|
||||
<Route path="quotes" element={<QuotesPage />} />
|
||||
<Route path="quotes/:id" element={<QuotesPage />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
</Route>
|
||||
|
||||
{/* 404 */}
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold text-gray-900">404</h1>
|
||||
<p className="text-gray-500">Pagina no encontrada</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
1
src/assets/react.svg
Normal file
1
src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
50
src/components/layout/Header.tsx
Normal file
50
src/components/layout/Header.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { Bell, Search } from 'lucide-react';
|
||||
import { useTallerStore } from '../../store/tallerStore';
|
||||
|
||||
export function Header() {
|
||||
const { selectedBay, workBays, setSelectedBay } = useTallerStore();
|
||||
|
||||
return (
|
||||
<header className="flex h-16 shrink-0 items-center gap-4 border-b border-gray-200 bg-white px-6">
|
||||
{/* Search */}
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<div className="relative w-96">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar ordenes, vehiculos, clientes..."
|
||||
className="w-full rounded-lg border border-gray-300 bg-gray-50 py-2 pl-10 pr-4 text-sm focus:border-diesel-500 focus:outline-none focus:ring-1 focus:ring-diesel-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bay selector */}
|
||||
{workBays.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">Bahia:</span>
|
||||
<select
|
||||
value={selectedBay?.id || ''}
|
||||
onChange={(e) => {
|
||||
const bay = workBays.find((b) => b.id === e.target.value);
|
||||
setSelectedBay(bay || null);
|
||||
}}
|
||||
className="rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm focus:border-diesel-500 focus:outline-none focus:ring-1 focus:ring-diesel-500"
|
||||
>
|
||||
<option value="">Sin bahia</option>
|
||||
{workBays.map((bay) => (
|
||||
<option key={bay.id} value={bay.id}>
|
||||
{bay.name} ({bay.status === 'available' ? 'Disponible' : 'Ocupada'})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notifications */}
|
||||
<button className="relative rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600">
|
||||
<Bell className="h-5 w-5" />
|
||||
<span className="absolute right-1 top-1 h-2 w-2 rounded-full bg-red-500" />
|
||||
</button>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
23
src/components/layout/MainLayout.tsx
Normal file
23
src/components/layout/MainLayout.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { Header } from './Header';
|
||||
|
||||
export function MainLayout() {
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-100">
|
||||
{/* Sidebar */}
|
||||
<Sidebar />
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<Header />
|
||||
|
||||
{/* Page content */}
|
||||
<main className="flex-1 overflow-y-auto p-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
131
src/components/layout/Sidebar.tsx
Normal file
131
src/components/layout/Sidebar.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Wrench,
|
||||
Stethoscope,
|
||||
Package,
|
||||
Truck,
|
||||
FileText,
|
||||
Settings,
|
||||
LogOut,
|
||||
} from 'lucide-react';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { useTallerStore } from '../../store/tallerStore';
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
||||
{ name: 'Ordenes de Servicio', href: '/orders', icon: Wrench },
|
||||
{ name: 'Diagnosticos', href: '/diagnostics', icon: Stethoscope },
|
||||
{ name: 'Inventario', href: '/inventory', icon: Package },
|
||||
{ name: 'Vehiculos', href: '/vehicles', icon: Truck },
|
||||
{ name: 'Cotizaciones', href: '/quotes', icon: FileText },
|
||||
];
|
||||
|
||||
const secondaryNavigation = [
|
||||
{ name: 'Configuracion', href: '/settings', icon: Settings },
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const { logout, user } = useAuthStore();
|
||||
const { currentTaller } = useTallerStore();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-64 flex-col bg-gray-900">
|
||||
{/* Logo */}
|
||||
<div className="flex h-16 shrink-0 items-center px-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg bg-diesel-500 flex items-center justify-center">
|
||||
<Wrench className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<span className="text-xl font-bold text-white">Mecanicas</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Taller info */}
|
||||
{currentTaller && (
|
||||
<div className="px-4 py-2">
|
||||
<div className="rounded-lg bg-gray-800 px-3 py-2">
|
||||
<p className="text-xs text-gray-400">Taller</p>
|
||||
<p className="text-sm font-medium text-white truncate">
|
||||
{currentTaller.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex flex-1 flex-col px-4 py-4">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-1">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<NavLink
|
||||
to={item.href}
|
||||
className={({ isActive }) =>
|
||||
`group flex gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
|
||||
isActive
|
||||
? 'bg-diesel-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-800 hover:text-white'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<item.icon className="h-5 w-5 shrink-0" />
|
||||
{item.name}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Secondary navigation */}
|
||||
<ul role="list" className="mt-auto space-y-1">
|
||||
{secondaryNavigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<NavLink
|
||||
to={item.href}
|
||||
className={({ isActive }) =>
|
||||
`group flex gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
|
||||
isActive
|
||||
? 'bg-gray-800 text-white'
|
||||
: 'text-gray-400 hover:bg-gray-800 hover:text-white'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<item.icon className="h-5 w-5 shrink-0" />
|
||||
{item.name}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
|
||||
{/* Logout */}
|
||||
<li>
|
||||
<button
|
||||
onClick={() => logout()}
|
||||
className="group flex w-full gap-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-400 transition-colors hover:bg-gray-800 hover:text-white"
|
||||
>
|
||||
<LogOut className="h-5 w-5 shrink-0" />
|
||||
Cerrar Sesion
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{/* User info */}
|
||||
{user && (
|
||||
<div className="mt-4 border-t border-gray-800 pt-4">
|
||||
<div className="flex items-center gap-3 px-3">
|
||||
<div className="h-8 w-8 rounded-full bg-gray-700 flex items-center justify-center">
|
||||
<span className="text-sm font-medium text-white">
|
||||
{user.full_name.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">
|
||||
{user.full_name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 truncate">{user.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/components/layout/index.ts
Normal file
3
src/components/layout/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { MainLayout } from './MainLayout';
|
||||
export { Sidebar } from './Sidebar';
|
||||
export { Header } from './Header';
|
||||
38
src/index.css
Normal file
38
src/index.css
Normal file
@ -0,0 +1,38 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a1a1a1;
|
||||
}
|
||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
137
src/pages/Dashboard.tsx
Normal file
137
src/pages/Dashboard.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import { Wrench, Truck, Package, FileText, Clock, CheckCircle } from 'lucide-react';
|
||||
|
||||
const stats = [
|
||||
{ name: 'Ordenes Activas', value: '12', icon: Wrench, color: 'bg-blue-500' },
|
||||
{ name: 'Vehiculos Atendidos (Mes)', value: '48', icon: Truck, color: 'bg-green-500' },
|
||||
{ name: 'Alertas de Stock', value: '5', icon: Package, color: 'bg-yellow-500' },
|
||||
{ name: 'Cotizaciones Pendientes', value: '8', icon: FileText, color: 'bg-purple-500' },
|
||||
];
|
||||
|
||||
const recentOrders = [
|
||||
{ id: 'OS-2025-0142', vehicle: 'Kenworth T800', status: 'En Reparacion', customer: 'Transportes del Norte' },
|
||||
{ id: 'OS-2025-0141', vehicle: 'Freightliner Cascadia', status: 'Diagnostico', customer: 'Carga Pesada SA' },
|
||||
{ id: 'OS-2025-0140', vehicle: 'International LT', status: 'Esperando Refacciones', customer: 'Logistica Express' },
|
||||
{ id: 'OS-2025-0139', vehicle: 'Peterbilt 579', status: 'Listo', customer: 'Fletes Rapidos' },
|
||||
];
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
||||
<p className="text-sm text-gray-500">Resumen de operaciones del taller</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{stats.map((stat) => (
|
||||
<div
|
||||
key={stat.name}
|
||||
className="flex items-center gap-4 rounded-lg bg-white p-4 shadow-sm"
|
||||
>
|
||||
<div className={`rounded-lg ${stat.color} p-3`}>
|
||||
<stat.icon className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-gray-900">{stat.value}</p>
|
||||
<p className="text-sm text-gray-500">{stat.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Content grid */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
{/* Recent orders */}
|
||||
<div className="rounded-lg bg-white p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Ordenes Recientes
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{recentOrders.map((order) => (
|
||||
<div
|
||||
key={order.id}
|
||||
className="flex items-center justify-between rounded-lg border border-gray-100 p-3 hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-gray-100 flex items-center justify-center">
|
||||
<Truck className="h-5 w-5 text-gray-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">{order.id}</p>
|
||||
<p className="text-sm text-gray-500">{order.vehicle}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">
|
||||
{order.status}
|
||||
</span>
|
||||
<p className="text-sm text-gray-500 mt-1">{order.customer}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick actions */}
|
||||
<div className="rounded-lg bg-white p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Acciones Rapidas
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button className="flex flex-col items-center gap-2 rounded-lg border border-gray-200 p-4 hover:border-diesel-500 hover:bg-diesel-50 transition-colors">
|
||||
<Wrench className="h-8 w-8 text-diesel-600" />
|
||||
<span className="text-sm font-medium text-gray-700">Nueva Orden</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center gap-2 rounded-lg border border-gray-200 p-4 hover:border-diesel-500 hover:bg-diesel-50 transition-colors">
|
||||
<FileText className="h-8 w-8 text-diesel-600" />
|
||||
<span className="text-sm font-medium text-gray-700">Nueva Cotizacion</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center gap-2 rounded-lg border border-gray-200 p-4 hover:border-diesel-500 hover:bg-diesel-50 transition-colors">
|
||||
<Truck className="h-8 w-8 text-diesel-600" />
|
||||
<span className="text-sm font-medium text-gray-700">Registrar Vehiculo</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center gap-2 rounded-lg border border-gray-200 p-4 hover:border-diesel-500 hover:bg-diesel-50 transition-colors">
|
||||
<Package className="h-8 w-8 text-diesel-600" />
|
||||
<span className="text-sm font-medium text-gray-700">Recibir Mercancia</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Today's schedule */}
|
||||
<div className="rounded-lg bg-white p-6 shadow-sm lg:col-span-2">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Programacion del Dia
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="rounded-lg border border-gray-200 p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Clock className="h-5 w-5 text-yellow-500" />
|
||||
<span className="font-medium text-gray-900">Pendientes</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-gray-900">4</p>
|
||||
<p className="text-sm text-gray-500">ordenes por iniciar</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Wrench className="h-5 w-5 text-blue-500" />
|
||||
<span className="font-medium text-gray-900">En Proceso</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-gray-900">6</p>
|
||||
<p className="text-sm text-gray-500">ordenes en reparacion</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
||||
<span className="font-medium text-gray-900">Completadas Hoy</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-gray-900">3</p>
|
||||
<p className="text-sm text-gray-500">ordenes entregadas</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
165
src/pages/Login.tsx
Normal file
165
src/pages/Login.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { Wrench, Eye, EyeOff } from 'lucide-react';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email('Email invalido'),
|
||||
password: z.string().min(6, 'Minimo 6 caracteres'),
|
||||
});
|
||||
|
||||
type LoginForm = z.infer<typeof loginSchema>;
|
||||
|
||||
export function Login() {
|
||||
const navigate = useNavigate();
|
||||
const { login } = useAuthStore();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<LoginForm>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data: LoginForm) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// TODO: Replace with actual API call
|
||||
// const response = await authApi.login(data);
|
||||
|
||||
// Mock login for development
|
||||
const mockUser = {
|
||||
id: '1',
|
||||
email: data.email,
|
||||
full_name: 'Usuario Demo',
|
||||
role: 'admin',
|
||||
permissions: ['*'],
|
||||
};
|
||||
|
||||
login(mockUser, 'mock-token', 'mock-refresh-token');
|
||||
navigate('/dashboard');
|
||||
} catch (err) {
|
||||
setError('Credenciales invalidas');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gray-100 px-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Logo */}
|
||||
<div className="mb-8 text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-diesel-500">
|
||||
<Wrench className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Mecanicas Diesel</h1>
|
||||
<p className="text-sm text-gray-500">Sistema de gestion de taller</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="rounded-xl bg-white p-8 shadow-lg">
|
||||
<h2 className="mb-6 text-xl font-semibold text-gray-900">
|
||||
Iniciar Sesion
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 rounded-lg bg-red-50 p-3 text-sm text-red-600">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Correo electronico
|
||||
</label>
|
||||
<input
|
||||
{...register('email')}
|
||||
type="email"
|
||||
id="email"
|
||||
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:border-diesel-500 focus:outline-none focus:ring-1 focus:ring-diesel-500"
|
||||
placeholder="usuario@taller.com"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-1 text-xs text-red-600">{errors.email.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Contrasena
|
||||
</label>
|
||||
<div className="relative mt-1">
|
||||
<input
|
||||
{...register('password')}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
id="password"
|
||||
className="block w-full rounded-lg border border-gray-300 px-4 py-2.5 pr-10 text-sm focus:border-diesel-500 focus:outline-none focus:ring-1 focus:ring-diesel-500"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{errors.password && (
|
||||
<p className="mt-1 text-xs text-red-600">
|
||||
{errors.password.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Forgot password */}
|
||||
<div className="flex items-center justify-end">
|
||||
<a
|
||||
href="#"
|
||||
className="text-sm text-diesel-600 hover:text-diesel-700"
|
||||
>
|
||||
Olvidaste tu contrasena?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full rounded-lg bg-diesel-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-diesel-700 focus:outline-none focus:ring-2 focus:ring-diesel-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? 'Ingresando...' : 'Ingresar'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<p className="mt-6 text-center text-xs text-gray-500">
|
||||
ERP Mecanicas Diesel - Sistema NEXUS
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
src/services/api/auth.ts
Normal file
43
src/services/api/auth.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { api } from './client';
|
||||
import type { User, ApiResponse } from '../../types';
|
||||
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
user: User;
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
full_name: string;
|
||||
taller_name?: string;
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
login: (data: LoginRequest) =>
|
||||
api.post<ApiResponse<LoginResponse>>('/auth/login', data),
|
||||
|
||||
register: (data: RegisterRequest) =>
|
||||
api.post<ApiResponse<LoginResponse>>('/auth/register', data),
|
||||
|
||||
logout: () =>
|
||||
api.post<ApiResponse<null>>('/auth/logout'),
|
||||
|
||||
refreshToken: (refreshToken: string) =>
|
||||
api.post<ApiResponse<{ token: string }>>('/auth/refresh', { refreshToken }),
|
||||
|
||||
getProfile: () =>
|
||||
api.get<ApiResponse<User>>('/auth/profile'),
|
||||
|
||||
changePassword: (currentPassword: string, newPassword: string) =>
|
||||
api.post<ApiResponse<null>>('/auth/change-password', {
|
||||
currentPassword,
|
||||
newPassword,
|
||||
}),
|
||||
};
|
||||
84
src/services/api/client.ts
Normal file
84
src/services/api/client.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
// API Base URL
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3041/api/v1';
|
||||
|
||||
// Create axios instance
|
||||
const apiClient: AxiosInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor - add auth token
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = useAuthStore.getState().token;
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// Response interceptor - handle errors
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
|
||||
|
||||
// Handle 401 - Unauthorized
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
try {
|
||||
const refreshToken = useAuthStore.getState().refreshToken;
|
||||
if (refreshToken) {
|
||||
const response = await axios.post(`${API_BASE_URL}/auth/refresh`, {
|
||||
refreshToken,
|
||||
});
|
||||
|
||||
const { token } = response.data.data;
|
||||
useAuthStore.getState().setToken(token);
|
||||
|
||||
if (originalRequest.headers) {
|
||||
originalRequest.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return apiClient(originalRequest);
|
||||
}
|
||||
} catch (refreshError) {
|
||||
// Refresh failed, logout user
|
||||
useAuthStore.getState().logout();
|
||||
window.location.href = '/login';
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
|
||||
// Generic API methods
|
||||
export const api = {
|
||||
get: <T>(url: string, config?: AxiosRequestConfig) =>
|
||||
apiClient.get<T>(url, config).then((res) => res.data),
|
||||
|
||||
post: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) =>
|
||||
apiClient.post<T>(url, data, config).then((res) => res.data),
|
||||
|
||||
put: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) =>
|
||||
apiClient.put<T>(url, data, config).then((res) => res.data),
|
||||
|
||||
patch: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) =>
|
||||
apiClient.patch<T>(url, data, config).then((res) => res.data),
|
||||
|
||||
delete: <T>(url: string, config?: AxiosRequestConfig) =>
|
||||
apiClient.delete<T>(url, config).then((res) => res.data),
|
||||
};
|
||||
124
src/services/api/serviceOrders.ts
Normal file
124
src/services/api/serviceOrders.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { api } from './client';
|
||||
import type { ApiResponse, PaginatedResult, BaseFilters, ServiceOrderStatus } from '../../types';
|
||||
|
||||
// Types
|
||||
export interface ServiceOrder {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
order_number: string;
|
||||
customer_id: string;
|
||||
customer_name: string;
|
||||
vehicle_id: string;
|
||||
vehicle_info: string;
|
||||
status: ServiceOrderStatus;
|
||||
priority: 'low' | 'medium' | 'high' | 'urgent';
|
||||
received_at: string;
|
||||
promised_at: string | null;
|
||||
mechanic_id: string | null;
|
||||
mechanic_name: string | null;
|
||||
bay_id: string | null;
|
||||
bay_name: string | null;
|
||||
symptoms: string;
|
||||
notes: string | null;
|
||||
labor_total: number;
|
||||
parts_total: number;
|
||||
tax: number;
|
||||
grand_total: number;
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface ServiceOrderItem {
|
||||
id: string;
|
||||
order_id: string;
|
||||
item_type: 'service' | 'part';
|
||||
service_id: string | null;
|
||||
part_id: string | null;
|
||||
description: string;
|
||||
quantity: number;
|
||||
unit_price: number;
|
||||
discount: number;
|
||||
subtotal: number;
|
||||
actual_hours: number | null;
|
||||
performed_by: string | null;
|
||||
}
|
||||
|
||||
export interface CreateServiceOrderRequest {
|
||||
customer_id: string;
|
||||
vehicle_id: string;
|
||||
symptoms: string;
|
||||
priority?: 'low' | 'medium' | 'high' | 'urgent';
|
||||
promised_at?: string;
|
||||
mechanic_id?: string;
|
||||
bay_id?: string;
|
||||
}
|
||||
|
||||
export interface ServiceOrderFilters extends BaseFilters {
|
||||
status?: ServiceOrderStatus;
|
||||
priority?: string;
|
||||
mechanic_id?: string;
|
||||
bay_id?: string;
|
||||
customer_id?: string;
|
||||
vehicle_id?: string;
|
||||
}
|
||||
|
||||
// API
|
||||
export const serviceOrdersApi = {
|
||||
// List orders with filters
|
||||
list: (filters?: ServiceOrderFilters) =>
|
||||
api.get<ApiResponse<PaginatedResult<ServiceOrder>>>('/service-orders', {
|
||||
params: filters,
|
||||
}),
|
||||
|
||||
// Get single order
|
||||
getById: (id: string) =>
|
||||
api.get<ApiResponse<ServiceOrder>>(`/service-orders/${id}`),
|
||||
|
||||
// Create new order
|
||||
create: (data: CreateServiceOrderRequest) =>
|
||||
api.post<ApiResponse<ServiceOrder>>('/service-orders', data),
|
||||
|
||||
// Update order
|
||||
update: (id: string, data: Partial<ServiceOrder>) =>
|
||||
api.patch<ApiResponse<ServiceOrder>>(`/service-orders/${id}`, data),
|
||||
|
||||
// Change status
|
||||
changeStatus: (id: string, status: ServiceOrderStatus, notes?: string) =>
|
||||
api.post<ApiResponse<ServiceOrder>>(`/service-orders/${id}/status`, {
|
||||
status,
|
||||
notes,
|
||||
}),
|
||||
|
||||
// Assign mechanic and bay
|
||||
assign: (id: string, mechanicId: string, bayId: string) =>
|
||||
api.post<ApiResponse<ServiceOrder>>(`/service-orders/${id}/assign`, {
|
||||
mechanic_id: mechanicId,
|
||||
bay_id: bayId,
|
||||
}),
|
||||
|
||||
// Get order items
|
||||
getItems: (orderId: string) =>
|
||||
api.get<ApiResponse<ServiceOrderItem[]>>(`/service-orders/${orderId}/items`),
|
||||
|
||||
// Add item to order
|
||||
addItem: (orderId: string, item: Partial<ServiceOrderItem>) =>
|
||||
api.post<ApiResponse<ServiceOrderItem>>(`/service-orders/${orderId}/items`, item),
|
||||
|
||||
// Remove item from order
|
||||
removeItem: (orderId: string, itemId: string) =>
|
||||
api.delete<ApiResponse<null>>(`/service-orders/${orderId}/items/${itemId}`),
|
||||
|
||||
// Close order
|
||||
close: (id: string, finalOdometer: number) =>
|
||||
api.post<ApiResponse<ServiceOrder>>(`/service-orders/${id}/close`, {
|
||||
final_odometer: finalOdometer,
|
||||
}),
|
||||
|
||||
// Get kanban view data
|
||||
getKanbanView: () =>
|
||||
api.get<ApiResponse<Record<ServiceOrderStatus, ServiceOrder[]>>>('/service-orders/kanban'),
|
||||
|
||||
// Get order history
|
||||
getHistory: (vehicleId: string) =>
|
||||
api.get<ApiResponse<ServiceOrder[]>>(`/vehicles/${vehicleId}/service-history`),
|
||||
};
|
||||
56
src/store/authStore.ts
Normal file
56
src/store/authStore.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { User, AuthState } from '../types';
|
||||
|
||||
interface AuthStore extends AuthState {
|
||||
// Actions
|
||||
login: (user: User, token: string, refreshToken: string) => void;
|
||||
logout: () => void;
|
||||
updateUser: (user: Partial<User>) => void;
|
||||
setToken: (token: string) => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
// Initial state
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
|
||||
// Actions
|
||||
login: (user, token, refreshToken) =>
|
||||
set({
|
||||
user,
|
||||
token,
|
||||
refreshToken,
|
||||
isAuthenticated: true,
|
||||
}),
|
||||
|
||||
logout: () =>
|
||||
set({
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
}),
|
||||
|
||||
updateUser: (userData) =>
|
||||
set((state) => ({
|
||||
user: state.user ? { ...state.user, ...userData } : null,
|
||||
})),
|
||||
|
||||
setToken: (token) => set({ token }),
|
||||
}),
|
||||
{
|
||||
name: 'mecanicas-auth-storage',
|
||||
partialize: (state) => ({
|
||||
token: state.token,
|
||||
refreshToken: state.refreshToken,
|
||||
user: state.user,
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
70
src/store/tallerStore.ts
Normal file
70
src/store/tallerStore.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
// Types
|
||||
export interface Taller {
|
||||
id: string;
|
||||
name: string;
|
||||
legal_name: string;
|
||||
rfc: string;
|
||||
address: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
logo_url?: string;
|
||||
}
|
||||
|
||||
export interface WorkBay {
|
||||
id: string;
|
||||
name: string;
|
||||
bay_type: 'general' | 'diesel' | 'heavy_duty';
|
||||
status: 'available' | 'occupied' | 'maintenance';
|
||||
current_order_id?: string;
|
||||
}
|
||||
|
||||
export interface TallerState {
|
||||
currentTaller: Taller | null;
|
||||
selectedBay: WorkBay | null;
|
||||
workBays: WorkBay[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface TallerStore extends TallerState {
|
||||
setTaller: (taller: Taller) => void;
|
||||
setSelectedBay: (bay: WorkBay | null) => void;
|
||||
setWorkBays: (bays: WorkBay[]) => void;
|
||||
updateBayStatus: (bayId: string, status: WorkBay['status']) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const initialState: TallerState = {
|
||||
currentTaller: null,
|
||||
selectedBay: null,
|
||||
workBays: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export const useTallerStore = create<TallerStore>((set) => ({
|
||||
...initialState,
|
||||
|
||||
setTaller: (taller) => set({ currentTaller: taller }),
|
||||
|
||||
setSelectedBay: (bay) => set({ selectedBay: bay }),
|
||||
|
||||
setWorkBays: (bays) => set({ workBays: bays }),
|
||||
|
||||
updateBayStatus: (bayId, status) =>
|
||||
set((state) => ({
|
||||
workBays: state.workBays.map((bay) =>
|
||||
bay.id === bayId ? { ...bay, status } : bay
|
||||
),
|
||||
})),
|
||||
|
||||
setLoading: (loading) => set({ isLoading: loading }),
|
||||
|
||||
setError: (error) => set({ error }),
|
||||
|
||||
reset: () => set(initialState),
|
||||
}));
|
||||
114
src/types/index.ts
Normal file
114
src/types/index.ts
Normal file
@ -0,0 +1,114 @@
|
||||
// =============================================================================
|
||||
// TIPOS BASE - ERP MECANICAS DIESEL
|
||||
// =============================================================================
|
||||
|
||||
// Tipos de paginacion
|
||||
export interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface PaginationParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// Tipos de filtros
|
||||
export interface BaseFilters extends PaginationParams {
|
||||
search?: string;
|
||||
status?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}
|
||||
|
||||
// Tipos de auditoria
|
||||
export interface AuditFields {
|
||||
created_at: string;
|
||||
created_by: string | null;
|
||||
updated_at: string | null;
|
||||
updated_by: string | null;
|
||||
deleted_at?: string | null;
|
||||
deleted_by?: string | null;
|
||||
}
|
||||
|
||||
// Tipo base para entidades
|
||||
export interface BaseEntity extends AuditFields {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
}
|
||||
|
||||
// Tipos de usuario y auth
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name: string;
|
||||
avatar_url?: string;
|
||||
role: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
refreshToken: string | null;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
// Tipos de API response
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
success: false;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: Record<string, string[]>;
|
||||
};
|
||||
}
|
||||
|
||||
// Status de ordenes de servicio
|
||||
export type ServiceOrderStatus =
|
||||
| 'received'
|
||||
| 'diagnosing'
|
||||
| 'quoted'
|
||||
| 'approved'
|
||||
| 'in_repair'
|
||||
| 'waiting_parts'
|
||||
| 'ready'
|
||||
| 'delivered'
|
||||
| 'cancelled';
|
||||
|
||||
// Tipos de diagnostico
|
||||
export type DiagnosticType =
|
||||
| 'obd_scanner'
|
||||
| 'injector_bench'
|
||||
| 'pump_bench'
|
||||
| 'measurements';
|
||||
|
||||
// Tipos de movimiento de inventario
|
||||
export type MovementType =
|
||||
| 'purchase'
|
||||
| 'sale'
|
||||
| 'transfer'
|
||||
| 'adjustment'
|
||||
| 'return'
|
||||
| 'production';
|
||||
|
||||
// Status de cotizacion
|
||||
export type QuoteStatus =
|
||||
| 'draft'
|
||||
| 'sent'
|
||||
| 'viewed'
|
||||
| 'approved'
|
||||
| 'rejected'
|
||||
| 'expired'
|
||||
| 'converted';
|
||||
39
tailwind.config.js
Normal file
39
tailwind.config.js
Normal file
@ -0,0 +1,39 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
950: '#172554',
|
||||
},
|
||||
diesel: {
|
||||
50: '#fef3c7',
|
||||
100: '#fde68a',
|
||||
200: '#fcd34d',
|
||||
300: '#fbbf24',
|
||||
400: '#f59e0b',
|
||||
500: '#d97706',
|
||||
600: '#b45309',
|
||||
700: '#92400e',
|
||||
800: '#78350f',
|
||||
900: '#451a03',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
28
tsconfig.app.json
Normal file
28
tsconfig.app.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
26
tsconfig.node.json
Normal file
26
tsconfig.node.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user