Merge branch 'SystemaFull' of https://github.com/BryamE/ProyectoLauck into ReportesNEW
This commit is contained in:
@@ -113,6 +113,24 @@
|
|||||||
- **Validación completa** de datos
|
- **Validación completa** de datos
|
||||||
- **Botón de crear cliente** sin salir del formulario
|
- **Botón de crear cliente** sin salir del formulario
|
||||||
|
|
||||||
|
### 🏭 Proveedores y Compras
|
||||||
|
- **Base de datos de proveedores**: Registro y gestión de datos de contacto
|
||||||
|
- **Órdenes de compra**: Generación de pedidos a proveedores para reabastecimiento
|
||||||
|
- **Actualización de inventario**: Vinculación con los productos para reponer stock
|
||||||
|
|
||||||
|
### 📅 Agenda de Turnos
|
||||||
|
- **Calendario integrado**: Visualización y gestión de citas del taller
|
||||||
|
- **Detalle de turno**: Información rápida de los turnos programados
|
||||||
|
|
||||||
|
### 💸 Control de Gastos
|
||||||
|
- **Registro de egresos**: Control de pagos de servicios, insumos y otros gastos del local
|
||||||
|
- **Categorización**: Descripción y fecha para tener seguimiento de los gastos
|
||||||
|
|
||||||
|
### 💾 Copias de Seguridad (Backups)
|
||||||
|
- **Generación manual**: Creación de respaldos completos de la base de datos (Solo Administradores)
|
||||||
|
- **Gestión de archivos**: Descarga, restauración y eliminación de copias de seguridad
|
||||||
|
- **Interfaz dedicada**: Panel de control específico para proteger la información
|
||||||
|
|
||||||
### 🌐 Catálogo Público
|
### 🌐 Catálogo Público
|
||||||
#### Vista para Visitantes (sin login):
|
#### Vista para Visitantes (sin login):
|
||||||
- **Listado público** de productos en stock
|
- **Listado público** de productos en stock
|
||||||
@@ -221,7 +239,8 @@ Client.php // Clientes con validaciones
|
|||||||
Sale.php // Ventas (cabecera)
|
Sale.php // Ventas (cabecera)
|
||||||
SaleDetail.php // Detalles de venta (renglones)
|
SaleDetail.php // Detalles de venta (renglones)
|
||||||
Appointment.php // Órdenes de taller
|
Appointment.php // Órdenes de taller
|
||||||
Supplier.php // Proveedores (implementación futura)
|
Supplier.php // Proveedores y pedidos
|
||||||
|
Expense.php // Gastos del local
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 🎮 Controladores (app/Http/Controllers)
|
#### 🎮 Controladores (app/Http/Controllers)
|
||||||
@@ -234,6 +253,11 @@ SaleController.php // Sistema de ventas (POS)
|
|||||||
ClientController.php // Gestión de clientes
|
ClientController.php // Gestión de clientes
|
||||||
TallerController.php // Gestión de taller (Kanban)
|
TallerController.php // Gestión de taller (Kanban)
|
||||||
CatalogoController.php // Catálogo público
|
CatalogoController.php // Catálogo público
|
||||||
|
ExpenseController.php // Gestión de gastos
|
||||||
|
AgendaController.php // Agenda de turnos
|
||||||
|
AppointmentController.php // Detalle de turnos
|
||||||
|
SupplierController.php // Proveedores y pedidos
|
||||||
|
Admin/BackupController.php // Copias de seguridad (Admin)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 👁️ Vistas (resources/views)
|
#### 👁️ Vistas (resources/views)
|
||||||
@@ -505,17 +529,7 @@ php artisan migrate:fresh --seed
|
|||||||
|
|
||||||
### Prioridad Alta 🔴
|
### Prioridad Alta 🔴
|
||||||
|
|
||||||
#### 1. Sistema de Compras a Proveedores
|
#### 1. Reportes y Estadísticas
|
||||||
```
|
|
||||||
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:
|
Dashboard mejorado con:
|
||||||
- Ventas del día/mes/año (gráficos)
|
- Ventas del día/mes/año (gráficos)
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
use Symfony\Component\Process\Exception\ProcessFailedException;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use ZipArchive;
|
||||||
|
|
||||||
|
class BackupDatabase extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'db:backup';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Crea una copia de seguridad comprimida de la base de datos MySQL y elimina respaldos antiguos.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->info('Iniciando copia de seguridad de la base de datos...');
|
||||||
|
|
||||||
|
$dbConfig = config('database.connections.mysql');
|
||||||
|
|
||||||
|
if (!$dbConfig) {
|
||||||
|
$this->error('No se pudo cargar la configuración de la base de datos MySQL.');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = $dbConfig['host'] ?? '127.0.0.1';
|
||||||
|
$port = $dbConfig['port'] ?? '3306';
|
||||||
|
$database = $dbConfig['database'] ?? 'lauck';
|
||||||
|
$username = $dbConfig['username'] ?? 'root';
|
||||||
|
$password = $dbConfig['password'] ?? '';
|
||||||
|
|
||||||
|
// Asegurarse de que el directorio de respaldos exista
|
||||||
|
$backupDir = storage_path('app/backups');
|
||||||
|
if (!File::exists($backupDir)) {
|
||||||
|
File::makeDirectory($backupDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nombre de los archivos
|
||||||
|
$timestamp = now()->format('Y-m-d_H-i-s');
|
||||||
|
$sqlFile = "backup-{$database}-{$timestamp}.sql";
|
||||||
|
$sqlPath = $backupDir . '/' . $sqlFile;
|
||||||
|
$zipFile = "backup-{$database}-{$timestamp}.zip";
|
||||||
|
$zipPath = $backupDir . '/' . $zipFile;
|
||||||
|
|
||||||
|
// Buscar el ejecutable mysqldump
|
||||||
|
$mysqldumpPath = 'mysqldump';
|
||||||
|
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
||||||
|
// En Windows/XAMPP comúnmente está en C:\xampp\mysql\bin\mysqldump.exe
|
||||||
|
$xamppPath = 'C:\\xampp\\mysql\\bin\\mysqldump.exe';
|
||||||
|
if (File::exists($xamppPath)) {
|
||||||
|
$mysqldumpPath = $xamppPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ejecutar mysqldump con Process
|
||||||
|
// Usamos variables de entorno para la contraseña por seguridad
|
||||||
|
$command = [
|
||||||
|
$mysqldumpPath,
|
||||||
|
"--host={$host}",
|
||||||
|
"--port={$port}",
|
||||||
|
"--user={$username}",
|
||||||
|
"--no-tablespaces",
|
||||||
|
$database
|
||||||
|
];
|
||||||
|
|
||||||
|
// Inherit SystemRoot and PATH from the system environment to prevent Winsock 10106 errors on Windows
|
||||||
|
$env = [
|
||||||
|
'SystemRoot' => getenv('SystemRoot') ?: 'C:\\Windows',
|
||||||
|
'windir' => getenv('windir') ?: 'C:\\Windows',
|
||||||
|
'PATH' => getenv('PATH'),
|
||||||
|
];
|
||||||
|
if ($password !== '') {
|
||||||
|
$env['MYSQL_PWD'] = $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Ejecutando volcado a archivo temporal SQL...");
|
||||||
|
|
||||||
|
$process = new Process($command, null, $env);
|
||||||
|
$process->setTimeout(300); // 5 minutos máximo
|
||||||
|
|
||||||
|
try {
|
||||||
|
$process->run();
|
||||||
|
|
||||||
|
if (!$process->isSuccessful()) {
|
||||||
|
throw new \Exception(trim($process->getErrorOutput()) ?: 'Error desconocido al ejecutar mysqldump.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardar salida en el archivo SQL
|
||||||
|
File::put($sqlPath, $process->getOutput());
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error('Error durante el volcado de la base de datos: ' . $e->getMessage());
|
||||||
|
// Limpieza si quedó el archivo a medio hacer
|
||||||
|
if (File::exists($sqlPath)) {
|
||||||
|
File::delete($sqlPath);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprimir el archivo SQL a ZIP
|
||||||
|
$this->info('Comprimiendo respaldo...');
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if ($zip->open($zipPath, ZipArchive::CREATE) === true) {
|
||||||
|
$zip->addFile($sqlPath, $sqlFile);
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
// Eliminar el archivo SQL temporal
|
||||||
|
File::delete($sqlPath);
|
||||||
|
$this->info("Copia de seguridad guardada con éxito en: {$zipFile}");
|
||||||
|
} else {
|
||||||
|
$this->error('No se pudo crear el archivo ZIP.');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retención de respaldos (limpieza de archivos antiguos)
|
||||||
|
$this->cleanupOldBackups($backupDir);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elimina respaldos antiguos que exceden el límite de días (30 días).
|
||||||
|
*/
|
||||||
|
protected function cleanupOldBackups($backupDir)
|
||||||
|
{
|
||||||
|
$this->info('Revisando si hay respaldos antiguos para limpiar...');
|
||||||
|
$days = 30; // Conservar los últimos 30 días de respaldos semanales
|
||||||
|
$files = File::files($backupDir);
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
// Verificar que sea un archivo de backup
|
||||||
|
if ($file->getExtension() === 'zip' && strpos($file->getFilename(), 'backup-') === 0) {
|
||||||
|
$lastModified = $file->getMTime();
|
||||||
|
$ageInDays = (time() - $lastModified) / (24 * 60 * 60);
|
||||||
|
|
||||||
|
if ($ageInDays > $days) {
|
||||||
|
$this->warn("Eliminando respaldo antiguo: " . $file->getFilename());
|
||||||
|
File::delete($file->getPathname());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->info('Limpieza finalizada.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class BackupController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Directorio de respaldos.
|
||||||
|
*/
|
||||||
|
protected $backupDir;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->backupDir = storage_path('app/backups');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Muestra el panel con el listado de copias de seguridad.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Asegurarse de que el directorio exista
|
||||||
|
if (!File::exists($this->backupDir)) {
|
||||||
|
File::makeDirectory($this->backupDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = File::files($this->backupDir);
|
||||||
|
$backups = [];
|
||||||
|
$totalSize = 0;
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if ($file->getExtension() === 'zip' && strpos($file->getFilename(), 'backup-') === 0) {
|
||||||
|
$size = $file->getSize();
|
||||||
|
$totalSize += $size;
|
||||||
|
|
||||||
|
$backups[] = [
|
||||||
|
'filename' => $file->getFilename(),
|
||||||
|
'size' => $this->formatBytes($size),
|
||||||
|
'raw_size' => $size,
|
||||||
|
'created_at' => \Carbon\Carbon::createFromTimestamp($file->getMTime())->format('d/m/Y H:i:s'),
|
||||||
|
'mtime' => $file->getMTime(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordenar de más nuevo a más viejo
|
||||||
|
usort($backups, function ($a, $b) {
|
||||||
|
return $b['mtime'] <=> $a['mtime'];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Estadísticas de disco
|
||||||
|
$freeDiskSpace = disk_free_space($this->backupDir);
|
||||||
|
$totalDiskSpace = disk_total_space($this->backupDir);
|
||||||
|
|
||||||
|
$diskStats = [
|
||||||
|
'total_backups_size' => $this->formatBytes($totalSize),
|
||||||
|
'free_space' => $this->formatBytes($freeDiskSpace),
|
||||||
|
'total_space' => $this->formatBytes($totalDiskSpace),
|
||||||
|
'free_percentage' => round(($freeDiskSpace / $totalDiskSpace) * 100, 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('admin.backups.index', compact('backups', 'diskStats'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$exitCode = Artisan::call('db:backup');
|
||||||
|
$output = Artisan::output();
|
||||||
|
|
||||||
|
if ($exitCode === 0) {
|
||||||
|
return redirect()->route('admin.backups.index')
|
||||||
|
->with('success', 'Copia de seguridad generada correctamente.');
|
||||||
|
} else {
|
||||||
|
\Illuminate\Support\Facades\Log::error("Artisan db:backup failed with exit code {$exitCode}. Output: " . $output);
|
||||||
|
return redirect()->route('admin.backups.index')
|
||||||
|
->with('error', 'Ocurrió un error al generar la copia de seguridad. Detalles: ' . trim($output));
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\Illuminate\Support\Facades\Log::error("Artisan db:backup threw exception: " . $e->getMessage());
|
||||||
|
return redirect()->route('admin.backups.index')
|
||||||
|
->with('error', 'Error: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descargar una copia de seguridad.
|
||||||
|
*/
|
||||||
|
public function download($filename)
|
||||||
|
{
|
||||||
|
// Validación de seguridad para evitar directory traversal
|
||||||
|
if (strpos($filename, '..') !== false || strpos($filename, '/') !== false || strpos($filename, '\\') !== false) {
|
||||||
|
abort(404, 'Nombre de archivo no válido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $this->backupDir . '/' . $filename;
|
||||||
|
|
||||||
|
if (!File::exists($path)) {
|
||||||
|
abort(404, 'El archivo no existe.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->download($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eliminar una copia de seguridad.
|
||||||
|
*/
|
||||||
|
public function destroy($filename)
|
||||||
|
{
|
||||||
|
// Validación de seguridad para evitar directory traversal
|
||||||
|
if (strpos($filename, '..') !== false || strpos($filename, '/') !== false || strpos($filename, '\\') !== false) {
|
||||||
|
abort(400, 'Nombre de archivo no válido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $this->backupDir . '/' . $filename;
|
||||||
|
|
||||||
|
if (File::exists($path)) {
|
||||||
|
File::delete($path);
|
||||||
|
return redirect()->route('admin.backups.index')
|
||||||
|
->with('success', 'Copia de seguridad eliminada correctamente.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('admin.backups.index')
|
||||||
|
->with('error', 'No se encontró el archivo a eliminar.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subir un archivo de copia de seguridad (.zip).
|
||||||
|
*/
|
||||||
|
public function upload(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'backup_file' => 'required|file|mimes:zip|max:50000', // 50MB max
|
||||||
|
]);
|
||||||
|
|
||||||
|
$file = $request->file('backup_file');
|
||||||
|
$filename = $file->getClientOriginalName();
|
||||||
|
|
||||||
|
// Validación de seguridad para el nombre del archivo
|
||||||
|
if (strpos($filename, 'backup-') !== 0 || $file->getClientOriginalExtension() !== 'zip') {
|
||||||
|
return redirect()->route('admin.backups.index')
|
||||||
|
->with('error', 'El archivo debe ser un .zip válido de respaldo (su nombre debe empezar con "backup-").');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asegurarse de que el directorio exista
|
||||||
|
if (!File::exists($this->backupDir)) {
|
||||||
|
File::makeDirectory($this->backupDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$file->move($this->backupDir, $filename);
|
||||||
|
|
||||||
|
return redirect()->route('admin.backups.index')
|
||||||
|
->with('success', 'Copia de seguridad subida correctamente.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restaurar la base de datos a partir de una copia de seguridad.
|
||||||
|
*/
|
||||||
|
public function restore($filename)
|
||||||
|
{
|
||||||
|
// Validación de seguridad para evitar directory traversal
|
||||||
|
if (strpos($filename, '..') !== false || strpos($filename, '/') !== false || strpos($filename, '\\') !== false) {
|
||||||
|
abort(400, 'Nombre de archivo no válido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$zipPath = $this->backupDir . '/' . $filename;
|
||||||
|
|
||||||
|
if (!File::exists($zipPath)) {
|
||||||
|
return redirect()->route('admin.backups.index')->with('error', 'El archivo no existe.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extraer el archivo SQL del ZIP
|
||||||
|
$zip = new \ZipArchive();
|
||||||
|
if ($zip->open($zipPath) === true) {
|
||||||
|
// Buscamos el primer archivo SQL dentro del zip
|
||||||
|
$sqlFilename = null;
|
||||||
|
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||||
|
$stat = $zip->statIndex($i);
|
||||||
|
if (pathinfo($stat['name'], PATHINFO_EXTENSION) === 'sql') {
|
||||||
|
$sqlFilename = $stat['name'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$sqlFilename) {
|
||||||
|
$zip->close();
|
||||||
|
return redirect()->route('admin.backups.index')->with('error', 'No se encontró ningún archivo SQL en el archivo comprimido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extraer el SQL a un directorio temporal de backups
|
||||||
|
$zip->extractTo($this->backupDir, $sqlFilename);
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
$sqlPath = $this->backupDir . '/' . $sqlFilename;
|
||||||
|
|
||||||
|
// Datos de conexión
|
||||||
|
$dbConfig = config('database.connections.mysql');
|
||||||
|
$host = $dbConfig['host'] ?? '127.0.0.1';
|
||||||
|
$port = $dbConfig['port'] ?? '3306';
|
||||||
|
$database = $dbConfig['database'] ?? 'lauck';
|
||||||
|
$username = $dbConfig['username'] ?? 'root';
|
||||||
|
$password = $dbConfig['password'] ?? '';
|
||||||
|
|
||||||
|
// Ejecutable de mysql
|
||||||
|
$mysqlPath = 'mysql';
|
||||||
|
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
||||||
|
$xamppPath = 'C:\\xampp\\mysql\\bin\\mysql.exe';
|
||||||
|
if (File::exists($xamppPath)) {
|
||||||
|
$mysqlPath = $xamppPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ejecutar importación usando Process y redirección de entrada de archivo
|
||||||
|
$command = [
|
||||||
|
$mysqlPath,
|
||||||
|
"--host={$host}",
|
||||||
|
"--port={$port}",
|
||||||
|
"--user={$username}",
|
||||||
|
$database
|
||||||
|
];
|
||||||
|
|
||||||
|
$env = [
|
||||||
|
'SystemRoot' => getenv('SystemRoot') ?: 'C:\\Windows',
|
||||||
|
'windir' => getenv('windir') ?: 'C:\\Windows',
|
||||||
|
'PATH' => getenv('PATH'),
|
||||||
|
];
|
||||||
|
if ($password !== '') {
|
||||||
|
$env['MYSQL_PWD'] = $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
$process = new \Symfony\Component\Process\Process($command, null, $env);
|
||||||
|
|
||||||
|
// Pasar el contenido del archivo SQL a la entrada estándar (stdin)
|
||||||
|
$process->setInput(File::get($sqlPath));
|
||||||
|
$process->setTimeout(300);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$process->run();
|
||||||
|
|
||||||
|
// Limpiar archivo SQL temporal
|
||||||
|
File::delete($sqlPath);
|
||||||
|
|
||||||
|
if (!$process->isSuccessful()) {
|
||||||
|
throw new \Exception(trim($process->getErrorOutput()) ?: 'Error desconocido al restaurar el respaldo.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('admin.backups.index')
|
||||||
|
->with('success', 'Base de datos restaurada correctamente a partir del respaldo: ' . $filename);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if (File::exists($sqlPath)) {
|
||||||
|
File::delete($sqlPath);
|
||||||
|
}
|
||||||
|
\Illuminate\Support\Facades\Log::error("Backup restore failed: " . $e->getMessage());
|
||||||
|
return redirect()->route('admin.backups.index')
|
||||||
|
->with('error', 'Error al restaurar base de datos: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return redirect()->route('admin.backups.index')->with('error', 'No se pudo abrir el archivo ZIP.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatear bytes a tamaño legible.
|
||||||
|
*/
|
||||||
|
private function formatBytes($bytes, $precision = 2)
|
||||||
|
{
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
|
||||||
|
$bytes = max($bytes, 0);
|
||||||
|
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||||
|
$pow = min($pow, count($units) - 1);
|
||||||
|
|
||||||
|
$bytes /= pow(1024, $pow);
|
||||||
|
|
||||||
|
return round($bytes, $precision) . ' ' . $units[$pow];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,13 +70,39 @@ class ProductosController extends Controller
|
|||||||
'min_stock_alert' => 'required|integer|min:0',
|
'min_stock_alert' => 'required|integer|min:0',
|
||||||
'type' => 'required|in:bike,accessory,clothing,spare,service,children,skate,rollers,other',
|
'type' => 'required|in:bike,accessory,clothing,spare,service,children,skate,rollers,other',
|
||||||
'serial_number' => 'nullable|string|max:100',
|
'serial_number' => 'nullable|string|max:100',
|
||||||
'image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
|
'image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:20480',
|
||||||
'suppliers_id' => 'required|exists:suppliers,id',
|
'suppliers_id' => 'required|exists:suppliers,id',
|
||||||
'description' => 'nullable|string'
|
'description' => 'nullable|string'
|
||||||
|
], [
|
||||||
|
'image.uploaded' => 'La imagen supera el límite de subida permitido por el servidor.',
|
||||||
|
'image.max' => 'La imagen no debe pesar más de 20 MB.',
|
||||||
|
'image.image' => 'El archivo debe ser una imagen válida.',
|
||||||
|
'image.mimes' => 'La imagen debe tener formato jpeg, png, jpg o webp.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($request->hasFile('image')) {
|
if ($request->hasFile('image')) {
|
||||||
$path = $request->file('image')->store('products', 'public');
|
$file = $request->file('image');
|
||||||
|
|
||||||
|
// Elevar temporalmente el límite de memoria para procesar la imagen
|
||||||
|
ini_set('memory_limit', '512M');
|
||||||
|
|
||||||
|
// Usar Intervention Image v4 con driver GD para procesar la imagen
|
||||||
|
$manager = new \Intervention\Image\ImageManager(new \Intervention\Image\Drivers\Gd\Driver());
|
||||||
|
$image = $manager->decode($file->getRealPath());
|
||||||
|
|
||||||
|
// Redimensionar proporcionalmente si excede 1200px
|
||||||
|
if ($image->width() > 1200 || $image->height() > 1200) {
|
||||||
|
$image->scale(width: 1200, height: 1200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprimir a JPEG con 80% de calidad
|
||||||
|
$encoded = $image->encode(new \Intervention\Image\Encoders\JpegEncoder(80));
|
||||||
|
|
||||||
|
// Nombre de archivo único
|
||||||
|
$path = 'products/' . uniqid() . '.jpg';
|
||||||
|
|
||||||
|
// Guardar en disco public
|
||||||
|
Storage::disk('public')->put($path, $encoded->toString());
|
||||||
$validated['image_path'] = $path;
|
$validated['image_path'] = $path;
|
||||||
}
|
}
|
||||||
unset($validated['image']);
|
unset($validated['image']);
|
||||||
@@ -113,9 +139,14 @@ class ProductosController extends Controller
|
|||||||
'stock_quantity' => 'required|integer|min:0',
|
'stock_quantity' => 'required|integer|min:0',
|
||||||
'min_stock_alert' => 'required|integer|min:0',
|
'min_stock_alert' => 'required|integer|min:0',
|
||||||
'serial_number' => 'nullable|string|max:100',
|
'serial_number' => 'nullable|string|max:100',
|
||||||
'image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
|
'image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:20480',
|
||||||
'suppliers_id' => 'required|exists:suppliers,id',
|
'suppliers_id' => 'required|exists:suppliers,id',
|
||||||
'description' => 'nullable|string'
|
'description' => 'nullable|string'
|
||||||
|
], [
|
||||||
|
'image.uploaded' => 'La imagen supera el límite de subida permitido por el servidor.',
|
||||||
|
'image.max' => 'La imagen no debe pesar más de 20 MB.',
|
||||||
|
'image.image' => 'El archivo debe ser una imagen válida.',
|
||||||
|
'image.mimes' => 'La imagen debe tener formato jpeg, png, jpg o webp.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($request->hasFile('image')) {
|
if ($request->hasFile('image')) {
|
||||||
@@ -123,7 +154,28 @@ class ProductosController extends Controller
|
|||||||
Storage::disk('public')->delete($product->image_path);
|
Storage::disk('public')->delete($product->image_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
$path = $request->file('image')->store('products', 'public');
|
$file = $request->file('image');
|
||||||
|
|
||||||
|
// Elevar temporalmente el límite de memoria para procesar la imagen
|
||||||
|
ini_set('memory_limit', '512M');
|
||||||
|
|
||||||
|
// Usar Intervention Image v4 con driver GD para procesar la imagen
|
||||||
|
$manager = new \Intervention\Image\ImageManager(new \Intervention\Image\Drivers\Gd\Driver());
|
||||||
|
$image = $manager->decode($file->getRealPath());
|
||||||
|
|
||||||
|
// Redimensionar proporcionalmente si excede 1200px
|
||||||
|
if ($image->width() > 1200 || $image->height() > 1200) {
|
||||||
|
$image->scale(width: 1200, height: 1200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprimir a JPEG con 80% de calidad
|
||||||
|
$encoded = $image->encode(new \Intervention\Image\Encoders\JpegEncoder(80));
|
||||||
|
|
||||||
|
// Nombre de archivo único
|
||||||
|
$path = 'products/' . uniqid() . '.jpg';
|
||||||
|
|
||||||
|
// Guardar en disco public
|
||||||
|
Storage::disk('public')->put($path, $encoded->toString());
|
||||||
$validated['image_path'] = $path;
|
$validated['image_path'] = $path;
|
||||||
}
|
}
|
||||||
unset($validated['image']);
|
unset($validated['image']);
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class CheckAdmin //
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
if (auth()->check() && auth()->user()->role === 'admin') {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(403, 'No tienes permiso para entrar aquí.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
//
|
//
|
||||||
|
$middleware->alias([
|
||||||
|
'admin' => \App\Http\Middleware\CheckAdmin::class,
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
|
"intervention/image": "^4.1",
|
||||||
"laravel-lang/common": "^6.7",
|
"laravel-lang/common": "^6.7",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/tinker": "^2.10.1"
|
"laravel/tinker": "^2.10.1"
|
||||||
|
|||||||
Generated
+145
-1
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "c5327d228d42dad14f6185dd57e05dde",
|
"content-hash": "54a99d812eb7e5412ada7f4366c3065c",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "archtechx/enums",
|
"name": "archtechx/enums",
|
||||||
@@ -1568,6 +1568,150 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-02-03T10:55:03+00:00"
|
"time": "2025-02-03T10:55:03+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "intervention/gif",
|
||||||
|
"version": "5.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Intervention/gif.git",
|
||||||
|
"reference": "bb395af960deffe64d70c976b4df9283f68e762d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Intervention/gif/zipball/bb395af960deffe64d70c976b4df9283f68e762d",
|
||||||
|
"reference": "bb395af960deffe64d70c976b4df9283f68e762d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^8.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^2.1",
|
||||||
|
"phpunit/phpunit": "^12.0",
|
||||||
|
"slevomat/coding-standard": "~8.0",
|
||||||
|
"squizlabs/php_codesniffer": "^4"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Intervention\\Gif\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Oliver Vogel",
|
||||||
|
"email": "oliver@intervention.io",
|
||||||
|
"homepage": "https://intervention.io/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP GIF Encoder/Decoder",
|
||||||
|
"homepage": "https://github.com/intervention/gif",
|
||||||
|
"keywords": [
|
||||||
|
"animation",
|
||||||
|
"gd",
|
||||||
|
"gif",
|
||||||
|
"image"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Intervention/gif/issues",
|
||||||
|
"source": "https://github.com/Intervention/gif/tree/5.0.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://paypal.me/interventionio",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Intervention",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://ko-fi.com/interventionphp",
|
||||||
|
"type": "ko_fi"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-03T06:04:47+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "intervention/image",
|
||||||
|
"version": "4.1.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Intervention/image.git",
|
||||||
|
"reference": "ba4a7cc8042882d479a78b0835f3f0e991e40a71"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Intervention/image/zipball/ba4a7cc8042882d479a78b0835f3f0e991e40a71",
|
||||||
|
"reference": "ba4a7cc8042882d479a78b0835f3f0e991e40a71",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"intervention/gif": "^5",
|
||||||
|
"php": "^8.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"phpstan/phpstan": "^2.1",
|
||||||
|
"phpunit/phpunit": "^12.0",
|
||||||
|
"slevomat/coding-standard": "~8.0",
|
||||||
|
"squizlabs/php_codesniffer": "^4"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-exif": "Recommended to be able to read EXIF data properly."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Intervention\\Image\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Oliver Vogel",
|
||||||
|
"email": "oliver@intervention.io",
|
||||||
|
"homepage": "https://intervention.io"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP Image Processing",
|
||||||
|
"homepage": "https://image.intervention.io",
|
||||||
|
"keywords": [
|
||||||
|
"gd",
|
||||||
|
"image",
|
||||||
|
"imagick",
|
||||||
|
"resize",
|
||||||
|
"thumbnail",
|
||||||
|
"watermark"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Intervention/image/issues",
|
||||||
|
"source": "https://github.com/Intervention/image/tree/4.1.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://paypal.me/interventionio",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Intervention",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://ko-fi.com/interventionphp",
|
||||||
|
"type": "ko_fi"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-23T06:51:28+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel-lang/actions",
|
"name": "laravel-lang/actions",
|
||||||
"version": "1.10.2",
|
"version": "1.10.2",
|
||||||
|
|||||||
@@ -81,5 +81,6 @@ class ProductosInicialesSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->command->info('¡Productos y proveedor insertados correctamente!');
|
$this->command->info('¡Productos y proveedor insertados correctamente!');
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,3 +23,12 @@
|
|||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
RewriteRule ^ index.php [L]
|
RewriteRule ^ index.php [L]
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule mod_php.c>
|
||||||
|
php_value upload_max_filesize 20M
|
||||||
|
php_value post_max_size 20M
|
||||||
|
php_value memory_limit 512M
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
upload_max_filesize = 20M
|
||||||
|
post_max_size = 20M
|
||||||
|
max_execution_time = 300
|
||||||
|
memory_limit = 256M
|
||||||
@@ -120,3 +120,21 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Ver contraseña, con JQuery
|
||||||
|
$(document).on('click', '#togglePassword', function () {
|
||||||
|
const input = $('#passwordInput');
|
||||||
|
const isPassword = input.attr('type') === 'password';
|
||||||
|
|
||||||
|
input.attr('type', isPassword ? 'text' : 'password');
|
||||||
|
$('#iconShow').toggleClass('hidden', isPassword);
|
||||||
|
$('#iconHide').toggleClass('hidden', !isPassword);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#togglePasswordConfirm', function () {
|
||||||
|
const input = $('#passwordConfirmInput');
|
||||||
|
const isPassword = input.attr('type') === 'password';
|
||||||
|
|
||||||
|
input.attr('type', isPassword ? 'text' : 'password');
|
||||||
|
$('#iconShowConfirm').toggleClass('hidden', isPassword);
|
||||||
|
$('#iconHideConfirm').toggleClass('hidden', !isPassword);
|
||||||
|
});
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
<x-layout title="Respaldos - Lauck">
|
||||||
|
|
||||||
|
<x-section-header subtitle="Respaldos" title="Administración de " highlight="Copias de Seguridad" />
|
||||||
|
|
||||||
|
<!-- Estadísticas de Disco y Resumen -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 w-full mb-8">
|
||||||
|
|
||||||
|
<!-- Tarjeta Espacio Total Utilizado -->
|
||||||
|
<div class="bg-gray-100 dark:bg-panel-bg p-6 rounded-xl border border-gray-300 dark:border-neutral-800 shadow-md">
|
||||||
|
<span class="text-xs font-bold text-gray-500 uppercase tracking-widest block mb-1">Espacio en Copias</span>
|
||||||
|
<div class="text-3xl font-black text-gray-900 dark:text-white font-mono">
|
||||||
|
{{ $diskStats['total_backups_size'] }}
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-600 dark:text-gray-400 mt-2">Total acumulado de archivos .zip</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tarjeta Espacio Libre en Disco -->
|
||||||
|
<div class="bg-gray-100 dark:bg-panel-bg p-6 rounded-xl border border-gray-300 dark:border-neutral-800 shadow-md">
|
||||||
|
<span class="text-xs font-bold text-gray-500 uppercase tracking-widest block mb-1">Espacio Libre del Servidor</span>
|
||||||
|
<div class="text-3xl font-black text-green-600 dark:text-neon-lime font-mono">
|
||||||
|
{{ $diskStats['free_space'] }}
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-600 dark:text-gray-400 mt-2">De un total de {{ $diskStats['total_space'] }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tarjeta Estado del Disco -->
|
||||||
|
<div class="bg-gray-100 dark:bg-panel-bg p-6 rounded-xl border border-gray-300 dark:border-neutral-800 shadow-md flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<span class="text-xs font-bold text-gray-500 uppercase tracking-widest block mb-1">Capacidad de Almacenamiento</span>
|
||||||
|
<div class="flex justify-between text-xs text-gray-700 dark:text-gray-300 font-mono mt-1">
|
||||||
|
<span>Libre: {{ $diskStats['free_percentage'] }}%</span>
|
||||||
|
<span>Usado: {{ 100 - $diskStats['free_percentage'] }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-gray-300 dark:bg-neutral-800 rounded-full h-2.5 mt-2 overflow-hidden">
|
||||||
|
<div class="bg-green-500 dark:bg-neon-lime h-2.5 rounded-full" style="width: {{ $diskStats['free_percentage'] }}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Acciones (Subir / Generar) -->
|
||||||
|
<div class="w-full flex flex-col md:flex-row justify-between items-center gap-4 mb-6 bg-gray-100 dark:bg-panel-bg p-4 rounded-xl border border-gray-300 dark:border-neutral-800 shadow-md">
|
||||||
|
|
||||||
|
<!-- Formulario de Subida -->
|
||||||
|
<div class="w-full md:w-auto">
|
||||||
|
<form action="{{ route('admin.backups.upload') }}" method="POST" enctype="multipart/form-data" class="flex flex-col sm:flex-row gap-2 items-center">
|
||||||
|
@csrf
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label class="block text-[10px] font-bold text-gray-500 uppercase mb-1">Subir Copia de Seguridad (.zip)</label>
|
||||||
|
<input type="file" name="backup_file" accept=".zip" required class="block w-full text-xs text-gray-900 dark:text-gray-300 border border-gray-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 focus:outline-none file:mr-4 file:py-2 file:px-4 file:border-0 file:text-xs file:font-bold file:bg-neutral-950 dark:file:bg-neon-lime file:text-neon-lime dark:file:text-neutral-900 hover:file:bg-neutral-900 hover:dark:file:bg-[#b3e600] cursor-pointer" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full sm:w-auto px-5 py-2 text-xs font-bold text-neon-lime dark:text-neutral-900 bg-neutral-950 dark:bg-neon-lime rounded-lg hover:bg-neutral-900/80 hover:dark:bg-[#b3e600] uppercase tracking-wide transition-colors self-end mt-4 sm:mt-0">
|
||||||
|
Subir Respaldo
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botón Generar Copia -->
|
||||||
|
<div class="w-full md:w-auto flex justify-end">
|
||||||
|
<form action="{{ route('admin.backups.create') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="w-full sm:w-auto text-center px-6 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/80 hover:dark:bg-[#b3e600] transition-colors uppercase tracking-wide flex items-center gap-2 shadow-lg dark:shadow-neon-lime/10">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
|
||||||
|
</svg>
|
||||||
|
Generar Nueva Copia
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Listado de Archivos -->
|
||||||
|
<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">Nombre del Archivo</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Fecha de Creación</th>
|
||||||
|
<th scope="col" class="px-6 py-3 font-mono">Tamaño</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-right">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($backups as $backup)
|
||||||
|
<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 whitespace-nowrap">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<!-- Icono archivo zip -->
|
||||||
|
<svg class="size-6 text-gray-500 group-hover:text-gray-900 dark:group-hover:text-neon-lime transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="font-semibold font-mono text-sm tracking-tight text-gray-900 dark:text-white">
|
||||||
|
{{ $backup['filename'] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<span class="text-xs text-gray-700 dark:text-gray-300">
|
||||||
|
{{ $backup['created_at'] }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 font-mono text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
{{ $backup['size'] }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-right flex items-center justify-end gap-2">
|
||||||
|
<!-- Restaurar -->
|
||||||
|
<form action="{{ route('admin.backups.restore', $backup['filename']) }}" method="POST" class="restore-form inline">
|
||||||
|
@csrf
|
||||||
|
<button title="Restaurar Base de Datos" type="submit" class="p-2 font-bold dark:font-medium text-amber-600 dark:text-amber-400 border-2 border-amber-600 rounded-lg hover:bg-amber-600 hover:text-white dark:hover:text-white transition-colors">
|
||||||
|
<svg class="size-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8m0 0H15.5M21 8V2m0 10a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16m0 0h5.5M3 16v6"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Descargar -->
|
||||||
|
<a href="{{ route('admin.backups.download', $backup['filename']) }}" title="Descargar" class="font-semibold p-2 text-blue-700 dark:text-blue-400 border-2 border-blue-700 rounded-lg hover:bg-blue-700 hover:text-white dark:hover:text-white transition-colors">
|
||||||
|
<svg class="size-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M4 16v1a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3v-1m-4-4-4 4m0 0-4-4m4 4V4"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Eliminar -->
|
||||||
|
<form action="{{ route('admin.backups.destroy', $backup['filename']) }}" method="POST" class="delete-form inline">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button title="Eliminar" type="submit" class="p-2 font-bold dark:font-medium text-red-600 dark:text-red-400 border-2 border-red-600 rounded-lg hover:bg-red-600 hover:text-white dark:hover:text-white transition-colors">
|
||||||
|
<svg class="size-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 7h14m-9 3v8m4-8v8M10 3h4a1 1 0 0 1 1 1v3H9V4a1 1 0 0 1 1-1ZM6 7h12v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7Z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="px-6 py-10 text-center text-gray-500">
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mb-3 text-gray-400 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-sm font-medium">No se han generado copias de seguridad de la base de datos todavía.</p>
|
||||||
|
<p class="text-xs text-gray-400 mt-1">Haga clic en "Generar Nueva Copia" para iniciar una.</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Script para confirmation modal -->
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const restoreForms = document.querySelectorAll('.restore-form');
|
||||||
|
restoreForms.forEach(form => {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (confirm('¡ATENCIÓN! Está a punto de restaurar la base de datos. Esta acción SOBREESCRIBIRÁ todos los datos actuales con los del archivo de respaldo. ¿Desea continuar?')); else return;
|
||||||
|
this.submit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
</x-layout>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="{{ route('taller.index') }}"
|
<a href="{{ route('taller.index') }}"
|
||||||
class="px-5 py-2.5 bg-neutral-900 text-white hover:bg-neutral-700 dark:bg-neon-lime dark:text-neutral-900 dark:hover:bg-[#b3e600] font-bold rounded-lg uppercase tracking-wide text-xs flex items-center gap-2">
|
class="w-full xl: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/80 hover:dark:bg-[#b3e600] transition-colors uppercase tracking-wide">
|
||||||
Volver a Taller
|
Volver a Taller
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
<a id="viewEventBtn"
|
<a id="viewEventBtn"
|
||||||
class="px-4 py-2 font-bold rounded-lg
|
class="px-4 py-2 font-bold rounded-lg
|
||||||
bg-neutral-900 hover:bg-neutral-700 text-white
|
bg-neutral-900 hover:bg-neutral-700 text-neon-lime
|
||||||
dark:bg-neon-lime dark:hover:bg-[#b3e600] dark:text-neutral-900">
|
dark:bg-neon-lime dark:hover:bg-[#b3e600] dark:text-neutral-900">
|
||||||
Ver turno
|
Ver turno
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -78,8 +78,8 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex-none gap-2">
|
||||||
<button type="submit" class="px-5 py-3 text-sm font-bold text-neutral-900 bg-neon-lime rounded-lg hover:bg-[#b3e600] transition-colors">
|
<button type="submit" class="w-full md:w-auto text-center px-10 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/80 hover:dark:bg-[#b3e600] transition-colors uppercase tracking-wide">
|
||||||
Filtrar
|
Filtrar
|
||||||
</button>
|
</button>
|
||||||
@if(request('search') || request('type') || request('tag'))
|
@if(request('search') || request('type') || request('tag'))
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
|
|
||||||
{{-- Botón --}}
|
{{-- Botón --}}
|
||||||
<a href="{{ route('catalogo.show', $product->id) }}"
|
<a href="{{ route('catalogo.show', $product->id) }}"
|
||||||
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">
|
class="font-bold uppercase mt-4 block w-full bg-neutral-900 dark:bg-neon-lime/80 text-neon-lime dark:text-black 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 Detalle
|
Ver Detalle
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
<div class="flex items-center justify-between md:justify-end space-x-4 border-t border-neutral-400 dark:border-neutral-700 pt-6">
|
<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('clients.index') }}" class="text-gray-800 dark:text-gray-400 hover:text-black hover:dark:text-white font-medium text-sm transition-colors">Cancelar</a>
|
<a href="{{ route('clients.index') }}" class="text-gray-800 dark:text-gray-400 hover:text-black hover:dark:text-white font-medium text-sm transition-colors">Cancelar</a>
|
||||||
<button type="submit" class="px-4 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">
|
<button type="submit" class="w-full xl: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/80 hover:dark:bg-[#b3e600] transition-colors uppercase tracking-wide">
|
||||||
Actualizar Cliente
|
Actualizar Cliente
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
@php
|
@php
|
||||||
$borderColor = match($color) {
|
$borderColor = match($color) {
|
||||||
'red' => 'border-l-red-500',
|
'red' => 'border-l-red-500 dark:border-l-red-500',
|
||||||
'yellow' => 'border-l-yellow-500',
|
'yellow' => 'border-l-yellow-500 dark:border-l-yellow-500',
|
||||||
'neon' => 'border-l-neon-lime',
|
'neon' => 'border-l-neon-lime dark:border-l-neon-lime',
|
||||||
default => 'border-l-gray-500'
|
default => 'border-l-gray-500 dark:border-l-gray-500'
|
||||||
};
|
};
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,14 @@
|
|||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16v-6M8 16V6m4 10v-3m4 3v-8m4 8v-5M4 20h16"/>
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16v-6M8 16V6m4 10v-3m4 3v-8m4 8v-5M4 20h16"/>
|
||||||
</svg>
|
</svg>
|
||||||
</x-ui.card>
|
</x-ui.card>
|
||||||
|
@if(auth()->user()->role === 'admin')
|
||||||
|
<!-- Tarjeta Respaldos (Solo Admin) -->
|
||||||
|
<x-ui.card href="{{ route('admin.backups.index') }}" title="Respaldos" description="Copias de seguridad de la base de datos." linkText="Administrar Copias">
|
||||||
|
<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="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2" />
|
||||||
|
</svg>
|
||||||
|
</x-ui.card>
|
||||||
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,38 @@
|
|||||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Genera nuevas ventas, consulta el historial, y gestiona la base de datos de tus clientes para ofrecer un servicio más personalizado.</p>
|
<p class="text-gray-600 dark:text-gray-400 mt-1">Genera nuevas ventas, consulta el historial, y gestiona la base de datos de tus clientes para ofrecer un servicio más personalizado.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Punto 5 -->
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-lime-100 dark:bg-lime-900/30 flex items-center justify-center text-lime-600 dark:text-neon-lime font-bold">5</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Proveedores y Pedidos</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mt-1">Administra la información de tus proveedores y genera órdenes de compra para reabastecer tu inventario de forma organizada.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Punto 6 -->
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-lime-100 dark:bg-lime-900/30 flex items-center justify-center text-lime-600 dark:text-neon-lime font-bold">6</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Control de Gastos</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mt-1">Registra los gastos operativos del local (servicios, insumos, etc.) para tener un control financiero preciso de tu negocio.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Punto 7 -->
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-lime-100 dark:bg-lime-900/30 flex items-center justify-center text-lime-600 dark:text-neon-lime font-bold">7</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Agenda de Turnos</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mt-1">Organiza las reservas y turnos de los clientes mediante un calendario integrado, facilitando la planificación del tiempo en el taller.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Punto 8 -->
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-lime-100 dark:bg-lime-900/30 flex items-center justify-center text-lime-600 dark:text-neon-lime font-bold">8</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Copias de Seguridad (Admin)</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mt-1">Si tienes rol de administrador, puedes generar, descargar y restaurar copias de seguridad de la base de datos para proteger la información del sistema.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -101,13 +133,39 @@
|
|||||||
<!-- FAQ 4 -->
|
<!-- FAQ 4 -->
|
||||||
<details class="group bg-white dark:bg-neutral-900 border border-gray-200 dark:border-neutral-800 rounded-lg shadow-sm [&_summary::-webkit-details-marker]:hidden">
|
<details class="group bg-white dark:bg-neutral-900 border border-gray-200 dark:border-neutral-800 rounded-lg shadow-sm [&_summary::-webkit-details-marker]:hidden">
|
||||||
<summary class="flex items-center justify-between p-5 cursor-pointer font-semibold text-gray-900 dark:text-white transition-colors hover:bg-gray-50 dark:hover:bg-neutral-800/50 rounded-lg">
|
<summary class="flex items-center justify-between p-5 cursor-pointer font-semibold text-gray-900 dark:text-white transition-colors hover:bg-gray-50 dark:hover:bg-neutral-800/50 rounded-lg">
|
||||||
<span>¿Cómo cambiar entre modo claro y oscuro?</span>
|
<span>¿Puedo cambiar el modo oscuro de la página por uno claro?</span>
|
||||||
<span class="transition-transform duration-300 group-open:-rotate-180 text-gray-500">
|
<span class="transition-transform duration-300 group-open:-rotate-180 text-gray-500">
|
||||||
<svg fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" viewBox="0 0 24 24" width="24"><path d="M6 9l6 6 6-6"></path></svg>
|
<svg fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" viewBox="0 0 24 24" width="24"><path d="M6 9l6 6 6-6"></path></svg>
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="px-5 pb-5 pt-2 text-gray-600 dark:text-gray-400 border-t border-gray-100 dark:border-neutral-800/50">
|
<div class="px-5 pb-5 pt-2 text-gray-600 dark:text-gray-400 border-t border-gray-100 dark:border-neutral-800/50">
|
||||||
<p>En la barra de navegación superior (Navbar), encontrarás un icono de un sol o una luna (dependiendo del modo actual en el que te encuentres). Haz clic sobre el ícono para alternar entre el tema claro y el oscuro. El sistema recordará tu preferencia para futuras sesiones.</p>
|
<p>Si. El sistema se sincroniza automáticamente con el tema de tu dispositivo. Para alternar entre ambos modos, modificá la preferencia visual directamente desde la configuración de tu navegador web.</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- FAQ 5 -->
|
||||||
|
<details class="group bg-white dark:bg-neutral-900 border border-gray-200 dark:border-neutral-800 rounded-lg shadow-sm [&_summary::-webkit-details-marker]:hidden">
|
||||||
|
<summary class="flex items-center justify-between p-5 cursor-pointer font-semibold text-gray-900 dark:text-white transition-colors hover:bg-gray-50 dark:hover:bg-neutral-800/50 rounded-lg">
|
||||||
|
<span>¿Cómo funciona el registro de gastos?</span>
|
||||||
|
<span class="transition-transform duration-300 group-open:-rotate-180 text-gray-500">
|
||||||
|
<svg fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" viewBox="0 0 24 24" width="24"><path d="M6 9l6 6 6-6"></path></svg>
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div class="px-5 pb-5 pt-2 text-gray-600 dark:text-gray-400 border-t border-gray-100 dark:border-neutral-800/50">
|
||||||
|
<p>En la sección <strong>Gastos</strong>, puedes registrar las salidas de dinero del negocio (por ejemplo, pago de servicios, compra de insumos, mantenimiento). Esto te ayudará a tener una visión más clara de la rentabilidad real de la tienda.</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- FAQ 6 -->
|
||||||
|
<details class="group bg-white dark:bg-neutral-900 border border-gray-200 dark:border-neutral-800 rounded-lg shadow-sm [&_summary::-webkit-details-marker]:hidden">
|
||||||
|
<summary class="flex items-center justify-between p-5 cursor-pointer font-semibold text-gray-900 dark:text-white transition-colors hover:bg-gray-50 dark:hover:bg-neutral-800/50 rounded-lg">
|
||||||
|
<span>¿Es seguro el sistema? ¿Cómo evito perder información?</span>
|
||||||
|
<span class="transition-transform duration-300 group-open:-rotate-180 text-gray-500">
|
||||||
|
<svg fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" viewBox="0 0 24 24" width="24"><path d="M6 9l6 6 6-6"></path></svg>
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div class="px-5 pb-5 pt-2 text-gray-600 dark:text-gray-400 border-t border-gray-100 dark:border-neutral-800/50">
|
||||||
|
<p>Los administradores tienen acceso al módulo de <strong>Copias de Seguridad (Backups)</strong>. Desde allí, puedes generar y descargar respaldos de tu base de datos en cualquier momento. Te recomendamos hacer copias periódicas por seguridad y resguardarlas fuera del servidor.</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,8 +36,27 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-2">Contraseña</label>
|
<label class="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-2">Contraseña</label>
|
||||||
<input type="password" name="password" required
|
<div class="relative">
|
||||||
class="w-full px-4 py-2 bg-white dark:bg-neutral-900 border border-gray-300 dark:border-neutral-700 text-gray-900 dark:text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-neon-lime focus:border-neon-lime transition-colors"/>
|
<input id="passwordInput" type="password" name="password" required
|
||||||
|
class="w-full px-4 py-2 bg-white dark:bg-neutral-900 border border-gray-300 dark:border-neutral-700 text-gray-900 dark:text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-neon-lime focus:border-neon-lime transition-colors pr-10"/>
|
||||||
|
<button type="button" id="togglePassword"
|
||||||
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-700 dark:hover:text-white transition-colors">
|
||||||
|
{{-- Ojo abierto --}}
|
||||||
|
<svg id="iconShow" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
{{-- Ojo cerrado --}}
|
||||||
|
<svg id="iconHide" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 hidden" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.477 0-8.268-2.943-9.542-7a9.956 9.956 0 012.293-3.95M6.938 6.938A9.956 9.956 0 0112 5c4.477 0 8.268 2.943 9.542 7a9.97 9.97 0 01-1.88 3.118M3 3l18 18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end mt-2">
|
<div class="flex justify-end mt-2">
|
||||||
<a href="{{ route('password.request') }}" class="text-xs font-medium text-green-600 dark:text-neon-lime hover:underline transition-colors">
|
<a href="{{ route('password.request') }}" class="text-xs font-medium text-green-600 dark:text-neon-lime hover:underline transition-colors">
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
<!-- Botones Acción -->
|
<!-- Botones Acción -->
|
||||||
<div class="flex items-center justify-between md:justify-end space-x-4 border-t border-neutral-400 dark:border-neutral-700 pt-6">
|
<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-800 dark:text-gray-400 hover:text-black hover:dark:text-white font-medium text-sm transition-colors">Cancelar</a>
|
<a href="{{ route('productos.index') }}" class="text-gray-800 dark:text-gray-400 hover:text-black hover:dark:text-white font-medium text-sm transition-colors">Cancelar</a>
|
||||||
<button type="submit" class="px-4 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">
|
<button type="submit" class="w-full xl: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/80 hover:dark:bg-[#b3e600] transition-colors uppercase tracking-wide">
|
||||||
Guardar
|
Guardar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<x-layout title="Lauck - Stock">
|
<x-layout title="Lauck - Stock">
|
||||||
|
|
||||||
<x-section-header subtitle="Gestión de Inventario" title="Listado de " highlight="Productos" />
|
<x-section-header subtitle="Gestión de Inventario" title="Listado de " highlight="Productos" />
|
||||||
|
|
||||||
|
|
||||||
@@ -46,7 +46,8 @@
|
|||||||
|
|
||||||
<div class="relative overflow-x-auto shadow-md sm:rounded-lg border border-gray-400 dark:border-neutral-800 w-full mb-4">
|
<div class="relative overflow-x-auto shadow-md sm:rounded-lg border border-gray-400 dark:border-neutral-800 w-full mb-4">
|
||||||
<table class="w-full text-sm text-left rtl:text-right text-neutral-900 dark:text-white">
|
<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">
|
<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>
|
<tr>
|
||||||
<th scope="col" class="px-6 py-3">
|
<th scope="col" class="px-6 py-3">
|
||||||
<div class="flex items-center gap-6">
|
<div class="flex items-center gap-6">
|
||||||
@@ -136,7 +137,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between px-4 py-3 bg-gray-200 dark:bg-neutral-900 border border-gray-400 dark:border-neutral-800 rounded-lg">
|
<div class="flex items-center justify-between px-4 py-3 bg-gray-200 dark:bg-neutral-900 border border-gray-400 dark:border-neutral-800 rounded-lg">
|
||||||
<div class="text-sm text-gray-700 dark:text-gray-400 font-semibold" id="pagination-info">
|
<div class="text-sm text-gray-700 dark:text-gray-400 font-semibold mx-2" id="pagination-info">
|
||||||
Cargando productos...
|
Cargando productos...
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -239,4 +240,4 @@
|
|||||||
renderPage();
|
renderPage();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</x-layout>
|
</x-layout>
|
||||||
|
|||||||
@@ -1,95 +1,110 @@
|
|||||||
<x-layout title="Register - Lauck">
|
<x-layout title="Register - Lauck">
|
||||||
|
|
||||||
<div class="justify-center">
|
<div class="justify-center">
|
||||||
<x-section-header
|
<x-section-header subtitle="Crear una cuenta" title="Bicicletería " highlight="Lauck" />
|
||||||
subtitle="Crear una cuenta"
|
</div>
|
||||||
title="Bicicletería "
|
|
||||||
highlight="Lauck"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col items-center space-y-6 w-full max-w-lg p-6">
|
<div class="flex flex-col items-center space-y-6 w-full max-w-lg p-6">
|
||||||
<div class="bg-zinc-900 p-8 rounded-xl shadow-lg w-full max-w-md border-2 border-gray-500">
|
<div class="bg-zinc-900 p-8 rounded-xl shadow-lg w-full max-w-md border-2 border-gray-500">
|
||||||
|
|
||||||
<form action="{{ route('register.store') }}" method="POST" class="space-y-5">
|
<form action="{{ route('register.store') }}" method="POST" class="space-y-5">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
{{-- Bloque de errores --}}
|
{{-- Bloque de errores --}}
|
||||||
@if ($errors->any())
|
@if ($errors->any())
|
||||||
<div class="bg-red-900 border border-red-700 text-white p-3 rounded-md">
|
<div class="bg-red-900 border border-red-700 text-white p-3 rounded-md">
|
||||||
<p class="text-sm font-medium">
|
<p class="text-sm font-medium">
|
||||||
{{ $errors->first() }}
|
{{ $errors->first() }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<x-forms.label class="block text-sm font-bold text-gray-600 mb-1">
|
<x-forms.label class="block text-sm font-bold text-gray-600 mb-1">
|
||||||
Nombre completo
|
Nombre completo
|
||||||
</x-forms.label>
|
</x-forms.label>
|
||||||
<x-forms.input
|
<x-forms.input type="text" value="{{ old('name') }}" name="name" required
|
||||||
type="text"
|
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-lime-400 text-gray-600" />
|
||||||
value="{{ old('name') }}"
|
|
||||||
name="name"
|
|
||||||
required
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-lime-400 text-gray-600"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<x-forms.label class="block text-sm font-bold text-gray-600 mb-1">
|
<x-forms.label class="block text-sm font-bold text-gray-600 mb-1">
|
||||||
Correo electrónico
|
Correo electrónico
|
||||||
</x-forms.label>
|
</x-forms.label>
|
||||||
<x-forms.input
|
<x-forms.input type="email" value="{{ old('email') }}" name="email" required
|
||||||
type="email"
|
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-lime-400 text-gray-600" />
|
||||||
value="{{ old('email') }}"
|
</div>
|
||||||
name="email"
|
|
||||||
required
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-lime-400 text-gray-600"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<x-forms.label class="block text-sm font-bold text-gray-600 mb-1">
|
<x-forms.label class="block text-sm font-bold text-gray-600 mb-1">
|
||||||
Contraseña
|
Contraseña
|
||||||
</x-forms.label>
|
</x-forms.label>
|
||||||
<x-forms.input
|
<div class="relative">
|
||||||
type="password"
|
<x-forms.input id="passwordInput" type="password" name="password" required
|
||||||
name="password"
|
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-lime-400 text-gray-600" />
|
||||||
required
|
<button type="button" id="togglePassword"
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-lime-400 text-gray-600"
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-700 dark:hover:text-white transition-colors">
|
||||||
/>
|
{{-- Ojo abierto --}}
|
||||||
</div>
|
<svg id="iconShow" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
{{-- Ojo cerrado --}}
|
||||||
|
<svg id="iconHide" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 hidden" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.477 0-8.268-2.943-9.542-7a9.956 9.956 0 012.293-3.95M6.938 6.938A9.956 9.956 0 0112 5c4.477 0 8.268 2.943 9.542 7a9.97 9.97 0 01-1.88 3.118M3 3l18 18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
</div>
|
||||||
<x-forms.label class="block text-sm font-bold text-gray-600 mb-1">
|
|
||||||
Confirmar contraseña
|
|
||||||
</x-forms.label>
|
|
||||||
<x-forms.input
|
|
||||||
type="password"
|
|
||||||
name="password_confirmation"
|
|
||||||
required
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-lime-400 text-gray-600"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<input
|
<x-forms.label class="block text-sm font-bold text-gray-600 mb-1">
|
||||||
type="submit"
|
Confirmar contraseña
|
||||||
value="Registrarse"
|
</x-forms.label>
|
||||||
class="w-full bg-lime-400 font-bold text-white py-2 rounded-md hover:bg-lime-500 transition-colors"
|
<div class="relative">
|
||||||
>
|
<x-forms.input id="passwordConfirmInput" type="password" name="password_confirmation" required
|
||||||
</div>
|
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-lime-400 text-gray-600 pr-10" />
|
||||||
|
|
||||||
<p class="text-center text-sm text-gray-600">
|
<button type="button" id="togglePasswordConfirm"
|
||||||
¿Ya tenés una cuenta?
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-700 dark:hover:text-white transition-colors">
|
||||||
<a href="{{ route('login') }}" class="text-blue-600 hover:underline">
|
{{-- Ojo abierto --}}
|
||||||
Iniciá sesión acá
|
<svg id="iconShowConfirm" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none"
|
||||||
</a>.
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</p>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
</form>
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
{{-- Ojo cerrado --}}
|
||||||
|
<svg id="iconHideConfirm" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 hidden" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.477 0-8.268-2.943-9.542-7a9.956 9.956 0 012.293-3.95M6.938 6.938A9.956 9.956 0 0112 5c4.477 0 8.268 2.943 9.542 7a9.97 9.97 0 01-1.88 3.118M3 3l18 18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="submit" value="Registrarse"
|
||||||
|
class="w-full bg-lime-400 font-bold text-white py-2 rounded-md hover:bg-lime-500 transition-colors">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-center text-sm text-gray-600">
|
||||||
|
¿Ya tenés una cuenta?
|
||||||
|
<a href="{{ route('login') }}" class="text-blue-600 hover:underline">
|
||||||
|
Iniciá sesión acá
|
||||||
|
</a>.
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</x-layout>
|
</x-layout>
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
<x-layout title="Historial de Ventas">
|
<x-layout title="Historial de Ventas">
|
||||||
|
|
||||||
<x-section-header subtitle="Finanzas" title="Historial de " highlight="Ventas" />
|
<x-section-header subtitle="Finanzas" title="Historial de " highlight="Ventas" />
|
||||||
|
|
||||||
<div class="mb-6 bg-gray-200/50 dark:bg-neutral-800/50 p-4 rounded-xl border border-neutral-400 dark:border-neutral-800 shadow-sm">
|
<div
|
||||||
|
class="mb-6 bg-gray-200/50 dark:bg-neutral-800/50 p-4 rounded-xl border border-neutral-400 dark:border-neutral-800 shadow-sm">
|
||||||
<form action="{{ route('sales.index') }}" method="GET" class="grid grid-cols-1 md:grid-cols-12 gap-4 items-end">
|
<form action="{{ route('sales.index') }}" method="GET" class="grid grid-cols-1 md:grid-cols-12 gap-4 items-end">
|
||||||
|
|
||||||
<div class="md:col-span-3">
|
<div class="md:col-span-2">
|
||||||
<x-forms.label value="N° Venta"/>
|
<x-forms.label value="N° Venta" />
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
<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" fill="none" viewBox="0 0 20 20">
|
<svg class="w-4 h-4 text-gray-500" 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"/>
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<x-forms.input type="text" name="search" value="{{ request('search') }}" class="ps-10" placeholder="Ej: 1420"/>
|
<x-forms.input type="text" name="search" value="{{ request('search') }}" class="ps-10"
|
||||||
|
placeholder="Ej: 1420" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:col-span-4">
|
<div class="md:col-span-5">
|
||||||
<x-forms.label value="Cliente"/>
|
<x-forms.label value="Cliente" />
|
||||||
<select name="client_id" class="w-full select2-filter">
|
<select name="client_id" class="w-full select2-filter block p-2.5 text-sm rounded-lg border border-neutral-400 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white bg-gray-300">
|
||||||
<option value="">-- Todos --</option>
|
<option value="">-- Todos --</option>
|
||||||
@foreach($clients as $client)
|
@foreach ($clients as $client)
|
||||||
<option value="{{ $client->id }}" {{ request('client_id') == $client->id ? 'selected' : '' }}>
|
<option value="{{ $client->id }}" {{ request('client_id') == $client->id ? 'selected' : '' }}>
|
||||||
{{ $client->name }}
|
{{ $client->name }}
|
||||||
</option>
|
</option>
|
||||||
@@ -30,25 +33,32 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<x-forms.label value="Desde"/>
|
<x-forms.label value="Desde" />
|
||||||
<x-forms.input type="date" name="date_from" value="{{ request('date_from') }}"/>
|
<x-forms.input type="date" name="date_from" value="{{ request('date_from') }}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<x-forms.label value="Hasta"/>
|
<x-forms.label value="Hasta" />
|
||||||
<x-forms.input type="date" name="date_to" value="{{ request('date_to') }}"/>
|
<x-forms.input type="date" name="date_to" value="{{ request('date_to') }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:col-span-1 flex gap-2">
|
<div class="md:col-span-1 flex gap-2">
|
||||||
<button type="submit" class="w-full bg-neutral-900 hover:bg-neutral-800 text-neon-lime dark:bg-neon-lime dark:hover:bg-[#b3e600] dark:text-neutral-900 font-bold rounded-lg text-sm p-2.5 transition-colors shadow-lg dark:shadow-neon-lime/20 flex items-center justify-center" title="Aplicar Filtros">
|
<button type="submit" title="Aplicar Filtros"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
class="w-full flex items-center justify-center p-2.5 rounded-lg transition-colors text-sm font-bold uppercase tracking-widebg-neutral-950 text-neon-lime hover:bg-neutral-900/80 dark:bg-neon-lime dark:text-neutral-900 dark:hover:bg-[#b3e600] bg-black">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@if(request()->hasAny(['search', 'client_id', 'date_from', 'date_to']))
|
@if (request()->hasAny(['search', 'client_id', 'date_from', 'date_to']))
|
||||||
<a href="{{ route('sales.index') }}" class="w-full hover:bg-red-600 hover:text-white text-red-600 border border-red-600 font-bold rounded-lg text-sm p-2.5 flex items-center justify-center transition-colors" title="Limpiar Filtros">
|
<a href="{{ route('sales.index') }}"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
class="w-full hover:bg-red-600 hover:text-white text-red-600 border border-red-600 font-bold rounded-lg text-sm p-2.5 flex items-center justify-center transition-colors"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
title="Limpiar Filtros">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
@@ -57,14 +67,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end mb-6">
|
<div class="flex justify-end mb-6">
|
||||||
<a href="{{ route('sales.create') }}" class="px-6 py-2.5 border bg-neon-lime border-neutral-900 text-neutral-900 hover:text-neon-lime hover:bg-neutral-900 dark:bg-neutral-800 dark:border-neon-lime dark:text-neon-lime hover:dark:bg-neon-lime hover:dark:text-neutral-900 font-bold rounded-lg transition-all shadow-lg dark:shadow-neon-lime/10 flex items-center gap-2">
|
<a href="{{ route('sales.create') }}"
|
||||||
<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="M12 4v16m8-8H4"></path></svg>
|
class="flex items-center gap-2 px-6 py-2.5 rounded-lg transition-all text-sm font-bold uppercase tracking-wide bg-neutral-950 text-neon-lime hover:bg-neutral-900/80 dark:bg-neon-lime dark:text-neutral-900 dark:hover:bg-[#b3e600] dark:shadow-neon-lime/10">
|
||||||
|
<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="M12 4v16m8-8H4"></path>
|
||||||
|
</svg>
|
||||||
Nueva Venta
|
Nueva Venta
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative overflow-x-auto shadow-md sm:rounded-lg border border-gray-400 dark:border-neutral-800">
|
<div class="relative overflow-x-auto shadow-md sm:rounded-lg border border-gray-400 dark:border-neutral-800">
|
||||||
<table class="w-full text-sm text-left text-neutral-900 dark:text-white">
|
<table class="w-full text-sm text-left 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">
|
<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>
|
<tr>
|
||||||
<th scope="col" class="px-6 py-3"># Ref</th>
|
<th scope="col" class="px-6 py-3"># Ref</th>
|
||||||
<th scope="col" class="px-6 py-3">Fecha</th>
|
<th scope="col" class="px-6 py-3">Fecha</th>
|
||||||
@@ -76,63 +90,78 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@forelse($sales as $sale)
|
@forelse($sales as $sale)
|
||||||
<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">
|
<tr
|
||||||
<td class="px-6 py-4 font-mono font-bold">
|
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">
|
||||||
#{{ str_pad($sale->id, 5, '0', STR_PAD_LEFT) }}
|
<td class="px-6 py-4 font-mono font-bold">
|
||||||
</td>
|
#{{ str_pad($sale->id, 5, '0', STR_PAD_LEFT) }}
|
||||||
|
</td>
|
||||||
|
|
||||||
<td class="px-6 py-4 text-gray-600 dark:text-gray-300">
|
<td class="px-6 py-4 text-gray-600 dark:text-gray-300">
|
||||||
{{ $sale->created_at->format('d/m/Y H:i') }}
|
{{ $sale->created_at->format('d/m/Y H:i') }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
@if($sale->client)
|
@if ($sale->client)
|
||||||
<div class="font-bold">{{ $sale->client->name }}</div>
|
<div class="font-bold">{{ $sale->client->name }}</div>
|
||||||
<div class="text-xs text-gray-700 dark:text-gray-400">{{ $sale->client->phone ?? '' }}</div>
|
<div class="text-xs text-gray-700 dark:text-gray-400">{{ $sale->client->phone ?? '' }}
|
||||||
@else
|
</div>
|
||||||
<span class="italic text-gray-600 dark:text-gray-400">Consumidor Final</span>
|
@else
|
||||||
@endif
|
<span class="italic text-gray-600 dark:text-gray-400">Consumidor Final</span>
|
||||||
</td>
|
@endif
|
||||||
|
</td>
|
||||||
|
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
@php
|
@php
|
||||||
$colors = [
|
$colors = [
|
||||||
'Efectivo' => 'text-white dark:text-green-400 bg-green-600/80 dark:bg-green-900/20 border-green-800',
|
'Efectivo' =>
|
||||||
'Transferencia' => 'text-white dark:text-blue-400 bg-blue-600/80 dark:bg-blue-900/20 border-blue-800',
|
'text-white dark:text-green-400 bg-green-600/80 dark:bg-green-900/20 border-green-800',
|
||||||
'Tarjeta de Débito' => 'text-white dark:text-purple-400 bg-purple-600/80 dark:bg-purple-900/20 border-purple-800',
|
'Transferencia' =>
|
||||||
'Tarjeta de Crédito' => 'text-white dark:text-orange-400 bg-orange-600/80 dark:bg-orange-900/20 border-orange-800',
|
'text-white dark:text-blue-400 bg-blue-600/80 dark:bg-blue-900/20 border-blue-800',
|
||||||
];
|
'Tarjeta de Débito' =>
|
||||||
$badgeClass = $colors[$sale->payment_method] ?? 'bg-gray-400 dark:bg-gray-800 border-gray-700';
|
'text-white dark:text-purple-400 bg-purple-600/80 dark:bg-purple-900/20 border-purple-800',
|
||||||
@endphp
|
'Tarjeta de Crédito' =>
|
||||||
<span class="{{ $badgeClass }} border px-2.5 py-0.5 rounded text-xs font-medium">
|
'text-white dark:text-orange-400 bg-orange-600/80 dark:bg-orange-900/20 border-orange-800',
|
||||||
{{ $sale->payment_method }}
|
];
|
||||||
</span>
|
$badgeClass =
|
||||||
</td>
|
$colors[$sale->payment_method] ?? 'bg-gray-400 dark:bg-gray-800 border-gray-700';
|
||||||
|
@endphp
|
||||||
|
<span class="{{ $badgeClass }} border px-2.5 py-0.5 rounded text-xs font-medium">
|
||||||
|
{{ $sale->payment_method }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td class="px-6 py-4 text-right font-bold text-lg text-zinc-900 dark:text-neon-lime">
|
<td class="px-6 py-4 text-right font-bold text-lg text-zinc-900 dark:text-neon-lime">
|
||||||
${{ number_format($sale->total, 2) }}
|
${{ number_format($sale->total, 2) }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="px-6 py-4 text-center">
|
<td class="px-6 py-4 text-center">
|
||||||
<a href="{{ route('sales.show', $sale) }}"
|
<a href="{{ route('sales.show', $sale) }}"
|
||||||
class="inline-flex items-center justify-center w-8 h-8 border-2 border-emerald-700 dark:border-green-600 rounded-4xl hover:bg-emerald-700 text-emerald-700 hover:dark:bg-green-600 dark:text-green-600 hover:text-white hover:dark:text-white transition-colors"
|
class="inline-flex items-center justify-center w-8 h-8 border-2 border-emerald-700 dark:border-green-600 rounded-4xl hover:bg-emerald-700 text-emerald-700 hover:dark:bg-green-600 dark:text-green-600 hover:text-white hover:dark:text-white transition-colors"
|
||||||
title="Ver Detalle">
|
title="Ver Detalle">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
</svg>
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
</a>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
</td>
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
</tr>
|
</svg>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="px-6 py-12 text-center text-gray-500">
|
<td colspan="6" class="px-6 py-12 text-center text-gray-500">
|
||||||
<div class="flex flex-col items-center justify-center">
|
<div class="flex flex-col items-center justify-center">
|
||||||
<svg class="w-12 h-12 mb-3 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg>
|
<svg class="w-12 h-12 mb-3 text-gray-600" fill="none" stroke="currentColor"
|
||||||
<p class="text-base">No se encontraron ventas registradas.</p>
|
viewBox="0 0 24 24">
|
||||||
</div>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
</td>
|
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">
|
||||||
</tr>
|
</path>
|
||||||
|
</svg>
|
||||||
|
<p class="text-base">No se encontraron ventas registradas.</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -141,4 +170,4 @@
|
|||||||
<div class="mt-4 w-full max-w-4xl">
|
<div class="mt-4 w-full max-w-4xl">
|
||||||
{{ $sales->links() }}
|
{{ $sales->links() }}
|
||||||
</div>
|
</div>
|
||||||
</x-layout>
|
</x-layout>
|
||||||
|
|||||||
@@ -1,29 +1,37 @@
|
|||||||
<x-layout title="Tablero de Taller">
|
<x-layout title="Tablero de Taller">
|
||||||
|
|
||||||
<x-section-header subtitle="Gestión" title="Tablero de " highlight="Taller" />
|
<x-section-header subtitle="Gestión" title="Tablero de " highlight="Taller" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="flex w-full justify-end items-end mb-4 gap-x-2">
|
<div class="flex w-full justify-end items-end mb-4 gap-x-2">
|
||||||
<a href="{{ route('agenda') }}"
|
<a href="{{ route('agenda') }}"
|
||||||
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">
|
class="w-full xl: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/80 hover:dark:bg-[#b3e600] transition-colors uppercase tracking-wide">
|
||||||
Ver Agenda
|
Ver Agenda
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<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">
|
<a href="{{ route('taller.create') }}" class="w-full xl: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/80 hover:dark:bg-[#b3e600] transition-colors uppercase tracking-wide">
|
||||||
+ Ingresar Bici
|
+ Ingresar Bici
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contenedor -->
|
<!-- Contenedor -->
|
||||||
<div class="flex flex-col lg:flex-row w-full gap-8 overflow-x-auto pb-4 h-[calc(100vh-250px)]">
|
<div class="flex flex-col lg:flex-row w-full gap-8 overflow-x-auto pb-4 h-[calc(100vh-250px)]">
|
||||||
|
|
||||||
<!-- COLUMNA 1 -->
|
<!-- Column 1: Pendientes / A Revisar -->
|
||||||
<div class="flex-1 min-w-[300px] bg-gray-50 dark:bg-neutral-900/50 border border-gray-300 dark:border-neutral-800 rounded-xl flex flex-col">
|
<div class="flex-1 min-w-[300px] flex flex-col rounded-xl border transition-colors
|
||||||
<div class="p-4 border-b border-gray-300 dark:border-neutral-800 bg-gray-200 dark:bg-neutral-800/50 rounded-t-xl flex justify-between items-center">
|
bg-stone-100 border-stone-300
|
||||||
<h3 class="font-bold text-gray-800 dark:text-gray-300 uppercase tracking-widest text-xs">🔴 Pendientes / A Revisar</h3>
|
dark:bg-neutral-900/50 dark:border-neutral-800">
|
||||||
<span class="bg-gray-300 dark:bg-neutral-900 text-gray-700 dark:text-gray-400 text-xs px-2 py-1 rounded-full">{{ $pending->count() }}</span>
|
|
||||||
|
<div class="p-4 flex justify-between items-center border-b rounded-t-xl
|
||||||
|
bg-stone-200/50 border-stone-300
|
||||||
|
dark:bg-neutral-800/50 dark:border-neutral-800">
|
||||||
|
<h3 class="font-bold uppercase tracking-widest text-xs text-red-600 dark:text-gray-300">
|
||||||
|
🔴 Pendientes / A Revisar
|
||||||
|
</h3>
|
||||||
|
<span class="text-xs px-2 py-1 rounded-full bg-stone-300 text-stone-700 dark:bg-neutral-900 dark:text-gray-400">
|
||||||
|
{{ $pending->count() }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 space-y-4 overflow-y-auto flex-1 custom-scrollbar">
|
<div class="p-4 space-y-4 overflow-y-auto flex-1 custom-scrollbar">
|
||||||
@foreach($pending as $job)
|
@foreach($pending as $job)
|
||||||
<x-taller-card :job="$job" color="red" />
|
<x-taller-card :job="$job" color="red" />
|
||||||
@@ -31,12 +39,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- COLUMNA 2 -->
|
<!-- Column 2: En Reparación -->
|
||||||
<div class="flex-1 min-w-[300px] bg-gray-50 dark:bg-neutral-900/50 border border-gray-300 dark:border-neutral-800 rounded-xl flex flex-col">
|
<div class="flex-1 min-w-[300px] flex flex-col rounded-xl border transition-colors
|
||||||
<div class="p-4 border-b border-gray-300 dark:border-neutral-800 bg-gray-200 dark:bg-neutral-800/50 rounded-t-xl flex justify-between items-center">
|
bg-stone-100 border-stone-300
|
||||||
<h3 class="font-bold text-yellow-600 dark:text-yellow-500 uppercase tracking-widest text-xs">🟡 En Reparación</h3>
|
dark:bg-neutral-900/50 dark:border-neutral-800">
|
||||||
<span class="bg-gray-300 dark:bg-neutral-900 text-gray-700 dark:text-gray-400 text-xs px-2 py-1 rounded-full">{{ $inProgress->count() }}</span>
|
|
||||||
|
<div class="p-4 flex justify-between items-center border-b rounded-t-xl
|
||||||
|
bg-stone-200/50 border-stone-300
|
||||||
|
dark:bg-neutral-800/50 dark:border-neutral-800">
|
||||||
|
<h3 class="font-bold uppercase tracking-widest text-xs text-yellow-600 dark:text-yellow-500">
|
||||||
|
🟡 En Reparación
|
||||||
|
</h3>
|
||||||
|
<span class="text-xs px-2 py-1 rounded-full bg-stone-300 text-stone-700 dark:bg-neutral-900 dark:text-gray-400">
|
||||||
|
{{ $inProgress->count() }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 space-y-4 overflow-y-auto flex-1 custom-scrollbar">
|
<div class="p-4 space-y-4 overflow-y-auto flex-1 custom-scrollbar">
|
||||||
@foreach($inProgress as $job)
|
@foreach($inProgress as $job)
|
||||||
<x-taller-card :job="$job" color="yellow" />
|
<x-taller-card :job="$job" color="yellow" />
|
||||||
@@ -44,12 +62,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- COLUMNA 3 -->
|
<!-- Column 3: Listas para Retirar -->
|
||||||
<div class="flex-1 min-w-[300px] bg-gray-50 dark:bg-neutral-900/50 border border-gray-300 dark:border-neutral-800 rounded-xl flex flex-col">
|
<div class="flex-1 min-w-[300px] flex flex-col rounded-xl border transition-colors
|
||||||
<div class="p-4 border-b border-gray-300 dark:border-neutral-800 bg-gray-200 dark:bg-neutral-800/50 rounded-t-xl flex justify-between items-center">
|
bg-stone-100 border-stone-300
|
||||||
<h3 class="font-bold text-green-600 dark:text-neon-lime uppercase tracking-widest text-xs">🟢 Listas para Retirar</h3>
|
dark:bg-neutral-900/50 dark:border-neutral-800">
|
||||||
<span class="bg-gray-300 dark:bg-neutral-900 text-gray-700 dark:text-gray-400 text-xs px-2 py-1 rounded-full">{{ $ready->count() }}</span>
|
|
||||||
|
<div class="p-4 flex justify-between items-center border-b rounded-t-xl
|
||||||
|
bg-stone-200/50 border-stone-300
|
||||||
|
dark:bg-neutral-800/50 dark:border-neutral-800">
|
||||||
|
<h3 class="font-bold uppercase tracking-widest text-xs text-green-600 dark:text-neon-lime">
|
||||||
|
🟢 Listas para Retirar
|
||||||
|
</h3>
|
||||||
|
<span class="text-xs px-2 py-1 rounded-full bg-stone-300 text-stone-700 dark:bg-neutral-900 dark:text-gray-400">
|
||||||
|
{{ $ready->count() }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 space-y-4 overflow-y-auto flex-1 custom-scrollbar">
|
<div class="p-4 space-y-4 overflow-y-auto flex-1 custom-scrollbar">
|
||||||
@foreach($ready as $job)
|
@foreach($ready as $job)
|
||||||
<x-taller-card :job="$job" color="neon" />
|
<x-taller-card :job="$job" color="neon" />
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
use Illuminate\Foundation\Inspiring;
|
use Illuminate\Foundation\Inspiring;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
|
||||||
Artisan::command('inspire', function () {
|
Artisan::command('inspire', function () {
|
||||||
$this->comment(Inspiring::quote());
|
$this->comment(Inspiring::quote());
|
||||||
})->purpose('Display an inspiring quote');
|
})->purpose('Display an inspiring quote');
|
||||||
|
|
||||||
|
Schedule::command('db:backup')->weekly();
|
||||||
|
|||||||
+19
-6
@@ -16,13 +16,13 @@ use App\Http\Controllers\SupplierController;
|
|||||||
use App\Http\Controllers\PasswordResetLinkController;
|
use App\Http\Controllers\PasswordResetLinkController;
|
||||||
use App\Http\Controllers\NewPasswordController;
|
use App\Http\Controllers\NewPasswordController;
|
||||||
use App\Http\Controllers\TagController;
|
use App\Http\Controllers\TagController;
|
||||||
|
use App\Http\Controllers\Admin\BackupController;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Session;
|
use Illuminate\Support\Facades\Session;
|
||||||
|
|
||||||
Route::get('/',HomeController::class);
|
// --- RUTAS PÚBLICAS (Cualquiera accede) ---
|
||||||
|
Route::get('/', HomeController::class);
|
||||||
// Route::get('/catalogo', [CatalogoController::class,'catalogo'])->name('catalogo');
|
|
||||||
Route::resource('catalogo', CatalogoController::class)->only(['index', 'show'])->parameters(['catalogo' => 'product']);
|
Route::resource('catalogo', CatalogoController::class)->only(['index', 'show'])->parameters(['catalogo' => 'product']);
|
||||||
|
|
||||||
Route::get('login', function(){ return view('login'); })->name('login');
|
Route::get('login', function(){ return view('login'); })->name('login');
|
||||||
@@ -47,13 +47,13 @@ Route::post('register', RegisterController::class)->name('register.store');
|
|||||||
|
|
||||||
Route::post('logout', function(){
|
Route::post('logout', function(){
|
||||||
Auth::guard('web')->logout();
|
Auth::guard('web')->logout();
|
||||||
|
|
||||||
Session::invalidate();
|
Session::invalidate();
|
||||||
Session::regenerateToken();
|
Session::regenerateToken();
|
||||||
|
|
||||||
return redirect('/');
|
return redirect('/');
|
||||||
})->name('logout');
|
})->name('logout');
|
||||||
|
|
||||||
|
|
||||||
|
// --- RUTAS PROTEGIDAS (Requieren Login) ---
|
||||||
Route::middleware(['auth'])->group(function () {
|
Route::middleware(['auth'])->group(function () {
|
||||||
Route::view('dashboard', 'dashboard')->name('dashboard');
|
Route::view('dashboard', 'dashboard')->name('dashboard');
|
||||||
Route::resource('clients', ClientController::class);
|
Route::resource('clients', ClientController::class);
|
||||||
@@ -67,7 +67,10 @@ Route::middleware(['auth'])->group(function () {
|
|||||||
'productos' => 'product'
|
'productos' => 'product'
|
||||||
]);
|
]);
|
||||||
Route::resource('sales', SaleController::class)->only(['index', 'create', 'store', 'show']);
|
Route::resource('sales', SaleController::class)->only(['index', 'create', 'store', 'show']);
|
||||||
// Rutas de Taller
|
Route::get('/agenda', [AgendaController::class, 'index'])->name('agenda');
|
||||||
|
Route::get('/appointments/{appointment}', [AppointmentController::class, 'show'])->name('appointments.show');
|
||||||
|
|
||||||
|
// Taller
|
||||||
Route::controller(TallerController::class)->prefix('taller')->name('taller.')->group(function () {
|
Route::controller(TallerController::class)->prefix('taller')->name('taller.')->group(function () {
|
||||||
Route::get('/', 'index')->name('index');
|
Route::get('/', 'index')->name('index');
|
||||||
Route::get('/create', 'create')->name('create');
|
Route::get('/create', 'create')->name('create');
|
||||||
@@ -78,4 +81,14 @@ Route::middleware(['auth'])->group(function () {
|
|||||||
Route::get('/appointments/{appointment}', [AppointmentController::class, 'show'])
|
Route::get('/appointments/{appointment}', [AppointmentController::class, 'show'])
|
||||||
->name('appointments.show');
|
->name('appointments.show');
|
||||||
Route::view('/faq', 'faq')->name('faq');
|
Route::view('/faq', 'faq')->name('faq');
|
||||||
|
|
||||||
|
// Copias de Seguridad (Solo Administradores)
|
||||||
|
Route::middleware(['admin'])->prefix('admin')->name('admin.')->group(function () {
|
||||||
|
Route::get('/backups', [BackupController::class, 'index'])->name('backups.index');
|
||||||
|
Route::post('/backups/create', [BackupController::class, 'create'])->name('backups.create');
|
||||||
|
Route::post('/backups/upload', [BackupController::class, 'upload'])->name('backups.upload');
|
||||||
|
Route::post('/backups/{filename}/restore', [BackupController::class, 'restore'])->name('backups.restore');
|
||||||
|
Route::get('/backups/{filename}/download', [BackupController::class, 'download'])->name('backups.download');
|
||||||
|
Route::delete('/backups/{filename}', [BackupController::class, 'destroy'])->name('backups.destroy');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user