- Created migration scripts for AnyDesk sessions and hardware assets. - Implemented apply_migration_115.py to execute migration for AnyDesk sessions. - Added set_customer_wiki_slugs.py script to update customer wiki slugs based on a predefined folder list. - Developed run_migration.py to apply AnyDesk migration schema. - Added tests for Service Contract Wizard to ensure functionality and dry-run mode.
884 lines
35 KiB
HTML
884 lines
35 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Kontakt Detaljer - BMC Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.contact-header {
|
|
background: var(--accent);
|
|
color: white;
|
|
padding: 3rem 2rem;
|
|
border-radius: 12px;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.contact-avatar-large {
|
|
width: 80px;
|
|
height: 80px;
|
|
border-radius: 50%;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: bold;
|
|
font-size: 2rem;
|
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
|
}
|
|
|
|
.nav-pills-vertical {
|
|
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
|
padding-right: 0;
|
|
}
|
|
|
|
.nav-pills-vertical .nav-link {
|
|
color: var(--text-secondary);
|
|
border-radius: 8px 0 0 8px;
|
|
padding: 1rem 1.5rem;
|
|
font-weight: 500;
|
|
margin-bottom: 0.5rem;
|
|
transition: all 0.2s;
|
|
text-align: left;
|
|
}
|
|
|
|
.nav-pills-vertical .nav-link:hover {
|
|
background: var(--accent-light);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.nav-pills-vertical .nav-link.active {
|
|
background: var(--accent);
|
|
color: white;
|
|
}
|
|
|
|
.nav-pills-vertical .nav-link i {
|
|
width: 20px;
|
|
margin-right: 0.75rem;
|
|
}
|
|
|
|
.info-card {
|
|
background: var(--bg-card);
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
border-radius: 12px;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.info-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.75rem 0;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.info-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.info-label {
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.info-value {
|
|
color: var(--text-primary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.company-card {
|
|
background: var(--bg-card);
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
border-radius: 12px;
|
|
padding: 1.5rem;
|
|
transition: all 0.2s;
|
|
position: relative;
|
|
}
|
|
|
|
.company-card:hover {
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.session-card {
|
|
background: var(--bg-card);
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
border-radius: 12px;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.session-meta {
|
|
font-size: 0.9rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.remove-company-btn {
|
|
position: absolute;
|
|
top: 1rem;
|
|
right: 1rem;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Contact Header -->
|
|
<div class="contact-header">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div class="d-flex align-items-center">
|
|
<div class="contact-avatar-large me-4" id="contactAvatar">?</div>
|
|
<div>
|
|
<h1 class="fw-bold mb-2" id="contactName">Loading...</h1>
|
|
<div class="d-flex gap-3 align-items-center">
|
|
<span id="contactTitle"></span>
|
|
<span class="badge bg-white bg-opacity-20" id="contactStatus"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-light btn-sm" onclick="editContact()">
|
|
<i class="bi bi-pencil me-2"></i>Rediger
|
|
</button>
|
|
<button class="btn btn-light btn-sm" onclick="window.location.href='/contacts'">
|
|
<i class="bi bi-arrow-left me-2"></i>Tilbage
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content Layout with Sidebar Navigation -->
|
|
<div class="row">
|
|
<div class="col-lg-3 col-md-4">
|
|
<!-- Vertical Navigation -->
|
|
<ul class="nav nav-pills nav-pills-vertical flex-column" role="tablist">
|
|
<li class="nav-item">
|
|
<a class="nav-link active" data-bs-toggle="tab" href="#overview">
|
|
<i class="bi bi-info-circle"></i>Oversigt
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#companies">
|
|
<i class="bi bi-building"></i>Firmaer
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#remote-sessions">
|
|
<i class="bi bi-display"></i>Remote Sessions
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="col-lg-9 col-md-8">
|
|
<!-- Tab Content -->
|
|
<div class="tab-content">
|
|
<!-- Overview Tab -->
|
|
<div class="tab-pane fade show active" id="overview">
|
|
<div class="row g-4">
|
|
<div class="col-lg-6">
|
|
<div class="info-card">
|
|
<h5 class="fw-bold mb-4">Kontakt Oplysninger</h5>
|
|
<div class="info-row">
|
|
<span class="info-label">Navn</span>
|
|
<span class="info-value" id="fullName">-</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Email</span>
|
|
<span class="info-value" id="email">-</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Telefon</span>
|
|
<span class="info-value" id="phone">-</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Mobil</span>
|
|
<span class="info-value" id="mobile">-</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-6">
|
|
<div class="info-card">
|
|
<h5 class="fw-bold mb-4">Rolle & Stilling</h5>
|
|
<div class="info-row">
|
|
<span class="info-label">Titel</span>
|
|
<span class="info-value" id="title">-</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Afdeling</span>
|
|
<span class="info-value" id="department">-</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Status</span>
|
|
<span class="info-value" id="activeStatus">-</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Antal Firmaer</span>
|
|
<span class="info-value" id="companyCount">-</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<div class="info-card">
|
|
<h5 class="fw-bold mb-3">System Info</h5>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="info-row">
|
|
<span class="info-label">vTiger ID</span>
|
|
<span class="info-value" id="vtigerId">-</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="info-row">
|
|
<span class="info-label">Oprettet</span>
|
|
<span class="info-value" id="createdAt">-</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Companies Tab -->
|
|
<div class="tab-pane fade" id="companies">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="fw-bold mb-0">Tilknyttede Firmaer</h5>
|
|
<button class="btn btn-primary btn-sm" onclick="showAddCompanyModal()">
|
|
<i class="bi bi-plus-lg me-2"></i>Tilføj Firma
|
|
</button>
|
|
</div>
|
|
<div class="row g-4" id="companiesContainer">
|
|
<div class="col-12 text-center py-5">
|
|
<div class="spinner-border text-primary"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Remote Sessions Tab -->
|
|
<div class="tab-pane fade" id="remote-sessions">
|
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4">
|
|
<h5 class="fw-bold mb-0">Remote Sessions</h5>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<select class="form-select form-select-sm" id="sessionCompanySelect" style="min-width: 200px;"></select>
|
|
<input type="number" class="form-control form-control-sm" id="sessionSagIdInput" placeholder="Sag ID (valgfri)" style="max-width: 140px;">
|
|
<button class="btn btn-primary btn-sm" onclick="startRemoteSession()">
|
|
<i class="bi bi-play-circle me-2"></i>Start Session
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="loadRemoteSessions()">
|
|
<i class="bi bi-arrow-repeat me-2"></i>Opdater
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="remoteSessionAlert" class="alert alert-info d-none"></div>
|
|
<div class="row g-3" id="remoteSessionsContainer">
|
|
<div class="col-12 text-center py-5">
|
|
<div class="spinner-border text-primary"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Company Modal -->
|
|
<div class="modal fade" id="addCompanyModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Tilføj Firma til Kontakt</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="addCompanyForm">
|
|
<div class="mb-3">
|
|
<label class="form-label">Vælg Firma</label>
|
|
<select class="form-select" id="companySelectModal" required>
|
|
<option value="">Vælg et firma...</option>
|
|
<!-- Populated dynamically -->
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Rolle</label>
|
|
<input type="text" class="form-control" id="roleInputModal" placeholder="Primær kontakt, Fakturering...">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Noter</label>
|
|
<textarea class="form-control" id="notesInputModal" rows="3"></textarea>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="isPrimaryInputModal">
|
|
<label class="form-check-label" for="isPrimaryInputModal">
|
|
Primær kontakt for dette firma
|
|
</label>
|
|
</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="addCompanyToContact()">
|
|
<i class="bi bi-plus-lg me-2"></i>Tilføj
|
|
</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">
|
|
<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>
|
|
|
|
<!-- Worklog Suggestion Modal -->
|
|
<div class="modal fade" id="worklogSuggestionModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Forslag til tidsregistrering</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="worklogSuggestionBody"></div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Luk</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
const contactId = parseInt(window.location.pathname.split('/').pop());
|
|
let contactData = null;
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadContact();
|
|
loadCompaniesForSelect();
|
|
|
|
// Load companies when tab is shown
|
|
document.querySelector('a[href="#companies"]').addEventListener('shown.bs.tab', () => {
|
|
loadCompanies();
|
|
});
|
|
|
|
const remoteSessionsTab = document.querySelector('a[href="#remote-sessions"]');
|
|
if (remoteSessionsTab) {
|
|
remoteSessionsTab.addEventListener('shown.bs.tab', () => {
|
|
loadRemoteSessions();
|
|
});
|
|
}
|
|
});
|
|
|
|
async function loadContact() {
|
|
try {
|
|
const response = await fetch(`/api/v1/contacts/${contactId}`);
|
|
if (!response.ok) {
|
|
throw new Error('Contact not found');
|
|
}
|
|
|
|
contactData = await response.json();
|
|
displayContact(contactData);
|
|
} catch (error) {
|
|
console.error('Failed to load contact:', error);
|
|
alert('Kunne ikke indlæse kontakt');
|
|
window.location.href = '/contacts';
|
|
}
|
|
}
|
|
|
|
function displayContact(contact) {
|
|
// Update page title
|
|
document.title = `${contact.first_name} ${contact.last_name} - BMC Hub`;
|
|
|
|
// Header
|
|
const initials = getInitials(contact.first_name, contact.last_name);
|
|
document.getElementById('contactAvatar').textContent = initials;
|
|
document.getElementById('contactName').textContent = `${contact.first_name} ${contact.last_name}`;
|
|
document.getElementById('contactTitle').textContent = contact.title || '';
|
|
|
|
const statusBadge = contact.is_active
|
|
? '<i class="bi bi-check-circle me-1"></i>Aktiv'
|
|
: '<i class="bi bi-x-circle me-1"></i>Inaktiv';
|
|
document.getElementById('contactStatus').innerHTML = statusBadge;
|
|
|
|
// Contact Information
|
|
document.getElementById('fullName').textContent = `${contact.first_name} ${contact.last_name}`;
|
|
document.getElementById('email').textContent = contact.email || '-';
|
|
document.getElementById('phone').textContent = contact.phone || '-';
|
|
document.getElementById('mobile').textContent = contact.mobile || '-';
|
|
|
|
// Role & Position
|
|
document.getElementById('title').textContent = contact.title || '-';
|
|
document.getElementById('department').textContent = contact.department || '-';
|
|
document.getElementById('activeStatus').innerHTML = contact.is_active
|
|
? '<span class="badge bg-success">Aktiv</span>'
|
|
: '<span class="badge bg-secondary">Inaktiv</span>';
|
|
document.getElementById('companyCount').textContent = contact.companies ? contact.companies.length : 0;
|
|
|
|
populateSessionCompanySelect(contact);
|
|
|
|
// System Info
|
|
document.getElementById('vtigerId').textContent = contact.vtiger_id || '-';
|
|
document.getElementById('createdAt').textContent = new Date(contact.created_at).toLocaleString('da-DK');
|
|
|
|
// Load companies if tab is active
|
|
if (document.querySelector('a[href="#companies"]').classList.contains('active')) {
|
|
displayCompanies(contact.companies);
|
|
}
|
|
}
|
|
|
|
function populateSessionCompanySelect(contact) {
|
|
const select = document.getElementById('sessionCompanySelect');
|
|
if (!select) return;
|
|
|
|
const companies = contact.companies || [];
|
|
if (companies.length === 0) {
|
|
select.innerHTML = '<option value="">Ingen firmaer tilknyttet</option>';
|
|
select.disabled = true;
|
|
return;
|
|
}
|
|
|
|
const primary = companies.find(c => c.is_primary) || companies[0];
|
|
select.innerHTML = companies.map(c => {
|
|
const label = c.is_primary ? `${c.name} (Primær)` : c.name;
|
|
return `<option value="${c.id}">${escapeHtml(label)}</option>`;
|
|
}).join('');
|
|
|
|
select.value = String(primary.id);
|
|
select.disabled = false;
|
|
}
|
|
|
|
async function loadCompanies() {
|
|
if (!contactData) return;
|
|
displayCompanies(contactData.companies);
|
|
}
|
|
|
|
function displayCompanies(companies) {
|
|
const container = document.getElementById('companiesContainer');
|
|
|
|
if (!companies || companies.length === 0) {
|
|
container.innerHTML = '<div class="col-12 text-center py-5 text-muted">Ingen firmaer tilknyttet</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = companies.map(company => `
|
|
<div class="col-md-6">
|
|
<div class="company-card">
|
|
<button class="btn btn-sm btn-danger remove-company-btn" onclick="removeCompany(${company.id})" title="Fjern firma">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
<div class="d-flex align-items-start mb-3">
|
|
<div class="flex-grow-1">
|
|
<h6 class="fw-bold mb-1">
|
|
<a href="/customers/${company.id}" class="text-decoration-none">${escapeHtml(company.name)}</a>
|
|
</h6>
|
|
${company.is_primary ? '<span class="badge bg-primary">Primær Kontakt</span>' : ''}
|
|
</div>
|
|
</div>
|
|
${company.role ? `
|
|
<div class="mb-2">
|
|
<small class="text-muted">Rolle:</small>
|
|
<div>${escapeHtml(company.role)}</div>
|
|
</div>
|
|
` : ''}
|
|
${company.notes ? `
|
|
<div>
|
|
<small class="text-muted">Noter:</small>
|
|
<div class="small">${escapeHtml(company.notes)}</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
async function loadRemoteSessions() {
|
|
const container = document.getElementById('remoteSessionsContainer');
|
|
if (!container) return;
|
|
|
|
container.innerHTML = `
|
|
<div class="col-12 text-center py-5">
|
|
<div class="spinner-border text-primary"></div>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
const response = await fetch(`/api/v1/anydesk/sessions?contact_id=${contactId}`);
|
|
if (!response.ok) {
|
|
throw new Error('Kunne ikke hente sessioner');
|
|
}
|
|
|
|
const data = await response.json();
|
|
displayRemoteSessions(data.sessions || []);
|
|
} catch (error) {
|
|
console.error('Failed to load sessions:', error);
|
|
container.innerHTML = '<div class="col-12 text-center py-5 text-muted">Kunne ikke hente sessioner</div>';
|
|
}
|
|
}
|
|
|
|
function displayRemoteSessions(sessions) {
|
|
const container = document.getElementById('remoteSessionsContainer');
|
|
if (!container) return;
|
|
|
|
if (!sessions || sessions.length === 0) {
|
|
container.innerHTML = '<div class="col-12 text-center py-5 text-muted">Ingen remote sessions endnu</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = sessions.map(session => {
|
|
const badgeClass = getSessionBadgeClass(session.status);
|
|
const duration = session.duration_minutes ? `${session.duration_minutes} min` : '—';
|
|
const startedAt = session.started_at ? new Date(session.started_at).toLocaleString('da-DK') : '-';
|
|
const endedAt = session.ended_at ? new Date(session.ended_at).toLocaleString('da-DK') : '-';
|
|
const linkButton = session.session_link
|
|
? `<a class="btn btn-sm btn-outline-primary" href="${session.session_link}" target="_blank">Åbn Link</a>`
|
|
: '';
|
|
const worklogButton = session.status === 'completed'
|
|
? `<button class="btn btn-sm btn-outline-secondary" onclick="showWorklogSuggestion(${session.id})">Vis forslag</button>`
|
|
: '';
|
|
|
|
return `
|
|
<div class="col-12">
|
|
<div class="session-card">
|
|
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3">
|
|
<div>
|
|
<div class="d-flex align-items-center gap-2 mb-2">
|
|
<span class="badge ${badgeClass}">${escapeHtml(session.status || 'ukendt')}</span>
|
|
<strong>Session #${session.id}</strong>
|
|
</div>
|
|
<div class="session-meta">Start: ${startedAt} | Slut: ${endedAt} | Varighed: ${duration}</div>
|
|
${session.sag_id ? `<div class="session-meta">Sag ID: ${session.sag_id}</div>` : ''}
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
${linkButton}
|
|
${worklogButton}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
async function startRemoteSession() {
|
|
const alertBox = document.getElementById('remoteSessionAlert');
|
|
const select = document.getElementById('sessionCompanySelect');
|
|
|
|
if (!contactData || !select || select.disabled) {
|
|
alert('Kontakt skal have mindst et firma for at starte en session');
|
|
return;
|
|
}
|
|
|
|
const customerId = parseInt(select.value);
|
|
const sagIdValue = document.getElementById('sessionSagIdInput').value;
|
|
const sagId = sagIdValue ? parseInt(sagIdValue) : null;
|
|
|
|
try {
|
|
const response = await fetch('/api/v1/anydesk/start-session', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
customer_id: customerId,
|
|
contact_id: contactId,
|
|
sag_id: sagId,
|
|
description: `Remote support for ${contactData.first_name} ${contactData.last_name}`
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Kunne ikke starte session');
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (alertBox) {
|
|
alertBox.classList.remove('d-none');
|
|
alertBox.innerHTML = `Session startet. ${data.session_link ? `Link: <a href="${data.session_link}" target="_blank">${data.session_link}</a>` : ''}`;
|
|
}
|
|
|
|
if (data.session_link) {
|
|
window.open(data.session_link, '_blank');
|
|
}
|
|
|
|
await loadRemoteSessions();
|
|
} catch (error) {
|
|
console.error('Failed to start session:', error);
|
|
alert('Fejl: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function showWorklogSuggestion(sessionId) {
|
|
try {
|
|
const response = await fetch(`/api/v1/anydesk/sessions/${sessionId}/worklog-suggestion`);
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Kunne ikke hente forslag');
|
|
}
|
|
|
|
const suggestion = await response.json();
|
|
const body = document.getElementById('worklogSuggestionBody');
|
|
if (body) {
|
|
body.innerHTML = `
|
|
<div class="mb-2"><strong>Varighed:</strong> ${suggestion.duration_hours} timer</div>
|
|
<div class="mb-2"><strong>Start:</strong> ${new Date(suggestion.start_time).toLocaleString('da-DK')}</div>
|
|
<div class="mb-2"><strong>Slut:</strong> ${new Date(suggestion.end_time).toLocaleString('da-DK')}</div>
|
|
<div class="mb-2"><strong>Beskrivelse:</strong> ${escapeHtml(suggestion.description || '')}</div>
|
|
<div class="small text-muted">Forslaget kan bruges til manuel tidsregistrering</div>
|
|
`;
|
|
}
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('worklogSuggestionModal'));
|
|
modal.show();
|
|
} catch (error) {
|
|
console.error('Failed to load suggestion:', error);
|
|
alert('Fejl: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function getSessionBadgeClass(status) {
|
|
switch ((status || '').toLowerCase()) {
|
|
case 'active':
|
|
return 'bg-success';
|
|
case 'completed':
|
|
return 'bg-primary';
|
|
case 'failed':
|
|
return 'bg-danger';
|
|
case 'cancelled':
|
|
return 'bg-secondary';
|
|
default:
|
|
return 'bg-light text-dark';
|
|
}
|
|
}
|
|
|
|
async function loadCompaniesForSelect() {
|
|
try {
|
|
const response = await fetch('/api/v1/customers?limit=1000');
|
|
const data = await response.json();
|
|
|
|
const select = document.getElementById('companySelectModal');
|
|
select.innerHTML = '<option value="">Vælg et firma...</option>' +
|
|
data.customers.map(c => `<option value="${c.id}">${escapeHtml(c.name)}</option>`).join('');
|
|
} catch (error) {
|
|
console.error('Failed to load companies:', error);
|
|
}
|
|
}
|
|
|
|
function showAddCompanyModal() {
|
|
// Reset form
|
|
document.getElementById('addCompanyForm').reset();
|
|
|
|
// Show modal
|
|
const modal = new bootstrap.Modal(document.getElementById('addCompanyModal'));
|
|
modal.show();
|
|
}
|
|
|
|
async function addCompanyToContact() {
|
|
const customerId = parseInt(document.getElementById('companySelectModal').value);
|
|
|
|
if (!customerId) {
|
|
alert('Vælg venligst et firma');
|
|
return;
|
|
}
|
|
|
|
const linkData = {
|
|
customer_id: customerId,
|
|
is_primary: document.getElementById('isPrimaryInputModal').checked,
|
|
role: document.getElementById('roleInputModal').value.trim() || null,
|
|
notes: document.getElementById('notesInputModal').value.trim() || null
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(`/api/v1/contacts/${contactId}/companies`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(linkData)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Kunne ikke tilføje firma');
|
|
}
|
|
|
|
// Close modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('addCompanyModal'));
|
|
modal.hide();
|
|
|
|
// Reload contact
|
|
await loadContact();
|
|
|
|
// Switch to companies tab
|
|
const companiesTab = new bootstrap.Tab(document.querySelector('a[href="#companies"]'));
|
|
companiesTab.show();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to add company:', error);
|
|
alert('Fejl: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function removeCompany(customerId) {
|
|
if (!confirm('Er du sikker på at du vil fjerne dette firma fra kontakten?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/v1/contacts/${contactId}/companies/${customerId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Kunne ikke fjerne firma');
|
|
}
|
|
|
|
// Reload contact
|
|
await loadContact();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to remove company:', error);
|
|
alert('Fejl: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function editContact() {
|
|
// Fill form with current contact data
|
|
if (contactData) {
|
|
document.getElementById('editFirstNameInput').value = contactData.first_name || '';
|
|
document.getElementById('editLastNameInput').value = contactData.last_name || '';
|
|
document.getElementById('editEmailInput').value = contactData.email || '';
|
|
document.getElementById('editPhoneInput').value = contactData.phone || '';
|
|
document.getElementById('editMobileInput').value = contactData.mobile || '';
|
|
document.getElementById('editTitleInput').value = contactData.title || '';
|
|
document.getElementById('editDepartmentInput').value = contactData.department || '';
|
|
document.getElementById('editIsActiveInput').checked = contactData.is_active || false;
|
|
|
|
// Show modal
|
|
const modal = new bootstrap.Modal(document.getElementById('editContactModal'));
|
|
modal.show();
|
|
}
|
|
}
|
|
|
|
async function saveEditContact() {
|
|
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
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('editContactModal'));
|
|
modal.hide();
|
|
|
|
// Reload contact
|
|
await loadContact();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to save contact:', error);
|
|
alert('Fejl: ' + 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 escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
</script>
|
|
{% endblock %}
|