bmc_hub/app/modules/sag/templates/create.html
Christian 891180f3f0 Refactor opportunities and settings management
- Removed opportunity detail page route from views.py.
- Deleted opportunity_service.py as it is no longer needed.
- Updated router.py to seed new setting for case_type_module_defaults.
- Enhanced settings.html to include standard modules per case type with UI for selection.
- Implemented JavaScript functions to manage case type module defaults.
- Added RelationService for handling case relations with a tree structure.
- Created migration scripts (128 and 129) for new pipeline fields and descriptions.
- Added script to fix relation types in the database.
2026-02-15 11:12:58 +01:00

926 lines
40 KiB
HTML

{% extends "shared/frontend/base.html" %}
{% block title %}Ny Sag - BMC Hub{% endblock %}
{% block extra_css %}
<style>
/* Gradient Header for the Card */
.card-header-custom {
background: linear-gradient(135deg, var(--bmc-blue, #0f4c75) 0%, #3282b8 100%);
color: white;
padding: 1.5rem;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.card-custom {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
overflow: visible; /* Changed from hidden to visible for shadows/tooltips */
}
.form-label {
font-weight: 600;
color: var(--text-primary);
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.form-control:focus, .form-select:focus {
border-color: #3282b8;
box-shadow: 0 0 0 0.25rem rgba(50, 130, 184, 0.25);
}
/* Search Results Dropdown */
.search-position-relative {
position: relative;
}
.search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg-surface, #ffffff);
border: 1px solid var(--border-color, rgba(0,0,0,0.1));
border-radius: 8px;
max-height: 250px;
overflow-y: auto;
z-index: 1050;
margin-top: 5px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.search-result-item {
padding: 10px 15px;
border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.05));
cursor: pointer;
transition: background-color 0.15s ease;
}
.search-result-item:hover {
background-color: var(--bg-hover, #f8f9fa);
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-name {
font-weight: 600;
color: var(--text-primary);
font-size: 0.95rem;
}
.search-result-meta {
font-size: 0.8rem;
color: var(--text-secondary, #6c757d);
}
/* Selected Items (Tags) */
.selected-item {
display: inline-flex;
align-items: center;
background-color: #e3f2fd;
color: #0f4c75;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
margin-right: 8px;
margin-bottom: 8px;
border: 1px solid #bbdefb;
}
.selected-item button {
background: none;
border: none;
color: #0f4c75;
margin-left: 8px;
padding: 0;
line-height: 1;
font-size: 1.1rem;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s;
}
.selected-item button:hover {
opacity: 1;
}
/* Dark Mode Adjustments */
[data-bs-theme="dark"] .card-custom {
background-color: var(--bg-surface);
}
[data-bs-theme="dark"] .selected-item {
background-color: rgba(50, 130, 184, 0.2);
color: #a6d5fa;
border-color: rgba(50, 130, 184, 0.4);
}
[data-bs-theme="dark"] .selected-item button {
color: #a6d5fa;
}
</style>
{% endblock %}
{% block content %}
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<!-- Main Card -->
<div class="card card-custom">
<div class="card-header-custom">
<h2 class="mb-0 fs-4 fw-bold"><i class="bi bi-plus-circle me-2"></i>Opret Ny Sag</h2>
<p class="mb-0 opacity-75 small mt-1">Udfyld formularen for at oprette en ny sag i systemet.</p>
</div>
<div class="card-body p-4">
<!-- Notifications -->
<div id="error" class="alert alert-danger d-none shadow-sm" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i><span id="error-text"></span>
</div>
<div id="success" class="alert alert-success d-none shadow-sm" role="alert">
<i class="bi bi-check-circle-fill me-2"></i><span id="success-text"></span>
</div>
<form id="createForm" novalidate>
<!-- Section: Relations -->
<h5 class="mb-3 text-muted fw-bold small text-uppercase">Relationer</h5>
<div class="row g-4 mb-4">
<!-- Contact Search -->
<div class="col-md-6">
<label class="form-label">Kontaktpersoner</label>
<div class="search-position-relative">
<div class="input-group">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-person-plus"></i></span>
<input type="text" id="contactSearch" class="form-control border-start-0 ps-0" placeholder="Søg kontakt...">
</div>
<div id="contactResults" class="search-results shadow-sm d-none"></div>
</div>
<div id="selectedContacts" class="mt-2 text-wrap"></div>
</div>
<!-- Company Search -->
<div class="col-md-6">
<label class="form-label">Firma</label>
<div class="search-position-relative">
<div class="input-group">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-building"></i></span>
<input type="text" id="customerSearch" class="form-control border-start-0 ps-0" placeholder="Søg firma (min. 2 tegn)...">
</div>
<div id="customerResults" class="search-results shadow-sm d-none"></div>
</div>
<div id="selectedCustomer" class="mt-2 min-vh-20"></div>
<input type="hidden" id="customer_id" name="customer_id">
</div>
</div>
<hr class="my-4 opacity-25">
<!-- Section: Basic Info -->
<div class="row g-4 mb-4">
<div class="col-md-12">
<label for="titel" class="form-label">Titel <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-type-h1"></i></span>
<input type="text" class="form-control border-start-0 ps-0" id="titel" placeholder="Kort og præcis titel (f.eks. 'Netværksproblemer hos X')" required>
</div>
</div>
<div class="col-md-12">
<label for="beskrivelse" class="form-label">Beskrivelse</label>
<textarea class="form-control" id="beskrivelse" rows="5" placeholder="Beskriv problemstillingen detaljeret..."></textarea>
<div class="form-text text-end" id="charCount">0 tegn</div>
</div>
</div>
<hr class="my-4 opacity-25">
<!-- Section: Hardware & AnyDesk -->
<h5 class="mb-3 text-muted fw-bold small text-uppercase">Hardware (AnyDesk)</h5>
<div class="row g-4 mb-4">
<div class="col-12">
<div id="hardwareList" class="border rounded-3 p-3 bg-light">
<div class="text-muted small">Vælg en kontakt for at se relateret hardware.</div>
</div>
</div>
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h6 class="fw-bold mb-3">Quick opret hardware</h6>
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Navn *</label>
<input type="text" class="form-control" id="hardwareNameInput" placeholder="PC, NAS, Server...">
</div>
<div class="col-md-4">
<label class="form-label">AnyDesk ID</label>
<input type="text" class="form-control" id="hardwareAnyDeskIdInput" placeholder="123-456-789">
<div class="form-text small text-muted">Link genereres automatisk.</div>
</div>
<div class="col-12 d-flex justify-content-end">
<button type="button" class="btn btn-outline-primary" onclick="quickCreateHardware()">
<i class="bi bi-plus-circle me-2"></i>Opret hardware
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<hr class="my-4 opacity-25">
<!-- Section: Metadata -->
<h5 class="mb-3 text-muted fw-bold small text-uppercase">Type, Status & Ansvar</h5>
<div class="row g-4 mb-4">
<div class="col-md-4">
<label for="type" class="form-label">Type <span class="text-danger">*</span></label>
<select class="form-select" id="type" required>
<option value="ticket" selected>🎫 Ticket</option>
<option value="opgave">🧩 Opgave</option>
<option value="ordre">🧾 Ordre</option>
<option value="projekt">📁 Projekt</option>
<option value="service">🛠️ Service</option>
</select>
</div>
<div class="col-md-4">
<label for="status" class="form-label">Status <span class="text-danger">*</span></label>
<select class="form-select" id="status" required>
<option value="åben" selected>🟢 Åben</option>
<option value="afventer">🟡 Afventer</option>
<option value="lukket">🔴 Lukket</option>
</select>
</div>
<div class="col-md-4">
<label for="ansvarlig_bruger_id" class="form-label">Ansvarlig (ID)</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-person-badge"></i></span>
<input type="number" class="form-control border-start-0 ps-0" id="ansvarlig_bruger_id" placeholder="Bruger ID">
</div>
</div>
<div class="col-md-4">
<label for="deadline" class="form-label">Deadline</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-calendar-event"></i></span>
<input type="datetime-local" class="form-control border-start-0 ps-0" id="deadline">
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="d-flex justify-content-end gap-3 mt-5">
<a href="/sag" class="btn btn-light border px-4 fw-bold">Annuller</a>
<button type="submit" class="btn btn-primary px-5 fw-bold shadow-sm" id="submitBtn">
<span class="d-flex align-items-center">
<i class="bi bi-check-lg me-2"></i>Opret Sag
</span>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
let selectedCustomer = null;
let selectedContacts = {};
let selectedContactsCompanies = {};
let customerSearchTimeout;
let contactSearchTimeout;
let telefoniPrefill = { contactId: null, title: null, callId: null, customerId: null, description: null };
// --- Character Counter ---
const beskrInput = document.getElementById('beskrivelse');
if (beskrInput) {
beskrInput.addEventListener('input', function(e) {
document.getElementById('charCount').textContent = e.target.value.length + " tegn";
});
}
// --- Search Logic ---
function initializeSearch() {
// Customer Search
const customerInput = document.getElementById('customerSearch');
if (customerInput) {
customerInput.addEventListener('input', (e) => handleSearch(e, 'customer'));
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.search-position-relative')) {
const cr = document.getElementById('customerResults');
if(cr) cr.classList.add('d-none');
}
});
}
// Contact Search
const contactInput = document.getElementById('contactSearch');
if (contactInput) {
contactInput.addEventListener('input', (e) => handleSearch(e, 'contact'));
document.addEventListener('click', (e) => {
if (!e.target.closest('.search-position-relative')) {
const cr = document.getElementById('contactResults');
if(cr) cr.classList.add('d-none');
}
});
}
}
function handleSearch(event, type) {
const query = event.target.value.trim();
const resultsId = type === 'customer' ? 'customerResults' : 'contactResults';
const timeoutVar = type === 'customer' ? customerSearchTimeout : contactSearchTimeout;
const resultsDiv = document.getElementById(resultsId);
clearTimeout(timeoutVar);
if (query.length < 2) {
resultsDiv.classList.add('d-none');
return;
}
const timeout = setTimeout(async () => {
try {
// Show loading state
resultsDiv.classList.remove('d-none');
resultsDiv.innerHTML = '<div class="p-3 text-muted small"><span class="spinner-border spinner-border-sm me-2"></span>Søger...</div>';
const endpoint = type === 'customer' ? '/api/v1/search/customers' : '/api/v1/search/contacts';
const response = await fetch(`${endpoint}?q=${encodeURIComponent(query)}`);
if (!response.ok) {
const errorText = await response.text();
resultsDiv.innerHTML = `<div class="p-3 text-danger small">Fejl ved søgning: ${errorText}</div>`;
return;
}
const data = await response.json();
if (!Array.isArray(data)) {
resultsDiv.innerHTML = '<div class="p-3 text-danger small">Fejl ved søgning</div>';
return;
}
if (data.length === 0) {
const quickAction = type === 'customer'
? `<button class="btn btn-sm btn-outline-primary mt-2" onclick="quickCreateCustomer('${query.replace(/'/g, "\\'")}')">Opret nyt firma</button>`
: `<button class="btn btn-sm btn-outline-primary mt-2" onclick="quickCreateContact('${query.replace(/'/g, "\\'")}')">Opret ny kontakt</button>`;
resultsDiv.innerHTML = `
<div class="p-3 text-muted small">Ingen fundet</div>
<div class="p-3 pt-0">${quickAction}</div>
`;
} else {
resultsDiv.innerHTML = data.map(item => {
const name = type === 'customer' ? item.name : `${item.first_name} ${item.last_name}`;
const meta = item.email || (type === 'customer' ? 'CVR: ' + (item.cvr_nummer || '-') : '-');
// Handle escaping for JS function call
const safeName = name.replace(/'/g, "\\'");
const fn = type === 'customer' ? `selectCustomer(${item.id}, '${safeName}')` : `selectContact(${item.id}, '${safeName}')`;
return `
<div class="search-result-item" onclick="${fn}">
<div class="search-result-name">${name}</div>
<div class="search-result-meta">${meta}</div>
</div>
`;
}).join('');
}
} catch (err) {
console.error('Search error:', err);
resultsDiv.innerHTML = '<div class="p-3 text-danger small">Fejl ved søgning</div>';
}
}, 300);
if (type === 'customer') customerSearchTimeout = timeout;
else contactSearchTimeout = timeout;
}
// --- Selection Logic ---
function selectCustomer(id, name) {
selectedCustomer = { id, name };
document.getElementById('customer_id').value = id;
document.getElementById('customerSearch').value = '';
document.getElementById('customerResults').classList.add('d-none');
renderSelections();
}
function removeCustomer() {
selectedCustomer = null;
document.getElementById('customer_id').value = '';
renderSelections();
}
async function selectContact(id, name) {
if (!selectedContacts[id]) {
selectedContacts[id] = { id, name };
}
document.getElementById('contactSearch').value = '';
document.getElementById('contactResults').classList.add('d-none');
renderSelections();
// Check for associated company (auto-select if single match)
try {
const response = await fetch(`/api/v1/contacts/${id}`);
if (response.ok) {
const data = await response.json();
selectedContactsCompanies[id] = data.companies || [];
if (data.companies && data.companies.length === 1) {
const company = data.companies[0];
if (!selectedCustomer) {
selectCustomer(company.id, company.name);
// Show brief notification
const successDiv = document.getElementById('success');
successDiv.classList.remove('d-none');
document.getElementById('success-text').textContent = `Valgte automatisk kunde: ${company.name}`;
setTimeout(() => successDiv.classList.add('d-none'), 3000);
}
}
}
} catch (e) {
console.error("Auto-select company failed", e);
}
loadHardwareForContacts();
}
function readTelefoniPrefill() {
const params = new URLSearchParams(window.location.search || '');
const contactIdRaw = params.get('contact_id');
const titleRaw = params.get('title');
const callIdRaw = params.get('telefoni_opkald_id');
const customerIdRaw = params.get('customer_id');
const descriptionRaw = params.get('description');
const contactId = contactIdRaw ? parseInt(contactIdRaw) : null;
const customerId = customerIdRaw ? parseInt(customerIdRaw) : null;
telefoniPrefill.contactId = Number.isFinite(contactId) ? contactId : null;
telefoniPrefill.customerId = Number.isFinite(customerId) ? customerId : null;
telefoniPrefill.title = titleRaw ? String(titleRaw) : null;
telefoniPrefill.callId = callIdRaw ? String(callIdRaw) : null;
telefoniPrefill.description = descriptionRaw ? String(descriptionRaw) : null;
}
async function applyTelefoniPrefill() {
readTelefoniPrefill();
if (telefoniPrefill.title) {
const titelInput = document.getElementById('titel');
if (titelInput && !titelInput.value.trim()) {
titelInput.value = telefoniPrefill.title;
}
}
if (telefoniPrefill.description) {
const beskrInput = document.getElementById('beskrivelse');
if (beskrInput && !beskrInput.value.trim()) {
beskrInput.value = telefoniPrefill.description;
const charCount = document.getElementById('charCount');
if (charCount) {
charCount.textContent = beskrInput.value.length + " tegn";
}
}
}
if (telefoniPrefill.customerId && !selectedCustomer) {
try {
const customerRes = await fetch(`/api/v1/customers/${telefoniPrefill.customerId}`);
if (customerRes.ok) {
const customer = await customerRes.json();
const customerName = customer.name || `Kunde #${telefoniPrefill.customerId}`;
selectCustomer(telefoniPrefill.customerId, customerName);
}
} catch (e) {
console.error('Customer prefill failed', e);
}
}
if (telefoniPrefill.contactId) {
try {
const res = await fetch(`/api/v1/contacts/${telefoniPrefill.contactId}`);
if (!res.ok) return;
const c = await res.json();
const name = `${c.first_name || ''} ${c.last_name || ''}`.trim() || `Kontakt #${telefoniPrefill.contactId}`;
await selectContact(telefoniPrefill.contactId, name);
} catch (e) {
console.error('Telefoni prefill failed', e);
}
}
}
function removeContact(id) {
delete selectedContacts[id];
delete selectedContactsCompanies[id];
renderSelections();
loadHardwareForContacts();
}
function renderSelections() {
// Customer
const custDiv = document.getElementById('selectedCustomer');
if (selectedCustomer) {
custDiv.innerHTML = `
<div class="selected-item border-primary bg-primary bg-opacity-10 text-primary">
<i class="bi bi-building me-2"></i>${selectedCustomer.name}
<button type="button" onclick="removeCustomer()" class="text-primary hover-opacity"><i class="bi bi-x"></i></button>
</div>`;
} else {
custDiv.innerHTML = '';
}
// Contacts
const contDiv = document.getElementById('selectedContacts');
contDiv.innerHTML = Object.values(selectedContacts).map(c => `
<div class="selected-item">
<i class="bi bi-person me-2"></i>${c.name}
<button type="button" onclick="removeContact(${c.id})"><i class="bi bi-x"></i></button>
</div>
`).join('');
}
async function loadHardwareForContacts() {
const hardwareList = document.getElementById('hardwareList');
if (!hardwareList) return;
const contactIds = Object.keys(selectedContacts).map(id => parseInt(id));
if (contactIds.length === 0) {
hardwareList.innerHTML = '<div class="text-muted small">Vælg en kontakt for at se relateret hardware.</div>';
return;
}
hardwareList.innerHTML = '<div class="text-muted small"><span class="spinner-border spinner-border-sm me-2"></span>Henter hardware...</div>';
try {
const responses = await Promise.all(
contactIds.map(contactId => fetch(`/api/v1/hardware/by-contact/${contactId}`))
);
const datasets = await Promise.all(responses.map(r => r.ok ? r.json() : []));
const merged = new Map();
datasets.flat().forEach(item => {
if (!merged.has(item.id)) {
merged.set(item.id, item);
}
});
renderHardwareList(Array.from(merged.values()));
} catch (err) {
console.error('Failed to load hardware:', err);
hardwareList.innerHTML = '<div class="text-danger small">Kunne ikke hente hardware.</div>';
}
}
function renderHardwareList(items) {
const hardwareList = document.getElementById('hardwareList');
if (!hardwareList) return;
if (!items || items.length === 0) {
hardwareList.innerHTML = '<div class="text-muted small">Ingen hardware fundet for valgte kontakt(er).</div>';
return;
}
hardwareList.innerHTML = items.map(item => {
const name = item.model || item.brand || `Hardware #${item.id}`;
const anydeskId = item.anydesk_id || '-';
const anydeskLink = item.anydesk_link || '';
const linkBtn = anydeskLink
? `<button type="button" class="btn btn-sm btn-outline-primary" onclick="openAnyDeskLink('${anydeskLink.replace(/'/g, "\\'")}')">Connect</button>`
: '';
const copyBtn = item.anydesk_id
? `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="copyAnyDeskId('${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 openAnyDeskLink(link) {
if (!link) return;
window.open(link, '_blank');
}
async function copyAnyDeskId(anydeskId) {
if (!anydeskId) return;
try {
await navigator.clipboard.writeText(anydeskId);
alert('AnyDesk ID kopieret');
} catch (err) {
console.error('Copy failed', err);
}
}
async function quickCreateHardware() {
const name = document.getElementById('hardwareNameInput').value.trim();
const anydeskId = document.getElementById('hardwareAnyDeskIdInput').value.trim();
const anydeskLink = anydeskId ? `anydesk://${anydeskId}` : null;
if (!name) {
alert('Navn er påkrævet');
return;
}
const customerId = selectedCustomer?.id || getSingleContactCompanyId();
if (!customerId) {
alert('Vælg et firma 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: 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('hardwareNameInput').value = '';
document.getElementById('hardwareAnyDeskIdInput').value = '';
await loadHardwareForContacts();
} catch (err) {
alert('Fejl: ' + err.message);
}
}
function getSingleContactCompanyId() {
const contactIds = Object.keys(selectedContactsCompanies);
if (contactIds.length !== 1) return null;
const companies = selectedContactsCompanies[contactIds[0]] || [];
if (companies.length !== 1) return null;
return companies[0].id;
}
async function quickCreateCustomer(name) {
if (!name || name.trim().length < 2) return;
const resultsDiv = document.getElementById('customerResults');
resultsDiv.innerHTML = '<div class="p-3 text-muted small"><span class="spinner-border spinner-border-sm me-2"></span>Opretter firma...</div>';
try {
const response = await fetch('/api/v1/customers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name.trim() })
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.detail || 'Kunne ikke oprette firma');
}
const created = await response.json();
selectCustomer(created.id, created.name);
const contactIds = Object.keys(selectedContacts).map(id => parseInt(id));
if (contactIds.length) {
const linkResponses = await Promise.all(contactIds.map(contactId =>
fetch(`/api/v1/contacts/${contactId}/companies`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customer_id: created.id, is_primary: false })
})
));
const failed = linkResponses.find(res => !res.ok);
if (failed) {
const err = await failed.json();
throw new Error(err.detail || 'Kunne ikke linke kontakt til firma');
}
}
return created;
} catch (err) {
resultsDiv.innerHTML = `<div class="p-3 text-danger small">${err.message}</div>`;
throw err;
}
}
async function quickCreateContact(fullName) {
if (!fullName || fullName.trim().length < 2) return;
const resultsDiv = document.getElementById('contactResults');
resultsDiv.innerHTML = '<div class="p-3 text-muted small"><span class="spinner-border spinner-border-sm me-2"></span>Opretter kontakt...</div>';
const parts = fullName.trim().split(/\s+/);
const firstName = parts.shift() || '';
const lastName = parts.join(' ');
try {
const payload = {
first_name: firstName,
last_name: lastName,
company_id: selectedCustomer ? selectedCustomer.id : null
};
const response = await fetch('/api/v1/contacts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.detail || 'Kunne ikke oprette kontakt');
}
const created = await response.json();
await selectContact(created.id, `${created.first_name} ${created.last_name}`.trim());
return created;
} catch (err) {
resultsDiv.innerHTML = `<div class="p-3 text-danger small">${err.message}</div>`;
throw err;
}
}
async function loadCaseTypesSelect() {
const select = document.getElementById('type');
if (!select) return;
try {
const res = await fetch('/api/v1/settings/case_types');
if (!res.ok) return;
const setting = await res.json();
const types = JSON.parse(setting.value || '[]');
if (!Array.isArray(types) || types.length === 0) return;
select.innerHTML = types
.map((type) => `<option value="${type}">${type}</option>`)
.join('');
} catch (err) {
console.error('Failed to load case types', err);
}
}
// --- Initialization ---
document.addEventListener('DOMContentLoaded', () => {
initializeSearch();
loadCaseTypesSelect();
applyTelefoniPrefill();
});
// --- Form Submission ---
document.getElementById('createForm').addEventListener('submit', async (e) => {
e.preventDefault();
// UI Reset
const errorDiv = document.getElementById('error');
const successDiv = document.getElementById('success');
const btn = document.getElementById('submitBtn');
errorDiv.classList.add('d-none');
successDiv.classList.add('d-none');
// Basic Validation
const titelInput = document.getElementById('titel');
const titel = titelInput.value;
const status = document.getElementById('status').value;
if (!titel.trim()) {
titelInput.classList.add('is-invalid');
errorDiv.classList.remove('d-none');
document.getElementById('error-text').textContent = "Titel er påkrævet";
return;
} else {
titelInput.classList.remove('is-invalid');
}
// Loading State
btn.disabled = true;
const originalBtnText = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Opretter...';
try {
const customerQuery = document.getElementById('customerSearch').value.trim();
if (!selectedCustomer && customerQuery.length >= 2) {
const res = await fetch(`/api/v1/search/customers?q=${encodeURIComponent(customerQuery)}`);
const matches = await res.json();
if (Array.isArray(matches) && matches.length > 0) {
throw new Error('Firma findes allerede. Vælg det fra listen.');
}
await quickCreateCustomer(customerQuery);
}
const contactQuery = document.getElementById('contactSearch').value.trim();
if (Object.keys(selectedContacts).length === 0 && contactQuery.length >= 2) {
const res = await fetch(`/api/v1/search/contacts?q=${encodeURIComponent(contactQuery)}`);
const matches = await res.json();
if (Array.isArray(matches) && matches.length > 0) {
throw new Error('Kontakt findes allerede. Vælg den fra listen.');
}
await quickCreateContact(contactQuery);
}
} catch (err) {
errorDiv.classList.remove('d-none');
document.getElementById('error-text').textContent = err.message;
btn.disabled = false;
btn.innerHTML = originalBtnText;
return;
}
const data = {
titel: titel,
beskrivelse: document.getElementById('beskrivelse').value || '',
type: document.getElementById('type').value,
status: status,
customer_id: selectedCustomer ? selectedCustomer.id : null,
ansvarlig_bruger_id: document.getElementById('ansvarlig_bruger_id').value ? parseInt(document.getElementById('ansvarlig_bruger_id').value) : null,
created_by_user_id: 1, // HARDCODED for now, should come from auth
deadline: document.getElementById('deadline').value || null
};
try {
const response = await fetch('/api/v1/sag', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
const result = await response.json();
// Add contacts if any
const contactPromises = Object.keys(selectedContacts).map(cid =>
fetch(`/api/v1/sag/${result.id}/contacts`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({contact_id: parseInt(cid), role: 'Kontakt'})
})
);
await Promise.all(contactPromises);
// Link telephony call -> case (best-effort)
if (telefoniPrefill.callId) {
try {
await fetch(`/api/v1/telefoni/calls/${encodeURIComponent(telefoniPrefill.callId)}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sag_id: result.id,
kontakt_id: telefoniPrefill.contactId || null
})
});
} catch (e) {
console.warn('Telefoni link failed', e);
}
}
// Ensure contact-company link exists
if (selectedCustomer) {
const linkPromises = Object.keys(selectedContacts).map(cid =>
fetch(`/api/v1/contacts/${parseInt(cid)}/companies`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ customer_id: selectedCustomer.id, is_primary: false })
})
);
const linkResponses = await Promise.all(linkPromises);
const linkFailed = linkResponses.find(res => !res.ok);
if (linkFailed) {
const err = await linkFailed.json();
throw new Error(err.detail || 'Kunne ikke linke kontakt til firma');
}
}
successDiv.classList.remove('d-none');
document.getElementById('success-text').textContent = "Sag oprettet succesfuldt! Omdirigerer...";
setTimeout(() => {
window.location.href = `/sag/${result.id}`;
}, 1000);
} else {
const errorText = await response.text();
let errMsg = "Kunne ikke oprette sag";
try {
const json = JSON.parse(errorText);
errMsg = json.detail || errMsg;
} catch(e) {}
throw new Error(errMsg);
}
} catch (err) {
console.error('Submit error:', err);
errorDiv.classList.remove('d-none');
document.getElementById('error-text').textContent = err.message;
btn.disabled = false;
btn.innerHTML = originalBtnText;
}
});
</script>
{% endblock %}