bmc_hub/static/js/case-create.js

345 lines
33 KiB
JavaScript
Raw Normal View History

/* Progressive case creation. Existing searches and submit retain their API contracts. */
(() => {
'use strict';
const $ = id => document.getElementById(id);
const esc = value => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
feat(planner): add overdue cases handling and improve case rendering - Introduced `overdueCases` state to track overdue cases in the planner. - Enhanced `loadCases` function to fetch and store overdue cases. - Updated `renderCases` to display overdue cases with appropriate labels. - Modified case card rendering to highlight overdue cases. fix(telefoni): ensure WebSocket connection on DOMContentLoaded - Adjusted WebSocket connection logic to handle cases where the script loads after DOMContentLoaded. test(tests): add tests for case creation and internal ticket handling - Added tests to ensure ticket creation commits all optional data. - Implemented tests for internal ticket creation, ensuring correct customer and user assignments. feat(migrations): add new tables for email configuration and case assignments - Created `email_connection_configurations` table for managing email settings. - Introduced `sag_case_assignees` table to handle individual case assignments. - Added `sag_case_details` table for structured case type details. - Updated existing case types to align with new templates and added support for internal tickets. fix(timetracking): allow manual time creation without case customer - Modified manual time creation to support entries without a customer. - Updated settlement options to recommend internal settlement for customerless time. feat(v4): enhance case detail template with new features - Added new features to the case detail template, including compact icons and inline relation creation. - Improved comment actions and time tracking functionalities.
2026-09-20 03:33:10 +02:00
const state = {tags: [], hardware: [], typeDetails: {}, tagCatalog: [], panels: {}, ready: false, restoring: false, key: null, pending: false, completed: false};
let saveTimer, duplicateTimer, duplicateToken = 0, workloadToken = 0, contactCasesToken = 0, tagSuggestionTimer, templates = [];
async function api(url, options) {
const response = await fetch(url, {credentials: 'include', ...options});
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(typeof body.detail === 'string' ? body.detail : 'Kunne ikke hente oplysninger');
return body;
}
function panel(id, title, elements, open = false) {
const detail = document.createElement('details');
detail.className = 'cc-panel'; detail.id = `cc-${id}`; detail.open = open;
const icon = {relations:'bi-people',tags:'bi-tags',hardware:'bi-pc-display',pipeline:'bi-graph-up-arrow',order:'bi-bag',metadata:'bi-sliders'}[id];
detail.innerHTML = `<summary><span class="cc-panel-icon"><i class="bi ${icon}" aria-hidden="true"></i></span><span class="cc-panel-label">${esc(title)}<small class="cc-summary"></small></span></summary><div class="cc-panel-body"></div>`;
elements[0].before(detail);
elements.forEach(el => detail.lastElementChild.append(el));
state.panels[id] = detail;
detail.addEventListener('toggle', scheduleSave);
return detail;
}
function summaries() {
const values = {
relations: `${selectedCustomer?.name || 'Vælg firma'} · ${Object.keys(selectedContacts).length} kontakter`,
tags: state.tags.map(t => t.name).join(', ') || 'Ingen tags',
hardware: `${state.hardware.length} enheder valgt`,
pipeline: [$('pipeline_stage_id').selectedOptions[0]?.text, $('pipeline_amount').value ? `${$('pipeline_amount').value} kr.` : ''].filter(Boolean).join(' · '),
order: `${document.querySelectorAll('#orderLines .order-line').length} linjer`,
metadata: [$('status').selectedOptions[0]?.text, $('ansvarlig_bruger_id').selectedOptions[0]?.text, $('deadline').value.replace('T', ' ')].filter(Boolean).join(' · ')
};
Object.entries(values).forEach(([key, value]) => { if (state.panels[key]) state.panels[key].querySelector('.cc-summary').textContent = value; });
}
function fieldSnapshot() {
return Object.fromEntries(Array.from($('createForm').querySelectorAll('input[id],select[id],textarea[id]')).filter(el => !['customerSearch','contactSearch'].includes(el.id)).map(el => [el.id, el.type === 'checkbox' ? el.checked : el.value]));
}
function snapshot() {
return {version: 1, savedAt: Date.now(), fields: fieldSnapshot(), customer: selectedCustomer, contacts: selectedContacts, contactCompanies: selectedContactsCompanies,
feat(planner): add overdue cases handling and improve case rendering - Introduced `overdueCases` state to track overdue cases in the planner. - Enhanced `loadCases` function to fetch and store overdue cases. - Updated `renderCases` to display overdue cases with appropriate labels. - Modified case card rendering to highlight overdue cases. fix(telefoni): ensure WebSocket connection on DOMContentLoaded - Adjusted WebSocket connection logic to handle cases where the script loads after DOMContentLoaded. test(tests): add tests for case creation and internal ticket handling - Added tests to ensure ticket creation commits all optional data. - Implemented tests for internal ticket creation, ensuring correct customer and user assignments. feat(migrations): add new tables for email configuration and case assignments - Created `email_connection_configurations` table for managing email settings. - Introduced `sag_case_assignees` table to handle individual case assignments. - Added `sag_case_details` table for structured case type details. - Updated existing case types to align with new templates and added support for internal tickets. fix(timetracking): allow manual time creation without case customer - Modified manual time creation to support entries without a customer. - Updated settlement options to recommend internal settlement for customerless time. feat(v4): enhance case detail template with new features - Added new features to the case detail template, including compact icons and inline relation creation. - Improved comment actions and time tracking functionalities.
2026-09-20 03:33:10 +02:00
tags: state.tags, hardware: state.hardware, typeDetails: state.typeDetails, orders: collectOrderItems(), prefill: telefoniPrefill,
customerSearch: $('customerSearch').value, contactSearch: $('contactSearch').value,
panels: Object.fromEntries(Object.entries(state.panels).map(([k, el]) => [k, el.open]))};
}
function save() {
if (!state.ready || state.pending || state.restoring || state.completed || !state.key) return;
try { localStorage.setItem(state.key, JSON.stringify(snapshot())); $('cc-save').textContent = 'Kladde gemt lokalt'; }
catch (error) { console.error('Kunne ikke gemme kladde', error); $('cc-save').textContent = 'Kladde kunne ikke gemmes i browseren'; }
}
function scheduleSave() {
summaries();
if (!state.ready || state.pending || state.restoring || state.completed) return;
$('cc-save').textContent = 'Gemmer kladde…'; clearTimeout(saveTimer); saveTimer = setTimeout(save, 500);
}
function renderTags() {
$('cc-tag-chips').innerHTML = state.tags.map(t => `<span class="badge me-1 mb-1" style="background-color:#64748b" data-tag-id="${Number(t.id)}">${/^bi-[a-z0-9-]+$/.test(t.icon || '') ? `<i class="bi ${esc(t.icon)}"></i> ` : ''}${esc(t.name)} <button type="button" class="btn-close btn-close-white" aria-label="Fjern ${esc(t.name)}"></button></span>`).join('');
state.tags.forEach(t => { const chip = $('cc-tag-chips').querySelector(`[data-tag-id="${Number(t.id)}"]`); if (/^#[0-9a-f]{6}$/i.test(t.color)) chip.style.backgroundColor = t.color; });
summaries();
renderTagSuggestions();
}
function renderTagSuggestions() {
const box = $('cc-tag-suggestions');
if (!box) return;
const haystack = `${$('titel')?.value || ''} ${$('beskrivelse')?.value || ''}`.toLocaleLowerCase('da-DK');
if (haystack.trim().length < 3 || !state.tagCatalog.length) { box.innerHTML = ''; return; }
const selected = new Set(state.tags.map(tag => Number(tag.id)));
const suggestions = state.tagCatalog.filter(tag => {
if (selected.has(Number(tag.id)) || !['brand', 'type'].includes(tag.type)) return false;
const words = Array.isArray(tag.catch_words) ? tag.catch_words : [];
return words.some(word => String(word).trim() && haystack.includes(String(word).trim().toLocaleLowerCase('da-DK')));
}).slice(0, 8);
box.innerHTML = suggestions.length ? `<div class="cc-tag-suggestions__label">Foreslået ud fra teksten</div><div class="d-flex flex-wrap gap-1">${suggestions.map(tag => `<button type="button" class="btn btn-sm btn-outline-primary" data-suggested-tag="${Number(tag.id)}" title="Tilføj ${esc(tag.name)}">${tag.icon ? `<i class="bi ${esc(tag.icon)}"></i> ` : ''}${esc(tag.name)} <i class="bi bi-plus-lg"></i></button>`).join('')}</div>` : '';
}
function scheduleTagSuggestions() {
clearTimeout(tagSuggestionTimer);
tagSuggestionTimer = setTimeout(renderTagSuggestions, 350);
}
async function addTag(tag) {
if (!state.tags.some(t => t.id === tag.id)) {
const groups = await api('/api/v1/tags/groups').catch(() => []);
if (groups.some(g => g.id === tag.tag_group_id && ['single','toggle'].includes(g.behavior))) state.tags = state.tags.filter(t => t.tag_group_id !== tag.tag_group_id);
state.tags.push(tag); renderTags(); scheduleSave();
}
}
function renderSelectedHardware() {
$('cc-hardware-chips').innerHTML = state.hardware.map(h => `<span class="badge bg-secondary me-1">${esc(h.name)} <button type="button" class="btn-close btn-close-white" data-remove-hardware="${Number(h.id)}" aria-label="Fjern hardware"></button></span>`).join('');
document.querySelectorAll('[data-hardware-choice]').forEach(el => { el.checked = state.hardware.some(h => h.id === Number(el.dataset.hardwareChoice)); });
summaries();
}
async function duplicates() {
const token = ++duplicateToken, customerId = selectedCustomer?.id, title = $('titel').value.trim();
$('cc-duplicates').innerHTML = '';
if (!customerId || title.length < 5) return;
try {
const rows = await api(`/api/v1/case-create/duplicates?customer_id=${customerId}&title=${encodeURIComponent(title)}`);
if (token !== duplicateToken) return;
$('cc-duplicates').innerHTML = rows.length ? `<div class="alert alert-warning"><strong>Mulige eksisterende sager</strong>${rows.map(r => `<div><a target="_blank" rel="noopener" href="/sag/${r.id}/v3">SAG-${r.id} · ${esc(r.titel)}</a><small> · ${esc(r.status)} · ${esc(r.ansvarlig_navn || 'Ingen ansvarlig')}</small></div>`).join('')}</div>` : '';
} catch { if (token === duplicateToken) $('cc-duplicates').textContent = 'Duplikatkontrol er midlertidigt utilgængelig. Du kan stadig oprette sagen.'; }
}
function scheduleDuplicates() { ++duplicateToken; clearTimeout(duplicateTimer); duplicateTimer = setTimeout(duplicates, 500); }
async function contactOpenCases() {
const box = $('cc-contact-cases');
if (!box) return;
const contacts = Object.values(selectedContacts);
const token = ++contactCasesToken;
if (!contacts.length) { box.hidden = true; box.innerHTML = ''; return; }
box.hidden = false;
box.innerHTML = '<span class="text-muted"><span class="spinner-border spinner-border-sm me-1"></span>Tjekker kontaktens åbne sager…</span>';
const query = contacts.map(contact => `contact_ids=${encodeURIComponent(contact.id)}`).join('&');
try {
const data = await api(`/api/v1/case-create/contacts-open-cases?${query}`);
if (token !== contactCasesToken) return;
const groups = contacts.map(contact => ({contact, data: data[contact.id] || {total:0, items:[]}}));
const total = groups.reduce((sum, group) => sum + Number(group.data.total || 0), 0);
const title = total ? `${total} åben${total === 1 ? '' : 'ne'} sag${total === 1 ? '' : 'er'} på valgt kontakt` : 'Ingen åbne sager på valgt kontakt';
box.innerHTML = `<div class="cc-contact-cases__header"><span class="cc-contact-cases__header-icon"><i class="bi ${total ? 'bi-clipboard2-check' : 'bi-check2-circle'}"></i></span><div><span class="cc-contact-cases__eyebrow">Dublettjek</span><span class="cc-contact-cases__title">${title}</span></div></div>${total ? `<div class="cc-contact-cases__body">${groups.map(({contact,data}) => data.total ? `<div class="cc-contact-cases__group"><span class="cc-contact-cases__group-name">${esc(contact.name)}</span>${data.items.map(item => `<div class="cc-contact-cases__item"><a href="/sag/${Number(item.id)}/v3" target="_blank" rel="noopener">SAG-${Number(item.id)} · ${esc(item.titel)}</a><small>${esc(item.status || '')} · ${esc(item.ansvarlig_navn || 'Ingen ansvarlig')}${item.deadline ? ` · ${esc(new Date(item.deadline).toLocaleDateString('da-DK'))}` : ''}</small></div>`).join('')}${data.total > data.items.length ? `<span class="cc-contact-cases__more">Viser ${data.items.length} af ${data.total} åbne sager</span>` : ''}</div>` : '').join('')}</div>` : '<div class="cc-contact-cases__empty">Du kan oprette sagen uden risiko for en kendt, åben sag på kontakten.</div>'}`;
} catch {
if (token === contactCasesToken) box.innerHTML = '<span class="text-muted">Kunne ikke tjekke kontaktens åbne sager. Du kan stadig oprette sagen.</span>';
}
}
async function workload() {
const token = ++workloadToken, id = $('ansvarlig_bruger_id').value;
$('cc-workload').textContent = id ? 'Henter åbne sager…' : 'Vælg en ansvarlig for at se åbne sager.';
if (!id) return;
try {
const data = await api(`/api/v1/case-create/workload?user_id=${id}`);
if (token !== workloadToken) return;
$('cc-workload').innerHTML = `<details><summary>${data.total} åbne sager · ${esc($('ansvarlig_bruger_id').selectedOptions[0].text)}</summary>${data.items.map(r => `<div class="cc-work-item"><a target="_blank" rel="noopener" href="/sag/${r.id}/v3">SAG-${r.id} · ${esc(r.titel)}</a><small>${esc(r.customer_name || '')} · ${esc(r.status)} <span class="${r.deadline && new Date(r.deadline) < new Date() ? 'text-danger' : ''}">${r.deadline ? esc(new Date(r.deadline).toLocaleString('da-DK')) : ''}</span></small></div>`).join('') || '<p class="text-success">Ingen åbne sager.</p>'}${data.total > 10 ? `<a href="/sag?ansvarlig_bruger_id=${id}" target="_blank" rel="noopener">Se alle ${data.total} sager</a>` : ''}</details>`;
} catch { if (token === workloadToken) $('cc-workload').textContent = 'Kunne ikke hente åbne sager.'; }
}
feat(planner): add overdue cases handling and improve case rendering - Introduced `overdueCases` state to track overdue cases in the planner. - Enhanced `loadCases` function to fetch and store overdue cases. - Updated `renderCases` to display overdue cases with appropriate labels. - Modified case card rendering to highlight overdue cases. fix(telefoni): ensure WebSocket connection on DOMContentLoaded - Adjusted WebSocket connection logic to handle cases where the script loads after DOMContentLoaded. test(tests): add tests for case creation and internal ticket handling - Added tests to ensure ticket creation commits all optional data. - Implemented tests for internal ticket creation, ensuring correct customer and user assignments. feat(migrations): add new tables for email configuration and case assignments - Created `email_connection_configurations` table for managing email settings. - Introduced `sag_case_assignees` table to handle individual case assignments. - Added `sag_case_details` table for structured case type details. - Updated existing case types to align with new templates and added support for internal tickets. fix(timetracking): allow manual time creation without case customer - Modified manual time creation to support entries without a customer. - Updated settlement options to recommend internal settlement for customerless time. feat(v4): enhance case detail template with new features - Added new features to the case detail template, including compact icons and inline relation creation. - Improved comment actions and time tracking functionalities.
2026-09-20 03:33:10 +02:00
function typeChanged(type, showTypeFields = false) {
if (type === 'intern_ticket') {
selectedCustomer = {id: 8, name: 'BMC Networks'};
$('customer_id').value = '8';
$('customerSearch').value = 'BMC Networks';
const creatorId = Number(window.caseCreateCurrentUserId || 0);
if (creatorId && Array.from($('ansvarlig_bruger_id').options).some(option => Number(option.value) === creatorId)) {
$('ansvarlig_bruger_id').value = String(creatorId);
}
window.renderSelections?.();
}
captureTypeDetails();
renderTypeDetails(type);
$('createForm').closest('.card-custom')?.classList.toggle('cc-postit-mode', type === 'postit');
state.panels['type-details']?.classList.toggle('cc-postit-panel', type === 'postit');
document.querySelectorAll('[data-case-type]').forEach(b => { const active = b.dataset.caseType === type; b.classList.toggle('active', active); b.setAttribute('aria-pressed', String(active)); });
feat(planner): add overdue cases handling and improve case rendering - Introduced `overdueCases` state to track overdue cases in the planner. - Enhanced `loadCases` function to fetch and store overdue cases. - Updated `renderCases` to display overdue cases with appropriate labels. - Modified case card rendering to highlight overdue cases. fix(telefoni): ensure WebSocket connection on DOMContentLoaded - Adjusted WebSocket connection logic to handle cases where the script loads after DOMContentLoaded. test(tests): add tests for case creation and internal ticket handling - Added tests to ensure ticket creation commits all optional data. - Implemented tests for internal ticket creation, ensuring correct customer and user assignments. feat(migrations): add new tables for email configuration and case assignments - Created `email_connection_configurations` table for managing email settings. - Introduced `sag_case_assignees` table to handle individual case assignments. - Added `sag_case_details` table for structured case type details. - Updated existing case types to align with new templates and added support for internal tickets. fix(timetracking): allow manual time creation without case customer - Modified manual time creation to support entries without a customer. - Updated settlement options to recommend internal settlement for customerless time. feat(v4): enhance case detail template with new features - Added new features to the case detail template, including compact icons and inline relation creation. - Improved comment actions and time tracking functionalities.
2026-09-20 03:33:10 +02:00
const visiblePanels = {
ticket: ['relations', 'type-details', 'hardware'],
salg: ['relations', 'type-details', 'pipeline', 'order'],
bogholderi: ['relations', 'type-details', 'order'],
admin: ['type-details'],
projekt: ['relations', 'type-details'],
postit: ['type-details'],
intern_ticket: ['type-details'],
bug_report: ['relations', 'type-details', 'hardware'],
service: ['relations', 'hardware'],
pipeline: ['relations', 'pipeline'],
ordre: ['relations', 'order'],
opgave: ['relations'],
abonnement: ['relations'],
}[type] || ['relations'];
['relations', 'type-details', 'hardware', 'pipeline', 'order'].forEach(key => state.panels[key]?.classList.toggle('d-none', !showTypeFields || !visiblePanels.includes(key)));
const sectionIds = {hardware: 'hardwareSection', pipeline: 'pipelineSection', order: 'orderSection'};
Object.entries(sectionIds).forEach(([key, id]) => $(id)?.classList.toggle('d-none', !showTypeFields || !visiblePanels.includes(key)));
document.querySelectorAll('.case-type-extra').forEach(el => el.classList.toggle('d-none', !showTypeFields));
$('cc-template')?.closest('.cc-template-toolbar')?.classList.toggle('d-none', !showTypeFields);
if (showTypeFields && !state.restoring) visiblePanels.forEach(panelKey => { if (state.panels[panelKey]) state.panels[panelKey].open = true; });
if ($('cc-more')) $('cc-more').open = Boolean($('cc-more').querySelector(`[data-case-type="${CSS.escape(type)}"]`));
scheduleSave();
}
feat(planner): add overdue cases handling and improve case rendering - Introduced `overdueCases` state to track overdue cases in the planner. - Enhanced `loadCases` function to fetch and store overdue cases. - Updated `renderCases` to display overdue cases with appropriate labels. - Modified case card rendering to highlight overdue cases. fix(telefoni): ensure WebSocket connection on DOMContentLoaded - Adjusted WebSocket connection logic to handle cases where the script loads after DOMContentLoaded. test(tests): add tests for case creation and internal ticket handling - Added tests to ensure ticket creation commits all optional data. - Implemented tests for internal ticket creation, ensuring correct customer and user assignments. feat(migrations): add new tables for email configuration and case assignments - Created `email_connection_configurations` table for managing email settings. - Introduced `sag_case_assignees` table to handle individual case assignments. - Added `sag_case_details` table for structured case type details. - Updated existing case types to align with new templates and added support for internal tickets. fix(timetracking): allow manual time creation without case customer - Modified manual time creation to support entries without a customer. - Updated settlement options to recommend internal settlement for customerless time. feat(v4): enhance case detail template with new features - Added new features to the case detail template, including compact icons and inline relation creation. - Improved comment actions and time tracking functionalities.
2026-09-20 03:33:10 +02:00
const typeFieldSets = {
ticket: [
['anydesk_id', 'AnyDesk-ID', 'text', '123-456-789'],
['hardware_information', 'Hardwareinformation', 'textarea', 'Enhed, model, serienummer eller fejl'],
['solution', 'Løsning', 'textarea', 'Den aftalte eller udførte løsning'],
],
salg: [
['customer_need', 'Kundebehov', 'textarea', 'Hvad ønsker kunden?'],
['expected_close_date', 'Forventet lukkedato', 'date', ''],
],
bogholderi: [
['voucher_reference', 'Bilags- eller fakturanummer', 'text', 'F.eks. F-12345'],
['supplier', 'Leverandør', 'text', 'Leverandørnavn'],
['due_date', 'Forfaldsdato', 'date', ''],
],
admin: [
['area', 'Område eller system', 'text', 'F.eks. brugeradministration'],
['requested_action', 'Ønsket handling', 'textarea', 'Hvad skal ændres eller undersøges?'],
],
projekt: [
['project_goal', 'Projektmål', 'textarea', 'Hvad skal projektet levere?'],
['start_date', 'Startdato', 'date', ''],
['end_date', 'Slutdato', 'date', ''],
],
postit: [
['note', 'Notat', 'textarea', 'Kort huskeseddel'],
['remind_date', 'Påmindelsesdato', 'date', ''],
],
intern_ticket: [
['system', 'System', 'text', 'Berørt system eller modul'],
['purpose', 'Formål', 'textarea', 'Hvad skal den interne opgave føre til?'],
['next_step', 'Næste skridt', 'textarea', 'Hvad skal der ske nu?'],
],
bug_report: [
['affected_area', 'Berørt område', 'text', 'System, side eller modul'],
['steps_to_reproduce', 'Trin til at genskabe', 'textarea', 'Beskriv trin for trin hvordan fejlen opstår'],
['expected_result', 'Forventet resultat', 'textarea', 'Hvad skulle der være sket?'],
['actual_result', 'Faktisk resultat', 'textarea', 'Hvad skete der i stedet?'],
],
};
function captureTypeDetails() {
const currentType = $('type')?.value;
if (!currentType) return;
const values = {};
document.querySelectorAll('[data-case-detail]').forEach(field => { values[field.dataset.caseDetail] = field.value; });
if (Object.keys(values).length) state.typeDetails[currentType] = values;
}
function renderTypeDetails(type) {
const section = $('typeDetailsSection'), box = $('typeDetailsFields');
if (!section || !box) return;
const fields = typeFieldSets[type] || [];
const values = state.typeDetails[type] || {};
box.innerHTML = fields.map(([key, label, kind, placeholder]) => `<div class="mb-3"><label class="form-label" for="case-detail-${esc(key)}">${esc(label)}</label>${kind === 'textarea' ? `<textarea class="form-control" rows="3" id="case-detail-${esc(key)}" data-case-detail="${esc(key)}" placeholder="${esc(placeholder)}">${esc(values[key] || '')}</textarea>` : `<input class="form-control" type="${esc(kind)}" id="case-detail-${esc(key)}" data-case-detail="${esc(key)}" value="${esc(values[key] || '')}" placeholder="${esc(placeholder)}">`}</div>`).join('');
section.classList.toggle('d-none', !fields.length);
}
function buildTypes() {
const options = Array.from($('type').options), current = $('type').value;
feat(planner): add overdue cases handling and improve case rendering - Introduced `overdueCases` state to track overdue cases in the planner. - Enhanced `loadCases` function to fetch and store overdue cases. - Updated `renderCases` to display overdue cases with appropriate labels. - Modified case card rendering to highlight overdue cases. fix(telefoni): ensure WebSocket connection on DOMContentLoaded - Adjusted WebSocket connection logic to handle cases where the script loads after DOMContentLoaded. test(tests): add tests for case creation and internal ticket handling - Added tests to ensure ticket creation commits all optional data. - Implemented tests for internal ticket creation, ensuring correct customer and user assignments. feat(migrations): add new tables for email configuration and case assignments - Created `email_connection_configurations` table for managing email settings. - Introduced `sag_case_assignees` table to handle individual case assignments. - Added `sag_case_details` table for structured case type details. - Updated existing case types to align with new templates and added support for internal tickets. fix(timetracking): allow manual time creation without case customer - Modified manual time creation to support entries without a customer. - Updated settlement options to recommend internal settlement for customerless time. feat(v4): enhance case detail template with new features - Added new features to the case detail template, including compact icons and inline relation creation. - Improved comment actions and time tracking functionalities.
2026-09-20 03:33:10 +02:00
const keys = [...new Set([current, ...options.map(o => o.value)])].filter(k => options.some(o => o.value === k));
const hints = {ticket:'Support og hjælp',salg:'Tilbud og salg',bogholderi:'Bilag og økonomi',admin:'Intern administration',projekt:'Større forløb',postit:'Huskeseddel',intern_ticket:'Intern opgave',bug_report:'Rapportér en fejl'};
const button = key => `<button type="button" class="btn btn-outline-primary" data-case-type="${esc(key)}" aria-pressed="false"><span class="cc-type-name">${esc(options.find(o => o.value === key).text)}</span><small class="cc-type-hint">${esc(hints[key] || 'Opret sag')}</small></button>`;
const messageButton = $('caseMessageTypeButton');
$('type').classList.add('d-none'); $('type').removeAttribute('required');
const div = document.createElement('div'); div.id = 'cc-types'; div.className = 'cc-types'; div.setAttribute('role', 'group'); div.setAttribute('aria-label', 'Sagstype');
div.innerHTML = keys.slice(0,4).map(button).join('') + (keys.length > 4 ? `<details id="cc-more"><summary>Flere</summary><div class="cc-types">${keys.slice(4).map(button).join('')}</div></details>` : '');
if (messageButton) { messageButton.classList.remove('cc-message-fallback'); messageButton.dataset.caseMessage = 'true'; div.insertBefore(messageButton, div.querySelector('#cc-more')); }
$('type').after(div);
div.addEventListener('click', e => { const message = e.target.closest('[data-case-message]'); if (message) { window.caseCreateUI?.openMessage(); return; } const b = e.target.closest('[data-case-type]'); if (b) { $('type').value = b.dataset.caseType; $('type').dispatchEvent(new Event('change', {bubbles:true})); } });
}
async function applyTemplate(id) {
const template = templates.find(t => t.id === Number(id)); if (!template) return;
const values = template.values, fields = {type: values.type, titel: values.titel, beskrivelse: values.beskrivelse, status: values.status, assigned_group_id: values.assigned_group_id ?? ''};
Object.entries(values.pipeline || {}).forEach(([k,v]) => { fields[`pipeline_${k === 'stage_id' ? 'stage_id' : k}`] = v ?? ''; });
if (Object.entries(fields).some(([k,v]) => $(k)?.value && String(v) !== $(k).value) && !confirm('Skabelonen ændrer udfyldte felter. Vil du bruge den?')) return;
Object.entries(fields).forEach(([k,v]) => { const el = $(k); if (el && (el.tagName !== 'SELECT' || Array.from(el.options).some(o => o.value === String(v)))) el.value = v; });
const tags = await api('/api/v1/tags?is_active=true');
for (const id of values.tag_ids || []) { const tag = tags.find(t => t.id === id); if (tag) await addTag(tag); }
feat(planner): add overdue cases handling and improve case rendering - Introduced `overdueCases` state to track overdue cases in the planner. - Enhanced `loadCases` function to fetch and store overdue cases. - Updated `renderCases` to display overdue cases with appropriate labels. - Modified case card rendering to highlight overdue cases. fix(telefoni): ensure WebSocket connection on DOMContentLoaded - Adjusted WebSocket connection logic to handle cases where the script loads after DOMContentLoaded. test(tests): add tests for case creation and internal ticket handling - Added tests to ensure ticket creation commits all optional data. - Implemented tests for internal ticket creation, ensuring correct customer and user assignments. feat(migrations): add new tables for email configuration and case assignments - Created `email_connection_configurations` table for managing email settings. - Introduced `sag_case_assignees` table to handle individual case assignments. - Added `sag_case_details` table for structured case type details. - Updated existing case types to align with new templates and added support for internal tickets. fix(timetracking): allow manual time creation without case customer - Modified manual time creation to support entries without a customer. - Updated settlement options to recommend internal settlement for customerless time. feat(v4): enhance case detail template with new features - Added new features to the case detail template, including compact icons and inline relation creation. - Improved comment actions and time tracking functionalities.
2026-09-20 03:33:10 +02:00
window.updateCaseTypeSections?.(true); $('charCount').textContent = `${$('beskrivelse').value.length} tegn`; scheduleSave(); scheduleDuplicates();
}
async function restore(draft) {
state.restoring = true;
try {
feat(planner): add overdue cases handling and improve case rendering - Introduced `overdueCases` state to track overdue cases in the planner. - Enhanced `loadCases` function to fetch and store overdue cases. - Updated `renderCases` to display overdue cases with appropriate labels. - Modified case card rendering to highlight overdue cases. fix(telefoni): ensure WebSocket connection on DOMContentLoaded - Adjusted WebSocket connection logic to handle cases where the script loads after DOMContentLoaded. test(tests): add tests for case creation and internal ticket handling - Added tests to ensure ticket creation commits all optional data. - Implemented tests for internal ticket creation, ensuring correct customer and user assignments. feat(migrations): add new tables for email configuration and case assignments - Created `email_connection_configurations` table for managing email settings. - Introduced `sag_case_assignees` table to handle individual case assignments. - Added `sag_case_details` table for structured case type details. - Updated existing case types to align with new templates and added support for internal tickets. fix(timetracking): allow manual time creation without case customer - Modified manual time creation to support entries without a customer. - Updated settlement options to recommend internal settlement for customerless time. feat(v4): enhance case detail template with new features - Added new features to the case detail template, including compact icons and inline relation creation. - Improved comment actions and time tracking functionalities.
2026-09-20 03:33:10 +02:00
selectedCustomer = draft.customer; selectedContacts = draft.contacts || {}; selectedContactsCompanies = draft.contactCompanies || {}; state.typeDetails = draft.typeDetails || {};
telefoniPrefill = draft.prefill || telefoniPrefill;
Object.entries(draft.fields || {}).forEach(([id,value]) => { const el = $(id); if (el && !id.startsWith('cc-')) { if (el.type === 'checkbox') el.checked = value; else el.value = value; } });
state.tags = draft.tags || []; state.hardware = draft.hardware || [];
$('orderLines').innerHTML = '';
(draft.orders || []).forEach(item => { addOrderLine(item.type); const row = $('orderLines').lastElementChild; Object.entries({description:'description',quantity:'quantity',unit:'unit',unit_price:'unit-price',amount:'amount',currency:'currency',status:'status',external_ref:'reference',purchase_purpose:'purpose'}).forEach(([k,cls]) => { const el = row.querySelector(`.order-${cls}`); if (el) el.value = item[k] ?? ''; }); });
renderSelections(); await loadHardwareForContacts(); renderTags(); renderSelectedHardware(); renderOrderLinesEmptyState();
await loadCreateTopAlertsForCustomer(selectedCustomer?.id || null);
if (selectedCustomer) await loadSelectedCustomerContacts(selectedCustomer.id);
$('customerSearch').value = draft.customerSearch || ''; $('contactSearch').value = draft.contactSearch || '';
feat(planner): add overdue cases handling and improve case rendering - Introduced `overdueCases` state to track overdue cases in the planner. - Enhanced `loadCases` function to fetch and store overdue cases. - Updated `renderCases` to display overdue cases with appropriate labels. - Modified case card rendering to highlight overdue cases. fix(telefoni): ensure WebSocket connection on DOMContentLoaded - Adjusted WebSocket connection logic to handle cases where the script loads after DOMContentLoaded. test(tests): add tests for case creation and internal ticket handling - Added tests to ensure ticket creation commits all optional data. - Implemented tests for internal ticket creation, ensuring correct customer and user assignments. feat(migrations): add new tables for email configuration and case assignments - Created `email_connection_configurations` table for managing email settings. - Introduced `sag_case_assignees` table to handle individual case assignments. - Added `sag_case_details` table for structured case type details. - Updated existing case types to align with new templates and added support for internal tickets. fix(timetracking): allow manual time creation without case customer - Modified manual time creation to support entries without a customer. - Updated settlement options to recommend internal settlement for customerless time. feat(v4): enhance case detail template with new features - Added new features to the case detail template, including compact icons and inline relation creation. - Improved comment actions and time tracking functionalities.
2026-09-20 03:33:10 +02:00
window.updateCaseTypeSections?.(true); Object.entries(draft.panels || {}).forEach(([k,v]) => { if (state.panels[k]) state.panels[k].open = v; });
$('charCount').textContent = `${$('beskrivelse').value.length} tegn`;
} finally { state.restoring = false; }
state.pending = false; $('cc-draft').remove(); scheduleSave(); scheduleDuplicates(); workload();
}
function validate() {
document.querySelectorAll('.cc-error').forEach(el => el.remove());
document.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
const errors = [];
const add = (el, text) => { errors.push(text); el.classList.add('is-invalid'); el.insertAdjacentHTML('afterend', `<div class="text-danger small cc-error">${esc(text)}</div>`); const detail = el.closest('details'); if (detail) { detail.open = true; if (!detail.querySelector('summary .cc-error')) detail.querySelector('summary').insertAdjacentHTML('beforeend','<span class="text-danger small cc-error"> · Kontrollér felter</span>'); } };
if (!$('titel').value.trim()) add($('titel'), 'Titel er påkrævet');
feat(planner): add overdue cases handling and improve case rendering - Introduced `overdueCases` state to track overdue cases in the planner. - Enhanced `loadCases` function to fetch and store overdue cases. - Updated `renderCases` to display overdue cases with appropriate labels. - Modified case card rendering to highlight overdue cases. fix(telefoni): ensure WebSocket connection on DOMContentLoaded - Adjusted WebSocket connection logic to handle cases where the script loads after DOMContentLoaded. test(tests): add tests for case creation and internal ticket handling - Added tests to ensure ticket creation commits all optional data. - Implemented tests for internal ticket creation, ensuring correct customer and user assignments. feat(migrations): add new tables for email configuration and case assignments - Created `email_connection_configurations` table for managing email settings. - Introduced `sag_case_assignees` table to handle individual case assignments. - Added `sag_case_details` table for structured case type details. - Updated existing case types to align with new templates and added support for internal tickets. fix(timetracking): allow manual time creation without case customer - Modified manual time creation to support entries without a customer. - Updated settlement options to recommend internal settlement for customerless time. feat(v4): enhance case detail template with new features - Added new features to the case detail template, including compact icons and inline relation creation. - Improved comment actions and time tracking functionalities.
2026-09-20 03:33:10 +02:00
if (['ticket', 'salg', 'bogholderi', 'projekt', 'service', 'pipeline', 'ordre', 'opgave', 'abonnement'].includes($('type').value) && !selectedCustomer && $('customerSearch').value.trim().length < 2) add($('customerSearch'), 'Vælg et firma');
$('createForm').querySelectorAll('input[type=number]').forEach(el => { if (!el.checkValidity()) add(el, 'Kontrollér tallets interval og format'); });
document.querySelectorAll('#orderLines .order-line').forEach(row => { for (const cls of ['description','amount']) { const el = row.querySelector(`.order-${cls}`); if (!el.value.trim()) add(el, cls === 'description' ? 'Ordrelinjen mangler beskrivelse' : 'Ordrelinjen mangler beløb'); } });
if (errors.length) { $('error').classList.remove('d-none'); $('error-text').textContent = [...new Set(errors)].join(' · '); document.querySelector('.is-invalid')?.focus(); return false; }
return true;
}
async function init() {
const form = $('createForm');
try {
const setting = await api('/api/v1/settings/case_statuses');
const statuses = JSON.parse(setting.value || '[]');
if (Array.isArray(statuses)) statuses.forEach(s => { const value = typeof s === 'string' ? s : s.value; if (value && !Array.from($('status').options).some(o => o.value === value)) $('status').add(new Option(typeof s === 'string' ? s : (s.label || value), value)); });
} catch { /* Keep the built-in statuses available. */ }
const headings = Array.from(form.querySelectorAll(':scope > h5'));
feat(planner): add overdue cases handling and improve case rendering - Introduced `overdueCases` state to track overdue cases in the planner. - Enhanced `loadCases` function to fetch and store overdue cases. - Updated `renderCases` to display overdue cases with appropriate labels. - Modified case card rendering to highlight overdue cases. fix(telefoni): ensure WebSocket connection on DOMContentLoaded - Adjusted WebSocket connection logic to handle cases where the script loads after DOMContentLoaded. test(tests): add tests for case creation and internal ticket handling - Added tests to ensure ticket creation commits all optional data. - Implemented tests for internal ticket creation, ensuring correct customer and user assignments. feat(migrations): add new tables for email configuration and case assignments - Created `email_connection_configurations` table for managing email settings. - Introduced `sag_case_assignees` table to handle individual case assignments. - Added `sag_case_details` table for structured case type details. - Updated existing case types to align with new templates and added support for internal tickets. fix(timetracking): allow manual time creation without case customer - Modified manual time creation to support entries without a customer. - Updated settlement options to recommend internal settlement for customerless time. feat(v4): enhance case detail template with new features - Added new features to the case detail template, including compact icons and inline relation creation. - Improved comment actions and time tracking functionalities.
2026-09-20 03:33:10 +02:00
panel('relations', 'Kunde og kontakter', [headings[0], headings[0].nextElementSibling], false);
for (const [key,id,title] of [['type-details','typeDetailsSection','Oplysninger til sagstypen'],['hardware','hardwareSection','Hardware og AnyDesk'],['pipeline','pipelineSection','Pipeline'],['order','orderSection','Indkøb og salg']]) panel(key, title, [$(id)]);
const metadata = headings[1]; panel('metadata', 'Ansvarlig', [metadata,metadata.nextElementSibling,metadata.nextElementSibling.nextElementSibling], true);
const tags = document.createElement('section'); tags.innerHTML = '<div id="cc-tag-chips"></div><button type="button" id="cc-tag-add" class="btn btn-sm btn-outline-primary">Tilføj tag</button><div id="cc-tag-suggestions" class="cc-tag-suggestions"></div>'; state.panels.hardware.before(tags); panel('tags', 'Tags', [tags], true);
$('cc-tag-add').onclick = () => window.tagPicker.showSelection(addTag);
window.setTagPickerContext?.('case', null, addTag);
$('cc-tag-chips').onclick = e => { const chip = e.target.closest('[data-tag-id]'); if (chip && e.target.closest('button')) { state.tags = state.tags.filter(t => t.id !== Number(chip.dataset.tagId)); renderTags(); scheduleSave(); } };
$('cc-tag-suggestions').onclick = e => { const button = e.target.closest('[data-suggested-tag]'); const tag = state.tagCatalog.find(item => item.id === Number(button?.dataset.suggestedTag)); if (tag) addTag(tag).catch(error => showCreateError(error.message)); };
const chips = document.createElement('div'); chips.id = 'cc-hardware-chips'; $('hardwareList').before(chips);
chips.onclick = e => { const b = e.target.closest('[data-remove-hardware]'); if (b) { state.hardware = state.hardware.filter(h => h.id !== Number(b.dataset.removeHardware)); renderSelectedHardware(); scheduleSave(); } };
const originalRenderHardware = window.renderHardwareList;
window.renderHardwareList = items => { originalRenderHardware(items); Array.from($('hardwareList').children).forEach((row,i) => { if (!items[i]) return; const item = items[i]; const input = document.createElement('input'); input.type = 'checkbox'; input.className = 'form-check-input me-2'; input.dataset.hardwareChoice = item.id; input.setAttribute('aria-label', `Vælg ${item.model || item.brand || item.id}`); input.onchange = () => { state.hardware = state.hardware.filter(h => h.id !== item.id); if (input.checked) state.hardware.push({id:item.id,name:item.model || item.brand || `Hardware #${item.id}`}); renderSelectedHardware(); scheduleSave(); }; row.prepend(input); }); renderSelectedHardware(); };
window.quickCreateHardware = async () => {
const name = $('hardwareNameInput').value.trim(), customerId = selectedCustomer?.id || getSingleContactCompanyId();
if (!name || !customerId) { showCreateError('Udfyld hardwarenavn og vælg et firma'); return; }
const anydeskId = $('hardwareAnyDeskIdInput').value.trim();
const button = $('hardwareNameInput').closest('.card-body').querySelector('button'); button.disabled = true;
try { const item = await api('/api/v1/hardware/quick', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,customer_id:customerId,anydesk_id:anydeskId || null,anydesk_link:anydeskId ? `anydesk://${anydeskId}` : null})}); state.hardware.push({id:item.id,name}); $('hardwareNameInput').value = ''; $('hardwareAnyDeskIdInput').value = ''; renderSelectedHardware(); scheduleSave(); } catch(e) { showCreateError(e.message); } finally { button.disabled = false; }
};
const originalRender = window.renderSelections;
window.renderSelections = () => { originalRender(); if (state.ready) { scheduleSave(); scheduleDuplicates(); contactOpenCases(); } };
const contactCases = document.createElement('aside'); contactCases.id = 'cc-contact-cases'; contactCases.className = 'cc-contact-cases'; contactCases.hidden = true; $('selectedContacts').after(contactCases);
const workloadEl = document.createElement('div'); workloadEl.id = 'cc-workload'; workloadEl.className = 'cc-workload'; state.panels.metadata.after(workloadEl);
const duplicateEl = document.createElement('div'); duplicateEl.id = 'cc-duplicates'; $('titel').closest('.row').after(duplicateEl);
const toolbar = document.createElement('div'); toolbar.className = 'mb-3'; toolbar.innerHTML = '<label class="form-label" for="cc-template">Hurtigskabelon</label><select id="cc-template" class="form-select"><option value="">Start uden skabelon</option></select><small id="cc-template-error" class="text-muted"></small>'; form.prepend(toolbar);
try { templates = await api('/api/v1/case-create/templates'); $('cc-template').insertAdjacentHTML('beforeend',templates.map(t => `<option value="${t.id}">${esc(t.name)}</option>`).join('')); } catch { $('cc-template-error').textContent = 'Skabeloner er ikke tilgængelige.'; }
$('cc-template').onchange = e => applyTemplate(e.target.value).catch(err => showCreateError(err.message));
const actions = $('submitBtn').parentElement; actions.classList.add('cc-actions'); actions.insertAdjacentHTML('afterbegin','<small id="cc-save" role="status" class="me-auto align-self-center">Klargør kladde…</small>');
toolbar.classList.add('cc-template-toolbar');
feat(planner): add overdue cases handling and improve case rendering - Introduced `overdueCases` state to track overdue cases in the planner. - Enhanced `loadCases` function to fetch and store overdue cases. - Updated `renderCases` to display overdue cases with appropriate labels. - Modified case card rendering to highlight overdue cases. fix(telefoni): ensure WebSocket connection on DOMContentLoaded - Adjusted WebSocket connection logic to handle cases where the script loads after DOMContentLoaded. test(tests): add tests for case creation and internal ticket handling - Added tests to ensure ticket creation commits all optional data. - Implemented tests for internal ticket creation, ensuring correct customer and user assignments. feat(migrations): add new tables for email configuration and case assignments - Created `email_connection_configurations` table for managing email settings. - Introduced `sag_case_assignees` table to handle individual case assignments. - Added `sag_case_details` table for structured case type details. - Updated existing case types to align with new templates and added support for internal tickets. fix(timetracking): allow manual time creation without case customer - Modified manual time creation to support entries without a customer. - Updated settlement options to recommend internal settlement for customerless time. feat(v4): enhance case detail template with new features - Added new features to the case detail template, including compact icons and inline relation creation. - Improved comment actions and time tracking functionalities.
2026-09-20 03:33:10 +02:00
buildTypes(); updateCaseTypeSections(false);
form.addEventListener('input', scheduleSave); form.addEventListener('change', scheduleSave); form.addEventListener('click', () => setTimeout(scheduleSave,0));
$('titel').addEventListener('input', () => { scheduleDuplicates(); scheduleTagSuggestions(); });
$('beskrivelse').addEventListener('input', scheduleTagSuggestions); $('ansvarlig_bruger_id').addEventListener('change', workload);
document.addEventListener('keydown', e => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter' && !document.querySelector('.modal.show')) { e.preventDefault(); if (!$('submitBtn').disabled) form.requestSubmit(); } });
window.addEventListener('pagehide', save);
try {
const user = await api('/api/v1/auth/me');
if (!(user.user_id || user.id)) throw new Error('Mangler bruger');
const params = new URLSearchParams(location.search); params.sort();
state.key = `bmc:case-draft:v1:${user.user_id || user.id}:${params.toString() || 'new'}`;
const raw = localStorage.getItem(state.key); let draft;
try { draft = raw ? JSON.parse(raw) : null; } catch { localStorage.removeItem(state.key); }
if (draft && (draft.version !== 1 || !Number.isFinite(draft.savedAt) || !draft.fields || !Array.isArray(draft.orders) || Date.now()-draft.savedAt > 7*86400000)) { localStorage.removeItem(state.key); draft=null; }
if (draft) {
state.pending = true; const prompt = document.createElement('div'); prompt.id = 'cc-draft'; prompt.className = 'alert alert-info'; prompt.innerHTML = 'Der findes en lokal kladde. <button type="button" class="btn btn-sm btn-primary" id="cc-restore">Gendan kladde</button> <button type="button" class="btn btn-sm btn-outline-secondary" id="cc-discard">Kassér</button>'; form.prepend(prompt);
$('cc-restore').onclick = () => restore(draft).catch(e => showCreateError(e.message));
$('cc-discard').onclick = () => { localStorage.removeItem(state.key); state.pending=false; prompt.remove(); scheduleSave(); };
}
$('cc-save').textContent = draft ? 'Vælg om kladden skal gendannes' : 'Kladde gemmes i denne browser';
} catch { $('cc-save').textContent = 'Autosave er ikke tilgængelig'; }
state.tagCatalog = await api('/api/v1/tags?is_active=true').catch(() => []);
state.ready = true; summaries(); renderTagSuggestions(); workload(); scheduleDuplicates(); contactOpenCases(); await loadHardwareForContacts();
}
feat(planner): add overdue cases handling and improve case rendering - Introduced `overdueCases` state to track overdue cases in the planner. - Enhanced `loadCases` function to fetch and store overdue cases. - Updated `renderCases` to display overdue cases with appropriate labels. - Modified case card rendering to highlight overdue cases. fix(telefoni): ensure WebSocket connection on DOMContentLoaded - Adjusted WebSocket connection logic to handle cases where the script loads after DOMContentLoaded. test(tests): add tests for case creation and internal ticket handling - Added tests to ensure ticket creation commits all optional data. - Implemented tests for internal ticket creation, ensuring correct customer and user assignments. feat(migrations): add new tables for email configuration and case assignments - Created `email_connection_configurations` table for managing email settings. - Introduced `sag_case_assignees` table to handle individual case assignments. - Added `sag_case_details` table for structured case type details. - Updated existing case types to align with new templates and added support for internal tickets. fix(timetracking): allow manual time creation without case customer - Modified manual time creation to support entries without a customer. - Updated settlement options to recommend internal settlement for customerless time. feat(v4): enhance case detail template with new features - Added new features to the case detail template, including compact icons and inline relation creation. - Improved comment actions and time tracking functionalities.
2026-09-20 03:33:10 +02:00
window.caseCreateUI = {init,typeChanged,contactsChanged:contactOpenCases,validate,openMessage:() => { const contacts=Object.values(selectedContacts); window.openInternalMessage({contact:contacts.length===1?contacts[0]:null}); },relations:() => { captureTypeDetails(); return {tag_ids:state.tags.map(t=>t.id),hardware_ids:state.hardware.map(h=>h.id),type_details:state.typeDetails[$('type').value] || {}}; },created: result => {
state.completed = true; clearTimeout(saveTimer);
try { if (state.key) localStorage.removeItem(state.key); if (result.tag_actions?.length) sessionStorage.setItem(`bmc:case-tag-actions:${result.id}`, JSON.stringify(result.tag_actions)); } catch { /* Case is already saved. */ }
}};
})();