2026-02-01 11:58:44 +01:00
{% extends "shared/frontend/base.html" %}
{% block title %}{{ case.titel }} - BMC Hub{% endblock %}
{% block extra_css %}
< style >
text-decoration: underline;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 0.8rem 0;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-weight: 600;
color: var(--text-secondary);
min-width: 150px;
}
.info-value {
color: var(--text-primary);
}
.tag {
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;
}
.person-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: var(--bg-body);
border-radius: 8px;
margin-bottom: 0.5rem;
}
.person-info strong {
display: block;
color: var(--accent);
}
.person-info small {
color: var(--text-secondary);
display: block;
}
.action-buttons {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin-top: 2rem;
}
.btn-delete {
background-color: #e74c3c;
color: white;
}
.btn-delete:hover {
background-color: #c0392b;
}
.form-control, .form-select {
border-radius: 8px;
border: 1px solid rgba(0,0,0,0.1);
}
.tag-closed {
background-color: #e0e0e0;
color: #666;
text-decoration: line-through;
}
[data-bs-theme="dark"] .tag-closed {
background-color: #3a3a3a;
color: #999;
}
.tag-state-badge {
font-size: 0.75rem;
padding: 0.2rem 0.4rem;
border-radius: 4px;
margin-left: 0.5rem;
font-weight: 600;
}
.tag-state-open {
background: #d4edda;
color: #155724;
}
.tag-state-closed {
background: #f8d7da;
color: #721c24;
}
[data-bs-theme="dark"] .tag-state-open {
background: #1e4620;
color: #7fd98d;
}
[data-bs-theme="dark"] .tag-state-closed {
background: #5c2b2f;
color: #f8a5ac;
}
2026-02-06 10:47:14 +01:00
.case-summary-card {
border: 1px solid rgba(0,0,0,0.06);
background: var(--bg-card);
box-shadow: 0 6px 18px rgba(15, 76, 117, 0.08);
}
.case-summary-header {
background: linear-gradient(135deg, rgba(15, 76, 117, 0.12), rgba(15, 76, 117, 0.02));
border-bottom: 1px solid rgba(0,0,0,0.06);
padding: 0.85rem 1rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.case-summary-title {
font-size: 1.15rem;
font-weight: 700;
color: var(--accent);
margin-bottom: 0.25rem;
}
.case-summary-meta {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.case-pill {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.2rem 0.6rem;
border-radius: 999px;
font-size: 0.7rem;
font-weight: 600;
background: rgba(15, 76, 117, 0.12);
color: var(--accent);
}
.case-pill-muted {
background: rgba(0,0,0,0.06);
color: var(--text-secondary);
}
.case-summary-body {
padding: 1rem;
}
.case-summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.75rem 1rem;
}
@media (max-width: 992px) {
.case-summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 576px) {
.case-summary-grid {
grid-template-columns: 1fr;
}
}
.summary-item {
padding: 0.4rem 0.5rem;
border-radius: 10px;
background: rgba(0,0,0,0.02);
border: 1px solid rgba(0,0,0,0.04);
min-height: 44px;
}
.summary-label {
font-size: 0.62rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-secondary);
margin-bottom: 0.15rem;
font-weight: 600;
}
.summary-value {
font-weight: 600;
font-size: 0.85rem;
color: var(--text-primary);
word-break: break-word;
}
.summary-link {
color: var(--accent);
cursor: pointer;
text-decoration: underline;
text-underline-offset: 3px;
}
.case-summary-desc {
margin-top: 0.9rem;
padding: 0.8rem 0.9rem;
border-radius: 12px;
background: rgba(15, 76, 117, 0.05);
border: 1px dashed rgba(15, 76, 117, 0.2);
}
.defer-controls {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
margin-top: 0.35rem;
}
.defer-controls .btn {
padding: 0.15rem 0.45rem;
font-size: 0.72rem;
}
.summary-inline {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
flex-wrap: wrap;
}
.summary-inline .summary-pill {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.2rem 0.5rem;
border-radius: 999px;
font-size: 0.72rem;
background: rgba(0,0,0,0.05);
color: var(--text-secondary);
}
.relation-tree {
list-style: none;
margin: 0;
padding-left: 0;
}
.relation-node {
position: relative;
padding-left: 1.2rem;
}
.relation-node:before {
content: "";
position: absolute;
left: 0.45rem;
top: 0.2rem;
bottom: 0.2rem;
width: 1px;
background: rgba(15, 76, 117, 0.25);
}
.relation-node:after {
content: "";
position: absolute;
left: 0.45rem;
top: 1.05rem;
width: 0.6rem;
height: 1px;
background: rgba(15, 76, 117, 0.35);
}
.relation-node:last-child:before {
bottom: 1.05rem;
}
.relation-children {
margin-left: 0.55rem;
padding-left: 0.65rem;
border-left: 1px dashed rgba(15, 76, 117, 0.25);
}
.relation-node-card {
background: rgba(15, 76, 117, 0.03);
border: 1px solid rgba(15, 76, 117, 0.12);
}
.relation-type-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.65rem;
padding: 0.15rem 0.45rem;
border-radius: 999px;
background: rgba(15, 76, 117, 0.12);
color: var(--accent);
font-weight: 600;
margin-right: 0.35rem;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.right-modules-grid {
display: grid;
grid-template-columns: 1fr;
gap: 0.75rem;
}
.right-modules-grid .card-header {
padding: 0.35rem 0.6rem;
}
.right-modules-grid .card-header h6 {
font-size: 0.8rem;
}
.right-modules-grid .card-body {
padding: 0.4rem;
}
.contact-list-header,
.contact-row {
display: grid;
grid-template-columns: 1.2fr 0.9fr 1fr auto;
gap: 0.5rem;
align-items: center;
}
.contact-list-header {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.03em;
color: #6c757d;
padding: 0 0.25rem 0.25rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.contact-row {
padding: 0.35rem 0.25rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
cursor: pointer;
}
.contact-row:last-child {
border-bottom: none;
}
.contact-row .contact-name {
font-weight: 600;
}
.contact-row small {
color: #6c757d;
}
.hardware-list-header,
.hardware-row,
.location-list-header,
.location-row,
.customer-list-header,
.customer-row {
display: grid;
align-items: center;
gap: 0.5rem;
}
.hardware-list-header,
.location-list-header,
.customer-list-header {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.03em;
color: #6c757d;
padding: 0 0.25rem 0.25rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.hardware-row,
.location-row,
.customer-row {
padding: 0.35rem 0.25rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.hardware-row:last-child,
.location-row:last-child,
.customer-row:last-child {
border-bottom: none;
}
.hardware-list-header,
.hardware-row {
grid-template-columns: 1.3fr 1fr auto;
}
.location-list-header,
.location-row {
grid-template-columns: 1.3fr 1fr auto;
}
.customer-list-header,
.customer-row {
grid-template-columns: 1.2fr 0.9fr 1fr auto;
}
2026-02-01 11:58:44 +01:00
.tag-toggle-btn {
background: none;
border: 1px solid rgba(0,0,0,0.2);
padding: 0.2rem 0.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.75rem;
margin-left: 0.5rem;
transition: all 0.2s;
}
.tag-toggle-btn:hover {
background: rgba(0,0,0,0.05);
}
.tag-toggle-open {
color: #28a745;
border-color: #28a745;
}
.tag-toggle-open:hover {
background: #28a745;
color: white;
}
.tag-toggle-closed {
color: #6c757d;
border-color: #6c757d;
}
.tag-toggle-closed:hover {
background: #6c757d;
color: white;
}
< / style >
{% endblock %}
{% block content %}
< div class = "container-fluid" style = "margin-top: 2rem; margin-bottom: 2rem; max-width: 1400px;" >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
<!-- Top Bar: Back Link + Global Tags -->
< div class = "d-flex justify-content-between align-items-start mb-2" >
< a href = "/sag" class = "back-link" >
< i class = "bi bi-chevron-left" > < / i > Tilbage til sager
< / a >
<!-- Global Tags Area -->
< div class = "d-flex align-items-center p-2 rounded" style = "background: rgba(0,0,0,0.02);" >
< i class = "bi bi-tags text-muted me-2 small" > < / i >
< div id = "case-tags" class = "d-flex flex-wrap justify-content-end gap-1 align-items-center" >
< span class = "spinner-border spinner-border-sm text-muted" > < / span >
< / div >
< button class = "btn btn-sm btn-link text-decoration-none ms-1 px-1 py-0 text-muted hover-primary"
onclick="window.showTagPicker('case', {{ case.id }}, () => window.renderEntityTags('case', {{ case.id }}, 'case-tags'))"
title="Tilføj tag">
< i class = "bi bi-plus-lg" > < / i >
< / button >
2026-02-06 10:47:14 +01:00
< button class = "btn btn-sm btn-link text-decoration-none ms-1 px-1 py-0 text-muted hover-primary"
onclick="openModuleControlModal()"
title="Vis/skjul moduler">
< i class = "bi bi-sliders" > < / i >
< / button >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / div >
< / div >
2026-02-01 11:58:44 +01:00
<!-- Quick Info Bar -->
< div class = "card mb-3" style = "background: var(--bg-card); border-left: 4px solid var(--accent); box-shadow: 0 1px 3px rgba(0,0,0,0.08);" >
< div class = "card-body py-2 px-3" >
< div class = "d-flex flex-wrap align-items-center gap-3" style = "font-size: 0.85rem;" >
< div class = "d-flex align-items-center" >
< strong style = "color: var(--accent); margin-right: 0.4rem;" > ID:< / strong >
< span > {{ case.id }}< / span >
< / div >
< div class = "d-flex align-items-center" style = "border-left: 1px solid rgba(0,0,0,0.1); padding-left: 0.75rem;" >
< strong style = "color: var(--accent); margin-right: 0.4rem;" > Kunde:< / strong >
< span > {{ customer.name if customer else 'Ingen kunde' }}< / span >
< / div >
< div class = "d-flex align-items-center" style = "border-left: 1px solid rgba(0,0,0,0.1); padding-left: 0.75rem;" >
< strong style = "color: var(--accent); margin-right: 0.4rem;" > Hovedkontakt:< / strong >
{% if hovedkontakt %}
< span style = "cursor: pointer; text-decoration: underline; color: var(--accent);"
onclick="showKontaktModal()"
title="Klik for at se kontaktinfo">
{{ hovedkontakt.first_name ~ ' ' ~ hovedkontakt.last_name }}
< / span >
{% else %}
< span > Ingen kontakt< / span >
{% endif %}
< / div >
< div class = "d-flex align-items-center" style = "border-left: 1px solid rgba(0,0,0,0.1); padding-left: 0.75rem;" >
< strong style = "color: var(--accent); margin-right: 0.4rem;" > Afdeling:< / strong >
< span style = "cursor: pointer; text-decoration: underline; color: var(--accent);"
onclick="showAfdelingModal()"
title="Klik for at ændre afdeling">
{{ customer.department if customer and customer.department else 'N/A' }}
< / span >
< / div >
< div class = "d-flex align-items-center" style = "border-left: 1px solid rgba(0,0,0,0.1); padding-left: 0.75rem;" >
< strong style = "color: var(--accent); margin-right: 0.4rem;" > Status:< / strong >
< span class = "badge" style = "background: var(--accent); font-size: 0.75rem;" > {{ case.status }}< / span >
< / div >
< div class = "d-flex align-items-center" style = "border-left: 1px solid rgba(0,0,0,0.1); padding-left: 0.75rem;" >
< strong style = "color: var(--accent); margin-right: 0.4rem;" > Opdateret:< / strong >
< span > {{ case.updated_at.strftime('%d/%m-%Y') if case.updated_at else 'N/A' }}< / span >
< / div >
< div class = "d-flex align-items-center" style = "border-left: 1px solid rgba(0,0,0,0.1); padding-left: 0.75rem;" >
< strong style = "color: var(--accent); margin-right: 0.4rem;" > Deadline:< / strong >
< span > {{ case.deadline.strftime('%d/%m-%Y') if case.deadline else 'Ikke sat' }}< / span >
< / div >
< / div >
< / div >
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
<!-- Tabs Navigation -->
< ul class = "nav nav-tabs mb-4" id = "caseTabs" role = "tablist" >
< li class = "nav-item" role = "presentation" >
< button class = "nav-link active" id = "details-tab" data-bs-toggle = "tab" data-bs-target = "#details" type = "button" role = "tab" >
< i class = "bi bi-card-text me-2" > < / i > Sagsdetaljer
< / button >
< / li >
< li class = "nav-item" role = "presentation" >
2026-02-06 10:47:14 +01:00
< button class = "nav-link" id = "solution-tab" data-bs-toggle = "tab" data-bs-target = "#solution" type = "button" role = "tab" data-module-tab = "solution" >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< i class = "bi bi-lightbulb me-2" > < / i > Løsning
{% if solution %}
< span class = "badge bg-success ms-1 rounded-pill" > < i class = "bi bi-check" > < / i > < / span >
{% endif %}
< / button >
< / li >
< li class = "nav-item" role = "presentation" >
2026-02-06 10:47:14 +01:00
< button class = "nav-link" id = "sales-tab" data-bs-toggle = "tab" data-bs-target = "#sales" type = "button" role = "tab" data-module-tab = "sales" >
< i class = "bi bi-basket3 me-2" > < / i > Varekøb & Salg
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / button >
< / li >
< li class = "nav-item" role = "presentation" >
2026-02-06 10:47:14 +01:00
< button class = "nav-link" id = "reminders-tab" data-bs-toggle = "tab" data-bs-target = "#reminders" type = "button" role = "tab" data-module-tab = "reminders" >
< i class = "bi bi-bell me-2" > < / i > Reminders
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / button >
< / li >
< / ul >
< div class = "tab-content" id = "caseTabsContent" >
<!-- Tab: Sagsdetaljer (Existing Content) -->
< div class = "tab-pane fade show active" id = "details" role = "tabpanel" tabindex = "0" >
2026-02-06 10:47:14 +01:00
< div class = "row g-4" >
< div class = "col-lg-8" id = "case-left-column" >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
2026-02-06 10:47:14 +01:00
<!-- ROW 1: Main Info -->
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< div class = "row mb-3" >
<!-- Main Case Info -->
2026-02-06 10:47:14 +01:00
< div class = "col-12 mb-3" >
< div class = "card h-100 d-flex flex-column case-summary-card" >
< div class = "card-header case-summary-header" >
2026-02-01 11:58:44 +01:00
< div >
2026-02-06 10:47:14 +01:00
< div class = "case-summary-title" > {{ case.titel }}< / div >
< div class = "case-summary-meta" >
< span class = "case-pill" > #{{ case.id }}< / span >
< span class = "case-pill" > {{ case.status }}< / span >
< span class = "case-pill case-pill-muted" > {{ case.type or 'ticket' }}< / span >
< / div >
< / div >
< div class = "d-flex align-items-center gap-2" >
< a href = "/sag/{{ case.id }}/edit" class = "btn btn-sm btn-outline-primary" >
2026-02-01 11:58:44 +01:00
< i class = "bi bi-pencil" > < / i >
< / a >
< button class = "btn btn-sm btn-outline-danger" onclick = "confirmDeleteCase()" >
< i class = "bi bi-trash" > < / i >
< / button >
< / div >
< / div >
2026-02-06 10:47:14 +01:00
< div class = "card-body case-summary-body" >
< div class = "case-summary-grid" >
< div class = "summary-item" >
< div class = "summary-label" > Kunde< / div >
< div class = "summary-value" > {{ customer.name if customer else 'Ingen kunde' }}< / div >
2026-02-01 11:58:44 +01:00
< / div >
2026-02-06 10:47:14 +01:00
< div class = "summary-item" >
< div class = "summary-label" > Hovedkontakt< / div >
< div class = "summary-value" >
{% if hovedkontakt %}
< span class = "summary-link" onclick = "showKontaktModal()" title = "Klik for at se kontaktinfo" >
{{ hovedkontakt.first_name ~ ' ' ~ hovedkontakt.last_name }}
< / span >
{% else %}
-
{% endif %}
< / div >
< / div >
< div class = "summary-item" >
< div class = "summary-label" > Afdeling< / div >
< div class = "summary-value" >
< span class = "summary-link" onclick = "showAfdelingModal()" title = "Klik for at ændre afdeling" >
{{ customer.department if customer and customer.department else 'N/A' }}
< / span >
< / div >
< / div >
< div class = "summary-item" >
< div class = "summary-label" > Oprettet< / div >
< div class = "summary-value" > {{ case.created_at|string|truncate(19, True, '') if case.created_at else 'Ikke sat' }}< / div >
< / div >
< div class = "summary-item" >
< div class = "summary-label" > Opdateret< / div >
< div class = "summary-value" > {{ case.updated_at.strftime('%d/%m-%Y') if case.updated_at else 'N/A' }}< / div >
< / div >
< div class = "summary-item" >
< div class = "summary-label" > Deadline< / div >
< div class = "summary-value" > {{ case.deadline.strftime('%d/%m-%Y') if case.deadline else 'Ikke sat' }}< / div >
< / div >
< div class = "summary-item" >
< div class = "summary-label" > Udsat start< / div >
< div class = "summary-value" >
< div class = "summary-inline" >
< span class = "summary-pill" >
{% if case.deferred_until %}
{{ case.deferred_until.strftime('%d/%m-%Y') }}
{% else %}
Ikke sat
{% endif %}
< / span >
< span class = "summary-pill" >
{% if case.deferred_until_case_id %}
Sag #{{ case.deferred_until_case_id }}
{% else %}
Ingen sag
{% endif %}
< / span >
< span class = "summary-pill" >
{{ case.deferred_until_status or 'Ingen status' }}
< / span >
< button class = "btn btn-sm btn-outline-primary" onclick = "openDeferredModal()" >
Rediger
< / button >
< / div >
< / div >
2026-02-01 11:58:44 +01:00
< / div >
< / div >
2026-02-06 10:47:14 +01:00
< div class = "case-summary-desc" >
< div class = "summary-label" > Beskrivelse< / div >
< div class = "summary-value" > {{ case.beskrivelse or 'Ingen beskrivelse' }}< / div >
< / div >
2026-02-01 11:58:44 +01:00
< / div >
< / div >
< / div >
2026-02-06 10:47:14 +01:00
2026-02-01 11:58:44 +01:00
< / div >
2026-02-06 10:47:14 +01:00
<!-- ROW 2: Relations -->
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< div class = "row mb-3" >
2026-02-06 10:47:14 +01:00
< div class = "col-12 mb-3" >
< div class = "card h-100 d-flex flex-column" data-module = "relations" data-has-content = "{{ 'true' if relation_tree else 'false' }}" >
2026-02-01 11:58:44 +01:00
< div class = "card-header d-flex justify-content-between align-items-center" >
2026-02-06 10:47:14 +01:00
< h6 class = "mb-0" style = "color: var(--accent);" > 🔗 Relationer< / h6 >
< div class = "d-flex gap-2" >
< button class = "btn btn-sm btn-outline-primary" onclick = "showRelationModal()" >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< i class = "bi bi-link-45deg" > < / i >
< / button >
2026-02-06 10:47:14 +01:00
< button class = "btn btn-sm btn-outline-primary" onclick = "showCreateRelatedModal()" >
< i class = "bi bi-plus-lg" > < / i >
< / button >
2026-02-01 11:58:44 +01:00
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / div >
< div class = "card-body flex-grow-1 overflow-auto" style = "max-height: 300px;" >
{% macro render_tree(nodes) %}
2026-02-06 10:47:14 +01:00
< ul class = "relation-tree" >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
{% for node in nodes %}
2026-02-06 10:47:14 +01:00
< li class = "relation-node" >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< div class = "d-flex align-items-center py-1" >
2026-02-06 10:47:14 +01:00
< div class = "d-flex align-items-center flex-grow-1 relation-node-card {{ 'fw-bold bg-light-subtle border border-primary-subtle' if node.is_current else '' }} rounded px-2 py-1" style = "min-height: 32px;" >
{% if node.relation_type %}
< span class = "relation-type-badge" > {{ node.relation_type }}< / span >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
{% endif %}
2026-02-06 10:47:14 +01:00
< a href = "/sag/{{ node.case.id }}" class = "text-decoration-none {{ 'text-dark' if node.is_current else 'text-muted' }}" >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< span class = "badge bg-secondary me-1" style = "font-size: 0.7rem;" > #{{ node.case.id }}< / span >
{{ node.case.titel }}
< / a >
< span class = "status-dot status-{{ node.case.status }} ms-2" title = "{{ node.case.status }}" > < / span >
2026-02-06 10:47:14 +01:00
{% if node.relation_id %}
< button onclick = "deleteRelation({{ node.relation_id }})" class = "btn btn-sm text-danger border-0 ms-1 p-0 opacity-50 hover-opacity-100" title = "Fjern relation" >
< i class = "bi bi-x-circle-fill" > < / i >
< / button >
{% endif %}
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / div >
< / div >
{% if node.children %}
2026-02-06 10:47:14 +01:00
< div class = "relation-children" >
{{ render_tree(node.children) }}
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
{% endif %}
< / li >
{% endfor %}
< / ul >
{% endmacro %}
{% if relation_tree %}
< div class = "relation-tree-container" >
2026-02-06 10:47:14 +01:00
{{ render_tree(relation_tree) }}
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / div >
2026-02-01 11:58:44 +01:00
{% else %}
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< p class = "text-muted text-center pt-3" > Ingen relaterede sager< / p >
2026-02-01 11:58:44 +01:00
{% endif %}
< / div >
< / div >
< / div >
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
<!-- ROW 3: Files + Linked Emails -->
< div class = "row mb-3" >
<!-- Files -->
2026-02-06 10:47:14 +01:00
< div class = "col-md-6 mb-3" >
< div class = "card h-100" data-module = "files" data-has-content = "unknown" >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< div class = "card-header d-flex justify-content-between align-items-center" >
< h6 class = "mb-0" style = "color: var(--accent);" > 📁 Filer & Dokumenter< / h6 >
< input type = "file" id = "fileInput" multiple style = "display: none;" onchange = "handleFileUpload(this.files)" >
< button class = "btn btn-sm btn-outline-primary" onclick = "document.getElementById('fileInput').click()" >
< i class = "bi bi-cloud-upload" > < / i > Upload
< / button >
< / div >
<!-- Drag & Drop Zone -->
< div class = "card-body p-0 d-flex flex-column" id = "fileDropZone" >
< div class = "p-4 text-center border-bottom bg-light" id = "fileDropMessage" style = "cursor: pointer;" onclick = "document.getElementById('fileInput').click()" >
< i class = "bi bi-cloud-arrow-up display-6 text-muted" > < / i >
< p class = "small text-muted mb-0 mt-2" > Træk filer hertil for at uploade< / p >
< / div >
< div class = "list-group list-group-flush flex-grow-1 overflow-auto" id = "files-list" style = "max-height: 300px;" >
< div class = "p-3 text-center text-muted" > Ingen filer fundet...< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Linked Emails -->
2026-02-06 10:47:14 +01:00
< div class = "col-md-6 mb-3" >
< div class = "card h-100" data-module = "emails" data-has-content = "unknown" >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< div class = "card-header" >
< h6 class = "mb-0" style = "color: var(--accent);" > 📧 Linkede Emails< / h6 >
< / div >
<!-- Email Drop Zone -->
< div class = "card-body p-0 d-flex flex-column" id = "emailDropZone" >
< div class = "p-3 border-bottom bg-light position-relative" >
< input type = "text" class = "form-control form-control-sm" id = "emailSearchInput" placeholder = "Søg og link email..." autocomplete = "off" >
< div class = "list-group position-absolute shadow-sm" id = "emailSearchResults" style = "z-index: 1000; display: none; top: 100%; left: 0; right: 0; max-height: 300px; overflow-y: auto;" > < / div >
< / div >
< div class = "text-center p-2 small text-muted fst-italic border-bottom" >
Træk .msg/.eml filer hertil for at importere
< / div >
< div class = "list-group list-group-flush flex-grow-1 overflow-auto" id = "linked-emails-list" style = "max-height: 250px;" >
< div class = "p-3 text-center text-muted" > Ingen emails linket...< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
2026-02-06 10:47:14 +01:00
<!-- File Preview Modal -->
< div class = "modal fade" id = "filePreviewModal" tabindex = "-1" data-bs-backdrop = "static" >
< div class = "modal-dialog modal-xl modal-dialog-centered" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" >
< i class = "bi bi-file-earmark-text me-2" > < / i >
< span id = "previewFileName" > Fil preview< / span >
< / h5 >
< div class = "ms-auto d-flex align-items-center gap-2" >
< a id = "previewDownloadBtn" href = "#" class = "btn btn-sm btn-outline-primary" download >
< i class = "bi bi-download me-1" > < / i > Download
< / a >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< / div >
< div class = "modal-body p-0" style = "min-height: 60vh; max-height: 80vh; overflow: hidden;" >
< div id = "previewContent" class = "w-100 h-100 d-flex align-items-center justify-content-center" >
< div class = "spinner-border text-primary" role = "status" >
< span class = "visually-hidden" > Indlæser...< / span >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
2026-02-01 11:58:44 +01:00
<!-- Search Modals -->
< div class = "modal fade" id = "contactSearchModal" tabindex = "-1" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Søg kontakt< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< input type = "text" id = "contactSearch" placeholder = "Søg efter kontakt..." class = "form-control mb-3" >
< div id = "contactSearchResults" style = "max-height: 300px; overflow-y: auto;" > < / div >
< / div >
< / div >
< / div >
< / div >
< div class = "modal fade" id = "customerSearchModal" tabindex = "-1" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Søg kunde< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< input type = "text" id = "customerSearch" placeholder = "Søg efter kunde..." class = "form-control mb-3" >
< div id = "customerSearchResults" style = "max-height: 300px; overflow-y: auto;" > < / div >
< / div >
< / div >
< / div >
< / div >
< div class = "modal fade" id = "relationModal" tabindex = "-1" >
< div class = "modal-dialog modal-lg" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > 🔗 Tilføj relation til sag< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< div class = "mb-3" >
< label class = "form-label fw-bold" > 1. Søg og vælg sag< / label >
< input type = "text"
id="relationCaseSearch"
placeholder="Søg efter sag ID, titel, kunde eller beskrivelse..."
class="form-control form-control-lg"
autocomplete="off">
< div id = "relationSearchResults"
style="max-height: 400px; overflow-y: auto; margin-top: 0.5rem;"
class="border rounded">< / div >
< / div >
< div id = "selectedCasePreview" style = "display: none;" class = "alert alert-info mb-3" >
< div class = "d-flex justify-content-between align-items-start" >
< div >
< strong > Valgt sag:< / strong >
< div id = "selectedCaseTitle" class = "mt-1" > < / div >
< / div >
< button type = "button" class = "btn btn-sm btn-outline-secondary" onclick = "clearSelectedRelationCase()" >
Ryd valg
< / button >
< / div >
< / div >
< div class = "mb-3" >
< label class = "form-label fw-bold" > 2. Vælg relationstype< / label >
< select id = "relationTypeSelect" class = "form-control form-control-lg" onchange = "updateAddRelationButton()" >
< option value = "" > Vælg hvordan sagerne er relateret...< / option >
< option value = "relateret" > 🔗 Relateret - Generel relation< / option >
< option value = "afhænger af" > ⏳ Afhænger af - Denne sag venter på den anden< / option >
< option value = "blokkerer" > 🚫 Blokkerer - Denne sag blokerer den anden< / option >
< option value = "duplikat" > 📋 Duplikat - Sagerne er den samme< / option >
< option value = "forårsaget af" > 🔄 Forårsaget af - Denne sag er konsekvens af den anden< / option >
< option value = "følger op på" > 📌 Følger op på - Fortsættelse af tidligere sag< / option >
< / select >
< / div >
< div class = "alert alert-light d-flex align-items-center" style = "font-size: 0.9rem;" >
< i class = "bi bi-info-circle me-2" > < / i >
< div >
< strong > Tip:< / strong > Brug pile (↑↓) til at navigere i søgeresultater, Enter til at vælge.
< / div >
< / div >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Luk< / button >
< button type = "button"
class="btn btn-primary btn-lg"
onclick="addRelation()"
id="addRelationBtn"
disabled>
< i class = "bi bi-plus-circle me-1" > < / i > Tilføj relation
< / button >
< / div >
< / div >
< / div >
< / div >
2026-02-06 10:47:14 +01:00
<!-- Contact Info Modal -->
< div class = "modal fade" id = "contactInfoModal" tabindex = "-1" aria-hidden = "true" >
< div class = "modal-dialog modal-dialog-centered modal-sm" >
2026-02-01 11:58:44 +01:00
< div class = "modal-content" >
< div class = "modal-header" >
2026-02-06 10:47:14 +01:00
< h5 class = "modal-title" id = "contactInfoName" > Kontakt< / h5 >
2026-02-01 11:58:44 +01:00
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
2026-02-06 10:47:14 +01:00
< div class = "mb-2" >
< div class = "text-muted small" > Titel< / div >
< div id = "contactInfoTitle" > -< / div >
< / div >
< div class = "mb-2" >
< div class = "text-muted small" > Kunde< / div >
< div id = "contactInfoCompany" > -< / div >
< / div >
< div class = "mb-2" >
< div class = "text-muted small" > Email< / div >
< div id = "contactInfoEmail" > -< / div >
< / div >
< div class = "mb-2" >
< div class = "text-muted small" > Telefon< / div >
< div id = "contactInfoPhone" > -< / div >
< / div >
< div class = "mb-2" >
< div class = "text-muted small" > Mobil< / div >
< div id = "contactInfoMobile" > -< / div >
< / div >
< div class = "mb-2" >
< div class = "text-muted small" > Rolle< / div >
< div id = "contactInfoRole" > -< / div >
< / div >
< div id = "contactInfoPrimary" class = "badge bg-primary d-none" > Hovedkontakt< / div >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-light" data-bs-dismiss = "modal" > Luk< / button >
< button type = "button" class = "btn btn-primary" onclick = "openContactRoleFromInfo()" > Rediger rolle< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Contact Role Modal -->
< div class = "modal fade" id = "contactRoleModal" tabindex = "-1" aria-hidden = "true" >
< div class = "modal-dialog modal-dialog-centered" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Rediger kontaktrolle< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< input type = "hidden" id = "contactRoleContactId" / >
< div class = "mb-2" >
< label class = "form-label" > Kontakt< / label >
< div id = "contactRoleName" class = "fw-semibold" > -< / div >
< / div >
< div class = "mb-3" >
< label class = "form-label" > Rolle< / label >
< input type = "text" class = "form-control" id = "contactRoleInput" placeholder = "fx ansvarlig, beslutningstager" >
< / div >
< div class = "form-check" >
< input class = "form-check-input" type = "checkbox" id = "contactRolePrimary" >
< label class = "form-check-label" for = "contactRolePrimary" > Sæt som hovedkontakt< / label >
2026-02-01 11:58:44 +01:00
< / div >
< / div >
2026-02-06 10:47:14 +01:00
< div class = "modal-footer" >
< button type = "button" class = "btn btn-light" data-bs-dismiss = "modal" > Annuller< / button >
< button type = "button" class = "btn btn-primary" onclick = "saveContactRole()" > Gem< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Deferred Modal -->
< div class = "modal fade" id = "deferredModal" tabindex = "-1" aria-hidden = "true" >
< div class = "modal-dialog modal-dialog-centered" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Udsat start< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< label class = "form-label" > Dato< / label >
< input
type="date"
class="form-control form-control-sm"
id="deferredUntilInput"
value="{{ case.deferred_until.strftime('%Y-%m-%d') if case.deferred_until else '' }}"
/>
< div class = "defer-controls mt-2" >
< button class = "btn btn-outline-primary" onclick = "shiftDeferredDays(1)" > +1 dag< / button >
< button class = "btn btn-outline-primary" onclick = "shiftDeferredDays(7)" > +1 uge< / button >
< button class = "btn btn-outline-primary" onclick = "shiftDeferredMonths(1)" > +1 mnd< / button >
< / div >
< label class = "form-label mt-3" > Udsat til sag‑ status< / label >
< select class = "form-select form-select-sm" id = "deferredCaseSelect" >
< option value = "" > Vælg relateret sag< / option >
{% for rc in related_case_options %}
< option value = "{{ rc.id }}" { % if case . deferred_until_case_id = = rc . id % } selected { % endif % } >
#{{ rc.id }} – {{ rc.titel }}
< / option >
{% endfor %}
< / select >
< select class = "form-select form-select-sm mt-2" id = "deferredStatusSelect" >
< option value = "" > Vælg status< / option >
{% for st in status_options %}
< option value = "{{ st }}" { % if case . deferred_until_status = = st % } selected { % endif % } >
{{ st }}
< / option >
{% endfor %}
< / select >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-light" data-bs-dismiss = "modal" > Luk< / button >
< button type = "button" class = "btn btn-outline-danger" onclick = "clearDeferredAll()" > Ryd< / button >
< button type = "button" class = "btn btn-primary" onclick = "saveDeferredAll()" > Gem< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Module Control Modal -->
< div class = "modal fade" id = "moduleControlModal" tabindex = "-1" aria-hidden = "true" >
< div class = "modal-dialog modal-dialog-centered" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Vis/skjul moduler< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" id = "moduleControlList" >
< div class = "text-muted small" > Indlæser...< / div >
< / div >
2026-02-01 11:58:44 +01:00
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Luk< / button >
< / div >
< / div >
< / div >
< / div >
< script >
const caseId = {{ case.id }};
let contactSearchTimeout;
let customerSearchTimeout;
let relationSearchTimeout;
let selectedRelationCaseId = null;
// Modal instances
2026-02-06 10:47:14 +01:00
let contactSearchModal, customerSearchModal, relationModal, contactInfoModal, createRelatedCaseModalInstance;
let currentContactInfo = null;
2026-02-01 11:58:44 +01:00
// Initialize everything when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
// Initialize modals
contactSearchModal = new bootstrap.Modal(document.getElementById('contactSearchModal'));
customerSearchModal = new bootstrap.Modal(document.getElementById('customerSearchModal'));
relationModal = new bootstrap.Modal(document.getElementById('relationModal'));
2026-02-06 10:47:14 +01:00
contactInfoModal = new bootstrap.Modal(document.getElementById('contactInfoModal'));
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
createRelatedCaseModalInstance = new bootstrap.Modal(document.getElementById('createRelatedCaseModal'));
2026-02-01 11:58:44 +01:00
// Setup search handlers
setupContactSearch();
setupCustomerSearch();
setupRelationSearch();
// Render Global Tags
if (window.renderEntityTags) {
window.renderEntityTags('case', {{ case.id }}, 'case-tags');
}
2026-02-06 10:47:14 +01:00
loadModulePrefs().then(() => applyViewFromTags());
2026-02-01 11:58:44 +01:00
// Set default context for keyboard shortcuts (Option+Shift+T)
if (window.setTagPickerContext) {
window.setTagPickerContext('case', {{ case.id }}, () => window.renderEntityTags('case', {{ case.id }}, 'case-tags'));
}
// Load Hardware & Locations
loadCaseHardware();
loadCaseLocations();
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
// Focus on title when create modal opens
const createModalEl = document.getElementById('createRelatedCaseModal');
if (createModalEl) {
createModalEl.addEventListener('shown.bs.modal', function () {
document.getElementById('newCaseTitle').focus();
});
}
2026-02-01 11:58:44 +01:00
});
// Show modal functions
function showContactSearch() {
contactSearchModal.show();
setTimeout(() => document.getElementById('contactSearch').focus(), 300);
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
function showCustomerSearch() {
customerSearchModal.show();
setTimeout(() => document.getElementById('customerSearch').focus(), 300);
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
function showRelationModal() {
relationModal.show();
setTimeout(() => document.getElementById('relationCaseSearch').focus(), 300);
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
2026-02-06 10:47:14 +01:00
function showContactInfoModal(el) {
currentContactInfo = {
id: el.dataset.contactId,
name: el.dataset.name || '-',
title: el.dataset.title || '-',
company: el.dataset.company || '-',
email: el.dataset.email || '-',
phone: el.dataset.phone || '-',
mobile: el.dataset.mobile || '-',
role: el.dataset.role || '-',
isPrimary: el.dataset.isPrimary === 'true'
};
document.getElementById('contactInfoName').textContent = currentContactInfo.name;
document.getElementById('contactInfoTitle').textContent = currentContactInfo.title || '-';
document.getElementById('contactInfoCompany').textContent = currentContactInfo.company || '-';
document.getElementById('contactInfoEmail').textContent = currentContactInfo.email || '-';
document.getElementById('contactInfoPhone').textContent = currentContactInfo.phone || '-';
document.getElementById('contactInfoMobile').textContent = currentContactInfo.mobile || '-';
document.getElementById('contactInfoRole').textContent = currentContactInfo.role || '-';
const primaryBadge = document.getElementById('contactInfoPrimary');
if (currentContactInfo.isPrimary) {
primaryBadge.classList.remove('d-none');
} else {
primaryBadge.classList.add('d-none');
}
contactInfoModal.show();
}
function openContactRoleFromInfo() {
if (!currentContactInfo) return;
contactInfoModal.hide();
openContactRoleModal(
currentContactInfo.id,
currentContactInfo.name,
currentContactInfo.role || 'Kontakt',
currentContactInfo.isPrimary
);
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
function showCreateRelatedModal() {
createRelatedCaseModalInstance.show();
}
async function createRelatedCase() {
const title = document.getElementById('newCaseTitle').value;
const relationType = document.getElementById('newCaseRelationType').value;
const description = document.getElementById('newCaseDescription').value;
if (!title) {
alert('Titel er påkrævet');
return;
}
// 1. Create the new case
try {
const caseResponse = await fetch('/api/v1/sag', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
titel: title,
beskrivelse: description,
customer_id: {{ case.customer_id }},
status: 'åben'
})
});
if (!caseResponse.ok) throw new Error('Kunne ikke oprette sag');
const newCase = await caseResponse.json();
// 2. Create the relation
const relationResponse = await fetch(`/api/v1/sag/${caseId}/relationer`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
målsag_id: newCase.id,
relationstype: relationType
})
});
if (!relationResponse.ok) throw new Error('Kunne ikke oprette relation');
// 3. Reload to show new relation
window.location.reload();
} catch (err) {
console.error('Error creating related case:', err);
alert('Der opstod en fejl: ' + err.message);
}
}
2026-02-01 11:58:44 +01:00
function confirmDeleteCase() {
if(confirm('Slet denne sag?')) {
fetch('/api/v1/sag/{{ case.id }}', {method: 'DELETE'})
.then(() => window.location='/sag');
}
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
// Contact Search
function setupContactSearch() {
const contactSearchInput = document.getElementById('contactSearch');
contactSearchInput.addEventListener('input', function(e) {
clearTimeout(contactSearchTimeout);
const query = e.target.value.trim();
if (query.length < 2 ) {
document.getElementById('contactSearchResults').innerHTML = '';
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('contactSearchResults');
if (contacts.length === 0) {
resultsDiv.innerHTML = '< div class = "p-3 text-muted" > Ingen kontakter fundet< / div > ';
} else {
resultsDiv.innerHTML = contacts.map(c => `
< div class = "list-group-item list-group-item-action" style = "cursor: pointer;"
onclick="addContact(${caseId}, ${c.id}, '${(c.first_name + ' ' + c.last_name).replace(/'/g, "\\'")}')">
< strong > ${c.first_name} ${c.last_name}< / strong >
< div class = "small text-muted" > ${c.email || ''} ${c.user_company ? '(' + c.user_company + ')' : ''}< / div >
< / div >
`).join('');
}
} catch (err) {
console.error('Error searching contacts:', err);
}
}, 300);
});
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
async function addContact(caseId, contactId, contactName) {
try {
const response = await fetch(`/api/v1/sag/${caseId}/contacts`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({contact_id: contactId, role: 'Kontakt'})
});
if (response.ok) {
contactSearchModal.hide();
window.location.reload();
} else {
const error = await response.json();
alert(`Fejl: ${error.detail}`);
}
} catch (err) {
alert('Fejl ved tilføjelse af kontakt: ' + err.message);
}
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
async function removeContact(caseId, contactId) {
if (confirm('Fjern denne kontakt fra sagen?')) {
const response = await fetch(`/api/v1/sag/${caseId}/contacts/${contactId}`, {method: 'DELETE'});
if (response.ok) {
window.location.reload();
} else {
alert('Fejl ved fjernelse af kontakt');
}
}
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
// Customer Search
function setupCustomerSearch() {
const customerSearchInput = document.getElementById('customerSearch');
customerSearchInput.addEventListener('input', function(e) {
clearTimeout(customerSearchTimeout);
const query = e.target.value.trim();
if (query.length < 2 ) {
document.getElementById('customerSearchResults').innerHTML = '';
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('customerSearchResults');
if (customers.length === 0) {
resultsDiv.innerHTML = '< div class = "p-3 text-muted" > Ingen kunder fundet< / div > ';
} else {
resultsDiv.innerHTML = customers.map(c => `
< div class = "list-group-item list-group-item-action" style = "cursor: pointer;"
onclick="addCustomer(${caseId}, ${c.id}, '${c.name.replace(/'/g, "\\'")}')">
< strong > ${c.name}< / strong >
< div class = "small text-muted" > ${c.email || ''} ${c.cvr_number ? '(CVR: ' + c.cvr_number + ')' : ''}< / div >
< / div >
`).join('');
}
} catch (err) {
console.error('Error searching customers:', err);
}
}, 300);
});
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
async function addCustomer(caseId, customerId, customerName) {
try {
const response = await fetch(`/api/v1/sag/${caseId}/customers`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({customer_id: customerId, role: 'Kunde'})
});
if (response.ok) {
customerSearchModal.hide();
window.location.reload();
} else {
const error = await response.json();
alert(`Fejl: ${error.detail}`);
}
} catch (err) {
alert('Fejl ved tilføjelse af kunde: ' + err.message);
}
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
async function removeCustomer(caseId, customerId) {
if (confirm('Fjern denne kunde fra sagen?')) {
const response = await fetch(`/api/v1/sag/${caseId}/customers/${customerId}`, {method: 'DELETE'});
if (response.ok) {
window.location.reload();
} else {
alert('Fejl ved fjernelse af kunde');
}
}
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
// Relation Search - Enhanced version
let currentFocusIndex = -1;
let searchResults = [];
2026-02-01 00:29:57 +01:00
2026-02-01 11:58:44 +01:00
function setupRelationSearch() {
const relationSearchInput = document.getElementById('relationCaseSearch');
// Input handler
relationSearchInput.addEventListener('input', function(e) {
clearTimeout(relationSearchTimeout);
const query = e.target.value.trim();
currentFocusIndex = -1;
if (query.length < 2 ) {
document.getElementById('relationSearchResults').innerHTML = '';
document.getElementById('relationSearchResults').style.display = 'none';
return;
}
relationSearchTimeout = setTimeout(async () => {
try {
const response = await fetch(`/api/v1/search/sag?q=${encodeURIComponent(query)}`);
const cases = await response.json();
searchResults = cases.filter(c => c.id !== caseId);
renderRelationSearchResults(searchResults);
} catch (err) {
console.error('Error searching cases:', err);
}
}, 200);
});
// Keyboard navigation
relationSearchInput.addEventListener('keydown', function(e) {
const resultsDiv = document.getElementById('relationSearchResults');
const items = resultsDiv.querySelectorAll('.relation-search-item');
if (e.key === 'ArrowDown') {
e.preventDefault();
currentFocusIndex = (currentFocusIndex + 1) % items.length;
updateFocusedItem(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
currentFocusIndex = currentFocusIndex < = 0 ? items.length - 1 : currentFocusIndex - 1;
updateFocusedItem(items);
} else if (e.key === 'Enter') {
e.preventDefault();
if (currentFocusIndex >= 0 & & currentFocusIndex < items.length ) {
items[currentFocusIndex].click();
}
}
});
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
function updateFocusedItem(items) {
items.forEach((item, index) => {
if (index === currentFocusIndex) {
item.classList.add('active');
item.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
} else {
item.classList.remove('active');
}
});
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
function renderRelationSearchResults(cases) {
const resultsDiv = document.getElementById('relationSearchResults');
if (cases.length === 0) {
resultsDiv.innerHTML = '< div class = "p-3 text-muted text-center" > < i class = "bi bi-search me-2" > < / i > Ingen sager fundet< / div > ';
resultsDiv.style.display = 'block';
return;
}
// Group by status
const grouped = {};
cases.forEach(c => {
const status = c.status || 'ukendt';
if (!grouped[status]) grouped[status] = [];
grouped[status].push(c);
});
let html = '< div class = "list-group list-group-flush" > ';
// Sort status groups: åben first, then others
const statusOrder = ['åben', 'under behandling', 'afventer', 'løst', 'lukket'];
const sortedStatuses = Object.keys(grouped).sort((a, b) => {
const aIndex = statusOrder.indexOf(a);
const bIndex = statusOrder.indexOf(b);
if (aIndex === -1 & & bIndex === -1) return a.localeCompare(b);
if (aIndex === -1) return 1;
if (bIndex === -1) return -1;
return aIndex - bIndex;
});
sortedStatuses.forEach(status => {
const statusCases = grouped[status];
// Status group header
html += `
< div class = "list-group-item bg-light" style = "padding: 0.5rem 1rem; font-weight: 600; font-size: 0.85rem; color: var(--text-secondary);" >
< span class = "status-badge status-${status}" > ${status}< / span >
< span class = "badge bg-secondary float-end" > ${statusCases.length}< / span >
< / div >
`;
statusCases.forEach(c => {
const createdDate = c.created_at ? new Date(c.created_at).toLocaleDateString('da-DK') : 'N/A';
const beskrivelse = c.beskrivelse ? c.beskrivelse.substring(0, 80) + '...' : '';
const customerName = c.customer_name || '';
const safeTitle = (c.titel || '').replace(/"/g, '" ').replace(/'/g, '' ');
const safeCustomer = customerName.replace(/"/g, '" ').replace(/'/g, '' ');
html += `
< div class = "list-group-item list-group-item-action relation-search-item"
style="cursor: pointer; padding: 0.75rem 1rem;"
onclick="selectRelationCase(${c.id}, '${safeTitle}', '${safeCustomer}', '${status}');"
data-case-id="${c.id}">
< div class = "d-flex justify-content-between align-items-start" >
< div style = "flex: 1;" >
< div class = "d-flex align-items-center gap-2 mb-1" >
< span class = "badge bg-primary" style = "font-size: 0.75rem;" > #${c.id}< / span >
< strong style = "font-size: 0.95rem;" > ${escapeHtml(c.titel)}< / strong >
< / div >
${c.customer_name ? `
< div class = "small text-muted mb-1" >
< i class = "bi bi-building me-1" > < / i > ${escapeHtml(c.customer_name)}
< / div >
` : ''}
${beskrivelse ? `
< div class = "small text-muted" style = "font-size: 0.8rem;" > ${escapeHtml(beskrivelse)}< / div >
` : ''}
< / div >
< div class = "text-end" style = "min-width: 100px;" >
< div class = "small text-muted" > ${createdDate}< / div >
< / div >
< / div >
< / div >
`;
});
});
html += '< / div > ';
resultsDiv.innerHTML = html;
resultsDiv.style.display = 'block';
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
function selectRelationCase(caseIdValue, caseTitel, customerName, status) {
selectedRelationCaseId = caseIdValue;
// Update preview
const previewDiv = document.getElementById('selectedCasePreview');
const titleDiv = document.getElementById('selectedCaseTitle');
titleDiv.innerHTML = `
< div class = "d-flex align-items-center gap-2 mb-1" >
< span class = "badge bg-primary" > #${caseIdValue}< / span >
< strong > ${escapeHtml(caseTitel)}< / strong >
< span class = "status-badge status-${status}" > ${status}< / span >
< / div >
${customerName ? `< div class = "small" > < i class = "bi bi-building me-1" > < / i > ${escapeHtml(customerName)}< / div > ` : ''}
`;
previewDiv.style.display = 'block';
document.getElementById('relationSearchResults').innerHTML = '';
document.getElementById('relationSearchResults').style.display = 'none';
document.getElementById('relationCaseSearch').value = '';
// Enable add button
updateAddRelationButton();
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
function clearSelectedRelationCase() {
selectedRelationCaseId = null;
document.getElementById('selectedCasePreview').style.display = 'none';
document.getElementById('relationCaseSearch').value = '';
document.getElementById('relationCaseSearch').focus();
updateAddRelationButton();
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
function updateAddRelationButton() {
const btn = document.getElementById('addRelationBtn');
const relationType = document.getElementById('relationTypeSelect').value;
btn.disabled = !selectedRelationCaseId || !relationType;
2026-02-01 00:29:57 +01:00
}
2026-02-01 11:58:44 +01:00
async function addRelation() {
const relationType = document.getElementById('relationTypeSelect').value;
const btn = document.getElementById('addRelationBtn');
if (!selectedRelationCaseId) {
alert('Vælg en sag først');
return;
}
if (!relationType) {
alert('Vælg en relationstype');
return;
}
// Disable button during request
btn.disabled = true;
btn.innerHTML = '< span class = "spinner-border spinner-border-sm me-2" > < / span > Tilføjer...';
try {
const response = await fetch(`/api/v1/sag/${caseId}/relationer`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
målsag_id: selectedRelationCaseId,
relationstype: relationType
})
});
if (response.ok) {
selectedRelationCaseId = null;
relationModal.hide();
window.location.reload();
} else {
const error = await response.json();
alert(`Fejl: ${error.detail}`);
btn.disabled = false;
btn.innerHTML = '< i class = "bi bi-plus-circle me-1" > < / i > Tilføj relation';
}
} catch (err) {
alert('Fejl ved tilføjelse af relation: ' + err.message);
btn.disabled = false;
btn.innerHTML = '< i class = "bi bi-plus-circle me-1" > < / i > Tilføj relation';
}
2026-02-01 00:29:57 +01:00
}
2026-01-31 23:16:24 +01:00
2026-02-01 11:58:44 +01:00
async function deleteRelation(relationId) {
if (confirm('Fjern denne relation?')) {
const response = await fetch(`/api/v1/sag/${caseId}/relationer/${relationId}`, {method: 'DELETE'});
if (response.ok) {
window.location.reload();
} else {
alert('Fejl ved fjernelse af relation');
}
}
}
2026-01-29 23:07:33 +01:00
2026-02-01 11:58:44 +01:00
// ============ Hardware Handling ============
async function loadCaseHardware() {
try {
const res = await fetch(`/api/v1/sag/{{ case.id }}/hardware`);
const hardware = await res.json();
const container = document.getElementById('hardware-list');
if (hardware.length === 0) {
container.innerHTML = '< div class = "p-3 text-center text-muted small" > Ingen hardware tilknyttet< / div > ';
return;
}
2026-02-06 10:47:14 +01:00
container.innerHTML = `
< div class = "hardware-list-header" >
< span > Enhed< / span >
< span > SN< / span >
< span > Slet< / span >
< / div >
${hardware.map(h => `
< div class = "hardware-row" >
< div >
< a href = "/hardware/${h.id}" class = "text-decoration-none fw-semibold" >
2026-02-01 11:58:44 +01:00
${h.brand} ${h.model}
< / a >
< / div >
2026-02-06 10:47:14 +01:00
< small > ${h.serial_number || '-'}< / small >
< button class = "btn btn-sm btn-delete" onclick = "unlinkHardware(${h.id})" title = "Slet" >
✕
< / button >
2026-01-29 23:07:33 +01:00
< / div >
2026-02-06 10:47:14 +01:00
`).join('')}
`;
2026-02-01 11:58:44 +01:00
} catch (e) {
console.error("Error loading hardware:", e);
document.getElementById('hardware-list').innerHTML = '< div class = "p-3 text-danger text-center" > Fejl ved hentning< / div > ';
}
}
2026-02-01 00:29:57 +01:00
2026-02-01 11:58:44 +01:00
async function promptLinkHardware() {
const id = prompt("Indtast Hardware ID:");
if (!id) return;
try {
const res = await fetch(`/api/v1/sag/{{ case.id }}/hardware`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ hardware_id: parseInt(id) })
});
if (!res.ok) throw await res.json();
loadCaseHardware();
} catch (e) {
alert("Fejl: " + (e.detail || e.message));
}
}
async function unlinkHardware(hwId) {
if(!confirm("Fjern link til dette hardware?")) return;
try {
await fetch(`/api/v1/sag/{{ case.id }}/hardware/${hwId}`, { method: 'DELETE' });
loadCaseHardware();
} catch (e) {
alert("Fejl ved sletning");
}
}
// ============ Location Handling ============
async function loadCaseLocations() {
try {
const res = await fetch(`/api/v1/sag/{{ case.id }}/locations`);
const locations = await res.json();
const container = document.getElementById('locations-list');
if (locations.length === 0) {
container.innerHTML = '< div class = "p-3 text-center text-muted small" > Ingen lokationer tilknyttet< / div > ';
return;
}
2026-02-06 10:47:14 +01:00
container.innerHTML = `
< div class = "location-list-header" >
< span > Navn< / span >
< span > Type< / span >
< span > Slet< / span >
< / div >
${locations.map(l => `
< div class = "location-row" >
< div class = "fw-semibold" >
2026-02-01 11:58:44 +01:00
< i class = "bi bi-geo-alt me-1 text-secondary" > < / i >
${l.name}
< / div >
2026-02-06 10:47:14 +01:00
< small > ${l.location_type || '-'}< / small >
< button class = "btn btn-sm btn-delete" onclick = "unlinkLocation(${l.id})" title = "Slet" >
✕
< / button >
2026-01-29 23:07:33 +01:00
< / div >
2026-02-06 10:47:14 +01:00
`).join('')}
`;
2026-02-01 11:58:44 +01:00
} catch (e) {
console.error("Error loading locations:", e);
document.getElementById('locations-list').innerHTML = '< div class = "p-3 text-danger text-center" > Fejl ved hentning< / div > ';
}
}
2026-01-29 23:07:33 +01:00
2026-02-01 11:58:44 +01:00
async function promptLinkLocation() {
const id = prompt("Indtast Lokations ID:");
if (!id) return;
try {
const res = await fetch(`/api/v1/sag/{{ case.id }}/locations`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ location_id: parseInt(id) })
});
if (!res.ok) throw await res.json();
loadCaseLocations();
} catch (e) {
alert("Fejl: " + (e.detail || e.message));
}
}
async function unlinkLocation(locId) {
if(!confirm("Fjern link til denne lokation?")) return;
try {
await fetch(`/api/v1/sag/{{ case.id }}/locations/${locId}`, { method: 'DELETE' });
loadCaseLocations();
} catch (e) {
alert("Fejl ved sletning");
}
}
// Initialize relation search when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupRelationSearch);
} else {
setupRelationSearch();
}
// Kontakt Modal functions
function showKontaktModal() {
const modal = new bootstrap.Modal(document.getElementById('kontaktModal'));
modal.show();
}
// Afdeling Modal functions
function showAfdelingModal() {
const modal = new bootstrap.Modal(document.getElementById('afdelingModal'));
modal.show();
}
async function updateAfdeling() {
const newAfdeling = document.getElementById('afdelingInput').value.trim();
try {
const response = await fetch('/api/v1/customers/{{ customer.id if customer else 0 }}', {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ department: newAfdeling })
});
if (!response.ok) throw await response.json();
// Reload page to show updated data
window.location.reload();
} catch (e) {
alert("Fejl ved opdatering: " + (e.detail || e.message));
}
}
< / script >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
2026-02-06 10:47:14 +01:00
< / div >
< div class = "col-lg-4" id = "case-right-column" >
< div class = "right-modules-grid" >
< div class = "card d-flex flex-column h-100 right-module-card" data-module = "hardware" data-has-content = "unknown" >
< div class = "card-header d-flex justify-content-between align-items-center" >
< h6 class = "mb-0" style = "color: var(--accent);" > 💻 Hardware< / h6 >
< button class = "btn btn-sm btn-outline-primary" onclick = "openSearchModal('hardware')" >
< i class = "bi bi-link-45deg" > < / i >
< / button >
< / div >
< div class = "card-body flex-grow-1 overflow-auto p-0" style = "max-height: 180px;" >
< div class = "list-group list-group-flush" id = "hardware-list" >
< div class = "p-3 text-center text-muted" > Henter hardware...< / div >
< / div >
< / div >
< / div >
< div class = "card h-100 d-flex flex-column right-module-card" data-module = "locations" data-has-content = "unknown" >
< div class = "card-header d-flex justify-content-between align-items-center" >
< h6 class = "mb-0" style = "color: var(--accent);" > 📍 Lokationer< / h6 >
< button class = "btn btn-sm btn-outline-primary" onclick = "openSearchModal('location')" >
< i class = "bi bi-plus-lg" > < / i >
< / button >
< / div >
< div class = "card-body flex-grow-1 overflow-auto p-0" style = "max-height: 180px;" >
< div class = "list-group list-group-flush" id = "locations-list" >
< div class = "p-3 text-center text-muted" > Henter lokationer...< / div >
< / div >
< / div >
< / div >
< div class = "card h-100 d-flex flex-column right-module-card" data-module = "contacts" data-has-content = "{{ 'true' if contacts else 'false' }}" >
< div class = "card-header d-flex justify-content-between align-items-center" >
< h6 class = "mb-0" style = "color: var(--accent);" > 👥 Kontakter< / h6 >
< button class = "btn btn-sm btn-outline-primary" onclick = "openSearchModal('contact')" >
< i class = "bi bi-plus-lg" > < / i >
< / button >
< / div >
< div class = "card-body flex-grow-1 overflow-auto" id = "contacts-container" style = "max-height: 180px;" >
{% if contacts %}
< div class = "contact-list-header" >
< span > Navn< / span >
< span > Title< / span >
< span > Kunde< / span >
< span > Slet< / span >
< / div >
{% for contact in contacts %}
< div
class="contact-row"
role="button"
tabindex="0"
onclick="showContactInfoModal(this)"
data-contact-id="{{ contact.contact_id }}"
data-name="{{ contact.contact_name|replace('"', '" ') }}"
data-title="{{ contact.title|default('', true)|replace('"', '" ') }}"
data-company="{{ contact.customer_name|default('', true)|replace('"', '" ') }}"
data-email="{{ contact.contact_email|default('', true)|replace('"', '" ') }}"
data-phone="{{ contact.phone|default('', true)|replace('"', '" ') }}"
data-mobile="{{ contact.mobile|default('', true)|replace('"', '" ') }}"
data-role="{{ contact.role|default('Kontakt')|replace('"', '" ') }}"
data-is-primary="{{ 'true' if contact.is_primary else 'false' }}"
>
< div class = "contact-name" > {{ contact.contact_name }}< / div >
< small > {{ contact.title or '-' }}< / small >
< small > {{ contact.customer_name or '-' }}< / small >
< button
class="btn btn-sm btn-delete"
onclick="event.stopPropagation(); removeContact({{ case.id }}, {{ contact.contact_id }})"
title="Slet"
>
✕
< / button >
< / div >
{% endfor %}
{% else %}
< p class = "text-muted text-center" > Ingen kontakter< / p >
{% endif %}
< / div >
< / div >
< div class = "card h-100 d-flex flex-column right-module-card" data-module = "customers" data-has-content = "{{ 'true' if customers else 'false' }}" >
< div class = "card-header d-flex justify-content-between align-items-center" >
< h6 class = "mb-0" style = "color: var(--accent);" > 🏢 Kunder< / h6 >
< button class = "btn btn-sm btn-outline-primary" onclick = "openSearchModal('customer')" >
< i class = "bi bi-plus-lg" > < / i >
< / button >
< / div >
< div class = "card-body flex-grow-1 overflow-auto" id = "customers-container" style = "max-height: 180px;" >
{% if customers %}
< div class = "customer-list-header" >
< span > Navn< / span >
< span > Rolle< / span >
< span > Email< / span >
< span > Slet< / span >
< / div >
{% for customer in customers %}
< div class = "customer-row" >
< div >
< a href = "/customers/{{ customer.customer_id }}" class = "text-decoration-none fw-semibold" >
{{ customer.customer_name }}
< / a >
< / div >
< small > {{ customer.role or '-' }}< / small >
< small > {{ customer.customer_email or '-' }}< / small >
< button onclick = "removeCustomer({{ case.id }}, {{ customer.customer_id }})" class = "btn btn-sm btn-delete" title = "Slet" > ✕< / button >
< / div >
{% endfor %}
{% else %}
< p class = "text-muted text-center" > Ingen kunder< / p >
{% endif %}
< / div >
< / div >
< div class = "card h-100 d-flex flex-column right-module-card" data-module = "time" data-has-content = "{{ 'true' if time_entries else 'false' }}" >
< div class = "card-header d-flex justify-content-between align-items-center" >
< h6 class = "mb-0 text-primary" > < i class = "bi bi-clock-history me-2" > < / i > Tid & Fakturering< / h6 >
< button class = "btn btn-sm btn-outline-primary" onclick = "showAddTimeModal()" >
< i class = "bi bi-plus-lg me-1" > < / i > Registrer Tid
< / button >
< / div >
< div class = "card-body p-0" style = "max-height: 180px; overflow: auto;" >
< div class = "table-responsive" >
< table class = "table table-hover mb-0" style = "vertical-align: middle;" >
< thead class = "bg-light" >
< tr >
< th class = "ps-3" > Dato< / th >
< th > Beskrivelse< / th >
< th > Bruger< / th >
< th > Timer< / th >
< / tr >
< / thead >
< tbody >
{% for entry in time_entries %}
< tr >
< td class = "ps-3" > {{ entry.worked_date }}< / td >
< td > {{ entry.description or '-' }}< / td >
< td > {{ entry.user_name }}< / td >
< td class = "fw-bold" > {{ entry.original_hours }}< / td >
< / tr >
{% else %}
< tr >
< td colspan = "4" class = "text-center py-3 text-muted" > Ingen tid registreret< / td >
< / tr >
{% endfor %}
< / tbody >
< / table >
< / div >
2026-02-08 01:45:00 +01:00
< div class = "border-top px-3 py-2 small text-muted" >
< div class = "fw-semibold text-primary mb-1" > < i class = "bi bi-credit-card me-1" > < / i > Klippekort< / div >
{% if prepaid_cards %}
< div class = "d-flex flex-column gap-1" >
{% for card in prepaid_cards %}
< div class = "d-flex justify-content-between" >
< span > #{{ card.card_number or card.id }}< / span >
< span > {{ '%.2f' % card.remaining_hours }}t< / span >
< / div >
{% endfor %}
< / div >
{% else %}
< div > Ingen aktive klippekort< / div >
{% endif %}
< / div >
2026-02-06 10:47:14 +01:00
< / div >
< / div >
< / div >
< / div >
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / div > <!-- End Details Tab -->
<!-- Solution Tab -->
2026-02-06 10:47:14 +01:00
< div class = "tab-pane fade" id = "solution" role = "tabpanel" tabindex = "0" data-module = "solution" data-has-content = "{{ 'true' if solution or is_nextcloud else 'false' }}" >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
<!-- Nextcloud Integration Box -->
{% if is_nextcloud %}
< div class = "card mb-3" >
< div class = "card-header d-flex justify-content-between align-items-center" >
< h6 class = "mb-0" style = "color: var(--accent);" > ☁️ Nextcloud Integration< / h6 >
{% if nextcloud_instance %}
< span class = "badge bg-success" style = "background: var(--accent) !important;" > Aktiv< / span >
{% else %}
< span class = "badge bg-warning text-dark" > Ingen instans< / span >
{% endif %}
2026-02-01 11:58:44 +01:00
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< div class = "card-body" >
{% if nextcloud_instance %}
<!-- Info Row -->
< div class = "d-flex flex-wrap gap-4 mb-3 border-bottom pb-3" >
2026-02-01 11:58:44 +01:00
< div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< span class = "text-muted small d-block" > Instans< / span >
< a href = "{{ nextcloud_instance.base_url }}" target = "_blank" style = "color: var(--accent); text-decoration: none; font-weight: 500;" >
{{ nextcloud_instance.base_url }} < i class = "bi bi-box-arrow-up-right small" > < / i >
< / a >
2026-01-31 23:16:24 +01:00
< / div >
2026-02-01 11:58:44 +01:00
< div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< span class = "text-muted small d-block" > Admin Konto< / span >
< span class = "font-monospace text-dark" > {{ nextcloud_instance.username }}< / span >
2026-02-01 00:29:57 +01:00
< / div >
2026-01-31 23:16:24 +01:00
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
<!-- Actions -->
< div >
< span class = "text-muted small d-block mb-2" > Handlinger< / span >
< div class = "d-flex flex-wrap gap-2" >
< button class = "btn btn-sm btn-outline-primary" data-bs-toggle = "modal" data-bs-target = "#ncCreateUserModal" >
< i class = "bi bi-person-plus me-1" > < / i > Opret bruger
< / button >
< button class = "btn btn-sm btn-outline-danger" data-bs-toggle = "modal" data-bs-target = "#ncDisableUserModal" >
< i class = "bi bi-person-lock me-1" > < / i > Luk bruger
< / button >
< button class = "btn btn-sm btn-outline-secondary" data-bs-toggle = "modal" data-bs-target = "#ncResetPasswordModal" >
< i class = "bi bi-key me-1" > < / i > Reset kode
< / button >
< button class = "btn btn-sm btn-outline-info" data-bs-toggle = "modal" data-bs-target = "#ncSendGuideModal" >
< i class = "bi bi-envelope me-1" > < / i > Send guide
< / button >
2026-02-01 11:58:44 +01:00
< / div >
< / div >
{% else %}
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< div class = "text-center py-2 text-muted" >
< i class = "bi bi-exclamation-triangle me-2 text-warning" > < / i >
Kunden mangler Nextcloud konfiguration
< / div >
2026-02-01 11:58:44 +01:00
{% endif %}
2026-01-31 23:16:24 +01:00
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / div >
{% endif %}
< div class = "card mb-3" >
< div class = "card-header d-flex justify-content-between align-items-center" >
< h6 class = "mb-0 text-primary" > < i class = "bi bi-lightbulb me-2" > < / i > Løsning< / h6 >
{% if not solution or request.query_params.get('edit_solution') %}
<!-- button to create/edit -->
{% endif %}
< / div >
< div class = "card-body" >
{% if solution %}
< div class = "mb-3" >
< label class = "small text-muted" > Titel< / label >
< div class = "fw-bold" > {{ solution.title }}< / div >
< / div >
< div class = "row mb-3" >
< div class = "col-md-6" >
< label class = "small text-muted" > Type< / label >
< div > < span class = "badge bg-secondary" > {{ solution.solution_type }}< / span > < / div >
< / div >
< div class = "col-md-6" >
< label class = "small text-muted" > Resultat< / label >
< div > < span class = "badge {{ 'bg-success' if solution.result == 'Løst' else 'bg-warning' }}" > {{ solution.result }}< / span > < / div >
< / div >
< / div >
< div >
< label class = "small text-muted" > Beskrivelse< / label >
< div class = "p-3 bg-light rounded" style = "white-space: pre-wrap;" > {{ solution.description }}< / div >
< / div >
{% else %}
< div class = "text-center py-5 text-muted" >
< i class = "bi bi-lightbulb display-4 mb-3 d-block opacity-25" > < / i >
< p > Ingen løsning registreret endnu.< / p >
< button class = "btn btn-primary" onclick = "showCreateSolutionModal()" >
< i class = "bi bi-plus-lg me-2" > < / i > Opret Løsning
< / button >
< / div >
{% endif %}
< / div >
< / div >
< / div >
<!-- Varekøb & Salg Tab -->
2026-02-06 10:47:14 +01:00
< div class = "tab-pane fade" id = "sales" role = "tabpanel" tabindex = "0" data-module = "sales" data-has-content = "unknown" >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< div class = "row g-3 mb-3" >
< div class = "col-lg-8" >
< div class = "card mb-3" >
< div class = "card-header d-flex justify-content-between align-items-center" >
< h6 class = "mb-0 text-primary" > < i class = "bi bi-bag-check me-2" > < / i > Salgslinjer< / h6 >
< span class = "badge bg-light text-dark border" id = "salesLinesSubtotal" > -< / span >
< button class = "btn btn-sm btn-outline-primary" onclick = "openSaleItemModal({ type: 'sale' })" >
< i class = "bi bi-plus-lg me-1" > < / i > Tilføj salgslinje
< / button >
< / div >
< div class = "card-body p-0" >
< div class = "table-responsive" >
< table class = "table table-hover mb-0" style = "vertical-align: middle;" >
< thead class = "bg-light" >
< tr >
< th class = "ps-4" > Dato< / th >
< th > Beskrivelse< / th >
< th > Antal< / th >
< th > Enhed< / th >
< th > Enhedspris< / th >
< th > Linjesum< / th >
< th > Kilde-sag< / th >
< th > Status< / th >
< th class = "text-end pe-4" > Handlinger< / th >
< / tr >
< / thead >
< tbody id = "saleItemsSalesBody" >
< tr >
< td colspan = "9" class = "text-center py-4 text-muted" > Indlæser salgslinjer...< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / div >
< / div >
< div class = "card" >
< div class = "card-header d-flex justify-content-between align-items-center" >
< h6 class = "mb-0 text-primary" > < i class = "bi bi-cart-x me-2" > < / i > Indkøbslinjer< / h6 >
< span class = "badge bg-light text-dark border" id = "purchaseLinesSubtotal" > -< / span >
< button class = "btn btn-sm btn-outline-primary" onclick = "openSaleItemModal({ type: 'purchase' })" >
< i class = "bi bi-plus-lg me-1" > < / i > Tilføj indkøbslinje
< / button >
< / div >
< div class = "card-body p-0" >
< div class = "table-responsive" >
< table class = "table table-hover mb-0" style = "vertical-align: middle;" >
< thead class = "bg-light" >
< tr >
< th class = "ps-4" > Dato< / th >
< th > Beskrivelse< / th >
< th > Antal< / th >
< th > Enhed< / th >
< th > Enhedspris< / th >
< th > Linjesum< / th >
< th > Kilde-sag< / th >
< th > Status< / th >
< th class = "text-end pe-4" > Handlinger< / th >
< / tr >
< / thead >
< tbody id = "saleItemsPurchaseBody" >
< tr >
< td colspan = "9" class = "text-center py-4 text-muted" > Indlæser indkøbslinjer...< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / div >
< / div >
< / div >
< div class = "col-lg-4" >
< div class = "card mb-3" >
< div class = "card-header" >
< h6 class = "mb-0 text-primary" > < i class = "bi bi-graph-up-arrow me-2" > < / i > Salg (samlet)< / h6 >
< / div >
< div class = "card-body" >
< div class = "d-flex justify-content-between align-items-center mb-2" >
< span class = "text-muted" > Total salg< / span >
< strong id = "salesTotalSale" > -< / strong >
< / div >
< div class = "d-flex justify-content-between align-items-center" >
< span class = "text-muted" > Netto< / span >
< strong id = "salesTotalNet" > -< / strong >
< / div >
< / div >
< / div >
< div class = "card mb-3" >
< div class = "card-header" >
< h6 class = "mb-0 text-primary" > < i class = "bi bi-bag-check me-2" > < / i > Indkøb (samlet)< / h6 >
< / div >
< div class = "card-body" >
< div class = "d-flex justify-content-between align-items-center" >
< span class = "text-muted" > Total køb< / span >
< strong id = "salesTotalPurchase" > -< / strong >
< / div >
< / div >
< / div >
< div class = "card mb-3" >
< div class = "card-header" >
< h6 class = "mb-0 text-primary" > < i class = "bi bi-clock-history me-2" > < / i > Tid (samlet)< / h6 >
< / div >
< div class = "card-body" >
< div class = "d-flex justify-content-between align-items-center mb-2" >
< span class = "text-muted" > Timer (total)< / span >
< strong id = "salesTotalHours" > -< / strong >
< / div >
< div class = "d-flex justify-content-between align-items-center" >
< span class = "text-muted" > Timer (fakturerbar)< / span >
< strong id = "salesBillableHours" > -< / strong >
< / div >
< div class = "small text-muted mt-3" >
Inkluderer alle under-sager
< / div >
< / div >
< / div >
< div class = "card" >
< div class = "card-header" >
< h6 class = "mb-0 text-primary" > < i class = "bi bi-clock-history me-2" > < / i > Tid (samlet)< / h6 >
< / div >
< div class = "card-body p-0" >
< div class = "table-responsive" >
< table class = "table table-hover mb-0" style = "vertical-align: middle;" >
< thead class = "bg-light" >
< tr >
< th class = "ps-3" > Dato< / th >
< th > Timer< / th >
< th > Kilde-sag< / th >
< / tr >
< / thead >
< tbody id = "salesTimeBody" >
< tr >
< td colspan = "3" class = "text-center py-4 text-muted" > Indlæser tid...< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / div >
2026-01-31 23:16:24 +01:00
< / div >
2026-01-29 23:07:33 +01:00
< / div >
2026-02-01 11:58:44 +01:00
< / div >
< / div >
2026-01-29 23:07:33 +01:00
2026-02-06 10:47:14 +01:00
<!-- Reminders Tab -->
< div class = "tab-pane fade" id = "reminders" role = "tabpanel" tabindex = "0" data-module = "reminders" data-has-content = "unknown" >
< div class = "row g-3" >
< div class = "col-lg-8" >
< div class = "card mb-3" >
< div class = "card-header d-flex justify-content-between align-items-center" >
< h6 class = "mb-0 text-primary" > < i class = "bi bi-bell me-2" > < / i > Reminders< / h6 >
< button class = "btn btn-sm btn-outline-primary" onclick = "openCreateReminderModal()" >
< i class = "bi bi-plus-lg me-1" > < / i > Opret reminder
< / button >
< / div >
< div class = "card-body p-0" >
< div class = "list-group list-group-flush" id = "remindersList" >
< div class = "p-4 text-center text-muted" > Indlæser reminders...< / div >
< / div >
< / div >
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / div >
2026-02-06 10:47:14 +01:00
< div class = "col-lg-4" >
< div class = "card mb-3" >
< div class = "card-header" >
< h6 class = "mb-0 text-primary" > < i class = "bi bi-sliders me-2" > < / i > Indstillinger< / h6 >
< / div >
< div class = "card-body" >
< p class = "text-muted small mb-2" > Reminders følger brugerens standardindstillinger (email, Mattermost og popup), medmindre du vælger at overskrive dem på reminderen.< / p >
< div class = "small text-muted" >
Tip: Brug "Status ændring" hvis reminderen skal trigges af status.
< / div >
< / div >
2026-01-29 23:07:33 +01:00
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / div >
< / div >
< / div >
2026-02-06 10:47:14 +01:00
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / div > <!-- End Tab Content -->
2026-02-06 10:47:14 +01:00
<!-- Create Reminder Modal -->
< div class = "modal fade" id = "createReminderModal" tabindex = "-1" aria-hidden = "true" >
< div class = "modal-dialog modal-lg" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Opret reminder< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< form id = "createReminderForm" >
< div class = "row g-3" >
< div class = "col-md-8" >
< label class = "form-label" > Titel *< / label >
< input type = "text" class = "form-control" id = "rem_title" required >
< / div >
< div class = "col-md-4" >
< label class = "form-label" > Prioritet< / label >
< select class = "form-select" id = "rem_priority" >
< option value = "low" > Lav< / option >
< option value = "normal" selected > Normal< / option >
< option value = "high" > Høj< / option >
< option value = "urgent" > Kritisk< / option >
< / select >
< / div >
< div class = "col-12" >
< label class = "form-label" > Besked< / label >
< textarea class = "form-control" id = "rem_message" rows = "3" > < / textarea >
< / div >
< div class = "col-md-6" >
< label class = "form-label" > Trigger type< / label >
< select class = "form-select" id = "rem_trigger_type" onchange = "updateReminderTriggerFields()" >
< option value = "time_based" selected > Tidspunkt< / option >
< option value = "status_change" > Status ændring< / option >
< / select >
< / div >
< div class = "col-md-6" id = "rem_trigger_time_wrap" >
< label class = "form-label" > Tidspunkt< / label >
< input type = "datetime-local" class = "form-control" id = "rem_scheduled_at" >
< / div >
< div class = "col-md-6 d-none" id = "rem_trigger_status_wrap" >
< label class = "form-label" > Status (target)< / label >
< select class = "form-select" id = "rem_target_status" >
< option value = "" > Vælg status< / option >
{% for status in status_options %}
< option value = "{{ status }}" > {{ status }}< / option >
{% endfor %}
< / select >
< / div >
< div class = "col-md-6" >
< label class = "form-label" > Gentagelse< / label >
< select class = "form-select" id = "rem_recurrence_type" onchange = "updateReminderRecurrenceFields()" >
< option value = "once" selected > Kun én gang< / option >
< option value = "daily" > Dagligt< / option >
< option value = "weekly" > Ugentligt< / option >
< option value = "monthly" > Månedligt< / option >
< / select >
< / div >
< div class = "col-md-6 d-none" id = "rem_recurrence_dow_wrap" >
< label class = "form-label" > Ugedag< / label >
< select class = "form-select" id = "rem_recurrence_dow" >
< option value = "0" > Mandag< / option >
< option value = "1" > Tirsdag< / option >
< option value = "2" > Onsdag< / option >
< option value = "3" > Torsdag< / option >
< option value = "4" > Fredag< / option >
< option value = "5" > Lørdag< / option >
< option value = "6" > Søndag< / option >
< / select >
< / div >
< div class = "col-md-6 d-none" id = "rem_recurrence_dom_wrap" >
< label class = "form-label" > Dag i måned< / label >
< input type = "number" class = "form-control" id = "rem_recurrence_dom" min = "1" max = "31" placeholder = "Fx 15" >
< / div >
< div class = "col-12" >
< label class = "form-label" > Kanaler< / label >
< div class = "d-flex flex-wrap gap-3" >
< div class = "form-check" >
< input class = "form-check-input" type = "checkbox" id = "rem_notify_frontend" checked >
< label class = "form-check-label" for = "rem_notify_frontend" > Popup< / label >
< / div >
< div class = "form-check" >
< input class = "form-check-input" type = "checkbox" id = "rem_notify_email" >
< label class = "form-check-label" for = "rem_notify_email" > Email< / label >
< / div >
< div class = "form-check" >
< input class = "form-check-input" type = "checkbox" id = "rem_notify_mattermost" >
< label class = "form-check-label" for = "rem_notify_mattermost" > Mattermost< / label >
< / div >
< / div >
< / div >
< div class = "col-12" >
< div class = "form-check" >
< input class = "form-check-input" type = "checkbox" id = "rem_override_prefs" >
< label class = "form-check-label" for = "rem_override_prefs" > Overskriv brugerens standardindstillinger< / label >
< / div >
< / div >
< div class = "col-12" >
< div class = "alert alert-warning small mb-0 d-none" id = "rem_user_warning" >
Mangler bruger-id. Log ind igen eller opdater siden.
< / div >
< / div >
< / div >
< / form >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Luk< / button >
< button type = "button" class = "btn btn-primary" onclick = "saveReminder()" > Gem reminder< / button >
< / div >
< / div >
< / div >
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
<!-- Global Comments Section (Visible on all tabs) -->
< div class = "row mb-4 mt-4" >
< div class = "col-12" >
< div class = "card" >
< div class = "card-header bg-light d-flex justify-content-between align-items-center" >
< h5 class = "mb-0" > < i class = "bi bi-chat-left-text me-2" > < / i > Kommentarer< / h5 >
< span class = "badge bg-secondary" > {{ comments|length if comments else 0 }}< / span >
2026-02-01 11:58:44 +01:00
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< div class = "card-body bg-light" style = "max-height: 500px; overflow-y: auto;" id = "comments-container" >
{% if comments %}
{% for comment in comments %}
< div class = "d-flex mb-3 {{ 'justify-content-end' if comment.forfatter == 'System' else '' }}" >
< div class = "card {{ 'border-info' if comment.forfatter == 'System' else '' }}" style = "max-width: 80%; width: fit-content;" >
< div class = "card-header py-1 px-3 small {{ 'bg-info text-white' if comment.forfatter == 'System' else 'bg-secondary text-white' }} d-flex justify-content-between align-items-center gap-3" >
< strong > {{ comment.forfatter }}< / strong >
< span > {{ comment.created_at.strftime('%d/%m-%Y %H:%M') }}< / span >
< / div >
< div class = "card-body py-2 px-3" >
{{ comment.indhold|replace('\n', '< br > ')|safe }}
< / div >
< / div >
< / div >
{% endfor %}
{% else %}
< p class = "text-center text-muted my-3" > Ingen kommentarer endnu.< / p >
{% endif %}
< / div >
< div class = "card-footer bg-white" >
< form id = "comment-form" onsubmit = "submitComment(event)" >
< div class = "input-group" >
< textarea class = "form-control" name = "indhold" required placeholder = "Skriv en kommentar..." rows = "2" style = "resize: none;" > < / textarea >
< button type = "submit" class = "btn btn-primary d-flex align-items-center" >
< i class = "bi bi-send me-2" > < / i > Send
< / button >
< / div >
< / form >
2026-01-29 23:07:33 +01:00
< / div >
< / div >
< / div >
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< script >
async function submitComment(event) {
event.preventDefault();
const form = event.target;
const content = form.indhold.value;
const btn = form.querySelector('button');
const originalText = btn.innerHTML;
btn.innerHTML = '< span class = "spinner-border spinner-border-sm me-2" > < / span > Sender...';
btn.disabled = true;
try {
const response = await fetch('/api/v1/sag/{{ case.id }}/kommentarer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
indhold: content,
forfatter: "Bruger"
})
});
if (response.ok) {
location.reload();
} else {
alert('Fejl ved oprettelse af kommentar');
btn.innerHTML = originalText;
btn.disabled = false;
}
} catch (error) {
console.error('Error:', error);
alert('Der skete en fejl. Prøv igen.');
btn.innerHTML = originalText;
btn.disabled = false;
}
}
// Scroll to bottom of comments
document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('comments-container');
if(container) {
container.scrollTop = container.scrollHeight;
}
});
< / script >
< script >
const salesCaseId = {{ case.id }};
function formatCurrency(value) {
const num = Number(value || 0);
return new Intl.NumberFormat('da-DK', { style: 'currency', currency: 'DKK' }).format(num);
}
function formatNumber(value) {
const num = Number(value || 0);
return new Intl.NumberFormat('da-DK', { minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(num);
}
let saleItemsCache = [];
async function loadVarekobSalg() {
try {
const res = await fetch(`/api/v1/sag/${salesCaseId}/varekob-salg?include_subcases=true`);
if (!res.ok) throw new Error('Failed to load aggregated data');
const data = await res.json();
document.getElementById('salesTotalPurchase').textContent = formatCurrency(data?.totals?.purchase_total);
document.getElementById('salesTotalSale').textContent = formatCurrency(data?.totals?.sale_total);
document.getElementById('salesTotalNet').textContent = formatCurrency(data?.totals?.net_total);
document.getElementById('salesTotalHours').textContent = formatNumber(data?.totals?.total_hours) + ' t';
document.getElementById('salesBillableHours').textContent = formatNumber(data?.totals?.billable_hours) + ' t';
saleItemsCache = data.sale_items || [];
renderSaleItems(saleItemsCache);
renderTimeEntries(data.time_entries || []);
} catch (error) {
console.error(error);
const saleBody = document.getElementById('saleItemsBody');
if (saleBody) {
saleBody.innerHTML = '< tr > < td colspan = "10" class = "text-center py-4 text-muted" > Kunne ikke hente data< / td > < / tr > ';
}
const timeBody = document.getElementById('salesTimeBody');
if (timeBody) {
timeBody.innerHTML = '< tr > < td colspan = "3" class = "text-center py-4 text-muted" > Kunne ikke hente data< / td > < / tr > ';
}
}
}
function renderSaleItems(items) {
const salesBody = document.getElementById('saleItemsSalesBody');
const purchaseBody = document.getElementById('saleItemsPurchaseBody');
const salesSubtotal = document.getElementById('salesLinesSubtotal');
const purchaseSubtotal = document.getElementById('purchaseLinesSubtotal');
if (!salesBody || !purchaseBody) return;
const salesItems = items.filter(item => (item.type || '').toLowerCase() !== 'purchase');
const purchaseItems = items.filter(item => (item.type || '').toLowerCase() === 'purchase');
const renderRows = (list) => {
if (!list.length) {
return '< tr > < td colspan = "9" class = "text-center py-4 text-muted" > Ingen linjer< / td > < / tr > ';
}
return list.map(item => {
const statusLabel = item.status || 'draft';
const isSubcase = item.sag_id & & item.sag_id !== salesCaseId;
const sourceBadge = isSubcase
? `< span class = "badge bg-warning text-dark ms-2" > Under-sag< / span > `
: `< span class = "badge bg-light text-dark border ms-2" > Denne sag< / span > `;
return `
< tr >
< td class = "ps-4" > ${item.line_date || '-'}< / td >
< td > ${item.description || '-'}< / td >
< td > ${item.quantity ?? '-'}< / td >
< td > ${item.unit || '-'}< / td >
< td > ${item.unit_price != null ? formatCurrency(item.unit_price) : '-'}< / td >
< td class = "fw-bold" > ${formatCurrency(item.amount)}< / td >
< td > ${item.source_sag_titel || '-'}${sourceBadge}< / td >
< td > < span class = "badge bg-light text-dark border" > ${statusLabel}< / span > < / td >
< td class = "text-end pe-4" >
< div class = "btn-group btn-group-sm" role = "group" >
< button class = "btn btn-outline-secondary" onclick = 'openSaleItemModalById(${item.id})' > < i class = "bi bi-pencil" > < / i > < / button >
< button class = "btn btn-outline-danger" onclick = 'deleteSaleItem(${item.id})' > < i class = "bi bi-trash" > < / i > < / button >
< / div >
< / td >
< / tr >
`;
}).join('');
};
salesBody.innerHTML = renderRows(salesItems);
purchaseBody.innerHTML = renderRows(purchaseItems);
const salesSum = salesItems.reduce((sum, item) => sum + Number(item.amount || 0), 0);
const purchaseSum = purchaseItems.reduce((sum, item) => sum + Number(item.amount || 0), 0);
if (salesSubtotal) salesSubtotal.textContent = formatCurrency(salesSum);
if (purchaseSubtotal) purchaseSubtotal.textContent = formatCurrency(purchaseSum);
}
function renderTimeEntries(entries) {
const tbody = document.getElementById('salesTimeBody');
if (!tbody) return;
if (!entries.length) {
tbody.innerHTML = '< tr > < td colspan = "3" class = "text-center py-4 text-muted" > Ingen tid registreret< / td > < / tr > ';
return;
}
tbody.innerHTML = entries.map(entry => {
const hours = entry.approved_hours || entry.original_hours || 0;
const isSubcase = entry.sag_id & & entry.sag_id !== salesCaseId;
const sourceBadge = isSubcase
? `< span class = "badge bg-warning text-dark ms-2" > Under-sag< / span > `
: `< span class = "badge bg-light text-dark border ms-2" > Denne sag< / span > `;
return `
< tr >
< td class = "ps-3" > ${entry.worked_date || '-'}< / td >
< td > ${formatNumber(hours)} t< / td >
< td > ${entry.source_sag_titel || '-'}${sourceBadge}< / td >
< / tr >
`;
}).join('');
}
function openSaleItemModal(item = null) {
document.getElementById('sale_item_id').value = item?.id || '';
document.getElementById('sale_type').value = item?.type || 'sale';
document.getElementById('sale_status').value = item?.status || 'draft';
document.getElementById('sale_date').value = item?.line_date || '';
document.getElementById('sale_description').value = item?.description || '';
document.getElementById('sale_quantity').value = item?.quantity ?? '';
document.getElementById('sale_unit').value = item?.unit || '';
document.getElementById('sale_unit_price').value = item?.unit_price ?? '';
document.getElementById('sale_amount').value = item?.amount ?? '';
document.getElementById('sale_currency').value = item?.currency || 'DKK';
document.getElementById('sale_external_ref').value = item?.external_ref || '';
new bootstrap.Modal(document.getElementById('saleItemModal')).show();
}
function openSaleItemModalById(itemId) {
const item = saleItemsCache.find((entry) => entry.id === itemId);
openSaleItemModal(item || null);
}
function updateSaleAmount() {
const qty = parseFloat(document.getElementById('sale_quantity').value || 0);
const price = parseFloat(document.getElementById('sale_unit_price').value || 0);
if (qty & & price) {
document.getElementById('sale_amount').value = (qty * price).toFixed(2);
}
}
2026-02-06 10:47:14 +01:00
async function saveSaleItem() {
const itemId = document.getElementById('sale_item_id').value;
const payload = {
type: document.getElementById('sale_type').value,
status: document.getElementById('sale_status').value,
line_date: document.getElementById('sale_date').value || null,
description: document.getElementById('sale_description').value,
quantity: document.getElementById('sale_quantity').value || null,
unit: document.getElementById('sale_unit').value || null,
unit_price: document.getElementById('sale_unit_price').value || null,
amount: document.getElementById('sale_amount').value,
currency: document.getElementById('sale_currency').value || 'DKK',
external_ref: document.getElementById('sale_external_ref').value || null
};
if (!payload.description || !payload.amount) {
alert('Beskrivelse og linjesum er påkrævet.');
return;
}
const method = itemId ? 'PATCH' : 'POST';
const url = itemId
? `/api/v1/sag/${salesCaseId}/sale-items/${itemId}`
: `/api/v1/sag/${salesCaseId}/sale-items`;
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) {
alert('Kunne ikke gemme varelinje');
return;
}
bootstrap.Modal.getInstance(document.getElementById('saleItemModal')).hide();
await loadVarekobSalg();
}
async function deleteSaleItem(itemId) {
if (!confirm('Vil du slette denne varelinje?')) return;
const res = await fetch(`/api/v1/sag/${salesCaseId}/sale-items/${itemId}`, { method: 'DELETE' });
if (!res.ok) {
alert('Kunne ikke slette varelinje');
return;
}
await loadVarekobSalg();
}
document.addEventListener('DOMContentLoaded', function() {
const qtyInput = document.getElementById('sale_quantity');
const priceInput = document.getElementById('sale_unit_price');
if (qtyInput) qtyInput.addEventListener('input', updateSaleAmount);
if (priceInput) priceInput.addEventListener('input', updateSaleAmount);
loadVarekobSalg();
});
< / script >
< script >
let reminderUserId = null;
function getReminderUserId() {
const token = localStorage.getItem('access_token') || sessionStorage.getItem('access_token');
if (token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.sub || payload.user_id;
} catch (e) {
console.warn('Could not decode token for reminder user_id');
}
}
const metaTag = document.querySelector('meta[name="user-id"]');
if (metaTag) return metaTag.getAttribute('content');
return null;
}
function formatReminderDate(value) {
if (!value) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '-';
return date.toLocaleString('da-DK');
}
function updateReminderTriggerFields() {
const triggerType = document.getElementById('rem_trigger_type')?.value;
const timeWrap = document.getElementById('rem_trigger_time_wrap');
const statusWrap = document.getElementById('rem_trigger_status_wrap');
if (timeWrap & & statusWrap) {
if (triggerType === 'status_change') {
timeWrap.classList.add('d-none');
statusWrap.classList.remove('d-none');
} else {
timeWrap.classList.remove('d-none');
statusWrap.classList.add('d-none');
}
}
}
function updateReminderRecurrenceFields() {
const recurrenceType = document.getElementById('rem_recurrence_type')?.value;
const dowWrap = document.getElementById('rem_recurrence_dow_wrap');
const domWrap = document.getElementById('rem_recurrence_dom_wrap');
if (!dowWrap || !domWrap) return;
dowWrap.classList.toggle('d-none', recurrenceType !== 'weekly');
domWrap.classList.toggle('d-none', recurrenceType !== 'monthly');
}
function openCreateReminderModal() {
reminderUserId = getReminderUserId();
const warning = document.getElementById('rem_user_warning');
if (warning) warning.classList.toggle('d-none', !!reminderUserId);
const form = document.getElementById('createReminderForm');
if (form) form.reset();
document.getElementById('rem_notify_frontend').checked = true;
document.getElementById('rem_priority').value = 'normal';
document.getElementById('rem_trigger_type').value = 'time_based';
document.getElementById('rem_recurrence_type').value = 'once';
updateReminderTriggerFields();
updateReminderRecurrenceFields();
new bootstrap.Modal(document.getElementById('createReminderModal')).show();
}
async function loadReminders() {
const list = document.getElementById('remindersList');
if (!list) return;
reminderUserId = getReminderUserId();
if (!reminderUserId) {
list.innerHTML = '< div class = "p-4 text-center text-muted" > Kunne ikke finde bruger-id.< / div > ';
return;
}
list.innerHTML = '< div class = "p-4 text-center text-muted" > < span class = "spinner-border spinner-border-sm" > < / span > Henter reminders...< / div > ';
try {
const res = await fetch(`/api/v1/sag/${caseIds}/reminders?user_id=${reminderUserId}`);
if (!res.ok) throw new Error('Kunne ikke hente reminders');
const reminders = await res.json();
renderReminders(reminders);
} catch (e) {
console.error(e);
list.innerHTML = '< div class = "p-4 text-center text-danger" > Fejl ved hentning af reminders< / div > ';
}
}
function renderReminders(reminders) {
const list = document.getElementById('remindersList');
if (!list) return;
if (!reminders || reminders.length === 0) {
list.innerHTML = '< div class = "p-4 text-center text-muted" > Ingen reminders endnu.< / div > ';
return;
}
const triggerLabels = {
time_based: 'Tidspunkt',
status_change: 'Status ændring',
deadline_approaching: 'Deadline'
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
};
2026-02-06 10:47:14 +01:00
const recurrenceLabels = {
once: 'Én gang',
daily: 'Dagligt',
weekly: 'Ugentligt',
monthly: 'Månedligt'
};
list.innerHTML = reminders.map(reminder => {
const nextCheck = formatReminderDate(reminder.next_check_at);
const createdAt = formatReminderDate(reminder.created_at);
const isActive = reminder.is_active;
const statusBadge = isActive
? '< span class = "badge bg-success" > Aktiv< / span > '
: '< span class = "badge bg-secondary" > Inaktiv< / span > ';
return `
< div class = "list-group-item" >
< div class = "d-flex justify-content-between align-items-start" >
< div class = "me-3" >
< div class = "fw-bold" > ${reminder.title}< / div >
< div class = "text-muted small" > ${reminder.message || '-'} < / div >
< div class = "small text-muted mt-1" >
Trigger: ${triggerLabels[reminder.trigger_type] || reminder.trigger_type} · Gentagelse: ${recurrenceLabels[reminder.recurrence_type] || reminder.recurrence_type}
< / div >
< div class = "small text-muted" > Næste: ${nextCheck} · Oprettet: ${createdAt}< / div >
< / div >
< div class = "d-flex flex-column align-items-end gap-2" >
${statusBadge}
< button class = "btn btn-sm btn-outline-danger" onclick = "deleteReminder(${reminder.id})" >
< i class = "bi bi-trash" > < / i >
< / button >
< / div >
< / div >
< / div >
`;
}).join('');
}
async function saveReminder() {
reminderUserId = getReminderUserId();
if (!reminderUserId) {
alert('Mangler bruger-id. Log ind igen.');
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
return;
}
2026-02-06 10:47:14 +01:00
const title = document.getElementById('rem_title').value.trim();
const message = document.getElementById('rem_message').value.trim();
const priority = document.getElementById('rem_priority').value;
const triggerType = document.getElementById('rem_trigger_type').value;
const scheduledAtValue = document.getElementById('rem_scheduled_at').value;
const targetStatus = document.getElementById('rem_target_status').value;
const recurrenceType = document.getElementById('rem_recurrence_type').value;
const recurrenceDow = document.getElementById('rem_recurrence_dow').value;
const recurrenceDom = document.getElementById('rem_recurrence_dom').value;
const notifyFrontend = document.getElementById('rem_notify_frontend').checked;
const notifyEmail = document.getElementById('rem_notify_email').checked;
const notifyMattermost = document.getElementById('rem_notify_mattermost').checked;
const overridePrefs = document.getElementById('rem_override_prefs').checked;
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
2026-02-06 10:47:14 +01:00
if (!title) {
alert('Titel er påkrævet');
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
return;
}
2026-02-06 10:47:14 +01:00
let triggerConfig = {};
let scheduledAt = null;
if (triggerType === 'status_change') {
if (!targetStatus) {
alert('Vælg en status for statusændring');
return;
}
triggerConfig = { target_status: targetStatus };
} else {
if (!scheduledAtValue) {
alert('Vælg et tidspunkt');
return;
}
scheduledAt = new Date(scheduledAtValue).toISOString();
}
const payload = {
title,
message: message || null,
priority,
trigger_type: triggerType,
trigger_config: triggerConfig,
recipient_user_ids: [Number(reminderUserId)],
recipient_emails: [],
notify_mattermost: notifyMattermost,
notify_email: notifyEmail,
notify_frontend: notifyFrontend,
override_user_preferences: overridePrefs,
recurrence_type: recurrenceType,
recurrence_day_of_week: recurrenceType === 'weekly' ? Number(recurrenceDow) : null,
recurrence_day_of_month: recurrenceType === 'monthly' ? Number(recurrenceDom) : null,
scheduled_at: scheduledAt
};
try {
const res = await fetch(`/api/v1/sag/${caseIds}/reminders?user_id=${reminderUserId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail || 'Kunne ikke oprette reminder');
}
bootstrap.Modal.getInstance(document.getElementById('createReminderModal')).hide();
await loadReminders();
} catch (e) {
alert('Fejl: ' + e.message);
}
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
}
2026-02-06 10:47:14 +01:00
async function deleteReminder(reminderId) {
if (!confirm('Vil du slette denne reminder?')) return;
try {
const res = await fetch(`/api/v1/sag/reminders/${reminderId}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Kunne ikke slette reminder');
await loadReminders();
} catch (e) {
alert('Fejl: ' + e.message);
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
}
}
document.addEventListener('DOMContentLoaded', function() {
2026-02-06 10:47:14 +01:00
updateReminderTriggerFields();
updateReminderRecurrenceFields();
loadReminders();
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
});
< / script >
<!-- Modals for Solution (Inserted here) -->
< div class = "modal fade" id = "createSolutionModal" tabindex = "-1" aria-hidden = "true" >
< div class = "modal-dialog modal-lg" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Opret Løsning< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< form id = "solutionForm" >
< input type = "hidden" id = "sol_sag_id" value = "{{ case.id }}" >
< div class = "mb-3" >
< label class = "form-label" > Titel *< / label >
< input type = "text" class = "form-control" id = "sol_title" required >
< / div >
< div class = "row mb-3" >
< div class = "col-md-6" >
< label class = "form-label" > Type< / label >
< select class = "form-select" id = "sol_type" >
< option value = "Support" > Support< / option >
< option value = "Drift" > Drift< / option >
< option value = "Konsulent" > Konsulent< / option >
< option value = "Infrastruktur" > Infrastruktur< / option >
< / select >
< / div >
< div class = "col-md-6" >
< label class = "form-label" > Resultat< / label >
< select class = "form-select" id = "sol_result" >
< option value = "Løst" > Løst< / option >
< option value = "Delvist" > Delvist< / option >
< option value = "Workaround" > Workaround< / option >
< option value = "Ej løst" > Ej løst< / option >
< / select >
< / div >
< / div >
< div class = "mb-3" >
< label class = "form-label" > Beskrivelse< / label >
< textarea class = "form-control" id = "sol_desc" rows = "5" > < / textarea >
< / div >
< div class = "border-top pt-3" >
< div class = "form-check mb-3" >
< input class = "form-check-input" type = "checkbox" id = "sol_add_time" >
< label class = "form-check-label" for = "sol_add_time" >
Registrer tid med det samme
< / label >
< / div >
< div id = "sol_time_fields" class = "row g-3 d-none" >
< div class = "col-md-4" >
< label class = "form-label" > Dato< / label >
< input type = "date" class = "form-control" id = "sol_time_date" >
< / div >
< div class = "col-md-4" >
< label class = "form-label" > Tid brugt< / label >
< div class = "input-group" >
< input type = "number" class = "form-control" id = "sol_time_hours" min = "0" placeholder = "tt" step = "1" >
< span class = "input-group-text" > :< / span >
< input type = "number" class = "form-control" id = "sol_time_minutes" min = "0" placeholder = "mm" step = "1" >
< / div >
< div class = "form-text" id = "sol_time_total" > Total: 0.00 timer< / div >
< / div >
< div class = "col-md-4" >
< label class = "form-label" > Beskrivelse< / label >
< input type = "text" class = "form-control" id = "sol_time_desc" placeholder = "F.eks. afsluttede løsning" >
< / div >
< div class = "col-12" >
< div class = "form-check" >
< input class = "form-check-input" type = "checkbox" id = "sol_time_internal" >
< label class = "form-check-label text-muted" for = "sol_time_internal" >
Skjul for kunde (intern registrering)
< / label >
< / div >
< / div >
< / div >
< / div >
< / form >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Luk< / button >
< button type = "button" class = "btn btn-primary" onclick = "saveSolution()" > Gem Løsning< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Modal for Sale Item -->
< div class = "modal fade" id = "saleItemModal" tabindex = "-1" aria-hidden = "true" >
< div class = "modal-dialog modal-lg" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > < i class = "bi bi-basket3" > < / i > Varelinje< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< form id = "saleItemForm" >
< input type = "hidden" id = "sale_item_id" >
< div class = "row g-3" >
< div class = "col-md-4" >
< label class = "form-label" > Type *< / label >
< select class = "form-select" id = "sale_type" >
< option value = "sale" > Salg< / option >
< option value = "purchase" > Køb< / option >
< / select >
< / div >
< div class = "col-md-4" >
< label class = "form-label" > Status *< / label >
< select class = "form-select" id = "sale_status" >
< option value = "draft" > Kladde< / option >
< option value = "confirmed" > Bekræftet< / option >
< option value = "cancelled" > Annulleret< / option >
< / select >
< / div >
< div class = "col-md-4" >
< label class = "form-label" > Dato< / label >
< input type = "date" class = "form-control" id = "sale_date" >
< / div >
< div class = "col-12" >
< label class = "form-label" > Beskrivelse *< / label >
< input type = "text" class = "form-control" id = "sale_description" placeholder = "F.eks. Switch, montage, kørsel" >
< / div >
< div class = "col-md-3" >
< label class = "form-label" > Antal< / label >
< input type = "number" class = "form-control" id = "sale_quantity" step = "0.01" min = "0" >
< / div >
< div class = "col-md-3" >
< label class = "form-label" > Enhed< / label >
< input type = "text" class = "form-control" id = "sale_unit" placeholder = "stk, timer" >
< / div >
< div class = "col-md-3" >
< label class = "form-label" > Enhedspris< / label >
< input type = "number" class = "form-control" id = "sale_unit_price" step = "0.01" min = "0" >
< / div >
< div class = "col-md-3" >
< label class = "form-label" > Linjesum *< / label >
< input type = "number" class = "form-control" id = "sale_amount" step = "0.01" min = "0" >
< / div >
< div class = "col-md-4" >
< label class = "form-label" > Valuta< / label >
< input type = "text" class = "form-control" id = "sale_currency" value = "DKK" >
< / div >
< div class = "col-md-8" >
< label class = "form-label" > Reference< / label >
< input type = "text" class = "form-control" id = "sale_external_ref" placeholder = "Valgfri reference" >
< / div >
< / div >
< / form >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Luk< / button >
< button type = "button" class = "btn btn-primary" onclick = "saveSaleItem()" > Gem varelinje< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Modal for Internal Time -->
2026-02-08 01:45:00 +01:00
< div class = "modal fade" id = "createTimeModal" tabindex = "-1" aria-hidden = "true" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > < i class = "bi bi-clock-history" > < / i > Registrer Tid< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< form id = "timeForm" >
< input type = "hidden" id = "time_sag_id" value = "{{ case.id }}" >
< div class = "row g-3" >
< div class = "col-6" >
< label class = "form-label" > Dato *< / label >
< input type = "date" class = "form-control" id = "time_date" required >
< / div >
< div class = "col-6" >
< label class = "form-label" > Tid brugt *< / label >
< div class = "input-group" >
< input type = "number" class = "form-control" id = "time_hours_input" min = "0" placeholder = "tt" step = "1" >
< span class = "input-group-text" > :< / span >
< input type = "number" class = "form-control" id = "time_minutes_input" min = "0" max = "59" placeholder = "mm" step = "1" >
< / div >
< div class = "form-text text-end" id = "timeTotalCalc" > Total: 0.00 timer< / div >
< / div >
< div class = "col-6" >
< label class = "form-label" > Type< / label >
< select class = "form-select" id = "time_work_type" >
< option value = "support" selected > Support< / option >
< option value = "troubleshooting" > Fejlsøgning< / option >
< option value = "development" > Udvikling< / option >
< option value = "on_site" > Kørsel / On-site< / option >
< option value = "meeting" > Møde< / option >
< option value = "other" > Andet< / option >
< / select >
< / div >
< div class = "col-6" >
< label class = "form-label" > Afregning< / label >
< select class = "form-select" id = "time_billing_method" >
< option value = "invoice" selected > Faktura< / option >
{% if prepaid_cards %}
< optgroup label = "Klippekort" >
{% for card in prepaid_cards %}
< option value = "card_{{ card.id }}" > 💳 Klippekort #{{ card.card_number or card.id }} ({{ '%.2f' % card.remaining_hours }}t tilbage{% if card.expires_at %} • Udløber {{ card.expires_at }}{% endif %})< / option >
{% endfor %}
< / optgroup >
{% endif %}
{% if fixed_price_agreements %}
< optgroup label = "Fastpris Aftaler" >
{% for agr in fixed_price_agreements %}
< option value = "fpa_{{ agr.id }}" > 📋 Fastpris #{{ agr.agreement_number }} ({{ '%.1f' % agr.remaining_hours_this_month }}t tilbage / {{ '%.0f' % agr.monthly_hours }}t/måned)< / option >
{% endfor %}
< / optgroup >
{% endif %}
< option value = "internal" > Internt / Ingen faktura< / option >
< option value = "warranty" > Garanti / Reklamation< / option >
< / select >
< / div >
< div class = "col-12" >
< label class = "form-label" > Beskrivelse< / label >
< textarea class = "form-control" id = "time_desc" rows = "3" placeholder = "Hvad er der brugt tid på?" > < / textarea >
< / div >
< div class = "col-12" >
< div class = "form-check form-switch" >
< input class = "form-check-input" type = "checkbox" id = "time_internal" >
< label class = "form-check-label text-muted" for = "time_internal" >
Skjul for kunde (Intern registrering)
< / label >
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / div >
< / div >
< / form >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Luk< / button >
< button type = "button" class = "btn btn-primary" onclick = "saveTime()" >
< i class = "bi bi-save" > < / i > Gem Tid
< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Script for Solution/Time -->
< script >
function showCreateSolutionModal() {
const addTimeCheckbox = document.getElementById('sol_add_time');
const timeFields = document.getElementById('sol_time_fields');
if (addTimeCheckbox & & timeFields) {
addTimeCheckbox.checked = false;
timeFields.classList.add('d-none');
}
const timeDate = document.getElementById('sol_time_date');
if (timeDate) timeDate.valueAsDate = new Date();
const timeHours = document.getElementById('sol_time_hours');
const timeMinutes = document.getElementById('sol_time_minutes');
const timeTotal = document.getElementById('sol_time_total');
if (timeHours) timeHours.value = '';
if (timeMinutes) timeMinutes.value = '';
if (timeTotal) timeTotal.textContent = 'Total: 0.00 timer';
const timeDesc = document.getElementById('sol_time_desc');
if (timeDesc) timeDesc.value = '';
const timeInternal = document.getElementById('sol_time_internal');
if (timeInternal) timeInternal.checked = false;
new bootstrap.Modal(document.getElementById('createSolutionModal')).show();
}
function updateSolutionTimeTotal() {
const h = parseInt(document.getElementById('sol_time_hours').value) || 0;
const m = parseInt(document.getElementById('sol_time_minutes').value) || 0;
const total = h + (m / 60);
const output = document.getElementById('sol_time_total');
if (output) output.textContent = `Total: ${total.toFixed(2)} timer`;
}
async function saveSolution() {
const data = {
sag_id: document.getElementById('sol_sag_id').value,
title: document.getElementById('sol_title').value,
solution_type: document.getElementById('sol_type').value,
result: document.getElementById('sol_result').value,
description: document.getElementById('sol_desc').value,
created_by_user_id: 1 // TODO: Get from auth
};
const addTime = document.getElementById('sol_add_time')?.checked;
const timeHours = parseInt(document.getElementById('sol_time_hours').value) || 0;
const timeMinutes = parseInt(document.getElementById('sol_time_minutes').value) || 0;
const timeTotal = timeHours + (timeMinutes / 60);
try {
const res = await fetch(`/api/v1/sag/${data.sag_id}/solution`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (res.ok) {
if (addTime & & timeTotal > 0) {
const solution = await res.json();
const timePayload = {
sag_id: data.sag_id,
solution_id: solution.id,
description: document.getElementById('sol_time_desc').value || data.title,
original_hours: timeTotal,
worked_date: document.getElementById('sol_time_date').value || null,
is_internal: document.getElementById('sol_time_internal').checked,
work_type: 'support'
};
const timeRes = await fetch('/api/v1/timetracking/entries/internal', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(timePayload)
});
if (!timeRes.ok) {
alert('Løsning oprettet, men tid kunne ikke registreres');
}
}
window.location.reload();
} else {
alert('Fejl ved oprettelse af løsning');
}
} catch(e) { console.error(e); alert('Fejl'); }
}
function showAddTimeModal() {
// Set date to today
document.getElementById('time_date').valueAsDate = new Date();
// Reset fields
if(document.getElementById('time_hours_input')) {
document.getElementById('time_hours_input').value = '';
document.getElementById('time_minutes_input').value = '';
document.getElementById('timeTotalCalc').textContent = 'Total: 0.00 timer';
}
document.getElementById('time_desc').value = '';
if(document.getElementById('time_internal')) document.getElementById('time_internal').checked = false;
if(document.getElementById('time_billing_method')) document.getElementById('time_billing_method').value = 'invoice';
if(document.getElementById('time_work_type')) document.getElementById('time_work_type').value = 'support';
new bootstrap.Modal(document.getElementById('createTimeModal')).show();
}
// Auto-calculate total hours
function updateTimeTotal() {
const h = parseInt(document.getElementById('time_hours_input').value) || 0;
const m = parseInt(document.getElementById('time_minutes_input').value) || 0;
const total = h + (m / 60);
if(document.getElementById('timeTotalCalc')) {
document.getElementById('timeTotalCalc').textContent = `Total: ${total.toFixed(2)} timer`;
}
}
// Add listeners safely
document.addEventListener('DOMContentLoaded', () => {
const hInput = document.getElementById('time_hours_input');
const mInput = document.getElementById('time_minutes_input');
if(hInput) hInput.addEventListener('input', updateTimeTotal);
if(mInput) mInput.addEventListener('input', updateTimeTotal);
const solAddTime = document.getElementById('sol_add_time');
const solFields = document.getElementById('sol_time_fields');
if (solAddTime & & solFields) {
solAddTime.addEventListener('change', () => {
solFields.classList.toggle('d-none', !solAddTime.checked);
});
}
const solHours = document.getElementById('sol_time_hours');
const solMinutes = document.getElementById('sol_time_minutes');
if (solHours) solHours.addEventListener('input', updateSolutionTimeTotal);
if (solMinutes) solMinutes.addEventListener('input', updateSolutionTimeTotal);
});
async function saveTime() {
let totalHours = 0;
// Check if we are using the new split inputs
const hInput = document.getElementById('time_hours_input');
if (hInput) {
const h = parseInt(hInput.value) || 0;
const m = parseInt(document.getElementById('time_minutes_input').value) || 0;
totalHours = h + (m/60);
} else {
// Fallback to old input if modal replacement didn't work (shouldn't happen)
totalHours = parseFloat(document.getElementById('time_hours').value) || 0;
}
if (totalHours < = 0) { alert('Indtast tid'); return; }
const billingSelect = document.getElementById('time_billing_method');
let billingMethod = billingSelect ? billingSelect.value : 'invoice';
let prepaidCardId = null;
2026-02-08 01:45:00 +01:00
let fixedPriceAgreementId = null;
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
// Handle prepaid card selection formatting (card_123)
if (billingMethod.startsWith('card_')) {
prepaidCardId = parseInt(billingMethod.split('_')[1]);
billingMethod = 'prepaid';
}
2026-02-08 01:45:00 +01:00
// Handle fixed-price agreement selection formatting (fpa_123)
if (billingMethod.startsWith('fpa_')) {
fixedPriceAgreementId = parseInt(billingMethod.split('_')[1]);
billingMethod = 'fixed_price';
}
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
const workTypeSelect = document.getElementById('time_work_type');
const internalCheck = document.getElementById('time_internal');
const data = {
sag_id: parseInt(document.getElementById('time_sag_id').value),
original_hours: totalHours,
description: document.getElementById('time_desc').value,
worked_date: document.getElementById('time_date').value,
work_type: workTypeSelect ? workTypeSelect.value : 'support',
billing_method: billingMethod,
is_internal: internalCheck ? internalCheck.checked : false
};
if (prepaidCardId) {
data.prepaid_card_id = prepaidCardId;
}
2026-02-08 01:45:00 +01:00
if (fixedPriceAgreementId) {
data.fixed_price_agreement_id = fixedPriceAgreementId;
}
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
try {
const res = await fetch(`/api/v1/timetracking/entries/internal`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (res.ok) {
window.location.reload();
} else {
const txt = await res.text();
alert('Fejl: ' + txt);
}
} catch(e) { console.error(e); alert('Fejl'); }
}
< / script >
<!-- Kontakt Info Modal -->
< div class = "modal fade" id = "kontaktModal" tabindex = "-1" aria-hidden = "true" >
< div class = "modal-dialog modal-dialog-centered" >
< div class = "modal-content" >
< div class = "modal-header" style = "background: var(--accent); color: white;" >
< h5 class = "modal-title" >
< i class = "bi bi-person-circle me-2" > < / i > Kontakt Information
< / h5 >
< button type = "button" class = "btn-close btn-close-white" data-bs-dismiss = "modal" aria-label = "Close" > < / button >
< / div >
< div class = "modal-body" >
{% if hovedkontakt %}
< div class = "mb-3" >
< label class = "small text-muted mb-1" > Navn< / label >
< div class = "fw-bold" > {{ hovedkontakt.first_name }} {{ hovedkontakt.last_name }}< / div >
< / div >
< div class = "mb-3" >
< label class = "small text-muted mb-1" > Email< / label >
< div >
{% if hovedkontakt.email %}
< a href = "mailto:{{ hovedkontakt.email }}" style = "color: var(--accent);" >
< i class = "bi bi-envelope me-1" > < / i > {{ hovedkontakt.email }}
< / a >
{% else %}
< span class = "text-muted" > Ingen email< / span >
{% endif %}
< / div >
< / div >
< div class = "mb-3" >
< label class = "small text-muted mb-1" > Telefon< / label >
< div >
{% if hovedkontakt.phone %}
< a href = "tel:{{ hovedkontakt.phone }}" style = "color: var(--accent);" >
< i class = "bi bi-telephone me-1" > < / i > {{ hovedkontakt.phone }}
< / a >
{% else %}
< span class = "text-muted" > Ingen telefon< / span >
{% endif %}
< / div >
< / div >
< div class = "mb-3" >
< label class = "small text-muted mb-1" > Mobil< / label >
< div >
{% if hovedkontakt.mobile %}
< a href = "tel:{{ hovedkontakt.mobile }}" style = "color: var(--accent);" >
< i class = "bi bi-phone me-1" > < / i > {{ hovedkontakt.mobile }}
< / a >
{% else %}
< span class = "text-muted" > Ingen mobil< / span >
{% endif %}
< / div >
< / div >
{% if hovedkontakt.title %}
< div class = "mb-3" >
< label class = "small text-muted mb-1" > Titel< / label >
< div > {{ hovedkontakt.title }}< / div >
< / div >
{% endif %}
{% else %}
< p class = "text-center text-muted mb-0" > Ingen kontakt tilknyttet< / p >
{% endif %}
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Luk< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Afdeling Modal -->
< div class = "modal fade" id = "afdelingModal" tabindex = "-1" aria-hidden = "true" >
< div class = "modal-dialog modal-dialog-centered" >
< div class = "modal-content" >
< div class = "modal-header" style = "background: var(--accent); color: white;" >
< h5 class = "modal-title" >
< i class = "bi bi-building me-2" > < / i > Rediger Afdeling
< / h5 >
< button type = "button" class = "btn-close btn-close-white" data-bs-dismiss = "modal" aria-label = "Close" > < / button >
< / div >
< div class = "modal-body" >
< label for = "afdelingInput" class = "form-label" > Afdeling< / label >
< input type = "text"
class="form-control"
id="afdelingInput"
value="{{ customer.department if customer and customer.department else '' }}"
placeholder="Indtast afdeling">
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Annuller< / button >
< button type = "button" class = "btn btn-primary" style = "background: var(--accent); border: none;" onclick = "updateAfdeling()" >
Gem
< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Nextcloud Modals -->
{% if nextcloud_instance %}
<!-- Create User Modal -->
< div class = "modal fade" id = "ncCreateUserModal" tabindex = "-1" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Opret Nextcloud Bruger< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< form id = "ncCreateUserForm" >
< div class = "mb-3" >
< label class = "form-label" > Email< / label >
< input type = "email" class = "form-control" name = "email" value = "{{ hovedkontakt.email if hovedkontakt else '' }}" required >
< / div >
< div class = "mb-3" >
< label class = "form-label" > Visningsnavn< / label >
< input type = "text" class = "form-control" name = "display_name" value = "{{ hovedkontakt.first_name if hovedkontakt else '' }} {{ hovedkontakt.last_name if hovedkontakt else '' }}" required >
< / div >
< div class = "mb-3" >
< label class = "form-label" > Bruger ID (UID)< / label >
< input type = "text" class = "form-control" name = "uid" value = "{{ hovedkontakt.email if hovedkontakt else '' }}" required >
< div class = "form-text" > Bruges til login. Oftest email.< / div >
< / div >
< div class = "mb-3" >
< label class = "form-label" > Grupper< / label >
< input type = "text" class = "form-control" name = "groups" value = "Kunder" placeholder = "f.eks. Kunder, Ekstern" >
< div class = "form-text" > Komma-separeret liste af grupper< / div >
< / div >
< div class = "form-check" >
< input class = "form-check-input" type = "checkbox" name = "send_welcome" id = "ncSendWelcome" checked >
< label class = "form-check-label" for = "ncSendWelcome" >
Send velkomst-email med kode
< / label >
< / div >
< / form >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Annuller< / button >
< button type = "button" class = "btn btn-success" onclick = "ncCreateUser()" > Opret Bruger< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Disable User Modal -->
< div class = "modal fade" id = "ncDisableUserModal" tabindex = "-1" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title text-danger" > Luk Nextcloud Bruger< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< form id = "ncDisableUserForm" >
< div class = "mb-3" >
< label class = "form-label" > Bruger ID (UID)< / label >
< input type = "text" class = "form-control" name = "uid" value = "{{ hovedkontakt.email if hovedkontakt else '' }}" required >
< / div >
< div class = "alert alert-warning small" >
Brugeren vil ikke længere kunne logge ind, men data bevares.
< / div >
< / form >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Annuller< / button >
< button type = "button" class = "btn btn-danger" onclick = "ncDisableUser()" > Luk Bruger< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Reset Password Modal -->
< div class = "modal fade" id = "ncResetPasswordModal" tabindex = "-1" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Reset Kodeord< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< form id = "ncResetPasswordForm" >
< div class = "mb-3" >
< label class = "form-label" > Bruger ID (UID)< / label >
< input type = "text" class = "form-control" name = "uid" value = "{{ hovedkontakt.email if hovedkontakt else '' }}" required >
< / div >
< div class = "form-check" >
< input class = "form-check-input" type = "checkbox" name = "send_email" id = "ncSendResetEmail" checked >
< label class = "form-check-label" for = "ncSendResetEmail" >
Send ny kode på email
< / label >
< / div >
< / form >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Annuller< / button >
< button type = "button" class = "btn btn-warning" onclick = "ncResetPassword()" > Reset Kode< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Send Guide Modal -->
< div class = "modal fade" id = "ncSendGuideModal" tabindex = "-1" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Send Guide< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< form id = "ncSendGuideForm" >
< div class = "mb-3" >
< label class = "form-label" > Bruger ID (UID)< / label >
< input type = "text" class = "form-control" name = "uid" value = "{{ hovedkontakt.email if hovedkontakt else '' }}" required >
< / div >
< p class = "small text-muted" > Sender en email med start-guide til Nextcloud til brugerens registrerede mail.< / p >
< / form >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Annuller< / button >
< button type = "button" class = "btn btn-info text-white" onclick = "ncSendGuide()" > Send Guide< / button >
< / div >
< / div >
< / div >
< / div >
< script >
{% endif %}
< / script >
<!-- Generic Search Modal -->
< div class = "modal fade" id = "entitySearchModal" tabindex = "-1" >
< div class = "modal-dialog modal-dialog-centered" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" id = "entitySearchTitle" > Søg< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< div class = "input-group mb-3" >
< span class = "input-group-text" > < i class = "bi bi-search" > < / i > < / span >
< input type = "text" class = "form-control" id = "entitySearchInput" placeholder = "Søg (min. 2 tegn)..." autocomplete = "off" >
< / div >
< div class = "text-center d-none" id = "entitySearchSpinner" >
< div class = "spinner-border text-primary" role = "status" > < / div >
< / div >
< div id = "entitySearchResults" class = "list-group list-group-flush" style = "max-height: 300px; overflow-y: auto;" >
<!-- Results go here -->
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Create Related Case Modal -->
< div class = "modal fade" id = "createRelatedCaseModal" tabindex = "-1" aria-hidden = "true" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Opret ny relateret sag< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< form id = "createRelatedForm" >
< div class = "mb-3" >
< label class = "form-label" > Titel *< / label >
< input type = "text" class = "form-control" id = "newCaseTitle" required placeholder = "F.eks. Opfølgning på..." >
< / div >
< div class = "mb-3" >
< label class = "form-label" > Relationstype *< / label >
< select class = "form-select" id = "newCaseRelationType" >
< option value = "Relateret til" > Relateret til< / option >
< option value = "Afledt af" > Afledt af< / option >
< option value = "Årsag til" > Årsag til< / option >
< / select >
< / div >
< div class = "mb-3" >
< label class = "form-label" > Beskrivelse< / label >
< textarea class = "form-control" id = "newCaseDescription" rows = "3" > < / textarea >
< / div >
< div class = "alert alert-info small mb-0" >
< i class = "bi bi-info-circle me-1" > < / i >
Sagen oprettes for kunden: < strong > {{ case.customer_name }}< / strong >
< / div >
< / form >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Annuller< / button >
< button type = "button" class = "btn btn-primary" onclick = "createRelatedCase()" > Opret & Link< / button >
< / div >
< / div >
< / div >
< / div >
< script >
let currentSearchType = null;
let searchDebounceIds = null;
const caseIds = {{ case.id }};
function openSearchModal(type) {
currentSearchType = type;
const titles = {
'hardware': 'Tilføj Hardware',
'location': 'Tilføj Lokation',
'contact': 'Tilføj Kontakt',
'customer': 'Tilføj Kunde'
};
document.getElementById('entitySearchTitle').textContent = titles[type] || 'Søg';
document.getElementById('entitySearchInput').value = '';
document.getElementById('entitySearchResults').innerHTML = '';
const modal = new bootstrap.Modal(document.getElementById('entitySearchModal'));
modal.show();
setTimeout(() => document.getElementById('entitySearchInput').focus(), 500);
}
document.getElementById('entitySearchInput').addEventListener('input', function(e) {
clearTimeout(searchDebounceIds);
const query = e.target.value.trim();
if (query.length < 2 ) {
document.getElementById('entitySearchResults').innerHTML = '';
return;
}
searchDebounceIds = setTimeout(() => performSearch(query), 300);
});
async function performSearch(query) {
document.getElementById('entitySearchSpinner').classList.remove('d-none');
document.getElementById('entitySearchResults').classList.add('d-none');
try {
let url = '';
if (currentSearchType === 'hardware') url = `/api/v1/search/hardware?q=${encodeURIComponent(query)}`;
else if (currentSearchType === 'location') url = `/api/v1/search/locations?q=${encodeURIComponent(query)}`;
else if (currentSearchType === 'contact') url = `/api/v1/search/contacts?q=${encodeURIComponent(query)}`;
else if (currentSearchType === 'customer') url = `/api/v1/search/customers?q=${encodeURIComponent(query)}`;
const res = await fetch(url);
if (!res.ok) throw new Error('Search failed');
const results = await res.json();
renderResults(results);
} catch (e) {
console.error(e);
document.getElementById('entitySearchResults').innerHTML = '< div class = "text-danger text-center p-3" > Fejl ved søgning< / div > ';
} finally {
document.getElementById('entitySearchSpinner').classList.add('d-none');
document.getElementById('entitySearchResults').classList.remove('d-none');
}
}
function renderResults(results) {
const container = document.getElementById('entitySearchResults');
if (results.length === 0) {
container.innerHTML = '< div class = "text-muted text-center p-3" > Ingen resultater fundet< / div > ';
return;
}
container.innerHTML = results.map(item => {
let title = '', subtitle = '', icon = '', id = item.id;
if (currentSearchType === 'hardware') {
title = `${item.brand} ${item.model}`;
subtitle = `SN: ${item.serial_number}`;
icon = 'bi-laptop';
} else if (currentSearchType === 'location') {
title = item.name;
subtitle = `${item.address_street || ''} ${item.address_city || ''}`;
icon = 'bi-geo-alt';
} else if (currentSearchType === 'contact') {
title = `${item.first_name} ${item.last_name}`;
subtitle = item.email;
icon = 'bi-person';
} else if (currentSearchType === 'customer') {
title = item.name;
subtitle = `CVR: ${item.cvr_nummer || 'N/A'}`;
icon = 'bi-building';
}
return `
< button type = "button" class = "list-group-item list-group-item-action d-flex align-items-center" onclick = "addEntity(${id})" >
< div class = "me-3 fs-4 text-muted" > < i class = "bi ${icon}" > < / i > < / div >
< div >
< div class = "fw-bold" > ${title}< / div >
< small class = "text-muted" > ${subtitle}< / small >
< / div >
< / button >
`;
}).join('');
}
async function addEntity(id) {
let url = '', body = {};
if (currentSearchType === 'hardware') {
url = `/api/v1/sag/${caseIds}/hardware`;
body = { hardware_id: id };
} else if (currentSearchType === 'location') {
url = `/api/v1/sag/${caseIds}/locations`;
body = { location_id: id };
} else if (currentSearchType === 'contact') {
url = `/api/v1/sag/${caseIds}/contacts`;
body = { contact_id: id };
} else if (currentSearchType === 'customer') {
url = `/api/v1/sag/${caseIds}/customers`;
body = { customer_id: id };
}
try {
const res = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
});
if (!res.ok) {
const err = await res.json();
alert("Fejl: " + (err.detail || 'Kunne ikke tilføje'));
return;
}
bootstrap.Modal.getInstance(document.getElementById('entitySearchModal')).hide();
window.location.reload();
} catch (e) {
alert("Fejl: " + e.message);
}
}
async function removeContact(caseId, contactId) {
if(!confirm("Fjern denne kontakt fra sagen?")) return;
try {
const res = await fetch(`/api/v1/sag/${caseId}/contacts/${contactId}`, { method: 'DELETE' });
if (res.ok) window.location.reload();
else alert("Fejl ved sletning");
} catch(e) { alert("Fejl: " + e.message); }
}
2026-02-06 10:47:14 +01:00
function openContactRoleModal(contactId, contactName, role, isPrimary) {
document.getElementById('contactRoleContactId').value = contactId;
document.getElementById('contactRoleName').textContent = contactName || '-';
document.getElementById('contactRoleInput').value = role || '';
document.getElementById('contactRolePrimary').checked = !!isPrimary;
const modal = new bootstrap.Modal(document.getElementById('contactRoleModal'));
modal.show();
}
async function saveContactRole() {
const contactId = document.getElementById('contactRoleContactId').value;
const role = document.getElementById('contactRoleInput').value.trim();
const isPrimary = document.getElementById('contactRolePrimary').checked;
try {
const res = await fetch(`/api/v1/sag/${caseIds}/contacts/${contactId}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ role, is_primary: isPrimary })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail || 'Kunne ikke opdatere kontakt');
}
bootstrap.Modal.getInstance(document.getElementById('contactRoleModal')).hide();
window.location.reload();
} catch (e) {
alert('Fejl: ' + e.message);
}
}
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
async function removeCustomer(caseId, customerId) {
if(!confirm("Fjern denne kunde fra sagen?")) return;
try {
const res = await fetch(`/api/v1/sag/${caseId}/customers/${customerId}`, { method: 'DELETE' });
if (res.ok) window.location.reload();
else alert("Fejl ved sletning");
} catch(e) { alert("Fejl: " + e.message); }
}
2026-02-06 10:47:14 +01:00
async function updateDeferredUntil(value) {
try {
const res = await fetch(`/api/v1/sag/${caseIds}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ deferred_until: value || null })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail || 'Kunne ikke opdatere');
}
window.location.reload();
} catch (e) {
alert('Fejl: ' + e.message);
}
}
function setDeferredFromInput() {
const input = document.getElementById('deferredUntilInput');
updateDeferredUntil(input.value || null);
}
function shiftDeferredDays(days) {
const input = document.getElementById('deferredUntilInput');
const base = input.value ? new Date(input.value) : new Date();
base.setDate(base.getDate() + days);
input.value = base.toISOString().slice(0, 10);
updateDeferredUntil(input.value);
}
function shiftDeferredMonths(months) {
const input = document.getElementById('deferredUntilInput');
const base = input.value ? new Date(input.value) : new Date();
base.setMonth(base.getMonth() + months);
input.value = base.toISOString().slice(0, 10);
updateDeferredUntil(input.value);
}
function clearDeferredUntil() {
const input = document.getElementById('deferredUntilInput');
input.value = '';
updateDeferredUntil(null);
}
function openDeferredModal() {
const modal = new bootstrap.Modal(document.getElementById('deferredModal'));
modal.show();
}
async function updateDeferredCaseAndStatus(caseId, status) {
try {
const res = await fetch(`/api/v1/sag/${caseIds}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
deferred_until_case_id: caseId ? parseInt(caseId, 10) : null,
deferred_until_status: status || null
})
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail || 'Kunne ikke opdatere');
}
window.location.reload();
} catch (e) {
alert('Fejl: ' + e.message);
}
}
function setDeferredCaseFromInputs() {
const caseSelect = document.getElementById('deferredCaseSelect');
const statusSelect = document.getElementById('deferredStatusSelect');
updateDeferredCaseAndStatus(caseSelect.value || null, statusSelect.value || null);
}
function clearDeferredCase() {
const caseSelect = document.getElementById('deferredCaseSelect');
const statusSelect = document.getElementById('deferredStatusSelect');
caseSelect.value = '';
statusSelect.value = '';
updateDeferredCaseAndStatus(null, null);
}
function saveDeferredAll() {
const input = document.getElementById('deferredUntilInput');
const caseSelect = document.getElementById('deferredCaseSelect');
const statusSelect = document.getElementById('deferredStatusSelect');
updateDeferredUntil(input.value || null);
updateDeferredCaseAndStatus(caseSelect.value || null, statusSelect.value || null);
}
function clearDeferredAll() {
const input = document.getElementById('deferredUntilInput');
const caseSelect = document.getElementById('deferredCaseSelect');
const statusSelect = document.getElementById('deferredStatusSelect');
input.value = '';
caseSelect.value = '';
statusSelect.value = '';
updateDeferredUntil(null);
updateDeferredCaseAndStatus(null, null);
}
// ==========================================
// VIEW CONTROL (Tag-based)
// ==========================================
let modulePrefs = {};
function moduleHasContent(el) {
const attr = el.getAttribute('data-has-content');
if (attr === 'true') return true;
if (attr === 'false') return false;
if (attr === 'unknown') return true;
if (el.querySelector('.person-card')) return true;
if (el.querySelector('.list-group-item')) return true;
return true;
}
function applyViewLayout(viewName) {
if (!viewName) return;
document.body.setAttribute('data-case-view', viewName);
const viewDefaults = {
'Pipeline': ['relations', 'sales', 'time'],
'Kundevisning': ['customers', 'contacts', 'locations'],
'Sag-detalje': ['hardware', 'locations', 'contacts', 'customers', 'relations', 'files', 'emails', 'solution', 'time', 'sales', 'reminders']
};
const standardModules = viewDefaults[viewName] || [];
document.querySelectorAll('[data-module]').forEach((el) => {
const moduleName = el.getAttribute('data-module');
const hasContent = moduleHasContent(el);
const pref = modulePrefs[moduleName];
const tabButton = document.querySelector(`[data-module-tab="${moduleName}"]`);
if (pref === false) {
el.classList.add('d-none');
if (tabButton) tabButton.classList.add('d-none');
return;
}
if (pref === true) {
el.classList.remove('d-none');
if (tabButton) tabButton.classList.remove('d-none');
return;
}
if (!standardModules.includes(moduleName) & & !hasContent) {
el.classList.add('d-none');
if (tabButton) tabButton.classList.add('d-none');
} else {
el.classList.remove('d-none');
if (tabButton) tabButton.classList.remove('d-none');
}
});
updateRightColumnVisibility();
}
function updateRightColumnVisibility() {
const rightColumn = document.getElementById('case-right-column');
const leftColumn = document.getElementById('case-left-column');
if (!rightColumn || !leftColumn) return;
const visibleRightModules = rightColumn.querySelectorAll('.right-module-card:not(.d-none)');
if (visibleRightModules.length === 0) {
rightColumn.classList.add('d-none');
rightColumn.classList.remove('col-lg-4');
leftColumn.classList.remove('col-lg-8');
leftColumn.classList.add('col-12');
} else {
rightColumn.classList.remove('d-none');
rightColumn.classList.add('col-lg-4');
leftColumn.classList.add('col-lg-8');
leftColumn.classList.remove('col-12');
}
}
async function applyViewFromTags() {
try {
const res = await fetch(`/api/v1/tags/entity/case/${caseIds}`);
if (!res.ok) return;
const tags = await res.json();
const viewTag = tags.find(t => ['Pipeline', 'Kundevisning', 'Sag-detalje'].includes(t.name));
applyViewLayout(viewTag ? viewTag.name : 'Sag-detalje');
} catch (e) {
console.error('View tag lookup failed', e);
}
}
async function loadModulePrefs() {
try {
const res = await fetch(`/api/v1/sag/${caseIds}/modules`);
if (!res.ok) return;
const prefs = await res.json();
modulePrefs = (prefs || []).reduce((acc, p) => {
acc[p.module_key] = p.is_enabled;
return acc;
}, {});
} catch (e) {
console.error('Module prefs load failed', e);
}
}
async function openModuleControlModal() {
const list = document.getElementById('moduleControlList');
list.innerHTML = '< div class = "text-muted small" > Indlæser...< / div > ';
const modules = Array.from(document.querySelectorAll('[data-module]')).map(el => {
const key = el.getAttribute('data-module');
return { key, label: key };
});
list.innerHTML = modules.map(m => {
const checked = modulePrefs[m.key] !== false;
return `
< div class = "form-check mb-2" >
< input class = "form-check-input" type = "checkbox" id = "module_${m.key}" $ { checked ? ' checked ' : ' ' }
onchange="toggleModulePref('${m.key}', this.checked)">
< label class = "form-check-label" for = "module_${m.key}" > ${m.label}< / label >
< / div >
`;
}).join('');
const modal = new bootstrap.Modal(document.getElementById('moduleControlModal'));
modal.show();
}
async function toggleModulePref(moduleKey, isEnabled) {
try {
const res = await fetch(`/api/v1/sag/${caseIds}/modules`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ module_key: moduleKey, is_enabled: isEnabled })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail || 'Kunne ikke opdatere modul');
}
modulePrefs[moduleKey] = isEnabled;
applyViewFromTags();
} catch (e) {
alert('Fejl: ' + e.message);
}
}
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
// ==========================================
// FILES & EMAILS LOGIC
// ==========================================
// ---------------- FILES ----------------
async function loadSagFiles() {
const container = document.getElementById('files-list');
if(!container) return;
container.innerHTML = '< div class = "p-3 text-center text-muted" > < span class = "spinner-border spinner-border-sm" > < / span > Henter filer...< / div > ';
try {
const res = await fetch(`/api/v1/sag/${caseIds}/files`);
if(res.ok) {
const files = await res.json();
renderFiles(files);
} else {
container.innerHTML = '< div class = "p-3 text-center text-danger" > Fejl ved hentning af filer< / div > ';
}
} catch(e) {
console.error(e);
container.innerHTML = '< div class = "p-3 text-center text-danger" > Fejl ved hentning af filer< / div > ';
}
}
function renderFiles(files) {
const container = document.getElementById('files-list');
if(!files || files.length === 0) {
container.innerHTML = '< div class = "p-3 text-center text-muted" > Ingen filer fundet...< / div > ';
return;
}
container.innerHTML = files.map(f => {
const size = (f.size_bytes / 1024 / 1024).toFixed(2) + ' MB';
return `
< div class = "list-group-item d-flex justify-content-between align-items-center" >
< div class = "ms-2 me-auto" >
< div class = "fw-bold text-truncate" style = "max-width: 250px;" >
2026-02-06 10:47:14 +01:00
< a href = "javascript:void(0);" onclick = "previewFile(${f.id}, '${f.filename.replace(/'/g, " \ \ ' " ) } ' , ' $ { f . content_type | | ' ' } ' ) " class = "text-decoration-none text-dark" >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< i class = "bi bi-file-earmark me-1" > < / i > ${f.filename}
< / a >
< / div >
< small class = "text-muted" > ${size} • ${new Date(f.created_at).toLocaleDateString()}< / small >
< / div >
2026-02-06 10:47:14 +01:00
< div class = "d-flex gap-1" >
< a href = "${f.download_url}?download=true" class = "btn btn-sm btn-outline-primary border-0" title = "Download" >
< i class = "bi bi-download" > < / i >
< / a >
< button class = "btn btn-sm btn-outline-danger border-0" onclick = "deleteFile(${f.id})" title = "Slet" >
< i class = "bi bi-x-lg" > < / i >
< / button >
< / div >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / div >
`;
}).join('');
}
async function handleFileUpload(fileList) {
if(!fileList || fileList.length === 0) return;
const formData = new FormData();
for (let i = 0; i < fileList.length ; i + + ) {
formData.append("files", fileList[i]);
}
// Show loading
document.getElementById('files-list').innerHTML += '< div class = "p-2 text-center text-muted fst-italic" > Uploader...< / div > ';
try {
const res = await fetch(`/api/v1/sag/${caseIds}/files`, {
method: 'POST',
body: formData
});
if(res.ok) {
loadSagFiles();
} else {
alert('Upload fejlede');
loadSagFiles(); // Reload to clear loading state
}
} catch(e) {
alert('Upload fejl: ' + e);
loadSagFiles();
}
}
async function deleteFile(fileId) {
if(!confirm("Slet denne fil?")) return;
try {
const res = await fetch(`/api/v1/sag/${caseIds}/files/${fileId}`, { method: 'DELETE' });
if(res.ok) loadSagFiles();
else alert("Kunne ikke slette fil");
} catch(e) { alert("Fejl: " + e); }
}
2026-02-06 10:47:14 +01:00
// File Preview
function previewFile(fileId, filename, contentType) {
const modal = new bootstrap.Modal(document.getElementById('filePreviewModal'));
const previewContent = document.getElementById('previewContent');
const fileNameEl = document.getElementById('previewFileName');
const downloadBtn = document.getElementById('previewDownloadBtn');
// Set filename and download link
fileNameEl.textContent = filename;
const fileUrl = `/api/v1/sag/${caseIds}/files/${fileId}`;
downloadBtn.href = `${fileUrl}?download=true`;
downloadBtn.download = filename;
// Show loading spinner
previewContent.innerHTML = `
< div class = "spinner-border text-primary" role = "status" >
< span class = "visually-hidden" > Indlæser...< / span >
< / div >
`;
modal.show();
// Determine file type and render preview
const ext = filename.split('.').pop().toLowerCase();
if (['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp'].includes(ext)) {
// Image preview
previewContent.innerHTML = `< img src = "${fileUrl}" class = "img-fluid" style = "max-height: 80vh;" alt = "${filename}" > `;
} else if (ext === 'pdf') {
// PDF preview using iframe
previewContent.innerHTML = `< iframe src = "${fileUrl}" class = "w-100 h-100 border-0" style = "min-height: 60vh;" > < / iframe > `;
} else if (['txt', 'log', 'md', 'json', 'xml', 'csv', 'html', 'css', 'js', 'py', 'sql'].includes(ext)) {
// Text file preview
fetch(fileUrl)
.then(res => res.text())
.then(text => {
previewContent.innerHTML = `< pre class = "p-3 m-0 overflow-auto h-100" style = "max-height: 80vh;" > < code > ${escapeHtml(text)}< / code > < / pre > `;
})
.catch(err => {
previewContent.innerHTML = `< div class = "alert alert-danger m-4" > Kunne ikke indlæse fil: ${err}< / div > `;
});
} else if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) {
// Office documents - use Google Docs Viewer
const encodedUrl = encodeURIComponent(window.location.origin + fileUrl);
previewContent.innerHTML = `< iframe src = "https://docs.google.com/viewer?url=${encodedUrl}&embedded=true" class = "w-100 h-100 border-0" style = "min-height: 60vh;" > < / iframe > `;
} else if (['mp4', 'webm', 'ogg'].includes(ext)) {
// Video preview
previewContent.innerHTML = `
< video controls class = "w-100" style = "max-height: 80vh;" >
< source src = "${fileUrl}" type = "video/${ext}" >
Din browser understøtter ikke video afspilning.
< / video >
`;
} else if (['mp3', 'wav', 'ogg', 'm4a'].includes(ext)) {
// Audio preview
previewContent.innerHTML = `
< div class = "p-5 text-center" >
< i class = "bi bi-music-note-beamed display-1 text-muted mb-4" > < / i >
< h5 > ${filename}< / h5 >
< audio controls class = "w-100 mt-3" >
< source src = "${fileUrl}" type = "audio/${ext}" >
Din browser understøtter ikke audio afspilning.
< / audio >
< / div >
`;
} else {
// Unsupported file type
previewContent.innerHTML = `
< div class = "p-5 text-center" >
< i class = "bi bi-file-earmark-x display-1 text-muted mb-4" > < / i >
< h5 > Kan ikke vise forhåndsvisning for denne filtype< / h5 >
< p class = "text-muted" > ${filename}< / p >
< a href = "${fileUrl}?download=true" class = "btn btn-primary mt-3" download = "${filename}" >
< i class = "bi bi-download me-2" > < / i > Download fil
< / a >
< / div >
`;
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
// File Drag & Drop
const fileDropZone = document.getElementById('fileDropZone');
if(fileDropZone) {
fileDropZone.addEventListener('dragover', e => { e.preventDefault(); fileDropZone.classList.add('bg-light-subtle'); });
fileDropZone.addEventListener('dragleave', e => { e.preventDefault(); fileDropZone.classList.remove('bg-light-subtle'); });
fileDropZone.addEventListener('drop', e => {
e.preventDefault();
fileDropZone.classList.remove('bg-light-subtle');
if(e.dataTransfer.files.length) handleFileUpload(e.dataTransfer.files);
});
}
// ---------------- EMAILS ----------------
async function loadLinkedEmails() {
const container = document.getElementById('linked-emails-list');
if(!container) return;
try {
const res = await fetch(`/api/v1/sag/${caseIds}/email-links`);
if(res.ok) {
const emails = await res.json();
renderLinkedEmails(emails);
}
} catch(e) { console.error(e); }
}
function renderLinkedEmails(emails) {
const container = document.getElementById('linked-emails-list');
if(!emails || emails.length === 0) {
container.innerHTML = '< div class = "p-3 text-center text-muted" > Ingen linkede emails...< / div > ';
return;
}
container.innerHTML = emails.map(e => `
< div class = "list-group-item" >
< div class = "d-flex justify-content-between align-items-start" >
< div class = "text-truncate" >
< i class = "bi bi-envelope text-primary me-1" > < / i >
< strong > ${e.subject || '(Ingen emne)'}< / strong >
< div class = "small text-muted text-truncate" > ${e.sender_email}< / div >
< / div >
< button class = "btn btn-sm btn-link text-danger p-0 ms-2" onclick = "unlinkEmail(${e.id})" >
< i class = "bi bi-link-45deg" style = "text-decoration: line-through;" > < / i >
< / button >
< / div >
< / div >
`).join('');
}
async function unlinkEmail(emailId) {
if(!confirm("Fjern link til denne email?")) return;
try {
const res = await fetch(`/api/v1/sag/${caseIds}/email-links/${emailId}`, { method: 'DELETE' });
if(res.ok) loadLinkedEmails();
} catch(e) { alert(e); }
}
// Email Search
const emailSearchInput = document.getElementById('emailSearchInput');
const emailSearchResults = document.getElementById('emailSearchResults');
let emailDebounce = null;
if(emailSearchInput) {
emailSearchInput.addEventListener('input', e => {
clearTimeout(emailDebounce);
const q = e.target.value.trim();
if(q.length < 2 ) {
emailSearchResults.style.display = 'none';
return;
}
emailDebounce = setTimeout(() => searchEmails(q), 300);
});
// Hide on outside click
document.addEventListener('click', e => {
if(!emailSearchInput.contains(e.target) & & !emailSearchResults.contains(e.target)) {
emailSearchResults.style.display = 'none';
}
});
}
async function searchEmails(query) {
try {
const res = await fetch(`/api/v1/emails?q=${encodeURIComponent(query)}&limit=5`);
if(res.ok) {
const emails = await res.json();
renderEmailSuggestions(emails);
emailSearchResults.style.display = 'block';
}
} catch(e) { console.error(e); }
}
function renderEmailSuggestions(emails) {
if(!emails.length) {
emailSearchResults.innerHTML = '< div class = "list-group-item text-muted" > Ingen fundet< / div > ';
return;
}
emailSearchResults.innerHTML = emails.map(e => `
< button class = "list-group-item list-group-item-action" onclick = "linkEmail(${e.id})" >
< div class = "fw-bold text-truncate" > ${e.subject}< / div >
< div class = "small text-muted" > ${e.sender_email}< / div >
< / button >
`).join('');
}
async function linkEmail(emailId) {
emailSearchInput.value = '';
emailSearchResults.style.display = 'none';
try {
const res = await fetch(`/api/v1/sag/${caseIds}/email-links`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email_id: emailId})
});
if(res.ok) loadLinkedEmails();
else alert("Kunne ikke linke email");
} catch(e) { alert(e); }
}
// Email Import Drag & Drop (.msg / .eml)
const emailDropZone = document.getElementById('emailDropZone');
if(emailDropZone) {
emailDropZone.addEventListener('dragover', e => { e.preventDefault(); emailDropZone.classList.add('bg-warning-subtle'); });
emailDropZone.addEventListener('dragleave', e => { e.preventDefault(); emailDropZone.classList.remove('bg-warning-subtle'); });
emailDropZone.addEventListener('drop', e => {
e.preventDefault();
emailDropZone.classList.remove('bg-warning-subtle');
const files = e.dataTransfer.files;
if(files.length) uploadEmailFile(files[0]);
});
}
async function uploadEmailFile(file) {
const formData = new FormData();
formData.append('file', file);
// Show busy indicator
emailDropZone.style.opacity = '0.5';
try {
const res = await fetch(`/api/v1/sag/${caseIds}/upload-email`, {
method: 'POST',
body: formData
});
if(res.ok) {
loadLinkedEmails();
} else {
alert('Import fejlede');
}
} catch(e) { alert(e); }
finally {
emailDropZone.style.opacity = '1';
}
}
// Load content on start
document.addEventListener('DOMContentLoaded', () => {
loadSagFiles();
loadLinkedEmails();
});
< / script >
2026-02-01 11:58:44 +01:00
< / div >
{% endblock %}