-- Customer Project System Database Schema -- Opretter tabeller for det nye strukturerede tilbuds-flow -- Kunde projekter CREATE TABLE IF NOT EXISTS customer_projects ( id INT AUTO_INCREMENT PRIMARY KEY, project_name VARCHAR(255) NOT NULL, customer_name VARCHAR(255) NOT NULL, customer_number VARCHAR(50), customer_email VARCHAR(255), customer_phone VARCHAR(50), customer_address TEXT, project_description TEXT, selected_packages JSON, suggestion_snapshot JSON, input_normalization_log JSON, project_status ENUM( 'draft', 'geometry_complete', 'smart_package_complete', 'review_pending', 'ready_for_ordrestyring', 'sent_to_ordrestyring', 'accepted', 'rejected', 'geometry_pending', 'labor_pending', 'materials_pending', 'calculation_ready', 'quote_generated', 'sent' ) DEFAULT 'draft', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_status (project_status), INDEX idx_customer (customer_name), INDEX idx_customer_number (customer_number), INDEX idx_created (created_at) ); -- Tag geometri specifikt for tag arbejde CREATE TABLE IF NOT EXISTS roof_geometry ( id INT AUTO_INCREMENT PRIMARY KEY, project_id INT NOT NULL, roof_type ENUM('gable', 'pult', 'flat', 'fladt_tag', 'skraat_tag', 'mansard', 'komplekst') NOT NULL, roof_material ENUM('tegl','tagpap','eternit','betontag','staal','andet') NULL, total_area DECIMAL(10,2) NOT NULL COMMENT 'Samlet areal i m²', roof_covering_area DECIMAL(10,2) NULL COMMENT 'Faktisk tagbeklædningsareal i m²', roof_pitch DECIMAL(5,2) COMMENT 'Taghældning i grader', roof_height DECIMAL(8,2) COMMENT 'Taghøjde i meter', wall_height DECIMAL(8,2) NULL COMMENT 'Væghøjde i meter', complexity_factor DECIMAL(3,2) DEFAULT 1.0 COMMENT 'Kompleksitetsfaktor 1.0-2.0', -- Detaljerede målinger length_main DECIMAL(8,2) COMMENT 'Hovedlængde i meter', width_main DECIMAL(8,2) COMMENT 'Hovedbredde i meter', -- Specielle forhold has_dormers BOOLEAN DEFAULT FALSE COMMENT 'Har kviste', has_chimneys BOOLEAN DEFAULT FALSE COMMENT 'Har skorstene', has_skylights BOOLEAN DEFAULT FALSE COMMENT 'Har ovenlys', access_difficulty ENUM('let', 'medium', 'svær') DEFAULT 'medium', -- Beregnet data estimated_work_hours DECIMAL(8,2) COMMENT 'Estimerede arbejdstimer', estimated_carpenters INT COMMENT 'Anbefalede antal tømrere', geometry_json JSON NULL, edges_json JSON NULL, planes_json JSON NULL, openings_json JSON NULL, provenance_json JSON NULL, replacement_scope VARCHAR(80) NULL, notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES customer_projects(id) ON DELETE CASCADE, UNIQUE INDEX unique_project (project_id), INDEX idx_project (project_id), INDEX idx_roof_type (roof_type), INDEX idx_roof_geometry_material (roof_material) ); -- Arbejdstimer og tømrere CREATE TABLE IF NOT EXISTS project_labor ( id INT AUTO_INCREMENT PRIMARY KEY, project_id INT NOT NULL, carpenter_count INT NOT NULL DEFAULT 1, estimated_hours_per_carpenter DECIMAL(8,2) NOT NULL, total_work_hours DECIMAL(8,2) NOT NULL, hourly_rate DECIMAL(8,2) NOT NULL DEFAULT 580.00 COMMENT 'Fast pris 580 kr/time', total_labor_cost DECIMAL(12,2) NOT NULL, -- Detaljer om arbejdsopgaver work_breakdown JSON COMMENT 'Detaljeret opdeling af arbejdsopgaver', notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES customer_projects(id) ON DELETE CASCADE, UNIQUE KEY unique_project_labor (project_id) ); -- Existing installs may contain duplicates from before saveProjectLabor used upsert. -- Keep the newest row, verify the cleanup, then enforce the one-row contract. DELETE stale FROM project_labor stale JOIN project_labor newest ON newest.project_id = stale.project_id AND newest.id > stale.id; SELECT project_id, COUNT(*) AS remaining_rows FROM project_labor GROUP BY project_id HAVING COUNT(*) > 1; CREATE UNIQUE INDEX IF NOT EXISTS unique_project_labor ON project_labor (project_id); -- Versioneret Smart Pakke-builder: klodser, rækkefølge, geometri og overrides. CREATE TABLE IF NOT EXISTS project_smart_package_workspaces ( id INT AUTO_INCREMENT PRIMARY KEY, project_id INT NOT NULL, version INT NOT NULL DEFAULT 0, workspace_json JSON NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY unique_project_workspace (project_id), CONSTRAINT fk_smart_package_workspace_project FOREIGN KEY (project_id) REFERENCES customer_projects(id) ON DELETE CASCADE ); -- Projekt materialer CREATE TABLE IF NOT EXISTS project_materials ( id INT AUTO_INCREMENT PRIMARY KEY, project_id INT NOT NULL, package_instance_id VARCHAR(100) NULL, source_package_id INT NULL, material_id INT NULL, material_name VARCHAR(255) NOT NULL, material_category VARCHAR(100), quantity DECIMAL(10,3) NOT NULL, unit VARCHAR(50) NOT NULL, unit_price DECIMAL(10,2) NOT NULL, total_price DECIMAL(12,2) NOT NULL, supplier VARCHAR(255), material_source ENUM('manual', 'database', 'bygma_api', 'package') DEFAULT 'manual', price_source VARCHAR(120) NULL, price_source_updated_at DATETIME NULL, notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES customer_projects(id) ON DELETE CASCADE, INDEX idx_project (project_id), INDEX idx_category (material_category), INDEX idx_project_materials_package_instance (project_id, package_instance_id), INDEX idx_project_materials_material_id (material_id) ); -- Projektudlejning og referenceydelser med Smart Pakke-ejerskab. CREATE TABLE IF NOT EXISTS project_rentals ( id INT AUTO_INCREMENT PRIMARY KEY, project_id INT NOT NULL, package_instance_id VARCHAR(100) NULL, source_package_id INT NULL, rental_name VARCHAR(255) NOT NULL, rental_category VARCHAR(100), quantity DECIMAL(10,3) NOT NULL, unit VARCHAR(50) NOT NULL, unit_price DECIMAL(10,2) NOT NULL, total_price DECIMAL(12,2) NOT NULL, supplier VARCHAR(255), rental_source ENUM('manual', 'database', 'import') DEFAULT 'manual', notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES customer_projects(id) ON DELETE CASCADE, INDEX idx_project (project_id), INDEX idx_category (rental_category), INDEX idx_project_rentals_package_instance (project_id, package_instance_id) ); -- Idempotente opgraderinger når bootstrap køres mod eksisterende tabeller. ALTER TABLE project_materials ADD COLUMN IF NOT EXISTS package_instance_id VARCHAR(100) NULL AFTER project_id, ADD COLUMN IF NOT EXISTS source_package_id INT NULL AFTER package_instance_id, ADD COLUMN IF NOT EXISTS material_id INT NULL AFTER source_package_id, ADD COLUMN IF NOT EXISTS price_source VARCHAR(120) NULL AFTER material_source, ADD COLUMN IF NOT EXISTS price_source_updated_at DATETIME NULL AFTER price_source, MODIFY COLUMN material_source ENUM('manual', 'database', 'bygma_api', 'package') DEFAULT 'manual'; ALTER TABLE project_rentals ADD COLUMN IF NOT EXISTS package_instance_id VARCHAR(100) NULL AFTER project_id, ADD COLUMN IF NOT EXISTS source_package_id INT NULL AFTER package_instance_id; ALTER TABLE material_packages ADD COLUMN IF NOT EXISTS geometry_basis VARCHAR(40) NULL AFTER unit, ADD COLUMN IF NOT EXISTS geometry_factor DECIMAL(10,3) NULL AFTER geometry_basis, ADD COLUMN IF NOT EXISTS default_count DECIMAL(10,3) NULL AFTER geometry_factor, ADD COLUMN IF NOT EXISTS default_quantity DECIMAL(10,3) NULL AFTER default_count, ADD COLUMN IF NOT EXISTS replacement_scope VARCHAR(40) NULL AFTER default_quantity, ADD COLUMN IF NOT EXISTS compatible_roof_materials JSON NULL AFTER replacement_scope, ADD COLUMN IF NOT EXISTS allowed_roof_forms JSON NULL AFTER compatible_roof_materials, ADD COLUMN IF NOT EXISTS min_pitch_degrees DECIMAL(5,2) NULL AFTER allowed_roof_forms, ADD COLUMN IF NOT EXISTS max_pitch_degrees DECIMAL(5,2) NULL AFTER min_pitch_degrees; ALTER TABLE roof_geometry ADD COLUMN IF NOT EXISTS roof_covering_area DECIMAL(10,2) NULL AFTER total_area, ADD COLUMN IF NOT EXISTS wall_height DECIMAL(8,2) NULL AFTER roof_height, ADD COLUMN IF NOT EXISTS geometry_json JSON NULL, ADD COLUMN IF NOT EXISTS edges_json JSON NULL, ADD COLUMN IF NOT EXISTS planes_json JSON NULL, ADD COLUMN IF NOT EXISTS openings_json JSON NULL, ADD COLUMN IF NOT EXISTS provenance_json JSON NULL, ADD COLUMN IF NOT EXISTS replacement_scope VARCHAR(80) NULL, MODIFY COLUMN roof_type ENUM('gable', 'pult', 'flat', 'fladt_tag', 'skraat_tag', 'mansard', 'komplekst') NOT NULL; -- Projekt beregninger (råtilbud) CREATE TABLE IF NOT EXISTS project_calculations ( id INT AUTO_INCREMENT PRIMARY KEY, project_id INT NOT NULL, -- Geometri opsummering total_area DECIMAL(10,2), complexity_factor DECIMAL(3,2), -- Arbejdstimer opsummering total_work_hours DECIMAL(8,2), carpenter_count INT, total_labor_cost DECIMAL(12,2), -- Materialer opsummering total_material_cost DECIMAL(12,2), material_count INT, -- Totaler subtotal DECIMAL(12,2), overhead_percentage DECIMAL(5,2) DEFAULT 15.00, overhead_amount DECIMAL(12,2), profit_percentage DECIMAL(5,2) DEFAULT 20.00, profit_amount DECIMAL(12,2), total_excl_vat DECIMAL(12,2), vat_percentage DECIMAL(5,2) DEFAULT 25.00, vat_amount DECIMAL(12,2), total_incl_vat DECIMAL(12,2), -- Metadata calculation_data JSON COMMENT 'Detaljerede beregningsdata', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES customer_projects(id) ON DELETE CASCADE, INDEX idx_project (project_id) ); -- Genererede tilbud CREATE TABLE IF NOT EXISTS generated_quotes ( id INT AUTO_INCREMENT PRIMARY KEY, project_id INT NOT NULL, calculation_id INT NOT NULL, quote_text TEXT NOT NULL COMMENT 'AI-genereret tilbudstekst', quote_format ENUM('text', 'html', 'pdf') DEFAULT 'html', -- AI usage tracking ai_tokens_used INT, ai_cost DECIMAL(8,4), ai_model VARCHAR(50), quote_status ENUM('draft', 'approved', 'sent', 'accepted', 'rejected') DEFAULT 'draft', sent_at TIMESTAMP NULL, valid_until DATE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES customer_projects(id) ON DELETE CASCADE, FOREIGN KEY (calculation_id) REFERENCES project_calculations(id) ON DELETE CASCADE, INDEX idx_project (project_id), INDEX idx_status (quote_status) ); CREATE TABLE IF NOT EXISTS ordrestyring_case_features ( case_number VARCHAR(64) PRIMARY KEY, customer_number VARCHAR(64), customer_name VARCHAR(255), description TEXT, remarks TEXT, search_text MEDIUMTEXT, inferred_roof_type VARCHAR(100), inferred_package_id VARCHAR(100), total_hours DECIMAL(10,2) DEFAULT 0, hour_entries INT DEFAULT 0, material_lines_count INT DEFAULT 0, material_total DECIMAL(12,2) DEFAULT 0, created_at_ordrestyring DATETIME NULL, last_activity_at_ordrestyring DATETIME NULL, synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_case_features_customer_number (customer_number), INDEX idx_case_features_customer_name (customer_name), INDEX idx_case_features_roof_type (inferred_roof_type), INDEX idx_case_features_package (inferred_package_id), INDEX idx_case_features_created_at (created_at_ordrestyring), INDEX idx_case_features_last_activity (last_activity_at_ordrestyring) ); CREATE TABLE IF NOT EXISTS ordrestyring_case_material_snapshots ( id BIGINT AUTO_INCREMENT PRIMARY KEY, case_number VARCHAR(64) NOT NULL, line_id INT NOT NULL, product_number VARCHAR(128), product_text TEXT, supplier VARCHAR(255), quantity DECIMAL(12,3) DEFAULT 0, cost_price DECIMAL(12,2) DEFAULT 0, sales_price DECIMAL(12,2) DEFAULT 0, created_at_ordrestyring DATETIME NULL, synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uniq_ordrestyring_case_material_line (case_number, line_id), INDEX idx_ordrestyring_case_material_case_number (case_number), INDEX idx_ordrestyring_case_material_supplier (supplier) ); CREATE TABLE IF NOT EXISTS ordrestyring_offer_snapshots ( offer_id INT PRIMARY KEY, offer_number VARCHAR(64), customer_name VARCHAR(255), customer_email VARCHAR(255), description TEXT, status_text VARCHAR(255), search_text MEDIUMTEXT, total_sales_price DECIMAL(12,2) DEFAULT 0, total_sales_price_with_vat DECIMAL(12,2) DEFAULT 0, created_at_ordrestyring DATETIME NULL, synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_offer_snapshots_customer_name (customer_name), INDEX idx_offer_snapshots_status (status_text), INDEX idx_offer_snapshots_created_at (created_at_ordrestyring) ); CREATE TABLE IF NOT EXISTS ordrestyring_offer_line_snapshots ( id BIGINT AUTO_INCREMENT PRIMARY KEY, offer_id INT NOT NULL, line_id INT NOT NULL, description TEXT, quantity DECIMAL(12,3) DEFAULT 0, unit VARCHAR(50), unit_price DECIMAL(12,2) DEFAULT 0, line_total DECIMAL(12,2) DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uniq_ordrestyring_offer_line (offer_id, line_id), INDEX idx_ordrestyring_offer_line_offer_id (offer_id), CONSTRAINT fk_ordrestyring_offer_line_offer FOREIGN KEY (offer_id) REFERENCES ordrestyring_offer_snapshots(offer_id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS ordrestyring_sync_metadata ( id INT AUTO_INCREMENT PRIMARY KEY, project_id INT NULL, case_number VARCHAR(64) NULL, offer_id INT NULL, offer_number VARCHAR(64) NULL, last_status VARCHAR(128) NULL, last_status_at DATETIME NULL, last_price_list_synced_at DATETIME NULL, last_material_sync_at DATETIME NULL, notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES customer_projects(id) ON DELETE SET NULL, INDEX idx_ordrestyring_metadata_project (project_id), INDEX idx_ordrestyring_metadata_case (case_number), UNIQUE KEY uniq_ordrestyring_metadata_case (case_number), UNIQUE KEY uniq_ordrestyring_metadata_offer (offer_id) ); -- Bygma API cache (for version 2) CREATE TABLE IF NOT EXISTS bygma_materials_cache ( id INT AUTO_INCREMENT PRIMARY KEY, product_id VARCHAR(100) UNIQUE, product_name VARCHAR(255), category VARCHAR(100), subcategory VARCHAR(100), unit VARCHAR(50), price DECIMAL(10,2), stock_status VARCHAR(50), supplier_info JSON, last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_category (category), INDEX idx_name (product_name), INDEX idx_updated (last_updated) ); -- Stark API cache (samme struktur som Bygma) CREATE TABLE IF NOT EXISTS stark_materials_cache ( id INT AUTO_INCREMENT PRIMARY KEY, product_id VARCHAR(100) UNIQUE, product_name VARCHAR(255), category VARCHAR(100), subcategory VARCHAR(100), unit VARCHAR(50), price DECIMAL(10,2), stock_status VARCHAR(50), supplier_info JSON, last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_category (category), INDEX idx_name (product_name), INDEX idx_updated (last_updated) );