6281 lines
246 KiB
PHP
6281 lines
246 KiB
PHP
<?php
|
|
|
|
use App\Http\Controllers\AuthController;
|
|
use App\Models\ContenidoWeb;
|
|
use App\Models\Baja;
|
|
use App\Models\Agenda;
|
|
use App\Models\Dia;
|
|
use App\Models\DiaDeAtencion;
|
|
use App\Models\DiaPreferencia;
|
|
use App\Models\EstadoTurno;
|
|
use App\Models\Feriado;
|
|
use App\Models\Formulario;
|
|
use App\Models\HorarioDeAtencion;
|
|
use App\Models\HorarioPreferencia;
|
|
use App\Models\HorarioReceso;
|
|
use App\Models\Modalidad;
|
|
use App\Models\ModoVacaciones;
|
|
use App\Models\CredencialProfesional;
|
|
use App\Models\Foto;
|
|
use App\Models\AccionLog;
|
|
use App\Models\Administrador;
|
|
use App\Models\CredencialCliente;
|
|
use App\Models\LogSeguridad;
|
|
use App\Models\Cliente;
|
|
use App\Models\DocumentacionCliente;
|
|
use App\Models\Bug;
|
|
use App\Models\Error;
|
|
use App\Models\FotoBug;
|
|
use App\Models\Persona;
|
|
use App\Models\Profesion;
|
|
use App\Models\Profesional;
|
|
use App\Models\Turno;
|
|
use App\Models\Servicio;
|
|
use App\Models\Telefono;
|
|
use App\Models\FaqAsistente;
|
|
use App\Models\AsistenteSinRespuesta;
|
|
use App\Models\Notificacion;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
use Illuminate\Support\Facades\Route;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\Database\QueryException;
|
|
|
|
|
|
if (!defined('FAQ_INTENCION_BURBUJA')) {
|
|
define('FAQ_INTENCION_BURBUJA', 'ui_burbuja');
|
|
}
|
|
if (!defined('FAQ_INTENCION_PANEL_INICIO')) {
|
|
define('FAQ_INTENCION_PANEL_INICIO', 'ui_panel_inicio');
|
|
}
|
|
if (!defined('FAQ_INTENCION_ERROR')) {
|
|
define('FAQ_INTENCION_ERROR', 'ui_error');
|
|
}
|
|
if (!defined('FAQ_INTENCION_NOMBRE')) {
|
|
define('FAQ_INTENCION_NOMBRE', 'ui_nombre');
|
|
}
|
|
if (!defined('FAQ_INTENCION_CHIPS')) {
|
|
define('FAQ_INTENCION_CHIPS', 'ui_chips');
|
|
}
|
|
|
|
$obtenerNombrePersonaLog = function (?int $personaResponsableId): ?string {
|
|
if (!$personaResponsableId) {
|
|
return null;
|
|
}
|
|
|
|
$persona = Persona::query()->find($personaResponsableId);
|
|
|
|
if (!$persona) {
|
|
return null;
|
|
}
|
|
|
|
$nombreCompleto = trim((string) (($persona->nombre ?? '') . ' ' . ($persona->apellido ?? '')));
|
|
|
|
return $nombreCompleto !== '' ? $nombreCompleto : null;
|
|
};
|
|
|
|
$registrarLogSeguridad = function (Request $request, string $accionDescripcion, string $descripcion) use ($obtenerNombrePersonaLog): void {
|
|
$accion = AccionLog::firstWhere('descripcion', $accionDescripcion);
|
|
|
|
if (!$accion) {
|
|
return;
|
|
}
|
|
|
|
$credencialIdSesion = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$personaResponsableId = Administrador::query()
|
|
->where('credencialprofesional_id', $credencialIdSesion)
|
|
->value('persona_id');
|
|
|
|
if (!$personaResponsableId) {
|
|
return;
|
|
}
|
|
|
|
LogSeguridad::create([
|
|
'descripcion' => $descripcion,
|
|
'fechahora' => now(),
|
|
'IPorigen' => (string) $request->ip(),
|
|
'rol' => 'ADMIN',
|
|
'persona_id' => (int) $personaResponsableId,
|
|
'accion_id' => $accion->id,
|
|
'accion_descripcion' => (string) ($accion->descripcion ?? $accionDescripcion),
|
|
'responsable_nombre' => $obtenerNombrePersonaLog((int) $personaResponsableId),
|
|
]);
|
|
};
|
|
|
|
$registrarLogSeguridadProfesional = function (Request $request, string $accionDescripcion, string $descripcion) use ($obtenerNombrePersonaLog): void {
|
|
$accion = AccionLog::firstWhere('descripcion', $accionDescripcion);
|
|
|
|
if (!$accion) {
|
|
return;
|
|
}
|
|
|
|
$credencialIdSesion = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$personaResponsableId = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialIdSesion)
|
|
->value('persona_id');
|
|
|
|
if (!$personaResponsableId) {
|
|
return;
|
|
}
|
|
|
|
LogSeguridad::create([
|
|
'descripcion' => $descripcion,
|
|
'fechahora' => now(),
|
|
'IPorigen' => (string) $request->ip(),
|
|
'rol' => 'PROFESIONAL',
|
|
'persona_id' => (int) $personaResponsableId,
|
|
'accion_id' => $accion->id,
|
|
'accion_descripcion' => (string) ($accion->descripcion ?? $accionDescripcion),
|
|
'responsable_nombre' => $obtenerNombrePersonaLog((int) $personaResponsableId),
|
|
]);
|
|
};
|
|
|
|
$registrarLogSeguridadDirecto = function (?int $personaResponsableId, string $rol, string $ipOrigen, string $accionDescripcion, string $descripcion) use ($obtenerNombrePersonaLog): void {
|
|
if (!$personaResponsableId) {
|
|
return;
|
|
}
|
|
|
|
$accion = AccionLog::firstWhere('descripcion', $accionDescripcion);
|
|
|
|
if (!$accion) {
|
|
return;
|
|
}
|
|
|
|
LogSeguridad::create([
|
|
'descripcion' => $descripcion,
|
|
'fechahora' => now(),
|
|
'IPorigen' => $ipOrigen,
|
|
'rol' => $rol,
|
|
'persona_id' => (int) $personaResponsableId,
|
|
'accion_id' => $accion->id,
|
|
'accion_descripcion' => (string) ($accion->descripcion ?? $accionDescripcion),
|
|
'responsable_nombre' => $obtenerNombrePersonaLog((int) $personaResponsableId),
|
|
]);
|
|
};
|
|
|
|
$validarSesionAdmin = function (Request $request) {
|
|
if (!$request->session()->get('personal_auth') || $request->session()->get('personal_rol') !== 'ADMIN') {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como administradora.');
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
$honeypotValido = function (Request $request): bool {
|
|
return trim((string) $request->input('website', '')) === '';
|
|
};
|
|
|
|
$hashearTokenRecuperacion = function (?string $token): string {
|
|
return hash('sha256', trim((string) ($token ?? '')));
|
|
};
|
|
|
|
$buscarCredencialClientePorResetToken = function (string $token) use ($hashearTokenRecuperacion): ?CredencialCliente {
|
|
$tokenPlano = trim((string) $token);
|
|
|
|
if ($tokenPlano === '') {
|
|
return null;
|
|
}
|
|
|
|
$tokenHasheado = $hashearTokenRecuperacion($tokenPlano);
|
|
|
|
return CredencialCliente::query()
|
|
->where(function ($query) use ($tokenPlano, $tokenHasheado): void {
|
|
$query->where('reset_token', $tokenHasheado)
|
|
->orWhere('reset_token', $tokenPlano);
|
|
})
|
|
->where('reset_expira_en', '>=', now())
|
|
->first();
|
|
};
|
|
|
|
$buscarCredencialProfesionalPorResetToken = function (string $token, ?string $rol = null) use ($hashearTokenRecuperacion): ?CredencialProfesional {
|
|
$tokenPlano = trim((string) $token);
|
|
|
|
if ($tokenPlano === '') {
|
|
return null;
|
|
}
|
|
|
|
$tokenHasheado = $hashearTokenRecuperacion($tokenPlano);
|
|
|
|
$query = CredencialProfesional::query()
|
|
->where(function ($subQuery) use ($tokenPlano, $tokenHasheado): void {
|
|
$subQuery->where('reset_token', $tokenHasheado)
|
|
->orWhere('reset_token', $tokenPlano);
|
|
})
|
|
->where('reset_expira_en', '>=', now());
|
|
|
|
if ($rol !== null && trim($rol) !== '') {
|
|
$query->where('rol', strtoupper(trim($rol)));
|
|
}
|
|
|
|
return $query->first();
|
|
};
|
|
|
|
$normalizarCelular = function (?string $celular): string {
|
|
return preg_replace('/\D+/', '', (string) ($celular ?? '')) ?? '';
|
|
};
|
|
|
|
$normalizarNombreArchivoPrivado = function (string $nombreArchivo): string {
|
|
return basename(str_replace(['\\', '/'], DIRECTORY_SEPARATOR, trim($nombreArchivo)));
|
|
};
|
|
|
|
$directorioDocumentacionPrivada = function (int $clienteId): string {
|
|
return storage_path('app/private/documentacion-clientes/cliente_' . $clienteId);
|
|
};
|
|
|
|
$directorioDocumentacionPublicaLegacy = function (int $clienteId): string {
|
|
return public_path('uploads/documentacion-clientes/cliente_' . $clienteId);
|
|
};
|
|
|
|
$resolverRutaDocumentacionCliente = function (int $clienteId, string $nombreArchivo, bool $migrarLegacy = true) use ($normalizarNombreArchivoPrivado, $directorioDocumentacionPrivada, $directorioDocumentacionPublicaLegacy): ?string {
|
|
$nombreSeguro = $normalizarNombreArchivoPrivado($nombreArchivo);
|
|
|
|
if ($nombreSeguro === '') {
|
|
return null;
|
|
}
|
|
|
|
$rutaPrivada = $directorioDocumentacionPrivada($clienteId) . DIRECTORY_SEPARATOR . $nombreSeguro;
|
|
if (File::exists($rutaPrivada)) {
|
|
return $rutaPrivada;
|
|
}
|
|
|
|
$rutaPublicaLegacy = $directorioDocumentacionPublicaLegacy($clienteId) . DIRECTORY_SEPARATOR . $nombreSeguro;
|
|
if (!File::exists($rutaPublicaLegacy)) {
|
|
return null;
|
|
}
|
|
|
|
if (!$migrarLegacy) {
|
|
return $rutaPublicaLegacy;
|
|
}
|
|
|
|
if (!File::isDirectory($directorioDocumentacionPrivada($clienteId))) {
|
|
File::makeDirectory($directorioDocumentacionPrivada($clienteId), 0755, true);
|
|
}
|
|
|
|
try {
|
|
File::move($rutaPublicaLegacy, $rutaPrivada);
|
|
return $rutaPrivada;
|
|
} catch (\Throwable $e) {
|
|
logger()->warning('No se pudo mover documentación sensible a almacenamiento privado.', [
|
|
'cliente_id' => $clienteId,
|
|
'archivo' => $nombreSeguro,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return $rutaPublicaLegacy;
|
|
}
|
|
};
|
|
|
|
$eliminarDocumentacionCliente = function (int $clienteId, string $nombreArchivo) use ($normalizarNombreArchivoPrivado, $directorioDocumentacionPrivada, $directorioDocumentacionPublicaLegacy): void {
|
|
$nombreSeguro = $normalizarNombreArchivoPrivado($nombreArchivo);
|
|
|
|
if ($nombreSeguro === '') {
|
|
return;
|
|
}
|
|
|
|
foreach ([
|
|
$directorioDocumentacionPrivada($clienteId) . DIRECTORY_SEPARATOR . $nombreSeguro,
|
|
$directorioDocumentacionPublicaLegacy($clienteId) . DIRECTORY_SEPARATOR . $nombreSeguro,
|
|
] as $rutaArchivo) {
|
|
if (File::exists($rutaArchivo)) {
|
|
File::delete($rutaArchivo);
|
|
}
|
|
}
|
|
};
|
|
|
|
$obtenerProfesionalesActivosCompatibles = function (int $profesionId) {
|
|
return Profesional::query()
|
|
->where('baja_id', 1)
|
|
->where(function ($query) use ($profesionId): void {
|
|
$query->where('profesion_id', $profesionId)
|
|
->orWhereHas('profesiones', fn ($q) => $q->where('profesiones.id', $profesionId));
|
|
})
|
|
->pluck('id');
|
|
};
|
|
|
|
$resolverProfesionalCompatible = function (int $profesionalId, int $profesionId): ?Profesional {
|
|
$profesionalBase = Profesional::query()->find($profesionalId);
|
|
|
|
if (!$profesionalBase) {
|
|
return null;
|
|
}
|
|
|
|
return Profesional::query()
|
|
->where('baja_id', 1)
|
|
->where(function ($query) use ($profesionalBase, $profesionalId): void {
|
|
$query->where('id', $profesionalId);
|
|
|
|
if ((int) ($profesionalBase->persona_id ?? 0) > 0) {
|
|
$query->orWhere('persona_id', (int) $profesionalBase->persona_id);
|
|
}
|
|
|
|
if ((int) ($profesionalBase->credencialprofesional_id ?? 0) > 0) {
|
|
$query->orWhere('credencialprofesional_id', (int) $profesionalBase->credencialprofesional_id);
|
|
}
|
|
})
|
|
->where(function ($query) use ($profesionId): void {
|
|
$query->where('profesion_id', $profesionId)
|
|
->orWhereHas('profesiones', fn ($q) => $q->where('profesiones.id', $profesionId));
|
|
})
|
|
->orderByRaw('CASE WHEN id = ? THEN 0 ELSE 1 END', [$profesionalId])
|
|
->first();
|
|
};
|
|
|
|
$formularioFueAceptadoPorOtroProfesional = function (int $formularioId, int $profesionalId): bool {
|
|
$estadoGeneralFormulario = mb_strtolower(trim((string) Formulario::query()
|
|
->where('id', $formularioId)
|
|
->value('estado')));
|
|
|
|
if ($estadoGeneralFormulario !== 'aceptado') {
|
|
return false;
|
|
}
|
|
|
|
return DB::table('profesionales_formularios')
|
|
->where('formulario_id', $formularioId)
|
|
->where('profesional_id', '!=', $profesionalId)
|
|
->whereRaw("LOWER(TRIM(estado)) = 'aceptado'")
|
|
->exists();
|
|
};
|
|
|
|
$recalcularEstadoGeneralFormulario = function (Formulario $formulario): string {
|
|
$profesionFormularioId = (int) $formulario->profesion_id;
|
|
|
|
$queryBase = DB::table('profesionales_formularios as pf')
|
|
->join('profesionales as p', 'p.id', '=', 'pf.profesional_id')
|
|
->where('pf.formulario_id', (int) $formulario->id)
|
|
->where('p.baja_id', 1)
|
|
->where(function ($query) use ($profesionFormularioId): void {
|
|
$query->where('p.profesion_id', $profesionFormularioId)
|
|
->orWhereExists(function ($subquery) use ($profesionFormularioId): void {
|
|
$subquery->select(DB::raw(1))
|
|
->from('profesionales_profesiones as pp')
|
|
->whereColumn('pp.profesional_id', 'p.id')
|
|
->where('pp.profesion_id', $profesionFormularioId);
|
|
});
|
|
});
|
|
|
|
$hayAceptado = (clone $queryBase)
|
|
->whereRaw("LOWER(TRIM(pf.estado)) = 'aceptado'")
|
|
->exists();
|
|
|
|
if ($hayAceptado) {
|
|
$nuevoEstado = 'Aceptado';
|
|
} else {
|
|
$asignadosActivos = (clone $queryBase)->count();
|
|
|
|
$hayActivosSinRechazar = (clone $queryBase)
|
|
->where(function ($query): void {
|
|
$query->whereNull('pf.estado')
|
|
->orWhereRaw("LOWER(TRIM(pf.estado)) <> 'rechazado'");
|
|
})
|
|
->exists();
|
|
|
|
$nuevoEstado = ($asignadosActivos > 0 && !$hayActivosSinRechazar)
|
|
? 'Rechazado por todos'
|
|
: 'Pendiente';
|
|
}
|
|
|
|
if (trim((string) $formulario->estado) !== $nuevoEstado) {
|
|
$formulario->update([
|
|
'estado' => $nuevoEstado,
|
|
]);
|
|
}
|
|
|
|
return $nuevoEstado;
|
|
};
|
|
|
|
$enviarCorreoTurno = function (
|
|
string $tipo,
|
|
string $correoDestino,
|
|
string $nombreDestino,
|
|
\Illuminate\Support\Carbon $inicio,
|
|
string $servicio,
|
|
string $modalidad,
|
|
string $nombreProfesional
|
|
): void {
|
|
$correo = trim($correoDestino);
|
|
if ($correo === '') {
|
|
return;
|
|
}
|
|
|
|
$nombreCliente = trim($nombreDestino) !== '' ? trim($nombreDestino) : 'paciente';
|
|
$fecha = $inicio->format('d/m/Y');
|
|
$hora = $inicio->format('H:i');
|
|
$servicioTxt = trim($servicio) !== '' ? trim($servicio) : 'Sin servicio';
|
|
$modalidadTxt = trim($modalidad) !== '' ? trim($modalidad) : 'Sin modalidad';
|
|
$profesionalTxt = trim($nombreProfesional) !== '' ? trim($nombreProfesional) : 'profesional asignado';
|
|
|
|
$esCancelacion = $tipo === 'cancelacion';
|
|
$esReprogramacion = $tipo === 'reprogramacion';
|
|
$tipoNotificacion = $esCancelacion
|
|
? 'turno_cancelado'
|
|
: ($esReprogramacion ? 'turno_reprogramado' : 'turno_otorgado');
|
|
|
|
$plantillasPorTipo = [
|
|
'turno_otorgado' => [
|
|
'mensaje_inicio' => 'Se asigno tu turno con los siguientes datos:',
|
|
'mensaje_final' => 'Si necesitas reprogramar o cancelar, por favor comunicate por WhatsApp al 343-4632444',
|
|
],
|
|
'turno_cancelado' => [
|
|
'mensaje_inicio' => 'Te informamos que tu turno fue cancelado con los siguientes datos:',
|
|
'mensaje_final' => 'Si necesitas coordinar un nuevo turno, por favor comunicate por WhatsApp al 343-4632444',
|
|
],
|
|
'turno_reprogramado' => [
|
|
'mensaje_inicio' => 'Te informamos que tu turno fue reprogramado con los siguientes datos:',
|
|
'mensaje_final' => 'Si tenes dudas sobre este cambio, por favor comunicate por WhatsApp al 343-4632444',
|
|
],
|
|
];
|
|
|
|
$plantilla = Notificacion::query()->firstOrCreate(
|
|
['tipo' => $tipoNotificacion],
|
|
$plantillasPorTipo[$tipoNotificacion]
|
|
);
|
|
|
|
$mensajeInicio = trim((string) ($plantilla->mensaje_inicio ?? ''));
|
|
$mensajeFinal = trim((string) ($plantilla->mensaje_final ?? ''));
|
|
|
|
$asunto = $esCancelacion
|
|
? 'Cancelacion de turno'
|
|
: ($esReprogramacion ? 'Reprogramacion de turno' : 'Confirmacion de turno asignado');
|
|
$intro = $mensajeInicio !== ''
|
|
? $mensajeInicio
|
|
: ($plantillasPorTipo[$tipoNotificacion]['mensaje_inicio'] ?? '');
|
|
|
|
$ubicacion = "Dr. Luis Pasteur 141, Paraná, Entre Ríos, Argentina";
|
|
$cuerpo = "Hola {$nombreCliente},\n\n"
|
|
. $intro . "\n"
|
|
. "- Fecha: {$fecha}\n"
|
|
. "- Hora: {$hora}\n"
|
|
. "- Servicio: {$servicioTxt}\n"
|
|
. "- Modalidad: {$modalidadTxt}\n"
|
|
. "- Profesional: {$profesionalTxt}\n"
|
|
. "- Ubicación: {$ubicacion}\n\n"
|
|
. ($mensajeFinal !== ''
|
|
? $mensajeFinal
|
|
: ($plantillasPorTipo[$tipoNotificacion]['mensaje_final'] ?? ''))
|
|
. "\n";
|
|
|
|
try {
|
|
Mail::raw($cuerpo, function ($message) use ($correo, $nombreCliente, $asunto): void {
|
|
$message->to($correo, $nombreCliente)->subject($asunto);
|
|
});
|
|
} catch (\Throwable $e) {
|
|
logger()->warning('No se pudo enviar correo de turno.', [
|
|
'tipo' => $tipo,
|
|
'correo' => $correo,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
};
|
|
|
|
// Devuelve advertencias de agenda/configuración. Si la agenda aún no tiene días de atención cargados,
|
|
// permite la asignación manual sin advertir por falta de configuración horaria.
|
|
$obtenerAdvertenciasTurno = function (int $agendaId, int $duracionMinutos, \Illuminate\Support\Carbon $inicio): array {
|
|
$fechaStr = $inicio->format('Y-m-d');
|
|
$finTurno = $inicio->copy()->addMinutes($duracionMinutos);
|
|
$advertencias = [];
|
|
|
|
$esFeriado = Feriado::query()
|
|
->where('agenda_id', $agendaId)
|
|
->where('fecha', $fechaStr)
|
|
->exists();
|
|
|
|
if ($esFeriado) {
|
|
$advertencias[] = 'El día elegido es feriado.';
|
|
}
|
|
|
|
$enVacaciones = ModoVacaciones::query()
|
|
->where('agenda_id', $agendaId)
|
|
->where('inicio', '<=', $fechaStr)
|
|
->where('fin', '>=', $fechaStr)
|
|
->exists();
|
|
|
|
if ($enVacaciones) {
|
|
$advertencias[] = 'El profesional se encuentra en período de vacaciones en esa fecha.';
|
|
}
|
|
|
|
$diaDescPorNumero = [
|
|
0 => ['domingo'],
|
|
1 => ['lunes'],
|
|
2 => ['martes'],
|
|
3 => ['miercoles', 'miércoles'],
|
|
4 => ['jueves'],
|
|
5 => ['viernes'],
|
|
6 => ['sabado', 'sábado'],
|
|
];
|
|
|
|
$diasPos = $diaDescPorNumero[(int) $inicio->dayOfWeek] ?? [];
|
|
$tieneDiasConfigurados = DiaDeAtencion::query()
|
|
->where('agenda_id', $agendaId)
|
|
->exists();
|
|
|
|
$diaAtencion = DiaDeAtencion::query()
|
|
->with(['horariosAtenciones', 'horariosRecesos'])
|
|
->where('agenda_id', $agendaId)
|
|
->whereHas('dias', function ($q) use ($diasPos) {
|
|
$q->whereRaw(
|
|
'LOWER(TRIM(descripcion)) IN (' . implode(',', array_fill(0, count($diasPos), '?')) . ')',
|
|
$diasPos
|
|
);
|
|
})
|
|
->first();
|
|
|
|
if (!$diaAtencion) {
|
|
if ($tieneDiasConfigurados) {
|
|
$advertencias[] = 'El día elegido no forma parte de los días laborales configurados.';
|
|
}
|
|
|
|
return $advertencias;
|
|
}
|
|
|
|
$inicioStr = $inicio->format('H:i:s');
|
|
$finStr = $finTurno->format('H:i:s');
|
|
|
|
if ($diaAtencion->horariosAtenciones->isNotEmpty()) {
|
|
$dentroDeHorario = $diaAtencion->horariosAtenciones->contains(function ($horario) use ($inicioStr, $finStr) {
|
|
$hi = substr((string) $horario->horariocomienzo, 0, 8);
|
|
$hf = substr((string) $horario->horariofin, 0, 8);
|
|
|
|
return $inicioStr >= $hi && $finStr <= $hf;
|
|
});
|
|
|
|
if (!$dentroDeHorario) {
|
|
$advertencias[] = 'El horario elegido está fuera del horario de atención configurado.';
|
|
}
|
|
}
|
|
|
|
$enReceso = $diaAtencion->horariosRecesos->contains(function ($receso) use ($inicioStr, $finStr) {
|
|
$ri = substr((string) $receso->comienzo, 0, 8);
|
|
$rf = substr((string) $receso->fin, 0, 8);
|
|
|
|
return $inicioStr < $rf && $finStr > $ri;
|
|
});
|
|
|
|
if ($enReceso) {
|
|
$advertencias[] = 'El horario elegido cae dentro de un receso.';
|
|
}
|
|
|
|
return $advertencias;
|
|
};
|
|
|
|
$verificarConflictoTurno = function (int $agendaId, int $duracionMinutos, \Illuminate\Support\Carbon $inicio, ?int $turnoIgnoradoId = null): ?string {
|
|
$finTurno = $inicio->copy()->addMinutes($duracionMinutos);
|
|
|
|
$estadoCanceladoId = (int) (EstadoTurno::query()
|
|
->whereRaw('LOWER(TRIM(descripcion)) = ?', ['cancelado'])
|
|
->value('id') ?? 2);
|
|
|
|
$hayConflicto = Turno::query()
|
|
->where('agenda_id', $agendaId)
|
|
->where('estadoturno_id', '!=', $estadoCanceladoId)
|
|
->when($turnoIgnoradoId !== null, function ($query) use ($turnoIgnoradoId) {
|
|
$query->where('id', '!=', $turnoIgnoradoId);
|
|
})
|
|
->where('inicio', '<', $finTurno)
|
|
->whereRaw('DATE_ADD(inicio, INTERVAL ? MINUTE) > ?', [$duracionMinutos, $inicio])
|
|
->exists();
|
|
|
|
if ($hayConflicto) {
|
|
return 'Ya existe un turno en ese horario.';
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
// Retorna null si el horario está disponible, o un string con el motivo si no lo está.
|
|
$verificarDisponibilidadTurno = function (int $agendaId, int $duracionMinutos, \Illuminate\Support\Carbon $inicio, ?int $turnoIgnoradoId = null) use ($obtenerAdvertenciasTurno, $verificarConflictoTurno): ?string {
|
|
$advertencias = $obtenerAdvertenciasTurno($agendaId, $duracionMinutos, $inicio);
|
|
if (!empty($advertencias)) {
|
|
return implode(' ', $advertencias);
|
|
}
|
|
|
|
return $verificarConflictoTurno($agendaId, $duracionMinutos, $inicio, $turnoIgnoradoId);
|
|
};
|
|
|
|
// Busca el primer turno disponible desde el dia siguiente, respetando agenda y preferencias del formulario.
|
|
$buscarTurnoAutomatico = function (Formulario $formulario, int $profesionalId) use ($verificarDisponibilidadTurno): ?array {
|
|
$agenda = Agenda::query()->firstOrCreate(
|
|
['profesional_id' => $profesionalId],
|
|
['estado' => 'Activa', 'duracionturno' => 30]
|
|
);
|
|
|
|
$agenda->load(['diaDeAtencion.dias', 'diaDeAtencion.horariosAtenciones']);
|
|
|
|
$duracion = (int) ($agenda->duracionturno ?? 30);
|
|
|
|
$numeroDiaSemana = [
|
|
'domingo' => 0,
|
|
'lunes' => 1,
|
|
'martes' => 2,
|
|
'miercoles' => 3,
|
|
'miércoles' => 3,
|
|
'jueves' => 4,
|
|
'viernes' => 5,
|
|
'sabado' => 6,
|
|
'sábado' => 6,
|
|
];
|
|
|
|
$horariosPorDia = [];
|
|
foreach ($agenda->diaDeAtencion ?? [] as $diaAtencion) {
|
|
$diaDesc = mb_strtolower(trim((string) ($diaAtencion->dias?->descripcion ?? '')));
|
|
$diaNumero = $numeroDiaSemana[$diaDesc] ?? null;
|
|
|
|
if ($diaNumero === null) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($diaAtencion->horariosAtenciones ?? [] as $horario) {
|
|
if (!$horario->horariocomienzo || !$horario->horariofin) {
|
|
continue;
|
|
}
|
|
|
|
$horariosPorDia[$diaNumero][] = [
|
|
'inicio' => substr((string) $horario->horariocomienzo, 0, 8),
|
|
'fin' => substr((string) $horario->horariofin, 0, 8),
|
|
];
|
|
}
|
|
}
|
|
|
|
$diasPreferidos = collect($formulario->diasPreferidos ?? [])
|
|
->map(fn ($dia) => mb_strtolower(trim((string) ($dia->descripcion ?? ''))))
|
|
->filter()
|
|
->values();
|
|
|
|
$diasPermitidos = null;
|
|
if ($diasPreferidos->isNotEmpty() && !$diasPreferidos->contains('indistinto')) {
|
|
$diasPermitidos = $diasPreferidos
|
|
->map(fn ($dia) => $numeroDiaSemana[$dia] ?? null)
|
|
->filter(fn ($diaNumero) => $diaNumero !== null)
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
$horariosPreferidos = collect($formulario->horariosPreferidos ?? [])
|
|
->map(fn ($horario) => mb_strtolower(trim((string) ($horario->descripcion ?? ''))))
|
|
->filter()
|
|
->values();
|
|
|
|
$preferenciaAM = $horariosPreferidos->contains('am');
|
|
$preferenciaPM = $horariosPreferidos->contains('pm');
|
|
$preferenciaIndistinto = $horariosPreferidos->contains('indistinto');
|
|
|
|
$cumpleHorarioPreferido = function (\Illuminate\Support\Carbon $inicioTurno) use ($preferenciaAM, $preferenciaPM, $preferenciaIndistinto): bool {
|
|
if ($preferenciaIndistinto || (!$preferenciaAM && !$preferenciaPM) || ($preferenciaAM && $preferenciaPM)) {
|
|
return true;
|
|
}
|
|
|
|
$hora = (int) $inicioTurno->format('H');
|
|
|
|
if ($preferenciaAM) {
|
|
return $hora < 12;
|
|
}
|
|
|
|
if ($preferenciaPM) {
|
|
return $hora >= 12;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
$inicioBusqueda = now()->addDay()->startOfDay();
|
|
$finBusqueda = now()->addDays(90)->endOfDay();
|
|
$cursor = $inicioBusqueda->copy();
|
|
|
|
while ($cursor->lte($finBusqueda)) {
|
|
$diaSemana = (int) $cursor->dayOfWeek;
|
|
|
|
if (is_array($diasPermitidos) && !in_array($diaSemana, $diasPermitidos, true)) {
|
|
$cursor->addDay();
|
|
continue;
|
|
}
|
|
|
|
$horariosDelDia = $horariosPorDia[$diaSemana] ?? [];
|
|
|
|
usort($horariosDelDia, fn ($a, $b) => strcmp((string) $a['inicio'], (string) $b['inicio']));
|
|
|
|
foreach ($horariosDelDia as $rango) {
|
|
$inicioRango = \Illuminate\Support\Carbon::parse($cursor->format('Y-m-d') . ' ' . $rango['inicio']);
|
|
$finRango = \Illuminate\Support\Carbon::parse($cursor->format('Y-m-d') . ' ' . $rango['fin']);
|
|
|
|
$slot = $inicioRango->copy();
|
|
|
|
while ($slot->copy()->addMinutes($duracion)->lte($finRango)) {
|
|
if ($slot->lt($inicioBusqueda)) {
|
|
$slot->addMinutes($duracion);
|
|
continue;
|
|
}
|
|
|
|
if (!$cumpleHorarioPreferido($slot)) {
|
|
$slot->addMinutes($duracion);
|
|
continue;
|
|
}
|
|
|
|
$errorDisponibilidad = $verificarDisponibilidadTurno((int) $agenda->id, $duracion, $slot->copy());
|
|
if ($errorDisponibilidad === null) {
|
|
return [
|
|
'agenda_id' => (int) $agenda->id,
|
|
'duracion' => $duracion,
|
|
'inicio' => $slot->copy(),
|
|
'fecha' => $slot->format('Y-m-d'),
|
|
'hora' => $slot->format('H:i'),
|
|
];
|
|
}
|
|
|
|
$slot->addMinutes($duracion);
|
|
}
|
|
}
|
|
|
|
$cursor->addDay();
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
Route::get('/', function () {
|
|
$contenidoWeb = ContenidoWeb::latest('id')->first();
|
|
$quienesSomos = $contenidoWeb?->quienessomos;
|
|
$versionSitio = trim((string) ($contenidoWeb?->version ?? '1.0.0'));
|
|
|
|
$profesionales = Profesional::with(['persona.Foto', 'profesion', 'profesiones'])
|
|
->where('baja_id', 1)
|
|
->get()
|
|
->groupBy('persona_id')
|
|
->map(function ($profesionalesPorPersona) {
|
|
$profesionalBase = $profesionalesPorPersona->first();
|
|
|
|
$profesionesCombinadas = $profesionalesPorPersona
|
|
->flatMap(function ($profesional) {
|
|
return $profesional->profesiones
|
|
->push($profesional->profesion)
|
|
->filter();
|
|
})
|
|
->unique('id')
|
|
->values();
|
|
|
|
$profesionalBase->setRelation('profesiones', $profesionesCombinadas);
|
|
|
|
return $profesionalBase;
|
|
})
|
|
->values();
|
|
|
|
$servicios = Servicio::with('foto')
|
|
->where('estado', 'activo')
|
|
->where('visibleenweb', 'si')
|
|
->get();
|
|
|
|
$profesiones = Profesion::query()
|
|
->where('visible_en_formulario', true)
|
|
->orderBy('titulo')
|
|
->get();
|
|
|
|
$modalidades = Modalidad::query()
|
|
->orderBy('descripcion')
|
|
->get();
|
|
|
|
$mensajeBurbujaAsistente = FaqAsistente::query()
|
|
->where('activo', true)
|
|
->where('intencion', FAQ_INTENCION_BURBUJA)
|
|
->value('respuesta') ?? '¡Hola, soy Clara!, ¿te puedo ayudar en algo?';
|
|
|
|
$mensajeInicioPanelAsistente = FaqAsistente::query()
|
|
->where('activo', true)
|
|
->where('intencion', FAQ_INTENCION_PANEL_INICIO)
|
|
->value('respuesta') ?? 'Hola, soy Clara, la asistente virtual de Abogadas del Litoral. Escribí una palabra clave con el tema con el que necesites ayuda.';
|
|
|
|
$nombreAsistente = FaqAsistente::query()
|
|
->where('activo', true)
|
|
->where('intencion', FAQ_INTENCION_NOMBRE)
|
|
->value('respuesta') ?? 'Clara';
|
|
|
|
$chipsAsistente = FaqAsistente::query()
|
|
->where('activo', true)
|
|
->where('intencion', FAQ_INTENCION_CHIPS)
|
|
->orderBy('orden')
|
|
->orderBy('id')
|
|
->pluck('respuesta')
|
|
->flatMap(fn ($respuesta) => explode("\n", (string) $respuesta))
|
|
->map(fn ($c) => trim($c))
|
|
->filter(fn ($c) => $c !== '')
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
return view('welcome', [
|
|
'quienesSomos' => $quienesSomos,
|
|
'versionSitio' => $versionSitio,
|
|
'profesionales' => $profesionales,
|
|
'servicios' => $servicios,
|
|
'profesiones' => $profesiones,
|
|
'modalidades' => $modalidades,
|
|
'mensajeBurbujaAsistente' => $mensajeBurbujaAsistente,
|
|
'mensajeInicioPanelAsistente' => $mensajeInicioPanelAsistente,
|
|
'nombreAsistente' => $nombreAsistente,
|
|
'chipsAsistente' => $chipsAsistente,
|
|
]);
|
|
});
|
|
|
|
Route::get('/instrucciones-uso', function () {
|
|
return view('instrucciones-uso');
|
|
});
|
|
|
|
Route::post('/asistente/chat', function (Request $request) {
|
|
$validated = $request->validate([
|
|
'mensaje' => ['required', 'string', 'max:500'],
|
|
]);
|
|
|
|
$mensaje = mb_strtolower(trim((string) $validated['mensaje']));
|
|
|
|
$mensajeError = FaqAsistente::query()
|
|
->where('activo', true)
|
|
->where('intencion', FAQ_INTENCION_ERROR)
|
|
->value('respuesta')
|
|
?? 'Lo siento, no tengo una respuesta exacta para eso.';
|
|
|
|
$respuesta = $mensajeError;
|
|
$encontrado = false;
|
|
|
|
try {
|
|
$faqs = FaqAsistente::query()
|
|
->where('activo', true)
|
|
->whereNotIn('intencion', [
|
|
FAQ_INTENCION_BURBUJA,
|
|
FAQ_INTENCION_PANEL_INICIO,
|
|
FAQ_INTENCION_ERROR,
|
|
FAQ_INTENCION_NOMBRE,
|
|
FAQ_INTENCION_CHIPS,
|
|
])
|
|
->orderBy('orden')
|
|
->orderBy('id')
|
|
->get();
|
|
|
|
foreach ($faqs as $faq) {
|
|
$palabrasClave = collect($faq->palabras_clave)
|
|
->filter(fn ($item) => is_string($item) && trim($item) !== '')
|
|
->map(fn ($item) => mb_strtolower(trim($item)))
|
|
->values();
|
|
|
|
$coincide = $palabrasClave->contains(fn ($keyword) => str_contains($mensaje, $keyword));
|
|
|
|
if ($coincide) {
|
|
$respuesta = (string) $faq->respuesta;
|
|
$encontrado = true;
|
|
break;
|
|
}
|
|
}
|
|
} catch (QueryException $e) {
|
|
// Si la tabla aún no existe, mantener fallback por defecto.
|
|
}
|
|
|
|
if (str_contains($respuesta, '{cantidad_servicios}')) {
|
|
$cantidadServicios = Servicio::query()
|
|
->where('estado', 'activo')
|
|
->where('visibleenweb', 'si')
|
|
->count();
|
|
|
|
$respuesta = str_replace('{cantidad_servicios}', (string) $cantidadServicios, $respuesta);
|
|
}
|
|
|
|
if (str_contains($respuesta, '{cantidad_profesionales}')) {
|
|
$cantidadProfesionales = Profesional::query()
|
|
->where('baja_id', 1)
|
|
->count();
|
|
|
|
$respuesta = str_replace('{cantidad_profesionales}', (string) $cantidadProfesionales, $respuesta);
|
|
}
|
|
|
|
if (!$encontrado) {
|
|
try {
|
|
AsistenteSinRespuesta::create(['consulta' => trim((string) $validated['mensaje'])]);
|
|
} catch (QueryException $e) {
|
|
// Tabla aún no migrada.
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'respuesta' => $respuesta,
|
|
]);
|
|
})->middleware('throttle:asistente-chat');
|
|
|
|
Route::get('/reportar-falla', function (Request $request) {
|
|
$origen = trim((string) $request->query('origen', ''));
|
|
|
|
return view('reportar-falla', [
|
|
'origen' => $origen,
|
|
]);
|
|
});
|
|
|
|
Route::post('/reportar-falla', function (Request $request) {
|
|
$validated = $request->validate([
|
|
'titulo' => ['required', 'string', 'max:255'],
|
|
'descripcion' => ['required', 'string', 'max:4500'],
|
|
'origen' => ['nullable', 'string', 'max:1000'],
|
|
'foto' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:4096'],
|
|
'captura_pantalla' => ['nullable', 'string', 'max:4000000'],
|
|
]);
|
|
|
|
$descripcion = trim($validated['descripcion']);
|
|
$origen = trim((string) ($validated['origen'] ?? ''));
|
|
$fotoBugId = null;
|
|
|
|
if ($origen !== '') {
|
|
$descripcion .= "\n\nOrigen: " . $origen;
|
|
}
|
|
|
|
$fotoFile = $request->file('foto');
|
|
if ($fotoFile) {
|
|
$directorio = public_path('images/bugs');
|
|
File::ensureDirectoryExists($directorio);
|
|
|
|
$extension = strtolower((string) $fotoFile->getClientOriginalExtension());
|
|
$mimeType = (string) $fotoFile->getClientMimeType();
|
|
$tamanioBytes = (int) $fotoFile->getSize();
|
|
$nombreArchivo = now()->format('YmdHis') . '_' . uniqid('bug_', true) . '.' . $extension;
|
|
|
|
$fotoFile->move($directorio, $nombreArchivo);
|
|
|
|
$fotoBug = FotoBug::create([
|
|
'extension' => $extension,
|
|
'tamanio_bytes' => $tamanioBytes,
|
|
'nombre' => $nombreArchivo,
|
|
'mime_type' => $mimeType,
|
|
]);
|
|
|
|
$fotoBugId = $fotoBug->id;
|
|
}
|
|
|
|
// Si no se adjuntó foto manual pero hay captura automática de pantalla, usarla.
|
|
if ($fotoBugId === null) {
|
|
$capturaBase64 = trim((string) ($validated['captura_pantalla'] ?? ''));
|
|
if ($capturaBase64 !== '' && str_starts_with($capturaBase64, 'data:image/')) {
|
|
try {
|
|
$partes = explode(',', $capturaBase64, 2);
|
|
$datosImagen = base64_decode($partes[1] ?? '');
|
|
if ($datosImagen !== false && strlen($datosImagen) > 0) {
|
|
$directorio = public_path('images/bugs');
|
|
File::ensureDirectoryExists($directorio);
|
|
$nombreArchivo = now()->format('YmdHis') . '_' . uniqid('cap_', true) . '.jpg';
|
|
file_put_contents($directorio . '/' . $nombreArchivo, $datosImagen);
|
|
|
|
$fotoBug = FotoBug::create([
|
|
'extension' => 'jpg',
|
|
'tamanio_bytes' => strlen($datosImagen),
|
|
'nombre' => $nombreArchivo,
|
|
'mime_type' => 'image/jpeg',
|
|
]);
|
|
|
|
$fotoBugId = $fotoBug->id;
|
|
}
|
|
} catch (\Throwable) {
|
|
// Si falla, continuar sin captura.
|
|
}
|
|
}
|
|
}
|
|
|
|
Bug::create([
|
|
'titulo' => $validated['titulo'],
|
|
'descripcion' => $descripcion,
|
|
'prioridad' => 'Media',
|
|
'estado' => 'Pendiente',
|
|
'version' => 'Web',
|
|
'fotobug_id' => $fotoBugId,
|
|
]);
|
|
|
|
return redirect('/reportar-falla' . ($origen !== '' ? '?origen=' . urlencode($origen) : ''))
|
|
->with('bug_success', 'La falla fue reportada correctamente.');
|
|
})->middleware('throttle:reportar-bugs');
|
|
|
|
Route::post('/formulario', function (Request $request) use ($obtenerProfesionalesActivosCompatibles, $honeypotValido, $resolverProfesionalCompatible) {
|
|
$validated = $request->validate([
|
|
'nombre' => ['required', 'string', 'max:100', 'regex:/^[\pL\s]+$/u'],
|
|
'apellido' => ['required', 'string', 'max:100', 'regex:/^[\pL\s]+$/u'],
|
|
'correo' => ['required', 'email', 'max:255'],
|
|
'celular' => ['required', 'string', 'max:30', 'regex:/^[0-9]+$/'],
|
|
'profesion_id' => ['required', 'exists:profesiones,id'],
|
|
'servicio_id' => [
|
|
'required',
|
|
Rule::exists('servicios', 'id')->where(fn ($query) => $query
|
|
->where('profesion_id', $request->input('profesion_id'))
|
|
->where('visibleenweb', 'si')
|
|
->where('estado', 'activo')),
|
|
],
|
|
'profesional_id' => [
|
|
'required',
|
|
function (string $attribute, mixed $value, \Closure $fail) use ($request, $resolverProfesionalCompatible): void {
|
|
if ($value === 'INDISTINTO') {
|
|
return;
|
|
}
|
|
|
|
$profesionalCompatible = $resolverProfesionalCompatible((int) $value, (int) $request->input('profesion_id'));
|
|
|
|
if (!$profesionalCompatible) {
|
|
$fail('El profesional seleccionado no es valido para la profesion elegida.');
|
|
}
|
|
},
|
|
],
|
|
'modalidad_id' => ['nullable'],
|
|
'dias_preferencia' => [
|
|
'required',
|
|
'array',
|
|
'min:1',
|
|
function (string $attribute, mixed $value, \Closure $fail): void {
|
|
if (!is_array($value)) {
|
|
return;
|
|
}
|
|
|
|
if (in_array('Indistinto', $value, true) && count($value) > 1) {
|
|
$fail('No podes elegir "Indistinto" junto con otros dias.');
|
|
}
|
|
},
|
|
],
|
|
'dias_preferencia.*' => ['required', 'string', 'in:Lunes,Martes,Miercoles,Jueves,Viernes,Indistinto'],
|
|
'horario_preferencia' => ['required', 'string', 'in:AM,PM,INDISTINTO'],
|
|
'descripcion' => ['required', 'string', 'max:3000'],
|
|
'website' => ['nullable', 'string', 'max:255'],
|
|
]);
|
|
|
|
if (!$honeypotValido($request)) {
|
|
return redirect('/#formulario')
|
|
->withErrors(['formulario' => 'No se pudo enviar el formulario.'])
|
|
->withInput();
|
|
}
|
|
|
|
$profesionalSeleccionadoId = null;
|
|
if ($validated['profesional_id'] !== 'INDISTINTO') {
|
|
$profesionalCompatible = $resolverProfesionalCompatible((int) $validated['profesional_id'], (int) $validated['profesion_id']);
|
|
$profesionalSeleccionadoId = $profesionalCompatible ? (int) $profesionalCompatible->id : null;
|
|
}
|
|
|
|
$formulario = Formulario::create([
|
|
'nombrecompleto' => trim($validated['nombre'] . ' ' . $validated['apellido']),
|
|
'correo' => $validated['correo'],
|
|
'celular' => $validated['celular'],
|
|
'ip_origen' => (string) $request->ip(),
|
|
'profesion_id' => (int) $validated['profesion_id'],
|
|
'servicio_id' => (int) $validated['servicio_id'],
|
|
'profesional_id' => $profesionalSeleccionadoId,
|
|
'modalidad_id' => 1,
|
|
'descripcion' => $validated['descripcion'],
|
|
'fechaenvio' => now()->toDateString(),
|
|
]);
|
|
|
|
$diasIds = collect($validated['dias_preferencia'])
|
|
->map(fn ($descripcion) => DiaPreferencia::firstOrCreate(['descripcion' => $descripcion])->id)
|
|
->values()
|
|
->all();
|
|
|
|
$horarioId = HorarioPreferencia::firstOrCreate([
|
|
'descripcion' => $validated['horario_preferencia'],
|
|
])->id;
|
|
|
|
$formulario->diasPreferidos()->sync($diasIds);
|
|
$formulario->horariosPreferidos()->sync([$horarioId]);
|
|
|
|
$profesionalesIds = $obtenerProfesionalesActivosCompatibles((int) $validated['profesion_id']);
|
|
$asignaciones = $profesionalesIds
|
|
->map(fn ($profesionalId) => [
|
|
'profesional_id' => (int) $profesionalId,
|
|
'formulario_id' => (int) $formulario->id,
|
|
'estado' => 'Pendiente',
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
if (!empty($asignaciones)) {
|
|
DB::table('profesionales_formularios')->insertOrIgnore($asignaciones);
|
|
}
|
|
|
|
return redirect('/#formulario')->with('form_success', 'Formulario enviado correctamente.');
|
|
});
|
|
|
|
Route::post('/cliente/formulario', function (Request $request) use ($obtenerProfesionalesActivosCompatibles, $honeypotValido, $resolverProfesionalCompatible) {
|
|
if (!$request->session()->get('cliente_auth')) {
|
|
return redirect('/login/cliente')->with('login_error', 'Debes iniciar sesión como cliente.');
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'profesion_id' => ['required', 'exists:profesiones,id'],
|
|
'servicio_id' => [
|
|
'required',
|
|
Rule::exists('servicios', 'id')->where(fn ($query) => $query
|
|
->where('profesion_id', $request->input('profesion_id'))
|
|
->where('visibleenweb', 'si')
|
|
->where('estado', 'activo')),
|
|
],
|
|
'profesional_id' => [
|
|
'required',
|
|
function (string $attribute, mixed $value, \Closure $fail) use ($request, $resolverProfesionalCompatible): void {
|
|
if ($value === 'INDISTINTO') {
|
|
return;
|
|
}
|
|
|
|
$profesionalCompatible = $resolverProfesionalCompatible((int) $value, (int) $request->input('profesion_id'));
|
|
|
|
if (!$profesionalCompatible) {
|
|
$fail('El profesional seleccionado no es valido para la profesion elegida.');
|
|
}
|
|
},
|
|
],
|
|
'modalidad_id' => ['nullable'],
|
|
'dias_preferencia' => [
|
|
'required',
|
|
'array',
|
|
'min:1',
|
|
function (string $attribute, mixed $value, \Closure $fail): void {
|
|
if (!is_array($value)) {
|
|
return;
|
|
}
|
|
|
|
if (in_array('Indistinto', $value, true) && count($value) > 1) {
|
|
$fail('No podes elegir "Indistinto" junto con otros dias.');
|
|
}
|
|
},
|
|
],
|
|
'dias_preferencia.*' => ['required', 'string', 'in:Lunes,Martes,Miercoles,Jueves,Viernes,Indistinto'],
|
|
'horario_preferencia' => ['required', 'string', 'in:AM,PM,INDISTINTO'],
|
|
'descripcion' => ['required', 'string', 'max:3000'],
|
|
'website' => ['nullable', 'string', 'max:255'],
|
|
]);
|
|
|
|
if (!$honeypotValido($request)) {
|
|
return redirect('/cliente/dashboard#formulario')
|
|
->withErrors(['formulario' => 'No se pudo enviar el formulario.'])
|
|
->withInput();
|
|
}
|
|
|
|
$clienteCorreoSesion = trim((string) $request->session()->get('cliente_correo', ''));
|
|
|
|
$cliente = Cliente::query()
|
|
->with(['persona.telefonos', 'credencialCliente'])
|
|
->whereHas('credencialCliente', fn ($query) => $query->where('correo', $clienteCorreoSesion))
|
|
->first();
|
|
|
|
$nombre = trim((string) ($cliente?->persona?->nombre ?? $request->session()->get('cliente_nombre', '')));
|
|
$apellido = trim((string) ($cliente?->persona?->apellido ?? $request->session()->get('cliente_apellido', '')));
|
|
$correo = trim((string) ($cliente?->correo ?? $cliente?->credencialCliente?->correo ?? $clienteCorreoSesion));
|
|
$celular = trim((string) ($cliente?->persona?->telefonos?->first()?->telefono ?? $request->session()->get('cliente_celular', '')));
|
|
|
|
if (!$cliente || !$cliente->id) {
|
|
return redirect('/cliente/dashboard#formulario')
|
|
->with('form_error', 'No se pudo identificar tu cuenta de cliente. Volvé a iniciar sesión.');
|
|
}
|
|
|
|
if ($nombre === '' || $apellido === '' || $correo === '' || $celular === '') {
|
|
return redirect('/cliente/dashboard#formulario')
|
|
->with('form_error', 'No se pudieron completar automaticamente tus datos personales. Verificá tus datos de cliente.');
|
|
}
|
|
|
|
$profesionalSeleccionadoId = null;
|
|
if ($validated['profesional_id'] !== 'INDISTINTO') {
|
|
$profesionalCompatible = $resolverProfesionalCompatible((int) $validated['profesional_id'], (int) $validated['profesion_id']);
|
|
$profesionalSeleccionadoId = $profesionalCompatible ? (int) $profesionalCompatible->id : null;
|
|
}
|
|
|
|
$formulario = Formulario::create([
|
|
'nombrecompleto' => trim($nombre . ' ' . $apellido),
|
|
'correo' => $correo,
|
|
'celular' => $celular,
|
|
'ip_origen' => (string) $request->ip(),
|
|
'profesion_id' => (int) $validated['profesion_id'],
|
|
'servicio_id' => (int) $validated['servicio_id'],
|
|
'profesional_id' => $profesionalSeleccionadoId,
|
|
'modalidad_id' => 1,
|
|
'cliente_id' => (int) $cliente->id,
|
|
'descripcion' => $validated['descripcion'],
|
|
'fechaenvio' => now()->toDateString(),
|
|
]);
|
|
|
|
$diasIds = collect($validated['dias_preferencia'])
|
|
->map(fn ($descripcion) => DiaPreferencia::firstOrCreate(['descripcion' => $descripcion])->id)
|
|
->values()
|
|
->all();
|
|
|
|
$horarioId = HorarioPreferencia::firstOrCreate([
|
|
'descripcion' => $validated['horario_preferencia'],
|
|
])->id;
|
|
|
|
$formulario->diasPreferidos()->sync($diasIds);
|
|
$formulario->horariosPreferidos()->sync([$horarioId]);
|
|
|
|
$profesionalesIds = $obtenerProfesionalesActivosCompatibles((int) $validated['profesion_id']);
|
|
$asignaciones = $profesionalesIds
|
|
->map(fn ($profesionalId) => [
|
|
'profesional_id' => (int) $profesionalId,
|
|
'formulario_id' => (int) $formulario->id,
|
|
'estado' => 'Pendiente',
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
if (!empty($asignaciones)) {
|
|
DB::table('profesionales_formularios')->insertOrIgnore($asignaciones);
|
|
}
|
|
|
|
return redirect('/cliente/dashboard#formulario')->with('form_success', 'Formulario enviado correctamente.');
|
|
});
|
|
|
|
Route::get('/login/cliente', function (Request $request) {
|
|
$contenidoWeb = ContenidoWeb::latest('id')->first();
|
|
$versionSitio = trim((string) ($contenidoWeb?->version ?? '1.0.0'));
|
|
$intentosRestantes = max(0, RateLimiter::remaining(md5('login-cliente-web' . $request->ip()), 5));
|
|
|
|
return view('auth.login-cliente', [
|
|
'versionSitio' => $versionSitio,
|
|
'intentosRestantes' => $intentosRestantes,
|
|
'intentosMaximos' => 5,
|
|
]);
|
|
});
|
|
|
|
Route::get('/login/personal', function (Request $request) {
|
|
$contenidoWeb = ContenidoWeb::latest('id')->first();
|
|
$versionSitio = trim((string) ($contenidoWeb?->version ?? '1.0.0'));
|
|
$intentosRestantes = max(0, RateLimiter::remaining(md5('login-personal-web' . $request->ip()), 5));
|
|
|
|
return view('auth.login-personal', [
|
|
'versionSitio' => $versionSitio,
|
|
'intentosRestantes' => $intentosRestantes,
|
|
'intentosMaximos' => 5,
|
|
]);
|
|
});
|
|
Route::post('/login/cliente', [AuthController::class, 'loginClienteWeb'])->middleware('throttle:login-cliente-web');
|
|
Route::post('/login/personal', [AuthController::class, 'loginPersonalWeb'])->middleware('throttle:login-personal-web');
|
|
|
|
Route::get('/cliente/recuperar-credenciales', function (Request $request) {
|
|
$intentosRestantes = max(0, RateLimiter::remaining(md5('recuperar-cliente' . $request->ip()), 5));
|
|
|
|
return view('auth.recuperar-credenciales', [
|
|
'intentosRestantes' => $intentosRestantes,
|
|
'intentosMaximos' => 5,
|
|
]);
|
|
});
|
|
|
|
Route::post('/cliente/recuperar-credenciales', function (Request $request) use ($honeypotValido, $registrarLogSeguridadDirecto, $hashearTokenRecuperacion) {
|
|
$request->validate([
|
|
'correo' => ['required', 'email', 'max:255'],
|
|
'website' => ['nullable', 'string', 'max:255'],
|
|
]);
|
|
|
|
if (!$honeypotValido($request)) {
|
|
return back()->withErrors(['recuperar_error' => 'No se pudo procesar la solicitud.'])->withInput($request->except(['website']));
|
|
}
|
|
|
|
$correo = trim((string) $request->input('correo'));
|
|
$credencial = CredencialCliente::where('correo', $correo)->first();
|
|
|
|
if ($credencial) {
|
|
$token = Str::random(64);
|
|
$credencial->update([
|
|
'reset_token' => $hashearTokenRecuperacion($token),
|
|
'reset_expira_en' => now()->addMinutes(30),
|
|
]);
|
|
|
|
$registrarLogSeguridadDirecto(
|
|
(int) ($credencial->cliente?->persona_id ?? 0),
|
|
'CLIENTE',
|
|
(string) $request->ip(),
|
|
'Solicitud cambio de contraseña',
|
|
'El cliente ID ' . (int) ($credencial->cliente?->id ?? 0) . ' solicitó un cambio de contraseña.'
|
|
);
|
|
|
|
$link = url('/cliente/recuperar-credenciales/' . $token);
|
|
$cuerpo = "Hola,\n\nRecibimos una solicitud para restablecer tu contraseña.\n"
|
|
. "Hacé clic en el siguiente enlace para crear una nueva (válido por 30 minutos):\n\n"
|
|
. $link . "\n\n"
|
|
. "Si no solicitaste este cambio, ignorá este mensaje.";
|
|
|
|
try {
|
|
Mail::raw($cuerpo, function ($message) use ($correo): void {
|
|
$message->to($correo)->subject('Restablecer contraseña - Abogadas del Litoral');
|
|
});
|
|
} catch (\Throwable $e) {
|
|
logger()->warning('No se pudo enviar correo de recuperación.', [
|
|
'correo' => $correo,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Siempre mostrar el mismo mensaje para no revelar si el correo existe
|
|
return back()->with('recuperar_success', 'Si el correo está registrado, recibirás un enlace para restablecer tu contraseña en los próximos minutos.');
|
|
})->middleware('throttle:recuperar-cliente');
|
|
|
|
Route::get('/cliente/recuperar-credenciales/{token}', function (string $token) use ($buscarCredencialClientePorResetToken) {
|
|
$credencial = $buscarCredencialClientePorResetToken($token);
|
|
|
|
if (!$credencial) {
|
|
return redirect('/cliente/recuperar-credenciales')
|
|
->with('recuperar_error', 'El enlace es inválido o ya expiró. Solicitá uno nuevo.');
|
|
}
|
|
|
|
return view('auth.nueva-contrasena', ['token' => $token]);
|
|
});
|
|
|
|
Route::post('/cliente/recuperar-credenciales/{token}', function (Request $request, string $token) use ($registrarLogSeguridadDirecto, $buscarCredencialClientePorResetToken) {
|
|
$credencial = $buscarCredencialClientePorResetToken($token);
|
|
|
|
if (!$credencial) {
|
|
return redirect('/cliente/recuperar-credenciales')
|
|
->with('recuperar_error', 'El enlace es inválido o ya expiró. Solicitá uno nuevo.');
|
|
}
|
|
|
|
$request->validate([
|
|
'contra' => ['required', 'string', 'min:6', 'max:30', 'confirmed'],
|
|
]);
|
|
|
|
$credencial->update([
|
|
'contra' => Hash::make((string) $request->input('contra')),
|
|
'reset_token' => null,
|
|
'reset_expira_en' => null,
|
|
]);
|
|
|
|
$registrarLogSeguridadDirecto(
|
|
(int) ($credencial->cliente?->persona_id ?? 0),
|
|
'CLIENTE',
|
|
(string) $request->ip(),
|
|
'Cambio de contraseña exitoso',
|
|
'El cliente ID ' . (int) ($credencial->cliente?->id ?? 0) . ' cambió su contraseña correctamente.'
|
|
);
|
|
|
|
return redirect('/login/cliente')
|
|
->with('login_success', 'Tu contraseña se actualizó correctamente. Ya podés iniciar sesión.');
|
|
});
|
|
|
|
Route::get('/personal/recuperar-credenciales', function (Request $request) {
|
|
$intentosRestantes = max(0, RateLimiter::remaining(md5('recuperar-personal' . $request->ip()), 5));
|
|
$profesiones = Profesion::query()->orderBy('titulo')->get(['id', 'titulo']);
|
|
|
|
return view('auth.recuperar-credenciales-personal', [
|
|
'intentosRestantes' => $intentosRestantes,
|
|
'intentosMaximos' => 5,
|
|
'profesiones' => $profesiones,
|
|
]);
|
|
});
|
|
|
|
Route::post('/personal/recuperar-credenciales', function (Request $request) use ($honeypotValido, $registrarLogSeguridadDirecto, $hashearTokenRecuperacion) {
|
|
$request->validate([
|
|
'dni' => ['required', 'string', 'max:20', 'regex:/^[0-9]+$/'],
|
|
'matricula' => ['required', 'string', 'max:100'],
|
|
'profesion_id' => ['required', 'integer', 'exists:profesiones,id'],
|
|
'correo' => ['required', 'email', 'max:255'],
|
|
'website' => ['nullable', 'string', 'max:255'],
|
|
]);
|
|
|
|
if (!$honeypotValido($request)) {
|
|
return back()->withErrors(['recuperar_error' => 'No se pudo procesar la solicitud.'])->withInput($request->except(['website']));
|
|
}
|
|
|
|
$dni = trim((string) $request->input('dni'));
|
|
$matricula = trim((string) $request->input('matricula'));
|
|
$profesionId = (int) $request->input('profesion_id');
|
|
$correo = trim((string) $request->input('correo'));
|
|
|
|
$profesional = Profesional::query()
|
|
->with('credencialProfesional')
|
|
->where('dni', $dni)
|
|
->where('matricula', $matricula)
|
|
->where('correo', $correo)
|
|
->where(function ($query) use ($profesionId): void {
|
|
$query->where('profesion_id', $profesionId)
|
|
->orWhereHas('profesiones', function ($q) use ($profesionId): void {
|
|
$q->where('profesiones.id', $profesionId);
|
|
});
|
|
})
|
|
->first();
|
|
|
|
if ($profesional?->credencialProfesional) {
|
|
$token = Str::random(64);
|
|
$profesional->credencialProfesional->update([
|
|
'reset_token' => $hashearTokenRecuperacion($token),
|
|
'reset_expira_en' => now()->addMinutes(30),
|
|
]);
|
|
|
|
$registrarLogSeguridadDirecto(
|
|
(int) ($profesional->persona_id ?? 0),
|
|
'PROFESIONAL',
|
|
(string) $request->ip(),
|
|
'Solicitud cambio de contraseña',
|
|
'El profesional ID ' . (int) $profesional->id . ' solicitó un cambio de contraseña.'
|
|
);
|
|
|
|
$link = url('/personal/recuperar-credenciales/' . $token);
|
|
$cuerpo = "Hola,\n\nRecibimos una solicitud para restablecer tu contraseña.\n"
|
|
. "Hacé clic en el siguiente enlace para crear una nueva (válido por 30 minutos):\n\n"
|
|
. $link . "\n\n"
|
|
. "Si no solicitaste este cambio, ignorá este mensaje.";
|
|
|
|
try {
|
|
Mail::raw($cuerpo, function ($message) use ($correo): void {
|
|
$message->to($correo)->subject('Restablecer contraseña - Personal - Abogadas del Litoral');
|
|
});
|
|
} catch (\Throwable $e) {
|
|
logger()->warning('No se pudo enviar correo de recuperación para personal.', [
|
|
'correo' => $correo,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
return back()->with('recuperar_success', 'Si los datos coinciden con un profesional registrado, recibirás un enlace para restablecer tu contraseña en los próximos minutos.');
|
|
})->middleware('throttle:recuperar-personal');
|
|
|
|
Route::get('/personal/recuperar-credenciales/{token}', function (string $token) use ($buscarCredencialProfesionalPorResetToken) {
|
|
$credencial = $buscarCredencialProfesionalPorResetToken($token);
|
|
|
|
if (!$credencial) {
|
|
return redirect('/personal/recuperar-credenciales')
|
|
->with('recuperar_error', 'El enlace es inválido o ya expiró. Solicitá uno nuevo.');
|
|
}
|
|
|
|
return view('auth.nueva-contrasena-personal', ['token' => $token]);
|
|
});
|
|
|
|
Route::post('/personal/recuperar-credenciales/{token}', function (Request $request, string $token) use ($registrarLogSeguridadDirecto, $buscarCredencialProfesionalPorResetToken) {
|
|
$credencial = $buscarCredencialProfesionalPorResetToken($token);
|
|
|
|
if (!$credencial) {
|
|
return redirect('/personal/recuperar-credenciales')
|
|
->with('recuperar_error', 'El enlace es inválido o ya expiró. Solicitá uno nuevo.');
|
|
}
|
|
|
|
$request->validate([
|
|
'contra' => ['required', 'string', 'min:6', 'max:30', 'confirmed'],
|
|
]);
|
|
|
|
$credencial->update([
|
|
'contra' => Hash::make((string) $request->input('contra')),
|
|
'reset_token' => null,
|
|
'reset_expira_en' => null,
|
|
]);
|
|
|
|
$registrarLogSeguridadDirecto(
|
|
(int) ($credencial->profesional?->persona_id ?? $credencial->administrador?->persona_id ?? 0),
|
|
strtoupper((string) ($credencial->rol ?? 'PROFESIONAL')) === 'ADMIN' ? 'ADMIN' : 'PROFESIONAL',
|
|
(string) $request->ip(),
|
|
'Cambio de contraseña exitoso',
|
|
'La cuenta personal ID ' . (int) $credencial->id . ' cambió su contraseña correctamente.'
|
|
);
|
|
|
|
return redirect('/login/personal')
|
|
->with('login_success', 'Tu contraseña se actualizó correctamente. Ya podés iniciar sesión.');
|
|
});
|
|
|
|
Route::get('/admin/recuperar-credenciales', function (Request $request) {
|
|
$intentosRestantes = max(0, RateLimiter::remaining(md5('recuperar-admin' . $request->ip()), 5));
|
|
|
|
return view('auth.recuperar-credenciales-admin', [
|
|
'intentosRestantes' => $intentosRestantes,
|
|
'intentosMaximos' => 5,
|
|
]);
|
|
});
|
|
|
|
Route::get('/admin/recuperar-credenciales/pregunta', function (Request $request) {
|
|
$correo = trim((string) $request->query('correo', ''));
|
|
|
|
if ($correo === '') {
|
|
return response()->json([
|
|
'encontrada' => false,
|
|
'pregunta' => '',
|
|
]);
|
|
}
|
|
|
|
$admin = Administrador::query()
|
|
->with('credencial')
|
|
->where('correo', $correo)
|
|
->first();
|
|
|
|
if (!$admin || strtoupper((string) ($admin->credencial?->rol ?? '')) !== 'ADMIN') {
|
|
return response()->json([
|
|
'encontrada' => false,
|
|
'pregunta' => '',
|
|
]);
|
|
}
|
|
|
|
$preguntaGuardada = trim((string) ($admin->pregunta_secreta_hash ?? ''));
|
|
$preguntaEsHash = Str::startsWith($preguntaGuardada, ['$2y$', '$2a$', '$2b$']);
|
|
|
|
return response()->json([
|
|
'encontrada' => !$preguntaEsHash && $preguntaGuardada !== '',
|
|
'pregunta' => !$preguntaEsHash ? $preguntaGuardada : '',
|
|
]);
|
|
})->middleware('throttle:recuperar-admin-pregunta');
|
|
|
|
Route::post('/admin/recuperar-credenciales', function (Request $request) use ($honeypotValido, $registrarLogSeguridadDirecto, $hashearTokenRecuperacion) {
|
|
$request->validate([
|
|
'celular' => ['required', 'string', 'max:30', 'regex:/^[0-9]+$/'],
|
|
'nombre' => ['required', 'string', 'max:100', 'regex:/^[\pL\s]+$/u'],
|
|
'apellido' => ['required', 'string', 'max:100', 'regex:/^[\pL\s]+$/u'],
|
|
'correo' => ['required', 'email', 'max:255'],
|
|
'pregunta_secreta' => ['nullable', 'string', 'max:255'],
|
|
'respuesta_secreta' => ['required', 'string', 'max:255'],
|
|
'website' => ['nullable', 'string', 'max:255'],
|
|
]);
|
|
|
|
if (!$honeypotValido($request)) {
|
|
return back()->withErrors(['recuperar_error' => 'No se pudo procesar la solicitud.'])->withInput($request->except(['website']));
|
|
}
|
|
|
|
$correo = trim((string) $request->input('correo'));
|
|
$nombre = mb_strtolower(trim((string) $request->input('nombre')));
|
|
$apellido = mb_strtolower(trim((string) $request->input('apellido')));
|
|
$celularNormalizado = preg_replace('/\D+/', '', (string) $request->input('celular'));
|
|
$respuestaSecreta = mb_strtolower(trim((string) $request->input('respuesta_secreta')));
|
|
|
|
$admin = Administrador::query()
|
|
->with(['persona.telefonos', 'credencial'])
|
|
->where('correo', $correo)
|
|
->first();
|
|
|
|
if ($admin && $admin->credencial && strtoupper((string) ($admin->credencial->rol ?? '')) === 'ADMIN') {
|
|
$nombreAdmin = mb_strtolower(trim((string) ($admin->persona?->nombre ?? '')));
|
|
$apellidoAdmin = mb_strtolower(trim((string) ($admin->persona?->apellido ?? '')));
|
|
$celularAdmin = preg_replace('/\D+/', '', (string) ($admin->persona?->telefonos?->first()?->telefono ?? ''));
|
|
|
|
$preguntaGuardada = trim((string) ($admin->pregunta_secreta_hash ?? ''));
|
|
$respuestaHash = (string) ($admin->respuesta_secreta_hash ?? '');
|
|
|
|
$coincide = $nombre !== ''
|
|
&& $apellido !== ''
|
|
&& $celularNormalizado !== ''
|
|
&& $nombre === $nombreAdmin
|
|
&& $apellido === $apellidoAdmin
|
|
&& $celularNormalizado === $celularAdmin
|
|
&& $preguntaGuardada !== ''
|
|
&& $respuestaHash !== ''
|
|
&& Hash::check($respuestaSecreta, $respuestaHash);
|
|
|
|
if ($coincide) {
|
|
$token = Str::random(64);
|
|
$admin->credencial->update([
|
|
'reset_token' => $hashearTokenRecuperacion($token),
|
|
'reset_expira_en' => now()->addMinutes(30),
|
|
]);
|
|
|
|
$registrarLogSeguridadDirecto(
|
|
(int) ($admin->persona_id ?? 0),
|
|
'ADMIN',
|
|
(string) $request->ip(),
|
|
'Solicitud cambio de contraseña',
|
|
'La administradora ID ' . (int) $admin->id . ' solicitó un cambio de contraseña.'
|
|
);
|
|
|
|
$link = url('/admin/recuperar-credenciales/' . $token);
|
|
$cuerpo = "Hola,\n\nRecibimos una solicitud para restablecer tu contraseña de administrador.\n"
|
|
. "Hacé clic en el siguiente enlace para crear una nueva (válido por 30 minutos):\n\n"
|
|
. $link . "\n\n"
|
|
. "Si no solicitaste este cambio, ignorá este mensaje.";
|
|
|
|
try {
|
|
Mail::raw($cuerpo, function ($message) use ($correo): void {
|
|
$message->to($correo)->subject('Restablecer contraseña - Administrador - Abogadas del Litoral');
|
|
});
|
|
} catch (\Throwable $e) {
|
|
logger()->warning('No se pudo enviar correo de recuperación para administrador.', [
|
|
'correo' => $correo,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return back()->with('recuperar_success', 'Si los datos coinciden con un administrador registrado, recibirás un enlace para restablecer tu contraseña en los próximos minutos.');
|
|
})->middleware('throttle:recuperar-admin');
|
|
|
|
Route::get('/admin/recuperar-credenciales/{token}', function (string $token) use ($buscarCredencialProfesionalPorResetToken) {
|
|
$credencial = $buscarCredencialProfesionalPorResetToken($token, 'ADMIN');
|
|
|
|
if (!$credencial) {
|
|
return redirect('/admin/recuperar-credenciales')
|
|
->with('recuperar_error', 'El enlace es inválido o ya expiró. Solicitá uno nuevo.');
|
|
}
|
|
|
|
return view('auth.nueva-contrasena-admin', [
|
|
'token' => $token,
|
|
'usuarioActual' => (string) ($credencial->usuario ?? ''),
|
|
]);
|
|
});
|
|
|
|
Route::post('/admin/recuperar-credenciales/{token}', function (Request $request, string $token) use ($registrarLogSeguridadDirecto, $buscarCredencialProfesionalPorResetToken) {
|
|
$credencial = $buscarCredencialProfesionalPorResetToken($token, 'ADMIN');
|
|
|
|
if (!$credencial) {
|
|
return redirect('/admin/recuperar-credenciales')
|
|
->with('recuperar_error', 'El enlace es inválido o ya expiró. Solicitá uno nuevo.');
|
|
}
|
|
|
|
$request->validate([
|
|
'usuario' => ['required', 'string', 'max:100', Rule::unique('credencialesprofesionales', 'usuario')->ignore((int) $credencial->id)],
|
|
'contra' => ['required', 'string', 'min:6', 'max:30', 'confirmed'],
|
|
]);
|
|
|
|
$usuarioNuevo = trim((string) $request->input('usuario'));
|
|
|
|
$credencial->update([
|
|
'usuario' => $usuarioNuevo,
|
|
'contra' => Hash::make((string) $request->input('contra')),
|
|
'reset_token' => null,
|
|
'reset_expira_en' => null,
|
|
]);
|
|
|
|
$registrarLogSeguridadDirecto(
|
|
(int) ($credencial->administrador?->persona_id ?? 0),
|
|
'ADMIN',
|
|
(string) $request->ip(),
|
|
'Cambio de credenciales exitoso',
|
|
'La cuenta administradora ID ' . (int) $credencial->id . ' actualizó su usuario y contraseña correctamente.'
|
|
);
|
|
|
|
return redirect('/login/personal')
|
|
->with('login_success', 'Tu usuario y contraseña de administrador se actualizaron correctamente.');
|
|
});
|
|
|
|
Route::post('/profesional/asignar-turno-directo', function (Request $request) use ($obtenerAdvertenciasTurno, $verificarConflictoTurno, $enviarCorreoTurno, $registrarLogSeguridadProfesional) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
|
|
$profesional = Profesional::query()
|
|
->with('profesiones')
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesional) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$profesionIdsProfesional = collect([(int) ($profesional->profesion_id ?? 0)])
|
|
->merge($profesional->profesiones->pluck('id')->map(fn ($id) => (int) $id))
|
|
->filter(fn ($id) => (int) $id > 0)
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
$validated = $request->validate([
|
|
'fecha_turno' => ['required', 'date', 'after_or_equal:today'],
|
|
'hora_turno' => ['required', 'date_format:H:i'],
|
|
'nombrecompleto' => ['required', 'string', 'max:200'],
|
|
'correo' => ['required', 'email', 'max:255'],
|
|
'celular' => ['required', 'string', 'regex:/^[0-9]{8,15}$/'],
|
|
'descripcion' => ['nullable', 'string', 'max:1000'],
|
|
'servicio_id' => [
|
|
'required',
|
|
Rule::exists('servicios', 'id')->where(function ($query) use ($profesionIdsProfesional) {
|
|
$query->where('estado', 'activo');
|
|
|
|
if (!empty($profesionIdsProfesional)) {
|
|
$query->whereIn('profesion_id', $profesionIdsProfesional);
|
|
}
|
|
}),
|
|
],
|
|
'modalidad_id' => ['required', 'exists:modalidades,id'],
|
|
'omitir_restricciones' => ['nullable', 'boolean'],
|
|
]);
|
|
|
|
$inicio = \Illuminate\Support\Carbon::parse($validated['fecha_turno'] . ' ' . $validated['hora_turno'] . ':00');
|
|
|
|
$agenda = Agenda::query()->firstOrCreate(
|
|
['profesional_id' => (int) $profesional->id],
|
|
['estado' => 'activo', 'duracionturno' => 30]
|
|
);
|
|
|
|
$duracion = (int) ($agenda->duracionturno ?? 30);
|
|
|
|
$advertencias = $obtenerAdvertenciasTurno((int) $agenda->id, $duracion, $inicio);
|
|
$errorDisponibilidad = $verificarConflictoTurno((int) $agenda->id, $duracion, $inicio);
|
|
|
|
if ($errorDisponibilidad !== null) {
|
|
return redirect('/profesional/dashboard')
|
|
->with('turno_directo_error', 'Ese dia y horario no se encuentra disponible: ' . $errorDisponibilidad)
|
|
->withInput();
|
|
}
|
|
|
|
if (!empty($advertencias) && !((bool) ($validated['omitir_restricciones'] ?? false))) {
|
|
return redirect('/profesional/dashboard')
|
|
->with('turno_directo_warning', [
|
|
'advertencias' => $advertencias,
|
|
'inputs' => [
|
|
'fecha_turno' => $validated['fecha_turno'],
|
|
'hora_turno' => $validated['hora_turno'],
|
|
'nombrecompleto' => $validated['nombrecompleto'],
|
|
'correo' => $validated['correo'],
|
|
'celular' => $validated['celular'],
|
|
'descripcion' => $validated['descripcion'] ?? '',
|
|
'servicio_id' => $validated['servicio_id'],
|
|
'modalidad_id' => $validated['modalidad_id'],
|
|
],
|
|
])
|
|
->withInput();
|
|
}
|
|
|
|
$estadoConfirmadoId = (int) (EstadoTurno::query()
|
|
->whereRaw('LOWER(TRIM(descripcion)) = ?', ['confirmado'])
|
|
->value('id') ?? 1);
|
|
|
|
Turno::create([
|
|
'inicio' => $inicio,
|
|
'correo' => $validated['correo'],
|
|
'celular' => $validated['celular'],
|
|
'nombrecompleto' => $validated['nombrecompleto'],
|
|
'descripcion' => trim((string) ($validated['descripcion'] ?? '')) !== ''
|
|
? trim((string) $validated['descripcion'])
|
|
: 'Turno asignado manualmente desde la agenda.',
|
|
'cliente_id' => null,
|
|
'estadoturno_id' => $estadoConfirmadoId,
|
|
'agenda_id' => (int) $agenda->id,
|
|
'profesional_id' => (int) $profesional->id,
|
|
'servicio_id' => (int) $validated['servicio_id'],
|
|
'modalidad_id' => (int) $validated['modalidad_id'],
|
|
]);
|
|
|
|
$profesional->loadMissing('persona');
|
|
$nombreProfesional = trim((string) (($profesional->persona?->nombre ?? '') . ' ' . ($profesional->persona?->apellido ?? '')));
|
|
$servicioTitulo = (string) (Servicio::query()->where('id', (int) $validated['servicio_id'])->value('titulo') ?? 'Servicio');
|
|
$modalidadDescripcion = (string) (Modalidad::query()->where('id', (int) $validated['modalidad_id'])->value('descripcion') ?? 'Modalidad');
|
|
|
|
$enviarCorreoTurno(
|
|
'asignacion',
|
|
(string) $validated['correo'],
|
|
(string) $validated['nombrecompleto'],
|
|
$inicio->copy(),
|
|
$servicioTitulo,
|
|
$modalidadDescripcion,
|
|
$nombreProfesional
|
|
);
|
|
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'Asignó un turno',
|
|
'El profesional ID ' . (int) $profesional->id . ' asignó un turno para "' . $validated['nombrecompleto'] . '" el ' . $inicio->format('d/m/Y H:i') . '.'
|
|
);
|
|
|
|
return redirect('/profesional/dashboard')
|
|
->with('turno_directo_success', 'El turno se asignó correctamente.');
|
|
});
|
|
|
|
Route::post('/profesional/turnos/{turno}/cancelar', function (Request $request, Turno $turno) use ($enviarCorreoTurno, $registrarLogSeguridadProfesional) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesional) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
if ((int) $turno->profesional_id !== (int) $profesional->id) {
|
|
return redirect('/profesional/dashboard')
|
|
->with('profesional_action_error', 'No podés cancelar un turno que no pertenece a tu agenda.');
|
|
}
|
|
|
|
$estadoCanceladoId = (int) (EstadoTurno::query()
|
|
->whereRaw('LOWER(TRIM(descripcion)) = ?', ['cancelado'])
|
|
->value('id') ?? 0);
|
|
|
|
if ($estadoCanceladoId <= 0) {
|
|
return redirect('/profesional/dashboard')
|
|
->with('profesional_action_error', 'No se encontró el estado Cancelado.');
|
|
}
|
|
|
|
if ((int) $turno->estadoturno_id === $estadoCanceladoId) {
|
|
return redirect('/profesional/dashboard')
|
|
->with('profesional_action_error', 'El turno seleccionado ya estaba cancelado.');
|
|
}
|
|
|
|
$turno->update([
|
|
'estadoturno_id' => $estadoCanceladoId,
|
|
]);
|
|
|
|
$turno->loadMissing(['servicio', 'modalidad']);
|
|
$profesional->loadMissing('persona');
|
|
|
|
$nombreProfesional = trim((string) (($profesional->persona?->nombre ?? '') . ' ' . ($profesional->persona?->apellido ?? '')));
|
|
$correoTurno = (string) ($turno->correo ?? '');
|
|
$nombreTurno = (string) ($turno->nombrecompleto ?? '');
|
|
$servicioTurno = (string) ($turno->servicio?->titulo ?? 'Servicio');
|
|
$modalidadTurno = (string) ($turno->modalidad?->descripcion ?? 'Modalidad');
|
|
$inicioTurno = $turno->inicio ? $turno->inicio->copy() : now();
|
|
|
|
$enviarCorreoTurno(
|
|
'cancelacion',
|
|
$correoTurno,
|
|
$nombreTurno,
|
|
$inicioTurno,
|
|
$servicioTurno,
|
|
$modalidadTurno,
|
|
$nombreProfesional
|
|
);
|
|
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'Canceló un turno',
|
|
'El profesional ID ' . (int) $profesional->id . ' canceló el turno ID ' . (int) $turno->id . ' de "' . $nombreTurno . '".'
|
|
);
|
|
|
|
return redirect('/profesional/dashboard')
|
|
->with('profesional_action_success', 'El turno se canceló correctamente.');
|
|
});
|
|
|
|
Route::post('/profesional/turnos/{turno}/reprogramar', function (Request $request, Turno $turno) use ($obtenerAdvertenciasTurno, $verificarConflictoTurno, $enviarCorreoTurno, $registrarLogSeguridadProfesional) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesional) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
if ((int) $turno->profesional_id !== (int) $profesional->id) {
|
|
return redirect('/profesional/dashboard')
|
|
->with('profesional_action_error', 'No podés reprogramar un turno que no pertenece a tu agenda.');
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'fecha_turno' => ['required', 'date', 'after_or_equal:today'],
|
|
'hora_turno' => ['required', 'date_format:H:i'],
|
|
'omitir_restricciones' => ['nullable', 'boolean'],
|
|
]);
|
|
|
|
$estadoCanceladoId = (int) (EstadoTurno::query()
|
|
->whereRaw('LOWER(TRIM(descripcion)) = ?', ['cancelado'])
|
|
->value('id') ?? 0);
|
|
|
|
if ($estadoCanceladoId > 0 && (int) $turno->estadoturno_id === $estadoCanceladoId) {
|
|
return redirect('/profesional/dashboard')
|
|
->with('profesional_action_error', 'No podés reprogramar un turno cancelado.');
|
|
}
|
|
|
|
$agenda = Agenda::query()->firstOrCreate(
|
|
['profesional_id' => (int) $profesional->id],
|
|
['estado' => 'activo', 'duracionturno' => 30]
|
|
);
|
|
|
|
$duracion = (int) ($agenda->duracionturno ?? 30);
|
|
$inicio = \Illuminate\Support\Carbon::parse($validated['fecha_turno'] . ' ' . $validated['hora_turno'] . ':00');
|
|
$advertencias = $obtenerAdvertenciasTurno((int) $agenda->id, $duracion, $inicio);
|
|
$errorDisponibilidad = $verificarConflictoTurno((int) $agenda->id, $duracion, $inicio, (int) $turno->id);
|
|
|
|
if ($errorDisponibilidad !== null) {
|
|
return redirect('/profesional/dashboard')
|
|
->with('profesional_action_error', 'No se pudo reprogramar el turno: ' . $errorDisponibilidad);
|
|
}
|
|
|
|
if (!empty($advertencias) && !((bool) ($validated['omitir_restricciones'] ?? false))) {
|
|
return redirect('/profesional/dashboard')
|
|
->with('reprogramar_turno_warning', [
|
|
'advertencias' => $advertencias,
|
|
'inputs' => [
|
|
'fecha_turno' => $validated['fecha_turno'],
|
|
'hora_turno' => $validated['hora_turno'],
|
|
],
|
|
'turno_id' => (int) $turno->id,
|
|
]);
|
|
}
|
|
|
|
$estadoReprogramadoId = (int) (EstadoTurno::query()
|
|
->whereRaw('LOWER(TRIM(descripcion)) = ?', ['reprogramado'])
|
|
->value('id') ?? 0);
|
|
|
|
$datosActualizar = [
|
|
'inicio' => $inicio,
|
|
'agenda_id' => (int) $agenda->id,
|
|
];
|
|
|
|
if ($estadoReprogramadoId > 0) {
|
|
$datosActualizar['estadoturno_id'] = $estadoReprogramadoId;
|
|
}
|
|
|
|
$turno->update($datosActualizar);
|
|
|
|
$turno->loadMissing(['servicio', 'modalidad']);
|
|
$profesional->loadMissing('persona');
|
|
|
|
$nombreProfesional = trim((string) (($profesional->persona?->nombre ?? '') . ' ' . ($profesional->persona?->apellido ?? '')));
|
|
$correoTurno = (string) ($turno->correo ?? '');
|
|
$nombreTurno = (string) ($turno->nombrecompleto ?? '');
|
|
$servicioTurno = (string) ($turno->servicio?->titulo ?? 'Servicio');
|
|
$modalidadTurno = (string) ($turno->modalidad?->descripcion ?? 'Modalidad');
|
|
|
|
$enviarCorreoTurno(
|
|
'reprogramacion',
|
|
$correoTurno,
|
|
$nombreTurno,
|
|
$inicio->copy(),
|
|
$servicioTurno,
|
|
$modalidadTurno,
|
|
$nombreProfesional
|
|
);
|
|
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'Reprogramó un turno',
|
|
'El profesional ID ' . (int) $profesional->id . ' reprogramó el turno ID ' . (int) $turno->id . ' de "' . $nombreTurno . '" para el ' . $inicio->format('d/m/Y H:i') . '.'
|
|
);
|
|
|
|
return redirect('/profesional/dashboard')
|
|
->with('profesional_action_success', 'El turno se reprogramó correctamente.');
|
|
});
|
|
|
|
Route::get('/profesional/dashboard', function () {
|
|
$credencialId = (int) session('personal_credencial_id', 0);
|
|
$profesionalSesion = Profesional::query()
|
|
->with(['profesion', 'profesiones'])
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesionalSesion) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$contenidoWeb = ContenidoWeb::latest('id')->first();
|
|
$quienesSomos = $contenidoWeb?->quienessomos;
|
|
$versionSitio = trim((string) ($contenidoWeb?->version ?? '1.0.0'));
|
|
|
|
$profesionales = Profesional::with(['persona.Foto', 'profesion', 'profesiones'])
|
|
->where('baja_id', 1)
|
|
->get();
|
|
|
|
$profesionIdsProfesional = collect([(int) ($profesionalSesion->profesion_id ?? 0)])
|
|
->merge($profesionalSesion->profesiones->pluck('id')->map(fn ($id) => (int) $id))
|
|
->filter(fn ($id) => (int) $id > 0)
|
|
->unique()
|
|
->values();
|
|
|
|
$servicios = Servicio::with('foto')
|
|
->where('estado', 'activo')
|
|
->where('visibleenweb', 'si')
|
|
->when($profesionIdsProfesional->isNotEmpty(), function ($query) use ($profesionIdsProfesional) {
|
|
$query->whereIn('profesion_id', $profesionIdsProfesional->all());
|
|
})
|
|
->orderBy('titulo')
|
|
->get();
|
|
|
|
$profesiones = Profesion::query()
|
|
->where('visible_en_formulario', true)
|
|
->orderBy('titulo')
|
|
->get();
|
|
|
|
$modalidades = Modalidad::query()
|
|
->orderBy('descripcion')
|
|
->get();
|
|
|
|
$turnos = Turno::query()
|
|
->with(['servicio', 'modalidad', 'estadoTurno', 'cliente.persona.telefonos'])
|
|
->where('profesional_id', (int) $profesionalSesion->id)
|
|
->orderBy('inicio')
|
|
->get();
|
|
|
|
$agendaProfesional = Agenda::query()
|
|
->with(['diaDeAtencion.dias', 'diaDeAtencion.horariosAtenciones', 'diaDeAtencion.horariosRecesos', 'feriado', 'modoVacaciones'])
|
|
->where('profesional_id', (int) $profesionalSesion->id)
|
|
->first();
|
|
|
|
$numeroDiaSemana = [
|
|
'domingo' => 0,
|
|
'lunes' => 1,
|
|
'martes' => 2,
|
|
'miercoles' => 3,
|
|
'miércoles' => 3,
|
|
'jueves' => 4,
|
|
'viernes' => 5,
|
|
'sabado' => 6,
|
|
'sábado' => 6,
|
|
];
|
|
|
|
$coloresEstado = [
|
|
'confirmado' => '#198754',
|
|
'cancelado' => '#dc3545',
|
|
'reprogramado' => '#fd7e14',
|
|
'cliente ausente' => '#6c757d',
|
|
'cliente presente' => '#0d6efd',
|
|
];
|
|
|
|
$duracionTurnoVisual = (int) ($agendaProfesional?->duracionturno ?? 30);
|
|
|
|
$eventosAgenda = $turnos
|
|
->filter(fn ($turno) => $turno->inicio)
|
|
->filter(function ($turno) {
|
|
$estadoDescripcion = mb_strtolower(trim((string) ($turno->estadoTurno?->descripcion ?? '')));
|
|
|
|
return $estadoDescripcion !== 'cancelado';
|
|
})
|
|
->map(function ($turno) use ($coloresEstado, $duracionTurnoVisual) {
|
|
$estadoDescripcion = trim((string) ($turno->estadoTurno?->descripcion ?? 'Sin estado'));
|
|
$estadoKey = mb_strtolower($estadoDescripcion);
|
|
$color = $coloresEstado[$estadoKey] ?? '#0d6efd';
|
|
|
|
// Naranja para turnos asignados manualmente (sin cliente vinculado y estado confirmado)
|
|
if ($estadoKey === 'confirmado' && $turno->cliente_id === null) {
|
|
$color = '#fd7e14';
|
|
}
|
|
|
|
$servicioTitulo = trim((string) ($turno->servicio?->titulo ?? 'Turno'));
|
|
$nombreCompleto = trim((string) ($turno->nombrecompleto ?? 'Cliente'));
|
|
$celularCliente = trim((string) ($turno->cliente?->persona?->telefonos?->first()?->telefono ?? ''));
|
|
if ($celularCliente === '') {
|
|
$celularCliente = trim((string) ($turno->celular ?? ''));
|
|
}
|
|
|
|
return [
|
|
'id' => (int) $turno->id,
|
|
'title' => $servicioTitulo . ' - ' . $nombreCompleto,
|
|
'start' => $turno->inicio->format('Y-m-d\TH:i:s'),
|
|
'end' => $turno->inicio->copy()->addMinutes($duracionTurnoVisual)->format('Y-m-d\TH:i:s'),
|
|
'backgroundColor' => $color,
|
|
'borderColor' => $color,
|
|
'extendedProps' => [
|
|
'prioridad' => 4,
|
|
'turnoId' => (int) $turno->id,
|
|
'cliente' => $nombreCompleto,
|
|
'celular' => $celularCliente !== '' ? $celularCliente : '-',
|
|
'correo' => (string) ($turno->correo ?? ''),
|
|
'descripcion' => (string) ($turno->descripcion ?? ''),
|
|
'servicio' => $servicioTitulo,
|
|
'modalidad' => (string) ($turno->modalidad?->descripcion ?? '-'),
|
|
'estado' => $estadoDescripcion,
|
|
'estadoKey' => $estadoKey,
|
|
'puedeCancelarse' => $estadoKey !== 'cancelado',
|
|
'puedeReprogramarse' => $estadoKey !== 'cancelado',
|
|
],
|
|
];
|
|
})
|
|
->values();
|
|
|
|
$inicioRango = now()->startOfDay();
|
|
$finRango = now()->addDays(90)->endOfDay();
|
|
|
|
$eventosDisponibilidad = collect($agendaProfesional?->diaDeAtencion ?? [])
|
|
->flatMap(function ($diaAtencion) use ($numeroDiaSemana, $inicioRango, $finRango) {
|
|
$diaDescripcion = mb_strtolower(trim((string) ($diaAtencion->dias?->descripcion ?? '')));
|
|
$diaSemana = $numeroDiaSemana[$diaDescripcion] ?? null;
|
|
|
|
if ($diaSemana === null) {
|
|
return [];
|
|
}
|
|
|
|
return collect($diaAtencion->horariosAtenciones ?? [])
|
|
->filter(fn ($horario) => $horario->horariocomienzo && $horario->horariofin)
|
|
->flatMap(function ($horario) use ($diaSemana, $diaAtencion, $inicioRango, $finRango) {
|
|
$eventos = [];
|
|
$cursor = $inicioRango->copy();
|
|
|
|
while ($cursor->lte($finRango)) {
|
|
if ((int) $cursor->dayOfWeek === (int) $diaSemana) {
|
|
$fecha = $cursor->format('Y-m-d');
|
|
$horaInicio = substr((string) $horario->horariocomienzo, 0, 8);
|
|
$horaFin = substr((string) $horario->horariofin, 0, 8);
|
|
|
|
$eventos[] = [
|
|
'id' => 'disp-' . $diaAtencion->id . '-' . $horario->id . '-' . $fecha,
|
|
'title' => 'Disponibilidad',
|
|
'start' => $fecha . 'T' . $horaInicio,
|
|
'end' => $fecha . 'T' . $horaFin,
|
|
'display' => 'background',
|
|
'classNames' => ['evento-disponibilidad'],
|
|
'backgroundColor' => '#198754',
|
|
'borderColor' => '#198754',
|
|
'extendedProps' => [
|
|
'prioridad' => 5,
|
|
'tipoEvento' => 'disponibilidad',
|
|
'estado' => 'Disponible',
|
|
'modalidad' => '-',
|
|
'servicio' => 'Horario de atención',
|
|
'cliente' => '-',
|
|
'correo' => '-',
|
|
'descripcion' => 'Franja horaria configurada en tu agenda.',
|
|
],
|
|
];
|
|
}
|
|
|
|
$cursor->addDay();
|
|
}
|
|
|
|
return $eventos;
|
|
});
|
|
})
|
|
->values();
|
|
|
|
$eventosReceso = collect($agendaProfesional?->diaDeAtencion ?? [])
|
|
->flatMap(function ($diaAtencion) use ($numeroDiaSemana, $inicioRango, $finRango) {
|
|
$diaDescripcion = mb_strtolower(trim((string) ($diaAtencion->dias?->descripcion ?? '')));
|
|
$diaSemana = $numeroDiaSemana[$diaDescripcion] ?? null;
|
|
|
|
if ($diaSemana === null) {
|
|
return [];
|
|
}
|
|
|
|
return collect($diaAtencion->horariosRecesos ?? [])
|
|
->filter(fn ($receso) => $receso->comienzo && $receso->fin)
|
|
->flatMap(function ($receso) use ($diaSemana, $diaAtencion, $inicioRango, $finRango) {
|
|
$eventos = [];
|
|
$cursor = $inicioRango->copy();
|
|
|
|
while ($cursor->lte($finRango)) {
|
|
if ((int) $cursor->dayOfWeek === (int) $diaSemana) {
|
|
$fecha = $cursor->format('Y-m-d');
|
|
$horaInicio = substr((string) $receso->comienzo, 0, 8);
|
|
$horaFin = substr((string) $receso->fin, 0, 8);
|
|
$descripcionReceso = trim((string) ($receso->descripcion ?? ''));
|
|
$tituloReceso = $descripcionReceso !== ''
|
|
? 'Receso: ' . $descripcionReceso
|
|
: 'Receso';
|
|
|
|
$eventos[] = [
|
|
'id' => 'receso-' . $diaAtencion->id . '-' . $receso->id . '-' . $fecha,
|
|
'title' => $tituloReceso,
|
|
'start' => $fecha . 'T' . $horaInicio,
|
|
'end' => $fecha . 'T' . $horaFin,
|
|
'display' => 'background',
|
|
'classNames' => ['evento-receso'],
|
|
'textColor' => '#5b1f23',
|
|
'backgroundColor' => '#dc3545',
|
|
'borderColor' => '#dc3545',
|
|
'extendedProps' => [
|
|
'prioridad' => 3,
|
|
'tipoEvento' => 'no_disponible',
|
|
'motivo' => 'Receso',
|
|
'estado' => 'No disponible',
|
|
'descripcion' => 'Franja horaria de receso configurada.',
|
|
],
|
|
];
|
|
}
|
|
|
|
$cursor->addDay();
|
|
}
|
|
|
|
return $eventos;
|
|
});
|
|
})
|
|
->values();
|
|
|
|
$eventosVacaciones = collect($agendaProfesional?->modoVacaciones ?? [])
|
|
->filter(fn ($vacacion) => $vacacion->inicio && $vacacion->fin)
|
|
->map(function ($vacacion) {
|
|
$inicio = (string) $vacacion->inicio;
|
|
$finExclusivo = \Illuminate\Support\Carbon::parse((string) $vacacion->fin)->addDay()->format('Y-m-d');
|
|
$descripcionVacacion = trim((string) ($vacacion->descripcion ?? ''));
|
|
|
|
return [
|
|
'id' => 'vacaciones-' . $vacacion->id,
|
|
'title' => $descripcionVacacion !== ''
|
|
? 'Vacaciones: ' . $descripcionVacacion
|
|
: 'Vacaciones',
|
|
'start' => $inicio,
|
|
'end' => $finExclusivo,
|
|
'allDay' => true,
|
|
'display' => 'background',
|
|
'classNames' => ['evento-vacaciones'],
|
|
'textColor' => '#5b1f23',
|
|
'backgroundColor' => '#dc3545',
|
|
'borderColor' => '#dc3545',
|
|
'extendedProps' => [
|
|
'prioridad' => 2,
|
|
'tipoEvento' => 'no_disponible',
|
|
'motivo' => 'Vacaciones',
|
|
'estado' => 'No disponible',
|
|
'descripcion' => (string) ($vacacion->descripcion ?: 'Vacaciones'),
|
|
],
|
|
];
|
|
})
|
|
->values();
|
|
|
|
$eventosFeriados = collect($agendaProfesional?->feriado ?? [])
|
|
->filter(fn ($feriado) => $feriado->fecha)
|
|
->map(function ($feriado) {
|
|
$fecha = (string) $feriado->fecha;
|
|
$finExclusivo = \Illuminate\Support\Carbon::parse($fecha)->addDay()->format('Y-m-d');
|
|
$descripcionFeriado = trim((string) ($feriado->descripcion ?? ''));
|
|
|
|
return [
|
|
'id' => 'feriado-' . $feriado->id,
|
|
'title' => $descripcionFeriado !== ''
|
|
? 'Feriado: ' . $descripcionFeriado
|
|
: 'Feriado',
|
|
'start' => $fecha,
|
|
'end' => $finExclusivo,
|
|
'allDay' => true,
|
|
'display' => 'background',
|
|
'classNames' => ['evento-feriado'],
|
|
'textColor' => '#5b1f23',
|
|
'backgroundColor' => '#dc3545',
|
|
'borderColor' => '#dc3545',
|
|
'extendedProps' => [
|
|
'prioridad' => 1,
|
|
'tipoEvento' => 'no_disponible',
|
|
'motivo' => 'Feriado',
|
|
'estado' => 'No disponible',
|
|
'descripcion' => (string) ($feriado->descripcion ?: 'Feriado'),
|
|
],
|
|
];
|
|
})
|
|
->values();
|
|
|
|
$duracionTurnoAgenda = (int) ($agendaProfesional?->duracionturno ?? 30);
|
|
|
|
$rangosTurnosOcupados = $turnos
|
|
->filter(fn ($turno) => $turno->inicio)
|
|
->filter(function ($turno) {
|
|
$estadoDescripcion = mb_strtolower(trim((string) ($turno->estadoTurno?->descripcion ?? '')));
|
|
|
|
return $estadoDescripcion !== 'cancelado';
|
|
})
|
|
->map(function ($turno) use ($duracionTurnoAgenda) {
|
|
$inicio = \Illuminate\Support\Carbon::parse((string) $turno->inicio);
|
|
|
|
return [
|
|
'inicio' => $inicio->copy(),
|
|
'fin' => $inicio->copy()->addMinutes($duracionTurnoAgenda),
|
|
];
|
|
})
|
|
->values();
|
|
|
|
$rangosNoDisponibles = collect()
|
|
->concat($rangosTurnosOcupados)
|
|
->concat($eventosReceso)
|
|
->concat($eventosVacaciones)
|
|
->concat($eventosFeriados)
|
|
->map(function ($evento) {
|
|
$inicio = isset($evento['start']) ? \Illuminate\Support\Carbon::parse((string) $evento['start']) : null;
|
|
$fin = isset($evento['end']) ? \Illuminate\Support\Carbon::parse((string) $evento['end']) : null;
|
|
|
|
if (isset($evento['inicio'], $evento['fin'])) {
|
|
return ['inicio' => $evento['inicio']->copy(), 'fin' => $evento['fin']->copy()];
|
|
}
|
|
|
|
if (!$inicio || !$fin) {
|
|
return null;
|
|
}
|
|
|
|
return ['inicio' => $inicio, 'fin' => $fin];
|
|
})
|
|
->filter()
|
|
->values();
|
|
|
|
$eventosDisponibilidad = $eventosDisponibilidad
|
|
->flatMap(function ($evento) use ($rangosNoDisponibles) {
|
|
if (!isset($evento['start'], $evento['end'])) {
|
|
return [$evento];
|
|
}
|
|
|
|
$inicioEvento = \Illuminate\Support\Carbon::parse((string) $evento['start']);
|
|
$finEvento = \Illuminate\Support\Carbon::parse((string) $evento['end']);
|
|
|
|
$rangosSolapados = $rangosNoDisponibles
|
|
->filter(function ($rango) use ($inicioEvento, $finEvento) {
|
|
return $inicioEvento->lt($rango['fin']) && $finEvento->gt($rango['inicio']);
|
|
})
|
|
->map(function ($rango) use ($inicioEvento, $finEvento) {
|
|
$inicio = $rango['inicio']->copy()->greaterThan($inicioEvento) ? $rango['inicio']->copy() : $inicioEvento->copy();
|
|
$fin = $rango['fin']->copy()->lessThan($finEvento) ? $rango['fin']->copy() : $finEvento->copy();
|
|
|
|
return ['inicio' => $inicio, 'fin' => $fin];
|
|
})
|
|
->values()
|
|
->all();
|
|
|
|
if (empty($rangosSolapados)) {
|
|
return [$evento];
|
|
}
|
|
|
|
usort($rangosSolapados, function ($a, $b) {
|
|
if ($a['inicio']->equalTo($b['inicio'])) {
|
|
return 0;
|
|
}
|
|
|
|
return $a['inicio']->lessThan($b['inicio']) ? -1 : 1;
|
|
});
|
|
|
|
$segmentos = [
|
|
['inicio' => $inicioEvento->copy(), 'fin' => $finEvento->copy()],
|
|
];
|
|
|
|
foreach ($rangosSolapados as $rango) {
|
|
$nuevosSegmentos = [];
|
|
|
|
foreach ($segmentos as $segmento) {
|
|
if ($segmento['fin']->lessThanOrEqualTo($rango['inicio']) || $segmento['inicio']->greaterThanOrEqualTo($rango['fin'])) {
|
|
$nuevosSegmentos[] = $segmento;
|
|
continue;
|
|
}
|
|
|
|
if ($segmento['inicio']->lt($rango['inicio'])) {
|
|
$nuevosSegmentos[] = [
|
|
'inicio' => $segmento['inicio']->copy(),
|
|
'fin' => $rango['inicio']->copy()->subSecond(),
|
|
];
|
|
}
|
|
|
|
if ($segmento['fin']->gt($rango['fin'])) {
|
|
$nuevosSegmentos[] = [
|
|
'inicio' => $rango['fin']->copy()->addSecond(),
|
|
'fin' => $segmento['fin']->copy(),
|
|
];
|
|
}
|
|
}
|
|
|
|
$segmentos = $nuevosSegmentos;
|
|
}
|
|
|
|
return collect($segmentos)
|
|
->filter(fn ($segmento) => $segmento['inicio']->lt($segmento['fin']))
|
|
->values()
|
|
->map(function ($segmento, $index) use ($evento) {
|
|
$eventoSegmentado = $evento;
|
|
$eventoSegmentado['id'] = (string) ($evento['id'] ?? 'disp') . '-seg-' . $index;
|
|
$eventoSegmentado['start'] = $segmento['inicio']->format('Y-m-d\TH:i:s');
|
|
$eventoSegmentado['end'] = $segmento['fin']->format('Y-m-d\TH:i:s');
|
|
|
|
return $eventoSegmentado;
|
|
})
|
|
->all();
|
|
})
|
|
->values();
|
|
|
|
$eventosAgenda = collect()
|
|
->concat($eventosFeriados)
|
|
->concat($eventosVacaciones)
|
|
->concat($eventosReceso)
|
|
->concat($eventosAgenda)
|
|
->concat($eventosDisponibilidad)
|
|
->values();
|
|
|
|
$turnosNoCancelados = $turnos->filter(function ($turno) {
|
|
if (!$turno->inicio) {
|
|
return false;
|
|
}
|
|
|
|
$estadoDescripcion = mb_strtolower(trim((string) ($turno->estadoTurno?->descripcion ?? '')));
|
|
|
|
return $estadoDescripcion !== 'cancelado';
|
|
});
|
|
|
|
return view('profesional.dashboard', [
|
|
'quienesSomos' => $quienesSomos,
|
|
'profesionales' => $profesionales,
|
|
'servicios' => $servicios,
|
|
'profesiones' => $profesiones,
|
|
'modalidades' => $modalidades,
|
|
'eventosAgenda' => $eventosAgenda,
|
|
'cantidadTurnosHoy' => $turnosNoCancelados
|
|
->filter(fn ($turno) => $turno->inicio && $turno->inicio->isToday())
|
|
->count(),
|
|
'cantidadTurnosFuturos' => $turnosNoCancelados
|
|
->filter(fn ($turno) => $turno->inicio && $turno->inicio->greaterThan(now()->endOfDay()))
|
|
->count(),
|
|
]);
|
|
});
|
|
|
|
Route::get('/profesional/ayuda', function (Request $request) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$profesionalSesionId = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->value('id');
|
|
|
|
if (!$profesionalSesionId) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
return view('profesional.ayuda');
|
|
});
|
|
|
|
Route::get('/profesional/clientes', function (Request $request) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$profesionalSesionId = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->value('id');
|
|
|
|
if (!$profesionalSesionId) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$filtroTexto = trim((string) $request->query('q', ''));
|
|
$filtroEstado = trim((string) $request->query('estado', ''));
|
|
|
|
$clientes = Cliente::with('persona')
|
|
->whereHas('profesionales', fn ($q) => $q->where('profesionales.id', (int) $profesionalSesionId))
|
|
->when($filtroTexto !== '', function ($query) use ($filtroTexto) {
|
|
$query->where(function ($subQuery) use ($filtroTexto): void {
|
|
$subQuery->where('dni', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('correo', 'like', '%' . $filtroTexto . '%')
|
|
->orWhereHas('persona', function ($qPersona) use ($filtroTexto): void {
|
|
$qPersona->where('nombre', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('apellido', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('dni', 'like', '%' . $filtroTexto . '%');
|
|
});
|
|
});
|
|
})
|
|
->when($filtroEstado === 'activos', fn ($query) => $query->where('baja_id', 1))
|
|
->when($filtroEstado === 'inactivos', fn ($query) => $query->where('baja_id', '!=', 1))
|
|
->orderBy('id')
|
|
->paginate(10)
|
|
->withQueryString();
|
|
|
|
return view('profesional.mis-clientes', [
|
|
'clientes' => $clientes,
|
|
]);
|
|
});
|
|
|
|
Route::get('/profesional/mis-datos', function (Request $request) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
|
|
$profesional = Profesional::with(['persona.Foto', 'profesion', 'credencialProfesional'])
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesional) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
return view('profesional.mis-datos', [
|
|
'profesional' => $profesional,
|
|
]);
|
|
});
|
|
|
|
Route::get('/profesional/configurar-agenda', function (Request $request) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesional) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$agenda = Agenda::query()->firstOrCreate(
|
|
['profesional_id' => (int) $profesional->id],
|
|
['estado' => 'activo', 'duracionturno' => 30]
|
|
);
|
|
|
|
$diasCatalogo = Dia::query()->orderBy('id')->get(['id', 'descripcion']);
|
|
|
|
$diasAtencion = DiaDeAtencion::query()
|
|
->with(['dias', 'horariosAtenciones', 'horariosRecesos'])
|
|
->where('agenda_id', (int) $agenda->id)
|
|
->get();
|
|
|
|
$bloquesAtencion = $diasAtencion
|
|
->flatMap(function ($diaAtencion) {
|
|
return $diaAtencion->horariosAtenciones->map(function ($horario) use ($diaAtencion) {
|
|
return [
|
|
'dia_id' => (int) ($diaAtencion->dia_id ?? 0),
|
|
'horariocomienzo' => substr((string) $horario->horariocomienzo, 0, 5),
|
|
'horariofin' => substr((string) $horario->horariofin, 0, 5),
|
|
'tipo' => strtoupper((string) $horario->tipo),
|
|
];
|
|
});
|
|
})
|
|
->values();
|
|
|
|
$bloquesReceso = $diasAtencion
|
|
->flatMap(function ($diaAtencion) {
|
|
return $diaAtencion->horariosRecesos->map(function ($receso) use ($diaAtencion) {
|
|
return [
|
|
'dia_id' => (int) ($diaAtencion->dia_id ?? 0),
|
|
'comienzo' => substr((string) $receso->comienzo, 0, 5),
|
|
'fin' => substr((string) $receso->fin, 0, 5),
|
|
];
|
|
});
|
|
})
|
|
->values();
|
|
|
|
$vacaciones = ModoVacaciones::query()
|
|
->where('agenda_id', (int) $agenda->id)
|
|
->orderBy('inicio')
|
|
->get(['inicio', 'fin', 'descripcion'])
|
|
->map(fn ($v) => [
|
|
'inicio' => (string) $v->inicio,
|
|
'fin' => (string) $v->fin,
|
|
'descripcion' => (string) $v->descripcion,
|
|
])
|
|
->values();
|
|
|
|
$feriados = Feriado::query()
|
|
->where('agenda_id', (int) $agenda->id)
|
|
->orderBy('fecha')
|
|
->get(['fecha', 'descripcion'])
|
|
->map(fn ($f) => [
|
|
'fecha' => (string) $f->fecha,
|
|
'descripcion' => (string) $f->descripcion,
|
|
])
|
|
->values();
|
|
|
|
return view('profesional.configurar-agenda', [
|
|
'diasCatalogo' => $diasCatalogo,
|
|
'bloquesAtencion' => $bloquesAtencion,
|
|
'bloquesReceso' => $bloquesReceso,
|
|
'vacaciones' => $vacaciones,
|
|
'feriados' => $feriados,
|
|
'duracionTurno' => (int) ($agenda->duracionturno ?? 30),
|
|
]);
|
|
});
|
|
|
|
Route::post('/profesional/configurar-agenda', function (Request $request) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesional) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$agenda = Agenda::query()->firstOrCreate(
|
|
['profesional_id' => (int) $profesional->id],
|
|
['estado' => 'activo', 'duracionturno' => 30]
|
|
);
|
|
|
|
$duracionTurno = (int) $request->input('duracionturno', $agenda->duracionturno ?? 30);
|
|
|
|
$diaIdsValidos = Dia::query()->pluck('id')->map(fn ($id) => (int) $id)->all();
|
|
$diaIdsLookup = array_flip($diaIdsValidos);
|
|
|
|
$diasInput = collect($request->input('dias', []))
|
|
->map(function ($fila) {
|
|
return [
|
|
'dia_id' => (int) ($fila['dia_id'] ?? 0),
|
|
'horariocomienzo' => trim((string) ($fila['horariocomienzo'] ?? '')),
|
|
'horariofin' => trim((string) ($fila['horariofin'] ?? '')),
|
|
'tipo' => strtoupper(trim((string) ($fila['tipo'] ?? ''))),
|
|
];
|
|
})
|
|
->filter(fn ($fila) => $fila['dia_id'] > 0 || $fila['horariocomienzo'] !== '' || $fila['horariofin'] !== '' || $fila['tipo'] !== '')
|
|
->values();
|
|
|
|
$recesosInput = collect($request->input('recesos', []))
|
|
->map(function ($fila) {
|
|
return [
|
|
'dia_id' => (int) ($fila['dia_id'] ?? 0),
|
|
'comienzo' => trim((string) ($fila['comienzo'] ?? '')),
|
|
'fin' => trim((string) ($fila['fin'] ?? '')),
|
|
];
|
|
})
|
|
->filter(fn ($fila) => $fila['dia_id'] > 0 || $fila['comienzo'] !== '' || $fila['fin'] !== '')
|
|
->values();
|
|
|
|
$vacacionesInput = collect($request->input('vacaciones', []))
|
|
->map(function ($fila) {
|
|
return [
|
|
'inicio' => trim((string) ($fila['inicio'] ?? '')),
|
|
'fin' => trim((string) ($fila['fin'] ?? '')),
|
|
'descripcion' => trim((string) ($fila['descripcion'] ?? '')),
|
|
];
|
|
})
|
|
->filter(fn ($fila) => $fila['inicio'] !== '' || $fila['fin'] !== '' || $fila['descripcion'] !== '')
|
|
->values();
|
|
|
|
$feriadosInput = collect($request->input('feriados', []))
|
|
->map(function ($fila) {
|
|
return [
|
|
'fecha' => trim((string) ($fila['fecha'] ?? '')),
|
|
'descripcion' => trim((string) ($fila['descripcion'] ?? '')),
|
|
];
|
|
})
|
|
->filter(fn ($fila) => $fila['fecha'] !== '' || $fila['descripcion'] !== '')
|
|
->values();
|
|
|
|
$errores = [];
|
|
|
|
if ($duracionTurno < 10 || $duracionTurno > 180 || $duracionTurno % 5 !== 0) {
|
|
$errores['duracionturno'] = 'La duración del turno debe ser un número entre 10 y 180 minutos, en intervalos de 5.';
|
|
}
|
|
|
|
if ($diasInput->count() > 10) {
|
|
$errores['dias'] = 'Solo puedes cargar hasta 10 horarios de atención.';
|
|
}
|
|
|
|
$cantidadAM = $diasInput->where('tipo', 'AM')->count();
|
|
$cantidadPM = $diasInput->where('tipo', 'PM')->count();
|
|
|
|
if ($cantidadAM > 5) {
|
|
$errores['dias_am'] = 'Solo puedes cargar hasta 5 horarios de tipo AM.';
|
|
}
|
|
|
|
if ($cantidadPM > 5) {
|
|
$errores['dias_pm'] = 'Solo puedes cargar hasta 5 horarios de tipo PM.';
|
|
}
|
|
|
|
foreach ($diasInput as $index => $fila) {
|
|
if (!isset($diaIdsLookup[$fila['dia_id']])) {
|
|
$errores['dias.' . $index . '.dia_id'] = 'El día seleccionado no es válido.';
|
|
}
|
|
|
|
if (!in_array($fila['tipo'], ['AM', 'PM'], true)) {
|
|
$errores['dias.' . $index . '.tipo'] = 'El tipo debe ser AM o PM.';
|
|
}
|
|
|
|
if ($fila['horariocomienzo'] === '' || $fila['horariofin'] === '') {
|
|
$errores['dias.' . $index . '.horario'] = 'Cada horario de atención debe tener inicio y fin.';
|
|
} elseif ($fila['horariocomienzo'] >= $fila['horariofin']) {
|
|
$errores['dias.' . $index . '.rango'] = 'El horario de inicio debe ser menor al de fin.';
|
|
}
|
|
}
|
|
|
|
foreach ($recesosInput as $index => $fila) {
|
|
if (!isset($diaIdsLookup[$fila['dia_id']])) {
|
|
$errores['recesos.' . $index . '.dia_id'] = 'El día del receso no es válido.';
|
|
}
|
|
|
|
if ($fila['comienzo'] === '' || $fila['fin'] === '') {
|
|
$errores['recesos.' . $index . '.horario'] = 'Cada receso debe tener horario de inicio y fin.';
|
|
} elseif ($fila['comienzo'] >= $fila['fin']) {
|
|
$errores['recesos.' . $index . '.rango'] = 'El receso debe tener inicio menor que fin.';
|
|
}
|
|
}
|
|
|
|
foreach ($vacacionesInput as $index => $fila) {
|
|
if ($fila['inicio'] === '' || $fila['fin'] === '') {
|
|
$errores['vacaciones.' . $index . '.fechas'] = 'Cada vacaciones debe tener fecha de inicio y fin.';
|
|
continue;
|
|
}
|
|
|
|
if ($fila['inicio'] > $fila['fin']) {
|
|
$errores['vacaciones.' . $index . '.rango'] = 'La fecha de inicio de vacaciones no puede ser mayor al fin.';
|
|
}
|
|
}
|
|
|
|
foreach ($feriadosInput as $index => $fila) {
|
|
if ($fila['fecha'] === '') {
|
|
$errores['feriados.' . $index . '.fecha'] = 'Cada feriado debe tener fecha.';
|
|
}
|
|
}
|
|
|
|
if (!empty($errores)) {
|
|
return back()->withErrors($errores)->withInput();
|
|
}
|
|
|
|
DB::transaction(function () use ($agenda, $duracionTurno, $diasInput, $recesosInput, $vacacionesInput, $feriadosInput): void {
|
|
$agenda->update([
|
|
'duracionturno' => $duracionTurno,
|
|
]);
|
|
|
|
$diasExistentes = DiaDeAtencion::query()
|
|
->where('agenda_id', (int) $agenda->id)
|
|
->pluck('id')
|
|
->map(fn ($id) => (int) $id)
|
|
->all();
|
|
|
|
if (!empty($diasExistentes)) {
|
|
HorarioDeAtencion::query()->whereIn('diadeatencion_id', $diasExistentes)->delete();
|
|
HorarioReceso::query()->whereIn('diadeatencion_id', $diasExistentes)->delete();
|
|
DiaDeAtencion::query()->whereIn('id', $diasExistentes)->delete();
|
|
}
|
|
|
|
ModoVacaciones::query()->where('agenda_id', (int) $agenda->id)->delete();
|
|
Feriado::query()->where('agenda_id', (int) $agenda->id)->delete();
|
|
|
|
$diaAtencionPorDia = [];
|
|
|
|
foreach ($diasInput as $fila) {
|
|
$diaId = (int) $fila['dia_id'];
|
|
|
|
if (!isset($diaAtencionPorDia[$diaId])) {
|
|
$diaAtencionPorDia[$diaId] = DiaDeAtencion::create([
|
|
'agenda_id' => (int) $agenda->id,
|
|
'dia_id' => $diaId,
|
|
]);
|
|
}
|
|
|
|
HorarioDeAtencion::create([
|
|
'horariocomienzo' => $fila['horariocomienzo'],
|
|
'horariofin' => $fila['horariofin'],
|
|
'tipo' => $fila['tipo'],
|
|
'diadeatencion_id' => (int) $diaAtencionPorDia[$diaId]->id,
|
|
]);
|
|
}
|
|
|
|
foreach ($recesosInput as $fila) {
|
|
$diaId = (int) $fila['dia_id'];
|
|
|
|
if (!isset($diaAtencionPorDia[$diaId])) {
|
|
$diaAtencionPorDia[$diaId] = DiaDeAtencion::create([
|
|
'agenda_id' => (int) $agenda->id,
|
|
'dia_id' => $diaId,
|
|
]);
|
|
}
|
|
|
|
HorarioReceso::create([
|
|
'comienzo' => $fila['comienzo'],
|
|
'fin' => $fila['fin'],
|
|
'diadeatencion_id' => (int) $diaAtencionPorDia[$diaId]->id,
|
|
]);
|
|
}
|
|
|
|
foreach ($vacacionesInput as $fila) {
|
|
ModoVacaciones::create([
|
|
'inicio' => $fila['inicio'],
|
|
'fin' => $fila['fin'],
|
|
'descripcion' => $fila['descripcion'] !== '' ? $fila['descripcion'] : 'Vacaciones :)',
|
|
'agenda_id' => (int) $agenda->id,
|
|
]);
|
|
}
|
|
|
|
foreach ($feriadosInput as $fila) {
|
|
Feriado::create([
|
|
'fecha' => $fila['fecha'],
|
|
'descripcion' => $fila['descripcion'] !== '' ? $fila['descripcion'] : 'Dia no laborable',
|
|
'agenda_id' => (int) $agenda->id,
|
|
]);
|
|
}
|
|
});
|
|
|
|
return redirect('/profesional/configurar-agenda')->with('agenda_success', 'Agenda actualizada correctamente.');
|
|
});
|
|
|
|
Route::get('/profesional/clientes/crear', function (Request $request) {
|
|
$dniConsultado = trim((string) $request->query('dni', old('dni', '')));
|
|
$personaDni = null;
|
|
$estadoDni = null;
|
|
|
|
if ($dniConsultado !== '') {
|
|
$personaDni = Persona::query()
|
|
->where('dni', $dniConsultado)
|
|
->first();
|
|
|
|
if ($personaDni) {
|
|
$clienteExistente = Cliente::query()
|
|
->where('persona_id', $personaDni->id)
|
|
->orWhere('dni', $dniConsultado)
|
|
->exists();
|
|
|
|
$estadoDni = $clienteExistente ? 'cliente-existente' : 'persona-existente';
|
|
} else {
|
|
$estadoDni = 'persona-nueva';
|
|
}
|
|
}
|
|
|
|
return view('profesional.agregar-cliente', [
|
|
'dniConsultado' => $dniConsultado,
|
|
'personaDni' => $personaDni,
|
|
'estadoDni' => $estadoDni,
|
|
]);
|
|
});
|
|
|
|
Route::post('/profesional/clientes/store', function (Request $request) use ($registrarLogSeguridadProfesional) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
$profesionalSesionId = (int) ($profesional?->id ?? 0);
|
|
|
|
if (!$profesionalSesionId) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'dni' => ['required', 'string', 'max:20', 'regex:/^[0-9A-Za-z]{7,20}$/'],
|
|
'nombre' => ['nullable', 'string', 'max:100', 'regex:/^[\pL\s]+$/u'],
|
|
'apellido' => ['nullable', 'string', 'max:100', 'regex:/^[\pL\s]+$/u'],
|
|
'cuil' => ['nullable', 'string', 'max:30', 'regex:/^[0-9]+$/'],
|
|
'correo' => ['required', 'email', 'max:255', 'unique:credencialesclientes,correo'],
|
|
'fechanac' => ['nullable', 'date'],
|
|
'telefono' => ['nullable', 'string', 'max:30', 'regex:/^[0-9]+$/'],
|
|
]);
|
|
|
|
$personaExistente = Persona::query()
|
|
->where('dni', $validated['dni'])
|
|
->first();
|
|
|
|
if ($personaExistente) {
|
|
$clienteExistente = Cliente::query()
|
|
->where('persona_id', $personaExistente->id)
|
|
->orWhere('dni', $validated['dni'])
|
|
->exists();
|
|
|
|
if ($clienteExistente) {
|
|
return back()
|
|
->withErrors(['dni' => 'El DNI ingresado ya pertenece a un cliente registrado.'])
|
|
->withInput();
|
|
}
|
|
} else {
|
|
$mensajesError = [];
|
|
|
|
if (blank($validated['nombre'] ?? null)) {
|
|
$mensajesError['nombre'] = 'El nombre es obligatorio cuando el DNI no pertenece a una persona registrada.';
|
|
}
|
|
|
|
if (blank($validated['apellido'] ?? null)) {
|
|
$mensajesError['apellido'] = 'El apellido es obligatorio cuando el DNI no pertenece a una persona registrada.';
|
|
}
|
|
|
|
if (blank($validated['cuil'] ?? null)) {
|
|
$mensajesError['cuil'] = 'El CUIL es obligatorio cuando el DNI no pertenece a una persona registrada.';
|
|
}
|
|
|
|
if (blank($validated['fechanac'] ?? null)) {
|
|
$mensajesError['fechanac'] = 'La fecha de nacimiento es obligatoria cuando el DNI no pertenece a una persona registrada.';
|
|
}
|
|
|
|
if (blank($validated['telefono'] ?? null)) {
|
|
$mensajesError['telefono'] = 'El telefono es obligatorio cuando el DNI no pertenece a una persona registrada.';
|
|
}
|
|
|
|
if (!empty($mensajesError)) {
|
|
return back()
|
|
->withErrors($mensajesError)
|
|
->withInput();
|
|
}
|
|
}
|
|
|
|
$clienteCreado = null;
|
|
|
|
DB::transaction(function () use ($validated, $personaExistente, $profesionalSesionId, &$clienteCreado): void {
|
|
$credencialCliente = \App\Models\CredencialCliente::create([
|
|
'correo' => $validated['correo'],
|
|
'contra' => Hash::make($validated['dni']),
|
|
'token' => null,
|
|
'fecha_hora' => now(),
|
|
]);
|
|
|
|
$persona = $personaExistente;
|
|
|
|
if (!$persona) {
|
|
$fotoDefaultId = Foto::query()
|
|
->where('ruta', 'images/avatar_default.png')
|
|
->value('id');
|
|
|
|
if (!$fotoDefaultId) {
|
|
$fotoDefaultId = Foto::query()->create([
|
|
'extension' => 'png',
|
|
'tamanio_bytes' => 0,
|
|
'nombre' => 'avatar_default',
|
|
'mime_type' => 'image/png',
|
|
'ruta' => 'images/avatar_default.png',
|
|
])->id;
|
|
}
|
|
|
|
$persona = Persona::create([
|
|
'dni' => $validated['dni'],
|
|
'nombre' => $validated['nombre'],
|
|
'apellido' => $validated['apellido'],
|
|
'cuil' => $validated['cuil'] ?? null,
|
|
'fechanac' => $validated['fechanac'] ?? null,
|
|
'foto_id' => (int) $fotoDefaultId,
|
|
]);
|
|
|
|
$telefono = Telefono::create([
|
|
'telefono' => $validated['telefono'],
|
|
]);
|
|
|
|
DB::table('personas_telefonos')->insert([
|
|
'dni' => $validated['dni'],
|
|
'persona_id' => $persona->id,
|
|
'telefono_id' => $telefono->id,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
$cliente = Cliente::create([
|
|
'dni' => $validated['dni'],
|
|
'correo' => $validated['correo'],
|
|
'credencialcliente_id' => $credencialCliente->id,
|
|
'persona_id' => $persona->id,
|
|
'baja_id' => 1,
|
|
]);
|
|
|
|
$clienteCreado = $cliente;
|
|
|
|
DB::table('profesionales_clientes')->insertOrIgnore([
|
|
'profesional_id' => $profesionalSesionId,
|
|
'cliente_id' => $cliente->id,
|
|
'estadorelacion' => 'Activo',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
});
|
|
|
|
if ($clienteCreado) {
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'Creación nuevo cliente',
|
|
'El profesional ID ' . $profesionalSesionId . ' creó el cliente ID ' . $clienteCreado->id . ' con DNI ' . $clienteCreado->dni . '.'
|
|
);
|
|
}
|
|
|
|
return redirect('/profesional/clientes')->with('success', 'Cliente agregado correctamente.');
|
|
});
|
|
|
|
Route::get('/profesional/clientes/{cliente}/editar', function (Request $request, Cliente $cliente) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$profesionalSesionId = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->value('id');
|
|
|
|
if (!$profesionalSesionId) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$clientePerteneceAlProfesional = DB::table('profesionales_clientes')
|
|
->where('profesional_id', (int) $profesionalSesionId)
|
|
->where('cliente_id', (int) $cliente->id)
|
|
->exists();
|
|
|
|
if (!$clientePerteneceAlProfesional) {
|
|
return redirect('/profesional/clientes')->withErrors(['cliente' => 'No puedes editar un cliente no asignado a tu cuenta.']);
|
|
}
|
|
|
|
$cliente->load(['persona', 'credencialCliente']);
|
|
$telefonoActual = $cliente->persona?->telefonos()->first();
|
|
|
|
return view('profesional.editar-cliente', [
|
|
'cliente' => $cliente,
|
|
'telefonoActual' => $telefonoActual,
|
|
]);
|
|
});
|
|
|
|
Route::post('/profesional/clientes/{cliente}/actualizar', function (Request $request, Cliente $cliente) use ($registrarLogSeguridadProfesional) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesional) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$clientePerteneceAlProfesional = DB::table('profesionales_clientes')
|
|
->where('profesional_id', (int) $profesional->id)
|
|
->where('cliente_id', (int) $cliente->id)
|
|
->exists();
|
|
|
|
if (!$clientePerteneceAlProfesional) {
|
|
return redirect('/profesional/clientes')->withErrors(['cliente' => 'No puedes editar un cliente no asignado a tu cuenta.']);
|
|
}
|
|
|
|
$cliente->load(['persona', 'credencialCliente']);
|
|
$dniAnterior = (string) $cliente->dni;
|
|
|
|
$dniRules = [
|
|
'required',
|
|
'string',
|
|
'regex:/^[0-9A-Za-z]{7,20}$/',
|
|
Rule::unique('clientes', 'dni')->ignore((int) $cliente->id),
|
|
];
|
|
|
|
if ($cliente->persona) {
|
|
$dniRules[] = Rule::unique('personas', 'dni')->ignore((int) $cliente->persona->id);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'dni' => $dniRules,
|
|
'nombre' => ['required', 'string', 'max:100', 'regex:/^[\pL\s]+$/u'],
|
|
'apellido' => ['required', 'string', 'max:100', 'regex:/^[\pL\s]+$/u'],
|
|
'cuil' => ['required', 'string', 'regex:/^[0-9]{11}$/'],
|
|
'fechanac' => ['required', 'date', 'before_or_equal:today'],
|
|
'correo' => [
|
|
'required',
|
|
'email',
|
|
'max:255',
|
|
Rule::unique('clientes', 'correo')->ignore((int) $cliente->id),
|
|
Rule::unique('credencialesclientes', 'correo')->ignore((int) ($cliente->credencialcliente_id ?? 0)),
|
|
],
|
|
'telefono' => ['required', 'string', 'regex:/^[0-9]{8,15}$/'],
|
|
]);
|
|
|
|
DB::transaction(function () use ($cliente, $validated, $profesional): void {
|
|
if ($cliente->persona) {
|
|
$cliente->persona->update([
|
|
'dni' => $validated['dni'],
|
|
'nombre' => $validated['nombre'],
|
|
'apellido' => $validated['apellido'],
|
|
'cuil' => $validated['cuil'],
|
|
'fechanac' => $validated['fechanac'],
|
|
]);
|
|
|
|
DB::table('personas_telefonos')
|
|
->where('persona_id', (int) $cliente->persona->id)
|
|
->update([
|
|
'dni' => $validated['dni'],
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$telefonoExistente = $cliente->persona->telefonos()->first();
|
|
|
|
if ($telefonoExistente) {
|
|
$telefonoExistente->update([
|
|
'telefono' => $validated['telefono'],
|
|
]);
|
|
} else {
|
|
$telefonoNuevo = Telefono::create([
|
|
'telefono' => $validated['telefono'],
|
|
]);
|
|
|
|
DB::table('personas_telefonos')->insertOrIgnore([
|
|
'dni' => $validated['dni'],
|
|
'persona_id' => (int) $cliente->persona->id,
|
|
'telefono_id' => (int) $telefonoNuevo->id,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
$cliente->update([
|
|
'dni' => $validated['dni'],
|
|
'correo' => $validated['correo'],
|
|
]);
|
|
|
|
DB::table('profesionales_clientes')
|
|
->where('profesional_id', (int) $profesional->id)
|
|
->where('cliente_id', (int) $cliente->id)
|
|
->update([
|
|
'estadorelacion' => 'Activo',
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
if ($cliente->credencialCliente) {
|
|
$cliente->credencialCliente->update([
|
|
'correo' => $validated['correo'],
|
|
]);
|
|
}
|
|
});
|
|
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'Edición datos cliente',
|
|
'El profesional ID ' . (int) $profesional->id . ' editó el cliente ID ' . (int) $cliente->id . '.'
|
|
);
|
|
|
|
if ($dniAnterior !== (string) $validated['dni']) {
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'Cambio de DNI Cliente',
|
|
'Cambio de DNI del cliente ID ' . (int) $cliente->id . ': "' . $dniAnterior . '" -> "' . $validated['dni'] . '".'
|
|
);
|
|
}
|
|
|
|
return redirect('/profesional/clientes')->with('success', 'Cliente actualizado correctamente.');
|
|
});
|
|
|
|
Route::post('/profesional/clientes/{cliente}/baja', function (Request $request, Cliente $cliente) use ($registrarLogSeguridadProfesional) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesional) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$clientePerteneceAlProfesional = DB::table('profesionales_clientes')
|
|
->where('profesional_id', (int) $profesional->id)
|
|
->where('cliente_id', (int) $cliente->id)
|
|
->exists();
|
|
|
|
if (!$clientePerteneceAlProfesional) {
|
|
return redirect('/profesional/clientes')->withErrors(['cliente' => 'No puedes modificar el estado de un cliente no asignado a tu cuenta.']);
|
|
}
|
|
|
|
$estabaDeBaja = (int) $cliente->baja_id !== 1;
|
|
$nombreCliente = trim(($cliente->persona?->nombre ?? '') . ' ' . ($cliente->persona?->apellido ?? ''));
|
|
|
|
DB::transaction(function () use ($cliente, $profesional): void {
|
|
if ((int) $cliente->baja_id !== 1) {
|
|
$cliente->baja_id = 1;
|
|
$cliente->save();
|
|
|
|
DB::table('profesionales_clientes')
|
|
->where('profesional_id', (int) $profesional->id)
|
|
->where('cliente_id', (int) $cliente->id)
|
|
->update([
|
|
'estadorelacion' => 'Activo',
|
|
'updated_at' => now(),
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$bajaId = (int) Baja::query()
|
|
->whereRaw('LOWER(TRIM(descripcion)) = ?', ['baja'])
|
|
->value('id');
|
|
|
|
if ($bajaId <= 0) {
|
|
$bajaId = 2;
|
|
}
|
|
|
|
$cliente->baja_id = $bajaId;
|
|
$cliente->save();
|
|
|
|
DB::table('profesionales_clientes')
|
|
->where('profesional_id', (int) $profesional->id)
|
|
->where('cliente_id', (int) $cliente->id)
|
|
->update([
|
|
'estadorelacion' => 'Baja',
|
|
'updated_at' => now(),
|
|
]);
|
|
});
|
|
|
|
if ($estabaDeBaja) {
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'Dar de alta cliente',
|
|
'El profesional ID ' . (int) $profesional->id . ' dio de alta al cliente ID ' . (int) $cliente->id . ' (' . $nombreCliente . ').'
|
|
);
|
|
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'dio de alta relacion con cliente',
|
|
'El profesional ID ' . (int) $profesional->id . ' dio de alta la relación con el cliente ID ' . (int) $cliente->id . ' (' . $nombreCliente . ').'
|
|
);
|
|
|
|
return redirect('/profesional/clientes')->with('success', 'Cliente reactivado correctamente.');
|
|
}
|
|
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'Dar de baja cliente',
|
|
'El profesional ID ' . (int) $profesional->id . ' dio de baja al cliente ID ' . (int) $cliente->id . ' (' . $nombreCliente . ').'
|
|
);
|
|
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'dio de baja relacion con cliente',
|
|
'El profesional ID ' . (int) $profesional->id . ' dio de baja la relación con el cliente ID ' . (int) $cliente->id . ' (' . $nombreCliente . ').'
|
|
);
|
|
|
|
return redirect('/profesional/clientes')->with('success', 'Cliente dado de baja correctamente.');
|
|
});
|
|
|
|
Route::get('/profesional/clientes/{cliente}/documentacion', function (Request $request, Cliente $cliente) use ($resolverRutaDocumentacionCliente) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesional) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$clientePerteneceAlProfesional = DB::table('profesionales_clientes')
|
|
->where('profesional_id', (int) $profesional->id)
|
|
->where('cliente_id', (int) $cliente->id)
|
|
->exists();
|
|
|
|
if (!$clientePerteneceAlProfesional) {
|
|
return redirect('/profesional/clientes')->withErrors(['cliente' => 'No puedes acceder a la documentación de un cliente no asignado a tu cuenta.']);
|
|
}
|
|
|
|
$cliente->load('persona');
|
|
|
|
$documentos = DocumentacionCliente::query()
|
|
->where('cliente_id', (int) $cliente->id)
|
|
->latest('id')
|
|
->get()
|
|
->map(function (DocumentacionCliente $documento) use ($cliente, $resolverRutaDocumentacionCliente) {
|
|
$rutaArchivo = $resolverRutaDocumentacionCliente((int) $cliente->id, (string) $documento->nombre);
|
|
$documento->ruta_relativa = $rutaArchivo ? ('documentacion-clientes/cliente_' . (int) $cliente->id . '/' . $documento->nombre) : null;
|
|
$documento->url_archivo = $rutaArchivo
|
|
? url('/profesional/clientes/' . $cliente->id . '/documentacion/' . $documento->id . '/ver')
|
|
: null;
|
|
$documento->archivo_existe = $rutaArchivo !== null && File::exists($rutaArchivo);
|
|
|
|
$nombreVisible = preg_replace('/^\d{14}_[a-z0-9]{6}_/i', '', (string) $documento->nombre);
|
|
$documento->nombre_visible = str_replace(['-', '_'], ' ', (string) $nombreVisible);
|
|
|
|
return $documento;
|
|
});
|
|
|
|
return view('profesional.documentacion-cliente', [
|
|
'cliente' => $cliente,
|
|
'documentos' => $documentos,
|
|
]);
|
|
});
|
|
|
|
Route::post('/profesional/clientes/{cliente}/documentacion', function (Request $request, Cliente $cliente) use ($registrarLogSeguridadProfesional, $directorioDocumentacionPrivada) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesional) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$clientePerteneceAlProfesional = DB::table('profesionales_clientes')
|
|
->where('profesional_id', (int) $profesional->id)
|
|
->where('cliente_id', (int) $cliente->id)
|
|
->exists();
|
|
|
|
if (!$clientePerteneceAlProfesional) {
|
|
return redirect('/profesional/clientes')->withErrors(['cliente' => 'No puedes cargar documentación a un cliente no asignado a tu cuenta.']);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'documentos' => ['required', 'array', 'min:1'],
|
|
'documentos.*' => ['required', 'file', 'mimes:pdf,jpg,jpeg,png,webp,doc,docx,xlsx,txt', 'max:5120'],
|
|
]);
|
|
|
|
$directorio = $directorioDocumentacionPrivada((int) $cliente->id);
|
|
|
|
if (!File::isDirectory($directorio)) {
|
|
File::makeDirectory($directorio, 0755, true);
|
|
}
|
|
|
|
foreach ($validated['documentos'] as $archivo) {
|
|
$nombreOriginal = (string) $archivo->getClientOriginalName();
|
|
$mimeType = (string) $archivo->getClientMimeType();
|
|
$rutaTemporal = $archivo->getRealPath();
|
|
$tamanioBytes = ($rutaTemporal && is_file($rutaTemporal)) ? (int) filesize($rutaTemporal) : 0;
|
|
$extension = strtolower((string) ($archivo->getClientOriginalExtension() ?: $archivo->extension() ?: 'bin'));
|
|
$nombreBase = Str::slug(pathinfo($nombreOriginal, PATHINFO_FILENAME));
|
|
|
|
if ($nombreBase === '') {
|
|
$nombreBase = 'documento';
|
|
}
|
|
|
|
$nombreGuardado = now()->format('YmdHis') . '_' . Str::lower(Str::random(6)) . '_' . $nombreBase . '.' . $extension;
|
|
$archivo->move($directorio, $nombreGuardado);
|
|
|
|
DocumentacionCliente::create([
|
|
'nombre' => $nombreGuardado,
|
|
'mime_type' => $mimeType,
|
|
'tamanio_bytes' => $tamanioBytes,
|
|
'extension' => $extension,
|
|
'cliente_id' => (int) $cliente->id,
|
|
'profesional_id' => (int) $profesional->id,
|
|
]);
|
|
}
|
|
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'Agregó documentación cliente',
|
|
'El profesional ID ' . (int) $profesional->id . ' agregó ' . count($validated['documentos']) . ' documento(s) al cliente ID ' . (int) $cliente->id . '.'
|
|
);
|
|
|
|
return redirect('/profesional/clientes/' . $cliente->id . '/documentacion')
|
|
->with('success', 'Documentación agregada correctamente.');
|
|
});
|
|
|
|
Route::get('/profesional/clientes/{cliente}/documentacion/{documentacion}/ver', function (Request $request, Cliente $cliente, DocumentacionCliente $documentacion) use ($resolverRutaDocumentacionCliente) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesional) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$clientePerteneceAlProfesional = DB::table('profesionales_clientes')
|
|
->where('profesional_id', (int) $profesional->id)
|
|
->where('cliente_id', (int) $cliente->id)
|
|
->exists();
|
|
|
|
if (!$clientePerteneceAlProfesional || (int) $documentacion->cliente_id !== (int) $cliente->id) {
|
|
return redirect('/profesional/clientes')->withErrors(['cliente' => 'No puedes visualizar documentación de un cliente no asignado a tu cuenta.']);
|
|
}
|
|
|
|
$rutaArchivo = $resolverRutaDocumentacionCliente((int) $cliente->id, (string) $documentacion->nombre);
|
|
|
|
if (!$rutaArchivo || !File::exists($rutaArchivo)) {
|
|
return redirect('/profesional/clientes/' . $cliente->id . '/documentacion')
|
|
->withErrors(['documento' => 'El archivo no se encuentra disponible para visualizar.']);
|
|
}
|
|
|
|
$nombreDescarga = $documentacion->nombre_visible ?? preg_replace('/^\d{14}_[a-z0-9]{6}_/i', '', (string) $documentacion->nombre);
|
|
$mimeType = trim((string) ($documentacion->mime_type ?? ''));
|
|
|
|
if ($mimeType === '') {
|
|
$mimeType = File::mimeType($rutaArchivo) ?: 'application/octet-stream';
|
|
}
|
|
|
|
return response()->file($rutaArchivo, [
|
|
'Content-Type' => $mimeType,
|
|
'Content-Disposition' => 'inline; filename="' . addslashes((string) $nombreDescarga) . '"',
|
|
'X-Content-Type-Options' => 'nosniff',
|
|
]);
|
|
});
|
|
|
|
Route::get('/profesional/clientes/{cliente}/documentacion/{documentacion}/descargar', function (Request $request, Cliente $cliente, DocumentacionCliente $documentacion) use ($resolverRutaDocumentacionCliente) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesional) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$clientePerteneceAlProfesional = DB::table('profesionales_clientes')
|
|
->where('profesional_id', (int) $profesional->id)
|
|
->where('cliente_id', (int) $cliente->id)
|
|
->exists();
|
|
|
|
if (!$clientePerteneceAlProfesional || (int) $documentacion->cliente_id !== (int) $cliente->id) {
|
|
return redirect('/profesional/clientes')->withErrors(['cliente' => 'No puedes descargar documentación de un cliente no asignado a tu cuenta.']);
|
|
}
|
|
|
|
$rutaArchivo = $resolverRutaDocumentacionCliente((int) $cliente->id, (string) $documentacion->nombre);
|
|
|
|
if (!$rutaArchivo || !File::exists($rutaArchivo)) {
|
|
return redirect('/profesional/clientes/' . $cliente->id . '/documentacion')
|
|
->withErrors(['documento' => 'El archivo no se encuentra disponible para descargar.']);
|
|
}
|
|
|
|
$nombreDescarga = $documentacion->nombre_visible ?? preg_replace('/^\d{14}_[a-z0-9]{6}_/i', '', (string) $documentacion->nombre);
|
|
|
|
return response()->download($rutaArchivo, (string) $nombreDescarga, [
|
|
'X-Content-Type-Options' => 'nosniff',
|
|
]);
|
|
});
|
|
|
|
Route::post('/profesional/clientes/{cliente}/documentacion/{documentacion}/eliminar', function (Request $request, Cliente $cliente, DocumentacionCliente $documentacion) use ($registrarLogSeguridadProfesional, $eliminarDocumentacionCliente) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesional) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$clientePerteneceAlProfesional = DB::table('profesionales_clientes')
|
|
->where('profesional_id', (int) $profesional->id)
|
|
->where('cliente_id', (int) $cliente->id)
|
|
->exists();
|
|
|
|
if (!$clientePerteneceAlProfesional || (int) $documentacion->cliente_id !== (int) $cliente->id) {
|
|
return redirect('/profesional/clientes')->withErrors(['cliente' => 'No puedes eliminar documentación de un cliente no asignado a tu cuenta.']);
|
|
}
|
|
|
|
$eliminarDocumentacionCliente((int) $cliente->id, (string) $documentacion->nombre);
|
|
|
|
$nombreDocumento = (string) $documentacion->nombre;
|
|
$documentacion->delete();
|
|
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'Eliminó documentación cliente',
|
|
'El profesional ID ' . (int) $profesional->id . ' eliminó el documento "' . $nombreDocumento . '" del cliente ID ' . (int) $cliente->id . '.'
|
|
);
|
|
|
|
return redirect('/profesional/clientes/' . $cliente->id . '/documentacion')
|
|
->with('success', 'Documento eliminado correctamente.');
|
|
});
|
|
|
|
Route::get('/profesional/formularios', function (Request $request) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$profesionalSesionId = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->value('id');
|
|
|
|
if (!$profesionalSesionId) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$filtroTexto = trim((string) $request->query('q', ''));
|
|
$filtroEstado = trim((string) $request->query('estado', ''));
|
|
$filtroCliente = trim((string) $request->query('cliente', ''));
|
|
|
|
$formulariosQuery = Formulario::query()
|
|
->with(['profesion', 'servicio', 'modalidad'])
|
|
->whereExists(function ($query) use ($profesionalSesionId): void {
|
|
$query->select(DB::raw(1))
|
|
->from('profesionales_formularios as pf')
|
|
->whereColumn('pf.formulario_id', 'formularios.id')
|
|
->where('pf.profesional_id', (int) $profesionalSesionId);
|
|
});
|
|
|
|
if ($filtroTexto !== '') {
|
|
$formulariosQuery->where(function ($query) use ($filtroTexto): void {
|
|
$query->where('formularios.nombrecompleto', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('formularios.correo', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('formularios.celular', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('formularios.descripcion', 'like', '%' . $filtroTexto . '%')
|
|
->orWhereHas('servicio', function ($qServicio) use ($filtroTexto): void {
|
|
$qServicio->where('titulo', 'like', '%' . $filtroTexto . '%');
|
|
})
|
|
->orWhereHas('profesion', function ($qProfesion) use ($filtroTexto): void {
|
|
$qProfesion->where('titulo', 'like', '%' . $filtroTexto . '%');
|
|
});
|
|
});
|
|
}
|
|
|
|
if ($filtroCliente === 'si') {
|
|
$formulariosQuery->whereNotNull('formularios.cliente_id');
|
|
} elseif ($filtroCliente === 'no') {
|
|
$formulariosQuery->whereNull('formularios.cliente_id');
|
|
}
|
|
|
|
if ($filtroEstado !== '') {
|
|
if ($filtroEstado === 'aceptado_por_otro') {
|
|
$formulariosQuery
|
|
->whereRaw("LOWER(TRIM(formularios.estado)) = 'aceptado'")
|
|
->whereExists(function ($query) use ($profesionalSesionId): void {
|
|
$query->select(DB::raw(1))
|
|
->from('profesionales_formularios as pf')
|
|
->whereColumn('pf.formulario_id', 'formularios.id')
|
|
->where('pf.profesional_id', '!=', (int) $profesionalSesionId)
|
|
->whereRaw("LOWER(TRIM(pf.estado)) = 'aceptado'");
|
|
});
|
|
} elseif ($filtroEstado === 'aceptado') {
|
|
$formulariosQuery
|
|
->whereRaw("LOWER(TRIM(formularios.estado)) = 'aceptado'")
|
|
->whereNotExists(function ($query) use ($profesionalSesionId): void {
|
|
$query->select(DB::raw(1))
|
|
->from('profesionales_formularios as pf')
|
|
->whereColumn('pf.formulario_id', 'formularios.id')
|
|
->where('pf.profesional_id', '!=', (int) $profesionalSesionId)
|
|
->whereRaw("LOWER(TRIM(pf.estado)) = 'aceptado'");
|
|
});
|
|
} elseif ($filtroEstado === 'rechazado') {
|
|
$formulariosQuery->whereRaw("LOWER(TRIM(formularios.estado)) = 'rechazado por todos'");
|
|
} else {
|
|
$formulariosQuery->whereRaw('LOWER(TRIM(formularios.estado)) = ?', [mb_strtolower($filtroEstado)]);
|
|
}
|
|
}
|
|
|
|
$formularios = $formulariosQuery
|
|
->orderByRaw('CASE WHEN formularios.cliente_id IS NOT NULL THEN 0 ELSE 1 END')
|
|
->latest('id')
|
|
->paginate(10)
|
|
->withQueryString();
|
|
|
|
$formulariosPaginaIds = $formularios->getCollection()->pluck('id')->map(fn ($id) => (int) $id)->all();
|
|
$aceptadosPorOtroIds = [];
|
|
|
|
if (!empty($formulariosPaginaIds)) {
|
|
$aceptadosPorOtroIds = DB::table('profesionales_formularios as pf')
|
|
->join('formularios as f', 'f.id', '=', 'pf.formulario_id')
|
|
->whereIn('pf.formulario_id', $formulariosPaginaIds)
|
|
->where('pf.profesional_id', '!=', (int) $profesionalSesionId)
|
|
->whereRaw("LOWER(TRIM(pf.estado)) = 'aceptado'")
|
|
->whereRaw("LOWER(TRIM(f.estado)) = 'aceptado'")
|
|
->pluck('pf.formulario_id')
|
|
->map(fn ($id) => (int) $id)
|
|
->all();
|
|
}
|
|
|
|
$formularios->getCollection()->transform(function ($formulario) use ($aceptadosPorOtroIds) {
|
|
$formulario->aceptado_por_otro = in_array((int) $formulario->id, $aceptadosPorOtroIds, true);
|
|
return $formulario;
|
|
});
|
|
|
|
return view('profesional.revisar-formularios', [
|
|
'formularios' => $formularios,
|
|
]);
|
|
});
|
|
|
|
Route::get('/profesional/formularios/{formulario}', function (Formulario $formulario) use ($buscarTurnoAutomatico, $formularioFueAceptadoPorOtroProfesional) {
|
|
$formulario->load(['profesion', 'servicio', 'modalidad', 'diasPreferidos', 'horariosPreferidos']);
|
|
|
|
$credencialId = (int) request()->session()->get('personal_credencial_id', 0);
|
|
$profesionalSesionId = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->value('id');
|
|
|
|
$profesionalPreferido = null;
|
|
if ($formulario->profesional_id) {
|
|
$profesionalPreferido = Profesional::with('persona')->find($formulario->profesional_id);
|
|
}
|
|
|
|
$estadoProfesionalFormulario = 'Pendiente';
|
|
$formularioAceptadoPorOtro = false;
|
|
$agendaEventosProfesional = collect();
|
|
$turnoAutomaticoSugerido = null;
|
|
if ($profesionalSesionId) {
|
|
$esAsignado = DB::table('profesionales_formularios')
|
|
->where('formulario_id', (int) $formulario->id)
|
|
->where('profesional_id', (int) $profesionalSesionId)
|
|
->exists();
|
|
|
|
if (!$esAsignado) {
|
|
return redirect('/profesional/formularios')
|
|
->with('profesional_action_error', 'No tenes permiso para ver este formulario.');
|
|
}
|
|
|
|
$estadoPivot = DB::table('profesionales_formularios')
|
|
->where('formulario_id', (int) $formulario->id)
|
|
->where('profesional_id', (int) $profesionalSesionId)
|
|
->value('estado');
|
|
|
|
if (is_string($estadoPivot) && trim($estadoPivot) !== '') {
|
|
$estadoProfesionalFormulario = trim($estadoPivot);
|
|
}
|
|
|
|
$formularioAceptadoPorOtro = $formularioFueAceptadoPorOtroProfesional((int) $formulario->id, (int) $profesionalSesionId);
|
|
|
|
$coloresEstado = [
|
|
'confirmado' => '#198754',
|
|
'cancelado' => '#dc3545',
|
|
'reprogramado' => '#fd7e14',
|
|
'cliente ausente' => '#6c757d',
|
|
'cliente presente' => '#0d6efd',
|
|
];
|
|
|
|
$duracionTurnoVisualForm = (int) (Agenda::query()
|
|
->where('profesional_id', (int) $profesionalSesionId)
|
|
->value('duracionturno') ?? 30);
|
|
|
|
$agendaEventosProfesional = Turno::query()
|
|
->with(['servicio', 'modalidad', 'estadoTurno', 'cliente.persona.telefonos'])
|
|
->where('profesional_id', (int) $profesionalSesionId)
|
|
->orderBy('inicio')
|
|
->get()
|
|
->filter(fn ($turno) => $turno->inicio)
|
|
->filter(function ($turno) {
|
|
$estadoDescripcion = mb_strtolower(trim((string) ($turno->estadoTurno?->descripcion ?? '')));
|
|
|
|
return $estadoDescripcion !== 'cancelado';
|
|
})
|
|
->map(function ($turno) use ($coloresEstado, $duracionTurnoVisualForm) {
|
|
$estadoDescripcion = trim((string) ($turno->estadoTurno?->descripcion ?? 'Sin estado'));
|
|
$estadoKey = mb_strtolower($estadoDescripcion);
|
|
$color = $coloresEstado[$estadoKey] ?? '#0d6efd';
|
|
|
|
// Naranja para turnos asignados manualmente (sin cliente vinculado y estado confirmado)
|
|
if ($estadoKey === 'confirmado' && $turno->cliente_id === null) {
|
|
$color = '#fd7e14';
|
|
}
|
|
|
|
$servicioTitulo = trim((string) ($turno->servicio?->titulo ?? 'Turno'));
|
|
$nombreCompleto = trim((string) ($turno->nombrecompleto ?? 'Cliente'));
|
|
$celularCliente = trim((string) ($turno->cliente?->persona?->telefonos?->first()?->telefono ?? ''));
|
|
if ($celularCliente === '') {
|
|
$celularCliente = trim((string) ($turno->celular ?? ''));
|
|
}
|
|
|
|
return [
|
|
'id' => (int) $turno->id,
|
|
'title' => $servicioTitulo . ' - ' . $nombreCompleto,
|
|
'start' => $turno->inicio->format('Y-m-d\TH:i:s'),
|
|
'end' => $turno->inicio->copy()->addMinutes($duracionTurnoVisualForm)->format('Y-m-d\TH:i:s'),
|
|
'backgroundColor' => $color,
|
|
'borderColor' => $color,
|
|
'extendedProps' => [
|
|
'prioridad' => 4,
|
|
'cliente' => $nombreCompleto,
|
|
'correo' => (string) ($turno->correo ?? ''),
|
|
'celular' => $celularCliente !== '' ? $celularCliente : '-',
|
|
'servicio' => $servicioTitulo,
|
|
'estado' => $estadoDescripcion,
|
|
'modalidad' => (string) ($turno->modalidad?->descripcion ?? '-'),
|
|
],
|
|
];
|
|
})
|
|
->values();
|
|
|
|
// Eventos de disponibilidad (verde) y no disponibilidad (rojo)
|
|
$numeroDiaSemanaForm = [
|
|
'domingo' => 0, 'lunes' => 1, 'martes' => 2,
|
|
'miercoles' => 3, 'miércoles' => 3, 'jueves' => 4,
|
|
'viernes' => 5, 'sabado' => 6, 'sábado' => 6,
|
|
];
|
|
|
|
$agendaForm = Agenda::query()
|
|
->with(['diaDeAtencion.dias', 'diaDeAtencion.horariosAtenciones', 'diaDeAtencion.horariosRecesos', 'feriado', 'modoVacaciones'])
|
|
->where('profesional_id', (int) $profesionalSesionId)
|
|
->first();
|
|
|
|
$inicioRangoForm = now()->startOfDay();
|
|
$finRangoForm = now()->addDays(90)->endOfDay();
|
|
|
|
$eventosDispForm = collect($agendaForm?->diaDeAtencion ?? [])
|
|
->flatMap(function ($diaAtencion) use ($numeroDiaSemanaForm, $inicioRangoForm, $finRangoForm) {
|
|
$diaDesc = mb_strtolower(trim((string) ($diaAtencion->dias?->descripcion ?? '')));
|
|
$diaSem = $numeroDiaSemanaForm[$diaDesc] ?? null;
|
|
if ($diaSem === null) {
|
|
return [];
|
|
}
|
|
return collect($diaAtencion->horariosAtenciones ?? [])
|
|
->filter(fn ($h) => $h->horariocomienzo && $h->horariofin)
|
|
->flatMap(function ($horario) use ($diaSem, $diaAtencion, $inicioRangoForm, $finRangoForm) {
|
|
$eventos = [];
|
|
$cursor = $inicioRangoForm->copy();
|
|
while ($cursor->lte($finRangoForm)) {
|
|
if ((int) $cursor->dayOfWeek === (int) $diaSem) {
|
|
$fecha = $cursor->format('Y-m-d');
|
|
$eventos[] = [
|
|
'id' => 'disp-f-' . $diaAtencion->id . '-' . $horario->id . '-' . $fecha,
|
|
'title' => 'Disponibilidad',
|
|
'start' => $fecha . 'T' . substr((string) $horario->horariocomienzo, 0, 8),
|
|
'end' => $fecha . 'T' . substr((string) $horario->horariofin, 0, 8),
|
|
'display' => 'background',
|
|
'classNames' => ['evento-disponibilidad'],
|
|
'backgroundColor' => '#198754',
|
|
'borderColor' => '#198754',
|
|
'extendedProps' => ['prioridad' => 5, 'tipoEvento' => 'disponibilidad', 'estado' => 'Disponible', 'modalidad' => '-'],
|
|
];
|
|
}
|
|
$cursor->addDay();
|
|
}
|
|
return $eventos;
|
|
});
|
|
})
|
|
->values();
|
|
|
|
$eventosRecesoForm = collect($agendaForm?->diaDeAtencion ?? [])
|
|
->flatMap(function ($diaAtencion) use ($numeroDiaSemanaForm, $inicioRangoForm, $finRangoForm) {
|
|
$diaDesc = mb_strtolower(trim((string) ($diaAtencion->dias?->descripcion ?? '')));
|
|
$diaSem = $numeroDiaSemanaForm[$diaDesc] ?? null;
|
|
if ($diaSem === null) {
|
|
return [];
|
|
}
|
|
return collect($diaAtencion->horariosRecesos ?? [])
|
|
->filter(fn ($r) => $r->comienzo && $r->fin)
|
|
->flatMap(function ($receso) use ($diaSem, $diaAtencion, $inicioRangoForm, $finRangoForm) {
|
|
$eventos = [];
|
|
$cursor = $inicioRangoForm->copy();
|
|
while ($cursor->lte($finRangoForm)) {
|
|
if ((int) $cursor->dayOfWeek === (int) $diaSem) {
|
|
$fecha = $cursor->format('Y-m-d');
|
|
$descripcionReceso = trim((string) ($receso->descripcion ?? ''));
|
|
$tituloReceso = $descripcionReceso !== ''
|
|
? 'Receso: ' . $descripcionReceso
|
|
: 'Receso';
|
|
|
|
$eventos[] = [
|
|
'id' => 'receso-f-' . $diaAtencion->id . '-' . $receso->id . '-' . $fecha,
|
|
'title' => $tituloReceso,
|
|
'start' => $fecha . 'T' . substr((string) $receso->comienzo, 0, 8),
|
|
'end' => $fecha . 'T' . substr((string) $receso->fin, 0, 8),
|
|
'display' => 'background',
|
|
'classNames' => ['evento-receso'],
|
|
'textColor' => '#5b1f23',
|
|
'backgroundColor' => '#dc3545',
|
|
'borderColor' => '#dc3545',
|
|
'extendedProps' => ['prioridad' => 3, 'tipoEvento' => 'no_disponible', 'motivo' => 'Receso', 'estado' => 'No disponible'],
|
|
];
|
|
}
|
|
$cursor->addDay();
|
|
}
|
|
return $eventos;
|
|
});
|
|
})
|
|
->values();
|
|
|
|
$eventosVacacionesForm = collect($agendaForm?->modoVacaciones ?? [])
|
|
->filter(fn ($v) => $v->inicio && $v->fin)
|
|
->map(function ($vacacion) {
|
|
$descripcionVacacion = trim((string) ($vacacion->descripcion ?? ''));
|
|
|
|
return [
|
|
'id' => 'vacaciones-f-' . $vacacion->id,
|
|
'title' => $descripcionVacacion !== ''
|
|
? 'Vacaciones: ' . $descripcionVacacion
|
|
: 'Vacaciones',
|
|
'start' => (string) $vacacion->inicio,
|
|
'end' => \Illuminate\Support\Carbon::parse((string) $vacacion->fin)->addDay()->format('Y-m-d'),
|
|
'allDay' => true,
|
|
'display' => 'background',
|
|
'classNames' => ['evento-vacaciones'],
|
|
'textColor' => '#5b1f23',
|
|
'backgroundColor' => '#dc3545',
|
|
'borderColor' => '#dc3545',
|
|
'extendedProps' => ['prioridad' => 2, 'tipoEvento' => 'no_disponible', 'motivo' => 'Vacaciones', 'estado' => 'No disponible'],
|
|
];
|
|
})
|
|
->values();
|
|
|
|
$eventosFeriadosForm = collect($agendaForm?->feriado ?? [])
|
|
->filter(fn ($f) => $f->fecha)
|
|
->map(function ($feriado) {
|
|
$descripcionFeriado = trim((string) ($feriado->descripcion ?? ''));
|
|
|
|
return [
|
|
'id' => 'feriado-f-' . $feriado->id,
|
|
'title' => $descripcionFeriado !== ''
|
|
? 'Feriado: ' . $descripcionFeriado
|
|
: 'Feriado',
|
|
'start' => (string) $feriado->fecha,
|
|
'end' => \Illuminate\Support\Carbon::parse((string) $feriado->fecha)->addDay()->format('Y-m-d'),
|
|
'allDay' => true,
|
|
'display' => 'background',
|
|
'classNames' => ['evento-feriado'],
|
|
'textColor' => '#5b1f23',
|
|
'backgroundColor' => '#dc3545',
|
|
'borderColor' => '#dc3545',
|
|
'extendedProps' => ['prioridad' => 1, 'tipoEvento' => 'no_disponible', 'motivo' => 'Feriado', 'estado' => 'No disponible'],
|
|
];
|
|
})
|
|
->values();
|
|
|
|
$duracionTurnoForm = (int) ($agendaForm?->duracionturno ?? 30);
|
|
|
|
$rangosTurnosOcupadosForm = Turno::query()
|
|
->with('estadoTurno')
|
|
->where('profesional_id', (int) $profesionalSesionId)
|
|
->get()
|
|
->filter(fn ($turno) => $turno->inicio)
|
|
->filter(function ($turno) {
|
|
$estadoDescripcion = mb_strtolower(trim((string) ($turno->estadoTurno?->descripcion ?? '')));
|
|
|
|
return $estadoDescripcion !== 'cancelado';
|
|
})
|
|
->map(function ($turno) use ($duracionTurnoForm) {
|
|
$inicio = \Illuminate\Support\Carbon::parse((string) $turno->inicio);
|
|
|
|
return [
|
|
'inicio' => $inicio->copy(),
|
|
'fin' => $inicio->copy()->addMinutes($duracionTurnoForm),
|
|
];
|
|
})
|
|
->values();
|
|
|
|
$rangosNoDispo = collect()
|
|
->concat($rangosTurnosOcupadosForm)
|
|
->concat($eventosRecesoForm)
|
|
->concat($eventosVacacionesForm)
|
|
->concat($eventosFeriadosForm)
|
|
->map(function ($ev) {
|
|
$ini = isset($ev['start']) ? \Illuminate\Support\Carbon::parse((string) $ev['start']) : null;
|
|
$fin = isset($ev['end']) ? \Illuminate\Support\Carbon::parse((string) $ev['end']) : null;
|
|
|
|
if (isset($ev['inicio'], $ev['fin'])) {
|
|
return ['inicio' => $ev['inicio']->copy(), 'fin' => $ev['fin']->copy()];
|
|
}
|
|
|
|
return ($ini && $fin) ? ['inicio' => $ini, 'fin' => $fin] : null;
|
|
})
|
|
->filter()
|
|
->values();
|
|
|
|
$eventosDispForm = $eventosDispForm
|
|
->flatMap(function ($evento) use ($rangosNoDispo) {
|
|
if (!isset($evento['start'], $evento['end'])) {
|
|
return [$evento];
|
|
}
|
|
$iniEv = \Illuminate\Support\Carbon::parse((string) $evento['start']);
|
|
$finEv = \Illuminate\Support\Carbon::parse((string) $evento['end']);
|
|
$solapados = $rangosNoDispo
|
|
->filter(fn ($r) => $iniEv->lt($r['fin']) && $finEv->gt($r['inicio']))
|
|
->map(fn ($r) => [
|
|
'inicio' => $r['inicio']->gt($iniEv) ? $r['inicio']->copy() : $iniEv->copy(),
|
|
'fin' => $r['fin']->lt($finEv) ? $r['fin']->copy() : $finEv->copy(),
|
|
])
|
|
->values()
|
|
->all();
|
|
if (empty($solapados)) {
|
|
return [$evento];
|
|
}
|
|
usort($solapados, fn ($a, $b) => $a['inicio']->lessThan($b['inicio']) ? -1 : ($a['inicio']->equalTo($b['inicio']) ? 0 : 1));
|
|
$segmentos = [['inicio' => $iniEv->copy(), 'fin' => $finEv->copy()]];
|
|
foreach ($solapados as $rango) {
|
|
$nuevos = [];
|
|
foreach ($segmentos as $seg) {
|
|
if ($seg['fin']->lessThanOrEqualTo($rango['inicio']) || $seg['inicio']->greaterThanOrEqualTo($rango['fin'])) {
|
|
$nuevos[] = $seg;
|
|
continue;
|
|
}
|
|
if ($seg['inicio']->lt($rango['inicio'])) {
|
|
$nuevos[] = ['inicio' => $seg['inicio']->copy(), 'fin' => $rango['inicio']->copy()];
|
|
}
|
|
if ($seg['fin']->gt($rango['fin'])) {
|
|
$nuevos[] = ['inicio' => $rango['fin']->copy(), 'fin' => $seg['fin']->copy()];
|
|
}
|
|
}
|
|
$segmentos = $nuevos;
|
|
}
|
|
return collect($segmentos)
|
|
->filter(fn ($s) => $s['inicio']->lt($s['fin']))
|
|
->values()
|
|
->map(function ($s, $idx) use ($evento) {
|
|
$ev = $evento;
|
|
$ev['id'] = (string) ($evento['id'] ?? 'disp') . '-seg-' . $idx;
|
|
$ev['start'] = $s['inicio']->format('Y-m-d\TH:i:s');
|
|
$ev['end'] = $s['fin']->format('Y-m-d\TH:i:s');
|
|
return $ev;
|
|
})
|
|
->all();
|
|
})
|
|
->values();
|
|
|
|
$agendaEventosProfesional = collect()
|
|
->concat($eventosFeriadosForm)
|
|
->concat($eventosVacacionesForm)
|
|
->concat($eventosRecesoForm)
|
|
->concat($agendaEventosProfesional)
|
|
->concat($eventosDispForm)
|
|
->values();
|
|
|
|
if (!$formularioAceptadoPorOtro) {
|
|
$turnoAutomaticoSugerido = $buscarTurnoAutomatico($formulario, (int) $profesionalSesionId);
|
|
}
|
|
}
|
|
|
|
return view('profesional.detalle-formulario', [
|
|
'formulario' => $formulario,
|
|
'profesionalPreferido' => $profesionalPreferido,
|
|
'estadoProfesionalFormulario' => $estadoProfesionalFormulario,
|
|
'formularioAceptadoPorOtro' => $formularioAceptadoPorOtro,
|
|
'agendaEventosProfesional' => $agendaEventosProfesional,
|
|
'turnoAutomaticoSugerido' => $turnoAutomaticoSugerido,
|
|
]);
|
|
});
|
|
|
|
Route::post('/profesional/formularios/{formulario}/rechazar', function (Request $request, Formulario $formulario) use ($formularioFueAceptadoPorOtroProfesional, $registrarLogSeguridadProfesional, $recalcularEstadoGeneralFormulario) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
$profesionalId = $profesional?->id;
|
|
|
|
if (!$profesionalId) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$estadoRelacionAnterior = mb_strtolower(trim((string) DB::table('profesionales_formularios')
|
|
->where('profesional_id', (int) $profesionalId)
|
|
->where('formulario_id', (int) $formulario->id)
|
|
->value('estado')));
|
|
|
|
$esAsignado = DB::table('profesionales_formularios')
|
|
->where('profesional_id', (int) $profesionalId)
|
|
->where('formulario_id', (int) $formulario->id)
|
|
->exists();
|
|
|
|
if (!$esAsignado) {
|
|
return redirect('/profesional/formularios')
|
|
->with('profesional_action_error', 'No tenes permiso para rechazar este formulario.');
|
|
}
|
|
|
|
if ($formularioFueAceptadoPorOtroProfesional((int) $formulario->id, (int) $profesionalId)) {
|
|
return redirect('/profesional/formularios/' . $formulario->id)
|
|
->with('profesional_action_error', 'Este formulario ya fue aceptado por otro profesional.');
|
|
}
|
|
|
|
$estadoNuevoProfesional = $estadoRelacionAnterior === 'aceptado' ? 'Pendiente' : 'Rechazado';
|
|
|
|
DB::table('profesionales_formularios')
|
|
->where('profesional_id', (int) $profesionalId)
|
|
->where('formulario_id', (int) $formulario->id)
|
|
->update([
|
|
'estado' => $estadoNuevoProfesional,
|
|
]);
|
|
|
|
$recalcularEstadoGeneralFormulario($formulario);
|
|
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
$estadoRelacionAnterior === 'aceptado' ? 'Devolvió un caso' : 'Rechazó un caso',
|
|
$estadoRelacionAnterior === 'aceptado'
|
|
? 'El profesional ID ' . $profesionalId . ' devolvió el formulario ID ' . $formulario->id . '.'
|
|
: 'El profesional ID ' . $profesionalId . ' rechazó el formulario ID ' . $formulario->id . '.'
|
|
);
|
|
|
|
return redirect('/profesional/formularios/' . $formulario->id)
|
|
->with('profesional_action_success', $estadoRelacionAnterior === 'aceptado'
|
|
? 'Formulario devuelto correctamente. Ahora quedó nuevamente pendiente.'
|
|
: 'Formulario rechazado correctamente.');
|
|
});
|
|
|
|
Route::post('/profesional/formularios/{formulario}/asignar-turno-manual', function (Request $request, Formulario $formulario) use ($obtenerAdvertenciasTurno, $verificarConflictoTurno, $formularioFueAceptadoPorOtroProfesional, $enviarCorreoTurno, $registrarLogSeguridadProfesional, $recalcularEstadoGeneralFormulario) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
$profesionalId = (int) ($profesional?->id ?? 0);
|
|
|
|
if (!$profesionalId) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$esAsignado = DB::table('profesionales_formularios')
|
|
->where('profesional_id', $profesionalId)
|
|
->where('formulario_id', (int) $formulario->id)
|
|
->exists();
|
|
|
|
if (!$esAsignado) {
|
|
return redirect('/profesional/formularios')
|
|
->with('profesional_action_error', 'No tenes permiso para asignar turno en este formulario.');
|
|
}
|
|
|
|
if ($formularioFueAceptadoPorOtroProfesional((int) $formulario->id, $profesionalId)) {
|
|
return redirect('/profesional/formularios/' . $formulario->id)
|
|
->with('profesional_action_error', 'Este formulario ya fue aceptado por otro profesional.');
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'fecha_turno' => ['required', 'date', 'after_or_equal:today'],
|
|
'hora_turno' => ['required', 'date_format:H:i'],
|
|
'omitir_restricciones' => ['nullable', 'boolean'],
|
|
]);
|
|
|
|
$inicio = \Illuminate\Support\Carbon::parse($validated['fecha_turno'] . ' ' . $validated['hora_turno'] . ':00');
|
|
|
|
$agenda = Agenda::query()->firstOrCreate(
|
|
['profesional_id' => $profesionalId],
|
|
[
|
|
'estado' => 'Activa',
|
|
'duracionturno' => 30,
|
|
]
|
|
);
|
|
|
|
$duracion = (int) ($agenda->duracionturno ?? 30);
|
|
|
|
$advertencias = $obtenerAdvertenciasTurno((int) $agenda->id, $duracion, $inicio);
|
|
$errorDisponibilidad = $verificarConflictoTurno((int) $agenda->id, $duracion, $inicio);
|
|
|
|
if ($errorDisponibilidad !== null) {
|
|
return redirect('/profesional/formularios/' . $formulario->id)
|
|
->with('profesional_action_error', 'Ese dia y horario no se encuentra disponible: ' . $errorDisponibilidad)
|
|
->withInput();
|
|
}
|
|
|
|
if (!empty($advertencias) && !((bool) ($validated['omitir_restricciones'] ?? false))) {
|
|
return redirect('/profesional/formularios/' . $formulario->id)
|
|
->with('profesional_action_error', 'El turno elegido coincide con restricciones de agenda. Revisá las advertencias para decidir si querés asignarlo igualmente.')
|
|
->with('formulario_turno_manual_warning', [
|
|
'advertencias' => $advertencias,
|
|
'inputs' => [
|
|
'fecha_turno' => $validated['fecha_turno'],
|
|
'hora_turno' => $validated['hora_turno'],
|
|
],
|
|
])
|
|
->withInput();
|
|
}
|
|
|
|
$estadoConfirmadoId = EstadoTurno::query()
|
|
->whereRaw('LOWER(TRIM(descripcion)) = ?', ['confirmado'])
|
|
->value('id') ?? 1;
|
|
|
|
Turno::create([
|
|
'inicio' => $inicio,
|
|
'correo' => (string) ($formulario->correo ?? ''),
|
|
'celular' => (string) ($formulario->celular ?? ''),
|
|
'nombrecompleto' => (string) ($formulario->nombrecompleto ?? 'Sin nombre'),
|
|
'descripcion' => (string) ($formulario->descripcion ?? 'Turno asignado manualmente.'),
|
|
'cliente_id' => $formulario->cliente_id ? (int) $formulario->cliente_id : null,
|
|
'estadoturno_id' => (int) $estadoConfirmadoId,
|
|
'agenda_id' => (int) $agenda->id,
|
|
'profesional_id' => $profesionalId,
|
|
'servicio_id' => (int) $formulario->servicio_id,
|
|
'modalidad_id' => (int) $formulario->modalidad_id,
|
|
]);
|
|
|
|
$profesional->loadMissing('persona');
|
|
$nombreProfesional = trim((string) (($profesional->persona?->nombre ?? '') . ' ' . ($profesional->persona?->apellido ?? '')));
|
|
$servicioTitulo = (string) (Servicio::query()->where('id', (int) $formulario->servicio_id)->value('titulo') ?? 'Servicio');
|
|
$modalidadDescripcion = (string) (Modalidad::query()->where('id', (int) $formulario->modalidad_id)->value('descripcion') ?? 'Modalidad');
|
|
|
|
$enviarCorreoTurno(
|
|
'asignacion',
|
|
(string) ($formulario->correo ?? ''),
|
|
(string) ($formulario->nombrecompleto ?? ''),
|
|
$inicio->copy(),
|
|
$servicioTitulo,
|
|
$modalidadDescripcion,
|
|
$nombreProfesional
|
|
);
|
|
|
|
DB::table('profesionales_formularios')
|
|
->where('profesional_id', $profesionalId)
|
|
->where('formulario_id', (int) $formulario->id)
|
|
->update([
|
|
'estado' => 'Aceptado',
|
|
]);
|
|
|
|
$recalcularEstadoGeneralFormulario($formulario);
|
|
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'Aceptó un caso',
|
|
'El profesional ID ' . $profesionalId . ' aceptó el formulario ID ' . $formulario->id . '.'
|
|
);
|
|
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'Asignó un turno',
|
|
'El profesional ID ' . $profesionalId . ' asignó manualmente un turno desde el formulario ID ' . $formulario->id . ' para el ' . $inicio->format('d/m/Y H:i') . '.'
|
|
);
|
|
|
|
return redirect('/profesional/formularios/' . $formulario->id)
|
|
->with('profesional_action_success', 'Turno asignado correctamente.');
|
|
});
|
|
|
|
Route::post('/profesional/formularios/{formulario}/asignar-turno-automatico', function (Request $request, Formulario $formulario) use ($buscarTurnoAutomatico, $verificarDisponibilidadTurno, $formularioFueAceptadoPorOtroProfesional, $enviarCorreoTurno, $registrarLogSeguridadProfesional, $recalcularEstadoGeneralFormulario) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
$profesionalId = (int) ($profesional?->id ?? 0);
|
|
|
|
if (!$profesionalId) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$esAsignado = DB::table('profesionales_formularios')
|
|
->where('profesional_id', $profesionalId)
|
|
->where('formulario_id', (int) $formulario->id)
|
|
->exists();
|
|
|
|
if (!$esAsignado) {
|
|
return redirect('/profesional/formularios')
|
|
->with('profesional_action_error', 'No tenes permiso para asignar turno en este formulario.');
|
|
}
|
|
|
|
if ($formularioFueAceptadoPorOtroProfesional((int) $formulario->id, $profesionalId)) {
|
|
return redirect('/profesional/formularios/' . $formulario->id)
|
|
->with('profesional_action_error', 'Este formulario ya fue aceptado por otro profesional.');
|
|
}
|
|
|
|
$formulario->load(['diasPreferidos', 'horariosPreferidos']);
|
|
$sugerencia = $buscarTurnoAutomatico($formulario, $profesionalId);
|
|
|
|
if (!$sugerencia || !isset($sugerencia['inicio'], $sugerencia['agenda_id'], $sugerencia['duracion'])) {
|
|
return redirect('/profesional/formularios/' . $formulario->id)
|
|
->with('profesional_action_error', 'No se encontró un turno automático disponible desde mañana.');
|
|
}
|
|
|
|
$inicio = $sugerencia['inicio']->copy();
|
|
$inicioMinimo = now()->addDay()->startOfDay();
|
|
|
|
if ($inicio->lt($inicioMinimo)) {
|
|
return redirect('/profesional/formularios/' . $formulario->id)
|
|
->with('profesional_action_error', 'El turno automático debe ser a partir del día siguiente.');
|
|
}
|
|
|
|
$errorDisponibilidad = $verificarDisponibilidadTurno((int) $sugerencia['agenda_id'], (int) $sugerencia['duracion'], $inicio->copy());
|
|
if ($errorDisponibilidad !== null) {
|
|
return redirect('/profesional/formularios/' . $formulario->id)
|
|
->with('profesional_action_error', 'No se pudo asignar automáticamente el turno: ' . $errorDisponibilidad);
|
|
}
|
|
|
|
$estadoConfirmadoId = EstadoTurno::query()
|
|
->whereRaw('LOWER(TRIM(descripcion)) = ?', ['confirmado'])
|
|
->value('id') ?? 1;
|
|
|
|
Turno::create([
|
|
'inicio' => $inicio,
|
|
'correo' => (string) ($formulario->correo ?? ''),
|
|
'celular' => (string) ($formulario->celular ?? ''),
|
|
'nombrecompleto' => (string) ($formulario->nombrecompleto ?? 'Sin nombre'),
|
|
'descripcion' => (string) ($formulario->descripcion ?? 'Turno asignado automáticamente.'),
|
|
'cliente_id' => $formulario->cliente_id ? (int) $formulario->cliente_id : null,
|
|
'estadoturno_id' => (int) $estadoConfirmadoId,
|
|
'agenda_id' => (int) $sugerencia['agenda_id'],
|
|
'profesional_id' => $profesionalId,
|
|
'servicio_id' => (int) $formulario->servicio_id,
|
|
'modalidad_id' => (int) $formulario->modalidad_id,
|
|
]);
|
|
|
|
$profesional->loadMissing('persona');
|
|
$nombreProfesional = trim((string) (($profesional->persona?->nombre ?? '') . ' ' . ($profesional->persona?->apellido ?? '')));
|
|
$servicioTitulo = (string) (Servicio::query()->where('id', (int) $formulario->servicio_id)->value('titulo') ?? 'Servicio');
|
|
$modalidadDescripcion = (string) (Modalidad::query()->where('id', (int) $formulario->modalidad_id)->value('descripcion') ?? 'Modalidad');
|
|
|
|
$enviarCorreoTurno(
|
|
'asignacion',
|
|
(string) ($formulario->correo ?? ''),
|
|
(string) ($formulario->nombrecompleto ?? ''),
|
|
$inicio->copy(),
|
|
$servicioTitulo,
|
|
$modalidadDescripcion,
|
|
$nombreProfesional
|
|
);
|
|
|
|
DB::table('profesionales_formularios')
|
|
->where('profesional_id', $profesionalId)
|
|
->where('formulario_id', (int) $formulario->id)
|
|
->update([
|
|
'estado' => 'Aceptado',
|
|
]);
|
|
|
|
$recalcularEstadoGeneralFormulario($formulario);
|
|
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'Aceptó un caso',
|
|
'El profesional ID ' . $profesionalId . ' aceptó el formulario ID ' . $formulario->id . '.'
|
|
);
|
|
|
|
$registrarLogSeguridadProfesional(
|
|
$request,
|
|
'Asignó un turno',
|
|
'El profesional ID ' . $profesionalId . ' asignó automáticamente un turno desde el formulario ID ' . $formulario->id . ' para el ' . $inicio->format('d/m/Y H:i') . '.'
|
|
);
|
|
|
|
return redirect('/profesional/formularios/' . $formulario->id)
|
|
->with('profesional_action_success', 'Turno automático asignado para el ' . $inicio->format('d/m/Y') . ' a las ' . $inicio->format('H:i') . '.');
|
|
});
|
|
|
|
Route::get('/profesional/notificaciones', function (Request $request) {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
|
|
$profesional = Profesional::query()
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$profesional) {
|
|
return redirect('/login/personal')->with('login_error', 'Debes iniciar sesión como profesional.');
|
|
}
|
|
|
|
$profesionalId = (int) $profesional->id;
|
|
$notificaciones = [];
|
|
|
|
// 1. Turnos de hoy
|
|
$hoy = now()->toDateString();
|
|
$turnosHoy = Turno::with(['servicio', 'estadoTurno'])
|
|
->where('profesional_id', $profesionalId)
|
|
->whereDate('inicio', $hoy)
|
|
->orderBy('inicio')
|
|
->get();
|
|
|
|
foreach ($turnosHoy as $turno) {
|
|
$nombre = trim((string) ($turno->nombrecompleto ?? 'Sin nombre'));
|
|
$hora = $turno->inicio ? $turno->inicio->format('H:i') : '-';
|
|
$servicio = trim((string) ($turno->servicio?->titulo ?? 'Turno'));
|
|
$estado = trim((string) ($turno->estadoTurno?->descripcion ?? ''));
|
|
$notificaciones[] = [
|
|
'tipo' => 'turno_hoy',
|
|
'titulo' => 'Turno hoy a las ' . $hora . ' — ' . $nombre,
|
|
'descripcion' => $servicio . ($estado !== '' ? ' · Estado: ' . $estado : ''),
|
|
'fecha' => 'Hoy ' . $hora,
|
|
'clave' => base64_encode('turno_hoy|' . ('Turno hoy a las ' . $hora . ' — ' . $nombre) . '|' . ('Hoy ' . $hora)),
|
|
'enlace' => '',
|
|
];
|
|
}
|
|
|
|
// 2. Turnos de mañana
|
|
$manana = now()->addDay()->toDateString();
|
|
$turnosManana = Turno::with(['servicio', 'estadoTurno'])
|
|
->where('profesional_id', $profesionalId)
|
|
->whereDate('inicio', $manana)
|
|
->orderBy('inicio')
|
|
->get();
|
|
|
|
foreach ($turnosManana as $turno) {
|
|
$nombre = trim((string) ($turno->nombrecompleto ?? 'Sin nombre'));
|
|
$hora = $turno->inicio ? $turno->inicio->format('H:i') : '-';
|
|
$servicio = trim((string) ($turno->servicio?->titulo ?? 'Turno'));
|
|
$estado = trim((string) ($turno->estadoTurno?->descripcion ?? ''));
|
|
$notificaciones[] = [
|
|
'tipo' => 'turno_manana',
|
|
'titulo' => 'Turno mañana a las ' . $hora . ' — ' . $nombre,
|
|
'descripcion' => $servicio . ($estado !== '' ? ' · Estado: ' . $estado : ''),
|
|
'fecha' => 'Mañana ' . $hora,
|
|
'clave' => base64_encode('turno_manana|' . ('Turno mañana a las ' . $hora . ' — ' . $nombre) . '|' . ('Mañana ' . $hora)),
|
|
'enlace' => '',
|
|
];
|
|
}
|
|
|
|
// 3. Turnos cancelados recientes (últimos 7 días)
|
|
$estadoCanceladoId = (int) (EstadoTurno::query()
|
|
->whereRaw('LOWER(TRIM(descripcion)) = ?', ['cancelado'])
|
|
->value('id') ?? 0);
|
|
|
|
if ($estadoCanceladoId > 0) {
|
|
$turnosCancelados = Turno::with(['servicio'])
|
|
->where('profesional_id', $profesionalId)
|
|
->where('estadoturno_id', $estadoCanceladoId)
|
|
->where('updated_at', '>=', now()->subDays(7))
|
|
->orderByDesc('updated_at')
|
|
->get();
|
|
|
|
foreach ($turnosCancelados as $turno) {
|
|
$nombre = trim((string) ($turno->nombrecompleto ?? 'Sin nombre'));
|
|
$fecha = $turno->inicio ? $turno->inicio->format('d/m/Y H:i') : '-';
|
|
$servicio = trim((string) ($turno->servicio?->titulo ?? 'Turno'));
|
|
$notificaciones[] = [
|
|
'tipo' => 'turno_cancelado',
|
|
'titulo' => 'Turno cancelado — ' . $nombre,
|
|
'descripcion' => $servicio . ' · Fecha del turno: ' . $fecha,
|
|
'fecha' => $turno->updated_at ? $turno->updated_at->format('d/m/Y') : '-',
|
|
'clave' => base64_encode('turno_cancelado|' . ('Turno cancelado — ' . $nombre) . '|' . ($turno->updated_at ? $turno->updated_at->format('d/m/Y') : '-')),
|
|
'enlace' => '',
|
|
];
|
|
}
|
|
}
|
|
|
|
return view('profesional.notificaciones', [
|
|
'notificaciones' => $notificaciones,
|
|
]);
|
|
});
|
|
|
|
Route::get('/administrador/dashboard', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$contenidoWeb = ContenidoWeb::latest('id')->first();
|
|
$quienesSomos = $contenidoWeb?->quienessomos;
|
|
|
|
$profesionales = Profesional::with(['persona.Foto', 'profesion', 'profesiones'])
|
|
->where('baja_id', 1)
|
|
->get();
|
|
|
|
$servicios = Servicio::with('foto')
|
|
->where('estado', 'activo')
|
|
->where('visibleenweb', 'si')
|
|
->get();
|
|
|
|
$profesiones = Profesion::query()
|
|
->where('visible_en_formulario', true)
|
|
->orderBy('titulo')
|
|
->get();
|
|
|
|
$modalidades = Modalidad::query()
|
|
->orderBy('descripcion')
|
|
->get();
|
|
|
|
return view('administrador.dashboard', [
|
|
'quienesSomos' => $quienesSomos,
|
|
'profesionales' => $profesionales,
|
|
'servicios' => $servicios,
|
|
'profesiones' => $profesiones,
|
|
'modalidades' => $modalidades,
|
|
]);
|
|
});
|
|
|
|
Route::get('/administrador/perfil', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
|
|
$administrador = Administrador::with(['persona.telefonos', 'credencial'])
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$administrador) {
|
|
return redirect('/administrador/dashboard')
|
|
->with('admin_action_error', 'No se encontró el perfil de administradora asociado a la sesión actual.');
|
|
}
|
|
|
|
return view('administrador.perfil', [
|
|
'administrador' => $administrador,
|
|
'telefonoActual' => $administrador->persona?->telefonos?->first(),
|
|
]);
|
|
});
|
|
|
|
Route::put('/administrador/perfil', function (Request $request) use ($validarSesionAdmin, $registrarLogSeguridad) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
|
|
$administrador = Administrador::with(['persona.telefonos', 'credencial'])
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$administrador) {
|
|
return redirect('/administrador/dashboard')
|
|
->with('admin_action_error', 'No se encontró el perfil de administradora asociado a la sesión actual.');
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'nombre' => ['required', 'string', 'max:100', 'regex:/^[\pL\s]+$/u'],
|
|
'apellido' => ['required', 'string', 'max:100', 'regex:/^[\pL\s]+$/u'],
|
|
'correo' => ['required', 'email', 'max:255'],
|
|
'telefono' => ['nullable', 'string', 'max:30', 'regex:/^[0-9]+$/'],
|
|
'usuario' => ['required', 'string', 'max:100', 'unique:credencialesprofesionales,usuario,' . $administrador->credencialprofesional_id],
|
|
'contra_actual' => ['required', 'string'],
|
|
'contra' => ['nullable', 'string', 'min:6', 'confirmed'],
|
|
]);
|
|
|
|
$nombreAnterior = (string) ($administrador->persona?->nombre ?? '');
|
|
$apellidoAnterior = (string) ($administrador->persona?->apellido ?? '');
|
|
$correoAnterior = (string) ($administrador->correo ?? '');
|
|
$telefonoAnterior = (string) ($administrador->persona?->telefonos?->first()?->telefono ?? '');
|
|
$usuarioAnterior = (string) ($administrador->credencial?->usuario ?? '');
|
|
|
|
$contraActualIngresada = (string) ($validated['contra_actual'] ?? '');
|
|
$contraActualGuardada = (string) ($administrador->credencial?->contra ?? '');
|
|
|
|
$contraActualValida = $contraActualIngresada !== ''
|
|
&& ($contraActualIngresada === $contraActualGuardada || Hash::check($contraActualIngresada, $contraActualGuardada));
|
|
|
|
if (!$contraActualValida) {
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Cambio de contraseña frustrado',
|
|
'La administradora ID ' . (int) $administrador->id . ' ingresó una contraseña actual inválida al intentar modificar sus credenciales.'
|
|
);
|
|
|
|
return back()
|
|
->withErrors(['contra_actual' => 'La contraseña actual no es correcta.'])
|
|
->withInput($request->except(['contra_actual', 'contra', 'contra_confirmation']));
|
|
}
|
|
|
|
DB::transaction(function () use ($administrador, $validated): void {
|
|
$administrador->persona?->update([
|
|
'nombre' => $validated['nombre'],
|
|
'apellido' => $validated['apellido'],
|
|
]);
|
|
|
|
if ($administrador->persona && !blank($validated['telefono'] ?? null)) {
|
|
$telefonoExistente = $administrador->persona->telefonos()->first();
|
|
|
|
if ($telefonoExistente) {
|
|
$telefonoExistente->update([
|
|
'telefono' => $validated['telefono'],
|
|
]);
|
|
} else {
|
|
$telefonoNuevo = Telefono::create([
|
|
'telefono' => $validated['telefono'],
|
|
]);
|
|
|
|
DB::table('personas_telefonos')->insertOrIgnore([
|
|
'dni' => (string) $administrador->dni,
|
|
'persona_id' => (int) $administrador->persona->id,
|
|
'telefono_id' => (int) $telefonoNuevo->id,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
$administrador->update([
|
|
'correo' => $validated['correo'],
|
|
]);
|
|
|
|
$datosCredencial = [
|
|
'usuario' => $validated['usuario'],
|
|
];
|
|
|
|
if (!empty($validated['contra'])) {
|
|
$datosCredencial['contra'] = Hash::make($validated['contra']);
|
|
}
|
|
|
|
$administrador->credencial?->update($datosCredencial);
|
|
});
|
|
|
|
$nombreCompleto = trim($validated['nombre'] . ' ' . $validated['apellido']);
|
|
$request->session()->put([
|
|
'personal_nombre' => $nombreCompleto,
|
|
'personal_apellido' => $validated['apellido'],
|
|
'personal_usuario' => $validated['usuario'],
|
|
]);
|
|
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Edito los datos del administrador',
|
|
'La administradora ID ' . (int) $administrador->id
|
|
. ' actualizó su perfil. Nombre: "' . $nombreAnterior . '" -> "' . $validated['nombre']
|
|
. '", Apellido: "' . $apellidoAnterior . '" -> "' . $validated['apellido']
|
|
. '", Correo: "' . $correoAnterior . '" -> "' . $validated['correo']
|
|
. '", Teléfono: "' . $telefonoAnterior . '" -> "' . ($validated['telefono'] ?? '')
|
|
. '", Usuario: "' . $usuarioAnterior . '" -> "' . $validated['usuario'] . '".'
|
|
);
|
|
|
|
if (!empty($validated['contra'])) {
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Cambio de contraseña exitoso',
|
|
'La administradora ID ' . (int) $administrador->id . ' cambió su contraseña desde su perfil.'
|
|
);
|
|
}
|
|
|
|
return redirect('/administrador/perfil')
|
|
->with('admin_action_success', 'Datos de administradora actualizados correctamente.');
|
|
});
|
|
|
|
Route::post('/administrador/perfil/pregunta-secreta', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
|
|
$administrador = Administrador::with('credencial')
|
|
->where('credencialprofesional_id', $credencialId)
|
|
->first();
|
|
|
|
if (!$administrador) {
|
|
return redirect('/administrador/dashboard')
|
|
->with('admin_action_error', 'No se encontró el perfil de administradora asociado a la sesión actual.');
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'contra_actual_secreta' => ['required', 'string'],
|
|
'pregunta_secreta' => ['required', 'string', 'max:255'],
|
|
'respuesta_secreta' => ['required', 'string', 'max:255'],
|
|
]);
|
|
|
|
$contraActualIngresada = (string) ($validated['contra_actual_secreta'] ?? '');
|
|
$contraActualGuardada = (string) ($administrador->credencial?->contra ?? '');
|
|
|
|
$contraActualValida = $contraActualIngresada !== ''
|
|
&& ($contraActualIngresada === $contraActualGuardada || Hash::check($contraActualIngresada, $contraActualGuardada));
|
|
|
|
if (!$contraActualValida) {
|
|
return back()
|
|
->withErrors(['contra_actual_secreta' => 'La contraseña actual no es correcta.'])
|
|
->withInput($request->except(['contra_actual_secreta', 'respuesta_secreta']));
|
|
}
|
|
|
|
$administrador->update([
|
|
'pregunta_secreta_hash' => mb_strtolower(trim((string) $validated['pregunta_secreta'])),
|
|
'respuesta_secreta_hash' => Hash::make(mb_strtolower(trim((string) $validated['respuesta_secreta']))),
|
|
]);
|
|
|
|
return redirect('/administrador/perfil')
|
|
->with('admin_action_success', 'Pregunta y respuesta secreta actualizadas correctamente.');
|
|
});
|
|
|
|
Route::get('/administrador/logs', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$query = LogSeguridad::query();
|
|
|
|
$filtroAccion = trim((string) $request->query('accion', ''));
|
|
$filtroDesde = trim((string) $request->query('desde', ''));
|
|
$filtroHasta = trim((string) $request->query('hasta', ''));
|
|
$filtroTexto = trim((string) $request->query('q', ''));
|
|
|
|
if ($filtroAccion !== '') {
|
|
$query->where('accion_descripcion', $filtroAccion);
|
|
}
|
|
|
|
if ($filtroDesde !== '') {
|
|
$query->whereDate('fechahora', '>=', $filtroDesde);
|
|
}
|
|
|
|
if ($filtroHasta !== '') {
|
|
$query->whereDate('fechahora', '<=', $filtroHasta);
|
|
}
|
|
|
|
if ($filtroTexto !== '') {
|
|
$query->where(function ($q) use ($filtroTexto): void {
|
|
$q->where('descripcion', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('IPorigen', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('rol', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('accion_descripcion', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('responsable_nombre', 'like', '%' . $filtroTexto . '%');
|
|
});
|
|
}
|
|
|
|
$logs = $query
|
|
->latest('id')
|
|
->paginate(10)
|
|
->withQueryString();
|
|
|
|
$accionesDisponibles = LogSeguridad::query()
|
|
->whereNotNull('accion_descripcion')
|
|
->distinct()
|
|
->orderBy('accion_descripcion')
|
|
->pluck('accion_descripcion');
|
|
|
|
return view('administrador.logs', [
|
|
'logs' => $logs,
|
|
'accionesDisponibles' => $accionesDisponibles,
|
|
]);
|
|
});
|
|
|
|
Route::get('/administrador/asistente-consultas', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$pendientes = AsistenteSinRespuesta::query()
|
|
->orderByDesc('created_at')
|
|
->paginate(15)
|
|
->withQueryString();
|
|
|
|
$totalSinRevisar = AsistenteSinRespuesta::query()
|
|
->where('revisado', false)
|
|
->count();
|
|
|
|
return view('administrador.asistente-consultas', [
|
|
'pendientes' => $pendientes,
|
|
'totalSinRevisar' => $totalSinRevisar,
|
|
]);
|
|
});
|
|
|
|
Route::post('/administrador/asistente-consultas/{id}/marcar-revisado', function (Request $request, int $id) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
AsistenteSinRespuesta::query()->where('id', $id)->update(['revisado' => true]);
|
|
|
|
return back()->with('admin_action_success', 'Consulta marcada como revisada.');
|
|
});
|
|
|
|
Route::delete('/administrador/asistente-consultas/{id}', function (Request $request, int $id) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
AsistenteSinRespuesta::query()->where('id', $id)->delete();
|
|
|
|
return back()->with('admin_action_success', 'Consulta eliminada.');
|
|
});
|
|
|
|
Route::get('/administrador/fallas', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$query = Error::query();
|
|
|
|
$filtroCodigo = trim((string) $request->query('codigo', ''));
|
|
$filtroDesde = trim((string) $request->query('desde', ''));
|
|
$filtroHasta = trim((string) $request->query('hasta', ''));
|
|
$filtroTexto = trim((string) $request->query('q', ''));
|
|
|
|
if ($filtroCodigo !== '') {
|
|
$query->where('codigo', $filtroCodigo);
|
|
}
|
|
|
|
if ($filtroDesde !== '') {
|
|
$query->whereDate('fecha_hora', '>=', $filtroDesde);
|
|
}
|
|
|
|
if ($filtroHasta !== '') {
|
|
$query->whereDate('fecha_hora', '<=', $filtroHasta);
|
|
}
|
|
|
|
if ($filtroTexto !== '') {
|
|
$query->where(function ($q) use ($filtroTexto): void {
|
|
$q->where('mensaje', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('url', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('track_trace', 'like', '%' . $filtroTexto . '%');
|
|
});
|
|
}
|
|
|
|
$fallas = $query
|
|
->latest('id')
|
|
->paginate(10)
|
|
->withQueryString();
|
|
|
|
$codigosDisponibles = Error::query()
|
|
->whereNotNull('codigo')
|
|
->where('codigo', '<>', '')
|
|
->distinct()
|
|
->orderBy('codigo')
|
|
->pluck('codigo');
|
|
|
|
return view('administrador.fallas', [
|
|
'fallas' => $fallas,
|
|
'codigosDisponibles' => $codigosDisponibles,
|
|
]);
|
|
});
|
|
|
|
Route::get('/administrador/logs/exportar-json', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$query = LogSeguridad::query();
|
|
|
|
$filtroAccion = trim((string) $request->query('accion', ''));
|
|
$filtroDesde = trim((string) $request->query('desde', ''));
|
|
$filtroHasta = trim((string) $request->query('hasta', ''));
|
|
$filtroTexto = trim((string) $request->query('q', ''));
|
|
|
|
if ($filtroAccion !== '') {
|
|
$query->where('accion_descripcion', $filtroAccion);
|
|
}
|
|
|
|
if ($filtroDesde !== '') {
|
|
$query->whereDate('fechahora', '>=', $filtroDesde);
|
|
}
|
|
|
|
if ($filtroHasta !== '') {
|
|
$query->whereDate('fechahora', '<=', $filtroHasta);
|
|
}
|
|
|
|
if ($filtroTexto !== '') {
|
|
$query->where(function ($q) use ($filtroTexto): void {
|
|
$q->where('descripcion', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('IPorigen', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('rol', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('accion_descripcion', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('responsable_nombre', 'like', '%' . $filtroTexto . '%');
|
|
});
|
|
}
|
|
|
|
$logs = $query
|
|
->latest('id')
|
|
->get()
|
|
->map(function ($log) {
|
|
return [
|
|
'id' => $log->id,
|
|
'fecha_hora' => $log->fechahora ? \Illuminate\Support\Carbon::parse($log->fechahora)->format('Y-m-d H:i:s') : null,
|
|
'accion_id' => $log->accion_id,
|
|
'accion' => $log->accion_descripcion,
|
|
'rol' => $log->rol,
|
|
'ip_origen' => $log->IPorigen,
|
|
'responsable' => $log->responsable_nombre,
|
|
'descripcion' => $log->descripcion,
|
|
];
|
|
})
|
|
->values();
|
|
|
|
$nombreArchivo = 'logs-seguridad-' . now()->format('Ymd-His') . '.json';
|
|
|
|
return response()->json([
|
|
'generado_en' => now()->format('Y-m-d H:i:s'),
|
|
'filtros_aplicados' => [
|
|
'accion' => $filtroAccion !== '' ? $filtroAccion : null,
|
|
'desde' => $filtroDesde !== '' ? $filtroDesde : null,
|
|
'hasta' => $filtroHasta !== '' ? $filtroHasta : null,
|
|
'texto' => $filtroTexto !== '' ? $filtroTexto : null,
|
|
],
|
|
'total_registros' => $logs->count(),
|
|
'logs' => $logs,
|
|
], 200, [
|
|
'Content-Disposition' => 'attachment; filename="' . $nombreArchivo . '"',
|
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
});
|
|
|
|
Route::get('/administrador/contenido-web', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
return redirect('/administrador/contenido/quienes-somos');
|
|
});
|
|
|
|
Route::get('/administrador/contenido/quienes-somos', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$contenidoWeb = ContenidoWeb::latest('id')->first();
|
|
|
|
return view('administrador.contenido-quienes-somos', [
|
|
'contenidoWeb' => $contenidoWeb,
|
|
]);
|
|
});
|
|
|
|
Route::get('/administrador/contenido/profesiones', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$profesiones = Profesion::query()
|
|
->orderBy('titulo')
|
|
->get();
|
|
|
|
return view('administrador.contenido-profesiones', [
|
|
'profesiones' => $profesiones,
|
|
]);
|
|
});
|
|
|
|
Route::get('/administrador/contenido/servicios', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$servicios = Servicio::with('profesion')
|
|
->orderBy('titulo')
|
|
->paginate(15)
|
|
->withQueryString();
|
|
|
|
return view('administrador.contenido-servicios', [
|
|
'servicios' => $servicios,
|
|
]);
|
|
});
|
|
|
|
Route::get('/administrador/contenido/asistente-virtual', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
return redirect('/administrador/contenido/asistente-virtual/ver-faqs');
|
|
});
|
|
|
|
Route::get('/administrador/contenido/asistente-virtual/agregar-faq', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
FaqAsistente::query()->firstOrCreate(
|
|
['intencion' => FAQ_INTENCION_BURBUJA],
|
|
[
|
|
'palabras_clave' => [],
|
|
'respuesta' => '¡Hola!, ¿te puedo ayudar en algo?',
|
|
'orden' => 1,
|
|
'activo' => true,
|
|
]
|
|
);
|
|
|
|
FaqAsistente::query()->firstOrCreate(
|
|
['intencion' => FAQ_INTENCION_PANEL_INICIO],
|
|
[
|
|
'palabras_clave' => [],
|
|
'respuesta' => 'Hola, soy el asistente virtual. Puedo ayudarte con servicios, profesionales, turnos, ubicación y contacto.',
|
|
'orden' => 2,
|
|
'activo' => true,
|
|
]
|
|
);
|
|
|
|
FaqAsistente::query()->firstOrCreate(
|
|
['intencion' => FAQ_INTENCION_ERROR],
|
|
[
|
|
'palabras_clave' => [],
|
|
'respuesta' => 'Lo siento, no tengo una respuesta exacta para eso.',
|
|
'orden' => 3,
|
|
'activo' => true,
|
|
]
|
|
);
|
|
|
|
FaqAsistente::query()->firstOrCreate(
|
|
['intencion' => FAQ_INTENCION_NOMBRE],
|
|
[
|
|
'palabras_clave' => [],
|
|
'respuesta' => 'Clara',
|
|
'orden' => 4,
|
|
'activo' => true,
|
|
]
|
|
);
|
|
|
|
return view('administrador.contenido-asistente-agregar-faq');
|
|
});
|
|
|
|
Route::get('/administrador/contenido/asistente-virtual/agregar-chips', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$chipsAsistenteAdmin = FaqAsistente::query()
|
|
->where('intencion', FAQ_INTENCION_CHIPS)
|
|
->orderBy('orden')
|
|
->orderBy('id')
|
|
->get();
|
|
|
|
return view('administrador.contenido-asistente-agregar-chips', [
|
|
'chipsAsistenteAdmin' => $chipsAsistenteAdmin,
|
|
]);
|
|
});
|
|
|
|
Route::get('/administrador/contenido/asistente-virtual/ver-faqs', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$filtroIntencion = mb_strtolower(trim((string) $request->query('intencion', '')));
|
|
$perPage = (int) $request->query('per_page', 10);
|
|
if (!in_array($perPage, [10, 20, 50], true)) {
|
|
$perPage = 10;
|
|
}
|
|
|
|
FaqAsistente::query()->firstOrCreate(
|
|
['intencion' => FAQ_INTENCION_BURBUJA],
|
|
[
|
|
'palabras_clave' => [],
|
|
'respuesta' => '¡Hola!, ¿te puedo ayudar en algo?',
|
|
'orden' => 1,
|
|
'activo' => true,
|
|
]
|
|
);
|
|
|
|
FaqAsistente::query()->firstOrCreate(
|
|
['intencion' => FAQ_INTENCION_PANEL_INICIO],
|
|
[
|
|
'palabras_clave' => [],
|
|
'respuesta' => 'Hola, soy el asistente virtual. Puedo ayudarte con servicios, profesionales, turnos, ubicación y contacto.',
|
|
'orden' => 2,
|
|
'activo' => true,
|
|
]
|
|
);
|
|
|
|
FaqAsistente::query()->firstOrCreate(
|
|
['intencion' => FAQ_INTENCION_ERROR],
|
|
[
|
|
'palabras_clave' => [],
|
|
'respuesta' => 'Lo siento, no tengo una respuesta exacta para eso.',
|
|
'orden' => 3,
|
|
'activo' => true,
|
|
]
|
|
);
|
|
|
|
FaqAsistente::query()->firstOrCreate(
|
|
['intencion' => FAQ_INTENCION_NOMBRE],
|
|
[
|
|
'palabras_clave' => [],
|
|
'respuesta' => 'Clara',
|
|
'orden' => 4,
|
|
'activo' => true,
|
|
]
|
|
);
|
|
|
|
$faqsAsistenteQuery = FaqAsistente::query()
|
|
->where(function ($query): void {
|
|
$query->whereNull('intencion')
|
|
->orWhere('intencion', '!=', FAQ_INTENCION_CHIPS);
|
|
});
|
|
|
|
if ($filtroIntencion !== '') {
|
|
$faqsAsistenteQuery->whereRaw('LOWER(COALESCE(intencion, "")) like ?', ['%' . $filtroIntencion . '%']);
|
|
}
|
|
|
|
$faqsAsistente = $faqsAsistenteQuery
|
|
->orderBy('orden')
|
|
->orderBy('id')
|
|
->paginate($perPage)
|
|
->withQueryString();
|
|
|
|
return view('administrador.contenido-asistente-ver-faqs', [
|
|
'faqsAsistente' => $faqsAsistente,
|
|
'filtroIntencion' => $filtroIntencion,
|
|
'perPage' => $perPage,
|
|
]);
|
|
});
|
|
|
|
Route::get('/administrador/emails', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$tipos = [
|
|
'turno_otorgado' => [
|
|
'titulo' => 'Turno otorgado',
|
|
'mensaje_inicio' => 'Se asigno tu turno con los siguientes datos:',
|
|
'mensaje_final' => 'Si necesitas reprogramar o cancelar, por favor comunicate por WhatsApp al 343-4632444',
|
|
],
|
|
'turno_cancelado' => [
|
|
'titulo' => 'Turno cancelado',
|
|
'mensaje_inicio' => 'Te informamos que tu turno fue cancelado con los siguientes datos:',
|
|
'mensaje_final' => 'Si necesitas coordinar un nuevo turno, por favor comunicate por WhatsApp al 343-4632444',
|
|
],
|
|
'turno_reprogramado' => [
|
|
'titulo' => 'Turno reprogramado',
|
|
'mensaje_inicio' => 'Te informamos que tu turno fue reprogramado con los siguientes datos:',
|
|
'mensaje_final' => 'Si tenes dudas sobre este cambio, por favor comunicate por WhatsApp al 343-4632444',
|
|
],
|
|
];
|
|
|
|
foreach ($tipos as $tipo => $defecto) {
|
|
Notificacion::query()->firstOrCreate(
|
|
['tipo' => $tipo],
|
|
[
|
|
'mensaje_inicio' => $defecto['mensaje_inicio'],
|
|
'mensaje_final' => $defecto['mensaje_final'],
|
|
]
|
|
);
|
|
}
|
|
|
|
$notificaciones = Notificacion::query()
|
|
->whereIn('tipo', array_keys($tipos))
|
|
->get()
|
|
->keyBy('tipo');
|
|
|
|
return view('administrador.emails', [
|
|
'tipos' => $tipos,
|
|
'notificaciones' => $notificaciones,
|
|
]);
|
|
});
|
|
|
|
Route::post('/administrador/emails', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$validado = $request->validate([
|
|
'turno_otorgado_mensaje_inicio' => ['required', 'string', 'max:5000'],
|
|
'turno_otorgado_mensaje_final' => ['required', 'string', 'max:5000'],
|
|
'turno_cancelado_mensaje_inicio' => ['required', 'string', 'max:5000'],
|
|
'turno_cancelado_mensaje_final' => ['required', 'string', 'max:5000'],
|
|
'turno_reprogramado_mensaje_inicio' => ['required', 'string', 'max:5000'],
|
|
'turno_reprogramado_mensaje_final' => ['required', 'string', 'max:5000'],
|
|
]);
|
|
|
|
$tipos = ['turno_otorgado', 'turno_cancelado', 'turno_reprogramado'];
|
|
|
|
foreach ($tipos as $tipo) {
|
|
Notificacion::query()->updateOrCreate(
|
|
['tipo' => $tipo],
|
|
[
|
|
'mensaje_inicio' => $validado[$tipo . '_mensaje_inicio'],
|
|
'mensaje_final' => $validado[$tipo . '_mensaje_final'],
|
|
]
|
|
);
|
|
}
|
|
|
|
return redirect('/administrador/emails')
|
|
->with('admin_action_success', 'Los textos de emails se actualizaron correctamente.');
|
|
});
|
|
|
|
Route::get('/administrador/bugs', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$bugs = Bug::query()
|
|
->with('fotoBug')
|
|
->orderByDesc('id')
|
|
->get();
|
|
|
|
return view('administrador.reportes-bugs', [
|
|
'bugs' => $bugs,
|
|
]);
|
|
});
|
|
|
|
Route::post('/administrador/bugs/{bug}/marcar-visto', function (Request $request, Bug $bug) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$estadoActual = mb_strtolower(trim((string) $bug->estado));
|
|
|
|
if ($estadoActual === 'pendiente') {
|
|
$bug->update([
|
|
'estado' => 'Visto',
|
|
]);
|
|
|
|
return redirect('/administrador/bugs')
|
|
->with('admin_action_success', 'El reporte se marcó como visto.');
|
|
}
|
|
|
|
return redirect('/administrador/bugs')
|
|
->with('admin_action_info', 'El reporte ya estaba en estado visto.');
|
|
});
|
|
|
|
Route::post('/administrador/contenido-web', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'quienessomos' => ['required', 'string', 'max:5000'],
|
|
]);
|
|
|
|
$contenidoWeb = ContenidoWeb::latest('id')->first();
|
|
|
|
if ($contenidoWeb) {
|
|
$contenidoWeb->update([
|
|
'quienessomos' => $validated['quienessomos'],
|
|
]);
|
|
} else {
|
|
ContenidoWeb::create([
|
|
'quienessomos' => $validated['quienessomos'],
|
|
]);
|
|
}
|
|
|
|
return redirect('/administrador/contenido/quienes-somos')
|
|
->with('admin_action_success', 'Se actualizó la sección Quienes Somos correctamente.');
|
|
});
|
|
|
|
Route::post('/administrador/contenido-web/faqs', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'intencion' => ['nullable', 'string', 'max:100'],
|
|
'palabras_clave' => ['nullable', 'string', 'max:1000'],
|
|
'respuesta' => ['required', 'string', 'max:5000'],
|
|
'orden' => ['required', 'integer', 'min:0', 'max:65535'],
|
|
'activo' => ['required', 'in:0,1'],
|
|
]);
|
|
|
|
$palabrasClave = collect(explode(',', (string) ($validated['palabras_clave'] ?? '')))
|
|
->map(fn ($palabra) => trim($palabra))
|
|
->filter(fn ($palabra) => $palabra !== '')
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
$intencionNormalizada = mb_strtolower(trim((string) ($validated['intencion'] ?? '')));
|
|
$esMensajeSistema = in_array($intencionNormalizada, [
|
|
FAQ_INTENCION_BURBUJA,
|
|
FAQ_INTENCION_PANEL_INICIO,
|
|
FAQ_INTENCION_ERROR,
|
|
FAQ_INTENCION_NOMBRE,
|
|
FAQ_INTENCION_CHIPS,
|
|
], true);
|
|
|
|
if (!$esMensajeSistema && empty($palabrasClave)) {
|
|
return back()
|
|
->withErrors(['palabras_clave' => 'Ingresá al menos una palabra clave válida para respuestas del chat.'])
|
|
->withInput();
|
|
}
|
|
|
|
FaqAsistente::create([
|
|
'intencion' => $intencionNormalizada !== '' ? $intencionNormalizada : null,
|
|
'palabras_clave' => $palabrasClave,
|
|
'respuesta' => $validated['respuesta'],
|
|
'orden' => (int) $validated['orden'],
|
|
'activo' => (bool) $validated['activo'],
|
|
]);
|
|
|
|
if ($intencionNormalizada === FAQ_INTENCION_CHIPS) {
|
|
return redirect('/administrador/contenido/asistente-virtual/agregar-chips')
|
|
->with('admin_action_success', 'ui_chip creado correctamente.');
|
|
}
|
|
|
|
return redirect('/administrador/contenido/asistente-virtual/agregar-faq')
|
|
->with('admin_action_success', 'FAQ del asistente creada correctamente.');
|
|
});
|
|
|
|
Route::put('/administrador/contenido-web/faqs/{faqAsistente}', function (Request $request, FaqAsistente $faqAsistente) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'intencion' => ['nullable', 'string', 'max:100'],
|
|
'palabras_clave' => ['nullable', 'string', 'max:1000'],
|
|
'respuesta' => ['required', 'string', 'max:5000'],
|
|
'orden' => ['required', 'integer', 'min:0', 'max:65535'],
|
|
'activo' => ['required', 'in:0,1'],
|
|
]);
|
|
|
|
$palabrasClave = collect(explode(',', (string) ($validated['palabras_clave'] ?? '')))
|
|
->map(fn ($palabra) => trim($palabra))
|
|
->filter(fn ($palabra) => $palabra !== '')
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
$intencionNormalizada = mb_strtolower(trim((string) ($validated['intencion'] ?? '')));
|
|
$esMensajeSistema = in_array($intencionNormalizada, [
|
|
FAQ_INTENCION_BURBUJA,
|
|
FAQ_INTENCION_PANEL_INICIO,
|
|
FAQ_INTENCION_ERROR,
|
|
FAQ_INTENCION_NOMBRE,
|
|
FAQ_INTENCION_CHIPS,
|
|
], true);
|
|
|
|
if (!$esMensajeSistema && empty($palabrasClave)) {
|
|
return back()
|
|
->withErrors(['palabras_clave' => 'Ingresá al menos una palabra clave válida para respuestas del chat.'])
|
|
->withInput();
|
|
}
|
|
|
|
$faqAsistente->update([
|
|
'intencion' => $intencionNormalizada !== '' ? $intencionNormalizada : null,
|
|
'palabras_clave' => $palabrasClave,
|
|
'respuesta' => $validated['respuesta'],
|
|
'orden' => (int) $validated['orden'],
|
|
'activo' => (bool) $validated['activo'],
|
|
]);
|
|
|
|
if ($intencionNormalizada === FAQ_INTENCION_CHIPS) {
|
|
return redirect('/administrador/contenido/asistente-virtual/agregar-chips')
|
|
->with('admin_action_success', 'ui_chip actualizado correctamente.');
|
|
}
|
|
|
|
return redirect('/administrador/contenido/asistente-virtual/ver-faqs')
|
|
->with('admin_action_success', 'FAQ del asistente actualizada correctamente.');
|
|
});
|
|
|
|
Route::delete('/administrador/contenido-web/faqs/{faqAsistente}', function (Request $request, FaqAsistente $faqAsistente) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$esChip = mb_strtolower(trim((string) ($faqAsistente->intencion ?? ''))) === FAQ_INTENCION_CHIPS;
|
|
|
|
$faqAsistente->delete();
|
|
|
|
if ($esChip) {
|
|
return redirect('/administrador/contenido/asistente-virtual/agregar-chips')
|
|
->with('admin_action_success', 'ui_chip borrado correctamente.');
|
|
}
|
|
|
|
return redirect('/administrador/contenido/asistente-virtual/ver-faqs')
|
|
->with('admin_action_success', 'FAQ del asistente borrada correctamente.');
|
|
});
|
|
|
|
Route::get('/administrador/profesiones/crear', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
return view('administrador.crear-profesion');
|
|
});
|
|
|
|
Route::post('/administrador/profesiones', function (Request $request) use ($registrarLogSeguridad, $validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'titulo' => ['required', 'string', 'max:255', 'unique:profesiones,titulo'],
|
|
'visible_en_formulario' => ['required', 'in:0,1'],
|
|
]);
|
|
|
|
$profesionCreada = Profesion::create([
|
|
'titulo' => $validated['titulo'],
|
|
'visible_en_formulario' => (bool) $validated['visible_en_formulario'],
|
|
]);
|
|
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Creación nueva profesion',
|
|
'Se creó la profesión ID ' . $profesionCreada->id . ' (' . $profesionCreada->titulo . ').'
|
|
);
|
|
|
|
return redirect('/administrador/contenido/profesiones')
|
|
->with('admin_action_success', 'Profesión creada correctamente.');
|
|
});
|
|
|
|
Route::get('/administrador/profesiones/{profesion}/editar', function (Request $request, Profesion $profesion) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
return view('administrador.editar-profesion', [
|
|
'profesion' => $profesion,
|
|
]);
|
|
});
|
|
|
|
Route::put('/administrador/profesiones/{profesion}', function (Request $request, Profesion $profesion) use ($registrarLogSeguridad, $validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'titulo' => ['required', 'string', 'max:255', 'unique:profesiones,titulo,' . $profesion->id],
|
|
'visible_en_formulario' => ['required', 'in:0,1'],
|
|
]);
|
|
|
|
$tituloAnterior = (string) $profesion->titulo;
|
|
$visibleAnterior = (bool) $profesion->visible_en_formulario;
|
|
|
|
$profesion->update([
|
|
'titulo' => $validated['titulo'],
|
|
'visible_en_formulario' => (bool) $validated['visible_en_formulario'],
|
|
]);
|
|
|
|
$visibleNuevo = (bool) $profesion->visible_en_formulario;
|
|
if ($visibleAnterior && !$visibleNuevo) {
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Baja profesion',
|
|
'Se dio de baja la profesión ID ' . $profesion->id . ' (' . $profesion->titulo . ').'
|
|
);
|
|
} elseif (!$visibleAnterior && $visibleNuevo) {
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Alta profesion',
|
|
'Se dio de alta la profesión ID ' . $profesion->id . ' (' . $profesion->titulo . ').'
|
|
);
|
|
} else {
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Edición datos profesion',
|
|
'Se editó la profesión ID ' . $profesion->id . '. Título: "' . $tituloAnterior . '" -> "' . $profesion->titulo . '".'
|
|
);
|
|
}
|
|
|
|
return redirect('/administrador/contenido/profesiones')
|
|
->with('admin_action_success', 'Profesión actualizada correctamente.');
|
|
});
|
|
|
|
Route::get('/administrador/servicios/crear', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$profesiones = Profesion::query()
|
|
->orderBy('titulo')
|
|
->get();
|
|
|
|
return view('administrador.crear-servicio', [
|
|
'profesiones' => $profesiones,
|
|
]);
|
|
});
|
|
|
|
Route::post('/administrador/servicios', function (Request $request) use ($registrarLogSeguridad, $validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$contenidoWeb = ContenidoWeb::latest('id')->first();
|
|
|
|
if (!$contenidoWeb) {
|
|
$contenidoWeb = ContenidoWeb::create([
|
|
'quienessomos' => '',
|
|
]);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'titulo' => ['required', 'string', 'max:255'],
|
|
'descripcion' => ['required', 'string', 'max:3000'],
|
|
'estado' => ['required', 'in:activo,inactivo'],
|
|
'visibleenweb' => ['required', 'in:si,no'],
|
|
'profesion_id' => ['required', 'exists:profesiones,id'],
|
|
'foto' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:4096'],
|
|
]);
|
|
|
|
$fotoDefaultId = Foto::query()
|
|
->where('ruta', 'storage/fotos/Servicio.jpg')
|
|
->value('id');
|
|
|
|
if (!$fotoDefaultId) {
|
|
$fotoDefaultId = Foto::query()->create([
|
|
'extension' => 'jpg',
|
|
'tamanio_bytes' => 0,
|
|
'nombre' => 'Servicio',
|
|
'mime_type' => 'image/jpeg',
|
|
'ruta' => 'storage/fotos/Servicio.jpg',
|
|
])->id;
|
|
}
|
|
|
|
$datos = [
|
|
'titulo' => $validated['titulo'],
|
|
'descripcion' => $validated['descripcion'],
|
|
'estado' => $validated['estado'],
|
|
'visibleenweb' => $validated['visibleenweb'],
|
|
'contenidoweb_id' => (int) $contenidoWeb->id,
|
|
'profesion_id' => (int) $validated['profesion_id'],
|
|
'foto_id' => (int) $fotoDefaultId,
|
|
];
|
|
|
|
$fotoFile = $request->file('foto');
|
|
if ($fotoFile) {
|
|
$directorio = public_path('images/servicios');
|
|
File::ensureDirectoryExists($directorio);
|
|
|
|
$extension = $fotoFile->getClientOriginalExtension();
|
|
$mimeType = $fotoFile->getClientMimeType();
|
|
$tamanioBytes = $fotoFile->getSize();
|
|
$nombreArchivo = uniqid('serv_', true) . '.' . $extension;
|
|
|
|
$fotoFile->move($directorio, $nombreArchivo);
|
|
|
|
$foto = Foto::create([
|
|
'extension' => $extension,
|
|
'nombre' => pathinfo($nombreArchivo, PATHINFO_FILENAME),
|
|
'mime_type' => $mimeType,
|
|
'tamanio_bytes' => $tamanioBytes,
|
|
'ruta' => 'images/servicios/' . $nombreArchivo,
|
|
]);
|
|
|
|
$datos['foto_id'] = $foto->id;
|
|
}
|
|
|
|
$servicioCreado = Servicio::create($datos);
|
|
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Creación nuevo servicio',
|
|
'Se creó el servicio ID ' . $servicioCreado->id . ' (' . $servicioCreado->titulo . ') para la profesión ID ' . (int) $servicioCreado->profesion_id . '.'
|
|
);
|
|
|
|
return redirect('/administrador/contenido/servicios')
|
|
->with('admin_action_success', 'Servicio creado correctamente.');
|
|
});
|
|
|
|
Route::get('/administrador/servicios/{servicio}/editar', function (Request $request, Servicio $servicio) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$profesiones = Profesion::query()
|
|
->orderBy('titulo')
|
|
->get();
|
|
|
|
return view('administrador.editar-servicio', [
|
|
'servicio' => $servicio,
|
|
'profesiones' => $profesiones,
|
|
]);
|
|
});
|
|
|
|
Route::put('/administrador/servicios/{servicio}', function (Request $request, Servicio $servicio) use ($registrarLogSeguridad, $validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'titulo' => ['required', 'string', 'max:255'],
|
|
'descripcion' => ['required', 'string', 'max:3000'],
|
|
'estado' => ['required', 'in:activo,inactivo'],
|
|
'visibleenweb' => ['required', 'in:si,no'],
|
|
'profesion_id' => ['required', 'exists:profesiones,id'],
|
|
'foto' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:4096'],
|
|
]);
|
|
|
|
$tituloAnterior = (string) $servicio->titulo;
|
|
$estadoAnterior = mb_strtolower(trim((string) $servicio->estado));
|
|
|
|
$datos = [
|
|
'titulo' => $validated['titulo'],
|
|
'descripcion' => $validated['descripcion'],
|
|
'estado' => $validated['estado'],
|
|
'visibleenweb' => $validated['visibleenweb'],
|
|
'profesion_id' => (int) $validated['profesion_id'],
|
|
];
|
|
|
|
$fotoFile = $request->file('foto');
|
|
if ($fotoFile) {
|
|
$directorio = public_path('images/servicios');
|
|
File::ensureDirectoryExists($directorio);
|
|
|
|
$extension = $fotoFile->getClientOriginalExtension();
|
|
$mimeType = $fotoFile->getClientMimeType();
|
|
$tamanioBytes = $fotoFile->getSize();
|
|
$nombreArchivo = uniqid('serv_', true) . '.' . $extension;
|
|
|
|
$fotoFile->move($directorio, $nombreArchivo);
|
|
|
|
$foto = Foto::create([
|
|
'extension' => $extension,
|
|
'nombre' => pathinfo($nombreArchivo, PATHINFO_FILENAME),
|
|
'mime_type' => $mimeType,
|
|
'tamanio_bytes' => $tamanioBytes,
|
|
'ruta' => 'images/servicios/' . $nombreArchivo,
|
|
]);
|
|
|
|
$datos['foto_id'] = $foto->id;
|
|
}
|
|
|
|
$servicio->update($datos);
|
|
|
|
$estadoNuevo = mb_strtolower(trim((string) $servicio->estado));
|
|
if ($estadoAnterior === 'activo' && $estadoNuevo !== 'activo') {
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Baja servicio',
|
|
'Se dio de baja el servicio ID ' . $servicio->id . ' (' . $servicio->titulo . ').'
|
|
);
|
|
} elseif ($estadoAnterior !== 'activo' && $estadoNuevo === 'activo') {
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Alta servicio',
|
|
'Se dio de alta el servicio ID ' . $servicio->id . ' (' . $servicio->titulo . ').'
|
|
);
|
|
} else {
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Edición datos servicio',
|
|
'Se editó el servicio ID ' . $servicio->id . '. Título: "' . $tituloAnterior . '" -> "' . $servicio->titulo . '".'
|
|
);
|
|
}
|
|
|
|
return redirect('/administrador/contenido/servicios')
|
|
->with('admin_action_success', 'Servicio actualizado correctamente.');
|
|
});
|
|
|
|
Route::get('/administrador/profesionales', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$filtroTexto = trim((string) $request->query('q', ''));
|
|
$filtroEstado = trim((string) $request->query('estado', ''));
|
|
|
|
$profesionales = Profesional::with([
|
|
'persona',
|
|
'profesion',
|
|
'profesiones',
|
|
'credencialProfesional',
|
|
])
|
|
->when($filtroTexto !== '', function ($query) use ($filtroTexto) {
|
|
$query->where(function ($subQuery) use ($filtroTexto): void {
|
|
$subQuery->where('dni', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('correo', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('matricula', 'like', '%' . $filtroTexto . '%')
|
|
->orWhereHas('persona', function ($qPersona) use ($filtroTexto): void {
|
|
$qPersona->where('nombre', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('apellido', 'like', '%' . $filtroTexto . '%')
|
|
->orWhere('dni', 'like', '%' . $filtroTexto . '%');
|
|
})
|
|
->orWhereHas('profesion', function ($qProfesion) use ($filtroTexto): void {
|
|
$qProfesion->where('titulo', 'like', '%' . $filtroTexto . '%');
|
|
});
|
|
});
|
|
})
|
|
->when($filtroEstado === 'activos', fn ($query) => $query->where('baja_id', 1))
|
|
->when($filtroEstado === 'inactivos', fn ($query) => $query->where('baja_id', '!=', 1))
|
|
->orderBy('id')
|
|
->paginate(10)
|
|
->withQueryString();
|
|
|
|
return view('administrador.profesionales', [
|
|
'profesionales' => $profesionales,
|
|
]);
|
|
});
|
|
|
|
Route::post('/administrador/profesionales/{profesional}/baja', function (Request $request, Profesional $profesional) use ($registrarLogSeguridad, $validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$estabaDeBaja = (int) $profesional->baja_id !== 1;
|
|
$nombreCompleto = trim(($profesional->persona?->nombre ?? '') . ' ' . ($profesional->persona?->apellido ?? ''));
|
|
|
|
DB::transaction(function () use ($profesional): void {
|
|
if ((int) $profesional->baja_id !== 1) {
|
|
$profesional->baja_id = 1;
|
|
$profesional->save();
|
|
return;
|
|
}
|
|
|
|
$baja = Baja::create([
|
|
'descripcion' => 'Baja realizada por administrador',
|
|
]);
|
|
|
|
$profesional->baja_id = $baja->id;
|
|
$profesional->save();
|
|
});
|
|
|
|
if ($estabaDeBaja) {
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Alta profesional',
|
|
'Se dio de alta al profesional ID ' . $profesional->id . ' (' . $nombreCompleto . ').'
|
|
);
|
|
|
|
return redirect('/administrador/profesionales')
|
|
->with('admin_action_success', 'Profesional reactivado correctamente.');
|
|
}
|
|
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Baja profesional',
|
|
'Se dio de baja al profesional ID ' . $profesional->id . ' (' . $nombreCompleto . ').'
|
|
);
|
|
|
|
return redirect('/administrador/profesionales')
|
|
->with('admin_action_success', 'Profesional dado de baja correctamente.');
|
|
});
|
|
|
|
Route::get('/administrador/profesionales/nuevo', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$profesiones = Profesion::query()
|
|
->orderBy('titulo')
|
|
->get();
|
|
|
|
$servicios = Servicio::query()
|
|
->where('estado', 'activo')
|
|
->orderBy('titulo')
|
|
->get();
|
|
|
|
return view('administrador.nuevo-profesional', [
|
|
'profesiones' => $profesiones,
|
|
'servicios' => $servicios,
|
|
]);
|
|
});
|
|
|
|
Route::get('/administrador/profesionales/buscar-persona', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$dni = trim((string) $request->query('dni', ''));
|
|
|
|
if (strlen($dni) < 3) {
|
|
return response()->json(['encontrada' => false, 'persona' => null]);
|
|
}
|
|
|
|
$persona = Persona::where('dni', $dni)->first();
|
|
|
|
if (!$persona) {
|
|
return response()->json(['encontrada' => false, 'persona' => null]);
|
|
}
|
|
|
|
$celular = trim((string) ($persona->telefonos()->value('telefono') ?? ''));
|
|
|
|
return response()->json([
|
|
'encontrada' => true,
|
|
'persona' => [
|
|
'nombre' => $persona->nombre,
|
|
'apellido' => $persona->apellido,
|
|
'cuil' => $persona->cuil,
|
|
'celular' => $celular,
|
|
'fechanac' => $persona->fechanac,
|
|
],
|
|
]);
|
|
});
|
|
|
|
Route::get('/administrador/profesionales/{profesional}/editar', function (Request $request, Profesional $profesional) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$profesiones = Profesion::query()
|
|
->orderBy('titulo')
|
|
->get();
|
|
|
|
$servicios = Servicio::query()
|
|
->where('estado', 'activo')
|
|
->orderBy('titulo')
|
|
->get();
|
|
|
|
$serviciosSeleccionados = $profesional->servicios()
|
|
->pluck('servicios.id')
|
|
->all();
|
|
|
|
return view('administrador.editar-profesional', [
|
|
'profesional' => $profesional,
|
|
'profesiones' => $profesiones,
|
|
'servicios' => $servicios,
|
|
'serviciosSeleccionados' => $serviciosSeleccionados,
|
|
]);
|
|
});
|
|
|
|
Route::get('/administrador/profesionales/{profesional}/asignaciones', function (Request $request, Profesional $profesional) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$profesional->load(['persona', 'profesion', 'profesiones', 'servicios']);
|
|
|
|
$servicios = Servicio::query()
|
|
->where('estado', 'activo')
|
|
->orderBy('titulo')
|
|
->get();
|
|
|
|
$serviciosSeleccionados = $profesional->servicios()
|
|
->pluck('servicios.id')
|
|
->all();
|
|
|
|
return view('administrador.asignaciones-profesional', [
|
|
'profesional' => $profesional,
|
|
'servicios' => $servicios,
|
|
'serviciosSeleccionados' => $serviciosSeleccionados,
|
|
]);
|
|
});
|
|
|
|
Route::put('/administrador/profesionales/{profesional}/asignaciones', function (Request $request, Profesional $profesional) use ($registrarLogSeguridad, $validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'servicio_ids' => ['required', 'array', 'min:1'],
|
|
'servicio_ids.*' => [
|
|
'required',
|
|
Rule::exists('servicios', 'id')->where(fn ($q) => $q
|
|
->where('profesion_id', (int) $profesional->profesion_id)
|
|
->where('estado', 'activo')),
|
|
],
|
|
]);
|
|
|
|
$profesional->profesiones()->sync([(int) $profesional->profesion_id]);
|
|
|
|
$profesional->servicios()->sync($validated['servicio_ids']);
|
|
|
|
$profesionActual = Profesion::query()->find((int) $profesional->profesion_id)?->titulo ?? 'Sin profesión';
|
|
$nombreCompleto = trim(($profesional->persona?->nombre ?? '') . ' ' . ($profesional->persona?->apellido ?? ''));
|
|
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Edición datos profesional',
|
|
'Se actualizaron servicios del profesional ID ' . $profesional->id . ' (' . $nombreCompleto . ') en la profesión "' . $profesionActual . '". Servicios asignados: ' . implode(', ', $validated['servicio_ids']) . '.'
|
|
);
|
|
|
|
return redirect('/administrador/profesionales')
|
|
->with('admin_action_success', 'Se actualizaron los servicios del profesional.');
|
|
});
|
|
|
|
Route::put('/administrador/profesionales/{profesional}', function (Request $request, Profesional $profesional) use ($registrarLogSeguridad, $validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$profesionBloqueadaId = (int) $profesional->profesion_id;
|
|
|
|
$validated = $request->validate([
|
|
'dni' => ['required', 'string', 'max:20', 'regex:/^[0-9A-Za-z]{7,20}$/', Rule::unique('personas', 'dni')->ignore((int) ($profesional->persona_id ?? 0))],
|
|
'nombre' => ['required', 'string', 'max:100', 'regex:/^[\pL\s]+$/u'],
|
|
'apellido' => ['required', 'string', 'max:100', 'regex:/^[\pL\s]+$/u'],
|
|
'cuil' => ['required', 'string', 'max:30', 'regex:/^[0-9]+$/'],
|
|
'fechanac' => ['required', 'date'],
|
|
'correo' => ['required', 'email', 'max:255'],
|
|
'matricula' => [
|
|
'required',
|
|
'string',
|
|
'max:100',
|
|
Rule::unique('profesionales', 'matricula')
|
|
->where(fn ($q) => $q->where('profesion_id', $profesionBloqueadaId))
|
|
->ignore((int) $profesional->id),
|
|
],
|
|
'profesion_id' => ['required', Rule::in([$profesionBloqueadaId])],
|
|
'foto' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:4096'],
|
|
'quitar_foto' => ['nullable', 'boolean'],
|
|
'servicio_ids' => ['required', 'array', 'min:1'],
|
|
'servicio_ids.*' => [
|
|
'required',
|
|
Rule::exists('servicios', 'id')->where(fn ($q) => $q
|
|
->where('profesion_id', $profesionBloqueadaId)
|
|
->where('estado', 'activo')),
|
|
],
|
|
]);
|
|
|
|
$dniAnterior = (string) $profesional->dni;
|
|
$cuilAnterior = (string) ($profesional->persona?->cuil ?? '');
|
|
$fechanacAnterior = (string) ($profesional->persona?->fechanac ?? '');
|
|
$nombreAnterior = (string) ($profesional->persona?->nombre ?? '');
|
|
$apellidoAnterior = (string) ($profesional->persona?->apellido ?? '');
|
|
$correoAnterior = $profesional->correo;
|
|
$matriculaAnterior = (string) $profesional->matricula;
|
|
$profesionAnteriorId = (int) $profesional->profesion_id;
|
|
$usuarioCredencialAnterior = (string) ($profesional->credencialProfesional?->usuario ?? '');
|
|
|
|
$nuevoUsuarioCredencial = $validated['dni'] . '-' . $profesionBloqueadaId;
|
|
if (
|
|
$nuevoUsuarioCredencial !== ''
|
|
&& $nuevoUsuarioCredencial !== $usuarioCredencialAnterior
|
|
&& CredencialProfesional::where('usuario', $nuevoUsuarioCredencial)
|
|
->where('id', '!=', (int) ($profesional->credencialprofesional_id ?? 0))
|
|
->exists()
|
|
) {
|
|
return back()
|
|
->withErrors(['profesion_id' => 'El usuario generado para esa combinación de DNI y profesión ya existe.'])
|
|
->withInput();
|
|
}
|
|
|
|
if ($profesional->persona) {
|
|
$profesional->persona->update([
|
|
'dni' => $validated['dni'],
|
|
'nombre' => $validated['nombre'],
|
|
'apellido' => $validated['apellido'],
|
|
'cuil' => $validated['cuil'],
|
|
'fechanac' => $validated['fechanac'],
|
|
]);
|
|
|
|
Profesional::query()
|
|
->where('persona_id', (int) $profesional->persona->id)
|
|
->update([
|
|
'dni' => $validated['dni'],
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
DB::table('personas_telefonos')
|
|
->where('persona_id', (int) $profesional->persona->id)
|
|
->update([
|
|
'dni' => $validated['dni'],
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$fotoFile = $request->file('foto');
|
|
if ($fotoFile) {
|
|
$directorio = public_path('images/profesionales');
|
|
File::ensureDirectoryExists($directorio);
|
|
|
|
$extension = $fotoFile->getClientOriginalExtension();
|
|
$mimeType = $fotoFile->getClientMimeType();
|
|
$tamanioBytes = $fotoFile->getSize();
|
|
$nombreArchivo = uniqid('prof_', true) . '.' . $extension;
|
|
|
|
$fotoFile->move($directorio, $nombreArchivo);
|
|
|
|
$foto = Foto::create([
|
|
'extension' => $extension,
|
|
'nombre' => pathinfo($nombreArchivo, PATHINFO_FILENAME),
|
|
'mime_type' => $mimeType,
|
|
'tamanio_bytes' => $tamanioBytes,
|
|
'ruta' => 'images/profesionales/' . $nombreArchivo,
|
|
]);
|
|
|
|
$profesional->persona->update([
|
|
'foto_id' => $foto->id,
|
|
]);
|
|
} elseif ((bool) ($validated['quitar_foto'] ?? false)) {
|
|
$fotoDefault = Foto::firstOrCreate(
|
|
['ruta' => 'images/avatar_default.png'],
|
|
[
|
|
'extension' => 'png',
|
|
'nombre' => 'avatar_default',
|
|
'mime_type' => 'image/png',
|
|
'tamanio_bytes' => 136788,
|
|
]
|
|
);
|
|
|
|
$profesional->persona->update([
|
|
'foto_id' => $fotoDefault->id,
|
|
]);
|
|
}
|
|
}
|
|
|
|
$profesional->update([
|
|
'dni' => $validated['dni'],
|
|
'matricula' => $validated['matricula'],
|
|
'correo' => $validated['correo'],
|
|
]);
|
|
|
|
$profesional->profesiones()->sync([$profesionBloqueadaId]);
|
|
|
|
$profesional->servicios()->sync($validated['servicio_ids']);
|
|
|
|
if ($profesional->credencialProfesional && $nuevoUsuarioCredencial !== '') {
|
|
$profesional->credencialProfesional->update([
|
|
'usuario' => $nuevoUsuarioCredencial,
|
|
]);
|
|
}
|
|
|
|
$nombreCompleto = trim(($profesional->persona?->nombre ?? '') . ' ' . ($profesional->persona?->apellido ?? ''));
|
|
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Edición datos profesional',
|
|
'Se editó el profesional ID ' . $profesional->id . ' (' . $nombreCompleto . '). Nombre: "' . $nombreAnterior . '" -> "' . $validated['nombre'] . '", Apellido: "' . $apellidoAnterior . '" -> "' . $validated['apellido'] . '", CUIL: "' . $cuilAnterior . '" -> "' . $validated['cuil'] . '", Fecha Nac.: "' . $fechanacAnterior . '" -> "' . $validated['fechanac'] . '", Correo: "' . $correoAnterior . '" -> "' . $validated['correo'] . '", Matrícula: "' . $matriculaAnterior . '" -> "' . $validated['matricula'] . '", Profesión ID: "' . $profesionAnteriorId . '", Usuario credencial: "' . $usuarioCredencialAnterior . '" -> "' . $nuevoUsuarioCredencial . '".'
|
|
);
|
|
|
|
if ($dniAnterior !== (string) $validated['dni']) {
|
|
$personaResponsableId = Administrador::query()
|
|
->where('credencialprofesional_id', (int) $request->session()->get('personal_credencial_id', 0))
|
|
->value('persona_id');
|
|
|
|
if ($personaResponsableId) {
|
|
LogSeguridad::create([
|
|
'descripcion' => 'Cambio de DNI del profesional ID ' . $profesional->id . ': "' . $dniAnterior . '" -> "' . $validated['dni'] . '".',
|
|
'fechahora' => now(),
|
|
'IPorigen' => (string) $request->ip(),
|
|
'rol' => 'ADMIN',
|
|
'persona_id' => (int) $personaResponsableId,
|
|
'accion_id' => 33,
|
|
]);
|
|
}
|
|
}
|
|
|
|
return redirect('/administrador/profesionales')
|
|
->with('admin_action_success', 'Profesional actualizado correctamente.');
|
|
});
|
|
|
|
Route::post('/administrador/profesionales', function (Request $request) use ($registrarLogSeguridad, $validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$dni = trim((string) $request->input('dni', ''));
|
|
$dniVerificado = trim((string) $request->input('dni_verificado', ''));
|
|
|
|
// Verificar en DB si la persona ya existe (autoritativo, no se confía en el campo oculto)
|
|
$personaExistente = Persona::where('dni', $dni)->first();
|
|
|
|
$reglas = [
|
|
'dni' => ['required', 'string', 'max:20', 'regex:/^[0-9A-Za-z]{7,20}$/'],
|
|
'dni_verificado' => ['required', 'string', 'max:20'],
|
|
'correo' => ['required', 'email', 'max:255'],
|
|
'foto' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:4096'],
|
|
'matricula' => ['required', 'string', 'max:100'],
|
|
'celular' => ['required', 'string', 'max:30', 'regex:/^[0-9]+$/'],
|
|
'profesion_id' => ['required', 'exists:profesiones,id'],
|
|
'servicio_ids' => ['required', 'array', 'min:1'],
|
|
'servicio_ids.*' => [
|
|
'required',
|
|
Rule::exists('servicios', 'id')->where(fn ($q) => $q
|
|
->where('profesion_id', $request->input('profesion_id'))
|
|
->where('estado', 'activo')),
|
|
],
|
|
'contra' => ['required', 'string', 'min:6'],
|
|
];
|
|
|
|
if (!$personaExistente) {
|
|
$reglas = array_merge($reglas, [
|
|
'nombre' => ['required', 'string', 'max:100', 'regex:/^[\pL\s]+$/u'],
|
|
'apellido' => ['required', 'string', 'max:100', 'regex:/^[\pL\s]+$/u'],
|
|
'cuil' => ['required', 'string', 'max:30', 'regex:/^[0-9]+$/'],
|
|
'fechanac' => ['required', 'date'],
|
|
]);
|
|
}
|
|
|
|
$validated = $request->validate($reglas);
|
|
|
|
if ($dniVerificado === '' || $dniVerificado !== $dni) {
|
|
return back()
|
|
->withErrors(['dni' => 'Debes verificar el DNI antes de guardar el profesional.'])
|
|
->withInput();
|
|
}
|
|
|
|
// El usuario se auto-genera como DNI-CODIGO_PROFESION (id de profesion)
|
|
$usuario = $validated['dni'] . '-' . $validated['profesion_id'];
|
|
|
|
// Verificar que no exista ya un profesional con ese DNI y esa profesión
|
|
if (Profesional::where('dni', $validated['dni'])->where('profesion_id', $validated['profesion_id'])->exists()) {
|
|
return back()
|
|
->withErrors(['dni' => "Este profesional ya está registrado en esa profesión."])
|
|
->withInput();
|
|
}
|
|
|
|
// Verificar que el usuario auto-generado no esté en uso
|
|
if (CredencialProfesional::where('usuario', $usuario)->exists()) {
|
|
return back()
|
|
->withErrors(['dni' => "El usuario auto-generado «{$usuario}» ya existe. Verifique los datos."])
|
|
->withInput();
|
|
}
|
|
|
|
$fotoFile = $request->file('foto');
|
|
|
|
$profesionalCreado = null;
|
|
|
|
DB::transaction(function () use ($validated, $fotoFile, $personaExistente, $usuario, &$profesionalCreado): void {
|
|
if ($personaExistente) {
|
|
$persona = $personaExistente;
|
|
|
|
if ($fotoFile) {
|
|
$directorio = public_path('images/profesionales');
|
|
File::ensureDirectoryExists($directorio);
|
|
|
|
$extension = $fotoFile->getClientOriginalExtension();
|
|
$mimeType = $fotoFile->getClientMimeType();
|
|
$tamanioBytes = $fotoFile->getSize();
|
|
$nombreArchivo = uniqid('prof_', true) . '.' . $extension;
|
|
|
|
$fotoFile->move($directorio, $nombreArchivo);
|
|
|
|
$foto = Foto::create([
|
|
'extension' => $extension,
|
|
'nombre' => pathinfo($nombreArchivo, PATHINFO_FILENAME),
|
|
'mime_type' => $mimeType,
|
|
'tamanio_bytes' => $tamanioBytes,
|
|
'ruta' => 'images/profesionales/' . $nombreArchivo,
|
|
]);
|
|
|
|
$persona->update([
|
|
'foto_id' => $foto->id,
|
|
]);
|
|
}
|
|
} else {
|
|
if ($fotoFile) {
|
|
$directorio = public_path('images/profesionales');
|
|
File::ensureDirectoryExists($directorio);
|
|
|
|
// Capturar metadatos ANTES de mover el archivo (move() invalida el objeto)
|
|
$extension = $fotoFile->getClientOriginalExtension();
|
|
$mimeType = $fotoFile->getClientMimeType();
|
|
$tamanioBytes = $fotoFile->getSize();
|
|
$nombreArchivo = uniqid('prof_', true) . '.' . $extension;
|
|
|
|
$fotoFile->move($directorio, $nombreArchivo);
|
|
|
|
$foto = Foto::create([
|
|
'extension' => $extension,
|
|
'nombre' => pathinfo($nombreArchivo, PATHINFO_FILENAME),
|
|
'mime_type' => $mimeType,
|
|
'tamanio_bytes' => $tamanioBytes,
|
|
'ruta' => 'images/profesionales/' . $nombreArchivo,
|
|
]);
|
|
} else {
|
|
$foto = Foto::firstOrCreate(
|
|
['ruta' => 'images/avatar_default.png'],
|
|
[
|
|
'extension' => 'png',
|
|
'nombre' => 'avatar_default',
|
|
'mime_type' => 'image/png',
|
|
'tamanio_bytes' => 136788,
|
|
]
|
|
);
|
|
}
|
|
|
|
$persona = Persona::create([
|
|
'dni' => $validated['dni'],
|
|
'nombre' => $validated['nombre'],
|
|
'apellido' => $validated['apellido'],
|
|
'cuil' => $validated['cuil'],
|
|
'fechanac' => $validated['fechanac'],
|
|
'foto_id' => $foto->id,
|
|
]);
|
|
}
|
|
|
|
$telefonoExistente = $persona->telefonos()->first();
|
|
if ($telefonoExistente) {
|
|
$telefonoExistente->update([
|
|
'telefono' => $validated['celular'],
|
|
]);
|
|
} else {
|
|
$telefonoNuevo = Telefono::create([
|
|
'telefono' => $validated['celular'],
|
|
]);
|
|
|
|
DB::table('personas_telefonos')->insertOrIgnore([
|
|
'dni' => (string) $persona->dni,
|
|
'persona_id' => (int) $persona->id,
|
|
'telefono_id' => (int) $telefonoNuevo->id,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
$credencial = CredencialProfesional::create([
|
|
'usuario' => $usuario,
|
|
'contra' => Hash::make($validated['contra']),
|
|
'rol' => 'PROFESIONAL',
|
|
]);
|
|
|
|
$profesional = Profesional::create([
|
|
'profesion_id' => (int) $validated['profesion_id'],
|
|
'matricula' => $validated['matricula'],
|
|
'correo' => $validated['correo'],
|
|
'dni' => $validated['dni'],
|
|
'persona_id' => $persona->id,
|
|
'credencialprofesional_id' => $credencial->id,
|
|
'baja_id' => 1,
|
|
]);
|
|
|
|
Agenda::create([
|
|
'profesional_id' => $profesional->id,
|
|
'estado' => 'activo',
|
|
'duracionturno' => 30,
|
|
]);
|
|
|
|
$profesional->profesiones()->sync([(int) $validated['profesion_id']]);
|
|
$profesional->servicios()->sync($validated['servicio_ids']);
|
|
$profesionalCreado = $profesional;
|
|
});
|
|
|
|
if ($profesionalCreado) {
|
|
$nombreCompleto = trim(($profesionalCreado->persona?->nombre ?? '') . ' ' . ($profesionalCreado->persona?->apellido ?? ''));
|
|
|
|
$registrarLogSeguridad(
|
|
$request,
|
|
'Creación nuevo profesional',
|
|
'Se creó el profesional ID ' . $profesionalCreado->id . ' (' . $nombreCompleto . ').'
|
|
);
|
|
}
|
|
|
|
return redirect('/administrador/profesionales')->with('admin_action_success', 'Profesional creado correctamente.');
|
|
});
|
|
|
|
Route::get('/cliente/dashboard', function (Request $request) {
|
|
if (!$request->session()->get('cliente_auth')) {
|
|
return redirect('/login/cliente')->with('login_error', 'Debes iniciar sesión como cliente.');
|
|
}
|
|
|
|
$clienteCorreoSesion = trim((string) $request->session()->get('cliente_correo', ''));
|
|
|
|
$cliente = Cliente::query()
|
|
->with(['persona.telefonos', 'credencialCliente'])
|
|
->whereHas('credencialCliente', fn ($query) => $query->where('correo', $clienteCorreoSesion))
|
|
->first();
|
|
|
|
$contenidoWeb = ContenidoWeb::latest('id')->first();
|
|
$quienesSomos = $contenidoWeb?->quienessomos;
|
|
$versionSitio = trim((string) ($contenidoWeb?->version ?? '1.0.0'));
|
|
|
|
$profesionales = Profesional::with(['persona.Foto', 'profesion', 'profesiones'])
|
|
->where('baja_id', 1)
|
|
->get()
|
|
->groupBy('persona_id')
|
|
->map(function ($profesionalesPorPersona) {
|
|
$profesionalBase = $profesionalesPorPersona->first();
|
|
|
|
$profesionesCombinadas = $profesionalesPorPersona
|
|
->flatMap(function ($profesional) {
|
|
return $profesional->profesiones
|
|
->push($profesional->profesion)
|
|
->filter();
|
|
})
|
|
->unique('id')
|
|
->values();
|
|
|
|
$profesionalBase->setRelation('profesiones', $profesionesCombinadas);
|
|
|
|
return $profesionalBase;
|
|
})
|
|
->values();
|
|
|
|
$servicios = Servicio::with('foto')
|
|
->where('estado', 'activo')
|
|
->where('visibleenweb', 'si')
|
|
->get();
|
|
|
|
$profesiones = Profesion::query()
|
|
->where('visible_en_formulario', true)
|
|
->orderBy('titulo')
|
|
->get();
|
|
|
|
$modalidades = Modalidad::query()
|
|
->orderBy('descripcion')
|
|
->get();
|
|
|
|
$mensajeBurbujaAsistente = FaqAsistente::query()
|
|
->where('activo', true)
|
|
->where('intencion', FAQ_INTENCION_BURBUJA)
|
|
->value('respuesta') ?? '¡Hola, soy Clara!, ¿te puedo ayudar en algo?';
|
|
|
|
$mensajeInicioPanelAsistente = FaqAsistente::query()
|
|
->where('activo', true)
|
|
->where('intencion', FAQ_INTENCION_PANEL_INICIO)
|
|
->value('respuesta') ?? 'Hola, soy Clara, la asistente virtual de Abogadas del Litoral. Escribí una palabra clave con el tema con el que necesites ayuda.';
|
|
|
|
$nombreAsistente = FaqAsistente::query()
|
|
->where('activo', true)
|
|
->where('intencion', FAQ_INTENCION_NOMBRE)
|
|
->value('respuesta') ?? 'Clara';
|
|
|
|
$chipsAsistente = FaqAsistente::query()
|
|
->where('activo', true)
|
|
->where('intencion', FAQ_INTENCION_CHIPS)
|
|
->orderBy('orden')
|
|
->orderBy('id')
|
|
->pluck('respuesta')
|
|
->flatMap(fn ($respuesta) => explode("\n", (string) $respuesta))
|
|
->map(fn ($c) => trim($c))
|
|
->filter(fn ($c) => $c !== '')
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
$nombreClienteForm = trim((string) ($cliente?->persona?->nombre ?? $request->session()->get('cliente_nombre', '')));
|
|
$apellidoClienteForm = trim((string) ($cliente?->persona?->apellido ?? $request->session()->get('cliente_apellido', '')));
|
|
$correoClienteForm = trim((string) ($cliente?->correo ?? $cliente?->credencialCliente?->correo ?? $clienteCorreoSesion));
|
|
$celularClienteForm = trim((string) ($cliente?->persona?->telefonos?->first()?->telefono ?? $request->session()->get('cliente_celular', '')));
|
|
|
|
return view('cliente.dashboard', [
|
|
'quienesSomos' => $quienesSomos,
|
|
'versionSitio' => $versionSitio,
|
|
'profesionales' => $profesionales,
|
|
'servicios' => $servicios,
|
|
'profesiones' => $profesiones,
|
|
'modalidades' => $modalidades,
|
|
'mensajeBurbujaAsistente' => $mensajeBurbujaAsistente,
|
|
'mensajeInicioPanelAsistente' => $mensajeInicioPanelAsistente,
|
|
'nombreAsistente' => $nombreAsistente,
|
|
'chipsAsistente' => $chipsAsistente,
|
|
'nombreClienteForm' => $nombreClienteForm,
|
|
'apellidoClienteForm' => $apellidoClienteForm,
|
|
'correoClienteForm' => $correoClienteForm,
|
|
'celularClienteForm' => $celularClienteForm,
|
|
]);
|
|
});
|
|
|
|
Route::get('/cliente/mis-turnos', function (Request $request) {
|
|
if (!$request->session()->get('cliente_auth')) {
|
|
return redirect('/login/cliente')->with('login_error', 'Debes iniciar sesión como cliente.');
|
|
}
|
|
|
|
$clienteIdSesion = (int) $request->session()->get('cliente_id', 0);
|
|
$clienteCorreoSesion = trim((string) $request->session()->get('cliente_correo', ''));
|
|
|
|
$periodo = trim((string) $request->query('periodo', 'todos'));
|
|
$estadoId = (int) $request->query('estado_id', 0);
|
|
|
|
$turnosQuery = Turno::query()
|
|
->with([
|
|
'profesional.persona',
|
|
'profesional.profesion',
|
|
'servicio',
|
|
'modalidad',
|
|
'estadoTurno',
|
|
])
|
|
->where(function ($query) use ($clienteIdSesion, $clienteCorreoSesion): void {
|
|
if ($clienteIdSesion > 0) {
|
|
$query->where('cliente_id', $clienteIdSesion);
|
|
}
|
|
|
|
if ($clienteCorreoSesion !== '') {
|
|
if ($clienteIdSesion > 0) {
|
|
$query->orWhere('correo', $clienteCorreoSesion);
|
|
} else {
|
|
$query->where('correo', $clienteCorreoSesion);
|
|
}
|
|
}
|
|
});
|
|
|
|
if ($periodo === 'proximos') {
|
|
$turnosQuery->where('inicio', '>=', now());
|
|
} elseif ($periodo === 'pasados') {
|
|
$turnosQuery->where('inicio', '<', now());
|
|
}
|
|
|
|
if ($estadoId > 0) {
|
|
$turnosQuery->where('estadoturno_id', $estadoId);
|
|
}
|
|
|
|
$turnos = $turnosQuery
|
|
->orderByDesc('inicio')
|
|
->paginate(5)
|
|
->withQueryString();
|
|
|
|
$estadosTurno = EstadoTurno::query()
|
|
->whereNotIn(DB::raw('LOWER(TRIM(descripcion))'), ['cliente ausente', 'cliente presente'])
|
|
->orderBy('descripcion')
|
|
->get(['id', 'descripcion']);
|
|
|
|
$contenidoWeb = ContenidoWeb::latest('id')->first();
|
|
$versionSitio = trim((string) ($contenidoWeb?->version ?? '1.0.0'));
|
|
|
|
return view('cliente.mis-turnos', [
|
|
'turnos' => $turnos,
|
|
'estadosTurno' => $estadosTurno,
|
|
'filtroPeriodo' => $periodo,
|
|
'filtroEstadoId' => $estadoId,
|
|
'versionSitio' => $versionSitio,
|
|
]);
|
|
});
|
|
|
|
Route::get('/cliente/ayuda', function (Request $request) {
|
|
if (!$request->session()->get('cliente_auth')) {
|
|
return redirect('/login/cliente')->with('login_error', 'Debes iniciar sesión como cliente.');
|
|
}
|
|
|
|
return view('cliente.ayuda');
|
|
});
|
|
|
|
Route::post('/cliente/turnos/{turno}/cancelar', function (Request $request, Turno $turno) {
|
|
if (!$request->session()->get('cliente_auth')) {
|
|
return redirect('/login/cliente')->with('login_error', 'Debes iniciar sesión como cliente.');
|
|
}
|
|
|
|
$clienteIdSesion = (int) $request->session()->get('cliente_id', 0);
|
|
$clienteCorreoSesion = trim((string) $request->session()->get('cliente_correo', ''));
|
|
|
|
$esTurnoDelCliente = ($clienteIdSesion > 0 && (int) $turno->cliente_id === $clienteIdSesion)
|
|
|| ($clienteCorreoSesion !== '' && strcasecmp((string) ($turno->correo ?? ''), $clienteCorreoSesion) === 0);
|
|
|
|
if (!$esTurnoDelCliente) {
|
|
return back()->with('turno_error', 'No podés cancelar un turno que no te pertenece.');
|
|
}
|
|
|
|
$estadoCanceladoId = (int) (EstadoTurno::query()
|
|
->whereRaw('LOWER(TRIM(descripcion)) = ?', ['cancelado'])
|
|
->value('id') ?? 0);
|
|
|
|
if ($estadoCanceladoId <= 0) {
|
|
return back()->with('turno_error', 'No se encontró el estado Cancelado.');
|
|
}
|
|
|
|
if ((int) $turno->estadoturno_id === $estadoCanceladoId) {
|
|
return back()->with('turno_error', 'El turno seleccionado ya está cancelado.');
|
|
}
|
|
|
|
$turno->update([
|
|
'estadoturno_id' => $estadoCanceladoId,
|
|
]);
|
|
|
|
$turno->loadMissing(['profesional.persona', 'servicio', 'modalidad']);
|
|
|
|
$correoProfesional = trim((string) ($turno->profesional?->correo ?? ''));
|
|
if ($correoProfesional !== '') {
|
|
$nombreProfesional = trim((string) (($turno->profesional?->persona?->nombre ?? '') . ' ' . ($turno->profesional?->persona?->apellido ?? '')));
|
|
$nombreCliente = trim((string) ($turno->nombrecompleto ?? 'Cliente'));
|
|
$fechaTurno = $turno->inicio ? $turno->inicio->format('d/m/Y') : '-';
|
|
$horaTurno = $turno->inicio ? $turno->inicio->format('H:i') : '-';
|
|
$servicioTurno = trim((string) ($turno->servicio?->titulo ?? 'Servicio'));
|
|
$modalidadTurno = trim((string) ($turno->modalidad?->descripcion ?? 'Modalidad'));
|
|
|
|
$asunto = 'Cancelación de turno por cliente';
|
|
$cuerpo = "Hola " . ($nombreProfesional !== '' ? $nombreProfesional : 'profesional') . ",\n\n"
|
|
. "El cliente {$nombreCliente} canceló su turno.\n"
|
|
. "- Fecha: {$fechaTurno}\n"
|
|
. "- Hora: {$horaTurno}\n"
|
|
. "- Servicio: {$servicioTurno}\n"
|
|
. "- Modalidad: {$modalidadTurno}\n";
|
|
|
|
try {
|
|
Mail::raw($cuerpo, function ($message) use ($correoProfesional, $asunto): void {
|
|
$message->to($correoProfesional)->subject($asunto);
|
|
});
|
|
} catch (\Throwable $e) {
|
|
logger()->warning('No se pudo enviar correo al profesional por cancelación de cliente.', [
|
|
'turno_id' => (int) $turno->id,
|
|
'correo_profesional' => $correoProfesional,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
return back()->with('turno_success', 'El turno se canceló correctamente y se notificó al profesional.');
|
|
});
|
|
|
|
Route::get('/logout', function (Request $request) use ($registrarLogSeguridadDirecto) {
|
|
$sesionPersonalActiva = (bool) $request->session()->get('personal_auth', false);
|
|
$eraCliente = (bool) $request->session()->get('cliente_auth', false) && !$sesionPersonalActiva;
|
|
|
|
if ($eraCliente) {
|
|
$clienteId = (int) $request->session()->get('cliente_id', 0);
|
|
$personaId = Cliente::query()->where('id', $clienteId)->value('persona_id');
|
|
|
|
$registrarLogSeguridadDirecto(
|
|
$personaId ? (int) $personaId : null,
|
|
'CLIENTE',
|
|
(string) $request->ip(),
|
|
'Cerró sesión',
|
|
'El cliente ID ' . $clienteId . ' cerró sesión.'
|
|
);
|
|
} else {
|
|
$credencialId = (int) $request->session()->get('personal_credencial_id', 0);
|
|
$rolSesion = strtoupper((string) $request->session()->get('personal_rol', 'PROFESIONAL'));
|
|
|
|
if ($rolSesion === 'ADMIN') {
|
|
$admin = Administrador::query()->where('credencialprofesional_id', $credencialId)->first();
|
|
|
|
$registrarLogSeguridadDirecto(
|
|
$admin?->persona_id ? (int) $admin->persona_id : null,
|
|
'ADMIN',
|
|
(string) $request->ip(),
|
|
'Cerró sesión',
|
|
'La administradora ID ' . (int) ($admin?->id ?? 0) . ' cerró sesión.'
|
|
);
|
|
} else {
|
|
$profesional = Profesional::query()->where('credencialprofesional_id', $credencialId)->first();
|
|
|
|
$registrarLogSeguridadDirecto(
|
|
$profesional?->persona_id ? (int) $profesional->persona_id : null,
|
|
'PROFESIONAL',
|
|
(string) $request->ip(),
|
|
'Cerró sesión',
|
|
'El profesional ID ' . (int) ($profesional?->id ?? 0) . ' cerró sesión.'
|
|
);
|
|
}
|
|
}
|
|
|
|
$request->session()->invalidate();
|
|
$request->session()->regenerateToken();
|
|
|
|
$redirectPath = $eraCliente ? '/login/cliente' : '/login/personal';
|
|
|
|
return redirect($redirectPath)->with('logout_success', 'Sesión cerrada correctamente.');
|
|
});
|
|
|
|
Route::get('/administrador/ayuda', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
return view('administrador.ayuda');
|
|
});
|
|
|
|
Route::get('/administrador/backups', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$disk = \Illuminate\Support\Facades\Storage::disk('local');
|
|
$backupFolder = trim((string) config('backup.backup.name', 'Laravel'), '/');
|
|
|
|
$archivos = collect($disk->files($backupFolder))
|
|
->filter(fn (string $file): bool => str_ends_with(strtolower($file), '.zip'))
|
|
->sortByDesc(fn (string $file): int => $disk->lastModified($file))
|
|
->map(fn (string $file): array => [
|
|
'nombre' => basename($file),
|
|
'ruta' => $file,
|
|
'tamanio' => round($disk->size($file) / 1024, 2),
|
|
'fecha' => \Illuminate\Support\Carbon::createFromTimestampUTC($disk->lastModified($file))
|
|
->setTimezone((string) config('app.timezone', 'America/Argentina/Buenos_Aires'))
|
|
->format('d/m/Y H:i:s'),
|
|
])
|
|
->values();
|
|
|
|
return view('administrador.backups', compact('archivos'));
|
|
});
|
|
|
|
Route::get('/administrador/backups/descargar', function (Request $request) use ($validarSesionAdmin) {
|
|
if ($respuesta = $validarSesionAdmin($request)) {
|
|
return $respuesta;
|
|
}
|
|
|
|
$archivo = (string) $request->query('archivo', '');
|
|
|
|
if ($archivo === '') {
|
|
abort(400, 'Archivo no especificado.');
|
|
}
|
|
|
|
$disk = \Illuminate\Support\Facades\Storage::disk('local');
|
|
|
|
// Validar que el archivo está dentro de la carpeta de backups (evitar path traversal)
|
|
$backupFolder = trim((string) config('backup.backup.name', 'Laravel'), '/');
|
|
$archivoNormalizado = ltrim(str_replace('\\', '/', $archivo), '/');
|
|
|
|
if (!str_starts_with($archivoNormalizado, $backupFolder . '/') || !str_ends_with(strtolower($archivoNormalizado), '.zip')) {
|
|
abort(403, 'Acceso no permitido.');
|
|
}
|
|
|
|
if (!$disk->exists($archivoNormalizado)) {
|
|
abort(404, 'Archivo no encontrado.');
|
|
}
|
|
|
|
return response()->streamDownload(function () use ($disk, $archivoNormalizado): void {
|
|
echo $disk->get($archivoNormalizado);
|
|
}, basename($archivoNormalizado), [
|
|
'Content-Type' => 'application/zip',
|
|
]);
|
|
});
|