bmc_hub/migrations/026_ticket_enhancements.sql

453 lines
18 KiB
MySQL
Raw Normal View History

-- ============================================================================
-- Migration 026: Ticket System Enhancements - Kravspecifikation Implementation
-- ============================================================================
-- Implementerer:
-- 1. Ticket relations (merge, split, parent/child hierarchy)
-- 2. Calendar events og deadlines
-- 3. Templates system
-- 4. AI suggestions (metadata only - ingen automatik)
-- 5. Enhanced contact identification
-- ============================================================================
-- ============================================================================
-- TICKET RELATIONS (flette, splitte, hierarki)
-- ============================================================================
CREATE TABLE IF NOT EXISTS tticket_relations (
id SERIAL PRIMARY KEY,
ticket_id INTEGER NOT NULL REFERENCES tticket_tickets(id) ON DELETE CASCADE,
related_ticket_id INTEGER NOT NULL REFERENCES tticket_tickets(id) ON DELETE CASCADE,
relation_type VARCHAR(20) NOT NULL CHECK (relation_type IN ('merged_into', 'split_from', 'parent_of', 'child_of', 'related_to')),
-- Metadata om relationen
created_by_user_id INTEGER, -- Reference til users.user_id (read-only)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
reason TEXT, -- Hvorfor blev relationen oprettet
CONSTRAINT unique_relation UNIQUE (ticket_id, related_ticket_id, relation_type),
CONSTRAINT no_self_reference CHECK (ticket_id != related_ticket_id)
);
CREATE INDEX IF NOT EXISTS idx_tticket_relations_ticket ON tticket_relations(ticket_id);
CREATE INDEX IF NOT EXISTS idx_tticket_relations_related ON tticket_relations(related_ticket_id);
CREATE INDEX IF NOT EXISTS idx_tticket_relations_type ON tticket_relations(relation_type);
-- View for at finde alle relationer for en ticket (begge retninger)
CREATE OR REPLACE VIEW tticket_all_relations AS
SELECT
ticket_id,
related_ticket_id,
relation_type,
created_by_user_id,
created_at,
reason
FROM tticket_relations
UNION ALL
SELECT
related_ticket_id as ticket_id,
ticket_id as related_ticket_id,
CASE
WHEN relation_type = 'parent_of' THEN 'child_of'
WHEN relation_type = 'child_of' THEN 'parent_of'
WHEN relation_type = 'merged_into' THEN 'merged_from'
WHEN relation_type = 'split_from' THEN 'split_into'
ELSE relation_type
END as relation_type,
created_by_user_id,
created_at,
reason
FROM tticket_relations;
-- ============================================================================
-- CALENDAR EVENTS (aftaler, deadlines, milepæle)
-- ============================================================================
CREATE TABLE IF NOT EXISTS tticket_calendar_events (
id SERIAL PRIMARY KEY,
ticket_id INTEGER NOT NULL REFERENCES tticket_tickets(id) ON DELETE CASCADE,
-- Event data
title VARCHAR(200) NOT NULL,
description TEXT,
event_type VARCHAR(20) DEFAULT 'appointment' CHECK (event_type IN ('appointment', 'deadline', 'milestone', 'reminder', 'follow_up')),
-- Tidspunkt
event_date DATE NOT NULL,
event_time TIME,
duration_minutes INTEGER, -- Varighed i minutter
all_day BOOLEAN DEFAULT false,
-- AI forslag
suggested_by_ai BOOLEAN DEFAULT false, -- Blev denne foreslået af AI?
ai_confidence DECIMAL(3,2), -- AI confidence score 0-1
ai_source_text TEXT, -- Tekst som AI fandt datoen i
-- Status
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'completed', 'cancelled')),
-- Metadata
created_by_user_id INTEGER, -- Reference til users.user_id
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP,
completed_at TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_tticket_calendar_ticket ON tticket_calendar_events(ticket_id);
CREATE INDEX IF NOT EXISTS idx_tticket_calendar_date ON tticket_calendar_events(event_date);
CREATE INDEX IF NOT EXISTS idx_tticket_calendar_type ON tticket_calendar_events(event_type);
CREATE INDEX IF NOT EXISTS idx_tticket_calendar_status ON tticket_calendar_events(status);
-- ============================================================================
-- TEMPLATES (svarskabeloner, guides, standardbreve)
-- ============================================================================
CREATE TABLE IF NOT EXISTS tticket_templates (
id SERIAL PRIMARY KEY,
-- Template metadata
name VARCHAR(200) NOT NULL,
description TEXT,
category VARCHAR(100), -- guide, standard_letter, technical, billing, etc.
-- Template indhold
subject_template VARCHAR(500), -- Emne med placeholders
body_template TEXT NOT NULL, -- Indhold med placeholders
-- Placeholders dokumentation
available_placeholders TEXT[], -- fx ['{{customer_name}}', '{{ticket_number}}']
-- Attachments (optional)
default_attachments JSONB, -- Array af fil-paths/URLs
-- Settings
is_active BOOLEAN DEFAULT true,
requires_approval BOOLEAN DEFAULT false, -- Kræver godkendelse før afsendelse
-- Metadata
created_by_user_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP,
last_used_at TIMESTAMP,
usage_count INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_tticket_templates_category ON tticket_templates(category);
CREATE INDEX IF NOT EXISTS idx_tticket_templates_active ON tticket_templates(is_active);
-- ============================================================================
-- TEMPLATE USAGE LOG (hvornår blev skabeloner brugt)
-- ============================================================================
CREATE TABLE IF NOT EXISTS tticket_template_usage (
id SERIAL PRIMARY KEY,
template_id INTEGER NOT NULL REFERENCES tticket_templates(id) ON DELETE CASCADE,
ticket_id INTEGER NOT NULL REFERENCES tticket_tickets(id) ON DELETE CASCADE,
user_id INTEGER, -- Reference til users.user_id
used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
was_modified BOOLEAN DEFAULT false -- Blev template redigeret før afsendelse?
);
CREATE INDEX IF NOT EXISTS idx_tticket_template_usage_template ON tticket_template_usage(template_id);
CREATE INDEX IF NOT EXISTS idx_tticket_template_usage_ticket ON tticket_template_usage(ticket_id);
-- ============================================================================
-- AI SUGGESTIONS (forslag til actions - aldrig automatisk)
-- ============================================================================
CREATE TABLE IF NOT EXISTS tticket_ai_suggestions (
id SERIAL PRIMARY KEY,
ticket_id INTEGER NOT NULL REFERENCES tticket_tickets(id) ON DELETE CASCADE,
-- Suggestion data
suggestion_type VARCHAR(50) NOT NULL CHECK (suggestion_type IN (
'contact_update', -- Opdater kontakt oplysninger
'new_contact', -- Ny kontakt opdaget
'category', -- Foreslå kategori
'tag', -- Foreslå tag
'priority', -- Foreslå prioritet
'deadline', -- Foreslå deadline
'calendar_event', -- Foreslå kalender event
'template', -- Foreslå skabelon
'merge', -- Foreslå flet med anden ticket
'related_ticket' -- Foreslå relation til anden ticket
)),
-- Suggestion content
suggestion_data JSONB NOT NULL, -- Struktureret data om forslaget
confidence DECIMAL(3,2), -- AI confidence 0-1
reasoning TEXT, -- Hvorfor blev dette foreslået
-- Source
source_text TEXT, -- Tekst som AI analyserede
source_comment_id INTEGER REFERENCES tticket_comments(id) ON DELETE CASCADE,
-- Status
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'rejected', 'auto_expired')),
reviewed_by_user_id INTEGER, -- Hvem behandlede forslaget
reviewed_at TIMESTAMP,
-- Metadata
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP -- Forslag udløber efter X dage
);
CREATE INDEX IF NOT EXISTS idx_tticket_ai_suggestions_ticket ON tticket_ai_suggestions(ticket_id);
CREATE INDEX IF NOT EXISTS idx_tticket_ai_suggestions_type ON tticket_ai_suggestions(suggestion_type);
CREATE INDEX IF NOT EXISTS idx_tticket_ai_suggestions_status ON tticket_ai_suggestions(status);
CREATE INDEX IF NOT EXISTS idx_tticket_ai_suggestions_created ON tticket_ai_suggestions(created_at);
-- ============================================================================
-- EMAIL METADATA (udvidet til contact identification)
-- ============================================================================
CREATE TABLE IF NOT EXISTS tticket_email_metadata (
id SERIAL PRIMARY KEY,
ticket_id INTEGER NOT NULL REFERENCES tticket_tickets(id) ON DELETE CASCADE,
-- Email headers
message_id VARCHAR(500) UNIQUE, -- Email Message-ID for threading
in_reply_to VARCHAR(500), -- In-Reply-To header
email_references TEXT, -- References header (renamed to avoid SQL keyword conflict)
-- Sender info (fra email)
from_email VARCHAR(255) NOT NULL,
from_name VARCHAR(255),
from_signature TEXT, -- Udtræk af signatur
-- Matched contact (hvis fundet)
matched_contact_id INTEGER, -- Reference til contacts.id
match_confidence DECIMAL(3,2), -- Hvor sikker er vi på match
match_method VARCHAR(50), -- email_exact, email_domain, name_similarity, etc.
-- Suggested contacts (hvis tvivl)
suggested_contacts JSONB, -- Array af {contact_id, confidence, reason}
-- Extracted data (AI analysis)
extracted_phone VARCHAR(50),
extracted_address TEXT,
extracted_company VARCHAR(255),
extracted_title VARCHAR(100),
-- Metadata
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_tticket_email_ticket ON tticket_email_metadata(ticket_id);
CREATE INDEX IF NOT EXISTS idx_tticket_email_message_id ON tticket_email_metadata(message_id);
CREATE INDEX IF NOT EXISTS idx_tticket_email_from ON tticket_email_metadata(from_email);
-- ============================================================================
-- Tilføj manglende kolonner til existing tticket_tickets
-- ============================================================================
ALTER TABLE tticket_tickets
ADD COLUMN IF NOT EXISTS deadline TIMESTAMP,
ADD COLUMN IF NOT EXISTS parent_ticket_id INTEGER REFERENCES tticket_tickets(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS is_merged BOOLEAN DEFAULT false,
ADD COLUMN IF NOT EXISTS merged_into_ticket_id INTEGER REFERENCES tticket_tickets(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_tticket_tickets_deadline ON tticket_tickets(deadline);
CREATE INDEX IF NOT EXISTS idx_tticket_tickets_parent ON tticket_tickets(parent_ticket_id);
-- ============================================================================
-- AUDIT LOG for ticket changes (sporbarhed)
-- ============================================================================
CREATE TABLE IF NOT EXISTS tticket_audit_log (
id SERIAL PRIMARY KEY,
ticket_id INTEGER NOT NULL REFERENCES tticket_tickets(id) ON DELETE CASCADE,
-- What changed
action VARCHAR(50) NOT NULL, -- created, updated, merged, split, status_change, etc.
field_name VARCHAR(100), -- Hvilket felt blev ændret
old_value TEXT,
new_value TEXT,
-- Who and when
user_id INTEGER, -- Reference til users.user_id
performed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Context
reason TEXT,
metadata JSONB -- Additional context
);
ALTER TABLE tticket_audit_log
ADD COLUMN IF NOT EXISTS field_name VARCHAR(100),
ADD COLUMN IF NOT EXISTS performed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS reason TEXT,
ADD COLUMN IF NOT EXISTS metadata JSONB;
CREATE INDEX IF NOT EXISTS idx_tticket_audit_ticket ON tticket_audit_log(ticket_id);
CREATE INDEX IF NOT EXISTS idx_tticket_audit_action ON tticket_audit_log(action);
CREATE INDEX IF NOT EXISTS idx_tticket_audit_performed ON tticket_audit_log(performed_at DESC);
-- ============================================================================
-- TRIGGERS for audit logging
-- ============================================================================
CREATE OR REPLACE FUNCTION tticket_log_ticket_changes()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'UPDATE' THEN
-- Log status changes
IF OLD.status != NEW.status THEN
INSERT INTO tticket_audit_log (ticket_id, action, field_name, old_value, new_value)
VALUES (NEW.id, 'status_change', 'status', OLD.status, NEW.status);
END IF;
-- Log priority changes
IF OLD.priority != NEW.priority THEN
INSERT INTO tticket_audit_log (ticket_id, action, field_name, old_value, new_value)
VALUES (NEW.id, 'priority_change', 'priority', OLD.priority, NEW.priority);
END IF;
-- Log assignment changes
IF OLD.assigned_to_user_id IS DISTINCT FROM NEW.assigned_to_user_id THEN
INSERT INTO tticket_audit_log (ticket_id, action, field_name, old_value, new_value)
VALUES (NEW.id, 'assignment_change', 'assigned_to_user_id',
OLD.assigned_to_user_id::TEXT, NEW.assigned_to_user_id::TEXT);
END IF;
-- Log deadline changes
IF OLD.deadline IS DISTINCT FROM NEW.deadline THEN
INSERT INTO tticket_audit_log (ticket_id, action, field_name, old_value, new_value)
VALUES (NEW.id, 'deadline_change', 'deadline',
OLD.deadline::TEXT, NEW.deadline::TEXT);
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tticket_audit_changes
AFTER UPDATE ON tticket_tickets
FOR EACH ROW
EXECUTE FUNCTION tticket_log_ticket_changes();
-- ============================================================================
-- VIEWS for enhanced queries
-- ============================================================================
-- View for tickets with hierarchy info
CREATE OR REPLACE VIEW tticket_tickets_with_hierarchy AS
SELECT
t.*,
parent.ticket_number as parent_ticket_number,
parent.subject as parent_subject,
(SELECT COUNT(*) FROM tticket_tickets WHERE parent_ticket_id = t.id) as child_count,
(SELECT COUNT(*) FROM tticket_relations WHERE ticket_id = t.id) as relation_count
FROM tticket_tickets t
LEFT JOIN tticket_tickets parent ON t.parent_ticket_id = parent.id;
-- View for tickets with pending AI suggestions
CREATE OR REPLACE VIEW tticket_tickets_with_suggestions AS
SELECT
t.id,
t.ticket_number,
t.subject,
t.status,
COUNT(DISTINCT s.id) FILTER (WHERE s.status = 'pending') as pending_suggestions,
COUNT(DISTINCT ce.id) FILTER (WHERE ce.suggested_by_ai = true AND ce.status = 'pending') as pending_calendar_suggestions
FROM tticket_tickets t
LEFT JOIN tticket_ai_suggestions s ON t.id = s.ticket_id
LEFT JOIN tticket_calendar_events ce ON t.id = ce.ticket_id
GROUP BY t.id, t.ticket_number, t.subject, t.status;
-- View for overdue tickets
CREATE OR REPLACE VIEW tticket_overdue_tickets AS
SELECT
t.id,
t.ticket_number,
t.subject,
t.status,
t.priority,
t.deadline,
t.assigned_to_user_id,
(CURRENT_TIMESTAMP - t.deadline) as overdue_duration,
c.name as customer_name
FROM tticket_tickets t
LEFT JOIN customers c ON t.customer_id = c.id
WHERE t.deadline < CURRENT_TIMESTAMP
AND t.status NOT IN ('resolved', 'closed')
ORDER BY t.deadline ASC;
-- ============================================================================
-- Seed data: Default templates
-- ============================================================================
INSERT INTO tticket_templates (name, description, category, subject_template, body_template, available_placeholders, is_active)
VALUES
(
'Tak for henvendelse',
'Standard svar ved modtagelse af ticket',
'standard_letter',
'Re: {{ticket_subject}}',
'Hej {{contact_name}},
Tak for din henvendelse. Vi har modtaget din sag og den er nu registreret som sag nr. {{ticket_number}}.
Vi vender tilbage hurtigst muligt med svar.
Med venlig hilsen,
BMC Networks',
ARRAY['{{contact_name}}', '{{customer_name}}', '{{ticket_number}}', '{{ticket_subject}}'],
true
),
(
'Løsning: Genstart router',
'Guide til genstart af router',
'guide',
'Re: {{ticket_subject}} - Løsning: Genstart router',
'Hej {{contact_name}},
Her er en guide til at genstarte din router:
1. Træk strømkablet ud af routeren
2. Vent 30 sekunder
3. Sæt strømkablet i igen
4. Vent 2-3 minutter mens routeren starter op
5. Test forbindelsen
Hvis problemet fortsætter, er du velkommen til at svare denne mail.
Med venlig hilsen,
BMC Networks
Sag: {{ticket_number}}',
ARRAY['{{contact_name}}', '{{customer_name}}', '{{ticket_number}}', '{{ticket_subject}}'],
true
),
(
'Afslutning af sag',
'Besked ved lukning af sag',
'standard_letter',
'Re: {{ticket_subject}} - Sag lukket',
'Hej {{contact_name}},
Vi betragter nu denne sag som løst og lukker den.
Hvis du har yderligere spørgsmål, er du velkommen til at kontakte os.
Med venlig hilsen,
BMC Networks
Sag: {{ticket_number}}',
ARRAY['{{contact_name}}', '{{customer_name}}', '{{ticket_number}}', '{{ticket_subject}}'],
true
);
-- ============================================================================
-- Comments
-- ============================================================================
COMMENT ON TABLE tticket_relations IS 'Ticket relationer: merge, split, parent/child hierarki';
COMMENT ON TABLE tticket_calendar_events IS 'Kalender events, deadlines og milepæle på tickets';
COMMENT ON TABLE tticket_templates IS 'Svarskabeloner med placeholders';
COMMENT ON TABLE tticket_ai_suggestions IS 'AI forslag der kræver manuel godkendelse';
COMMENT ON TABLE tticket_email_metadata IS 'Email metadata og contact identification data';
COMMENT ON TABLE tticket_audit_log IS 'Audit trail for alle ticket ændringer';
-- ============================================================================
-- Migration complete
-- ============================================================================
-- Dette modul tilføjer:
-- ✅ Ticket relations (merge, split, hierarchy)
-- ✅ Calendar events med AI forslag
-- ✅ Templates system
-- ✅ AI suggestions (kun forslag)
-- ✅ Enhanced email/contact matching
-- ✅ Full audit trail
-- ============================================================================