- Implemented a new simplified time tracking wizard (wizard2) for approval processes. - Added a registrations view to list all time tracking entries. - Enhanced the existing wizard.html to include a billable checkbox for entries. - Updated JavaScript logic to handle billable state and travel status for time entries. - Introduced a cleanup step in the deployment script to remove old images. - Created a new HTML template for registrations with filtering and pagination capabilities.
274 lines
10 KiB
HTML
274 lines
10 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Tidsregistreringer - BMC Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
/* Clean Filters */
|
|
.filter-card {
|
|
background: white;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 12px;
|
|
padding: 1.5rem;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.03);
|
|
}
|
|
|
|
/* Table Styling */
|
|
.registrations-table {
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
|
overflow: hidden;
|
|
border: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.registrations-table th {
|
|
background-color: #f8f9fa;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.5px;
|
|
padding: 1rem;
|
|
border-bottom: 2px solid #e9ecef;
|
|
color: #64748b;
|
|
}
|
|
|
|
.registrations-table td {
|
|
vertical-align: middle;
|
|
padding: 1rem;
|
|
border-bottom: 1px solid #f1f5f9;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.registrations-table tr:hover {
|
|
background-color: #f8fafc;
|
|
}
|
|
|
|
.status-badge {
|
|
font-size: 0.75rem;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 4px;
|
|
text-transform: uppercase;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.status-pending { background: #fff3cd; color: #856404; }
|
|
.status-approved { background: #d1e7dd; color: #0f5132; }
|
|
.status-rejected { background: #f8d7da; color: #842029; }
|
|
.status-billed { background: #cfe2ff; color: #084298; }
|
|
|
|
</style>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4 px-4 m-0" style="max-width: 1600px; margin: 0 auto;">
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1 class="mb-1">Tidsregistreringer</h1>
|
|
<p class="text-muted mb-0">Søg og filtrer i alle registreringer</p>
|
|
</div>
|
|
<button class="btn btn-outline-primary" onclick="loadData()">
|
|
<i class="bi bi-arrow-clockwise"></i> Opdater
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="filter-card mb-4">
|
|
<div class="row g-3">
|
|
<div class="col-md-3">
|
|
<label class="form-label small text-muted text-uppercase fw-bold">Søgning</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-white"><i class="bi bi-search"></i></span>
|
|
<input type="text" id="filter-search" class="form-control" placeholder="Kunde, beskrivelse, case..." onkeyup="debounceLoad()">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small text-muted text-uppercase fw-bold">Status</label>
|
|
<select id="filter-status" class="form-select" onchange="loadData()">
|
|
<option value="">Alle</option>
|
|
<option value="pending">Afventer</option>
|
|
<option value="approved">Godkendt</option>
|
|
<option value="billed">Faktureret</option>
|
|
<option value="rejected">Afvist</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small text-muted text-uppercase fw-bold">Tekniker</label>
|
|
<input type="text" id="filter-user" class="form-control" placeholder="Navn..." onkeyup="debounceLoad()">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<div class="registrations-table">
|
|
<div class="table-responsive">
|
|
<table class="table mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Dato</th>
|
|
<th style="width: 20%;">Kunde</th>
|
|
<th style="width: 25%;">Beskrivelse / Case</th>
|
|
<th>Tekniker</th>
|
|
<th class="text-center">Timer</th>
|
|
<th class="text-center">Fakt.</th>
|
|
<th>Status</th>
|
|
<th class="text-end">Handlinger</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="table-body">
|
|
<tr><td colspan="8" class="text-center py-5 text-muted">Henter data...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<!-- Pagination logic could go here -->
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Details Modal -->
|
|
<div class="modal fade" id="detailsModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Detaljer</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="details-content">
|
|
<!-- Content -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
|
<script>
|
|
let debounceTimer;
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadData();
|
|
});
|
|
|
|
function debounceLoad() {
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(loadData, 500);
|
|
}
|
|
|
|
async function loadData() {
|
|
const tbody = document.getElementById('table-body');
|
|
const search = document.getElementById('filter-search').value;
|
|
const status = document.getElementById('filter-status').value;
|
|
const user = document.getElementById('filter-user').value;
|
|
|
|
// Build URL
|
|
const params = new URLSearchParams({
|
|
limit: 100, // Hardcoded limit for now
|
|
offset: 0
|
|
});
|
|
|
|
if (search) params.append('search', search);
|
|
if (status) params.append('status', status);
|
|
if (user) params.append('user_name', user);
|
|
|
|
try {
|
|
const response = await fetch(`/api/v1/timetracking/times?${params.toString()}`);
|
|
const data = await response.json();
|
|
|
|
if (data.times.length === 0) {
|
|
tbody.innerHTML = `<tr><td colspan="8" class="text-center py-5">Ingen resultater fundet</td></tr>`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.times.map(t => `
|
|
<tr>
|
|
<td>
|
|
<div class="fw-bold">${formatDate(t.worked_date)}</div>
|
|
<div class="small text-muted">${t.created_at ? new Date(t.created_at).toLocaleTimeString().slice(0,5) : ''}</div>
|
|
</td>
|
|
<td>
|
|
<div class="fw-bold text-dark">${t.customer_name || 'Ukendt'}</div>
|
|
</td>
|
|
<td>
|
|
<div class="small fw-bold text-primary mb-1">
|
|
${t.case_vtiger_id ? `<a href="https://bmcnetworks.od2.vtiger.com/index.php?module=HelpDesk&view=Detail&record=${t.case_vtiger_id.replace('39x','')}" target="_blank">${t.case_title || 'Ingen Case'}</a>` : (t.case_title || 'Ingen Case')}
|
|
</div>
|
|
<div class="text-secondary small" style="max-height: 3em; overflow: hidden; text-overflow: ellipsis;">
|
|
${t.description || '-'}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
${t.user_name || '-'}
|
|
</td>
|
|
<td class="text-center">
|
|
<span class="badge bg-light text-dark border">${parseFloat(t.original_hours).toFixed(2)}</span>
|
|
</td>
|
|
<td class="text-center">
|
|
${t.billable ? '<i class="bi bi-check-circle-fill text-success"></i>' : '<i class="bi bi-dash-circle text-muted"></i>'}
|
|
</td>
|
|
<td>
|
|
<span class="status-badge status-${t.status}">${getStatusLabel(t.status)}</span>
|
|
</td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="showDetails(${t.id})">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
<a href="/timetracking/wizard2?customer_id=${t.customer_id}&time_id=${t.id}" class="btn btn-sm btn-outline-primary" title="Gå til godkendelse">
|
|
<i class="bi bi-arrow-right"></i>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
} catch (error) {
|
|
console.error(error);
|
|
tbody.innerHTML = `<tr><td colspan="8" class="text-center py-5 text-danger">Fejl ved hentning af data</td></tr>`;
|
|
}
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '';
|
|
return new Date(dateStr).toLocaleDateString('da-DK');
|
|
}
|
|
|
|
function getStatusLabel(status) {
|
|
const labels = {
|
|
'pending': 'Afventer',
|
|
'approved': 'Godkendt',
|
|
'rejected': 'Afvist',
|
|
'billed': 'Faktureret'
|
|
};
|
|
return labels[status] || status;
|
|
}
|
|
|
|
async function showDetails(id) {
|
|
const modal = new bootstrap.Modal(document.getElementById('detailsModal'));
|
|
const content = document.getElementById('details-content');
|
|
content.innerHTML = '<div class="text-center"><div class="spinner-border"></div></div>';
|
|
modal.show();
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/timetracking/times/${id}`);
|
|
const t = await res.json();
|
|
|
|
content.innerHTML = `
|
|
<table class="table table-bordered">
|
|
<tr><th>ID</th><td>${t.id}</td></tr>
|
|
<tr><th>Kunde</th><td>${t.customer_name}</td></tr>
|
|
<tr><th>Case</th><td>${t.case_title}</td></tr>
|
|
<tr><th>Beskrivelse</th><td>${t.description}</td></tr>
|
|
<tr><th>Timer</th><td>${t.original_hours}</td></tr>
|
|
<tr><th>Status</th><td>${t.status}</td></tr>
|
|
<tr><th>Raw Data</th><td><pre class="bg-light p-2 small">${JSON.stringify(t, null, 2)}</pre></td></tr>
|
|
</table>
|
|
`;
|
|
} catch (e) {
|
|
content.innerHTML = `<div class="alert alert-danger">Fejl: ${e.message}</div>`;
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|