671 lines
26 KiB
HTML
671 lines
26 KiB
HTML
|
|
<!-- QuickCreate Modal - AI-Powered Case Creation -->
|
||
|
|
<div class="modal fade" id="quickCreateModal" tabindex="-1" aria-labelledby="quickCreateModalLabel" aria-hidden="true">
|
||
|
|
<div class="modal-dialog modal-lg">
|
||
|
|
<div class="modal-content">
|
||
|
|
<div class="modal-header">
|
||
|
|
<h5 class="modal-title" id="quickCreateModalLabel">
|
||
|
|
<i class="bi bi-plus-circle"></i> Opret Sag
|
||
|
|
<span id="quickCreateLoadingSpinner" class="spinner-border spinner-border-sm ms-2 d-none" role="status">
|
||
|
|
<span class="visually-hidden">Analyserer...</span>
|
||
|
|
</span>
|
||
|
|
</h5>
|
||
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Luk"></button>
|
||
|
|
</div>
|
||
|
|
<div class="modal-body">
|
||
|
|
<form id="quickCreateForm">
|
||
|
|
<!-- Main Text Input -->
|
||
|
|
<div class="mb-4">
|
||
|
|
<label for="quickCreateText" class="form-label fw-bold">
|
||
|
|
Beskriv sagen <span class="text-muted">(AI analyserer automatisk)</span>
|
||
|
|
</label>
|
||
|
|
<textarea
|
||
|
|
class="form-control"
|
||
|
|
id="quickCreateText"
|
||
|
|
rows="6"
|
||
|
|
placeholder="Beskriv kort hvad sagen handler om... Eksempel: 'Kunde 24 ringer. Printer i Hellerup virker ikke. Skal fixes hurtigt.'"
|
||
|
|
autofocus
|
||
|
|
></textarea>
|
||
|
|
<div class="form-text">
|
||
|
|
<i class="bi bi-lightbulb"></i> AI genkender automatisk kunde, prioritet, tekniker og tags mens du skriver.
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- AI Analysis Results (initially hidden) -->
|
||
|
|
<div id="quickCreateAnalysisSection" class="d-none">
|
||
|
|
<hr class="my-4">
|
||
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
|
|
<h6 class="mb-0">
|
||
|
|
<i class="bi bi-robot"></i> AI-forslag
|
||
|
|
<span id="quickCreateConfidenceBadge" class="badge bg-secondary ms-2"></span>
|
||
|
|
</h6>
|
||
|
|
<small class="text-muted" id="quickCreateAiReasoning"></small>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Title -->
|
||
|
|
<div class="mb-3">
|
||
|
|
<label for="quickCreateTitle" class="form-label">Titel</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
class="form-control"
|
||
|
|
id="quickCreateTitle"
|
||
|
|
maxlength="200"
|
||
|
|
required
|
||
|
|
>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Description -->
|
||
|
|
<div class="mb-3">
|
||
|
|
<label for="quickCreateDescription" class="form-label">Beskrivelse</label>
|
||
|
|
<textarea
|
||
|
|
class="form-control"
|
||
|
|
id="quickCreateDescription"
|
||
|
|
rows="4"
|
||
|
|
></textarea>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Customer -->
|
||
|
|
<div class="mb-3">
|
||
|
|
<label for="quickCreateCustomer" class="form-label">Kunde *</label>
|
||
|
|
<div class="input-group">
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
class="form-control"
|
||
|
|
id="quickCreateCustomerSearch"
|
||
|
|
placeholder="Søg kunde..."
|
||
|
|
autocomplete="off"
|
||
|
|
>
|
||
|
|
<button class="btn btn-outline-secondary" type="button" id="quickCreateCustomerClear">
|
||
|
|
<i class="bi bi-x"></i>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<input type="hidden" id="quickCreateCustomerId">
|
||
|
|
<div id="quickCreateCustomerResults" class="list-group mt-1 position-absolute" style="z-index: 1050; max-height: 200px; overflow-y: auto;"></div>
|
||
|
|
<div class="form-text">
|
||
|
|
<span id="quickCreateCustomerDisplay" class="text-success"></span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="row">
|
||
|
|
<!-- Priority -->
|
||
|
|
<div class="col-md-6 mb-3">
|
||
|
|
<label class="form-label">Prioritet</label>
|
||
|
|
<div class="btn-group w-100" role="group" id="quickCreatePriorityGroup">
|
||
|
|
<input type="radio" class="btn-check" name="priority" id="priorityLow" value="low">
|
||
|
|
<label class="btn btn-outline-secondary" for="priorityLow">
|
||
|
|
<i class="bi bi-arrow-down"></i> Lav
|
||
|
|
</label>
|
||
|
|
|
||
|
|
<input type="radio" class="btn-check" name="priority" id="priorityNormal" value="normal" checked>
|
||
|
|
<label class="btn btn-outline-primary" for="priorityNormal">
|
||
|
|
<i class="bi bi-dash"></i> Normal
|
||
|
|
</label>
|
||
|
|
|
||
|
|
<input type="radio" class="btn-check" name="priority" id="priorityHigh" value="high">
|
||
|
|
<label class="btn btn-outline-warning" for="priorityHigh">
|
||
|
|
<i class="bi bi-arrow-up"></i> Høj
|
||
|
|
</label>
|
||
|
|
|
||
|
|
<input type="radio" class="btn-check" name="priority" id="priorityUrgent" value="urgent">
|
||
|
|
<label class="btn btn-outline-danger" for="priorityUrgent">
|
||
|
|
<i class="bi bi-exclamation-triangle"></i> Kritisk
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Technician -->
|
||
|
|
<div class="col-md-6 mb-3">
|
||
|
|
<label for="quickCreateTechnician" class="form-label">Tekniker</label>
|
||
|
|
<select class="form-select" id="quickCreateTechnician">
|
||
|
|
<option value="">Vælg tekniker...</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Group -->
|
||
|
|
<div class="mb-3">
|
||
|
|
<label for="quickCreateGroup" class="form-label">Gruppe</label>
|
||
|
|
<select class="form-select" id="quickCreateGroup">
|
||
|
|
<option value="">Vælg gruppe...</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Tags -->
|
||
|
|
<div class="mb-3">
|
||
|
|
<label class="form-label">Tags</label>
|
||
|
|
<div id="quickCreateTagsContainer" class="d-flex flex-wrap gap-2 mb-2">
|
||
|
|
<!-- Tags will be rendered here -->
|
||
|
|
</div>
|
||
|
|
<div class="input-group">
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
class="form-control"
|
||
|
|
id="quickCreateTagInput"
|
||
|
|
placeholder="Tilføj tag..."
|
||
|
|
>
|
||
|
|
<button class="btn btn-outline-secondary" type="button" id="quickCreateAddTag">
|
||
|
|
<i class="bi bi-plus"></i> Tilføj
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Hardware References (if any) -->
|
||
|
|
<div id="quickCreateHardwareSection" class="mb-3 d-none">
|
||
|
|
<label class="form-label">
|
||
|
|
<i class="bi bi-laptop"></i> Hardware-referencer fundet
|
||
|
|
</label>
|
||
|
|
<div id="quickCreateHardwareList" class="list-group">
|
||
|
|
<!-- Hardware references will be rendered here -->
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Manual Mode Message (shown when AI fails) -->
|
||
|
|
<div id="quickCreateManualMode" class="alert alert-info d-none" role="alert">
|
||
|
|
<i class="bi bi-info-circle"></i> AI-assistent utilgængelig. Udfyld felterne manuelt.
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
<div class="modal-footer">
|
||
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
|
||
|
|
<button type="button" class="btn btn-primary" id="quickCreateSubmit" disabled>
|
||
|
|
<i class="bi bi-check-circle"></i> Opret Sag
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
(function() {
|
||
|
|
const modal = document.getElementById('quickCreateModal');
|
||
|
|
const form = document.getElementById('quickCreateForm');
|
||
|
|
const textInput = document.getElementById('quickCreateText');
|
||
|
|
const analysisSection = document.getElementById('quickCreateAnalysisSection');
|
||
|
|
const manualMode = document.getElementById('quickCreateManualMode');
|
||
|
|
const loadingSpinner = document.getElementById('quickCreateLoadingSpinner');
|
||
|
|
const submitBtn = document.getElementById('quickCreateSubmit');
|
||
|
|
|
||
|
|
// Form fields
|
||
|
|
const titleInput = document.getElementById('quickCreateTitle');
|
||
|
|
const descriptionInput = document.getElementById('quickCreateDescription');
|
||
|
|
const customerSearchInput = document.getElementById('quickCreateCustomerSearch');
|
||
|
|
const customerIdInput = document.getElementById('quickCreateCustomerId');
|
||
|
|
const customerDisplay = document.getElementById('quickCreateCustomerDisplay');
|
||
|
|
const customerResults = document.getElementById('quickCreateCustomerResults');
|
||
|
|
const customerClearBtn = document.getElementById('quickCreateCustomerClear');
|
||
|
|
const technicianSelect = document.getElementById('quickCreateTechnician');
|
||
|
|
const groupSelect = document.getElementById('quickCreateGroup');
|
||
|
|
const tagsContainer = document.getElementById('quickCreateTagsContainer');
|
||
|
|
const tagInput = document.getElementById('quickCreateTagInput');
|
||
|
|
const addTagBtn = document.getElementById('quickCreateAddTag');
|
||
|
|
const confidenceBadge = document.getElementById('quickCreateConfidenceBadge');
|
||
|
|
const aiReasoning = document.getElementById('quickCreateAiReasoning');
|
||
|
|
const hardwareSection = document.getElementById('quickCreateHardwareSection');
|
||
|
|
const hardwareList = document.getElementById('quickCreateHardwareList');
|
||
|
|
|
||
|
|
let analysisDebounce;
|
||
|
|
let currentTags = [];
|
||
|
|
let currentHardware = [];
|
||
|
|
let customerSearchDebounce;
|
||
|
|
let cachedUserId = null;
|
||
|
|
|
||
|
|
// Get user ID from JWT token or meta tag
|
||
|
|
function getUserId() {
|
||
|
|
if (cachedUserId) return cachedUserId;
|
||
|
|
|
||
|
|
// Try JWT token first
|
||
|
|
const token = localStorage.getItem('access_token') || sessionStorage.getItem('access_token');
|
||
|
|
if (token) {
|
||
|
|
try {
|
||
|
|
const payload = JSON.parse(atob(token.split('.')[1]));
|
||
|
|
cachedUserId = payload.sub || payload.user_id;
|
||
|
|
return cachedUserId;
|
||
|
|
} catch (e) {
|
||
|
|
console.warn('Could not decode token for user_id');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fallback to meta tag
|
||
|
|
const metaTag = document.querySelector('meta[name="user-id"]');
|
||
|
|
if (metaTag) {
|
||
|
|
cachedUserId = metaTag.getAttribute('content');
|
||
|
|
return cachedUserId;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Last resort: hardcoded value (not ideal but prevents errors)
|
||
|
|
console.warn('Could not get user_id, using default value 1');
|
||
|
|
return '1';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Initialize on modal show
|
||
|
|
modal.addEventListener('show.bs.modal', function() {
|
||
|
|
resetForm();
|
||
|
|
loadTechnicians();
|
||
|
|
loadGroups();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Text input handler - triggers AI analysis
|
||
|
|
textInput.addEventListener('input', function(e) {
|
||
|
|
clearTimeout(analysisDebounce);
|
||
|
|
const text = e.target.value.trim();
|
||
|
|
|
||
|
|
if (text.length < 10) {
|
||
|
|
hideAnalysisSection();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
showLoadingState();
|
||
|
|
|
||
|
|
analysisDebounce = setTimeout(async () => {
|
||
|
|
await performAnalysis(text);
|
||
|
|
}, 800); // 800ms debounce
|
||
|
|
});
|
||
|
|
|
||
|
|
// Customer search with debounce
|
||
|
|
customerSearchInput.addEventListener('input', function(e) {
|
||
|
|
clearTimeout(customerSearchDebounce);
|
||
|
|
const query = e.target.value.trim();
|
||
|
|
|
||
|
|
if (query.length < 2) {
|
||
|
|
customerResults.innerHTML = '';
|
||
|
|
customerResults.classList.remove('show');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
customerSearchDebounce = setTimeout(async () => {
|
||
|
|
await searchCustomers(query);
|
||
|
|
}, 300);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Customer clear button
|
||
|
|
customerClearBtn.addEventListener('click', function() {
|
||
|
|
customerSearchInput.value = '';
|
||
|
|
customerIdInput.value = '';
|
||
|
|
customerDisplay.textContent = '';
|
||
|
|
customerResults.innerHTML = '';
|
||
|
|
validateForm();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Tag management
|
||
|
|
addTagBtn.addEventListener('click', addTag);
|
||
|
|
tagInput.addEventListener('keypress', function(e) {
|
||
|
|
if (e.key === 'Enter') {
|
||
|
|
e.preventDefault();
|
||
|
|
addTag();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Form submission
|
||
|
|
submitBtn.addEventListener('click', async function() {
|
||
|
|
await submitCase();
|
||
|
|
});
|
||
|
|
|
||
|
|
async function performAnalysis(text) {
|
||
|
|
try {
|
||
|
|
const userId = getUserId();
|
||
|
|
const response = await fetch(`/api/v1/sag/analyze-quick-create?user_id=${userId}`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {'Content-Type': 'application/json'},
|
||
|
|
credentials: 'include',
|
||
|
|
body: JSON.stringify({text})
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Analysis failed');
|
||
|
|
}
|
||
|
|
|
||
|
|
const analysis = await response.json();
|
||
|
|
populateFields(analysis);
|
||
|
|
showAnalysisSection();
|
||
|
|
hideLoadingState();
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error('AI analysis error:', error);
|
||
|
|
showManualMode();
|
||
|
|
hideLoadingState();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function populateFields(analysis) {
|
||
|
|
// Title and description
|
||
|
|
titleInput.value = analysis.suggested_title || '';
|
||
|
|
descriptionInput.value = analysis.suggested_description || '';
|
||
|
|
|
||
|
|
// Customer
|
||
|
|
if (analysis.suggested_customer_id) {
|
||
|
|
customerIdInput.value = analysis.suggested_customer_id;
|
||
|
|
customerSearchInput.value = analysis.suggested_customer_name || '';
|
||
|
|
customerDisplay.textContent = `✓ ${analysis.suggested_customer_name}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Priority
|
||
|
|
const priorityValue = analysis.suggested_priority || 'normal';
|
||
|
|
document.getElementById(`priority${capitalizeFirst(priorityValue)}`).checked = true;
|
||
|
|
|
||
|
|
// Technician
|
||
|
|
if (analysis.suggested_technician_id) {
|
||
|
|
technicianSelect.value = analysis.suggested_technician_id;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Group
|
||
|
|
if (analysis.suggested_group_id) {
|
||
|
|
groupSelect.value = analysis.suggested_group_id;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Tags
|
||
|
|
currentTags = analysis.suggested_tags || [];
|
||
|
|
renderTags();
|
||
|
|
|
||
|
|
// Hardware
|
||
|
|
currentHardware = analysis.hardware_references || [];
|
||
|
|
if (currentHardware.length > 0) {
|
||
|
|
renderHardware();
|
||
|
|
hardwareSection.classList.remove('d-none');
|
||
|
|
} else {
|
||
|
|
hardwareSection.classList.add('d-none');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Confidence badge
|
||
|
|
const confidence = analysis.confidence || 0;
|
||
|
|
updateConfidenceBadge(confidence);
|
||
|
|
|
||
|
|
// AI reasoning
|
||
|
|
if (analysis.ai_reasoning) {
|
||
|
|
aiReasoning.textContent = analysis.ai_reasoning;
|
||
|
|
aiReasoning.classList.remove('d-none');
|
||
|
|
} else {
|
||
|
|
aiReasoning.classList.add('d-none');
|
||
|
|
}
|
||
|
|
|
||
|
|
validateForm();
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateConfidenceBadge(confidence) {
|
||
|
|
let badgeClass = 'bg-secondary';
|
||
|
|
let text = 'Lav sikkerhed';
|
||
|
|
|
||
|
|
if (confidence >= 0.8) {
|
||
|
|
badgeClass = 'bg-success';
|
||
|
|
text = 'Høj sikkerhed';
|
||
|
|
} else if (confidence >= 0.6) {
|
||
|
|
badgeClass = 'bg-info';
|
||
|
|
text = 'Middel sikkerhed';
|
||
|
|
} else if (confidence >= 0.4) {
|
||
|
|
badgeClass = 'bg-warning';
|
||
|
|
text = 'Lav sikkerhed';
|
||
|
|
} else {
|
||
|
|
badgeClass = 'bg-danger';
|
||
|
|
text = 'Meget lav sikkerhed';
|
||
|
|
}
|
||
|
|
|
||
|
|
confidenceBadge.className = `badge ${badgeClass} ms-2`;
|
||
|
|
confidenceBadge.textContent = `${text} (${Math.round(confidence * 100)}%)`;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function searchCustomers(query) {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/v1/search/customers?q=${encodeURIComponent(query)}`, {
|
||
|
|
credentials: 'include'
|
||
|
|
});
|
||
|
|
const customers = await response.json();
|
||
|
|
|
||
|
|
if (customers.length === 0) {
|
||
|
|
customerResults.innerHTML = '<div class="list-group-item text-muted">Ingen kunder fundet</div>';
|
||
|
|
} else {
|
||
|
|
customerResults.innerHTML = customers.map(c => `
|
||
|
|
<button type="button" class="list-group-item list-group-item-action" data-id="${c.id}" data-name="${c.name}">
|
||
|
|
<strong>${c.name}</strong>
|
||
|
|
${c.cvr_nummer ? `<br><small class="text-muted">CVR: ${c.cvr_nummer}</small>` : ''}
|
||
|
|
</button>
|
||
|
|
`).join('');
|
||
|
|
|
||
|
|
// Add click handlers
|
||
|
|
customerResults.querySelectorAll('button').forEach(btn => {
|
||
|
|
btn.addEventListener('click', function() {
|
||
|
|
selectCustomer(this.dataset.id, this.dataset.name);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
customerResults.classList.add('show');
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Customer search error:', error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function selectCustomer(id, name) {
|
||
|
|
customerIdInput.value = id;
|
||
|
|
customerSearchInput.value = name;
|
||
|
|
customerDisplay.textContent = `✓ ${name}`;
|
||
|
|
customerResults.innerHTML = '';
|
||
|
|
customerResults.classList.remove('show');
|
||
|
|
validateForm();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadTechnicians() {
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/v1/users?is_active=true', {
|
||
|
|
credentials: 'include'
|
||
|
|
});
|
||
|
|
const users = await response.json();
|
||
|
|
|
||
|
|
technicianSelect.innerHTML = '<option value="">Vælg tekniker...</option>' +
|
||
|
|
users.map(u => `<option value="${u.id}">${u.full_name || u.username}</option>`).join('');
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error loading technicians:', error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadGroups() {
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/v1/groups', {
|
||
|
|
credentials: 'include'
|
||
|
|
});
|
||
|
|
const groups = await response.json();
|
||
|
|
|
||
|
|
groupSelect.innerHTML = '<option value="">Vælg gruppe...</option>' +
|
||
|
|
groups.map(g => `<option value="${g.id}">${g.name}</option>`).join('');
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error loading groups:', error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function addTag() {
|
||
|
|
const tag = tagInput.value.trim();
|
||
|
|
if (tag && !currentTags.includes(tag)) {
|
||
|
|
currentTags.push(tag);
|
||
|
|
renderTags();
|
||
|
|
tagInput.value = '';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function removeTag(tag) {
|
||
|
|
currentTags = currentTags.filter(t => t !== tag);
|
||
|
|
renderTags();
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderTags() {
|
||
|
|
tagsContainer.innerHTML = currentTags.map(tag => `
|
||
|
|
<span class="badge bg-secondary">
|
||
|
|
${tag}
|
||
|
|
<button type="button" class="btn-close btn-close-white ms-1" style="font-size: 0.6rem;" data-tag="${tag}"></button>
|
||
|
|
</span>
|
||
|
|
`).join('');
|
||
|
|
|
||
|
|
// Add remove handlers
|
||
|
|
tagsContainer.querySelectorAll('.btn-close').forEach(btn => {
|
||
|
|
btn.addEventListener('click', function() {
|
||
|
|
removeTag(this.dataset.tag);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderHardware() {
|
||
|
|
hardwareList.innerHTML = currentHardware.map(hw => `
|
||
|
|
<div class="list-group-item">
|
||
|
|
<strong>${hw.brand} ${hw.model}</strong>
|
||
|
|
${hw.serial_number ? `<br><small class="text-muted">Serial: ${hw.serial_number}</small>` : ''}
|
||
|
|
</div>
|
||
|
|
`).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
function validateForm() {
|
||
|
|
const hasTitle = titleInput.value.trim().length > 0;
|
||
|
|
const hasCustomer = customerIdInput.value.length > 0;
|
||
|
|
|
||
|
|
submitBtn.disabled = !(hasTitle && hasCustomer);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add validation listeners
|
||
|
|
titleInput.addEventListener('input', validateForm);
|
||
|
|
|
||
|
|
async function submitCase() {
|
||
|
|
const priority = document.querySelector('input[name="priority"]:checked').value;
|
||
|
|
const userId = getUserId();
|
||
|
|
|
||
|
|
const caseData = {
|
||
|
|
titel: titleInput.value.trim(),
|
||
|
|
beskrivelse: descriptionInput.value.trim(),
|
||
|
|
customer_id: parseInt(customerIdInput.value),
|
||
|
|
ansvarlig_bruger_id: technicianSelect.value ? parseInt(technicianSelect.value) : null,
|
||
|
|
assigned_group_id: groupSelect.value ? parseInt(groupSelect.value) : null,
|
||
|
|
priority: priority,
|
||
|
|
created_by_user_id: parseInt(userId),
|
||
|
|
template_key: 'quickcreate'
|
||
|
|
};
|
||
|
|
|
||
|
|
try {
|
||
|
|
submitBtn.disabled = true;
|
||
|
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Opretter...';
|
||
|
|
|
||
|
|
const response = await fetch('/api/v1/sag', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {'Content-Type': 'application/json'},
|
||
|
|
credentials: 'include',
|
||
|
|
body: JSON.stringify(caseData)
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Failed to create case');
|
||
|
|
}
|
||
|
|
|
||
|
|
const newCase = await response.json();
|
||
|
|
|
||
|
|
// TODO: Add tags via separate endpoint if any exist
|
||
|
|
|
||
|
|
// Redirect to case detail
|
||
|
|
window.location.href = `/sag/${newCase.id}`;
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error creating case:', error);
|
||
|
|
alert('Fejl ved oprettelse af sag. Prøv igen.');
|
||
|
|
submitBtn.disabled = false;
|
||
|
|
submitBtn.innerHTML = '<i class="bi bi-check-circle"></i> Opret Sag';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function showLoadingState() {
|
||
|
|
loadingSpinner.classList.remove('d-none');
|
||
|
|
}
|
||
|
|
|
||
|
|
function hideLoadingState() {
|
||
|
|
loadingSpinner.classList.add('d-none');
|
||
|
|
}
|
||
|
|
|
||
|
|
function showAnalysisSection() {
|
||
|
|
analysisSection.classList.remove('d-none');
|
||
|
|
manualMode.classList.add('d-none');
|
||
|
|
}
|
||
|
|
|
||
|
|
function hideAnalysisSection() {
|
||
|
|
analysisSection.classList.add('d-none');
|
||
|
|
manualMode.classList.add('d-none');
|
||
|
|
}
|
||
|
|
|
||
|
|
function showManualMode() {
|
||
|
|
manualMode.classList.remove('d-none');
|
||
|
|
analysisSection.classList.remove('d-none');
|
||
|
|
|
||
|
|
// Pre-fill description with original text
|
||
|
|
descriptionInput.value = textInput.value;
|
||
|
|
}
|
||
|
|
|
||
|
|
function resetForm() {
|
||
|
|
form.reset();
|
||
|
|
textInput.value = '';
|
||
|
|
customerIdInput.value = '';
|
||
|
|
customerDisplay.textContent = '';
|
||
|
|
currentTags = [];
|
||
|
|
currentHardware = [];
|
||
|
|
hideAnalysisSection();
|
||
|
|
hideLoadingState();
|
||
|
|
submitBtn.disabled = true;
|
||
|
|
renderTags();
|
||
|
|
}
|
||
|
|
|
||
|
|
function capitalizeFirst(str) {
|
||
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Close customer results when clicking outside
|
||
|
|
document.addEventListener('click', function(e) {
|
||
|
|
if (!customerSearchInput.contains(e.target) && !customerResults.contains(e.target)) {
|
||
|
|
customerResults.innerHTML = '';
|
||
|
|
customerResults.classList.remove('show');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
})();
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
#quickCreateModal .modal-body {
|
||
|
|
max-height: 70vh;
|
||
|
|
overflow-y: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
#quickCreateCustomerResults {
|
||
|
|
max-width: 100%;
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
#quickCreateCustomerResults.show {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
|
||
|
|
#quickCreateTagsContainer .badge {
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
padding: 0.5rem 0.75rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
#quickCreateHardwareList .list-group-item {
|
||
|
|
padding: 0.75rem 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Priority button group styling */
|
||
|
|
#quickCreatePriorityGroup label {
|
||
|
|
flex: 1;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
#quickCreatePriorityGroup .btn-check:checked + label.btn-outline-secondary {
|
||
|
|
background-color: #6c757d;
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
|
||
|
|
#quickCreatePriorityGroup .btn-check:checked + label.btn-outline-primary {
|
||
|
|
background-color: var(--bs-primary);
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
|
||
|
|
#quickCreatePriorityGroup .btn-check:checked + label.btn-outline-warning {
|
||
|
|
background-color: #ffc107;
|
||
|
|
color: #000;
|
||
|
|
}
|
||
|
|
|
||
|
|
#quickCreatePriorityGroup .btn-check:checked + label.btn-outline-danger {
|
||
|
|
background-color: #dc3545;
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
</style>
|