Agrego carpeta del source sql de la Base de Datos
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
-- ============================================================================
|
||||
-- fc_eliminar_dia_especial
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Elimina la planificación especial de una fecha y restaura los turnos a
|
||||
-- la plantilla regular vigente para esa fecha. Las reservas activas
|
||||
-- sobre turnos especiales que no calzan con la plantilla regular se
|
||||
-- rescatan como reservas huérfanas.
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Implementa la operación **"restablecer una fecha al horario regular"**
|
||||
-- descrita en Documentation/DominioHorarios.md §7.4 ("Un día especial
|
||||
-- también puede ser restablecido"). Es un cambio estructural §8/§11.
|
||||
--
|
||||
-- Como puede romper reservas existentes (los bloques especiales no
|
||||
-- compatibles con la plantilla regular vigente quedan sin destino),
|
||||
-- genera **reservas huérfanas** §7.8: el "match suave" del cuerpo elige
|
||||
-- qué turnos sobreviven (los compatibles con la plantilla regular) y
|
||||
-- cuáles caen junto con sus reservas. A partir de esas huérfanas, otro
|
||||
-- flujo se encarga de la notificación al cliente afectado (§7.9 y §6).
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_token UUID sesión del actor.
|
||||
-- p_fecha DATE fecha del día especial a eliminar. Debe ser hoy o
|
||||
-- posterior.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'modificar_horarios'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'No se pueden alterar horarios en fechas pasadas.'
|
||||
-- - 'No existe un día especial para la fecha X.'
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB con el estado actualizado de la fecha (mismo formato que un día
|
||||
-- en fc_obtener_horarios).
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- - INSERT en reservas_huerfanas para las reservas que no calzan con la
|
||||
-- plantilla regular vigente para esa fecha.
|
||||
-- - DELETE selectivo de turnos especiales que no calzan con la regular.
|
||||
-- - UPDATE de los turnos que sí calzan: es_especial = false.
|
||||
-- - INSERT (con ON CONFLICT DO NOTHING) de los turnos faltantes para
|
||||
-- materializar la plantilla regular en la fecha.
|
||||
-- - DELETE del registro en dias_especiales (CASCADE limpia
|
||||
-- horario_actividad_especial asociado).
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_eliminar_dia_especial(
|
||||
p_token UUID,
|
||||
p_fecha DATE
|
||||
)
|
||||
RETURNS JSONB
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
SET timezone = 'America/Argentina/Buenos_Aires'
|
||||
VOLATILE
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_dia_especial_id INT;
|
||||
v_dia_semana SMALLINT;
|
||||
v_bloque RECORD;
|
||||
v_hora_iter TIME;
|
||||
BEGIN
|
||||
PERFORM internal.validate_permission(p_token, 'modificar_horarios');
|
||||
|
||||
IF p_fecha < CURRENT_DATE THEN
|
||||
RAISE EXCEPTION 'No se pueden alterar horarios en fechas pasadas.';
|
||||
END IF;
|
||||
|
||||
SELECT id INTO v_dia_especial_id
|
||||
FROM dias_especiales
|
||||
WHERE fecha = p_fecha;
|
||||
|
||||
IF v_dia_especial_id IS NULL THEN
|
||||
RAISE EXCEPTION 'No existe un día especial para la fecha %.', p_fecha;
|
||||
END IF;
|
||||
|
||||
v_dia_semana := EXTRACT(ISODOW FROM p_fecha)::SMALLINT;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 1. Match suave contra la plantilla REGULAR vigente para esta fecha
|
||||
-- (no contra el JSON: la plantilla regular es la referencia)
|
||||
------------------------------------------------------------
|
||||
|
||||
-- 1.A Rescate de huérfanas: reservas activas sobre turnos que NO calzan
|
||||
-- con ningún rango de la plantilla regular para esta fecha.
|
||||
INSERT INTO reservas_huerfanas
|
||||
(cliente_id, actividad_nombre, fecha_original, hora_inicio_original)
|
||||
SELECT r.cliente_id, a.nombre, t.fecha, t.hora_inicio
|
||||
FROM reservas r
|
||||
JOIN turnos t ON r.turno_id = t.id
|
||||
JOIN actividades a ON t.actividad_id = a.id
|
||||
WHERE t.fecha = p_fecha
|
||||
AND r.cancelada = false
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM horario_actividad ha
|
||||
WHERE ha.dia_semana = v_dia_semana
|
||||
AND ha.actividad_id = t.actividad_id
|
||||
AND ha.valido_desde <= p_fecha
|
||||
AND (ha.valido_hasta IS NULL OR ha.valido_hasta >= p_fecha)
|
||||
AND t.hora_inicio >= ha.hora_inicio
|
||||
AND t.hora_fin <= ha.hora_fin
|
||||
);
|
||||
|
||||
-- 1.B Borrado selectivo de turnos que no calzan con la plantilla regular.
|
||||
DELETE FROM turnos t
|
||||
WHERE t.fecha = p_fecha
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM horario_actividad ha
|
||||
WHERE ha.dia_semana = v_dia_semana
|
||||
AND ha.actividad_id = t.actividad_id
|
||||
AND ha.valido_desde <= p_fecha
|
||||
AND (ha.valido_hasta IS NULL OR ha.valido_hasta >= p_fecha)
|
||||
AND t.hora_inicio >= ha.hora_inicio
|
||||
AND t.hora_fin <= ha.hora_fin
|
||||
);
|
||||
|
||||
-- 1.C Para los turnos que SÍ calzaron, marcar es_especial=false:
|
||||
-- ya no estamos bajo régimen especial, son turnos regulares ahora.
|
||||
UPDATE turnos
|
||||
SET es_especial = false
|
||||
WHERE fecha = p_fecha
|
||||
AND es_especial = true;
|
||||
|
||||
-- 1.D Parcheo: materializar los turnos regulares que falten.
|
||||
FOR v_bloque IN (
|
||||
SELECT ha.actividad_id AS act_id,
|
||||
ha.hora_inicio AS h_in,
|
||||
ha.hora_fin AS h_out,
|
||||
a.duracion, a.capacidad_por_defecto
|
||||
FROM horario_actividad ha
|
||||
JOIN actividades a ON ha.actividad_id = a.id
|
||||
WHERE ha.dia_semana = v_dia_semana
|
||||
AND ha.valido_desde <= p_fecha
|
||||
AND (ha.valido_hasta IS NULL OR ha.valido_hasta >= p_fecha)
|
||||
AND a.activo = true
|
||||
) LOOP
|
||||
v_hora_iter := v_bloque.h_in;
|
||||
WHILE v_hora_iter + (v_bloque.duracion || ' minutes')::INTERVAL <= v_bloque.h_out LOOP
|
||||
INSERT INTO turnos
|
||||
(actividad_id, fecha, hora_inicio, hora_fin,
|
||||
capacidad_maxima, es_especial, dia_semana)
|
||||
VALUES (
|
||||
v_bloque.act_id, p_fecha, v_hora_iter,
|
||||
v_hora_iter + (v_bloque.duracion || ' minutes')::INTERVAL,
|
||||
v_bloque.capacidad_por_defecto, false, v_dia_semana
|
||||
) ON CONFLICT DO NOTHING;
|
||||
v_hora_iter := v_hora_iter + (v_bloque.duracion || ' minutes')::INTERVAL;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 2. Borrar el día especial (CASCADE limpia horario_actividad_especial)
|
||||
------------------------------------------------------------
|
||||
DELETE FROM dias_especiales WHERE id = v_dia_especial_id;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 3. Devolver el estado actualizado de la fecha
|
||||
------------------------------------------------------------
|
||||
RETURN public.fc_obtener_horarios(p_token, p_fecha, 1::smallint);
|
||||
END;
|
||||
$function$;
|
||||
@@ -0,0 +1,89 @@
|
||||
-- ============================================================================
|
||||
-- fc_insertar_horario_con_actividades
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Facade unificada para insertar/actualizar horarios. El frontend llama a
|
||||
-- esta función sin importar si quiere insertar un horario regular o un
|
||||
-- horario especial de un día puntual; la función enruta al helper
|
||||
-- internal correspondiente y devuelve el estado final consolidado.
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Edición de la estructura del horario: modifica el **horario regular**
|
||||
-- (Documentation/DominioHorarios.md §7.1) o establece/edita un **día
|
||||
-- especial** (§7.4). Es un **cambio estructural** en términos de §8 y
|
||||
-- §11: potencialmente produce **reservas huérfanas** (§7.8) en cadena,
|
||||
-- y por eso queda sujeto a trazabilidad obligada (§11).
|
||||
--
|
||||
-- El parámetro `alcance` materializa la decisión del operador sobre cómo
|
||||
-- convive su edición con las planificaciones futuras ya programadas
|
||||
-- (ver §7.1: el dominio admite que existan planificaciones programadas
|
||||
-- para el futuro y la edición del horario debe poder pisarlas o
|
||||
-- respetarlas). Los valores 'indefinido', 'hasta_proximo' y 'hasta'
|
||||
-- son la implementación concreta de ese alcance.
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_token UUID sesión del actor.
|
||||
-- p_datos JSONB con el payload. Claves:
|
||||
-- fecha DATE (obligatorio) fecha de referencia (la del
|
||||
-- día especial, o un día representativo del
|
||||
-- dia_semana para el regular).
|
||||
-- es_especial BOOLEAN (opcional, default false) si true → especial,
|
||||
-- si false → regular.
|
||||
-- valido_desde DATE (sólo para regular, opcional). Default = fecha.
|
||||
-- alcance OBJECT (sólo para regular). { tipo, fecha? } con
|
||||
-- tipo en {'indefinido', 'hasta_proximo', 'hasta'}.
|
||||
-- motivo TEXT (sólo para especial).
|
||||
-- rangos ARRAY de {actividad_id, hora_inicio, hora_fin}.
|
||||
-- Para especial: si no se manda o está vacío,
|
||||
-- el día queda como 'cerrado'.
|
||||
-- El parseo y las reglas específicas viven en los helpers internal.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'modificar_horarios'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'El campo fecha es obligatorio en el nodo raíz del JSON.'
|
||||
-- - Más los errores propios de los helpers (ver internal.upsert_horario_*).
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB con el estado de la fecha tras la operación, en el mismo formato
|
||||
-- que un único día de fc_obtener_horarios.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- Delega a internal.upsert_horario_regular o internal.upsert_horario_especial.
|
||||
-- ============================================================================
|
||||
|
||||
-- FACADE para que el frontend llame a la misma función sin importar qué carajo quiere insertar
|
||||
CREATE OR REPLACE FUNCTION public.fc_insertar_horario_con_actividades(p_token uuid, p_datos jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path TO 'public'
|
||||
SET timezone = 'America/Argentina/Buenos_Aires'
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_fecha DATE;
|
||||
v_es_especial BOOLEAN;
|
||||
v_fecha_retorno DATE;
|
||||
BEGIN
|
||||
PERFORM internal.validate_permission(p_token, 'modificar_horarios');
|
||||
|
||||
IF (p_datos->>'fecha') IS NULL THEN
|
||||
RAISE EXCEPTION 'El campo fecha es obligatorio en el nodo raíz del JSON.';
|
||||
END IF;
|
||||
|
||||
v_fecha := (p_datos->>'fecha')::DATE;
|
||||
v_es_especial := COALESCE((p_datos->>'es_especial')::BOOLEAN, false);
|
||||
|
||||
-- Enrutamiento interno
|
||||
IF v_es_especial THEN
|
||||
v_fecha_retorno := internal.upsert_horario_especial(p_datos);
|
||||
ELSE
|
||||
v_fecha_retorno := internal.upsert_horario_regular(p_datos);
|
||||
END IF;
|
||||
|
||||
-- Retornamos el estado final usando la función unificada de lectura
|
||||
RETURN public.fc_obtener_horarios(p_token, v_fecha_retorno, 1::smallint);
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
@@ -0,0 +1,166 @@
|
||||
-- ============================================================================
|
||||
-- fc_obtener_horarios
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Devuelve la "agenda" de un rango de días (1 a 31): para cada día
|
||||
-- indica si es normal, día con horario diferente, o día cerrado, y trae
|
||||
-- sus rangos horarios con la actividad asociada.
|
||||
--
|
||||
-- Para días normales, también incluye la vigencia (valido_desde,
|
||||
-- valido_hasta) de la plantilla regular vigente, lo que permite al
|
||||
-- frontend mostrar contexto temporal de la planificación.
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Lectura del estado de la oferta del gimnasio para un rango de días
|
||||
-- (Documentation/DominioHorarios.md §7.1 horario regular + §7.4 día
|
||||
-- especial). Es la fuente con la que se construye la "grilla del
|
||||
-- gimnasio" que §13 le exige a la interfaz mostrar al operador. Los tres
|
||||
-- estados ('normal', 'horario_diferente', 'cerrado') son la
|
||||
-- materialización de los conceptos §7.1 y §7.4 en términos JSON. No
|
||||
-- produce efectos; lee el estado actual.
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_token UUID sesión del actor.
|
||||
-- p_fecha_inicio DATE primer día del rango.
|
||||
-- p_cantidad_dias SMALLINT (default 7) cantidad de días a incluir (1..31).
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'ver_horarios'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'p_cantidad_dias debe estar entre 1 y 31.'
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB objeto con clave = fecha (YYYY-MM-DD), valor = objeto día.
|
||||
-- Cada día tiene: dia_semana, tipo ('normal' | 'horario_diferente' |
|
||||
-- 'cerrado'), motivo (sólo en especiales), horarios (array de rangos
|
||||
-- con actividad), y valido_desde/valido_hasta (sólo en 'normal').
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- Ninguno (STABLE).
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_obtener_horarios(
|
||||
p_token UUID,
|
||||
p_fecha_inicio DATE,
|
||||
p_cantidad_dias SMALLINT DEFAULT 7
|
||||
)
|
||||
RETURNS JSONB
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
STABLE
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_fecha DATE;
|
||||
v_dia_offset INT;
|
||||
v_dia_semana SMALLINT;
|
||||
v_resultado JSONB := '{}'::JSONB;
|
||||
v_dia_json JSONB;
|
||||
v_especial RECORD;
|
||||
v_vigencia RECORD;
|
||||
BEGIN
|
||||
PERFORM internal.validate_permission(p_token, 'ver_horarios');
|
||||
|
||||
IF p_cantidad_dias < 1 OR p_cantidad_dias > 31 THEN
|
||||
RAISE EXCEPTION 'p_cantidad_dias debe estar entre 1 y 31.';
|
||||
END IF;
|
||||
|
||||
FOR v_dia_offset IN 0..(p_cantidad_dias - 1) LOOP
|
||||
v_fecha := p_fecha_inicio + v_dia_offset;
|
||||
v_dia_semana := EXTRACT(ISODOW FROM v_fecha)::SMALLINT;
|
||||
|
||||
SELECT id, tipo, motivo
|
||||
INTO v_especial
|
||||
FROM dias_especiales
|
||||
WHERE fecha = v_fecha;
|
||||
|
||||
IF FOUND AND v_especial.tipo = 'cerrado' THEN
|
||||
----------------------------------------------------
|
||||
-- CASO 1: Día cerrado
|
||||
----------------------------------------------------
|
||||
v_dia_json := jsonb_build_object(
|
||||
'dia_semana', v_dia_semana,
|
||||
'tipo', 'cerrado',
|
||||
'motivo', v_especial.motivo,
|
||||
'horarios', '[]'::JSONB
|
||||
);
|
||||
|
||||
ELSIF FOUND AND v_especial.tipo = 'horario_diferente' THEN
|
||||
----------------------------------------------------
|
||||
-- CASO 2: Día con horario especial (override puntual)
|
||||
----------------------------------------------------
|
||||
v_dia_json := jsonb_build_object(
|
||||
'dia_semana', v_dia_semana,
|
||||
'tipo', 'horario_diferente',
|
||||
'motivo', v_especial.motivo,
|
||||
'horarios', (
|
||||
SELECT COALESCE(jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'id', hae.id,
|
||||
'hora_inicio', to_char(hae.hora_inicio, 'HH24:MI'),
|
||||
'hora_fin', to_char(hae.hora_fin, 'HH24:MI'),
|
||||
'actividad', jsonb_build_object(
|
||||
'id', a.id,
|
||||
'nombre', a.nombre,
|
||||
'duracion', a.duracion,
|
||||
'capacidad', a.capacidad_por_defecto
|
||||
)
|
||||
) ORDER BY hae.hora_inicio ASC
|
||||
), '[]'::JSONB)
|
||||
FROM horario_actividad_especial hae
|
||||
JOIN actividades a ON hae.actividad_id = a.id
|
||||
WHERE hae.dia_especial_id = v_especial.id
|
||||
AND a.activo = true
|
||||
)
|
||||
);
|
||||
|
||||
ELSE
|
||||
----------------------------------------------------
|
||||
-- CASO 3: Día normal: plantilla regular vigente esa fecha
|
||||
-- Adicionalmente devolvemos la vigencia para que el
|
||||
-- frontend pueda renderizar contexto temporal.
|
||||
----------------------------------------------------
|
||||
SELECT valido_desde, valido_hasta
|
||||
INTO v_vigencia
|
||||
FROM horario_actividad
|
||||
WHERE dia_semana = v_dia_semana
|
||||
AND valido_desde <= v_fecha
|
||||
AND (valido_hasta IS NULL OR valido_hasta >= v_fecha)
|
||||
LIMIT 1; -- Todas las filas de la plantilla vigente comparten la misma vigencia.
|
||||
|
||||
v_dia_json := jsonb_build_object(
|
||||
'dia_semana', v_dia_semana,
|
||||
'tipo', 'normal',
|
||||
'valido_desde', v_vigencia.valido_desde::TEXT,
|
||||
'valido_hasta', v_vigencia.valido_hasta::TEXT,
|
||||
'horarios', (
|
||||
SELECT COALESCE(jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'id', ha.id,
|
||||
'hora_inicio', to_char(ha.hora_inicio, 'HH24:MI'),
|
||||
'hora_fin', to_char(ha.hora_fin, 'HH24:MI'),
|
||||
'actividad', jsonb_build_object(
|
||||
'id', a.id,
|
||||
'nombre', a.nombre,
|
||||
'duracion', a.duracion,
|
||||
'capacidad', a.capacidad_por_defecto
|
||||
)
|
||||
) ORDER BY ha.hora_inicio ASC
|
||||
), '[]'::JSONB)
|
||||
FROM horario_actividad ha
|
||||
JOIN actividades a ON ha.actividad_id = a.id
|
||||
WHERE ha.dia_semana = v_dia_semana
|
||||
AND a.activo = true
|
||||
AND ha.valido_desde <= v_fecha
|
||||
AND (ha.valido_hasta IS NULL OR ha.valido_hasta >= v_fecha)
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
|
||||
v_resultado := v_resultado || jsonb_build_object(v_fecha::TEXT, v_dia_json);
|
||||
END LOOP;
|
||||
|
||||
RETURN v_resultado;
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,82 @@
|
||||
-- ============================================================================
|
||||
-- fc_obtener_horarios_especiales
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Devuelve los días con horario especial (sea cerrado o con horario
|
||||
-- diferente) configurados. Opcionalmente filtra por una fecha puntual.
|
||||
-- Pensada para que el frontend liste y muestre el detalle de los días
|
||||
-- especiales programados.
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Consulta del catálogo de **días especiales** programados
|
||||
-- (Documentation/DominioHorarios.md §7.4). Sirve a la vista de
|
||||
-- administración de excepciones del horario regular. No describe el
|
||||
-- detalle del estado de cada fecha en su grilla diaria (eso es trabajo
|
||||
-- de fc_obtener_horarios): acá lo que se trae es el listado de las
|
||||
-- excepciones como tales.
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_token UUID sesión del actor.
|
||||
-- p_fecha DATE (opcional, default NULL) si se pasa, sólo retorna el
|
||||
-- día especial para esa fecha. NULL = todos.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'ver_horarios'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- Ninguno propio (sólo los del validador de permiso).
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB array (puede ser vacío) de días especiales. Cada ítem:
|
||||
-- { fecha, tipo, motivo, rangos_actividades: [{id, hora_inicio,
|
||||
-- hora_fin, actividad}] }. Ordenado por fecha descendente.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- Ninguno (STABLE).
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_obtener_horarios_especiales(
|
||||
p_token UUID,
|
||||
p_fecha DATE DEFAULT NULL
|
||||
)
|
||||
RETURNS JSONB
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
STABLE
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
PERFORM internal.validate_permission(p_token, 'ver_horarios');
|
||||
|
||||
RETURN (
|
||||
SELECT COALESCE(jsonb_agg(dia_completo ORDER BY dia_completo->>'fecha' DESC), '[]'::jsonb)
|
||||
FROM (
|
||||
SELECT
|
||||
de.fecha,
|
||||
de.tipo,
|
||||
de.motivo,
|
||||
-- Si el día es especial (no cerrado), adjuntamos sus actividades
|
||||
COALESCE(
|
||||
(SELECT jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'id', hae.id,
|
||||
'hora_inicio', to_char(hae.hora_inicio, 'HH24:MI'),
|
||||
'hora_fin', to_char(hae.hora_fin, 'HH24:MI'),
|
||||
'actividad', jsonb_build_object(
|
||||
'id', a.id,
|
||||
'nombre', a.nombre,
|
||||
'duracion', a.duracion
|
||||
)
|
||||
) ORDER BY hae.hora_inicio ASC
|
||||
)
|
||||
FROM public.horario_actividad_especial hae
|
||||
JOIN public.actividades a ON hae.actividad_id = a.id
|
||||
WHERE hae.dia_especial_id = de.id AND a.activo = true),
|
||||
'[]'::jsonb
|
||||
) as rangos_actividades
|
||||
FROM public.dias_especiales de
|
||||
WHERE (p_fecha IS NULL OR de.fecha = p_fecha)
|
||||
) AS dia_completo
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,110 @@
|
||||
-- ============================================================================
|
||||
-- fc_obtener_planificaciones_futuras
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Devuelve las plantillas horarias regulares programadas hacia el futuro
|
||||
-- para un día de la semana específico, dentro de un horizonte temporal
|
||||
-- (default 6 meses). Cada plantilla es un grupo de rangos horarios con
|
||||
-- vigencia común (valido_desde, valido_hasta).
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Sirve a la noción del dominio de **cambio de horario programado**
|
||||
-- (Documentation/DominioHorarios.md §7.1: "el operador puede dejar
|
||||
-- programado un cambio para que entre en vigencia desde una fecha
|
||||
-- futura"). La función habilita al frontend a mostrarle al operador el
|
||||
-- mapa de planificaciones futuras antes de editar un horario, para que
|
||||
-- pueda decidir el alcance de su edición (pisar las planificaciones
|
||||
-- futuras, o respetar la más próxima como tope de la suya).
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_token UUID sesión del actor.
|
||||
-- p_dia_semana SMALLINT día de la semana ISO (1=Lunes, 7=Domingo).
|
||||
-- p_desde DATE fecha de referencia; trae plantillas con
|
||||
-- valido_desde estrictamente posterior a esta.
|
||||
-- p_meses INT (default 6) horizonte en meses desde p_desde.
|
||||
-- Rango válido 1..24.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'ver_horarios'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'p_dia_semana debe estar entre 1 y 7 (recibido: X).'
|
||||
-- - 'p_meses debe estar entre 1 y 24 (recibido: X).'
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB array (puede ser vacío). Cada ítem: { valido_desde, valido_hasta,
|
||||
-- rangos: [{id, hora_inicio, hora_fin, actividad}] }. Ordenado por
|
||||
-- valido_desde ascendente.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- Ninguno (STABLE).
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_obtener_planificaciones_futuras(p_token uuid, p_dia_semana smallint, p_desde date, p_meses integer DEFAULT 6)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
STABLE SECURITY DEFINER
|
||||
SET search_path TO 'public'
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_resultado JSONB;
|
||||
v_hasta DATE;
|
||||
BEGIN
|
||||
PERFORM internal.validate_permission(p_token, 'ver_horarios');
|
||||
|
||||
IF p_dia_semana < 1 OR p_dia_semana > 7 THEN
|
||||
RAISE EXCEPTION 'p_dia_semana debe estar entre 1 y 7 (recibido: %).', p_dia_semana;
|
||||
END IF;
|
||||
|
||||
IF p_meses < 1 OR p_meses > 24 THEN
|
||||
RAISE EXCEPTION 'p_meses debe estar entre 1 y 24 (recibido: %).', p_meses;
|
||||
END IF;
|
||||
|
||||
v_hasta := p_desde + (p_meses || ' months')::INTERVAL;
|
||||
|
||||
-- Agrupamos por (valido_desde, valido_hasta): cada grupo es UNA plantilla
|
||||
-- futura con sus N rangos horarios. Solo traemos plantillas cuyo
|
||||
-- valido_desde es estrictamente posterior a p_desde y cae dentro del
|
||||
-- horizonte (p_desde + p_meses).
|
||||
WITH plantillas AS (
|
||||
SELECT
|
||||
ha.valido_desde,
|
||||
ha.valido_hasta,
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'id', ha.id,
|
||||
'hora_inicio', to_char(ha.hora_inicio, 'HH24:MI'),
|
||||
'hora_fin', to_char(ha.hora_fin, 'HH24:MI'),
|
||||
'actividad', jsonb_build_object(
|
||||
'id', a.id,
|
||||
'nombre', a.nombre,
|
||||
'duracion', a.duracion,
|
||||
'capacidad', a.capacidad_por_defecto
|
||||
)
|
||||
) ORDER BY ha.hora_inicio ASC, a.nombre ASC
|
||||
) AS rangos
|
||||
FROM horario_actividad ha
|
||||
JOIN actividades a ON ha.actividad_id = a.id
|
||||
WHERE ha.dia_semana = p_dia_semana
|
||||
AND ha.valido_desde > p_desde
|
||||
AND ha.valido_desde <= v_hasta
|
||||
AND a.activo = true
|
||||
GROUP BY ha.valido_desde, ha.valido_hasta
|
||||
)
|
||||
SELECT COALESCE(
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'valido_desde', valido_desde::TEXT,
|
||||
'valido_hasta', valido_hasta::TEXT,
|
||||
'rangos', rangos
|
||||
) ORDER BY valido_desde ASC
|
||||
),
|
||||
'[]'::JSONB
|
||||
)
|
||||
INTO v_resultado
|
||||
FROM plantillas;
|
||||
|
||||
RETURN v_resultado;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
@@ -0,0 +1,79 @@
|
||||
-- ============================================================================
|
||||
-- internal.limpiar_dias_especiales_antiguos
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Higiene operativa: elimina filas de public.dias_especiales cuya
|
||||
-- `fecha` esté fuera de la ventana de retención. Un día especial es
|
||||
-- una excepción puntual al régimen regular (cerrado u horario
|
||||
-- diferente); una vez pasado, no tiene valor operativo ni se archiva.
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Implementa la higiene de días especiales descrita en
|
||||
-- Documentation/PoliticaRetencion.md §7. La ventana es corta (1 mes
|
||||
-- por default) porque un día especial sólo importa mientras condiciona
|
||||
-- la generación de turnos; vencido el mes, ya no afecta nada vigente.
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- Ninguno. La ventana en meses se lee de `internal.app_config` con la
|
||||
-- clave `retencion.dias_especiales_meses` (default 1). El ajuste
|
||||
-- operativo se hace en la tabla, sin redeploy.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- No valida permiso propio. El schema `internal` no es alcanzable por
|
||||
-- roles cliente (ver hardening en `database/schema/00_schemas.sql`).
|
||||
-- Callers legítimos: pg_cron y fachadas `public.fc_*` SECURITY DEFINER
|
||||
-- que orquesten el pipeline mensual (US-R17).
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- Ninguno propio.
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB con {status, fecha_corte, meses_conservados,
|
||||
-- dias_eliminados, mensaje}.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- DELETE de dias_especiales con fecha < (CURRENT_DATE - ventana). Las
|
||||
-- filas de horario_actividad_especial asociadas caen por CASCADE
|
||||
-- (horario_actividad_especial_dia_fkey ON DELETE CASCADE). No registra
|
||||
-- evento.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION internal.limpiar_dias_especiales_antiguos()
|
||||
RETURNS JSONB
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
SET timezone = 'America/Argentina/Buenos_Aires'
|
||||
VOLATILE
|
||||
AS $$
|
||||
DECLARE
|
||||
v_meses INT;
|
||||
v_fecha_limite DATE;
|
||||
v_eliminados INT;
|
||||
BEGIN
|
||||
v_meses := internal.get_config_int('retencion.dias_especiales_meses', 1);
|
||||
v_fecha_limite := CURRENT_DATE - (v_meses * INTERVAL '1 month');
|
||||
|
||||
DELETE FROM dias_especiales
|
||||
WHERE fecha < v_fecha_limite;
|
||||
|
||||
GET DIAGNOSTICS v_eliminados = ROW_COUNT;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'status', 'success',
|
||||
'fecha_corte', v_fecha_limite,
|
||||
'meses_conservados', v_meses,
|
||||
'dias_eliminados', v_eliminados,
|
||||
'mensaje', format(
|
||||
'Se eliminaron %s días especiales anteriores a %s (rangos horarios asociados eliminados por CASCADE).',
|
||||
v_eliminados, v_fecha_limite
|
||||
)
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Defensa en profundidad: aunque el hardening del schema (00_schemas.sql)
|
||||
-- ya bloquee USAGE y EXECUTE por default, revocamos explícitamente acá
|
||||
-- por si en algún futuro alguien afloja las defensas a nivel schema.
|
||||
REVOKE ALL ON FUNCTION internal.limpiar_dias_especiales_antiguos()
|
||||
FROM PUBLIC, anon, authenticated;
|
||||
@@ -0,0 +1,144 @@
|
||||
-- ============================================================================
|
||||
-- internal.upsert_horario_especial
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Helper interno invocado por fc_insertar_horario_con_actividades cuando
|
||||
-- el payload corresponde a un día con horario especial (override puntual
|
||||
-- o cierre). Crea/actualiza el registro en dias_especiales y materializa
|
||||
-- o limpia los turnos en consecuencia.
|
||||
--
|
||||
-- Si rangos[] viene vacío o no viene → el día queda como 'cerrado'.
|
||||
-- Si trae rangos → el día queda como 'horario_diferente'.
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_datos JSONB con: fecha (obligatorio), motivo?, rangos[]?.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- No valida permiso propio (lo hizo la fachada antes de invocar).
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'No se pueden alterar horarios en fechas pasadas.'
|
||||
--
|
||||
-- RETORNA
|
||||
-- DATE = la fecha del día especial.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- - UPSERT en dias_especiales (ON CONFLICT (fecha) DO UPDATE).
|
||||
-- - DELETE de horario_actividad_especial previo (reemplazo).
|
||||
-- - Si tipo='horario_diferente': INSERT de los rangos nuevos en
|
||||
-- horario_actividad_especial.
|
||||
-- - Si la fecha tenía turnos materializados:
|
||||
-- - tipo='cerrado': rescata TODAS las reservas activas como huérfanas
|
||||
-- y borra todos los turnos del día.
|
||||
-- - tipo='horario_diferente': rescate selectivo + delete selectivo +
|
||||
-- parcheo JIT (mismo patrón que upsert_horario_regular).
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION internal.upsert_horario_especial(
|
||||
p_datos JSONB
|
||||
)
|
||||
RETURNS DATE
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = public
|
||||
SET timezone = 'America/Argentina/Buenos_Aires'
|
||||
VOLATILE
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_fecha DATE;
|
||||
v_dia_semana SMALLINT;
|
||||
v_tipo VARCHAR(25);
|
||||
v_motivo TEXT;
|
||||
v_dia_especial_id INT;
|
||||
v_turnos_generados BOOLEAN;
|
||||
v_bloque RECORD;
|
||||
v_hora_iter TIME;
|
||||
BEGIN
|
||||
v_fecha := (p_datos->>'fecha')::DATE;
|
||||
v_dia_semana := EXTRACT(ISODOW FROM v_fecha)::SMALLINT;
|
||||
|
||||
|
||||
IF v_fecha < (now() AT TIME ZONE 'America/Argentina/Buenos_Aires')::DATE THEN
|
||||
RAISE EXCEPTION 'No se pueden alterar horarios en fechas pasadas.';
|
||||
END IF;
|
||||
|
||||
v_motivo := p_datos->>'motivo';
|
||||
|
||||
IF (p_datos->'rangos') IS NULL OR jsonb_typeof(p_datos->'rangos') <> 'array' OR jsonb_array_length(p_datos->'rangos') = 0 THEN
|
||||
v_tipo := 'cerrado';
|
||||
ELSE
|
||||
v_tipo := 'horario_diferente';
|
||||
END IF;
|
||||
|
||||
-- 1. Upsert del día especial
|
||||
INSERT INTO dias_especiales (fecha, tipo, motivo)
|
||||
VALUES (v_fecha, v_tipo, v_motivo)
|
||||
ON CONFLICT (fecha) DO UPDATE
|
||||
SET tipo = EXCLUDED.tipo, motivo = EXCLUDED.motivo
|
||||
RETURNING id INTO v_dia_especial_id;
|
||||
|
||||
DELETE FROM horario_actividad_especial WHERE dia_especial_id = v_dia_especial_id;
|
||||
|
||||
IF v_tipo = 'horario_diferente' THEN
|
||||
INSERT INTO horario_actividad_especial (dia_especial_id, actividad_id, hora_inicio, hora_fin)
|
||||
SELECT DISTINCT v_dia_especial_id, (rango->>'actividad_id')::INT, (rango->>'hora_inicio')::TIME, (rango->>'hora_fin')::TIME
|
||||
FROM jsonb_array_elements(p_datos->'rangos') AS rango;
|
||||
END IF;
|
||||
|
||||
-- 2. Análisis del estado JIT
|
||||
SELECT EXISTS (SELECT 1 FROM turnos WHERE fecha = v_fecha) INTO v_turnos_generados;
|
||||
|
||||
IF v_turnos_generados THEN
|
||||
IF v_tipo = 'cerrado' THEN
|
||||
-- Rescate total
|
||||
INSERT INTO reservas_huerfanas (cliente_id, actividad_nombre, fecha_original, hora_inicio_original)
|
||||
SELECT r.cliente_id, a.nombre, t.fecha, t.hora_inicio
|
||||
FROM reservas r JOIN turnos t ON r.turno_id = t.id JOIN actividades a ON t.actividad_id = a.id
|
||||
WHERE t.fecha = v_fecha AND r.cancelada = false;
|
||||
|
||||
-- Borrado total
|
||||
DELETE FROM turnos WHERE fecha = v_fecha;
|
||||
ELSE
|
||||
-- Rescate parcial (Match Suave)
|
||||
INSERT INTO reservas_huerfanas (cliente_id, actividad_nombre, fecha_original, hora_inicio_original)
|
||||
SELECT r.cliente_id, a.nombre, t.fecha, t.hora_inicio
|
||||
FROM reservas r JOIN turnos t ON r.turno_id = t.id JOIN actividades a ON t.actividad_id = a.id
|
||||
WHERE t.fecha = v_fecha
|
||||
AND r.cancelada = false
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM jsonb_array_elements(p_datos->'rangos') AS rng
|
||||
WHERE (rng->>'actividad_id')::INT = t.actividad_id
|
||||
AND t.hora_inicio >= (rng->>'hora_inicio')::TIME
|
||||
AND t.hora_fin <= (rng->>'hora_fin')::TIME
|
||||
);
|
||||
|
||||
-- Borrado parcial
|
||||
DELETE FROM turnos t
|
||||
WHERE t.fecha = v_fecha
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM jsonb_array_elements(p_datos->'rangos') AS rng
|
||||
WHERE (rng->>'actividad_id')::INT = t.actividad_id
|
||||
AND t.hora_inicio >= (rng->>'hora_inicio')::TIME
|
||||
AND t.hora_fin <= (rng->>'hora_fin')::TIME
|
||||
);
|
||||
|
||||
-- Parcheo JIT
|
||||
FOR v_bloque IN (
|
||||
SELECT (r->>'actividad_id')::INT AS act_id, (r->>'hora_inicio')::TIME AS h_in, (r->>'hora_fin')::TIME AS h_out,
|
||||
a.duracion, a.capacidad_por_defecto
|
||||
FROM jsonb_array_elements(p_datos->'rangos') AS r
|
||||
JOIN actividades a ON a.id = (r->>'actividad_id')::INT
|
||||
) LOOP
|
||||
v_hora_iter := v_bloque.h_in;
|
||||
WHILE v_hora_iter + (v_bloque.duracion || ' minutes')::INTERVAL <= v_bloque.h_out LOOP
|
||||
INSERT INTO turnos (actividad_id, fecha, hora_inicio, hora_fin, capacidad_maxima, es_especial, dia_semana)
|
||||
VALUES (v_bloque.act_id, v_fecha, v_hora_iter, v_hora_iter + (v_bloque.duracion || ' minutes')::INTERVAL, v_bloque.capacidad_por_defecto, true, v_dia_semana)
|
||||
ON CONFLICT DO NOTHING;
|
||||
v_hora_iter := v_hora_iter + (v_bloque.duracion || ' minutes')::INTERVAL;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN v_fecha;
|
||||
END;
|
||||
$function$;
|
||||
@@ -0,0 +1,228 @@
|
||||
-- ============================================================================
|
||||
-- internal.upsert_horario_regular
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Helper interno invocado por fc_insertar_horario_con_actividades cuando
|
||||
-- el payload corresponde a un horario regular (semanal). Reemplaza /
|
||||
-- crea la plantilla horaria del día de la semana para una vigencia
|
||||
-- declarada, cierra plantillas previas que se solapan y sincroniza los
|
||||
-- turnos JIT ya materializados.
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_datos JSONB con (todos los campos parseados acá; ver fachada para
|
||||
-- descripción completa): fecha, valido_desde?, alcance.{tipo,
|
||||
-- fecha?}, rangos[].
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- No valida permiso propio (lo hizo la fachada antes de invocar).
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'valido_desde no puede ser una fecha pasada.'
|
||||
-- - 'alcance.tipo inválido: X. Debe ser ''indefinido'', ''hasta_proximo''
|
||||
-- o ''hasta''.'
|
||||
-- - 'alcance.fecha es obligatorio cuando alcance.tipo = ''hasta''.'
|
||||
-- - 'alcance.fecha (X) no puede ser anterior a valido_desde (Y).'
|
||||
-- - 'Conflicto: existe una planificación con valido_desde = X dentro
|
||||
-- del intervalo [Y, Z]. Modificá el alcance o eliminá la planificación
|
||||
-- previamente.'
|
||||
--
|
||||
-- RETORNA
|
||||
-- DATE = valido_desde efectivamente aplicado.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- - Según el alcance: borra plantillas futuras del mismo día_semana, o
|
||||
-- deja vigencia abierta, o cierra contra la próxima futura.
|
||||
-- - DELETE de plantilla con valido_desde exactamente igual (reemplazo).
|
||||
-- - UPDATE de plantillas anteriores que se solapan: valido_hasta =
|
||||
-- valido_desde - 1.
|
||||
-- - INSERT de los rangos nuevos en horario_actividad.
|
||||
-- - Para las fechas afectadas con turnos ya materializados: rescate de
|
||||
-- huérfanas + delete selectivo + parcheo JIT (mismo patrón que usan
|
||||
-- fc_eliminar_dia_especial e internal.cerrar_actividad).
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION internal.upsert_horario_regular(
|
||||
p_datos JSONB
|
||||
)
|
||||
RETURNS DATE
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = public
|
||||
SET timezone = 'America/Argentina/Buenos_Aires'
|
||||
VOLATILE
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_fecha DATE;
|
||||
v_dia_semana SMALLINT;
|
||||
v_valido_desde DATE;
|
||||
v_alcance_tipo TEXT;
|
||||
v_alcance_fecha DATE;
|
||||
v_valido_hasta_nuevo DATE;
|
||||
v_fecha_conflicto DATE;
|
||||
v_fechas_afectadas DATE[];
|
||||
v_f DATE;
|
||||
v_bloque RECORD;
|
||||
v_hora_iter TIME;
|
||||
BEGIN
|
||||
------------------------------------------------------------
|
||||
-- 0. Parseo y validación de entrada
|
||||
------------------------------------------------------------
|
||||
v_fecha := (p_datos->>'fecha')::DATE;
|
||||
v_dia_semana := EXTRACT(ISODOW FROM v_fecha)::SMALLINT;
|
||||
v_valido_desde := COALESCE((p_datos->>'valido_desde')::DATE, v_fecha);
|
||||
|
||||
IF v_valido_desde < (now() AT TIME ZONE 'America/Argentina/Buenos_Aires')::DATE THEN
|
||||
RAISE EXCEPTION 'valido_desde no puede ser una fecha pasada.';
|
||||
END IF;
|
||||
|
||||
v_alcance_tipo := COALESCE(p_datos->'alcance'->>'tipo', 'hasta_proximo');
|
||||
|
||||
IF v_alcance_tipo NOT IN ('indefinido', 'hasta_proximo', 'hasta') THEN
|
||||
RAISE EXCEPTION
|
||||
'alcance.tipo inválido: %. Debe ser ''indefinido'', ''hasta_proximo'' o ''hasta''.',
|
||||
v_alcance_tipo;
|
||||
END IF;
|
||||
|
||||
IF v_alcance_tipo = 'hasta' THEN
|
||||
v_alcance_fecha := (p_datos->'alcance'->>'fecha')::DATE;
|
||||
IF v_alcance_fecha IS NULL THEN
|
||||
RAISE EXCEPTION 'alcance.fecha es obligatorio cuando alcance.tipo = ''hasta''.';
|
||||
END IF;
|
||||
IF v_alcance_fecha < v_valido_desde THEN
|
||||
RAISE EXCEPTION
|
||||
'alcance.fecha (%) no puede ser anterior a valido_desde (%).',
|
||||
v_alcance_fecha, v_valido_desde;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 1. Resolver el valido_hasta del nuevo intervalo según el alcance
|
||||
------------------------------------------------------------
|
||||
IF v_alcance_tipo = 'indefinido' THEN
|
||||
-- Purga todas las plantillas futuras del mismo dia_semana
|
||||
DELETE FROM horario_actividad
|
||||
WHERE dia_semana = v_dia_semana
|
||||
AND valido_desde > v_valido_desde;
|
||||
v_valido_hasta_nuevo := NULL;
|
||||
|
||||
ELSIF v_alcance_tipo = 'hasta_proximo' THEN
|
||||
-- Cerrar antes del próximo futuro, o quedar abierto si no hay
|
||||
SELECT MIN(valido_desde) - 1
|
||||
INTO v_valido_hasta_nuevo
|
||||
FROM horario_actividad
|
||||
WHERE dia_semana = v_dia_semana
|
||||
AND valido_desde > v_valido_desde;
|
||||
|
||||
ELSE -- 'hasta'
|
||||
-- Validar que no haya futuros dentro del intervalo solicitado
|
||||
SELECT MIN(valido_desde)
|
||||
INTO v_fecha_conflicto
|
||||
FROM horario_actividad
|
||||
WHERE dia_semana = v_dia_semana
|
||||
AND valido_desde > v_valido_desde
|
||||
AND valido_desde <= v_alcance_fecha;
|
||||
|
||||
IF v_fecha_conflicto IS NOT NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'Conflicto: existe una planificación con valido_desde = % dentro del intervalo [%, %]. Modificá el alcance o eliminá la planificación previamente.',
|
||||
v_fecha_conflicto, v_valido_desde, v_alcance_fecha;
|
||||
END IF;
|
||||
v_valido_hasta_nuevo := v_alcance_fecha;
|
||||
END IF;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 2. Limpieza estructural en horario_actividad
|
||||
------------------------------------------------------------
|
||||
-- 2.A Colisión exacta: si ya existía una plantilla con el mismo valido_desde,
|
||||
-- la borramos para reemplazarla con los rangos nuevos.
|
||||
DELETE FROM horario_actividad
|
||||
WHERE dia_semana = v_dia_semana
|
||||
AND valido_desde = v_valido_desde;
|
||||
|
||||
-- 2.B Cerrar la(s) plantilla(s) anterior(es) que se solapen con el nuevo desde.
|
||||
UPDATE horario_actividad
|
||||
SET valido_hasta = v_valido_desde - 1
|
||||
WHERE dia_semana = v_dia_semana
|
||||
AND valido_desde < v_valido_desde
|
||||
AND (valido_hasta IS NULL OR valido_hasta >= v_valido_desde);
|
||||
|
||||
-- 2.C Insertar los rangos nuevos con la vigencia ya resuelta.
|
||||
INSERT INTO horario_actividad
|
||||
(dia_semana, actividad_id, hora_inicio, hora_fin, valido_desde, valido_hasta)
|
||||
SELECT DISTINCT
|
||||
v_dia_semana,
|
||||
(rango->>'actividad_id')::INT,
|
||||
(rango->>'hora_inicio')::TIME,
|
||||
(rango->>'hora_fin')::TIME,
|
||||
v_valido_desde,
|
||||
v_valido_hasta_nuevo
|
||||
FROM jsonb_array_elements(p_datos->'rangos') AS rango;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 3. Match suave sobre turnos JIT ya materializados
|
||||
-- Acotado al intervalo efectivo del nuevo (fix del bug viejo:
|
||||
-- no tocar turnos que pertenezcan a futuros que se respetan).
|
||||
------------------------------------------------------------
|
||||
SELECT array_agg(DISTINCT fecha)
|
||||
INTO v_fechas_afectadas
|
||||
FROM turnos
|
||||
WHERE dia_semana = v_dia_semana
|
||||
AND fecha >= v_valido_desde
|
||||
AND fecha <= COALESCE(v_valido_hasta_nuevo, 'infinity'::date);
|
||||
|
||||
IF v_fechas_afectadas IS NOT NULL THEN
|
||||
-- 3.A Rescate de huérfanas
|
||||
INSERT INTO reservas_huerfanas
|
||||
(cliente_id, actividad_nombre, fecha_original, hora_inicio_original)
|
||||
SELECT r.cliente_id, a.nombre, t.fecha, t.hora_inicio
|
||||
FROM reservas r
|
||||
JOIN turnos t ON r.turno_id = t.id
|
||||
JOIN actividades a ON t.actividad_id = a.id
|
||||
WHERE t.fecha = ANY(v_fechas_afectadas)
|
||||
AND r.cancelada = false
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM jsonb_array_elements(p_datos->'rangos') AS rng
|
||||
WHERE (rng->>'actividad_id')::INT = t.actividad_id
|
||||
AND t.hora_inicio >= (rng->>'hora_inicio')::TIME
|
||||
AND t.hora_fin <= (rng->>'hora_fin')::TIME
|
||||
);
|
||||
|
||||
-- 3.B Borrado selectivo de turnos que ya no calzan con los rangos nuevos
|
||||
DELETE FROM turnos t
|
||||
WHERE t.fecha = ANY(v_fechas_afectadas)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM jsonb_array_elements(p_datos->'rangos') AS rng
|
||||
WHERE (rng->>'actividad_id')::INT = t.actividad_id
|
||||
AND t.hora_inicio >= (rng->>'hora_inicio')::TIME
|
||||
AND t.hora_fin <= (rng->>'hora_fin')::TIME
|
||||
);
|
||||
|
||||
-- 3.C Parcheo JIT iterativo para esas fechas
|
||||
FOREACH v_f IN ARRAY v_fechas_afectadas LOOP
|
||||
FOR v_bloque IN (
|
||||
SELECT (r->>'actividad_id')::INT AS act_id,
|
||||
(r->>'hora_inicio')::TIME AS h_in,
|
||||
(r->>'hora_fin')::TIME AS h_out,
|
||||
a.duracion, a.capacidad_por_defecto
|
||||
FROM jsonb_array_elements(p_datos->'rangos') AS r
|
||||
JOIN actividades a ON a.id = (r->>'actividad_id')::INT
|
||||
) LOOP
|
||||
v_hora_iter := v_bloque.h_in;
|
||||
WHILE v_hora_iter + (v_bloque.duracion || ' minutes')::INTERVAL <= v_bloque.h_out LOOP
|
||||
INSERT INTO turnos
|
||||
(actividad_id, fecha, hora_inicio, hora_fin,
|
||||
capacidad_maxima, es_especial, dia_semana)
|
||||
VALUES (
|
||||
v_bloque.act_id, v_f, v_hora_iter,
|
||||
v_hora_iter + (v_bloque.duracion || ' minutes')::INTERVAL,
|
||||
v_bloque.capacidad_por_defecto, false, v_dia_semana
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
v_hora_iter := v_hora_iter + (v_bloque.duracion || ' minutes')::INTERVAL;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
RETURN v_valido_desde;
|
||||
END;
|
||||
$function$;
|
||||
Reference in New Issue
Block a user