- Introduced Technician Dashboard V1 (tech_v1_overview.html) with KPI cards and new cases overview. - Implemented Technician Dashboard V2 (tech_v2_workboard.html) featuring a workboard layout for daily tasks and opportunities. - Developed Technician Dashboard V3 (tech_v3_table_focus.html) with a power table for detailed case management. - Created a dashboard selector page (technician_dashboard_selector.html) for easy navigation between dashboard versions. - Added user dashboard preferences migration (130_user_dashboard_preferences.sql) to store default dashboard paths. - Enhanced sag_sager table with assigned group ID (131_sag_assignment_group.sql) for better case management. - Updated sag_subscriptions table to include cancellation rules and billing dates (132_subscription_cancellation.sql, 134_subscription_billing_dates.sql). - Implemented subscription staging for CRM integration (136_simply_subscription_staging.sql). - Added a script to move time tracking section in detail view (move_time_section.py). - Created a test script for subscription processing (test_subscription_processing.py).
225 lines
8.9 KiB
HTML
225 lines
8.9 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Ordre - BMC Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.ordre-card {
|
|
background: var(--bg-card);
|
|
border-radius: 12px;
|
|
padding: 1rem 1.25rem;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
}
|
|
.ordre-title {
|
|
font-size: 0.8rem;
|
|
text-transform: uppercase;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 0.35rem;
|
|
letter-spacing: 0.6px;
|
|
}
|
|
.ordre-value {
|
|
font-size: 1.35rem;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
}
|
|
.table thead th {
|
|
font-size: 0.8rem;
|
|
text-transform: uppercase;
|
|
color: var(--text-secondary);
|
|
background: var(--accent);
|
|
color: white;
|
|
}
|
|
.order-row {
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
}
|
|
.order-row:hover {
|
|
background-color: rgba(var(--accent-rgb, 15, 76, 117), 0.05);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
<div class="d-flex flex-wrap justify-content-between align-items-center mb-3">
|
|
<div>
|
|
<h2 class="mb-1"><i class="bi bi-receipt me-2"></i>Ordre</h2>
|
|
<div class="text-muted">Oversigt over alle ordre</div>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<a href="/ordre/create/new" class="btn btn-success"><i class="bi bi-plus-circle me-1"></i>Opret ny ordre</a>
|
|
<button class="btn btn-outline-primary" onclick="loadOrders()"><i class="bi bi-arrow-clockwise me-1"></i>Opdater</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-3"><div class="ordre-card"><div class="ordre-title">Total ordre</div><div id="sumOrders" class="ordre-value">0</div></div></div>
|
|
<div class="col-md-3"><div class="ordre-card"><div class="ordre-title">Seneste måned</div><div id="sumRecent" class="ordre-value">0</div></div></div>
|
|
<div class="col-md-3"><div class="ordre-card"><div class="ordre-title">Eksporteret</div><div id="sumExported" class="ordre-value">0</div></div></div>
|
|
<div class="col-md-3"><div class="ordre-card"><div class="ordre-title">Ikke eksporteret</div><div id="sumNotExported" class="ordre-value">0</div></div></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Ordre #</th>
|
|
<th>Titel</th>
|
|
<th>Kunde</th>
|
|
<th>Linjer</th>
|
|
<th>Oprettet</th>
|
|
<th>Sidst opdateret</th>
|
|
<th>Sidst eksporteret</th>
|
|
<th>Status</th>
|
|
<th>Handlinger</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="ordersTableBody">
|
|
<tr><td colspan="9" class="text-muted text-center py-4">Indlæser...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
let orders = [];
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '-';
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleDateString('da-DK', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
function renderOrders() {
|
|
const tbody = document.getElementById('ordersTableBody');
|
|
if (!orders.length) {
|
|
tbody.innerHTML = '<tr><td colspan="9" class="text-muted text-center py-4">Ingen ordre fundet</td></tr>';
|
|
updateSummary();
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = orders.map(order => {
|
|
const lines = Array.isArray(order.lines_json) ? order.lines_json : [];
|
|
const hasExported = order.last_exported_at ? true : false;
|
|
const statusBadge = hasExported
|
|
? '<span class="badge bg-success">Eksporteret</span>'
|
|
: '<span class="badge bg-warning text-dark">Ikke eksporteret</span>';
|
|
|
|
return `
|
|
<tr class="order-row" onclick="window.location.href='/ordre/${order.id}'">
|
|
<td><strong>#${order.id}</strong></td>
|
|
<td>${order.title || '-'}</td>
|
|
<td>${order.customer_id ? `Kunde ${order.customer_id}` : '-'}</td>
|
|
<td><span class="badge bg-primary">${lines.length} linjer</span></td>
|
|
<td>${formatDate(order.created_at)}</td>
|
|
<td>${formatDate(order.updated_at)}</td>
|
|
<td>${formatDate(order.last_exported_at)}</td>
|
|
<td>${statusBadge}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation(); window.location.href='/ordre/${order.id}'">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger" onclick="event.stopPropagation(); deleteOrder(${order.id})">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
updateSummary();
|
|
}
|
|
|
|
function updateSummary() {
|
|
const now = new Date();
|
|
const oneMonthAgo = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
|
|
|
|
const recentOrders = orders.filter(order => new Date(order.created_at) >= oneMonthAgo);
|
|
const exportedOrders = orders.filter(order => order.last_exported_at);
|
|
const notExportedOrders = orders.filter(order => !order.last_exported_at);
|
|
|
|
document.getElementById('sumOrders').textContent = orders.length;
|
|
document.getElementById('sumRecent').textContent = recentOrders.length;
|
|
document.getElementById('sumExported').textContent = exportedOrders.length;
|
|
document.getElementById('sumNotExported').textContent = notExportedOrders.length;
|
|
}
|
|
|
|
async function loadOrders() {
|
|
const tbody = document.getElementById('ordersTableBody');
|
|
tbody.innerHTML = '<tr><td colspan="9" class="text-muted text-center py-4">Indlæser...</td></tr>';
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/ordre/drafts?limit=100');
|
|
if (!res.ok) {
|
|
const errorData = await res.json().catch(() => ({}));
|
|
console.error('API Error:', res.status, errorData);
|
|
throw new Error(errorData.detail || `HTTP ${res.status}: Kunne ikke hente ordre`);
|
|
}
|
|
|
|
const data = await res.json();
|
|
console.log('Fetched orders:', data);
|
|
|
|
orders = Array.isArray(data) ? data : [];
|
|
|
|
if (orders.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="9" class="text-muted text-center py-4">Ingen ordre fundet. <a href="/ordre/create/new" class="btn btn-sm btn-success ms-2">Opret første ordre</a></td></tr>';
|
|
updateSummary();
|
|
return;
|
|
}
|
|
|
|
// Fetch lines_json for each order to get line count
|
|
const detailPromises = orders.map(async (order) => {
|
|
try {
|
|
const detailRes = await fetch(`/api/v1/ordre/drafts/${order.id}`);
|
|
if (detailRes.ok) {
|
|
const detail = await detailRes.json();
|
|
order.lines_json = detail.lines_json || [];
|
|
} else {
|
|
order.lines_json = [];
|
|
}
|
|
} catch (e) {
|
|
console.error(`Failed to fetch details for order ${order.id}:`, e);
|
|
order.lines_json = [];
|
|
}
|
|
});
|
|
|
|
await Promise.all(detailPromises);
|
|
|
|
renderOrders();
|
|
} catch (error) {
|
|
console.error('Load orders error:', error);
|
|
tbody.innerHTML = `<tr><td colspan="9" class="text-danger text-center py-4">${error.message || 'Kunne ikke hente ordre'}</td></tr>`;
|
|
orders = [];
|
|
updateSummary();
|
|
}
|
|
}
|
|
|
|
async function deleteOrder(orderId) {
|
|
if (!confirm('Er du sikker på, at du vil slette denne ordre?')) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/ordre/drafts/${orderId}`, { method: 'DELETE' });
|
|
if (!res.ok) {
|
|
const error = await res.json();
|
|
throw new Error(error.detail || 'Kunne ikke slette ordre');
|
|
}
|
|
|
|
await loadOrders();
|
|
alert('Ordre slettet');
|
|
} catch (error) {
|
|
alert(`Fejl: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadOrders();
|
|
});
|
|
</script>
|
|
{% endblock %}
|