diff --git a/README.md b/README.md index 75c347a..779f1a0 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,746 @@ -

Laravel Logo

+# 🚴 Sistema de Gestión Lauck -

-Build Status -Total Downloads -Latest Stable Version -License -

+**Sistema web integral para gestión de bicicletería** - Inventario, ventas, taller, clientes y catálogo público. -## About Laravel +[![Laravel](https://img.shields.io/badge/Laravel-12-red.svg)](https://laravel.com) +[![PHP](https://img.shields.io/badge/PHP-8.2+-blue.svg)](https://php.net) +[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-4.0-38bdf8.svg)](https://tailwindcss.com) +[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) -Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: +## 📋 Descripción -- [Simple, fast routing engine](https://laravel.com/docs/routing). -- [Powerful dependency injection container](https://laravel.com/docs/container). -- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. -- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). -- Database agnostic [schema migrations](https://laravel.com/docs/migrations). -- [Robust background job processing](https://laravel.com/docs/queues). -- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). +**Lauck** es un sistema de gestión completo diseñado específicamente para bicicleterías. Integra la gestión de inventario, ventas, taller mecánico, base de datos de clientes y un catálogo público para visitantes, todo en una única plataforma web moderna y responsiva. -Laravel is accessible, powerful, and provides tools required for large, robust applications. +## ✨ Características Principales -## Learning Laravel +### 🔐 Sistema de Autenticación y Roles +- **Registro de usuarios** con validación completa +- **Login seguro** con rate limiting (5 intentos/minuto) +- **Gestión de sesiones** en base de datos +- **Sistema de roles**: Admin, Empleado, Invitado +- **Protección de rutas** con middleware +- **Dashboard personalizado** según permisos -Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. +### 📦 Gestión Avanzada de Productos +#### Atributos del Producto: +- Nombre, SKU (generación automática) +- Tipo: Bicicleta, Accesorio, Indumentaria, Repuesto +- Precio de venta y costo (solo admin) +- Stock actual y alerta de stock mínimo +- Descripción detallada +- **Número de serie** (para bicicletas) +- **Imágenes** con upload y visualización -You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch. +#### Funcionalidades: +- ✅ **CRUD completo** (Crear, Leer, Actualizar, Eliminar) +- 🔍 **Buscador avanzado** (nombre, SKU) +- 🎨 **Filtros dinámicos** por estado de stock (Alerta, Bajo, Normal) +- 📊 **Indicadores visuales** de stock con colores (rojo/amarillo/verde) +- 🖼️ **Gestión de imágenes** con upload y storage +- 📄 **Paginación** optimizada (10 productos/página) +- 🏷️ **Badges de tipo** con diseño diferenciado -If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. +### 💰 Sistema de Ventas (POS) +#### Características del Punto de Venta: +- **Interfaz intuitiva** estilo POS moderno +- **Selector de productos** con Select2 y búsqueda en tiempo real +- **Bloqueo de productos duplicados** en el mismo ticket +- **Cálculo automático** de subtotales y total +- **Gestión de clientes**: Asociar venta a cliente o "Consumidor Final" +- **Métodos de pago**: Efectivo, Débito, Crédito, Transferencia +- **Agregar/quitar productos** dinámicamente +- **Contador de items** en tiempo real -## Laravel Sponsors +#### Procesamiento de Ventas: +- ✅ **Transacciones atómicas** (rollback automático en caso de error) +- 📉 **Descuento automático de stock** al confirmar venta +- 💾 **Registro de detalles** (SaleDetails) por cada producto vendido +- 🧾 **Comprobante imprimible** en formato ticket profesional +- 📊 **Historial de ventas** con paginación -We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). +#### Comprobante de Venta: +- Diseño profesional tipo factura +- Header con logo y datos del negocio +- Información del cliente (o "Consumidor Final") +- Tabla detallada de productos +- Método de pago destacado +- Total en tamaño grande +- **Botón de impresión/PDF** optimizado +- Estilos específicos para impresión -### Premium Partners +### 👥 Gestión de Clientes +- **Base de datos completa** de clientes ciclistas +- Campos: Nombre, Teléfono/WhatsApp, Email, Dirección +- **CRUD completo** de clientes +- **Búsqueda** por nombre, teléfono o email +- **Paginación** de resultados +- **Protección contra eliminación** si tiene ventas registradas +- **Integración con ventas**: Seleccionar cliente en POS +- **Creación rápida** desde POS con redirección automática -- **[Vehikl](https://vehikl.com)** -- **[Tighten Co.](https://tighten.co)** -- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** -- **[64 Robots](https://64robots.com)** -- **[Curotec](https://www.curotec.com/services/technologies/laravel)** -- **[DevSquad](https://devsquad.com/hire-laravel-developers)** -- **[Redberry](https://redberry.international/laravel-development)** -- **[Active Logic](https://activelogic.com)** +### 🔧 Gestión de Taller (Mecánica) +#### Sistema Kanban de Órdenes de Reparación: +- **Tablero visual** estilo Trello con 3 columnas: + - 🔴 **Pendientes** - Bicis recién ingresadas + - 🟡 **En Reparación** - Trabajo en progreso + - 🟢 **Listas para Retirar** - Trabajos finalizados -## Contributing +#### Información de Orden: +- Cliente asociado +- Modelo y color de bicicleta +- Descripción del problema +- **Lista de repuestos necesarios** con precios +- **Presupuesto estimado** (calculado automáticamente) +- Fecha/hora prometida de entrega +- Teléfono de contacto rápido +- Notas del mecánico -Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). +#### Funcionalidades Kanban: +- ✅ **Mover cards** entre estados (flechas) +- ↩️ **Regresar al estado anterior** si es necesario +- 💬 **Botón de WhatsApp** para avisar al cliente +- ✔️ **Marcar como entregado** (archiva la orden) +- 🎨 **Bordes de color** según estado +- 📱 **Scroll independiente** por columna +- 🔢 **Contador de items** por columna -## Code of Conduct +#### Creación de Orden: +- **Selector de cliente** con búsqueda +- **Auto-completar teléfono** al seleccionar cliente +- **Buscador de repuestos** en tiempo real +- **Lista visual** de repuestos seleccionados +- **Cálculo automático** del presupuesto total +- **Validación completa** de datos +- **Botón de crear cliente** sin salir del formulario -In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). +### 🌐 Catálogo Público +#### Vista para Visitantes (sin login): +- **Listado público** de productos en stock +- **Cards atractivos** con imagen, nombre, precio +- **Búsqueda integrada** en navbar +- **Paginación** automática (12 productos/página) +- **Vista de detalle** profesional con: + - Imagen a tamaño completo + - Información completa del producto + - Indicador de stock en tiempo real + - Precio destacado + - Botones de contacto (teléfono, WhatsApp) + - Diseño responsivo premium -## Security Vulnerabilities +#### Carrousel Destacados: +- **Slider automático** de productos destacados +- **Controles manuales** (flechas y puntos) +- **Auto-play** cada 5 segundos +- **Diseño dual**: Imagen + Info +- **Precio flotante** con efecto +- **Navegación fluida** con transiciones -If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. +### 🎨 Sistema de Diseño -## License +#### Paleta de Colores Personalizada: +- **Neon Lime** (`#ccff00`): Color principal/acción +- **Dark Background** (`#1a1a1a`): Fondo principal +- **Panel Background** (`#242424`): Paneles/tarjetas +- **Neutral 800/900**: Componentes UI +- **Gradientes**: Efectos visuales modernos -The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). +#### Componentes Reutilizables: +``` +components/ +├── layout.blade.php # Layout principal autenticado +├── appc.blade.php # Layout público (visitantes) +├── navbar.blade.php # Navegación contextual +├── footer.blade.php # Pie de página +├── section-header.blade.php # Encabezados de sección +├── taller-card.blade.php # Card de orden de taller +├── ui/ +│ ├── alert.blade.php # Alertas de feedback +│ ├── badge.blade.php # Etiquetas de estado +│ ├── card.blade.php # Tarjetas de dashboard +│ ├── imgcard.blade.php # Card de producto con imagen +│ └── carrousel.blade.php # Carrousel de destacados +└── forms/ + ├── input.blade.php # Input con validación + ├── label.blade.php # Label estandarizado + └── select.blade.php # Select estilizado +``` + +#### Efectos Visuales: +- 🌟 **Patrón de grilla de fondo** con efecto técnico +- 💫 **Hover states** en todos los elementos interactivos +- 🎭 **Transiciones suaves** en navegación +- 📱 **Diseño responsivo** 100% mobile-first +- 🎨 **Scrollbar personalizado** con colores del tema +- ⚡ **Loading indicators** donde corresponde + +## 🏗️ Arquitectura del Sistema + +### Flujo de Navegación Completo + +``` +┌────────────────────────────────────────────────────┐ +│ Landing Page (/) │ +│ - Carrousel de productos destacados │ +│ - Navbar con catálogo público │ +│ - Botón "Ingresar" para staff │ +└─────────────────┬──────────────────────────────────┘ + │ + ┌─────────┴─────────┐ + │ │ + VISITANTE STAFF + │ │ + ▼ ▼ +┌──────────────┐ ┌──────────────┐ +│ Catálogo │ │ Login │ +│ Público │ │ │ +│ /catalogo │ │ /login │ +└──────────────┘ └──────┬───────┘ + │ │ + │ ▼ + │ ┌──────────────┐ + │ │ Dashboard │ + │ │ (autenticado)│ + │ └──────┬───────┘ + │ │ + │ ┌───────────┼───────────┬──────────┐ + │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ + ┌─────┐ ┌─────┐ ┌──────┐ ┌───────┐ ┌─────┐ + │ Ver │ │Stock│ │Ventas│ │Clientes│ │Taller│ + │Prod.│ │/prod│ │/sales│ │/clients│ │/taller│ + └─────┘ └─────┘ └──────┘ └────────┘ └─────┘ +``` + +### Estructura MVC Detallada + +#### 📁 Modelos (app/Models) +```php +User.php // Usuarios con roles (admin/employee/guest) +Product.php // Productos con relaciones +Client.php // Clientes con validaciones +Sale.php // Ventas (cabecera) +SaleDetail.php // Detalles de venta (renglones) +Appointment.php // Órdenes de taller +Supplier.php // Proveedores (implementación futura) +``` + +#### 🎮 Controladores (app/Http/Controllers) +```php +HomeController.php // Landing page con destacados +LoginController.php // Autenticación +RegisterController.php // Registro de usuarios +ProductosController.php // CRUD de productos +SaleController.php // Sistema de ventas (POS) +ClientController.php // Gestión de clientes +TallerController.php // Gestión de taller (Kanban) +CatalogoController.php // Catálogo público +``` + +#### 👁️ Vistas (resources/views) +``` +views/ +├── welcome.blade.php # Landing page +├── login.blade.php # Inicio de sesión +├── register.blade.php # Registro +├── dashboard.blade.php # Panel principal +├── productos/ +│ ├── index.blade.php # Listado de productos +│ ├── create.blade.php # Crear producto +│ ├── edit.blade.php # Editar producto +│ └── show.blade.php # Ver detalle +├── sales/ +│ ├── index.blade.php # Historial de ventas +│ ├── create.blade.php # POS (punto de venta) +│ └── show.blade.php # Comprobante de venta +├── clients/ +│ ├── index.blade.php # Listado de clientes +│ ├── create.blade.php # Alta de cliente +│ └── edit.blade.php # Modificar cliente +├── taller/ +│ ├── index.blade.php # Tablero Kanban +│ └── create.blade.php # Nueva orden +└── catalogo/ + ├── index.blade.php # Catálogo público + └── show.blade.php # Detalle producto público +``` + +## 🚀 Stack Tecnológico + +### Backend +- **Laravel 12.x** - Framework PHP moderno +- **PHP 8.2+** - Lenguaje de programación +- **SQLite** - Base de datos (configurable a MySQL/PostgreSQL) +- **Eloquent ORM** - Gestión de base de datos +- **Laravel Factories & Seeders** - Datos de prueba + +### Frontend +- **Tailwind CSS 4.0** - Framework CSS utility-first +- **Blade Components** - Sistema de componentes reutilizables +- **jQuery 3.7.1** - Manipulación DOM +- **Select2 4.1** - Selectores avanzados con búsqueda +- **Vite 7.0** - Build tool y HMR +- **Montserrat Font** - Tipografía principal + +### JavaScript/Bibliotecas +```json +{ + "jquery": "^3.7.1", + "select2": "^4.1.0-rc.0", + "axios": "^1.8.2" +} +``` + +### Herramientas de Desarrollo +- **Composer** - Gestor de dependencias PHP +- **NPM** - Gestor de paquetes JavaScript +- **Laravel Pint** - Code styling +- **Laravel Sail** - Entorno Docker (opcional) +- **Concurrently** - Múltiples procesos en desarrollo + +## 📊 Base de Datos + +### Esquema Completo + +#### Tabla: `users` +```sql +id BIGINT PRIMARY KEY +name VARCHAR(255) +email VARCHAR(255) UNIQUE +role ENUM('admin', 'employee', 'guest') +email_verified_at TIMESTAMP NULLABLE +password VARCHAR(255) +remember_token VARCHAR(100) NULLABLE +created_at TIMESTAMP +updated_at TIMESTAMP +``` + +#### Tabla: `products` +```sql +id BIGINT PRIMARY KEY +name VARCHAR(255) +sku VARCHAR(50) UNIQUE NULLABLE +description TEXT NULLABLE +price DECIMAL(10,2) +cost DECIMAL(10,2) NULLABLE +stock_quantity INTEGER DEFAULT 0 +min_stock_alert INTEGER +type ENUM('bike','accessory','clothing','spare') +serial_number VARCHAR(100) NULLABLE +image_path VARCHAR(255) NULLABLE +suppliers_id BIGINT FOREIGN KEY +created_at TIMESTAMP +updated_at TIMESTAMP +``` + +#### Tabla: `clients` +```sql +id BIGINT PRIMARY KEY +name VARCHAR(255) +phone VARCHAR(50) NULLABLE +email VARCHAR(255) NULLABLE UNIQUE +address TEXT NULLABLE +created_at TIMESTAMP +updated_at TIMESTAMP +``` + +#### Tabla: `sales` +```sql +id BIGINT PRIMARY KEY +client_id BIGINT FOREIGN KEY NULLABLE +total DECIMAL(10,2) +payment_method VARCHAR(255) +created_at TIMESTAMP +updated_at TIMESTAMP +``` + +#### Tabla: `sale_details` +```sql +id BIGINT PRIMARY KEY +sale_id BIGINT FOREIGN KEY ON DELETE CASCADE +product_id BIGINT FOREIGN KEY +quantity INTEGER +price DECIMAL(10,2) +created_at TIMESTAMP +updated_at TIMESTAMP +``` + +#### Tabla: `appointments` (Taller) +```sql +id BIGINT PRIMARY KEY +client_id BIGINT FOREIGN KEY ON DELETE CASCADE +contact_phone VARCHAR(50) NULLABLE +scheduled_at DATETIME +bike_model VARCHAR(255) +problem_description TEXT +parts_needed TEXT NULLABLE +estimated_cost DECIMAL(10,2) NULLABLE +status ENUM('pending','in_progress','ready','delivered') +notes TEXT NULLABLE +created_at TIMESTAMP +updated_at TIMESTAMP +``` + +#### Tabla: `suppliers` +```sql +id BIGINT PRIMARY KEY +name VARCHAR(255) +phone VARCHAR(50) +email VARCHAR(255) +created_at TIMESTAMP +updated_at TIMESTAMP +``` + +### Relaciones del Modelo + +``` +User (1) ──────────── (N) Sale +Client (1) ────┬───── (N) Sale + └───── (N) Appointment +Product (1) ──┬─────── (N) SaleDetail + └─────── (1) Supplier +Sale (1) ────────────── (N) SaleDetail +``` + +## 🚀 Instalación + +### Requisitos Previos +```bash +PHP >= 8.2 +Composer >= 2.x +Node.js >= 18.x +NPM >= 9.x +SQLite (o MySQL/PostgreSQL) +``` + +### Instalación Paso a Paso + +```bash +# 1. Clonar el repositorio +git clone https://github.com/tu-usuario/lauck.git +cd lauck + +# 2. Instalar dependencias de PHP +composer install + +# 3. Instalar dependencias de JavaScript +npm install + +# 4. Configurar variables de entorno +cp .env.example .env + +# 5. Generar clave de aplicación +php artisan key:generate + +# 6. Crear base de datos SQLite +touch database/database.sqlite + +# 7. Configurar .env (verificar estas líneas) +DB_CONNECTION=sqlite +DB_DATABASE=/ruta/absoluta/database/database.sqlite + +# 8. Ejecutar migraciones +php artisan migrate + +# 9. Poblar base de datos con datos de prueba +php artisan db:seed + +# 10. Crear enlace simbólico para imágenes +php artisan storage:link + +# 11. Compilar assets para desarrollo +npm run dev +# O para producción: +npm run build + +# 12. Iniciar servidor de desarrollo +php artisan serve +``` + +### Desarrollo Concurrente (Recomendado) + +Laravel incluye un script `dev` en `composer.json` que ejecuta: +```bash +composer run dev +``` + +Esto inicia simultáneamente: +- 🌐 Servidor PHP (puerto 8000) +- 📦 Queue listener +- 📋 Pail (logs en tiempo real) +- ⚡ Vite (HMR) + +### Acceso a la Aplicación + +``` +URL Principal: http://localhost:8000 +Catálogo: http://localhost:8000/catalogo +Admin: http://localhost:8000/dashboard +``` + +### Credenciales de Prueba (Seeder) + +**Administrador:** +- Email: `admin@lauck.com` +- Password: `password` + +**Empleado:** +- Email: `taller@lauck.com` +- Password: `password` + +## 🧪 Datos de Prueba + +El seeder carga automáticamente: +- ✅ 2 usuarios (1 admin, 1 empleado) +- ✅ 1 proveedor genérico +- ✅ 50+ productos variados (bicis, accesorios, repuestos) +- ✅ 1 cliente de ejemplo +- ✅ 1 orden de taller de ejemplo + +```bash +# Limpiar y recargar todo +php artisan migrate:fresh --seed +``` + +## 🎯 Próximas Funcionalidades (Roadmap) + +### Prioridad Alta 🔴 + +#### 1. Sistema de Compras a Proveedores +``` +Features: +- Registrar pedidos a proveedores +- Control de órdenes de compra +- Recepción de mercadería +- Actualización automática de stock +- Historial de compras por proveedor +``` + +#### 2. Reportes y Estadísticas +``` +Dashboard mejorado con: +- Ventas del día/mes/año (gráficos) +- Productos más vendidos +- Stock crítico (alertas) +- Rendimiento por categoría +- Clientes frecuentes +- Exportación a PDF/Excel +``` + +#### 3. Sistema de Usuarios Completo +``` +- Gestión de empleados +- Permisos granulares por módulo +- Registro de actividad (logs) +- Auditoría de cambios +``` + +#### 4. Mejoras en el POS +``` +- Descuentos por producto/venta +- Venta a crédito / financiación +- Múltiples formas de pago en una venta +- Impresión directa de tickets +- Devoluciones y notas de crédito +``` + +### Prioridad Media 🟡 + +#### 5. Notificaciones Automáticas +``` +- Email de comprobante de venta +- WhatsApp API para avisos de taller +- Recordatorios de retiro +- Alertas de stock bajo +``` + +#### 6. Historial de Taller Completo +``` +- Archivo de trabajos entregados +- Búsqueda de histórico por cliente +- Estadísticas de reparaciones +- Reputación del mecánico +``` + +#### 7. Catálogo Mejorado +``` +- Sistema de categorías jerárquicas +- Comparador de productos +- Favoritos/Wishlist +- Reseñas de clientes +- Galería de múltiples imágenes +``` + +#### 8. Integración con MercadoPago/Stripe +``` +- Pagos online para catálogo +- Reservas con seña +- Panel de conciliación +``` + +### Prioridad Baja 🟢 + +#### 9. App Móvil (PWA) +``` +- Instalable como app +- Notificaciones push +- Modo offline básico +``` + +#### 10. Integraciones +``` +- Sincronización con contabilidad +- API pública RESTful +- Webhooks para eventos +- Importación masiva (Excel/CSV) +``` + +#### 11. Marketing +``` +- Email marketing a clientes +- Campañas promocionales +- Cupones de descuento +- Programa de puntos/fidelidad +``` + +#### 12. Multi-sucursal +``` +- Gestión de múltiples locales +- Transferencias entre sucursales +- Stock por local +- Reportes consolidados +``` + +## 🐛 Issues Conocidos y Soluciones + +### 1. Select2 no se inicializa en filas dinámicas +**Problema**: Al agregar nueva fila en POS, Select2 no funciona. + +**Solución actual**: +```javascript +$('.select2-product').select2('destroy'); // Destruir +const newRow = firstRow.clone(); // Clonar +initSelect2(); // Re-inicializar todos +``` + +**Mejora sugerida**: +```javascript +function addRow() { + const newRow = firstRow.clone(); + container.append(newRow); + newRow.find('.select2-product').select2({ /* config */ }); +} +``` + +### 2. Imágenes no se muestran después de upload +**Problema**: Falta ejecutar `storage:link`. + +**Solución**: +```bash +php artisan storage:link +``` + +### 3. Error en eliminación de cliente con ventas +**Problema**: Violación de constraint de foreign key. + +**Solución implementada**: Try-catch con mensaje amigable. + +**Mejora**: Soft deletes +```php +// En migración +$table->softDeletes(); + +// En modelo +use SoftDeletes; +``` + +### 4. Problema de concurrencia en stock +**Escenario**: Dos ventas simultáneas del mismo producto. + +**Solución recomendada**: Optimistic locking +```php +// En modelo Product +protected $casts = [ + 'version' => 'integer' +]; + +// Al actualizar +Product::where('id', $id) + ->where('version', $currentVersion) + ->update([ + 'stock_quantity' => $newStock, + 'version' => $currentVersion + 1 + ]); +``` + +## 📝 Convenciones y Estándares + +### Nombres +- **Variables**: `camelCase` (`$productoActual`) +- **Funciones**: `camelCase` (`getUserData()`) +- **Clases**: `PascalCase` (`ProductController`) +- **Tablas**: `snake_case` plural (`products`, `sale_details`) +- **Rutas**: `kebab-case` (`/productos/crear`) + +### Commits +``` +Add: Nueva funcionalidad +Fix: Corrección de bugs +Update: Actualización de código existente +Refactor: Refactorización sin cambios funcionales +Docs: Cambios en documentación +Style: Formato/estilo (sin cambios lógicos) +Test: Agregar/modificar tests +Chore: Tareas de mantenimiento +``` + +### Branches +``` +main/master - Producción +Bryam - Desarrollo particular de Bryam +Lucas - Desarrollo particular de Lucas +Giane - Desarrollo particular de Giane +``` + +## 🤝 Contribuir + +### Proceso +1. Fork del proyecto +2. Crear rama: `git checkout -b (Tu Nombre)` +3. Commit: `git commit -m 'Add: nueva funcionalidad'` +4. Push: `git push origin (Tu Nombre)` +5. Abrir Pull Request + +### Checklist antes del PR +- [ ] Código sigue las convenciones +- [ ] Tests pasan (`php artisan test`) +- [ ] No hay errores de linting (`composer run lint`) +- [ ] Documentación actualizada +- [ ] Migraciones incluidas si aplica +- [ ] Seeders actualizados si aplica + +## 📄 Licencia + +Este proyecto está bajo la Licencia MIT. Ver archivo `LICENSE` para detalles. + +## 🙏 Agradecimientos + +- [Laravel](https://laravel.com) - El framework PHP para artesanos web +- [Tailwind CSS](https://tailwindcss.com) - Framework CSS utility-first +- [Select2](https://select2.org) - Selectores avanzados +- [jQuery](https://jquery.com) - Manipulación DOM simplificada +- Comunidad de Laravel por su excelente documentación + +## 📞 Contacto + +**Bicicletería Lauck** +- Dirección: Av. Francisco Ramírez 1389, Paraná, Entre Ríos +- Teléfono: 343 422-0103 +- Web: En desarrollo + +--- + +**Versión**: 0.2.0 +**Estado**: En desarrollo activo 🚧 +**Última actualización**: Enero 2026 + +**Desarrollado con ❤️ para Bicicletería Lauck** \ No newline at end of file diff --git a/RECOMENDACIONES.md b/RECOMENDACIONES.md new file mode 100644 index 0000000..82eb622 --- /dev/null +++ b/RECOMENDACIONES.md @@ -0,0 +1,356 @@ +## 🔧 Mejoras Técnicas Recomendadas + +### Optimización de Rendimiento + +#### 1. Implementar Caché Estratégico +```php +// ProductosController.php +public function index(Request $request) +{ + $cacheKey = 'products.' . md5(serialize($request->all())); + + $products = Cache::remember($cacheKey, 300, function () use ($request) { + return Product::query() + ->when($request->search, fn($q, $term) => + $q->where('name', 'like', "%{$term}%") + ) + ->paginate(10); + }); +} + +// Limpiar caché al modificar productos +Cache::forget('products.*'); +``` + +#### 2. Eager Loading para Relaciones +```php +// Evitar N+1 queries +$sales = Sale::with(['client', 'details.product']) + ->latest() + ->paginate(15); + +// En lugar de: +$sales = Sale::all(); // Genera queries adicionales +``` + +#### 3. Índices de Base de Datos +```php +// En migraciones +Schema::table('products', function (Blueprint $table) { + $table->index('sku'); + $table->index('type'); + $table->index(['stock_quantity', 'min_stock_alert']); +}); + +Schema::table('sales', function (Blueprint $table) { + $table->index('created_at'); + $table->index(['client_id', 'created_at']); +}); +``` + +#### 4. Paginación con Cursor +```php +// Para datasets grandes +$products = Product::latest()->cursorPaginate(20); +``` + +### Refactorización de Código + +#### 1. Services para Lógica de Negocio +```php +// app/Services/SaleService.php +class SaleService +{ + public function processSale(array $data): Sale + { + return DB::transaction(function () use ($data) { + $sale = Sale::create([...]); + + foreach ($data['items'] as $item) { + $this->addItem($sale, $item); + } + + return $sale; + }); + } + + private function addItem(Sale $sale, array $item): void + { + $product = Product::findOrFail($item['product_id']); + + if ($product->stock_quantity < $item['quantity']) { + throw new InsufficientStockException(); + } + + SaleDetail::create([...]); + $product->decrement('stock_quantity', $item['quantity']); + } +} +``` + +#### 2. Form Request Validation +```php +// app/Http/Requests/StoreSaleRequest.php +class StoreSaleRequest extends FormRequest +{ + public function rules(): array + { + return [ + 'client_id' => 'nullable|exists:clients,id', + 'payment_method' => 'required|string', + 'items' => 'required|array|min:1', + 'items.*.product_id' => 'required|exists:products,id', + 'items.*.quantity' => 'required|integer|min:1', + ]; + } + + public function messages(): array + { + return [ + 'items.required' => 'Debe agregar al menos un producto.', + 'items.*.quantity.min' => 'La cantidad debe ser mayor a 0.', + ]; + } +} + +// En el controlador +public function store(StoreSaleRequest $request) +{ + // $request ya está validado +} +``` + +#### 3. Repository Pattern +```php +// app/Repositories/ProductRepository.php +interface ProductRepositoryInterface +{ + public function search(string $term): Collection; + public function lowStock(): Collection; + public function byType(string $type): Collection; +} + +class ProductRepository implements ProductRepositoryInterface +{ + public function search(string $term): Collection + { + return Product::where('name', 'like', "%{$term}%") + ->orWhere('sku', 'like', "%{$term}%") + ->get(); + } +} + +// Registrar en AppServiceProvider +$this->app->bind( + ProductRepositoryInterface::class, + ProductRepository::class +); +``` + +#### 4. Policies para Autorización +```php +// app/Policies/ProductPolicy.php +class ProductPolicy +{ + public function viewAny(User $user): bool + { + return in_array($user->role, ['admin', 'employee']); + } + + public function create(User $user): bool + { + return $user->role === 'admin'; + } + + public function update(User $user, Product $product): bool + { + return $user->role === 'admin'; + } + + public function delete(User $user, Product $product): bool + { + return $user->role === 'admin'; + } +} + +// En controlador +$this->authorize('create', Product::class); +``` + +### Mejoras de Seguridad + +#### 1. Rate Limiting Específico +```php +// routes/web.php +Route::post('/sales', [SaleController::class, 'store']) + ->middleware(['auth', 'throttle:sales']); + +// app/Providers/RouteServiceProvider.php +RateLimiter::for('sales', function (Request $request) { + return Limit::perMinute(10)->by($request->user()?->id); +}); +``` + +#### 2. Sanitización de Inputs +```php +// En Form Request +protected function prepareForValidation() +{ + $this->merge([ + 'name' => strip_tags($this->name), + 'description' => Purifier::clean($this->description), + ]); +} +``` + +#### 3. Protección CSRF Explícita +```php +// Verificar en AJAX +$.ajaxSetup({ + headers: { + 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') + } +}); +``` + +#### 4. Validación de Imágenes +```php +'image' => [ + 'nullable', + 'image', + 'mimes:jpeg,png,jpg,webp', + 'max:2048', // 2MB + 'dimensions:min_width=100,min_height=100,max_width=2000,max_height=2000' +] +``` + +### Mejoras de UI/UX + +#### 1. Loading States +```blade + + + +``` + +#### 2. Confirmaciones Elegantes +```javascript +// Usar SweetAlert2 +Swal.fire({ + title: '¿Eliminar producto?', + text: "Esta acción no se puede revertir", + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#ccff00', + cancelButtonColor: '#d33', + confirmButtonText: 'Sí, eliminar', + cancelButtonText: 'Cancelar' +}).then((result) => { + if (result.isConfirmed) { + // Ejecutar eliminación + } +}); +``` + +#### 3. Toast Notifications +```javascript +// Mensajes no intrusivos +const Toast = Swal.mixin({ + toast: true, + position: 'top-end', + showConfirmButton: false, + timer: 3000 +}); + +Toast.fire({ + icon: 'success', + title: 'Producto guardado' +}); +``` + +#### 4. Validación en Tiempo Real +```javascript +$('input[name="sku"]').on('blur', function() { + const sku = $(this).val(); + + $.get(`/api/products/check-sku/${sku}`, function(data) { + if (data.exists) { + // Mostrar error + $(this).addClass('border-red-500'); + } + }); +}); +``` + +### Testing + +#### 1. Tests de Feature +```php +// tests/Feature/ProductTest.php +public function test_admin_can_create_product() +{ + $admin = User::factory()->create(['role' => 'admin']); + + $this->actingAs($admin) + ->post('/productos', [ + 'name' => 'Test Product', + 'price' => 100, + 'stock_quantity' => 10, + 'min_stock_alert' => 5, + 'type' => 'accessory' + ]) + ->assertRedirect('/productos') + ->assertSessionHas('success'); + + $this->assertDatabaseHas('products', [ + 'name' => 'Test Product' + ]); +} + +public function test_guest_cannot_create_product() +{ + $response = $this->post('/productos', []); + $response->assertRedirect('/login'); +} +``` + +#### 2. Tests Unitarios +```php +// tests/Unit/ProductTest.php +public function test_product_has_low_stock() +{ + $product = Product::factory()->create([ + 'stock_quantity' => 3, + 'min_stock_alert' => 5 + ]); + + $this->assertTrue($product->hasLowStock()); +} +``` + +#### 3. Browser Tests (Dusk) +```php +// tests/Browser/PosTest.php +public function test_can_complete_sale() +{ + $this->browse(function (Browser $browser) { + $browser->loginAs(User::find(1)) + ->visit('/sales/create') + ->select('items[0][product_id]', 1) + ->type('items[0][quantity]', 2) + ->press('Confirmar Venta') + ->assertPathIs('/sales/1'); + }); +} +``` \ No newline at end of file diff --git a/app/Http/Controllers/CatalogoController.php b/app/Http/Controllers/CatalogoController.php index 425580d..82eecb7 100644 --- a/app/Http/Controllers/CatalogoController.php +++ b/app/Http/Controllers/CatalogoController.php @@ -10,7 +10,7 @@ class CatalogoController extends Controller /** * Muestra los registros de ventas con buscador y paginación. */ - public function catalogo(Request $request) + public function index(Request $request) { // Consulta base $query = Product::query(); @@ -20,17 +20,22 @@ class CatalogoController extends Controller $searchTerm = $request->input('search'); $query->where(function($q) use ($searchTerm) { - $q->where('name', 'like', "%{$searchTerm}%") // Buscar por nombre - ->orWhere('sku', 'like', "%{$searchTerm}%"); // O por código SKU + $q->where('name', 'like', "%{$searchTerm}%") + ->orWhere('sku', 'like', "%{$searchTerm}%"); }); } - $query->whereIn('type', ['bike', 'accessory']);// Filtros para el público + $query->whereIn('type', ['bike', 'accessory', 'clothing', 'spare']); $query->where('stock_quantity', '>', 0); // Solo mostrar si tiene stock // Resultados paginados $products = $query->paginate(12)->withQueryString(); // withQueryString mantiene la búsqueda al cambiar de página // Devuelve la vista - return view('catalogo', compact('products')); + return view('catalogo.index', compact('products')); + } + + public function show(Product $product) + { + return view('catalogo.show', compact('product')); } } diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index b456d43..da56e93 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -20,7 +20,6 @@ class ClientController extends Controller return $q->where('name', 'like', "%{$query}%"); }) ->orderBy('name', 'asc') - //->orderBy('create_at', 'asc') ->paginate(10) // Paginamos de a 10 ->withQueryString(); // Mantiene el filtro de búsqueda al cambiar de página @@ -53,13 +52,18 @@ class ClientController extends Controller // 2. Verificamos el origen if ($request->input('origin') === 'sales') { - // Si vino de ventas, volvemos a ventas - // Y pasamos el ID del nuevo cliente para auto-seleccionarlo + // Si vino de ventas, volvemos a ventas con el cliente nuevo return redirect()->route('sales.create', ['new_client_id' => $client->id]) ->with('success', 'Cliente creado. Ya puedes seleccionarlo.'); } + else if ($request->input('origin') === 'taller') { + + // Si vino de taller, volvemos a taller con el cliente nuevo + return redirect()->route('taller.create', ['new_client_id' => $client->id]) + ->with('success', 'Cliente creado. Ya puedes seleccionarlo.'); + } - // 3. Si no, comportamiento normal (volver al index de clientes) + // 3. Si no, vuelvo al index de clientes return redirect()->route('clients.index') ->with('success', 'Cliente creado correctamente.'); } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index fd970d4..24379cc 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -11,9 +11,9 @@ class HomeController extends Controller { // Si tuvieras un campo 'sales_count', podrías usar ->orderByDesc('sales_count') $destacados = Product::where('type', 'bike') - ->latest() // Las más nuevas - ->take(5) - ->get(); + ->latest() // Las más nuevas + ->take(5) + ->get(); // Si no hay bicis, traemos cualquier cosa para que no se rompa if ($destacados->isEmpty()) { diff --git a/app/Http/Controllers/LoginController.php b/app/Http/Controllers/LoginController.php index d198e28..3397c06 100644 --- a/app/Http/Controllers/LoginController.php +++ b/app/Http/Controllers/LoginController.php @@ -14,13 +14,13 @@ class LoginController extends Controller 'email' => ['required', 'email'], 'password' => ['required'], ]); - + if (Auth::attempt($credentials)) { $request->session()->regenerate(); - + return redirect()->intended('dashboard'); } - + return back()->withErrors([ 'email' => 'Los datos no coinciden.', ])->onlyInput('email'); diff --git a/app/Http/Controllers/ProductosController.php b/app/Http/Controllers/ProductosController.php index 92e1c74..c7e37e7 100644 --- a/app/Http/Controllers/ProductosController.php +++ b/app/Http/Controllers/ProductosController.php @@ -4,8 +4,8 @@ namespace App\Http\Controllers; use App\Models\Product; use Illuminate\Http\Request; -use Illuminate\Validation\Rule; // Necesario para validar unicidad al editar -use Illuminate\Support\Facades\Storage; // <--- IMPORTANTE: Agregar esto arriba +use Illuminate\Validation\Rule; +use Illuminate\Support\Facades\Storage; class ProductosController extends Controller { @@ -14,33 +14,30 @@ class ProductosController extends Controller */ public function index(Request $request) { - // Recuperamos lo que el usuario escribió en el buscador (si escribió algo) + // Recuperamos lo escrito en el buscador (si aplica) $query = $request->input('search'); $status = $request->input('stock_status'); // Construimos la consulta $products = Product::query() ->when($query, function ($q) use ($query) { - // Si hay búsqueda, filtra por nombre o SKU + // Filtra por nombre o SKU return $q->where('name', 'like', "%{$query}%") - ->orWhere('sku', 'like', "%{$query}%"); + ->orWhere('sku', 'like', "%{$query}%"); }) ->when($status, function ($q) use ($status) { if ($status === 'low') { // Rojo: Menor o igual a la alerta - return $q->whereColumn('stock_quantity', '<', 'min_stock_alert') - ->where('type', '!=', 'service'); // Ignoramos servicios + return $q->whereColumn('stock_quantity', '<', 'min_stock_alert'); } elseif ($status === 'medium') { - // Amarillo: Mayor a alerta PERO menor o igual a alerta + 2 (margen pequeño) + // Amarillo: Mayor al min y menor o igual al min + 5 return $q->whereColumn('stock_quantity', '>=', 'min_stock_alert') - ->whereRaw('stock_quantity <= (min_stock_alert + 1)') // Ajusta este "+ 5" según tu criterio de "amarillo" - ->where('type', '!=', 'service'); + ->whereRaw('stock_quantity <= (min_stock_alert + 5)'); } elseif ($status === 'ok') { // Verde: Stock saludable - return $q->whereRaw('stock_quantity > (min_stock_alert + 1)') - ->where('type', '!=', 'service'); + return $q->whereRaw('stock_quantity > (min_stock_alert + 5)'); } }) ->orderBy('stock_quantity', 'asc') // Ordenamos primero los que tienen poco stock (Alerta visual) @@ -72,7 +69,7 @@ class ProductosController extends Controller 'cost' => 'nullable|numeric|min:0', // Costo opcional 'stock_quantity' => 'required|integer|min:0', 'min_stock_alert' => 'required|integer|min:0', - 'type' => 'required|in:bike,accessory,service', // Solo permite estos 3 valores + 'type' => 'required|in:bike,accessory,clothing,spare', // Solo permite estos valores 'serial_number' => 'nullable|string|max:100', 'image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048', ]); @@ -93,6 +90,7 @@ class ProductosController extends Controller // Para no romper la logica del supplier $validated['suppliers_id'] = 1; + // 3. Creamos el producto Product::create($validated); @@ -103,7 +101,6 @@ class ProductosController extends Controller /** * Muestra el detalle de un producto. - * Usamos Route Model Binding: Laravel busca el ID solo. */ public function show(Product $product) { @@ -132,7 +129,7 @@ class ProductosController extends Controller 'cost' => 'nullable|numeric|min:0', 'stock_quantity' => 'required|integer|min:0', 'min_stock_alert' => 'required|integer|min:0', - 'type' => 'required|in:bike,accessory,service', + 'type' => 'required|in:bike,accessory,clothing,spare', 'serial_number' => 'nullable|string|max:100', 'image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048', diff --git a/app/Http/Controllers/RegisterController.php b/app/Http/Controllers/RegisterController.php index 39f4fe1..9185db0 100644 --- a/app/Http/Controllers/RegisterController.php +++ b/app/Http/Controllers/RegisterController.php @@ -18,7 +18,6 @@ class RegisterController extends Controller ]); $userData['password'] = bcrypt($userData['password']); - $user = User::create($userData); Auth::login($user); diff --git a/app/Http/Controllers/TallerController.php b/app/Http/Controllers/TallerController.php new file mode 100644 index 0000000..2ff9ab0 --- /dev/null +++ b/app/Http/Controllers/TallerController.php @@ -0,0 +1,83 @@ +whereIn('status', ['pending', 'in_progress', 'ready']) // Entregados/historial no se muestra, implementar vista aparte + ->orderBy('scheduled_at', 'asc') + ->get(); + + // Agrupado por estado para las columnas + $pending = $jobs->where('status', 'pending'); + $inProgress = $jobs->where('status', 'in_progress'); + $ready = $jobs->where('status', 'ready'); + + return view('taller.index', compact('pending', 'inProgress', 'ready')); + } + + /** + * Formulario para nuevo servicio + */ + public function create() + { + $clients = Client::orderBy('name')->get(); + + $products = Product::whereIn('type', ['accessory','spare']) + ->where('stock_quantity', '>', 0) + ->get(['id', 'name', 'price', 'sku', 'stock_quantity']); // Solo campos necesarios + + return view('taller.create', compact('clients', 'products')); + } + + /** + * Guardar el servicio + */ + public function store(Request $request) + { + $request->validate([ + 'client_id' => 'required|exists:clients,id', + 'bike_model' => 'required|string', + 'problem_description' => 'required|string', + 'scheduled_at' => 'required|date', + 'estimated_cost' => 'nullable|numeric', + ]); + + // Buscamos el cliente para copiar su teléfono + $client = Client::find($request->client_id); + + Appointment::create([ + 'client_id' => $request->client_id, + 'contact_phone' => $request->contact_phone ?? $client->phone, + 'bike_model' => $request->bike_model, + 'problem_description' => $request->problem_description, + 'parts_needed' => $request->parts_needed, + 'estimated_cost' => $request->estimated_cost, + 'scheduled_at' => $request->scheduled_at, + 'status' => 'pending', // Estado inicial: Pendiente + ]); + + return redirect()->route('taller.index')->with('success', 'Bicicleta ingresada al taller correctamente.'); + } + + /** + * Mover de estado + */ + public function updateStatus(Request $request, Appointment $appointment) + { + $appointment->update(['status' => $request->status]); + return back()->with('success', 'Estado actualizado.'); + } +} \ No newline at end of file diff --git a/app/Models/Appointment.php b/app/Models/Appointment.php index 85d1752..87a42b2 100644 --- a/app/Models/Appointment.php +++ b/app/Models/Appointment.php @@ -11,9 +11,12 @@ class Appointment extends Model protected $fillable = [ 'client_id', + 'contact_phone', 'scheduled_at', 'bike_model', 'problem_description', + 'parts_needed', + 'estimated_cost', 'status', 'notes' ]; diff --git a/app/Models/Sale.php b/app/Models/Sale.php index 67f7dcb..7057337 100644 --- a/app/Models/Sale.php +++ b/app/Models/Sale.php @@ -8,22 +8,22 @@ class Sale extends Model { //use HasFactory; - // Permitimos asignación masiva para poder guardar rápido + // Permitimos asignación masiva protected $guarded = []; - // Relación 1: Una venta la realiza un Usuario (User) + // Rel 1: Una venta la hace un Usuario public function user() { return $this->belongsTo(User::class); } - // Relación 2: Una venta pertenece a un Cliente (User) + // Rel 2: Una venta pertenece a un Cliente public function client() { return $this->belongsTo(Client::class); } - // Relación 3: Una venta tiene muchos items o detalles + // Rel 3: Una venta tiene muchos items public function details() { return $this->hasMany(SaleDetail::class); diff --git a/app/Models/SaleDetail.php b/app/Models/SaleDetail.php index 597eff4..94dcac5 100644 --- a/app/Models/SaleDetail.php +++ b/app/Models/SaleDetail.php @@ -9,13 +9,13 @@ class SaleDetail extends Model //use HasFactory; protected $guarded = []; - // este detalle pertenece a una Venta específica + // Rel 1: Este detalle pertenece a una Venta específica public function sale() { return $this->belongsTo(Sale::class); } - // este detalle corresponde a un Producto + // Rel 2: Este detalle pertenece a un Producto public function product() { return $this->belongsTo(Product::class); diff --git a/config/app.php b/config/app.php index 423eed5..25f728f 100644 --- a/config/app.php +++ b/config/app.php @@ -78,7 +78,7 @@ return [ | */ - 'locale' => env('APP_LOCALE', 'en'), + 'locale' => env('APP_LOCALE', 'es'), 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), diff --git a/database/factories/ProductFactory.php b/database/factories/ProductFactory.php index 2ab688a..2ce0624 100644 --- a/database/factories/ProductFactory.php +++ b/database/factories/ProductFactory.php @@ -19,23 +19,24 @@ class ProductFactory extends Factory $brands = ['Shimano', 'Venzo', 'Trek', 'Specialized', 'Maxxis', 'Sram', 'Raleigh']; $bikeModels = ['Loki', 'Marlin 5', 'Chisel', 'Talon 3', 'Aspect 950']; $accessories = ['Guantes Grip', 'Calco Reflectora', 'Coderas', 'Rodilleras', 'Casco MTB', 'Luz Delantera USB', 'Cubierta Kevlar']; - // PARA DESPUES $spareParts = ['Manubrio', 'Pedales Aluminio', 'Cámara 29"', 'Rayos x50', 'Disco de Freno', 'Cable de Freno', 'Asiento Goma', 'Cadena 9v']; - - $type = $this->faker->randomElement(['bike', 'accessory', 'service']); + $cats = ['Pro', 'Basic', 'Comp', 'Elite']; + $type = $this->faker->randomElement(['bike', 'accessory', 'clothing', 'spare']); // Generar nombre según el tipo if ($type === 'bike') { $name = $this->faker->randomElement($brands) . ' ' . $this->faker->randomElement($bikeModels); } elseif ($type === 'accessory') { - $name = $this->faker->randomElement($accessories) . ' ' . $this->faker->randomElement(['Pro', 'Basic', 'Comp', 'Elite']); + $name = $this->faker->randomElement($accessories) .' '. $this->faker->randomElement($cats); + } elseif ($type === 'clothing') { + $name = $this->faker->randomElement(['Remera', 'Pantalon', 'Casco', 'Coderas', 'Guantes']).' '.$this->faker->randomElement($cats); } else { - $name = $this->faker->randomElement(['Service General', 'Ajuste Cambios', 'Centrado de Rueda', 'Lavado y Engrase']); + $name = $this->faker->randomElement($spareParts) .' '. $this->faker->randomElement($cats); } // Lógica de Precios - $price = $this->faker->numberBetween(5000, 800000); - $cost = $price * $this->faker->randomFloat(2, 0.5, 0.7); + $price = $this->faker->numberBetween(5000, 200000); + $cost = $price * $this->faker->randomFloat(2, 0.1, 0.8); return [ 'name' => $name, diff --git a/database/migrations/2025_12_06_063046_add_role_to_users_table.php b/database/migrations/2025_12_06_063046_add_role_to_users_table.php index 78352f6..de5bce5 100644 --- a/database/migrations/2025_12_06_063046_add_role_to_users_table.php +++ b/database/migrations/2025_12_06_063046_add_role_to_users_table.php @@ -12,7 +12,7 @@ return new class extends Migration public function up(): void { Schema::table('users', function (Blueprint $table) { - $table->enum('role', ['admin', 'employee'])->default('employee')->after('email'); + $table->enum('role', ['admin', 'employee', 'guest'])->default('guest')->after('email'); }); } diff --git a/database/migrations/2025_12_06_182358_create_products_table.php b/database/migrations/2025_12_06_182358_create_products_table.php index 883d9d3..326973c 100644 --- a/database/migrations/2025_12_06_182358_create_products_table.php +++ b/database/migrations/2025_12_06_182358_create_products_table.php @@ -24,8 +24,8 @@ return new class extends Migration $table->integer('stock_quantity')->default(0); $table->integer('min_stock_alert'); // Alerta - - $table->string('rolled')->nullable(); // Solo para bicis + $table->enum('type', ['bike', 'accessory', 'clothing', 'spare']); // Tipo de producto + $table->string('serial_number')->nullable(); // Solo para bicis $table->foreignId('suppliers_id')->constrained()->default(1); $table->timestamps(); diff --git a/database/migrations/2026_01_23_135512_add_taller_fields_to_appointments_table.php b/database/migrations/2026_01_23_135512_add_taller_fields_to_appointments_table.php new file mode 100644 index 0000000..3239982 --- /dev/null +++ b/database/migrations/2026_01_23_135512_add_taller_fields_to_appointments_table.php @@ -0,0 +1,35 @@ +text('parts_needed')->nullable()->after('problem_description'); + + // Costo (Presupuesto) + $table->decimal('estimated_cost', 10, 2)->nullable()->after('parts_needed'); + + // Teléfono de contacto rápido (por si es distinto al del cliente registrado) + $table->string('contact_phone')->nullable()->after('client_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('appointments', function (Blueprint $table) { + $table->dropColumn(['parts_needed', 'estimated_cost', 'contact_phone']); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 925158c..0ccfcb8 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -16,7 +16,7 @@ class DatabaseSeeder extends Seeder { //Crear el Super Admin User::factory()->create([ - 'name' => 'Jose Admin', + 'name' => 'Sergio Lauck', 'email' => 'admin@lauck.com', 'password' => bcrypt('password'), // Cambiar en producción 'role' => 'admin', @@ -60,9 +60,9 @@ class DatabaseSeeder extends Seeder 'suppliers_id' => 1 ]); - // Generar 10 productos aleatorios más + // Generar 50 productos aleatorios más Product::factory(50)->create(); - + // Clientes y Turnos $client = Client::create([ 'name' => 'Juan Perez', @@ -77,5 +77,7 @@ class DatabaseSeeder extends Seeder 'problem_description' => 'Service completo y ajuste de cambios', 'status' => 'pending' ]); + + $this->call(ProductSeeder::class); } } diff --git a/database/seeders/ProductSeeder.php b/database/seeders/ProductSeeder.php new file mode 100644 index 0000000..2a80423 --- /dev/null +++ b/database/seeders/ProductSeeder.php @@ -0,0 +1,299 @@ + 'Bicicleta R12 nena-varon rueda maciza', + 'sku' => 'BIC-001', + 'price' => 148000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Bicicleta R12 con rayos varon', + 'sku' => 'BIC-002', + 'price' => 175000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Bicicleta R12 reina flecos canasto', + 'sku' => 'BIC-003', + 'price' => 999, + 'cost' => 111, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Bicicleta R12 topmega nena', + 'sku' => 'BIC-004', + 'price' => 250000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Bicicleta R12 topmega varon', + 'sku' => 'BIC-005', + 'price' => 250000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Bicicleta R12 Max SLP', + 'sku' => 'BIC-006', + 'price' => 250000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', + ], + [ + 'name' => 'Bicicleta R12 Max SLP rueda megnesio', + 'sku' => 'BIC-007', + 'price' => 268000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Bicicleta R12 SLP dolphin', + 'sku' => 'BIC-008', + 'price' => 250000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Camicleta SLP varon', + 'sku' => 'BIC-009', + 'price' => 139000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Camicleta SLP ruedas macizas', + 'sku' => 'BIC-010', + 'price' => 138000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Camicleta SLP rayos', + 'sku' => 'BIC-011', + 'price' => 185000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Camicleta Economica ruedas macizas rosa', + 'sku' => 'BIC-012', + 'price' => 100000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Bicicleta R14 Cross varon', + 'sku' => 'BIC-013', + 'price' => 228000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Bicicleta R14 Full nena', + 'sku' => 'BIC-014', + 'price' => 228000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Bicicleta R16 Peretti nena canasto', + 'sku' => 'BIC-015', + 'price' => 239000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Bicicleta R16 Firebird', + 'sku' => 'BIC-016', + 'price' => 269000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Bicicleta R16 Full nena', + 'sku' => 'BIC-017', + 'price' => 239000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', // bike, accessory, clothing, spare + ], + [ + 'name' => 'Bicicleta R16 dolphin/zest nena', + 'sku' => 'BIC-018', + 'price' => 269000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', + ], + [ + 'name' => 'Bicicleta R16 Cross estabilizadores', + 'sku' => 'BIC-019', + 'price' => 230000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', + ], + [ + 'name' => 'Bicicleta R16 Peretti Cross', + 'sku' => 'BIC-020', + 'price' => 235000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', + ], + [ + 'name' => 'Bicicleta R16 Max SLP', + 'sku' => 'BIC-021', + 'price' => 268000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', + ], + [ + 'name' => 'Bicicleta R16 Cross Peretti cromada', + 'sku' => 'BIC-022', + 'price' => 240000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', + ], + [ + 'name' => 'Bicicleta R16 Cross Topmega', + 'sku' => 'BIC-023', + 'price' => 240000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', + ], + [ + 'name' => 'Bicicleta R20 Cross', + 'sku' => 'BIC-024', + 'price' => 245000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', + ], + [ + 'name' => 'Bicicleta R20 Peretti Cross varon', + 'sku' => 'BIC-025', + 'price' => 245000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', + ], + [ + 'name' => 'Bicicleta R20 Max SLP varon', + 'sku' => 'BIC-026', + 'price' => 298000, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', + ], + [ + 'name' => 'Bicicleta R20 Firebird', + 'sku' => 'BIC-027', + 'price' => 1, + 'cost' => 100000, + 'stock_quantity' => 5, + 'min_stock_alert' => 2, + 'type' => 'bike', + ], + // 2. ACCESORIOS + [ + 'name' => 'Cámara 29 Valvula Auto', + 'sku' => 'CAM-29-A', + 'price' => 5000, + 'cost' => 2500, + 'stock_quantity' => 50, + 'min_stock_alert' => 10, + 'type' => 'accessory', + ], + [ + 'name' => 'Servicio de Lavado', + 'sku' => 'SRV-LAV', + 'price' => 3000, + 'cost' => 0, + 'stock_quantity' => 999, + 'min_stock_alert' => 0, + 'type' => 'clothing', + ], + ]; + + // BUCLE DE CARGA + foreach ($products as $item) { + Product::updateOrCreate( + ['sku' => $item['sku']], + [ + 'name' => $item['name'], + 'price' => $item['price'], + 'cost' => $item['cost'] ?? 0, + 'stock_quantity' => $item['stock_quantity'] ?? 0, + 'min_stock_alert' => $item['min_stock_alert'] ?? 5, + 'type' => $item['type'] ?? 'accessory', + 'description' => $item['description'] ?? null, + ] + ); + } + } +} diff --git a/resources/css/app.css b/resources/css/app.css index 24bd901..7bef085 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -3,6 +3,7 @@ /* 2. Importar Tailwind (v4) */ @import "tailwindcss"; +@custom-variant dark (&:is(.dark *)); @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; @source '../../storage/framework/views/*.php'; @source '../**/*.blade.php'; @@ -25,13 +26,16 @@ /* 4. Patrón de fondo estilo "Técnico" */ .bg-grid-pattern { - /* CAMBIO: Usamos rgba(255,255,255, 0.1) para que las líneas sean claras sobre fondo oscuro */ - background-image: linear-gradient(to right, rgba(255, 255, 255, 0.05) 1px, transparent 1px), - linear-gradient(to bottom, rgba(255, 255, 255, 0.05) 1px, transparent 1px); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.1) 1px, transparent 1px), + linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 1px, transparent 1px); background-size: 40px 40px; background-position: center; } +:is(.dark) .bg-grid-pattern { + background-image: linear-gradient(to right, rgba(255, 255, 255, 0.05) 1px, transparent 1px), + linear-gradient(to bottom, rgba(255, 255, 255, 0.05) 1px, transparent 1px); +} /* 5. Personalización del Scrollbar */ ::-webkit-scrollbar { width: 8px; diff --git a/resources/views/catalogo.blade.php b/resources/views/catalogo.blade.php deleted file mode 100644 index 0d33c3b..0000000 --- a/resources/views/catalogo.blade.php +++ /dev/null @@ -1,54 +0,0 @@ - - - - - -
- - @if ($products->count() > 0) -
- @foreach ($products as $prod) -
- {{-- Imagen si existe --}} - @if (!empty($prod->image_path)) - {{ $prod->name }} - @else -
- Sin imagen -
- @endif - - {{-- Nombre --}} -

