3
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Agregar Slide')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0">➕ Agregar Slide</h2>
|
||||
<a href="{{ route('admin.carousel.index') }}" class="btn btn-secondary shadow-sm">
|
||||
<i class="bi bi-arrow-left"></i> Volver a la Lista
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<form action="{{ route('admin.carousel.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label font-weight-bold">Título</label>
|
||||
<input type="text" name="titulo" class="form-control" value="{{ old('titulo') }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label font-weight-bold">Subtítulo</label>
|
||||
<input type="text" name="subtitulo" class="form-control" value="{{ old('subtitulo') }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label font-weight-bold">Texto del Botón</label>
|
||||
<input type="text" name="boton_texto" class="form-control" value="{{ old('boton_texto') }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label font-weight-bold">Enlace del Botón (URL)</label>
|
||||
<input type="text" name="boton_enlace" class="form-control" value="{{ old('boton_enlace') }}" placeholder="Ej: /eventos o https://...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label font-weight-bold">Imagen <span class="text-danger">*</span></label>
|
||||
<input type="file" name="imagen" class="form-control" required accept="image/*">
|
||||
<small class="text-muted d-block mt-1">Formatos: JPG, PNG, WEBP. Tamaño ideal: ancho de pantalla, bajo peso.</small>
|
||||
@error('imagen')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="col-md-2 mt-4">
|
||||
<div class="form-check form-switch fs-5 h-100 d-flex align-items-center">
|
||||
<input class="form-check-input mt-0 me-2" type="checkbox" role="switch" name="activo" id="activo" checked value="1" style="height: 25px; width: 50px;">
|
||||
<label class="form-check-label ms-2 mt-1 fs-6" for="activo">Visible</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label font-weight-bold">Orden</label>
|
||||
<input type="number" name="orden" class="form-control" value="{{ old('orden', 0) }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-end mt-4">
|
||||
<button type="submit" class="btn btn-primary px-4 shadow-sm">
|
||||
<i class="bi bi-save me-1"></i> Guardar Slide
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,76 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Editar Slide')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0">✏️ Editar Slide</h2>
|
||||
<a href="{{ route('admin.carousel.index') }}" class="btn btn-secondary shadow-sm">
|
||||
<i class="bi bi-arrow-left"></i> Volver a la Lista
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<form action="{{ route('admin.carousel.update', $carouselItem->id) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label font-weight-bold">Título</label>
|
||||
<input type="text" name="titulo" class="form-control" value="{{ old('titulo', $carouselItem->titulo) }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label font-weight-bold">Subtítulo</label>
|
||||
<input type="text" name="subtitulo" class="form-control" value="{{ old('subtitulo', $carouselItem->subtitulo) }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label font-weight-bold">Texto del Botón</label>
|
||||
<input type="text" name="boton_texto" class="form-control" value="{{ old('boton_texto', $carouselItem->boton_texto) }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label font-weight-bold">Enlace del Botón (URL)</label>
|
||||
<input type="text" name="boton_enlace" class="form-control" value="{{ old('boton_enlace', $carouselItem->boton_enlace) }}" placeholder="Ej: /eventos o https://...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 mt-4 align-items-center">
|
||||
<div class="col-md-3 text-center mb-3">
|
||||
<label class="form-label font-weight-bold w-100 text-start">Imagen Actual</label>
|
||||
<img src="{{ Str::startsWith($carouselItem->imagen, 'http') ? $carouselItem->imagen : asset($carouselItem->imagen) }}"
|
||||
alt="Slide actual" class="img-thumbnail w-100 shadow-sm" style="max-height: 150px; object-fit: cover;">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label font-weight-bold">Reemplazar Imagen (Opcional)</label>
|
||||
<input type="file" name="imagen" class="form-control" accept="image/*">
|
||||
<small class="text-muted d-block mt-1">Si no subís nada se conservará la actual.</small>
|
||||
@error('imagen')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 mt-4 text-center">
|
||||
<div class="form-check form-switch fs-5 h-100 d-flex flex-column align-items-center justify-content-center">
|
||||
<label class="form-check-label ms-2 fs-6 mb-2" for="activo">Visible</label>
|
||||
<input class="form-check-input mt-0" type="checkbox" role="switch" name="activo" id="activo" value="1" style="height: 25px; width: 50px;" {{ $carouselItem->activo ? 'checked' : '' }}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label font-weight-bold">Orden</label>
|
||||
<input type="number" name="orden" class="form-control" value="{{ old('orden', $carouselItem->orden) }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-end mt-4">
|
||||
<button type="submit" class="btn btn-primary px-4 shadow-sm">
|
||||
<i class="bi bi-save me-1"></i> Actualizar Slide
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,83 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Carrusel Principal - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-5 d-flex justify-content-between align-items-end">
|
||||
<div>
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-2">Visual Merchandising</span>
|
||||
<h1 class="display-4 fw-bold font-header mb-0">Carrusel Principal<span class="text-primary">.</span></h1>
|
||||
</div>
|
||||
<a href="{{ route('admin.carousel.create') }}" class="btn-admin-primary">
|
||||
<i class="bi bi-plus-lg me-2"></i> AGREGAR SLIDE
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
@if($items->isEmpty())
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-images text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<p class="fs-5 text-muted">No hay slides en el carrusel.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="kinetic-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;">Orden</th>
|
||||
<th>Imagen</th>
|
||||
<th>Título / Subtítulo</th>
|
||||
<th>Botón de Acción</th>
|
||||
<th>Estado</th>
|
||||
<th class="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($items as $item)
|
||||
<tr>
|
||||
<td><span class="badge bg-light text-dark fw-bold border p-2">{{ $item->orden }}</span></td>
|
||||
<td>
|
||||
<div class="bg-dark" style="width: 120px; height: 60px; overflow: hidden; border: 1px solid var(--admin-outline);">
|
||||
<img src="{{ Str::startsWith($item->imagen, 'http') ? $item->imagen : asset($item->imagen) }}"
|
||||
alt="Slide" style="width: 100%; height: 100%; object-fit: cover;">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-bold d-block text-uppercase">{{ $item->titulo ?: '(SIN TÍTULO)' }}</span>
|
||||
<small class="text-muted text-uppercase tracking-tighter">{{ $item->subtitulo }}</small>
|
||||
</td>
|
||||
<td>
|
||||
@if($item->boton_texto)
|
||||
<div class="small fw-bold text-primary">{{ $item->boton_texto }}</div>
|
||||
<div class="small text-muted font-monospace" style="font-size: 0.75rem;">{{ Str::limit($item->boton_enlace, 20) }}</div>
|
||||
@else
|
||||
<span class="text-muted small">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($item->activo)
|
||||
<span class="badge bg-success text-white text-uppercase px-2 py-1">ACTIVO</span>
|
||||
@else
|
||||
<span class="badge bg-light text-muted border text-uppercase px-2 py-1">OCULTO</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('admin.carousel.edit', $item->id) }}" class="btn btn-sm btn-light border me-1">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form action="{{ route('admin.carousel.destroy', $item->id) }}" method="POST" class="d-inline delete-form confirm-submit" data-confirm-text="¿Eliminar este slide?">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,76 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', ($categoria ? 'Editar' : 'Nueva') . ' Categoría - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-tags-fill"></i> {{ $categoria ? 'Editar Categoría' : 'Nueva Categoría' }}</h2>
|
||||
<a href="{{ route('admin.categorias.index') }}" class="btn-admin-outline">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ $categoria ? route('admin.categorias.update', $categoria->id_categoria) : route('admin.categorias.store') }}" class="admin-form">
|
||||
@csrf
|
||||
@if($categoria) @method('PUT') @endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nombre (Ej: U15, U17) *</label>
|
||||
<input type="text" name="nombre" class="form-control" value="{{ old('nombre', $categoria->nombre ?? '') }}" required>
|
||||
@error('nombre') <div class="text-danger small mt-1">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Género (Opcional)</label>
|
||||
<select name="genero" class="form-select">
|
||||
<option value="">Mixto / Ambos</option>
|
||||
<option value="M" {{ old('genero', $categoria->genero ?? '') == 'M' ? 'selected' : '' }}>Masculino</option>
|
||||
<option value="F" {{ old('genero', $categoria->genero ?? '') == 'F' ? 'selected' : '' }}>Femenino</option>
|
||||
</select>
|
||||
@error('genero') <div class="text-danger small mt-1">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Edad Mínima (Años a cumplir en el año en curso) *</label>
|
||||
<input type="number" name="edad_min" class="form-control" value="{{ old('edad_min', $categoria->edad_min ?? 0) }}" min="0" required>
|
||||
@error('edad_min') <div class="text-danger small mt-1">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Edad Máxima (Años a cumplir en el año en curso) *</label>
|
||||
<input type="number" name="edad_max" class="form-control" value="{{ old('edad_max', $categoria->edad_max ?? 99) }}" min="0" required>
|
||||
@error('edad_max') <div class="text-danger small mt-1">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="es_libre" name="es_libre" value="1" {{ old('es_libre', $categoria->es_libre ?? false) ? 'checked' : '' }}>
|
||||
<label class="form-check-label ms-2" for="es_libre">
|
||||
<strong>Categoría Libre (Aforo/Promoción)</strong><br>
|
||||
<small class="text-muted">Si está marcado, los jugadores de esta categoría podrán solicitar un QR para eventos ajenos a su club (obtendrán QR válido + 50% descuento en entrada).</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-admin mt-3">
|
||||
<i class="bi bi-save"></i> {{ $categoria ? 'Actualizar' : 'Guardar' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,68 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Categorías - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-5 d-flex justify-content-between align-items-end">
|
||||
<div>
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-2">Estructura de Competencia</span>
|
||||
<h1 class="display-4 fw-bold font-header mb-0">Categorías<span class="text-primary">.</span></h1>
|
||||
</div>
|
||||
<a href="{{ route('admin.categorias.create') }}" class="btn-admin-primary">
|
||||
<i class="bi bi-plus-lg me-2"></i> NUEVA CATEGORÍA
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
@if($categorias->isEmpty())
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-tag text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<p class="fs-5 text-muted">No hay categorías registradas.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="kinetic-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Categoría</th>
|
||||
<th>Rango de Edad</th>
|
||||
<th>Género</th>
|
||||
<th>Tipo</th>
|
||||
<th class="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($categorias as $cat)
|
||||
<tr>
|
||||
<td><span class="fw-bold fs-5 text-uppercase">{{ $cat->nombre }}</span></td>
|
||||
<td>
|
||||
<span class="fw-bold d-block">{{ $cat->edad_min }} - {{ $cat->edad_max }} años</span>
|
||||
</td>
|
||||
<td><span class="badge bg-light text-dark text-uppercase px-2 py-1">{{ $cat->genero ?? 'Mixto' }}</span></td>
|
||||
<td>
|
||||
@if($cat->es_libre)
|
||||
<span class="badge bg-primary text-white text-uppercase px-2 py-1"><i class="bi bi-star-fill me-1"></i> LIBRE</span>
|
||||
@else
|
||||
<span class="text-muted fw-bold small text-uppercase">Estándar</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('admin.categorias.edit', $cat->id_categoria) }}" class="btn btn-sm btn-light border me-1">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form action="{{ route('admin.categorias.destroy', $cat->id_categoria) }}" method="POST" class="d-inline delete-form confirm-submit" data-confirm-text="¿Eliminar esta categoría?">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,170 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', ($club ? 'Editar' : 'Nuevo') . ' Club - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-shield-fill"></i> {{ $club ? 'Editar Club' : 'Nuevo Club' }}</h2>
|
||||
<a href="{{ session('admin_role') == 1 ? route('admin.clubes.index') : route('admin.dashboard') }}" class="btn-admin-outline">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ $club ? route('admin.clubes.update', $club->id_club) : route('admin.clubes.store') }}" class="admin-form" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@if($club) @method('PUT') @endif
|
||||
|
||||
<div class="row">
|
||||
<!-- Columna Formulario -->
|
||||
<div class="col-lg-7">
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Nombre del Club *</label>
|
||||
<input type="text" name="nombre" class="form-control" value="{{ old('nombre', $club->nombre ?? '') }}" required {{ session('admin_role') == 2 ? 'readonly' : '' }}>
|
||||
@error('nombre')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="es_seleccion" id="es_seleccion" value="1"
|
||||
{{ old('es_seleccion', $club->es_seleccion ?? false) ? 'checked' : '' }}
|
||||
{{ session('admin_role') == 2 ? 'disabled' : '' }}>
|
||||
<label class="form-check-label fw-bold" for="es_seleccion">
|
||||
<i class="bi bi-star-fill text-warning me-1"></i> Es Selección
|
||||
</label>
|
||||
<div class="text-muted small mt-1">Los jugadores pueden pertenecer a su club habitual y además ser convocados a esta selección.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-light border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-3 border-bottom pb-2">Identidad del Club</h5>
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label"><i class="bi bi-shield-shaded"></i> Escudo / Logo del Club</label>
|
||||
<input type="file" name="logo_club" class="form-control" accept="image/*">
|
||||
<small class="text-muted d-block mt-1">Se mostrará en la cartelera de partidos y perfiles.</small>
|
||||
@error('logo_club')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<div class="p-2 bg-white rounded border d-inline-block shadow-sm">
|
||||
@if($club && $club->imagen)
|
||||
<img src="{{ asset($club->imagen) }}" alt="Logo Actual" style="height: 60px; object-fit: contain;">
|
||||
@else
|
||||
<i class="bi bi-shield-fill text-muted" style="font-size: 3rem;"></i>
|
||||
@endif
|
||||
</div>
|
||||
<div class="small text-muted mt-1">Logo Actual</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($club)
|
||||
<div class="card bg-light border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-3 border-bottom pb-2">Personalización de QR</h5>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label"><i class="bi bi-image"></i> Imagen de Fondo (Plantilla QR)</label>
|
||||
<input type="file" name="qr_background" id="qr_background_input" class="form-control" accept="image/*">
|
||||
<small class="text-muted d-block mt-1">Recomendado formato vertical (ej. 1080x1920px).</small>
|
||||
@error('qr_background')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="bi bi-palette"></i> Color Texto QR</label>
|
||||
<input type="color" name="qr_color_texto" id="qr_color_input" class="form-control form-control-color w-100" value="{{ old('qr_color_texto', $club->qr_color_texto ?? '#000000') }}" title="Elegir color">
|
||||
<small class="text-muted d-block mt-1">Este color se aplicará al texto sobre la plantilla.</small>
|
||||
@error('qr_color_texto')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<button type="submit" class="btn-admin w-100 py-3">
|
||||
<i class="bi bi-check-lg"></i> {{ $club ? 'Guardar Cambios' : 'Crear Club' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Columna Preview -->
|
||||
<div class="col-lg-5 text-center">
|
||||
<div class="sticky-top" style="top: 20px;">
|
||||
<h6 class="text-muted mb-3 text-uppercase fw-bold" style="font-size: 0.8rem; letter-spacing: 1px;">Vista Previa TICKET</h6>
|
||||
|
||||
<div id="qr_preview_card" class="mx-auto shadow-lg"
|
||||
style="width: 320px; height: 500px; border-radius: 25px; overflow: hidden; position: relative; transition: all 0.3s ease;
|
||||
{{ ($club && $club->qr_background) ? "background: url('".asset($club->qr_background)."') no-repeat center center; background-size: cover;" : "background: #fff; border: 1px solid #ddd;" }}">
|
||||
|
||||
<div id="preview_overlay" style="position: absolute; inset: 0; background: rgba(255,255,255,0.1); z-index: 1; {{ ($club && $club->qr_background) ? '' : 'display:none;' }}"></div>
|
||||
|
||||
<div id="preview_content" class="d-flex flex-column align-items-center justify-content-between p-4 h-100 text-center"
|
||||
style="position: relative; z-index: 5; color: {{ $club->qr_color_texto ?? '#333' }};">
|
||||
|
||||
<div class="mt-2 w-100 text-center">
|
||||
<img src="{{ asset('logo.png') }}" alt="OnAPB" style="height: 40px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));">
|
||||
<h6 class="mt-2 fw-bold" style="font-family: 'Bebas Neue', cursive; letter-spacing: 1px; font-size: 1.2rem;">ENTRADA DIGITAL</h6>
|
||||
</div>
|
||||
|
||||
<div class="qr-box p-2 bg-white shadow" style="border-radius: 15px;">
|
||||
<img src="https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=PREVIEW" alt="QR" style="width: 150px; opacity: 0.8;">
|
||||
</div>
|
||||
|
||||
<div class="w-100 mb-2">
|
||||
<h6 class="fw-bold mb-0 text-uppercase" style="font-size: 0.9rem;">JUAN PÉREZ</h6>
|
||||
<p class="small mb-0 opacity-75" style="font-size: 0.7rem;">PARTIDO: LOCAL VS VISITANTE</p>
|
||||
<p class="small mb-0 opacity-75" style="font-size: 0.7rem; font-weight: bold;">CATEGORÍA: U19 / PRIMERA</p>
|
||||
<p class="small mb-0 opacity-75" style="font-size: 0.7rem;">FECHA: 00/00/0000</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small mt-3 px-4">Esta es una simulación de cómo verá el jugador su ticket en el celular.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const bgInput = document.getElementById('qr_background_input');
|
||||
const colorInput = document.getElementById('qr_color_input');
|
||||
const previewCard = document.getElementById('qr_preview_card');
|
||||
const previewContent = document.getElementById('preview_content');
|
||||
const previewOverlay = document.getElementById('preview_overlay');
|
||||
|
||||
if (bgInput) {
|
||||
bgInput.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
previewCard.style.backgroundImage = `url('${event.target.result}')`;
|
||||
previewCard.style.backgroundSize = 'cover';
|
||||
previewCard.style.backgroundPosition = 'center';
|
||||
previewCard.style.border = 'none';
|
||||
previewOverlay.style.display = 'block';
|
||||
}
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (colorInput) {
|
||||
colorInput.addEventListener('input', function(e) {
|
||||
previewContent.style.color = e.target.value;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,139 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Clubes - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-5">
|
||||
<div class="d-flex justify-content-between align-items-end mb-4">
|
||||
<div>
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-2">Gestión de Afiliados</span>
|
||||
<h1 class="display-4 fw-bold font-header mb-0">Clubes<span class="text-primary">.</span></h1>
|
||||
</div>
|
||||
<a href="{{ route('admin.clubes.create') }}" class="btn-admin-primary">
|
||||
<i class="bi bi-plus-lg me-2"></i> NUEVO CLUB
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-lg-6">
|
||||
<form method="GET" action="{{ route('admin.clubes.index') }}" class="search-box-kinetic" id="clubes-search-form">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" name="q" id="clubes-search-input" class="form-control" placeholder="Buscar club por nombre..." value="{{ $search ?? '' }}" autocomplete="off">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="clubes-list-container">
|
||||
<div class="admin-card">
|
||||
@if($clubes->isEmpty())
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-shield text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<p class="fs-5 text-muted">{{ !empty($search) ? 'No se encontraron clubes para "' . $search . '"' : 'No hay clubes registrados.' }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="kinetic-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Club</th>
|
||||
<th>Equipos</th>
|
||||
<th>Jugadores</th>
|
||||
<th class="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($clubes as $club)
|
||||
<tr>
|
||||
<td><span class="badge bg-light text-dark px-2 py-1">#{{ $club->id_club }}</span></td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
@if($club->imagen)
|
||||
<img src="{{ asset($club->imagen) }}" alt="{{ $club->nombre }}" style="width: 40px; height: 40px; object-fit: contain;">
|
||||
@else
|
||||
<div class="bg-light d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
||||
<i class="bi bi-shield-shaded text-muted"></i>
|
||||
</div>
|
||||
@endif
|
||||
<span class="fw-bold fs-5">{{ $club->nombre }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-bold text-primary">{{ $club->equipos_count }}</span>
|
||||
<span class="small text-muted text-uppercase ms-1">Equipos</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-bold text-success">{{ $club->jugadores_count }}</span>
|
||||
<span class="small text-muted text-uppercase ms-1">Jugadores</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('admin.clubes.edit', $club->id_club) }}" class="btn btn-sm btn-light fw-bold text-uppercase border me-2">
|
||||
<i class="bi bi-pencil me-1"></i> Editar
|
||||
</a>
|
||||
<form action="{{ route('admin.clubes.destroy', $club->id_club) }}" method="POST" class="d-inline delete-form confirm-submit" data-confirm-text="¿Eliminar este club?">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger fw-bold text-uppercase">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
(function () {
|
||||
const form = document.getElementById('clubes-search-form');
|
||||
const input = document.getElementById('clubes-search-input');
|
||||
const container = document.getElementById('clubes-list-container');
|
||||
if (!form || !input || !container) return;
|
||||
|
||||
let debounceTimer = null;
|
||||
let activeRequest = null;
|
||||
const baseUrl = form.getAttribute('action');
|
||||
|
||||
form.addEventListener('submit', e => { e.preventDefault(); doSearch(input.value); });
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
clearTimeout(debounceTimer);
|
||||
const value = this.value;
|
||||
debounceTimer = setTimeout(() => doSearch(value), 350);
|
||||
});
|
||||
|
||||
async function doSearch(query) {
|
||||
const url = baseUrl + (query ? ('?q=' + encodeURIComponent(query)) : '');
|
||||
history.replaceState(null, '', url);
|
||||
|
||||
if (activeRequest) activeRequest.abort();
|
||||
activeRequest = new AbortController();
|
||||
|
||||
container.style.opacity = '0.55';
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'text/html' },
|
||||
signal: activeRequest.signal
|
||||
});
|
||||
const html = await res.text();
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const fresh = doc.getElementById('clubes-list-container');
|
||||
if (fresh) container.innerHTML = fresh.innerHTML;
|
||||
} catch (err) {
|
||||
if (err.name !== 'AbortError') console.error('Búsqueda en vivo falló:', err);
|
||||
} finally {
|
||||
container.style.opacity = '';
|
||||
activeRequest = null;
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,106 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Dashboard - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-5">
|
||||
@if($miClub)
|
||||
<div class="d-flex align-items-center gap-4">
|
||||
@if($miClub->qr_background)
|
||||
<div class="bg-white p-2 shadow-sm" style="border: 1px solid var(--admin-outline);">
|
||||
<img src="{{ asset($miClub->qr_background) }}" alt="Logo" style="height: 80px; width: 80px; object-fit: contain;">
|
||||
</div>
|
||||
@endif
|
||||
<div>
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-1">Panel de Club</span>
|
||||
<h1 class="display-4 fw-bold font-header mb-0">{{ $miClub->nombre }}<span class="text-primary">.</span></h1>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-2">Resumen General</span>
|
||||
<h1 class="display-4 fw-bold font-header">Panel de <span class="text-primary">Control.</span></h1>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Stats Row -->
|
||||
<div class="row g-4 mb-5" data-stagger>
|
||||
@if(session('admin_role') == 1)
|
||||
<div class="col-md-3">
|
||||
<div class="stat-card" data-tilt data-tilt-max="6">
|
||||
<div class="stat-value kfx-counter" data-target="{{ $stats['clubes'] }}">0</div>
|
||||
<div class="stat-label">Clubes</div>
|
||||
<i class="bi bi-shield-shaded position-absolute bottom-0 end-0 m-3 opacity-25 fs-1"></i>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="stat-card" data-tilt data-tilt-max="6" style="background: linear-gradient(135deg, #c20000, #8a0000) !important;">
|
||||
<div class="stat-value kfx-counter" data-target="{{ $stats['jugadores'] }}">0</div>
|
||||
<div class="stat-label">Jugadores</div>
|
||||
<i class="bi bi-person-badge position-absolute bottom-0 end-0 m-3 opacity-25 fs-1"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="stat-card" data-tilt data-tilt-max="6">
|
||||
<div class="stat-value kfx-counter" data-target="{{ $stats['eventos'] }}">0</div>
|
||||
<div class="stat-label">Partidos</div>
|
||||
<i class="bi bi-calendar-check position-absolute bottom-0 end-0 m-3 opacity-25 fs-1"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('admin_role') == 1)
|
||||
<div class="col-md-3">
|
||||
<div class="stat-card" data-tilt data-tilt-max="6">
|
||||
<div class="stat-value kfx-counter" data-target="{{ $stats['noticias'] }}">0</div>
|
||||
<div class="stat-label">Noticias</div>
|
||||
<i class="bi bi-newspaper position-absolute bottom-0 end-0 m-3 opacity-25 fs-1"></i>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="row g-5">
|
||||
<!-- Quick Actions -->
|
||||
<div class="col-lg-4 order-lg-2">
|
||||
<div class="admin-card" style="background: #000; color: #fff;">
|
||||
<div class="admin-card-header border-secondary border-opacity-50">
|
||||
<h3 class="text-white text-uppercase">Acciones Rápidas</h3>
|
||||
</div>
|
||||
<div class="d-grid gap-3">
|
||||
<a href="{{ route('admin.escanear') }}" class="btn btn-admin-primary py-3">
|
||||
<i class="bi bi-qr-code-scan me-2"></i> ESCANEAR QR
|
||||
</a>
|
||||
@if(session('admin_role') == 1)
|
||||
<a href="{{ route('admin.noticias.create') }}" class="btn btn-outline-light py-3 fw-bold text-uppercase" style="border-radius: 0;">
|
||||
<i class="bi bi-pencil-square me-2"></i> NUEVA NOTICIA
|
||||
</a>
|
||||
@endif
|
||||
<a href="{{ route('admin.eventos.index') }}" class="btn btn-outline-light py-3 fw-bold text-uppercase" style="border-radius: 0;">
|
||||
<i class="bi bi-calendar-plus me-2"></i> GESTIONAR PARTIDOS
|
||||
</a>
|
||||
@if($miClub)
|
||||
<a href="{{ route('admin.clubes.edit', $miClub->id_club) }}" class="btn btn-outline-light py-3 fw-bold text-uppercase" style="border-radius: 0;">
|
||||
<i class="bi bi-gear me-2"></i> CONFIGURAR CLUB
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Column -->
|
||||
<div class="col-lg-8 order-lg-1">
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3>Resumen de Actividad</h3>
|
||||
</div>
|
||||
<p class="text-muted">Bienvenido al nuevo panel de control de OnAPB. Desde aquí podés gestionar toda la información de la asociación con una interfaz optimizada para el rendimiento.</p>
|
||||
<div class="mt-4 p-4 bg-light border-start border-primary border-4">
|
||||
<h5 class="fw-bold mb-2">Tip de Administración</h5>
|
||||
<p class="small mb-0 text-muted">Recordá que podés escanear los QRs de los aficionados directamente desde el botón en la barra superior o en las acciones rápidas para validar ingresos en tiempo real.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,80 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', ($equipo ? 'Editar' : 'Nuevo') . ' Equipo - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-people-fill"></i> {{ $equipo ? 'Editar Equipo' : 'Nuevo Equipo' }}</h2>
|
||||
<a href="{{ route('admin.equipos.index') }}" class="btn-admin-outline">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ $equipo ? route('admin.equipos.update', $equipo->id_equipo) : route('admin.equipos.store') }}" class="admin-form">
|
||||
@csrf
|
||||
@if($equipo) @method('PUT') @endif
|
||||
|
||||
<div class="row">
|
||||
@if(session('admin_role') == 1)
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Club *</label>
|
||||
<select name="id_club" class="form-select" required>
|
||||
<option value="">Seleccionar club...</option>
|
||||
@foreach($clubes as $club)
|
||||
<option value="{{ $club->id_club }}" {{ old('id_club', $equipo->id_club ?? '') == $club->id_club ? 'selected' : '' }}>
|
||||
{{ $club->nombre }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('id_club')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<input type="hidden" name="id_club" value="{{ session('admin_id_club') }}">
|
||||
@endif
|
||||
<div class="col-md-{{ session('admin_role') == 1 ? '4' : '6' }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Categoría *</label>
|
||||
<select name="categoria" class="form-select" required>
|
||||
<option value="">Seleccionar categoría...</option>
|
||||
@foreach($categorias as $cat)
|
||||
<option value="{{ $cat->nombre }}" {{ old('categoria', $equipo->categoria ?? '') == $cat->nombre ? 'selected' : '' }}>
|
||||
{{ $cat->nombre }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('categoria')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">División *</label>
|
||||
<select name="division" class="form-select" required>
|
||||
<option value="">Seleccionar...</option>
|
||||
@foreach(['A', 'B', 'C', 'D'] as $div)
|
||||
<option value="{{ $div }}" {{ old('division', $equipo->division ?? '') == $div ? 'selected' : '' }}>
|
||||
{{ $div }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('division')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-admin">
|
||||
<i class="bi bi-check-lg"></i> {{ $equipo ? 'Actualizar' : 'Crear Equipo' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,139 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Equipos - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-5">
|
||||
<div class="d-flex justify-content-between align-items-end mb-4">
|
||||
<div>
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-2">Gestión Deportiva</span>
|
||||
<h1 class="display-4 fw-bold font-header mb-0">Equipos<span class="text-primary">.</span></h1>
|
||||
</div>
|
||||
<a href="{{ route('admin.equipos.create') }}" class="btn-admin-primary">
|
||||
<i class="bi bi-plus-lg me-2"></i> NUEVO EQUIPO
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-lg-6">
|
||||
<form method="GET" action="{{ route('admin.equipos.index') }}" class="search-box-kinetic" id="equipos-search-form">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" name="q" id="equipos-search-input" class="form-control"
|
||||
placeholder="Buscar por club, categoría, división o ID..."
|
||||
value="{{ $search ?? '' }}" autocomplete="off">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="equipos-list-container">
|
||||
<div class="admin-card">
|
||||
@if($equipos->isEmpty())
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-people text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<p class="fs-5 text-muted">
|
||||
{{ !empty($search) ? 'No se encontraron equipos para "' . $search . '"' : 'No hay equipos registrados.' }}
|
||||
</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="kinetic-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
@if(session('admin_role') == 1)
|
||||
<th>Club</th>
|
||||
@endif
|
||||
<th>Categoría</th>
|
||||
<th>División</th>
|
||||
<th>Plantel</th>
|
||||
<th class="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($equipos as $equipo)
|
||||
<tr>
|
||||
<td><span class="badge bg-light text-dark px-2 py-1">#{{ $equipo->id_equipo }}</span></td>
|
||||
@if(session('admin_role') == 1)
|
||||
<td><span class="fw-bold">{{ $equipo->club->nombre ?? '—' }}</span></td>
|
||||
@endif
|
||||
<td><span class="badge bg-dark text-white text-uppercase px-3 py-2" style="font-size: 0.75rem; letter-spacing: 0.05em;">{{ $equipo->categoria }}</span></td>
|
||||
<td><span class="text-muted fw-bold">{{ $equipo->division ?? '—' }}</span></td>
|
||||
<td>
|
||||
<a href="{{ route('admin.equipos.jugadores', $equipo->id_equipo) }}" class="text-decoration-none d-flex align-items-center gap-2">
|
||||
<span class="fw-bold text-success fs-5">{{ $equipo->jugadores_count }}</span>
|
||||
<span class="small text-muted text-uppercase">Jugadores registrados <i class="bi bi-arrow-right small"></i></span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('admin.equipos.edit', $equipo->id_equipo) }}" class="btn btn-sm btn-light fw-bold text-uppercase border me-2">
|
||||
<i class="bi bi-pencil me-1"></i> Editar
|
||||
</a>
|
||||
<form action="{{ route('admin.equipos.destroy', $equipo->id_equipo) }}" method="POST" class="d-inline delete-form confirm-submit" data-confirm-text="¿Eliminar este equipo?">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger fw-bold text-uppercase">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
(function () {
|
||||
const form = document.getElementById('equipos-search-form');
|
||||
const input = document.getElementById('equipos-search-input');
|
||||
const container = document.getElementById('equipos-list-container');
|
||||
if (!form || !input || !container) return;
|
||||
|
||||
let debounceTimer = null;
|
||||
let activeRequest = null;
|
||||
const baseUrl = form.getAttribute('action');
|
||||
|
||||
form.addEventListener('submit', e => { e.preventDefault(); doSearch(input.value); });
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
clearTimeout(debounceTimer);
|
||||
const value = this.value;
|
||||
debounceTimer = setTimeout(() => doSearch(value), 350);
|
||||
});
|
||||
|
||||
async function doSearch(query) {
|
||||
const url = baseUrl + (query ? ('?q=' + encodeURIComponent(query)) : '');
|
||||
history.replaceState(null, '', url);
|
||||
|
||||
if (activeRequest) activeRequest.abort();
|
||||
activeRequest = new AbortController();
|
||||
|
||||
container.style.opacity = '0.55';
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'text/html' },
|
||||
signal: activeRequest.signal
|
||||
});
|
||||
const html = await res.text();
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const fresh = doc.getElementById('equipos-list-container');
|
||||
if (fresh) {
|
||||
container.innerHTML = fresh.innerHTML;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name !== 'AbortError') console.error('Búsqueda en vivo falló:', err);
|
||||
} finally {
|
||||
container.style.opacity = '';
|
||||
activeRequest = null;
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,151 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Gestionar Jugadores - ' . $equipo->categoria . ' - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="mb-0"><i class="bi bi-person-plus-fill"></i> Gestionar Jugadores: {{ $equipo->categoria }} {{ $equipo->division }}</h2>
|
||||
<p class="text-muted mb-0">{{ $equipo->club->nombre }}</p>
|
||||
</div>
|
||||
<a href="{{ route('admin.equipos.index') }}" class="btn-admin-outline">
|
||||
<i class="bi bi-arrow-left"></i> Volver a Equipos
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Buscador de Jugadores -->
|
||||
<div class="col-md-5">
|
||||
<div class="admin-card">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">Buscar Jugadores del Club</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||
<input type="text" id="player-search" class="form-control" placeholder="Nombre, Apellido o DNI..." autocomplete="off">
|
||||
</div>
|
||||
<div id="search-results" class="list-group mt-2 shadow-sm" style="display: none; position: absolute; z-index: 1000; width: calc(100% - 40px);">
|
||||
<!-- Resultados AJAX -->
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">Escribe al menos 3 caracteres para buscar.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista de Jugadores Asignados -->
|
||||
<div class="col-md-7">
|
||||
<div class="admin-card">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Jugadores en este Equipo</h5>
|
||||
<span class="badge bg-primary">{{ $equipo->jugadores->count() }} Jugadores</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($equipo->jugadores->isEmpty())
|
||||
<div class="p-4 text-center text-muted">
|
||||
<i class="bi bi-info-circle mb-2 d-block fs-3"></i>
|
||||
No hay jugadores asignados a este equipo todavía.
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="admin-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Jugador</th>
|
||||
<th>DNI</th>
|
||||
<th>Acción</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($equipo->jugadores as $jugador)
|
||||
<tr>
|
||||
<td>{{ $jugador->id_jugador }}</td>
|
||||
<td><strong>{{ $jugador->apellido }}, {{ $jugador->nombre }}</strong></td>
|
||||
<td>{{ $jugador->documento }}</td>
|
||||
<td>
|
||||
<form action="{{ route('admin.equipos.jugadores.remove', [$equipo->id_equipo, $jugador->id_jugador]) }}" method="POST" class="confirm-submit" data-confirm-text="¿Remover al jugador de este equipo?">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulario oculto para añadir -->
|
||||
<form id="add-player-form" action="{{ route('admin.equipos.jugadores.add', $equipo->id_equipo) }}" method="POST" style="display: none;">
|
||||
@csrf
|
||||
<input type="hidden" name="id_jugador" id="target-player-id">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('player-search');
|
||||
const resultsDiv = document.getElementById('search-results');
|
||||
const addForm = document.getElementById('add-player-form');
|
||||
const targetInput = document.getElementById('target-player-id');
|
||||
|
||||
let timeout = null;
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(timeout);
|
||||
const query = this.value.trim();
|
||||
|
||||
if (query.length < 2) {
|
||||
resultsDiv.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
fetch(`{{ route('admin.jugadores.search.ajax') }}?q=${query}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
resultsDiv.innerHTML = '';
|
||||
if (data.length > 0) {
|
||||
data.forEach(player => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center';
|
||||
btn.innerHTML = `
|
||||
<div>
|
||||
<strong>${player.apellido}, ${player.nombre}</strong><br>
|
||||
<small class="text-muted">DNI: ${player.documento} | ID: ${player.id_jugador}</small>
|
||||
</div>
|
||||
<i class="bi bi-plus-circle text-success fs-5"></i>
|
||||
`;
|
||||
btn.onclick = () => {
|
||||
targetInput.value = player.id_jugador;
|
||||
addForm.submit();
|
||||
};
|
||||
resultsDiv.appendChild(btn);
|
||||
});
|
||||
resultsDiv.style.display = 'block';
|
||||
} else {
|
||||
resultsDiv.innerHTML = '<div class="list-group-item text-muted">No se encontraron jugadores</div>';
|
||||
resultsDiv.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Cerrar resultados al hacer clic fuera
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target !== searchInput && e.target !== resultsDiv) {
|
||||
resultsDiv.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,391 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Escanear QR - Admin OnAPB')
|
||||
|
||||
@section('styles')
|
||||
<style>
|
||||
.scanner-wrapper {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
#reader {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
height: 420px;
|
||||
margin: 0 auto;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #1a1a2e;
|
||||
}
|
||||
.evento-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #b00000, #d32f2f);
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.qr-result-card {
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
margin-top: 20px;
|
||||
animation: fadeInUp 0.3s ease;
|
||||
}
|
||||
.qr-result-card.success {
|
||||
background: #d4edda;
|
||||
border: 2px solid #28a745;
|
||||
color: #155724;
|
||||
}
|
||||
.qr-result-card.error {
|
||||
background: #f8d7da;
|
||||
border: 2px solid #dc3545;
|
||||
color: #721c24;
|
||||
}
|
||||
.qr-result-card.warning {
|
||||
background: #fff3cd;
|
||||
border: 2px solid #ffc107;
|
||||
color: #856404;
|
||||
}
|
||||
.qr-result-card h4 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.scanner-btn {
|
||||
padding: 12px 28px;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
transition: all 0.2s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.scanner-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.manual-input-group {
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-qr-code-scan"></i> Escanear / Validar QR</h2>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="card-body">
|
||||
<div class="scanner-wrapper text-center">
|
||||
|
||||
{{-- 1. Selector de Evento --}}
|
||||
<div class="mb-4">
|
||||
<label for="eventoSelect" class="form-label fw-bold" style="font-size: 1rem;">
|
||||
<i class="bi bi-calendar-event"></i> Seleccionar evento:
|
||||
</label>
|
||||
<select id="eventoSelect" class="form-select" style="max-width: 420px; margin: 0 auto;">
|
||||
<option value="">-- Seleccioná un evento --</option>
|
||||
@foreach($eventos as $ev)
|
||||
<option value="{{ $ev->id_evento }}"
|
||||
data-nombre="{{ $ev->nombre_evento }} ({{ $ev->fecha_evento ? \Carbon\Carbon::parse($ev->fecha_evento)->format('d/m/Y') : '' }} {{ substr($ev->hora_inicio, 0, 5) }})">
|
||||
{{ $ev->nombre_evento }} ({{ $ev->fecha_evento ? \Carbon\Carbon::parse($ev->fecha_evento)->format('d/m/Y') : '' }} {{ substr($ev->hora_inicio, 0, 5) }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Badge del evento seleccionado --}}
|
||||
<div id="eventoSeleccionado" style="display: none;"></div>
|
||||
|
||||
<p class="text-muted mb-3">Apuntá con la cámara al código QR o ingresalo manualmente.</p>
|
||||
|
||||
{{-- 2. Botones de cámara --}}
|
||||
<div class="mb-3 d-flex gap-2 justify-content-center">
|
||||
<button id="startBtn" class="scanner-btn btn btn-primary d-none">
|
||||
<i class="bi bi-camera-video-fill"></i> Iniciar escaneo
|
||||
</button>
|
||||
<button id="stopBtn" class="scanner-btn btn btn-danger d-none">
|
||||
<i class="bi bi-stop-circle-fill"></i> Detener
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- 3. Visor de cámara --}}
|
||||
<div id="reader" class="d-none"></div>
|
||||
|
||||
{{-- 4. Input manual (fallback) --}}
|
||||
<div class="mt-4">
|
||||
<p class="text-muted small mb-2"><i class="bi bi-keyboard"></i> O ingresá el código manualmente:</p>
|
||||
<div class="input-group manual-input-group">
|
||||
<input type="text" id="qrManualInput" class="form-control form-control-lg" placeholder="Código QR...">
|
||||
<button class="btn btn-admin" id="btnManualValidar" onclick="validarManual()">
|
||||
<i class="bi bi-search"></i> Validar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 5. Resultado --}}
|
||||
<div id="qrResult" style="position: relative; z-index: 10;"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Sonidos de feedback --}}
|
||||
<audio id="beep-ok" src="{{ asset('static/sounds/beep-ok.mp3') }}" preload="auto"></audio>
|
||||
<audio id="beep-error" src="{{ asset('static/sounds/beep-error.mp3') }}" preload="auto"></audio>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
{{-- Librería html5-qrcode --}}
|
||||
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
|
||||
<script>
|
||||
let html5QrCode;
|
||||
const VALIDAR_URL = '{{ route("admin.validar.qr") }}';
|
||||
const CSRF_TOKEN = '{{ csrf_token() }}';
|
||||
|
||||
// ─────────────────────────────────
|
||||
// Evento select → mostrar/ocultar botones
|
||||
// ─────────────────────────────────
|
||||
document.getElementById('eventoSelect').addEventListener('change', function(e) {
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const eventoInfo = document.getElementById('eventoSeleccionado');
|
||||
const selected = e.target.options[e.target.selectedIndex];
|
||||
|
||||
if (e.target.value) {
|
||||
startBtn.classList.remove('d-none');
|
||||
eventoInfo.innerHTML = '<span class="evento-badge"><i class="bi bi-check-circle-fill"></i> ' + selected.dataset.nombre + '</span>';
|
||||
eventoInfo.style.display = 'block';
|
||||
} else {
|
||||
startBtn.classList.add('d-none');
|
||||
eventoInfo.style.display = 'none';
|
||||
eventoInfo.innerHTML = '';
|
||||
}
|
||||
});
|
||||
|
||||
// ─────────────────────────────────
|
||||
// Feedback (sonido + vibración)
|
||||
// ─────────────────────────────────
|
||||
function playFeedback(success) {
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(success ? 200 : [100, 100, 200]);
|
||||
}
|
||||
const audio = document.getElementById(success ? 'beep-ok' : 'beep-error');
|
||||
if (audio) {
|
||||
audio.currentTime = 0;
|
||||
audio.play().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────
|
||||
// Mostrar resultado (SweetAlert2)
|
||||
// ─────────────────────────────────
|
||||
function showResult(data) {
|
||||
const resultDiv = document.getElementById('qrResult');
|
||||
|
||||
// Limpiar mensaje anterior del div (opcional)
|
||||
resultDiv.innerHTML = '';
|
||||
|
||||
if (data.valid) {
|
||||
playFeedback(true);
|
||||
|
||||
// Determinar icono y titulo segun tipo
|
||||
let icon = 'success';
|
||||
let title = '¡QR Válido!';
|
||||
let confirmColor = '#2e7d32';
|
||||
|
||||
if (data.data.tipo === 'libre_50') {
|
||||
icon = 'info';
|
||||
title = '¡Descuento 50%!';
|
||||
confirmColor = '#0277bd';
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
icon: icon,
|
||||
title: title,
|
||||
iconColor: confirmColor,
|
||||
html: `
|
||||
<div class="text-start">
|
||||
<p class="mb-2"><strong>${data.message}</strong></p>
|
||||
<p class="mb-1 small"><b>Titular:</b> ${data.data.titular}</p>
|
||||
<p class="mb-1 small"><b>Categoría:</b> ${data.data.categoria || 'N/A'}</p>
|
||||
<p class="mb-1 small"><b>Evento:</b> ${data.data.evento}</p>
|
||||
<p class="mb-0 small text-muted">Aforo restante: ${data.data.restantes}</p>
|
||||
</div>
|
||||
`,
|
||||
confirmButtonText: 'Siguiente',
|
||||
confirmButtonColor: confirmColor,
|
||||
timer: 5000,
|
||||
timerProgressBar: true
|
||||
});
|
||||
} else {
|
||||
playFeedback(false);
|
||||
const isWarning = data.message.includes('⏳');
|
||||
const errorColor = isWarning ? '#fbc02d' : '#b00000';
|
||||
|
||||
Swal.fire({
|
||||
icon: isWarning ? 'warning' : 'error',
|
||||
title: isWarning ? 'Evento no iniciado' : 'QR Inválido',
|
||||
text: data.message,
|
||||
iconColor: errorColor,
|
||||
confirmButtonText: 'Reintentar',
|
||||
confirmButtonColor: errorColor
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────
|
||||
// Validar QR (llamada al backend)
|
||||
// ─────────────────────────────────
|
||||
function validarQr(id_qr) {
|
||||
const id_evento = document.getElementById('eventoSelect').value;
|
||||
const resultDiv = document.getElementById('qrResult');
|
||||
resultDiv.innerHTML = '<div class="text-muted mt-3"><i class="bi bi-hourglass-split"></i> Validando...</div>';
|
||||
|
||||
fetch(VALIDAR_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': CSRF_TOKEN,
|
||||
},
|
||||
body: JSON.stringify({ id_qr: id_qr, id_evento: id_evento })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => showResult(data))
|
||||
.catch(err => {
|
||||
playFeedback(false);
|
||||
resultDiv.innerHTML = `
|
||||
<div class="qr-result-card error">
|
||||
<h4><i class="bi bi-x-circle-fill"></i> Error de conexión</h4>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────────────────────────
|
||||
// Escaneo con cámara
|
||||
// ─────────────────────────────────
|
||||
function onScanSuccess(decodedText) {
|
||||
stopScanner();
|
||||
|
||||
let id_qr;
|
||||
try {
|
||||
// Intentar parsear como URL (el QR viejo tiene ?id_qr=...)
|
||||
const url = new URL(decodedText);
|
||||
id_qr = url.searchParams.get('id_qr');
|
||||
} catch (e) {
|
||||
// Si no es URL, usar el texto directamente como id_qr
|
||||
id_qr = decodedText.trim();
|
||||
}
|
||||
|
||||
if (!id_qr) {
|
||||
playFeedback(false);
|
||||
document.getElementById('qrResult').innerHTML = `
|
||||
<div class="qr-result-card error">
|
||||
<h4><i class="bi bi-x-circle-fill"></i> QR inválido</h4>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const id_evento = document.getElementById('eventoSelect').value;
|
||||
if (!id_evento) {
|
||||
playFeedback(false);
|
||||
document.getElementById('qrResult').innerHTML = `
|
||||
<div class="qr-result-card warning">
|
||||
<h4><i class="bi bi-exclamation-triangle-fill"></i> Seleccioná un evento antes de escanear</h4>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
validarQr(id_qr);
|
||||
}
|
||||
|
||||
function stopScanner() {
|
||||
if (html5QrCode) {
|
||||
html5QrCode.stop().then(() => {
|
||||
document.getElementById('reader').innerHTML = '';
|
||||
document.getElementById('reader').classList.add('d-none');
|
||||
document.getElementById('startBtn').classList.remove('d-none');
|
||||
document.getElementById('stopBtn').classList.add('d-none');
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Iniciar
|
||||
document.getElementById('startBtn').addEventListener('click', () => {
|
||||
// Verificar Contexto Seguro (Indispensable para cámara en móviles)
|
||||
if (!window.isSecureContext && window.location.protocol !== 'http:' && window.location.hostname !== 'localhost') {
|
||||
playFeedback(false);
|
||||
document.getElementById('qrResult').innerHTML = `
|
||||
<div class="qr-result-card error">
|
||||
<h4><i class="bi bi-shield-lock-fill"></i> Error: Conexión no segura</h4>
|
||||
<p>La cámara solo funciona bajo <b>HTTPS</b>. Asegurate de estar usando la URL que empieza con https://.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const readerEl = document.getElementById('reader');
|
||||
readerEl.classList.remove('d-none');
|
||||
|
||||
// Si ya existe instancia previa, limpiarla
|
||||
if (html5QrCode) {
|
||||
html5QrCode.clear();
|
||||
}
|
||||
|
||||
html5QrCode = new Html5Qrcode('reader');
|
||||
|
||||
html5QrCode.start(
|
||||
{ facingMode: 'environment' },
|
||||
{ fps: 10, qrbox: { width: 300, height: 300 } },
|
||||
onScanSuccess
|
||||
).then(() => {
|
||||
document.getElementById('startBtn').classList.add('d-none');
|
||||
document.getElementById('stopBtn').classList.remove('d-none');
|
||||
}).catch(err => {
|
||||
playFeedback(false);
|
||||
let errorMsg = err;
|
||||
if (err.includes('Permission denied')) errorMsg = "Permiso de cámara denegado. Habilitá la cámara en los ajustes del sitio.";
|
||||
if (err.includes('not supported')) errorMsg = "Tu navegador no soporta streaming de cámara o requiere HTTPS.";
|
||||
|
||||
document.getElementById('qrResult').innerHTML = `
|
||||
<div class="qr-result-card error">
|
||||
<h4><i class="bi bi-camera-video-off-fill"></i> Error al iniciar cámara</h4>
|
||||
<p class="small">${errorMsg}</p>
|
||||
<p class="mt-2 small text-muted">Asegurate de usar Chrome o Safari y haber aceptado los permisos.</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
});
|
||||
|
||||
// Detener manual
|
||||
document.getElementById('stopBtn').addEventListener('click', stopScanner);
|
||||
|
||||
// ─────────────────────────────────
|
||||
// Validar manual
|
||||
// ─────────────────────────────────
|
||||
function validarManual() {
|
||||
const code = document.getElementById('qrManualInput').value.trim();
|
||||
if (!code) return;
|
||||
|
||||
validarQr(code);
|
||||
document.getElementById('qrManualInput').value = '';
|
||||
document.getElementById('qrManualInput').focus();
|
||||
}
|
||||
|
||||
document.getElementById('qrManualInput').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') validarManual();
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,288 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', ($evento ? 'Editar' : 'Nuevo') . ' Evento - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-calendar-event-fill"></i> {{ $evento ? 'Editar Evento' : 'Nuevo Evento' }}</h2>
|
||||
<a href="{{ route('admin.eventos.index') }}" class="btn-admin-outline">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ $evento ? route('admin.eventos.update', $evento->id_evento) : route('admin.eventos.store') }}" class="admin-form">
|
||||
@csrf
|
||||
@if($evento) @method('PUT') @endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nombre del Evento (Opcional)</label>
|
||||
<input type="text" name="nombre_evento" class="form-control" value="{{ old('nombre_evento', $evento->nombre_evento ?? '') }}" placeholder="Ej: Paracao vs Talleres (Primera A)" {{ session('admin_role') == 2 ? 'readonly' : '' }}>
|
||||
<small class="text-muted">Si se deja en blanco, se generará automáticamente usando los nombres de los clubes.</small>
|
||||
@error('nombre_evento')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-primary fw-bold">Torneo</label>
|
||||
<select name="id_torneo" id="id_torneo" class="form-select border-primary" {{ (session('admin_role') == 2 || $evento) ? 'disabled' : '' }}>
|
||||
<option value="">— Ninguno / Amistoso —</option>
|
||||
@foreach($torneos as $t)
|
||||
<option value="{{ $t->id }}" {{ old('id_torneo', $evento->id_torneo ?? '') == $t->id ? 'selected' : '' }}>
|
||||
{{ $t->nombre }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if($evento)
|
||||
<input type="hidden" name="id_torneo" value="{{ $evento->id_torneo }}">
|
||||
@endif
|
||||
@error('id_torneo')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-primary fw-bold">Grupo / Zona</label>
|
||||
<select id="select_grupo" class="form-select border-primary" {{ (session('admin_role') == 2 || $evento) ? 'disabled' : '' }}>
|
||||
<option value="">Seleccione Torneo primero...</option>
|
||||
</select>
|
||||
<small class="text-muted">Solo obligatorio para torneos.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Fecha *</label>
|
||||
<input type="date" name="fecha_evento" class="form-control" value="{{ old('fecha_evento', $evento && $evento->fecha_evento ? \Carbon\Carbon::parse($evento->fecha_evento)->format('Y-m-d') : '') }}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Hora Inicio *</label>
|
||||
<input type="time" name="hora_inicio" class="form-control" value="{{ old('hora_inicio', $evento ? substr($evento->hora_inicio, 0, 5) : '') }}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Hora Fin *</label>
|
||||
<input type="time" name="hora_fin" class="form-control" value="{{ old('hora_fin', $evento ? substr($evento->hora_fin, 0, 5) : '') }}" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Sede *</label>
|
||||
<input type="text" name="sede" class="form-control" value="{{ old('sede', $evento->sede ?? '') }}" {{ session('admin_role') == 2 ? 'readonly' : '' }} required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Equipo Local *</label>
|
||||
<select name="id_equipo_local" id="id_equipo_local" class="form-select equipo-select" {{ $evento ? 'disabled' : '' }} required>
|
||||
<option value="">Seleccionar...</option>
|
||||
@foreach($equipos as $eq)
|
||||
<option value="{{ $eq->id_equipo }}"
|
||||
data-torneos="{{ $eq->torneos->pluck('id')->join(',') }}"
|
||||
{{ old('id_equipo_local', $evento->id_equipo_local ?? '') == $eq->id_equipo ? 'selected' : '' }}>
|
||||
{{ $eq->club->nombre ?? '?' }} - {{ $eq->categoria }} {{ $eq->division }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if($evento)
|
||||
<input type="hidden" name="id_equipo_local" value="{{ $evento->id_equipo_local }}">
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Equipo Visitante *</label>
|
||||
<select name="id_equipo_visitante" id="id_equipo_visitante" class="form-select equipo-select" {{ $evento ? 'disabled' : '' }} required>
|
||||
<option value="">Seleccionar...</option>
|
||||
@foreach($equipos as $eq)
|
||||
<option value="{{ $eq->id_equipo }}"
|
||||
data-torneos="{{ $eq->torneos->pluck('id')->join(',') }}"
|
||||
{{ old('id_equipo_visitante', $evento->id_equipo_visitante ?? '') == $eq->id_equipo ? 'selected' : '' }}>
|
||||
{{ $eq->club->nombre ?? '?' }} - {{ $eq->categoria }} {{ $eq->division }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if($evento)
|
||||
<input type="hidden" name="id_equipo_visitante" value="{{ $evento->id_equipo_visitante }}">
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Precio ($)</label>
|
||||
<input type="number" step="0.01" name="precio" class="form-control" value="{{ old('precio', $evento->precio ?? 0) }}" {{ session('admin_role') == 2 ? 'readonly' : '' }}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Límite QRs por Jugador *</label>
|
||||
<input type="number" name="limite_qr_jugador" class="form-control" value="{{ old('limite_qr_jugador', $evento->limite_qr_jugador ?? 3) }}" required min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 bg-light rounded border border-primary border-opacity-25">
|
||||
<label class="form-label d-block fw-bold text-primary mb-3"><i class="bi bi-scoreboard"></i> RESULTADO FINAL (Opcional)</label>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="flex-grow-1">
|
||||
<small class="text-muted d-block mb-1 text-center">LOCAL</small>
|
||||
<input type="number" name="marcador_local" class="form-control text-center fs-4 fw-bold" value="{{ old('marcador_local', $evento->marcador_local ?? 0) }}" min="0">
|
||||
</div>
|
||||
<div class="pt-3 fs-3 fw-bold">:</div>
|
||||
<div class="flex-grow-1">
|
||||
<small class="text-muted d-block mb-1 text-center">VISITANTE</small>
|
||||
<input type="number" name="marcador_visitante" class="form-control text-center fs-4 fw-bold" value="{{ old('marcador_visitante', $evento->marcador_visitante ?? 0) }}" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn-admin-primary px-5 py-3">
|
||||
<i class="bi bi-check-lg me-2"></i> {{ $noticia ?? '' ? 'Actualizar' : ($evento ? 'ACTUALIZAR EVENTO' : 'CREAR EVENTO') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const torneoSelect = document.getElementById('id_torneo');
|
||||
const grupoSelect = document.getElementById('select_grupo');
|
||||
const equipoLocalSelect = document.getElementById('id_equipo_local');
|
||||
const equipoVisitanteSelect = document.getElementById('id_equipo_visitante');
|
||||
|
||||
// Inyectamos el mapeo desde PHP
|
||||
const mapping = @json($torneoEquipos);
|
||||
// Equipos actuales (para cuando no hay torneo)
|
||||
const initialEquiposHtml = equipoLocalSelect.innerHTML;
|
||||
|
||||
function updateGroups() {
|
||||
const torneoId = torneoSelect.value;
|
||||
grupoSelect.innerHTML = '';
|
||||
|
||||
if (!torneoId) {
|
||||
grupoSelect.innerHTML = '<option value="">N/A (Amistoso)</option>';
|
||||
grupoSelect.disabled = true;
|
||||
unlockTeams(true); // Desbloquear para amistosos
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtener grupos únicos para este torneo
|
||||
const grupos = [...new Set(mapping.filter(m => m.id_torneo == torneoId).map(m => m.grupo || 'General'))];
|
||||
|
||||
grupoSelect.innerHTML = '<option value="">Seleccione Grupo...</option>';
|
||||
grupos.forEach(g => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = g;
|
||||
opt.textContent = g;
|
||||
grupoSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
grupoSelect.disabled = false;
|
||||
unlockTeams(false); // Bloquear equipos hasta elegir grupo
|
||||
}
|
||||
|
||||
function unlockTeams(unlock) {
|
||||
equipoLocalSelect.disabled = !unlock;
|
||||
equipoVisitanteSelect.disabled = !unlock;
|
||||
if (!unlock) {
|
||||
equipoLocalSelect.value = "";
|
||||
equipoVisitanteSelect.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function filterEquiposByGroup() {
|
||||
const torneoId = torneoSelect.value;
|
||||
const grupo = grupoSelect.value;
|
||||
|
||||
if (!torneoId) {
|
||||
// Caso amistoso: restaurar todos
|
||||
equipoLocalSelect.innerHTML = initialEquiposHtml;
|
||||
equipoVisitanteSelect.innerHTML = initialEquiposHtml;
|
||||
unlockTeams(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!grupo) {
|
||||
unlockTeams(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtener equipos del torneo y grupo
|
||||
const equiposInGroup = mapping.filter(m => m.id_torneo == torneoId && (m.grupo == grupo || (!m.grupo && grupo == 'General')));
|
||||
const idsInGroup = equiposInGroup.map(m => m.id_equipo.toString());
|
||||
|
||||
[equipoLocalSelect, equipoVisitanteSelect].forEach(select => {
|
||||
const currentVal = select.value;
|
||||
select.innerHTML = '<option value="">Seleccionar...</option>';
|
||||
|
||||
// Re-poblar solo con los válidos
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(`<div>${initialEquiposHtml}</div>`, 'text/html');
|
||||
const originalOptions = doc.querySelectorAll('option');
|
||||
|
||||
originalOptions.forEach(opt => {
|
||||
if (opt.value && idsInGroup.includes(opt.value)) {
|
||||
const newOpt = document.createElement('option');
|
||||
newOpt.value = opt.value;
|
||||
newOpt.textContent = opt.textContent;
|
||||
select.appendChild(newOpt);
|
||||
}
|
||||
});
|
||||
|
||||
select.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
torneoSelect.addEventListener('change', updateGroups);
|
||||
grupoSelect.addEventListener('change', filterEquiposByGroup);
|
||||
|
||||
// Manejar estado inicial (Edición)
|
||||
if (torneoSelect.value) {
|
||||
updateGroups();
|
||||
|
||||
// Intentar inferir el grupo del equipo local
|
||||
const firstTeamId = document.getElementsByName('id_equipo_local')[0].value;
|
||||
if (firstTeamId) {
|
||||
const rel = mapping.find(m => m.id_torneo == torneoSelect.value && m.id_equipo == firstTeamId);
|
||||
if (rel) {
|
||||
grupoSelect.value = rel.grupo || 'General';
|
||||
filterEquiposByGroup();
|
||||
|
||||
// Asegurar que los selectores (aunque deshabilitados) muestren los equipos correctos
|
||||
@if($evento)
|
||||
equipoLocalSelect.value = "{{ $evento->id_equipo_local }}";
|
||||
equipoVisitanteSelect.value = "{{ $evento->id_equipo_visitante }}";
|
||||
@endif
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Si no hay torneo y no es edición restringida, dejar libre
|
||||
@if(!$evento)
|
||||
unlockTeams(true);
|
||||
@endif
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,125 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Partidos - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-5 d-flex justify-content-between align-items-end">
|
||||
<div>
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-2">Calendario de Juego</span>
|
||||
<h1 class="display-4 fw-bold font-header mb-0">Partidos<span class="text-primary">.</span></h1>
|
||||
</div>
|
||||
@if(session('admin_role') == 1)
|
||||
<a href="{{ route('admin.eventos.create') }}" class="btn-admin-primary">
|
||||
<i class="bi bi-plus-lg me-2"></i> NUEVO EVENTO
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mb-4 d-flex gap-2 animate-on-scroll">
|
||||
<a href="{{ route('admin.eventos.index', ['estado' => 'todos']) }}"
|
||||
class="btn {{ $estado == 'todos' ? 'btn-primary' : 'btn-outline-primary' }} btn-sm fw-bold text-uppercase px-4 rounded-pill shadow-sm transition-all">
|
||||
<i class="bi bi-collection-play me-1"></i> TODOS
|
||||
</a>
|
||||
<a href="{{ route('admin.eventos.index', ['estado' => 'pendientes']) }}"
|
||||
class="btn {{ $estado == 'pendientes' ? 'btn-primary' : 'btn-outline-primary' }} btn-sm fw-bold text-uppercase px-4 rounded-pill shadow-sm transition-all">
|
||||
<i class="bi bi-clock-history me-1"></i> PENDIENTES
|
||||
</a>
|
||||
<a href="{{ route('admin.eventos.index', ['estado' => 'finalizados']) }}"
|
||||
class="btn {{ $estado == 'finalizados' ? 'btn-primary' : 'btn-outline-primary' }} btn-sm fw-bold text-uppercase px-4 rounded-pill shadow-sm transition-all">
|
||||
<i class="bi bi-check-circle-fill me-1"></i> FINALIZADOS
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
@if($eventos->isEmpty())
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-calendar-event text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<p class="fs-5 text-muted">No hay eventos registrados.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="kinetic-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Estado</th>
|
||||
<th>Partido</th>
|
||||
<th>Fecha / Hora</th>
|
||||
<th>Sede</th>
|
||||
<th class="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($eventos as $e)
|
||||
<tr>
|
||||
<td>
|
||||
@php
|
||||
$ahoraStr = \Carbon\Carbon::now()->toDateTimeString();
|
||||
|
||||
// Extraer fecha y hora de forma segura (soportando tanto strings como objetos Carbon)
|
||||
$f = $e->fecha_evento instanceof \Carbon\Carbon ? $e->fecha_evento->format('Y-m-d') : substr((string)$e->fecha_evento, 0, 10);
|
||||
$h = $e->hora_fin instanceof \Carbon\Carbon ? $e->hora_fin->format('H:i:s') : (string)$e->hora_fin;
|
||||
if (strlen($h) == 5) $h .= ':00'; // Asegurar formato H:i:s
|
||||
|
||||
$momentoFin = "$f $h";
|
||||
|
||||
// Un evento esta finalizado si tiene marcadores Y el tiempo de fin ya paso
|
||||
$tieneMarcadores = !is_null($e->marcador_local) && !is_null($e->marcador_visitante);
|
||||
$finalizado = $tieneMarcadores && ($ahoraStr >= $momentoFin);
|
||||
@endphp
|
||||
|
||||
@if($finalizado)
|
||||
<span class="badge bg-success-container text-success px-2 py-1 x-small fw-bold">FINALIZADO</span>
|
||||
<div class="mt-1 small fw-bold text-center border rounded py-1">
|
||||
{{ $e->marcador_local }} - {{ $e->marcador_visitante }}
|
||||
</div>
|
||||
@else
|
||||
<span class="badge bg-warning-container text-warning px-2 py-1 x-small fw-bold">PENDIENTE</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="text-end" style="width: 150px;">
|
||||
<span class="fw-bold d-block">{{ $e->equipoLocal->club->nombre ?? 'Local' }}</span>
|
||||
<span class="small text-muted text-uppercase">{{ $e->equipoLocal->categoria ?? '' }} {{ $e->equipoLocal->division ?? '' }}</span>
|
||||
</div>
|
||||
<span class="fw-bold text-primary px-2">VS</span>
|
||||
<div style="width: 150px;">
|
||||
<span class="fw-bold d-block">{{ $e->equipoVisitante->club->nombre ?? 'Visitante' }}</span>
|
||||
<span class="small text-muted text-uppercase">{{ $e->equipoVisitante->categoria ?? '' }} {{ $e->equipoVisitante->division ?? '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-bold d-block">{{ $e->fecha_evento ? \Carbon\Carbon::parse($e->fecha_evento)->format('d/m/Y') : '—' }}</span>
|
||||
<span class="small text-muted">{{ $e->hora_inicio }} - {{ $e->hora_fin }}</span>
|
||||
</td>
|
||||
<td><span class="small fw-bold text-uppercase"><i class="bi bi-geo-alt me-1"></i> {{ $e->sede ?? 'TBD' }}</span></td>
|
||||
<td class="text-end">
|
||||
@if(session('admin_role') == 1)
|
||||
<a href="{{ route('admin.eventos.edit', $e->id_evento) }}" class="btn btn-sm btn-light fw-bold text-uppercase border me-1">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
@endif
|
||||
@if($e->id_equipo_local && $e->id_equipo_visitante)
|
||||
<a href="{{ route('admin.eventos.stats', $e->id_evento) }}" class="btn btn-sm btn-outline-success fw-bold text-uppercase me-1" title="Estadísticas">
|
||||
<i class="bi bi-person-lines-fill"></i>
|
||||
</a>
|
||||
@endif
|
||||
@if(session('admin_role') == 1)
|
||||
<form action="{{ route('admin.eventos.destroy', $e->id_evento) }}" method="POST" class="d-inline delete-form confirm-submit" data-confirm-text="¿Eliminar este evento?">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger fw-bold text-uppercase">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,121 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Cargar Resultados - ' . $evento->nombre_evento)
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="mb-0"><i class="bi bi-person-lines-fill"></i> Estadísticas de Jugadores</h2>
|
||||
<p class="text-muted mb-0">{{ $evento->nombre_evento }} | {{ $evento->fecha_evento->format('d/m/Y') }}</p>
|
||||
</div>
|
||||
<a href="{{ route('admin.eventos.index') }}" class="btn-admin-outline">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 text-primary fw-bold">Planilla de Juego</h5>
|
||||
<div class="fs-4 fw-bold">
|
||||
{{ $evento->marcador_local }} - {{ $evento->marcador_visitante }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('admin.eventos.stats.store', $evento->id_evento) }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="row">
|
||||
<!-- Equipo Local -->
|
||||
<div class="col-md-6 border-end">
|
||||
<h5 class="fw-bold mb-3 text-uppercase small tracking-widest">{{ $evento->equipoLocal->club->nombre ?? 'Local' }}</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Jugador</th>
|
||||
<th width="80" class="text-center">PTS</th>
|
||||
<th width="80" class="text-center">FALTAS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($evento->equipoLocal->jugadores as $j)
|
||||
@php
|
||||
$stat = $stats->get($j->id_jugador);
|
||||
$canEdit = (session('admin_role') == 1) || (session('admin_role') == 2 && $j->id_club_actual == session('admin_id_club'));
|
||||
@endphp
|
||||
<tr class="{{ !$canEdit ? 'row-disabled' : '' }}">
|
||||
<td>
|
||||
<div class="fw-bold">{{ $j->apellido }}, {{ $j->nombre }}</div>
|
||||
<small class="text-muted">DNI: {{ $j->documento }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" name="stats[{{ $j->id_jugador }}][puntos]" class="form-control form-control-sm text-center {{ !$canEdit ? 'input-disabled' : '' }}" value="{{ $stat->puntos ?? 0 }}" min="0" {{ $canEdit ? '' : 'readonly' }}>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" name="stats[{{ $j->id_jugador }}][faltas]" class="form-control form-control-sm text-center {{ !$canEdit ? 'input-disabled' : '' }}" value="{{ $stat->faltas ?? 0 }}" min="0" max="5" {{ $canEdit ? '' : 'readonly' }}>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Equipo Visitante -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="fw-bold mb-3 text-uppercase small tracking-widest">{{ $evento->equipoVisitante->club->nombre ?? 'Visitante' }}</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Jugador</th>
|
||||
<th width="80" class="text-center">PTS</th>
|
||||
<th width="80" class="text-center">FALTAS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($evento->equipoVisitante->jugadores as $j)
|
||||
@php
|
||||
$stat = $stats->get($j->id_jugador);
|
||||
$canEdit = (session('admin_role') == 1) || (session('admin_role') == 2 && $j->id_club_actual == session('admin_id_club'));
|
||||
@endphp
|
||||
<tr class="{{ !$canEdit ? 'row-disabled' : '' }}">
|
||||
<td>
|
||||
<div class="fw-bold">{{ $j->apellido }}, {{ $j->nombre }}</div>
|
||||
<small class="text-muted">DNI: {{ $j->documento }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" name="stats[{{ $j->id_jugador }}][puntos]" class="form-control form-control-sm text-center {{ !$canEdit ? 'input-disabled' : '' }}" value="{{ $stat->puntos ?? 0 }}" min="0" {{ $canEdit ? '' : 'readonly' }}>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" name="stats[{{ $j->id_jugador }}][faltas]" class="form-control form-control-sm text-center {{ !$canEdit ? 'input-disabled' : '' }}" value="{{ $stat->faltas ?? 0 }}" min="0" max="5" {{ $canEdit ? '' : 'readonly' }}>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-3 border-top text-end">
|
||||
<button type="submit" class="btn-admin-primary px-5">
|
||||
<i class="bi bi-save me-2"></i> GUARDAR ESTADÍSTICAS
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@section('styles')
|
||||
<style>
|
||||
.row-disabled {
|
||||
opacity: 0.7;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.input-disabled {
|
||||
background-color: #e9ecef !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
@endsection
|
||||
@@ -0,0 +1,154 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', ($jugador ? 'Editar' : 'Nuevo') . ' Jugador - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-person-badge-fill"></i> {{ $jugador ? 'Editar Jugador' : 'Nuevo Jugador' }}</h2>
|
||||
<a href="{{ route('admin.jugadores.index') }}" class="btn-admin-outline">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ $jugador ? route('admin.jugadores.update', $jugador->id_jugador) : route('admin.jugadores.store') }}" class="admin-form">
|
||||
@csrf
|
||||
@if($jugador) @method('PUT') @endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Documento (DNI) *</label>
|
||||
<input type="text" name="documento" class="form-control" value="{{ old('documento', $jugador->documento ?? '') }}" required>
|
||||
@error('documento')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nombre *</label>
|
||||
<input type="text" name="nombre" class="form-control" value="{{ old('nombre', $jugador->nombre ?? '') }}" required>
|
||||
@error('nombre')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Apellido *</label>
|
||||
<input type="text" name="apellido" class="form-control" value="{{ old('apellido', $jugador->apellido ?? '') }}" required>
|
||||
@error('apellido')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Fecha de Nacimiento *</label>
|
||||
<input type="date" name="fecha_nacimiento" id="fecha_nacimiento_input" class="form-control"
|
||||
value="{{ old('fecha_nacimiento', $jugador && $jugador->fecha_nacimiento ? $jugador->fecha_nacimiento->format('Y-m-d') : '') }}" required>
|
||||
@error('fecha_nacimiento')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
{{-- Badge de categoría calculada automáticamente --}}
|
||||
<div id="categoria-sugerida" class="mt-2" style="display:none;">
|
||||
<span class="badge fs-6 px-3 py-2" style="background-color:#212529;color:#fff;">
|
||||
<i class="bi bi-tag-fill me-1"></i>
|
||||
Categoría: <strong id="categoria-nombre">—</strong>
|
||||
</span>
|
||||
<div class="text-muted small mt-1">Calculada automáticamente según la fecha.</div>
|
||||
</div>
|
||||
@if($jugador && $jugador->fecha_nacimiento)
|
||||
<div class="mt-2">
|
||||
<span class="badge fs-6 px-3 py-2" style="background-color:#6c757d;color:#fff;">
|
||||
<i class="bi bi-tag-fill me-1"></i>
|
||||
Categoría actual: <strong>{{ $jugador->categoria_calculada }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Club Actual</label>
|
||||
@if(session('admin_role') == 1)
|
||||
<select name="id_club_actual" class="form-select">
|
||||
<option value="">Sin club</option>
|
||||
@foreach($clubes as $club)
|
||||
<option value="{{ $club->id_club }}" {{ old('id_club_actual', $jugador->id_club_actual ?? '') == $club->id_club ? 'selected' : '' }}>
|
||||
{{ $club->nombre }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@else
|
||||
<input type="text" class="form-control bg-light" value="{{ session('admin_club_nombre', 'Tu Club') }}" disabled>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Club Origen</label>
|
||||
<select name="id_club_origen" class="form-select">
|
||||
<option value="">Sin club</option>
|
||||
@foreach($clubes as $club)
|
||||
<option value="{{ $club->id_club }}" {{ old('id_club_origen', $jugador->id_club_origen ?? '') == $club->id_club ? 'selected' : '' }}>
|
||||
{{ $club->nombre }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" name="email" class="form-control" value="{{ old('email', $jugador->email ?? '') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Teléfono</label>
|
||||
<input type="text" name="telefono" class="form-control" value="{{ old('telefono', $jugador->telefono ?? '') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-admin">
|
||||
<i class="bi bi-check-lg"></i> {{ $jugador ? 'Actualizar' : 'Crear Jugador' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fechaInput = document.getElementById('fecha_nacimiento_input');
|
||||
const categoriaBadge = document.getElementById('categoria-sugerida');
|
||||
const categoriaNombre = document.getElementById('categoria-nombre');
|
||||
|
||||
if (!fechaInput) return;
|
||||
|
||||
fechaInput.addEventListener('change', function () {
|
||||
const fecha = this.value;
|
||||
if (!fecha) { categoriaBadge.style.display = 'none'; return; }
|
||||
|
||||
fetch(`/admin/jugadores/categoria-por-edad?fecha=${fecha}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
categoriaNombre.textContent = data.categoria || 'Sin categoría';
|
||||
categoriaBadge.style.display = 'block';
|
||||
})
|
||||
.catch(() => { categoriaBadge.style.display = 'none'; });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,180 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Jugadores - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-5">
|
||||
<div class="d-flex justify-content-between align-items-end mb-4">
|
||||
<div>
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-2">Base de Datos Deportiva</span>
|
||||
<h1 class="display-4 fw-bold font-header mb-0">Jugadores<span class="text-primary">.</span></h1>
|
||||
</div>
|
||||
<a href="{{ route('admin.jugadores.create') }}" class="btn-admin-primary">
|
||||
<i class="bi bi-person-plus me-2"></i> NUEVO JUGADOR
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Search & Tools Row -->
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-lg-6">
|
||||
<form method="GET" action="{{ route('admin.jugadores.index') }}" class="search-box-kinetic" id="jugadores-search-form">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" name="q" id="jugadores-search-input" class="form-control" placeholder="Buscar por nombre, apellido o DNI..." value="{{ $search ?? '' }}" autocomplete="off">
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-lg-6 d-flex justify-content-lg-end gap-2">
|
||||
@if(session('admin_role') == 1 || session('admin_role') == 2)
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-dark fw-bold text-uppercase dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-file-earmark-arrow-up me-1"></i> Herramientas CSV
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-end p-3" style="width: 300px; border-radius: 0; border: 2px solid #000;">
|
||||
<form action="{{ route('admin.jugadores.import') }}" method="POST" enctype="multipart/form-data" id="importForm" class="mb-3">
|
||||
@csrf
|
||||
<label class="form-label small fw-bold text-uppercase">Importar Plantel</label>
|
||||
|
||||
@if(session('admin_role') == 1)
|
||||
<select name="id_club" class="form-select form-select-sm mb-2" required>
|
||||
<option value="" selected disabled>Seleccionar Club...</option>
|
||||
<option value="99">ID 99 (General)</option>
|
||||
@foreach($clubes as $c)
|
||||
<option value="{{ $c->id_club }}">{{ $c->nombre }} (ID: {{ $c->id_club }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@endif
|
||||
|
||||
<input type="file" name="csv_file" class="form-control form-control-sm mb-2" accept=".csv,.txt">
|
||||
<button type="submit" class="btn btn-dark btn-sm w-100 fw-bold text-uppercase">Subir CSV</button>
|
||||
</form>
|
||||
<hr>
|
||||
<a href="{{ route('admin.jugadores.export') }}" class="btn btn-outline-success btn-sm w-100 fw-bold text-uppercase">
|
||||
<i class="bi bi-file-earmark-arrow-down me-1"></i> Exportar Base Completa
|
||||
</a>
|
||||
<div class="mt-2 text-muted" style="font-size: 0.7rem;">
|
||||
<strong>Formatos soportados:</strong><br>
|
||||
- GES CAB (Confederación Argentina de Basquetbol)<br>
|
||||
- Formato Interno OnAPB<br>
|
||||
- Legado: DNI; Apellido; Nombre; ddmmaaaa; id_club
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="jugadores-list-container">
|
||||
<div class="admin-card">
|
||||
@if($jugadores->isEmpty())
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-person-badge text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<p class="fs-5 text-muted">{{ $search ? 'No se encontraron resultados para "' . $search . '"' : 'No hay jugadores registrados.' }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="kinetic-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Jugador</th>
|
||||
<th>Documento</th>
|
||||
<th>Categoría</th>
|
||||
<th>Club Actual</th>
|
||||
<th>Estado</th>
|
||||
<th class="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($jugadores as $j)
|
||||
<tr>
|
||||
<td>
|
||||
<span class="fw-bold d-block">{{ $j->apellido }}, {{ $j->nombre }}</span>
|
||||
</td>
|
||||
<td><span class="text-muted fw-bold">{{ $j->documento }}</span></td>
|
||||
<td><span class="badge bg-dark text-white text-uppercase px-2 py-1" style="font-size: 0.7rem;">{{ $j->categoria_calculada }}</span></td>
|
||||
<td><span class="fw-bold">{{ $j->clubActual->nombre ?? '—' }}</span></td>
|
||||
<td>
|
||||
@if($j->activo)
|
||||
<span class="badge bg-success text-uppercase px-2" style="font-size: 0.65rem;">Activo</span>
|
||||
@else
|
||||
<span class="badge bg-secondary text-uppercase px-2" style="font-size: 0.65rem;">Inactivo</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('admin.jugadores.edit', $j->id_jugador) }}" class="btn btn-sm btn-light border me-1">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form action="{{ route('admin.jugadores.destroy', $j->id_jugador) }}" method="POST" class="d-inline delete-form confirm-submit" data-confirm-text="¿Eliminar este jugador?">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex justify-content-center">
|
||||
{{ $jugadores->appends(['q' => $search])->links('pagination::bootstrap-5') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
(function () {
|
||||
const form = document.getElementById('jugadores-search-form');
|
||||
const input = document.getElementById('jugadores-search-input');
|
||||
const container = document.getElementById('jugadores-list-container');
|
||||
if (!form || !input || !container) return;
|
||||
|
||||
let debounceTimer = null;
|
||||
let activeRequest = null;
|
||||
const baseUrl = form.getAttribute('action');
|
||||
|
||||
// Evitar submit completo del form (recarga de página) al apretar Enter.
|
||||
form.addEventListener('submit', e => { e.preventDefault(); doSearch(input.value); });
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
clearTimeout(debounceTimer);
|
||||
const value = this.value;
|
||||
debounceTimer = setTimeout(() => doSearch(value), 350);
|
||||
});
|
||||
|
||||
async function doSearch(query) {
|
||||
const url = baseUrl + (query ? ('?q=' + encodeURIComponent(query)) : '');
|
||||
// Mantener URL sincronizada para que recargas/back conserven el filtro.
|
||||
history.replaceState(null, '', url);
|
||||
|
||||
// Cancelar request previa si el usuario sigue tipeando.
|
||||
if (activeRequest) activeRequest.abort();
|
||||
activeRequest = new AbortController();
|
||||
|
||||
container.style.opacity = '0.55';
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'text/html' },
|
||||
signal: activeRequest.signal
|
||||
});
|
||||
const html = await res.text();
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const fresh = doc.getElementById('jugadores-list-container');
|
||||
if (fresh) {
|
||||
container.innerHTML = fresh.innerHTML;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name !== 'AbortError') console.error('Búsqueda en vivo falló:', err);
|
||||
} finally {
|
||||
container.style.opacity = '';
|
||||
activeRequest = null;
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,335 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@yield('title', 'Admin - OnAPB')</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset('favicon-32.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ asset('favicon-16.png') }}">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
|
||||
<link rel="stylesheet" href="{{ asset('static/admin-kinetic.css') }}?v={{ time() }}">
|
||||
<link rel="stylesheet" href="{{ asset('static/kinetic-arena-v3.min.css') }}?v={{ @filemtime(public_path('static/kinetic-arena-v3.min.css')) ?: '3' }}">
|
||||
@yield('styles')
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<!-- Sidebar -->
|
||||
<aside class="admin-sidebar" id="adminSidebar">
|
||||
<div class="sidebar-header d-flex align-items-center">
|
||||
<a href="{{ route('admin.dashboard') }}" class="text-decoration-none d-flex align-items-center">
|
||||
<img src="{{ asset('logo.png') }}" alt="OnAPB" class="sidebar-logo">
|
||||
<span class="sidebar-title">ADMIN<span class="text-primary">.</span></span>
|
||||
</a>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<div class="px-4 mb-3"><span class="small fw-bold text-muted text-uppercase tracking-widest">General</span></div>
|
||||
<a href="{{ route('admin.dashboard') }}" class="sidebar-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||
<i class="bi bi-grid-fill"></i> Dashboard
|
||||
</a>
|
||||
<a href="{{ route('documentacion.index') }}" class="sidebar-link" target="_blank">
|
||||
<i class="bi bi-book-fill"></i> Manual / Ayuda
|
||||
</a>
|
||||
|
||||
@if(session('admin_role') == 1 || session('admin_role') == 2)
|
||||
<div class="px-4 mt-4 mb-3"><span class="small fw-bold text-muted text-uppercase tracking-widest">Estructura</span></div>
|
||||
@endif
|
||||
|
||||
@if(session('admin_role') == 1)
|
||||
<a href="{{ route('admin.clubes.index') }}" class="sidebar-link {{ request()->routeIs('admin.clubes.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-shield-shaded"></i> Clubes
|
||||
</a>
|
||||
@endif
|
||||
|
||||
{{-- Equipos visible para SuperAdmin y Admin de Club --}}
|
||||
@if(session('admin_role') == 1 || session('admin_role') == 2)
|
||||
<a href="{{ route('admin.equipos.index') }}" class="sidebar-link {{ request()->routeIs('admin.equipos.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-people-fill"></i> Equipos
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if(session('admin_role') == 1)
|
||||
<a href="{{ route('admin.categorias.index') }}" class="sidebar-link {{ request()->routeIs('admin.categorias.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-tags-fill"></i> Categorías
|
||||
</a>
|
||||
<a href="{{ route('admin.torneos.index') }}" class="sidebar-link {{ request()->routeIs('admin.torneos.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-trophy-fill"></i> Torneos
|
||||
</a>
|
||||
@endif
|
||||
|
||||
{{-- Gestionar Club (Solo para Admin de Club) --}}
|
||||
@if(session('admin_role') == 2)
|
||||
<a href="{{ route('admin.clubes.edit', session('admin_id_club')) }}" class="sidebar-link {{ request()->routeIs('admin.clubes.edit') ? 'active' : '' }}">
|
||||
<i class="bi bi-shield-shaded"></i> Gestionar Club
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<div class="px-4 mt-4 mb-3"><span class="small fw-bold text-muted text-uppercase tracking-widest">Competición</span></div>
|
||||
<a href="{{ route('admin.jugadores.index') }}" class="sidebar-link {{ request()->routeIs('admin.jugadores.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-person-badge-fill"></i> Jugadores
|
||||
</a>
|
||||
<a href="{{ route('admin.pases.index') ?? '#' }}" class="sidebar-link {{ request()->routeIs('admin.pases.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-arrow-left-right"></i> Pases
|
||||
</a>
|
||||
<a href="{{ route('admin.eventos.index') }}" class="sidebar-link {{ request()->routeIs('admin.eventos.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-calendar-check-fill"></i> Partidos
|
||||
</a>
|
||||
<a href="{{ route('admin.escanear') }}" class="sidebar-link {{ request()->routeIs('admin.escanear') ? 'active' : '' }}">
|
||||
<i class="bi bi-qr-code-scan"></i> Escanear QR
|
||||
</a>
|
||||
|
||||
@if(session('admin_role') == 1)
|
||||
<div class="px-4 mt-4 mb-3"><span class="small fw-bold text-muted text-uppercase tracking-widest">Editorial</span></div>
|
||||
<a href="{{ route('admin.promociones.index') }}" class="sidebar-link {{ request()->routeIs('admin.promociones.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-geo-alt-fill"></i> Lugares
|
||||
</a>
|
||||
<a href="{{ route('admin.noticias.index') }}" class="sidebar-link {{ request()->routeIs('admin.noticias.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-newspaper"></i> Noticias
|
||||
</a>
|
||||
<a href="{{ route('admin.carousel.index') }}" class="sidebar-link {{ request()->routeIs('admin.carousel.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-images"></i> Carrusel
|
||||
</a>
|
||||
<a href="{{ route('admin.sponsors.index') }}" class="sidebar-link {{ request()->routeIs('admin.sponsors.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-star-fill text-warning"></i> Sponsors
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if(session('admin_role') == 1)
|
||||
<div class="px-4 mt-4 mb-3"><span class="small fw-bold text-muted text-uppercase tracking-widest">Sistema</span></div>
|
||||
<a href="{{ route('admin.usuarios.index') }}" class="sidebar-link {{ request()->routeIs('admin.usuarios.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-people-fill"></i> Administradores
|
||||
</a>
|
||||
<a href="{{ route('admin.settings.index') }}" class="sidebar-link {{ request()->routeIs('admin.settings.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-gear-fill"></i> Configuración
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<div class="mt-5 pt-4 border-top border-secondary border-opacity-25 mx-4">
|
||||
<a href="{{ route('home') }}" class="sidebar-link p-0 mb-3 small d-flex align-items-center opacity-75">
|
||||
<i class="bi bi-house-door me-2"></i> Ver Sitio
|
||||
</a>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="sidebar-link p-0 small d-flex align-items-center text-danger" style="background:none;border:none;cursor:pointer;">
|
||||
<i class="bi bi-box-arrow-right me-2"></i> Salir
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="admin-main">
|
||||
<!-- Top bar -->
|
||||
<header class="admin-topbar no-line">
|
||||
<button class="btn btn-link sidebar-toggle d-lg-none no-ripple" id="sidebarToggle">
|
||||
<i class="bi bi-list fs-3 text-dark"></i>
|
||||
</button>
|
||||
<div class="d-none d-md-flex align-items-center gap-2 ms-2">
|
||||
<span class="kfx-live-pill">
|
||||
<span>Live · Panel activo</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="ms-auto d-flex align-items-center gap-3">
|
||||
<div class="text-end d-none d-sm-block">
|
||||
<span class="d-block small fw-bold text-uppercase text-muted">Autenticado como</span>
|
||||
<span class="fw-bold">{{ session('admin_username', 'Admin') }}</span>
|
||||
</div>
|
||||
<div class="bg-primary text-white d-flex align-items-center justify-content-center" style="width: 45px; height: 45px;" title="{{ session('admin_username', 'Admin') }}">
|
||||
<i class="bi bi-person-fill fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Page content -->
|
||||
<main class="admin-content">
|
||||
@yield('content')
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overlay for mobile sidebar -->
|
||||
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
// Sidebar toggle
|
||||
document.getElementById('sidebarToggle')?.addEventListener('click', () => {
|
||||
document.getElementById('adminSidebar').classList.toggle('show');
|
||||
document.getElementById('sidebarOverlay').classList.toggle('show');
|
||||
});
|
||||
document.getElementById('sidebarOverlay')?.addEventListener('click', () => {
|
||||
document.getElementById('adminSidebar').classList.remove('show');
|
||||
document.getElementById('sidebarOverlay').classList.remove('show');
|
||||
});
|
||||
|
||||
// Notifications
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const brandColors = {
|
||||
primary: '#B00000',
|
||||
success: '#2e7d32',
|
||||
info: '#0277bd',
|
||||
danger: '#B00000',
|
||||
cancel: '#6c757d'
|
||||
};
|
||||
|
||||
@if(session('admin_msg'))
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'LISTO',
|
||||
text: '{{ session("admin_msg") }}',
|
||||
timer: 4000,
|
||||
timerProgressBar: true,
|
||||
showConfirmButton: false,
|
||||
position: 'top-end',
|
||||
toast: true,
|
||||
iconColor: brandColors.success
|
||||
});
|
||||
@endif
|
||||
|
||||
@if(session('admin_error'))
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'ERROR',
|
||||
text: '{{ session("admin_error") }}',
|
||||
timer: 5000,
|
||||
timerProgressBar: true,
|
||||
showConfirmButton: false,
|
||||
position: 'top-end',
|
||||
toast: true,
|
||||
iconColor: brandColors.danger
|
||||
});
|
||||
@endif
|
||||
|
||||
@if(session('admin_error_modal'))
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'ALGO SALIÓ MAL',
|
||||
text: @json(session('admin_error_modal')),
|
||||
confirmButtonText: 'ENTENDIDO',
|
||||
confirmButtonColor: brandColors.danger,
|
||||
iconColor: brandColors.danger,
|
||||
allowOutsideClick: false,
|
||||
allowEscapeKey: false
|
||||
});
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
let errorHtml = '<ul class="text-start mb-0" style="list-style-type: none; padding-left: 0;">';
|
||||
@foreach($errors->all() as $error)
|
||||
errorHtml += '<li>{{ $error }}</li>';
|
||||
@endforeach
|
||||
errorHtml += '</ul>';
|
||||
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'ERRORES DE VALIDACIÓN',
|
||||
html: errorHtml,
|
||||
confirmButtonColor: brandColors.danger
|
||||
});
|
||||
@endif
|
||||
|
||||
// ── Sistema Proactivo de Validación y Protección de Formularios ──
|
||||
const handleFormValidation = () => {
|
||||
const forms = document.querySelectorAll('form:not(.no-auto-validate)');
|
||||
|
||||
forms.forEach(form => {
|
||||
const submitBtn = form.querySelector('button[type="submit"]:not(.no-lock)');
|
||||
if (!submitBtn) return;
|
||||
|
||||
const requiredFields = form.querySelectorAll('[required]');
|
||||
if (requiredFields.length === 0) return;
|
||||
|
||||
const originalBtnHtml = submitBtn.innerHTML;
|
||||
|
||||
const validate = () => {
|
||||
let allValid = true;
|
||||
requiredFields.forEach(field => {
|
||||
const isFieldValid = field.value.trim() !== '';
|
||||
if (!isFieldValid) {
|
||||
allValid = false;
|
||||
// Feedback visual suave: borde sutil si interactuó
|
||||
if (field.dataset.touched) field.style.borderColor = '#ffcdd2';
|
||||
} else {
|
||||
field.style.borderColor = '';
|
||||
}
|
||||
});
|
||||
submitBtn.disabled = !allValid;
|
||||
submitBtn.style.opacity = allValid ? '1' : '0.5';
|
||||
submitBtn.style.cursor = allValid ? 'pointer' : 'not-allowed';
|
||||
};
|
||||
|
||||
// Monitorear cambios
|
||||
form.addEventListener('input', (e) => {
|
||||
if (e.target.hasAttribute('required')) {
|
||||
e.target.dataset.touched = 'true';
|
||||
}
|
||||
validate();
|
||||
});
|
||||
|
||||
// Estado inicial
|
||||
validate();
|
||||
|
||||
// Protección contra Doble Click y Feedback de Carga
|
||||
form.addEventListener('submit', (e) => {
|
||||
if (form.classList.contains('confirm-submit')) return; // Confirma vía SweetAlert primero
|
||||
|
||||
if (!submitBtn.disabled) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span> PROCESANDO...`;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
handleFormValidation();
|
||||
|
||||
// Interceptor Global para Confirmaciones
|
||||
document.addEventListener('submit', function(e) {
|
||||
const form = e.target;
|
||||
if (form.classList.contains('confirm-submit')) {
|
||||
e.preventDefault();
|
||||
const text = form.getAttribute('data-confirm-text') || '¿Estás seguro de realizar esta acción?';
|
||||
const confirmButtonText = form.getAttribute('data-confirm-button') || 'SÍ, CONFIRMAR';
|
||||
const icon = form.getAttribute('data-confirm-icon') || 'warning';
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
|
||||
let confirmColor = brandColors.primary;
|
||||
if (icon === 'success') confirmColor = brandColors.success;
|
||||
if (icon === 'question') confirmColor = brandColors.info;
|
||||
if (form.classList.contains('delete-form')) confirmColor = brandColors.danger;
|
||||
|
||||
Swal.fire({
|
||||
title: 'CONFIRMAR ACCIÓN',
|
||||
text: text,
|
||||
icon: icon,
|
||||
iconColor: confirmColor,
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: confirmColor,
|
||||
cancelButtonColor: brandColors.cancel,
|
||||
confirmButtonText: confirmButtonText,
|
||||
cancelButtonText: 'CANCELAR',
|
||||
reverseButtons: true,
|
||||
allowOutsideClick: () => !Swal.isLoading()
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span> PROCESANDO...`;
|
||||
}
|
||||
form.classList.remove('confirm-submit');
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{-- Kinetic FX: capa de innovación visual también en admin --}}
|
||||
<script src="{{ asset('static/kinetic-fx.js') }}?v={{ @filemtime(public_path('static/kinetic-fx.js')) ?: '3' }}" defer></script>
|
||||
@yield('scripts')
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,83 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', ($noticia ? 'Editar' : 'Nueva') . ' Noticia - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-newspaper"></i> {{ $noticia ? 'Editar Noticia' : 'Nueva Noticia' }}</h2>
|
||||
<a href="{{ route('admin.noticias.index') }}" class="btn-admin-outline">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ $noticia ? route('admin.noticias.update', $noticia->id) : route('admin.noticias.store') }}" class="admin-form" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@if($noticia) @method('PUT') @endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Título de la Noticia *</label>
|
||||
<input type="text" name="titulo" class="form-control form-control-lg" value="{{ old('titulo', $noticia->titulo ?? '') }}" required placeholder="Escribe un título impactante...">
|
||||
@error('titulo')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Categoría / Etiqueta</label>
|
||||
<input type="text" name="categoria" class="form-control" value="{{ old('categoria', $noticia->categoria ?? '') }}" placeholder="Ej: EFEMÉRIDES, TORNEO, CLUBES">
|
||||
<small class="text-muted">Aparecerá como una etiqueta sobre la noticia.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Torneo Relacionado</label>
|
||||
<select name="id_torneo" class="form-select">
|
||||
<option value="">— Ninguno —</option>
|
||||
@foreach($torneos as $t)
|
||||
<option value="{{ $t->id }}" {{ old('id_torneo', $noticia->id_torneo ?? '') == $t->id ? 'selected' : '' }}>
|
||||
{{ $t->nombre }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Imagen de Portada</label>
|
||||
<input type="file" name="imagen_file" class="form-control" accept="image/*">
|
||||
@if($noticia && $noticia->imagen)
|
||||
<div class="mt-2">
|
||||
<small class="d-block text-muted mb-1">Imagen actual:</small>
|
||||
<img src="{{ asset($noticia->imagen) }}" alt="" class="img-thumbnail" style="height: 60px;">
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Contenido de la Noticia *</label>
|
||||
<textarea name="contenido" class="form-control" rows="15" required placeholder="Escribe el cuerpo de la noticia aquí...">{{ old('contenido', $noticia->contenido ?? '') }}</textarea>
|
||||
@error('contenido')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end gap-3 border-top pt-4">
|
||||
<a href="{{ route('admin.noticias.index') }}" class="btn btn-light px-4">Cancelar</a>
|
||||
<button type="submit" class="btn-admin-primary px-5">
|
||||
<i class="bi bi-check-lg me-2"></i> {{ $noticia ? 'ACTUALIZAR NOTICIA' : 'PUBLICAR NOTICIA' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,77 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Noticias - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-5 d-flex justify-content-between align-items-end">
|
||||
<div>
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-2">Comunicación Oficial</span>
|
||||
<h1 class="display-4 fw-bold font-header mb-0">Noticias<span class="text-primary">.</span></h1>
|
||||
</div>
|
||||
<a href="{{ route('admin.noticias.create') }}" class="btn-admin-primary">
|
||||
<i class="bi bi-plus-lg me-2"></i> NUEVA NOTICIA
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
@if($noticias->isEmpty())
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-newspaper text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<p class="fs-5 text-muted">No hay noticias publicadas.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="kinetic-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Imagen</th>
|
||||
<th>Título / Categoría</th>
|
||||
<th>Publicación</th>
|
||||
<th class="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($noticias as $n)
|
||||
<tr>
|
||||
<td><span class="badge bg-light text-dark px-2 py-1">#{{ $n->id }}</span></td>
|
||||
<td>
|
||||
@if($n->imagen)
|
||||
<img src="{{ asset($n->imagen) }}" alt="" class="rounded" style="width: 60px; height: 40px; object-fit: cover; border: 1px solid #eee;">
|
||||
@else
|
||||
<div class="bg-light rounded d-flex align-items-center justify-content-center text-muted" style="width: 60px; height: 40px; font-size: 0.8rem;">
|
||||
<i class="bi bi-image"></i>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($n->categoria)
|
||||
<span class="badge bg-primary-subtle text-primary border-primary border-opacity-25 mb-1">{{ strtoupper($n->categoria) }}</span>
|
||||
@endif
|
||||
<span class="fw-bold fs-5 d-block">{{ $n->titulo }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-bold d-block text-muted text-uppercase" style="font-size: 0.8rem;">
|
||||
<i class="bi bi-clock me-1"></i> {{ $n->fecha ? \Carbon\Carbon::parse($n->fecha)->format('d/m/Y H:i') : '—' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('admin.noticias.edit', $n->id) }}" class="btn btn-sm btn-light border me-1">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form action="{{ route('admin.noticias.destroy', $n->id) }}" method="POST" class="d-inline delete-form confirm-submit" data-confirm-text="¿Eliminar esta noticia?">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,59 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Solicitar Pase - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-arrow-left-right"></i> Solicitar Pase</h2>
|
||||
<a href="{{ route('admin.pases.index') }}" class="btn-admin-outline">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.pases.store') }}" class="admin-form">
|
||||
@csrf
|
||||
|
||||
<div class="alert alert-info border-0 shadow-sm rounded-4">
|
||||
<i class="bi bi-info-circle-fill"></i> Ingrese el DNI del jugador que desea solicitar para su club.
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">DNI del Jugador *</label>
|
||||
<input type="text" name="documento" class="form-control form-control-lg rounded-pill px-4 shadow-sm" placeholder="Ej: 41234567" required>
|
||||
@error('documento')
|
||||
<div class="text-danger small mt-1 ps-3">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('admin_role') == 1)
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Club Destino (Como Superadmin, ¿para qué club solicita?)</label>
|
||||
<select name="id_club_destino" class="form-select form-select-lg rounded-pill shadow-sm" required>
|
||||
<option value="">Seleccione club</option>
|
||||
@foreach(\App\Models\Club::orderBy('nombre')->get() as $club)
|
||||
<option value="{{ $club->id_club }}">{{ $club->nombre }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('id_club_destino')
|
||||
<div class="text-danger small mt-1 ps-3">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn-admin w-100 p-2 fs-5">
|
||||
<i class="bi bi-send-fill"></i> Enviar Solicitud
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,83 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Pases - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-5 d-flex justify-content-between align-items-end">
|
||||
<div>
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-2">Mercado de Jugadores</span>
|
||||
<h1 class="display-4 fw-bold font-header mb-0">Solicitudes de <span class="text-primary">Pase.</span></h1>
|
||||
</div>
|
||||
<a href="{{ route('admin.pases.create') }}" class="btn-admin-primary">
|
||||
<i class="bi bi-plus-lg me-2"></i> SOLICITAR PASE
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
@if($pases->isEmpty())
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-person-badge text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<p class="fs-5 text-muted">No hay solicitudes de pase registradas.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="kinetic-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Jugador</th>
|
||||
<th>Origen</th>
|
||||
<th>Destino</th>
|
||||
<th>Estado</th>
|
||||
<th>Fecha Solicitud</th>
|
||||
@if(session('admin_role') == 1)
|
||||
<th class="text-end">Acciones</th>
|
||||
@endif
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($pases as $pase)
|
||||
<tr>
|
||||
<td>
|
||||
<span class="fw-bold d-block">{{ $pase->jugador->apellido ?? '—' }}, {{ $pase->jugador->nombre ?? '—' }}</span>
|
||||
<span class="small text-muted text-uppercase">DNI: {{ $pase->jugador->documento ?? '—' }}</span>
|
||||
</td>
|
||||
<td><span class="fw-bold">{{ $pase->clubOrigen->nombre ?? 'Libre' }}</span></td>
|
||||
<td><span class="fw-bold text-primary">{{ $pase->clubDestino->nombre ?? '—' }}</span></td>
|
||||
<td>
|
||||
@if($pase->estado === 'Pendiente')
|
||||
<span class="badge bg-warning text-dark text-uppercase px-2" style="font-size: 0.65rem;"><i class="bi bi-hourglass-split"></i> Pendiente</span>
|
||||
@elseif($pase->estado === 'Aprobado')
|
||||
<span class="badge bg-success text-uppercase px-2" style="font-size: 0.65rem;"><i class="bi bi-check-circle"></i> Aprobado</span>
|
||||
@else
|
||||
<span class="badge bg-danger text-uppercase px-2" style="font-size: 0.65rem;"><i class="bi bi-x-circle"></i> Rechazado</span>
|
||||
@endif
|
||||
</td>
|
||||
<td><span class="small fw-bold text-muted">{{ $pase->created_at->format('d/m/Y') }}</span></td>
|
||||
@if(session('admin_role') == 1)
|
||||
<td class="text-end">
|
||||
@if($pase->estado === 'Pendiente')
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<form action="{{ route('admin.pases.aprobar', $pase->id_pase) }}" method="POST" class="confirm-submit" data-confirm-text="¿Aprobar este pase?">
|
||||
@csrf @method('PUT')
|
||||
<button type="submit" class="btn btn-sm btn-dark fw-bold text-uppercase">Aprobar</button>
|
||||
</form>
|
||||
<form action="{{ route('admin.pases.rechazar', $pase->id_pase) }}" method="POST" class="confirm-submit" data-confirm-text="¿Rechazar este pase?">
|
||||
@csrf @method('PUT')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger fw-bold text-uppercase">Rechazar</button>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
@endif
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex justify-content-center">
|
||||
{{ $pases->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,103 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', ($promocion ? 'Editar' : 'Nueva') . ' Promoción - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-geo-alt-fill"></i> {{ $promocion ? 'Editar Promoción' : 'Nueva Promoción' }}</h2>
|
||||
<a href="{{ route('admin.promociones.index') }}" class="btn-admin-outline">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ $promocion ? route('admin.promociones.update', $promocion->id) : route('admin.promociones.store') }}" class="admin-form" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@if($promocion) @method('PUT') @endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nombre *</label>
|
||||
<input type="text" name="nombre" class="form-control" value="{{ old('nombre', $promocion->nombre ?? '') }}" required>
|
||||
@error('nombre')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Dirección *</label>
|
||||
<input type="text" name="direccion" class="form-control" value="{{ old('direccion', $promocion->direccion ?? '') }}" required>
|
||||
@error('direccion')
|
||||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Latitud</label>
|
||||
<input type="number" step="any" name="lat" class="form-control" value="{{ old('lat', $promocion->lat ?? '') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Longitud</label>
|
||||
<input type="number" step="any" name="lng" class="form-control" value="{{ old('lng', $promocion->lng ?? '') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Contacto</label>
|
||||
<input type="text" name="contacto" class="form-control" value="{{ old('contacto', $promocion->contacto ?? '') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Categoría</label>
|
||||
<input type="text" name="categoria" class="form-control" value="{{ old('categoria', $promocion->categoria ?? '') }}" placeholder="Ej: Gastronomía, Deportes...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Descripción del beneficio</label>
|
||||
<textarea name="descripcion" class="form-control" rows="3">{{ old('descripcion', $promocion->descripcion ?? '') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Descripción del lugar</label>
|
||||
<textarea name="descripcion_lugar" class="form-control" rows="3">{{ old('descripcion_lugar', $promocion->descripcion_lugar ?? '') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Imagen</label>
|
||||
<input type="file" name="imagen_file" class="form-control" accept="image/*">
|
||||
@if($promocion && $promocion->imagen)
|
||||
<div class="mt-2">
|
||||
<img src="{{ asset($promocion->imagen) }}" alt="" style="max-height: 80px; border-radius: 6px;">
|
||||
<small class="text-muted d-block mt-1">Imagen actual: {{ $promocion->imagen }}</small>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-admin">
|
||||
<i class="bi bi-check-lg"></i> {{ $promocion ? 'Actualizar' : 'Crear Promoción' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,70 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Promociones - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-geo-alt-fill"></i> Promociones / Lugares</h2>
|
||||
<a href="{{ route('admin.promociones.create') }}" class="btn-admin-primary">
|
||||
<i class="bi bi-plus-lg me-2"></i> NUEVA PROMOCIÓN
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="card-body">
|
||||
@if($promociones->isEmpty())
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
<p>No hay promociones registradas.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nombre</th>
|
||||
<th>Dirección</th>
|
||||
<th>Categoría</th>
|
||||
<th>QRs</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($promociones as $p)
|
||||
<tr>
|
||||
<td data-label="ID">{{ $p->id }}</td>
|
||||
<td data-label="Nombre">
|
||||
<strong>{{ $p->nombre }}</strong>
|
||||
@if($p->imagen)
|
||||
<br><img src="{{ asset($p->imagen) }}" alt="" style="max-height: 40px; border-radius: 4px; margin-top: 4px;">
|
||||
@endif
|
||||
</td>
|
||||
<td data-label="Dirección">{{ $p->direccion }}</td>
|
||||
<td data-label="Categoría">
|
||||
<span class="badge bg-info">{{ $p->categoria ?? 'Sin categoría' }}</span>
|
||||
</td>
|
||||
<td data-label="QRs">
|
||||
<span class="badge bg-secondary">{{ $p->promo_qrs_count }}</span>
|
||||
</td>
|
||||
<td data-label="Acciones">
|
||||
<a href="{{ route('admin.promociones.edit', $p->id) }}" class="btn-action edit">
|
||||
<i class="bi bi-pencil"></i> Editar
|
||||
</a>
|
||||
<form action="{{ route('admin.promociones.destroy', $p->id) }}" method="POST" class="delete-form confirm-submit" data-confirm-text="¿Eliminar esta promoción?">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn-action delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,131 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Configuración del Sistema')
|
||||
|
||||
@section('content')
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-white py-3 border-0">
|
||||
<h4 class="mb-0 text-primary"><i class="bi bi-gear-fill me-2"></i>Configuración General</h4>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form action="{{ route('admin.settings.update') }}" method="POST" class="confirm-submit" data-confirm-text="¿Deseas actualizar la configuración del sistema?">
|
||||
@csrf
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="dias_expiracion_eventos" class="form-label fw-bold">Días para expiración de eventos</label>
|
||||
<div class="input-group">
|
||||
<input type="number" name="dias_expiracion_eventos" id="dias_expiracion_eventos"
|
||||
class="form-control @error('dias_expiracion_eventos') is-invalid @enderror"
|
||||
value="{{ old('dias_expiracion_eventos', $diasExpiracion) }}"
|
||||
min="1" required>
|
||||
<span class="input-group-text">Días</span>
|
||||
</div>
|
||||
<div class="form-text text-muted mt-2">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Los eventos cuya fecha sea anterior a esta cantidad de días serán eliminados automáticamente junto con sus QRs asociados.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="backup_frequency" class="form-label fw-bold">Frecuencia del Backup Automático</label>
|
||||
<select name="backup_frequency" id="backup_frequency" class="form-select @error('backup_frequency') is-invalid @enderror">
|
||||
<option value="daily" {{ old('backup_frequency', $backupFreq) == 'daily' ? 'selected' : '' }}>Diario (Cada noche)</option>
|
||||
<option value="weekly" {{ old('backup_frequency', $backupFreq) == 'weekly' ? 'selected' : '' }}>Semanal (Cada lunes)</option>
|
||||
<option value="monthly" {{ old('backup_frequency', $backupFreq) == 'monthly' ? 'selected' : '' }}>Mensual (Día 1 de cada mes)</option>
|
||||
</select>
|
||||
<div class="form-text text-muted mt-2">
|
||||
<i class="bi bi-shield-check me-1"></i>
|
||||
Define con qué frecuencia el sistema generará un respaldo completo de la base de datos y archivos multimedia.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="email_reportes" class="form-label fw-bold">Email para Reportes Semanales</label>
|
||||
<input type="email" name="email_reportes" id="email_reportes"
|
||||
class="form-control @error('email_reportes') is-invalid @enderror"
|
||||
value="{{ old('email_reportes', $emailReportes) }}"
|
||||
placeholder="ejemplo@onapb.com" required>
|
||||
<div class="form-text text-muted mt-2">
|
||||
<i class="bi bi-envelope-at me-1"></i>
|
||||
Dirección donde se enviará el resumen de actividad de la liga todos los lunes.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted small">
|
||||
<i class="bi bi-clock-history me-1"></i>
|
||||
Tarea programada: <code>app:cleanup-old-events</code> (Ejecución diaria)
|
||||
</span>
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-save me-2"></i>Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-header bg-white py-3 border-0">
|
||||
<h4 class="mb-0 text-success"><i class="bi bi-heart-pulse-fill me-2"></i>Salud del Sistema (CRON)</h4>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row align-items-center mb-4">
|
||||
<div class="col-md-6 border-end">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<i class="bi bi-calendar-check fs-3 text-success me-3"></i>
|
||||
<div>
|
||||
<small class="text-muted d-block uppercase small">Última ejecución del Programador</small>
|
||||
<span class="fw-bold fs-5 text-dark">{{ $lastRun }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-clock fs-3 text-info me-3"></i>
|
||||
<div>
|
||||
<small class="text-muted d-block uppercase small">Hora actual del servidor</small>
|
||||
<span class="fw-bold fs-5">{{ now()->format('d/m/Y H:i:s') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 ps-md-4 mt-3 mt-md-0">
|
||||
<div class="alert alert-info border-0 shadow-sm py-2 px-3 mb-0">
|
||||
<i class="bi bi-info-circle-fill me-2"></i>
|
||||
<small>Si el "Última ejecución" no coincide con el minuto actual, es posible que el <b>Cron Job de Hostinger</b> no esté configurado correctamente.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="text-dark mb-3 small fw-bold">EJECUCIÓN MANUAL DE TAREAS</h5>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<form action="{{ route('admin.settings.manual.task') }}" method="POST" class="confirm-submit" data-confirm-text="¿Ejecutar limpieza de QRs antiguos ahora?">
|
||||
@csrf
|
||||
<input type="hidden" name="command" value="cleanup">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">
|
||||
<i class="bi bi-trash me-1"></i>Limpiar QRs Antiguos
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form action="{{ route('admin.settings.manual.task') }}" method="POST" class="confirm-submit" data-confirm-text="¿Iniciar backup manual? Esto puede tardar unos segundos.">
|
||||
@csrf
|
||||
<input type="hidden" name="command" value="backup">
|
||||
<button type="submit" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-cloud-arrow-up me-1"></i>Forzar Backup
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form action="{{ route('admin.settings.manual.task') }}" method="POST" class="confirm-submit" data-confirm-text="¿Enviar informe semanal de rendimiento ahora?">
|
||||
@csrf
|
||||
<input type="hidden" name="command" value="report">
|
||||
<button type="submit" class="btn btn-outline-info btn-sm">
|
||||
<i class="bi bi-envelope me-1"></i>Enviar Informe Semanal
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,98 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', (isset($sponsor) ? 'Editar' : 'Nuevo') . ' Sponsor - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-star-fill text-warning"></i> {{ isset($sponsor) ? 'Editar' : 'Nuevo' }} Sponsor</h2>
|
||||
<a href="{{ route('admin.sponsors.index') }}" class="btn-admin-outline">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="card-body">
|
||||
<form action="{{ isset($sponsor) ? route('admin.sponsors.update', $sponsor->id_sponsor) : route('admin.sponsors.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@if(isset($sponsor))
|
||||
@method('PUT')
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nombre del Sponsor *</label>
|
||||
<input type="text" name="nombre" class="form-control" value="{{ old('nombre', $sponsor->nombre ?? '') }}" required maxLength="100">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">URL del sitio (opcional)</label>
|
||||
<input type="url" name="url" class="form-control" value="{{ old('url', $sponsor->url ?? '') }}" placeholder="https://example.com">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Orden de aparición</label>
|
||||
<input type="number" name="orden" class="form-control" value="{{ old('orden', $sponsor->orden ?? 0) }}">
|
||||
<small class="text-muted">Menor número aparece primero.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 pt-4">
|
||||
<div class="form-check form-switch mt-2">
|
||||
<input class="form-check-input" type="checkbox" name="activo" id="activo" value="1" {{ old('activo', $sponsor->activo ?? true) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="activo">Sponsor Activo</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Logo / Imagen *</label>
|
||||
<input type="file" name="imagen" class="form-control" {{ isset($sponsor) ? '' : 'required' }} accept="image/*">
|
||||
<div class="mt-3 text-center border p-3 rounded bg-light" style="min-height: 150px; display: flex; align-items: center; justify-content: center;">
|
||||
@if(isset($sponsor) && $sponsor->imagen)
|
||||
<img src="{{ asset($sponsor->imagen) }}" id="preview" style="max-width: 100%; max-height: 120px; object-fit: contain;">
|
||||
@else
|
||||
<div id="no-preview" class="text-muted">
|
||||
<i class="bi bi-image fs-1 d-block"></i>
|
||||
Vista previa
|
||||
</div>
|
||||
<img src="#" id="preview" style="max-width: 100%; max-height: 120px; object-fit: contain; display: none;">
|
||||
@endif
|
||||
</div>
|
||||
<small class="text-muted d-block mt-2 text-center">Formato recomendado: PNG transparente o fondo blanco.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-save"></i> {{ isset($sponsor) ? 'Actualizar' : 'Guardar' }} Sponsor
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
document.querySelector('input[name="imagen"]').addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
const preview = document.getElementById('preview');
|
||||
const noPreview = document.getElementById('no-preview');
|
||||
preview.src = event.target.result;
|
||||
preview.style.display = 'block';
|
||||
if (noPreview) noPreview.style.display = 'none';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@endsection
|
||||
@@ -0,0 +1,78 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Sponsors - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-5 d-flex justify-content-between align-items-end">
|
||||
<div>
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-2">Alianzas Estratégicas</span>
|
||||
<h1 class="display-4 fw-bold font-header mb-0">Sponsors<span class="text-primary">.</span></h1>
|
||||
</div>
|
||||
<a href="{{ route('admin.sponsors.create') }}" class="btn-admin-primary">
|
||||
<i class="bi bi-plus-lg me-2"></i> NUEVO SPONSOR
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
@if($sponsors->isEmpty())
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-star text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<p class="fs-5 text-muted">No hay sponsors registrados.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="kinetic-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;">Orden</th>
|
||||
<th>Imagen</th>
|
||||
<th>Nombre</th>
|
||||
<th>Enlace</th>
|
||||
<th>Estado</th>
|
||||
<th class="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($sponsors as $s)
|
||||
<tr>
|
||||
<td><span class="badge bg-light text-dark fw-bold border p-2">{{ $s->orden }}</span></td>
|
||||
<td>
|
||||
<div class="bg-white p-2 border" style="width: 100px; height: 50px; display: inline-flex; align-items: center; justify-content: center;">
|
||||
<img src="{{ asset($s->imagen) }}" alt="{{ $s->nombre }}" style="max-width: 100%; max-height: 100%; object-fit: contain;">
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="fw-bold fs-5 text-uppercase">{{ $s->nombre }}</span></td>
|
||||
<td>
|
||||
@if($s->url)
|
||||
<a href="{{ $s->url }}" target="_blank" class="small text-muted font-monospace">{{ Str::limit($s->url, 30) }}</a>
|
||||
@else
|
||||
<span class="text-muted small">SIN ENLACE</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($s->activo)
|
||||
<span class="badge bg-success text-white text-uppercase px-2 py-1">ACTIVO</span>
|
||||
@else
|
||||
<span class="badge bg-light text-muted border text-uppercase px-2 py-1">OCULTO</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('admin.sponsors.edit', $s->id_sponsor) }}" class="btn btn-sm btn-light border me-1">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form action="{{ route('admin.sponsors.destroy', $s->id_sponsor) }}" method="POST" class="d-inline delete-form confirm-submit" data-confirm-text="¿Eliminar este sponsor?">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,88 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Preview Fixture — ' . $torneo->nombre)
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-calendar3-range-fill"></i> Preview del Fixture</h2>
|
||||
<a href="{{ route('admin.torneos.edit', $torneo->id) }}" class="btn-admin-outline">
|
||||
<i class="bi bi-arrow-left"></i> Volver al torneo
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card mb-4">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h5 class="mb-0 fw-bold">{{ $torneo->nombre }}</h5>
|
||||
<span class="text-muted small">{{ count($partidosEnriquecidos) }} partidos a generar</span>
|
||||
</div>
|
||||
<span class="badge bg-warning text-dark fs-6 px-3 py-2">
|
||||
<i class="bi bi-eye me-1"></i> Solo vista previa — aún no se guardó nada
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
{{-- Agrupar por jornada --}}
|
||||
@php
|
||||
$jornadas = collect($partidosEnriquecidos)->groupBy('jornada');
|
||||
@endphp
|
||||
|
||||
@foreach($jornadas as $numJornada => $partidos)
|
||||
<div class="mb-4">
|
||||
<h6 class="text-uppercase fw-bold text-muted border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-calendar-event me-1"></i> Jornada {{ $numJornada }}
|
||||
<span class="text-primary ms-2">{{ \Carbon\Carbon::parse($partidos->first()['fecha_evento'])->format('d/m/Y') }}</span>
|
||||
</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Local</th>
|
||||
<th class="text-center">vs</th>
|
||||
<th>Visitante</th>
|
||||
<th>Fecha</th>
|
||||
<th>Hora</th>
|
||||
<th>Sede</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($partidos as $p)
|
||||
<tr>
|
||||
<td class="fw-bold">{{ $p['nombre_local'] }}</td>
|
||||
<td class="text-center text-muted">🆚</td>
|
||||
<td class="fw-bold">{{ $p['nombre_visitante'] }}</td>
|
||||
<td>{{ \Carbon\Carbon::parse($p['fecha_evento'])->format('d/m/Y') }}</td>
|
||||
<td>{{ substr($p['hora_inicio'], 0, 5) }}</td>
|
||||
<td class="text-muted">{{ $p['sede'] ?: '—' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
{{-- Botón de confirmación --}}
|
||||
<div class="border-top pt-4 mt-2 d-flex gap-3 justify-content-end">
|
||||
<a href="{{ route('admin.torneos.edit', $torneo->id) }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-x-lg me-1"></i> Cancelar
|
||||
</a>
|
||||
<form method="POST" action="{{ route('admin.torneos.fixture.confirmar', $torneo->id) }}" class="confirm-submit"
|
||||
data-confirm-text="Se crearán {{ count($partidosEnriquecidos) }} partidos en la base de datos. Esta acción no se puede deshacer fácilmente."
|
||||
data-confirm-button="Sí, generar fixture"
|
||||
data-confirm-icon="warning">
|
||||
@csrf
|
||||
<input type="hidden" name="fecha_inicio" value="{{ $fixtureParams['fecha_inicio'] }}">
|
||||
<input type="hidden" name="dias_entre_jornadas" value="{{ $fixtureParams['dias_entre_jornadas'] }}">
|
||||
<input type="hidden" name="sede_default" value="{{ $fixtureParams['sede_default'] }}">
|
||||
<input type="hidden" name="doble_rueda" value="{{ $fixtureParams['doble_rueda'] ? 1 : 0 }}">
|
||||
<button type="submit" class="btn-admin px-5 py-3">
|
||||
<i class="bi bi-check2-circle me-2"></i> Confirmar y Generar Fixture
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,237 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', ($torneo ? 'Editar' : 'Nuevo') . ' Torneo - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-trophy-fill"></i> {{ $torneo ? 'Editar Torneo' : 'Nuevo Torneo' }}</h2>
|
||||
<a href="{{ route('admin.torneos.index') }}" class="btn-admin-outline">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="{{ $torneo ? 'col-lg-5' : 'col-12' }}">
|
||||
<div class="admin-card mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold">Información General</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ $torneo ? route('admin.torneos.update', $torneo->id) : route('admin.torneos.store') }}" class="admin-form">
|
||||
@csrf
|
||||
@if($torneo) @method('PUT') @endif
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nombre del Torneo *</label>
|
||||
<input type="text" name="nombre" class="form-control" value="{{ old('nombre', $torneo->nombre ?? '') }}" required placeholder="Ej: Torneo Apertura 2026">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Fecha de Inicio</label>
|
||||
<input type="date" name="fecha_inicio" class="form-control" value="{{ old('fecha_inicio', ($torneo && $torneo->fecha_inicio) ? $torneo->fecha_inicio->format('Y-m-d') : '') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Fecha de Fin</label>
|
||||
<input type="date" name="fecha_fin" class="form-control" value="{{ old('fecha_fin', ($torneo && $torneo->fecha_fin) ? $torneo->fecha_fin->format('Y-m-d') : '') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-admin w-100 py-3 mt-3">
|
||||
<i class="bi bi-check-lg"></i> {{ $torneo ? 'Actualizar Torneo' : 'Crear Torneo' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($torneo)
|
||||
<div class="col-lg-7">
|
||||
<!-- Assign Teams -->
|
||||
<div class="admin-card mb-4">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 fw-bold">Equipos Participantes</h5>
|
||||
<span class="badge bg-primary px-3 rounded-pill">{{ $torneo->equipos->count() }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('admin.torneos.equipos.add', $torneo->id) }}" method="POST" class="row g-2 mb-4 align-items-end">
|
||||
@csrf
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small text-muted">Asignar Equipo al Torneo</label>
|
||||
<select name="id_equipo" class="form-select select2-basic" required>
|
||||
<option value="">Buscar equipo por club / categoría...</option>
|
||||
@foreach($clubes as $club)
|
||||
<optgroup label="{{ $club->nombre }}">
|
||||
@foreach($club->equipos as $eq)
|
||||
<option value="{{ $eq->id_equipo }}">
|
||||
{{ $eq->categoria }} {{ $eq->division ? '('.$eq->division.')' : '' }}
|
||||
</option>
|
||||
@endforeach
|
||||
</optgroup>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-muted">Grupo / División (Opcional)</label>
|
||||
<input type="text" name="grupo" class="form-control" placeholder="Ej: Zona B">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="submit" class="btn-admin w-100">
|
||||
<i class="bi bi-plus-lg"></i> Agregar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead class="bg-light sticky-top">
|
||||
<tr>
|
||||
<th>Club</th>
|
||||
<th>Categoría Base</th>
|
||||
<th>Grupo en Torneo</th>
|
||||
<th class="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($torneo->equipos as $te)
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ $te->club->nombre }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge border text-dark">{{ $te->categoria }} {{ $te->division }}</span>
|
||||
</td>
|
||||
<td>
|
||||
@if($te->pivot->grupo)
|
||||
<span class="badge bg-info text-white">{{ $te->pivot->grupo }}</span>
|
||||
@else
|
||||
<span class="text-muted small italic">Usar Base</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<form action="{{ route('admin.torneos.equipos.remove', [$torneo->id, $te->id_equipo]) }}" method="POST" class="d-inline">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-link text-danger p-0 border-0" onclick="return confirm('¿Remover este equipo del torneo?')">
|
||||
<i class="bi bi-x-circle-fill fs-5"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-4 text-muted">No hay equipos asignados a este torneo.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($torneo && $torneo->equipos->count() >= 2)
|
||||
<!-- Generar Fixture -->
|
||||
<div class="admin-card mt-4">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h5 class="mb-0 fw-bold"><i class="bi bi-calendar3-range-fill text-primary me-2"></i>Generar Fixture Regular</h5>
|
||||
<span class="text-muted small">Round-Robin automático para los {{ $torneo->equipos->count() }} equipos del torneo</span>
|
||||
</div>
|
||||
<a href="{{ route('admin.torneos.importar', $torneo->id) }}" class="btn btn-sm btn-outline-success">
|
||||
<i class="bi bi-upload"></i> Importar Masivo (CSV/Texto)
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.torneos.fixture.preview', $torneo->id) }}">
|
||||
@csrf
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small fw-bold text-muted text-uppercase">Fecha de inicio *</label>
|
||||
<input type="date" name="fecha_inicio" class="form-control" required
|
||||
min="{{ date('Y-m-d') }}" value="{{ date('Y-m-d', strtotime('+7 days')) }}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small fw-bold text-muted text-uppercase">Días entre jornadas</label>
|
||||
<input type="number" name="dias_entre_jornadas" class="form-control" value="7" min="1" max="60">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small fw-bold text-muted text-uppercase">Sede por defecto</label>
|
||||
<input type="text" name="sede_default" class="form-control" placeholder="Ej: Estadio Municipal">
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-center gap-2 pt-3">
|
||||
<input type="checkbox" name="doble_rueda" value="1" class="form-check-input" id="doble_rueda" style="width:20px;height:20px;">
|
||||
<label for="doble_rueda" class="form-check-label small fw-bold">Ida y vuelta</label>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="submit" class="btn-admin w-100 py-3" title="Ver preview">
|
||||
<i class="bi bi-eye"></i> Preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generar Playoffs -->
|
||||
<div class="admin-card mt-4 border-primary">
|
||||
<div class="card-header bg-light py-3">
|
||||
<h5 class="mb-0 fw-bold text-primary"><i class="bi bi-diagram-3-fill me-2"></i>Fase Eliminatoria (Playoffs)</h5>
|
||||
<span class="text-muted small">Generar cruces 1º vs 8º basado en la tabla actual</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.torneos.playoffs.generar', $torneo->id) }}">
|
||||
@csrf
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small fw-bold text-muted text-uppercase">Grupo / Categoría</label>
|
||||
<select name="grupo" class="form-select" required>
|
||||
<option value="">Seleccione grupo...</option>
|
||||
@php
|
||||
$grupos = \DB::table('torneo_equipo')->where('id_torneo', $torneo->id)->distinct()->pluck('grupo')->filter();
|
||||
@endphp
|
||||
@foreach($grupos as $g)
|
||||
<option value="{{ $g }}">{{ $g }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small fw-bold text-muted text-uppercase">Formato de Serie</label>
|
||||
<select name="formato" class="form-select" required>
|
||||
<option value="1">Partido Único</option>
|
||||
<option value="3">Mejor de 3</option>
|
||||
<option value="5">Mejor de 5</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="alert alert-info py-2 mb-0 small">
|
||||
<i class="bi bi-info-circle"></i> Los 8 mejores del grupo iniciarán en Cuartos.
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="submit" class="btn btn-primary w-100 py-3" onclick="return confirm('¿Generar Cuartos de Final con este formato?')">
|
||||
<i class="bi bi-lightning-fill"></i> Generar Cuartos
|
||||
</button>
|
||||
<a href="{{ route('admin.torneos.playoffs.manage', $torneo->id) }}" class="btn btn-outline-primary w-100 mt-2">
|
||||
<i class="bi bi-diagram-3"></i> Gestionar LLaves Existentes
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@elseif($torneo)
|
||||
<div class="alert alert-warning mt-4 d-flex align-items-center gap-2">
|
||||
<i class="bi bi-exclamation-triangle-fill fs-5"></i>
|
||||
<span>Necesitás al menos <strong>2 equipos</strong> asignados al torneo para generar el fixture.</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Importar Fixture Histórico</h1>
|
||||
<a href="{{ route('admin.torneos.edit', $torneo->id) }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Instrucciones</h5>
|
||||
<p class="text-muted">
|
||||
Pega aquí los partidos ya jugados para cargarlos masivamente.
|
||||
El formato debe ser CSV con las siguientes <strong>9 columnas</strong>:
|
||||
</p>
|
||||
<div class="bg-light p-3 rounded mb-3">
|
||||
<code>Fecha(AAAA-MM-DD), Club Local, Cat Local, Club Visitante, Cat Visitante, Marcador L, Marcador V, Sede(opc), Grupo(opc)</code>
|
||||
</div>
|
||||
<p class="small text-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Manejo de Equipos:</strong> El sistema busca por Club Y Categoría. Esto permite diferenciar, por ejemplo, los equipos de "San Martin" en "Primera A" de los de "Primera B" en el mismo torneo.
|
||||
</p>
|
||||
|
||||
<form action="{{ route('admin.torneos.importar.store', $torneo->id) }}" method="POST">
|
||||
@csrf
|
||||
<div class="mb-3">
|
||||
<label for="texto_importar" class="form-label">Datos a importar (CSV)</label>
|
||||
<textarea class="form-control" id="texto_importar" name="texto_importar" rows="10" placeholder="2026-03-14, Recreativo, Primera B, Talleres, Primera B, 64, 61, , Primera B"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Esta acción creará nuevos registros de eventos. Evita duplicar partidos que ya existan en el sistema.
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<i class="fas fa-upload"></i> Procesar e Importar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,57 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Gestión de Torneos - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-trophy-fill"></i> Torneos</h2>
|
||||
<a href="{{ route('admin.torneos.create') }}" class="btn-admin-primary">
|
||||
<i class="bi bi-plus-lg me-2"></i> NUEVO TORNEO
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="table-responsive">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Inicio</th>
|
||||
<th>Fin</th>
|
||||
<th class="text-center">Equipos</th>
|
||||
<th class="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($torneos as $torneo)
|
||||
<tr>
|
||||
<td class="fw-bold text-dark">{{ $torneo->nombre }}</td>
|
||||
<td>{{ $torneo->fecha_inicio ? $torneo->fecha_inicio->format('d/m/Y') : '—' }}</td>
|
||||
<td>{{ $torneo->fecha_fin ? $torneo->fecha_fin->format('d/m/Y') : '—' }}</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-light text-dark border">{{ $torneo->equipos_count }}</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<a href="{{ route('admin.torneos.edit', $torneo->id) }}" class="btn-admin-icon" title="Editar / Gestionar Equipos">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
</a>
|
||||
<form action="{{ route('admin.torneos.destroy', $torneo->id) }}" method="POST" onsubmit="return confirm('¿Eliminar este torneo? Esto no borrará los equipos ni los eventos vinculados.')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn-admin-icon text-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-muted">No hay torneos creados.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,142 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Gestión de Playoffs - ' . $torneo->nombre)
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<a href="{{ route('admin.torneos.edit', $torneo->id) }}" class="btn-admin-outline p-2">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
</a>
|
||||
<h2 class="mb-0"><i class="bi bi-diagram-3-fill text-primary"></i> Gestión de Playoffs</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card mb-4 border-primary">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<h4 class="fw-bold mb-1">{{ $torneo->nombre }}</h4>
|
||||
<p class="text-muted mb-0">Control de llaves y avance manual de ganadores por serie.</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<button class="btn btn-admin" onclick="window.location.reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Actualizar Estado
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$fases = [
|
||||
\App\Models\Evento::FASE_CUARTOS => 'Cuartos de Final',
|
||||
\App\Models\Evento::FASE_SEMIS => 'Semifinales',
|
||||
\App\Models\Evento::FASE_FINAL => 'Gran Final'
|
||||
];
|
||||
@endphp
|
||||
|
||||
@foreach($fases as $faseId => $faseNombre)
|
||||
<div class="mb-5">
|
||||
<h3 class="h4 fw-bold text-uppercase border-bottom pb-2 mb-4">
|
||||
<span class="badge bg-primary me-2">{{ $faseNombre }}</span>
|
||||
</h3>
|
||||
|
||||
<div class="row g-4">
|
||||
@php $hayPartidos = isset($bracket[$faseId]) && count($bracket[$faseId]) > 0; @endphp
|
||||
|
||||
@if($hayPartidos)
|
||||
@foreach($bracket[$faseId] as $nroBracket => $serie)
|
||||
<div class="col-lg-6 col-xl-3">
|
||||
<div class="admin-card h-100 border-{{ ($serie['wins_local'] >= 2 || $serie['wins_visitante'] >= 2) ? 'success' : 'light' }}">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center py-2">
|
||||
<span class="small fw-bold text-muted">LLAVE #{{ $nroBracket }}</span>
|
||||
<span class="badge bg-light text-dark border small">Serie: {{ $serie['wins_local'] }} - {{ $serie['wins_visitante'] }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="team-row d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@if($serie['equipo_local'])
|
||||
<img src="{{ $serie['equipo_local']->club->imagen ? asset('storage/'.$serie['equipo_local']->club->imagen) : asset('static/escudo_default.png') }}" width="24" class="rounded-circle border">
|
||||
<span class="fw-bold {{ $serie['wins_local'] > $serie['wins_visitante'] ? 'text-success' : '' }}">
|
||||
{{ $serie['equipo_local']->club->nombre }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-muted italic">Por definir</span>
|
||||
@endif
|
||||
</div>
|
||||
<span class="badge {{ $serie['wins_local'] > $serie['wins_visitante'] ? 'bg-success' : 'bg-secondary' }}">{{ $serie['wins_local'] }}</span>
|
||||
</div>
|
||||
|
||||
<div class="team-row d-flex justify-content-between align-items-center mb-4">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@if($serie['equipo_visitante'])
|
||||
<img src="{{ $serie['equipo_visitante']->club->imagen ? asset('storage/'.$serie['equipo_visitante']->club->imagen) : asset('static/escudo_default.png') }}" width="24" class="rounded-circle border">
|
||||
<span class="fw-bold {{ $serie['wins_visitante'] > $serie['wins_local'] ? 'text-success' : '' }}">
|
||||
{{ $serie['equipo_visitante']->club->nombre }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-muted fst-italic">Por definir</span>
|
||||
@endif
|
||||
</div>
|
||||
<span class="badge {{ $serie['wins_visitante'] > $serie['wins_local'] ? 'bg-success' : 'bg-secondary' }}">{{ $serie['wins_visitante'] }}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<!-- Botón de Avance -->
|
||||
@php
|
||||
$needed = floor($serie['total_partidos'] / 2) + 1;
|
||||
$readyToAdvance = ($serie['wins_local'] >= $needed || $serie['wins_visitante'] >= $needed);
|
||||
$winnerId = $serie['wins_local'] > $serie['wins_visitante'] ? ($serie['equipo_local']->id_equipo ?? null) : ($serie['equipo_visitante']->id_equipo ?? null);
|
||||
@endphp
|
||||
|
||||
@if($readyToAdvance && $faseId < \App\Models\Evento::FASE_FINAL)
|
||||
<form action="{{ route('admin.torneos.playoffs.avanzar', $torneo->id) }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="fase" value="{{ $faseId }}">
|
||||
<input type="hidden" name="nro_bracket" value="{{ $nroBracket }}">
|
||||
<input type="hidden" name="id_ganador" value="{{ $winnerId }}">
|
||||
<button type="submit" class="btn btn-success btn-sm w-100 fw-bold">
|
||||
<i class="bi bi-chevron-double-right"></i> PROMOCIONAR GANADOR
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#matches-{{ $faseId }}-{{ $nroBracket }}">
|
||||
Ver Partidos ({{ $serie['total_partidos'] }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="collapse mt-3" id="matches-{{ $faseId }}-{{ $nroBracket }}">
|
||||
<ul class="list-group list-group-flush small">
|
||||
@foreach($serie['matches'] as $idx => $m)
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span>G{{ $idx+1 }} - {{ $m->fecha_evento->format('d/m') }}</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="fw-bold">{{ $m->marcador_local ?? '-' }} : {{ $m->marcador_visitante ?? '-' }}</span>
|
||||
<a href="{{ route('admin.eventos.edit', $m->id_evento) }}" class="btn btn-sm p-0 text-primary">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@else
|
||||
<div class="col-12">
|
||||
<div class="alert alert-light border fst-italic py-4 text-center">
|
||||
No hay llaves generadas para esta fase.
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,95 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', ($usuario ? 'Editar' : 'Nuevo') . ' Administrador - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h2><i class="bi bi-person-fill-lock"></i> {{ $usuario ? 'Editar Administrador' : 'Nuevo Administrador' }}</h2>
|
||||
<a href="{{ route('admin.usuarios.index') }}" class="btn-admin-outline">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ $usuario ? route('admin.usuarios.update', $usuario->id) : route('admin.usuarios.store') }}" class="admin-form">
|
||||
@csrf
|
||||
@if($usuario) @method('PUT') @endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nombre de Usuario *</label>
|
||||
<input type="text" name="username" class="form-control" value="{{ old('username', $usuario->username ?? '') }}" placeholder="Ej: admin_paracao" required>
|
||||
@error('username') <div class="text-danger small mt-1">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Contraseña {{ $usuario ? '(Dejar en blanco para no cambiarla)' : '*' }}</label>
|
||||
<input type="password" name="password" class="form-control" placeholder="******" {{ $usuario ? '' : 'required' }}>
|
||||
@error('password') <div class="text-danger small mt-1">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Rol del Administrador *</label>
|
||||
<select name="role" id="roleSelect" class="form-select" required>
|
||||
<option value="1" {{ old('role', $usuario->role ?? '') == 1 ? 'selected' : '' }}>Super Administrador (Acceso total)</option>
|
||||
<option value="2" {{ old('role', $usuario->role ?? '') == 2 || (!old('role') && !$usuario) ? 'selected' : '' }}>Administrador de Club</option>
|
||||
</select>
|
||||
@error('role') <div class="text-danger small mt-1">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6" id="clubSelectContainer">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Club Asociado (Solo para Admin de Club)</label>
|
||||
<select name="id_club" class="form-select">
|
||||
<option value="">Seleccione el Club...</option>
|
||||
@foreach($clubes as $club)
|
||||
<option value="{{ $club->id_club }}" {{ old('id_club', $usuario->id_club ?? '') == $club->id_club ? 'selected' : '' }}>
|
||||
{{ $club->nombre }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('id_club') <div class="text-danger small mt-1">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-admin mt-3">
|
||||
<i class="bi bi-save"></i> {{ $usuario ? 'Guardar Cambios' : 'Crear Administrador' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const roleSelect = document.getElementById('roleSelect');
|
||||
const clubContainer = document.getElementById('clubSelectContainer');
|
||||
const clubSelect = clubContainer.querySelector('select');
|
||||
|
||||
function toggleClubSelect() {
|
||||
if (roleSelect.value == '2') {
|
||||
clubContainer.style.display = 'block';
|
||||
clubSelect.setAttribute('required', 'required');
|
||||
} else {
|
||||
clubContainer.style.display = 'none';
|
||||
clubSelect.removeAttribute('required');
|
||||
clubSelect.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
roleSelect.addEventListener('change', toggleClubSelect);
|
||||
toggleClubSelect(); // Ejecutar al cargar la página
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,74 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('title', 'Administradores - Admin OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-5 d-flex justify-content-between align-items-end">
|
||||
<div>
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-2">Control de Accesos</span>
|
||||
<h1 class="display-4 fw-bold font-header mb-0">Administradores<span class="text-primary">.</span></h1>
|
||||
</div>
|
||||
<a href="{{ route('admin.usuarios.create') }}" class="btn-admin-primary">
|
||||
<i class="bi bi-person-plus-fill me-2"></i> NUEVO USUARIO
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
@if($usuarios->isEmpty())
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-person-lock text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<p class="fs-5 text-muted">No hay otros administradores registrados.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="kinetic-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Usuario</th>
|
||||
<th>Nivel de Acceso</th>
|
||||
<th>Club Responsable</th>
|
||||
<th class="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($usuarios as $user)
|
||||
<tr>
|
||||
<td>
|
||||
<span class="fw-bold fs-5">{{ $user->username }}</span>
|
||||
</td>
|
||||
<td>
|
||||
@if($user->role == 1)
|
||||
<span class="badge bg-dark text-white text-uppercase px-2 py-1"><i class="bi bi-shield-lock-fill me-1"></i> Súper Admin</span>
|
||||
@else
|
||||
<span class="badge bg-light text-dark border text-uppercase px-2 py-1"><i class="bi bi-shield-check me-1"></i> Admin Club</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-bold">{{ $user->club->nombre ?? 'SISTEMA INTEGRAL' }}</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('admin.usuarios.edit', $user->id) }}" class="btn btn-sm btn-light border me-1">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
@if($user->id != session('admin_id'))
|
||||
<form action="{{ route('admin.usuarios.destroy', $user->id) }}" method="POST" class="d-inline delete-form confirm-submit" data-confirm-text="¿Eliminar este administrador?">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex justify-content-center">
|
||||
{{ $usuarios->links('pagination::bootstrap-5') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user