552 lines
22 KiB
HTML
552 lines
22 KiB
HTML
|
|
<!-- Alert Note Create/Edit Modal -->
|
|||
|
|
<div class="modal fade" id="alertNoteFormModal" tabindex="-1" aria-labelledby="alertNoteFormModalLabel" aria-hidden="true">
|
|||
|
|
<div class="modal-dialog modal-lg">
|
|||
|
|
<div class="modal-content">
|
|||
|
|
<div class="modal-header bg-warning bg-opacity-10 border-bottom border-warning">
|
|||
|
|
<h5 class="modal-title d-flex align-items-center" id="alertNoteFormModalLabel">
|
|||
|
|
<i class="bi bi-exclamation-triangle-fill text-warning me-2" style="font-size: 1.3rem;"></i>
|
|||
|
|
<span id="alertFormTitle" class="fw-bold">Opret Alert Note</span>
|
|||
|
|
</h5>
|
|||
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Luk"></button>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
|
|||
|
|
<form id="alertNoteForm">
|
|||
|
|
<input type="hidden" id="alertNoteId" value="">
|
|||
|
|
<input type="hidden" id="alertEntityType" value="">
|
|||
|
|
<input type="hidden" id="alertEntityId" value="">
|
|||
|
|
|
|||
|
|
<!-- Titel Section -->
|
|||
|
|
<div class="mb-4">
|
|||
|
|
<label for="alertTitle" class="form-label fw-semibold">
|
|||
|
|
Titel <span class="text-danger">*</span>
|
|||
|
|
</label>
|
|||
|
|
<input type="text"
|
|||
|
|
class="form-control form-control-lg"
|
|||
|
|
id="alertTitle"
|
|||
|
|
required
|
|||
|
|
maxlength="255"
|
|||
|
|
placeholder="Kort beskrivende titel">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Besked Section -->
|
|||
|
|
<div class="mb-4">
|
|||
|
|
<label for="alertMessage" class="form-label fw-semibold">
|
|||
|
|
Besked <span class="text-danger">*</span>
|
|||
|
|
</label>
|
|||
|
|
<textarea class="form-control"
|
|||
|
|
id="alertMessage"
|
|||
|
|
rows="6"
|
|||
|
|
required
|
|||
|
|
placeholder="Detaljeret information der skal vises..."
|
|||
|
|
style="font-family: inherit; line-height: 1.6;"></textarea>
|
|||
|
|
<div class="form-text mt-2">
|
|||
|
|
<i class="bi bi-info-circle me-1"></i>
|
|||
|
|
Du kan bruge linjeskift for formatering
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Alvorlighed Section -->
|
|||
|
|
<div class="mb-4">
|
|||
|
|
<label for="alertSeverity" class="form-label fw-semibold">
|
|||
|
|
Alvorlighed <span class="text-danger">*</span>
|
|||
|
|
</label>
|
|||
|
|
<select class="form-select form-select-lg" id="alertSeverity" required>
|
|||
|
|
<option value="info">ℹ️ Info - General kontekst</option>
|
|||
|
|
<option value="warning" selected>⚠️ Advarsel - Særlige forhold</option>
|
|||
|
|
<option value="critical">🚨 Kritisk - Følsomme forhold</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Checkboxes Section -->
|
|||
|
|
<div class="mb-4 p-3 bg-light rounded">
|
|||
|
|
<div class="form-check mb-3">
|
|||
|
|
<input class="form-check-input"
|
|||
|
|
type="checkbox"
|
|||
|
|
id="alertRequiresAck"
|
|||
|
|
checked>
|
|||
|
|
<label class="form-check-label" for="alertRequiresAck">
|
|||
|
|
<strong>Kræv bekræftelse</strong>
|
|||
|
|
<div class="text-muted small mt-1">
|
|||
|
|
Brugere skal klikke "Forstået" for at bekræfte at de har set advarslen
|
|||
|
|
</div>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-check">
|
|||
|
|
<input class="form-check-input"
|
|||
|
|
type="checkbox"
|
|||
|
|
id="alertActive"
|
|||
|
|
checked>
|
|||
|
|
<label class="form-check-label" for="alertActive">
|
|||
|
|
<strong>Aktiv</strong>
|
|||
|
|
<div class="text-muted small mt-1">
|
|||
|
|
Alert noten vises på kunde/kontakt siden
|
|||
|
|
</div>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<hr class="my-4">
|
|||
|
|
|
|||
|
|
<!-- Restrictions Section -->
|
|||
|
|
<div class="mb-3">
|
|||
|
|
<label class="form-label fw-semibold d-flex align-items-center mb-3">
|
|||
|
|
<i class="bi bi-shield-lock me-2 text-primary"></i>
|
|||
|
|
Begrænsninger (Valgfri)
|
|||
|
|
</label>
|
|||
|
|
<div class="alert alert-info d-flex align-items-start mb-4">
|
|||
|
|
<i class="bi bi-info-circle-fill me-2 mt-1"></i>
|
|||
|
|
<div>
|
|||
|
|
<strong>Hvad er begrænsninger?</strong>
|
|||
|
|
<p class="mb-0 mt-1 small">
|
|||
|
|
Angiv hvilke grupper eller brugere der må håndtere denne kunde/kontakt.
|
|||
|
|
Lad felterne stå tomme hvis alle må håndtere kunden.
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="row">
|
|||
|
|
<div class="col-md-6 mb-3">
|
|||
|
|
<label for="alertGroups" class="form-label fw-semibold">
|
|||
|
|
<i class="bi bi-people-fill me-1"></i>
|
|||
|
|
Godkendte Grupper
|
|||
|
|
</label>
|
|||
|
|
<select class="form-select" id="alertGroups" multiple size="5">
|
|||
|
|
<!-- Populated via JavaScript -->
|
|||
|
|
</select>
|
|||
|
|
<div class="form-text mt-2">
|
|||
|
|
<i class="bi bi-hand-index me-1"></i>
|
|||
|
|
Hold Ctrl/Cmd for at vælge flere
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="col-md-6 mb-3">
|
|||
|
|
<label for="alertUsers" class="form-label fw-semibold">
|
|||
|
|
<i class="bi bi-person-fill me-1"></i>
|
|||
|
|
Godkendte Brugere
|
|||
|
|
</label>
|
|||
|
|
<select class="form-select" id="alertUsers" multiple size="5">
|
|||
|
|
<!-- Populated via JavaScript -->
|
|||
|
|
</select>
|
|||
|
|
<div class="form-text mt-2">
|
|||
|
|
<i class="bi bi-hand-index me-1"></i>
|
|||
|
|
Hold Ctrl/Cmd for at vælge flere
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer bg-light">
|
|||
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|||
|
|
<i class="bi bi-x-circle me-2"></i>
|
|||
|
|
Annuller
|
|||
|
|
</button>
|
|||
|
|
<button type="button" class="btn btn-primary btn-lg" id="saveAlertNoteBtn" onclick="saveAlertNote()">
|
|||
|
|
<i class="bi bi-save me-2"></i>
|
|||
|
|
Gem Alert Note
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<style>
|
|||
|
|
/* Modal Header Styling */
|
|||
|
|
#alertNoteFormModal .modal-header {
|
|||
|
|
padding: 1.25rem 1.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#alertNoteFormModal .modal-body {
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#alertNoteFormModal .modal-footer {
|
|||
|
|
padding: 1rem 1.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Form Labels */
|
|||
|
|
#alertNoteFormModal .form-label {
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: var(--bs-body-color);
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Input Fields */
|
|||
|
|
#alertNoteFormModal .form-control,
|
|||
|
|
#alertNoteFormModal .form-select {
|
|||
|
|
border-radius: 8px;
|
|||
|
|
border: 1px solid #dee2e6;
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#alertNoteFormModal .form-control:focus,
|
|||
|
|
#alertNoteFormModal .form-select:focus {
|
|||
|
|
border-color: var(--bs-primary);
|
|||
|
|
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.15);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Textarea specific */
|
|||
|
|
#alertNoteFormModal textarea.form-control {
|
|||
|
|
resize: vertical;
|
|||
|
|
min-height: 150px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Multiselect Styling */
|
|||
|
|
#alertNoteFormModal select[multiple] {
|
|||
|
|
border: 2px solid #e0e0e0;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 0.5rem;
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#alertNoteFormModal select[multiple]:focus {
|
|||
|
|
border-color: var(--bs-primary);
|
|||
|
|
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.15);
|
|||
|
|
outline: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#alertNoteFormModal select[multiple] option {
|
|||
|
|
padding: 10px 12px;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
margin-bottom: 4px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.15s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#alertNoteFormModal select[multiple] option:hover {
|
|||
|
|
background: rgba(13, 110, 253, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#alertNoteFormModal select[multiple] option:checked {
|
|||
|
|
background: var(--bs-primary);
|
|||
|
|
color: white;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Checkbox Container */
|
|||
|
|
#alertNoteFormModal .form-check {
|
|||
|
|
padding: 0.75rem;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
transition: background 0.2s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#alertNoteFormModal .form-check:hover {
|
|||
|
|
background: rgba(0, 0, 0, 0.02);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[data-bs-theme="dark"] #alertNoteFormModal .form-check:hover {
|
|||
|
|
background: rgba(255, 255, 255, 0.05);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#alertNoteFormModal .form-check-input {
|
|||
|
|
width: 1.25rem;
|
|||
|
|
height: 1.25rem;
|
|||
|
|
margin-top: 0.125rem;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#alertNoteFormModal .form-check-label {
|
|||
|
|
cursor: pointer;
|
|||
|
|
user-select: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Alert Info Box */
|
|||
|
|
#alertNoteFormModal .alert-info {
|
|||
|
|
border-left: 4px solid var(--bs-info);
|
|||
|
|
background: rgba(13, 202, 240, 0.1);
|
|||
|
|
border-radius: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[data-bs-theme="dark"] #alertNoteFormModal .alert-info {
|
|||
|
|
background: rgba(13, 202, 240, 0.15);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Background Color Theme Support */
|
|||
|
|
[data-bs-theme="dark"] #alertNoteFormModal .bg-light {
|
|||
|
|
background: rgba(255, 255, 255, 0.05) !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[data-bs-theme="dark"] #alertNoteFormModal .modal-header {
|
|||
|
|
background: rgba(255, 193, 7, 0.1) !important;
|
|||
|
|
border-bottom-color: rgba(255, 193, 7, 0.3) !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Form Text Helpers */
|
|||
|
|
#alertNoteFormModal .form-text {
|
|||
|
|
font-size: 0.875rem;
|
|||
|
|
color: #6c757d;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Divider */
|
|||
|
|
#alertNoteFormModal hr {
|
|||
|
|
margin: 1.5rem 0;
|
|||
|
|
opacity: 0.1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Responsive adjustments */
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
#alertNoteFormModal .row > .col-md-6 {
|
|||
|
|
margin-bottom: 1rem !important;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
let alertFormModal = null;
|
|||
|
|
let currentAlertEntityType = null;
|
|||
|
|
let currentAlertEntityId = null;
|
|||
|
|
|
|||
|
|
async function openAlertNoteForm(entityType, entityId, alertId = null) {
|
|||
|
|
currentAlertEntityType = entityType;
|
|||
|
|
currentAlertEntityId = entityId;
|
|||
|
|
|
|||
|
|
// Load groups and users for restrictions
|
|||
|
|
await loadGroupsAndUsers();
|
|||
|
|
|
|||
|
|
if (alertId) {
|
|||
|
|
// Edit mode
|
|||
|
|
await loadAlertForEdit(alertId);
|
|||
|
|
document.getElementById('alertFormTitle').textContent = 'Rediger Alert Note';
|
|||
|
|
} else {
|
|||
|
|
// Create mode
|
|||
|
|
document.getElementById('alertFormTitle').textContent = 'Opret Alert Note';
|
|||
|
|
document.getElementById('alertNoteForm').reset();
|
|||
|
|
document.getElementById('alertNoteId').value = '';
|
|||
|
|
document.getElementById('alertRequiresAck').checked = true;
|
|||
|
|
document.getElementById('alertActive').checked = true;
|
|||
|
|
document.getElementById('alertSeverity').value = 'warning';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.getElementById('alertEntityType').value = entityType;
|
|||
|
|
document.getElementById('alertEntityId').value = entityId;
|
|||
|
|
|
|||
|
|
// Show modal
|
|||
|
|
const modalEl = document.getElementById('alertNoteFormModal');
|
|||
|
|
alertFormModal = new bootstrap.Modal(modalEl);
|
|||
|
|
alertFormModal.show();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadGroupsAndUsers() {
|
|||
|
|
try {
|
|||
|
|
// Load groups
|
|||
|
|
const groupsResponse = await fetch('/api/v1/admin/groups', {
|
|||
|
|
credentials: 'include'
|
|||
|
|
});
|
|||
|
|
if (groupsResponse.ok) {
|
|||
|
|
const groups = await groupsResponse.json();
|
|||
|
|
const groupsSelect = document.getElementById('alertGroups');
|
|||
|
|
groupsSelect.innerHTML = groups.map(g =>
|
|||
|
|
`<option value="${g.id}">${g.name}</option>`
|
|||
|
|
).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Load users
|
|||
|
|
const usersResponse = await fetch('/api/v1/admin/users', {
|
|||
|
|
credentials: 'include'
|
|||
|
|
});
|
|||
|
|
if (usersResponse.ok) {
|
|||
|
|
const users = await usersResponse.json();
|
|||
|
|
const usersSelect = document.getElementById('alertUsers');
|
|||
|
|
usersSelect.innerHTML = users.map(u =>
|
|||
|
|
`<option value="${u.user_id}">${u.full_name || u.username} (${u.username})</option>`
|
|||
|
|
).join('');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error loading groups/users:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadAlertForEdit(alertId) {
|
|||
|
|
try {
|
|||
|
|
const response = await fetch(`/api/v1/alert-notes?entity_type=${currentAlertEntityType}&entity_id=${currentAlertEntityId}`, {
|
|||
|
|
credentials: 'include'
|
|||
|
|
});
|
|||
|
|
if (!response.ok) throw new Error('Failed to load alert');
|
|||
|
|
|
|||
|
|
const alerts = await response.json();
|
|||
|
|
const alert = alerts.find(a => a.id === alertId);
|
|||
|
|
|
|||
|
|
if (!alert) throw new Error('Alert not found');
|
|||
|
|
|
|||
|
|
document.getElementById('alertNoteId').value = alert.id;
|
|||
|
|
document.getElementById('alertTitle').value = alert.title;
|
|||
|
|
document.getElementById('alertMessage').value = alert.message;
|
|||
|
|
document.getElementById('alertSeverity').value = alert.severity;
|
|||
|
|
document.getElementById('alertRequiresAck').checked = alert.requires_acknowledgement;
|
|||
|
|
document.getElementById('alertActive').checked = alert.active;
|
|||
|
|
|
|||
|
|
// Set restrictions
|
|||
|
|
if (alert.restrictions && alert.restrictions.length > 0) {
|
|||
|
|
const groupIds = alert.restrictions
|
|||
|
|
.filter(r => r.restriction_type === 'group')
|
|||
|
|
.map(r => r.restriction_id);
|
|||
|
|
const userIds = alert.restrictions
|
|||
|
|
.filter(r => r.restriction_type === 'user')
|
|||
|
|
.map(r => r.restriction_id);
|
|||
|
|
|
|||
|
|
// Select options
|
|||
|
|
Array.from(document.getElementById('alertGroups').options).forEach(opt => {
|
|||
|
|
opt.selected = groupIds.includes(parseInt(opt.value));
|
|||
|
|
});
|
|||
|
|
Array.from(document.getElementById('alertUsers').options).forEach(opt => {
|
|||
|
|
opt.selected = userIds.includes(parseInt(opt.value));
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error loading alert for edit:', error);
|
|||
|
|
alert('Kunne ikke indlæse alert. Prøv igen.');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function saveAlertNote() {
|
|||
|
|
const form = document.getElementById('alertNoteForm');
|
|||
|
|
if (!form.checkValidity()) {
|
|||
|
|
form.reportValidity();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const alertId = document.getElementById('alertNoteId').value;
|
|||
|
|
const isEdit = !!alertId;
|
|||
|
|
|
|||
|
|
// Get selected groups and users
|
|||
|
|
const selectedGroups = Array.from(document.getElementById('alertGroups').selectedOptions)
|
|||
|
|
.map(opt => parseInt(opt.value));
|
|||
|
|
const selectedUsers = Array.from(document.getElementById('alertUsers').selectedOptions)
|
|||
|
|
.map(opt => parseInt(opt.value));
|
|||
|
|
|
|||
|
|
// Build data object - different structure for create vs update
|
|||
|
|
let data;
|
|||
|
|
if (isEdit) {
|
|||
|
|
// PATCH: Only send fields to update (no entity_type, entity_id)
|
|||
|
|
data = {
|
|||
|
|
title: document.getElementById('alertTitle').value,
|
|||
|
|
message: document.getElementById('alertMessage').value,
|
|||
|
|
severity: document.getElementById('alertSeverity').value,
|
|||
|
|
requires_acknowledgement: document.getElementById('alertRequiresAck').checked,
|
|||
|
|
active: document.getElementById('alertActive').checked,
|
|||
|
|
restriction_group_ids: selectedGroups,
|
|||
|
|
restriction_user_ids: selectedUsers
|
|||
|
|
};
|
|||
|
|
} else {
|
|||
|
|
// POST: Include entity_type and entity_id for creation
|
|||
|
|
data = {
|
|||
|
|
entity_type: document.getElementById('alertEntityType').value,
|
|||
|
|
entity_id: parseInt(document.getElementById('alertEntityId').value),
|
|||
|
|
title: document.getElementById('alertTitle').value,
|
|||
|
|
message: document.getElementById('alertMessage').value,
|
|||
|
|
severity: document.getElementById('alertSeverity').value,
|
|||
|
|
requires_acknowledgement: document.getElementById('alertRequiresAck').checked,
|
|||
|
|
active: document.getElementById('alertActive').checked,
|
|||
|
|
restriction_group_ids: selectedGroups,
|
|||
|
|
restriction_user_ids: selectedUsers
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const saveBtn = document.getElementById('saveAlertNoteBtn');
|
|||
|
|
saveBtn.disabled = true;
|
|||
|
|
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Gemmer...';
|
|||
|
|
|
|||
|
|
// Debug logging
|
|||
|
|
console.log('Saving alert note:', { isEdit, alertId, data });
|
|||
|
|
|
|||
|
|
let response;
|
|||
|
|
if (isEdit) {
|
|||
|
|
// Update existing
|
|||
|
|
response = await fetch(`/api/v1/alert-notes/${alertId}`, {
|
|||
|
|
method: 'PATCH',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
credentials: 'include',
|
|||
|
|
body: JSON.stringify(data)
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// Create new
|
|||
|
|
response = await fetch('/api/v1/alert-notes', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
credentials: 'include',
|
|||
|
|
body: JSON.stringify(data)
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
let errorMsg = 'Failed to save alert note';
|
|||
|
|
try {
|
|||
|
|
const error = await response.json();
|
|||
|
|
console.error('API Error Response:', error);
|
|||
|
|
|
|||
|
|
// Handle Pydantic validation errors
|
|||
|
|
if (error.detail && Array.isArray(error.detail)) {
|
|||
|
|
errorMsg = error.detail.map(e => `${e.loc.join('.')}: ${e.msg}`).join('\n');
|
|||
|
|
} else if (error.detail) {
|
|||
|
|
errorMsg = error.detail;
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
errorMsg = `HTTP ${response.status}: ${response.statusText}`;
|
|||
|
|
}
|
|||
|
|
throw new Error(errorMsg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Success
|
|||
|
|
alertFormModal.hide();
|
|||
|
|
|
|||
|
|
// Reload alerts on page
|
|||
|
|
loadAndDisplayAlerts(
|
|||
|
|
currentAlertEntityType,
|
|||
|
|
currentAlertEntityId,
|
|||
|
|
'inline',
|
|||
|
|
'alert-notes-container'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Show success message
|
|||
|
|
showSuccessToast(isEdit ? 'Alert note opdateret!' : 'Alert note oprettet!');
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error saving alert note:', error);
|
|||
|
|
|
|||
|
|
// Show detailed error message
|
|||
|
|
const errorDiv = document.createElement('div');
|
|||
|
|
errorDiv.className = 'alert alert-danger alert-dismissible fade show mt-3';
|
|||
|
|
errorDiv.innerHTML = `
|
|||
|
|
<strong>Kunne ikke gemme alert note:</strong><br>
|
|||
|
|
<pre style="white-space: pre-wrap; margin-top: 10px; font-size: 0.9em;">${error.message}</pre>
|
|||
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// Insert error before form
|
|||
|
|
const modalBody = document.querySelector('#alertNoteFormModal .modal-body');
|
|||
|
|
modalBody.insertBefore(errorDiv, modalBody.firstChild);
|
|||
|
|
|
|||
|
|
// Auto-remove after 10 seconds
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (errorDiv.parentNode) {
|
|||
|
|
errorDiv.remove();
|
|||
|
|
}
|
|||
|
|
}, 10000);
|
|||
|
|
} finally {
|
|||
|
|
const saveBtn = document.getElementById('saveAlertNoteBtn');
|
|||
|
|
saveBtn.disabled = false;
|
|||
|
|
saveBtn.innerHTML = '<i class="bi bi-save me-2"></i>Gem Alert Note';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showSuccessToast(message) {
|
|||
|
|
// Simple toast notification
|
|||
|
|
const toast = document.createElement('div');
|
|||
|
|
toast.className = 'alert alert-success position-fixed bottom-0 end-0 m-3';
|
|||
|
|
toast.style.zIndex = '9999';
|
|||
|
|
toast.innerHTML = `<i class="bi bi-check-circle me-2"></i>${message}`;
|
|||
|
|
document.body.appendChild(toast);
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
toast.classList.add('fade');
|
|||
|
|
setTimeout(() => toast.remove(), 150);
|
|||
|
|
}, 3000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Make functions globally available
|
|||
|
|
window.openAlertNoteForm = openAlertNoteForm;
|
|||
|
|
window.saveAlertNote = saveAlertNote;
|
|||
|
|
</script>
|