Initial commit: Lash Vanshy - Complete project with admin panel, gallery, products, and contact

This commit is contained in:
2026-04-08 00:23:16 -06:00
commit e07e065791
111 changed files with 17939 additions and 0 deletions

83
app/Helpers/helpers.php Executable file
View File

@@ -0,0 +1,83 @@
<?php
use App\Models\AdminUser;
use App\Models\Configuracion;
use Illuminate\Support\Facades\Auth;
/**
* Helper para obtener configuraciones del sitio
*
* @param string $clave La clave de configuración
* @param mixed $default Valor por defecto si no existe
* @return mixed
*/
if (! function_exists('config_site')) {
function config_site(string $clave, $default = null)
{
return Configuracion::get($clave, $default);
}
}
/**
* Helper para establecer una configuración
*
* @param string $clave La clave de configuración
* @param mixed $valor El valor a guardar
* @return Configuracion
*/
if (! function_exists('set_config_site')) {
function set_config_site(string $clave, $valor): Configuracion
{
return Configuracion::set($clave, $valor);
}
}
/**
* Helper para obtener todas las configuraciones como array
*
* @return array
*/
if (! function_exists('all_config_site')) {
function all_config_site(): array
{
return Configuracion::allAsArray();
}
}
/**
* Helper para verificar si el usuario actual es admin
*
* @return bool
*/
if (! function_exists('is_admin')) {
function is_admin(): bool
{
return Auth::guard('admin')->check();
}
}
/**
* Helper para obtener el usuario admin actual
*
* @return AdminUser|null
*/
if (! function_exists('admin_user')) {
function admin_user(): ?AdminUser
{
return Auth::guard('admin')->user();
}
}
/**
* Helper para verificar si el usuario actual es super_admin
*
* @return bool
*/
if (! function_exists('is_super_admin')) {
function is_super_admin(): bool
{
$user = Auth::guard('admin')->user();
return $user && $user->isSuperAdmin();
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\AdminUserRequest;
use App\Models\AdminUser;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
class AdminUserController extends Controller
{
/**
* Mostrar lista de usuarios admin
*/
public function index(): View
{
$users = AdminUser::orderBy('created_at', 'desc')->paginate(15);
return view('admin.usuarios.index', compact('users'));
}
/**
* Mostrar formulario de creación
*/
public function create(): View
{
return view('admin.usuarios.create');
}
/**
* Guardar nuevo usuario admin
*/
public function store(AdminUserRequest $request): RedirectResponse
{
// Verificar permisos - solo super_admin puede crear otros admins
$currentUser = Auth::guard('admin')->user();
if (! $currentUser || ! $currentUser->isSuperAdmin()) {
return back()->withErrors(['error' => 'No tienes permisos para crear administradores.']);
}
// Si no es super_admin, no puede crear super_admin
if ($request->rol === 'super_admin' && ! $currentUser->isSuperAdmin()) {
return back()->withErrors(['error' => 'No tienes permisos para crear super administradores.']);
}
$data = $request->validated();
// Manejar upload de avatar
if ($request->hasFile('avatar')) {
$data['avatar'] = $this->uploadFile($request->file('avatar'), 'avatars');
}
AdminUser::create($data);
return redirect()->route('admin.users.index')->with('success', 'Usuario administrativo creado correctamente.');
}
/**
* Mostrar formulario de edición
*/
public function edit(AdminUser $admin_user): \Illuminate\Contracts\View\View|RedirectResponse
{
$user = Auth::guard('admin')->user();
// Solo super_admin puede editar otros usuarios
// Un usuario puede editar su propio perfil
if (! $user->isSuperAdmin() && $user->id !== $admin_user->id) {
return back()->withErrors(['error' => 'No tienes permisos para editar este usuario.']);
}
return view('admin.usuarios.edit', compact('admin_user'));
}
/**
* Actualizar usuario admin
*/
public function update(AdminUserRequest $request, AdminUser $admin_user): RedirectResponse
{
// Verificar permisos
$currentUser = Auth::guard('admin')->user();
// Un usuario puede editar su propio perfil, pero no cambiar su rol
if ($currentUser->id === $admin_user->id) {
// No permitir cambiar rol de uno mismo
if ($request->has('rol') && $request->rol !== $admin_user->rol) {
return back()->withErrors(['error' => 'No puedes cambiar tu propio rol.']);
}
} elseif (! $currentUser->isSuperAdmin()) {
return back()->withErrors(['error' => 'No tienes permisos para editar este usuario.']);
}
// Si no es super_admin, no puede crear/asignar super_admin
if ($request->rol === 'super_admin' && ! $currentUser->isSuperAdmin()) {
return back()->withErrors(['error' => 'No tienes permisos para asignar rol de super administrador.']);
}
$data = $request->validated();
// Si no se proporciona password, eliminar del array
if (empty($data['password'])) {
unset($data['password']);
}
// Manejar upload de avatar
if ($request->hasFile('avatar')) {
// Eliminar avatar anterior
if ($admin_user->avatar) {
Storage::disk('public')->delete($admin_user->avatar);
}
$data['avatar'] = $this->uploadFile($request->file('avatar'), 'avatars');
}
$admin_user->update($data);
return redirect()->route('admin.users.index')->with('success', 'Usuario administrativo actualizado correctamente.');
}
/**
* Eliminar usuario admin
*/
public function destroy(AdminUser $admin_user): RedirectResponse
{
// Verificar permisos - solo super_admin puede eliminar
$currentUser = Auth::guard('admin')->user();
if (! $currentUser || ! $currentUser->isSuperAdmin()) {
return back()->withErrors(['error' => 'No tienes permisos para eliminar administradores.']);
}
// No permitir eliminarse a sí mismo
if ($currentUser->id === $admin_user->id) {
return back()->withErrors(['error' => 'No puedes eliminar tu propia cuenta.']);
}
// Eliminar avatar
if ($admin_user->avatar) {
Storage::disk('public')->delete($admin_user->avatar);
}
$admin_user->delete();
return redirect()->route('admin.users.index')->with('success', 'Usuario administrativo eliminado correctamente.');
}
/**
* Subir archivo a almacenamiento
*/
private function uploadFile($file, string $directory): string
{
return $file->store($directory, 'public');
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\LoginRequest;
use App\Models\AdminUser;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
class AuthController extends Controller
{
/**
* Mostrar formulario de login
*/
public function showLogin(): \Illuminate\Contracts\View\View|RedirectResponse
{
if (Auth::guard('admin')->check()) {
return redirect()->route('admin.dashboard');
}
return view('admin.auth.login');
}
/**
* Procesar login
*/
public function login(LoginRequest $request): RedirectResponse
{
$credentials = $request->only('email', 'password');
// Buscar usuario por email
$adminUser = AdminUser::where('email', $credentials['email'])->first();
if (! $adminUser) {
return back()->withErrors([
'email' => 'Las credenciales no son válidas.',
])->withInput($request->except('password'));
}
// Verificar password
if (! $adminUser->validatePassword($credentials['password'])) {
return back()->withErrors([
'email' => 'Las credenciales no son válidas.',
])->withInput($request->except('password'));
}
// Login manual
Auth::guard('admin')->login($adminUser);
$request->session()->regenerate();
return redirect()->intended(route('admin.dashboard'))->with('success', 'Bienvenido al panel de administración.');
}
/**
* Cerrar sesión
*/
public function logout(): RedirectResponse
{
Auth::guard('admin')->logout();
request()->session()->invalidate();
request()->session()->regenerateToken();
return redirect()->route('admin.login')->with('success', 'Sesión cerrada correctamente.');
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Configuracion;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class ConfiguracionController extends Controller
{
/**
* Mostrar configuración
*/
public function index(): View
{
$configuracion = Configuracion::allAsArray();
return view('admin.configuracion.index', compact('configuracion'));
}
/**
* Actualizar configuración
*/
public function update(): RedirectResponse
{
$fields = [
'nombre_sitio',
'telefono',
'email',
'direccion',
'horario',
'facebook',
'instagram',
'whatsapp',
'tiktok',
'youtube',
'seo_titulo',
'seo_descripcion',
];
foreach ($fields as $field) {
$value = request($field);
// Only save if the field has a value
if ($value !== null && $value !== '') {
Configuracion::set($field, $value);
} else {
// Optionally clear empty fields
Configuracion::remove($field);
}
}
return redirect()->route('admin.configuracion.index')->with('success', 'Configuración guardada correctamente.');
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Galeria;
use App\Models\Mensaje;
use App\Models\Producto;
use Illuminate\View\View;
class DashboardController extends Controller
{
/**
* Mostrar el dashboard principal
*/
public function index(): View
{
// Obtener estadísticas
$stats = [
'mensajes_no_leidos' => Mensaje::noLeidos()->count(),
'total_modelos' => Galeria::count(),
'total_productos' => Producto::count(),
'productos_destacados' => Producto::where('destacado', true)->count(),
'modelos_activos' => Galeria::where('activo', true)->count(),
];
// Mensajes recientes
$mensajes_recientes = Mensaje::orderBy('created_at', 'desc')->take(5)->get();
return view('admin.dashboard.index', compact('stats', 'mensajes_recientes'));
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\GaleriaRequest;
use App\Models\Galeria;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
class GaleriaController extends Controller
{
public function index(): View
{
$galerias = Galeria::ordenado()->paginate(15);
return view('admin.galeria.index', compact('galerias'));
}
public function create(): View
{
return view('admin.galeria.create');
}
public function store(GaleriaRequest $request): RedirectResponse
{
try {
$data = $request->validated();
Log::info('GaleriaRequest validated', $data);
if ($request->hasFile('archivo')) {
Log::info('Has archivo file', ['file' => $request->file('archivo')->getClientOriginalName()]);
$data['archivo'] = $this->uploadFile($request->file('archivo'), 'galeria');
} else {
Log::warning('No archivo file in request');
Log::info('All files:', ['files' => $request->allFiles()]);
}
if ($request->hasFile('thumbnail')) {
$data['thumbnail'] = $this->uploadFile($request->file('thumbnail'), 'galeria/thumbnails');
}
$data['activo'] = $request->has('activo');
$data['orden'] = $data['orden'] ?? 0;
Galeria::create($data);
return redirect()->route('admin.galeria.index')->with('success', 'Modelo registrado correctamente.');
} catch (\Exception $e) {
Log::error('Error creating Galeria: '.$e->getMessage());
return redirect()->back()->with('error', 'Error al guardar: '.$e->getMessage())->withInput();
}
}
public function edit(Galeria $galeria): View
{
return view('admin.galeria.edit', compact('galeria'));
}
public function update(GaleriaRequest $request, Galeria $galeria): RedirectResponse
{
try {
$data = $request->validated();
if ($request->hasFile('archivo')) {
if ($galeria->archivo) {
Storage::disk('public')->delete($galeria->archivo);
}
$data['archivo'] = $this->uploadFile($request->file('archivo'), 'galeria');
}
if ($request->hasFile('thumbnail')) {
if ($galeria->thumbnail) {
Storage::disk('public')->delete($galeria->thumbnail);
}
$data['thumbnail'] = $this->uploadFile($request->file('thumbnail'), 'galeria/thumbnails');
}
$data['activo'] = $request->has('activo');
$data['orden'] = $data['orden'] ?? $galeria->orden;
$galeria->update($data);
return redirect()->route('admin.galeria.index')->with('success', 'Modelo actualizado correctamente.');
} catch (\Exception $e) {
Log::error('Error updating Galeria: '.$e->getMessage());
return redirect()->back()->with('error', 'Error al actualizar: '.$e->getMessage())->withInput();
}
}
public function destroy(Galeria $galeria): RedirectResponse
{
if ($galeria->archivo) {
Storage::disk('public')->delete($galeria->archivo);
}
if ($galeria->thumbnail) {
Storage::disk('public')->delete($galeria->thumbnail);
}
$galeria->delete();
return redirect()->route('admin.galeria.index')->with('success', 'Modelo eliminado correctamente.');
}
private function uploadFile($file, string $directory): string
{
return $file->store($directory, 'public');
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Mensaje;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class MensajeController extends Controller
{
/**
* Mostrar lista de mensajes
*/
public function index(): View
{
$mensajes = Mensaje::orderBy('created_at', 'desc')->paginate(15);
return view('admin.mensajes.index', compact('mensajes'));
}
/**
* Mostrar mensaje específico
*/
public function show(Mensaje $mensaje): View
{
// Marcar como leído al ver
if (! $mensaje->leido) {
$mensaje->marcarLeido();
}
return view('admin.mensajes.show', compact('mensaje'));
}
/**
* Alternar estado de leído/no leído
*/
public function markRead(Mensaje $mensaje): RedirectResponse
{
if ($mensaje->leido) {
$mensaje->marcarNoLeido();
$message = 'Mensaje marcado como no leído.';
} else {
$mensaje->marcarLeido();
$message = 'Mensaje marcado como leído.';
}
return back()->with('success', $message);
}
/**
* Eliminar mensaje
*/
public function destroy(Mensaje $mensaje): RedirectResponse
{
$mensaje->delete();
return redirect()->route('admin.mensajes.index')->with('success', 'Mensaje eliminado correctamente.');
}
/**
* Marcar todos los mensajes como leídos
*/
public function markAllRead(): RedirectResponse
{
Mensaje::where('leido', false)->update(['leido' => true]);
return back()->with('success', 'Todos los mensajes marcados como leídos.');
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\ProductoRequest;
use App\Models\Producto;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
class ProductoController extends Controller
{
/**
* Mostrar lista de productos
*/
public function index(): View
{
$productos = Producto::orderBy('orden', 'asc')->orderBy('created_at', 'desc')->paginate(15);
return view('admin.productos.index', compact('productos'));
}
/**
* Mostrar formulario de creación
*/
public function create(): View
{
return view('admin.productos.create');
}
/**
* Guardar nuevo registro
*/
public function store(ProductoRequest $request): RedirectResponse
{
$data = $request->validated();
// Manejar upload de imagen
if ($request->hasFile('imagen')) {
$data['imagen'] = $this->uploadFile($request->file('imagen'), 'productos');
}
// Valores por defecto
$data['destacado'] = $request->has('destacado');
$data['activo'] = $request->has('activo');
$data['orden'] = $data['orden'] ?? 0;
Producto::create($data);
return redirect()->route('admin.productos.index')->with('success', 'Producto registrado correctamente.');
}
/**
* Mostrar formulario de edición
*/
public function edit(Producto $producto): View
{
return view('admin.productos.edit', compact('producto'));
}
/**
* Actualizar registro
*/
public function update(ProductoRequest $request, Producto $producto): RedirectResponse
{
$data = $request->validated();
// Manejar upload de imagen si se proporciona una nueva
if ($request->hasFile('imagen')) {
// Eliminar imagen anterior
if ($producto->imagen) {
Storage::disk('public')->delete($producto->imagen);
}
$data['imagen'] = $this->uploadFile($request->file('imagen'), 'productos');
}
// Valores por defecto
$data['destacado'] = $request->has('destacado');
$data['activo'] = $request->has('activo');
$data['orden'] = $data['orden'] ?? $producto->orden;
$producto->update($data);
return redirect()->route('admin.productos.index')->with('success', 'Producto actualizado correctamente.');
}
/**
* Eliminar registro
*/
public function destroy(Producto $producto): RedirectResponse
{
// Eliminar imagen
if ($producto->imagen) {
Storage::disk('public')->delete($producto->imagen);
}
$producto->delete();
return redirect()->route('admin.productos.index')->with('success', 'Producto eliminado correctamente.');
}
/**
* Subir archivo a almacenamiento
*/
private function uploadFile($file, string $directory): string
{
return $file->store($directory, 'public');
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\Frontend;
use App\Http\Controllers\Controller;
use App\Models\Galeria;
use Illuminate\Http\Request;
use Illuminate\View\View;
class GaleriaController extends Controller
{
/**
* Mostrar galería pública
*/
public function index(Request $request): View
{
$tipo = $request->get('tipo', 'todos');
$query = Galeria::activo()->ordenado();
if ($tipo !== 'todos') {
$query->where('tipo', $tipo);
}
$galeria = $query->paginate(12);
// Obtener estadísticas para los filtros
$stats = [
'total' => Galeria::activo()->count(),
'imagenes' => Galeria::activo()->where('tipo', 'imagen')->count(),
'videos' => Galeria::activo()->where('tipo', 'video')->count(),
];
return view('frontend.galeria.index', compact('galeria', 'stats', 'tipo'));
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Frontend;
use App\Http\Controllers\Controller;
use App\Models\Configuracion;
use App\Models\Galeria;
use App\Models\Producto;
use Illuminate\View\View;
class HomeController extends Controller
{
/**
* Mostrar página de inicio
*/
public function index(): View
{
$configuracion = Configuracion::allAsArray();
// Productos destacados para el home
$productosDestacados = Producto::activo()
->where('destacado', true)
->orderBy('orden', 'asc')
->take(6)
->get();
// Galería para mostrar en home (solo activos)
$galeria = Galeria::activo()
->ordenado()
->take(8)
->get();
return view('frontend.home.index', compact(
'productosDestacados',
'galeria',
'configuracion'
));
}
/**
* Mostrar página de contacto
*/
public function contacto(): View
{
$configuracion = Configuracion::allAsArray();
return view('frontend.contacto.index', compact('configuracion'));
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers\Frontend;
use App\Http\Controllers\Controller;
use App\Models\Producto;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ProductoController extends Controller
{
/**
* Mostrar lista de productos/servicios
*/
public function index(Request $request): View
{
$categoria = $request->get('categoria', 'todos');
$query = Producto::activo()->orderBy('orden', 'asc');
if ($categoria !== 'todos') {
$query->where('categoria', $categoria);
}
$productos = $query->paginate(12);
// Obtener categorías únicas
$categorias = Producto::activo()
->distinct()
->pluck('categoria')
->filter()
->values();
// Productos destacados
$destacados = Producto::activo()
->where('destacado', true)
->orderBy('orden', 'asc')
->take(4)
->get();
return view('frontend.productos.index', compact(
'productos',
'categorias',
'destacados',
'categoria'
));
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class AdminAuth
{
/**
* Handle an incoming request.
*
* Verifica que el usuario esté autenticado en el guard 'admin'
*/
public function handle(Request $request, Closure $next): Response
{
if (! Auth::guard('admin')->check()) {
// Si no está autenticado, redirigir al login
return redirect()->route('admin.login')->with('error', 'Debes iniciar sesión para acceder.');
}
return $next($request);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Content-Security-Policy', "default-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://fonts.googleapis.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.googleapis.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' 'unsafe-inline' https://fonts.gstatic.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com data:; img-src 'self' data: https:; connect-src 'self' https:;");
return $response;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class SuperAdminOnly
{
public function handle(Request $request, Closure $next): Response
{
$user = Auth::guard('admin')->user();
if (! $user || ! $user->isSuperAdmin()) {
abort(403, 'Acceso restringido a super administrador.');
}
return $next($request);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class AdminUserRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$rules = [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:admin_users,email'],
'password' => ['required', 'string', 'min:8', 'max:255'],
'rol' => ['required', 'in:super_admin,admin'],
'avatar' => ['nullable', 'image', 'mimes:jpeg,png,gif,webp', 'max:5120'],
];
// En actualización, password no es obligatorio (puede mantener el actual)
if ($this->isMethod('PATCH') || $this->isMethod('PUT')) {
$rules['password'] = ['nullable', 'string', 'min:8', 'max:255'];
$rules['email'] = ['required', 'string', 'email', 'max:255', 'unique:admin_users,email,'.$this->route('admin_user')];
$rules['avatar'] = ['nullable', 'image', 'mimes:jpeg,png,gif,webp', 'max:5120'];
}
return $rules;
}
public function messages(): array
{
return [
'name.required' => 'El nombre es obligatorio.',
'name.max' => 'El nombre no puede exceder 255 caracteres.',
'email.required' => 'El email es obligatorio.',
'email.email' => 'El email debe ser una dirección válida.',
'email.max' => 'El email no puede exceder 255 caracteres.',
'email.unique' => 'El email ya está en uso.',
'password.required' => 'La contraseña es obligatoria.',
'password.min' => 'La contraseña debe tener al menos 6 caracteres.',
'password.max' => 'La contraseña no puede exceder 255 caracteres.',
'rol.required' => 'El rol es obligatorio.',
'rol.in' => 'El rol debe ser super_admin o admin.',
'avatar.image' => 'El avatar debe ser una imagen.',
'avatar.mimes' => 'El formato del avatar no es válido.',
'avatar.max' => 'El avatar no puede exceder 5MB.',
];
}
/**
* Configure the validator instance.
*/
public function withValidator($validator): void
{
$validator->after(function ($validator) {
// Validar que un admin no pueda crear/editar otro super_admin
// Esta validación se maneja en el controlador
});
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class GaleriaRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$rules = [
'titulo' => ['required', 'string', 'max:255'],
'descripcion' => ['nullable', 'string', 'max:1000'],
'tipo' => ['required', 'in:imagen,video'],
'archivo' => ['required', 'file', 'max:102400'],
'thumbnail' => ['nullable', 'image', 'max:10240'],
'orden' => ['nullable', 'integer', 'min:0'],
'activo' => ['nullable', 'boolean'],
];
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
$rules['archivo'] = ['nullable', 'file', 'max:102400'];
}
return $rules;
}
public function messages(): array
{
return [
'titulo.required' => 'El titulo es obligatorio.',
'tipo.required' => 'Debes seleccionar el tipo (imagen o video).',
'archivo.required' => 'Debes subir un archivo.',
'archivo.mimes' => 'El formato del archivo no es valido.',
'archivo.max' => 'El archivo no puede exceder 100MB.',
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'email' => ['required', 'string', 'email', 'max:255'],
'password' => ['required', 'string', 'min:1'],
];
}
public function messages(): array
{
return [
'email.required' => 'El email es obligatorio.',
'email.email' => 'El email debe ser una dirección válida.',
'email.max' => 'El email no puede exceder 255 caracteres.',
'password.required' => 'La contraseña es obligatoria.',
];
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class MensajeRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$rules = [
'nombre' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255'],
'telefono' => ['nullable', 'string', 'max:50', 'regex:/^[+]?[\d\s\-()]+$/'],
'mensaje' => ['required', 'string', 'min:1', 'max:10000'],
'leido' => ['nullable', 'boolean'],
];
return $rules;
}
public function messages(): array
{
return [
'nombre.required' => 'El nombre es obligatorio.',
'nombre.max' => 'El nombre no puede exceder 255 caracteres.',
'email.required' => 'El email es obligatorio.',
'email.email' => 'El email debe ser una dirección válida.',
'email.max' => 'El email no puede exceder 255 caracteres.',
'telefono.max' => 'El teléfono no puede exceder 50 caracteres.',
'telefono.regex' => 'El formato del teléfono no es válido.',
'mensaje.required' => 'El mensaje es obligatorio.',
'mensaje.min' => 'El mensaje no puede estar vacío.',
'mensaje.max' => 'El mensaje no puede exceder 10000 caracteres.',
];
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ProductoRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$rules = [
'nombre' => ['required', 'string', 'max:255'],
'descripcion' => ['required', 'string', 'max:5000'],
'precio' => ['required', 'numeric', 'min:0', 'regex:/^\d+(\.\d{1,2})?$/'],
'imagen' => ['nullable', 'image', 'mimes:jpeg,png,gif,webp', 'max:10240'],
'categoria' => ['required', 'string', 'max:100'],
'destacado' => ['nullable', 'boolean'],
'activo' => ['nullable', 'boolean'],
'orden' => ['nullable', 'integer', 'min:0'],
];
// En actualización, imagen no es obligatoria
if ($this->isMethod('PATCH') || $this->isMethod('PUT')) {
$rules['imagen'] = ['nullable', 'image', 'mimes:jpeg,png,gif,webp', 'max:10240'];
}
return $rules;
}
public function messages(): array
{
return [
'nombre.required' => 'El nombre es obligatorio.',
'nombre.max' => 'El nombre no puede exceder 255 caracteres.',
'descripcion.required' => 'La descripción es obligatoria.',
'descripcion.max' => 'La descripción no puede exceder 5000 caracteres.',
'precio.required' => 'El precio es obligatorio.',
'precio.numeric' => 'El precio debe ser un número válido.',
'precio.min' => 'El precio no puede ser negativo.',
'precio.regex' => 'El precio debe tener como máximo 2 decimales.',
'imagen.image' => 'La imagen debe ser un archivo de imagen.',
'imagen.mimes' => 'El formato de imagen no es válido.',
'imagen.max' => 'La imagen no puede exceder 10MB.',
'categoria.required' => 'La categoría es obligatoria.',
'categoria.max' => 'La categoría no puede exceder 100 caracteres.',
'orden.integer' => 'El orden debe ser un número entero.',
'orden.min' => 'El orden no puede ser negativo.',
];
}
}

81
app/Models/AdminUser.php Executable file
View File

@@ -0,0 +1,81 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Facades\Hash;
class AdminUser extends Authenticatable
{
use HasFactory;
protected $table = 'admin_users';
protected $fillable = [
'name',
'email',
'password',
'rol',
'avatar',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Validar password
*/
public function validatePassword(string $password): bool
{
return Hash::check($password, $this->password);
}
/**
* Set password attribute - hash automatically
*/
public function setPasswordAttribute(string $value): void
{
$this->attributes['password'] = Hash::make($value);
}
/**
* Scope para filtrar super admins
*/
public function scopeSuperAdmin(Builder $query): Builder
{
return $query->where('rol', 'super_admin');
}
/**
* Scope para filtrar admins
*/
public function scopeAdmin(Builder $query): Builder
{
return $query->where('rol', 'admin');
}
/**
* Verificar si es super admin
*/
public function isSuperAdmin(): bool
{
return $this->rol === 'super_admin';
}
/**
* Verificar si tiene permiso para gestionar otros admins
*/
public function canManageAdmins(): bool
{
return $this->rol === 'super_admin';
}
}

75
app/Models/Configuracion.php Executable file
View File

@@ -0,0 +1,75 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Configuracion extends Model
{
use HasFactory;
protected $table = 'configuraciones';
protected $fillable = [
'clave',
'valor',
'descripcion',
];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Obtener configuración por clave
*
* @param string $clave La clave de configuración
* @param mixed $default Valor por defecto si no existe
* @return mixed
*/
public static function get(string $clave, $default = null)
{
$config = self::where('clave', $clave)->first();
if (! $config) {
return $default;
}
return $config->valor;
}
/**
* Obtener todas las configuraciones como array asociativo
*/
public static function allAsArray(): array
{
return self::pluck('valor', 'clave')->toArray();
}
/**
* Establecer una configuración
*
* @param string $clave La clave de configuración
* @param mixed $valor El valor a guardar
*/
public static function set(string $clave, $valor): self
{
$config = self::where('clave', $clave)->firstOrNew(['clave' => $clave]);
$config->valor = $valor;
$config->save();
return $config;
}
/**
* Eliminar una configuración
*
* @param string $clave La clave de configuración
*/
public static function remove(string $clave): bool
{
return self::where('clave', $clave)->delete() > 0;
}
}

71
app/Models/Galeria.php Executable file
View File

@@ -0,0 +1,71 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Galeria extends Model
{
use HasFactory;
protected $table = 'galerias';
protected $fillable = [
'titulo',
'descripcion',
'tipo',
'archivo',
'thumbnail',
'orden',
'activo',
];
protected $casts = [
'activo' => 'boolean',
'orden' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Scope para filtrar registros activos
*/
public function scopeActivo(Builder $query): Builder
{
return $query->where('activo', true);
}
/**
* Scope para ordenar por campo orden
*/
public function scopeOrdenado(Builder $query): Builder
{
return $query->orderBy('orden', 'asc')->orderBy('created_at', 'desc');
}
/**
* Obtener el tipo de archivo como clase CSS
*/
public function getTipoClaseAttribute(): string
{
return $this->tipo === 'video' ? 'video' : 'imagen';
}
/**
* Verificar si es video
*/
public function esVideo(): bool
{
return $this->tipo === 'video';
}
/**
* Verificar si es imagen
*/
public function esImagen(): bool
{
return $this->tipo === 'imagen';
}
}

68
app/Models/Mensaje.php Executable file
View File

@@ -0,0 +1,68 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Mensaje extends Model
{
use HasFactory;
protected $table = 'mensajes';
protected $fillable = [
'nombre',
'email',
'telefono',
'mensaje',
'leido',
];
protected $casts = [
'leido' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Scope para filtrar mensajes no leídos
*/
public function scopeNoLeidos(Builder $query): Builder
{
return $query->where('leido', false);
}
/**
* Marcar mensaje como leído
*/
public function marcarLeido(): bool
{
return $this->update(['leido' => true]);
}
/**
* Marcar mensaje como no leído
*/
public function marcarNoLeido(): bool
{
return $this->update(['leido' => false]);
}
/**
* Obtener iniciales del nombre
*/
public function getInicialesAttribute(): string
{
$nombres = explode(' ', $this->nombre);
$iniciales = '';
foreach ($nombres as $nombre) {
if (! empty($nombre)) {
$iniciales .= strtoupper($nombre[0]);
}
}
return $iniciales;
}
}

76
app/Models/Producto.php Executable file
View File

@@ -0,0 +1,76 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Producto extends Model
{
use HasFactory;
protected $table = 'productos';
protected $fillable = [
'nombre',
'descripcion',
'precio',
'imagen',
'categoria',
'destacado',
'activo',
'orden',
];
protected $casts = [
'precio' => 'decimal:2',
'destacado' => 'boolean',
'activo' => 'boolean',
'orden' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Scope para filtrar productos activos
*/
public function scopeActivo(Builder $query): Builder
{
return $query->where('activo', true);
}
/**
* Scope para filtrar productos destacados
*/
public function scopeDestacado(Builder $query): Builder
{
return $query->where('destacado', true);
}
/**
* Scope para filtrar por categoría
*/
public function scopePorCategoria(Builder $query, string $categoria): Builder
{
return $query->where('categoria', $categoria);
}
/**
* Obtener precio formateado
*/
public function getPrecioFormateadoAttribute(): string
{
$currency = config('currency');
return $currency['symbol'].number_format($this->precio, $currency['decimal_places'], $currency['decimal_separator'], $currency['thousands_separator']);
}
/**
* Verificar si tiene imagen
*/
public function tieneImagen(): bool
{
return ! empty($this->imagen);
}
}

32
app/Models/User.php Executable file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}