bmc_hub/app/timetracking/frontend/orders.html
Christian 34555d1e36 feat(timetracking): Implement time tracking module with frontend views, HTML templates, and database migrations
- 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.
2025-12-09 22:46:30 +01:00

470 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>