bmc_hub/app/modules/sag/templates/create.html
Christian 29acdf3e01 Add tests for new SAG module endpoints and module deactivation
- Implement test script for new SAG module endpoints BE-003 (Tag State Management) and BE-004 (Bulk Operations).
- Create test cases for creating, updating, and bulk operations on cases and tags.
- Add a test for module deactivation to ensure data integrity is maintained.
- Include setup and teardown for tests to clear database state before and after each test.
2026-01-31 23:16:24 +01:00

451 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "shared/frontend/base.html" %}
{% block title %}Ny Sag - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.form-container {
max-width: 600px;
margin: 2rem auto;
}
.form-container h1 {
margin-bottom: 2rem;
}
.card {
border: none;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
border: 1px solid rgba(0,0,0,0.1);
}
.card-body {
padding: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
font-weight: 600;
color: var(--accent);
margin-bottom: 0.5rem;
display: block;
}
.form-control, .form-select {
border-radius: 8px;
border: 1px solid rgba(0,0,0,0.1);
}
.btn-submit {
background-color: var(--accent);
color: white;
padding: 0.7rem 2rem;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-submit:hover {
background-color: #0056b3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(15, 76, 117, 0.3);
}
.btn-cancel {
background-color: #6c757d;
color: white;
padding: 0.7rem 2rem;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
text-decoration: none;
display: inline-block;
transition: all 0.2s;
}
.btn-cancel:hover {
background-color: #5a6268;
}
.button-group {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
.error {
display: none;
padding: 1rem;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 8px;
color: #721c24;
margin-bottom: 1rem;
}
[data-bs-theme="dark"] .error {
background-color: #5c2b2f;
border-color: #8c3b3f;
color: #f8a5ac;
}
.success {
display: none;
padding: 1rem;
background-color: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 8px;
color: #155724;
margin-bottom: 1rem;
}
.search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg-body);
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
max-height: 300px;
overflow-y: auto;
z-index: 1000;
margin-top: 0.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.search-result-item {
padding: 0.8rem 1rem;
cursor: pointer;
border-bottom: 1px solid rgba(0,0,0,0.05);
transition: background 0.2s;
}
.search-result-item:hover {
background: var(--accent-light);
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-name {
font-weight: 600;
color: var(--accent);
}
.search-result-meta {
font-size: 0.85rem;
color: var(--text-secondary);
}
.selected-item {
display: inline-block;
background: var(--accent-light);
color: var(--accent);
padding: 0.4rem 0.8rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
}
.selected-item button {
background: none;
border: none;
color: var(--accent);
cursor: pointer;
margin-left: 0.4rem;
font-weight: bold;
}
</style>
{% endblock %}
{% block content %}
<div class="form-container">
<div class="card">
<div class="card-body">
<h1 style="color: var(--accent); margin-bottom: 2rem;">📝 Opret Ny Sag</h1>
<div id="error" class="error"></div>
<div id="success" class="success"></div>
<form id="createForm">
<div class="form-group">
<label for="titel">Titel *</label>
<input type="text" class="form-control" id="titel" placeholder="Indtast sagens titel" required>
</div>
<div class="form-group">
<label for="beskrivelse">Beskrivelse</label>
<textarea class="form-control" id="beskrivelse" rows="4" placeholder="Optionalt: Detaljeret beskrivelse af sagen"></textarea>
</div>
<div class="form-group">
<label for="status">Status *</label>
<select class="form-select" id="status" required>
<option value="">Vælg status</option>
<option value="åben">Åben</option>
<option value="lukket">Lukket</option>
</select>
</div>
<div class="form-group">
<label>Kunde (valgfrit)</label>
<div style="position: relative; margin-bottom: 1rem;">
<input type="text" id="customerSearch" class="form-control" placeholder="Søg efter kunde...">
<div id="customerResults" class="search-results" style="display: none;"></div>
</div>
<div id="selectedCustomer" style="min-height: 1.5rem;"></div>
<input type="hidden" id="customer_id" name="customer_id">
</div>
<div class="form-group">
<label>Kontakter (valgfrit)</label>
<div style="position: relative; margin-bottom: 1rem;">
<input type="text" id="contactSearch" class="form-control" placeholder="Søg efter kontakt...">
<div id="contactResults" class="search-results" style="display: none;"></div>
</div>
<div id="selectedContacts" style="min-height: 1.5rem;"></div>
</div>
<div class="form-group">
<label for="ansvarlig_bruger_id">Ansvarlig Bruger (valgfrit)</label>
<input type="number" class="form-control" id="ansvarlig_bruger_id" placeholder="Brugers ID">
</div>
<div class="form-group">
<label for="deadline">Deadline (valgfrit)</label>
<input type="datetime-local" class="form-control" id="deadline">
</div>
<div class="button-group">
<button type="submit" class="btn-submit">Opret Sag</button>
<a href="/cases" class="btn-cancel">Annuller</a>
</div>
</form>
</div>
</div>
</div>
<script>
let selectedCustomer = null;
let selectedContacts = {};
let customerSearchTimeout;
let contactSearchTimeout;
// Initialize search functionality
function initializeSearch() {
const customerSearchInput = document.getElementById('customerSearch');
const contactSearchInput = document.getElementById('contactSearch');
if (customerSearchInput) {
customerSearchInput.addEventListener('input', function(e) {
clearTimeout(customerSearchTimeout);
const query = e.target.value.trim();
if (query.length < 2) {
document.getElementById('customerResults').style.display = 'none';
return;
}
customerSearchTimeout = setTimeout(async () => {
try {
const response = await fetch(`/api/v1/search/customers?q=${encodeURIComponent(query)}`);
const customers = await response.json();
const resultsDiv = document.getElementById('customerResults');
if (customers.length === 0) {
resultsDiv.innerHTML = '<div style="padding: 10px; color: #999;">Ingen kunder fundet</div>';
} else {
resultsDiv.innerHTML = customers.map(c => `
<div class="search-result-item" onclick="selectCustomer(${c.id}, '${c.name.replace(/'/g, "\\'")}')">
<div class="search-result-name">${c.name}</div>
<div class="search-result-meta">${c.email || 'Ingen email'}</div>
</div>
`).join('');
}
resultsDiv.style.display = 'block';
} catch (err) {
console.error('Error searching customers:', err);
}
}, 300);
});
}
if (contactSearchInput) {
contactSearchInput.addEventListener('input', function(e) {
clearTimeout(contactSearchTimeout);
const query = e.target.value.trim();
if (query.length < 2) {
document.getElementById('contactResults').style.display = 'none';
return;
}
contactSearchTimeout = setTimeout(async () => {
try {
const response = await fetch(`/api/v1/search/contacts?q=${encodeURIComponent(query)}`);
const contacts = await response.json();
const resultsDiv = document.getElementById('contactResults');
if (contacts.length === 0) {
resultsDiv.innerHTML = '<div style="padding: 10px; color: #999;">Ingen kontakter fundet</div>';
} else {
resultsDiv.innerHTML = contacts.map(c => `
<div class="search-result-item" onclick="selectContact(${c.id}, '${(c.first_name + ' ' + c.last_name).replace(/'/g, "\\'")}')">
<div class="search-result-name">${c.first_name} ${c.last_name}</div>
<div class="search-result-meta">${c.email || 'Ingen email'}</div>
</div>
`).join('');
}
resultsDiv.style.display = 'block';
} catch (err) {
console.error('Error searching contacts:', err);
}
}, 300);
});
}
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeSearch);
} else {
initializeSearch();
}
function selectCustomer(customerId, customerName) {
selectedCustomer = { id: customerId, name: customerName };
document.getElementById('customer_id').value = customerId;
document.getElementById('customerSearch').value = '';
document.getElementById('customerResults').style.display = 'none';
updateSelectedCustomer();
}
function updateSelectedCustomer() {
const div = document.getElementById('selectedCustomer');
if (selectedCustomer) {
div.innerHTML = `
<span class="selected-item">
🏢 ${selectedCustomer.name}
<button type="button" onclick="removeCustomer()">×</button>
</span>
`;
} else {
div.innerHTML = '';
}
}
function removeCustomer() {
selectedCustomer = null;
document.getElementById('customer_id').value = '';
updateSelectedCustomer();
}
function selectContact(contactId, contactName) {
if (!selectedContacts[contactId]) {
selectedContacts[contactId] = { id: contactId, name: contactName };
}
document.getElementById('contactSearch').value = '';
document.getElementById('contactResults').style.display = 'none';
updateSelectedContacts();
}
function updateSelectedContacts() {
const div = document.getElementById('selectedContacts');
const items = Object.values(selectedContacts).map(c => `
<span class="selected-item">
👥 ${c.name}
<button type="button" onclick="removeContact(${c.id})">×</button>
</span>
`).join('');
div.innerHTML = items;
}
function removeContact(contactId) {
delete selectedContacts[contactId];
updateSelectedContacts();
}
document.getElementById('createForm').addEventListener('submit', async (e) => {
e.preventDefault();
const titel = document.getElementById('titel').value;
const status = document.getElementById('status').value;
if (!titel || !status) {
document.getElementById('error').textContent = `❌ Udfyld alle påkrævede felter`;
document.getElementById('error').style.display = 'block';
return;
}
const data = {
titel: titel,
beskrivelse: document.getElementById('beskrivelse').value || '',
status: status,
customer_id: document.getElementById('customer_id').value ? parseInt(document.getElementById('customer_id').value) : null,
ansvarlig_bruger_id: document.getElementById('ansvarlig_bruger_id').value ? parseInt(document.getElementById('ansvarlig_bruger_id').value) : null,
created_by_user_id: 1,
deadline: document.getElementById('deadline').value || null
};
console.log('Sending data:', data);
try {
const response = await fetch('/api/v1/cases', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
console.log('Response status:', response.status);
if (response.ok) {
const result = await response.json();
console.log('Created case:', result);
// Add selected contacts to the case
for (const contactId of Object.keys(selectedContacts)) {
await fetch(`/api/v1/cases/${result.id}/contacts`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({contact_id: parseInt(contactId), role: 'Kontakt'})
});
}
document.getElementById('success').textContent = `✅ Sag oprettet! Omdirigerer...`;
document.getElementById('success').style.display = 'block';
setTimeout(() => {
window.location.href = `/cases/${result.id}`;
}, 1000);
} else {
const errorText = await response.text();
console.error('Error response:', errorText);
try {
const error = JSON.parse(errorText);
document.getElementById('error').textContent = `❌ Fejl: ${error.detail || errorText}`;
} catch {
document.getElementById('error').textContent = `❌ Fejl: ${errorText}`;
}
document.getElementById('error').style.display = 'block';
}
} catch (err) {
console.error('Exception:', err);
document.getElementById('error').textContent = `❌ Fejl: ${err.message}`;
document.getElementById('error').style.display = 'block';
}
});
</script>
{% endblock %}