3
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
import './bootstrap';
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
@@ -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
|
||||
@@ -0,0 +1,244 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Unite - OnAPB')
|
||||
|
||||
@section('styles')
|
||||
<link rel="stylesheet" href="{{ asset('static/asociate.css?v=5') }}">
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid py-5" style="background: var(--surface);">
|
||||
<div class="container py-4">
|
||||
<div class="mb-5 text-center">
|
||||
<span class="text-primary fw-bold tracking-widest text-uppercase d-block mb-2">Comunidad OnAPB</span>
|
||||
<h1 class="display-1 fw-bold mb-3" style="line-height: 0.9;">Unite al Juego<span class="text-primary">.</span></h1>
|
||||
<p class="fs-4 text-muted mx-auto" style="max-width: 700px;">Formá parte de la comunidad del básquet entrerriano y accedé a beneficios exclusivos.</p>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<!-- Tab Navigation Editorial -->
|
||||
<div class="d-flex justify-content-center mb-5">
|
||||
<div class="bg-white p-2 shadow-sm d-inline-flex" style="border: 1px solid var(--primary-container);">
|
||||
<button class="nav-link px-5 py-3 fw-bold text-uppercase border-0 @if(!isset($tab) || $tab != 'jugador') active bg-primary text-white @else bg-white text-muted @endif"
|
||||
id="aficionado-tab" data-bs-toggle="pill" data-bs-target="#aficionado" type="button" style="transition: 0.3s; width: 250px;">
|
||||
Aficionado
|
||||
</button>
|
||||
<button class="nav-link px-5 py-3 fw-bold text-uppercase border-0 @if(isset($tab) && $tab == 'jugador') active bg-primary text-white @else bg-white text-muted @endif"
|
||||
id="jugador-tab" data-bs-toggle="pill" data-bs-target="#jugador" type="button" style="transition: 0.3s; width: 250px;">
|
||||
Jugador
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="asociateTabContent">
|
||||
<!-- Formulario Aficionado -->
|
||||
<div class="tab-pane fade @if(!isset($tab) || $tab != 'jugador') show active @endif" id="aficionado" role="tabpanel">
|
||||
<div class="bg-white p-5 shadow-lg" style="border-top: 10px solid var(--primary);">
|
||||
<div class="row g-5">
|
||||
<div class="col-md-4 border-end border-light">
|
||||
<h2 class="fw-bold mb-4">Registro de Aficionado</h2>
|
||||
<p class="text-muted">Como aficionado, podés seguir tus equipos favoritos y acceder a descuentos en clubes y comercios adheridos.</p>
|
||||
<div class="mt-5 p-4 bg-light">
|
||||
<h5 class="fw-bold text-primary mb-3">Beneficios</h5>
|
||||
<ul class="list-unstyled small fw-bold text-uppercase">
|
||||
<li class="mb-2"><i class="bi bi-check2 text-primary"></i> QRs de acceso</li>
|
||||
<li class="mb-2"><i class="bi bi-check2 text-primary"></i> Mapa de beneficios</li>
|
||||
<li class="mb-2"><i class="bi bi-check2 text-primary"></i> Noticias exclusivas</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger mb-4">
|
||||
<ul class="mb-0 small">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('registro.aficionado') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="tipo" value="aficionado">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Nombre *</label>
|
||||
<input type="text" name="nombre" class="form-control border-0 bg-light p-3" required style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Apellido *</label>
|
||||
<input type="text" name="apellido" class="form-control border-0 bg-light p-3" required style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">DNI *</label>
|
||||
<input type="text" name="dni" class="form-control border-0 bg-light p-3" required style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Fecha de Nacimiento</label>
|
||||
<input type="date" name="fecha_nacimiento" class="form-control border-0 bg-light p-3" style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Email *</label>
|
||||
<input type="email" name="email" class="form-control border-0 bg-light p-3" required style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Teléfono (Opcional)</label>
|
||||
<input type="text" name="telefono" class="form-control border-0 bg-light p-3" style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Localidad</label>
|
||||
<input type="text" name="localidad" class="form-control border-0 bg-light p-3" style="border-radius: 0;">
|
||||
</div>
|
||||
|
||||
<div class="col-12"><hr class="my-4"></div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Contraseña *</label>
|
||||
<input type="password" name="password" class="form-control border-0 bg-light p-3" required style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Confirmar Contraseña *</label>
|
||||
<input type="password" name="password_confirmation" class="form-control border-0 bg-light p-3" required style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="cf-turnstile mt-2" data-sitekey="{{ config('services.turnstile.site_key') }}"></div>
|
||||
</div>
|
||||
<div class="col-12 mt-4">
|
||||
<button type="submit" class="btn-kinetic-primary w-100 py-3">CREAR MI CUENTA</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulario Jugador -->
|
||||
<div class="tab-pane fade @if(isset($tab) && $tab == 'jugador') show active @endif" id="jugador" role="tabpanel">
|
||||
<div class="bg-white p-5 shadow-lg" style="border-top: 10px solid var(--primary);">
|
||||
<div class="row g-5">
|
||||
<div class="col-md-4 border-end border-light">
|
||||
<h2 class="fw-bold mb-4">Portal de Jugador</h2>
|
||||
<p class="text-muted">Si estás fichado en un club de la APB, activá tu cuenta para gestionar tus datos y acceder a beneficios únicos para deportistas.</p>
|
||||
<div class="mt-5 p-4 bg-dark text-white">
|
||||
<h5 class="fw-bold text-primary mb-3">Verificación</h5>
|
||||
<p class="small mb-0">Usamos tu DNI para validar tu ficha técnica actual en el sistema de la asociación.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
@if(isset($jugador_encontrado))
|
||||
<div class="p-4 bg-success text-white mb-4">
|
||||
<h4 class="fw-bold mb-1"><i class="bi bi-check-circle-fill"></i> ¡Jugador Encontrado!</h4>
|
||||
<p class="mb-0 small">Validamos tus datos en {{ $jugador_encontrado['club'] }}. Completá tu perfil abajo.</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('registro.jugador.completar') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="dni" value="{{ $jugador_encontrado['documento'] }}">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Nombre</label>
|
||||
<div class="fs-5 fw-bold p-3 bg-light">{{ $jugador_encontrado['nombre'] }}</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Apellido</label>
|
||||
<div class="fs-5 fw-bold p-3 bg-light">{{ $jugador_encontrado['apellido'] }}</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Email de Contacto *</label>
|
||||
<input type="email" name="email" class="form-control border-0 bg-light p-3" required style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Contraseña *</label>
|
||||
<input type="password" name="password" class="form-control border-0 bg-light p-3" required style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Confirmar Contraseña *</label>
|
||||
<input type="password" name="password_confirmation" class="form-control border-0 bg-light p-3" required style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check p-3 bg-light border">
|
||||
<input type="checkbox" class="form-check-input ms-1" id="aceptoJug" name="acepto" required>
|
||||
<label class="form-check-label ms-2 small fw-bold" for="aceptoJug">ACEPTO LOS TÉRMINOS Y CONDICIONES</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="cf-turnstile mt-2" data-sitekey="{{ config('services.turnstile.site_key') }}"></div>
|
||||
</div>
|
||||
<div class="col-12 mt-4">
|
||||
<button type="submit" class="btn-kinetic-primary w-100 py-3">FINALIZAR ACTIVACIÓN</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@else
|
||||
<form method="POST" action="{{ route('registro.jugador.buscar') }}">
|
||||
@csrf
|
||||
<h3 class="fw-bold mb-4">Buscá tus datos</h3>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Nombre *</label>
|
||||
<input type="text" name="nombre" class="form-control border-0 bg-light p-3" required style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">Apellido *</label>
|
||||
<input type="text" name="apellido" class="form-control border-0 bg-light p-3" required style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="small fw-bold text-muted text-uppercase mb-2 d-block">DNI *</label>
|
||||
<input type="text" name="dni" class="form-control border-0 bg-light p-3" placeholder="Sin puntos" required style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check p-3 bg-light border">
|
||||
<input type="checkbox" class="form-check-input ms-1" id="aceptoBusq" name="acepto" required>
|
||||
<label class="form-check-label ms-2 small fw-bold" for="aceptoBusq">ACEPTO LA VALIDACIÓN DE DATOS ASOCIATIVOS</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 mt-4">
|
||||
<button type="submit" class="btn-kinetic-primary w-100 py-3">BUSCAR MI FICHA <i class="bi bi-search ms-2"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@section('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const tabs = document.querySelectorAll('[data-bs-toggle="pill"]');
|
||||
const panes = document.querySelectorAll('.tab-pane');
|
||||
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const targetId = this.getAttribute('data-bs-target').replace('#', '');
|
||||
|
||||
// Update Tabs
|
||||
tabs.forEach(t => {
|
||||
t.classList.remove('active', 'bg-primary', 'text-white');
|
||||
t.classList.add('bg-white', 'text-muted');
|
||||
});
|
||||
this.classList.add('active', 'bg-primary', 'text-white');
|
||||
this.classList.remove('bg-white', 'text-muted');
|
||||
|
||||
// Update Panes
|
||||
panes.forEach(pane => {
|
||||
pane.classList.remove('show', 'active');
|
||||
if (pane.id === targetId) {
|
||||
pane.classList.add('show', 'active');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@endsection
|
||||
@@ -0,0 +1,58 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Recuperar contraseña - OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="container mt-5 mb-5">
|
||||
<div class="kinetic-card p-5 mx-auto" style="max-width:500px;">
|
||||
<h3 class="mb-4 text-center fw-bold" style="color: var(--primary);">RECUPERAR CONTRASEÑA</h3>
|
||||
<p class="text-center text-muted mb-4 small">Ingresá tu DNI y tu correo electrónico. Te enviaremos un enlace para restablecerla de forma segura.</p>
|
||||
|
||||
@if(session('mensaje'))
|
||||
<div class="alert border-0 mb-4 p-3 d-flex align-items-center" style="background: rgba(13, 202, 240, 0.1); border-radius: var(--radius-sm);">
|
||||
<i class="bi bi-info-circle-fill text-info fs-3 me-3"></i>
|
||||
<div>
|
||||
<span class="text-muted small">{!! session('mensaje') !!}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger border-0 mb-4 p-3 d-flex align-items-center" style="background: rgba(220, 53, 69, 0.1); border-radius: var(--radius-sm);">
|
||||
<i class="bi bi-exclamation-triangle-fill text-danger fs-3 me-3"></i>
|
||||
<div>
|
||||
@foreach($errors->all() as $error)
|
||||
<span class="text-muted small d-block">{{ $error }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('recuperar') }}">
|
||||
@csrf
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" name="dni" class="form-control" id="dniRecInput" value="{{ old('dni') }}" placeholder="DNI sin puntos" required>
|
||||
<label for="dniRecInput">DNI *</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-4">
|
||||
<input type="email" name="email" class="form-control" id="emailRecInput" value="{{ old('email') }}" placeholder="ejemplo@correo.com" required>
|
||||
<label for="emailRecInput">Correo Electrónico *</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="cf-turnstile" data-sitekey="{{ config('services.turnstile.site_key') }}"></div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100">Enviar enlace de recuperación</button>
|
||||
|
||||
<div class="text-center mt-4 pt-3 border-top border-light">
|
||||
<a href="{{ route('home') }}" class="text-decoration-none text-muted small fw-bold">
|
||||
<i class="bi bi-arrow-left me-1"></i> Volver al inicio
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,55 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Restablecer contraseña - OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="container mt-5 mb-5">
|
||||
<div class="kinetic-card p-5 mx-auto" style="max-width:500px;">
|
||||
<h4 class="mb-4 text-center fw-bold">RESTABLECER CONTRASEÑA</h4>
|
||||
|
||||
@if(session('mensaje'))
|
||||
<div class="alert border-0 text-center mb-4" style="background: rgba(13,202,240,0.1); border-radius: var(--radius-sm);">
|
||||
{!! session('mensaje') !!}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger border-0 mb-4" style="border-radius: var(--radius-sm);">
|
||||
@foreach($errors->all() as $error)
|
||||
<p class="mb-0 small">{{ $error }}</p>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('reset.password.post') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="token" value="{{ $token }}">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold text-uppercase text-muted">Nueva contraseña</label>
|
||||
<div class="input-group">
|
||||
<input type="password" name="password" class="form-control" required minlength="6">
|
||||
<button class="btn btn-outline-secondary toggle-password" type="button">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label small fw-bold text-uppercase text-muted">Confirmar nueva contraseña</label>
|
||||
<div class="input-group">
|
||||
<input type="password" name="password_confirmation" class="form-control" required minlength="6">
|
||||
<button class="btn btn-outline-secondary toggle-password" type="button">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100 btn-lg">Cambiar contraseña</button>
|
||||
<div class="text-center mt-4 pt-3 border-top border-light">
|
||||
<a href="{{ route('home') }}" class="text-muted small fw-bold text-decoration-none">
|
||||
<i class="bi bi-arrow-left me-1"></i> Volver al inicio
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,380 @@
|
||||
{{-- OnAPB Genius Agent — Chat Bubble --}}
|
||||
<style>
|
||||
#genius-btn {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
right: 1.5rem;
|
||||
z-index: 1050;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #0d6efd, #6610f2);
|
||||
color: #fff;
|
||||
border: none;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,.25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
transition: transform .15s;
|
||||
}
|
||||
#genius-btn:hover { transform: scale(1.08); }
|
||||
|
||||
#genius-panel {
|
||||
position: fixed;
|
||||
bottom: 5rem;
|
||||
right: 1.5rem;
|
||||
z-index: 1049;
|
||||
width: 360px;
|
||||
max-width: calc(100vw - 2rem);
|
||||
background: #fff;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,.18);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
transition: opacity .2s, transform .2s;
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
#genius-panel.genius-hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: scale(.92);
|
||||
}
|
||||
|
||||
/* Mobile: panel casi pantalla completa para que el teclado no tape el input. */
|
||||
@media (max-width: 576px) {
|
||||
#genius-panel {
|
||||
left: .5rem;
|
||||
right: .5rem;
|
||||
bottom: .5rem;
|
||||
top: auto;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
border-radius: .85rem;
|
||||
max-height: calc(100dvh - 1rem);
|
||||
}
|
||||
#genius-messages { max-height: none; flex: 1 1 auto; }
|
||||
}
|
||||
|
||||
#genius-header {
|
||||
background: linear-gradient(135deg, #0d6efd, #6610f2);
|
||||
color: #fff;
|
||||
padding: .75rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
}
|
||||
#genius-header .genius-title { font-weight: 600; font-size: .95rem; flex: 1; }
|
||||
#genius-header button { background: none; border: none; color: #fff; font-size: 1rem; line-height: 1; padding: 0; cursor: pointer; opacity: .8; }
|
||||
#genius-header button:hover { opacity: 1; }
|
||||
|
||||
#genius-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: .75rem 1rem;
|
||||
max-height: 340px;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.genius-msg {
|
||||
padding: .5rem .75rem;
|
||||
border-radius: .75rem;
|
||||
font-size: .875rem;
|
||||
line-height: 1.45;
|
||||
max-width: 88%;
|
||||
word-break: break-word;
|
||||
}
|
||||
.genius-msg.user {
|
||||
align-self: flex-end;
|
||||
background: #0d6efd;
|
||||
color: #fff;
|
||||
border-bottom-right-radius: .2rem;
|
||||
}
|
||||
.genius-msg.assistant {
|
||||
align-self: flex-start;
|
||||
background: #f1f3f5;
|
||||
color: #212529;
|
||||
border-bottom-left-radius: .2rem;
|
||||
}
|
||||
.genius-msg.error { background: #fff3cd; color: #856404; }
|
||||
.genius-msg strong { font-weight: 600; }
|
||||
.genius-msg em { font-style: italic; }
|
||||
.genius-msg code {
|
||||
background: rgba(0,0,0,.08);
|
||||
padding: .1rem .3rem;
|
||||
border-radius: .25rem;
|
||||
font-size: .85em;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
.genius-msg.user code { background: rgba(255,255,255,.22); }
|
||||
|
||||
#genius-typing {
|
||||
align-self: flex-start;
|
||||
padding: .4rem .75rem;
|
||||
background: #f1f3f5;
|
||||
border-radius: .75rem;
|
||||
font-size: .8rem;
|
||||
color: #6c757d;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#genius-form {
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding: .6rem .75rem;
|
||||
display: flex;
|
||||
gap: .4rem;
|
||||
background: #fff;
|
||||
}
|
||||
#genius-input {
|
||||
flex: 1;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: .5rem;
|
||||
padding: .4rem .6rem;
|
||||
font-size: .875rem;
|
||||
outline: none;
|
||||
resize: none;
|
||||
max-height: 80px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#genius-input:focus { border-color: #0d6efd; }
|
||||
#genius-send {
|
||||
background: #0d6efd;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: .5rem;
|
||||
padding: .4rem .7rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background .15s;
|
||||
align-self: flex-end;
|
||||
}
|
||||
#genius-send:hover { background: #0b5ed7; }
|
||||
#genius-send:disabled { background: #adb5bd; cursor: not-allowed; }
|
||||
</style>
|
||||
|
||||
<button id="genius-btn" title="OnAPB Genius" aria-label="Abrir asistente">
|
||||
<i class="bi bi-stars"></i>
|
||||
</button>
|
||||
|
||||
<div id="genius-panel" class="genius-hidden">
|
||||
<div id="genius-header">
|
||||
<i class="bi bi-stars"></i>
|
||||
<span class="genius-title">OnAPB Genius</span>
|
||||
<button id="genius-close" aria-label="Cerrar"><i class="bi bi-x-lg"></i></button>
|
||||
</div>
|
||||
<div id="genius-messages">
|
||||
<div class="genius-msg assistant">
|
||||
¡Hola! Soy OnAPB Genius. ¿En qué puedo ayudarte hoy?
|
||||
</div>
|
||||
<div id="genius-typing">Escribiendo<span id="genius-dots">...</span></div>
|
||||
</div>
|
||||
<form id="genius-form">
|
||||
@csrf
|
||||
<textarea id="genius-input" placeholder="Escribí tu consulta..." rows="1" maxlength="1000"></textarea>
|
||||
<button type="submit" id="genius-send"><i class="bi bi-send-fill"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const btn = document.getElementById('genius-btn');
|
||||
const panel = document.getElementById('genius-panel');
|
||||
const closeBtn = document.getElementById('genius-close');
|
||||
const form = document.getElementById('genius-form');
|
||||
const input = document.getElementById('genius-input');
|
||||
const sendBtn = document.getElementById('genius-send');
|
||||
const msgs = document.getElementById('genius-messages');
|
||||
const typing = document.getElementById('genius-typing');
|
||||
|
||||
const STORAGE_KEY = 'onapb_genius_chat_v1';
|
||||
|
||||
let threadId = null;
|
||||
let isOpen = false;
|
||||
|
||||
function loadState() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(STORAGE_KEY);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveState() {
|
||||
try {
|
||||
const messages = Array.from(msgs.querySelectorAll('.genius-msg')).map(el => ({
|
||||
role: el.classList.contains('user') ? 'user'
|
||||
: el.classList.contains('error') ? 'error'
|
||||
: 'assistant',
|
||||
text: el.dataset.text ?? el.textContent
|
||||
}));
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||
messages, threadId, isOpen
|
||||
}));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return s.replace(/[&<>"']/g, c => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||
}[c]));
|
||||
}
|
||||
|
||||
function renderMarkdown(text) {
|
||||
let s = escapeHtml(text);
|
||||
|
||||
const codes = [];
|
||||
s = s.replace(/`([^`\n]+?)`/g, (_, code) => {
|
||||
codes.push(code);
|
||||
return `C${codes.length - 1}`;
|
||||
});
|
||||
|
||||
s = s.replace(/\*\*\*([^*\n]+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
||||
s = s.replace(/\*\*([^*\n]+?)\*\*/g, '<strong>$1</strong>');
|
||||
s = s.replace(/(^|[^*\w])\*([^*\n]+?)\*(?!\*)/g, '$1<em>$2</em>');
|
||||
s = s.replace(/(^|[^_\w])_([^_\n]+?)_(?!\w)/g, '$1<em>$2</em>');
|
||||
|
||||
s = s.replace(/C(\d+)/g, (_, i) => `<code>${codes[i]}</code>`);
|
||||
s = s.replace(/\n/g, '<br>');
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
function renderMessage(role, text) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'genius-msg ' + role;
|
||||
div.dataset.text = text;
|
||||
if (role === 'assistant') {
|
||||
div.innerHTML = renderMarkdown(text);
|
||||
} else {
|
||||
div.textContent = text;
|
||||
}
|
||||
msgs.insertBefore(div, typing);
|
||||
}
|
||||
|
||||
function rehydrate() {
|
||||
const saved = loadState();
|
||||
if (!saved) return;
|
||||
|
||||
if (Array.isArray(saved.messages) && saved.messages.length > 0) {
|
||||
msgs.querySelectorAll('.genius-msg').forEach(el => el.remove());
|
||||
saved.messages.forEach(m => renderMessage(m.role, m.text));
|
||||
msgs.scrollTop = msgs.scrollHeight;
|
||||
}
|
||||
if (saved.threadId) threadId = saved.threadId;
|
||||
if (saved.isOpen) {
|
||||
isOpen = true;
|
||||
panel.classList.remove('genius-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
isOpen = !isOpen;
|
||||
panel.classList.toggle('genius-hidden', !isOpen);
|
||||
if (isOpen) input.focus();
|
||||
adjustForKeyboard();
|
||||
saveState();
|
||||
}
|
||||
|
||||
// En mobile el teclado virtual reduce el visualViewport pero el layout viewport
|
||||
// queda igual, así que un fixed con bottom queda tapado. Compensamos manualmente.
|
||||
function adjustForKeyboard() {
|
||||
if (!window.visualViewport || window.innerWidth > 576) {
|
||||
panel.style.bottom = '';
|
||||
return;
|
||||
}
|
||||
if (!isOpen) { panel.style.bottom = ''; return; }
|
||||
const vv = window.visualViewport;
|
||||
const keyboardHeight = window.innerHeight - vv.height - vv.offsetTop;
|
||||
panel.style.bottom = (keyboardHeight > 80 ? keyboardHeight + 8 : 8) + 'px';
|
||||
}
|
||||
|
||||
if (window.visualViewport) {
|
||||
window.visualViewport.addEventListener('resize', adjustForKeyboard);
|
||||
window.visualViewport.addEventListener('scroll', adjustForKeyboard);
|
||||
}
|
||||
window.addEventListener('resize', adjustForKeyboard);
|
||||
|
||||
btn.addEventListener('click', togglePanel);
|
||||
closeBtn.addEventListener('click', togglePanel);
|
||||
|
||||
// Auto-resize textarea
|
||||
input.addEventListener('input', function () {
|
||||
this.style.height = 'auto';
|
||||
this.style.height = Math.min(this.scrollHeight, 80) + 'px';
|
||||
});
|
||||
|
||||
// Submit on Enter (Shift+Enter = nueva línea)
|
||||
input.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
form.dispatchEvent(new Event('submit'));
|
||||
}
|
||||
});
|
||||
|
||||
function appendMsg(role, text) {
|
||||
typing.style.display = 'none';
|
||||
renderMessage(role, text);
|
||||
msgs.scrollTop = msgs.scrollHeight;
|
||||
saveState();
|
||||
}
|
||||
|
||||
function setLoading(loading) {
|
||||
sendBtn.disabled = loading;
|
||||
input.disabled = loading;
|
||||
typing.style.display = loading ? 'block' : 'none';
|
||||
if (loading) msgs.scrollTop = msgs.scrollHeight;
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const message = input.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
appendMsg('user', message);
|
||||
input.value = '';
|
||||
input.style.height = 'auto';
|
||||
setLoading(true);
|
||||
|
||||
const csrfToken = document.querySelector('input[name="_token"]')?.value
|
||||
|| document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
|
||||
const body = { message };
|
||||
if (threadId) body.thread_id = threadId;
|
||||
|
||||
try {
|
||||
const res = await fetch('{{ route("agent.chat") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
appendMsg('error', data.message || 'Ocurrió un error. Intentá de nuevo.');
|
||||
} else {
|
||||
if (data.thread_id) threadId = data.thread_id;
|
||||
appendMsg('assistant', data.reply);
|
||||
}
|
||||
} catch (err) {
|
||||
appendMsg('error', 'No se pudo conectar con el agente. Verificá tu conexión.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
input.focus();
|
||||
}
|
||||
});
|
||||
|
||||
rehydrate();
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,75 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Manual de Usuario - OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="container mt-4 mb-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-10 mx-auto">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 pb-3 border-bottom">
|
||||
<div>
|
||||
<span class="text-primary fw-bold text-uppercase small d-block mb-1">Centro de Ayuda</span>
|
||||
<h1 class="display-5 fw-bold mb-0">Manual de Usuario<span class="text-primary">.</span></h1>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ route('documentacion.download') }}" class="btn btn-primary d-flex align-items-center">
|
||||
<i class="bi bi-file-earmark-pdf-fill me-2"></i> Descargar PDF
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0" style="border-radius: var(--radius-lg); overflow: hidden;">
|
||||
<div class="card-body p-4 p-md-5 bg-white doc-content">
|
||||
{!! $content !!}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 p-4 rounded bg-light border">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-1">
|
||||
<i class="bi bi-info-circle-fill text-primary" style="font-size: 2rem;"></i>
|
||||
</div>
|
||||
<div class="col-md-11">
|
||||
<h5 class="fw-bold mb-1">¿Necesitás más ayuda?</h5>
|
||||
<p class="mb-0 text-muted">Si tenés dudas técnicas o problemas con el sistema, contactá con el administrador de tu club o con la mesa de ayuda de la APB.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.doc-content h1, .doc-content h2, .doc-content h3 {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-weight: 700;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
.doc-content p {
|
||||
line-height: 1.8;
|
||||
color: #555;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.doc-content ul, .doc-content ol {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.doc-content li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.doc-content code {
|
||||
background: #f8f9fa;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
color: #b00000;
|
||||
}
|
||||
.doc-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
margin: 1rem 0;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
@@ -0,0 +1,147 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Manual de Usuario - OnAPB</title>
|
||||
<style>
|
||||
@page {
|
||||
margin: 100px 50px;
|
||||
}
|
||||
|
||||
header {
|
||||
position: fixed;
|
||||
top: -70px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
height: 60px;
|
||||
border-bottom: 2px solid #b00000;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
header .logo {
|
||||
float: left;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
header .title {
|
||||
float: right;
|
||||
line-height: 40px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #b00000;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: fixed;
|
||||
bottom: -60px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
height: 40px;
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 5px;
|
||||
font-size: 10px;
|
||||
color: #777;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pagenum:before {
|
||||
content: counter(page);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Helvetica', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #b00000;
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #444;
|
||||
font-size: 18px;
|
||||
margin-top: 30px;
|
||||
border-left: 4px solid #b00000;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
table th, table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table th {
|
||||
background-color: #f8f8f8;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.intro-box {
|
||||
background-color: #fff9f9;
|
||||
border: 1px solid #ffebeb;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.section-separator {
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
background: #f9f9f9;
|
||||
border-left: 10px solid #ccc;
|
||||
margin: 1.5em 10px;
|
||||
padding: 0.5em 10px;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f4f4f4;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Limpieza para floats */
|
||||
.clearfix::after {
|
||||
content: "";
|
||||
clear: both;
|
||||
display: table;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="clearfix">
|
||||
@if($logo)
|
||||
<img src="{{ $logo }}" class="logo">
|
||||
@endif
|
||||
<div class="title">Manual de Usuario — OnAPB</div>
|
||||
</header>
|
||||
|
||||
<footer>
|
||||
© {{ date('Y') }} OnAPB - Kinetic Arena Experience | Página <span class="pagenum"></span>
|
||||
</footer>
|
||||
|
||||
<main>
|
||||
{!! $content !!}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tus QRs para el Evento - OnAPB</title>
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 0; background-color: #f4f4f4; }
|
||||
.container { max-width: 600px; margin: 20px auto; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
|
||||
.header { background: linear-gradient(135deg, #198754 0%, #157347 100%); color: #fff; padding: 30px 20px; text-align: center; }
|
||||
.header h1 { margin: 0; font-size: 24px; text-transform: uppercase; }
|
||||
.content { padding: 30px; }
|
||||
.event-card { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 10px; padding: 20px; margin: 20px 0; text-align: center; }
|
||||
.event-name { font-size: 20px; font-weight: bold; color: #198754; margin-bottom: 5px; }
|
||||
.event-details { color: #666; font-size: 14px; }
|
||||
.qr-count { font-size: 50px; font-weight: bold; color: #198754; margin: 15px 0; }
|
||||
.footer { background: #222; color: #888; text-align: center; padding: 20px; font-size: 12px; }
|
||||
.btn { display: inline-block; padding: 12px 25px; background: #198754; color: #fff; text-decoration: none; border-radius: 6px; font-weight: bold; margin-top: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>TUS ENTRADAS LISTAS</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>¡Hola, <strong>{{ $user->nombre }}</strong>!</p>
|
||||
<p>Se han generado exitosamente tus QRs para el siguiente evento:</p>
|
||||
|
||||
<div class="event-card">
|
||||
<div class="event-name">{{ $evento->nombre_evento }}</div>
|
||||
<div class="event-details">
|
||||
Fecha: {{ \Carbon\Carbon::parse($evento->fecha_evento)->format('d/m/Y') }}<br>
|
||||
Sede: {{ $evento->sede ?? 'A confirmar' }}
|
||||
</div>
|
||||
<div class="qr-count">{{ $cantidad }}</div>
|
||||
<p>QRs Generados</p>
|
||||
</div>
|
||||
|
||||
<p>Ya podés verlos y descargarlos desde la sección "Mis QRs" en tu panel personal:</p>
|
||||
<center>
|
||||
<a href="{{ route('panel.mis.qrs', ['evento' => $evento->id_evento]) }}" class="btn">VER MIS ENTRADAS</a>
|
||||
</center>
|
||||
|
||||
<p style="margin-top:20px; font-size: 14px; color: #777;">Recordá presentar estos QRs en la entrada del estadio para su escaneo.</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
© {{ date('Y') }} OnAPB - Asociación Paranaense de Básquetbol.<br>
|
||||
Este es un correo automático, por favor no lo respondas.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reporte Semanal ONAPB</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background: #f5f5f5; margin: 0; padding: 20px; color: #333; }
|
||||
.container { max-width: 700px; margin: 0 auto; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
|
||||
.header { background: #b00000; color: white; padding: 28px 32px; }
|
||||
.header h1 { margin: 0; font-size: 24px; }
|
||||
.header p { margin: 6px 0 0; opacity: 0.85; font-size: 14px; }
|
||||
.section { padding: 20px 32px; border-bottom: 1px solid #eee; }
|
||||
.section h2 { font-size: 16px; color: #b00000; margin: 0 0 14px; border-left: 4px solid #b00000; padding-left: 10px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
th { background: #f0f0f0; text-align: left; padding: 8px 10px; font-size: 12px; text-transform: uppercase; color: #666; }
|
||||
td { padding: 8px 10px; border-bottom: 1px solid #f0f0f0; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
.stat-row { display: flex; gap: 16px; flex-wrap: wrap; }
|
||||
.stat-box { flex: 1; min-width: 120px; background: #f9f9f9; border-radius: 6px; padding: 14px 16px; text-align: center; border: 1px solid #eee; }
|
||||
.stat-box .num { font-size: 28px; font-weight: bold; color: #b00000; }
|
||||
.stat-box .lbl { font-size: 12px; color: #666; margin-top: 4px; }
|
||||
.footer { padding: 16px 32px; font-size: 12px; color: #999; text-align: center; }
|
||||
.empty { color: #aaa; font-style: italic; padding: 10px 0; font-size: 13px; }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: bold; }
|
||||
.badge-win { background: #e8f5e9; color: #2e7d32; }
|
||||
.badge-lose { background: #ffebee; color: #c62828; }
|
||||
.badge-draw { background: #fff3e0; color: #e65100; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📊 Reporte Semanal — ONAPB</h1>
|
||||
<p>Semana del {{ $semanaAnteriorDesde->format('d/m/Y') }} al {{ $semanaAnteriorHasta->format('d/m/Y') }}</p>
|
||||
</div>
|
||||
|
||||
{{-- Estadísticas rápidas --}}
|
||||
<div class="section">
|
||||
<h2>Resumen</h2>
|
||||
<div class="stat-row">
|
||||
<div class="stat-box">
|
||||
<div class="num">{{ $jugados->count() }}</div>
|
||||
<div class="lbl">Partidos jugados</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="num">{{ $proximos->count() }}</div>
|
||||
<div class="lbl">Próximos partidos</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="num">{{ $qrsSemana }}</div>
|
||||
<div class="lbl">QRs generados</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="num">{{ $qrsValidados }}</div>
|
||||
<div class="lbl">QRs validados</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Resultados de la semana --}}
|
||||
<div class="section">
|
||||
<h2>Resultados de la Semana</h2>
|
||||
@if($jugados->isEmpty())
|
||||
<p class="empty">No hubo partidos con resultado cargado esta semana.</p>
|
||||
@else
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Local</th>
|
||||
<th>Resultado</th>
|
||||
<th>Visitante</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($jugados as $e)
|
||||
@php
|
||||
$ml = $e->marcador_local;
|
||||
$mv = $e->marcador_visitante;
|
||||
$badgeLocal = $ml > $mv ? 'badge-win' : ($ml < $mv ? 'badge-lose' : 'badge-draw');
|
||||
$badgeVisitante= $mv > $ml ? 'badge-win' : ($mv < $ml ? 'badge-lose' : 'badge-draw');
|
||||
@endphp
|
||||
<tr>
|
||||
<td>{{ $e->fecha_evento->format('d/m') }}</td>
|
||||
<td><span class="badge {{ $badgeLocal }}">{{ $e->equipoLocal->club->nombre ?? '?' }}</span></td>
|
||||
<td style="text-align:center; font-weight:bold;">{{ $ml }} — {{ $mv }}</td>
|
||||
<td><span class="badge {{ $badgeVisitante }}">{{ $e->equipoVisitante->club->nombre ?? '?' }}</span></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Próximos partidos --}}
|
||||
<div class="section">
|
||||
<h2>Próximos Partidos (7 días)</h2>
|
||||
@if($proximos->isEmpty())
|
||||
<p class="empty">No hay partidos programados para los próximos 7 días.</p>
|
||||
@else
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Fecha</th><th>Hora</th><th>Partido</th><th>Sede</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($proximos as $e)
|
||||
<tr>
|
||||
<td>{{ $e->fecha_evento->format('d/m') }}</td>
|
||||
<td>{{ $e->hora_inicio ? \Carbon\Carbon::parse($e->hora_inicio)->format('H:i') : '—' }}</td>
|
||||
<td>{{ $e->equipoLocal->club->nombre ?? '?' }} vs {{ $e->equipoVisitante->club->nombre ?? '?' }}</td>
|
||||
<td>{{ $e->sede ?: '—' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Top goleadores --}}
|
||||
<div class="section">
|
||||
<h2>🏆 Top Goleadores</h2>
|
||||
@if($topGoleadores->isEmpty())
|
||||
<p class="empty">Sin estadísticas de puntos cargadas aún.</p>
|
||||
@else
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>#</th><th>Jugador</th><th>Pts totales</th><th>Partidos</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($topGoleadores as $i => $g)
|
||||
<tr>
|
||||
<td>{{ $i + 1 }}</td>
|
||||
<td>{{ $g->apellido }}, {{ $g->nombre }}</td>
|
||||
<td><strong>{{ $g->total_puntos }}</strong></td>
|
||||
<td>{{ $g->partidos }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Este reporte fue generado automáticamente por el sistema OnAPB el {{ now()->format('d/m/Y H:i') }}.<br>
|
||||
<a href="https://onapb.com/admin" style="color:#b00000;">Ir al panel de administración</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Recuperar Contraseña - OnAPB</title>
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 0; background-color: #f4f4f4; }
|
||||
.container { max-width: 600px; margin: 20px auto; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
|
||||
.header { background: linear-gradient(135deg, #cc0000 0%, #aa0000 100%); color: #fff; padding: 40px 20px; text-align: center; }
|
||||
.header h1 { margin: 0; font-size: 28px; text-transform: uppercase; letter-spacing: 2px; }
|
||||
.content { padding: 30px; }
|
||||
.content h2 { color: #cc0000; margin-top: 0; }
|
||||
.footer { background: #222; color: #888; text-align: center; padding: 20px; font-size: 12px; }
|
||||
.btn { display: inline-block; padding: 12px 25px; background: #cc0000; color: #fff; text-decoration: none; border-radius: 6px; font-weight: bold; margin-top: 20px; text-transform: uppercase; }
|
||||
.info-box { background: #fff8f8; padding: 15px; border-radius: 8px; border: 1px solid #ffcccc; margin: 20px 0; color: #660000; font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>ONAPB</h1>
|
||||
<p>Recuperación de Acceso</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>Hola, {{ $user->nombre }}</h2>
|
||||
<p>Hemos recibido una solicitud para restablecer la contraseña de tu cuenta en <strong>OnAPB</strong>.</p>
|
||||
|
||||
<p>Para continuar con el proceso, hacé clic en el siguiente botón:</p>
|
||||
|
||||
<center>
|
||||
<a href="{{ url('/reset-password/' . $token) }}" class="btn">RESTABLECER CONTRASEÑA</a>
|
||||
</center>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>Importante:</strong> Este enlace vencerá en 1 hora por razones de seguridad. Si vos no solicitaste este cambio, podés ignorar este correo; tu contraseña seguirá siendo la misma.
|
||||
</div>
|
||||
|
||||
<p>Si tenés problemas con el botón, copiá y pegá el siguiente enlace en tu navegador:</p>
|
||||
<p style="word-break: break-all; font-size: 12px; color: #666;">
|
||||
{{ url('/reset-password/' . $token) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
© {{ date('Y') }} OnAPB - Asociación Paranaense de Básquetbol.<br>
|
||||
Este es un correo automático, por favor no lo respondas.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bienvenido a OnAPB</title>
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 0; background-color: #f4f4f4; }
|
||||
.container { max-width: 600px; margin: 20px auto; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
|
||||
.header { background: linear-gradient(135deg, #cc0000 0%, #aa0000 100%); color: #fff; padding: 40px 20px; text-align: center; }
|
||||
.header h1 { margin: 0; font-size: 28px; text-transform: uppercase; letter-spacing: 2px; }
|
||||
.content { padding: 30px; }
|
||||
.content h2 { color: #cc0000; margin-top: 0; }
|
||||
.footer { background: #222; color: #888; text-align: center; padding: 20px; font-size: 12px; }
|
||||
.btn { display: inline-block; padding: 12px 25px; background: #cc0000; color: #fff; text-decoration: none; border-radius: 6px; font-weight: bold; margin-top: 20px; }
|
||||
.details { background: #f9f9f9; padding: 15px; border-radius: 8px; border-left: 4px solid #cc0000; margin: 20px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>ONAPB</h1>
|
||||
<p>La nueva forma de vivir el básquet</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>¡Hola, {{ $user->nombre }}!</h2>
|
||||
<p>Es un gusto darte la bienvenida a <strong>OnAPB</strong>, la plataforma oficial de la Asociación Paranaense de Básquetbol.</p>
|
||||
|
||||
<p>Te has registrado correctamente como <strong>{{ ucfirst($tipo) }}</strong>. Ya podés acceder a tu panel para ver tus QRs, noticias y beneficios exclusivos.</p>
|
||||
|
||||
<div class="details">
|
||||
<strong>Tus datos registrados:</strong><br>
|
||||
Nombre: {{ $user->nombre }} {{ $user->apellido }}<br>
|
||||
DNI: {{ $user->dni ?? $user->documento }}<br>
|
||||
Email: {{ $user->email }}
|
||||
</div>
|
||||
|
||||
<p>Para ingresar al sistema, hacé clic en el siguiente botón:</p>
|
||||
<center>
|
||||
<a href="{{ url('/') }}" class="btn">INGRESAR AL PANEL</a>
|
||||
</center>
|
||||
</div>
|
||||
<div class="footer">
|
||||
© {{ date('Y') }} OnAPB - Asociación Paranaense de Básquetbol.<br>
|
||||
Este es un correo automático, por favor no lo respondas.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,208 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Plantilla - ' . ($equipo->club->nombre ?? 'Equipo'))
|
||||
|
||||
@section('content')
|
||||
<div class="team-detail-container py-5">
|
||||
<div class="container">
|
||||
<!-- Header del Equipo -->
|
||||
<div class="kinetic-card mb-5 overflow-hidden border-0">
|
||||
<div class="card-body p-0">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4 bg-dark d-flex align-items-center justify-content-center p-5">
|
||||
@if($equipo->club && $equipo->club->imagen)
|
||||
<img src="{{ asset($equipo->club->imagen) }}" alt="{{ $equipo->club->nombre }}" class="img-fluid team-logo-large">
|
||||
@else
|
||||
<div class="text-white opacity-25 display-1">
|
||||
<i class="bi bi-shield-shaded"></i>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="col-md-8 p-5 d-flex flex-column justify-content-center">
|
||||
<span class="badge bg-primary text-uppercase tracking-widest mb-3 px-3 py-2" style="width: fit-content;">Ficha Oficial de Equipo</span>
|
||||
<h1 class="display-4 fw-bold text-uppercase italic tracking-tighter mb-1">
|
||||
{{ $equipo->club->nombre ?? 'Equipo' }}
|
||||
</h1>
|
||||
<h2 class="h3 text-muted text-uppercase fw-light">
|
||||
{{ $equipo->categoria }} {{ $equipo->division }}
|
||||
</h2>
|
||||
|
||||
<div class="d-flex gap-4 mt-4 text-muted flex-wrap align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-people-fill fs-4 text-primary"></i>
|
||||
<span class="fw-bold">{{ $equipo->jugadores->count() }} Jugadores</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-geo-alt-fill fs-4 text-primary"></i>
|
||||
<span>{{ $equipo->club->nombre ?? 'Sede Oficial' }}</span>
|
||||
</div>
|
||||
|
||||
{{-- Botón Seguir (solo para usuarios logueados) --}}
|
||||
@if(session('user_logged_in'))
|
||||
<button id="btn-seguir"
|
||||
class="btn btn-outline-warning fw-bold ms-auto"
|
||||
data-equipo="{{ $equipo->id_equipo }}"
|
||||
data-csrf="{{ csrf_token() }}"
|
||||
style="min-width:140px;">
|
||||
<i class="bi bi-star me-1" id="seguir-icon"></i>
|
||||
<span id="seguir-label">Cargando...</span>
|
||||
</button>
|
||||
@else
|
||||
<a href="/login" class="btn btn-outline-secondary ms-auto fw-bold">
|
||||
<i class="bi bi-star me-1"></i> Seguir equipo
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Lista de Jugadores -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-end mb-4">
|
||||
<div>
|
||||
<h3 class="fw-bold text-uppercase mb-0">Plantilla <span class="text-primary">Actual</span></h3>
|
||||
<p class="text-muted mb-0">Temporada {{ date('Y') }}</p>
|
||||
</div>
|
||||
<a href="javascript:history.back()" class="btn btn-outline-dark btn-sm text-uppercase fw-bold">
|
||||
<i class="bi bi-arrow-left me-2"></i>Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="kinetic-card overflow-hidden border-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-dark text-white text-uppercase small tracking-widest fw-bold">
|
||||
<tr>
|
||||
<th class="ps-4 py-3">Jugador</th>
|
||||
<th class="py-3 text-center d-none d-md-table-cell">Categoría</th>
|
||||
<th class="pe-4 py-3 text-end">Estado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($equipo->jugadores as $j)
|
||||
<tr>
|
||||
<td class="ps-4 py-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar-sm bg-primary text-white rounded-circle d-none d-sm-flex align-items-center justify-content-center me-3 fw-bold">
|
||||
{{ substr($j->nombre, 0, 1) }}{{ substr($j->apellido, 0, 1) }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-uppercase fw-bold text-truncate" style="max-width: 180px;">{{ $j->apellido }}, {{ $j->nombre }}</div>
|
||||
<div class="d-md-none small text-muted">{{ $j->categoria }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 text-center d-none d-md-table-cell">
|
||||
<span class="badge bg-light text-dark border">{{ $j->categoria }}</span>
|
||||
</td>
|
||||
<td class="pe-4 py-3 text-end">
|
||||
@if($j->activo)
|
||||
<span class="badge bg-success-subtle text-success border border-success border-opacity-25 px-2 py-1">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
<span class="d-none d-sm-inline ms-1 small fw-bold text-uppercase">Habilitado</span>
|
||||
</td>
|
||||
@else
|
||||
<span class="badge bg-danger-subtle text-danger border border-danger border-opacity-25 px-2 py-1">
|
||||
<i class="bi bi-clock-fill"></i>
|
||||
<span class="d-none d-sm-inline ms-1 small fw-bold text-uppercase">Pendiente</span>
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="p-5 text-center text-muted italic">
|
||||
No hay jugadores registrados en este equipo todavía.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.team-detail-container {
|
||||
background-color: var(--surface);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.team-logo-large {
|
||||
max-height: 200px;
|
||||
filter: drop-shadow(0 10px 15px rgba(0,0,0,0.3));
|
||||
}
|
||||
.avatar-sm {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.italic { font-style: italic; }
|
||||
.tracking-widest { letter-spacing: 0.1em; }
|
||||
.tracking-tighter { letter-spacing: -0.05em; }
|
||||
</style>
|
||||
|
||||
@if(session('user_logged_in'))
|
||||
<script>
|
||||
(function() {
|
||||
const btn = document.getElementById('btn-seguir');
|
||||
const icon = document.getElementById('seguir-icon');
|
||||
const label = document.getElementById('seguir-label');
|
||||
if (!btn) return;
|
||||
|
||||
const equipoId = btn.dataset.equipo;
|
||||
const csrf = btn.dataset.csrf;
|
||||
|
||||
// 1. Cargar estado actual
|
||||
fetch(`/seguimiento/estado/${equipoId}`)
|
||||
.then(r => r.json())
|
||||
.then(data => actualizarBoton(data.siguiendo))
|
||||
.catch(() => label.textContent = 'Seguir');
|
||||
|
||||
// 2. Toggle al hacer click
|
||||
btn.addEventListener('click', function() {
|
||||
btn.disabled = true;
|
||||
label.textContent = '...';
|
||||
|
||||
fetch(`/seguimiento/equipo/${equipoId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': csrf,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
actualizarBoton(data.siguiendo);
|
||||
// Mostrar toast
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({ toast: true, position: 'top-end', icon: 'success', title: data.msg, showConfirmButton: false, timer: 2000 });
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => btn.disabled = false);
|
||||
});
|
||||
|
||||
function actualizarBoton(siguiendo) {
|
||||
if (siguiendo) {
|
||||
btn.classList.replace('btn-outline-warning', 'btn-warning');
|
||||
icon.className = 'bi bi-star-fill me-1';
|
||||
label.textContent = 'Siguiendo';
|
||||
} else {
|
||||
btn.classList.replace('btn-warning', 'btn-outline-warning');
|
||||
icon.className = 'bi bi-star me-1';
|
||||
label.textContent = 'Seguir equipo';
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@endif
|
||||
@endsection
|
||||
@@ -0,0 +1,105 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Partidos - OnAPB')
|
||||
|
||||
@section('styles')
|
||||
<link rel="stylesheet" href="{{ asset('static/eventos.css?v=5') }}">
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid py-5" style="background: var(--surface);">
|
||||
<div class="container py-4">
|
||||
<h1 class="display-1 fw-bold mb-5" style="letter-spacing: -0.05em;">Calendario <span class="text-primary">ONAPB</span></h1>
|
||||
|
||||
<!-- Horizontal Date Scroller -->
|
||||
<div class="mb-5 overflow-hidden position-relative">
|
||||
<div class="d-flex gap-3 overflow-auto pb-3 no-scrollbar" style="scroll-behavior: smooth;">
|
||||
@foreach($fechasNav as $nav)
|
||||
<a href="{{ route('eventos.index', ['fecha' => $nav['fecha']]) }}"
|
||||
class="flex-shrink-0 text-decoration-none px-4 py-3 text-center d-flex flex-column justify-content-center {{ $nav['active'] ? 'bg-primary text-white' : 'bg-white text-kinetic-muted' }}"
|
||||
style="min-width: 100px; transition: all 0.3s ease; border: 1px solid var(--outline-variant);">
|
||||
<span class="small fw-bold text-uppercase">{{ explode(' ', $nav['label'])[0] }}</span>
|
||||
<span class="fs-4 fw-bold">{{ explode(' ', $nav['label'])[1] }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
<!-- Linear gradients for scroll indication -->
|
||||
<div class="position-absolute top-0 start-0 h-100" style="width: 40px; background: linear-gradient(to right, var(--surface), transparent); pointer-events: none;"></div>
|
||||
<div class="position-absolute top-0 end-0 h-100" style="width: 40px; background: linear-gradient(to left, var(--surface), transparent); pointer-events: none;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Match List -->
|
||||
<div class="row g-4">
|
||||
@if($eventos->isEmpty())
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="kinetic-card p-5">
|
||||
<i class="bi bi-calendar-x text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<p class="fs-4 text-muted">No hay partidos programados para esta fecha.</p>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
@foreach($eventos as $e)
|
||||
@php
|
||||
$localClub = $e->equipoLocal?->club;
|
||||
$visitanteClub = $e->equipoVisitante?->club;
|
||||
$isLive = $e->estado === 'Activo';
|
||||
@endphp
|
||||
<div class="col-12">
|
||||
<a href="{{ route('eventos.show', $e->id_evento) }}" class="text-decoration-none">
|
||||
<div class="kinetic-card p-0 overflow-hidden d-flex flex-column flex-md-row align-items-stretch"
|
||||
style="border-left: {{ $isLive ? '8px solid var(--primary)' : 'none' }}; min-height: 120px;">
|
||||
|
||||
<!-- Status/Time (Asymmetric Side) -->
|
||||
<div class="p-4 d-flex flex-column justify-content-center text-center bg-light" style="width: 150px; border-right: 1px solid var(--outline-variant);">
|
||||
@if($isLive)
|
||||
<span class="badge bg-primary mb-2 pulse">EN VIVO</span>
|
||||
@else
|
||||
<span class="small fw-bold text-uppercase text-muted mb-1">{{ $e->estado }}</span>
|
||||
@endif
|
||||
<span class="fs-3 fw-bold">{{ \Carbon\Carbon::parse($e->hora_inicio)->format('H:i') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Teams (Center) -->
|
||||
<div class="flex-grow-1 p-4 d-flex align-items-center justify-content-between px-md-5">
|
||||
<div class="d-flex align-items-center gap-3 text-start" style="width: 40%;">
|
||||
<div style="width: 50px; height: 50px;">
|
||||
@if($localClub && $localClub->imagen)
|
||||
<img src="{{ asset($localClub->imagen) }}" class="img-fluid rounded-circle" alt="{{ $localClub->nombre }}">
|
||||
@else
|
||||
<div class="w-100 h-100 bg-light rounded-circle d-flex align-items-center justify-content-center text-muted"><i class="bi bi-shield"></i></div>
|
||||
@endif
|
||||
</div>
|
||||
<h3 class="h4 mb-0 fw-bold">{{ $localClub?->nombre ?? 'Local' }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="text-center px-3">
|
||||
<span class="fs-2 fw-bold text-muted">VS</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-3 text-end justify-content-end" style="width: 40%;">
|
||||
<h3 class="h4 mb-0 fw-bold">{{ $visitanteClub?->nombre ?? 'Visitante' }}</h3>
|
||||
<div style="width: 50px; height: 50px;">
|
||||
@if($visitanteClub && $visitanteClub->imagen)
|
||||
<img src="{{ asset($visitanteClub->imagen) }}" class="img-fluid rounded-circle" alt="{{ $visitanteClub->nombre }}">
|
||||
@else
|
||||
<div class="w-100 h-100 bg-light rounded-circle d-flex align-items-center justify-content-center text-muted"><i class="bi bi-shield"></i></div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Venue (Right) -->
|
||||
<div class="p-4 d-none d-md-flex flex-column justify-content-center text-muted" style="width: 250px; border-left: 1px solid var(--outline-variant);">
|
||||
<span class="small fw-bold text-uppercase">Sede</span>
|
||||
<span class="small"><i class="bi bi-geo-alt"></i> {{ $e->sede ?? 'TBD' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,119 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Detalle Partido - OnAPB')
|
||||
|
||||
@section('content')
|
||||
@section('content')
|
||||
<div class="container-fluid py-5" style="background: var(--surface);">
|
||||
<div class="container py-4">
|
||||
<?php
|
||||
$nombreEvento = $evento->nombre_evento ?? (
|
||||
($evento->equipoLocal?->club?->nombre ?? 'Local') . ' vs ' . ($evento->equipoVisitante?->club?->nombre ?? 'Visitante')
|
||||
);
|
||||
$localClub = $evento->equipoLocal?->club;
|
||||
$visitanteClub = $evento->equipoVisitante?->club;
|
||||
?>
|
||||
|
||||
<div class="row g-5">
|
||||
<!-- Header Editorial Side -->
|
||||
<div class="col-lg-8">
|
||||
<div class="mb-5">
|
||||
<span class="text-primary fw-bold tracking-widest text-uppercase mb-2 d-block">Match Profile</span>
|
||||
<h1 class="display-2 fw-bold mb-4" style="line-height: 0.9;">{{ $nombreEvento }}</h1>
|
||||
<div style="width: 120px; height: 10px; background: var(--primary);"></div>
|
||||
</div>
|
||||
|
||||
<!-- Score/Teams Editorial -->
|
||||
<div class="kinetic-card p-0 mb-5 overflow-hidden">
|
||||
<div class="row g-0 align-items-center text-center">
|
||||
<div class="col-5 p-5 bg-light">
|
||||
@if($localClub && $localClub->imagen)
|
||||
<img src="{{ asset($localClub->imagen) }}" class="img-fluid rounded-circle mb-3" style="width: 120px;" alt="{{ $localClub->nombre }}">
|
||||
@endif
|
||||
<h3 class="h2 fw-bold text-uppercase">{{ $localClub?->nombre ?? 'Local' }}</h3>
|
||||
<span class="text-muted small fw-bold">{{ $evento->equipoLocal?->categoria }}</span>
|
||||
</div>
|
||||
<div class="col-2 p-4">
|
||||
<span class="display-4 fw-bold text-muted">VS</span>
|
||||
</div>
|
||||
<div class="col-5 p-5 bg-white">
|
||||
@if($visitanteClub && $visitanteClub->imagen)
|
||||
<img src="{{ asset($visitanteClub->imagen) }}" class="img-fluid rounded-circle mb-3" style="width: 120px;" alt="{{ $visitanteClub->nombre }}">
|
||||
@endif
|
||||
<h3 class="h2 fw-bold text-uppercase">{{ $visitanteClub?->nombre ?? 'Visitante' }}</h3>
|
||||
<span class="text-muted small fw-bold">{{ $evento->equipoVisitante?->categoria }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Grid -->
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-md-4">
|
||||
<div class="bg-white p-4" style="border-bottom: 4px solid var(--primary-container);">
|
||||
<span class="text-muted small fw-bold text-uppercase d-block mb-1">Fecha</span>
|
||||
<span class="fs-5 fw-bold">{{ \Carbon\Carbon::parse($evento->fecha_evento)->translatedFormat('d F Y') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="bg-white p-4" style="border-bottom: 4px solid var(--primary-container);">
|
||||
<span class="text-muted small fw-bold text-uppercase d-block mb-1">Horario</span>
|
||||
<span class="fs-5 fw-bold">{{ \Carbon\Carbon::parse($evento->hora_inicio)->format('H:i') }} hs</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="bg-white p-4" style="border-bottom: 4px solid var(--primary-container);">
|
||||
<span class="text-muted small fw-bold text-uppercase d-block mb-1">Estado</span>
|
||||
<span class="fs-5 fw-bold {{ $evento->estado === 'Activo' ? 'text-primary' : '' }} text-uppercase">{{ $evento->estado }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Venue Map Placeholder -->
|
||||
<div class="kinetic-card p-4 bg-light">
|
||||
<h4 class="fw-bold mb-3">UBICACIÓN</h4>
|
||||
<p class="fs-5 mb-0"><i class="bi bi-geo-alt-fill text-primary"></i> {{ $evento->sede ?? 'Sede a confirmar' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<div class="sticky-top" style="top: 100px;">
|
||||
<div class="kinetic-card bg-primary text-white p-5 text-center">
|
||||
<h4 class="display-6 fw-bold mb-4">ENTRADAS</h4>
|
||||
<p class="fs-4 mb-5">
|
||||
@if($evento->precio > 0)
|
||||
${{ number_format($evento->precio, 0, ',', '.') }} <span class="fs-6 opacity-75">/ entrada</span>
|
||||
@else
|
||||
FREE <span class="fs-6 opacity-75">/ asociados</span>
|
||||
@endif
|
||||
</p>
|
||||
|
||||
@if($evento->estado !== 'Finalizado')
|
||||
@if($isUser)
|
||||
<form method="POST" action="{{ route('panel.solicitar.qr') }}" class="confirm-submit mb-3" data-confirm-text="¿Solicitar QR (entrada) para este evento?">
|
||||
@csrf
|
||||
<input type="hidden" name="id_evento" value="{{ $evento->id_evento }}">
|
||||
<button type="submit" class="btn btn-light btn-lg w-100 fw-bold py-3" style="border-radius: 0;">SOLICITAR QR</button>
|
||||
</form>
|
||||
<a href="{{ route('panel.mis.qrs', ['evento' => $evento->id_evento]) }}" class="text-white text-decoration-none small fw-bold">VER MIS REGISTROS <i class="bi bi-arrow-right"></i></a>
|
||||
@elseif(!$isAdmin)
|
||||
<button class="btn btn-outline-light btn-lg w-100 fw-bold" style="border-radius: 0;" data-bs-toggle="modal" data-bs-target="#loginModal">INICIAR SESIÓN</button>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@if($isAdmin)
|
||||
<div class="mt-5 pt-5 border-top border-white border-opacity-25">
|
||||
<a href="{{ route('admin.eventos.edit', $evento->id_evento) }}" class="btn btn-dark w-100 text-uppercase fw-bold mb-2">Editar Evento</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<a href="{{ route('eventos.index') }}" class="text-decoration-none text-muted small fw-bold"><i class="bi bi-arrow-left"></i> VOLVER AL CALENDARIO</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,740 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@yield('title', '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') }}">
|
||||
{{-- PWA --}}
|
||||
<link rel="manifest" href="/manifest.json?v=4">
|
||||
<meta name="theme-color" content="#b00000">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="OnAPB">
|
||||
<link rel="apple-touch-icon" href="/icons/icon-192.png?v=4">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152.png?v=4">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192.png?v=4">
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="/icons/icon-192.png?v=4">
|
||||
|
||||
{{-- Preload del logo del navbar (LCP-critical) --}}
|
||||
<link rel="preload" as="image" href="{{ asset('logo.webp') }}" type="image/webp" fetchpriority="high">
|
||||
|
||||
{{-- CSS critico (bloqueante, requerido para el layout/navbar) --}}
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
{{-- Tipografía: Antonio (display) + DM Sans (body) — cargadas en kinetic-arena.css --}}
|
||||
<link rel="stylesheet" href="{{ asset('static/base.css?v=5') }}">
|
||||
<link rel="stylesheet" href="{{ asset('static/kinetic-arena.css?v=5') }}">
|
||||
<link rel="stylesheet" href="{{ asset('static/kinetic-arena-v3.min.css') }}?v={{ @filemtime(public_path('static/kinetic-arena-v3.min.css')) ?: '3' }}">
|
||||
|
||||
{{-- CSS no critico: cargar async via preload-swap (no bloquea el primer paint) --}}
|
||||
<link rel="preload" as="style" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<link rel="preload" as="style" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
|
||||
</noscript>
|
||||
|
||||
@yield('styles')
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<style>
|
||||
.password-toggle {
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Splash Screen (X Style) -->
|
||||
<div id="splash-screen">
|
||||
<img src="/icons/icon-192.png?v=4" alt="OnAPB" id="splash-logo">
|
||||
</div>
|
||||
<script>
|
||||
// Ocultar el splash INMEDIATAMENTE si ya se mostró en esta sesión.
|
||||
// Este script es síncrono: corre antes de que el navegador pinte nada.
|
||||
if (sessionStorage.getItem('splashShown')) {
|
||||
document.getElementById('splash-screen').style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg custom-navbar no-line">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{{ route('home') }}" data-magnetic data-magnetic-strength="0.3">
|
||||
<picture>
|
||||
<source srcset="{{ asset('logo.webp') }}" type="image/webp">
|
||||
<img src="{{ asset('logo.png') }}" alt="OnAPB" class="navbar-logo" width="80" height="80" fetchpriority="high" decoding="async">
|
||||
</picture>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item"><a class="nav-link {{ request()->routeIs('home') ? 'active' : '' }}" href="{{ route('home') }}">Inicio</a></li>
|
||||
<li class="nav-item"><a class="nav-link {{ request()->routeIs('eventos.*') ? 'active' : '' }}" href="{{ route('eventos.index') }}">Partidos</a></li>
|
||||
<li class="nav-item"><a class="nav-link {{ request()->routeIs('asociate') ? 'active' : '' }}" href="{{ route('asociate') }}">Unite</a></li>
|
||||
<li class="nav-item"><a class="nav-link {{ request()->routeIs('promos.*') ? 'active' : '' }}" href="{{ route('promos.index') }}">Lugares</a></li>
|
||||
<li class="nav-item"><a class="nav-link {{ request()->routeIs('noticias.*') ? 'active' : '' }}" href="{{ route('noticias.index') }}">Noticias</a></li>
|
||||
<li class="nav-item"><a class="nav-link fw-bold text-primary {{ request()->routeIs('documentacion.*') ? 'active' : '' }}" href="{{ route('documentacion.index') }}"><i class="bi bi-question-circle-fill me-1"></i> Ayuda</a></li>
|
||||
|
||||
@if(isset($navTorneos) && $navTorneos->count() > 0)
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
Torneos
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
@foreach($navTorneos as $nt)
|
||||
<li class="dropdown-header text-uppercase small fw-bold">{{ $nt->nombre }}</li>
|
||||
<li><a class="dropdown-item" href="{{ route('torneos.standings', $nt->id) }}">Posiciones</a></li>
|
||||
<li><a class="dropdown-item" href="{{ route('torneos.topScorers', $nt->id) }}">Goleadores</a></li>
|
||||
@if(!$loop->last)<li><hr class="dropdown-divider"></li>@endif
|
||||
@endforeach
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$isAdmin = session()->has('admin_logged_in') && session('admin_logged_in');
|
||||
$isUser = session()->has('user_logged_in') && session('user_logged_in');
|
||||
$userName = session('user_name', session('admin_username', ''));
|
||||
$avatarClass = $isAdmin ? 'text-danger fw-bold' : ($isUser ? 'text-primary fw-bold' : '');
|
||||
@endphp
|
||||
|
||||
@if($isUser || $isAdmin)
|
||||
{{-- 🔔 Badge de notificaciones (solo para usuarios, no admins puros) --}}
|
||||
@if($isUser)
|
||||
<li class="nav-item me-1">
|
||||
<a class="nav-link position-relative" href="{{ route('notificaciones.index') }}" id="notif-bell" title="Mis notificaciones">
|
||||
<i class="bi bi-bell-fill fs-5"></i>
|
||||
@if(isset($notifCount) && $notifCount > 0)
|
||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" id="notif-badge" style="font-size:0.65rem;">
|
||||
{{ $notifCount > 99 ? '99+' : $notifCount }}
|
||||
</span>
|
||||
@else
|
||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger d-none" id="notif-badge" style="font-size:0.65rem;"></span>
|
||||
@endif
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle {{ $avatarClass }}" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle"></i> {{ $userName }}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
@if($isUser)
|
||||
<li class="dropdown-header">Usuario</li>
|
||||
<li><span class="dropdown-item-text">{{ session('user_tipo') === 'jugador' ? '🏀' : '👤' }} {{ $userName }} ({{ session('user_tipo') }})</span></li>
|
||||
<li><a class="dropdown-item" href="{{ route('panel.usuario') }}">Mi Panel</a></li>
|
||||
<li>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="dropdown-item text-danger">Salir</button>
|
||||
</form>
|
||||
</li>
|
||||
@endif
|
||||
@if($isAdmin)
|
||||
@if($isUser)<li><hr class="dropdown-divider"></li>@endif
|
||||
<li class="dropdown-header">Admin</li>
|
||||
@if(session('admin_role') == 1 || session('admin_role') == 2)
|
||||
<li><a class="dropdown-item" href="{{ route('admin.dashboard') }}">Gestionar</a></li>
|
||||
@endif
|
||||
<li>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="dropdown-item text-danger">Salir Admin</button>
|
||||
</form>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</li>
|
||||
@else
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" data-bs-toggle="modal" data-bs-target="#loginModal">Iniciar sesión</a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Contenido -->
|
||||
<div class="content">
|
||||
@yield('content')
|
||||
</div>
|
||||
|
||||
<!-- Banner PWA: Instalar la app -->
|
||||
<div id="pwa-install-banner" style="display:none; position:fixed; bottom:0; left:0; right:0; z-index:9999;
|
||||
background: white; color: #111;
|
||||
padding:15px 20px; align-items:center; justify-content:space-between;
|
||||
gap:12px; box-shadow:0 -4px 30px rgba(0,0,0,0.15); flex-wrap:wrap; border-top: 1px solid #eee;">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div>
|
||||
<img src="/icons/icon-96.png?v=2" alt="OnAPB" style="width:45px; height:45px; object-fit: contain;" onerror="this.style.display='none'">
|
||||
</div>
|
||||
<div id="pwa-text-container">
|
||||
<div class="fw-bold text-dark" style="font-size:1rem; letter-spacing: -0.01em;">Instalá OnAPB en tu celular</div>
|
||||
<div class="text-muted" style="font-size:0.82rem;">Accedé rápido a partidos, tu QR y notificaciones</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<button id="pwa-install-btn" class="btn btn-light btn-sm fw-bold px-4 hover-scale" style="border-radius:20px; height: 38px;">
|
||||
<i class="bi bi-download me-1"></i> Instalar
|
||||
</button>
|
||||
<button id="pwa-install-dismiss" class="btn btn-link text-white p-2" style="text-decoration:none;" title="Cerrar">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer" style="background-color: var(--surface-container-low); border-top: 1px solid var(--outline-variant);">
|
||||
|
||||
<div class="sponsor-carousel-container" style="background-color: var(--surface-container-lowest); border-bottom: 1px solid var(--outline-variant);">
|
||||
<div class="sponsor-carousel-track" id="sponsorTrack">
|
||||
{{-- Logo OnAPB siempre va primero --}}
|
||||
<div class="sponsor-item">
|
||||
<img src="{{ asset('logo.png') }}" alt="OnAPB">
|
||||
</div>
|
||||
{{-- Sponsors si los hay --}}
|
||||
@if(isset($footerSponsors) && $footerSponsors->count() > 0)
|
||||
@foreach($footerSponsors as $s)
|
||||
<div class="sponsor-item">
|
||||
@if($s->url)
|
||||
<a href="{{ $s->url }}" target="_blank">
|
||||
<img src="{{ asset($s->imagen) }}" alt="{{ $s->nombre }}">
|
||||
</a>
|
||||
@else
|
||||
<img src="{{ asset($s->imagen) }}" alt="{{ $s->nombre }}">
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
{{-- Duplicamos para loop infinito --}}
|
||||
<div class="sponsor-item">
|
||||
<img src="{{ asset('logo.png') }}" alt="OnAPB">
|
||||
</div>
|
||||
@foreach($footerSponsors as $s)
|
||||
<div class="sponsor-item">
|
||||
@if($s->url)
|
||||
<a href="{{ $s->url }}" target="_blank">
|
||||
<img src="{{ asset($s->imagen) }}" alt="{{ $s->nombre }}">
|
||||
</a>
|
||||
@else
|
||||
<img src="{{ asset($s->imagen) }}" alt="{{ $s->nombre }}">
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
@else
|
||||
{{-- Sin sponsors: solo el logo centrado, sin animación --}}
|
||||
<div class="sponsor-item">
|
||||
<img src="{{ asset('logo.png') }}" alt="OnAPB">
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="container mt-4">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 py-3">
|
||||
<p class="mb-0 text-kinetic-muted small">
|
||||
<span class="kfx-dot me-2"></span>
|
||||
© {{ date('Y') }} OnAPB · <span class="kfx-text-gradient fw-bold">Kinetic Arena Experience</span>
|
||||
</p>
|
||||
<div class="d-flex gap-3 small">
|
||||
<a href="{{ route('home') }}" class="kfx-link text-kinetic-muted">Inicio</a>
|
||||
<a href="{{ route('eventos.index') }}" class="kfx-link text-kinetic-muted">Partidos</a>
|
||||
<a href="{{ route('promos.index') }}" class="kfx-link text-kinetic-muted">Lugares</a>
|
||||
<a href="{{ route('documentacion.index') }}" class="kfx-link text-kinetic-muted">Ayuda</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Modal de Login -->
|
||||
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<ul class="nav nav-tabs" id="loginTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @if(session('login_tab') != 'admin') active @endif" id="player-tab" data-bs-toggle="tab" data-bs-target="#playerLogin" type="button" role="tab">Jugador / Aficionado</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @if(session('login_tab') == 'admin') active @endif" id="admin-tab" data-bs-toggle="tab" data-bs-target="#adminLogin" type="button" role="tab">Admin</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content p-3">
|
||||
<!-- Error handled via Toast -->
|
||||
|
||||
<!-- Login Jugador/Aficionado -->
|
||||
<div class="tab-pane fade @if(session('login_tab') != 'admin') show active @endif" id="playerLogin" role="tabpanel">
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="tipo" value="player">
|
||||
<div class="mb-3">
|
||||
<input type="text" name="dni" class="form-control" placeholder="DNI" required>
|
||||
</div>
|
||||
<div class="mb-3 input-group">
|
||||
<input type="password" name="password" class="form-control" placeholder="Contraseña" required>
|
||||
<button class="btn btn-outline-secondary toggle-password" type="button">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="cf-turnstile" data-sitekey="{{ config('services.turnstile.site_key') }}"></div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">Ingresar</button>
|
||||
<div class="text-center mt-2">
|
||||
<a href="{{ route('recuperar') }}" class="small text-secondary">¿Olvidaste tu contraseña?</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Login Admin -->
|
||||
<div class="tab-pane fade @if(session('login_tab') == 'admin') show active @endif" id="adminLogin" role="tabpanel">
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="tipo" value="admin">
|
||||
<div class="mb-3">
|
||||
<input type="text" name="username" class="form-control" placeholder="Usuario" required>
|
||||
</div>
|
||||
<div class="mb-3 input-group">
|
||||
<input type="password" name="password" class="form-control" placeholder="Contraseña" required>
|
||||
<button class="btn btn-outline-secondary toggle-password" type="button">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="cf-turnstile" data-sitekey="{{ config('services.turnstile.site_key') }}"></div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Ingresar como Admin</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
// Toggle visibilidad de contraseña
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.toggle-password')) {
|
||||
const btn = e.target.closest('.toggle-password');
|
||||
const input = btn.parentElement.querySelector('input');
|
||||
const icon = btn.querySelector('i');
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
icon.classList.replace('bi-eye', 'bi-eye-slash');
|
||||
} else {
|
||||
input.type = 'password';
|
||||
icon.classList.replace('bi-eye-slash', 'bi-eye');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Notificaciones con SweetAlert
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Inicializar tooltips de Bootstrap
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Configuración Global SweetAlert2
|
||||
const brandColors = {
|
||||
primary: '#b00000',
|
||||
success: '#2e7d32',
|
||||
info: '#0277bd',
|
||||
danger: '#b00000',
|
||||
cancel: '#6c757d'
|
||||
};
|
||||
|
||||
// Función centralizada para notificaciones Toast con estilo de marca
|
||||
const showToast = (icon, title, text) => {
|
||||
let iconColor = brandColors.info;
|
||||
if (icon === 'success') iconColor = brandColors.success;
|
||||
if (icon === 'error') iconColor = brandColors.danger;
|
||||
if (icon === 'warning') iconColor = '#fbc02d';
|
||||
|
||||
Swal.fire({
|
||||
icon: icon,
|
||||
title: title,
|
||||
text: text,
|
||||
timer: 5000,
|
||||
timerProgressBar: true,
|
||||
showConfirmButton: false,
|
||||
position: 'top-end',
|
||||
toast: true,
|
||||
iconColor: iconColor
|
||||
});
|
||||
};
|
||||
|
||||
// ── Intersection Observer: animaciones al scroll ──
|
||||
const io = new IntersectionObserver((entries) => {
|
||||
entries.forEach(e => {
|
||||
if (e.isIntersecting) {
|
||||
e.target.classList.add('visible');
|
||||
io.unobserve(e.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' });
|
||||
|
||||
document.querySelectorAll('.animate-on-scroll, .animate-slide-left').forEach(el => io.observe(el));
|
||||
|
||||
// Mensaje de logout o cualquier mensaje por URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
['logout_msg', 'registro_msg', 'panel_msg', 'error_msg', 'admin_msg'].forEach(param => {
|
||||
const val = urlParams.get(param);
|
||||
if (val) {
|
||||
const decoded = decodeURIComponent(val);
|
||||
const isError = decoded.includes('⚠️') || decoded.toLowerCase().includes('error');
|
||||
showToast(isError ? 'error' : 'success', isError ? '¡Ups!' : '¡Listo!', decoded);
|
||||
}
|
||||
});
|
||||
|
||||
// Capturar mensajes de sesión de Laravel
|
||||
@if(session('mensaje'))
|
||||
showToast('info', 'Información', `{!! addslashes(session("mensaje")) !!}`);
|
||||
@endif
|
||||
|
||||
@if(session('registro_msg'))
|
||||
@php $isErr = strpos(session("registro_msg"), "⚠️") !== false || strpos(session("registro_msg"), "Error") !== false; @endphp
|
||||
showToast('{{ $isErr ? "error" : "success" }}', '{{ $isErr ? "¡Ups!" : "¡Listo!" }}', `{!! addslashes(session("registro_msg")) !!}`);
|
||||
@endif
|
||||
|
||||
@if(session('panel_msg'))
|
||||
showToast('success', '¡Listo!', `{!! addslashes(session("panel_msg")) !!}`);
|
||||
@endif
|
||||
|
||||
@if(session('panel_error'))
|
||||
showToast('error', '¡Ups!', `{!! addslashes(session("panel_error")) !!}`);
|
||||
@endif
|
||||
|
||||
@if(session('admin_msg'))
|
||||
showToast('success', '¡Listo!', `{!! addslashes(session("admin_msg")) !!}`);
|
||||
@endif
|
||||
|
||||
@if(session('admin_error'))
|
||||
showToast('error', '¡Ups!', `{!! addslashes(session("admin_error")) !!}`);
|
||||
@endif
|
||||
|
||||
@if(session('login_error'))
|
||||
showToast('error', 'Error de acceso', `{!! addslashes(session("login_error")) !!}`);
|
||||
// Abrir el modal automáticamente si hay error de login para permitir re-intento
|
||||
try {
|
||||
var myModal = new bootstrap.Modal(document.getElementById('loginModal'));
|
||||
myModal.show();
|
||||
} catch (e) {
|
||||
console.error("Error opening login modal:", e);
|
||||
}
|
||||
@endif
|
||||
|
||||
// Interceptor Global para Confirmaciones Estilizadas
|
||||
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';
|
||||
|
||||
// Determinar color del botón según el ícono o la clase
|
||||
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
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
form.classList.remove('confirm-submit');
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- SISTEMA DE NOTIFICACIONES PROACTIVAS ---
|
||||
@if(session()->has('user_logged_in'))
|
||||
(function() {
|
||||
let lastNotifId = localStorage.getItem('last_notif_id') || 0;
|
||||
|
||||
function actualizarBadgeGlobal() {
|
||||
fetch('/notificaciones/count')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const badge = document.getElementById('notif-badge');
|
||||
if (!badge) return;
|
||||
if (data.count > 0) {
|
||||
badge.textContent = data.count > 99 ? '99+' : data.count;
|
||||
badge.classList.remove('d-none');
|
||||
} else {
|
||||
badge.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function checkNewNotifications() {
|
||||
if (!("Notification" in window)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('{{ route("notificaciones.latest") }}');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.id > 0 && data.id > lastNotifId) {
|
||||
if (Notification.permission === "granted" && document.visibilityState !== 'visible') {
|
||||
const n = new Notification(data.titulo || "OnAPB", {
|
||||
body: data.mensaje || "Tenés una nueva notificación",
|
||||
icon: "/icons/icon-192.png"
|
||||
});
|
||||
n.onclick = () => {
|
||||
window.focus();
|
||||
location.href = "{{ route('notificaciones.index') }}";
|
||||
};
|
||||
}
|
||||
|
||||
actualizarBadgeGlobal();
|
||||
lastNotifId = data.id;
|
||||
localStorage.setItem('last_notif_id', lastNotifId);
|
||||
} else if (data.id > 0) {
|
||||
lastNotifId = Math.max(lastNotifId, data.id);
|
||||
localStorage.setItem('last_notif_id', lastNotifId);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error polling notifications:", e);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if ("Notification" in window && Notification.permission === "default") {
|
||||
setTimeout(() => {
|
||||
Notification.requestPermission();
|
||||
}, 5000);
|
||||
}
|
||||
checkNewNotifications();
|
||||
setInterval(checkNewNotifications, 300000); // 5 min
|
||||
});
|
||||
})();
|
||||
@endif
|
||||
</script>
|
||||
|
||||
@yield('scripts')
|
||||
|
||||
{{-- PWA: Registro del Service Worker y banner de instalación --}}
|
||||
<script>
|
||||
// Registrar Service Worker
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js').catch(err => {
|
||||
console.warn('SW registration failed:', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Banner de instalación a la app (PWA)
|
||||
let deferredPrompt = null;
|
||||
const installBanner = document.getElementById('pwa-install-banner');
|
||||
const installBtn = document.getElementById('pwa-install-btn');
|
||||
const installDismiss= document.getElementById('pwa-install-dismiss');
|
||||
const pwaTextContainer = document.getElementById('pwa-text-container');
|
||||
|
||||
// Detección de iOS y Standalone
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
const isStandalone = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone;
|
||||
|
||||
// Lógica para iOS (Safari no dispara beforeinstallprompt)
|
||||
if (isIOS && !isStandalone && !localStorage.getItem('pwa-dismissed')) {
|
||||
if (installBanner) {
|
||||
installBanner.style.display = 'flex';
|
||||
if (installBtn) installBtn.style.display = 'none';
|
||||
if (pwaTextContainer) {
|
||||
pwaTextContainer.innerHTML = `
|
||||
<div class="fw-bold" style="font-size:0.95rem;">Instalá OnAPB en tu iPhone</div>
|
||||
<div style="font-size:0.8rem; opacity:0.9;">Tocá <i class="bi bi-share"></i> y luego "Añadir a pantalla de inicio" <i class="bi bi-plus-square"></i></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
// Solo mostrar si no es iOS (donde ya manejamos el banner manual) y no está descartado
|
||||
if (!isIOS && installBanner && !localStorage.getItem('pwa-dismissed')) {
|
||||
installBanner.style.display = 'flex';
|
||||
}
|
||||
});
|
||||
|
||||
if (installBtn) {
|
||||
installBtn.addEventListener('click', async () => {
|
||||
if (!deferredPrompt) return;
|
||||
deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
if (outcome === 'accepted') {
|
||||
installBanner.style.display = 'none';
|
||||
localStorage.setItem('pwa-dismissed', '1');
|
||||
}
|
||||
deferredPrompt = null;
|
||||
});
|
||||
}
|
||||
|
||||
if (installDismiss) {
|
||||
installDismiss.addEventListener('click', () => {
|
||||
if (installBanner) installBanner.style.display = 'none';
|
||||
localStorage.setItem('pwa-dismissed', '1');
|
||||
});
|
||||
}
|
||||
|
||||
// --- Lógica de Web Push Notifications ---
|
||||
@if(session('user_logged_in'))
|
||||
async function subscribeUserToPush() {
|
||||
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
||||
console.warn('Push messaging is not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
|
||||
// Suscribirse
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array("{{ env('VITE_VAPID_PUBLIC_KEY') }}")
|
||||
});
|
||||
|
||||
console.log('User is subscribed:', subscription);
|
||||
|
||||
// Enviar suscripción al backend
|
||||
await fetch("{{ route('notificaciones.subscribe') }}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify(subscription)
|
||||
});
|
||||
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: '¡Activado!',
|
||||
text: 'Ya vas con recibir notificaciones en tu equipo.',
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
timer: 3000,
|
||||
showConfirmButton: false
|
||||
});
|
||||
|
||||
// Ocultar botón si existe
|
||||
const pushBtn = document.getElementById('pwa-push-btn');
|
||||
if (pushBtn) pushBtn.classList.add('d-none');
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to subscribe the user: ', err);
|
||||
if (Notification.permission === 'denied') {
|
||||
Swal.fire('Atención', 'Bloqueaste las notificaciones. Por favor, habilitalas en la configuración de tu navegador.', 'warning');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/\-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
}
|
||||
|
||||
// Verificar permisos al cargar
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if ('Notification' in window && Notification.permission === 'granted') {
|
||||
// Ya tiene permiso, intentar renovar suscripción silenciosamente
|
||||
navigator.serviceWorker.ready.then(reg => {
|
||||
reg.pushManager.getSubscription().then(sub => {
|
||||
if (sub) {
|
||||
// Opcional: Re-enviar al backend por si cambió
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (isStandalone && 'Notification' in window && Notification.permission !== 'denied') {
|
||||
// Si es PWA instalada, mostrar botón de habilitar notis
|
||||
const installBanner = document.getElementById('pwa-install-banner');
|
||||
const pwaTextContainer = document.getElementById('pwa-text-container');
|
||||
const installBtn = document.getElementById('pwa-install-btn');
|
||||
|
||||
if (installBanner) {
|
||||
installBanner.style.display = 'flex';
|
||||
if (installBtn) installBtn.style.display = 'none';
|
||||
if (pwaTextContainer) {
|
||||
pwaTextContainer.innerHTML = `
|
||||
<div class="fw-bold text-dark" style="font-size:0.95rem;">Habilitá las notificaciones</div>
|
||||
<div class="text-muted" style="font-size:0.8rem;">Recibí avisos de partidos y novedades al instante</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Agregar botón de habilitar
|
||||
const actionContainer = installBanner.querySelector('.d-flex.gap-2');
|
||||
const pushBtn = document.createElement('button');
|
||||
pushBtn.id = 'pwa-push-btn';
|
||||
pushBtn.className = 'btn btn-primary btn-sm fw-bold px-4 hover-scale';
|
||||
pushBtn.style.borderRadius = '20px';
|
||||
pushBtn.style.height = '38px';
|
||||
pushBtn.innerHTML = '<i class="bi bi-bell-fill me-1"></i> Habilitar';
|
||||
pushBtn.onclick = subscribeUserToPush;
|
||||
actionContainer.insertBefore(pushBtn, actionContainer.firstChild);
|
||||
}
|
||||
}
|
||||
});
|
||||
@endif
|
||||
|
||||
// --- Control del Splash Screen ---
|
||||
// Solo se muestra una vez por sesión (no en cada navegación)
|
||||
window.addEventListener('load', () => {
|
||||
const splash = document.getElementById('splash-screen');
|
||||
if (!splash) return;
|
||||
|
||||
if (sessionStorage.getItem('splashShown')) {
|
||||
// Ya se mostró en esta sesión: ocultar inmediatamente sin animación
|
||||
splash.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.setItem('splashShown', '1');
|
||||
setTimeout(() => {
|
||||
splash.classList.add('splash-hidden');
|
||||
setTimeout(() => splash.remove(), 600);
|
||||
}, 800);
|
||||
});
|
||||
|
||||
// Fallback por si algo falla al cargar
|
||||
setTimeout(() => {
|
||||
const splash = document.getElementById('splash-screen');
|
||||
if (splash) splash.classList.add('splash-hidden');
|
||||
}, 5000);
|
||||
</script>
|
||||
|
||||
{{-- Kinetic FX: capa de innovación visual (reveal, magnetic, ripple, parallax, cursor, contadores) --}}
|
||||
<script src="{{ asset('static/kinetic-fx.js') }}?v={{ @filemtime(public_path('static/kinetic-fx.js')) ?: '3' }}" defer></script>
|
||||
@include('components.genius-chat')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Noticias - OnAPB Media Hub')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid py-5" style="background: var(--surface);">
|
||||
<div class="container py-4">
|
||||
<div class="mb-5">
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-2">Comunidad & Actualidad</span>
|
||||
<h1 class="display-1 fw-bold mb-3" style="line-height: 0.9;">Noticias<span class="text-primary">.</span></h1>
|
||||
<p class="fs-4 text-muted mx-auto" style="max-width: 800px; margin-left: 0 !important;">El pulso del básquet entrerriano. Crónicas, resultados y las efemérides que marcan nuestra historia.</p>
|
||||
</div>
|
||||
|
||||
@if($noticias->isEmpty())
|
||||
<div class="p-5 text-center bg-white shadow-sm" style="border-top: 10px solid var(--primary-container);">
|
||||
<i class="bi bi-newspaper text-muted display-1 mb-4 d-block"></i>
|
||||
<h3 class="fw-bold text-uppercase">Silencio en la cancha</h3>
|
||||
<p class="text-muted">No hay noticias publicadas por el momento. Volvé pronto.</p>
|
||||
</div>
|
||||
@else
|
||||
@php $featured = $noticias->first(); $rest = $noticias->skip(1); @endphp
|
||||
|
||||
<!-- Featured News: High Impact -->
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<div class="bg-white p-0 shadow-lg overflow-hidden d-flex flex-column flex-lg-row" style="min-height: 500px; border: 1px solid var(--primary-container);">
|
||||
<div class="col-lg-7 p-0 overflow-hidden">
|
||||
@if($featured->imagen)
|
||||
<img src="{{ str_starts_with($featured->imagen, 'http') ? $featured->imagen : asset($featured->imagen) }}" alt="{{ $featured->titulo }}" class="w-100 h-100" style="object-fit: cover; min-height: 350px;">
|
||||
@else
|
||||
<div class="w-100 h-100 bg-dark d-flex align-items-center justify-content-center" style="min-height: 350px;">
|
||||
<i class="bi bi-image text-muted display-1"></i>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="col-lg-5 p-5 d-flex flex-column justify-content-center">
|
||||
<div class="mb-4">
|
||||
@if($featured->categoria)
|
||||
<span class="bg-primary text-white px-3 py-1 fw-bold text-uppercase small tracking-widest">{{ $featured->categoria }}</span>
|
||||
@else
|
||||
<span class="bg-primary text-white px-3 py-1 fw-bold text-uppercase small tracking-widest">Destacado</span>
|
||||
@endif
|
||||
<span class="ms-3 text-muted fw-bold small text-uppercase"><i class="bi bi-calendar3 me-2"></i> {{ $featured->fecha->translatedFormat('d M Y') }}</span>
|
||||
</div>
|
||||
<h2 class="display-4 fw-bold font-header mb-4" style="line-height: 1;">{{ $featured->titulo }}<span class="text-primary">.</span></h2>
|
||||
<p class="fs-5 text-muted mb-5">
|
||||
{{ Str::limit(strip_tags($featured->contenido), 200) }}
|
||||
</p>
|
||||
<a href="{{ route('noticias.show', $featured->id) }}" class="btn-kinetic-primary w-100 py-3 text-center">LEER CRÓNICA COMPLETA <i class="bi bi-arrow-right ms-2"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- News Grid: Editorial Cards -->
|
||||
<div class="row g-5">
|
||||
@foreach($rest as $noticia)
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="bg-white p-0 h-100 d-flex flex-column" style="border-top: 6px solid var(--outline-variant); transition: 0.3s;" onmouseover="this.style.borderTopColor='var(--primary)'" onmouseout="this.style.borderTopColor='var(--outline-variant)'">
|
||||
@if($noticia->imagen)
|
||||
<div style="height: 250px; overflow: hidden;">
|
||||
<img src="{{ str_starts_with($noticia->imagen, 'http') ? $noticia->imagen : asset($noticia->imagen) }}" alt="{{ $noticia->titulo }}" class="w-100 h-100" style="object-fit: cover;">
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-light d-flex align-items-center justify-content-center" style="height: 250px;">
|
||||
<i class="bi bi-image text-muted display-4"></i>
|
||||
</div>
|
||||
@endif
|
||||
<div class="p-4 flex-grow-1 d-flex flex-column">
|
||||
<div class="mb-3 d-flex justify-content-between align-items-center">
|
||||
<span class="text-primary fw-bold text-uppercase small tracking-widest">{{ $noticia->categoria ?? 'Actualidad' }}</span>
|
||||
<span class="text-muted fw-bold small">{{ $noticia->fecha->translatedFormat('d/m/Y') }}</span>
|
||||
</div>
|
||||
<h3 class="fw-bold font-header mb-3" style="font-size: 1.5rem; line-height: 1.2;">{{ $noticia->titulo }}</h3>
|
||||
<p class="text-muted small mb-4 flex-grow-1">
|
||||
{{ Str::limit(strip_tags($noticia->contenido), 120) }}
|
||||
</p>
|
||||
<a href="{{ route('noticias.show', $noticia->id) }}" class="text-dark fw-bold text-uppercase small tracking-widest text-decoration-none hover-primary">
|
||||
Continuar leyendo <i class="bi bi-chevron-right ms-1"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('admin_logged_in') && session('admin_role') == 1)
|
||||
<div class="mt-5 pt-5 border-top">
|
||||
<div class="p-5 bg-dark text-white d-flex flex-column flex-md-row justify-content-between align-items-center">
|
||||
<div>
|
||||
<h3 class="fw-bold font-header mb-1 text-uppercase">Modo Editor Activo</h3>
|
||||
<p class="mb-md-0 text-muted">Añadí nueva información al Media Hub de la OnAPB.</p>
|
||||
</div>
|
||||
<a href="{{ route('admin.noticias.create') }}" class="btn-kinetic-primary px-5 py-3">➕ NUEVA NOTICIA</a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hover-primary:hover {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
@@ -0,0 +1,103 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', $noticia->titulo . ' - OnAPB Media Hub')
|
||||
|
||||
@section('content')
|
||||
<article class="news-article" style="background: var(--surface);">
|
||||
<!-- Article Hero: Digital High Impact -->
|
||||
<header class="article-hero position-relative overflow-hidden mb-5" style="min-height: 400px; background: #000;">
|
||||
@if($noticia->imagen)
|
||||
<img src="{{ str_starts_with($noticia->imagen, 'http') ? $noticia->imagen : asset($noticia->imagen) }}"
|
||||
class="w-100 h-100 position-absolute top-0 start-0"
|
||||
style="object-fit: cover; opacity: 0.6; filter: grayscale(30%) contrast(1.1);"
|
||||
alt="{{ $noticia->titulo }}">
|
||||
@endif
|
||||
|
||||
<div class="container position-relative py-5 d-flex flex-column justify-content-end" style="min-height: 400px; z-index: 2;">
|
||||
<div class="col-lg-10 col-xl-8">
|
||||
<div class="mb-4 animate-on-scroll">
|
||||
@if($noticia->categoria)
|
||||
<span class="bg-primary text-white px-3 py-1 fw-bold text-uppercase small tracking-widest d-inline-block mb-3">
|
||||
{{ $noticia->categoria }}
|
||||
</span>
|
||||
@else
|
||||
<span class="bg-primary text-white px-3 py-1 fw-bold text-uppercase small tracking-widest d-inline-block mb-3">
|
||||
Comunicado
|
||||
</span>
|
||||
@endif
|
||||
<div class="text-white opacity-75 small fw-bold text-uppercase tracking-widest mb-2 font-header">
|
||||
<i class="bi bi-calendar3 me-2 text-primary"></i> {{ $noticia->fecha->translatedFormat('d F, Y') }}
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="display-3 fw-bold text-white mb-0 font-header" style="line-height: 1.1; letter-spacing: -2px;">
|
||||
{{ $noticia->titulo }}<span class="text-primary">.</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gradient Overlay -->
|
||||
<div class="position-absolute bottom-0 start-0 w-100 h-100"
|
||||
style="background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.3) 50%, rgba(0,0,0,0.1) 100%);">
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Article Body -->
|
||||
<div class="container pb-5">
|
||||
<div class="row g-5">
|
||||
<!-- Main Content Column -->
|
||||
<div class="col-lg-8 mx-auto">
|
||||
<div class="article-content fs-5 text-dark lh-base font-body mb-5" style="letter-spacing: -0.01em;">
|
||||
{!! nl2br(e($noticia->contenido)) !!}
|
||||
</div>
|
||||
|
||||
<!-- Share & Interaction -->
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center py-5 border-top border-bottom mb-5 gap-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="text-muted fw-bold small text-uppercase tracking-widest">Compartir</span>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="https://www.facebook.com/sharer/sharer.php?u={{ urlencode(url()->current()) }}" target="_blank" class="btn btn-outline-dark btn-sm rounded-circle" style="width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;"><i class="bi bi-facebook"></i></a>
|
||||
<a href="https://twitter.com/intent/tweet?url={{ urlencode(url()->current()) }}&text={{ urlencode($noticia->titulo) }}" target="_blank" class="btn btn-outline-dark btn-sm rounded-circle" style="width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;"><i class="bi bi-twitter-x"></i></a>
|
||||
<a href="https://api.whatsapp.com/send?text={{ urlencode($noticia->titulo . ' ' . url()->current()) }}" target="_blank" class="btn btn-outline-dark btn-sm rounded-circle" style="width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;"><i class="bi bi-whatsapp"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ route('noticias.index') }}" class="btn-kinetic-outline d-inline-flex align-items-center px-4 py-2 small fw-bold text-uppercase tracking-widest text-decoration-none border border-dark">
|
||||
<i class="bi bi-arrow-left me-2"></i> Volver al Media Hub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comments/Engagement (Optional Placeholder) -->
|
||||
<div class="p-5 bg-white shadow-sm text-center mb-5" style="border-top: 8px solid var(--primary);">
|
||||
<i class="bi bi-chat-left-quote text-muted display-4 mb-4 d-block"></i>
|
||||
<h3 class="fw-bold font-header mb-3">Sé parte de la conversación</h3>
|
||||
<p class="text-muted mb-4 mx-auto" style="max-width: 500px;">Sumate a la comunidad OnAPB y compartí tus comentarios sobre esta cobertura en nuestras redes sociales oficiales.</p>
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<a href="https://instagram.com/prensa_apb" target="_blank" class="btn btn-dark px-4 py-2 rounded-0 fw-bold small"><i class="bi bi-instagram me-2"></i> INSTAGRAM</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar (Optional for related content) -->
|
||||
<!-- We keep it empty for now to maintain the clean editorial look, or add related tournaments -->
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.news-article {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.article-content p {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.article-hero {
|
||||
border-bottom: 8px solid var(--primary);
|
||||
}
|
||||
.btn-kinetic-outline:hover {
|
||||
background: var(--primary);
|
||||
color: white !important;
|
||||
border-color: var(--primary) !important;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
@@ -0,0 +1,197 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Mis Notificaciones — OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="container py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0">
|
||||
<i class="bi bi-bell-fill text-warning me-2"></i>
|
||||
Mis Notificaciones
|
||||
@if($totalNoLeidas > 0)
|
||||
<span class="badge bg-danger ms-2">{{ $totalNoLeidas }} nuevas</span>
|
||||
@endif
|
||||
</h2>
|
||||
</h2>
|
||||
<div class="d-flex gap-2">
|
||||
@if($totalNoLeidas > 0)
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm px-3 fw-bold" onclick="marcarTodasLeidas()">
|
||||
<i class="bi bi-check2-all me-1"></i>MARCAR LEÍDAS
|
||||
</button>
|
||||
@endif
|
||||
@if(!$notificaciones->isEmpty())
|
||||
<button type="button" class="btn btn-outline-danger btn-sm px-3 fw-bold" onclick="eliminarTodas()">
|
||||
<i class="bi bi-trash me-1"></i>BORRAR TODAS
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($notificaciones->isEmpty())
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-bell-slash display-1 text-muted"></i>
|
||||
<p class="mt-3 text-muted">No tenés notificaciones todavía.</p>
|
||||
<a href="{{ route('panel.usuario') }}" class="btn btn-primary mt-2">Ir a mi panel</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="list-group shadow-sm">
|
||||
@foreach($notificaciones as $notif)
|
||||
<div class="list-group-item list-group-item-action d-flex gap-3 py-3 {{ !$notif->leida ? 'border-start border-4 border-primary bg-light' : '' }}"
|
||||
id="notif-{{ $notif->id }}">
|
||||
{{-- Ícono según tipo --}}
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
@php
|
||||
$iconos = [
|
||||
'partido' => ['class' => 'bi-calendar-event', 'color' => 'text-primary'],
|
||||
'resultado' => ['class' => 'bi-trophy-fill', 'color' => 'text-warning'],
|
||||
'sistema' => ['class' => 'bi-info-circle-fill','color' => 'text-secondary'],
|
||||
'seguimiento' => ['class' => 'bi-star-fill', 'color' => 'text-success'],
|
||||
];
|
||||
$icono = $iconos[$notif->tipo] ?? ['class' => 'bi-bell-fill', 'color' => 'text-muted'];
|
||||
@endphp
|
||||
<i class="bi {{ $icono['class'] }} {{ $icono['color'] }} fs-4"></i>
|
||||
</div>
|
||||
|
||||
{{-- Contenido --}}
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h6 class="mb-1 {{ !$notif->leida ? 'fw-bold' : '' }}">{{ $notif->titulo }}</h6>
|
||||
<small class="text-muted text-nowrap ms-2">{{ $notif->creada_en->diffForHumans() }}</small>
|
||||
</div>
|
||||
<p class="mb-1 text-secondary small">{{ $notif->mensaje }}</p>
|
||||
<div class="d-flex gap-2 mt-1">
|
||||
@if($notif->url_accion)
|
||||
<a href="{{ $notif->url_accion }}" class="btn btn-sm btn-outline-primary py-0 px-2"
|
||||
onclick="marcarLeida({{ $notif->id }})">
|
||||
Ver detalle <i class="bi bi-arrow-right ms-1"></i>
|
||||
</a>
|
||||
@endif
|
||||
@if(!$notif->leida)
|
||||
<button class="btn btn-sm btn-outline-secondary py-0 px-2"
|
||||
onclick="marcarLeida({{ $notif->id }}, true)">
|
||||
<i class="bi bi-check2 me-1"></i>Marcar leída
|
||||
</button>
|
||||
@endif
|
||||
<button class="btn btn-sm btn-link text-danger p-0 ms-auto"
|
||||
onclick="eliminarNotificacion({{ $notif->id }})" title="Eliminar">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Paginación --}}
|
||||
<div class="mt-4">
|
||||
{{ $notificaciones->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
|
||||
function marcarLeida(id, soloMarcar = false) {
|
||||
fetch(`/notificaciones/${id}/leer`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(r => r.json())
|
||||
.then(data => {
|
||||
const el = document.getElementById('notif-' + id);
|
||||
if (el) {
|
||||
el.classList.remove('border-start', 'border-4', 'border-primary', 'bg-light');
|
||||
const titulo = el.querySelector('h6');
|
||||
if (titulo) titulo.classList.remove('fw-bold');
|
||||
const btnLeida = el.querySelector('button[onclick*="marcarLeida"]');
|
||||
if (btnLeida) btnLeida.remove();
|
||||
}
|
||||
actualizarBadge();
|
||||
});
|
||||
}
|
||||
|
||||
function marcarTodasLeidas() {
|
||||
Swal.fire({
|
||||
title: '¿Marcar todas como leídas?',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#b00000',
|
||||
confirmButtonText: 'Sí, marcar todas',
|
||||
cancelButtonText: 'Cancelar'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
fetch('{{ route("notificaciones.leer.todas") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
||||
}).then(() => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function eliminarNotificacion(id) {
|
||||
Swal.fire({
|
||||
title: '¿Eliminar notificación?',
|
||||
text: 'Esta acción no se puede deshacer.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
confirmButtonText: 'Sí, eliminar',
|
||||
cancelButtonText: 'Cancelar'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
fetch(`/notificaciones/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
||||
}).then(() => {
|
||||
const el = document.getElementById('notif-' + id);
|
||||
if (el) el.remove();
|
||||
actualizarBadge();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function eliminarTodas() {
|
||||
Swal.fire({
|
||||
title: '¿Eliminar TODO el historial?',
|
||||
text: 'Se borrarán todas tus notificaciones permanentemente.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
confirmButtonText: 'Sí, borrar todo',
|
||||
cancelButtonText: 'Cancelar'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
fetch('{{ route("notificaciones.eliminar.todas") }}', {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
||||
}).then(() => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function actualizarBadge() {
|
||||
fetch('/notificaciones/count')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const badge = document.getElementById('notif-badge');
|
||||
if (!badge) return;
|
||||
if (data.count > 0) {
|
||||
badge.textContent = data.count > 99 ? '99+' : data.count;
|
||||
badge.classList.remove('d-none');
|
||||
} else {
|
||||
badge.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,27 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Sin conexión — OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex align-items-center justify-content-center" style="min-height: 70vh;">
|
||||
<div class="text-center px-4">
|
||||
<div class="mb-4" style="font-size: 5rem;">🏀</div>
|
||||
<h1 class="fw-bold display-5">Sin conexión</h1>
|
||||
<p class="text-muted mt-3 mb-4 fs-5">
|
||||
Parece que no tenés internet en este momento.<br>
|
||||
Algunas páginas pueden estar disponibles en caché.
|
||||
</p>
|
||||
<div class="d-flex gap-3 justify-content-center flex-wrap">
|
||||
<button onclick="window.location.reload()" class="btn btn-primary btn-lg px-5">
|
||||
<i class="bi bi-arrow-clockwise me-2"></i>Reintentar
|
||||
</button>
|
||||
<a href="/" class="btn btn-outline-secondary btn-lg px-5">
|
||||
<i class="bi bi-house me-2"></i>Inicio
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-muted small mt-4">
|
||||
Si venías a ver resultados o tu QR, probá volviendo cuando tengas conexión.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,323 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Mi Panel - OnAPB')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="container-fluid py-5" style="background: var(--surface);">
|
||||
<div class="container py-4">
|
||||
<div class="mb-5 d-flex justify-content-between align-items-end">
|
||||
<div>
|
||||
<span class="text-primary fw-bold tracking-widest text-uppercase d-block mb-2">Mi Perfil</span>
|
||||
<h1 class="display-1 fw-bold mb-0" style="line-height: 0.9;">{{ $user->nombre }}<span class="text-primary">.</span></h1>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="badge bg-dark px-3 py-2 text-uppercase">{{ $userTipo }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('panel_msg'))
|
||||
<div class="bg-white p-4 mb-4 border-start border-success border-5 shadow-sm">
|
||||
<p class="mb-0 fw-bold text-success"><i class="bi bi-check-circle-fill me-2"></i> {{ session('panel_msg') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Columna Izquierda: Datos y Config -->
|
||||
<div class="col-lg-4">
|
||||
<div class="kinetic-card p-4 mb-4">
|
||||
<h3 class="fw-bold mb-4 text-uppercase border-bottom pb-2">Datos Personales</h3>
|
||||
<form method="POST" action="{{ route('panel.actualizar') }}">
|
||||
@csrf
|
||||
<div class="mb-3">
|
||||
<label class="small fw-bold text-muted text-uppercase d-block mb-1">Nombre Completo</label>
|
||||
<div class="fs-5 fw-bold">{{ $user->nombre }} {{ $user->apellido }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="small fw-bold text-muted text-uppercase d-block mb-1">DNI / Documento</label>
|
||||
<div class="fs-5">{{ $userTipo === 'jugador' ? $user->documento : $user->dni }}</div>
|
||||
</div>
|
||||
@if($userTipo === 'jugador')
|
||||
<div class="mb-3">
|
||||
<label class="small fw-bold text-muted text-uppercase d-block mb-1">Categoría</label>
|
||||
<div class="fs-5 fw-bold text-primary">{{ $user->categoria_calculada }}</div>
|
||||
</div>
|
||||
@endif
|
||||
<hr class="my-4">
|
||||
<div class="mb-3">
|
||||
<label class="small fw-bold text-muted text-uppercase d-block mb-1">Email</label>
|
||||
<input type="email" name="email" class="form-control border-0 bg-light p-3" value="{{ $user->email }}" required style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="small fw-bold text-muted text-uppercase d-block mb-1">Teléfono</label>
|
||||
<input type="text" name="telefono" class="form-control border-0 bg-light p-3" value="{{ $user->telefono }}" required style="border-radius: 0;">
|
||||
</div>
|
||||
<button type="submit" class="btn-kinetic-primary w-100 py-3 mt-2">ACTUALIZAR DATOS</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="kinetic-card p-4">
|
||||
<h3 class="fw-bold mb-4 text-uppercase border-bottom pb-2">Seguridad</h3>
|
||||
<form method="POST" action="{{ route('panel.password') }}">
|
||||
@csrf
|
||||
<div class="mb-3">
|
||||
<input type="password" name="password_actual" class="form-control border-0 bg-light p-3" placeholder="Contraseña actual" required style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="password" name="password_nueva" class="form-control border-0 bg-light p-3" placeholder="Nueva contraseña" required style="border-radius: 0;">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-dark w-100 py-3" style="border-radius: 0;">CAMBIAR CONTRASEÑA</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Columna Derecha: Actividad y QRs -->
|
||||
<div class="col-lg-8">
|
||||
@if($userTipo === 'jugador')
|
||||
<div class="kinetic-card p-4 mb-4" style="background: var(--primary-container);">
|
||||
<h3 class="fw-bold mb-4 text-uppercase">Mi Club</h3>
|
||||
<div class="d-flex align-items-center bg-white p-4 shadow-sm">
|
||||
<div class="me-4" style="width: 80px; height: 80px;">
|
||||
@if($user->clubActual && $user->clubActual->imagen)
|
||||
<img src="{{ asset($user->clubActual->imagen) }}" alt="{{ $user->clubActual->nombre }}" class="w-100 h-100" style="object-fit: contain;">
|
||||
@else
|
||||
<div class="w-100 h-100 bg-primary d-flex align-items-center justify-content-center text-white">
|
||||
<i class="bi bi-shield-fill fs-1"></i>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="fw-bold mb-0">{{ $user->clubActual->nombre ?? 'Sin Club Asignado' }}</h2>
|
||||
@if($user->equipos && $user->equipos->count() > 0)
|
||||
<p class="text-muted mb-0">{{ $user->equipos->count() }} equipos registrados</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Mis Entradas -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 class="fw-bold mb-0 text-uppercase">Mis Entradas (QRs)</h3>
|
||||
<a href="{{ route('panel.mis.qrs') }}" class="text-primary fw-bold text-decoration-none">VER TODAS <i class="bi bi-arrow-right"></i></a>
|
||||
</div>
|
||||
|
||||
@if($qrCodes->isEmpty())
|
||||
<div class="bg-white p-5 text-center shadow-sm border-dashed">
|
||||
<p class="text-muted mb-0">No tenés entradas activas.</p>
|
||||
<a href="{{ route('eventos.index') }}" class="btn-kinetic-primary mt-3 px-4">BUSCAR EVENTOS</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="row g-3">
|
||||
@foreach($qrCodes->take(4) as $qr)
|
||||
<div class="col-md-6">
|
||||
<div class="bg-white p-4 shadow-sm border-start border-primary border-5 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h5 class="fw-bold mb-1">{{ $qr->evento ? $qr->evento->nombre_evento : 'Evento' }}</h5>
|
||||
<span class="badge {{ $qr->escaneos_restantes > 0 ? 'bg-success' : 'bg-danger' }} text-uppercase">
|
||||
{{ $qr->escaneos_restantes > 0 ? 'Disponible' : 'Usado' }}
|
||||
</span>
|
||||
</div>
|
||||
<a href="{{ route('panel.mis.qrs', ['evento' => $qr->id_evento]) }}" class="btn btn-outline-dark btn-sm" style="border-radius: 0;">VER QR</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Mis Beneficios -->
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 class="fw-bold mb-0 text-uppercase">Mis Beneficios</h3>
|
||||
<a href="{{ route('promos.index') }}" class="text-primary fw-bold text-decoration-none">BUSCAR MÁS <i class="bi bi-arrow-right"></i></a>
|
||||
</div>
|
||||
|
||||
@if($promoQrs->isEmpty())
|
||||
<div class="bg-white p-5 text-center shadow-sm border-dashed">
|
||||
<p class="text-muted mb-0">No reclamaste beneficios aún.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive bg-white shadow-sm p-3">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="border-0 text-uppercase small fw-bold">Promoción</th>
|
||||
<th class="border-0 text-uppercase small fw-bold text-center">Estado</th>
|
||||
<th class="border-0 text-uppercase small fw-bold text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($promoQrs->take(5) as $pq)
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold">{{ $pq->promocion ? $pq->promocion->nombre : 'Beneficio' }}</div>
|
||||
<div class="small text-muted">{{ $pq->promocion->direccion ?? '' }}</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge {{ $pq->usado ? 'bg-light text-muted' : 'bg-primary' }} text-uppercase">
|
||||
{{ $pq->usado ? 'Usado' : 'Disponible' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
@if(!$pq->usado)
|
||||
<a href="{{ route('panel.promo.qr.ver', $pq->id_qr) }}" class="btn btn-link text-primary p-0">Ver QR</a>
|
||||
@else
|
||||
<span class="text-muted small">—</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ═══════════════════════════════════════════ --}}
|
||||
{{-- SECCIÓN: MIS EQUIPOS SEGUIDOS --}}
|
||||
{{-- ═══════════════════════════════════════════ --}}
|
||||
<div class="mt-5" id="mis-equipos-seguidos">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold text-uppercase mb-0">
|
||||
<i class="bi bi-star-fill text-warning me-2"></i>Equipos Seguidos
|
||||
</h2>
|
||||
<a href="{{ route('eventos.index') }}" class="text-primary fw-bold text-decoration-none small">
|
||||
Ver todos los partidos <i class="bi bi-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="equipos-seguidos-container">
|
||||
<div class="text-center py-4 text-muted">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div> Cargando...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Si es jugador, mostrar sus equipos propios --}}
|
||||
@if($userTipo === 'jugador' && $user->equipos && $user->equipos->count() > 0)
|
||||
<div class="mt-4">
|
||||
<h5 class="fw-bold text-muted text-uppercase small border-bottom pb-2 mb-3">Mis Equipos (asignados)</h5>
|
||||
<div class="row g-3">
|
||||
@foreach($user->equipos as $eq)
|
||||
<div class="col-md-4">
|
||||
<div class="bg-white p-3 shadow-sm d-flex align-items-center gap-3">
|
||||
<div class="bg-primary bg-opacity-10 rounded p-2">
|
||||
<i class="bi bi-shield-fill text-primary fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold">{{ $eq->club->nombre ?? '?' }}</div>
|
||||
<div class="small text-muted">{{ $eq->categoria }} {{ $eq->division }}</div>
|
||||
</div>
|
||||
<a href="{{ route('equipos.show', $eq->id_equipo) }}" class="ms-auto btn btn-sm btn-outline-primary">Ver</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Buscar más equipos para seguir --}}
|
||||
<div class="text-center mt-4">
|
||||
<p class="text-muted small">¿Querés seguir más equipos?</p>
|
||||
<a href="{{ route('torneos.standings', ['id' => optional(\App\Models\Torneo::latest('fecha_inicio')->first())->id ?? 0]) }}" class="btn btn-outline-dark btn-sm">
|
||||
<i class="bi bi-trophy me-1"></i> Ver torneos y equipos
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
// Cargar equipos seguidos vía AJAX
|
||||
fetch('/seguimiento/mis-equipos')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const container = document.getElementById('equipos-seguidos-container');
|
||||
if (!container) return;
|
||||
|
||||
if (!data.equipos || data.equipos.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="bg-white p-4 text-center shadow-sm">
|
||||
<i class="bi bi-star display-4 text-muted opacity-50"></i>
|
||||
<p class="mt-2 text-muted">No seguís ningún equipo todavía.</p>
|
||||
<p class="text-muted small">Andá a la página de un equipo o torneo y presioná "Seguir".</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Próximos partidos
|
||||
let partidosHtml = '';
|
||||
if (data.proximos_partidos && data.proximos_partidos.length > 0) {
|
||||
partidosHtml = `<h6 class="text-uppercase fw-bold text-muted mt-4 mb-3 small">Próximos partidos</h6><div class="row g-3">`;
|
||||
data.proximos_partidos.forEach(p => {
|
||||
partidosHtml += `
|
||||
<div class="col-md-6">
|
||||
<div class="bg-white p-3 shadow-sm border-start border-3 border-primary">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="fw-bold">${p.local} <span class="text-muted">vs</span> ${p.visitante}</div>
|
||||
<div class="small text-muted mt-1">
|
||||
<i class="bi bi-calendar3 me-1"></i>${p.fecha}
|
||||
${p.hora ? '<i class="bi bi-clock ms-2 me-1"></i>' + p.hora : ''}
|
||||
${p.sede ? '<i class="bi bi-geo-alt ms-2 me-1"></i>' + p.sede : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
partidosHtml += `</div>`;
|
||||
} else {
|
||||
partidosHtml = `<p class="text-muted small mt-3"><i class="bi bi-calendar-x me-1"></i>No hay partidos próximos para tus equipos seguidos.</p>`;
|
||||
}
|
||||
|
||||
// Cards de equipos seguidos
|
||||
let equiposHtml = `<div class="row g-3">`;
|
||||
data.equipos.forEach(eq => {
|
||||
equiposHtml += `
|
||||
<div class="col-md-4 col-6">
|
||||
<div class="bg-white p-3 shadow-sm d-flex align-items-center justify-content-between gap-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-star-fill text-warning"></i>
|
||||
<div>
|
||||
<div class="fw-bold small">${eq.club}</div>
|
||||
<div class="text-muted" style="font-size:0.75rem;">${eq.categoria}${eq.division ? ' ' + eq.division : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-danger border-0 p-1 dejar-seguir"
|
||||
data-id="${eq.id}" data-csrf="{{ csrf_token() }}"
|
||||
title="Dejar de seguir">
|
||||
<i class="bi bi-x-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
equiposHtml += `</div>`;
|
||||
|
||||
container.innerHTML = equiposHtml + partidosHtml;
|
||||
|
||||
// Botones "Dejar de seguir"
|
||||
document.querySelectorAll('.dejar-seguir').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const id = this.dataset.id;
|
||||
fetch(`/seguimiento/equipo/${id}`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': this.dataset.csrf, 'Content-Type': 'application/json' }
|
||||
}).then(r => r.json()).then(data => {
|
||||
if (data.ok) this.closest('.col-md-4, .col-6').remove();
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
const c = document.getElementById('equipos-seguidos-container');
|
||||
if (c) c.innerHTML = '<p class="text-muted small">No se pudieron cargar los equipos.</p>';
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Mis QRs - OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="container mt-4 mb-5">
|
||||
<div class="d-flex justify-content-between align-items-end mb-5">
|
||||
<div>
|
||||
<span class="text-primary fw-bold text-uppercase small d-block mb-1">Mi Panel</span>
|
||||
<h1 class="display-3 fw-bold mb-0" style="line-height:0.9;">Mis QRs<span class="text-primary">.</span></h1>
|
||||
@if($evento)<p class="text-muted mt-2 mb-0">{{ $evento->nombre_evento }}</p>@endif
|
||||
</div>
|
||||
<a href="{{ route('panel.usuario') }}" class="btn btn-outline-dark btn-sm text-uppercase fw-bold" style="border-radius:0;">
|
||||
<i class="bi bi-arrow-left me-1"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@if($evento)
|
||||
<div class="kinetic-card p-4 mb-4 border-start border-primary border-4">
|
||||
<h5 class="mb-1 fw-bold">{{ $evento->nombre_evento }}</h5>
|
||||
<p class="text-muted mb-0 small">
|
||||
<i class="bi bi-calendar3 me-1"></i>{{ $evento->fecha_evento ? \Carbon\Carbon::parse($evento->fecha_evento)->format('d/m/Y') : '—' }}
|
||||
·
|
||||
<i class="bi bi-clock me-1"></i>{{ $evento->hora_inicio ? substr($evento->hora_inicio, 0, 5) : '' }} - {{ $evento->hora_fin ? substr($evento->hora_fin, 0, 5) : '' }}
|
||||
@if($evento->sede)
|
||||
· <i class="bi bi-geo-alt me-1"></i>{{ $evento->sede }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($qrs->isEmpty())
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-ticket-perforated text-muted" style="font-size: 3rem;"></i>
|
||||
<p class="text-muted mt-3">No tenés QRs{{ $evento ? ' para este evento' : '' }}.</p>
|
||||
<a href="{{ route('eventos.index') }}" class="btn btn-primary">Ver eventos disponibles</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="row">
|
||||
@foreach($qrs as $qr)
|
||||
@php
|
||||
$agotado = (int)$qr->escaneos_restantes <= 0;
|
||||
$club = ($qr->evento && $qr->evento->equipoLocal && $qr->evento->equipoLocal->club) ? $qr->evento->equipoLocal->club : null;
|
||||
$bgPath = ($club && $club->qr_background) ? asset($club->qr_background) : null;
|
||||
$textColor = ($club && $club->qr_color_texto) ? $club->qr_color_texto : '#333';
|
||||
@endphp
|
||||
<div class="col-lg-4 col-md-6 mb-4">
|
||||
<div class="qr-ticket-card {{ $agotado ? 'qr-agotado' : '' }}"
|
||||
style="border: 1px solid var(--outline-variant); border-radius: var(--radius); overflow: hidden; position: relative; min-height: 480px;
|
||||
{{ $bgPath ? "background: url('$bgPath') no-repeat center center; background-size: cover;" : "background: var(--surface-container-lowest);" }}">
|
||||
|
||||
{{-- Overlay semi-transparente si hay fondo para legibilidad --}}
|
||||
@if($bgPath)
|
||||
<div style="position: absolute; inset: 0; background: rgba(255,255,255,0.1); z-index: 1;"></div>
|
||||
@endif
|
||||
|
||||
<div class="card-body d-flex flex-column align-items-center justify-content-between text-center" style="position: relative; z-index: 5; color: {{ $textColor }};">
|
||||
|
||||
{{-- Logo Cabecera --}}
|
||||
<div class="mt-2 d-flex align-items-center justify-content-center gap-2">
|
||||
<img src="{{ asset('logo.png') }}" alt="OnAPB" style="height: 35px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));">
|
||||
@if($userTipo === 'jugador' && $user->clubActual && $user->clubActual->imagen)
|
||||
<div style="height: 35px; width: 35px; background: #fff; border-radius: 50%; padding: 3px; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<img src="{{ asset($user->clubActual->imagen) }}" alt="{{ $user->clubActual->nombre }}" style="max-height: 100%; max-width: 100%; object-fit: contain;">
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Datos Jugador / Evento --}}
|
||||
<div class="my-2">
|
||||
<h4 class="fw-bold mb-0" style="text-transform: uppercase; letter-spacing: 1px;">
|
||||
{{ $user->nombre }} {{ $user->apellido }}
|
||||
</h4>
|
||||
<p class="small mb-1 opacity-75">{{ $qr->evento->nombre_evento ?? 'Partido - OnAPB' }}</p>
|
||||
@if($qr->evento && $qr->evento->equipoLocal)
|
||||
<p class="small mb-1 fw-bold opacity-75" style="font-size: 0.75rem;">
|
||||
CATEGORIA: {{ $qr->evento->grupo_nombre ?? ($qr->evento->equipoLocal->categoria . ($qr->evento->equipoLocal->division ? ' ' . $qr->evento->equipoLocal->division : '')) }}
|
||||
</p>
|
||||
@endif
|
||||
@if($qr->tipo_qr === 'libre_50')
|
||||
<span class="badge bg-warning text-dark px-3 py-2 rounded-pill shadow-sm">
|
||||
<i class="bi bi-percent"></i> 50% DESCUENTO
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- QR visual --}}
|
||||
<div class="qr-container p-3 bg-white shadow-lg" style="border-radius: var(--radius); position: relative;">
|
||||
@if($agotado)
|
||||
<div style="position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 10; background: rgba(255,255,255,0.8); border-radius: var(--radius);">
|
||||
<span style="font-size: 5rem; transform: rotate(-15deg);">🚫</span>
|
||||
</div>
|
||||
@endif
|
||||
<img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={{ urlencode($qr->id_qr) }}"
|
||||
alt="QR {{ $qr->id_qr }}"
|
||||
class="img-fluid"
|
||||
style="max-width: 180px; {{ $agotado ? 'filter: grayscale(100%);' : '' }}">
|
||||
</div>
|
||||
|
||||
{{-- Footer Ticket --}}
|
||||
<div class="w-100 mt-3 p-2 rounded" style="background: rgba(0,0,0,0.05); backdrop-filter: blur(5px);">
|
||||
@if($agotado)
|
||||
<div class="text-danger fw-bold">❌ QR YA UTILIZADO</div>
|
||||
@else
|
||||
<div class="fw-bold mb-1">✅ QR VÁLIDO</div>
|
||||
<div class="small opacity-75">Escaneos: {{ $qr->escaneos_restantes }} de 1</div>
|
||||
@endif
|
||||
@if($qr->evento && $qr->evento->equipoLocal)
|
||||
<p class="small mb-0 opacity-75" style="font-size: 0.7rem; font-weight: bold;">
|
||||
CATEGORIA: {{ $qr->evento->grupo_nombre ?? ($qr->evento->equipoLocal->categoria . ($qr->evento->equipoLocal->division ? ' ' . $qr->evento->equipoLocal->division : '')) }}
|
||||
</p>
|
||||
@endif
|
||||
<div class="mt-1 small" style="font-size: 0.7rem; font-family: monospace;">{{ $qr->id_qr }}</div>
|
||||
|
||||
@if(!$agotado)
|
||||
<div class="mt-2 pt-2 border-top">
|
||||
<a href="{{ route('panel.qr.descargar', $qr->id_qr) }}" class="btn btn-outline-dark btn-sm w-100" style="font-size: 0.75rem; font-weight: bold; border-radius: 0;">
|
||||
<i class="bi bi-file-earmark-pdf me-1"></i> DESCARGAR PDF
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-3">
|
||||
<i class="bi bi-info-circle-fill"></i>
|
||||
Estos QRs ya están registrados en el sistema. Mostralos en la entrada para ingresar al partido.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,83 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'QR de Beneficio - OnAPB')
|
||||
|
||||
@section('content')
|
||||
<div class="container mt-4 mb-5">
|
||||
<div class="text-center">
|
||||
<h2 class="mb-3">🎟️ QR de beneficio</h2>
|
||||
|
||||
@if(session('panel_msg'))
|
||||
<div class="alert alert-success alert-dismissible fade show mx-auto" style="max-width: 450px;" role="alert">
|
||||
{{ session('panel_msg') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$club = ($userTipo === 'jugador' && $user->clubActual) ? $user->clubActual : null;
|
||||
$bgPath = ($club && $club->qr_background) ? asset($club->qr_background) : null;
|
||||
$textColor = ($club && $club->qr_color_texto) ? $club->qr_color_texto : '#333';
|
||||
@endphp
|
||||
|
||||
<div class="qr-ticket-container my-4 mx-auto"
|
||||
style="max-width: 400px; border-radius: 25px; overflow: hidden; position: relative; min-height: 550px; box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
{{ $bgPath ? "background: url('$bgPath') no-repeat center center; background-size: cover;" : "background: #fff; border: 1px solid #eee;" }}">
|
||||
|
||||
@if($bgPath)
|
||||
<div style="position: absolute; inset: 0; background: rgba(255,255,255,0.1); z-index: 1;"></div>
|
||||
@endif
|
||||
|
||||
<div class="d-flex flex-column align-items-center justify-content-between p-4 h-100 text-center" style="position: relative; z-index: 5; color: {{ $textColor }}; min-height: 550px;">
|
||||
|
||||
<div class="mt-2 d-flex align-items-center justify-content-center gap-2">
|
||||
<img src="{{ asset('logo.png') }}" alt="OnAPB" style="height: 40px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));">
|
||||
@if($userTipo === 'jugador' && $user->clubActual && $user->clubActual->imagen)
|
||||
<div style="height: 40px; width: 40px; background: #fff; border-radius: 50%; padding: 4px; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<img src="{{ asset($user->clubActual->imagen) }}" alt="{{ $user->clubActual->nombre }}" style="max-height: 100%; max-width: 100%; object-fit: contain;">
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<h5 class="mt-3 fw-bold" style="font-family: 'Bebas Neue', cursive; letter-spacing: 1px; font-size: 1.5rem;">CÓDIGO DE BENEFICIO</h5>
|
||||
|
||||
<div class="qr-box p-3 bg-white shadow-lg" style="border-radius: 25px;">
|
||||
<img src="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data={{ urlencode($promoQr->id_qr) }}"
|
||||
alt="QR Promoción"
|
||||
class="img-fluid"
|
||||
style="max-width: 250px;">
|
||||
</div>
|
||||
|
||||
<div class="w-100 mt-4">
|
||||
<h6 class="fw-bold mb-1" style="text-transform: uppercase;">{{ $user->nombre }} {{ $user->apellido }}</h6>
|
||||
<p class="small mb-2 opacity-75">Válido en: {{ $promoQr->promocion->nombre ?? 'Comercio adherido' }}</p>
|
||||
|
||||
<div class="alert alert-warning py-2 px-3 mb-0" style="font-size: 0.75rem; border-radius: 15px; border: none; background: rgba(255, 193, 7, 0.9);">
|
||||
<strong>⚠️ IMPORTANTE:</strong> Presentalo únicamente en el local. Si se escanea por error, se pierde.
|
||||
</div>
|
||||
<div class="mt-2 small opacity-50" style="font-family: monospace; font-size: 0.65rem;">{{ $promoQr->id_qr }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($promoQr->promocion)
|
||||
<div class="card shadow-sm mx-auto" style="max-width: 450px;">
|
||||
<div class="card-body">
|
||||
<h5>{{ $promoQr->promocion->nombre }}</h5>
|
||||
@if($promoQr->promocion->descripcion)
|
||||
<p class="mb-1"><strong>Beneficio:</strong> {{ $promoQr->promocion->descripcion }}</p>
|
||||
@endif
|
||||
@if($promoQr->promocion->direccion)
|
||||
<p class="mb-0 text-muted"><i class="bi bi-geo-alt"></i> {{ $promoQr->promocion->direccion }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{{ route('panel.usuario') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Volver a Mi Panel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,145 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
@page {
|
||||
margin: 0;
|
||||
size: 320pt 500pt;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Helvetica', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 320pt;
|
||||
height: 500pt;
|
||||
color: {{ $club->qr_color_texto ?? '#333' }};
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
}
|
||||
.ticket-container {
|
||||
width: 320pt;
|
||||
height: 500pt;
|
||||
position: relative;
|
||||
margin: 0; padding: 0;
|
||||
}
|
||||
.background-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 320pt;
|
||||
height: 500pt;
|
||||
z-index: 1;
|
||||
}
|
||||
/* Tabla para centrado total */
|
||||
.main-table {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 320pt;
|
||||
height: 500pt;
|
||||
z-index: 10;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.content-td {
|
||||
padding: 30pt 20pt;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
.header { margin-bottom: 20pt; }
|
||||
.logo { height: 40pt; margin-bottom: 5pt; }
|
||||
.title { font-size: 16pt; font-weight: bold; text-transform: uppercase; letter-spacing: 2pt; margin-top: 5pt; }
|
||||
|
||||
.qr-box {
|
||||
margin: 15pt auto;
|
||||
padding: 8pt;
|
||||
background: #fff;
|
||||
display: inline-block;
|
||||
border-radius: 12pt;
|
||||
box-shadow: 0 4pt 10pt rgba(0,0,0,0.15);
|
||||
}
|
||||
.qr-image { width: 145pt; height: 145pt; display: block; border: 1pt solid #eee; }
|
||||
|
||||
.user-info { margin: 15pt 0; }
|
||||
.user-name { font-size: 19pt; font-weight: bold; text-transform: uppercase; margin-bottom: 5pt; }
|
||||
.event-details { font-size: 10pt; opacity: 1; line-height: 1.5; font-weight: bold; }
|
||||
|
||||
.footer-info {
|
||||
position: absolute;
|
||||
bottom: 25pt;
|
||||
left: 0;
|
||||
right: 0;
|
||||
font-size: 8pt;
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
z-index: 20;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4pt 12pt;
|
||||
background: #ffc107;
|
||||
color: #000;
|
||||
border-radius: 15pt;
|
||||
font-size: 9pt;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15pt;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ticket-container">
|
||||
@if($backgroundBase64)
|
||||
<img src="{{ $backgroundBase64 }}" class="background-image">
|
||||
@endif
|
||||
|
||||
<table class="main-table">
|
||||
<tr>
|
||||
<td class="content-td">
|
||||
<div class="header">
|
||||
@if($logoBase64)
|
||||
<img src="{{ $logoBase64 }}" class="logo">
|
||||
@else
|
||||
<div style="height: 40pt;"></div>
|
||||
@endif
|
||||
<div class="title">ENTRADA DIGITAL</div>
|
||||
</div>
|
||||
|
||||
<div class="qr-box">
|
||||
@if($qrImageBase64)
|
||||
<img src="{{ $qrImageBase64 }}" class="qr-image">
|
||||
@else
|
||||
<div style="width: 145pt; height: 145pt; padding: 20pt; font-size: 9pt; color: #999;">Error QR</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="user-info">
|
||||
@if($qr->tipo_qr === 'libre_50')
|
||||
<div class="badge">50% DESCUENTO - CAT. LIBRE</div>
|
||||
@endif
|
||||
|
||||
<div class="user-name">{{ $user->nombre }} {{ $user->apellido }}</div>
|
||||
|
||||
<div class="event-details">
|
||||
<div style="margin-bottom: 4pt; font-size: 11pt; text-decoration: underline;">
|
||||
{{ $qr->evento->nombre_evento ?? 'EVENTO ONAPB' }}
|
||||
</div>
|
||||
FECHA: {{ $qr->evento->fecha_evento ? $qr->evento->fecha_evento->format('d/m/Y') : '—' }}<br>
|
||||
HORA: {{ $qr->evento->hora_inicio ? $qr->evento->hora_inicio->format('H:i') : '—' }} - {{ $qr->evento->hora_fin ? $qr->evento->hora_fin->format('H:i') : '—' }}<br>
|
||||
@if($qr->evento->sede)SEDE: {{ $qr->evento->sede }}<br>@endif
|
||||
@if($qr->evento && $qr->evento->equipoLocal)
|
||||
CATEGORÍA: {{ $qr->evento->equipoLocal->categoria }} {{ $qr->evento->equipoLocal->division }}<br>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="footer-info">
|
||||
ID TICKET: {{ $qr->id_qr }}<br>
|
||||
Generado por OnAPB Experience - {{ date('d/m/Y H:i') }}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,275 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Lugares - OnAPB')
|
||||
|
||||
@section('styles')
|
||||
<link rel="stylesheet" href="{{ asset('static/lugares.css?v=5') }}">
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid py-5" style="background: var(--surface);">
|
||||
<div class="container py-4">
|
||||
<div class="mb-5">
|
||||
<span class="text-primary fw-bold tracking-widest text-uppercase d-block mb-2">Comunidad</span>
|
||||
<h1 class="display-1 fw-bold mb-3" style="line-height: 0.9;">Lugares de Interés</h1>
|
||||
<div style="width: 150px; height: 10px; background: var(--primary);"></div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Editorial -->
|
||||
<div class="bg-white p-5 mb-5 shadow-sm" style="border-left: 10px solid var(--primary-container);">
|
||||
<div class="row g-4 align-items-end">
|
||||
<div class="col-md-5">
|
||||
<label class="small fw-bold text-uppercase text-muted mb-2 d-block">Búsqueda</label>
|
||||
<input type="text" id="searchFilter" class="form-control form-control-lg border-0 bg-light p-3" placeholder="Club, comercio, etc..." oninput="filterPromos()" style="border-radius: 0;">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="small fw-bold text-uppercase text-muted mb-2 d-block">Categoría</label>
|
||||
<select id="categoryFilter" class="form-select form-select-lg border-0 bg-light p-3" onchange="filterPromos()" style="border-radius: 0;">
|
||||
<option value="">Todas</option>
|
||||
@foreach($categorias as $cat)
|
||||
<option value="{{ strtolower($cat) }}">{{ ucfirst($cat) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button class="btn-kinetic-primary w-100 py-3" onclick="filterPromos()">FILTRAR</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Promo Grid -->
|
||||
<div id="promo-grid" class="row g-4">
|
||||
@forelse($promociones as $p)
|
||||
<div class="col-md-4 promo-card" data-categoria="{{ strtolower($p->categoria ?? '') }}" data-nombre="{{ strtolower($p->nombre) }}">
|
||||
<div class="kinetic-card p-0 h-100 overflow-hidden d-flex flex-column" onclick="showPromoDetail({{ $p->id }})" style="cursor: pointer; border-radius: 0;">
|
||||
@if($p->imagen)
|
||||
<div style="height: 250px; overflow: hidden; position: relative;">
|
||||
<img src="{{ asset('storage/' . $p->imagen) }}" class="w-100 h-100" style="object-fit: cover;" alt="{{ $p->nombre }}">
|
||||
<div class="position-absolute bottom-0 start-0 bg-primary text-white px-3 py-1 small fw-bold text-uppercase">{{ $p->categoria }}</div>
|
||||
</div>
|
||||
@endif
|
||||
<div class="p-4 flex-grow-1">
|
||||
<h4 class="h3 fw-bold mb-3">{{ $p->nombre }}</h4>
|
||||
<p class="text-muted small mb-4">{{ Str::limit($p->descripcion_lugar ?? $p->descripcion, 100) }}</p>
|
||||
|
||||
@if($p->descripcion && trim($p->descripcion))
|
||||
<span class="text-primary small fw-bold"><i class="bi bi-star-fill"></i> BENEFICIO ACTIVO</span>
|
||||
@endif
|
||||
<p class="small text-muted mb-0 mt-3"><i class="bi bi-geo-alt"></i> {{ $p->direccion }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="kinetic-card p-5">
|
||||
<i class="bi bi-geo text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<p class="fs-4 text-muted">No se encontraron lugares.</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<!-- Detail Modal Redux (Inline) -->
|
||||
<div id="promo-detail" class="bg-white shadow-lg p-5 my-5" style="display: none; border: 2px solid var(--primary-container);">
|
||||
<div class="row g-5 align-items-center">
|
||||
<div class="col-md-5">
|
||||
<img id="detail-img" src="" class="w-100 shadow-sm" style="height: 350px; object-fit: cover;" alt="Promo detail">
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div>
|
||||
<span id="detail-categoria" class="badge bg-primary text-uppercase mb-2"></span>
|
||||
<h2 id="detail-nombre" class="display-3 fw-bold mb-0"></h2>
|
||||
</div>
|
||||
<button class="btn-close" onclick="closeDetail()"></button>
|
||||
</div>
|
||||
<p id="detail-descripcion" class="fs-5 text-muted mb-4"></p>
|
||||
<p class="fs-5 mb-4"><i class="bi bi-geo-alt-fill text-primary"></i> <span id="detail-direccion"></span></p>
|
||||
|
||||
<div id="detail-promo" class="p-4 bg-light mb-4 border-start border-primary border-5" style="display: none;">
|
||||
<h5 class="fw-bold text-primary mb-2">Beneficio para Asociados:</h5>
|
||||
<p id="detail-promo-text" class="fs-5 mb-0 fw-bold"></p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-3 mt-4">
|
||||
<div id="detail-actions"></div>
|
||||
<button class="btn btn-dark btn-lg px-4" onclick="ubicarEnMapa()" style="border-radius: 0;">VER MAPA</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('admin_logged_in'))
|
||||
<div class="text-center mt-5">
|
||||
<a href="{{ route('admin.promociones.index') }}" class="btn-kinetic-primary px-5 py-3">ADMINISTRAR LUGARES</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mapa -->
|
||||
<div class="container-fluid mt-4 px-0">
|
||||
<div id="map" style="height: 400px;"></div>
|
||||
</div>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
|
||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.Default.css" />
|
||||
|
||||
<script>
|
||||
const promos = @json($promociones);
|
||||
let map;
|
||||
let markers = {};
|
||||
let markersCluster;
|
||||
let selectedPromoId = null;
|
||||
|
||||
function formatCategoria(c) {
|
||||
if (!c) return "Sin categoría";
|
||||
return c.charAt(0).toUpperCase() + c.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
function getIcon(categoria) {
|
||||
const colors = {
|
||||
'clubes': '#e74c3c',
|
||||
'comida': '#f39c12',
|
||||
'salud': '#27ae60',
|
||||
'vestimenta_y_estilo': '#9b59b6',
|
||||
'calzado_deportivo': '#3498db',
|
||||
'entretenimiento': '#1abc9c',
|
||||
'hospedaje': '#34495e'
|
||||
};
|
||||
const color = colors[categoria?.toLowerCase()] || '#95a5a6';
|
||||
|
||||
return L.divIcon({
|
||||
className: 'custom-marker',
|
||||
html: `<div style="background:${color};width:30px;height:30px;border-radius:50%;border:3px solid white;box-shadow:0 2px 5px rgba(0,0,0,0.3);"></div>`,
|
||||
iconSize: [30, 30],
|
||||
iconAnchor: [15, 15]
|
||||
});
|
||||
}
|
||||
|
||||
function initMap() {
|
||||
map = L.map('map').setView([-31.744, -60.53], 13);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap'
|
||||
}).addTo(map);
|
||||
|
||||
markersCluster = L.markerClusterGroup({
|
||||
disableClusteringAtZoom: 13,
|
||||
spiderfyOnMaxZoom: true,
|
||||
showCoverageOnHover: false,
|
||||
maxClusterRadius: 50
|
||||
});
|
||||
|
||||
promos.forEach(p => {
|
||||
if (p.lat && p.lng) {
|
||||
const marker = L.marker([p.lat, p.lng], { icon: getIcon(p.categoria) });
|
||||
const popupContent = `
|
||||
<strong>${p.nombre}</strong><br>
|
||||
<small>${p.direccion || ''}</small><br>
|
||||
<small>${p.descripcion_lugar || p.descripcion || ''}</small><br>
|
||||
<a href="javascript:void(0)" onclick="event.preventDefault(); showPromoDetail(${p.id});">Ver detalle</a>
|
||||
`;
|
||||
marker.bindPopup(popupContent);
|
||||
markersCluster.addLayer(marker);
|
||||
markers[p.id] = marker;
|
||||
}
|
||||
});
|
||||
|
||||
map.addLayer(markersCluster);
|
||||
}
|
||||
|
||||
function filterPromos() {
|
||||
const search = document.getElementById('searchFilter').value.toLowerCase().trim();
|
||||
const cat = document.getElementById('categoryFilter').value;
|
||||
|
||||
document.querySelectorAll('.promo-card').forEach(card => {
|
||||
const nombre = card.dataset.nombre;
|
||||
const categoria = card.dataset.categoria;
|
||||
|
||||
const matchSearch = !search || nombre.includes(search);
|
||||
const matchCat = !cat || categoria === cat;
|
||||
|
||||
card.style.display = (matchSearch && matchCat) ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function showPromoDetail(id) {
|
||||
const p = promos.find(x => x.id === id);
|
||||
if (!p) return;
|
||||
|
||||
selectedPromoId = id;
|
||||
|
||||
document.getElementById('detail-img').src = p.imagen ? '/storage/' + p.imagen : '';
|
||||
document.getElementById('detail-nombre').textContent = p.nombre;
|
||||
document.getElementById('detail-categoria').innerHTML = '<span class="badge bg-secondary">' + formatCategoria(p.categoria) + '</span>';
|
||||
document.getElementById('detail-descripcion').textContent = p.descripcion_lugar || '';
|
||||
document.getElementById('detail-direccion').textContent = p.direccion || '';
|
||||
|
||||
if (p.descripcion && p.descripcion.trim()) {
|
||||
document.getElementById('detail-promo').style.display = 'block';
|
||||
document.getElementById('detail-promo-text').textContent = p.descripcion;
|
||||
} else {
|
||||
document.getElementById('detail-promo').style.display = 'none';
|
||||
}
|
||||
|
||||
// Mostrar botón solo si tiene beneficio y está logueado
|
||||
const actionsDiv = document.getElementById('detail-actions');
|
||||
@if(session('user_logged_in'))
|
||||
if (p.descripcion && p.descripcion.trim()) {
|
||||
actionsDiv.innerHTML = '<form method="POST" action="{{ route('panel.generar.promo.qr') }}" class="d-inline confirm-submit" data-confirm-text="¿Generar QR de beneficio?" data-confirm-button="Generar QR" data-confirm-icon="question"><input type="hidden" name="_token" value="{{ csrf_token() }}"><input type="hidden" name="id_promo" value="' + id + '"><button type="submit" class="btn btn-success">🎟️ Solicitar beneficio</button></form>';
|
||||
} else {
|
||||
actionsDiv.innerHTML = '';
|
||||
}
|
||||
@else
|
||||
if (p.descripcion && p.descripcion.trim()) {
|
||||
actionsDiv.innerHTML = '<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#loginModal">Iniciá sesión para el beneficio</button>';
|
||||
} else {
|
||||
actionsDiv.innerHTML = '';
|
||||
}
|
||||
@endif
|
||||
|
||||
document.getElementById('promo-detail').style.display = 'block';
|
||||
|
||||
// Scroll hacia el detalle
|
||||
document.getElementById('promo-detail').scrollIntoView({ behavior: 'smooth' });
|
||||
|
||||
// Auto-ubicar en el mapa
|
||||
if (p.lat && p.lng) {
|
||||
map.setView([p.lat, p.lng], 16);
|
||||
if (markers[id]) {
|
||||
markers[id].openPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ubicarEnMapa() {
|
||||
if (selectedPromoId && markers[selectedPromoId]) {
|
||||
const p = promos.find(x => x.id === selectedPromoId);
|
||||
if (p && p.lat && p.lng) {
|
||||
map.setView([p.lat, p.lng], 16);
|
||||
markers[selectedPromoId].openPopup();
|
||||
document.getElementById('map').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
document.getElementById('promo-detail').style.display = 'none';
|
||||
selectedPromoId = null;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initMap();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.custom-marker { background: transparent; border: none; }
|
||||
.promo-clickable { cursor: pointer; transition: transform 0.2s; }
|
||||
.promo-clickable:hover { transform: scale(1.02); }
|
||||
</style>
|
||||
@endsection
|
||||
@@ -0,0 +1,207 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Playoffs - ' . $torneo->nombre)
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid py-5" style="background: var(--surface);">
|
||||
<div class="container py-4">
|
||||
<div class="mb-5">
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="{{ route('noticias.index') }}" class="text-decoration-none text-muted">Media Hub</a></li>
|
||||
<li class="breadcrumb-item active text-primary fw-bold" aria-current="page">{{ $torneo->nombre }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-2">Fase Eliminatoria</span>
|
||||
<h1 class="display-3 fw-bold mb-0" style="line-height: 1;">Brackets de Playoffs<span class="text-primary">.</span></h1>
|
||||
<p class="fs-4 text-muted mt-2">{{ $torneo->nombre }}</p>
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="d-flex border-bottom mb-4 gap-4 overflow-x-auto">
|
||||
<a href="{{ route('torneos.standings', $torneo->id) }}"
|
||||
class="pb-3 text-uppercase fw-bold text-decoration-none {{ Request::routeIs('torneos.standings') ? 'text-primary border-bottom border-primary border-3' : 'text-muted' }}">
|
||||
Posiciones
|
||||
</a>
|
||||
<a href="{{ route('torneos.topScorers', $torneo->id) }}"
|
||||
class="pb-3 text-uppercase fw-bold text-decoration-none {{ Request::routeIs('torneos.topScorers') ? 'text-primary border-bottom border-primary border-3' : 'text-muted' }}">
|
||||
Goleadores
|
||||
</a>
|
||||
<a href="{{ route('torneos.playoffs', $torneo->id) }}"
|
||||
class="pb-3 text-uppercase fw-bold text-decoration-none {{ Request::routeIs('torneos.playoffs') ? 'text-primary border-bottom border-primary border-3' : 'text-muted' }}">
|
||||
Playoffs
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(count($grupos) > 0)
|
||||
<div class="d-flex flex-wrap gap-2 mt-4">
|
||||
@foreach($grupos as $g)
|
||||
<a href="{{ route('torneos.playoffs', [$torneo->id, 'grupo' => $g]) }}"
|
||||
class="btn {{ $selectedGroup == $g ? 'btn-primary' : 'btn-outline-primary' }} btn-sm text-uppercase fw-bold px-4">
|
||||
{{ $g }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($bracket['cuartos']->isEmpty() && $bracket['semis']->isEmpty() && $bracket['final']->isEmpty())
|
||||
<div class="text-center py-5 bg-white border rounded shadow-sm">
|
||||
<i class="bi bi-diagram-3 text-muted display-1 d-block mb-3"></i>
|
||||
<h3 class="fw-bold">Aún no hay llaves generadas</h3>
|
||||
<p class="text-muted">Los playoffs se habilitarán al finalizar la fase regular.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="bracket-container overflow-auto py-4">
|
||||
<div class="d-flex gap-5 align-items-center justify-content-center" style="min-width: 800px;">
|
||||
|
||||
<!-- CUARTOS -->
|
||||
<div class="bracket-column d-flex flex-column gap-4 justify-content-around h-100">
|
||||
<h5 class="text-center text-uppercase small fw-bold text-muted mb-3 border-bottom pb-2">Cuartos de Final</h5>
|
||||
@forelse($bracket['cuartos'] as $nro => $serie)
|
||||
<div class="match-card bg-white shadow-sm border p-3" style="border-radius: var(--radius-sm);" style="width: 250px;">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1 text-muted x-small text-uppercase fw-bold">
|
||||
<span>Serie</span>
|
||||
<span>{{ $serie['wins_local'] }} - {{ $serie['wins_visitante'] }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 {{ $serie['wins_local'] > $serie['wins_visitante'] ? 'fw-bold border-start border-primary border-3 ps-2' : '' }}">
|
||||
<div class="d-flex align-items-center text-truncate">
|
||||
@if($serie['equipo_local'])
|
||||
<img src="{{ $serie['equipo_local']->club->imagen ? asset($serie['equipo_local']->club->imagen) : asset('logo.png') }}"
|
||||
class="me-2 rounded-circle border" style="width: 20px; height: 20px; object-fit: cover;"
|
||||
onerror="this.onerror=null;this.src='{{ asset('logo.png') }}';">
|
||||
<span class="text-truncate">{{ $serie['equipo_local']->club->nombre }}</span>
|
||||
@else
|
||||
<span class="text-muted italic">Por definir</span>
|
||||
@endif
|
||||
</div>
|
||||
<span class="badge bg-light text-dark">{{ $serie['wins_local'] }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center {{ $serie['wins_visitante'] > $serie['wins_local'] ? 'fw-bold border-start border-primary border-3 ps-2' : '' }}">
|
||||
<div class="d-flex align-items-center text-truncate">
|
||||
@if($serie['equipo_visitante'])
|
||||
<img src="{{ $serie['equipo_visitante']->club->imagen ? asset($serie['equipo_visitante']->club->imagen) : asset('logo.png') }}"
|
||||
class="me-2 rounded-circle border" style="width: 20px; height: 20px; object-fit: cover;"
|
||||
onerror="this.onerror=null;this.src='{{ asset('logo.png') }}';">
|
||||
<span class="text-truncate">{{ $serie['equipo_visitante']->club->nombre }}</span>
|
||||
@else
|
||||
<span class="text-muted italic">Por definir</span>
|
||||
@endif
|
||||
</div>
|
||||
<span class="badge bg-light text-dark">{{ $serie['wins_visitante'] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-muted italic py-4">Pendiente</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="bracket-connector d-none d-lg-block"><i class="bi bi-chevron-right text-muted fs-4"></i></div>
|
||||
|
||||
<!-- SEMIS -->
|
||||
<div class="bracket-column d-flex flex-column gap-5 justify-content-around h-100">
|
||||
<h5 class="text-center text-uppercase small fw-bold text-muted mb-3 border-bottom pb-2">Semifinales</h5>
|
||||
@for($i = 1; $i <= 2; $i++)
|
||||
@php $serie = $bracket['semis']->get($i); @endphp
|
||||
<div class="match-card {{ $serie ? 'bg-white shadow-sm border' : 'bg-light border-dashed' }} p-3" style="border-radius: var(--radius-sm);" style="width: 250px; min-height: 80px;">
|
||||
@if($serie)
|
||||
<div class="d-flex justify-content-between align-items-center mb-1 text-muted x-small text-uppercase">
|
||||
<span>Serie</span>
|
||||
<span>{{ $serie['wins_local'] }} - {{ $serie['wins_visitante'] }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 {{ $serie['wins_local'] > $serie['wins_visitante'] ? 'fw-bold text-primary' : '' }}">
|
||||
<div class="d-flex align-items-center text-truncate">
|
||||
@if($serie['equipo_local'])
|
||||
<img src="{{ $serie['equipo_local']->club->imagen ? asset($serie['equipo_local']->club->imagen) : asset('logo.png') }}"
|
||||
class="me-2 rounded-circle border" style="width: 20px; height: 20px; object-fit: cover;"
|
||||
onerror="this.onerror=null;this.src='{{ asset('logo.png') }}';">
|
||||
<span class="text-truncate">{{ $serie['equipo_local']->club->nombre }}</span>
|
||||
@else
|
||||
<span class="text-muted fst-italic">TBD</span>
|
||||
@endif
|
||||
</div>
|
||||
<span class="badge {{ $serie['wins_local'] > $serie['wins_visitante'] ? 'bg-primary' : 'bg-light text-dark' }}">{{ $serie['wins_local'] }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center {{ $serie['wins_visitante'] > $serie['wins_local'] ? 'fw-bold text-primary' : '' }}">
|
||||
<div class="d-flex align-items-center text-truncate">
|
||||
@if($serie['equipo_visitante'])
|
||||
<img src="{{ $serie['equipo_visitante']->club->imagen ? asset($serie['equipo_visitante']->club->imagen) : asset('logo.png') }}"
|
||||
class="me-2 rounded-circle border" style="width: 20px; height: 20px; object-fit: cover;"
|
||||
onerror="this.onerror=null;this.src='{{ asset('logo.png') }}';">
|
||||
<span class="text-truncate">{{ $serie['equipo_visitante']->club->nombre }}</span>
|
||||
@else
|
||||
<span class="text-muted fst-italic">TBD</span>
|
||||
@endif
|
||||
</div>
|
||||
<span class="badge {{ $serie['wins_visitante'] > $serie['wins_local'] ? 'bg-primary' : 'bg-light text-dark' }}">{{ $serie['wins_visitante'] }}</span>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center text-muted py-3 small">TBD</div>
|
||||
@endif
|
||||
</div>
|
||||
@endfor
|
||||
</div>
|
||||
|
||||
<div class="bracket-connector d-none d-lg-block"><i class="bi bi-chevron-right text-muted fs-4"></i></div>
|
||||
|
||||
<!-- FINAL -->
|
||||
<div class="bracket-column d-flex flex-column justify-content-center h-100">
|
||||
<h5 class="text-center text-uppercase small fw-bold text-primary mb-3 border-bottom border-primary pb-2">Gran Final</h5>
|
||||
@php $serie = $bracket['final']->get(1); @endphp
|
||||
<div class="match-card {{ $serie ? 'bg-primary text-white shadow-md' : 'bg-light border-dashed' }} p-4" style="border-radius: var(--radius-sm);" style="width: 310px;">
|
||||
@if($serie)
|
||||
<div class="text-center mb-3 text-uppercase small border-bottom border-white border-opacity-25 pb-1">
|
||||
Serie: {{ $serie['wins_local'] }} - {{ $serie['wins_visitante'] }}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 {{ $serie['wins_local'] > $serie['wins_visitante'] ? 'fs-4 fw-bold' : '' }}">
|
||||
<div class="d-flex align-items-center text-truncate">
|
||||
@if($serie['equipo_local'])
|
||||
<img src="{{ $serie['equipo_local']->club->imagen ? asset($serie['equipo_local']->club->imagen) : asset('logo.png') }}"
|
||||
class="me-2 rounded-circle border border-white border-opacity-50" style="width: 32px; height: 32px; object-fit: cover;"
|
||||
onerror="this.onerror=null;this.src='{{ asset('logo.png') }}';">
|
||||
<span class="text-truncate">{{ $serie['equipo_local']->club->nombre }}</span>
|
||||
@else
|
||||
<span>TBD</span>
|
||||
@endif
|
||||
</div>
|
||||
<span class="badge bg-white text-primary fs-5">{{ $serie['wins_local'] }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center {{ $serie['wins_visitante'] > $serie['wins_local'] ? 'fs-4 fw-bold' : '' }}">
|
||||
<div class="d-flex align-items-center text-truncate">
|
||||
@if($serie['equipo_visitante'])
|
||||
<img src="{{ $serie['equipo_visitante']->club->imagen ? asset($serie['equipo_visitante']->club->imagen) : asset('logo.png') }}"
|
||||
class="me-2 rounded-circle border border-white border-opacity-50" style="width: 32px; height: 32px; object-fit: cover;"
|
||||
onerror="this.onerror=null;this.src='{{ asset('logo.png') }}';">
|
||||
<span class="text-truncate">{{ $serie['equipo_visitante']->club->nombre }}</span>
|
||||
@else
|
||||
<span>TBD</span>
|
||||
@endif
|
||||
</div>
|
||||
<span class="badge bg-white text-primary fs-5">{{ $serie['wins_visitante'] }}</span>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-4 text-muted fst-italic">Por definir</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bracket-container {
|
||||
mask-image: linear-gradient(to right, transparent, black 5%, black 95%, transparent);
|
||||
}
|
||||
.match-card {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.match-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
.border-dashed {
|
||||
border: 2px dashed var(--outline-variant);
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
@@ -0,0 +1,116 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Goleadores - ' . $torneo->nombre)
|
||||
|
||||
@section('content')
|
||||
<div class="news-container py-5">
|
||||
<div class="container">
|
||||
<div class="text-center mb-5">
|
||||
<span class="badge bg-primary px-3 py-2 text-uppercase tracking-widest mb-2">Estadísticas Individuales</span>
|
||||
<h1 class="display-4 fw-bold text-uppercase italic tracking-tighter">TABLA DE <span class="text-primary">GOLEADORES</span></h1>
|
||||
<p class="lead text-muted">{{ $torneo->nombre }}</p>
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="d-flex border-bottom mb-4 gap-4 overflow-x-auto justify-content-center">
|
||||
<a href="{{ route('torneos.standings', $torneo->id) }}"
|
||||
class="pb-3 text-uppercase fw-bold text-decoration-none {{ Request::routeIs('torneos.standings') ? 'text-primary border-bottom border-primary border-3' : 'text-muted' }}">
|
||||
Posiciones
|
||||
</a>
|
||||
<a href="{{ route('torneos.topScorers', $torneo->id) }}"
|
||||
class="pb-3 text-uppercase fw-bold text-decoration-none {{ Request::routeIs('torneos.topScorers') ? 'text-primary border-bottom border-primary border-3' : 'text-muted' }}">
|
||||
Goleadores
|
||||
</a>
|
||||
<a href="{{ route('torneos.playoffs', $torneo->id) }}"
|
||||
class="pb-3 text-uppercase fw-bold text-decoration-none {{ Request::routeIs('torneos.playoffs') ? 'text-primary border-bottom border-primary border-3' : 'text-muted' }}">
|
||||
Playoffs
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(count($grupos) > 0)
|
||||
<div class="d-flex flex-wrap gap-2 mt-4 justify-content-center">
|
||||
<a href="{{ route('torneos.topScorers', $torneo->id) }}"
|
||||
class="btn {{ !$selectedGroup ? 'btn-primary' : 'btn-outline-primary' }} btn-sm text-uppercase fw-bold rounded-pill px-4">
|
||||
Todos los Grupos
|
||||
</a>
|
||||
@foreach($grupos as $g)
|
||||
<a href="{{ route('torneos.topScorers', [$torneo->id, 'grupo' => $g]) }}"
|
||||
class="btn {{ $selectedGroup == $g ? 'btn-primary' : 'btn-outline-primary' }} btn-sm text-uppercase fw-bold rounded-pill px-4">
|
||||
{{ $g }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="kinetic-card overflow-hidden">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="bg-dark text-white text-uppercase small tracking-widest">
|
||||
<tr>
|
||||
<th class="ps-4 py-3" width="60">#</th>
|
||||
<th class="py-3">Jugador</th>
|
||||
<th class="py-3">Club</th>
|
||||
<th class="py-3 text-center" width="80">PJ</th>
|
||||
<th class="py-3 text-center" width="100">PTS</th>
|
||||
<th class="pe-4 py-3 text-center" width="100">PROM</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($scorers as $index => $s)
|
||||
<tr class="{{ $index < 3 ? 'bg-light fw-bold' : '' }}">
|
||||
<td class="ps-4 py-3">
|
||||
@if($index == 0) <span class="fs-4">🥇</span>
|
||||
@elseif($index == 1) <span class="fs-4">🥈</span>
|
||||
@elseif($index == 2) <span class="fs-4">🥉</span>
|
||||
@else <span class="fw-bold text-muted">#{{ $index + 1 }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-3">
|
||||
<div class="text-uppercase tracking-tighter">{{ $s->jugador->apellido }}, {{ $s->jugador->nombre }}</div>
|
||||
</td>
|
||||
<td class="py-3">
|
||||
<span class="small text-muted text-uppercase">{{ $s->jugador->clubActual->nombre ?? 'N/A' }}</span>
|
||||
</td>
|
||||
<td class="py-3 text-center fw-bold">{{ $s->partidos_jugados }}</td>
|
||||
<td class="py-3 text-center">
|
||||
<span class="badge {{ $index < 3 ? 'bg-primary' : 'bg-dark' }} fs-6 px-3">{{ $s->total_puntos }}</span>
|
||||
</td>
|
||||
<td class="pe-4 py-3 text-center text-muted">
|
||||
{{ number_format($s->total_puntos / max(1, $s->partidos_jugados), 1) }}
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="p-5 text-center text-muted italic">
|
||||
No hay estadísticas registradas para este torneo todavía.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4 small text-muted text-uppercase tracking-widest italic">
|
||||
Actualizado el {{ date('d/m/Y H:i') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.news-container {
|
||||
background-color: var(--surface);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(0,0,0,0.02);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
@@ -0,0 +1,166 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Tabla de Posiciones - ' . $torneo->nombre)
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid py-5" style="background: var(--surface);">
|
||||
<div class="container py-4">
|
||||
<div class="mb-5">
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="{{ route('noticias.index') }}" class="text-decoration-none text-muted">Media Hub</a></li>
|
||||
<li class="breadcrumb-item active text-primary fw-bold" aria-current="page">{{ $torneo->nombre }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<span class="text-primary fw-bold text-uppercase tracking-widest d-block mb-2">Competición Oficial</span>
|
||||
<h1 class="display-3 fw-bold mb-0" style="line-height: 1;">Tabla de Posiciones<span class="text-primary">.</span></h1>
|
||||
<p class="fs-4 text-muted mt-2">{{ $torneo->nombre }} ({{ $torneo->fecha_inicio->format('Y') }})</p>
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="d-flex border-bottom mb-4 gap-4 overflow-x-auto">
|
||||
<a href="{{ route('torneos.standings', $torneo->id) }}"
|
||||
class="pb-3 text-uppercase fw-bold text-decoration-none {{ Request::routeIs('torneos.standings') ? 'text-primary border-bottom border-primary border-3' : 'text-muted' }}">
|
||||
Posiciones
|
||||
</a>
|
||||
<a href="{{ route('torneos.topScorers', $torneo->id) }}"
|
||||
class="pb-3 text-uppercase fw-bold text-decoration-none {{ Request::routeIs('torneos.topScorers') ? 'text-primary border-bottom border-primary border-3' : 'text-muted' }}">
|
||||
Goleadores
|
||||
</a>
|
||||
<a href="{{ route('torneos.playoffs', $torneo->id) }}"
|
||||
class="pb-3 text-uppercase fw-bold text-decoration-none {{ Request::routeIs('torneos.playoffs') ? 'text-primary border-bottom border-primary border-3' : 'text-muted' }}">
|
||||
Playoffs
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(count($grupos) > 0)
|
||||
<div class="d-flex flex-wrap gap-2 mt-4">
|
||||
<a href="{{ route('torneos.standings', $torneo->id) }}"
|
||||
class="btn {{ !$selectedGroup ? 'btn-primary' : 'btn-outline-primary' }} btn-sm text-uppercase fw-bold px-4">
|
||||
Todos los Grupos
|
||||
</a>
|
||||
@foreach($grupos as $g)
|
||||
<a href="{{ route('torneos.standings', [$torneo->id, 'grupo' => $g]) }}"
|
||||
class="btn {{ $selectedGroup == $g ? 'btn-primary' : 'btn-outline-primary' }} btn-sm text-uppercase fw-bold px-4">
|
||||
{{ $g }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-xl overflow-hidden" style="border-top: 10px solid var(--primary); border: 1px solid var(--outline-variant);">
|
||||
<div class="table-responsive">
|
||||
@foreach($stats as $groupName => $teams)
|
||||
<div class="group-section mb-5">
|
||||
<h2 class="h4 fw-bold text-uppercase bg-light p-3 border-start border-primary border-4 mb-0">
|
||||
{{ $groupName }}
|
||||
</h2>
|
||||
<table class="table table-hover mb-0 kinetic-standings-table">
|
||||
<thead class="bg-dark text-white text-uppercase tracking-widest small">
|
||||
<tr>
|
||||
<th class="px-4 py-3">#</th>
|
||||
<th class="px-4 py-3">Equipo</th>
|
||||
<th class="px-4 py-3 text-center">PJ</th>
|
||||
<th class="px-4 py-3 text-center">PG</th>
|
||||
<th class="px-4 py-3 text-center">PP</th>
|
||||
<th class="px-4 py-3 text-center">TF</th>
|
||||
<th class="px-4 py-3 text-center">TC</th>
|
||||
<th class="px-4 py-3 text-center">DIF</th>
|
||||
<th class="px-4 py-3 text-center bg-primary">PTS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="fs-5">
|
||||
@foreach($teams as $index => $s)
|
||||
<tr>
|
||||
<td class="px-4 py-4 fw-bold text-muted">{{ $index + 1 }}</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="d-flex align-items-center">
|
||||
@if(session('user_logged_in'))
|
||||
@php $isFollowing = in_array($s['id'], $followedTeamIds); @endphp
|
||||
<button class="btn btn-sm p-0 me-2 follow-star-btn"
|
||||
data-equipo-id="{{ $s['id'] }}"
|
||||
title="{{ $isFollowing ? 'Dejar de seguir' : 'Seguir equipo' }}">
|
||||
<i class="bi {{ $isFollowing ? 'bi-star-fill text-warning' : 'bi-star text-muted' }}" style="font-size: 1.2rem;"></i>
|
||||
</button>
|
||||
@endif
|
||||
<a href="{{ route('equipos.show', $s['id']) }}" class="text-decoration-none hover-primary d-flex align-items-center">
|
||||
<img src="{{ $s['logo'] ? asset($s['logo']) : asset('logo.png') }}"
|
||||
class="me-3 rounded-circle border shadow-sm"
|
||||
style="width: 32px; height: 32px; object-fit: cover;"
|
||||
onerror="this.onerror=null;this.src='{{ asset('logo.png') }}';">
|
||||
<span class="fw-bold font-header text-dark">{{ $s['nombre'] }}</span>
|
||||
</a>
|
||||
{{-- Only show base category if different from group name --}}
|
||||
@if($s['categoria'] != $groupName)
|
||||
<span class="ms-3 badge bg-light text-muted border small">{{ $s['categoria'] }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-center">{{ $s['pj'] }}</td>
|
||||
<td class="px-4 py-4 text-center text-success fw-bold">{{ $s['pg'] }}</td>
|
||||
<td class="px-4 py-4 text-center text-danger fw-bold">{{ $s['pp'] }}</td>
|
||||
<td class="px-4 py-4 text-center text-muted">{{ $s['tf'] }}</td>
|
||||
<td class="px-4 py-4 text-center text-muted">{{ $s['tc'] }}</td>
|
||||
<td class="px-4 py-4 text-center fw-bold {{ ($s['tf'] - $s['tc']) >= 0 ? 'text-dark' : 'text-danger' }}">
|
||||
{{ ($s['tf'] - $s['tc']) > 0 ? '+' : '' }}{{ $s['tf'] - $s['tc'] }}
|
||||
</td>
|
||||
<td class="px-4 py-4 text-center fw-bold bg-primary bg-opacity-10 text-primary" style="font-size: 1.4rem;">
|
||||
{{ $s['pts'] }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@if(empty($stats))
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-info-circle text-muted display-4 d-block mb-3"></i>
|
||||
<p class="text-muted fs-4">Aún no se han registrado resultados en este torneo.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-4 bg-white border shadow-sm" style="border-radius: var(--radius);">
|
||||
<h5 class="fw-bold mb-3 small text-uppercase tracking-widest text-muted">Sistema de Puntuación</h5>
|
||||
<div class="row text-center g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="p-3 border" style="border-radius: var(--radius-sm);">
|
||||
<span class="d-block fs-3 fw-bold text-primary">2 PTS</span>
|
||||
<span class="text-muted small">Victoria</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="p-3 border" style="border-radius: var(--radius-sm);">
|
||||
<span class="d-block fs-3 fw-bold text-dark">1 PT</span>
|
||||
<span class="text-muted small">Derrota</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="p-3 border" style="border-radius: var(--radius-sm);">
|
||||
<span class="d-block fs-3 fw-bold text-danger">0 PTS</span>
|
||||
<span class="text-muted small">W.O. / Incomparecencia</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.kinetic-standings-table tbody tr {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.kinetic-standings-table tbody tr:hover {
|
||||
background-color: var(--primary-container) !important;
|
||||
}
|
||||
.font-header {
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.hover-primary:hover span {
|
||||
color: var(--primary) !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
@@ -0,0 +1,307 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'OnAPB - Inicio')
|
||||
|
||||
@section('styles')
|
||||
<link rel="stylesheet" href="{{ asset('static/index.css') }}">
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div id="main-content">
|
||||
<!-- HERO SECTION: The Editorial Statement -->
|
||||
<div id="heroCarousel" class="carousel slide carousel-fade" data-bs-ride="carousel">
|
||||
<div class="carousel-inner">
|
||||
@foreach($carouselItems as $index => $item)
|
||||
<div class="carousel-item {{ $index === 0 ? 'active' : '' }}">
|
||||
<div class="row g-0 align-items-center" style="min-height: 80vh; background: var(--surface);">
|
||||
<div class="col-md-6 p-5">
|
||||
<div class="p-lg-5">
|
||||
<span class="kfx-live-pill mb-4">ONAPB Exclusivo · En vivo</span>
|
||||
<h1 class="hero-title fw-bold mt-3" style="font-size: clamp(3rem, 10vw, 6rem); transition: all 0.5s ease;">{{ $item->titulo }}</h1>
|
||||
<p class="fs-5 mb-5 text-kinetic-muted kfx-reveal" data-fx="up" style="max-width: 500px;">{{ $item->subtitulo }}</p>
|
||||
@if($item->boton_texto && $item->boton_enlace)
|
||||
<a href="{{ url($item->boton_enlace) }}" class="btn-kinetic-primary text-decoration-none" data-magnetic data-magnetic-strength="0.18" style="padding: 1.2rem 3rem; font-size: 1.1rem; border-radius: 0;">{{ $item->boton_texto }} <span class="ms-2">→</span></a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 position-relative kfx-img-cover">
|
||||
<img src="{{ Str::startsWith($item->imagen, 'http') ? $item->imagen : asset($item->imagen) }}" class="w-100 hero-clip" data-parallax="0.08" style="height: 80vh; object-fit: cover; clip-path: polygon(10% 0, 100% 0, 100% 100%, 0% 100%);" alt="{{ $item->titulo }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NEWS & TOURNAMENTS: The Pulse of ONAPB -->
|
||||
<div class="container-fluid py-5" style="background: var(--surface);">
|
||||
<div class="container py-4">
|
||||
<div class="row g-5">
|
||||
<!-- Latest News Column -->
|
||||
<div class="col-lg-8">
|
||||
<div class="d-flex justify-content-between align-items-end mb-4">
|
||||
<div>
|
||||
<span class="kfx-section-tag">Últimas Noticias</span>
|
||||
<h2 class="display-5 fw-bold mb-0 kinetic-reveal-text">Cobertura Exclusiva<span class="text-primary">.</span></h2>
|
||||
</div>
|
||||
<a href="{{ route('noticias.index') }}" class="kfx-link text-muted small fw-bold">VER TODO <i class="bi bi-arrow-right"></i></a>
|
||||
</div>
|
||||
|
||||
@foreach($noticias as $n)
|
||||
<div class="kinetic-news-item mb-4 pb-4 border-bottom">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-4 col-md-3">
|
||||
<div class="ratio ratio-1x1 overflow-hidden">
|
||||
<img src="{{ str_starts_with($n->imagen, 'http') ? $n->imagen : asset($n->imagen) }}" class="object-fit-cover" alt="{{ $n->titulo }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-8 col-md-9">
|
||||
<span class="badge bg-primary-container text-primary mb-2">{{ $n->categoria ?? 'Media' }}</span>
|
||||
<h3 class="h4 fw-bold mb-2">
|
||||
<a href="{{ route('noticias.show', $n->id) }}" class="text-decoration-none text-dark hover-primary">{{ $n->titulo }}</a>
|
||||
</h3>
|
||||
<p class="text-muted small mb-0 line-clamp-2">{{ Str::limit(strip_tags($n->contenido), 120) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@if($noticias->isEmpty())
|
||||
<p class="text-muted italic">No hay noticias recientes.</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Active Tournaments Column -->
|
||||
<div class="col-lg-4">
|
||||
<div class="p-4 bg-dark text-white h-100 position-relative animate-on-scroll" style="border-left: 8px solid var(--primary); min-height: 540px; display: flex; flex-direction: column;">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="h3 fw-bold mb-0">Tablas <span class="text-primary">Live</span></h2>
|
||||
</div>
|
||||
|
||||
<div id="standingsCarousel" class="carousel slide flex-grow-1" data-bs-ride="carousel">
|
||||
<!-- Custom Indicators -->
|
||||
<div class="carousel-indicators" style="bottom: -40px;">
|
||||
@php $slideIdx = 0; @endphp
|
||||
@foreach($standingsData as $torneoId => $grupos)
|
||||
@foreach($grupos as $nombreGrupo => $equipos)
|
||||
<button type="button" data-bs-target="#standingsCarousel" data-bs-slide-to="{{ $slideIdx }}" class="{{ $slideIdx === 0 ? 'active' : '' }}" aria-current="{{ $slideIdx === 0 ? 'true' : '' }}" aria-label="Slide {{ $slideIdx + 1 }}" style="width: 12px; height: 12px; border-radius: 50%; border: 2px solid var(--primary); background: transparent;"></button>
|
||||
@php $slideIdx++; @endphp
|
||||
@endforeach
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="carousel-inner">
|
||||
@php $isFirst = true; $totalSlides = 0; @endphp
|
||||
@foreach($standingsData as $torneoId => $grupos)
|
||||
@php $torneoObj = $torneos->firstWhere('id', $torneoId); @endphp
|
||||
@foreach($grupos as $nombreGrupo => $equipos)
|
||||
@php $totalSlides++; @endphp
|
||||
<div class="carousel-item {{ $isFirst ? 'active' : '' }}">
|
||||
<div class="standings-header mb-3">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<span class="text-primary fw-bold small text-uppercase" style="letter-spacing: 0.1em;">{{ $torneoObj->nombre }}</span>
|
||||
<h4 class="h5 fw-bold text-white mb-0">{{ $nombreGrupo }}</h4>
|
||||
</div>
|
||||
<a href="{{ route('torneos.topScorers', $torneoId) }}" class="text-kinetic-muted hover-primary" title="Ver Goleadores">
|
||||
<i class="bi bi-person-lines-fill fs-5"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover table-borderless align-middle mb-0" style="font-size: 0.85rem;">
|
||||
<thead>
|
||||
<tr class="text-kinetic-muted border-bottom border-secondary border-opacity-50">
|
||||
<th class="ps-0 py-2" style="width: 30px;">#</th>
|
||||
<th class="py-2">EQUIPO</th>
|
||||
<th class="py-2 text-center">PJ</th>
|
||||
<th class="pe-0 py-2 text-end">PTS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(array_slice($equipos, 0, 8) as $index => $e)
|
||||
<tr class="border-bottom border-secondary border-opacity-10">
|
||||
<td class="ps-0 py-2 fw-bold {{ $index < 2 ? 'text-primary' : '' }}">{{ $index + 1 }}</td>
|
||||
<td class="py-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<img src="{{ $e['logo'] ? asset($e['logo']) : asset('logo.png') }}"
|
||||
class="me-2 rounded-circle border border-secondary border-opacity-25"
|
||||
style="width: 24px; height: 24px; object-fit: cover;"
|
||||
onerror="this.onerror=null;this.src='{{ asset('logo.png') }}';">
|
||||
<a href="{{ route('equipos.show', $e['id']) }}" class="text-decoration-none text-white hover-primary text-truncate d-inline-block" style="max-width: 120px;">
|
||||
{{ $e['nombre'] ?? 'TBD' }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2 text-center">{{ $e['pj'] }}</td>
|
||||
<td class="pe-0 py-2 text-end fw-bold text-primary">{{ $e['pts'] }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-2">
|
||||
<a href="{{ route('torneos.standings', $torneoId) }}" class="btn btn-outline-light btn-sm rounded-0 w-100 py-2 fw-bold" style="letter-spacing: 0.05em;">
|
||||
VER TABLA COMPLETA <i class="bi bi-arrow-right ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@php $isFirst = false; @endphp
|
||||
@endforeach
|
||||
@endforeach
|
||||
|
||||
@if($totalSlides === 0)
|
||||
<div class="carousel-item active text-center py-5">
|
||||
<i class="bi bi-info-circle text-kinetic-muted display-4 mb-3"></i>
|
||||
<p class="text-kinetic-muted">No hay posiciones registradas para este torneo.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($totalSlides > 1)
|
||||
<div class="d-flex justify-content-center gap-3 mt-5">
|
||||
<button class="btn btn-dark btn-sm rounded-circle border-secondary bg-opacity-50" type="button" data-bs-target="#standingsCarousel" data-bs-slide="prev" style="width: 32px; height: 32px; padding: 0;">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</button>
|
||||
<button class="btn btn-dark btn-sm rounded-circle border-secondary bg-opacity-50" type="button" data-bs-target="#standingsCarousel" data-bs-slide="next" style="width: 32px; height: 32px; padding: 0;">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-auto p-3 bg-secondary bg-opacity-10 border border-secondary border-opacity-25 mt-4">
|
||||
<span class="d-block small text-primary fw-bold mb-1">CENTRO DE ESTADÍSTICAS</span>
|
||||
<p class="small text-kinetic-muted mb-0">Datos actualizados en tiempo real según los últimos informes arbitrales.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid py-5" style="background: var(--surface-container-low);">
|
||||
<div class="container py-5">
|
||||
<div class="d-flex justify-content-between align-items-end mb-5">
|
||||
<div>
|
||||
<span class="kfx-section-tag">Agenda</span>
|
||||
<h2 class="display-3 fw-bold mb-0 kinetic-reveal-text">Próximos Partidos</h2>
|
||||
<div class="kfx-divider-sweep" style="width: 140px; margin-top: 1rem; margin-bottom: 0;"></div>
|
||||
</div>
|
||||
<a href="{{ route('eventos.index') }}" class="kfx-link text-muted small fw-bold d-none d-md-inline">VER CALENDARIO COMPLETO <i class="bi bi-arrow-right"></i></a>
|
||||
</div>
|
||||
|
||||
@if(count($eventos) === 0)
|
||||
<div class="kinetic-card text-center p-5">
|
||||
<p class="text-kinetic-muted mb-0 fs-5">No hay eventos activos.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="row g-4" data-stagger>
|
||||
@php
|
||||
$eventosList = is_array($eventos) ? $eventos : $eventos->toArray();
|
||||
@endphp
|
||||
@foreach(array_slice($eventosList, 0, 6) as $e)
|
||||
<div class="col-md-4">
|
||||
<div class="kinetic-card h-100 d-flex flex-column" data-tilt data-tilt-max="5" style="padding: 0; overflow: hidden; border-radius: 0;">
|
||||
<div class="p-4 flex-grow-1">
|
||||
<div class="d-flex justify-content-between mb-4">
|
||||
@if($e['estado'] === 'Activo')
|
||||
<span class="kfx-live-pill" style="font-size: 0.6rem; padding: 4px 10px;">En Vivo</span>
|
||||
@else
|
||||
<span class="fw-bold small text-kinetic-muted text-uppercase tracking-widest">{{ $e['estado'] }}</span>
|
||||
@endif
|
||||
<span class="small text-kinetic-muted">{{ \Carbon\Carbon::parse($e['fecha_evento'])->translatedFormat('d M') }}</span>
|
||||
</div>
|
||||
<h3 class="mb-4 h2">{{ $e['nombre_evento'] ?? ($e['clubLocal']['nombre'] . ' vs ' . $e['clubVisitante']['nombre']) }}</h3>
|
||||
<p class="text-kinetic-muted small mb-0"><i class="bi bi-geo-alt"></i> {{ $e['sede'] ?? 'TBD' }}</p>
|
||||
</div>
|
||||
<a href="{{ route('eventos.show', $e['id_evento']) }}" class="bg-primary text-white p-3 text-center text-decoration-none fw-bold small kfx-event-cta justify-content-center" style="letter-spacing: 0.1em;">VER FICHA TÉCNICA</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PROMOS: Editorial Grid -->
|
||||
<div class="container py-5 my-5">
|
||||
<div class="text-center mb-5">
|
||||
<span class="kfx-section-tag justify-content-center" style="display: inline-flex;">Beneficios para Socios</span>
|
||||
<h2 class="display-3 fw-bold mb-0 kinetic-reveal-text">Beneficios <span class="kfx-text-gradient">Premium</span></h2>
|
||||
</div>
|
||||
@if($promociones->isEmpty())
|
||||
<div class="kinetic-card text-center p-5">
|
||||
<p class="text-kinetic-muted mb-0 fs-5">Sin promociones activas.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="row g-5" data-stagger>
|
||||
@foreach($promociones->take(3) as $p)
|
||||
<div class="col-md-4">
|
||||
<div class="position-relative overflow-hidden mb-4 kfx-img-cover kfx-img-spotlight" style="height: 400px;">
|
||||
<img src="{{ asset('storage/' . $p->imagen) }}" class="w-100 h-100" style="object-fit: cover;" alt="{{ $p->nombre }}">
|
||||
<div class="position-absolute bottom-0 start-0 p-4 w-100" style="background: linear-gradient(transparent, rgba(245, 243, 239, 0.95)); z-index: 2;">
|
||||
<span class="badge bg-primary mb-2">{{ $p->categoria }}</span>
|
||||
<h4 class="h3 mb-0">{{ $p->nombre }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-kinetic-muted small mb-4">{{ $p->descripcion }}</p>
|
||||
<a href="{{ route('promo.qr', $p->id) }}" class="kfx-link fw-bold text-primary small d-inline-flex align-items-center gap-2">
|
||||
SOLICITAR BENEFICIO <i class="bi bi-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#splash {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: #ffffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
color: #ff0000ff;
|
||||
z-index: 9999;
|
||||
transition: opacity 0.6s ease, visibility 0.6s ease;
|
||||
}
|
||||
#splash img {
|
||||
width: 300px;
|
||||
margin-bottom: 20px;
|
||||
animation: bounce 1.5s infinite;
|
||||
}
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-15px); }
|
||||
}
|
||||
#splash.hide {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
window.addEventListener("load", function() {
|
||||
const splash = document.getElementById("splash");
|
||||
const main = document.getElementById("main-content");
|
||||
|
||||
// Solo mostrar splash si es la primera vez en esta sesión
|
||||
if (!sessionStorage.getItem('splashShown')) {
|
||||
sessionStorage.setItem('splashShown', 'true');
|
||||
splash.style.display = 'flex';
|
||||
setTimeout(() => {
|
||||
splash.style.opacity = '0';
|
||||
splash.style.visibility = 'hidden';
|
||||
main.style.display = 'block';
|
||||
}, 2000);
|
||||
} else {
|
||||
main.style.display = 'block';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user