chore: perauset customer project
This commit is contained in:
50
sql/0001-init.sql
Normal file
50
sql/0001-init.sql
Normal file
@@ -0,0 +1,50 @@
|
||||
CREATE SCHEMA IF NOT EXISTS app;
|
||||
|
||||
-- ============================================================
|
||||
-- Enum types
|
||||
-- ============================================================
|
||||
|
||||
CREATE TYPE app.user_status AS ENUM ('active', 'inactive', 'archived');
|
||||
CREATE TYPE app.stay_request_type AS ENUM ('guest', 'volunteer', 'staff', 'facilitator', 'day_visit');
|
||||
CREATE TYPE app.stay_request_status AS ENUM ('submitted', 'under_review', 'approved', 'rejected', 'cancelled');
|
||||
CREATE TYPE app.stay_type AS ENUM ('guest', 'volunteer', 'staff', 'facilitator', 'day_visitor');
|
||||
CREATE TYPE app.stay_status AS ENUM ('pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled');
|
||||
CREATE TYPE app.room_type AS ENUM ('private', 'shared', 'dorm', 'facilitator', 'staff');
|
||||
CREATE TYPE app.room_block_type AS ENUM ('maintenance', 'retreat_reserved', 'admin_hold', 'other');
|
||||
CREATE TYPE app.room_request_status AS ENUM ('submitted', 'reviewed', 'satisfied', 'unsatisfied', 'cancelled');
|
||||
CREATE TYPE app.room_allocation_status AS ENUM ('planned', 'active', 'completed', 'cancelled');
|
||||
CREATE TYPE app.boat_trip_status AS ENUM ('planned', 'open', 'full', 'closed', 'cancelled', 'completed');
|
||||
CREATE TYPE app.boat_reservation_status AS ENUM ('submitted', 'under_review', 'confirmed', 'waitlisted', 'declined', 'cancelled');
|
||||
CREATE TYPE app.boat_manifest_status AS ENUM ('confirmed', 'boarded', 'no_show', 'cancelled');
|
||||
CREATE TYPE app.task_category AS ENUM ('operations', 'maintenance', 'kitchen', 'retreat', 'transport', 'housekeeping');
|
||||
CREATE TYPE app.task_status AS ENUM ('planned', 'open', 'assigned', 'in_progress', 'blocked', 'completed', 'cancelled');
|
||||
CREATE TYPE app.task_assignment_status AS ENUM ('proposed', 'accepted', 'declined', 'reassigned', 'completed');
|
||||
|
||||
-- ============================================================
|
||||
-- Users
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE app."user" (
|
||||
user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
phone TEXT,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
display_name TEXT,
|
||||
roles TEXT[] NOT NULL DEFAULT '{}',
|
||||
status app.user_status NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE app.user_profile (
|
||||
profile_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL UNIQUE REFERENCES app."user"(user_id) ON DELETE CASCADE,
|
||||
date_of_birth DATE,
|
||||
nationality TEXT,
|
||||
notes TEXT,
|
||||
emergency_contact_name TEXT,
|
||||
emergency_contact_phone TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
88
sql/0002-audit-log.sql
Normal file
88
sql/0002-audit-log.sql
Normal file
@@ -0,0 +1,88 @@
|
||||
-- Audit log table
|
||||
CREATE TABLE app.audit_log (
|
||||
audit_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
table_name TEXT NOT NULL,
|
||||
record_id TEXT NOT NULL,
|
||||
action TEXT NOT NULL, -- 'INSERT', 'UPDATE', 'DELETE'
|
||||
user_id UUID, -- from session variable, nullable for system actions
|
||||
changed_fields JSONB, -- UPDATE: {field: {old, new}}; INSERT: full row; DELETE: full row
|
||||
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_log_table_record ON app.audit_log(table_name, record_id);
|
||||
CREATE INDEX idx_audit_log_user ON app.audit_log(user_id);
|
||||
CREATE INDEX idx_audit_log_occurred ON app.audit_log(occurred_at);
|
||||
|
||||
-- Generic trigger function
|
||||
CREATE OR REPLACE FUNCTION app.audit_trigger_func() RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
_user_id UUID;
|
||||
_record_id TEXT;
|
||||
_changed JSONB;
|
||||
_old JSONB;
|
||||
_new JSONB;
|
||||
_key TEXT;
|
||||
BEGIN
|
||||
-- Get current user from session variable (set per-request by app)
|
||||
BEGIN
|
||||
_user_id := current_setting('app.current_user_id', true)::UUID;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
_user_id := NULL;
|
||||
END;
|
||||
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
_record_id := (row_to_json(OLD) ->> TG_ARGV[0])::TEXT;
|
||||
INSERT INTO app.audit_log (table_name, record_id, action, user_id, changed_fields)
|
||||
VALUES (TG_TABLE_NAME, _record_id, 'DELETE', _user_id, row_to_json(OLD)::JSONB);
|
||||
RETURN OLD;
|
||||
|
||||
ELSIF TG_OP = 'INSERT' THEN
|
||||
_record_id := (row_to_json(NEW) ->> TG_ARGV[0])::TEXT;
|
||||
INSERT INTO app.audit_log (table_name, record_id, action, user_id, changed_fields)
|
||||
VALUES (TG_TABLE_NAME, _record_id, 'INSERT', _user_id, row_to_json(NEW)::JSONB);
|
||||
RETURN NEW;
|
||||
|
||||
ELSIF TG_OP = 'UPDATE' THEN
|
||||
_record_id := (row_to_json(NEW) ->> TG_ARGV[0])::TEXT;
|
||||
_old := row_to_json(OLD)::JSONB;
|
||||
_new := row_to_json(NEW)::JSONB;
|
||||
_changed := '{}'::JSONB;
|
||||
|
||||
FOR _key IN SELECT jsonb_object_keys(_new)
|
||||
LOOP
|
||||
IF _key IN ('updated_at', 'created_at') THEN CONTINUE; END IF;
|
||||
IF (_old ->> _key) IS DISTINCT FROM (_new ->> _key) THEN
|
||||
_changed := _changed || jsonb_build_object(
|
||||
_key, jsonb_build_object('old', _old -> _key, 'new', _new -> _key)
|
||||
);
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- Skip if nothing actually changed
|
||||
IF _changed = '{}'::JSONB THEN RETURN NEW; END IF;
|
||||
|
||||
INSERT INTO app.audit_log (table_name, record_id, action, user_id, changed_fields)
|
||||
VALUES (TG_TABLE_NAME, _record_id, 'UPDATE', _user_id, _changed);
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Helper to attach audit trigger to a table
|
||||
CREATE OR REPLACE FUNCTION app.enable_audit(target_table TEXT, pk_column TEXT DEFAULT 'id') RETURNS VOID AS $$
|
||||
DECLARE
|
||||
trigger_name TEXT;
|
||||
BEGIN
|
||||
trigger_name := 'audit_' || replace(target_table, '"', '');
|
||||
EXECUTE format(
|
||||
'CREATE TRIGGER %I AFTER INSERT OR UPDATE OR DELETE ON app.%s FOR EACH ROW EXECUTE FUNCTION app.audit_trigger_func(%L)',
|
||||
trigger_name, target_table, pk_column
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Enable audit on existing tables
|
||||
SELECT app.enable_audit('"user"', 'user_id');
|
||||
SELECT app.enable_audit('user_profile', 'profile_id');
|
||||
3
sql/0003-seed-data.sql
Normal file
3
sql/0003-seed-data.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Admin user
|
||||
INSERT INTO app."user" (email, first_name, last_name, display_name, roles, status)
|
||||
VALUES ('admin@perauset.org', 'Admin', 'User', 'Admin', '{admin}', 'active');
|
||||
44
sql/0004-stays.sql
Normal file
44
sql/0004-stays.sql
Normal file
@@ -0,0 +1,44 @@
|
||||
-- Stay requests (user intent)
|
||||
CREATE TABLE app.stay_request (
|
||||
request_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
request_type app.stay_request_type NOT NULL DEFAULT 'guest',
|
||||
requested_start_at DATE NOT NULL,
|
||||
requested_end_at DATE NOT NULL,
|
||||
retreat_id UUID,
|
||||
notes TEXT,
|
||||
status app.stay_request_status NOT NULL DEFAULT 'submitted',
|
||||
reviewed_by_user_id UUID REFERENCES app."user"(user_id),
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
review_notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_stay_request_user ON app.stay_request(user_id);
|
||||
CREATE INDEX idx_stay_request_status ON app.stay_request(status);
|
||||
|
||||
-- Stays (confirmed island presence)
|
||||
CREATE TABLE app.stay (
|
||||
stay_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
stay_type app.stay_type NOT NULL DEFAULT 'guest',
|
||||
source_request_id UUID REFERENCES app.stay_request(request_id),
|
||||
retreat_id UUID,
|
||||
start_at DATE NOT NULL,
|
||||
end_at DATE NOT NULL,
|
||||
status app.stay_status NOT NULL DEFAULT 'pending',
|
||||
checked_in_at TIMESTAMPTZ,
|
||||
checked_out_at TIMESTAMPTZ,
|
||||
created_by_user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
updated_by_user_id UUID REFERENCES app."user"(user_id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_stay_user ON app.stay(user_id);
|
||||
CREATE INDEX idx_stay_status ON app.stay(status);
|
||||
CREATE INDEX idx_stay_dates ON app.stay(start_at, end_at);
|
||||
|
||||
SELECT app.enable_audit('stay_request', 'request_id');
|
||||
SELECT app.enable_audit('stay', 'stay_id');
|
||||
65
sql/0005-rooms.sql
Normal file
65
sql/0005-rooms.sql
Normal file
@@ -0,0 +1,65 @@
|
||||
-- Rooms
|
||||
CREATE TABLE app.room (
|
||||
room_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
room_type app.room_type NOT NULL DEFAULT 'private',
|
||||
capacity INT NOT NULL DEFAULT 1,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Room blocks (maintenance, retreat holds, admin reservations)
|
||||
CREATE TABLE app.room_block (
|
||||
block_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
room_id UUID NOT NULL REFERENCES app.room(room_id),
|
||||
block_type app.room_block_type NOT NULL DEFAULT 'maintenance',
|
||||
retreat_id UUID,
|
||||
starts_at DATE NOT NULL,
|
||||
ends_at DATE NOT NULL,
|
||||
reason TEXT,
|
||||
created_by_user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_room_block_room ON app.room_block(room_id);
|
||||
CREATE INDEX idx_room_block_dates ON app.room_block(starts_at, ends_at);
|
||||
|
||||
-- Room requests (preferences)
|
||||
CREATE TABLE app.room_request (
|
||||
request_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
stay_id UUID NOT NULL REFERENCES app.stay(stay_id),
|
||||
requested_room_type app.room_type,
|
||||
preference_notes TEXT,
|
||||
status app.room_request_status NOT NULL DEFAULT 'submitted',
|
||||
reviewed_by_user_id UUID REFERENCES app."user"(user_id),
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Room allocations (actual assignment)
|
||||
CREATE TABLE app.room_allocation (
|
||||
allocation_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
stay_id UUID NOT NULL REFERENCES app.stay(stay_id),
|
||||
room_id UUID NOT NULL REFERENCES app.room(room_id),
|
||||
starts_at DATE NOT NULL,
|
||||
ends_at DATE NOT NULL,
|
||||
status app.room_allocation_status NOT NULL DEFAULT 'planned',
|
||||
allocation_reason TEXT,
|
||||
created_by_user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
updated_by_user_id UUID REFERENCES app."user"(user_id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_room_allocation_room ON app.room_allocation(room_id);
|
||||
CREATE INDEX idx_room_allocation_stay ON app.room_allocation(stay_id);
|
||||
CREATE INDEX idx_room_allocation_dates ON app.room_allocation(starts_at, ends_at);
|
||||
|
||||
SELECT app.enable_audit('room', 'room_id');
|
||||
SELECT app.enable_audit('room_block', 'block_id');
|
||||
SELECT app.enable_audit('room_request', 'request_id');
|
||||
SELECT app.enable_audit('room_allocation', 'allocation_id');
|
||||
84
sql/0006-boats.sql
Normal file
84
sql/0006-boats.sql
Normal file
@@ -0,0 +1,84 @@
|
||||
-- Boat routes
|
||||
CREATE TABLE app.boat_route (
|
||||
route_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
origin TEXT NOT NULL,
|
||||
destination TEXT NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Boats
|
||||
CREATE TABLE app.boat (
|
||||
boat_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
passenger_capacity INT NOT NULL,
|
||||
cargo_capacity_kg NUMERIC,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Boat trips
|
||||
CREATE TABLE app.boat_trip (
|
||||
trip_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
boat_id UUID REFERENCES app.boat(boat_id),
|
||||
route_id UUID NOT NULL REFERENCES app.boat_route(route_id),
|
||||
scheduled_departure_at TIMESTAMPTZ NOT NULL,
|
||||
scheduled_arrival_at TIMESTAMPTZ,
|
||||
status app.boat_trip_status NOT NULL DEFAULT 'planned',
|
||||
notes TEXT,
|
||||
created_by_user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
updated_by_user_id UUID REFERENCES app."user"(user_id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_boat_trip_status ON app.boat_trip(status);
|
||||
CREATE INDEX idx_boat_trip_departure ON app.boat_trip(scheduled_departure_at);
|
||||
|
||||
-- Boat reservation requests (user intent)
|
||||
CREATE TABLE app.boat_reservation_request (
|
||||
request_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
trip_id UUID NOT NULL REFERENCES app.boat_trip(trip_id),
|
||||
user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
stay_id UUID REFERENCES app.stay(stay_id),
|
||||
retreat_id UUID,
|
||||
requested_seats INT NOT NULL DEFAULT 1,
|
||||
cargo_notes TEXT,
|
||||
special_requirements TEXT,
|
||||
priority_reason TEXT,
|
||||
status app.boat_reservation_status NOT NULL DEFAULT 'submitted',
|
||||
reviewed_by_user_id UUID REFERENCES app."user"(user_id),
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
review_notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_boat_reservation_trip ON app.boat_reservation_request(trip_id);
|
||||
CREATE INDEX idx_boat_reservation_user ON app.boat_reservation_request(user_id);
|
||||
CREATE INDEX idx_boat_reservation_status ON app.boat_reservation_request(status);
|
||||
|
||||
-- Boat manifest entries (confirmed passengers)
|
||||
CREATE TABLE app.boat_manifest_entry (
|
||||
entry_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
trip_id UUID NOT NULL REFERENCES app.boat_trip(trip_id),
|
||||
reservation_request_id UUID REFERENCES app.boat_reservation_request(request_id),
|
||||
user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
stay_id UUID REFERENCES app.stay(stay_id),
|
||||
status app.boat_manifest_status NOT NULL DEFAULT 'confirmed',
|
||||
confirmed_by_user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_boat_manifest_trip ON app.boat_manifest_entry(trip_id);
|
||||
|
||||
SELECT app.enable_audit('boat_route', 'route_id');
|
||||
SELECT app.enable_audit('boat', 'boat_id');
|
||||
SELECT app.enable_audit('boat_trip', 'trip_id');
|
||||
SELECT app.enable_audit('boat_reservation_request', 'request_id');
|
||||
SELECT app.enable_audit('boat_manifest_entry', 'entry_id');
|
||||
56
sql/0007-tasks.sql
Normal file
56
sql/0007-tasks.sql
Normal file
@@ -0,0 +1,56 @@
|
||||
-- Task templates
|
||||
CREATE TABLE app.task_template (
|
||||
template_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category app.task_category NOT NULL DEFAULT 'operations',
|
||||
is_claimable BOOLEAN NOT NULL DEFAULT false,
|
||||
default_location TEXT,
|
||||
estimated_minutes INT,
|
||||
recurrence_cron TEXT, -- cron expression, e.g. '0 8 * * 1,3,5'
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_by_user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Task instances
|
||||
CREATE TABLE app.task_instance (
|
||||
task_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
template_id UUID REFERENCES app.task_template(template_id),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category app.task_category NOT NULL DEFAULT 'operations',
|
||||
location TEXT,
|
||||
linked_retreat_id UUID,
|
||||
linked_stay_id UUID REFERENCES app.stay(stay_id),
|
||||
scheduled_start_at TIMESTAMPTZ,
|
||||
scheduled_end_at TIMESTAMPTZ,
|
||||
status app.task_status NOT NULL DEFAULT 'planned',
|
||||
created_by_user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_task_instance_status ON app.task_instance(status);
|
||||
CREATE INDEX idx_task_instance_category ON app.task_instance(category);
|
||||
CREATE INDEX idx_task_instance_template ON app.task_instance(template_id);
|
||||
|
||||
-- Task assignments
|
||||
CREATE TABLE app.task_assignment (
|
||||
assignment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
task_instance_id UUID NOT NULL REFERENCES app.task_instance(task_id),
|
||||
assigned_user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
assigned_by_user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
status app.task_assignment_status NOT NULL DEFAULT 'proposed',
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_task_assignment_task ON app.task_assignment(task_instance_id);
|
||||
CREATE INDEX idx_task_assignment_user ON app.task_assignment(assigned_user_id);
|
||||
|
||||
SELECT app.enable_audit('task_template', 'template_id');
|
||||
SELECT app.enable_audit('task_instance', 'task_id');
|
||||
SELECT app.enable_audit('task_assignment', 'assignment_id');
|
||||
13
sql/0008-notifications.sql
Normal file
13
sql/0008-notifications.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE app.notification (
|
||||
notification_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
type TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
body TEXT,
|
||||
entity_type TEXT,
|
||||
entity_id UUID,
|
||||
read_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_notification_user ON app.notification(user_id, read_at, created_at DESC);
|
||||
80
sql/0009-retreats.sql
Normal file
80
sql/0009-retreats.sql
Normal file
@@ -0,0 +1,80 @@
|
||||
-- ============================================================
|
||||
-- Retreat enum types
|
||||
-- ============================================================
|
||||
|
||||
CREATE TYPE app.retreat_status AS ENUM ('draft', 'published', 'active', 'completed', 'cancelled');
|
||||
CREATE TYPE app.retreat_person_role AS ENUM ('participant', 'facilitator', 'assistant_facilitator', 'organizer', 'support_staff');
|
||||
CREATE TYPE app.retreat_attendance_status AS ENUM ('invited', 'registered', 'confirmed', 'checked_in', 'cancelled');
|
||||
|
||||
-- ============================================================
|
||||
-- Retreats
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE app.retreat (
|
||||
retreat_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
start_at DATE NOT NULL,
|
||||
end_at DATE NOT NULL,
|
||||
status app.retreat_status NOT NULL DEFAULT 'draft',
|
||||
capacity INT,
|
||||
default_check_in_at TIMESTAMPTZ,
|
||||
default_check_out_at TIMESTAMPTZ,
|
||||
created_by_user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_retreat_status ON app.retreat(status);
|
||||
CREATE INDEX idx_retreat_dates ON app.retreat(start_at, end_at);
|
||||
|
||||
-- ============================================================
|
||||
-- Retreat persons
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE app.retreat_person (
|
||||
retreat_person_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
retreat_id UUID NOT NULL REFERENCES app.retreat(retreat_id) ON DELETE CASCADE,
|
||||
user_id UUID REFERENCES app."user"(user_id),
|
||||
stay_id UUID REFERENCES app.stay(stay_id),
|
||||
full_name TEXT,
|
||||
email TEXT,
|
||||
phone TEXT,
|
||||
role_type app.retreat_person_role NOT NULL DEFAULT 'participant',
|
||||
attendance_status app.retreat_attendance_status NOT NULL DEFAULT 'registered',
|
||||
arrival_override_at TIMESTAMPTZ,
|
||||
departure_override_at TIMESTAMPTZ,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE(retreat_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_retreat_person_retreat ON app.retreat_person(retreat_id);
|
||||
CREATE INDEX idx_retreat_person_user ON app.retreat_person(user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Retreat schedule items
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE app.retreat_schedule_item (
|
||||
item_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
retreat_id UUID NOT NULL REFERENCES app.retreat(retreat_id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
starts_at TIMESTAMPTZ NOT NULL,
|
||||
ends_at TIMESTAMPTZ NOT NULL,
|
||||
location TEXT,
|
||||
lead_retreat_person_id UUID REFERENCES app.retreat_person(retreat_person_id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_retreat_schedule_retreat ON app.retreat_schedule_item(retreat_id);
|
||||
CREATE INDEX idx_retreat_schedule_times ON app.retreat_schedule_item(starts_at, ends_at);
|
||||
|
||||
-- Enable audit
|
||||
SELECT app.enable_audit('retreat', 'retreat_id');
|
||||
SELECT app.enable_audit('retreat_person', 'retreat_person_id');
|
||||
SELECT app.enable_audit('retreat_schedule_item', 'item_id');
|
||||
76
sql/0010-kitchen.sql
Normal file
76
sql/0010-kitchen.sql
Normal file
@@ -0,0 +1,76 @@
|
||||
-- ============================================================
|
||||
-- Kitchen / Dietary enum types
|
||||
-- ============================================================
|
||||
|
||||
CREATE TYPE app.meal_type AS ENUM ('breakfast', 'lunch', 'dinner', 'snack');
|
||||
CREATE TYPE app.meal_status AS ENUM ('planned', 'ready', 'served', 'completed');
|
||||
CREATE TYPE app.meal_attendance_status AS ENUM ('expected', 'confirmed', 'cancelled', 'served');
|
||||
CREATE TYPE app.meal_attendance_source AS ENUM ('stay', 'retreat', 'manual');
|
||||
|
||||
-- ============================================================
|
||||
-- Dietary profiles
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE app.dietary_profile (
|
||||
profile_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL UNIQUE REFERENCES app."user"(user_id) ON DELETE CASCADE,
|
||||
is_vegan BOOLEAN NOT NULL DEFAULT false,
|
||||
is_vegetarian BOOLEAN NOT NULL DEFAULT false,
|
||||
is_gluten_free BOOLEAN NOT NULL DEFAULT false,
|
||||
is_dairy_free BOOLEAN NOT NULL DEFAULT false,
|
||||
has_nut_allergy BOOLEAN NOT NULL DEFAULT false,
|
||||
allergy_notes TEXT,
|
||||
other_requirements TEXT,
|
||||
dislikes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_dietary_profile_user ON app.dietary_profile(user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Meal services
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE app.meal_service (
|
||||
service_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
service_type app.meal_type NOT NULL,
|
||||
service_date DATE NOT NULL,
|
||||
retreat_id UUID REFERENCES app.retreat(retreat_id),
|
||||
planned_headcount INT,
|
||||
menu_notes TEXT,
|
||||
status app.meal_status NOT NULL DEFAULT 'planned',
|
||||
created_by_user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
updated_by_user_id UUID REFERENCES app."user"(user_id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_meal_service_date ON app.meal_service(service_date);
|
||||
CREATE INDEX idx_meal_service_type ON app.meal_service(service_type);
|
||||
CREATE INDEX idx_meal_service_retreat ON app.meal_service(retreat_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Meal attendance
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE app.meal_attendance (
|
||||
attendance_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
meal_service_id UUID NOT NULL REFERENCES app.meal_service(service_id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
stay_id UUID REFERENCES app.stay(stay_id),
|
||||
source_type app.meal_attendance_source NOT NULL DEFAULT 'manual',
|
||||
status app.meal_attendance_status NOT NULL DEFAULT 'expected',
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE(meal_service_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_meal_attendance_service ON app.meal_attendance(meal_service_id);
|
||||
CREATE INDEX idx_meal_attendance_user ON app.meal_attendance(user_id);
|
||||
|
||||
-- Enable audit
|
||||
SELECT app.enable_audit('dietary_profile', 'profile_id');
|
||||
SELECT app.enable_audit('meal_service', 'service_id');
|
||||
SELECT app.enable_audit('meal_attendance', 'attendance_id');
|
||||
49
sql/0011-inventory.sql
Normal file
49
sql/0011-inventory.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
-- ============================================================
|
||||
-- Inventory enum types
|
||||
-- ============================================================
|
||||
|
||||
CREATE TYPE app.inventory_request_status AS ENUM ('submitted', 'approved', 'rejected', 'fulfilled', 'cancelled');
|
||||
|
||||
-- ============================================================
|
||||
-- Inventory items
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE app.inventory_item (
|
||||
item_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
unit TEXT NOT NULL DEFAULT 'unit',
|
||||
current_quantity NUMERIC NOT NULL DEFAULT 0,
|
||||
minimum_quantity NUMERIC NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_inventory_item_active ON app.inventory_item(is_active);
|
||||
|
||||
-- ============================================================
|
||||
-- Inventory requests
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE app.inventory_request (
|
||||
request_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
requested_by_user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
item_id UUID REFERENCES app.inventory_item(item_id),
|
||||
item_name TEXT NOT NULL,
|
||||
quantity NUMERIC NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
purpose TEXT,
|
||||
status app.inventory_request_status NOT NULL DEFAULT 'submitted',
|
||||
reviewed_by_user_id UUID REFERENCES app."user"(user_id),
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
review_notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_inventory_request_status ON app.inventory_request(status);
|
||||
CREATE INDEX idx_inventory_request_user ON app.inventory_request(requested_by_user_id);
|
||||
|
||||
-- Enable audit
|
||||
SELECT app.enable_audit('inventory_item', 'item_id');
|
||||
SELECT app.enable_audit('inventory_request', 'request_id');
|
||||
6
sql/0012-inventory-categories.sql
Normal file
6
sql/0012-inventory-categories.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
-- Add category (enum) and subcategory (free text) to inventory items
|
||||
ALTER TABLE app.inventory_item
|
||||
ADD COLUMN category TEXT NOT NULL DEFAULT 'operations',
|
||||
ADD COLUMN subcategory TEXT;
|
||||
|
||||
CREATE INDEX idx_inventory_item_category ON app.inventory_item(category);
|
||||
38
sql/0013-finance.sql
Normal file
38
sql/0013-finance.sql
Normal file
@@ -0,0 +1,38 @@
|
||||
-- ============================================================
|
||||
-- Finance records
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE app.finance_record (
|
||||
finance_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
amount NUMERIC(12,2) NOT NULL,
|
||||
currency TEXT NOT NULL DEFAULT 'EUR',
|
||||
record_type TEXT NOT NULL DEFAULT 'expense', -- expense, income, reimbursement
|
||||
purchased_by_user_id UUID REFERENCES app."user"(user_id),
|
||||
purchased_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
category TEXT NOT NULL DEFAULT 'operations', -- same enum as tasks/inventory
|
||||
created_by_user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_finance_record_category ON app.finance_record(category);
|
||||
CREATE INDEX idx_finance_record_purchased_at ON app.finance_record(purchased_at);
|
||||
CREATE INDEX idx_finance_record_purchased_by ON app.finance_record(purchased_by_user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Finance links (polymorphic — links finance records to any entity)
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE app.finance_link (
|
||||
link_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
finance_id UUID NOT NULL REFERENCES app.finance_record(finance_id) ON DELETE CASCADE,
|
||||
entity_type TEXT NOT NULL, -- inventory_item, inventory_request, stay, boat_trip, retreat, task, room
|
||||
entity_id UUID NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_finance_link_finance ON app.finance_link(finance_id);
|
||||
CREATE INDEX idx_finance_link_entity ON app.finance_link(entity_type, entity_id);
|
||||
11
sql/0014-inventory-transactions.sql
Normal file
11
sql/0014-inventory-transactions.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE app.inventory_transaction (
|
||||
transaction_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
item_id UUID NOT NULL REFERENCES app.inventory_item(item_id),
|
||||
quantity_change NUMERIC NOT NULL,
|
||||
reason TEXT NOT NULL, -- 'stocktake', 'adjustment', 'consumption', 'restock', 'request_fulfilled'
|
||||
notes TEXT,
|
||||
created_by_user_id UUID NOT NULL REFERENCES app."user"(user_id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX idx_inventory_transaction_item ON app.inventory_transaction(item_id);
|
||||
CREATE INDEX idx_inventory_transaction_created ON app.inventory_transaction(created_at);
|
||||
1
sql/0015-task-role-assignment.sql
Normal file
1
sql/0015-task-role-assignment.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE app.task_instance ADD COLUMN assigned_role TEXT;
|
||||
4
sql/0016-user-phone-fields.sql
Normal file
4
sql/0016-user-phone-fields.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- Add mobile and WhatsApp number fields to user table
|
||||
ALTER TABLE app."user"
|
||||
ADD COLUMN mobile_number TEXT,
|
||||
ADD COLUMN whatsapp_number TEXT;
|
||||
2
sql/0017-room-price.sql
Normal file
2
sql/0017-room-price.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- Add price per night to rooms
|
||||
ALTER TABLE app.room ADD COLUMN price_per_night NUMERIC(10,2);
|
||||
327
sql/0018-seed-demo-data.sql
Normal file
327
sql/0018-seed-demo-data.sql
Normal file
@@ -0,0 +1,327 @@
|
||||
-- ============================================================
|
||||
-- Demo seed data for PerAuset
|
||||
-- Run after all migrations to populate with realistic test data
|
||||
-- ============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- Users (diverse roles)
|
||||
-- ============================================================
|
||||
INSERT INTO app."user" (email, first_name, last_name, display_name, roles, status, phone, mobile_number, whatsapp_number) VALUES
|
||||
('sarah@perauset.org', 'Sarah', 'El-Masry', 'Sarah', '{coordinator}', 'active', '+20 100 123 4567', '+20 100 123 4567', '+20 100 123 4567'),
|
||||
('omar@perauset.org', 'Omar', 'Hassan', 'Omar', '{facilitator}', 'active', '+20 101 234 5678', '+20 101 234 5678', null),
|
||||
('fatima@perauset.org', 'Fatima', 'Abdel-Rahman', 'Fatima', '{kitchen_lead}', 'active', '+20 102 345 6789', '+20 102 345 6789', '+20 102 345 6789'),
|
||||
('ahmed@perauset.org', 'Ahmed', 'Nour', 'Ahmed', '{boat_coordinator}', 'active', '+20 103 456 7890', '+20 103 456 7890', null),
|
||||
('layla@perauset.org', 'Layla', 'Ibrahim', 'Layla', '{staff}', 'active', null, '+20 104 567 8901', '+20 104 567 8901'),
|
||||
('youssef@perauset.org', 'Youssef', 'Kamal', 'Youssef', '{volunteer}', 'active', null, '+20 105 678 9012', null),
|
||||
('nadia@perauset.org', 'Nadia', 'Farouk', 'Nadia', '{community_member}', 'active', null, null, null),
|
||||
('marco@example.com', 'Marco', 'Rossi', 'Marco', '{guest}', 'active', '+39 333 123 4567', null, '+39 333 123 4567'),
|
||||
('anna@example.com', 'Anna', 'Müller', 'Anna', '{guest}', 'active', '+49 171 234 5678', null, null),
|
||||
('james@example.com', 'James', 'Chen', 'James', '{volunteer}', 'active', null, '+1 555 987 6543', '+1 555 987 6543')
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
|
||||
-- ============================================================
|
||||
-- Dietary profiles
|
||||
-- ============================================================
|
||||
INSERT INTO app.dietary_profile (user_id, is_vegetarian, is_vegan, is_gluten_free, is_dairy_free, has_nut_allergy, allergy_notes, other_requirements)
|
||||
SELECT u.user_id, r.veg, r.vgn, r.gf, r.df, r.nut, r.allergy_notes, r.other
|
||||
FROM (VALUES
|
||||
('fatima@perauset.org', true, false, false, false, false, null, 'No onions please'),
|
||||
('marco@example.com', false, true, true, true, true, 'Severe nut allergy', 'Strict vegan - no honey'),
|
||||
('anna@example.com', false, false, false, true, false, 'Lactose intolerant', null),
|
||||
('omar@perauset.org', false, false, false, false, false, null, 'Halal only'),
|
||||
('nadia@perauset.org', false, false, false, false, false, 'Shellfish allergy', null)
|
||||
) AS r(email, veg, vgn, gf, df, nut, allergy_notes, other)
|
||||
JOIN app."user" u ON u.email = r.email
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
-- ============================================================
|
||||
-- Rooms
|
||||
-- ============================================================
|
||||
INSERT INTO app.room (code, name, room_type, capacity, price_per_night, notes) VALUES
|
||||
('R-101', 'Sunrise Room', 'private', 2, 45.00, 'East-facing, morning sun'),
|
||||
('R-102', 'Nile View', 'private', 2, 55.00, 'River view, premium room'),
|
||||
('R-103', 'Palm Suite', 'facilitator', 2, 60.00, 'Reserved for retreat facilitators'),
|
||||
('R-201', 'Garden Dorm A', 'dorm', 6, 15.00, 'Ground floor, garden access'),
|
||||
('R-202', 'Garden Dorm B', 'dorm', 6, 15.00, 'Ground floor, garden access'),
|
||||
('R-203', 'Rooftop Dorm', 'dorm', 4, 18.00, 'Rooftop terrace access'),
|
||||
('R-301', 'Staff Room 1', 'staff', 2, null, 'Staff accommodation'),
|
||||
('R-302', 'Staff Room 2', 'staff', 2, null, 'Staff accommodation'),
|
||||
('R-401', 'Shared Room 1', 'shared', 3, 25.00, 'Shared bathroom'),
|
||||
('R-402', 'Shared Room 2', 'shared', 3, 25.00, 'Shared bathroom')
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- ============================================================
|
||||
-- Boat routes and boats
|
||||
-- ============================================================
|
||||
INSERT INTO app.boat_route (name, origin, destination) VALUES
|
||||
('Main Ferry', 'Aswan Marina', 'Per Auset Island'),
|
||||
('Express Shuttle', 'Aswan Corniche', 'Per Auset Island'),
|
||||
('Supply Run', 'Aswan Market Dock', 'Per Auset Island')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO app.boat (name, passenger_capacity) VALUES
|
||||
('Lotus', 20),
|
||||
('Felucca Star', 12),
|
||||
('Supply Barge', 8)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ============================================================
|
||||
-- Boat trips (upcoming)
|
||||
-- ============================================================
|
||||
INSERT INTO app.boat_trip (route_id, boat_id, status, scheduled_departure_at, scheduled_arrival_at, notes, created_by_user_id)
|
||||
SELECT
|
||||
r.route_id, b.boat_id, 'open'::app.boat_trip_status,
|
||||
(CURRENT_DATE + interval '1 day' + interval '8 hours')::timestamptz,
|
||||
(CURRENT_DATE + interval '1 day' + interval '8 hours 30 minutes')::timestamptz,
|
||||
'Morning ferry', u.user_id
|
||||
FROM app.boat_route r, app.boat b, app."user" u
|
||||
WHERE r.name = 'Main Ferry' AND b.name = 'Lotus' AND u.email = 'ahmed@perauset.org'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO app.boat_trip (route_id, boat_id, status, scheduled_departure_at, scheduled_arrival_at, notes, created_by_user_id)
|
||||
SELECT
|
||||
r.route_id, b.boat_id, 'open'::app.boat_trip_status,
|
||||
(CURRENT_DATE + interval '1 day' + interval '16 hours')::timestamptz,
|
||||
(CURRENT_DATE + interval '1 day' + interval '16 hours 30 minutes')::timestamptz,
|
||||
'Evening return', u.user_id
|
||||
FROM app.boat_route r, app.boat b, app."user" u
|
||||
WHERE r.name = 'Main Ferry' AND b.name = 'Lotus' AND u.email = 'ahmed@perauset.org'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO app.boat_trip (route_id, boat_id, status, scheduled_departure_at, scheduled_arrival_at, notes, created_by_user_id)
|
||||
SELECT
|
||||
r.route_id, b.boat_id, 'planned'::app.boat_trip_status,
|
||||
(CURRENT_DATE + interval '3 days' + interval '10 hours')::timestamptz,
|
||||
(CURRENT_DATE + interval '3 days' + interval '10 hours 45 minutes')::timestamptz,
|
||||
'Weekly supply run', u.user_id
|
||||
FROM app.boat_route r, app.boat b, app."user" u
|
||||
WHERE r.name = 'Supply Run' AND b.name = 'Supply Barge' AND u.email = 'ahmed@perauset.org'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ============================================================
|
||||
-- Stay requests & stays
|
||||
-- ============================================================
|
||||
INSERT INTO app.stay_request (user_id, request_type, requested_start_at, requested_end_at, status, notes)
|
||||
SELECT u.user_id, r.request_type::app.stay_request_type, r.start_at::date, r.end_at::date, r.status::app.stay_request_status, r.notes
|
||||
FROM (VALUES
|
||||
('marco@example.com', 'guest', CURRENT_DATE + 2, CURRENT_DATE + 9, 'submitted', 'First visit, excited to learn about permaculture'),
|
||||
('anna@example.com', 'guest', CURRENT_DATE + 5, CURRENT_DATE + 12, 'submitted', 'Interested in the kitchen program'),
|
||||
('james@example.com', 'volunteer', CURRENT_DATE + 7, CURRENT_DATE + 37, 'approved', 'Gardening and construction skills'),
|
||||
('nadia@perauset.org', 'day_visit', CURRENT_DATE + 3, CURRENT_DATE + 3, 'approved', null)
|
||||
) AS r(email, request_type, start_at, end_at, status, notes)
|
||||
JOIN app."user" u ON u.email = r.email;
|
||||
|
||||
-- Create stays from approved requests
|
||||
INSERT INTO app.stay (user_id, source_request_id, start_at, end_at, stay_type, status, created_by_user_id)
|
||||
SELECT sr.user_id, sr.request_id, sr.requested_start_at, sr.requested_end_at,
|
||||
(CASE sr.request_type::text
|
||||
WHEN 'guest' THEN 'guest'
|
||||
WHEN 'volunteer' THEN 'volunteer'
|
||||
WHEN 'day_visit' THEN 'day_visitor'
|
||||
ELSE 'guest'
|
||||
END)::app.stay_type,
|
||||
'confirmed'::app.stay_status, (SELECT user_id FROM app."user" WHERE email = 'sarah@perauset.org')
|
||||
FROM app.stay_request sr
|
||||
WHERE sr.status = 'approved';
|
||||
|
||||
-- ============================================================
|
||||
-- Room allocations
|
||||
-- ============================================================
|
||||
INSERT INTO app.room_allocation (room_id, stay_id, starts_at, ends_at, status, allocation_reason, created_by_user_id)
|
||||
SELECT r.room_id, s.stay_id, s.start_at, s.end_at, 'active'::app.room_allocation_status, 'Guest stay',
|
||||
(SELECT user_id FROM app."user" WHERE email = 'sarah@perauset.org')
|
||||
FROM app.stay s
|
||||
JOIN app."user" u ON u.user_id = s.user_id
|
||||
JOIN app.room r ON r.code = CASE u.email
|
||||
WHEN 'james@example.com' THEN 'R-201'
|
||||
ELSE 'R-101'
|
||||
END
|
||||
WHERE s.status = 'confirmed'
|
||||
LIMIT 2;
|
||||
|
||||
-- ============================================================
|
||||
-- Inventory items
|
||||
-- ============================================================
|
||||
INSERT INTO app.inventory_item (name, unit, current_quantity, minimum_quantity, category, subcategory) VALUES
|
||||
('Rice', 'kg', 50, 20, 'kitchen', 'staples'),
|
||||
('Olive Oil', 'liter', 15, 5, 'kitchen', 'cooking-oils'),
|
||||
('Flour', 'kg', 30, 10, 'kitchen', 'staples'),
|
||||
('Tomatoes', 'kg', 8, 5, 'kitchen', 'fresh-produce'),
|
||||
('Onions', 'kg', 12, 5, 'kitchen', 'fresh-produce'),
|
||||
('Garlic', 'kg', 3, 1, 'kitchen', 'fresh-produce'),
|
||||
('Lentils', 'kg', 25, 10, 'kitchen', 'staples'),
|
||||
('Chickpeas (dried)', 'kg', 15, 5, 'kitchen', 'staples'),
|
||||
('Tahini', 'jar', 6, 3, 'kitchen', 'condiments'),
|
||||
('Soap (handmade)', 'bar', 40, 15, 'rooms', 'toiletries'),
|
||||
('Towels', 'piece', 30, 10, 'rooms', 'linens'),
|
||||
('Bed Sheets', 'set', 20, 8, 'rooms', 'linens'),
|
||||
('Mosquito Nets', 'piece', 15, 5, 'rooms', 'supplies'),
|
||||
('Garden Hose', 'piece', 3, 1, 'garden', 'tools'),
|
||||
('Compost Bags', 'roll', 5, 2, 'garden', 'supplies'),
|
||||
('Garden Shears', 'piece', 4, 2, 'garden', 'tools'),
|
||||
('Solar Panel Cleaner', 'liter', 4, 2, 'equipment', 'cleaning'),
|
||||
('First Aid Kit', 'kit', 3, 2, 'equipment', 'safety'),
|
||||
('Boat Fuel', 'liter', 80, 30, 'equipment', 'fuel'),
|
||||
('Life Jackets', 'piece', 25, 15, 'equipment', 'safety'),
|
||||
('Yoga Mats', 'piece', 20, 10, 'other', 'retreat'),
|
||||
('Whiteboard Markers', 'pack', 5, 2, 'other', 'office')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ============================================================
|
||||
-- Task templates
|
||||
-- ============================================================
|
||||
INSERT INTO app.task_template (title, description, category, is_claimable, default_location, estimated_minutes, recurrence_cron, created_by_user_id)
|
||||
SELECT t.title, t.description, t.category::app.task_category, t.is_claimable, t.location, t.minutes, t.cron,
|
||||
(SELECT user_id FROM app."user" WHERE email = 'sarah@perauset.org')
|
||||
FROM (VALUES
|
||||
('Morning kitchen prep', 'Prepare breakfast ingredients and set up kitchen', 'kitchen', true, 'Main Kitchen', 60, 'daily'),
|
||||
('Evening cleanup', 'Clean kitchen and dining area after dinner', 'kitchen', true, 'Main Kitchen', 45, 'daily'),
|
||||
('Garden watering', 'Water all garden beds and check irrigation', 'maintenance', true, 'Garden', 30, 'daily'),
|
||||
('Guest room turnover', 'Clean and prepare rooms for new guests', 'housekeeping', true, null, 45, null),
|
||||
('Compost management', 'Turn compost piles and add new material', 'maintenance', true, 'Compost Area', 30, 'weekly'),
|
||||
('Boat inspection', 'Check fuel, safety equipment, and hull condition', 'transport', false, 'Dock', 30, 'weekly'),
|
||||
('Inventory check', 'Review kitchen inventory and note low-stock items', 'operations', false, 'Storage Room', 45, 'weekly'),
|
||||
('Solar panel cleaning', 'Clean all solar panels', 'maintenance', true, 'Rooftop', 60, 'monthly')
|
||||
) AS t(title, description, category, is_claimable, location, minutes, cron);
|
||||
|
||||
-- ============================================================
|
||||
-- Task instances (active)
|
||||
-- ============================================================
|
||||
INSERT INTO app.task_instance (title, description, category, status, location, scheduled_start_at, scheduled_end_at, created_by_user_id)
|
||||
SELECT t.title, t.description, t.category::app.task_category, t.status::app.task_status, t.location,
|
||||
t.start_at::timestamptz, t.end_at::timestamptz,
|
||||
(SELECT user_id FROM app."user" WHERE email = 'sarah@perauset.org')
|
||||
FROM (VALUES
|
||||
('Morning kitchen prep', 'Prepare breakfast', 'kitchen', 'open', 'Main Kitchen',
|
||||
CURRENT_DATE + interval '6 hours', CURRENT_DATE + interval '7 hours'),
|
||||
('Garden watering', 'Water beds and check drip lines', 'maintenance', 'open', 'Garden',
|
||||
CURRENT_DATE + interval '7 hours', CURRENT_DATE + interval '7 hours 30 minutes'),
|
||||
('Guest room turnover - R-101', 'Clean Sunrise Room for new arrival', 'housekeeping', 'assigned', null,
|
||||
CURRENT_DATE + interval '10 hours', CURRENT_DATE + interval '11 hours'),
|
||||
('Evening cleanup', 'Kitchen and dining cleanup', 'kitchen', 'open', 'Main Kitchen',
|
||||
CURRENT_DATE + interval '20 hours', CURRENT_DATE + interval '21 hours'),
|
||||
('Fix leaky faucet', 'Bathroom faucet in R-201 is dripping', 'maintenance', 'in_progress', 'R-201',
|
||||
CURRENT_DATE - interval '1 day', null),
|
||||
('Organize storage room', 'Sort supplies and update inventory labels', 'operations', 'open', 'Storage Room',
|
||||
CURRENT_DATE + interval '2 days', CURRENT_DATE + interval '2 days 2 hours'),
|
||||
('Prepare yoga space', 'Set up mats and clean the retreat hall', 'retreat', 'open', 'Retreat Hall',
|
||||
CURRENT_DATE + interval '3 days' + interval '8 hours', CURRENT_DATE + interval '3 days' + interval '9 hours')
|
||||
) AS t(title, description, category, status, location, start_at, end_at);
|
||||
|
||||
-- Assign some tasks
|
||||
INSERT INTO app.task_assignment (task_instance_id, assigned_user_id, assigned_by_user_id, status)
|
||||
SELECT ti.task_id, u.user_id,
|
||||
(SELECT user_id FROM app."user" WHERE email = 'sarah@perauset.org'),
|
||||
'accepted'::app.task_assignment_status
|
||||
FROM app.task_instance ti
|
||||
JOIN app."user" u ON u.email = CASE ti.title
|
||||
WHEN 'Guest room turnover - R-101' THEN 'layla@perauset.org'
|
||||
WHEN 'Fix leaky faucet' THEN 'youssef@perauset.org'
|
||||
END
|
||||
WHERE ti.status IN ('assigned', 'in_progress');
|
||||
|
||||
-- ============================================================
|
||||
-- Meal services (this week)
|
||||
-- ============================================================
|
||||
INSERT INTO app.meal_service (service_type, service_date, planned_headcount, menu_notes, created_by_user_id)
|
||||
SELECT m.service_type::app.meal_type, m.service_date::date, m.headcount, m.notes,
|
||||
(SELECT user_id FROM app."user" WHERE email = 'fatima@perauset.org')
|
||||
FROM (VALUES
|
||||
('breakfast', CURRENT_DATE, 12, 'Ful medames, falafel, bread, tahini, fresh vegetables'),
|
||||
('lunch', CURRENT_DATE, 15, 'Koshari with lentils and pasta, salad, pickles'),
|
||||
('dinner', CURRENT_DATE, 14, 'Grilled vegetables, rice, molokhia soup'),
|
||||
('breakfast', CURRENT_DATE + 1, 12, 'Shakshuka, cheese, olives, fresh bread'),
|
||||
('lunch', CURRENT_DATE + 1, 15, 'Stuffed vine leaves, hummus, tabbouleh'),
|
||||
('dinner', CURRENT_DATE + 1, 14, 'Lentil soup, grilled fish, salad'),
|
||||
('breakfast', CURRENT_DATE + 2, 14, 'Pancakes, fruit, yogurt, honey'),
|
||||
('lunch', CURRENT_DATE + 2, 16, 'Vegetable tagine, couscous, mint tea')
|
||||
) AS m(service_type, service_date, headcount, notes);
|
||||
|
||||
-- ============================================================
|
||||
-- Retreats
|
||||
-- ============================================================
|
||||
INSERT INTO app.retreat (name, slug, description, start_at, end_at, capacity, status, created_by_user_id)
|
||||
SELECT r.name, r.slug, r.description, r.start_at::date, r.end_at::date, r.capacity, r.status::app.retreat_status,
|
||||
(SELECT user_id FROM app."user" WHERE email = 'omar@perauset.org')
|
||||
FROM (VALUES
|
||||
('Spring Permaculture Intensive', 'spring-permaculture-2026',
|
||||
'A 7-day deep dive into permaculture design, soil building, and sustainable food systems on the island.',
|
||||
CURRENT_DATE + 14, CURRENT_DATE + 21, 20, 'published'),
|
||||
('Yoga & Meditation Retreat', 'yoga-meditation-april',
|
||||
'A 5-day retreat focused on daily yoga, meditation, and mindful living by the Nile.',
|
||||
CURRENT_DATE + 30, CURRENT_DATE + 35, 15, 'draft'),
|
||||
('Community Building Workshop', 'community-building-may',
|
||||
'Learn participatory decision-making, conflict resolution, and community governance.',
|
||||
CURRENT_DATE + 45, CURRENT_DATE + 48, 25, 'draft')
|
||||
) AS r(name, slug, description, start_at, end_at, capacity, status);
|
||||
|
||||
-- Add people to the published retreat
|
||||
INSERT INTO app.retreat_person (retreat_id, user_id, role_type, attendance_status)
|
||||
SELECT ret.retreat_id, u.user_id, r.role::app.retreat_person_role, 'confirmed'::app.retreat_attendance_status
|
||||
FROM (VALUES
|
||||
('omar@perauset.org', 'facilitator'),
|
||||
('sarah@perauset.org', 'organizer'),
|
||||
('marco@example.com', 'participant'),
|
||||
('anna@example.com', 'participant'),
|
||||
('james@example.com', 'participant'),
|
||||
('nadia@perauset.org', 'participant')
|
||||
) AS r(email, role)
|
||||
JOIN app."user" u ON u.email = r.email
|
||||
JOIN app.retreat ret ON ret.slug = 'spring-permaculture-2026';
|
||||
|
||||
-- Add schedule to the published retreat
|
||||
INSERT INTO app.retreat_schedule_item (retreat_id, title, description, starts_at, ends_at, location)
|
||||
SELECT ret.retreat_id, s.title, s.description,
|
||||
(ret.start_at + s.day_offset + s.start_time)::timestamptz,
|
||||
(ret.start_at + s.day_offset + s.end_time)::timestamptz,
|
||||
s.location
|
||||
FROM (VALUES
|
||||
('Welcome Circle', 'Introduction and intention setting', interval '0 days', interval '16 hours', interval '18 hours', 'Retreat Hall'),
|
||||
('Permaculture Principles', 'Theory session on 12 permaculture principles', interval '1 day', interval '9 hours', interval '12 hours', 'Classroom'),
|
||||
('Garden Walk', 'Guided tour of existing permaculture gardens', interval '1 day', interval '14 hours', interval '16 hours', 'Garden'),
|
||||
('Soil Building Workshop', 'Hands-on composting and soil preparation', interval '2 days', interval '9 hours', interval '12 hours', 'Compost Area'),
|
||||
('Water Systems Design', 'Designing rainwater harvesting and greywater systems', interval '3 days', interval '9 hours', interval '12 hours', 'Classroom'),
|
||||
('Food Forest Planning', 'Planning and planting a food forest', interval '4 days', interval '9 hours', interval '12 hours', 'Garden'),
|
||||
('Closing Circle', 'Reflection, sharing, and next steps', interval '6 days', interval '16 hours', interval '18 hours', 'Retreat Hall')
|
||||
) AS s(title, description, day_offset, start_time, end_time, location)
|
||||
JOIN app.retreat ret ON ret.slug = 'spring-permaculture-2026';
|
||||
|
||||
-- ============================================================
|
||||
-- Finance records
|
||||
-- ============================================================
|
||||
INSERT INTO app.finance_record (name, description, amount, currency, record_type, category, purchased_at, created_by_user_id, purchased_by_user_id)
|
||||
SELECT f.name, f.description, f.amount, 'EUR', f.record_type, f.category,
|
||||
(CURRENT_DATE - f.days_ago)::timestamptz,
|
||||
(SELECT user_id FROM app."user" WHERE email = 'sarah@perauset.org'),
|
||||
(SELECT user_id FROM app."user" WHERE email = f.buyer)
|
||||
FROM (VALUES
|
||||
('Weekly market groceries', 'Vegetables, fruits, bread from Aswan market', 180.00, 'expense', 'kitchen', 2, 'fatima@perauset.org'),
|
||||
('Boat fuel', 'Diesel for Lotus and Supply Barge', 95.00, 'expense', 'transport', 3, 'ahmed@perauset.org'),
|
||||
('Guest donation - Marco', 'Voluntary contribution for stay', 200.00, 'income', 'operations', 1, 'sarah@perauset.org'),
|
||||
('Solar panel parts', 'Replacement inverter for roof array', 320.00, 'expense', 'maintenance', 7, 'sarah@perauset.org'),
|
||||
('Retreat deposit - Spring Permaculture', 'Deposit for materials and facilitator', 500.00, 'expense', 'retreat', 10, 'omar@perauset.org'),
|
||||
('Soap making supplies', 'Olive oil, lye, essential oils', 45.00, 'expense', 'operations', 5, 'layla@perauset.org'),
|
||||
('Workshop fee income', 'Day workshop attendee fees', 150.00, 'income', 'retreat', 4, 'sarah@perauset.org'),
|
||||
('Plumbing repair parts', 'Faucet and pipe fittings', 35.00, 'expense', 'maintenance', 1, 'youssef@perauset.org'),
|
||||
('Monthly internet', 'Satellite internet subscription', 75.00, 'expense', 'operations', 15, 'sarah@perauset.org'),
|
||||
('Guest donation - Anna', 'Contribution for kitchen program stay', 150.00, 'income', 'kitchen', 3, 'sarah@perauset.org')
|
||||
) AS f(name, description, amount, record_type, category, days_ago, buyer);
|
||||
|
||||
-- ============================================================
|
||||
-- Notifications
|
||||
-- ============================================================
|
||||
INSERT INTO app.notification (user_id, type, title, body)
|
||||
SELECT u.user_id, 'info', n.title, n.body
|
||||
FROM (VALUES
|
||||
('sarah@perauset.org', 'New stay request', 'Marco Rossi has submitted a stay request for Mar 22 - Mar 29'),
|
||||
('sarah@perauset.org', 'New stay request', 'Anna Müller has submitted a stay request for Mar 25 - Apr 1'),
|
||||
('fatima@perauset.org', 'Low stock alert', 'Tomatoes are below minimum quantity (8 kg < 5 kg threshold)'),
|
||||
('ahmed@perauset.org', 'Boat trip tomorrow', 'Morning ferry to Per Auset Island departing at 8:00 AM'),
|
||||
('layla@perauset.org', 'Task assigned', 'You have been assigned: Guest room turnover - R-101'),
|
||||
('youssef@perauset.org', 'Task assigned', 'You have been assigned: Fix leaky faucet in R-201'),
|
||||
('omar@perauset.org', 'Retreat published', 'Spring Permaculture Intensive is now published with 6 participants'),
|
||||
('marco@example.com', 'Stay request submitted', 'Your stay request for Mar 22 - Mar 29 has been submitted'),
|
||||
('james@example.com', 'Stay approved', 'Your volunteer stay request has been approved. Welcome!'),
|
||||
('admin@perauset.org', 'Weekly summary', '3 active stays, 2 pending requests, 7 open tasks, 2 boat trips scheduled')
|
||||
) AS n(email, title, body)
|
||||
JOIN app."user" u ON u.email = n.email;
|
||||
12
sql/0019-exchange-rates.sql
Normal file
12
sql/0019-exchange-rates.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- Exchange rates table for daily currency conversion
|
||||
CREATE TABLE app.exchange_rate (
|
||||
rate_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
base_currency TEXT NOT NULL DEFAULT 'EGP',
|
||||
target_currency TEXT NOT NULL,
|
||||
rate NUMERIC(12,6) NOT NULL,
|
||||
fetched_at DATE NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_exchange_rate_unique ON app.exchange_rate(base_currency, target_currency, fetched_at);
|
||||
CREATE INDEX idx_exchange_rate_date ON app.exchange_rate(fetched_at DESC);
|
||||
5
sql/0020-finance-base-amount.sql
Normal file
5
sql/0020-finance-base-amount.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- Add base amount in EGP for all finance records
|
||||
ALTER TABLE app.finance_record ADD COLUMN amount_egp NUMERIC(12,2);
|
||||
|
||||
-- For existing EGP records, set amount_egp = amount
|
||||
UPDATE app.finance_record SET amount_egp = amount WHERE currency = 'EGP';
|
||||
1
sql/0021-user-avatar.sql
Normal file
1
sql/0021-user-avatar.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE app."user" ADD COLUMN avatar_url TEXT;
|
||||
Reference in New Issue
Block a user