bmc_hub/app/ticket/frontend/ticket_new.html

1274 lines
46 KiB
HTML
Raw Normal View History

{% 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 %}