Merge branch 'bryam' of https://github.com/BryamE/ProyectoLauck into giane
This commit is contained in:
@@ -1,61 +1,746 @@
|
||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
# 🚴 Sistema de Gestión Lauck
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
**Sistema web integral para gestión de bicicletería** - Inventario, ventas, taller, clientes y catálogo público.
|
||||
|
||||
## About Laravel
|
||||
[](https://laravel.com)
|
||||
[](https://php.net)
|
||||
[](https://tailwindcss.com)
|
||||
[](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**
|
||||
@@ -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
|
||||
<button type="submit" id="submit-btn" class="...">
|
||||
<span class="loading hidden">
|
||||
<svg class="animate-spin h-5 w-5" ...></svg>
|
||||
</span>
|
||||
<span class="text">Guardar</span>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
$('#form').on('submit', function() {
|
||||
$('#submit-btn .loading').removeClass('hidden');
|
||||
$('#submit-btn .text').addClass('hidden');
|
||||
$('#submit-btn').prop('disabled', true);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 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');
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
// 3. Si no, comportamiento normal (volver al index de clientes)
|
||||
// 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, vuelvo al index de clientes
|
||||
return redirect()->route('clients.index')
|
||||
->with('success', 'Cliente creado correctamente.');
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -18,7 +18,6 @@ class RegisterController extends Controller
|
||||
]);
|
||||
|
||||
$userData['password'] = bcrypt($userData['password']);
|
||||
|
||||
$user = User::create($userData);
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Appointment;
|
||||
use App\Models\Client;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TallerController extends Controller
|
||||
{
|
||||
/**
|
||||
* Vista principal
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// Trabajos activos ordenados por fecha
|
||||
$jobs = Appointment::with('client')
|
||||
->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.');
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
];
|
||||
|
||||
+4
-4
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
'locale' => env('APP_LOCALE', 'es'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('appointments', function (Blueprint $table) {
|
||||
// Repuestos estimados (texto simple por ahora)
|
||||
$table->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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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,7 +60,7 @@ 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
|
||||
@@ -77,5 +77,7 @@ class DatabaseSeeder extends Seeder
|
||||
'problem_description' => 'Service completo y ajuste de cambios',
|
||||
'status' => 'pending'
|
||||
]);
|
||||
|
||||
$this->call(ProductSeeder::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\Product;
|
||||
|
||||
class ProductSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Productos del Excel
|
||||
$products = [
|
||||
// 1. BICICLETAS
|
||||
[
|
||||
'name' => '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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
<x-layout title="Catalogo - Lauck">
|
||||
|
||||
<!-- Header -->
|
||||
<x-section-header subtitle="Inicio" title="Catalogo de " highlight="Productos"/>
|
||||
|
||||
<div class="w-full max-w-5xl">
|
||||
|
||||
@if ($products->count() > 0)
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
@foreach ($products as $prod)
|
||||
<div class="group p-4 border border-neutral-800 rounded-xl hover:border-neon-lime hover:-translate-y-1 transition-all duration-300">
|
||||
{{-- Imagen si existe --}}
|
||||
@if (!empty($prod->image_path))
|
||||
<img src="{{ asset('storage/' . $prod->image_path) }}" alt="{{ $prod->name }}"
|
||||
class="w-full h-40 object-cover rounded mb-4">
|
||||
@else
|
||||
<div class="w-full border border-neutral-800 h-40 bg-stone-900 flex items-center justify-center rounded mb-4">
|
||||
<span class="text-neon-lime text-md uppercase font-bold">Sin imagen</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Nombre --}}
|
||||
<h2 class="text-lg font-semibold text-white">{{ $prod->name }}</h2>
|
||||
|
||||
{{-- Descripción corta --}}
|
||||
<div class="h-10 overflow-y-auto [scrollbar-width:none]">
|
||||
@if (!empty($prod->description))
|
||||
<p class="text-sm text-gray-400 mt-2">{{ $prod->description }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Precio --}}
|
||||
@if (!empty($prod->price))
|
||||
<p class="text-white group-hover:text-neon-lime font-bold mt-3 transition-colors">${{ number_format($prod->price, 2, ',', '.') }}</p>
|
||||
@endif
|
||||
|
||||
{{-- Botón --}}
|
||||
<a href="{{ route('productos.show', $prod) }}"
|
||||
class="mt-4 block w-full bg-neon-lime/80 uppercase text-black font-semibold py-2 rounded-lg text-center shadow-md shadow-neon-lime/10 hover:bg-neutral-900/70 border border-transparent hover:text-neon-lime hover:shadow-neon-lime/20 hover:border-neutral-800 transition-all">
|
||||
Ver +
|
||||
</a>
|
||||
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-center text-gray-600 mt-10">No hay productos disponibles.</p>
|
||||
@endif
|
||||
|
||||
<div class="mt-3">
|
||||
{{ $products->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</x-layout>
|
||||
@@ -0,0 +1,54 @@
|
||||
<x-layout title="Catalogo - Lauck">
|
||||
|
||||
<!-- Header -->
|
||||
<x-section-header subtitle="Inicio" title="Catalogo de " highlight="Productos"/>
|
||||
|
||||
<div class="w-full max-w-5xl">
|
||||
|
||||
@if ($products->count() > 0)
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@foreach ($products as $product)
|
||||
<div class="group p-4 border bg-stone-200/30 dark:bg-neutral-950/30 border-neutral-200 dark:border-neutral-800 rounded-xl hover:border-lime-500 hover:dark:border-neon-lime hover:-translate-y-1 transition-all duration-300">
|
||||
{{-- Imagen si existe --}}
|
||||
@if (!empty($product->image_path))
|
||||
<img src="{{ asset('storage/' . $product->image_path) }}" alt="{{ $product->name }}"
|
||||
class="w-full h-40 object-cover rounded mb-4">
|
||||
@else
|
||||
<div class="w-full border border-neutral-200 dark:border-neutral-800 h-40 bg-stone-200 dark:bg-stone-900 flex items-center justify-center rounded mb-4">
|
||||
<span class="text-lime-800 dark:text-neon-lime text-md uppercase font-bold">Sin imagen</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Nombre --}}
|
||||
<h2 class="text-lg font-semibold text-black dark:text-white">{{ $product->name }}</h2>
|
||||
|
||||
{{-- Descripción corta --}}
|
||||
<div class="h-10 overflow-y-auto [scrollbar-width:none]">
|
||||
@if (!empty($product->description))
|
||||
<p class="text-sm text-gray-700 dark:text-gray-400 mt-2">{{ $product->description }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Precio --}}
|
||||
@if (!empty($product->price))
|
||||
<p class="text-neutral-900 dark:text-white group-hover:text-lime-700 group-hover:dark:text-neon-lime font-bold mt-3 transition-colors">${{ number_format($product->price, 2, ',', '.') }}</p>
|
||||
@endif
|
||||
|
||||
{{-- Botón --}}
|
||||
<a href="{{ route('catalogo.show', $product) }}"
|
||||
class="mt-4 block w-full bg-neutral-900 dark:bg-neon-lime/80 uppercase text-neon-lime dark:text-black font-semibold py-2 rounded-lg text-center shadow-md shadow-gray-900/10 dark:shadow-neon-lime/10 hover:bg-neon-lime hover:dark:bg-neutral-900/70 border border-transparent hover:text-black hover:dark:text-neon-lime hover:border-black hover:dark:border-neon-lime transition-all">
|
||||
Ver +
|
||||
</a>
|
||||
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-center text-gray-400 dark:text-gray-600 mt-10">No hay productos disponibles.</p>
|
||||
@endif
|
||||
|
||||
<div class="mt-3">
|
||||
{{ $products->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</x-layout>
|
||||
@@ -0,0 +1,111 @@
|
||||
<x-layout title="{{ $product->name }} - Lauck">
|
||||
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
|
||||
<!-- Botón Volver -->
|
||||
<a href="{{ route('catalogo.index') }}" class="inline-flex items-center text-gray-700 dark:text-gray-400 hover:text-black hover:dark:text-white mb-6 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||||
Volver al Catálogo
|
||||
</a>
|
||||
|
||||
<div class="bg-slate-100 dark:bg-panel-bg rounded-xl shadow-2xl overflow-hidden border border-neutral-300 dark:border-neutral-800">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2">
|
||||
|
||||
<!-- PANEL IZQUIERDO -->
|
||||
<div class="bg-zinc-300 dark:bg-neutral-800 relative h-96 md:h-auto min-h-[500px] flex items-center justify-center py-4 group">
|
||||
|
||||
@if($product->image_path)
|
||||
<img src="{{ asset('storage/' . $product->image_path) }}"
|
||||
alt="{{ $product->name }}"
|
||||
class="w-full h-full object-contain max-h-[500px] transform drop-shadow-lg">
|
||||
@else
|
||||
<div class="text-neutral-400 dark:text-neutral-700 flex flex-col items-center">
|
||||
<svg class="w-48 h-48" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="0.5" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path></svg>
|
||||
<span class="mt-4 text-gray-500 text-sm uppercase tracking-widest">Sin Imagen</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Badge de Tipo Flotante -->
|
||||
<div class="absolute top-6 left-6">
|
||||
<span class="bg-white/50 dark:bg-black/50 backdrop-blur text-black dark:text-white px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wide border border-black/40 dark:border-white/10">
|
||||
@if($product->type == 'bike')
|
||||
Bicicleta
|
||||
@elseif($product->type == 'clothing')
|
||||
Indumentaria
|
||||
@elseif($product->type == 'spare')
|
||||
Repuesto
|
||||
@else
|
||||
Accesorio
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- PANEL DERECHO -->
|
||||
<div class="p-8 md:p-12 flex flex-col justify-center bg-neutral-200 dark:bg-panel-bg">
|
||||
|
||||
<!-- Encabezado -->
|
||||
<div class="mb-6 border-b border-neutral-400 dark:border-neutral-700 pb-6">
|
||||
<p class="text-stone-500 dark:text-neon-lime text-sm font-bold uppercase tracking-widest mb-2">SKU: {{ $product->sku ?? 'N/A' }}</p>
|
||||
<h1 class="text-4xl font-black text-black dark:text-white italic mb-2 leading-tight">{{ $product->name }}</h1>
|
||||
|
||||
<!-- Stock Status -->
|
||||
<div class="flex items-center gap-2 mt-4">
|
||||
@if($product->stock_quantity > 5)
|
||||
<span class="w-3 h-3 bg-green-600 dark:bg-green-400 rounded-full animate-pulse"></span>
|
||||
<span class="text-green-700 dark:text-green-400 text-sm font-bold">En Stock ({{ $product->stock_quantity }} unid.)</span>
|
||||
@elseif($product->stock_quantity > 0)
|
||||
<span class="w-3 h-3 bg-yellow-600 dark:bg-yellow-400 rounded-full animate-pulse"></span>
|
||||
<span class="text-yellow-900 dark:text-yellow-400 text-sm font-bold">¡Últimas Unidades! ({{ $product->stock_quantity }})</span>
|
||||
@else
|
||||
<span class="w-3 h-3 bg-red-500 rounded-full"></span>
|
||||
<span class="text-red-700 dark:text-red-400 text-sm font-bold">Agotado</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Precio -->
|
||||
<div class="mb-8">
|
||||
<span class="block text-gray-700 dark:text-gray-400 text-xs uppercase mb-1">Precio Contado</span>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="text-5xl font-black text-black dark:text-white">${{ number_format($product->price, 0, ',', '.') }}</span>
|
||||
<span class="text-gray-500 font-medium">ARG</span>
|
||||
</div>
|
||||
<p class="text-gray-500 text-xs mt-2">* Consultar financiación en el local.</p>
|
||||
</div>
|
||||
|
||||
<!-- Descripción -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-black dark:text-white font-bold uppercase text-sm mb-3">Descripción</h3>
|
||||
<p class="text-gray-700 dark:text-gray-400 leading-relaxed">
|
||||
{{ $product->description ?? 'Sin descripción detallada disponible para este producto. Por favor, acérquese al local para más información.' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Botones de Acción -->
|
||||
<div class="mt-auto grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<button class="bg-neutral-900 dark:bg-neon-lime text-neon-lime dark:text-neutral-900 font-black py-4 px-6 rounded-lg hover:bg-neutral-800 hover:dark:bg-[#b3e600] transition-all uppercase tracking-wide shadow-lg shadow-black/20 dark:shadow-neon-lime/20 flex items-center justify-center gap-2 transform hover:-translate-y-1">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path></svg>
|
||||
Consultar
|
||||
</button>
|
||||
|
||||
<a href="https://wa.me/3434718739?text=Hola,%20me%20interesa%20el%20producto:%20{{ $product->name }}" target="_blank" class="bg-green-500 dark:bg-neutral-800 text-white font-bold py-4 px-6 rounded-lg hover:bg-green-600 hover:dark:bg-neutral-700 border border-white dark:border-neutral-700 transition-all uppercase tracking-wide flex items-center justify-center gap-2">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.008-.57-.008-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"/></svg>
|
||||
WhatsApp
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-layout>
|
||||
{{-- <a href="{{ route('productos.edit', $product ) }}" class="w-full sm:w-auto px-5 py-2 rounded-full text-white font-semibold bg-blue-600 hover:bg-blue-700">Editar</a>
|
||||
<form action="{{route('productos.destroy',$product)}}" method="post">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="w-full sm:w-auto px-5 py-2 rounded-full text-white font-semibold bg-red-600 hover:bg-red-700">
|
||||
Borrar
|
||||
</button>
|
||||
</form> --}}
|
||||
@@ -1,3 +1,6 @@
|
||||
|
||||
{{--
|
||||
DEPRECADO: Este componente ha sido reemplazado por layout.blade.php para una mejor organización.
|
||||
@props(['auth' => false])
|
||||
|
||||
<!DOCTYPE html>
|
||||
@@ -120,4 +123,4 @@
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html> --}}
|
||||
@@ -1,16 +1,16 @@
|
||||
<footer class="bg-neutral-900 border-t border-white/5 mt-auto z-10 w-full">
|
||||
<footer class="bg-slate-100 dark:bg-neutral-900 border-t border-black/5 dark:border-white/5 mt-auto z-10 w-full">
|
||||
<div class="w-full max-w-7xl mx-auto p-8">
|
||||
<div class="sm:flex sm:items-center sm:justify-between">
|
||||
<a href="{{ url('/') }}" class="flex items-center mb-4 sm:mb-0 space-x-3 rtl:space-x-reverse grayscale opacity-70 hover:grayscale-0 hover:opacity-100 transition-all">
|
||||
<svg class="h-8 text-neon-lime" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7l10 5 10-5-10-5zm0 9l2.5-1.25L12 8.5l-2.5 1.25L12 11zm0 2.5l-5-2.5-5 2.5L12 22l10-8.5-5-2.5-5 2.5z"/></svg>
|
||||
<span class="self-center text-xl font-bold whitespace-nowrap text-white">Cicles Lauck</span>
|
||||
<svg class="h-8 text-lime-600 dark:text-neon-lime" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7l10 5 10-5-10-5zm0 9l2.5-1.25L12 8.5l-2.5 1.25L12 11zm0 2.5l-5-2.5-5 2.5L12 22l10-8.5-5-2.5-5 2.5z"/></svg>
|
||||
<span class="self-center text-xl font-bold whitespace-nowrap dark:text-white">Cicles Lauck</span>
|
||||
</a>
|
||||
<ul class="flex flex-wrap items-center mb-6 text-sm font-medium text-gray-500 sm:mb-0">
|
||||
<li><a href="#" class="hover:text-neon-lime hover:underline me-4 md:me-6 transition-colors">Soporte</a></li>
|
||||
<li><a href="#" class="hover:text-neon-lime hover:underline transition-colors">Contacto</a></li>
|
||||
<li><a href="#" class="hover:text-lime-600 hover:dark:text-neon-lime hover:underline me-4 md:me-6 transition-colors">Soporte</a></li>
|
||||
<li><a href="#" class="hover:text-lime-600 hover:dark:text-neon-lime hover:underline transition-colors">Contacto</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr class="my-6 border-neutral-800 sm:mx-auto lg:my-8">
|
||||
<span class="block text-sm text-gray-600 sm:text-center">© {{ date('Y') }} <a href="#" class="hover:text-neon-lime transition-colors">Lauck Systems™</a>.</span>
|
||||
<hr class="my-6 border-neutral-200 dark:border-neutral-800 sm:mx-auto lg:my-8">
|
||||
<span class="block text-sm text-gray-600 sm:text-center">© {{ date('Y') }} <a href="#" class="hover:text-lime-700 hover:dark:text-neon-lime transition-colors">Lauck Systems™</a>.</span>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -1,6 +1,6 @@
|
||||
@props(['disabled' => false, 'error' => null])
|
||||
|
||||
<input {{ $disabled ? 'disabled' : ''}} {!! $attributes->merge(['class' => 'bg-neutral-800 border-neutral-700 text-white text-sm rounded-lg focus:ring-neon-lime focus:border-neon-lime block w-full p-2.5 placeholder-gray-500 transition-colors ' . ($error ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : '')]) !!}>
|
||||
<input {{ $disabled ? 'disabled' : ''}} {!! $attributes->merge(['class' => 'bg-gray-300 dark:bg-neutral-800 border border-neutral-400 dark:border-neutral-700 dark:text-white text-sm rounded-lg focus:ring-neon-lime focus:border-neon-lime block w-full p-2.5 placeholder-neutral-600 dark:placeholder-gray-400 transition-colors ' . ($error ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : '')]) !!}>
|
||||
|
||||
@if($error)
|
||||
<p class="mt-1 text-xs text-red-400">{{ $error }}</p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@props(['value'])
|
||||
|
||||
<label {!! $attributes->merge(['class' => 'block mb-2 text-xs font-bold text-gray-400 uppercase tracking-wider']) !!}>
|
||||
<label {!! $attributes->merge(['class' => 'block mb-2 text-xs font-bold text-gray-700 dark:text-gray-300 uppercase tracking-wider']) !!}>
|
||||
{{$value ?? $slot}}
|
||||
</label>
|
||||
|
||||
@@ -4,13 +4,33 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<script type="module">
|
||||
// Revisamos el almacenamiento local o las preferencias del sistema
|
||||
if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
// Función global para cambiar el tema desde cualquier botón
|
||||
window.toggleTheme = function() {
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<title>{{ $title ?? 'Lauck Dashboard' }}</title>
|
||||
@stack('styles')
|
||||
|
||||
<!-- VITE: Esto carga tu Tailwind compilado y JS -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
|
||||
<body class="bg-dark-bg text-gray-300 font-sans antialiased min-h-screen flex flex-col relative overflow-x-hidden">
|
||||
<body class="bg-gray-50 dark:bg-dark-bg text-gray-900 dark:text-gray-300 font-sans antialiased min-h-screen flex flex-col relative transition-colors overflow-x-hidden">
|
||||
|
||||
<!-- Fondo grilla global -->
|
||||
<div class="inset-0 bg-grid-pattern opacity-[0.15] z-0 pointer-events-none fixed"></div>
|
||||
@@ -34,6 +54,8 @@
|
||||
if(btn && menu) {
|
||||
btn.addEventListener('click', () => menu.classList.toggle('hidden'));
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
@stack('scripts')
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<nav class="bg-neutral-900/90 backdrop-blur-md border-b border-white/10 w-full z-50 sticky top-0">
|
||||
<nav class="bg-slate-100/90 dark:bg-neutral-900/90 backdrop-blur-md border-b border-black/10 dark:border-white/10 w-full z-50 sticky top-0">
|
||||
<div class="max-w-7xl flex flex-wrap items-center justify-between mx-auto p-4">
|
||||
|
||||
<!-- Logo -->
|
||||
@@ -8,11 +8,11 @@
|
||||
@guest
|
||||
<a href="{{ url('/') }}" class="flex items-center space-x-3 rtl:space-x-reverse group">
|
||||
@endguest
|
||||
<div class="h-10 w-10 bg-neutral-800 rounded-full flex items-center justify-center border-2 border-transparent group-hover:border-neon-lime transition-all duration-300">
|
||||
<svg class="w-6 h-6 text-white group-hover:text-neon-lime transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
||||
<div class="h-10 w-10 bg-slate-200 dark:bg-neutral-800 rounded-full flex items-center justify-center border-2 border-transparent group-hover:border-lime-700 group-hover:dark:border-neon-lime transition-all duration-300">
|
||||
<svg class="w-6 h-6 dark:text-white group-hover:text-lime-700 group-hover:dark:text-neon-lime transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
||||
</div>
|
||||
<span class="self-center text-2xl font-black whitespace-nowrap text-white italic tracking-wide group-hover:text-neon-lime transition-colors duration-300">
|
||||
LAUCK<span class="text-neon-lime font-light not-italic">SYS</span>
|
||||
<span class="self-center text-2xl font-black whitespace-nowrap dark:text-white italic tracking-wide group-hover:text-lime-700 group-hover:dark:text-neon-lime transition-colors duration-300">
|
||||
LAUCK<span class="text-lime-700 dark:text-neon-lime font-light not-italic">SYS</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -31,47 +31,54 @@
|
||||
@auth
|
||||
<li>
|
||||
<a href="{{ url('/dashboard') }}"
|
||||
class="block py-2 px-3 md:p-0 transition-colors {{ request()->is('dashboard') ? 'text-neon-lime border-b-2 border-neon-lime' : 'text-white hover:text-neon-lime' }}">
|
||||
Dashboard
|
||||
class="block py-2 px-3 md:p-0 transition-colors {{ request()->is('dashboard') ? 'text-lime-600 dark:text-neon-lime border-b-2 border-lime-600 dark:border-neon-lime' : 'dark:text-white hover:text-lime-600 hover:dark:text-neon-lime' }}">
|
||||
Inicio
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url('/productos') }}"
|
||||
class="block py-2 px-3 md:p-0 transition-colors {{ request()->is('productos*') ? 'text-neon-lime border-b-2 border-neon-lime' : 'text-white hover:text-neon-lime' }}">
|
||||
Stock
|
||||
class="block py-2 px-3 md:p-0 transition-colors {{ request()->is('productos*') ? 'text-lime-600 dark:text-neon-lime border-b-2 border-lime-600 dark:border-neon-lime' : 'dark:text-white hover:text-lime-600 hover:dark:text-neon-lime' }}">
|
||||
Stock
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block py-2 px-3 md:p-0 transition-colors {{ request()->is('mantenimiento*') ? 'text-neon-lime border-b-2 border-neon-lime' : 'text-white hover:text-neon-lime' }}">
|
||||
Taller
|
||||
<a href="{{ route('taller.index') }}"
|
||||
class="block py-2 px-3 md:p-0 transition-colors {{ request()->is('taller*') ? 'text-lime-600 dark:text-neon-lime border-b-2 border-lime-600 dark:border-neon-lime' : 'dark:text-white hover:text-lime-600 hover:dark:text-neon-lime' }}">
|
||||
Taller
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ route('sales.index') }}"
|
||||
class="block py-2 px-3 md:p-0 transition-colors {{ request()->is('sales*') ? 'text-neon-lime border-b-2 border-neon-lime' : 'text-white hover:text-neon-lime' }}">
|
||||
class="block py-2 px-3 md:p-0 transition-colors {{ request()->is('sales*') ? 'text-lime-600 dark:text-neon-lime border-b-2 border-lime-600 dark:border-neon-lime' : 'dark:text-white hover:text-lime-600 hover:dark:text-neon-lime' }}">
|
||||
Ventas
|
||||
</a>
|
||||
</li>
|
||||
@endauth
|
||||
|
||||
<button onclick="toggleTheme()" class="p-2 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors rounded-full hover:bg-gray-200 dark:hover:bg-neutral-800">
|
||||
<!-- Icono de Sol (Se muestra en modo oscuro) -->
|
||||
<svg class="w-5 h-5 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
|
||||
<!-- Icono de Luna (Se muestra en modo claro) -->
|
||||
<svg class="w-5 h-5 block dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg>
|
||||
</button>
|
||||
|
||||
{{-- LÓGICA DE VISITANTE (GUEST) --}}
|
||||
@guest
|
||||
<li>
|
||||
<a href="{{ url('/catalogo') }}"
|
||||
class="block py-2 px-3 md:p-0 transition-colors {{ request()->is('catalogo*') ? 'text-neon-lime border-b-2 border-neon-lime' : 'text-white hover:text-neon-lime' }}">
|
||||
class="block py-2 px-3 md:p-0 transition-colors {{ request()->is('catalogo*') ? 'text-lime-600 dark:text-neon-lime border-b-2 border-lime-600 dark:border-neon-lime' : 'dark:text-white hover:text-lime-600 hover:dark:text-neon-lime' }}">
|
||||
Catalogo
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Buscador Integrado -->
|
||||
<li class="w-full md:w-auto my-2 md:my-0">
|
||||
<form action="{{ route('catalogo') }}" method="GET">
|
||||
<form action="{{ route('catalogo.index') }}" method="GET">
|
||||
<div class="relative">
|
||||
<input type="text" name="search"
|
||||
class="block w-full md:w-48 p-2 ps-3 text-xs text-white border border-neutral-600 rounded-full bg-neutral-800/50 focus:ring-neon-lime focus:border-neon-lime placeholder-gray-500 transition-all focus:w-full md:focus:w-64"
|
||||
class="block w-full md:w-48 p-2 ps-3 text-xs dark:text-white border border-slate-400 dark:border-neutral-600 rounded-full bg-slate-300/50 dark:bg-neutral-800/50 focus:ring-neon-lime focus:border-neon-lime placeholder-gray-500 transition-all focus:w-full md:focus:w-64"
|
||||
placeholder="Buscar productos...">
|
||||
<button type="submit" class="absolute top-0 end-0 p-2 text-sm font-medium h-full text-white rounded-e-lg hover:text-neon-lime">
|
||||
<button type="submit" class="absolute top-0 end-0 p-2 text-sm font-medium h-full dark:text-white rounded-e-lg hover:text-lime-600 hover:dark:text-neon-lime">
|
||||
<svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
|
||||
</svg>
|
||||
@@ -83,8 +90,8 @@
|
||||
<!-- Botón Login Resaltado -->
|
||||
<li>
|
||||
<a href="{{ route('login') }}"
|
||||
class="block py-2 px-5 text-center text-neutral-900 bg-neon-lime font-black rounded hover:bg-[#b3e600] transition-colors uppercase tracking-wider text-xs shadow-lg shadow-neon-lime/20 hover:shadow-neon-lime/40 transform hover:-translate-y-0.5 duration-200">
|
||||
Ingresar
|
||||
class="block py-2 px-5 text-center text-neon-lime dark:text-neutral-900 bg-neutral-900 dark:bg-neon-lime font-black rounded hover:dark:bg-[#b3e600] transition-colors uppercase tracking-wider text-xs shadow-lg shadow-gray-300 dark:shadow-neon-lime/20 hover:dark:shadow-neon-lime/40 transform hover:-translate-y-0.5 duration-200">
|
||||
Ingresar
|
||||
</a>
|
||||
</li>
|
||||
@endguest
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
@props(['subtitle', 'title', 'highlight' => null])
|
||||
|
||||
<div class="w-full text-left mb-10 border-l-4 border-neon-lime pl-6 py-2">
|
||||
<h2 class="text-neutral-400 text-sm font-bold uppercase tracking-widest mb-1">{{ $subtitle }}</h2>
|
||||
<h1 class="font-black text-4xl md:text-5xl text-white uppercase italic">
|
||||
<div class="w-full text-left mb-10 border-l-4 border-lime-600 dark:border-neon-lime pl-6 py-2">
|
||||
<h2 class="text-neutral-600 dark:text-neutral-400 text-sm font-bold uppercase tracking-widest mb-1">{{ $subtitle }}</h2>
|
||||
<h1 class="font-black text-4xl md:text-5xl text-black dark:text-white uppercase italic">
|
||||
{{ $title }}
|
||||
@if($highlight)
|
||||
<span class="text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-500 pe-2">{{ $highlight }}</span>
|
||||
<span class="text-transparent bg-clip-text bg-gradient-to-r from-neutral-900 dark:from-white to-lime-500 dark:to-gray-500 pe-2">{{ $highlight }}</span>
|
||||
@endif
|
||||
</h1>
|
||||
</div>
|
||||
@@ -0,0 +1,72 @@
|
||||
@props(['job', 'color'])
|
||||
|
||||
@php
|
||||
$borderColor = match($color) {
|
||||
'red' => 'border-l-red-500',
|
||||
'yellow' => 'border-l-yellow-500',
|
||||
'neon' => 'border-l-neon-lime',
|
||||
default => 'border-l-gray-500'
|
||||
};
|
||||
@endphp
|
||||
|
||||
<div class="bg-panel-bg p-4 rounded-lg border border-neutral-700 border-l-4 {{ $borderColor }} shadow-lg group hover:bg-neutral-800 transition-colors">
|
||||
|
||||
<!-- Encabezado Tarjeta -->
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<span class="text-xs font-bold text-gray-400">{{ $job->client->name }}</span>
|
||||
<span class="text-xs font-mono text-gray-500">{{ $job->scheduled_at->format('d/m H:i') }}</span>
|
||||
</div>
|
||||
|
||||
<h4 class="text-white font-bold text-sm mb-1">{{ $job->bike_model }}</h4>
|
||||
<p class="text-gray-400 text-xs line-clamp-2 mb-3">{{ $job->problem_description }}</p>
|
||||
|
||||
@if($job->estimated_cost)
|
||||
<div class="text-right mb-3">
|
||||
<span class="text-xs text-gray-500">Presupuesto:</span>
|
||||
<span class="text-sm font-mono text-white font-bold">${{ number_format($job->estimated_cost, 0) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Controles de Estado -->
|
||||
<div class="flex justify-between items-center pt-2 border-t border-neutral-700 mt-2 opacity-100 md:opacity-50 group-hover:opacity-100 transition-opacity">
|
||||
|
||||
<!-- Botón Mover Atrás -->
|
||||
@if($job->status !== 'pending')
|
||||
<form action="{{ route('taller.updateStatus', $job) }}" method="POST">
|
||||
@csrf @method('PATCH')
|
||||
<input type="hidden" name="status" value="{{ $job->status === 'ready' ? 'in_progress' : 'pending' }}">
|
||||
<button class="text-gray-500 hover:text-white p-1" title="Volver al estado anterior">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<div></div> <!-- Espaciador -->
|
||||
@endif
|
||||
|
||||
<!-- Enlace a WhatsApp -->
|
||||
<a href="https://wa.me/{{ $job->contact_phone }}?text=Hola {{ $job->client->name }}, te escribimos de Lauck para avisarte sobre tu {{ $job->bike_model }}..." target="_blank" class="text-green-500 hover:text-green-400">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M17.472 14.382c..."></path></svg> <!-- (Usa el mismo SVG de whatsapp de antes) -->
|
||||
<span class="text-xs ml-1">Avisar</span>
|
||||
</a>
|
||||
|
||||
<!-- Botón Mover Adelante -->
|
||||
@if($job->status !== 'ready')
|
||||
<form action="{{ route('taller.updateStatus', $job) }}" method="POST">
|
||||
@csrf @method('PATCH')
|
||||
<input type="hidden" name="status" value="{{ $job->status === 'pending' ? 'in_progress' : 'ready' }}">
|
||||
<button class="text-neon-lime hover:text-white p-1" title="Avanzar estado">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||
</button>
|
||||
</form>
|
||||
@elseif($job->status === 'ready')
|
||||
<!-- Botón Entregar (Archivar) -->
|
||||
<form action="{{ route('taller.updateStatus', $job) }}" method="POST">
|
||||
@csrf @method('PATCH')
|
||||
<input type="hidden" name="status" value="delivered">
|
||||
<button class="text-xs bg-neon-lime text-black px-2 py-1 rounded font-bold hover:bg-white" title="Marcar como entregado">
|
||||
Entregar
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,11 +1,11 @@
|
||||
@if (session('success'))
|
||||
<div class="p-4 mb-4 text-sm text-green-400 rounded-lg bg-neutral-800 border border-green-800/50" role="alert">
|
||||
<div class="p-4 mb-4 text-md text-black rounded-lg bg-green-500 border border-green-800/50" role="alert">
|
||||
<span class="font-bold">¡Éxito!</span> {{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (session('error'))
|
||||
<div class="p-4 mb-4 text-sm text-red-400 rounded-lg bg-neutral-800 border border-red-800/50" role="alert">
|
||||
<div class="p-4 mb-4 text-md text-black rounded-lg bg-red-500 border border-red-800/50" role="alert">
|
||||
<span class="font-bold">Error:</span> {{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
@php
|
||||
$colors = [
|
||||
'gray' => 'bg-gray-700 text-gray-300',
|
||||
'red' => 'bg-red-900/50 text-red-300 border border-red-800',
|
||||
'green' => 'bg-green-900/50 text-green-300 border border-green-800',
|
||||
'yellow' => 'bg-yellow-900/50 text-yellow-300 border border-yellow-800',
|
||||
'neon' => 'bg-[#ccff00]/10 text-[#ccff00] border border-[#ccff00]/50',
|
||||
'gray' => 'bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-300',
|
||||
'red' => 'bg-red-200 dark:bg-red-900/50 text-red-800 dark:text-red-300 border border-red-500 dark:border-red-800',
|
||||
'blue' => 'bg-blue-200 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300 border border-blue-500 dark:border-blue-800',
|
||||
'green' => 'bg-green-200 dark:bg-green-900/50 text-green-800 dark:text-green-300 border border-green-500 dark:border-green-800',
|
||||
'yellow' => 'bg-yellow-200 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-300 border border-yellow-500 dark:border-yellow-800',
|
||||
'neon' => 'bg-lime-200 dark:bg-[#ccff00]/10 text-lime-800 dark:text-[#ccff00] border border-lime-700 dark:border-[#ccff00]/50',
|
||||
];
|
||||
$classes = $colors[$color] ?? $colors['gray'];
|
||||
@endphp
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
@props(['href' => '#', 'title', 'description', 'linkText' => 'Ver detalles'])
|
||||
|
||||
<a href="{{ $href }}" class="group relative block p-6 bg-panel-bg border border-neutral-800 rounded-xl hover:border-neon-lime hover:-translate-y-1 transition-all duration-300 shadow-lg hover:shadow-neon-lime/20 h-full">
|
||||
<a href="{{ $href }}" class="group relative block p-6 bg-white dark:bg-panel-bg border border-neutral-200 dark:border-neutral-800 rounded-xl hover:border-neutral-600 hover:dark:border-neon-lime hover:-translate-y-1 transition-all duration-300 shadow-lg hover:dark:shadow-neon-lime/20 h-full">
|
||||
<!-- slot para SVG -->
|
||||
<div class="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-100 transition-opacity">
|
||||
<div class="absolute top-0 right-0 p-4 opacity-30 dark:opacity-10 group-hover:opacity-100 transition-opacity">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
<h3 class="mt-4 text-xl font-bold text-white group-hover:text-neon-lime transition-colors">{{ $title }}</h3>
|
||||
<p class="mt-2 text-sm text-gray-500">{{ $description }}</p>
|
||||
<h3 class="mt-4 text-xl font-bold text-black dark:text-white group-hover:text-lime-500 group-hover:dark:text-neon-lime transition-colors">{{ $title }}</h3>
|
||||
<p class="mt-2 text-sm text-gray-700 dark:text-gray-500">{{ $description }}</p>
|
||||
|
||||
<div class="mt-6 inline-flex items-center text-xs font-bold text-neon-lime uppercase tracking-wider">
|
||||
<div class="mt-6 inline-flex items-center text-xs font-bold text-lime-500 dark:text-neon-lime uppercase tracking-wider">
|
||||
{{ $linkText }} <span class="ml-2 group-hover:translate-x-1 transition-transform">→</span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -1,10 +1,10 @@
|
||||
@props(['items'])
|
||||
|
||||
<div class="relative w-full overflow-hidden rounded-xl border border-neutral-800 bg-panel-bg shadow-xl group" id="lauck-carousel">
|
||||
<div class="relative w-full overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800 bg-slate-50 dark:bg-panel-bg shadow-xl group" id="lauck-carousel">
|
||||
|
||||
<!-- Título Flotante (Opcional) -->
|
||||
<div class="absolute top-4 left-6 z-10 bg-black/50 backdrop-blur-sm px-3 py-1 rounded border border-neon-lime/30">
|
||||
<span class="text-xs font-bold text-neon-lime uppercase tracking-widest">Destacados</span>
|
||||
<div class="absolute top-4 left-6 z-10 bg-neon-lime/50 dark:bg-black/50 backdrop-blur-sm px-3 py-1 rounded border border-black/30 dark:border-neon-lime/30">
|
||||
<span class="text-xs font-bold text-black dark:text-neon-lime uppercase tracking-widest">Destacados</span>
|
||||
</div>
|
||||
|
||||
<!-- Contenedor de Slides (Track) -->
|
||||
@@ -13,40 +13,40 @@
|
||||
<div class="w-full flex-shrink-0 flex flex-col md:flex-row h-full relative">
|
||||
|
||||
<!-- Imagen / Visual (Izquierda o Fondo) -->
|
||||
<div class="w-full md:w-1/2 bg-neutral-800 flex items-center justify-center relative overflow-hidden">
|
||||
<div class="w-full md:w-1/2 bg-neutral-200 dark:bg-neutral-800 flex items-center justify-center relative overflow-hidden">
|
||||
<!-- Decoración de fondo -->
|
||||
<div class="absolute inset-0 bg-grid-pattern opacity-10"></div>
|
||||
|
||||
<!-- Icono Gigante (Placeholder de Bici) -->
|
||||
<div class="text-neutral-700 transform group-hover:scale-110 transition-transform duration-700">
|
||||
<div class="text-neutral-300 dark:text-neutral-700 transform group-hover:scale-110 transition-transform duration-700">
|
||||
@if($item->image_path)
|
||||
<img src="{{ asset('storage/' . $item->image_path) }}" alt="{{ $item->name }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<!-- Placeholder si no tiene foto -->
|
||||
<div class="w-full h-full flex items-center justify-center bg-neutral-800">
|
||||
<div class="w-full h-full flex items-center justify-center bg-neutral-200 dark:bg-neutral-800">
|
||||
<svg class="w-48 h-48" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="0.5" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path></svg>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Precio Flotante -->
|
||||
<div class="absolute bottom-4 left-4 bg-neon-lime text-neutral-900 font-black px-4 py-2 rounded-lg text-xl shadow-lg shadow-neon-lime/20">
|
||||
<div class="absolute bottom-4 left-4 bg-neutral-900 dark:bg-neon-lime text-neon-lime dark:text-neutral-900 font-black px-4 py-2 rounded-lg text-xl shadow-lg shadow-gray-800/10 dark:shadow-neon-lime/20">
|
||||
${{ number_format($item->price, 0, ',', '.') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info (Derecha) -->
|
||||
<div class="w-full md:w-1/2 p-8 md:p-12 flex flex-col justify-center bg-gradient-to-br from-panel-bg to-neutral-900">
|
||||
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-2">{{ $item->sku }}</h3>
|
||||
<h2 class="text-3xl md:text-4xl font-black text-white italic mb-4 leading-tight">
|
||||
<div class="w-full md:w-1/2 p-8 md:p-12 flex flex-col justify-center bg-gradient-to-br from-gray-200 dark:from-panel-bg to-neutral-100 dark:to-neutral-900">
|
||||
<h3 class="text-sm font-bold text-gray-600 dark:text-gray-400 uppercase tracking-widest mb-2">{{ $item->sku }}</h3>
|
||||
<h2 class="text-3xl md:text-4xl font-black text-black dark:text-white italic mb-4 leading-tight">
|
||||
{{ $item->name }}
|
||||
</h2>
|
||||
<p class="text-gray-400 text-sm md:text-base mb-8 line-clamp-3">
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm md:text-base mb-8 line-clamp-3">
|
||||
{{ $item->description ?? 'Una bicicleta diseñada para el máximo rendimiento en todo terreno. Consultar especificaciones técnicas en el catálogo.' }}
|
||||
</p>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<a href="{{ route('catalogo', ['search' => $item->sku]) }}" class="px-6 py-3 bg-white text-neutral-900 font-bold uppercase tracking-wider rounded hover:bg-gray-200 transition-colors">
|
||||
<a href="{{ route('catalogo.show', $item) }}" class="px-6 py-3 bg-black dark:bg-white text-neutral-100 dark:text-neutral-900 font-bold uppercase tracking-wider rounded hover:bg-neutral-800 hover:dark:bg-gray-200 transition-colors">
|
||||
Ver Detalles
|
||||
</a>
|
||||
</div>
|
||||
@@ -54,7 +54,7 @@
|
||||
</div>
|
||||
@empty
|
||||
<!-- Slide de Fallback por si no hay datos -->
|
||||
<div class="w-full flex-shrink-0 flex items-center justify-center h-full bg-neutral-800 text-gray-500">
|
||||
<div class="w-full flex-shrink-0 flex items-center justify-center h-full bg-neutral-200 dark:bg-neutral-800 text-gray-500">
|
||||
<p>No hay productos destacados disponibles.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
@@ -72,7 +72,7 @@
|
||||
<!-- Indicadores (Puntos) -->
|
||||
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex space-x-2 z-20">
|
||||
@foreach($items as $index => $item)
|
||||
<button class="carousel-dot w-3 h-3 rounded-full bg-white/20 hover:bg-neon-lime transition-all" data-index="{{ $index }}"></button>
|
||||
<button class="carousel-dot w-3 h-3 rounded-full bg-black/20 dark:bg-white/20 hover:bg-lime-500 hover:dark:bg-neon-lime transition-all" data-index="{{ $index }}"></button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@@ -91,8 +91,8 @@
|
||||
$track.css('transform', `translateX(${translateX}%)`);
|
||||
|
||||
// Actualizar puntos
|
||||
$('.carousel-dot').removeClass('bg-neon-lime scale-125').addClass('bg-white/20');
|
||||
$(`.carousel-dot[data-index="${currentIndex}"]`).addClass('bg-neon-lime scale-125').removeClass('bg-white/20');
|
||||
$('.carousel-dot').removeClass('bg-lime-500 dark:bg-neon-lime scale-125').addClass('bg-black/20 dark:bg-white/20');
|
||||
$(`.carousel-dot[data-index="${currentIndex}"]`).addClass('bg-lime-500 dark:bg-neon-lime scale-125').removeClass('bg-black/20 dark:bg-white/20');
|
||||
}
|
||||
|
||||
function nextSlide() {
|
||||
|
||||
@@ -9,30 +9,49 @@
|
||||
|
||||
<!-- Grid de Tarjetas -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 w-full mb-12">
|
||||
|
||||
<!-- Tarjeta Catálogo -->
|
||||
<x-ui.card href="{{ url('/catalogo') }}" title="Catálogo" description="Administrar bicicletas y repuestos." linkText="Ir al stock">
|
||||
<svg class="w-12 h-12 text-neon-lime" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" /></svg>
|
||||
</x-ui.card>
|
||||
|
||||
<!-- Tarjeta Taller -->
|
||||
<x-ui.card href="#" title="Taller" description="Gestión de services y reparaciones." linkText="Ver agenda">
|
||||
<svg class="w-12 h-12 text-neon-lime" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||
<x-ui.card href="{{ url('/catalogo') }}" title="Catálogo" description="Ver productos." linkText="Ir al catalogo">
|
||||
<svg class="w-12 h-12 text-lime-500 dark:text-neon-lime" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" /></svg>
|
||||
</x-ui.card>
|
||||
|
||||
<!-- Tarjeta Clientes -->
|
||||
<x-ui.card href="{{ url('/clients') }}" title="Clientes" description="Base de datos de ciclistas." linkText="Buscar usuario">
|
||||
<svg class="w-12 h-12 text-neon-lime" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
|
||||
<x-ui.card href="{{ url('/clients') }}" title="Clientes" description="Base de datos de ciclistas." linkText="Ver Clientes">
|
||||
<svg class="w-12 h-12 text-lime-500 dark:text-neon-lime" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
|
||||
</x-ui.card>
|
||||
|
||||
<!-- Tarjeta Ventas -->
|
||||
<x-ui.card href="{{ url('/sales') }}" title="Ventas" description="Registro de Ventas" linkText="Ver Ventas">
|
||||
<svg class="w-14 h-14 text-lime-500 dark:text-neon-lime" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.6 16.733c.234.269.548.456.895.534a1.4 1.4 0 0 0 1.75-.762c.172-.615-.446-1.287-1.242-1.481-.796-.194-1.41-.861-1.241-1.481a1.4 1.4 0 0 1 1.75-.762c.343.077.654.26.888.524m-1.358 4.017v.617m0-5.939v.725M4 15v4m3-6v6M6 8.5 10.5 5 14 7.5 18 4m0 0h-3.5M18 4v3m2 8a5 5 0 1 1-10 0 5 5 0 0 1 10 0Z"></path>
|
||||
</svg>
|
||||
</x-ui.card>
|
||||
|
||||
<!-- Tarjeta Taller -->
|
||||
<x-ui.card href="{{ url('/taller') }}" title="Taller" description="Gestión de services y reparaciones." linkText="Ver agenda">
|
||||
<svg class="w-12 h-12 text-lime-500 dark:text-neon-lime" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linejoin="round" stroke-width="1.5" d="M7.58209 8.96025 9.8136 11.1917l-1.61782 1.6178c-1.08305-.1811-2.23623.1454-3.07364.9828-1.1208 1.1208-1.32697 2.8069-.62368 4.1363.14842.2806.42122.474.73509.5213.06726.0101.1347.0133.20136.0098-.00351.0666-.00036.1341.00977.2013.04724.3139.24069.5867.52125.7351 1.32944.7033 3.01552.4971 4.13627-.6237.8375-.8374 1.1639-1.9906.9829-3.0736l4.8107-4.8108c1.0831.1811 2.2363-.1454 3.0737-.9828 1.1208-1.1208 1.3269-2.80688.6237-4.13632-.1485-.28056-.4213-.474-.7351-.52125-.0673-.01012-.1347-.01327-.2014-.00977.0035-.06666.0004-.13409-.0098-.20136-.0472-.31386-.2406-.58666-.5212-.73508-1.3294-.70329-3.0155-.49713-4.1363.62367-.8374.83741-1.1639 1.9906-.9828 3.07365l-1.7788 1.77875-2.23152-2.23148-1.41419 1.41424Zm1.31056-3.1394c-.04235-.32684-.24303-.61183-.53647-.76186l-1.98183-1.0133c-.38619-.19746-.85564-.12345-1.16234.18326l-.86321.8632c-.3067.3067-.38072.77616-.18326 1.16235l1.0133 1.98182c.15004.29345.43503.49412.76187.53647l1.1127.14418c.3076.03985.61628-.06528.8356-.28461l.86321-.8632c.21932-.21932.32446-.52801.2846-.83561l-.14417-1.1127ZM19.4448 16.4052l-3.1186-3.1187c-.7811-.781-2.0474-.781-2.8285 0l-.1719.172c-.7811.781-.7811 2.0474 0 2.8284l3.1186 3.1187c.7811.781 2.0474.781 2.8285 0l.1719-.172c.7811-.781.7811-2.0474 0-2.8284Z"/>
|
||||
</svg>
|
||||
</x-ui.card>
|
||||
|
||||
<!-- Tarjeta Productos -->
|
||||
<x-ui.card href="{{ url('/productos') }}" title="Stock" description="Administrar bicicletas y repuestos." linkText="Ir al stock">
|
||||
<svg class="w-12 h-12 text-lime-500 dark:text-neon-lime" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" /></svg>
|
||||
</x-ui.card>
|
||||
|
||||
<!-- Tarjeta Proveedores -->
|
||||
<x-ui.card href="#" title="Proveedores" description="Base de datos de proveedores." linkText="Ver Proveedores">
|
||||
<svg class="w-12 h-12 text-lime-500 dark:text-neon-lime" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
</x-ui.card>
|
||||
|
||||
<!-- Tarjeta Compras -->
|
||||
<x-ui.card href="#" title="Gastos" description="Registro de compras y pedidos a proovedores." linkText="Ver Gastos">
|
||||
<svg class="w-12 h-12 text-lime-500 dark:text-neon-lime" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 4h3a1 1 0 0 1 1 1v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h3m0 3h6m-3 5h3m-6 0h.01M12 16h3m-6 0h.01M10 3v4h4V3h-4Z"/>
|
||||
</svg>
|
||||
</x-ui.card>
|
||||
|
||||
<!-- Estadísticas (Sin template) -->
|
||||
<div class="p-6 bg-gradient-to-br from-neutral-800 to-neutral-900 border border-neutral-700 rounded-xl flex flex-col justify-center items-center text-center">
|
||||
<span class="text-4xl font-black text-white">85%</span>
|
||||
<span class="text-xs uppercase tracking-widest text-gray-500 mt-1">Eficiencia Taller</span>
|
||||
<div class="w-full bg-gray-700 rounded-full h-1.5 mt-4">
|
||||
<div class="bg-neon-lime h-1.5 rounded-full" style="width: 85%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botón de Logout -->
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
/>
|
||||
-->
|
||||
|
||||
<div class="bg-zinc-900 p-8 rounded-xl shadow-lg w-full max-w-md border-2 border-gray-500">
|
||||
<div class="bg-panel-bg p-8 rounded-xl shadow-lg w-full max-w-md border border-neutral-700">
|
||||
<form action="{{ route('login.attempt') }}" method="POST" class="space-y-5">
|
||||
@csrf
|
||||
{{-- No muestra los errores, ver --}}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<x-section-header subtitle="Inventario" title="Nuevo " highlight="Producto" />
|
||||
|
||||
<div class="w-full max-w-4xl mx-auto bg-panel-bg border border-neutral-800 rounded-xl p-8 shadow-lg">
|
||||
<div class="w-full max-w-4xl mx-auto bg-gray-200 dark:bg-panel-bg border border-neutral-400 dark:border-neutral-800 rounded-xl p-8 shadow-lg">
|
||||
|
||||
<form action="{{ route('productos.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@@ -25,10 +25,11 @@
|
||||
<!-- Tipo -->
|
||||
<div class="md:col-span-2">
|
||||
<x-forms.label for="type" value="Tipo de Producto" />
|
||||
<select id="type" name="type" class="bg-neutral-800 border-neutral-700 text-white text-sm rounded-lg focus:ring-neon-lime focus:border-neon-lime block w-full p-2.5">
|
||||
<option value="accessory" {{ old('type') == 'accessory' ? 'selected' : '' }}>Accesorio / Repuesto</option>
|
||||
<select id="type" name="type" class="bg-gray-300 dark:bg-neutral-800 border border-neutral-400 dark:border-neutral-700 dark:text-white text-sm rounded-lg focus:ring-neon-lime focus:border-neon-lime block w-full p-2.5">
|
||||
<option value="accessory" {{ old('type') == 'accessory' ? 'selected' : '' }}>Accesorio</option>
|
||||
<option value="bike" {{ old('type') == 'bike' ? 'selected' : '' }}>Bicicleta</option>
|
||||
<option value="service" {{ old('type') == 'service' ? 'selected' : '' }}>Servicio / Mano de Obra</option>
|
||||
<option value="clothing" {{ old('type') == 'clothing' ? 'selected' : '' }}>Indumentaria</option>
|
||||
<option value="spare" {{ old('type') == 'spare' ? 'selected' : '' }}>Repuesto</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -57,22 +58,22 @@
|
||||
<!-- Descripción (2 columnas) -->
|
||||
<div class="md:col-span-4">
|
||||
<x-forms.label for="description" value="Descripción / Notas" />
|
||||
<textarea id="description" name="description" rows="3" class="bg-neutral-800 border-neutral-700 text-white text-sm rounded-lg focus:ring-neon-lime focus:border-neon-lime block w-full p-2.5 placeholder-gray-500">{{ old('description') }}</textarea>
|
||||
<textarea id="description" name="description" rows="3" class="bg-gray-300 dark:bg-neutral-800 border border-neutral-400 dark:border-neutral-700 dark:text-white text-sm rounded-lg focus:ring-neon-lime focus:border-neon-lime block w-full p-2.5 placeholder-neutral-600 dark:placeholder-gray-400">{{ old('description') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<x-forms.label for="image" value="Imagen del Producto" />
|
||||
<x-forms.input type="file" name="image" class="block w-full text-sm text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-full
|
||||
file:border-0 file:text-sm file:font-semibold file:bg-neon-lime file:text-neutral-900 hover:file:bg-[#b3e600]
|
||||
<x-forms.input type="file" name="image" class="block w-full text-sm dark:text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-full
|
||||
file:border-0 file:text-sm file:font-semibold file:bg-lime-500 dark:file:bg-neon-lime file:text-neutral-900 hover:file:bg-[#b3e600]
|
||||
" :error="$errors->first('image')"/>
|
||||
{{-- @error('image') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror --}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botones Acción -->
|
||||
<div class="flex items-center justify-between md:justify-end space-x-4 border-t border-neutral-800 pt-6">
|
||||
<a href="{{ route('productos.index') }}" class="text-gray-400 hover:text-white font-medium text-sm transition-colors">Cancelar</a>
|
||||
<button type="submit" class="px-6 py-2.5 bg-neon-lime text-neutral-900 font-bold rounded-lg hover:bg-[#b3e600] transition-colors shadow-lg shadow-neon-lime/20">
|
||||
<div class="flex items-center justify-between md:justify-end space-x-4 border-t border-neutral-400 dark:border-neutral-700 pt-6">
|
||||
<a href="{{ route('productos.index') }}" class="text-gray-700 dark:text-gray-400 hover:text-black hover:dark:text-white font-medium text-sm transition-colors">Cancelar</a>
|
||||
<button type="submit" class="px-6 py-2.5 bg-neutral-900 dark:bg-neon-lime text-neon-lime dark:text-neutral-900 font-bold rounded-lg hover:bg-neutral-800 hover:dark:bg-[#b3e600] transition-colors shadow-lg dark:shadow-neon-lime/20">
|
||||
Guardar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,59 +1,3 @@
|
||||
{{-- <x-appc>
|
||||
<x-slot name="title">Lauck - Editar</x-slot>
|
||||
<x-slot name="navTitle">Editar - {{$product->nombre}}</x-slot>
|
||||
|
||||
<div class="w-full h-fit flex flex-col justify-start items-center gap-3 p-18">
|
||||
<form class="w-lg mx-auto" action="{{route('productos.update',$product)}}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<div class="relative z-0 w-full mb-5 group">
|
||||
<input type="text" value="{{$product->nombre}}" name="nombre" id="nombre" class="block py-2.5 px-0 w-full text-sm text-gray-300 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"/>
|
||||
<label for="nombre" class="peer-focus:font-medium absolute text-sm text-gray-300 dark:text-gray-300 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Titulo</label>
|
||||
</div>
|
||||
<div class="relative z-0 w-full mb-5 group">
|
||||
<input type="text" value="{{$product->marca}}" name="marca" id="marca" class="block py-2.5 px-0 w-full text-sm text-gray-300 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"/>
|
||||
<label for="marca" class="peer-focus:font-medium absolute text-sm text-gray-300 dark:text-gray-300 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Marca</label>
|
||||
</div>
|
||||
<div class="relative z-0 w-full mb-5 group">
|
||||
<input type="text" value="{{$product->modelo}}" name="modelo" id="modelo" class="block py-2.5 px-0 w-full text-sm text-gray-300 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"/>
|
||||
<label for="modelo" class="peer-focus:font-medium absolute text-sm text-gray-300 dark:text-gray-300 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Modelo</label>
|
||||
</div>
|
||||
<div class="relative z-0 w-full mb-5 group">
|
||||
<textarea type="text" name="descripcion" id="descripcion" class="block py-2.5 px-0 w-full text-sm text-gray-300 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer">{{$product->descripcion}}</textarea>
|
||||
<label for="descripcion" class="peer-focus:font-medium absolute text-sm text-gray-300 dark:text-gray-300 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Descripcion</label>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 md:gap-6">
|
||||
<div class="relative z-0 w-full mb-5 group">
|
||||
<input type="number" value="{{$product->rodado}}" name="rodado" id="rodado" class="block py-2.5 px-0 w-full text-sm text-gray-300 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"/>
|
||||
<label for="rodado" class="peer-focus:font-medium absolute text-sm text-gray-300 dark:text-gray-300 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Rodado</label>
|
||||
</div>
|
||||
<div class="relative z-0 w-full mb-5 group">
|
||||
<input type="text" value="{{$product->color}}" name="color" id="color"class="block py-2.5 px-0 w-full text-sm text-gray-300 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"/>
|
||||
<label for="color" class="peer-focus:font-medium absolute text-sm text-gray-300 dark:text-gray-300 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Color</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 md:gap-6">
|
||||
<div class="relative z-0 w-full mb-5 group">
|
||||
<input type="text" value="{{$product->tipo}}" name="tipo" id="tipo" class="block py-2.5 px-0 w-full text-sm text-gray-300 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"/>
|
||||
<label for="tipo" class="peer-focus:font-medium absolute text-sm text-gray-300 dark:text-gray-300 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Tipo</label>
|
||||
</div>
|
||||
<div class="relative z-0 w-full mb-5 group">
|
||||
<input type="number" value={{$product->precio}} name="precio" id="precio" class="block py-2.5 px-0 w-full text-sm text-gray-300 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer" step="0.01"/>
|
||||
<label for="precio" class="peer-focus:font-medium absolute text-sm text-gray-300 dark:text-gray-300 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Precio (Ej. $9999)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-5">
|
||||
<button type="submit" class="w-full sm:w-auto px-4 py-2 rounded-full text-white font-semibold bg-blue-700 hover:bg-blue-800 dark:bg-blue-700 dark:hover:bg-blue-800">
|
||||
Editar
|
||||
</button>
|
||||
<a href="{{route('productos.show',$product)}}" class="w-full sm:w-auto px-5 py-2 rounded-full text-white font-semibold bg-stone-500">
|
||||
Volver
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</x-appc> --}}
|
||||
<x-layout title="Lauck - Editar Producto">
|
||||
<x-section-header subtitle="Inventario" title="Edicion de " highlight="Producto" />
|
||||
|
||||
@@ -76,9 +20,10 @@
|
||||
<div class="md:col-span-2">
|
||||
<x-forms.label for="type" value="Tipo de Producto" />
|
||||
<select id="type" name="type" class="bg-neutral-800 border-neutral-700 text-white text-sm rounded-lg focus:ring-neon-lime focus:border-neon-lime block w-full p-2.5">
|
||||
<option value="accessory" {{ $product->type == 'accessory' ? 'selected' : '' }}>Accesorio / Repuesto</option>
|
||||
<option value="accessory" {{ $product->type == 'accessory' ? 'selected' : '' }}>Accesorio</option>
|
||||
<option value="bike" {{ $product->type == 'bike' ? 'selected' : '' }}>Bicicleta</option>
|
||||
<option value="service" {{ $product->type == 'service' ? 'selected' : '' }}>Servicio / Mano de Obra</option>
|
||||
<option value="clothing" {{ $product->type == 'clothing' ? 'selected' : '' }}>Indumentaria</option>
|
||||
<option value="spare" {{ $product->type == 'spare' ? 'selected' : '' }}>Repuesto</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Precios -->
|
||||
|
||||
@@ -12,36 +12,36 @@
|
||||
<form action="{{ route('productos.index') }}" method="GET" class="w-full lg:w-2/3 flex flex-col sm:flex-row gap-3">
|
||||
<div class="relative w-full sm:w-2/3">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||
<svg class="w-4 h-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<svg class="w-4 h-4 text-gray-600 dark:text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<input type="text" name="search" value="{{ request('search') }}"
|
||||
class="block w-full p-3 ps-10 text-sm text-white border border-neutral-700 rounded-lg bg-neutral-800 focus:ring-neon-lime focus:border-neon-lime placeholder-gray-500"
|
||||
class="block w-full p-3 ps-10 text-sm text-black dark:text-white border border-neutral-400 dark:border-neutral-700 rounded-lg bg-neutral-200 dark:bg-neutral-800 placeholder-gray-600 dark:placeholder-gray-500"
|
||||
placeholder="Buscar por nombre, SKU o marca...">
|
||||
</div>
|
||||
<!-- Filtro de Estado de Stock -->
|
||||
<div class="w-full sm:w-1/3">
|
||||
<select name="stock_status" onchange="this.form.submit()"
|
||||
class="block w-full p-3 text-sm text-white border border-neutral-700 rounded-lg bg-neutral-800 focus:ring-neon-lime focus:border-neon-lime cursor-pointer">
|
||||
class="block w-full p-3 text-sm text-black dark:text-white border border-neutral-300 dark:border-neutral-700 rounded-lg bg-neutral-200 dark:bg-neutral-800 focus:ring-neon-lime focus:border-neon-lime cursor-pointer">
|
||||
<option value="">Todos</option>
|
||||
<option value="low" {{ request('stock_status') == 'low' ? 'selected' : '' }} class="text-red-300">Stock en Alerta</option>
|
||||
<option value="medium" {{ request('stock_status') == 'medium' ? 'selected' : '' }} class="text-yellow-300">Stock Bajo</option>
|
||||
<option value="ok" {{ request('stock_status') == 'ok' ? 'selected' : '' }} class="text-green-300">Stock Normal</option>
|
||||
<option value="low" {{ request('stock_status') == 'low' ? 'selected' : '' }} class="text-red-600 dark:text-red-300">Stock en Alerta</option>
|
||||
<option value="medium" {{ request('stock_status') == 'medium' ? 'selected' : '' }} class="text-yellow-600 dark:text-yellow-300">Stock Bajo</option>
|
||||
<option value="ok" {{ request('stock_status') == 'ok' ? 'selected' : '' }} class="text-green-600 dark:text-green-300">Stock Normal</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- Botón Nuevo -->
|
||||
<a href="{{ route('productos.create') }}" class="w-full md:w-auto text-center px-5 py-3 text-sm font-bold text-neutral-900 bg-neon-lime rounded-lg hover:bg-[#b3e600] transition-colors uppercase tracking-wide">
|
||||
<a href="{{ route('productos.create') }}" class="w-full md:w-auto text-center px-5 py-3 text-sm font-bold text-neon-lime dark:text-neutral-900 bg-neutral-950 dark:bg-neon-lime rounded-lg hover:bg-neutral-900 hover:dark:bg-[#b3e600] transition-colors uppercase tracking-wide">
|
||||
+ Nuevo Producto
|
||||
</a>
|
||||
</div>
|
||||
<!-- Tabla de Productos -->
|
||||
<div class="relative overflow-x-auto shadow-md sm:rounded-lg border border-neutral-800 w-full">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-400">
|
||||
<thead class="text-xs text-gray-300 uppercase bg-neutral-800 border-b border-neutral-700">
|
||||
<div class="relative overflow-x-auto shadow-md sm:rounded-lg border border-gray-400 dark:border-neutral-800 w-full">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-neutral-900 dark:text-white">
|
||||
<thead class="text-xs text-neutral-800 dark:text-gray-300 uppercase bg-gray-400 dark:bg-neutral-800 border-b border-gray-300 dark:border-neutral-700">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">Producto / SKU</th>
|
||||
<th scope="col" class="px-6 py-3">Tipo</th>
|
||||
@@ -52,32 +52,31 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($products as $product)
|
||||
<tr class="bg-neutral-900/50 border-b border-neutral-800 hover:bg-neutral-800 transition-colors group">
|
||||
<td class="px-6 py-4 font-medium text-white whitespace-nowrap">
|
||||
<tr class="bg-gray-200/50 dark:bg-neutral-900/50 border-b border-gray-400 dark:border-neutral-800 hover:bg-gray-400 hover:dark:bg-neutral-800 transition-colors group">
|
||||
<td class="px-6 py-4 font-medium whitespace-nowrap">
|
||||
<div class="text-base font-bold">{{ $product->name }}</div>
|
||||
<div class="text-xs text-gray-500 font-mono">{{ $product->sku }}</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400 font-mono">{{ $product->sku }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
@if($product->type === 'bike') <x-ui.badge color="neon">Bicicleta</x-ui.badge>
|
||||
@elseif($product->type === 'accessory') <x-ui.badge color="gray">Accesorio</x-ui.badge>
|
||||
@else <x-ui.badge color="yellow">Servicio</x-ui.badge> @endif
|
||||
@elseif($product->type === 'clothing') <x-ui.badge color="blue">Indumentaria</x-ui.badge>
|
||||
@elseif($product->type === 'spare') <x-ui.badge color="gray">Repuesto</x-ui.badge>
|
||||
@else <x-ui.badge color="yellow">Accesorio</x-ui.badge> @endif
|
||||
</td>
|
||||
<td class="px-6 py-4 font-mono text-white">
|
||||
<td class="px-6 py-4 font-mono">
|
||||
${{ number_format($product->price, 2) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
@if($product->type === 'service')
|
||||
<span class="text-gray-600">-</span>
|
||||
@elseif($product->stock_quantity < $product->min_stock_alert)
|
||||
@if($product->stock_quantity < $product->min_stock_alert)
|
||||
<x-ui.badge color="red">{{ $product->stock_quantity }}</x-ui.badge>
|
||||
@elseif($product->stock_quantity <= $product->min_stock_alert+1)
|
||||
@elseif($product->stock_quantity <= $product->min_stock_alert+5)
|
||||
<x-ui.badge color="yellow">{{ $product->stock_quantity }}</x-ui.badge>
|
||||
@else
|
||||
<x-ui.badge color="green">{{ $product->stock_quantity }}</x-ui.badge>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<a href="{{ route('productos.edit', $product) }}" class="font-medium text-blue-400 hover:underline mr-3">Editar</a>
|
||||
<a href="{{ route('productos.edit', $product) }}" class="font-semibold dark:font-medium text-blue-800 dark:text-blue-400 hover:underline mr-3">Editar</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<x-appc>
|
||||
<div class="max-w-6xl mx-auto px-4 py-8 text-white">
|
||||
<a href="{{ route('catalogo') }}">Volver a catalogo</a>
|
||||
<h1>Titulo: {{ $producto->nombre }}</h1>
|
||||
<h1>Titulo: {{ $producto->name }}</h1>
|
||||
<p>
|
||||
<b>Categoria:</b> {{ $producto->Categoria }}
|
||||
<b>Categoria:</b> {{ $producto->type }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $producto->content }}
|
||||
|
||||
@@ -36,32 +36,23 @@
|
||||
|
||||
<x-section-header subtitle="Punto de Venta" title="Nueva " highlight="Venta" />
|
||||
|
||||
<x-ui.alert />
|
||||
|
||||
@if($errors->any())
|
||||
<div class="bg-red-900/50 border border-red-500 text-red-300 px-4 py-3 rounded-lg mb-6 shadow-sm">
|
||||
<ul class="list-disc list-inside">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="w-full max-w-5xl mx-auto bg-panel-bg border border-neutral-800 rounded-xl p-8 shadow-lg">
|
||||
|
||||
@if(session('success'))
|
||||
<div class="bg-green-900/50 border border-green-500 text-green-300 px-4 py-3 rounded-lg mb-6 shadow-sm">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="bg-red-900/50 border border-red-500 text-red-300 px-4 py-3 rounded-lg mb-6 shadow-sm">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
@if($errors->any())
|
||||
<div class="bg-red-900/50 border border-red-500 text-red-300 px-4 py-3 rounded-lg mb-6 shadow-sm">
|
||||
<ul class="list-disc list-inside">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('sales.store') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-6 mb-8 border-b border-neutral-800 pb-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-6 mb-4 border-b border-neutral-700 pb-8">
|
||||
<div class="md:col-span-10">
|
||||
<x-forms.label for="client_id" value="Cliente" />
|
||||
<div class="relative">
|
||||
@@ -73,14 +64,14 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-2 flex items-end">
|
||||
<a href="{{ route('clients.create', ['origin' => 'sales']) }}" target="_blank" class="bg-indigo-100 text-indigo-700 hover:bg-indigo-200 px-4 py-2 rounded-md font-bold flex items-center border border-indigo-200" title="Crear nuevo cliente">
|
||||
+
|
||||
</a>
|
||||
<div class="md:col-span-2 flex items-end mb-1">
|
||||
<a href="{{ route('clients.create', ['origin' => 'sales']) }}" target="_blank"
|
||||
class="inline-flex items-center text-neon-lime hover:text-[#b3e600] font-bold text-sm transition-colors"
|
||||
title="Crear nuevo cliente">+ Nuevo Cliente</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between items-end mb-4">
|
||||
<x-forms.label value="Detalle de Productos" />
|
||||
<div class="text-sm text-gray-400">
|
||||
@@ -107,7 +98,7 @@
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-gray-400 text-xs mb-1.5 uppercase tracking-wider font-semibold">Cant.</label>
|
||||
<input type="number" name="items[0][quantity]" value="1" min="1"
|
||||
class="quantity-input bg-neutral-800 border-neutral-700 text-white text-sm rounded-lg focus:ring-neon-lime focus:border-neon-lime block w-full p-2.5 h-[42px]">
|
||||
class="quantity-input bg-neutral-800 border-neutral-700 border rounded-lg text-white text-sm focus:ring-neon-lime focus:border-neon-lime block w-full p-1 px-2 h-7">
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-3 text-right flex flex-col justify-center h-[42px] mt-6">
|
||||
@@ -155,7 +146,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-4">
|
||||
<a href="{{ route('sales.create') }}" class="px-6 py-2.5 text-gray-400 hover:text-white font-medium text-sm transition-colors flex items-center">
|
||||
<a href="{{ route('sales.index') }}" class="px-6 py-2.5 text-gray-400 hover:text-white font-medium text-sm transition-colors flex items-center">
|
||||
Cancelar
|
||||
</a>
|
||||
<button type="submit" class="px-8 py-2.5 bg-neon-lime text-neutral-900 font-bold rounded-lg hover:bg-[#b3e600] transition-colors shadow-lg shadow-neon-lime/20 transform hover:scale-105">
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
<x-layout title="Ingreso Taller">
|
||||
|
||||
<x-section-header subtitle="Taller" title="Nueva " highlight="Orden de Reparación" />
|
||||
|
||||
<x-ui.alert />
|
||||
|
||||
@if($errors->any())
|
||||
<div class="w-120 bg-red-900/50 border border-red-500 text-red-300 px-4 py-3 rounded-lg mb-6 shadow-sm">
|
||||
<ul class="list-disc list-inside">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="max-w-5xl mx-auto bg-panel-bg border border-neutral-800 rounded-xl p-8 shadow-lg">
|
||||
<form action="{{ route('taller.store') }}" method="POST" class="space-y-8" id="taller-form">
|
||||
@csrf
|
||||
|
||||
<!-- 1. Datos del Cliente -->
|
||||
<div class="border-b border-neutral-800 pb-6">
|
||||
<h3 class="text-white font-bold uppercase tracking-wider text-sm mb-4 flex items-center gap-2">
|
||||
<span class="bg-neutral-800 text-neon-lime px-2 py-0.5 rounded text-xs">1</span> Cliente
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2">Propietario</label>
|
||||
<select name="client_id" class="w-full bg-neutral-900 border border-neutral-700 text-white rounded-lg p-2.5 focus:ring-neon-lime focus:border-neon-lime">
|
||||
@foreach($clients as $client)
|
||||
<option value="{{ $client->id }}">{{ $client->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<a href="{{ route('clients.create', ['origin' => 'taller']) }}" class="text-xs text-neon-lime hover:underline mt-1 inline-block">+ Nuevo Cliente</a>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2">Teléfono Aviso</label>
|
||||
<input type="text" name="contact_phone" placeholder="Ingresar numero de telefono" class="w-full bg-neutral-900 border border-neutral-700 text-white rounded-lg p-2.5 focus:ring-neon-lime focus:border-neon-lime">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Diagnóstico y Repuestos -->
|
||||
<div class="border-b border-neutral-800 pb-6">
|
||||
<h3 class="text-white font-bold uppercase tracking-wider text-sm mb-4 flex items-center gap-2">
|
||||
<span class="bg-neutral-800 text-neon-lime px-2 py-0.5 rounded text-xs">2</span> Diagnóstico y Repuestos
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Columna Izquierda: Datos Bici -->
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2">Modelo de Bici / Color</label>
|
||||
<input type="text" name="bike_model" required placeholder="Ej: Venzo Loki Roja R29" class="w-full bg-neutral-900 border border-neutral-700 text-white rounded-lg p-2.5 focus:ring-neon-lime focus:border-neon-lime">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2">Problema / Servicio</label>
|
||||
<textarea name="problem_description" rows="3" required class="w-full bg-neutral-900 border border-neutral-700 text-white rounded-lg p-2.5 focus:ring-neon-lime focus:border-neon-lime" placeholder="Ej: Service completo, hace ruido la caja..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Columna Derecha: Selector de Repuestos -->
|
||||
<div class="bg-neutral-900/50 p-4 rounded-xl border border-neutral-800">
|
||||
<label class="block text-xs font-bold text-neon-lime uppercase mb-2">Agregar Repuestos / Servicios</label>
|
||||
|
||||
<!-- Buscador -->
|
||||
<div class="relative mb-3">
|
||||
<input type="text" id="part-search" class="w-full bg-neutral-800 border border-neutral-700 text-white rounded-lg p-2 text-sm focus:ring-neon-lime focus:border-neon-lime" placeholder="Buscar repuesto..." autocomplete="off">
|
||||
<!-- Lista desplegable de sugerencias -->
|
||||
<div id="suggestions-box" class="absolute left-0 right-0 z-50 bg-neutral-800 border border-neutral-600 rounded-b-lg shadow-xl max-h-40 overflow-y-auto hidden"></div>
|
||||
</div>
|
||||
|
||||
<!-- Lista Visual de Seleccionados -->
|
||||
<div id="selected-parts-container" class="space-y-2 mb-3 min-h-13">
|
||||
<p class="text-xs text-gray-600 italic text-center py-2" id="no-parts-msg">Sin repuestos seleccionados</p>
|
||||
</div>
|
||||
|
||||
<!-- Input Oculto Real (Este es el que se envía a la BD) -->
|
||||
<textarea name="parts_needed" id="parts_needed_real" class="hidden"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Agenda y Costos -->
|
||||
<div>
|
||||
<h3 class="text-white font-bold uppercase tracking-wider text-sm mb-4 flex items-center gap-2">
|
||||
<span class="bg-neutral-800 text-neon-lime px-2 py-0.5 rounded text-xs">3</span> Acuerdo
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 items-end">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2">Fecha Prometida / Retiro</label>
|
||||
<input type="datetime-local" name="scheduled_at" required class="w-full bg-neutral-900 border border-neutral-700 text-white rounded-lg p-2.5 focus:ring-neon-lime focus:border-neon-lime">
|
||||
</div>
|
||||
|
||||
<div class="bg-neutral-800 p-4 rounded-lg border border-neutral-700">
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-1">Presupuesto Estimado</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-500">$</span>
|
||||
<!-- Input de costo: Se autocalcula pero el usuario puede editarlo manualmente -->
|
||||
<input type="number" id="estimated_cost" name="estimated_cost" step="0.01" class="w-full bg-transparent border-0 text-white text-2xl font-black focus:ring-0 p-0" placeholder="0.00">
|
||||
</div>
|
||||
<p class="text-[10px] text-gray-500 mt-1">* Calculado automáticamente según repuestos. Editable.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-4 border-t border-neutral-800">
|
||||
<a href="{{ route('taller.index') }}" class="mr-4 px-6 py-3 text-gray-400 hover:text-white font-medium">Cancelar</a>
|
||||
<button type="submit" class="px-8 py-3 bg-neon-lime text-neutral-900 font-bold rounded-lg hover:bg-[#b3e600] uppercase tracking-wide shadow-lg shadow-neon-lime/20">
|
||||
Ingresar Bicicleta
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Scripts para el Buscador de Repuestos -->
|
||||
@push('scripts')
|
||||
<script type="module">
|
||||
const partsDB = @json($products);
|
||||
const clientsDB = @json($clients);
|
||||
let selectedParts = [];
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
// Telefono de Cliente al cambiar selección
|
||||
$('select[name="client_id"]').on('change', function() {
|
||||
let clientId = $(this).val();
|
||||
let client = clientsDB.find(c => c.id == clientId);
|
||||
if (client && client.phone) {
|
||||
$('input[name="contact_phone"]').val(client.phone);
|
||||
} else {
|
||||
$('input[name="contact_phone"]').val('');
|
||||
}
|
||||
});
|
||||
|
||||
// --- 1. Lógica del Buscador ---
|
||||
$('#part-search').on('keyup focus', function() {
|
||||
let term = $(this).val().toLowerCase();
|
||||
let $box = $('#suggestions-box');
|
||||
|
||||
if (term.length === 0) {
|
||||
$box.addClass('hidden').empty();
|
||||
return;
|
||||
}
|
||||
|
||||
let matches = partsDB.filter(p => p.name.toLowerCase().includes(term) || (p.sku && p.sku.toLowerCase().includes(term)));
|
||||
|
||||
let html = '';
|
||||
if (matches.length > 0) {
|
||||
matches.forEach(p => {
|
||||
html += `
|
||||
<div class="p-2 hover:bg-neutral-700 cursor-pointer text-xs text-white border-b border-neutral-700 last:border-0 suggestion-item flex justify-between items-center"
|
||||
data-id="${p.id}" data-name="${p.name}" data-price="${p.price}">
|
||||
<span>${p.name}</span>
|
||||
<span class="text-neon-lime font-mono">$${p.price}</span>
|
||||
</div>`;
|
||||
});
|
||||
} else {
|
||||
html = '<div class="p-2 text-xs text-gray-500 italic">No encontrado</div>';
|
||||
}
|
||||
$box.html(html).removeClass('hidden');
|
||||
});
|
||||
|
||||
// Ocultar sugerencias al hacer clic fuera
|
||||
$(document).click(function(e) {
|
||||
if (!$(e.target).closest('#part-search, #suggestions-box').length) {
|
||||
$('#suggestions-box').addClass('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// --- 2. Agregar Repuesto ---
|
||||
$(document).on('click', '.suggestion-item', function() {
|
||||
let item = {
|
||||
id: $(this).data('id'),
|
||||
name: $(this).data('name'),
|
||||
price: parseFloat($(this).data('price'))
|
||||
};
|
||||
|
||||
// Agregar al array
|
||||
selectedParts.push(item);
|
||||
|
||||
// Limpiar buscador
|
||||
$('#part-search').val('').focus();
|
||||
$('#suggestions-box').addClass('hidden');
|
||||
|
||||
renderList();
|
||||
});
|
||||
|
||||
// --- 3. Eliminar Repuesto ---
|
||||
window.removePart = function(index) {
|
||||
selectedParts.splice(index, 1);
|
||||
renderList();
|
||||
};
|
||||
|
||||
// --- 4. Renderizar Lista y Calcular ---
|
||||
function renderList() {
|
||||
let $container = $('#selected-parts-container');
|
||||
$container.empty();
|
||||
let totalCost = 0;
|
||||
let partsText = ""; // Texto para la BD
|
||||
|
||||
if (selectedParts.length === 0) {
|
||||
$container.html('<p class="text-xs text-gray-600 italic text-center py-2" id="no-parts-msg">Sin repuestos seleccionados</p>');
|
||||
} else {
|
||||
selectedParts.forEach((p, index) => {
|
||||
totalCost += p.price;
|
||||
|
||||
// Construimos texto para BD: "- Cámara ($5000)\n"
|
||||
partsText += `- ${p.name} ($${p.price})\n`;
|
||||
|
||||
// Visual
|
||||
$container.append(`
|
||||
<div class="flex justify-between items-center bg-neutral-800 p-2 rounded border border-neutral-700 text-xs">
|
||||
<div class="text-white">
|
||||
${p.name}
|
||||
<span class="text-gray-500 ml-1">($${p.price})</span>
|
||||
</div>
|
||||
<button type="button" onclick="removePart(${index})" class="text-red-500 hover:text-red-400 font-bold px-1">×</button>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar Inputs del Formulario
|
||||
$('#estimated_cost').val(totalCost > 0 ? totalCost : ''); // Presupuesto
|
||||
$('#parts_needed_real').val(partsText); // Texto para la BD
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
</x-layout>
|
||||
@@ -0,0 +1,61 @@
|
||||
<x-layout title="Tablero de Taller">
|
||||
|
||||
<x-section-header subtitle="Gestión" title="Tablero de " highlight="Taller" />
|
||||
|
||||
@if(session('success'))
|
||||
<div class="bg-green-900/50 border border-green-500 text-green-300 px-4 py-3 rounded-lg mb-6 shadow-sm">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex w-full justify-end items-end mb-4">
|
||||
<a href="{{ route('taller.create') }}" class="px-5 py-2.5 bg-neon-lime text-neutral-900 font-bold rounded-lg hover:bg-[#b3e600] uppercase tracking-wide text-xs flex items-center gap-2">
|
||||
+ Ingresar Bici
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Contenedor -->
|
||||
<div class="flex flex-col lg:flex-row w-full gap-8 overflow-x-auto pb-4 h-[calc(100vh-250px)]">
|
||||
|
||||
<!-- COLUMNA 1 -->
|
||||
<div class="flex-1 min-w-[300px] bg-neutral-900/50 border border-neutral-800 rounded-xl flex flex-col">
|
||||
<div class="p-4 border-b border-neutral-800 bg-neutral-800/50 rounded-t-xl flex justify-between items-center">
|
||||
<h3 class="font-bold text-gray-300 uppercase tracking-widest text-xs">🔴 Pendientes / A Revisar</h3>
|
||||
<span class="bg-neutral-900 text-gray-400 text-xs px-2 py-1 rounded-full">{{ $pending->count() }}</span>
|
||||
</div>
|
||||
<div class="p-4 space-y-4 overflow-y-auto flex-1 custom-scrollbar">
|
||||
@foreach($pending as $job)
|
||||
<x-taller-card :job="$job" color="red" />
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- COLUMNA 2 -->
|
||||
<div class="flex-1 min-w-[300px] bg-neutral-900/50 border border-neutral-800 rounded-xl flex flex-col">
|
||||
<div class="p-4 border-b border-neutral-800 bg-neutral-800/50 rounded-t-xl flex justify-between items-center">
|
||||
<h3 class="font-bold text-yellow-500 uppercase tracking-widest text-xs">🟡 En Reparación</h3>
|
||||
<span class="bg-neutral-900 text-gray-400 text-xs px-2 py-1 rounded-full">{{ $inProgress->count() }}</span>
|
||||
</div>
|
||||
<div class="p-4 space-y-4 overflow-y-auto flex-1 custom-scrollbar">
|
||||
@foreach($inProgress as $job)
|
||||
<x-taller-card :job="$job" color="yellow" />
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- COLUMNA 3 -->
|
||||
<div class="flex-1 min-w-[300px] bg-neutral-900/50 border border-neutral-800 rounded-xl flex flex-col">
|
||||
<div class="p-4 border-b border-neutral-800 bg-neutral-800/50 rounded-t-xl flex justify-between items-center">
|
||||
<h3 class="font-bold text-neon-lime uppercase tracking-widest text-xs">🟢 Listas para Retirar</h3>
|
||||
<span class="bg-neutral-900 text-gray-400 text-xs px-2 py-1 rounded-full">{{ $ready->count() }}</span>
|
||||
</div>
|
||||
<div class="p-4 space-y-4 overflow-y-auto flex-1 custom-scrollbar">
|
||||
@foreach($ready as $job)
|
||||
<x-taller-card :job="$job" color="neon" />
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</x-layout>
|
||||
+15
-12
@@ -9,22 +9,21 @@ use App\Http\Controllers\HomeController;
|
||||
use App\Http\Controllers\LoginController;
|
||||
use App\Http\Controllers\ProductosController;
|
||||
use App\Http\Controllers\RegisterController;
|
||||
use App\Http\Controllers\TallerController;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
Route::get('/',HomeController::class);
|
||||
Route::get('/catalogo', [CatalogoController::class,'catalogo'])->name('catalogo');
|
||||
|
||||
Route::get('login', function(){
|
||||
return view('login');
|
||||
})->name('login');
|
||||
// Route::get('/catalogo', [CatalogoController::class,'catalogo'])->name('catalogo');
|
||||
Route::resource('catalogo', CatalogoController::class)->only(['index', 'show'])->parameters(['catalogo' => 'product']);
|
||||
|
||||
Route::post('login', LoginController::class)
|
||||
->middleware('throttle:5,1')
|
||||
->name('login.attempt');
|
||||
Route::get('login', function(){ return view('login'); })->name('login');
|
||||
Route::post('login', LoginController::class)->middleware('throttle:5,1')->name('login.attempt');
|
||||
|
||||
Route::view('dashboard', 'dashboard')->middleware('auth')->name('dashboard');
|
||||
Route::view('register', 'register')->name('register');
|
||||
Route::post('register', RegisterController::class)->name('register.store');
|
||||
|
||||
Route::post('logout', function(){
|
||||
Auth::guard('web')->logout();
|
||||
@@ -35,16 +34,20 @@ Route::post('logout', function(){
|
||||
return redirect('/');
|
||||
})->name('logout');
|
||||
|
||||
Route::view('register', 'register')->name('register');
|
||||
Route::post('register', RegisterController::class)->name('register.store');
|
||||
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::view('dashboard', 'dashboard')->name('dashboard');
|
||||
Route::resource('clients', ClientController::class);
|
||||
Route::resource('productos', ProductosController::class)->parameters([
|
||||
'productos' => 'product'
|
||||
]);
|
||||
Route::resource('sales', SaleController::class)->only(['index', 'create', 'store', 'show']);
|
||||
|
||||
// Rutas de Taller
|
||||
Route::controller(TallerController::class)->prefix('taller')->name('taller.')->group(function () {
|
||||
Route::get('/', 'index')->name('index');
|
||||
Route::get('/create', 'create')->name('create');
|
||||
Route::post('/', 'store')->name('store');
|
||||
Route::patch('/{appointment}/status', 'updateStatus')->name('updateStatus');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: "selector",
|
||||
content: [
|
||||
"./resources/**/*.blade.php",
|
||||
"./resources/**/*.js",
|
||||
|
||||
Reference in New Issue
Block a user