{{ $prod->name }}

- - {{-- Descripción corta --}} -
- @if (!empty($prod->description)) -

{{ $prod->description }}

- @endif -
- - {{-- Precio --}} - @if (!empty($prod->price)) -

${{ number_format($prod->price, 2, ',', '.') }}

- @endif - - {{-- Botón --}} - - Ver + - - -
- @endforeach -
- @else -

No hay productos disponibles.

- @endif - -
- {{ $products->links() }} -
-
-
diff --git a/resources/views/catalogo/index.blade.php b/resources/views/catalogo/index.blade.php new file mode 100644 index 0000000..b50f89f --- /dev/null +++ b/resources/views/catalogo/index.blade.php @@ -0,0 +1,54 @@ + + + + + +
+ + @if ($products->count() > 0) +
+ @foreach ($products as $product) +
+ {{-- Imagen si existe --}} + @if (!empty($product->image_path)) + {{ $product->name }} + @else +
+ Sin imagen +
+ @endif + + {{-- Nombre --}} +

{{ $product->name }}

+ + {{-- Descripción corta --}} +
+ @if (!empty($product->description)) +

{{ $product->description }}

+ @endif +
+ + {{-- Precio --}} + @if (!empty($product->price)) +

${{ number_format($product->price, 2, ',', '.') }}

