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
1274 lines
46 KiB
HTML
1274 lines
46 KiB
HTML
{% 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 %}
|