bmc_hub/app/ticket/frontend/ticket_new.html
Christian ffb3d335bc feat: Add Simply-CRM integration setup documentation and configuration details
docs: Create vTiger & Simply-CRM integration setup guide with credential requirements

feat: Implement ticket system enhancements including relations, calendar events, templates, and AI suggestions

refactor: Update ticket system migration to include audit logging and enhanced email metadata
2025-12-16 15:36:11 +01:00

1274 lines
46 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "shared/frontend/base.html" %}
{% block title %}Opret Ny Sag - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.form-wizard {
background: var(--bg-card);
border-radius: var(--border-radius);
box-shadow: 0 4px 30px rgba(0,0,0,0.05);
overflow: hidden;
}
.wizard-header {
background: linear-gradient(135deg, var(--accent) 0%, #1565a6 100%);
padding: 2rem;
color: white;
text-align: center;
}
.wizard-header h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.wizard-header p {
opacity: 0.9;
margin-bottom: 0;
}
.wizard-steps {
display: flex;
justify-content: center;
padding: 2rem 2rem 0;
gap: 1rem;
background: var(--accent-light);
border-bottom: 2px solid var(--accent);
}
.wizard-step {
flex: 1;
max-width: 200px;
text-align: center;
padding-bottom: 1.5rem;
position: relative;
cursor: pointer;
transition: all 0.3s;
}
.wizard-step::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background: transparent;
transition: background 0.3s;
}
.wizard-step.active::after {
background: var(--accent);
}
.wizard-step.active .step-number {
background: var(--accent);
color: white;
transform: scale(1.1);
}
.step-number {
width: 40px;
height: 40px;
border-radius: 50%;
background: white;
border: 2px solid var(--accent);
color: var(--accent);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 0.5rem;
font-weight: 700;
transition: all 0.3s;
}
.step-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-secondary);
}
.wizard-step.active .step-label {
color: var(--accent);
}
.wizard-content {
padding: 3rem 2rem;
min-height: 400px;
}
.step-content {
display: none;
animation: fadeIn 0.3s;
}
.step-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.form-floating {
margin-bottom: 1.5rem;
}
.form-floating > .form-control,
.form-floating > .form-select {
height: calc(3.5rem + 2px);
padding: 1rem 1rem;
border: 2px solid #eee;
border-radius: 12px;
transition: all 0.3s;
}
.form-floating > .form-control:focus,
.form-floating > .form-select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 4px rgba(15, 76, 117, 0.1);
}
.form-floating > label {
padding: 1rem 1rem;
font-weight: 500;
color: var(--text-secondary);
}
.priority-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.priority-card {
background: white;
border: 2px solid #eee;
border-radius: 12px;
padding: 1.5rem 1rem;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.priority-card:hover {
transform: translateY(-4px);
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
}
.priority-card.selected {
border-color: var(--accent);
background: var(--accent-light);
}
.priority-card input[type="radio"] {
display: none;
}
.priority-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.priority-label {
font-weight: 600;
font-size: 0.9rem;
}
.customer-search {
position: relative;
}
.customer-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 2px solid var(--accent);
border-top: none;
border-radius: 0 0 12px 12px;
max-height: 300px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
display: none;
}
.customer-results.show {
display: block;
}
.customer-item {
padding: 1rem 1.5rem;
cursor: pointer;
border-bottom: 1px solid #eee;
transition: background 0.2s;
}
.customer-item:hover {
background: var(--accent-light);
}
.customer-item:last-child {
border-bottom: none;
}
.customer-name {
font-weight: 600;
color: var(--text-primary);
}
.customer-id {
font-size: 0.85rem;
color: var(--text-secondary);
}
.wizard-actions {
display: flex;
justify-content: space-between;
padding: 2rem;
background: var(--accent-light);
border-top: 1px solid #ddd;
}
.btn {
padding: 0.8rem 2rem;
border-radius: 10px;
font-weight: 600;
transition: all 0.3s;
}
.btn-primary {
background: var(--accent);
border: none;
}
.btn-primary:hover {
background: #0a3655;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(15, 76, 117, 0.3);
}
.btn-outline-secondary {
border: 2px solid var(--text-secondary);
color: var(--text-secondary);
}
.btn-outline-secondary:hover {
background: var(--text-secondary);
color: white;
}
.success-animation {
text-align: center;
padding: 3rem;
}
.success-icon {
font-size: 5rem;
color: #28a745;
animation: scaleIn 0.5s;
}
@keyframes scaleIn {
from { transform: scale(0); }
to { transform: scale(1); }
}
.tag-input-wrapper {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.75rem;
border: 2px solid #eee;
border-radius: 12px;
min-height: 3.5rem;
align-items: center;
cursor: text;
transition: all 0.3s;
}
.tag-input-wrapper:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 4px rgba(15, 76, 117, 0.1);
}
.tag {
background: var(--accent);
color: white;
padding: 0.4rem 0.8rem;
border-radius: 20px;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.tag-remove {
cursor: pointer;
font-weight: 700;
opacity: 0.8;
}
.tag-remove:hover {
opacity: 1;
}
.tag-input-wrapper input {
border: none;
outline: none;
flex: 1;
min-width: 150px;
font-size: 1rem;
}
.quick-templates {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.template-card {
background: white;
border: 2px solid #eee;
border-radius: 12px;
padding: 1.5rem;
cursor: pointer;
transition: all 0.3s;
}
.template-card:hover {
border-color: var(--accent);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
}
.template-title {
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--accent);
}
.template-desc {
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 0;
}
.file-upload-area {
border: 3px dashed #ddd;
border-radius: 12px;
padding: 3rem;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.file-upload-area:hover,
.file-upload-area.drag-over {
border-color: var(--accent);
background: var(--accent-light);
}
.file-upload-icon {
font-size: 3rem;
color: var(--accent);
margin-bottom: 1rem;
}
.uploaded-files {
margin-top: 1.5rem;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: var(--accent-light);
border-radius: 8px;
margin-bottom: 0.5rem;
}
.file-info {
display: flex;
align-items: center;
gap: 1rem;
}
.file-icon {
font-size: 1.5rem;
color: var(--accent);
}
.ai-suggest-box {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem;
border-radius: 12px;
margin-bottom: 2rem;
text-align: center;
}
.ai-suggest-box i {
font-size: 2rem;
margin-bottom: 0.5rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-8">
<form id="ticketForm" class="form-wizard">
<!-- Header -->
<div class="wizard-header">
<h1><i class="bi bi-ticket-detailed me-2"></i>Opret Ny Support Sag</h1>
<p>Følg trinene for at oprette en struktureret support ticket</p>
</div>
<!-- Progress Steps -->
<div class="wizard-steps">
<div class="wizard-step active" data-step="1">
<div class="step-number">1</div>
<div class="step-label">Kunde</div>
</div>
<div class="wizard-step" data-step="2">
<div class="step-number">2</div>
<div class="step-label">Problem</div>
</div>
<div class="wizard-step" data-step="3">
<div class="step-number">3</div>
<div class="step-label">Detaljer</div>
</div>
<div class="wizard-step" data-step="4">
<div class="step-number">4</div>
<div class="step-label">Vedhæft</div>
</div>
</div>
<!-- Form Content -->
<div class="wizard-content">
<!-- Step 1: Customer Selection -->
<div class="step-content active" data-step="1">
<h3 class="mb-4"><i class="bi bi-person-circle me-2"></i>Vælg Kunde</h3>
<div class="customer-search">
<div class="form-floating">
<input type="text" class="form-control" id="customerSearch"
placeholder="Søg efter kunde eller kontaktperson..." autocomplete="off">
<label for="customerSearch">
<i class="bi bi-search me-2"></i>Søg efter firma eller kontaktperson
</label>
</div>
<div class="customer-results" id="customerResults"></div>
</div>
<input type="hidden" id="customerId" name="customer_id">
<input type="hidden" id="contactId" name="contact_id">
<div id="selectedCustomer" class="alert alert-info mt-3" style="display: none;">
<strong><i class="bi bi-check-circle me-2"></i>Valgt:</strong>
<span id="selectedCustomerName"></span>
</div>
<div class="alert alert-light mt-3">
<i class="bi bi-info-circle me-2"></i>
<small class="text-muted">Søg efter firmanavn, kontaktperson, CVR-nummer eller email</small>
</div>
</div>
<!-- Step 2: Problem Description -->
<div class="step-content" data-step="2">
<h3 class="mb-4"><i class="bi bi-exclamation-triangle me-2"></i>Beskriv Problemet</h3>
<div class="ai-suggest-box" id="aiSuggestBox" style="display: none;">
<i class="bi bi-stars"></i>
<h5>AI Forslag</h5>
<p class="mb-0" id="aiSuggestion"></p>
<button type="button" class="btn btn-sm btn-light mt-2" onclick="applyAISuggestion()">
<i class="bi bi-check-circle me-1"></i>Anvend Forslag
</button>
</div>
<div class="form-floating">
<input type="text" class="form-control" id="subject" name="subject"
placeholder="Kort beskrivelse" required>
<label for="subject">
<i class="bi bi-card-heading me-2"></i>Emne / Kort Beskrivelse
</label>
</div>
<div class="form-floating">
<textarea class="form-control" id="description" name="description"
placeholder="Detaljeret beskrivelse"
style="height: 200px;" required
oninput="analyzeDescription()"></textarea>
<label for="description">
<i class="bi bi-text-paragraph me-2"></i>Detaljeret Beskrivelse
</label>
</div>
<div id="suggestedTagsBox" style="display: none; margin: 1rem 0;">
<div class="alert alert-info" style="margin-bottom: 0.5rem; padding: 0.75rem;">
<i class="bi bi-lightbulb me-2"></i>
<strong>AI Foreslog Tags:</strong>
<small class="text-muted ms-2">(Klik for at tilføje)</small>
</div>
<div id="suggestedTags" style="display: flex; flex-wrap: wrap; gap: 0.5rem;"></div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-floating">
<select class="form-select" id="category" name="category">
<option value="">Vælg kategori...</option>
<option value="network">Netværk</option>
<option value="hardware">Hardware</option>
<option value="software">Software</option>
<option value="security">Sikkerhed</option>
<option value="email">Email</option>
<option value="other">Andet</option>
</select>
<label for="category"><i class="bi bi-folder me-2"></i>Kategori</label>
</div>
</div>
<div class="col-md-6">
<div class="form-floating">
<select class="form-select" id="channel" name="channel">
<option value="email">Email</option>
<option value="phone">Telefon</option>
<option value="portal">Web Portal</option>
<option value="manual">Walk-in</option>
<option value="api">API</option>
</select>
<label for="channel"><i class="bi bi-signpost me-2"></i>Henvendelseskanal</label>
</div>
</div>
</div>
</div>
<!-- Step 3: Details -->
<div class="step-content" data-step="3">
<h3 class="mb-4"><i class="bi bi-sliders me-2"></i>Prioritet & Detaljer</h3>
<label class="form-label fw-bold mb-3">
<i class="bi bi-flag me-2"></i>Vælg Prioritet
</label>
<div class="priority-selector">
<label class="priority-card">
<input type="radio" name="priority" value="low" checked>
<div class="priority-icon">🟢</div>
<div class="priority-label">Lav</div>
<small class="text-muted">Kan vente</small>
</label>
<label class="priority-card">
<input type="radio" name="priority" value="normal">
<div class="priority-icon">🟡</div>
<div class="priority-label">Normal</div>
<small class="text-muted">Standard responstid</small>
</label>
<label class="priority-card">
<input type="radio" name="priority" value="high">
<div class="priority-icon">🟠</div>
<div class="priority-label">Høj</div>
<small class="text-muted">Hurtig handling</small>
</label>
<label class="priority-card">
<input type="radio" name="priority" value="urgent">
<div class="priority-icon">🔴</div>
<div class="priority-label">Akut</div>
<small class="text-muted">Med det samme</small>
</label>
<label class="priority-card">
<input type="radio" name="priority" value="critical">
<div class="priority-icon"></div>
<div class="priority-label">Kritisk</div>
<small class="text-muted">Alt andet stopper</small>
</label>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-floating">
<input type="date" class="form-control" id="dueDate" name="due_date">
<label for="dueDate"><i class="bi bi-calendar-event me-2"></i>Deadline (valgfri)</label>
</div>
</div>
<div class="col-md-6">
<div class="form-floating">
<select class="form-select" id="assignedTo" name="assigned_to_user_id">
<option value="">Ikke tildelt endnu</option>
<option value="1">Christian</option>
<option value="2">Support Team</option>
<option value="3">Netværk Team</option>
</select>
<label for="assignedTo"><i class="bi bi-person-badge me-2"></i>Tildel til</label>
</div>
</div>
</div>
<label class="form-label fw-bold mb-2 mt-3">
<i class="bi bi-tags me-2"></i>Tags (tryk Enter for at tilføje)
</label>
<div class="tag-input-wrapper" id="tagInputWrapper">
<input type="text" id="tagInput" placeholder="Skriv tag...">
</div>
<input type="hidden" id="tags" name="tags">
<div class="form-floating mt-3">
<textarea class="form-control" id="internalNote" name="internal_note"
placeholder="Intern note" style="height: 100px;"></textarea>
<label for="internalNote">
<i class="bi bi-shield-lock me-2"></i>Intern Note (kun synlig for medarbejdere)
</label>
</div>
</div>
<!-- Step 4: Attachments -->
<div class="step-content" data-step="4">
<h3 class="mb-4"><i class="bi bi-paperclip me-2"></i>Vedhæft Filer (Valgfrit)</h3>
<div class="file-upload-area" id="fileUploadArea">
<div class="file-upload-icon">
<i class="bi bi-cloud-upload"></i>
</div>
<h5>Træk og slip filer her</h5>
<p class="text-muted">eller klik for at vælge filer</p>
<input type="file" id="fileInput" multiple hidden>
<button type="button" class="btn btn-outline-primary mt-2" onclick="document.getElementById('fileInput').click()">
<i class="bi bi-folder2-open me-2"></i>Vælg Filer
</button>
</div>
<div class="uploaded-files" id="uploadedFiles"></div>
<div class="alert alert-info mt-4">
<i class="bi bi-info-circle me-2"></i>
<strong>Tip:</strong> Du kan også tilføje skærmbilleder, logfiler eller screenshots
der kan hjælpe med at diagnosticere problemet.
</div>
</div>
</div>
<!-- Navigation Actions -->
<div class="wizard-actions">
<button type="button" class="btn btn-outline-secondary" id="prevBtn" onclick="changeStep(-1)" style="display: none;">
<i class="bi bi-arrow-left me-2"></i>Forrige
</button>
<div></div>
<button type="button" class="btn btn-primary" id="nextBtn" onclick="changeStep(1)">
Næste<i class="bi bi-arrow-right ms-2"></i>
</button>
<button type="submit" class="btn btn-primary" id="submitBtn" style="display: none;">
<i class="bi bi-check-circle me-2"></i>Opret Sag
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Success Modal -->
<div class="modal fade" id="successModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body success-animation">
<div class="success-icon">
<i class="bi bi-check-circle-fill"></i>
</div>
<h3 class="mb-3">Sag Oprettet!</h3>
<p class="text-muted mb-4">Din support sag er blevet oprettet med nummer:</p>
<h2 class="text-primary mb-4" id="ticketNumber"></h2>
<a href="/ticket/tickets" class="btn btn-primary me-2">
<i class="bi bi-list-ul me-2"></i>Se Alle Sager
</a>
<button type="button" class="btn btn-outline-secondary" onclick="window.location.reload()">
<i class="bi bi-plus-circle me-2"></i>Opret Ny Sag
</button>
</div>
</div>
</div>
</div>
<script>
let currentStep = 1;
const totalSteps = 4;
const tags = [];
let uploadedFiles = [];
// Initialize
document.addEventListener('DOMContentLoaded', function() {
updateStepDisplay();
initCustomerSearch();
initTagInput();
initFileUpload();
initPriorityCards();
});
// Step Navigation
function changeStep(direction) {
if (direction === 1 && !validateStep(currentStep)) {
return;
}
currentStep += direction;
if (currentStep < 1) currentStep = 1;
if (currentStep > totalSteps) currentStep = totalSteps;
updateStepDisplay();
}
function updateStepDisplay() {
// Update step content
document.querySelectorAll('.step-content').forEach(content => {
content.classList.remove('active');
});
document.querySelector(`.step-content[data-step="${currentStep}"]`).classList.add('active');
// Update progress indicators
document.querySelectorAll('.wizard-step').forEach(step => {
const stepNum = parseInt(step.dataset.step);
step.classList.toggle('active', stepNum === currentStep);
});
// Update buttons
document.getElementById('prevBtn').style.display = currentStep === 1 ? 'none' : 'block';
document.getElementById('nextBtn').style.display = currentStep === totalSteps ? 'none' : 'block';
document.getElementById('submitBtn').style.display = currentStep === totalSteps ? 'block' : 'none';
}
function validateStep(step) {
switch(step) {
case 1:
const customerId = document.getElementById('customerId').value;
if (!customerId || customerId === '0') {
alert('⚠️ Vælg venligst en kunde fra søgeresultaterne');
return false;
}
return true;
case 2:
const subject = document.getElementById('subject').value.trim();
const description = document.getElementById('description').value.trim();
if (!subject || !description) {
alert('⚠️ Udfyld venligst emne og beskrivelse');
return false;
}
return true;
default:
return true;
}
}
// Customer Search
function initCustomerSearch() {
const searchInput = document.getElementById('customerSearch');
const results = document.getElementById('customerResults');
let debounceTimer;
searchInput.addEventListener('input', function() {
clearTimeout(debounceTimer);
const query = this.value.trim();
if (query.length < 2) {
results.classList.remove('show');
return;
}
debounceTimer = setTimeout(() => searchCustomers(query), 300);
});
// Close results when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.customer-search')) {
results.classList.remove('show');
}
});
}
async function searchCustomers(query) {
try {
const response = await fetch(`/api/v1/customers?search=${encodeURIComponent(query)}&limit=20`);
const data = await response.json();
const results = document.getElementById('customerResults');
const customers = data.customers || [];
if (customers.length === 0) {
results.innerHTML = '<div class="customer-item">Ingen resultater fundet</div>';
} else {
results.innerHTML = customers.map(c => {
// Build display info
const contactInfo = [];
if (c.contact_name && c.contact_name.trim() !== '' && c.contact_name.trim() !== ' ') {
contactInfo.push(`<i class="bi bi-person me-1"></i>${c.contact_name}`);
}
if (c.contact_email) {
contactInfo.push(`<i class="bi bi-envelope me-1"></i>${c.contact_email}`);
} else if (c.email) {
contactInfo.push(`<i class="bi bi-envelope me-1"></i>${c.email}`);
}
if (c.contact_phone) {
contactInfo.push(`<i class="bi bi-telephone me-1"></i>${c.contact_phone}`);
} else if (c.phone) {
contactInfo.push(`<i class="bi bi-telephone me-1"></i>${c.phone}`);
}
const displayInfo = contactInfo.length > 0 ? contactInfo.join(' ') : `#${c.id}`;
const cityInfo = c.city ? ` <span class="badge bg-secondary">${c.city}</span>` : '';
return `
<div class="customer-item" onclick='selectCustomer(${JSON.stringify(c).replace(/'/g, "\\'")}, null)'}>
<div class="customer-name">${c.name}${cityInfo}</div>
<div class="customer-id">${displayInfo}</div>
</div>
`;
}).join('');
}
results.classList.add('show');
} catch (error) {
console.error('Customer search error:', error);
document.getElementById('customerResults').innerHTML = '<div class="customer-item text-danger">Fejl ved søgning</div>';
document.getElementById('customerResults').classList.add('show');
}
}
function selectCustomer(customer, contact) {
document.getElementById('customerId').value = customer.id;
document.getElementById('contactId').value = contact ? contact.id : '';
// Set search field text
if (contact && contact.name) {
document.getElementById('customerSearch').value = `${contact.name} (${customer.name})`;
} else if (customer.contact_name && customer.contact_name.trim() !== '' && customer.contact_name.trim() !== ' ') {
document.getElementById('customerSearch').value = `${customer.contact_name} (${customer.name})`;
} else {
document.getElementById('customerSearch').value = customer.name;
}
// Build detailed display
let displayText = `<strong>${customer.name}</strong> (#${customer.id})`;
if (customer.city) {
displayText += ` <span class="badge bg-secondary">${customer.city}</span>`;
}
// Add contact info if available
if (contact) {
displayText += `<br><i class="bi bi-person me-2"></i>${contact.name}`;
if (contact.email) {
displayText += ` <span class="text-muted">• ${contact.email}</span>`;
}
} else if (customer.contact_name && customer.contact_name.trim() !== '' && customer.contact_name.trim() !== ' ') {
displayText += `<br><i class="bi bi-person me-2"></i>${customer.contact_name}`;
if (customer.contact_email) {
displayText += ` <span class="text-muted">• ${customer.contact_email}</span>`;
}
}
document.getElementById('selectedCustomerName').innerHTML = displayText;
document.getElementById('selectedCustomer').style.display = 'block';
document.getElementById('customerResults').classList.remove('show');
}
// Tag Input
function initTagInput() {
const tagInput = document.getElementById('tagInput');
tagInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
const tag = this.value.trim();
if (tag && !tags.includes(tag)) {
tags.push(tag);
updateTagDisplay();
this.value = '';
}
}
});
document.getElementById('tagInputWrapper').addEventListener('click', function() {
tagInput.focus();
});
}
function updateTagDisplay() {
const wrapper = document.getElementById('tagInputWrapper');
const input = document.getElementById('tagInput');
// Remove existing tags
wrapper.querySelectorAll('.tag').forEach(tag => tag.remove());
// Add tags
tags.forEach((tag, index) => {
const tagEl = document.createElement('div');
tagEl.className = 'tag';
tagEl.innerHTML = `
${tag}
<span class="tag-remove" onclick="removeTag(${index})">×</span>
`;
wrapper.insertBefore(tagEl, input);
});
document.getElementById('tags').value = JSON.stringify(tags);
}
function removeTag(index) {
tags.splice(index, 1);
updateTagDisplay();
}
// File Upload
function initFileUpload() {
const uploadArea = document.getElementById('fileUploadArea');
const fileInput = document.getElementById('fileInput');
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('drag-over');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('drag-over');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('drag-over');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
}
function handleFiles(files) {
Array.from(files).forEach(file => {
uploadedFiles.push(file);
addFileToDisplay(file);
});
}
function addFileToDisplay(file) {
const filesContainer = document.getElementById('uploadedFiles');
const fileEl = document.createElement('div');
fileEl.className = 'file-item';
fileEl.innerHTML = `
<div class="file-info">
<i class="bi bi-file-earmark file-icon"></i>
<div>
<div class="fw-bold">${file.name}</div>
<small class="text-muted">${(file.size / 1024).toFixed(1)} KB</small>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeFile('${file.name}')">
<i class="bi bi-trash"></i>
</button>
`;
filesContainer.appendChild(fileEl);
}
function removeFile(fileName) {
uploadedFiles = uploadedFiles.filter(f => f.name !== fileName);
document.getElementById('uploadedFiles').innerHTML = '';
uploadedFiles.forEach(f => addFileToDisplay(f));
}
// Priority Cards
function initPriorityCards() {
document.querySelectorAll('.priority-card').forEach(card => {
card.addEventListener('click', function() {
document.querySelectorAll('.priority-card').forEach(c => c.classList.remove('selected'));
this.classList.add('selected');
});
});
}
// AI Analysis
let aiSuggestion = null;
let suggestedTagsList = [];
let analysisTimeout = null;
function analyzeDescription() {
clearTimeout(analysisTimeout);
const description = document.getElementById('description').value.trim();
if (description.length < 20) {
document.getElementById('aiSuggestBox').style.display = 'none';
return;
}
analysisTimeout = setTimeout(async () => {
try {
// Simple keyword-based AI simulation
const subject = document.getElementById('subject').value.trim().toLowerCase();
const text = (description + ' ' + subject).toLowerCase();
let suggestedCategory = '';
let suggestedPriority = 'normal';
suggestedTagsList = [];
console.log('Analyzing text:', text);
// Category detection
if (text.includes('netværk') || text.includes('internet') || text.includes('wifi') || text.includes('forbindelse')) {
suggestedCategory = 'network';
} else if (text.includes('email') || text.includes('mail') || text.includes('outlook')) {
suggestedCategory = 'email';
} else if (text.includes('firewall') || text.includes('sikkerhed') || text.includes('virus') || text.includes('malware')) {
suggestedCategory = 'security';
} else if (text.includes('computer') || text.includes('pc') || text.includes('laptop') || text.includes('skærm')) {
suggestedCategory = 'hardware';
} else if (text.includes('software') || text.includes('program') || text.includes('app')) {
suggestedCategory = 'software';
}
// Priority detection
if (text.includes('nede') || text.includes('virker ikke') || text.includes('total') || text.includes('alle')) {
suggestedPriority = 'urgent';
} else if (text.includes('vigtig') || text.includes('hurtig') || text.includes('asap')) {
suggestedPriority = 'high';
} else if (text.includes('når tid') || text.includes('ikke haster')) {
suggestedPriority = 'low';
}
// Tag detection
const tagKeywords = {
'wifi': ['wifi', 'trådløs', 'wireless', 'wlan'],
'vpn': ['vpn', 'fjernforbindelse', 'remote', 'fjern'],
'printer': ['printer', 'print', 'udskriv', 'printe'],
'firewall': ['firewall', 'brandmur'],
'switch': ['switch', 'netværksswitch'],
'backup': ['backup', 'sikkerhedskopi', 'back-up'],
'password': ['password', 'kodeord', 'adgangskode', 'password'],
'server': ['server', 'serveren'],
'email': ['email', 'mail', 'outlook', 'e-mail'],
'windows': ['windows', 'pc'],
'mac': ['mac', 'macos', 'apple'],
'office365': ['office365', 'o365', 'office 365'],
'teams': ['teams', 'microsoft teams'],
'onedrive': ['onedrive', 'one drive'],
'sharepoint': ['sharepoint', 'share point'],
'router': ['router', 'routeren'],
'internet': ['internet', 'internettet', 'nettet'],
'lan': ['lan', 'lokalnetværk'],
'wan': ['wan'],
'dns': ['dns'],
'dhcp': ['dhcp'],
'ip': ['ip-adresse', 'ip adresse', 'ip'],
'kabel': ['kabel', 'ledning', 'kabling'],
'opdatering': ['opdatering', 'update', 'opdater'],
'fejl': ['fejl', 'error', 'fejlmelding', 'fejler'],
'langsom': ['langsom', 'slow', 'træg', 'hurtig'],
'adgang': ['adgang', 'access', 'login', 'logge'],
'installation': ['installation', 'installer', 'opsætning', 'installere'],
'telefon': ['telefon', 'mobil', 'opkald', 'viderestille', 'viderestillet', 'omstille', 'omstilling'],
'netværk': ['netværk', 'network', 'net'],
'forbindelse': ['forbindelse', 'tilslutning', 'forbinder', 'connect'],
'sikkerhed': ['sikkerhed', 'security', 'virus', 'malware', 'hacking'],
'laptop': ['laptop', 'bærbar', 'notebook'],
'skærm': ['skærm', 'monitor', 'display'],
'mus': ['mus', 'mouse'],
'tastatur': ['tastatur', 'keyboard'],
'webex': ['webex', 'web ex'],
'zoom': ['zoom'],
'licens': ['licens', 'license', 'abonnement'],
'bruger': ['bruger', 'user', 'medarbejder']
};
for (const [tag, keywords] of Object.entries(tagKeywords)) {
if (keywords.some(kw => text.includes(kw))) {
suggestedTagsList.push(tag);
}
}
console.log('Found tags:', suggestedTagsList);
// Limit to max 5 tags
suggestedTagsList = suggestedTagsList.slice(0, 5);
// Store for later use
aiSuggestion = {
category: suggestedCategory,
priority: suggestedPriority,
tags: suggestedTagsList
};
// Show category/priority AI box if found
if (suggestedCategory || suggestedPriority !== 'normal') {
const categoryNames = {
'network': 'Netværk',
'hardware': 'Hardware',
'software': 'Software',
'security': 'Sikkerhed',
'email': 'Email'
};
const priorityNames = {
'low': 'Lav',
'normal': 'Normal',
'high': 'Høj',
'urgent': 'Akut',
'critical': 'Kritisk'
};
let suggestionText = '🤖 ';
if (suggestedCategory) {
suggestionText += `Kategori: ${categoryNames[suggestedCategory]}`;
}
if (suggestedCategory && suggestedPriority !== 'normal') {
suggestionText += ' • ';
}
if (suggestedPriority !== 'normal') {
suggestionText += `Prioritet: ${priorityNames[suggestedPriority]}`;
}
document.getElementById('aiSuggestion').textContent = suggestionText;
document.getElementById('aiSuggestBox').style.display = 'block';
} else {
document.getElementById('aiSuggestBox').style.display = 'none';
}
// Show suggested tags immediately (separate from category/priority)
console.log('Suggested tags:', suggestedTagsList);
if (suggestedTagsList.length > 0) {
showSuggestedTags();
} else {
document.getElementById('suggestedTagsBox').style.display = 'none';
}
} catch (error) {
console.error('AI analysis error:', error);
}
}, 1000);
}
function showSuggestedTags() {
if (!suggestedTagsList || suggestedTagsList.length === 0) return;
const container = document.getElementById('suggestedTags');
container.innerHTML = suggestedTagsList.map(tag => `
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addSuggestedTag('${tag}')">
<i class="bi bi-plus-circle me-1"></i>${tag}
</button>
`).join('');
document.getElementById('suggestedTagsBox').style.display = 'block';
}
function addSuggestedTag(tag) {
if (!tags.includes(tag)) {
tags.push(tag);
updateTagDisplay();
}
// Remove from suggested
suggestedTagsList = suggestedTagsList.filter(t => t !== tag);
showSuggestedTags();
if (suggestedTagsList.length === 0) {
document.getElementById('suggestedTagsBox').style.display = 'none';
}
}
function applyAISuggestion() {
if (!aiSuggestion) return;
if (aiSuggestion.category) {
document.getElementById('category').value = aiSuggestion.category;
}
if (aiSuggestion.priority) {
const priorityRadio = document.querySelector(`input[name="priority"][value="${aiSuggestion.priority}"]`);
if (priorityRadio) {
priorityRadio.checked = true;
priorityRadio.closest('.priority-card').click();
}
}
document.getElementById('aiSuggestBox').style.display = 'none';
// Show success feedback
const box = document.getElementById('aiSuggestBox');
box.style.background = 'linear-gradient(135deg, #28a745 0%, #20c997 100%)';
box.innerHTML = '<i class="bi bi-check-circle"></i> <strong>Forslag anvendt!</strong>';
box.style.display = 'block';
setTimeout(() => {
box.style.display = 'none';
box.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
box.innerHTML = '<i class="bi bi-stars"></i><h5>AI Forslag</h5><p class="mb-0" id="aiSuggestion"></p><button type="button" class="btn btn-sm btn-light mt-2" onclick="applyAISuggestion()"><i class="bi bi-check-circle me-1"></i>Anvend Forslag</button>';
}, 2000);
}
// Form Submission
document.getElementById('ticketForm').addEventListener('submit', async function(e) {
e.preventDefault();
if (!validateStep(currentStep)) {
return;
}
const customerId = document.getElementById('customerId').value;
const contactId = document.getElementById('contactId').value;
const formData = {
customer_id: parseInt(customerId),
contact_id: contactId ? parseInt(contactId) : null,
subject: document.getElementById('subject').value,
description: document.getElementById('description').value,
priority: document.querySelector('input[name="priority"]:checked').value,
category: document.getElementById('category').value || null,
source: document.getElementById('channel').value,
due_date: document.getElementById('dueDate').value || null,
assigned_to_user_id: document.getElementById('assignedTo').value ? parseInt(document.getElementById('assignedTo').value) : null,
tags: tags.length > 0 ? tags : null,
status: 'open'
};
console.log('Creating ticket with data:', formData);
try {
const response = await fetch('/api/v1/ticket/tickets', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
const result = await response.json();
console.log('Server response:', result);
if (!response.ok) {
throw new Error(result.detail || 'Failed to create ticket');
}
// Show success modal
document.getElementById('ticketNumber').textContent = result.ticket_number;
const modal = new bootstrap.Modal(document.getElementById('successModal'));
modal.show();
} catch (error) {
console.error('Error creating ticket:', error);
alert('❌ Fejl ved oprettelse af sag: ' + error.message);
}
});
</script>
{% endblock %}