- Created migration 146 to seed case type tags with various categories and keywords. - Created migration 147 to seed brand and type tags, including a comprehensive list of brands and case types. - Added migration 148 to introduce a new column `is_next` in `sag_todo_steps` for persistent next-task selection. - Implemented a new script `run_migrations.py` to facilitate running SQL migrations against the PostgreSQL database with options for dry runs and error handling.
453 lines
18 KiB
PL/PgSQL
453 lines
18 KiB
PL/PgSQL
-- ============================================================================
|
|
-- 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 på 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
|
|
-- ============================================================================
|