-- Full Database Schema for GPI (PostgreSQL) CREATE SCHEMA IF NOT EXISTS gpi; CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- 1. Organizations CREATE TABLE IF NOT EXISTS gpi.organizations ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), clerk_id TEXT UNIQUE, -- Legacy logto_id TEXT UNIQUE, name TEXT NOT NULL, is_banned BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 2. Users CREATE TABLE IF NOT EXISTS gpi.users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), clerk_id TEXT UNIQUE, -- Legacy logto_id TEXT UNIQUE, email TEXT UNIQUE NOT NULL, name TEXT NOT NULL, role TEXT CHECK (role IN ('guest', 'user', 'admin')) DEFAULT 'guest', is_banned BOOLEAN DEFAULT FALSE, last_seen_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 3. many-to-many user_organizations CREATE TABLE IF NOT EXISTS gpi.user_organizations ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES gpi.users(id) ON DELETE CASCADE, organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE, role TEXT CHECK (role IN ('guest', 'user', 'admin')) DEFAULT 'guest', is_banned BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE (user_id, organization_id) ); -- 4. Technical Data Sheets (Fichas Técnicas) CREATE TABLE IF NOT EXISTS gpi.technical_data_sheets ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE, name TEXT NOT NULL, manufacturer TEXT, type TEXT, file_url TEXT, solids_volume DECIMAL, density DECIMAL, mixing_ratio TEXT, yield_theoretical DECIMAL, wft_min DECIMAL, wft_max DECIMAL, dft_min DECIMAL, dft_max DECIMAL, reducer TEXT, yield_factor DECIMAL DEFAULT 1.0, min_stock DECIMAL DEFAULT 0, notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 5. Projects CREATE TABLE IF NOT EXISTS gpi.projects ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE, name TEXT NOT NULL, client TEXT NOT NULL, start_date DATE, end_date DATE, technician TEXT, environment TEXT, weight_kg DECIMAL, status TEXT DEFAULT 'active', created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 6. Parts CREATE TABLE IF NOT EXISTS gpi.parts ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE, description TEXT NOT NULL, dimensions TEXT, weight DECIMAL, type TEXT, area DECIMAL, complexity INTEGER, quantity INTEGER DEFAULT 1, notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 7. Painting Schemes CREATE TABLE IF NOT EXISTS gpi.painting_schemes ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE, name TEXT NOT NULL, type TEXT, coat TEXT, -- Primer, Intermediate, Finish solids_volume DECIMAL, yield_theoretical DECIMAL, eps_min DECIMAL, eps_max DECIMAL, dilution DECIMAL, manufacturer TEXT, color TEXT, notes TEXT, paint_id UUID REFERENCES gpi.technical_data_sheets(id), thinner_id UUID REFERENCES gpi.technical_data_sheets(id), color_hex TEXT, thinner_symbol TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 8. Application Records (Lotes de Pintura) CREATE TABLE IF NOT EXISTS gpi.application_records ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE, coat_stage TEXT, piece_description TEXT, date DATE, operator TEXT, real_weight DECIMAL, volume_used DECIMAL, area_painted DECIMAL, wet_thickness_avg DECIMAL, dry_thickness_calc DECIMAL, real_yield DECIMAL, method TEXT, diluent_used DECIMAL, notes TEXT, items JSONB DEFAULT '[]', -- List of {partId, quantity} created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 9. Instruments CREATE TABLE IF NOT EXISTS gpi.instruments ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE, name TEXT NOT NULL, serial_number TEXT, type TEXT, last_calibration DATE, calibration_due DATE, status TEXT DEFAULT 'active', notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 10. Stock Items CREATE TABLE IF NOT EXISTS gpi.stock_items ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE, data_sheet_id UUID REFERENCES gpi.technical_data_sheets(id), batch_number TEXT NOT NULL, manufacturing_date DATE, expiration_date DATE, initial_quantity DECIMAL NOT NULL, current_quantity DECIMAL NOT NULL, unit TEXT DEFAULT 'L', location TEXT, status TEXT DEFAULT 'active', notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 11. Inspections CREATE TABLE IF NOT EXISTS gpi.inspections ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE, application_record_id UUID REFERENCES gpi.application_records(id), stock_item_id UUID REFERENCES gpi.stock_items(id), instrument_id UUID REFERENCES gpi.instruments(id), type TEXT CHECK (type IN ('painting', 'surface_treatment')), date DATE, inspector TEXT, part_temperature DECIMAL, weight_kg DECIMAL, appearance TEXT, -- approved, rejected, notes defects TEXT, photos TEXT[] DEFAULT '{}', piece_description TEXT, eps_points DECIMAL[] DEFAULT '{}', adhesion_test TEXT, batch TEXT, treatment_executor TEXT, treatment_type TEXT, cleaning_degree TEXT, roughness_readings DECIMAL[] DEFAULT '{}', flash_rust TEXT, temperature DECIMAL, relative_humidity DECIMAL, period TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 12. Stock Movements CREATE TABLE IF NOT EXISTS gpi.stock_movements ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE, stock_item_id UUID REFERENCES gpi.stock_items(id) ON DELETE CASCADE, type TEXT CHECK (type IN ('in', 'out', 'adjust')), quantity DECIMAL NOT NULL, reason TEXT, project_id UUID REFERENCES gpi.projects(id), user_id TEXT, -- external_id created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 13. System Settings CREATE TABLE IF NOT EXISTS gpi.system_settings ( settings_id TEXT PRIMARY KEY DEFAULT 'global', app_name TEXT DEFAULT 'GPI', app_subtitle TEXT DEFAULT 'Gestão de Pintura Industrial', app_logo_url TEXT, updated_by TEXT, updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 14. Notifications CREATE TABLE IF NOT EXISTS gpi.notifications ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE, recipient_id UUID REFERENCES gpi.users(id) ON DELETE CASCADE, title TEXT NOT NULL, message TEXT NOT NULL, type TEXT CHECK (type IN ('info', 'warning', 'error', 'success')) DEFAULT 'info', is_read BOOLEAN DEFAULT FALSE, is_archived BOOLEAN DEFAULT FALSE, archived_by UUID[] DEFAULT '{}', deleted_by UUID[] DEFAULT '{}', metadata JSONB DEFAULT '{}', created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 15. Yield Studies CREATE TABLE IF NOT EXISTS gpi.yield_studies ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE, data_sheet_id UUID REFERENCES gpi.technical_data_sheets(id), name TEXT NOT NULL, target_dft DECIMAL NOT NULL, dilution_percent DECIMAL DEFAULT 0, categories JSONB DEFAULT '[]', total_weight DECIMAL, estimated_paint_volume DECIMAL, estimated_reducer_volume DECIMAL, estimated_paint_volume_by_area DECIMAL, estimated_reducer_volume_by_area DECIMAL, average_complexity DECIMAL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 16. Stock Audit Logs CREATE TABLE IF NOT EXISTS gpi.stock_audit_logs ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE, stock_item_id UUID REFERENCES gpi.stock_items(id) ON DELETE CASCADE, movement_id UUID, movement_number INTEGER, user_id TEXT, -- external_id user_name TEXT, action TEXT NOT NULL, details TEXT, old_values JSONB, new_values JSONB, timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 17. Messages CREATE TABLE IF NOT EXISTS gpi.messages ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE, from_user_id TEXT NOT NULL, -- external_id to_user_id TEXT NOT NULL, -- external_id message TEXT NOT NULL, is_read BOOLEAN DEFAULT FALSE, read_at TIMESTAMP WITH TIME ZONE, is_archived BOOLEAN DEFAULT FALSE, is_deleted_by_recipient BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 18. Geometry Types CREATE TABLE IF NOT EXISTS gpi.geometry_types ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE, name TEXT NOT NULL, efficiency_loss DECIMAL DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE (organization_id, name) ); -- 19. Stored Files CREATE TABLE IF NOT EXISTS gpi.stored_files ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE, filename TEXT NOT NULL, original_name TEXT, mimetype TEXT, size INTEGER, path TEXT, category TEXT, reference_id TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Indexes CREATE INDEX IF NOT EXISTS idx_projects_org ON gpi.projects(organization_id); CREATE INDEX IF NOT EXISTS idx_parts_project ON gpi.parts(project_id); CREATE INDEX IF NOT EXISTS idx_schemes_project ON gpi.painting_schemes(project_id); CREATE INDEX IF NOT EXISTS idx_records_project ON gpi.application_records(project_id); CREATE INDEX IF NOT EXISTS idx_inspections_project ON gpi.inspections(project_id); CREATE INDEX IF NOT EXISTS idx_stock_org ON gpi.stock_items(organization_id); CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON gpi.notifications(recipient_id); CREATE INDEX IF NOT EXISTS idx_messages_to ON gpi.messages(to_user_id, is_read); CREATE INDEX IF NOT EXISTS idx_geometry_types_org ON gpi.geometry_types(organization_id);