-- 0004_perfil_maestros_avatares.sql -- 3 bloques: -- A) tabla prestamos.maestros (catalogo de tutores) + RLS -- B) profiles.tutor_id + profiles.foto_path -- C) bucket 'avatares' publico con RLS por carpeta = uid -- + update de crear_solicitud: fallback a nombre del tutor si el -- alumno deja p_maestro_responsable vacio. begin; -- ============================================================ -- A) Tabla de maestros (catalogo admin-managed) -- ============================================================ create table if not exists prestamos.maestros ( id serial primary key, nombre text not null unique, activo boolean not null default true, created_at timestamptz not null default now() ); alter table prestamos.maestros enable row level security; drop policy if exists maestros_read_all on prestamos.maestros; drop policy if exists maestros_admin_write on prestamos.maestros; create policy maestros_read_all on prestamos.maestros for select to authenticated using (true); create policy maestros_admin_write on prestamos.maestros for all to authenticated using (prestamos.is_admin()) with check (prestamos.is_admin()); grant select, insert, update, delete on prestamos.maestros to authenticated, service_role; grant usage, select on prestamos.maestros_id_seq to authenticated, service_role; insert into prestamos.maestros (nombre) values ('Sin especificar') on conflict (nombre) do nothing; -- ============================================================ -- B) profiles: tutor_id + foto_path -- ============================================================ alter table prestamos.profiles add column if not exists tutor_id int references prestamos.maestros(id) on delete set null, add column if not exists foto_path text; -- policy profiles_update_self (definida en 0001) usa: -- with check (id = auth.uid() and rol = (select rol from ...)) -- solo bloquea cambios al rol, permite editar matricula/semestre/tutor_id/foto_path. -- No requiere cambios. -- ============================================================ -- C) Bucket 'avatares' publico + policies por carpeta = uid -- Path pattern esperado: '/.' -- ============================================================ insert into storage.buckets (id, name, public) values ('avatares', 'avatares', true) on conflict (id) do update set public = true; drop policy if exists "avatares_read_all" on storage.objects; drop policy if exists "avatares_owner_write" on storage.objects; create policy "avatares_read_all" on storage.objects for select to anon, authenticated using (bucket_id = 'avatares'); create policy "avatares_owner_write" on storage.objects for all to authenticated using ( bucket_id = 'avatares' and (storage.foldername(name))[1] = auth.uid()::text ) with check ( bucket_id = 'avatares' and (storage.foldername(name))[1] = auth.uid()::text ); -- ============================================================ -- D) crear_solicitud: si p_maestro_responsable llega vacio, -- autocompletar con el nombre del tutor guardado en profiles. -- Si el usuario tampoco tiene tutor, sigue el raise -- 'maestro_responsable_requerido' original -> el checkout ya -- guarda contra ese caso (perfil incompleto). -- ============================================================ create or replace function prestamos.crear_solicitud( p_maestro_responsable text, p_notas text, p_items jsonb ) returns bigint language plpgsql security definer set search_path = prestamos, pg_temp as $$ declare v_alumno uuid := auth.uid(); v_solicitud_id bigint; v_item jsonb; v_material_id int; v_cantidad int; v_descripcion text; v_disponible int; v_estado_mat text; v_trackeado boolean; v_unidad_id bigint; v_asignadas int; v_maestro text; begin if v_alumno is null then raise exception 'no_autenticado'; end if; v_maestro := nullif(trim(coalesce(p_maestro_responsable, '')), ''); -- fallback al tutor del perfil si el alumno no escribio nada if v_maestro is null then select m.nombre into v_maestro from prestamos.profiles p join prestamos.maestros m on m.id = p.tutor_id where p.id = v_alumno; end if; if v_maestro is null then raise exception 'maestro_responsable_requerido'; end if; if p_items is null or jsonb_typeof(p_items) <> 'array' or jsonb_array_length(p_items) = 0 then raise exception 'items_requeridos'; end if; for v_material_id in select distinct (elem->>'material_id')::int from jsonb_array_elements(p_items) elem order by 1 loop perform 1 from prestamos.materiales where id = v_material_id for update; end loop; insert into prestamos.solicitudes (alumno_id, estado, maestro_responsable, notas) values (v_alumno, 'pendiente', v_maestro, nullif(trim(coalesce(p_notas, '')), '')) returning id into v_solicitud_id; for v_item in select * from jsonb_array_elements(p_items) loop v_material_id := (v_item->>'material_id')::int; v_cantidad := (v_item->>'cantidad')::int; v_descripcion := nullif(trim(coalesce(v_item->>'descripcion', '')), ''); if v_material_id is null or v_cantidad is null or v_cantidad <= 0 then raise exception 'item_invalido'; end if; select cantidad_disponible, estado, trackeado_por_unidad into v_disponible, v_estado_mat, v_trackeado from prestamos.materiales where id = v_material_id; if v_estado_mat is null then raise exception 'material_no_existe: %', v_material_id; end if; if v_estado_mat <> 'disponible' then raise exception 'material_no_disponible: %', v_material_id; end if; if v_disponible < v_cantidad then raise exception 'stock_insuficiente: %', v_material_id; end if; if v_trackeado then v_asignadas := 0; while v_asignadas < v_cantidad loop select id into v_unidad_id from prestamos.material_unidades where material_id = v_material_id and estado = 'disponible' order by id for update skip locked limit 1; if v_unidad_id is null then raise exception 'sin_unidad_disponible: %', v_material_id; end if; insert into prestamos.solicitud_items (solicitud_id, material_id, cantidad, descripcion, material_unidad_id) values (v_solicitud_id, v_material_id, 1, v_descripcion, v_unidad_id); update prestamos.material_unidades set estado = 'prestado' where id = v_unidad_id; v_asignadas := v_asignadas + 1; end loop; else insert into prestamos.solicitud_items (solicitud_id, material_id, cantidad, descripcion) values (v_solicitud_id, v_material_id, v_cantidad, v_descripcion); end if; end loop; return v_solicitud_id; end; $$; grant execute on function prestamos.crear_solicitud(text, text, jsonb) to authenticated; commit;