Vistas del panel de clientes

This commit is contained in:
Lucho
2026-06-24 16:19:32 -03:00
parent 2a27c84d24
commit 12a5df4ee0
3 changed files with 1085 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ayuda - Cliente</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body id="top" class="d-flex flex-column min-vh-100 bg-light">
<header class="app-navbar">
<nav class="navbar navbar-expand-lg">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="/cliente/dashboard">
<div class="d-flex align-items-center justify-content-center" style="width: 130px; height: 52px;">
<img src="{{ asset('images/logo.png') }}" alt="Logo" class="img-fluid" style="max-height: 70px; width: auto; object-fit: contain;">
</div>
<span class="app-navbar-greeting ms-2">¡Hola, {{ explode(' ', trim((string) session('cliente_nombre', 'Cliente')))[0] }}!</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#menuPrincipal" aria-controls="menuPrincipal" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="menuPrincipal">
<ul class="navbar-nav mx-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/dashboard#servicios">Servicios</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/dashboard#quienes-somos">Quienes Somos</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/dashboard#equipo">Equipo</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/dashboard#ubicacion">Ubicacion</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/dashboard#formulario">Pedir Turno</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/mis-turnos">Mis Turnos</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/ayuda" title="Ayuda para clientes" aria-label="Ayuda para clientes">¿Necesitas Ayuda?</a></li>
</ul>
<a class="btn app-navbar-link" href="/logout">Cerrar Sesion</a>
</div>
</div>
</nav>
</header>
<main class="container py-5 flex-grow-1">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="h4 mb-0">Instrucciones para utilizar el sitio web</h1>
<a href="/cliente/dashboard" class="btn btn-outline-secondary btn-sm">Volver</a>
</div>
<div class="card border shadow-sm mb-3">
<div class="card-body">
<h2 class="h6">Navegación del sitio</h2>
<p class="mb-2">Para navegar a través del sitio web, podrá utilizar los botones que aparecen en la parte superior de la misma, o también podrá utilizar la rueda de su mouse para visualizar los distintos apartados de la página. Si quiere volver al inicio de la página, podrá apretar un botón redondo azul con una flecha apuntando hacia arriba en la parte inferior derecha.</p>
<p class="mb-0">Para visualizar todos los servicios y profesionales, usted podrá presionar los botones laterales para ir viendo todos los servicios y profesionales que el estudio jurídico ofrece.</p>
</div>
</div>
<div class="card border shadow-sm mb-3">
<div class="card-body">
<h2 class="h6">¿Qué hacer en caso de detectar un error en la página?</h2>
<p class="mb-0">Si se encuentra con un error en el sitio, agradeceríamos que lo reporte con el botón que aparece en la parte superior de la página con el símbolo 🐞. Al apretarlo se sacará una foto a su pantalla automáticamente y se le pedirá que complete un pequeño formulario explicando que fue lo que ocurrió.</p>
</div>
</div>
<div class="card border shadow-sm mb-3">
<div class="card-body">
<h2 class="h6">¿Cómo puedo utilizar a Clara, la asistente virtual?</h2>
<p class="mb-2">Clara es un asistente virtual que da respuestas dependiendo de la palabra clave que el usuario utilice. Por ejemplo, si el usuario quiere preguntar por los honorarios de los profesionales, debería utilizar alguna de las siguientes palabras claves: honorario, honorarios, precio, precios, costo, costos, etc...</p>
<p class="mb-0">Si utiliza una palabra que la asistente no conoce, lamentablemente no podrá responder a su duda. En estos casos se enviará un reporte automático al administrador informándole que el asistente no pudo responder una pregunta para poder corregir el problema.</p>
</div>
</div>
<div class="card border shadow-sm mb-3">
<div class="card-body">
<h2 class="h6">¿Cómo si luego de pedir un turno se me asignó un turno?</h2>
<p class="mb-0">En caso de que algún profesional acepte su caso, usted deberá revisar regularmente su correo electrónico con el que envió el formulario, a través de este medio se le informará todos los detalles de su turno. Además, podrá ver su turno asignado en la sección de "Mis turnos".</p>
</div>
</div>
<div class="card border shadow-sm mb-3">
<div class="card-body">
<h2 class="h6">Mis turnos</h2>
<p class="mb-0">Aquí se podrá ver todo el historial de turnos (turnos pasados y turnos futuros). Al seleccionar alguno de los turnos, podrá ver el detalle de este y también podrá cancelarlo si así lo desea.</p>
</div>
</div>
<div class="card border shadow-sm mb-4">
<div class="card-body">
<h2 class="h6">Recuperación de credenciales</h2>
<p class="mb-0">En caso de olvidar su usuario o contraseña, deberá presionar el botón debajo de la vista de inicio de sesión “¿Olvidó sus credenciales? y luego de llenar un pequeño formulario, tendrá la posibilidad de cambiar contraseña mediante un enlace que se le enviará a su correo electrónico (debe ser el mismo correo con el que está registrado en el sistema).</p>
</div>
</div>
</div>
</div>
</main>
<footer class="app-footer mt-auto py-4">
<div class="container">
<div class="row g-4 align-items-start text-center">
<div class="col-12 col-md-4">
<div class="d-inline-flex align-items-center justify-content-center" style="width: 120px; height: 52px;">
<img src="{{ asset('images/logo.png') }}" alt="Logo" class="img-fluid" style="max-height: 70px; width: auto; object-fit: contain;">
</div>
</div>
<div class="col-12 col-md-4 text-center">
<h3 class="h6 mb-2">Redes Sociales</h3>
<p class="mb-2"><a href="https://www.instagram.com/abogadasdellitoral?utm_source=ig_web_button_share_sheet&igsh=ZDNlZDc0MzIxNw==" target="_blank" rel="noopener noreferrer">Instagram</a></p>
<p class="small mb-0">Desarrollado por Luciano Belini</p>
</div>
<div class="col-12 col-md-4">
<h3 class="h6 mb-2">Ubicacion</h3>
<p class="mb-0"><a href="https://www.google.com/maps/place/Dr.+Luis+Pasteur+141,+Paran%C3%A1,+Entre+R%C3%ADos,+Argentina" target="_blank" rel="noopener noreferrer">Dr. Luis Pasteur 141, Paraná, Entre Ríos, Argentina</a></p>
</div>
</div>
</div>
</footer>
<a href="#top" class="btn btn-primary rounded-circle position-fixed bottom-0 end-0 m-4 d-flex align-items-center justify-content-center" style="width: 44px; height: 44px;" aria-label="Volver arriba" title="Volver arriba">
</a>
@include('partials.reportar-falla-boton')
</body>
</html>
+794
View File
@@ -0,0 +1,794 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dashboard Cliente</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
.profesional-card-img {
width: 100%;
height: 260px;
object-fit: cover;
object-position: center;
}
.assistant-toggle {
width: 56px;
height: 56px;
border-radius: 50%;
position: fixed;
left: 1.2rem;
bottom: 4.8rem;
z-index: 1050;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2);
}
.assistant-panel {
position: fixed;
left: 1.2rem;
bottom: 8.8rem;
width: min(360px, calc(100vw - 2.4rem));
max-height: 70vh;
z-index: 1050;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 14px;
overflow: hidden;
display: none;
background: #fff;
}
.assistant-panel.open {
display: block;
}
.assistant-tip {
position: fixed;
left: 1.2rem;
bottom: 9.8rem;
z-index: 1050;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 12px;
padding: 0.65rem 0.8rem;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.16);
font-size: 0.9rem;
max-width: min(280px, calc(100vw - 2.4rem));
}
.assistant-tip.hidden {
display: none;
}
.assistant-messages {
height: 300px;
overflow-y: auto;
background: #fffdf8;
padding: 0.9rem;
}
.assistant-msg {
padding: 0.55rem 0.7rem;
border-radius: 10px;
margin-bottom: 0.6rem;
max-width: 90%;
line-height: 1.35;
font-size: 0.92rem;
}
.assistant-msg.bot {
background: #f3f4f6;
margin-right: auto;
}
.assistant-msg.user {
background: #ffdeaf;
margin-left: auto;
}
.assistant-chips {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
padding: 0 0.2rem 0.4rem;
}
.assistant-chip {
font-size: 0.82rem;
border-radius: 999px;
white-space: nowrap;
}
.carousel-cards .carousel-control-prev,
.carousel-cards .carousel-control-next {
width: 2.5rem;
background: rgba(0, 0, 0, 0.15);
border-radius: 6px;
opacity: 1;
}
.carousel-cards .carousel-inner {
padding: 0 2.8rem;
}
</style>
</head>
<body id="top" class="d-flex flex-column min-vh-100 bg-light">
<header class="app-navbar">
<nav class="navbar navbar-expand-lg">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="/cliente/dashboard">
<div class="d-flex align-items-center justify-content-center" style="width: 130px; height: 52px;">
<img src="{{ asset('images/logo.png') }}" alt="Logo" class="img-fluid" style="max-height: 70px; width: auto; object-fit: contain;">
</div>
<span class="app-navbar-greeting ms-2">¡Hola, {{ explode(' ', trim((string) session('cliente_nombre', 'Cliente')))[0] }}!</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#menuPrincipal" aria-controls="menuPrincipal" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="menuPrincipal">
<ul class="navbar-nav mx-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="btn app-navbar-link" href="#servicios">Servicios</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="#quienes-somos">Quienes Somos</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="#equipo">Equipo</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="#ubicacion">Ubicacion</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="#formulario">Pedir Turno</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/mis-turnos">Mis Turnos</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/ayuda" title="Ayuda para clientes" aria-label="Ayuda para clientes">¿Necesitas Ayuda?</a></li>
</ul>
<a class="btn app-navbar-link" href="/logout">Cerrar Sesion</a>
</div>
</div>
</nav>
</header>
<main class="container py-5 flex-grow-1 text-center d-flex flex-column align-items-center">
<section id="servicios" class="mb-5 w-100">
<h2 class="h4">Servicios</h2>
@if(collect($servicios)->isEmpty())
<p class="text-muted mt-2">No hay servicios disponibles.</p>
@else
<div id="carouselServicios" class="carousel slide carousel-cards mt-2" data-bs-theme="dark">
<div class="carousel-inner">
@foreach(collect($servicios)->chunk(3) as $chunk)
<div class="carousel-item {{ $loop->first ? 'active' : '' }}">
<div class="row justify-content-center g-4">
@foreach($chunk as $servicio)
@php
$foto = $servicio->foto ?? null;
$fotoSrc = $foto ? asset($foto->ruta) : null;
@endphp
<div class="col-12 col-sm-6 col-md-4">
<div class="card h-100 mx-auto" style="max-width: 18rem;">
@if($fotoSrc)
<img src="{{ $fotoSrc }}" class="card-img-top servicio-card-img" alt="{{ $servicio->titulo }}">
@endif
<div class="card-body">
<h5 class="card-title">{{ $servicio->titulo }}</h5>
<p class="card-text text-muted small">{{ $servicio->descripcion }}</p>
</div>
</div>
</div>
@endforeach
</div>
</div>
@endforeach
</div>
@if(collect($servicios)->count() > 3)
<button class="carousel-control-prev" type="button" data-bs-target="#carouselServicios" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Anterior</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carouselServicios" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Siguiente</span>
</button>
@endif
</div>
@endif
</section>
<hr class="my-4 w-50 mx-auto border-2 border-dark opacity-100">
<section id="quienes-somos" class="mb-5 w-100">
<h2 class="h4">Quienes Somos</h2>
<p class="text-muted mb-0">
{!! nl2br(e($quienesSomos ?? 'Aun no hay contenido cargado para Quienes Somos.')) !!}
</p>
</section>
<hr class="my-4 w-50 mx-auto border-2 border-dark opacity-100">
<section id="equipo" class="mb-5 w-100">
<h2 class="h4">Equipo</h2>
@if(collect($profesionales)->isEmpty())
<p class="text-muted mt-2">No hay profesionales registrados.</p>
@else
<div id="carouselEquipo" class="carousel slide carousel-cards mt-2" data-bs-theme="dark">
<div class="carousel-inner">
@foreach(collect($profesionales)->chunk(3) as $chunk)
<div class="carousel-item {{ $loop->first ? 'active' : '' }}">
<div class="row justify-content-center g-4">
@foreach($chunk as $profesional)
@php
$persona = $profesional->persona;
$foto = $persona->Foto ?? null;
$fotoSrc = $foto ? asset($foto->ruta) : asset('images/avatar_default.png');
$profesionesTexto = $profesional->profesiones
->pluck('titulo')
->push($profesional->profesion?->titulo)
->filter()
->unique()
->implode(' | ');
@endphp
<div class="col-12 col-sm-6 col-md-4">
<div class="card h-100 mx-auto" style="max-width: 18rem;">
<img src="{{ $fotoSrc }}" class="card-img-top profesional-card-img" alt="{{ $persona->nombre }} {{ $persona->apellido }}">
<div class="card-body">
<h5 class="card-title">{{ $persona->nombre }} {{ $persona->apellido }}</h5>
<p class="card-text text-muted small">{{ $profesionesTexto }}</p>
</div>
</div>
</div>
@endforeach
</div>
</div>
@endforeach
</div>
@if(collect($profesionales)->count() > 3)
<button class="carousel-control-prev" type="button" data-bs-target="#carouselEquipo" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Anterior</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carouselEquipo" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Siguiente</span>
</button>
@endif
</div>
@endif
</section>
<hr class="my-4 w-50 mx-auto border-2 border-dark opacity-100">
<section id="ubicacion" class="mb-5 w-100">
<h2 class="h4">Ubicacion</h2>
<p class="text-muted mb-3">Pasteur 141</p>
<div class="ratio ratio-16x9 mx-auto" style="max-width: 900px;" data-map-widget>
<iframe
data-map-embed
data-map-src="https://maps.google.com/maps?q=Pasteur%20141&t=&z=15&ie=UTF8&iwloc=&output=embed"
style="border: 0;"
allowfullscreen
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
title="Mapa de ubicacion - Pasteur 141"></iframe>
<div class="d-none border rounded bg-light p-3 text-center d-flex align-items-center justify-content-center" data-map-fallback>
<div>
<p class="mb-2 fw-semibold">No se pudo cargar el mapa sin conexión.</p>
<p class="mb-2">Dirección: Dr. Luis Pasteur 141, Paraná, Entre Ríos, Argentina.</p>
<a href="https://www.google.com/maps/place/Dr.+Luis+Pasteur+141,+Paran%C3%A1,+Entre+R%C3%ADos,+Argentina" target="_blank" rel="noopener noreferrer">Abrir en Google Maps</a>
</div>
</div>
</div>
</section>
<hr class="my-4 w-50 mx-auto border-2 border-dark opacity-100">
<section id="formulario" class="w-100">
<h2 class="h4">Formulario de Consulta</h2>
<div class="card shadow-sm mx-auto text-start" style="max-width: 920px;">
<div class="card-body p-4 p-md-5">
@if(session('form_success'))
<div class="alert alert-success" role="alert">
{{ session('form_success') }}
</div>
@endif
@if(session('form_error'))
<div class="alert alert-danger" role="alert">
{{ session('form_error') }}
</div>
@endif
@if($errors->any())
<div class="alert alert-danger" role="alert">
<ul class="mb-0 ps-3">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="/cliente/formulario" class="row g-3">
@csrf
<div class="col-12 col-md-6">
<label for="nombre" class="form-label">Nombre</label>
<input type="text" id="nombre" class="form-control" value="{{ $nombreClienteForm }}" readonly>
</div>
<div class="col-12 col-md-6">
<label for="apellido" class="form-label">Apellido</label>
<input type="text" id="apellido" class="form-control" value="{{ $apellidoClienteForm }}" readonly>
</div>
<div class="col-12 col-md-6">
<label for="correo" class="form-label">Correo</label>
<input type="email" id="correo" class="form-control" value="{{ $correoClienteForm }}" readonly>
</div>
<div class="col-12 col-md-6">
<label for="celular" class="form-label">Celular</label>
<input type="text" id="celular" class="form-control" value="{{ $celularClienteForm }}" readonly>
</div>
<div class="col-12 col-md-6">
<label for="profesion_id" class="form-label">Profesion</label>
<select id="profesion_id" name="profesion_id" class="form-select" required>
<option value="">Seleccionar profesion</option>
@foreach($profesiones as $profesion)
<option value="{{ $profesion->id }}" @selected(old('profesion_id') == $profesion->id)>
{{ $profesion->titulo }}
</option>
@endforeach
</select>
</div>
<div class="col-12 col-md-6">
<label for="servicio_id" class="form-label">Servicio</label>
<select id="servicio_id" name="servicio_id" class="form-select" required>
<option value="" data-default-text="Seleccionar servicio">Primero elegi una profesion</option>
@foreach($servicios as $servicio)
<option
value="{{ $servicio->id }}"
data-profesion-ids="{{ $servicio->profesion_id }}"
@selected(old('servicio_id') == $servicio->id)
>
{{ $servicio->titulo }}
</option>
@endforeach
</select>
<div class="form-text">Solo se muestran servicios de la profesion seleccionada.</div>
</div>
<div class="col-12 col-md-6">
<label for="profesional_id" class="form-label">Profesional</label>
<select id="profesional_id" name="profesional_id" class="form-select" required>
<option value="" data-default-text="Seleccionar profesional">Primero elegi una profesion</option>
<option value="INDISTINTO" data-indistinto="1" @selected(old('profesional_id') === 'INDISTINTO')>INDISTINTO</option>
@foreach($profesionales as $profesional)
@php
$profesionesIds = $profesional->profesiones
->pluck('id')
->push((int) $profesional->profesion_id)
->unique()
->implode(',');
@endphp
<option
value="{{ $profesional->id }}"
data-profesion-ids="{{ $profesionesIds }}"
@selected(old('profesional_id') == $profesional->id)
>
{{ $profesional->persona->nombre }} {{ $profesional->persona->apellido }}
</option>
@endforeach
</select>
<div class="form-text">Solo se muestran profesionales de la profesion seleccionada o la opcion INDISTINTO.</div>
</div>
<input type="hidden" name="modalidad_id" value="1">
<div class="col-12 col-md-8">
<label class="form-label d-block">Dias de preferencia</label>
@php
$diasDisponibles = ['Lunes', 'Martes', 'Miercoles', 'Jueves', 'Viernes', 'Indistinto'];
$diasSeleccionados = old('dias_preferencia', []);
@endphp
<div class="d-flex flex-wrap gap-3" id="dias-preferencia-container">
@foreach($diasDisponibles as $dia)
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="dia_{{ strtolower($dia) }}"
name="dias_preferencia[]"
value="{{ $dia }}"
@checked(in_array($dia, $diasSeleccionados, true))
>
<label class="form-check-label" for="dia_{{ strtolower($dia) }}">{{ $dia }}</label>
</div>
@endforeach
</div>
<div class="form-text">Si elegis Indistinto, no podes combinarlo con otros dias.</div>
</div>
<div class="col-12 col-md-4">
<label for="horario_preferencia" class="form-label">Horario de preferencia</label>
<select id="horario_preferencia" name="horario_preferencia" class="form-select" required>
<option value="">Seleccionar horario</option>
<option value="AM" @selected(old('horario_preferencia') == 'AM')>AM</option>
<option value="PM" @selected(old('horario_preferencia') == 'PM')>PM</option>
<option value="INDISTINTO" @selected(old('horario_preferencia') == 'INDISTINTO')>INDISTINTO</option>
</select>
</div>
<div class="col-12">
<label for="descripcion" class="form-label">Descripcion</label>
<textarea id="descripcion" placeholder="Describa brevemente el motivo de su consulta" name="descripcion" class="form-control" rows="4" maxlength="3000" required>{{ old('descripcion') }}</textarea>
</div>
<div style="position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden;" aria-hidden="true">
<label for="website">No completar este campo</label>
<input type="text" id="website" name="website" value="{{ old('website') }}" tabindex="-1" autocomplete="off">
</div>
<div class="col-12 d-grid d-md-flex justify-content-md-end">
<button type="button" class="btn btn-primary px-4" data-bs-toggle="modal" data-bs-target="#confirmarEnvioModal">Enviar</button>
</div>
</form>
<!-- Modal de confirmación de envío -->
<div class="modal fade" id="confirmarEnvioModal" tabindex="-1" aria-labelledby="confirmarEnvioModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="confirmarEnvioModalLabel">Confirmar envío</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body">
¿Deseás enviar el formulario?<br>
<span class="text-muted">Si se confirma tu turno, se te enviará un correo electronico con los detalles del mismo</span>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary" id="btnConfirmarEnvioFormCliente">Confirmar envío</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</section>
</main>
<footer class="app-footer mt-auto py-4">
<div class="container">
<div class="row g-4 align-items-start text-center">
<div class="col-12 col-md-4">
<div class="d-inline-flex align-items-center justify-content-center" style="width: 120px; height: 52px;">
<img src="{{ asset('images/logo.png') }}" alt="Logo" class="img-fluid" style="max-height: 70px; width: auto; object-fit: contain;">
</div>
</div>
<div class="col-12 col-md-4 text-center">
<h3 class="h6 mb-2">Redes Sociales</h3>
<p class="mb-2"><a href="https://www.instagram.com/abogadasdellitoral?utm_source=ig_web_button_share_sheet&igsh=ZDNlZDc0MzIxNw==" target="_blank" rel="noopener noreferrer">Instagram</a></p>
<p class="small mb-0">Desarrollado por Luciano Belini</p>
</div>
<div class="col-12 col-md-4">
<h3 class="h6 mb-2">Ubicacion</h3>
<p class="mb-0"> <a href="https://www.google.com/maps/place/Dr.+Luis+Pasteur+141,+Paran%C3%A1,+Entre+R%C3%ADos,+Argentina" target="_blank" rel="noopener noreferrer">Dr. Luis Pasteur 141, Paraná, Entre Ríos, Argentina</a></p>
</div>
</div>
</div>
</footer>
<a href="#top" class="btn btn-primary rounded-circle position-fixed bottom-0 end-0 m-4 d-flex align-items-center justify-content-center" style="width: 44px; height: 44px;" aria-label="Volver arriba" title="Volver arriba">
</a>
<button id="assistant-toggle" type="button" class="btn btn-warning assistant-toggle" aria-label="Abrir {{ $nombreAsistente }}" title="{{ $nombreAsistente }}">
🤖
</button>
<div id="assistant-tip" class="assistant-tip" role="status" aria-live="polite">
{{ $mensajeBurbujaAsistente }}
</div>
<section id="assistant-panel" class="assistant-panel shadow" aria-label="{{ $nombreAsistente }}">
<div class="bg-warning-subtle border-bottom px-3 py-2 d-flex justify-content-between align-items-center">
<strong>{{ $nombreAsistente }}</strong>
<button id="assistant-close" type="button" class="btn btn-sm btn-outline-secondary">Cerrar</button>
</div>
<div id="assistant-messages" class="assistant-messages">
<div class="assistant-msg bot">{{ $mensajeInicioPanelAsistente }}</div>
@if(!empty($chipsAsistente))
<div id="assistant-chips" class="assistant-chips">
@foreach($chipsAsistente as $chip)
<button type="button" class="btn btn-sm btn-outline-warning assistant-chip">{{ $chip }}</button>
@endforeach
</div>
@endif
</div>
<form id="assistant-form" class="p-2 border-top bg-white">
<div class="input-group">
<input id="assistant-input" type="text" class="form-control" maxlength="500" placeholder="Escribí tu consulta..." required>
<button class="btn btn-primary" type="submit">Enviar</button>
</div>
</form>
</section>
<script>
(function () {
const profesionSelect = document.getElementById('profesion_id');
const servicioSelect = document.getElementById('servicio_id');
const profesionalSelect = document.getElementById('profesional_id');
const diaIndistinto = document.getElementById('dia_indistinto');
const diasContainer = document.getElementById('dias-preferencia-container');
const normalizarTexto = (texto) => {
return (texto || '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.trim();
};
const inicializarSelectBuscable = (select) => {
if (!select || select.dataset.buscableInicializado === '1') {
return { refresh: () => {} };
}
select.dataset.buscableInicializado = '1';
select.classList.add('d-none');
const wrapper = document.createElement('div');
wrapper.className = 'position-relative';
const trigger = document.createElement('button');
trigger.type = 'button';
trigger.className = 'form-select text-start';
trigger.setAttribute('aria-haspopup', 'listbox');
trigger.setAttribute('aria-expanded', 'false');
const dropdown = document.createElement('div');
dropdown.className = 'position-absolute w-100 bg-white border rounded shadow-sm mt-1 p-2';
dropdown.style.zIndex = '1080';
dropdown.style.display = 'none';
dropdown.style.maxHeight = '280px';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.className = 'form-control form-control-sm mb-2';
searchInput.placeholder = 'Buscar...';
const list = document.createElement('div');
list.className = 'd-flex flex-column gap-1 overflow-auto';
list.style.maxHeight = '210px';
dropdown.appendChild(searchInput);
dropdown.appendChild(list);
wrapper.appendChild(trigger);
wrapper.appendChild(dropdown);
select.insertAdjacentElement('afterend', wrapper);
const obtenerLabelSeleccionado = () => {
const selected = select.options[select.selectedIndex];
if (!selected) {
return 'Seleccionar opcion';
}
return selected.textContent.trim();
};
const renderOpciones = () => {
const termino = normalizarTexto(searchInput.value);
const opcionesVisibles = Array.from(select.options).filter((option) => !option.hidden && !option.disabled);
list.innerHTML = '';
opcionesVisibles
.filter((option) => {
if (!termino) {
return true;
}
return normalizarTexto(option.textContent).includes(termino);
})
.forEach((option) => {
const item = document.createElement('button');
item.type = 'button';
item.className = 'btn btn-sm btn-light text-start';
item.textContent = option.textContent.trim();
item.dataset.value = option.value;
if (option.value === select.value) {
item.classList.add('active');
}
item.addEventListener('click', () => {
select.value = option.value;
select.dispatchEvent(new Event('change', { bubbles: true }));
dropdown.style.display = 'none';
trigger.setAttribute('aria-expanded', 'false');
});
list.appendChild(item);
});
if (!list.children.length) {
const vacio = document.createElement('div');
vacio.className = 'small text-muted px-1 py-1';
vacio.textContent = 'Sin resultados';
list.appendChild(vacio);
}
};
const refresh = () => {
trigger.textContent = obtenerLabelSeleccionado();
trigger.disabled = select.disabled;
if (select.disabled) {
dropdown.style.display = 'none';
trigger.setAttribute('aria-expanded', 'false');
}
renderOpciones();
};
trigger.addEventListener('click', () => {
if (trigger.disabled) {
return;
}
const abierto = dropdown.style.display === 'block';
dropdown.style.display = abierto ? 'none' : 'block';
trigger.setAttribute('aria-expanded', abierto ? 'false' : 'true');
if (!abierto) {
searchInput.focus();
searchInput.select();
}
});
searchInput.addEventListener('input', renderOpciones);
select.addEventListener('change', refresh);
document.addEventListener('click', (event) => {
if (!wrapper.contains(event.target)) {
dropdown.style.display = 'none';
trigger.setAttribute('aria-expanded', 'false');
}
});
refresh();
return { refresh };
};
if (profesionSelect && servicioSelect && profesionalSelect) {
const servicioOptions = Array.from(servicioSelect.querySelectorAll('option[value]'));
const profesionalOptions = Array.from(profesionalSelect.querySelectorAll('option[value]'));
const buscadorProfesion = inicializarSelectBuscable(profesionSelect);
const buscadorServicio = inicializarSelectBuscable(servicioSelect);
const buscadorProfesional = inicializarSelectBuscable(profesionalSelect);
const aplicarFiltro = (select, options, profesionId) => {
const selectedOption = select.options[select.selectedIndex];
const placeholderOption = select.querySelector('option[value=""]');
options.forEach((option) => {
const esIndistinto = option.dataset.indistinto === '1';
const profesionesDelProfesional = (option.dataset.profesionIds || '')
.split(',')
.map((id) => id.trim())
.filter((id) => id !== '');
const coincide = Boolean(profesionId) && (esIndistinto || profesionesDelProfesional.includes(profesionId));
option.hidden = !coincide;
option.disabled = !coincide;
});
if (placeholderOption) {
const defaultText = placeholderOption.dataset.defaultText || 'Seleccionar opcion';
placeholderOption.textContent = profesionId ? defaultText : 'Primero elegi una profesion';
}
select.disabled = !profesionId;
if (selectedOption && selectedOption.value && (selectedOption.disabled || selectedOption.hidden)) {
select.value = '';
}
};
const filtrarPorProfesion = () => {
const profesionId = profesionSelect.value;
aplicarFiltro(servicioSelect, servicioOptions, profesionId);
aplicarFiltro(profesionalSelect, profesionalOptions, profesionId);
buscadorProfesion.refresh();
buscadorServicio.refresh();
buscadorProfesional.refresh();
};
filtrarPorProfesion();
profesionSelect.addEventListener('change', filtrarPorProfesion);
}
if (diaIndistinto && diasContainer) {
const checkboxes = Array.from(diasContainer.querySelectorAll('input[type="checkbox"]'));
const otrosDias = checkboxes.filter((checkbox) => checkbox !== diaIndistinto);
const syncDias = () => {
if (diaIndistinto.checked) {
otrosDias.forEach((checkbox) => {
checkbox.checked = false;
checkbox.disabled = true;
});
return;
}
otrosDias.forEach((checkbox) => {
checkbox.disabled = false;
});
const algunOtroDiaSeleccionado = otrosDias.some((checkbox) => checkbox.checked);
diaIndistinto.disabled = algunOtroDiaSeleccionado;
};
checkboxes.forEach((checkbox) => checkbox.addEventListener('change', syncDias));
syncDias();
}
const assistantToggle = document.getElementById('assistant-toggle');
const assistantPanel = document.getElementById('assistant-panel');
const assistantClose = document.getElementById('assistant-close');
const assistantForm = document.getElementById('assistant-form');
const assistantInput = document.getElementById('assistant-input');
const assistantMessages = document.getElementById('assistant-messages');
const assistantTip = document.getElementById('assistant-tip');
const appendAssistantMessage = (text, type) => {
const msg = document.createElement('div');
msg.className = `assistant-msg ${type}`;
msg.textContent = text;
assistantMessages.appendChild(msg);
assistantMessages.scrollTop = assistantMessages.scrollHeight;
};
assistantToggle?.addEventListener('click', () => {
assistantPanel?.classList.add('open');
assistantTip?.classList.add('hidden');
assistantInput?.focus();
});
assistantClose?.addEventListener('click', () => {
assistantPanel?.classList.remove('open');
});
document.querySelectorAll('.assistant-chip').forEach((chip) => {
chip.addEventListener('click', () => {
const texto = chip.textContent.trim();
if (!texto) return;
assistantInput.value = texto;
assistantForm.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
});
});
assistantForm?.addEventListener('submit', async (e) => {
e.preventDefault();
const mensaje = assistantInput.value.trim();
if (!mensaje) {
return;
}
appendAssistantMessage(mensaje, 'user');
assistantInput.value = '';
try {
const res = await fetch('/asistente/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
body: JSON.stringify({ mensaje }),
});
if (!res.ok) {
throw new Error('No se pudo obtener respuesta.');
}
const data = await res.json();
appendAssistantMessage(data.respuesta || 'No pude responder en este momento.', 'bot');
} catch (error) {
appendAssistantMessage('Estoy con inconvenientes técnicos. Probá nuevamente en unos segundos.', 'bot');
}
});
})();
</script>
@include('partials.reportar-falla-boton')
</body>
</html>
@@ -0,0 +1,167 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mis Turnos</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body id="top" class="d-flex flex-column min-vh-100 bg-light">
<header class="app-navbar">
<nav class="navbar navbar-expand-lg">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="/cliente/dashboard">
<div class="d-flex align-items-center justify-content-center" style="width: 130px; height: 52px;">
<img src="{{ asset('images/logo.png') }}" alt="Logo" class="img-fluid" style="max-height: 70px; width: auto; object-fit: contain;">
</div>
<span class="app-navbar-greeting ms-2">¡Hola, {{ explode(' ', trim((string) session('cliente_nombre', 'Cliente')))[0] }}!</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#menuPrincipal" aria-controls="menuPrincipal" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="menuPrincipal">
<ul class="navbar-nav mx-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/dashboard#servicios">Servicios</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/dashboard#quienes-somos">Quienes Somos</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/dashboard#equipo">Equipo</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/dashboard#ubicacion">Ubicacion</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/dashboard#formulario">Pedir Turno</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/mis-turnos">Mis Turnos</a></li>
<li class="nav-item"><a class="btn app-navbar-link" href="/cliente/ayuda" title="Ayuda para clientes" aria-label="Ayuda para clientes">¿Necesitas Ayuda?</a></li>
</ul>
<a class="btn app-navbar-link" href="/logout">Cerrar Sesion</a>
</div>
</div>
</nav>
</header>
<main class="container py-5 flex-grow-1">
<div class="row justify-content-center">
<div class="col-12 col-lg-10">
<h1 class="h3 mb-4 text-center">Mis Turnos</h1>
<div class="card shadow-sm mb-4">
<div class="card-body">
<form method="GET" action="/cliente/mis-turnos" class="row g-3 align-items-end">
<div class="col-12 col-md-5">
<label for="periodo" class="form-label">Periodo</label>
<select id="periodo" name="periodo" class="form-select">
<option value="todos" {{ ($filtroPeriodo ?? 'todos') === 'todos' ? 'selected' : '' }}>Todos</option>
<option value="proximos" {{ ($filtroPeriodo ?? '') === 'proximos' ? 'selected' : '' }}>Proximos</option>
<option value="pasados" {{ ($filtroPeriodo ?? '') === 'pasados' ? 'selected' : '' }}>Pasados</option>
</select>
</div>
<div class="col-12 col-md-5">
<label for="estado_id" class="form-label">Estado</label>
<select id="estado_id" name="estado_id" class="form-select">
<option value="0">Todos</option>
@foreach(($estadosTurno ?? collect()) as $estado)
<option value="{{ $estado->id }}" {{ (int) ($filtroEstadoId ?? 0) === (int) $estado->id ? 'selected' : '' }}>
{{ $estado->descripcion }}
</option>
@endforeach
</select>
</div>
<div class="col-12 col-md-2 d-grid">
<button type="submit" class="btn btn-primary">Filtrar</button>
</div>
</form>
</div>
</div>
@if(session('turno_success'))
<div class="alert alert-success" role="alert">
{{ session('turno_success') }}
</div>
@endif
@if(session('turno_error'))
<div class="alert alert-danger" role="alert">
{{ session('turno_error') }}
</div>
@endif
@forelse($turnos as $turno)
<div class="card shadow-sm mb-4">
<div class="card-body">
@php
$estadoDescripcion = trim((string) ($turno->estadoTurno?->descripcion ?? 'Sin estado'));
$estadoClave = mb_strtolower($estadoDescripcion);
$estadoClase = str_contains($estadoClave, 'cancel')
? 'danger'
: (str_contains($estadoClave, 'confirm') ? 'success' : 'secondary');
$puedeCancelar = !str_contains($estadoClave, 'cancel')
&& $turno->inicio
&& $turno->inicio->greaterThanOrEqualTo(now());
@endphp
<div class="row">
<div class="col-12 col-md-6">
<p class="mb-2"><strong>Profesional:</strong> {{ trim((string) (($turno->profesional?->persona?->nombre ?? '') . ' ' . ($turno->profesional?->persona?->apellido ?? ''))) ?: '-' }}</p>
<p class="mb-2"><strong>Profesión:</strong> {{ $turno->profesional?->profesion?->titulo ?? '-' }}</p>
<p class="mb-2"><strong>Servicio:</strong> {{ $turno->servicio?->titulo ?? '-' }}</p>
<p class="mb-2"><strong>Modalidad:</strong> {{ $turno->modalidad?->descripcion ?? '-' }}</p>
</div>
<div class="col-12 col-md-6">
<p class="mb-2"><strong>Fecha:</strong> {{ optional($turno->inicio)->format('d/m/Y') ?? '-' }}</p>
<p class="mb-2"><strong>Horario:</strong> {{ optional($turno->inicio)->format('H:i') ?? '-' }}</p>
<p class="mb-2"><strong>Estado:</strong> <span class="badge bg-{{ $estadoClase }}">{{ $estadoDescripcion }}</span></p>
</div>
</div>
@if($puedeCancelar)
<div class="d-flex justify-content-end mt-3">
<form method="POST" action="/cliente/turnos/{{ $turno->id }}/cancelar" onsubmit="return confirm('¿Confirmás cancelar este turno?');">
@csrf
<button type="submit" class="btn btn-sm btn-outline-danger">Cancelar turno</button>
</form>
</div>
@endif
</div>
</div>
@empty
<div class="alert alert-info text-center" role="alert">
No tienes turnos registrados. <a href="/cliente/dashboard#formulario">Solicitá un turno</a>
</div>
@endforelse
@if(method_exists($turnos, 'links'))
<div class="d-flex justify-content-center mt-4">
{{ $turnos->links() }}
</div>
@endif
</div>
</div>
</main>
<footer class="app-footer mt-auto py-4">
<div class="container">
<div class="row g-4 align-items-start text-center">
<div class="col-12 col-md-4">
<div class="d-inline-flex align-items-center justify-content-center" style="width: 120px; height: 52px;">
<img src="{{ asset('images/logo.png') }}" alt="Logo" class="img-fluid" style="max-height: 70px; width: auto; object-fit: contain;">
</div>
</div>
<div class="col-12 col-md-4 text-center">
<h3 class="h6 mb-2">Redes Sociales</h3>
<p class="mb-2"><a href="https://www.instagram.com/abogadasdellitoral?utm_source=ig_web_button_share_sheet&igsh=ZDNlZDc0MzIxNw==" target="_blank" rel="noopener noreferrer">Instagram</a></p>
<p class="small mb-0">Desarrollado por Luciano Belini</p>
</div>
<div class="col-12 col-md-4">
<h3 class="h6 mb-2">Ubicacion</h3>
<p class="mb-0">Direccion pendiente de definir</p>
</div>
</div>
</div>
</footer>
<a href="#top" class="btn btn-primary rounded-circle position-fixed bottom-0 end-0 m-4 d-flex align-items-center justify-content-center" style="width: 44px; height: 44px;" aria-label="Volver arriba" title="Volver arriba">
</a>
@include('partials.reportar-falla-boton')
</body>
</html>