- Added FastAPI router for time tracking views including dashboard, wizard, and orders. - Created HTML templates for the time tracking wizard with responsive design and Bootstrap integration. - Developed SQL migration script for the time tracking module, including tables for customers, cases, time entries, orders, and audit logs. - Introduced a script to list all registered routes, focusing on time tracking routes. - Added test script to verify route registration and specifically check for time tracking routes.
470 lines
19 KiB
HTML
470 lines
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="da">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Ordrer - BMC Hub</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||
<style>
|
||
:root {
|
||
--bg-body: #f8f9fa;
|
||
--bg-card: #ffffff;
|
||
--text-primary: #2c3e50;
|
||
--text-secondary: #6c757d;
|
||
--accent: #0f4c75;
|
||
--accent-light: #eef2f5;
|
||
--success: #28a745;
|
||
--warning: #ffc107;
|
||
--danger: #dc3545;
|
||
--border-radius: 12px;
|
||
}
|
||
|
||
[data-theme="dark"] {
|
||
--bg-body: #1a1a1a;
|
||
--bg-card: #2d2d2d;
|
||
--text-primary: #e4e4e4;
|
||
--text-secondary: #a0a0a0;
|
||
--accent-light: #1e3a52;
|
||
}
|
||
|
||
body {
|
||
background-color: var(--bg-body);
|
||
color: var(--text-primary);
|
||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
padding-top: 80px;
|
||
transition: background-color 0.3s, color 0.3s;
|
||
}
|
||
|
||
.navbar {
|
||
background: var(--bg-card);
|
||
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
|
||
padding: 1rem 0;
|
||
}
|
||
|
||
.navbar-brand {
|
||
font-weight: 700;
|
||
color: var(--accent);
|
||
font-size: 1.25rem;
|
||
}
|
||
|
||
.nav-link {
|
||
color: var(--text-secondary);
|
||
padding: 0.6rem 1.2rem !important;
|
||
border-radius: var(--border-radius);
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.nav-link:hover, .nav-link.active {
|
||
background-color: var(--accent-light);
|
||
color: var(--accent);
|
||
}
|
||
|
||
.card {
|
||
border: none;
|
||
border-radius: var(--border-radius);
|
||
box-shadow: 0 2px 15px rgba(0,0,0,0.05);
|
||
background: var(--bg-card);
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.table {
|
||
background: var(--bg-card);
|
||
}
|
||
|
||
.table th {
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
font-size: 0.85rem;
|
||
text-transform: uppercase;
|
||
border-bottom: 2px solid var(--accent-light);
|
||
}
|
||
|
||
.order-row {
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.order-row:hover {
|
||
background-color: var(--accent-light);
|
||
}
|
||
|
||
.order-details {
|
||
background: var(--accent-light);
|
||
border-radius: var(--border-radius);
|
||
padding: 1.5rem;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.line-item {
|
||
padding: 0.75rem;
|
||
background: var(--bg-card);
|
||
border-radius: 8px;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.modal-body .info-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 0.75rem 0;
|
||
border-bottom: 1px solid var(--accent-light);
|
||
}
|
||
|
||
.modal-body .info-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Navigation -->
|
||
<nav class="navbar navbar-expand-lg fixed-top">
|
||
<div class="container-fluid">
|
||
<a class="navbar-brand" href="/dashboard">
|
||
<i class="bi bi-grid-3x3-gap-fill"></i> BMC Hub
|
||
</a>
|
||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||
<span class="navbar-toggler-icon"></span>
|
||
</button>
|
||
<div class="collapse navbar-collapse" id="navbarNav">
|
||
<ul class="navbar-nav">
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="/dashboard">Dashboard</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="/customers">Kunder</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link active" href="/timetracking">
|
||
<i class="bi bi-clock-history"></i> Tidsregistrering
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
<ul class="navbar-nav ms-auto">
|
||
<li class="nav-item">
|
||
<button class="btn btn-link nav-link" onclick="toggleTheme()">
|
||
<i class="bi bi-moon-fill" id="theme-icon"></i>
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- Main Content -->
|
||
<div class="container-fluid py-4">
|
||
<!-- Header -->
|
||
<div class="row mb-4">
|
||
<div class="col-12">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h1 class="mb-1">
|
||
<i class="bi bi-receipt text-primary"></i> Ordrer
|
||
</h1>
|
||
<p class="text-muted mb-0">Oversigt over genererede ordrer og eksport til e-conomic</p>
|
||
</div>
|
||
<a href="/timetracking" class="btn btn-outline-secondary">
|
||
<i class="bi bi-arrow-left"></i> Tilbage
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Safety Banner -->
|
||
<div class="row mb-4">
|
||
<div class="col-12">
|
||
<div class="alert alert-warning d-flex align-items-center" role="alert">
|
||
<i class="bi bi-shield-exclamation me-2"></i>
|
||
<div>
|
||
<strong>DRY-RUN Mode Aktiv</strong> -
|
||
Eksport til e-conomic er i test-mode. Fakturaer oprettes ikke i e-conomic.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Orders Table -->
|
||
<div class="row">
|
||
<div class="col-12">
|
||
<div class="card">
|
||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||
<h5 class="mb-0">Alle Ordrer</h5>
|
||
<button class="btn btn-sm btn-outline-primary" onclick="loadOrders()">
|
||
<i class="bi bi-arrow-clockwise"></i> Opdater
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="loading" class="text-center py-4">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">Indlæser...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="orders-table" class="d-none">
|
||
<table class="table table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th>Ordrenr.</th>
|
||
<th>Kunde</th>
|
||
<th>Dato</th>
|
||
<th class="text-center">Linjer</th>
|
||
<th class="text-end">Total</th>
|
||
<th class="text-center">Status</th>
|
||
<th class="text-end">Handlinger</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="orders-tbody">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="no-orders" class="text-center py-5 d-none">
|
||
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||
<p class="text-muted mt-3">Ingen ordrer endnu</p>
|
||
<a href="/timetracking" class="btn btn-primary">
|
||
<i class="bi bi-arrow-left"></i> Godkend tider først
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Order Details Modal -->
|
||
<div class="modal fade" id="orderModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
<i class="bi bi-receipt"></i> Ordre Detaljer
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div id="order-details-content">
|
||
<!-- Will be populated dynamically -->
|
||
</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-success" id="export-order-btn" onclick="exportCurrentOrder()">
|
||
<i class="bi bi-cloud-upload"></i> Eksporter til e-conomic
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script>
|
||
let currentOrderId = null;
|
||
let orderModal = null;
|
||
|
||
// Theme toggle
|
||
function toggleTheme() {
|
||
const html = document.documentElement;
|
||
const icon = document.getElementById('theme-icon');
|
||
if (html.getAttribute('data-theme') === 'dark') {
|
||
html.removeAttribute('data-theme');
|
||
icon.className = 'bi bi-moon-fill';
|
||
localStorage.setItem('theme', 'light');
|
||
} else {
|
||
html.setAttribute('data-theme', 'dark');
|
||
icon.className = 'bi bi-sun-fill';
|
||
localStorage.setItem('theme', 'dark');
|
||
}
|
||
}
|
||
|
||
// Load saved theme
|
||
if (localStorage.getItem('theme') === 'dark') {
|
||
document.documentElement.setAttribute('data-theme', 'dark');
|
||
document.getElementById('theme-icon').className = 'bi bi-sun-fill';
|
||
}
|
||
|
||
// Initialize modal
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
orderModal = new bootstrap.Modal(document.getElementById('orderModal'));
|
||
loadOrders();
|
||
});
|
||
|
||
// Load all orders
|
||
async function loadOrders() {
|
||
document.getElementById('loading').classList.remove('d-none');
|
||
document.getElementById('orders-table').classList.add('d-none');
|
||
document.getElementById('no-orders').classList.add('d-none');
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/timetracking/orders');
|
||
const orders = await response.json();
|
||
|
||
if (orders.length === 0) {
|
||
document.getElementById('loading').classList.add('d-none');
|
||
document.getElementById('no-orders').classList.remove('d-none');
|
||
return;
|
||
}
|
||
|
||
const tbody = document.getElementById('orders-tbody');
|
||
tbody.innerHTML = orders.map(order => {
|
||
const statusBadge = getStatusBadge(order);
|
||
const exportedIcon = order.exported_to_economic
|
||
? '<i class="bi bi-check-circle text-success" title="Eksporteret"></i>'
|
||
: '';
|
||
|
||
return `
|
||
<tr class="order-row" onclick="viewOrder(${order.id})">
|
||
<td>
|
||
<strong>${order.order_number}</strong>
|
||
${exportedIcon}
|
||
</td>
|
||
<td>${order.customer_name}</td>
|
||
<td>${new Date(order.order_date).toLocaleDateString('da-DK')}</td>
|
||
<td class="text-center">${order.line_count || 0}</td>
|
||
<td class="text-end"><strong>${parseFloat(order.total_amount).toFixed(2)} DKK</strong></td>
|
||
<td class="text-center">${statusBadge}</td>
|
||
<td class="text-end">
|
||
<button class="btn btn-sm btn-outline-primary"
|
||
onclick="event.stopPropagation(); viewOrder(${order.id})">
|
||
<i class="bi bi-eye"></i>
|
||
</button>
|
||
${!order.exported_to_economic ? `
|
||
<button class="btn btn-sm btn-success"
|
||
onclick="event.stopPropagation(); exportOrder(${order.id})">
|
||
<i class="bi bi-cloud-upload"></i>
|
||
</button>
|
||
` : ''}
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
|
||
document.getElementById('loading').classList.add('d-none');
|
||
document.getElementById('orders-table').classList.remove('d-none');
|
||
|
||
} catch (error) {
|
||
console.error('Error loading orders:', error);
|
||
document.getElementById('loading').innerHTML = `
|
||
<div class="alert alert-danger">
|
||
Fejl ved indlæsning: ${error.message}
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// Get status badge
|
||
function getStatusBadge(order) {
|
||
if (order.cancelled_at) {
|
||
return '<span class="badge bg-danger">Annulleret</span>';
|
||
}
|
||
if (order.exported_to_economic) {
|
||
return '<span class="badge bg-success">Eksporteret</span>';
|
||
}
|
||
return '<span class="badge bg-warning">Pending</span>';
|
||
}
|
||
|
||
// View order details
|
||
async function viewOrder(orderId) {
|
||
currentOrderId = orderId;
|
||
|
||
try {
|
||
const response = await fetch(`/api/v1/timetracking/orders/${orderId}`);
|
||
const order = await response.json();
|
||
|
||
const content = document.getElementById('order-details-content');
|
||
content.innerHTML = `
|
||
<div class="info-row">
|
||
<span class="fw-bold">Ordrenummer:</span>
|
||
<span>${order.order_number}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="fw-bold">Kunde:</span>
|
||
<span>${order.customer_name}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="fw-bold">Dato:</span>
|
||
<span>${new Date(order.order_date).toLocaleDateString('da-DK')}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="fw-bold">Total:</span>
|
||
<span class="fs-5 fw-bold text-primary">${parseFloat(order.total_amount).toFixed(2)} DKK</span>
|
||
</div>
|
||
|
||
<hr class="my-3">
|
||
|
||
<h6 class="mb-3">Ordrelinjer:</h6>
|
||
${order.lines.map(line => `
|
||
<div class="line-item">
|
||
<div class="d-flex justify-content-between mb-1">
|
||
<strong>${line.description}</strong>
|
||
<strong>${parseFloat(line.line_total).toFixed(2)} DKK</strong>
|
||
</div>
|
||
<div class="d-flex justify-content-between text-muted small">
|
||
<span>${line.quantity} timer × ${parseFloat(line.unit_price).toFixed(2)} DKK</span>
|
||
<span>${new Date(line.time_date).toLocaleDateString('da-DK')}</span>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
|
||
${order.exported_to_economic ? `
|
||
<div class="alert alert-success mt-3 mb-0">
|
||
<i class="bi bi-check-circle"></i>
|
||
Eksporteret til e-conomic den ${new Date(order.exported_at).toLocaleDateString('da-DK')}
|
||
${order.economic_draft_invoice_number ? `<br>Kladde nr.: ${order.economic_draft_invoice_number}` : ''}
|
||
</div>
|
||
` : ''}
|
||
`;
|
||
|
||
// Update export button
|
||
const exportBtn = document.getElementById('export-order-btn');
|
||
if (order.exported_to_economic) {
|
||
exportBtn.disabled = true;
|
||
exportBtn.innerHTML = '<i class="bi bi-check-circle"></i> Allerede eksporteret';
|
||
} else {
|
||
exportBtn.disabled = false;
|
||
exportBtn.innerHTML = '<i class="bi bi-cloud-upload"></i> Eksporter til e-conomic';
|
||
}
|
||
|
||
orderModal.show();
|
||
|
||
} catch (error) {
|
||
alert('Fejl ved indlæsning af ordre: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Export order
|
||
async function exportOrder(orderId) {
|
||
if (!confirm('Eksporter ordre til e-conomic?\n\nDette opretter en kladde-faktura i e-conomic.')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/v1/timetracking/export/${orderId}`, {
|
||
method: 'POST'
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.dry_run) {
|
||
alert(`DRY-RUN MODE:\n\nFakturaen ville blive oprettet med:\n- Kladde nr.: ${result.draft_invoice_number}\n- Total: ${result.total_amount} DKK\n\nIngen ændringer er foretaget i e-conomic.`);
|
||
} else {
|
||
alert(`Ordre eksporteret!\n\nKladde nr.: ${result.draft_invoice_number}\nTotal: ${result.total_amount} DKK`);
|
||
}
|
||
|
||
loadOrders();
|
||
if (orderModal._isShown) {
|
||
orderModal.hide();
|
||
}
|
||
|
||
} catch (error) {
|
||
alert('Fejl ved eksport: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Export current order from modal
|
||
function exportCurrentOrder() {
|
||
if (currentOrderId) {
|
||
exportOrder(currentOrderId);
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|