1511 lines
52 KiB
HTML
1511 lines
52 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Kontakter - BMC Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.contacts-toolbar {
|
|
gap: 1rem;
|
|
}
|
|
|
|
.toolbar-search-slot {
|
|
flex: 1;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.search-wrap {
|
|
position: relative;
|
|
min-width: 280px;
|
|
max-width: 520px;
|
|
width: min(52vw, 520px);
|
|
}
|
|
|
|
.search-label {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.35rem;
|
|
font-size: 0.74rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
margin-bottom: 0.35rem;
|
|
}
|
|
|
|
.search-input-shell {
|
|
position: relative;
|
|
border: 1px solid rgba(15, 76, 117, 0.28);
|
|
border-radius: 12px;
|
|
background: linear-gradient(180deg, rgba(15, 76, 117, 0.09) 0%, rgba(15, 76, 117, 0.04) 100%);
|
|
box-shadow: 0 8px 20px rgba(2, 32, 71, 0.1);
|
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
|
|
.search-input-shell:focus-within {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px rgba(15, 76, 117, 0.18), 0 10px 24px rgba(2, 32, 71, 0.14);
|
|
}
|
|
|
|
.search-icon {
|
|
position: absolute;
|
|
left: 0.8rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: var(--accent);
|
|
font-size: 0.95rem;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.search-wrap .header-search {
|
|
width: 100%;
|
|
border: 0;
|
|
background: transparent;
|
|
padding-left: 2.25rem;
|
|
padding-right: 2.4rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.search-wrap .header-search::placeholder {
|
|
color: var(--text-secondary);
|
|
opacity: 0.95;
|
|
}
|
|
|
|
.search-clear {
|
|
position: absolute;
|
|
right: 0.45rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
border: 0;
|
|
width: 1.8rem;
|
|
height: 1.8rem;
|
|
border-radius: 50%;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--text-secondary);
|
|
background: transparent;
|
|
}
|
|
|
|
.search-clear:hover {
|
|
background: rgba(15, 76, 117, 0.12);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.search-clear.d-none {
|
|
display: none !important;
|
|
}
|
|
|
|
.filter-btn {
|
|
background: var(--bg-card);
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
color: var(--text-secondary);
|
|
padding: 0.5rem 1.2rem;
|
|
border-radius: 20px;
|
|
font-size: 0.9rem;
|
|
transition: all 0.2s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.filter-btn:hover, .filter-btn.active {
|
|
background: var(--accent);
|
|
color: white;
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.table-utility-bar {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-bottom: 0.8rem;
|
|
}
|
|
|
|
.column-toggle-btn {
|
|
border: 1px solid rgba(15, 76, 117, 0.2);
|
|
background: var(--bg-card);
|
|
color: var(--text-primary);
|
|
border-radius: 10px;
|
|
padding: 0.35rem 0.7rem;
|
|
font-size: 0.82rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.column-toggle-btn:hover {
|
|
background: rgba(15, 76, 117, 0.08);
|
|
}
|
|
|
|
.column-toggle-menu {
|
|
border: 1px solid rgba(15, 76, 117, 0.16);
|
|
border-radius: 12px;
|
|
padding: 0.4rem;
|
|
min-width: 200px;
|
|
box-shadow: 0 10px 24px rgba(2, 32, 71, 0.14);
|
|
}
|
|
|
|
.column-toggle-item {
|
|
border-radius: 8px;
|
|
padding: 0.3rem 0.45rem;
|
|
margin-bottom: 0.1rem;
|
|
}
|
|
|
|
.column-toggle-item:hover {
|
|
background: rgba(15, 76, 117, 0.08);
|
|
}
|
|
|
|
.contacts-shell {
|
|
border: 1px solid rgba(15, 76, 117, 0.12);
|
|
border-radius: 14px;
|
|
box-shadow: 0 10px 30px rgba(2, 32, 71, 0.06);
|
|
}
|
|
|
|
.contacts-table-wrap {
|
|
border: 1px solid rgba(15, 76, 117, 0.12);
|
|
border-radius: 12px;
|
|
overflow-x: auto;
|
|
overflow-y: visible;
|
|
}
|
|
|
|
.contacts-table {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.contacts-shell .table > :not(caption) > * > * {
|
|
padding-top: 0.85rem;
|
|
padding-bottom: 0.85rem;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.contacts-shell .table-hover > tbody > tr:hover {
|
|
--bs-table-accent-bg: rgba(15, 76, 117, 0.05);
|
|
}
|
|
|
|
.contacts-shell .table tbody tr {
|
|
cursor: pointer;
|
|
transition: background-color 0.18s ease;
|
|
}
|
|
|
|
.contacts-shell .table tbody tr:nth-child(even) {
|
|
background: rgba(15, 76, 117, 0.015);
|
|
}
|
|
|
|
.contacts-shell .table thead th {
|
|
font-size: 0.78rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: var(--text-secondary);
|
|
border-bottom-width: 1px;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 2;
|
|
background: var(--bg-card);
|
|
box-shadow: 0 1px 0 rgba(15, 76, 117, 0.12);
|
|
}
|
|
|
|
.sort-btn {
|
|
border: 0;
|
|
background: transparent;
|
|
color: inherit;
|
|
font-size: inherit;
|
|
letter-spacing: inherit;
|
|
text-transform: inherit;
|
|
font-weight: 700;
|
|
padding: 0;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.28rem;
|
|
}
|
|
|
|
.sort-btn:hover {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.sort-btn .sort-indicator {
|
|
font-size: 0.72rem;
|
|
opacity: 0.45;
|
|
}
|
|
|
|
.sort-btn.active .sort-indicator {
|
|
opacity: 1;
|
|
}
|
|
|
|
.contacts-shell .table thead th.col-key {
|
|
min-width: 220px;
|
|
}
|
|
|
|
.contacts-shell .table thead th.col-updated {
|
|
min-width: 130px;
|
|
}
|
|
|
|
.contact-name {
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.contact-subline {
|
|
font-size: 0.82rem;
|
|
color: var(--text-secondary);
|
|
margin-top: 0.1rem;
|
|
}
|
|
|
|
.contact-info-main {
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.contact-quick-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.35rem;
|
|
margin-top: 0.08rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.contact-quick-actions .btn {
|
|
border-radius: 999px;
|
|
padding: 0.04rem 0.45rem;
|
|
font-size: 0.69rem;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.contact-data-stack {
|
|
display: grid;
|
|
gap: 0.1rem;
|
|
}
|
|
|
|
.key-line {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.32rem;
|
|
min-height: 1.2rem;
|
|
line-height: 1.15;
|
|
}
|
|
|
|
.key-label {
|
|
font-size: 0.61rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
border-radius: 999px;
|
|
padding: 0.08rem 0.35rem;
|
|
background: rgba(15, 76, 117, 0.08);
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.key-value {
|
|
font-size: 0.8rem;
|
|
color: var(--text-primary);
|
|
font-weight: 500;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.key-value:hover {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.muted-data {
|
|
color: var(--text-secondary);
|
|
font-size: 0.79rem;
|
|
}
|
|
|
|
.updated-at {
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.company-count-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
border: 1px solid rgba(15, 76, 117, 0.2);
|
|
background: rgba(15, 76, 117, 0.06);
|
|
color: var(--accent);
|
|
border-radius: 999px;
|
|
padding: 0.2rem 0.58rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.status-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
border-radius: 999px;
|
|
font-size: 0.72rem;
|
|
font-weight: 600;
|
|
padding: 0.24rem 0.62rem;
|
|
border: 1px solid transparent;
|
|
}
|
|
|
|
.status-pill.active {
|
|
background: rgba(17, 153, 84, 0.12);
|
|
border-color: rgba(17, 153, 84, 0.24);
|
|
color: #0b6b3a;
|
|
}
|
|
|
|
.status-pill.inactive {
|
|
background: rgba(108, 117, 125, 0.13);
|
|
border-color: rgba(108, 117, 125, 0.24);
|
|
color: #5b6570;
|
|
}
|
|
|
|
.btn-table-action {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(15, 76, 117, 0.16);
|
|
background: var(--bg-card);
|
|
color: var(--accent);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.btn-table-action:hover {
|
|
background: rgba(15, 76, 117, 0.08);
|
|
border-color: rgba(15, 76, 117, 0.28);
|
|
}
|
|
|
|
.contact-avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background: var(--accent-light);
|
|
color: var(--accent);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: bold;
|
|
font-size: 0.9rem;
|
|
box-shadow: inset 0 0 0 1px rgba(15, 76, 117, 0.12);
|
|
}
|
|
|
|
.pagination-btn {
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
padding: 0.5rem 1rem;
|
|
background: var(--bg-card);
|
|
color: var(--text-primary);
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.pagination-btn:hover:not(:disabled) {
|
|
background: var(--accent-light);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.pagination-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.create-contact-modal .modal-content {
|
|
border: 1px solid rgba(15, 76, 117, 0.14);
|
|
border-radius: 16px;
|
|
box-shadow: 0 22px 50px rgba(2, 32, 71, 0.22);
|
|
}
|
|
|
|
.create-contact-modal .modal-header {
|
|
border-bottom: 1px solid rgba(15, 76, 117, 0.12);
|
|
background: linear-gradient(180deg, rgba(15, 76, 117, 0.06) 0%, rgba(15, 76, 117, 0.02) 100%);
|
|
}
|
|
|
|
.create-contact-modal .modal-title {
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.company-picker {
|
|
border: 1px solid rgba(15, 76, 117, 0.18);
|
|
border-radius: 12px;
|
|
padding: 0.6rem;
|
|
background: rgba(15, 76, 117, 0.02);
|
|
}
|
|
|
|
.company-search-input {
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.company-results {
|
|
margin-top: 0.5rem;
|
|
max-height: 180px;
|
|
overflow: auto;
|
|
border: 1px solid rgba(15, 76, 117, 0.12);
|
|
border-radius: 10px;
|
|
background: var(--bg-card);
|
|
}
|
|
|
|
.company-result-item {
|
|
width: 100%;
|
|
border: 0;
|
|
border-bottom: 1px solid rgba(15, 76, 117, 0.08);
|
|
padding: 0.5rem 0.65rem;
|
|
text-align: left;
|
|
background: transparent;
|
|
color: var(--text-primary);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.5rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.company-result-item:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.company-result-item:hover {
|
|
background: rgba(15, 76, 117, 0.08);
|
|
}
|
|
|
|
.company-result-item.selected {
|
|
background: rgba(15, 76, 117, 0.12);
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.selected-companies {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.4rem;
|
|
margin-top: 0.55rem;
|
|
}
|
|
|
|
.company-chip {
|
|
border-radius: 999px;
|
|
border: 1px solid rgba(15, 76, 117, 0.25);
|
|
background: rgba(15, 76, 117, 0.1);
|
|
color: var(--accent);
|
|
font-size: 0.76rem;
|
|
padding: 0.22rem 0.55rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.35rem;
|
|
}
|
|
|
|
.company-chip button {
|
|
border: 0;
|
|
background: transparent;
|
|
color: inherit;
|
|
line-height: 1;
|
|
padding: 0;
|
|
}
|
|
|
|
@media (max-width: 992px) {
|
|
.contacts-toolbar {
|
|
width: 100%;
|
|
flex-direction: column;
|
|
align-items: stretch !important;
|
|
}
|
|
|
|
.toolbar-search-slot {
|
|
width: 100%;
|
|
justify-content: stretch;
|
|
}
|
|
|
|
.search-wrap {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-5 contacts-toolbar">
|
|
<div>
|
|
<h2 class="fw-bold mb-1">Kontakter</h2>
|
|
<p class="text-muted mb-0">Administrer kontaktpersoner</p>
|
|
</div>
|
|
<div class="toolbar-search-slot">
|
|
<div class="search-wrap">
|
|
<div class="search-label"><i class="bi bi-search"></i>Hurtig søgning</div>
|
|
<div class="search-input-shell">
|
|
<i class="bi bi-search search-icon"></i>
|
|
<input type="search" id="searchInput" class="header-search" placeholder="Søg navn, email, telefon eller firma..." autocomplete="off" spellcheck="false">
|
|
<button type="button" id="searchClearBtn" class="search-clear d-none" aria-label="Ryd søgning" title="Ryd søgning">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="showCreateContactModal()">
|
|
<i class="bi bi-plus-lg me-2"></i>Opret Kontakt
|
|
</button>
|
|
</div>
|
|
|
|
<div class="mb-4 d-flex gap-2 flex-wrap">
|
|
<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">
|
|
Alle Kontakter <span id="countAll" class="ms-1"></span>
|
|
</button>
|
|
<button class="filter-btn" data-filter="active" onclick="setFilter('active')">
|
|
Aktive <span id="countActive" class="ms-1"></span>
|
|
</button>
|
|
<button class="filter-btn" data-filter="inactive" onclick="setFilter('inactive')">
|
|
Inaktive <span id="countInactive" class="ms-1"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="card p-4 contacts-shell">
|
|
<div class="table-utility-bar">
|
|
<div class="dropdown">
|
|
<button class="column-toggle-btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
<i class="bi bi-layout-three-columns me-1"></i>Kolonner
|
|
</button>
|
|
<div class="dropdown-menu dropdown-menu-end column-toggle-menu" id="columnToggleMenu">
|
|
<label class="dropdown-item column-toggle-item">
|
|
<input class="form-check-input me-2 column-toggle-input" type="checkbox" data-column="title" checked>
|
|
Titel
|
|
</label>
|
|
<label class="dropdown-item column-toggle-item">
|
|
<input class="form-check-input me-2 column-toggle-input" type="checkbox" data-column="companies" checked>
|
|
Firmaer
|
|
</label>
|
|
<label class="dropdown-item column-toggle-item">
|
|
<input class="form-check-input me-2 column-toggle-input" type="checkbox" data-column="updated" checked>
|
|
Opdateret
|
|
</label>
|
|
<label class="dropdown-item column-toggle-item">
|
|
<input class="form-check-input me-2 column-toggle-input" type="checkbox" data-column="status" checked>
|
|
Status
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive contacts-table-wrap">
|
|
<table class="table table-hover align-middle contacts-table">
|
|
<thead>
|
|
<tr>
|
|
<th>
|
|
<button type="button" class="sort-btn" data-sort-key="name" onclick="setSort('name')">
|
|
Navn <i class="bi bi-arrow-down-up sort-indicator"></i>
|
|
</button>
|
|
</th>
|
|
<th class="col-key">Vigtig Info</th>
|
|
<th class="col-title">
|
|
<button type="button" class="sort-btn" data-sort-key="title" onclick="setSort('title')">
|
|
Titel <i class="bi bi-arrow-down-up sort-indicator"></i>
|
|
</button>
|
|
</th>
|
|
<th class="col-companies">
|
|
<button type="button" class="sort-btn" data-sort-key="company_count" onclick="setSort('company_count')">
|
|
Firmaer <i class="bi bi-arrow-down-up sort-indicator"></i>
|
|
</button>
|
|
</th>
|
|
<th class="col-updated">
|
|
<button type="button" class="sort-btn" data-sort-key="updated_at" onclick="setSort('updated_at')">
|
|
Opdateret <i class="bi bi-arrow-down-up sort-indicator"></i>
|
|
</button>
|
|
</th>
|
|
<th class="col-status">
|
|
<button type="button" class="sort-btn" data-sort-key="is_active" onclick="setSort('is_active')">
|
|
Status <i class="bi bi-arrow-down-up sort-indicator"></i>
|
|
</button>
|
|
</th>
|
|
<th class="text-end">Handlinger</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="contactsTableBody">
|
|
<tr>
|
|
<td colspan="7" class="text-center py-5">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div class="d-flex justify-content-between align-items-center mt-4">
|
|
<div class="text-muted small">
|
|
Viser <span id="showingStart">0</span>-<span id="showingEnd">0</span> af <span id="totalCount">0</span> kontakter
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="pagination-btn" id="prevBtn" onclick="previousPage()">
|
|
<i class="bi bi-chevron-left"></i> Forrige
|
|
</button>
|
|
<button class="pagination-btn" id="nextBtn" onclick="nextPage()">
|
|
Næste <i class="bi bi-chevron-right"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Contact Modal -->
|
|
<div class="modal fade create-contact-modal" id="createContactModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Opret Ny Kontakt</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="createContactForm">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Fornavn <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="firstNameInput" required>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Efternavn <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="lastNameInput" required>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Email</label>
|
|
<input type="email" class="form-control" id="emailInput">
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Telefon</label>
|
|
<input type="text" class="form-control" id="phoneInput">
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Mobil</label>
|
|
<input type="text" class="form-control" id="mobileInput">
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Titel</label>
|
|
<input type="text" class="form-control" id="titleInput" placeholder="CEO, CTO, Manager...">
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Afdeling</label>
|
|
<input type="text" class="form-control" id="departmentInput">
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Rolle</label>
|
|
<input type="text" class="form-control" id="roleInput" placeholder="Primær kontakt, Fakturering...">
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<label class="form-label">Firmaer</label>
|
|
<div class="company-picker">
|
|
<input
|
|
type="search"
|
|
id="companySearchInput"
|
|
class="form-control company-search-input"
|
|
placeholder="Søg firma..."
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
>
|
|
<div class="company-results" id="companyResults"></div>
|
|
<div class="selected-companies" id="selectedCompanies"></div>
|
|
</div>
|
|
<div class="form-text">Vælg et eller flere firmaer ved at søge og klikke.</div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="isPrimaryInput">
|
|
<label class="form-check-label" for="isPrimaryInput">
|
|
Primær kontakt (for første valgte firma)
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<label class="form-label">Noter</label>
|
|
<textarea class="form-control" id="notesInput" rows="3"></textarea>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="isActiveInput" checked>
|
|
<label class="form-check-label" for="isActiveInput">
|
|
Aktiv kontakt
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</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="createContact()">
|
|
<i class="bi bi-plus-lg me-2"></i>Opret Kontakt
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Contact Modal -->
|
|
<div class="modal fade" id="editContactModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Rediger Kontakt</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="editContactForm">
|
|
<input type="hidden" id="editContactId">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Fornavn <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="editFirstNameInput" required>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Efternavn <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="editLastNameInput" required>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Email</label>
|
|
<input type="email" class="form-control" id="editEmailInput">
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Telefon</label>
|
|
<input type="text" class="form-control" id="editPhoneInput">
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Mobil</label>
|
|
<input type="text" class="form-control" id="editMobileInput">
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Titel</label>
|
|
<input type="text" class="form-control" id="editTitleInput" placeholder="CEO, CTO, Manager...">
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label">Afdeling</label>
|
|
<input type="text" class="form-control" id="editDepartmentInput">
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="editIsActiveInput">
|
|
<label class="form-check-label" for="editIsActiveInput">
|
|
Aktiv kontakt
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</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="saveEditContact()">
|
|
<i class="bi bi-check-lg me-2"></i>Gem Ændringer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
let currentPage = 0;
|
|
let pageSize = 20;
|
|
let currentFilter = 'all';
|
|
let searchQuery = '';
|
|
let totalContacts = 0;
|
|
let searchTimeout = null;
|
|
let currentRequestController = null;
|
|
let lastLoadedQueryKey = '';
|
|
let availableCompanies = [];
|
|
let selectedCompanyIds = new Set();
|
|
let currentContactsData = [];
|
|
let currentSort = {
|
|
key: 'name',
|
|
direction: 'asc'
|
|
};
|
|
let visibleColumns = {
|
|
title: true,
|
|
companies: true,
|
|
updated: true,
|
|
status: true
|
|
};
|
|
|
|
// Load contacts on page load
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadTablePreferences();
|
|
applyColumnVisibility();
|
|
updateSortIndicators();
|
|
loadContacts();
|
|
loadCompaniesForSelect();
|
|
|
|
const searchInput = document.getElementById('searchInput');
|
|
const clearBtn = document.getElementById('searchClearBtn');
|
|
|
|
const triggerSearch = () => {
|
|
const nextSearch = searchInput.value.trim();
|
|
if (nextSearch === searchQuery) {
|
|
toggleClearButton(nextSearch);
|
|
return;
|
|
}
|
|
searchQuery = nextSearch;
|
|
currentPage = 0;
|
|
toggleClearButton(searchQuery);
|
|
loadContacts();
|
|
};
|
|
|
|
searchInput.addEventListener('input', (e) => {
|
|
clearTimeout(searchTimeout);
|
|
toggleClearButton(e.target.value.trim());
|
|
searchTimeout = setTimeout(() => {
|
|
triggerSearch();
|
|
}, 300);
|
|
});
|
|
|
|
searchInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
clearTimeout(searchTimeout);
|
|
triggerSearch();
|
|
return;
|
|
}
|
|
if (e.key === 'Escape') {
|
|
if (!searchInput.value) {
|
|
toggleClearButton('');
|
|
return;
|
|
}
|
|
searchInput.value = '';
|
|
clearTimeout(searchTimeout);
|
|
triggerSearch();
|
|
}
|
|
});
|
|
|
|
clearBtn.addEventListener('click', () => {
|
|
if (!searchInput.value) {
|
|
toggleClearButton('');
|
|
return;
|
|
}
|
|
searchInput.value = '';
|
|
clearTimeout(searchTimeout);
|
|
triggerSearch();
|
|
searchInput.focus();
|
|
});
|
|
|
|
document.getElementById('companySearchInput')?.addEventListener('input', (e) => {
|
|
renderCompanyResults(e.target.value || '');
|
|
});
|
|
|
|
document.querySelectorAll('.column-toggle-input').forEach((input) => {
|
|
input.addEventListener('change', (e) => {
|
|
const column = e.target.getAttribute('data-column');
|
|
visibleColumns[column] = !!e.target.checked;
|
|
persistTablePreferences();
|
|
applyColumnVisibility();
|
|
});
|
|
});
|
|
});
|
|
|
|
function setFilter(filter) {
|
|
currentFilter = filter;
|
|
currentPage = 0;
|
|
|
|
// Update active button
|
|
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
event.target.classList.add('active');
|
|
|
|
loadContacts();
|
|
}
|
|
|
|
async function loadContacts() {
|
|
const tbody = document.getElementById('contactsTableBody');
|
|
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5"><div class="spinner-border text-primary"></div></td></tr>';
|
|
|
|
if (currentRequestController) {
|
|
currentRequestController.abort();
|
|
}
|
|
currentRequestController = new AbortController();
|
|
|
|
try {
|
|
// Build query parameters
|
|
let params = new URLSearchParams({
|
|
limit: pageSize,
|
|
offset: currentPage * pageSize
|
|
});
|
|
|
|
if (searchQuery) {
|
|
params.append('search', searchQuery);
|
|
}
|
|
|
|
if (currentFilter === 'active') {
|
|
params.append('is_active', 'true');
|
|
} else if (currentFilter === 'inactive') {
|
|
params.append('is_active', 'false');
|
|
}
|
|
|
|
const queryKey = `${currentPage}|${pageSize}|${searchQuery}|${currentFilter}`;
|
|
if (queryKey === lastLoadedQueryKey) {
|
|
return;
|
|
}
|
|
lastLoadedQueryKey = queryKey;
|
|
|
|
const response = await fetch(`/api/v1/contacts?${params}`, { signal: currentRequestController.signal });
|
|
const data = await response.json();
|
|
|
|
totalContacts = data.total;
|
|
currentContactsData = Array.isArray(data.contacts) ? data.contacts : [];
|
|
displayContacts(currentContactsData);
|
|
updatePagination(data.total);
|
|
|
|
} catch (error) {
|
|
if (error.name === 'AbortError') {
|
|
return;
|
|
}
|
|
console.error('Failed to load contacts:', error);
|
|
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5 text-danger">Kunne ikke indlæse kontakter</td></tr>';
|
|
} finally {
|
|
currentRequestController = null;
|
|
}
|
|
}
|
|
|
|
function toggleClearButton(value) {
|
|
document.getElementById('searchClearBtn')?.classList.toggle('d-none', !value);
|
|
}
|
|
|
|
function displayContacts(contacts) {
|
|
const tbody = document.getElementById('contactsTableBody');
|
|
const sortedContacts = getSortedContacts(contacts);
|
|
|
|
if (!sortedContacts || sortedContacts.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5 text-muted">Ingen kontakter fundet</td></tr>';
|
|
applyColumnVisibility();
|
|
updateSortIndicators();
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = sortedContacts.map(contact => {
|
|
const initials = getInitials(contact.first_name, contact.last_name);
|
|
const statusBadge = contact.is_active
|
|
? '<span class="status-pill active">Aktiv</span>'
|
|
: '<span class="status-pill inactive">Inaktiv</span>';
|
|
|
|
const companyCount = contact.company_count || 0;
|
|
const companyNames = contact.company_names || [];
|
|
const fallbackCompany = (contact.user_company || '').trim();
|
|
const companyDisplay = companyNames.length > 0
|
|
? companyNames.slice(0, 2).join(', ') + (companyNames.length > 2 ? '...' : '')
|
|
: (fallbackCompany || '-');
|
|
const effectiveCompanyCount = companyCount > 0 ? companyCount : (fallbackCompany ? 1 : 0);
|
|
const fullName = `${contact.first_name || ''} ${contact.last_name || ''}`.trim();
|
|
const preferredPhone = contact.mobile || contact.phone || '';
|
|
const hasEmail = !!contact.email;
|
|
const hasPreferredPhone = !!preferredPhone;
|
|
const safeName = escapeHtml(`${contact.first_name || ''} ${contact.last_name || ''}`.trim() || '-');
|
|
const safeDepartment = escapeHtml(contact.department || '-');
|
|
const safeEmail = escapeHtml(contact.email || '-');
|
|
const safeTitle = escapeHtml(contact.title || '-');
|
|
const safePhone = escapeHtml(preferredPhone || '-');
|
|
const companiesTitle = escapeHtml(companyNames.length ? companyNames.join(', ') : fallbackCompany);
|
|
const updatedAt = formatContactDate(contact.updated_at || contact.created_at);
|
|
|
|
return `
|
|
<tr onclick="viewContact(${contact.id})">
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="contact-avatar me-3">${initials}</div>
|
|
<div>
|
|
<div class="contact-name">${safeName}</div>
|
|
<div class="contact-subline">${safeDepartment}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="contact-data-stack">
|
|
<div class="key-line">
|
|
<span class="key-label">Email</span>
|
|
${hasEmail
|
|
? `<a class="key-value" href="mailto:${safeEmail}" onclick="event.stopPropagation()">${safeEmail}</a>`
|
|
: '<span class="muted-data">Ikke angivet</span>'}
|
|
</div>
|
|
<div class="key-line">
|
|
<span class="key-label">Telefon</span>
|
|
<span class="key-value">${safePhone}</span>
|
|
</div>
|
|
<div class="contact-quick-actions">
|
|
${hasPreferredPhone
|
|
? `<button class="btn btn-sm btn-outline-success py-0 px-2" onclick="event.stopPropagation(); contactsCallViaYealink('${escapeHtml(preferredPhone)}')">Ring op</button>`
|
|
: ''}
|
|
${contact.mobile
|
|
? `<button class="btn btn-sm btn-outline-primary py-0 px-2" onclick="event.stopPropagation(); openSmsPrompt('${escapeHtml(contact.mobile)}', '${escapeHtml(fullName)}', ${contact.id || 'null'})">SMS</button>`
|
|
: ''}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="text-muted col-title">${safeTitle}</td>
|
|
<td class="col-companies">
|
|
<span class="company-count-chip" title="${companiesTitle}">
|
|
<i class="bi bi-building"></i>${effectiveCompanyCount}
|
|
</span>
|
|
${companyDisplay !== '-' ? '<div class="small text-muted mt-1">' + escapeHtml(companyDisplay) + '</div>' : ''}
|
|
</td>
|
|
<td class="col-updated">
|
|
<span class="updated-at">${updatedAt}</span>
|
|
</td>
|
|
<td class="col-status">${statusBadge}</td>
|
|
<td class="text-end">
|
|
<div class="btn-group">
|
|
<button class="btn btn-sm btn-table-action" onclick="event.stopPropagation(); viewContact(${contact.id})" title="Vis kontakt">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-table-action" onclick="event.stopPropagation(); editContact(${contact.id})" title="Rediger kontakt">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
applyColumnVisibility();
|
|
updateSortIndicators();
|
|
}
|
|
|
|
function setSort(key) {
|
|
if (currentSort.key === key) {
|
|
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
currentSort.key = key;
|
|
currentSort.direction = 'asc';
|
|
}
|
|
|
|
persistTablePreferences();
|
|
displayContacts(currentContactsData);
|
|
}
|
|
|
|
function getSortedContacts(contacts) {
|
|
const list = Array.isArray(contacts) ? [...contacts] : [];
|
|
const direction = currentSort.direction === 'desc' ? -1 : 1;
|
|
|
|
return list.sort((a, b) => {
|
|
const key = currentSort.key;
|
|
|
|
if (key === 'company_count') {
|
|
return ((Number(a.company_count) || 0) - (Number(b.company_count) || 0)) * direction;
|
|
}
|
|
|
|
if (key === 'updated_at') {
|
|
const dateA = new Date(a.updated_at || a.created_at || 0).getTime() || 0;
|
|
const dateB = new Date(b.updated_at || b.created_at || 0).getTime() || 0;
|
|
return (dateA - dateB) * direction;
|
|
}
|
|
|
|
if (key === 'is_active') {
|
|
return ((a.is_active ? 1 : 0) - (b.is_active ? 1 : 0)) * direction;
|
|
}
|
|
|
|
if (key === 'title') {
|
|
const titleA = String(a.title || '').toLowerCase();
|
|
const titleB = String(b.title || '').toLowerCase();
|
|
return titleA.localeCompare(titleB, 'da') * direction;
|
|
}
|
|
|
|
const nameA = `${a.last_name || ''} ${a.first_name || ''}`.trim().toLowerCase();
|
|
const nameB = `${b.last_name || ''} ${b.first_name || ''}`.trim().toLowerCase();
|
|
return nameA.localeCompare(nameB, 'da') * direction;
|
|
});
|
|
}
|
|
|
|
function updateSortIndicators() {
|
|
document.querySelectorAll('.sort-btn').forEach((btn) => {
|
|
const key = btn.getAttribute('data-sort-key');
|
|
const icon = btn.querySelector('.sort-indicator');
|
|
if (!icon) return;
|
|
|
|
btn.classList.remove('active');
|
|
icon.className = 'bi bi-arrow-down-up sort-indicator';
|
|
|
|
if (key === currentSort.key) {
|
|
btn.classList.add('active');
|
|
icon.className = currentSort.direction === 'asc'
|
|
? 'bi bi-sort-down-alt sort-indicator'
|
|
: 'bi bi-sort-up-alt sort-indicator';
|
|
}
|
|
});
|
|
}
|
|
|
|
function applyColumnVisibility() {
|
|
toggleColumnClass('col-title', visibleColumns.title);
|
|
toggleColumnClass('col-companies', visibleColumns.companies);
|
|
toggleColumnClass('col-updated', visibleColumns.updated);
|
|
toggleColumnClass('col-status', visibleColumns.status);
|
|
|
|
document.querySelectorAll('.column-toggle-input').forEach((input) => {
|
|
const key = input.getAttribute('data-column');
|
|
input.checked = !!visibleColumns[key];
|
|
});
|
|
}
|
|
|
|
function toggleColumnClass(columnClass, isVisible) {
|
|
document.querySelectorAll(`.${columnClass}`).forEach((el) => {
|
|
el.classList.toggle('d-none', !isVisible);
|
|
});
|
|
}
|
|
|
|
function loadTablePreferences() {
|
|
try {
|
|
const raw = localStorage.getItem('contactsTablePrefsV1');
|
|
if (!raw) return;
|
|
const parsed = JSON.parse(raw);
|
|
|
|
if (parsed && parsed.visibleColumns) {
|
|
visibleColumns = {
|
|
...visibleColumns,
|
|
...parsed.visibleColumns
|
|
};
|
|
}
|
|
|
|
if (parsed && parsed.currentSort && parsed.currentSort.key) {
|
|
currentSort = {
|
|
key: parsed.currentSort.key,
|
|
direction: parsed.currentSort.direction === 'desc' ? 'desc' : 'asc'
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.warn('Kunne ikke indlæse tabel-indstillinger', error);
|
|
}
|
|
}
|
|
|
|
function persistTablePreferences() {
|
|
try {
|
|
localStorage.setItem('contactsTablePrefsV1', JSON.stringify({
|
|
visibleColumns,
|
|
currentSort
|
|
}));
|
|
} catch (error) {
|
|
console.warn('Kunne ikke gemme tabel-indstillinger', error);
|
|
}
|
|
}
|
|
|
|
function updatePagination(total) {
|
|
const start = currentPage * pageSize + 1;
|
|
const end = Math.min((currentPage + 1) * pageSize, total);
|
|
|
|
document.getElementById('showingStart').textContent = total > 0 ? start : 0;
|
|
document.getElementById('showingEnd').textContent = end;
|
|
document.getElementById('totalCount').textContent = total;
|
|
|
|
// Update buttons
|
|
document.getElementById('prevBtn').disabled = currentPage === 0;
|
|
document.getElementById('nextBtn').disabled = end >= total;
|
|
}
|
|
|
|
function previousPage() {
|
|
if (currentPage > 0) {
|
|
currentPage--;
|
|
loadContacts();
|
|
}
|
|
}
|
|
|
|
function nextPage() {
|
|
if ((currentPage + 1) * pageSize < totalContacts) {
|
|
currentPage++;
|
|
loadContacts();
|
|
}
|
|
}
|
|
|
|
function viewContact(contactId) {
|
|
window.location.href = `/contacts/${contactId}`;
|
|
}
|
|
|
|
function editContact(contactId) {
|
|
// Load contact data and open edit modal
|
|
loadContactForEdit(contactId);
|
|
}
|
|
|
|
let contactsCurrentUserId = null;
|
|
|
|
async function ensureContactsCurrentUserId() {
|
|
if (contactsCurrentUserId !== null) return contactsCurrentUserId;
|
|
try {
|
|
const res = await fetch('/api/v1/auth/me', { credentials: 'include' });
|
|
if (!res.ok) return null;
|
|
const me = await res.json();
|
|
contactsCurrentUserId = Number(me?.id) || null;
|
|
return contactsCurrentUserId;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function contactsCallViaYealink(number) {
|
|
const clean = String(number || '').trim();
|
|
if (!clean || clean === '-') {
|
|
alert('Intet gyldigt nummer at ringe til');
|
|
return;
|
|
}
|
|
|
|
const userId = await ensureContactsCurrentUserId();
|
|
try {
|
|
const res = await fetch('/api/v1/telefoni/click-to-call', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({ number: clean, user_id: userId })
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const t = await res.text();
|
|
alert('Ring ud fejlede: ' + t);
|
|
return;
|
|
}
|
|
alert('Ringer ud via Yealink...');
|
|
} catch (e) {
|
|
alert('Kunne ikke starte opkald');
|
|
}
|
|
}
|
|
|
|
async function loadContactForEdit(contactId) {
|
|
try {
|
|
const response = await fetch(`/api/v1/contacts/${contactId}`);
|
|
if (!response.ok) throw new Error('Kunne ikke indlæse kontakt');
|
|
|
|
const contact = await response.json();
|
|
|
|
// Fill form
|
|
document.getElementById('editContactId').value = contactId;
|
|
document.getElementById('editFirstNameInput').value = contact.first_name || '';
|
|
document.getElementById('editLastNameInput').value = contact.last_name || '';
|
|
document.getElementById('editEmailInput').value = contact.email || '';
|
|
document.getElementById('editPhoneInput').value = contact.phone || '';
|
|
document.getElementById('editMobileInput').value = contact.mobile || '';
|
|
document.getElementById('editTitleInput').value = contact.title || '';
|
|
document.getElementById('editDepartmentInput').value = contact.department || '';
|
|
document.getElementById('editIsActiveInput').checked = contact.is_active || false;
|
|
|
|
// Show modal
|
|
const modal = new bootstrap.Modal(document.getElementById('editContactModal'));
|
|
modal.show();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load contact:', error);
|
|
alert('Fejl: Kunne ikke indlæse kontakt');
|
|
}
|
|
}
|
|
|
|
async function saveEditContact() {
|
|
const contactId = document.getElementById('editContactId').value;
|
|
const firstName = document.getElementById('editFirstNameInput').value.trim();
|
|
const lastName = document.getElementById('editLastNameInput').value.trim();
|
|
|
|
if (!firstName || !lastName) {
|
|
alert('Fornavn og efternavn er påkrævet');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/v1/contacts/${contactId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
first_name: firstName,
|
|
last_name: lastName,
|
|
email: document.getElementById('editEmailInput').value || null,
|
|
phone: document.getElementById('editPhoneInput').value || null,
|
|
mobile: document.getElementById('editMobileInput').value || null,
|
|
title: document.getElementById('editTitleInput').value || null,
|
|
department: document.getElementById('editDepartmentInput').value || null,
|
|
is_active: document.getElementById('editIsActiveInput').checked
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Kunne ikke gemme kontakt');
|
|
}
|
|
|
|
// Close modal and reload
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('editContactModal'));
|
|
modal.hide();
|
|
loadContacts();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to save contact:', error);
|
|
alert('Fejl: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function loadCompaniesForSelect() {
|
|
try {
|
|
const response = await fetch('/api/v1/customers?limit=1000');
|
|
const data = await response.json();
|
|
|
|
availableCompanies = Array.isArray(data.customers)
|
|
? data.customers.map((c) => ({ id: Number(c.id), name: String(c.name || '').trim() }))
|
|
: [];
|
|
renderCompanyResults(document.getElementById('companySearchInput')?.value || '');
|
|
renderSelectedCompanies();
|
|
} catch (error) {
|
|
console.error('Failed to load companies:', error);
|
|
}
|
|
}
|
|
|
|
function renderCompanyResults(query) {
|
|
const host = document.getElementById('companyResults');
|
|
if (!host) return;
|
|
|
|
const needle = String(query || '').trim().toLowerCase();
|
|
let list = availableCompanies;
|
|
if (needle) {
|
|
list = availableCompanies.filter((c) => c.name.toLowerCase().includes(needle));
|
|
}
|
|
|
|
list = list.slice(0, 80);
|
|
|
|
if (!list.length) {
|
|
host.innerHTML = '<div class="px-3 py-2 text-muted small">Ingen firmaer fundet</div>';
|
|
return;
|
|
}
|
|
|
|
host.innerHTML = list.map((c) => {
|
|
const selected = selectedCompanyIds.has(c.id);
|
|
return `
|
|
<button type="button" class="company-result-item ${selected ? 'selected' : ''}" onclick="toggleCompanySelection(${c.id})">
|
|
<span>${escapeHtml(c.name)}</span>
|
|
<span>${selected ? '<i class="bi bi-check2"></i>' : ''}</span>
|
|
</button>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function toggleCompanySelection(companyId) {
|
|
const id = Number(companyId);
|
|
if (!Number.isFinite(id)) return;
|
|
|
|
if (selectedCompanyIds.has(id)) {
|
|
selectedCompanyIds.delete(id);
|
|
} else {
|
|
selectedCompanyIds.add(id);
|
|
}
|
|
|
|
renderSelectedCompanies();
|
|
renderCompanyResults(document.getElementById('companySearchInput')?.value || '');
|
|
}
|
|
|
|
function renderSelectedCompanies() {
|
|
const host = document.getElementById('selectedCompanies');
|
|
if (!host) return;
|
|
|
|
const selected = availableCompanies.filter((c) => selectedCompanyIds.has(c.id));
|
|
if (!selected.length) {
|
|
host.innerHTML = '<span class="text-muted small">Ingen firmaer valgt</span>';
|
|
return;
|
|
}
|
|
|
|
host.innerHTML = selected.map((c) => `
|
|
<span class="company-chip">
|
|
${escapeHtml(c.name)}
|
|
<button type="button" title="Fjern" onclick="toggleCompanySelection(${c.id})"><i class="bi bi-x-lg"></i></button>
|
|
</span>
|
|
`).join('');
|
|
}
|
|
|
|
function showCreateContactModal() {
|
|
// Reset form
|
|
document.getElementById('createContactForm').reset();
|
|
document.getElementById('isActiveInput').checked = true;
|
|
selectedCompanyIds = new Set();
|
|
const companySearchInput = document.getElementById('companySearchInput');
|
|
if (companySearchInput) {
|
|
companySearchInput.value = '';
|
|
}
|
|
renderCompanyResults('');
|
|
renderSelectedCompanies();
|
|
|
|
// Show modal
|
|
const modal = new bootstrap.Modal(document.getElementById('createContactModal'));
|
|
modal.show();
|
|
}
|
|
|
|
async function createContact() {
|
|
const firstName = document.getElementById('firstNameInput').value.trim();
|
|
const lastName = document.getElementById('lastNameInput').value.trim();
|
|
|
|
if (!firstName || !lastName) {
|
|
alert('Fornavn og efternavn er påkrævet');
|
|
return;
|
|
}
|
|
|
|
// Get selected company IDs
|
|
const companyIds = Array.from(selectedCompanyIds);
|
|
|
|
const contactData = {
|
|
first_name: firstName,
|
|
last_name: lastName,
|
|
email: document.getElementById('emailInput').value.trim() || null,
|
|
phone: document.getElementById('phoneInput').value.trim() || null,
|
|
mobile: document.getElementById('mobileInput').value.trim() || null,
|
|
title: document.getElementById('titleInput').value.trim() || null,
|
|
department: document.getElementById('departmentInput').value.trim() || null,
|
|
company_ids: companyIds,
|
|
is_primary: document.getElementById('isPrimaryInput').checked,
|
|
role: document.getElementById('roleInput').value.trim() || null,
|
|
notes: document.getElementById('notesInput').value.trim() || null,
|
|
is_active: document.getElementById('isActiveInput').checked
|
|
};
|
|
|
|
try {
|
|
const response = await fetch('/api/v1/contacts', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(contactData)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Kunne ikke oprette kontakt');
|
|
}
|
|
|
|
const newContact = await response.json();
|
|
|
|
// Close modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('createContactModal'));
|
|
modal.hide();
|
|
|
|
// Reload contact list
|
|
await loadContacts();
|
|
|
|
// Show success message
|
|
alert('Kontakt oprettet succesfuldt!');
|
|
|
|
} catch (error) {
|
|
console.error('Failed to create contact:', error);
|
|
alert('Fejl ved oprettelse af kontakt: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function getInitials(firstName, lastName) {
|
|
if (!firstName && !lastName) return '?';
|
|
const first = firstName ? firstName[0] : '';
|
|
const last = lastName ? lastName[0] : '';
|
|
return (first + last).toUpperCase();
|
|
}
|
|
|
|
function formatContactDate(value) {
|
|
if (!value) return '-';
|
|
const parsed = new Date(value);
|
|
if (Number.isNaN(parsed.getTime())) return '-';
|
|
return parsed.toLocaleDateString('da-DK', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: '2-digit'
|
|
});
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
</script>
|
|
{% endblock %}
|