Add ability to change case customer from case detail

This commit is contained in:
Christian 2026-04-03 01:24:20 +02:00
parent fb2243f0d4
commit 1f834160ca
2 changed files with 84 additions and 5 deletions

View File

@ -161,6 +161,14 @@ def _validate_group_id(group_id: Optional[int], field_name: str = "assigned_grou
if not exists: if not exists:
raise HTTPException(status_code=400, detail=f"Invalid {field_name}") raise HTTPException(status_code=400, detail=f"Invalid {field_name}")
def _validate_customer_id(customer_id: Optional[int], field_name: str = "customer_id") -> None:
if customer_id is None:
return
exists = execute_query("SELECT 1 FROM customers WHERE id = %s", (customer_id,))
if not exists:
raise HTTPException(status_code=400, detail=f"Invalid {field_name}")
# ============================================================================ # ============================================================================
# QUICKCREATE AI ANALYSIS # QUICKCREATE AI ANALYSIS
# ============================================================================ # ============================================================================
@ -973,6 +981,9 @@ async def update_sag(sag_id: int, updates: dict):
if "assigned_group_id" in updates: if "assigned_group_id" in updates:
updates["assigned_group_id"] = _coerce_optional_int(updates.get("assigned_group_id"), "assigned_group_id") updates["assigned_group_id"] = _coerce_optional_int(updates.get("assigned_group_id"), "assigned_group_id")
_validate_group_id(updates["assigned_group_id"]) _validate_group_id(updates["assigned_group_id"])
if "customer_id" in updates:
updates["customer_id"] = _coerce_optional_int(updates.get("customer_id"), "customer_id")
_validate_customer_id(updates["customer_id"])
# Build dynamic update query # Build dynamic update query
allowed_fields = [ allowed_fields = [
@ -980,6 +991,7 @@ async def update_sag(sag_id: int, updates: dict):
"beskrivelse", "beskrivelse",
"template_key", "template_key",
"status", "status",
"customer_id",
"ansvarlig_bruger_id", "ansvarlig_bruger_id",
"assigned_group_id", "assigned_group_id",
"priority", "priority",

View File

@ -2376,9 +2376,19 @@
<div class="case-meta-cell"> <div class="case-meta-cell">
<div class="case-meta-label"><i class="bi bi-building"></i>Kunde</div> <div class="case-meta-label"><i class="bi bi-building"></i>Kunde</div>
{% if customer %} {% if customer %}
<div class="d-flex align-items-center gap-2 flex-wrap">
<a href="/customers/{{ customer.id }}" class="case-meta-value case-meta-link" style="color:{{ tcolor }}; font-size:1.0rem; font-weight:700;">{{ customer.name }}</a> <a href="/customers/{{ customer.id }}" class="case-meta-value case-meta-link" style="color:{{ tcolor }}; font-size:1.0rem; font-weight:700;">{{ customer.name }}</a>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="showCustomerSearch('replace')" title="Skift primær kunde på sagen">
<i class="bi bi-arrow-left-right me-1"></i>Skift kunde
</button>
</div>
{% else %} {% else %}
<div class="d-flex align-items-center gap-2 flex-wrap">
<span class="case-meta-value text-muted fst-italic">Ingen</span> <span class="case-meta-value text-muted fst-italic">Ingen</span>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="showCustomerSearch('replace')" title="Vælg kunde til sagen">
<i class="bi bi-plus-lg me-1"></i>Vælg kunde
</button>
</div>
{% endif %} {% endif %}
</div> </div>
@ -2608,6 +2618,9 @@
<div id="sag-titel-view" class="d-flex align-items-center gap-2"> <div id="sag-titel-view" class="d-flex align-items-center gap-2">
<h2 id="sag-titel-text" class="mb-2 fw-bolder" style="color: var(--accent); font-size: 1.8rem; letter-spacing: -0.5px;">{{ case.titel }}</h2> <h2 id="sag-titel-text" class="mb-2 fw-bolder" style="color: var(--accent); font-size: 1.8rem; letter-spacing: -0.5px;">{{ case.titel }}</h2>
<button class="btn btn-sm btn-link text-muted p-0 mb-1" onclick="startTitelEdit()" title="Rediger overskrift"><i class="bi bi-pencil-square"></i></button> <button class="btn btn-sm btn-link text-muted p-0 mb-1" onclick="startTitelEdit()" title="Rediger overskrift"><i class="bi bi-pencil-square"></i></button>
<button class="btn btn-sm btn-outline-primary mb-1" onclick="openAssignmentQuick()" title="Ændr hvem sagen er tildelt til">
<i class="bi bi-person-check me-1"></i>Tildel sag
</button>
</div> </div>
<!-- Title edit --> <!-- Title edit -->
<div id="sag-titel-editor" class="d-none"> <div id="sag-titel-editor" class="d-none">
@ -2991,7 +3004,7 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Søg kunde</h5> <h5 class="modal-title" id="customerSearchModalTitle">Søg kunde</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@ -3325,6 +3338,7 @@
let relationSearchTimeout; let relationSearchTimeout;
let wikiSearchTimeout; let wikiSearchTimeout;
let selectedRelationCaseId = null; let selectedRelationCaseId = null;
let customerSearchMode = 'link';
const caseTypeKey = {{ ((case.template_key or case.type or 'ticket')|lower)|tojson }}; const caseTypeKey = {{ ((case.template_key or case.type or 'ticket')|lower)|tojson }};
function escapeCaseTopAlertHtml(value) { function escapeCaseTopAlertHtml(value) {
@ -3548,7 +3562,12 @@
setTimeout(() => document.getElementById('contactSearch').focus(), 300); setTimeout(() => document.getElementById('contactSearch').focus(), 300);
} }
function showCustomerSearch() { function showCustomerSearch(mode = 'link') {
customerSearchMode = mode === 'replace' ? 'replace' : 'link';
const title = document.getElementById('customerSearchModalTitle');
if (title) {
title.textContent = customerSearchMode === 'replace' ? 'Skift kunde på sag' : 'Søg kunde';
}
customerSearchModal.show(); customerSearchModal.show();
setTimeout(() => document.getElementById('customerSearch').focus(), 300); setTimeout(() => document.getElementById('customerSearch').focus(), 300);
} }
@ -4025,7 +4044,7 @@
} else { } else {
resultsDiv.innerHTML = customers.map(c => ` resultsDiv.innerHTML = customers.map(c => `
<div class="list-group-item list-group-item-action" style="cursor: pointer;" <div class="list-group-item list-group-item-action" style="cursor: pointer;"
onclick="addCustomer(${caseId}, ${c.id}, '${c.name.replace(/'/g, "\\'")}')"> onclick="selectCustomerFromSearch(${caseId}, ${c.id}, '${c.name.replace(/'/g, "\\'")}')">
<strong>${c.name}</strong> <strong>${c.name}</strong>
<div class="small text-muted">${c.email || ''} ${c.cvr_number ? '(CVR: ' + c.cvr_number + ')' : ''}</div> <div class="small text-muted">${c.email || ''} ${c.cvr_number ? '(CVR: ' + c.cvr_number + ')' : ''}</div>
</div> </div>
@ -4038,6 +4057,34 @@
}); });
} }
async function selectCustomerFromSearch(caseId, customerId, customerName) {
if (customerSearchMode === 'replace') {
await replaceCaseCustomer(caseId, customerId, customerName);
return;
}
await addCustomer(caseId, customerId, customerName);
}
async function replaceCaseCustomer(caseId, customerId, customerName) {
try {
const response = await fetch(`/api/v1/sag/${caseId}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({customer_id: customerId})
});
if (response.ok) {
customerSearchModal.hide();
window.location.reload();
} else {
const error = await response.json().catch(() => ({}));
alert(`Fejl ved skift af kunde: ${error.detail || response.statusText || 'Ukendt fejl'}`);
}
} catch (err) {
alert('Fejl ved skift af kunde: ' + err.message);
}
}
async function addCustomer(caseId, customerId, customerName) { async function addCustomer(caseId, customerId, customerName) {
try { try {
const response = await fetch(`/api/v1/sag/${caseId}/customers`, { const response = await fetch(`/api/v1/sag/${caseId}/customers`, {
@ -10010,6 +10057,26 @@
} }
} }
function openAssignmentQuick() {
const preferred = document.getElementById('tabsAssignmentUserSelect')
|| document.getElementById('assignmentUserSelect');
const fallback = document.getElementById('tabsAssignmentGroupSelect')
|| document.getElementById('assignmentGroupSelect');
const target = preferred || fallback;
if (!target) {
alert('Kunne ikke finde tildelingsfelter på siden.');
return;
}
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => target.focus(), 200);
if (typeof showToast === 'function') {
showToast('Vælg ansvarlig bruger/gruppe i feltet, så gemmes det med det samme.', 'info');
}
}
async function savePipeline() { async function savePipeline() {
const stageValue = document.getElementById('pipelineStageSelect').value; const stageValue = document.getElementById('pipelineStageSelect').value;
const probabilityValue = document.getElementById('pipelineProbabilityInput').value; const probabilityValue = document.getElementById('pipelineProbabilityInput').value;