485 lines
19 KiB
MySQL
485 lines
19 KiB
MySQL
|
|
-- ============================================================================
|
||
|
|
-- Migration 025: Ticket System & Klippekort Modul (Isoleret)
|
||
|
|
-- ============================================================================
|
||
|
|
-- Dette modul er 100% isoleret og kan slettes uden at påvirke eksisterende data.
|
||
|
|
-- Alle tabeller har prefix 'tticket_' for at markere tilhørsforhold til modulet.
|
||
|
|
-- Ved uninstall køres DROP-scriptet i bunden af denne fil.
|
||
|
|
-- ============================================================================
|
||
|
|
|
||
|
|
-- Metadata tabel til at tracke modulets tilstand
|
||
|
|
CREATE TABLE IF NOT EXISTS tticket_metadata (
|
||
|
|
id SERIAL PRIMARY KEY,
|
||
|
|
module_version VARCHAR(20) NOT NULL DEFAULT '1.0.0',
|
||
|
|
installed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
installed_by INTEGER, -- Reference til users.user_id (read-only, ingen FK)
|
||
|
|
last_sync_at TIMESTAMP,
|
||
|
|
is_active BOOLEAN DEFAULT true,
|
||
|
|
settings JSONB DEFAULT '{}'::jsonb
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Indsæt initial metadata
|
||
|
|
INSERT INTO tticket_metadata (module_version, is_active)
|
||
|
|
VALUES ('1.0.0', true)
|
||
|
|
ON CONFLICT DO NOTHING;
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- TICKETS (hovedtabel)
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE IF NOT EXISTS tticket_tickets (
|
||
|
|
id SERIAL PRIMARY KEY,
|
||
|
|
ticket_number VARCHAR(50) UNIQUE NOT NULL, -- Format: TKT-YYYYMMDD-XXX
|
||
|
|
|
||
|
|
-- Core felter
|
||
|
|
subject VARCHAR(500) NOT NULL,
|
||
|
|
description TEXT,
|
||
|
|
status VARCHAR(20) DEFAULT 'open' CHECK (status IN ('open', 'in_progress', 'waiting_customer', 'waiting_internal', 'resolved', 'closed')),
|
||
|
|
priority VARCHAR(20) DEFAULT 'normal' CHECK (priority IN ('low', 'normal', 'high', 'urgent')),
|
||
|
|
category VARCHAR(100), -- support, bug, feature_request, question, etc.
|
||
|
|
|
||
|
|
-- Relationer (read-only references - INGEN FK til core tables)
|
||
|
|
customer_id INTEGER, -- Reference til customers.id
|
||
|
|
contact_id INTEGER, -- Reference til contacts.id
|
||
|
|
assigned_to_user_id INTEGER, -- Reference til users.user_id
|
||
|
|
created_by_user_id INTEGER, -- Reference til users.user_id
|
||
|
|
|
||
|
|
-- Kilde
|
||
|
|
source VARCHAR(50) DEFAULT 'manual' CHECK (source IN ('email', 'portal', 'phone', 'manual', 'api')),
|
||
|
|
|
||
|
|
-- Metadata
|
||
|
|
tags TEXT[],
|
||
|
|
custom_fields JSONB,
|
||
|
|
|
||
|
|
-- Timestamps
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
updated_at TIMESTAMP,
|
||
|
|
first_response_at TIMESTAMP, -- For SLA tracking (future)
|
||
|
|
resolved_at TIMESTAMP,
|
||
|
|
closed_at TIMESTAMP
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_tticket_tickets_number ON tticket_tickets(ticket_number);
|
||
|
|
CREATE INDEX idx_tticket_tickets_customer ON tticket_tickets(customer_id);
|
||
|
|
CREATE INDEX idx_tticket_tickets_assigned ON tticket_tickets(assigned_to_user_id);
|
||
|
|
CREATE INDEX idx_tticket_tickets_status ON tticket_tickets(status);
|
||
|
|
CREATE INDEX idx_tticket_tickets_priority ON tticket_tickets(priority);
|
||
|
|
CREATE INDEX idx_tticket_tickets_created ON tticket_tickets(created_at DESC);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- COMMENTS (beskeder på tickets)
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE IF NOT EXISTS tticket_comments (
|
||
|
|
id SERIAL PRIMARY KEY,
|
||
|
|
ticket_id INTEGER NOT NULL REFERENCES tticket_tickets(id) ON DELETE CASCADE,
|
||
|
|
user_id INTEGER, -- Reference til users.user_id (read-only, ingen FK)
|
||
|
|
comment_text TEXT NOT NULL,
|
||
|
|
is_internal BOOLEAN DEFAULT false, -- Intern note vs. kunde-synlig
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
updated_at TIMESTAMP
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_tticket_comments_ticket ON tticket_comments(ticket_id);
|
||
|
|
CREATE INDEX idx_tticket_comments_created ON tticket_comments(created_at);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- ATTACHMENTS (filer på tickets)
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE IF NOT EXISTS tticket_attachments (
|
||
|
|
id SERIAL PRIMARY KEY,
|
||
|
|
ticket_id INTEGER NOT NULL REFERENCES tticket_tickets(id) ON DELETE CASCADE,
|
||
|
|
file_name VARCHAR(255) NOT NULL,
|
||
|
|
file_path VARCHAR(500) NOT NULL,
|
||
|
|
file_size INTEGER,
|
||
|
|
mime_type VARCHAR(100),
|
||
|
|
uploaded_by_user_id INTEGER, -- Reference til users.user_id (read-only)
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_tticket_attachments_ticket ON tticket_attachments(ticket_id);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- WORKLOG (tidsregistrering på tickets)
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE IF NOT EXISTS tticket_worklog (
|
||
|
|
id SERIAL PRIMARY KEY,
|
||
|
|
ticket_id INTEGER NOT NULL REFERENCES tticket_tickets(id) ON DELETE CASCADE,
|
||
|
|
|
||
|
|
-- Arbejdsdata
|
||
|
|
work_date DATE NOT NULL,
|
||
|
|
hours DECIMAL(5,2) NOT NULL CHECK (hours > 0),
|
||
|
|
work_type VARCHAR(50) DEFAULT 'support' CHECK (work_type IN ('support', 'development', 'troubleshooting', 'on_site', 'meeting', 'other')),
|
||
|
|
description TEXT,
|
||
|
|
|
||
|
|
-- Fakturering
|
||
|
|
billing_method VARCHAR(20) DEFAULT 'invoice' CHECK (billing_method IN ('prepaid_card', 'invoice', 'internal', 'warranty')),
|
||
|
|
status VARCHAR(20) DEFAULT 'draft' CHECK (status IN ('draft', 'billable', 'billed', 'non_billable')),
|
||
|
|
prepaid_card_id INTEGER, -- Reference til tticket_prepaid_cards (read-only)
|
||
|
|
|
||
|
|
-- Metadata
|
||
|
|
user_id INTEGER, -- Reference til users.user_id (read-only)
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
updated_at TIMESTAMP,
|
||
|
|
billed_at TIMESTAMP
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_tticket_worklog_ticket ON tticket_worklog(ticket_id);
|
||
|
|
CREATE INDEX idx_tticket_worklog_status ON tticket_worklog(status);
|
||
|
|
CREATE INDEX idx_tticket_worklog_date ON tticket_worklog(work_date);
|
||
|
|
CREATE INDEX idx_tticket_worklog_card ON tticket_worklog(prepaid_card_id);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- PREPAID CARDS (klippekort - kun 1 aktivt per virksomhed)
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE IF NOT EXISTS tticket_prepaid_cards (
|
||
|
|
id SERIAL PRIMARY KEY,
|
||
|
|
card_number VARCHAR(50) UNIQUE NOT NULL, -- Format: CARD-YYYYMMDD-XXX
|
||
|
|
|
||
|
|
-- Tilknytning (kun 1 aktivt kort per customer)
|
||
|
|
customer_id INTEGER NOT NULL, -- Reference til customers.id (read-only, ingen FK)
|
||
|
|
|
||
|
|
-- Saldo
|
||
|
|
purchased_hours DECIMAL(8,2) NOT NULL CHECK (purchased_hours > 0),
|
||
|
|
used_hours DECIMAL(8,2) DEFAULT 0 CHECK (used_hours >= 0),
|
||
|
|
remaining_hours DECIMAL(8,2) GENERATED ALWAYS AS (purchased_hours - used_hours) STORED,
|
||
|
|
|
||
|
|
-- Priser
|
||
|
|
price_per_hour DECIMAL(10,2) NOT NULL,
|
||
|
|
total_amount DECIMAL(12,2) NOT NULL,
|
||
|
|
|
||
|
|
-- Lifecycle
|
||
|
|
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'depleted', 'expired', 'cancelled')),
|
||
|
|
purchased_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
expires_at TIMESTAMP, -- NULL = ingen udløb
|
||
|
|
|
||
|
|
-- e-conomic integration
|
||
|
|
economic_invoice_number VARCHAR(50),
|
||
|
|
economic_product_number VARCHAR(50),
|
||
|
|
|
||
|
|
-- Metadata
|
||
|
|
notes TEXT,
|
||
|
|
created_by_user_id INTEGER, -- Reference til users.user_id (read-only)
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
updated_at TIMESTAMP
|
||
|
|
);
|
||
|
|
|
||
|
|
-- CRITICAL: Kun 1 aktivt kort per customer
|
||
|
|
CREATE UNIQUE INDEX idx_tticket_prepaid_unique_active ON tticket_prepaid_cards(customer_id)
|
||
|
|
WHERE status = 'active';
|
||
|
|
|
||
|
|
CREATE INDEX idx_tticket_prepaid_customer ON tticket_prepaid_cards(customer_id);
|
||
|
|
CREATE INDEX idx_tticket_prepaid_status ON tticket_prepaid_cards(status);
|
||
|
|
CREATE INDEX idx_tticket_prepaid_expires ON tticket_prepaid_cards(expires_at);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- PREPAID TRANSACTIONS (immutable log af forbrug)
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE IF NOT EXISTS tticket_prepaid_transactions (
|
||
|
|
id SERIAL PRIMARY KEY,
|
||
|
|
card_id INTEGER NOT NULL REFERENCES tticket_prepaid_cards(id) ON DELETE CASCADE,
|
||
|
|
worklog_id INTEGER, -- Reference til tticket_worklog (read-only, NULL for purchases/top-ups)
|
||
|
|
|
||
|
|
-- Transaction type
|
||
|
|
transaction_type VARCHAR(20) NOT NULL CHECK (transaction_type IN ('purchase', 'top_up', 'usage', 'refund', 'expiration', 'cancellation')),
|
||
|
|
|
||
|
|
-- Beløb (positiv = tilføj, negativ = træk)
|
||
|
|
hours DECIMAL(8,2) NOT NULL,
|
||
|
|
balance_after DECIMAL(8,2) NOT NULL,
|
||
|
|
|
||
|
|
-- Beskrivelse
|
||
|
|
description TEXT,
|
||
|
|
|
||
|
|
-- Metadata (immutable)
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
created_by_user_id INTEGER -- Reference til users.user_id (read-only)
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_tticket_transactions_card ON tticket_prepaid_transactions(card_id);
|
||
|
|
CREATE INDEX idx_tticket_transactions_worklog ON tticket_prepaid_transactions(worklog_id);
|
||
|
|
CREATE INDEX idx_tticket_transactions_created ON tticket_prepaid_transactions(created_at DESC);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- EMAIL INTEGRATION LOG (email → ticket mapping)
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE IF NOT EXISTS tticket_email_log (
|
||
|
|
id SERIAL PRIMARY KEY,
|
||
|
|
ticket_id INTEGER REFERENCES tticket_tickets(id) ON DELETE CASCADE,
|
||
|
|
email_id INTEGER, -- Reference til email_messages.id (read-only, ingen FK)
|
||
|
|
email_message_id VARCHAR(500), -- Email Message-ID header for threading
|
||
|
|
action VARCHAR(50) NOT NULL, -- created|comment_added|updated|linked
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_tticket_email_log_ticket ON tticket_email_log(ticket_id);
|
||
|
|
CREATE INDEX idx_tticket_email_log_email ON tticket_email_log(email_id);
|
||
|
|
CREATE INDEX idx_tticket_email_log_message_id ON tticket_email_log(email_message_id);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- AUDIT LOG (alle ændringer)
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE IF NOT EXISTS tticket_audit_log (
|
||
|
|
id SERIAL PRIMARY KEY,
|
||
|
|
ticket_id INTEGER, -- NULL for system-level events
|
||
|
|
entity_type VARCHAR(50) NOT NULL, -- ticket, comment, worklog, prepaid_card, etc.
|
||
|
|
entity_id INTEGER,
|
||
|
|
user_id INTEGER, -- Reference til users.user_id (read-only)
|
||
|
|
action VARCHAR(50) NOT NULL, -- created, updated, deleted, status_changed, assigned, etc.
|
||
|
|
old_value TEXT,
|
||
|
|
new_value TEXT,
|
||
|
|
details JSONB,
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_tticket_audit_ticket ON tticket_audit_log(ticket_id);
|
||
|
|
CREATE INDEX idx_tticket_audit_entity ON tticket_audit_log(entity_type, entity_id);
|
||
|
|
CREATE INDEX idx_tticket_audit_created ON tticket_audit_log(created_at DESC);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- FUNCTIONS
|
||
|
|
-- ============================================================================
|
||
|
|
|
||
|
|
-- Auto-update timestamp function
|
||
|
|
CREATE OR REPLACE FUNCTION tticket_update_timestamp()
|
||
|
|
RETURNS TRIGGER AS $$
|
||
|
|
BEGIN
|
||
|
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
||
|
|
RETURN NEW;
|
||
|
|
END;
|
||
|
|
$$ LANGUAGE plpgsql;
|
||
|
|
|
||
|
|
-- Generate ticket number function
|
||
|
|
CREATE OR REPLACE FUNCTION tticket_generate_ticket_number()
|
||
|
|
RETURNS VARCHAR AS $$
|
||
|
|
DECLARE
|
||
|
|
today_str VARCHAR(8);
|
||
|
|
last_number INTEGER;
|
||
|
|
next_number INTEGER;
|
||
|
|
new_ticket_number VARCHAR(50);
|
||
|
|
BEGIN
|
||
|
|
-- Format: TKT-YYYYMMDD-XXX
|
||
|
|
today_str := TO_CHAR(CURRENT_DATE, 'YYYYMMDD');
|
||
|
|
|
||
|
|
-- Find last ticket number for today
|
||
|
|
SELECT COALESCE(
|
||
|
|
MAX(CAST(SPLIT_PART(ticket_number, '-', 3) AS INTEGER)),
|
||
|
|
0
|
||
|
|
) INTO last_number
|
||
|
|
FROM tticket_tickets
|
||
|
|
WHERE ticket_number LIKE 'TKT-' || today_str || '-%';
|
||
|
|
|
||
|
|
next_number := last_number + 1;
|
||
|
|
new_ticket_number := 'TKT-' || today_str || '-' || LPAD(next_number::TEXT, 3, '0');
|
||
|
|
|
||
|
|
RETURN new_ticket_number;
|
||
|
|
END;
|
||
|
|
$$ LANGUAGE plpgsql;
|
||
|
|
|
||
|
|
-- Generate prepaid card number function
|
||
|
|
CREATE OR REPLACE FUNCTION tticket_generate_card_number()
|
||
|
|
RETURNS VARCHAR AS $$
|
||
|
|
DECLARE
|
||
|
|
today_str VARCHAR(8);
|
||
|
|
last_number INTEGER;
|
||
|
|
next_number INTEGER;
|
||
|
|
new_card_number VARCHAR(50);
|
||
|
|
BEGIN
|
||
|
|
-- Format: CARD-YYYYMMDD-XXX
|
||
|
|
today_str := TO_CHAR(CURRENT_DATE, 'YYYYMMDD');
|
||
|
|
|
||
|
|
-- Find last card number for today
|
||
|
|
SELECT COALESCE(
|
||
|
|
MAX(CAST(SPLIT_PART(card_number, '-', 3) AS INTEGER)),
|
||
|
|
0
|
||
|
|
) INTO last_number
|
||
|
|
FROM tticket_prepaid_cards
|
||
|
|
WHERE card_number LIKE 'CARD-' || today_str || '-%';
|
||
|
|
|
||
|
|
next_number := last_number + 1;
|
||
|
|
new_card_number := 'CARD-' || today_str || '-' || LPAD(next_number::TEXT, 3, '0');
|
||
|
|
|
||
|
|
RETURN new_card_number;
|
||
|
|
END;
|
||
|
|
$$ LANGUAGE plpgsql;
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- TRIGGERS
|
||
|
|
-- ============================================================================
|
||
|
|
|
||
|
|
-- Auto-update timestamps
|
||
|
|
CREATE TRIGGER tticket_tickets_update
|
||
|
|
BEFORE UPDATE ON tticket_tickets
|
||
|
|
FOR EACH ROW EXECUTE FUNCTION tticket_update_timestamp();
|
||
|
|
|
||
|
|
CREATE TRIGGER tticket_comments_update
|
||
|
|
BEFORE UPDATE ON tticket_comments
|
||
|
|
FOR EACH ROW EXECUTE FUNCTION tticket_update_timestamp();
|
||
|
|
|
||
|
|
CREATE TRIGGER tticket_worklog_update
|
||
|
|
BEFORE UPDATE ON tticket_worklog
|
||
|
|
FOR EACH ROW EXECUTE FUNCTION tticket_update_timestamp();
|
||
|
|
|
||
|
|
CREATE TRIGGER tticket_prepaid_cards_update
|
||
|
|
BEFORE UPDATE ON tticket_prepaid_cards
|
||
|
|
FOR EACH ROW EXECUTE FUNCTION tticket_update_timestamp();
|
||
|
|
|
||
|
|
-- Auto-generate ticket number if not provided
|
||
|
|
CREATE OR REPLACE FUNCTION tticket_auto_generate_ticket_number()
|
||
|
|
RETURNS TRIGGER AS $$
|
||
|
|
BEGIN
|
||
|
|
IF NEW.ticket_number IS NULL OR NEW.ticket_number = '' THEN
|
||
|
|
NEW.ticket_number := tticket_generate_ticket_number();
|
||
|
|
END IF;
|
||
|
|
RETURN NEW;
|
||
|
|
END;
|
||
|
|
$$ LANGUAGE plpgsql;
|
||
|
|
|
||
|
|
CREATE TRIGGER tticket_tickets_generate_number
|
||
|
|
BEFORE INSERT ON tticket_tickets
|
||
|
|
FOR EACH ROW EXECUTE FUNCTION tticket_auto_generate_ticket_number();
|
||
|
|
|
||
|
|
-- Auto-generate card number if not provided
|
||
|
|
CREATE OR REPLACE FUNCTION tticket_auto_generate_card_number()
|
||
|
|
RETURNS TRIGGER AS $$
|
||
|
|
BEGIN
|
||
|
|
IF NEW.card_number IS NULL OR NEW.card_number = '' THEN
|
||
|
|
NEW.card_number := tticket_generate_card_number();
|
||
|
|
END IF;
|
||
|
|
RETURN NEW;
|
||
|
|
END;
|
||
|
|
$$ LANGUAGE plpgsql;
|
||
|
|
|
||
|
|
CREATE TRIGGER tticket_prepaid_cards_generate_number
|
||
|
|
BEFORE INSERT ON tticket_prepaid_cards
|
||
|
|
FOR EACH ROW EXECUTE FUNCTION tticket_auto_generate_card_number();
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- VIEWS (common queries)
|
||
|
|
-- ============================================================================
|
||
|
|
|
||
|
|
-- View: Open tickets with statistics
|
||
|
|
CREATE OR REPLACE VIEW tticket_open_tickets AS
|
||
|
|
SELECT
|
||
|
|
t.*,
|
||
|
|
COUNT(DISTINCT c.id) AS comment_count,
|
||
|
|
COUNT(DISTINCT a.id) AS attachment_count,
|
||
|
|
SUM(w.hours) FILTER (WHERE w.status IN ('draft', 'billable')) AS pending_hours,
|
||
|
|
SUM(w.hours) FILTER (WHERE w.status = 'billed') AS billed_hours,
|
||
|
|
MAX(c.created_at) AS last_comment_at,
|
||
|
|
EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - t.created_at))/3600 AS age_hours
|
||
|
|
FROM tticket_tickets t
|
||
|
|
LEFT JOIN tticket_comments c ON t.id = c.ticket_id
|
||
|
|
LEFT JOIN tticket_attachments a ON t.id = a.ticket_id
|
||
|
|
LEFT JOIN tticket_worklog w ON t.id = w.ticket_id
|
||
|
|
WHERE t.status IN ('open', 'in_progress', 'waiting_customer', 'waiting_internal')
|
||
|
|
GROUP BY t.id;
|
||
|
|
|
||
|
|
-- View: Worklog entries ready for billing
|
||
|
|
CREATE OR REPLACE VIEW tticket_billable_worklog AS
|
||
|
|
SELECT
|
||
|
|
w.*,
|
||
|
|
t.ticket_number,
|
||
|
|
t.subject AS ticket_subject,
|
||
|
|
t.customer_id,
|
||
|
|
t.status AS ticket_status,
|
||
|
|
CASE
|
||
|
|
WHEN w.billing_method = 'prepaid_card' THEN pc.remaining_hours >= w.hours
|
||
|
|
ELSE true
|
||
|
|
END AS has_sufficient_balance
|
||
|
|
FROM tticket_worklog w
|
||
|
|
JOIN tticket_tickets t ON w.ticket_id = t.id
|
||
|
|
LEFT JOIN tticket_prepaid_cards pc ON w.prepaid_card_id = pc.id
|
||
|
|
WHERE w.status = 'billable';
|
||
|
|
|
||
|
|
-- View: Prepaid card balances
|
||
|
|
CREATE OR REPLACE VIEW tticket_prepaid_balances AS
|
||
|
|
SELECT
|
||
|
|
pc.*,
|
||
|
|
COUNT(w.id) AS usage_count,
|
||
|
|
SUM(w.hours) AS total_hours_used,
|
||
|
|
COUNT(w.id) FILTER (WHERE w.status = 'billed') AS billed_usage_count
|
||
|
|
FROM tticket_prepaid_cards pc
|
||
|
|
LEFT JOIN tticket_worklog w ON pc.id = w.prepaid_card_id
|
||
|
|
GROUP BY pc.id;
|
||
|
|
|
||
|
|
-- View: Ticket statistics by status
|
||
|
|
CREATE OR REPLACE VIEW tticket_stats_by_status AS
|
||
|
|
SELECT
|
||
|
|
status,
|
||
|
|
priority,
|
||
|
|
COUNT(*) AS ticket_count,
|
||
|
|
AVG(EXTRACT(EPOCH FROM (COALESCE(resolved_at, CURRENT_TIMESTAMP) - created_at))/3600) AS avg_age_hours
|
||
|
|
FROM tticket_tickets
|
||
|
|
GROUP BY status, priority;
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- INITIAL DATA
|
||
|
|
-- ============================================================================
|
||
|
|
|
||
|
|
-- Log installation in audit log
|
||
|
|
INSERT INTO tticket_audit_log (entity_type, action, details)
|
||
|
|
VALUES (
|
||
|
|
'system',
|
||
|
|
'module_installed',
|
||
|
|
jsonb_build_object(
|
||
|
|
'version', '1.0.0',
|
||
|
|
'timestamp', CURRENT_TIMESTAMP,
|
||
|
|
'tables_created', ARRAY[
|
||
|
|
'tticket_metadata', 'tticket_tickets', 'tticket_comments',
|
||
|
|
'tticket_attachments', 'tticket_worklog', 'tticket_prepaid_cards',
|
||
|
|
'tticket_prepaid_transactions', 'tticket_email_log', 'tticket_audit_log'
|
||
|
|
]
|
||
|
|
)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- UNINSTALL SCRIPT (bruges ved modul-sletning)
|
||
|
|
-- ============================================================================
|
||
|
|
-- ADVARSEL: Dette script sletter ALLE data i modulet!
|
||
|
|
-- Kør kun hvis modulet skal fjernes fuldstændigt.
|
||
|
|
--
|
||
|
|
-- For at uninstalle, kør følgende kommandoer i rækkefølge:
|
||
|
|
--
|
||
|
|
-- -- Drop views
|
||
|
|
-- DROP VIEW IF EXISTS tticket_stats_by_status CASCADE;
|
||
|
|
-- DROP VIEW IF EXISTS tticket_prepaid_balances CASCADE;
|
||
|
|
-- DROP VIEW IF EXISTS tticket_billable_worklog CASCADE;
|
||
|
|
-- DROP VIEW IF EXISTS tticket_open_tickets CASCADE;
|
||
|
|
--
|
||
|
|
-- -- Drop triggers
|
||
|
|
-- DROP TRIGGER IF EXISTS tticket_prepaid_cards_generate_number ON tticket_prepaid_cards;
|
||
|
|
-- DROP TRIGGER IF EXISTS tticket_tickets_generate_number ON tticket_tickets;
|
||
|
|
-- DROP TRIGGER IF EXISTS tticket_prepaid_cards_update ON tticket_prepaid_cards;
|
||
|
|
-- DROP TRIGGER IF EXISTS tticket_worklog_update ON tticket_worklog;
|
||
|
|
-- DROP TRIGGER IF EXISTS tticket_comments_update ON tticket_comments;
|
||
|
|
-- DROP TRIGGER IF EXISTS tticket_tickets_update ON tticket_tickets;
|
||
|
|
--
|
||
|
|
-- -- Drop functions
|
||
|
|
-- DROP FUNCTION IF EXISTS tticket_generate_card_number() CASCADE;
|
||
|
|
-- DROP FUNCTION IF EXISTS tticket_generate_ticket_number() CASCADE;
|
||
|
|
-- DROP FUNCTION IF EXISTS tticket_auto_generate_card_number() CASCADE;
|
||
|
|
-- DROP FUNCTION IF EXISTS tticket_auto_generate_ticket_number() CASCADE;
|
||
|
|
-- DROP FUNCTION IF EXISTS tticket_update_timestamp() CASCADE;
|
||
|
|
--
|
||
|
|
-- -- Drop tables (reverse dependency order)
|
||
|
|
-- DROP TABLE IF EXISTS tticket_audit_log CASCADE;
|
||
|
|
-- DROP TABLE IF EXISTS tticket_email_log CASCADE;
|
||
|
|
-- DROP TABLE IF EXISTS tticket_prepaid_transactions CASCADE;
|
||
|
|
-- DROP TABLE IF EXISTS tticket_prepaid_cards CASCADE;
|
||
|
|
-- DROP TABLE IF EXISTS tticket_worklog CASCADE;
|
||
|
|
-- DROP TABLE IF EXISTS tticket_attachments CASCADE;
|
||
|
|
-- DROP TABLE IF EXISTS tticket_comments CASCADE;
|
||
|
|
-- DROP TABLE IF EXISTS tticket_tickets CASCADE;
|
||
|
|
-- DROP TABLE IF EXISTS tticket_metadata CASCADE;
|
||
|
|
--
|
||
|
|
-- -- Log uninstall
|
||
|
|
-- -- (Dette vil fejle hvis tticket_audit_log er droppet, men det er OK)
|
||
|
|
-- DO $$
|
||
|
|
-- BEGIN
|
||
|
|
-- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'tticket_audit_log') THEN
|
||
|
|
-- INSERT INTO tticket_audit_log (entity_type, action, details)
|
||
|
|
-- VALUES ('system', 'module_uninstalled', jsonb_build_object('timestamp', CURRENT_TIMESTAMP));
|
||
|
|
-- END IF;
|
||
|
|
-- EXCEPTION WHEN OTHERS THEN
|
||
|
|
-- NULL; -- Ignorer fejl
|
||
|
|
-- END $$;
|
||
|
|
--
|
||
|
|
-- ============================================================================
|