bmc_hub/app/alert_notes/frontend/alert_form_modal.html

552 lines
22 KiB
HTML
Raw Normal View History

<!-- 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>