+ @endif + + {{-- Botón --}} + + Ver + + + +
+ @endforeach +
+ @else +

No hay productos disponibles.

+ @endif + +
+ {{ $products->links() }} +
+
+
diff --git a/resources/views/catalogo/show.blade.php b/resources/views/catalogo/show.blade.php new file mode 100644 index 0000000..a137c90 --- /dev/null +++ b/resources/views/catalogo/show.blade.php @@ -0,0 +1,111 @@ + + +
+ + + + + Volver al Catálogo + + +
+
+ + +
+ + @if($product->image_path) + {{ $product->name }} + @else +
+ + Sin Imagen +
+ @endif + + +
+ + @if($product->type == 'bike') + Bicicleta + @elseif($product->type == 'clothing') + Indumentaria + @elseif($product->type == 'spare') + Repuesto + @else + Accesorio + @endif + +
+ +
+ + +
+ + +
+

SKU: {{ $product->sku ?? 'N/A' }}

+

{{ $product->name }}

+ + +
+ @if($product->stock_quantity > 5) + + En Stock ({{ $product->stock_quantity }} unid.) + @elseif($product->stock_quantity > 0) + + ¡Últimas Unidades! ({{ $product->stock_quantity }}) + @else + + Agotado + @endif +
+
+ + +
+ Precio Contado +
+ ${{ number_format($product->price, 0, ',', '.') }} + ARG +
+

* Consultar financiación en el local.

+
+ + +
+

Descripción

+

+ {{ $product->description ?? 'Sin descripción detallada disponible para este producto. Por favor, acérquese al local para más información.' }} +

+
+ + +
+ + + + + WhatsApp + +
+ +
+
+
+
+
+ {{-- Editar +
+ @csrf + @method('DELETE') + +
--}} \ No newline at end of file diff --git a/resources/views/components/appc.blade.php b/resources/views/components/appc.blade.php index a054ae7..c1e4eaf 100644 --- a/resources/views/components/appc.blade.php +++ b/resources/views/components/appc.blade.php @@ -1,3 +1,6 @@ + +{{-- +DEPRECADO: Este componente ha sido reemplazado por layout.blade.php para una mejor organización. @props(['auth' => false]) @@ -120,4 +123,4 @@ }); - \ No newline at end of file + --}} \ No newline at end of file diff --git a/resources/views/components/footer.blade.php b/resources/views/components/footer.blade.php index c1105a8..94645f9 100644 --- a/resources/views/components/footer.blade.php +++ b/resources/views/components/footer.blade.php @@ -1,16 +1,16 @@ -