Initial deploy commit

This commit is contained in:
rckrdmrd 2025-12-12 14:39:27 -06:00
commit 3f91de3672
29 changed files with 5281 additions and 0 deletions

24
.gitignore vendored Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

41
package.json Normal file
View 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
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

1
public/vite.svg Normal file
View 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
View 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
View 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

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -0,0 +1,3 @@
export { MainLayout } from './MainLayout';
export { Sidebar } from './Sidebar';
export { Header } from './Header';

38
src/index.css Normal file
View 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
View 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
View 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
View 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
View 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,
}),
};

View 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),
};

View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

26
tsconfig.node.json Normal file
View 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
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})