diff --git a/supabase/migrations/0004_perfil_maestros_avatares.sql b/supabase/migrations/0004_perfil_maestros_avatares.sql new file mode 100644 index 0000000..f4c1bf8 --- /dev/null +++ b/supabase/migrations/0004_perfil_maestros_avatares.sql @@ -0,0 +1,203 @@ +-- 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;