Betrifft UI-Texte (DE/EN/PL), Route (/intern/bereiche -> /intern/essenszeiten), Code-Kommentare, Changelog und README. Fachlich unverändert (weiterhin die Tabelle/Komponente "sections"/ SectionManager im Code), da "Essenszeiten" den Zeitbezug des Features klarer ausdrückt als der bisherige Sammelbegriff.
218 lines
7.7 KiB
PL/PgSQL
218 lines
7.7 KiB
PL/PgSQL
-- =============================================================
|
||
-- Speisekarte – Initiales Schema
|
||
-- Rollen: admin (Benutzerverwaltung, Branding) / staff (Kantine)
|
||
-- Öffentliche Speisekarte: lesbar ohne Login (anon)
|
||
-- =============================================================
|
||
|
||
-- ---------- Profile (1:1 zu auth.users) ----------
|
||
create table public.profiles (
|
||
id uuid primary key references auth.users (id) on delete cascade,
|
||
email text not null,
|
||
full_name text not null default '',
|
||
role text not null default 'staff' check (role in ('admin', 'staff')),
|
||
created_at timestamptz not null default now()
|
||
);
|
||
|
||
-- Rolle des eingeloggten Benutzers ermitteln.
|
||
-- security definer: umgeht RLS auf profiles und verhindert Policy-Rekursion.
|
||
create or replace function public.app_role()
|
||
returns text
|
||
language sql
|
||
stable
|
||
security definer
|
||
set search_path = public
|
||
as $$
|
||
select role from public.profiles where id = auth.uid()
|
||
$$;
|
||
|
||
-- Profil automatisch anlegen, wenn ein Auth-Benutzer erstellt wird.
|
||
create or replace function public.handle_new_user()
|
||
returns trigger
|
||
language plpgsql
|
||
security definer
|
||
set search_path = public
|
||
as $$
|
||
begin
|
||
insert into public.profiles (id, email, full_name, role)
|
||
values (
|
||
new.id,
|
||
new.email,
|
||
coalesce(new.raw_user_meta_data ->> 'full_name', ''),
|
||
coalesce(new.raw_user_meta_data ->> 'role', 'staff')
|
||
)
|
||
on conflict (id) do nothing;
|
||
return new;
|
||
end;
|
||
$$;
|
||
|
||
create trigger on_auth_user_created
|
||
after insert on auth.users
|
||
for each row execute function public.handle_new_user();
|
||
|
||
-- ---------- Essenszeiten der Speisekarte ----------
|
||
create table public.sections (
|
||
key text primary key,
|
||
label text not null,
|
||
time_range text not null,
|
||
sort int not null
|
||
);
|
||
|
||
insert into public.sections (key, label, time_range, sort) values
|
||
('brotzeit', 'Brotzeit', '09:00 - 10:00', 1),
|
||
('mittagessen', 'Mittagessen', '11:30 - 13:30', 2),
|
||
('mittagessen_veg', 'Mittagessen vegetarisch', '11:30 - 13:30', 3),
|
||
('suppe', 'Suppe', '11:30 - 13:30', 4);
|
||
|
||
-- ---------- Allergene & Zusatzstoffe ----------
|
||
-- Gelten für die komplette Speisekarte (keine Zuordnung pro Gericht)
|
||
-- und werden unterhalb der Karte angezeigt. Pflege durch Kantinen-Team.
|
||
create table public.allergens (
|
||
code text primary key,
|
||
name text not null,
|
||
sort int not null default 0
|
||
);
|
||
|
||
create table public.additives (
|
||
code text primary key,
|
||
name text not null,
|
||
sort int not null default 0
|
||
);
|
||
|
||
-- ---------- Gerichte ----------
|
||
create table public.dishes (
|
||
id uuid primary key default gen_random_uuid(),
|
||
name text not null,
|
||
price numeric(6, 2) not null default 0,
|
||
created_at timestamptz not null default now(),
|
||
updated_at timestamptz not null default now()
|
||
);
|
||
|
||
create or replace function public.touch_updated_at()
|
||
returns trigger
|
||
language plpgsql
|
||
as $$
|
||
begin
|
||
new.updated_at := now();
|
||
return new;
|
||
end;
|
||
$$;
|
||
|
||
create trigger dishes_touch_updated_at
|
||
before update on public.dishes
|
||
for each row execute function public.touch_updated_at();
|
||
|
||
-- ---------- Zuweisung Gericht -> Tag/Essenszeit ----------
|
||
create table public.menu_entries (
|
||
id uuid primary key default gen_random_uuid(),
|
||
menu_date date not null,
|
||
section_key text not null references public.sections (key),
|
||
dish_id uuid not null references public.dishes (id) on delete cascade,
|
||
position int not null default 0,
|
||
created_at timestamptz not null default now(),
|
||
unique (menu_date, section_key, dish_id)
|
||
);
|
||
|
||
create index menu_entries_date_idx on public.menu_entries (menu_date);
|
||
|
||
-- ---------- Einstellungen (Farben, Logo) – genau eine Zeile ----------
|
||
create table public.settings (
|
||
id int primary key check (id = 1),
|
||
colors jsonb not null default '{}'::jsonb,
|
||
logo_path text,
|
||
updated_at timestamptz not null default now()
|
||
);
|
||
|
||
insert into public.settings (id, colors) values (
|
||
1,
|
||
'{
|
||
"primary": "#003388",
|
||
"primaryDark": "#0d315c",
|
||
"accent": "#ff6900",
|
||
"headerText": "#ffffff",
|
||
"rowBg": "#e3e4f1",
|
||
"rowBgAlt": "#f4f4fa",
|
||
"addBtn": "#6da54c",
|
||
"deleteBtn": "#e2371d"
|
||
}'::jsonb
|
||
);
|
||
|
||
create trigger settings_touch_updated_at
|
||
before update on public.settings
|
||
for each row execute function public.touch_updated_at();
|
||
|
||
-- =============================================================
|
||
-- Row Level Security
|
||
-- =============================================================
|
||
alter table public.profiles enable row level security;
|
||
alter table public.sections enable row level security;
|
||
alter table public.allergens enable row level security;
|
||
alter table public.additives enable row level security;
|
||
alter table public.dishes enable row level security;
|
||
alter table public.menu_entries enable row level security;
|
||
alter table public.settings enable row level security;
|
||
|
||
-- Profiles: eigenes Profil lesen, Admin liest alle.
|
||
-- Anlegen/Ändern/Löschen von Benutzern läuft serverseitig über den
|
||
-- Service-Role-Key (umgeht RLS) und wird dort auf Admins beschränkt.
|
||
create policy "profiles_select_own_or_admin"
|
||
on public.profiles for select
|
||
to authenticated
|
||
using (id = (select auth.uid()) or public.app_role() = 'admin');
|
||
|
||
-- Öffentlich lesbare Tabellen (Speisekarte ohne Login)
|
||
create policy "sections_public_read" on public.sections for select to anon, authenticated using (true);
|
||
create policy "allergens_public_read" on public.allergens for select to anon, authenticated using (true);
|
||
create policy "additives_public_read" on public.additives for select to anon, authenticated using (true);
|
||
create policy "dishes_public_read" on public.dishes for select to anon, authenticated using (true);
|
||
create policy "menu_entries_public_read" on public.menu_entries for select to anon, authenticated using (true);
|
||
create policy "settings_public_read" on public.settings for select to anon, authenticated using (true);
|
||
|
||
-- Kantinenmitarbeiter (staff) und Admin: Gerichte & Zuweisungen pflegen
|
||
create policy "dishes_staff_write" on public.dishes
|
||
for all to authenticated
|
||
using (public.app_role() in ('staff', 'admin'))
|
||
with check (public.app_role() in ('staff', 'admin'));
|
||
|
||
create policy "menu_entries_staff_write" on public.menu_entries
|
||
for all to authenticated
|
||
using (public.app_role() in ('staff', 'admin'))
|
||
with check (public.app_role() in ('staff', 'admin'));
|
||
|
||
create policy "allergens_staff_write" on public.allergens
|
||
for all to authenticated
|
||
using (public.app_role() in ('staff', 'admin'))
|
||
with check (public.app_role() in ('staff', 'admin'));
|
||
|
||
create policy "additives_staff_write" on public.additives
|
||
for all to authenticated
|
||
using (public.app_role() in ('staff', 'admin'))
|
||
with check (public.app_role() in ('staff', 'admin'));
|
||
|
||
-- Einstellungen: nur Admin ändert
|
||
create policy "settings_admin_update" on public.settings
|
||
for update to authenticated
|
||
using (public.app_role() = 'admin')
|
||
with check (public.app_role() = 'admin');
|
||
|
||
-- =============================================================
|
||
-- Storage: Bucket "branding" für das Logo (öffentlich lesbar)
|
||
-- =============================================================
|
||
insert into storage.buckets (id, name, public)
|
||
values ('branding', 'branding', true);
|
||
|
||
create policy "branding_public_read" on storage.objects
|
||
for select to anon, authenticated
|
||
using (bucket_id = 'branding');
|
||
|
||
create policy "branding_admin_insert" on storage.objects
|
||
for insert to authenticated
|
||
with check (bucket_id = 'branding' and public.app_role() = 'admin');
|
||
|
||
create policy "branding_admin_update" on storage.objects
|
||
for update to authenticated
|
||
using (bucket_id = 'branding' and public.app_role() = 'admin');
|
||
|
||
create policy "branding_admin_delete" on storage.objects
|
||
for delete to authenticated
|
||
using (bucket_id = 'branding' and public.app_role() = 'admin');
|