388 lines
12 KiB
HTML
388 lines
12 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Sager - BMC Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.page-header {
|
|
margin-bottom: 2rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.page-header h1 {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
margin: 0;
|
|
}
|
|
|
|
.btn-new-case {
|
|
background-color: var(--accent);
|
|
color: white;
|
|
border: none;
|
|
padding: 0.6rem 1.5rem;
|
|
border-radius: 8px;
|
|
font-weight: 500;
|
|
transition: all 0.3s ease;
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.btn-new-case:hover {
|
|
background-color: #0056b3;
|
|
color: white;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(15, 76, 117, 0.3);
|
|
}
|
|
|
|
.filter-section {
|
|
background: var(--bg-card);
|
|
padding: 1.5rem;
|
|
border-radius: 12px;
|
|
margin-bottom: 2rem;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.filter-section label {
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
margin-bottom: 0.5rem;
|
|
display: block;
|
|
}
|
|
|
|
.filter-section input,
|
|
.filter-section select {
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.sag-card {
|
|
display: block;
|
|
background: var(--bg-card);
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
border-radius: 12px;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1rem;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
transition: all 0.2s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.sag-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.sag-title {
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.sag-meta {
|
|
display: flex;
|
|
gap: 1rem;
|
|
font-size: 0.85rem;
|
|
flex-wrap: wrap;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 0.3rem 0.8rem;
|
|
border-radius: 20px;
|
|
font-weight: 500;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.status-åben {
|
|
background-color: #d1ecf1;
|
|
color: #0c5460;
|
|
}
|
|
|
|
.status-lukket {
|
|
background-color: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
[data-bs-theme="dark"] .status-åben {
|
|
background-color: #1a4d5c;
|
|
color: #66d9e8;
|
|
}
|
|
|
|
[data-bs-theme="dark"] .status-lukket {
|
|
background-color: #5c2b2f;
|
|
color: #f8a5ac;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 3rem 1rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.empty-state p {
|
|
font-size: 1.1rem;
|
|
margin: 0;
|
|
}
|
|
|
|
#bulkActions {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 1rem;
|
|
background: var(--accent-light);
|
|
margin-bottom: 1rem;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--accent);
|
|
}
|
|
|
|
.case-checkbox {
|
|
position: absolute;
|
|
top: 1rem;
|
|
left: 1rem;
|
|
width: 20px;
|
|
height: 20px;
|
|
cursor: pointer;
|
|
z-index: 5;
|
|
}
|
|
|
|
.sag-card-with-checkbox {
|
|
padding-left: 3rem;
|
|
}
|
|
|
|
.bulk-action-btn {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 6px;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.bulk-action-btn:hover {
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.btn-bulk-close {
|
|
background: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
.btn-bulk-close:hover {
|
|
background: #218838;
|
|
}
|
|
|
|
.btn-bulk-tag {
|
|
background: var(--accent);
|
|
color: white;
|
|
}
|
|
|
|
.btn-bulk-tag:hover {
|
|
background: #0056b3;
|
|
}
|
|
|
|
.btn-bulk-clear {
|
|
background: #6c757d;
|
|
color: white;
|
|
}
|
|
|
|
.btn-bulk-clear:hover {
|
|
background: #5a6268;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container" style="margin-top: 2rem; margin-bottom: 2rem;">
|
|
<!-- Page Header -->
|
|
<div class="page-header">
|
|
<h1>📋 Sager</h1>
|
|
<a href="/cases/new" class="btn-new-case">
|
|
<i class="bi bi-plus-lg"></i> Ny Sag
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Bulk Actions Bar (hidden by default) -->
|
|
<div id="bulkActions" style="display: none;">
|
|
<span id="selectedCount" style="font-weight: 600; color: var(--accent);">0 sager valgt</span>
|
|
<div style="display: inline-flex; gap: 0.5rem;">
|
|
<button onclick="bulkClose()" class="bulk-action-btn btn-bulk-close">Luk alle</button>
|
|
<button onclick="bulkAddTag()" class="bulk-action-btn btn-bulk-tag">Tilføj tag</button>
|
|
<button onclick="clearSelection()" class="bulk-action-btn btn-bulk-clear">Ryd</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Section -->
|
|
<div class="filter-section">
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<label>Søg</label>
|
|
<input type="text" id="searchInput" class="form-control" placeholder="Søg efter sag...">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label>Status</label>
|
|
<select id="statusFilter" class="form-select">
|
|
<option value="">Alle statuser</option>
|
|
{% for status in statuses %}
|
|
<option value="{{ status }}">{{ status }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label>Tags</label>
|
|
<select id="tagFilter" class="form-select">
|
|
<option value="">Alle tags</option>
|
|
{% for tag in all_tags %}
|
|
<option value="{{ tag }}">{{ tag }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cases List -->
|
|
<div>
|
|
{% if sager %}
|
|
{% for sag in sager %}
|
|
<div class="sag-card sag-card-with-checkbox" data-status="{{ sag.status }}" style="cursor: default; position: relative;">
|
|
<input type="checkbox" class="case-checkbox" data-case-id="{{ sag.id }}">
|
|
<div style="cursor: pointer;" onclick="window.location.href='/cases/{{ sag.id }}'">
|
|
<div class="sag-title">{{ sag.titel }}</div>
|
|
{% if sag.beskrivelse %}
|
|
<div style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 0.5rem;">{{ sag.beskrivelse[:150] }}{% if sag.beskrivelse|length > 150 %}...{% endif %}</div>
|
|
{% endif %}
|
|
<div class="sag-meta">
|
|
<span class="status-badge status-{{ sag.status }}">{{ sag.status }}</span>
|
|
<span style="color: var(--text-secondary);">{{ sag.created_at.strftime('%Y-%m-%d') if sag.created_at else '' }}</span>
|
|
</div>
|
|
</div>
|
|
<div style="position: absolute; top: 1rem; right: 1rem; display: flex; gap: 0.5rem;">
|
|
<a href="/cases/{{ sag.id }}/edit" class="btn" style="background-color: var(--accent-color); color: white; padding: 0.4rem 0.8rem; border-radius: 6px; text-decoration: none; font-size: 0.85rem; display: flex; align-items: center; gap: 0.3rem;" title="Rediger">✏️</a>
|
|
<a href="/cases/{{ sag.id }}" class="btn" style="background-color: #6c757d; color: white; padding: 0.4rem 0.8rem; border-radius: 6px; text-decoration: none; font-size: 0.85rem; display: flex; align-items: center; gap: 0.3rem;" title="Detaljer">→</a>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p><i class="bi bi-inbox" style="font-size: 2rem; display: block; margin-bottom: 1rem;"></i>Ingen sager fundet</p>
|
|
<a href="/cases/new" class="btn-new-case">Opret første sag</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Bulk selection state
|
|
let selectedCases = new Set();
|
|
|
|
// Bulk selection handler
|
|
document.addEventListener('change', function(e) {
|
|
if (e.target.classList.contains('case-checkbox')) {
|
|
const caseId = parseInt(e.target.dataset.caseId);
|
|
if (e.target.checked) {
|
|
selectedCases.add(caseId);
|
|
} else {
|
|
selectedCases.delete(caseId);
|
|
}
|
|
updateBulkBar();
|
|
}
|
|
});
|
|
|
|
function updateBulkBar() {
|
|
const count = selectedCases.size;
|
|
const bar = document.getElementById('bulkActions');
|
|
if (count > 0) {
|
|
bar.style.display = 'flex';
|
|
document.getElementById('selectedCount').textContent = `${count} sager valgt`;
|
|
} else {
|
|
bar.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
async function bulkClose() {
|
|
const caseIds = Array.from(selectedCases);
|
|
if (!confirm(`Luk ${caseIds.length} sager?`)) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/v1/cases/bulk', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
case_ids: caseIds,
|
|
action: 'close_all'
|
|
})
|
|
});
|
|
if (response.ok) {
|
|
window.location.reload();
|
|
} else {
|
|
const error = await response.json();
|
|
alert(`Fejl: ${error.detail}`);
|
|
}
|
|
} catch (err) {
|
|
alert('Fejl ved bulk lukning: ' + err.message);
|
|
}
|
|
}
|
|
|
|
async function bulkAddTag() {
|
|
const tagName = prompt('Indtast tag navn:');
|
|
if (!tagName) return;
|
|
|
|
const caseIds = Array.from(selectedCases);
|
|
try {
|
|
const response = await fetch('/api/v1/cases/bulk', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
case_ids: caseIds,
|
|
action: 'add_tag',
|
|
params: {tag_navn: tagName}
|
|
})
|
|
});
|
|
if (response.ok) {
|
|
window.location.reload();
|
|
} else {
|
|
const error = await response.json();
|
|
alert(`Fejl: ${error.detail}`);
|
|
}
|
|
} catch (err) {
|
|
alert('Fejl ved bulk tag tilføjelse: ' + err.message);
|
|
}
|
|
}
|
|
|
|
function clearSelection() {
|
|
selectedCases.clear();
|
|
document.querySelectorAll('.case-checkbox').forEach(cb => cb.checked = false);
|
|
updateBulkBar();
|
|
}
|
|
|
|
// Search functionality
|
|
document.getElementById('searchInput').addEventListener('keyup', function(e) {
|
|
const search = e.target.value.toLowerCase();
|
|
document.querySelectorAll('.sag-card').forEach(card => {
|
|
const text = card.textContent.toLowerCase();
|
|
const display = text.includes(search) ? 'block' : 'none';
|
|
card.style.display = display;
|
|
});
|
|
});
|
|
|
|
// Status filter
|
|
document.getElementById('statusFilter').addEventListener('change', function(e) {
|
|
const status = e.target.value;
|
|
document.querySelectorAll('.sag-card').forEach(card => {
|
|
const cardStatus = card.dataset.status;
|
|
const display = status === '' || cardStatus === status ? 'block' : 'none';
|
|
card.style.display = display;
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|