2025-12-16 15:36:11 +01:00
|
|
|
|
{% 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>
|
|
|
|
|
|
|
2026-02-10 14:40:38 +01:00
|
|
|
|
<div class="card mt-3 border-0 shadow-sm">
|
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
|
<h6 class="fw-bold mb-3"><i class="bi bi-display me-2"></i>Hardware (AnyDesk)</h6>
|
|
|
|
|
|
<div id="ticketHardwareList" class="border rounded-3 p-3 bg-light">
|
|
|
|
|
|
<div class="text-muted small">Vælg en kunde eller kontakt for at se relateret hardware.</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="row g-3 mt-3">
|
|
|
|
|
|
<div class="col-md-4">
|
|
|
|
|
|
<label class="form-label">Navn *</label>
|
|
|
|
|
|
<input type="text" class="form-control" id="ticketHardwareNameInput" placeholder="PC, NAS, Server...">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-md-4">
|
|
|
|
|
|
<label class="form-label">AnyDesk ID</label>
|
|
|
|
|
|
<input type="text" class="form-control" id="ticketHardwareAnyDeskIdInput" placeholder="123-456-789">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-md-4">
|
|
|
|
|
|
<label class="form-label">AnyDesk Link</label>
|
|
|
|
|
|
<input type="text" class="form-control" id="ticketHardwareAnyDeskLinkInput" placeholder="anydesk://...">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-12 d-flex justify-content-end">
|
|
|
|
|
|
<button type="button" class="btn btn-outline-primary" onclick="quickCreateTicketHardware()">
|
|
|
|
|
|
<i class="bi bi-plus-circle me-2"></i>Opret hardware
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-16 15:36:11 +01:00
|
|
|
|
<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');
|
2026-02-10 14:40:38 +01:00
|
|
|
|
|
|
|
|
|
|
loadHardwareForTicket();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadHardwareForTicket() {
|
|
|
|
|
|
const list = document.getElementById('ticketHardwareList');
|
|
|
|
|
|
if (!list) return;
|
|
|
|
|
|
|
|
|
|
|
|
const customerId = document.getElementById('customerId').value;
|
|
|
|
|
|
const contactId = document.getElementById('contactId').value;
|
|
|
|
|
|
|
|
|
|
|
|
if (!customerId && !contactId) {
|
|
|
|
|
|
list.innerHTML = '<div class="text-muted small">Vælg en kunde eller kontakt for at se relateret hardware.</div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
list.innerHTML = '<div class="text-muted small"><span class="spinner-border spinner-border-sm me-2"></span>Henter hardware...</div>';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const endpoint = contactId
|
|
|
|
|
|
? `/api/v1/hardware/by-contact/${contactId}`
|
|
|
|
|
|
: `/api/v1/hardware/by-customer/${customerId}`;
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(endpoint);
|
|
|
|
|
|
const items = response.ok ? await response.json() : [];
|
|
|
|
|
|
renderTicketHardware(items || []);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Failed to load hardware:', err);
|
|
|
|
|
|
list.innerHTML = '<div class="text-danger small">Kunne ikke hente hardware.</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderTicketHardware(items) {
|
|
|
|
|
|
const list = document.getElementById('ticketHardwareList');
|
|
|
|
|
|
if (!list) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (!items || items.length === 0) {
|
|
|
|
|
|
list.innerHTML = '<div class="text-muted small">Ingen hardware fundet for valgt kunde/kontakt.</div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
list.innerHTML = items.map(item => {
|
|
|
|
|
|
const name = item.model || item.brand || `Hardware #${item.id}`;
|
|
|
|
|
|
const anydeskId = item.anydesk_id || '-';
|
|
|
|
|
|
const linkBtn = item.anydesk_link
|
|
|
|
|
|
? `<button type="button" class="btn btn-sm btn-outline-primary" onclick="openTicketAnyDeskLink('${item.anydesk_link.replace(/'/g, "\\'")}')">Connect</button>`
|
|
|
|
|
|
: '';
|
|
|
|
|
|
const copyBtn = item.anydesk_id
|
|
|
|
|
|
? `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="copyTicketAnyDeskId('${item.anydesk_id.replace(/'/g, "\\'")}')">Kopiér ID</button>`
|
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="d-flex flex-wrap align-items-center justify-content-between border-bottom py-2">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="fw-bold">${name}</div>
|
|
|
|
|
|
<div class="text-muted small">AnyDesk ID: ${anydeskId}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="d-flex gap-2">
|
|
|
|
|
|
${linkBtn}
|
|
|
|
|
|
${copyBtn}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openTicketAnyDeskLink(link) {
|
|
|
|
|
|
if (!link) return;
|
|
|
|
|
|
window.open(link, '_blank');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function copyTicketAnyDeskId(anydeskId) {
|
|
|
|
|
|
if (!anydeskId) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await navigator.clipboard.writeText(anydeskId);
|
|
|
|
|
|
alert('AnyDesk ID kopieret');
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Copy failed', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function quickCreateTicketHardware() {
|
|
|
|
|
|
const name = document.getElementById('ticketHardwareNameInput').value.trim();
|
|
|
|
|
|
const anydeskId = document.getElementById('ticketHardwareAnyDeskIdInput').value.trim();
|
|
|
|
|
|
const anydeskLink = document.getElementById('ticketHardwareAnyDeskLinkInput').value.trim();
|
|
|
|
|
|
const customerId = document.getElementById('customerId').value;
|
|
|
|
|
|
|
|
|
|
|
|
if (!name) {
|
|
|
|
|
|
alert('Navn er påkrævet');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!customerId) {
|
|
|
|
|
|
alert('Vælg en kunde før du opretter hardware');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/v1/hardware/quick', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
name,
|
|
|
|
|
|
customer_id: parseInt(customerId),
|
|
|
|
|
|
anydesk_id: anydeskId || null,
|
|
|
|
|
|
anydesk_link: anydeskLink || null
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
const err = await response.json();
|
|
|
|
|
|
throw new Error(err.detail || 'Kunne ikke oprette hardware');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('ticketHardwareNameInput').value = '';
|
|
|
|
|
|
document.getElementById('ticketHardwareAnyDeskIdInput').value = '';
|
|
|
|
|
|
document.getElementById('ticketHardwareAnyDeskLinkInput').value = '';
|
|
|
|
|
|
await loadHardwareForTicket();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
alert('Fejl: ' + err.message);
|
|
|
|
|
|
}
|
2025-12-16 15:36:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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 %}
|