diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8e0ffd3 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# API Configuration +VITE_API_URL=http://localhost:3011/api/v1 + +# Environment +VITE_APP_NAME=Mecanicas Diesel +VITE_APP_ENV=development diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -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? diff --git a/README.md b/README.md index a36f1f3..aa4c548 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,152 @@ -# erp-mecanicas-diesel-frontend-v2 +# Frontend - ERP Mecanicas Diesel -Frontend de erp-mecanicas-diesel - Workspace V2 \ No newline at end of file +## 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* diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/eslint.config.js @@ -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, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..072a57e --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + +
+ + + +Pagina no encontrada
+Taller
++ {currentTaller.name} +
+{message}
+{message}
} +{message}
+{toast.title}
+ {toast.message && ( +{toast.message}
+ )} +Cliente no encontrado
+ + Volver a clientes + +Codigo: {customer.code}
+ )} +{vehicles.length}
+{pendingOrders}
+{completedOrders}
++ ${totalSpent.toLocaleString('es-MX', { minimumFractionDigits: 2 })} +
+Contacto
+{customer.contact_name}
+Telefono
+{customer.contact_phone}
+{customer.contact_email}
+Cliente desde
++ {new Date(customer.created_at).toLocaleDateString('es-MX')} +
+Notas
+{customer.notes}
+Sin vehiculos registrados
++ {vehicle.make} {vehicle.model} {vehicle.year} +
+{vehicle.licensePlate}
+Sin historial de servicio
+{order.order_number}
+{order.vehicle_info}
++ ${order.grand_total.toLocaleString('es-MX', { minimumFractionDigits: 2 })} +
+Gestiona los clientes y flotas
+Error al cargar clientes
+No hay clientes registrados
+ +{customer.code}
+ )} +{customer.contact_name}
+ )} + {customer.contact_phone && ( +
+
+
Resumen de operaciones del taller
+{stat.value}
+ )} +{stat.name}
+Ingresos Totales
+ {statsLoading ? ( + + ) : ( ++ ${stats.totalRevenue.toLocaleString('es-MX', { minimumFractionDigits: 2 })} +
+ )} +Ticket Promedio
+ {statsLoading ? ( + + ) : ( ++ ${stats.averageTicket.toLocaleString('es-MX', { minimumFractionDigits: 2 })} +
+ )} +No hay ordenes recientes
+{order.order_number}
+{order.vehicle_info}
+{order.customer_name}
+{stats.pendingOrders}
+ )} +ordenes por iniciar
+{stats.inProgressOrders}
+ )} +ordenes en reparacion
+{stats.completedToday}
+ )} +ordenes entregadas
+Error al cargar el diagnostico
+ + Volver a diagnosticos + +{typeConfig?.label || diagnostic.diagnostic_type}
+{diagnostic.vehicle_info}
+{diagnostic.customer_name}
+No hay pruebas registradas
+| + Componente + | ++ Prueba + | ++ Valor + | ++ Rango + | ++ Resultado + | +
|---|---|---|---|---|
| + {item.component} + | ++ {item.test_name} + | ++ {item.value} {item.unit} + | ++ {item.min_value !== null && item.max_value !== null + ? `${item.min_value} - ${item.max_value} ${item.unit || ''}` + : '-'} + | +
+
+ |
+
Establecer Resultado:
+Tipo
+{typeConfig?.label}
+Equipo
+{diagnostic.equipment}
+Fecha
++ {new Date(diagnostic.performed_at).toLocaleDateString('es-MX', { + day: '2-digit', + month: 'long', + year: 'numeric', + })} +
+Tecnico
+{diagnostic.technician_name}
+{diagnostic.summary}
+Pruebas y analisis de vehiculos
+Error al cargar diagnosticos
+No hay diagnosticos registrados
+ + Realizar primer diagnostico + +| + Vehiculo + | ++ Tipo + | ++ Resultado + | ++ Orden + | ++ Fecha + | ++ Acciones + | +
|---|---|---|---|---|---|
|
+
+
+
+
+
+
+ {diagnostic.vehicle_info} +{diagnostic.customer_name} + |
+
+
+
+ |
+
+ {resultConfig ? (
+
+ {ResultIcon && |
+ + {diagnostic.order_number ? ( + + {diagnostic.order_number} + + ) : ( + - + )} + | +
+
+
+ |
+
+
+ |
+
Registre pruebas y analisis del vehiculo
+Gestiona las refacciones y materiales
+Total Productos
+{data?.data?.total || 0}
+Stock Bajo
+{lowStockParts.length}
+Valor en Inventario
++ ${totalValue.toLocaleString('es-MX', { minimumFractionDigits: 2 })} +
+Error al cargar inventario
+No hay refacciones registradas
+ +| + Producto + | ++ SKU + | ++ Marca + | ++ Stock + | ++ Precio + | ++ Acciones + | +
|---|---|---|---|---|---|
|
+
+
+
+
+
+
+ {part.name} + {part.description && ( +{part.description} + )} + |
+
+
+ |
+ + {part.brand || '-'} + | +
+
+ {isLowStock && (
+
+ Min: {part.minStock} + |
+
+ + ${part.price.toLocaleString('es-MX', { minimumFractionDigits: 2 })} + + {part.cost && ( ++ Costo: ${part.cost.toLocaleString('es-MX', { minimumFractionDigits: 2 })} + + )} + |
+
+
+
+
+
+ |
+
Refaccion no encontrada
+ + Volver a inventario + +SKU: {part.sku}
+SKU
+{part.sku}
+Codigo de Barras
+{part.barcode}
+Marca
+{part.brand}
+Fabricante
+{part.manufacturer}
+Unidad
+{part.unit}
+Registrado
++ {new Date(part.createdAt).toLocaleDateString('es-MX')} +
+Descripcion
+{part.description}
+Motores Compatibles
+Costo
++ ${(part.cost || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 })} +
+Precio Venta
++ ${part.price.toLocaleString('es-MX', { minimumFractionDigits: 2 })} +
+Utilidad
++ ${profit.toLocaleString('es-MX', { minimumFractionDigits: 2 })} +
+Margen
+{profitMargin}%
+Actual
++ {part.currentStock} +
+{part.unit}
+Reservado
+{part.reservedStock}
+{part.unit}
+Disponible
+{availableStock}
+{part.unit}
+Minimo
+{part.minStock}
+{part.unit}
+Sistema de gestion de taller
++ No tienes cuenta?{' '} + + Registra tu taller + +
+ + {/* Footer */} ++ ERP Mecanicas Diesel - Sistema NEXUS +
+Error al cargar la cotizacion
+ + Volver a cotizaciones + ++ Creada el {new Date(quote.created_at).toLocaleDateString('es-MX', { + day: '2-digit', + month: 'long', + year: 'numeric', + })} +
+{quote.quote_number}
+Fecha de Emision
++ {new Date(quote.created_at).toLocaleDateString('es-MX')} +
+Valida Hasta
++ {quote.valid_until + ? new Date(quote.valid_until).toLocaleDateString('es-MX') + : 'Sin fecha limite'} +
+{quote.customer_name}
+{quote.vehicle_info}
+| Descripcion | +Cant | +Precio | +Total | +
|---|---|---|---|
|
+ {item.description} + |
+ {item.quantity} | ++ ${item.unit_price.toLocaleString('es-MX', { minimumFractionDigits: 2 })} + | ++ ${item.subtotal.toLocaleString('es-MX', { minimumFractionDigits: 2 })} + | +
| Descripcion | +Cant | +Precio | +Total | +
|---|---|---|---|
|
+ {item.description} + |
+ {item.quantity} | ++ ${item.unit_price.toLocaleString('es-MX', { minimumFractionDigits: 2 })} + | ++ ${item.subtotal.toLocaleString('es-MX', { minimumFractionDigits: 2 })} + | +
{quote.notes}
+* Precios expresados en Pesos Mexicanos (MXN)
+* Los precios estan sujetos a disponibilidad de refacciones
+{quote.customer_name}
+ {quote.customer_id && ( + + Ver perfil del cliente + + )} +{quote.vehicle_info}
+Gestiona las cotizaciones para clientes
+Error al cargar cotizaciones
+No hay cotizaciones
+ + Crear primera cotizacion + +| + Cotizacion + | ++ Cliente / Vehiculo + | ++ Estado + | ++ Vigencia + | ++ Total + | ++ Acciones + | +
|---|---|---|---|---|---|
|
+
+ {quote.quote_number}
+
+ + {new Date(quote.created_at).toLocaleDateString('es-MX')} + + |
+
+
+
+ {quote.customer_name} +{quote.vehicle_info} + |
+ + + {statusConfig.label} + + | +
+
+
+ |
+ + ${quote.grand_total.toLocaleString('es-MX', { minimumFractionDigits: 2 })} + | +
+
+ {quote.status === 'draft' && (
+
+ )}
+ {quote.status === 'approved' && (
+
+ )}
+
+
+ |
+
Registra tu taller
++ Ya tienes cuenta?{' '} + + Inicia sesion + +
+ + {/* Footer */} ++ ERP Mecanicas Diesel - Sistema NEXUS +
+Orden no encontrada
+ + Volver a ordenes + ++ Creada el {new Date(order.received_at).toLocaleDateString('es-MX', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} +
+{order.customer_name}
+{order.vehicle_info}
++ {order.symptoms || 'Sin sintomas registrados'} +
+No hay items agregados
+ ) : ( +{item.description}
+ {item.actual_hours && ( +{item.actual_hours} hrs
+ )} ++ ${item.subtotal.toLocaleString('es-MX', { minimumFractionDigits: 2 })} +
+{item.description}
++ {item.quantity} x ${item.unit_price.toLocaleString('es-MX', { minimumFractionDigits: 2 })} +
++ ${item.subtotal.toLocaleString('es-MX', { minimumFractionDigits: 2 })} +
+{order.notes}
+Registra un nuevo vehiculo para servicio
+Gestiona las órdenes de trabajo del taller
+Error al cargar órdenes
+Verifica que el servidor esté activo
+No hay órdenes de servicio
+ + Crear primera orden + +| + Orden + | ++ Cliente / Vehículo + | ++ Estado + | ++ Prioridad + | ++ Fecha + | ++ Total + | +
|---|---|---|---|---|---|
| + + {order.order_number} + + | +
+
+
+ {order.customer_name}
+ {order.vehicle_info}
+ |
+ + + {statusConfig.label} + + | ++ + {priorityConfig.label} + + | +
+
+
+ |
+ + ${order.grand_total.toLocaleString('es-MX', { minimumFractionDigits: 2 })} + | +
{order.symptoms}
+ )} + +Error al cargar ordenes
++ {activeOrders.length} ordenes activas +
+{col.label}
+{count}
+ {urgentCount > 0 && ( + ({urgentCount} urgentes) + )} +Error al cargar configuracion
+Ajustes del taller y preferencias del sistema
+Gestiona los usuarios del sistema
+| + Usuario + | ++ Rol + | ++ Estado + | ++ Último acceso + | + {canManageUsers && ( ++ Acciones + | + )} +
|---|---|---|---|---|
|
+
+
+
+ {user.avatar_url ? (
+
+
+
+ {user.full_name}
+ {user.email}
+ |
+ + + {roleInfo.label} + + | +
+ {user.isActive ? (
+
+ |
+ + {user.lastLoginAt + ? new Date(user.lastLoginAt).toLocaleDateString('es-MX', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + : 'Nunca'} + | + {canManageUsers && ( +
+
+
+
+ {user.id !== currentUser?.id && (
+
+ )}
+
+ |
+ )}
+
Vehiculo no encontrado
+ + Volver a vehiculos + +{vehicle.licensePlate}
+Placas
+{vehicle.licensePlate}
+No. Economico
+{vehicle.economicNumber}
+Tipo
+{VEHICLE_TYPE_LABELS[vehicle.vehicleType]}
+Color
+{vehicle.color}
+VIN
+{vehicle.vin}
+Odometro
++ {vehicle.currentOdometer.toLocaleString()} km +
+Registrado
++ {new Date(vehicle.createdAt).toLocaleDateString('es-MX')} +
+Notas
+{vehicle.notes}
+Sin historial de servicio
+{order.order_number}
+{order.symptoms}
++ {new Date(order.received_at).toLocaleDateString('es-MX', { + day: '2-digit', + month: 'short', + year: 'numeric', + })} +
++ ${order.grand_total.toLocaleString('es-MX', { minimumFractionDigits: 2 })} +
+Cliente
+ + {customer.name} + +Contacto
+{customer.contact_name}
+Telefono
+{customer.contact_phone}
+Cargando...
+ )} +Gestiona los vehiculos registrados
+Error al cargar vehiculos
+No hay vehiculos registrados
+ +| + Vehiculo + | ++ Placas + | ++ Tipo + | ++ Odometro + | ++ Estado + | ++ Acciones + | +
|---|---|---|---|---|---|
|
+
+
+
+
+
+
+ {vehicle.make} {vehicle.model} +{vehicle.year} + |
+
+ {vehicle.licensePlate} + {vehicle.economicNumber && ( +Eco: {vehicle.economicNumber} + )} + |
+ + {typeLabel} + | +
+ {vehicle.currentOdometer ? (
+
+ |
+ + + {statusConfig.label} + + | +
+
+
+
+
+ |
+
) => + api.patch>(`/quotes/${id}`, data), + + // Change status + changeStatus: (id: string, status: QuoteStatus) => + api.post >(`/quotes/${id}/status`, { status }), + + // Get quote items + getItems: (quoteId: string) => + api.get >(`/quotes/${quoteId}/items`), + + // Add item to quote + addItem: (quoteId: string, item: Partial ) => + api.post >(`/quotes/${quoteId}/items`, item), + + // Remove item from quote + removeItem: (quoteId: string, itemId: string) => + api.delete >(`/quotes/${quoteId}/items/${itemId}`), + + // Convert to service order + convertToOrder: (quoteId: string) => + api.post >(`/quotes/${quoteId}/convert`), + + // Send to customer + send: (quoteId: string, email?: string) => + api.post >(`/quotes/${quoteId}/send`, { email }), +}; diff --git a/src/services/api/serviceOrders.ts b/src/services/api/serviceOrders.ts new file mode 100644 index 0000000..36c24cf --- /dev/null +++ b/src/services/api/serviceOrders.ts @@ -0,0 +1,138 @@ +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 >>('/service-orders', { + params: filters, + }), + + // Get single order + getById: (id: string) => + api.get >(`/service-orders/${id}`), + + // Create new order + create: (data: CreateServiceOrderRequest) => + api.post >('/service-orders', data), + + // Update order + update: (id: string, data: Partial ) => + api.patch >(`/service-orders/${id}`, data), + + // Change status + changeStatus: (id: string, status: ServiceOrderStatus, notes?: string) => + api.post >(`/service-orders/${id}/status`, { + status, + notes, + }), + + // Assign mechanic and bay + assign: (id: string, mechanicId: string, bayId: string) => + api.post >(`/service-orders/${id}/assign`, { + mechanic_id: mechanicId, + bay_id: bayId, + }), + + // Get order items + getItems: (orderId: string) => + api.get >(`/service-orders/${orderId}/items`), + + // Add item to order + addItem: (orderId: string, item: Partial ) => + api.post >(`/service-orders/${orderId}/items`, item), + + // Remove item from order + removeItem: (orderId: string, itemId: string) => + api.delete >(`/service-orders/${orderId}/items/${itemId}`), + + // Close order + close: (id: string, finalOdometer: number) => + api.post >(`/service-orders/${id}/close`, { + final_odometer: finalOdometer, + }), + + // Get kanban view data + getKanbanView: () => + api.get >>('/service-orders/kanban'), + + // Get order history + getHistory: (vehicleId: string) => + api.get >(`/vehicles/${vehicleId}/service-history`), + + // Get dashboard stats + getStats: () => + api.get >('/service-orders/stats'), +}; + +// Dashboard Stats type +export interface DashboardStats { + totalOrders: number; + pendingOrders: number; + inProgressOrders: number; + completedToday: number; + totalRevenue: number; + averageTicket: number; +} diff --git a/src/services/api/settings.ts b/src/services/api/settings.ts new file mode 100644 index 0000000..e8cfc62 --- /dev/null +++ b/src/services/api/settings.ts @@ -0,0 +1,143 @@ +import { api } from './client'; +import type { ApiResponse } from '../../types'; + +// Tenant/Workshop Settings +export interface TenantSettings { + id: string; + name: string; + legal_name: string; + rfc: string; + address: string; + city: string; + state: string; + zip_code: string; + phone: string; + email: string; + website: string; + logo_url: string | null; + default_tax_rate: number; + labor_rate: number; + working_hours_start: string; + working_hours_end: string; + quote_validity_days: number; + currency: string; + timezone: string; + created_at: string; + updated_at: string; +} + +export interface UpdateTenantSettingsRequest { + name?: string; + legal_name?: string; + rfc?: string; + address?: string; + city?: string; + state?: string; + zip_code?: string; + phone?: string; + email?: string; + website?: string; + logo_url?: string | null; + default_tax_rate?: number; + labor_rate?: number; + working_hours_start?: string; + working_hours_end?: string; + quote_validity_days?: number; +} + +// Work Bay Types +export interface WorkBay { + id: string; + tenant_id: string; + name: string; + bay_type: 'general' | 'diesel' | 'heavy_duty'; + status: 'available' | 'occupied' | 'maintenance'; + current_order_id: string | null; + notes: string | null; + created_at: string; + updated_at: string; +} + +export interface CreateWorkBayRequest { + name: string; + bay_type: 'general' | 'diesel' | 'heavy_duty'; + notes?: string; +} + +export interface UpdateWorkBayRequest { + name?: string; + bay_type?: 'general' | 'diesel' | 'heavy_duty'; + status?: 'available' | 'occupied' | 'maintenance'; + notes?: string; +} + +// Service Catalog +export interface ServiceCatalogItem { + id: string; + tenant_id: string; + code: string; + name: string; + description: string | null; + category: string; + default_price: number; + estimated_hours: number; + is_active: boolean; + created_at: string; + updated_at: string; +} + +export interface CreateServiceCatalogItemRequest { + code: string; + name: string; + description?: string; + category: string; + default_price: number; + estimated_hours: number; +} + +// API +export const settingsApi = { + // Get current tenant settings + getSettings: () => + api.get >('/settings'), + + // Update tenant settings + updateSettings: (data: UpdateTenantSettingsRequest) => + api.patch >('/settings', data), + + // Upload logo + uploadLogo: (file: File) => { + const formData = new FormData(); + formData.append('logo', file); + return api.post >('/settings/logo', formData); + }, + + // Work Bays + listBays: () => + api.get >('/settings/bays'), + + createBay: (data: CreateWorkBayRequest) => + api.post >('/settings/bays', data), + + updateBay: (id: string, data: UpdateWorkBayRequest) => + api.patch >(`/settings/bays/${id}`, data), + + deleteBay: (id: string) => + api.delete >(`/settings/bays/${id}`), + + // Service Catalog + listServices: () => + api.get >('/settings/services'), + + createService: (data: CreateServiceCatalogItemRequest) => + api.post >('/settings/services', data), + + updateService: (id: string, data: Partial ) => + api.patch >(`/settings/services/${id}`, data), + + deleteService: (id: string) => + api.delete >(`/settings/services/${id}`), + + toggleServiceActive: (id: string, isActive: boolean) => + api.patch >(`/settings/services/${id}`, { is_active: isActive }), +}; diff --git a/src/services/api/users.ts b/src/services/api/users.ts new file mode 100644 index 0000000..b5b9d3c --- /dev/null +++ b/src/services/api/users.ts @@ -0,0 +1,49 @@ +import { api } from './client'; +import type { ApiResponse, UserDetail, UserFilters, PaginatedResult, UserRole } from '../../types'; + +export interface CreateUserRequest { + email: string; + password: string; + fullName: string; + role: UserRole; + avatarUrl?: string; +} + +export interface UpdateUserRequest { + fullName?: string; + role?: UserRole; + isActive?: boolean; + avatarUrl?: string | null; +} + +export interface ResetPasswordRequest { + newPassword: string; +} + +export const usersApi = { + list: (filters?: UserFilters) => { + const params = new URLSearchParams(); + if (filters?.role) params.append('role', filters.role); + if (filters?.isActive !== undefined) params.append('isActive', String(filters.isActive)); + if (filters?.search) params.append('search', filters.search); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.limit) params.append('limit', String(filters.limit)); + + return api.get >>(`/users?${params.toString()}`); + }, + + getById: (id: string) => + api.get >(`/users/${id}`), + + create: (data: CreateUserRequest) => + api.post >('/users', data), + + update: (id: string, data: UpdateUserRequest) => + api.patch >(`/users/${id}`, data), + + delete: (id: string) => + api.delete >(`/users/${id}`), + + resetPassword: (id: string, data: ResetPasswordRequest) => + api.patch >(`/users/${id}/reset-password`, data), +}; diff --git a/src/services/api/vehicles.ts b/src/services/api/vehicles.ts new file mode 100644 index 0000000..792d5fc --- /dev/null +++ b/src/services/api/vehicles.ts @@ -0,0 +1,71 @@ +import { api } from './client'; +import type { + ApiResponse, + Vehicle, + VehicleFilters, + PaginatedResult, + VehicleType, + VehicleStatus, +} from '../../types'; + +export interface CreateVehicleRequest { + customerId: string; + fleetId?: string; + vin?: string; + licensePlate: string; + economicNumber?: string; + make: string; + model: string; + year: number; + color?: string; + vehicleType?: VehicleType; + currentOdometer?: number; + photoUrl?: string; + notes?: string; +} + +export interface UpdateVehicleRequest { + fleetId?: string | null; + vin?: string; + licensePlate?: string; + economicNumber?: string; + make?: string; + model?: string; + year?: number; + color?: string; + vehicleType?: VehicleType; + currentOdometer?: number; + photoUrl?: string | null; + status?: VehicleStatus; + notes?: string; +} + +export const vehiclesApi = { + list: (filters?: VehicleFilters) => { + const params = new URLSearchParams(); + if (filters?.vehicleType) params.append('vehicleType', filters.vehicleType); + if (filters?.customerId) params.append('customerId', filters.customerId); + if (filters?.status) params.append('status', filters.status); + if (filters?.search) params.append('search', filters.search); + if (filters?.page) params.append('page', String(filters.page)); + if (filters?.pageSize) params.append('limit', String(filters.pageSize)); + + return api.get >>(`/vehicles?${params.toString()}`); + }, + + getById: (id: string) => + api.get >(`/vehicles/${id}`), + + create: (data: CreateVehicleRequest) => + api.post >('/vehicles', data), + + update: (id: string, data: UpdateVehicleRequest) => + api.patch >(`/vehicles/${id}`, data), + + delete: (id: string) => + api.delete >(`/vehicles/${id}`), + + // Get service history for a vehicle + getServiceHistory: (vehicleId: string) => + api.get >(`/vehicles/${vehicleId}/service-history`), +}; diff --git a/src/store/authStore.ts b/src/store/authStore.ts new file mode 100644 index 0000000..05a6203 --- /dev/null +++ b/src/store/authStore.ts @@ -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 ) => void; + setToken: (token: string) => void; +} + +export const useAuthStore = create ()( + 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, + }), + } + ) +); diff --git a/src/store/tallerStore.ts b/src/store/tallerStore.ts new file mode 100644 index 0000000..34caab0 --- /dev/null +++ b/src/store/tallerStore.ts @@ -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 ((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), +})); diff --git a/src/store/toastStore.ts b/src/store/toastStore.ts new file mode 100644 index 0000000..803c8dc --- /dev/null +++ b/src/store/toastStore.ts @@ -0,0 +1,66 @@ +import { create } from 'zustand'; + +export type ToastType = 'success' | 'error' | 'warning' | 'info'; + +export interface Toast { + id: string; + type: ToastType; + title: string; + message?: string; + duration?: number; +} + +interface ToastState { + toasts: Toast[]; + addToast: (toast: Omit ) => void; + removeToast: (id: string) => void; + clearToasts: () => void; +} + +export const useToastStore = create ((set) => ({ + toasts: [], + + addToast: (toast) => { + const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; + const newToast: Toast = { + ...toast, + id, + duration: toast.duration ?? 5000, + }; + + set((state) => ({ + toasts: [...state.toasts, newToast], + })); + + // Auto remove after duration + if (newToast.duration && newToast.duration > 0) { + setTimeout(() => { + set((state) => ({ + toasts: state.toasts.filter((t) => t.id !== id), + })); + }, newToast.duration); + } + }, + + removeToast: (id) => { + set((state) => ({ + toasts: state.toasts.filter((t) => t.id !== id), + })); + }, + + clearToasts: () => { + set({ toasts: [] }); + }, +})); + +// Helper functions for convenience +export const toast = { + success: (title: string, message?: string) => + useToastStore.getState().addToast({ type: 'success', title, message }), + error: (title: string, message?: string) => + useToastStore.getState().addToast({ type: 'error', title, message }), + warning: (title: string, message?: string) => + useToastStore.getState().addToast({ type: 'warning', title, message }), + info: (title: string, message?: string) => + useToastStore.getState().addToast({ type: 'info', title, message }), +}; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..c74c272 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,248 @@ +// ============================================================================= +// TIPOS BASE - ERP MECANICAS DIESEL +// ============================================================================= + +// Tipos de paginacion +export interface PaginatedResult { + 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 { + success: boolean; + data: T; + message?: string; +} + +export interface ApiError { + success: false; + error: { + code: string; + message: string; + details?: Record ; + }; +} + +// 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'; + +// ============================================================================= +// ENTIDADES DE DOMINIO +// ============================================================================= + +// Service Order +export type ServiceOrderPriority = 'low' | 'medium' | 'high' | 'urgent'; + +export interface ServiceOrder { + id: string; + tenantId: string; + orderNumber: string; + customerId: string; + vehicleId: string; + quoteId?: string; + assignedTo?: string; + bayId?: string; + status: ServiceOrderStatus; + priority: ServiceOrderPriority; + receivedAt: string; + promisedAt?: string; + startedAt?: string; + completedAt?: string; + deliveredAt?: string; + odometerIn?: number; + odometerOut?: number; + customerSymptoms?: string; + laborTotal: number; + partsTotal: number; + discountAmount: number; + discountPercent: number; + tax: number; + grandTotal: number; + internalNotes?: string; + customerNotes?: string; + createdBy?: string; + createdAt: string; + updatedAt: string; + // Relations + vehicle?: Vehicle; + assignedUser?: User; +} + +// Vehicle +export type VehicleType = 'truck' | 'trailer' | 'bus' | 'pickup' | 'other'; +export type VehicleStatus = 'active' | 'inactive' | 'sold'; + +export interface Vehicle { + id: string; + tenantId: string; + customerId: string; + fleetId?: string; + vin?: string; + licensePlate: string; + economicNumber?: string; + make: string; + model: string; + year: number; + color?: string; + vehicleType: VehicleType; + currentOdometer?: number; + odometerUpdatedAt?: string; + photoUrl?: string; + status: VehicleStatus; + notes?: string; + createdAt: string; + updatedAt: string; +} + +// Part (Refaccion) +export interface Part { + id: string; + tenantId: string; + sku: string; + name: string; + description?: string; + categoryId?: string; + brand?: string; + manufacturer?: string; + compatibleEngines?: string[]; + cost?: number; + price: number; + currentStock: number; + reservedStock: number; + minStock: number; + maxStock?: number; + reorderPoint?: number; + locationId?: string; + unit: string; + barcode?: string; + preferredSupplierId?: string; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + +// User (para gestión de usuarios) +export interface UserDetail extends User { + tenantId: string; + isActive: boolean; + emailVerified: boolean; + lastLoginAt?: string; + createdAt: string; + updatedAt: string; +} + +export type UserRole = 'admin' | 'jefe_taller' | 'mecanico' | 'recepcion' | 'almacen'; + +// Filters +export interface ServiceOrderFilters extends BaseFilters { + status?: ServiceOrderStatus; + priority?: ServiceOrderPriority; + assignedTo?: string; + vehicleId?: string; +} + +export interface VehicleFilters extends BaseFilters { + vehicleType?: VehicleType; + customerId?: string; +} + +export interface PartFilters extends BaseFilters { + categoryId?: string; + isActive?: boolean; + lowStock?: boolean; +} + +export interface UserFilters { + role?: UserRole; + isActive?: boolean; + search?: string; + page?: number; + limit?: number; +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..9a1b672 --- /dev/null +++ b/tailwind.config.js @@ -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: [], +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/tsconfig.app.json @@ -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"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/tsconfig.node.json @@ -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"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +})