bmc_hub/app/modules/sag/templates/solutions_management.html
Christian 1cfe5aee76 feat(migrations): add AI benchmark and vTiger archive tables
- Created tables for AI benchmark runs and results to facilitate model evaluation.
- Added expected answers column to benchmark results.
- Introduced tables for internet connection change cases and vTiger archive management, including records, relations, and checkpoints.
- Implemented triggers to enforce append-only behavior for vTiger archive records and files.
- Enhanced solution management with soft delete capabilities for sag_solutions and knowledge_articles.

feat(scripts): add CRM benchmarking script for Ollama models

- Developed a Python script to benchmark CRM models exposed through Ollama, including various test cases and scoring mechanisms.

test(tests): add comprehensive tests for new features

- Implemented tests for internet change case service, vTiger archive functionality, and sag solution knowledge management.
- Ensured coverage for edge cases and error handling in the new features.
2026-08-31 13:01:35 +02:00

59 lines
14 KiB
HTML

{% extends "shared/frontend/base.html" %}
{% block title %}Løsninger - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.sol-hero{background:linear-gradient(125deg,#12263f,#0f4c75 55%,#2563eb);color:#fff;border-radius:22px;padding:1.6rem 1.8rem;box-shadow:0 16px 40px rgba(15,76,117,.2)}
.sol-toolbar,.sol-card{background:var(--bg-card,#fff);border:1px solid rgba(15,76,117,.12);border-radius:16px}
.sol-card{transition:.16s ease}.sol-card:hover{border-color:rgba(37,99,235,.3);box-shadow:0 10px 24px rgba(15,76,117,.09)}
.sol-summary{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.sol-filter.active{background:#0f4c75;color:#fff;border-color:#0f4c75}.sol-chip{border-radius:999px;padding:.25rem .65rem;font-size:.75rem;font-weight:600}
.offcanvas.sol-editor{width:min(760px,100vw)}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid px-4 py-4">
<section class="sol-hero mb-4 d-flex flex-wrap justify-content-between align-items-center gap-3">
<div><div class="small text-uppercase fw-semibold opacity-75">Sager · Viden</div><h1 class="h2 fw-bold mb-1">Løsninger</h1><p class="mb-0 opacity-75">Rediger, kvalitetssikr og udgiv sagernes løsninger.</p></div>
<div class="d-flex gap-2"><a href="/knowledge" class="btn btn-light"><i class="bi bi-journal-richtext me-1"></i>Vidensdatabase</a><a href="/sag" class="btn btn-outline-light"><i class="bi bi-folder2 me-1"></i>Sager</a></div>
</section>
<section class="sol-toolbar p-3 mb-4">
<div class="row g-3 align-items-center"><div class="col-xl-5"><div class="input-group"><span class="input-group-text bg-transparent"><i class="bi bi-search"></i></span><input id="solSearch" class="form-control" type="search" placeholder="Søg i titel, problem, løsning, sag eller kunde…"></div></div>
<div class="col-xl-7 d-flex flex-wrap gap-2 justify-content-xl-end"><button class="btn btn-sm btn-outline-secondary sol-filter active" data-status="">Alle</button><button class="btn btn-sm btn-outline-secondary sol-filter" data-status="draft">Kladder</button><button class="btn btn-sm btn-outline-secondary sol-filter" data-status="pending">Afventer</button><button class="btn btn-sm btn-outline-secondary sol-filter" data-status="approved">Godkendte</button><button id="archivedToggle" class="btn btn-sm btn-outline-danger"><i class="bi bi-archive me-1"></i>Arkiverede</button></div></div>
</section>
<div class="d-flex justify-content-between align-items-center mb-3"><h2 class="h5 mb-0" id="solHeading">Aktive løsninger</h2><span class="badge rounded-pill text-bg-light border" id="solCount">Henter…</span></div>
<div id="solState" class="text-center text-muted py-5"><div class="spinner-border spinner-border-sm me-2"></div>Henter løsninger…</div><div id="solGrid" class="row g-3"></div>
</div>
<div class="offcanvas offcanvas-end sol-editor" tabindex="-1" id="solutionEditor"><div class="offcanvas-header border-bottom"><div><div class="small text-uppercase text-primary fw-semibold">Løsning</div><h2 class="offcanvas-title h4 mb-0" id="editorHeading">Rediger løsning</h2></div><button class="btn-close" data-bs-dismiss="offcanvas"></button></div><div class="offcanvas-body">
<input type="hidden" id="editSagId"><div class="mb-3"><label class="form-label">Titel *</label><input class="form-control" id="editTitle"></div>
<div class="row g-3 mb-3"><div class="col-md-4"><label class="form-label">Type</label><select class="form-select" id="editType"><option>Support</option><option>Drift</option><option>Konsulent</option><option>Infrastruktur</option><option>Ekstern</option></select></div><div class="col-md-4"><label class="form-label">Resultat</label><select class="form-select" id="editResult"><option>Løst</option><option>Delvist</option><option>Workaround</option><option>Ej løst</option></select></div><div class="col-md-4"><label class="form-label">Synlighed</label><select class="form-select" id="editVisibility"><option value="internal">Kun internt</option><option value="general">Generel viden</option><option value="customer">Kun denne kunde</option></select></div></div>
<div class="mb-3"><label class="form-label">Problem og symptomer</label><textarea class="form-control" id="editProblem" rows="3"></textarea></div>
<div class="row g-3 mb-3"><div class="col-md-6"><label class="form-label">Årsag</label><textarea class="form-control" id="editCause" rows="3"></textarea></div><div class="col-md-6"><label class="form-label">Undersøgelse</label><textarea class="form-control" id="editInvestigation" rows="3"></textarea></div></div>
<div class="mb-3"><label class="form-label">Endelig løsning *</label><textarea class="form-control" id="editDescription" rows="5"></textarea></div><div class="mb-3"><label class="form-label">Workaround</label><textarea class="form-control" id="editWorkaround" rows="2"></textarea></div>
<div class="row g-3 mb-3"><div class="col-md-6"><label class="form-label">Tags</label><input class="form-control" id="editTags"></div><div class="col-md-6"><label class="form-label">Produkter/systemer</label><input class="form-control" id="editProducts"></div></div><div class="mb-4"><label class="form-label">Ændringsnote</label><input class="form-control" id="editNote" placeholder="Hvad er ændret?"></div>
<div class="d-flex justify-content-between gap-2 border-top pt-3"><a id="editorCaseLink" class="btn btn-outline-secondary"><i class="bi bi-box-arrow-up-right me-1"></i>Åbn sag</a><button class="btn btn-primary" onclick="saveManagedSolution()"><i class="bi bi-check2 me-1"></i>Gem ændringer</button></div>
</div></div>
<div class="modal fade" id="archiveSolutionModal" tabindex="-1" data-bs-backdrop="static"><div class="modal-dialog modal-dialog-centered"><div class="modal-content border-0 shadow"><div class="modal-body p-4"><div class="d-flex gap-3"><div class="fs-2 text-danger"><i class="bi bi-archive"></i></div><div><h2 class="h4">Arkivér løsningen?</h2><p class="text-muted mb-2">Løsningen fjernes fra sagen og en udgivet vidensartikel skjules. Historik og data bevares.</p><div class="fw-semibold" id="archiveSolutionName"></div></div></div></div><div class="modal-footer border-0 pt-0"><button class="btn btn-outline-secondary" data-bs-dismiss="modal">Annuller</button><button class="btn btn-danger" id="archiveConfirmBtn">Ja, arkivér</button></div></div></div></div>
<script>
(() => {
const state={items:new Map(),status:'',archived:false,timer:null,archiveId:null}; const esc=v=>String(v??'').replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]));
const labels={draft:'Kladde',pending:'Afventer godkendelse',approved:'Godkendt',outdated:'Forældet',rejected:'Afvist'};
async function api(url,options={}){const r=await fetch(url,{credentials:'include',...options});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(typeof e.detail==='string'?e.detail:'Handlingen mislykkedes');}return r.json();}
async function load(){const box=document.getElementById('solState'),grid=document.getElementById('solGrid'),q=document.getElementById('solSearch').value.trim();box.classList.remove('d-none');box.innerHTML='<div class="spinner-border spinner-border-sm me-2"></div>Henter…';grid.innerHTML='';
try{const d=await api(`/api/v1/solutions?q=${encodeURIComponent(q)}&approval_status=${encodeURIComponent(state.status)}&include_archived=${state.archived}`);state.items=new Map(d.items.map(x=>[x.id,x]));document.getElementById('solCount').textContent=`${d.total} løsninger`;document.getElementById('solHeading').textContent=state.archived?'Arkiverede løsninger':'Aktive løsninger';box.classList.toggle('d-none',d.items.length>0);if(!d.items.length)box.innerHTML='<i class="bi bi-lightbulb fs-1 d-block mb-2"></i>Ingen løsninger matcher.';
grid.innerHTML=d.items.map(s=>`<div class="col-xxl-4 col-lg-6"><article class="sol-card h-100 p-4"><div class="d-flex justify-content-between gap-2 mb-2"><div class="d-flex gap-1"><span class="sol-chip ${s.approval_status==='approved'?'bg-success-subtle text-success-emphasis':s.approval_status==='pending'?'bg-warning-subtle text-warning-emphasis':'bg-secondary-subtle text-secondary-emphasis'}">${labels[s.approval_status]||esc(s.approval_status)}</span><span class="sol-chip bg-light border">${s.visibility==='general'?'Generel':s.visibility==='customer'?'Kunde':'Intern'}</span></div><span class="small text-muted">#${s.sag_id}</span></div><h3 class="h5 fw-bold">${esc(s.title)}</h3><p class="sol-summary text-muted">${esc(s.problem||s.description||'Ingen beskrivelse')}</p><div class="small text-muted mb-3">${esc(s.customer_name||'Ingen kunde')} · ${esc(s.updated_by||s.created_by||'Ukendt')}</div><div class="d-flex flex-wrap gap-2"><a class="btn btn-sm btn-outline-secondary" href="/sag/${s.sag_id}/v3#solution">Sag</a>${state.archived?`<button class="btn btn-sm btn-success" onclick="restoreManagedSolution(${s.id})"><i class="bi bi-arrow-counterclockwise me-1"></i>Gendan</button>`:`<button class="btn btn-sm btn-outline-primary" onclick="openManagedSolution(${s.id})"><i class="bi bi-pencil me-1"></i>Rediger</button>${s.approval_status==='pending'?`<button class="btn btn-sm btn-success" onclick="solutionAction(${s.sag_id},'approve')">Godkend</button>`:''}${s.approval_status==='approved'?`<button class="btn btn-sm btn-primary" onclick="publishManagedSolution(${s.sag_id})">Udgiv</button>`:''}<button class="btn btn-sm btn-outline-danger ms-auto" onclick="askArchiveSolution(${s.id})" title="Arkivér"><i class="bi bi-archive"></i></button>`}</div></article></div>`).join('');
}catch(e){box.classList.remove('d-none');box.innerHTML=`<i class="bi bi-exclamation-triangle text-danger d-block fs-2"></i>${esc(e.message)}`;}}
window.openManagedSolution=id=>{const s=state.items.get(id);if(!s)return;document.getElementById('editSagId').value=s.sag_id;document.getElementById('editorHeading').textContent=s.title;document.getElementById('editTitle').value=s.title||'';document.getElementById('editType').value=s.solution_type||'Support';document.getElementById('editResult').value=s.result||'Løst';document.getElementById('editVisibility').value=s.visibility||'internal';document.getElementById('editProblem').value=s.problem||'';document.getElementById('editCause').value=s.root_cause||'';document.getElementById('editInvestigation').value=s.investigation||'';document.getElementById('editDescription').value=s.description||'';document.getElementById('editWorkaround').value=s.workaround||'';document.getElementById('editTags').value=(s.tags||[]).join(', ');document.getElementById('editProducts').value=(s.products||[]).join(', ');document.getElementById('editNote').value='';document.getElementById('editorCaseLink').href=`/sag/${s.sag_id}/v3#solution`;bootstrap.Offcanvas.getOrCreateInstance(document.getElementById('solutionEditor')).show();};
window.saveManagedSolution=async()=>{const id=document.getElementById('editSagId').value,payload={title:document.getElementById('editTitle').value.trim(),solution_type:document.getElementById('editType').value,result:document.getElementById('editResult').value,visibility:document.getElementById('editVisibility').value,problem:document.getElementById('editProblem').value.trim(),root_cause:document.getElementById('editCause').value.trim(),investigation:document.getElementById('editInvestigation').value.trim(),description:document.getElementById('editDescription').value.trim(),workaround:document.getElementById('editWorkaround').value.trim(),tags:document.getElementById('editTags').value.split(',').map(x=>x.trim()).filter(Boolean),products:document.getElementById('editProducts').value.split(',').map(x=>x.trim()).filter(Boolean),change_note:document.getElementById('editNote').value.trim()||'Redigeret fra Løsninger'};if(!payload.title||!payload.description){alert('Titel og endelig løsning skal udfyldes');return;}try{await api(`/api/v1/sag/${id}/solution`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});bootstrap.Offcanvas.getInstance(document.getElementById('solutionEditor'))?.hide();load();}catch(e){alert(e.message);}};
window.solutionAction=async(sag,action)=>{try{await api(`/api/v1/sag/${sag}/solution/workflow`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action})});load();}catch(e){alert(e.message);}};
window.publishManagedSolution=async sag=>{try{const a=await api(`/api/v1/sag/${sag}/solution/publish`,{method:'POST'});location.href=`/knowledge/${a.id}`;}catch(e){alert(e.message);}};
window.askArchiveSolution=id=>{state.archiveId=id;document.getElementById('archiveSolutionName').textContent=state.items.get(id)?.title||'';bootstrap.Modal.getOrCreateInstance(document.getElementById('archiveSolutionModal')).show();};
document.getElementById('archiveConfirmBtn').addEventListener('click',async()=>{try{await api(`/api/v1/solutions/${state.archiveId}`,{method:'DELETE'});bootstrap.Modal.getInstance(document.getElementById('archiveSolutionModal'))?.hide();load();}catch(e){alert(e.message);}});
window.restoreManagedSolution=async id=>{try{await api(`/api/v1/solutions/${id}/restore`,{method:'POST'});load();}catch(e){alert(e.message);}};
document.querySelectorAll('.sol-filter').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('.sol-filter').forEach(x=>x.classList.remove('active'));b.classList.add('active');state.status=b.dataset.status;load();}));document.getElementById('archivedToggle').addEventListener('click',e=>{state.archived=!state.archived;e.currentTarget.classList.toggle('btn-danger',state.archived);e.currentTarget.classList.toggle('btn-outline-danger',!state.archived);load();});document.getElementById('solSearch').addEventListener('input',()=>{clearTimeout(state.timer);state.timer=setTimeout(load,280)});load();
})();
</script>
{% endblock %}