bmc_hub/app/modules/fedex/frontend/fedex_overview.html

352 lines
14 KiB
HTML
Raw Permalink Normal View History

{% extends "shared/frontend/base.html" %}
{% block title %}FedEx Overblik - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.fedex-shell {
display: grid;
gap: 1rem;
}
.fedex-hero {
border-radius: 14px;
border: 1px solid rgba(15, 76, 117, 0.16);
background: linear-gradient(135deg, rgba(15, 76, 117, 0.1), rgba(26, 117, 159, 0.08));
padding: 1rem 1.1rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.fedex-kpis {
display: grid;
gap: 0.7rem;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.fedex-kpi {
border: 1px solid rgba(15, 76, 117, 0.16);
border-radius: 12px;
background: var(--bg-card);
padding: 0.75rem 0.85rem;
}
.fedex-kpi .label {
color: var(--text-secondary);
font-size: 0.76rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.fedex-kpi .value {
font-size: 1.35rem;
font-weight: 800;
margin-top: 0.2rem;
}
.fedex-filter-card {
border: 1px solid rgba(15, 76, 117, 0.16);
border-radius: 12px;
}
.fedex-table-wrap {
border: 1px solid rgba(15, 76, 117, 0.14);
border-radius: 12px;
overflow: auto;
max-height: 70vh;
}
.fedex-table thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bg-card);
color: var(--text-secondary);
text-transform: uppercase;
font-size: 0.74rem;
letter-spacing: 0.04em;
white-space: nowrap;
}
.fedex-table tbody td {
vertical-align: middle;
font-size: 0.88rem;
}
.fedex-status {
border-radius: 999px;
padding: 0.2rem 0.55rem;
font-size: 0.72rem;
font-weight: 700;
border: 1px solid transparent;
display: inline-flex;
align-items: center;
}
.fedex-status.draft { background: rgba(108, 117, 125, 0.15); border-color: rgba(108, 117, 125, 0.3); color: #5f6b76; }
.fedex-status.submitted, .fedex-status.booked { background: rgba(13, 110, 253, 0.12); border-color: rgba(13, 110, 253, 0.3); color: #0a58ca; }
.fedex-status.in_transit { background: rgba(255, 193, 7, 0.15); border-color: rgba(255, 193, 7, 0.35); color: #996f00; }
.fedex-status.delivered { background: rgba(25, 135, 84, 0.14); border-color: rgba(25, 135, 84, 0.3); color: #146c43; }
.fedex-status.cancelled, .fedex-status.failed { background: rgba(220, 53, 69, 0.14); border-color: rgba(220, 53, 69, 0.3); color: #b02a37; }
.fedex-row-title {
font-weight: 700;
color: var(--text-primary);
}
.fedex-row-meta {
color: var(--text-secondary);
font-size: 0.78rem;
margin-top: 0.15rem;
}
@media (max-width: 1100px) {
.fedex-kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.fedex-kpis {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block content %}
<div class="fedex-shell">
<div class="fedex-hero">
<div>
<h2 class="fw-bold mb-1">FedEx Overblik</h2>
<div class="text-muted">Samlet visning af alle FedEx bestillinger og deres status.</div>
</div>
<button class="btn btn-outline-primary" id="refreshFedexBtn"><i class="bi bi-arrow-clockwise me-1"></i>Opdater</button>
</div>
<div class="fedex-kpis">
<div class="fedex-kpi"><div class="label">Total</div><div class="value" id="kpiTotal">0</div></div>
<div class="fedex-kpi"><div class="label">Aktive</div><div class="value" id="kpiActive">0</div></div>
<div class="fedex-kpi"><div class="label">Leveret</div><div class="value" id="kpiDelivered">0</div></div>
<div class="fedex-kpi"><div class="label">Fejl/Annulleret</div><div class="value" id="kpiFailed">0</div></div>
</div>
<div class="card fedex-filter-card">
<div class="card-body">
<div class="row g-3">
<div class="col-lg-5">
<label class="form-label small text-muted" for="fedexSearchInput">Søg</label>
<input id="fedexSearchInput" class="form-control" placeholder="Booking ref, tracking, modtager, by, case id...">
</div>
<div class="col-lg-3 col-md-6">
<label class="form-label small text-muted" for="fedexStatusFilter">Status</label>
<select id="fedexStatusFilter" class="form-select">
<option value="all">Alle</option>
<option value="draft">Draft</option>
<option value="submitted">Submitted</option>
<option value="booked">Booked</option>
<option value="in_transit">In transit</option>
<option value="delivered">Delivered</option>
<option value="cancelled">Cancelled</option>
<option value="failed">Failed</option>
</select>
</div>
<div class="col-lg-2 col-md-6">
<label class="form-label small text-muted" for="fedexSortSelect">Sortering</label>
<select id="fedexSortSelect" class="form-select">
<option value="newest">Nyeste først</option>
<option value="oldest">Ældste først</option>
<option value="status">Status</option>
</select>
</div>
<div class="col-lg-2 d-flex align-items-end">
<button class="btn btn-light border w-100" id="fedexClearBtn">Ryd filtre</button>
</div>
</div>
</div>
</div>
<div class="fedex-table-wrap">
<table class="table table-hover fedex-table mb-0">
<thead>
<tr>
<th>Bestilling</th>
<th>Status</th>
<th>Tracking</th>
<th>Case</th>
<th>Afhentning</th>
<th>Pris</th>
<th class="text-end">Handlinger</th>
</tr>
</thead>
<tbody id="fedexTableBody">
<tr><td colspan="7" class="text-center py-4 text-muted">Henter FedEx bestillinger...</td></tr>
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
(() => {
const state = {
bookings: [],
filtered: [],
};
function escapeHtml(value) {
const span = document.createElement('span');
span.textContent = value ?? '';
return span.innerHTML;
}
function formatDate(value) {
if (!value) return '-';
const dt = new Date(value);
if (Number.isNaN(dt.getTime())) return '-';
return dt.toLocaleString('da-DK');
}
function formatMoney(amount, currency) {
if (amount === null || amount === undefined || Number.isNaN(Number(amount))) return '-';
return `${Number(amount).toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${currency || 'DKK'}`;
}
function statusBadge(status) {
const s = String(status || 'draft').toLowerCase();
return `<span class="fedex-status ${escapeHtml(s)}">${escapeHtml(s.replaceAll('_', ' '))}</span>`;
}
function applyFilters() {
const q = (document.getElementById('fedexSearchInput')?.value || '').trim().toLowerCase();
const status = (document.getElementById('fedexStatusFilter')?.value || 'all').toLowerCase();
const sortBy = (document.getElementById('fedexSortSelect')?.value || 'newest').toLowerCase();
const rows = state.bookings.filter((item) => {
const itemStatus = String(item.shipment_status || '').toLowerCase();
if (status !== 'all' && itemStatus !== status) return false;
if (!q) return true;
const haystack = [
item.booking_ref,
item.tracking_number,
item.recipient_name,
item.city,
item.country_code,
item.service_type,
item.case_id,
].map((v) => String(v || '').toLowerCase()).join(' ');
return haystack.includes(q);
});
rows.sort((a, b) => {
if (sortBy === 'status') {
return String(a.shipment_status || '').localeCompare(String(b.shipment_status || ''), 'da');
}
const ta = new Date(a.created_at || 0).getTime() || 0;
const tb = new Date(b.created_at || 0).getTime() || 0;
return sortBy === 'oldest' ? ta - tb : tb - ta;
});
state.filtered = rows;
renderTable();
renderKpis();
}
function renderKpis() {
const total = state.filtered.length;
const delivered = state.filtered.filter((item) => item.shipment_status === 'delivered').length;
const failed = state.filtered.filter((item) => ['failed', 'cancelled'].includes(String(item.shipment_status || '').toLowerCase())).length;
const active = state.filtered.filter((item) => ['draft', 'submitted', 'booked', 'in_transit'].includes(String(item.shipment_status || '').toLowerCase())).length;
document.getElementById('kpiTotal').textContent = String(total);
document.getElementById('kpiActive').textContent = String(active);
document.getElementById('kpiDelivered').textContent = String(delivered);
document.getElementById('kpiFailed').textContent = String(failed);
}
function renderTable() {
const tbody = document.getElementById('fedexTableBody');
if (!tbody) return;
if (!state.filtered.length) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-4 text-muted">Ingen FedEx bestillinger matcher filteret.</td></tr>';
return;
}
tbody.innerHTML = state.filtered.map((item) => {
const trackingNumber = String(item.tracking_number || '').trim();
const trackingUrl = String(item.tracking_url || (trackingNumber ? `https://www.fedex.com/fedextrack/?trknbr=${encodeURIComponent(trackingNumber)}` : '')).trim();
const labelUrl = String(item.label_url || '').trim();
const openCaseUrl = Number(item.case_id) > 0 ? `/sag/${Number(item.case_id)}/v3` : '/sag';
return `
<tr>
<td>
<div class="fedex-row-title">${escapeHtml(item.booking_ref || '-')}</div>
<div class="fedex-row-meta">${escapeHtml(item.recipient_name || '-')} • ${escapeHtml(item.city || '-')} (${escapeHtml(item.country_code || '-')})</div>
</td>
<td>${statusBadge(item.shipment_status)}</td>
<td>
${trackingNumber ? `<span class="small fw-semibold">${escapeHtml(trackingNumber)}</span>` : '<span class="text-muted">-</span>'}
</td>
<td><a href="${openCaseUrl}" class="text-decoration-none">#${Number(item.case_id || 0)}</a></td>
<td>${escapeHtml(formatDate(item.pickup_window_start))}</td>
<td>${escapeHtml(formatMoney(item.total_amount, item.currency))}</td>
<td class="text-end">
${trackingUrl ? `<a href="${trackingUrl}" target="_blank" rel="noopener" class="btn btn-sm btn-outline-secondary"><i class="bi bi-box-arrow-up-right"></i></a>` : ''}
${labelUrl ? `<a href="${labelUrl}" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary ms-1"><i class="bi bi-file-earmark-text"></i></a>` : ''}
</td>
</tr>
`;
}).join('');
}
async function loadBookings() {
const tbody = document.getElementById('fedexTableBody');
if (tbody) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-4 text-muted"><span class="spinner-border spinner-border-sm me-2"></span>Henter FedEx bestillinger...</td></tr>';
}
try {
const response = await fetch('/api/v1/fedex/bookings', { credentials: 'include' });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const payload = await response.json();
state.bookings = Array.isArray(payload?.items) ? payload.items : [];
applyFilters();
} catch (error) {
if (tbody) {
tbody.innerHTML = `<tr><td colspan="7" class="text-center py-4 text-danger">Kunne ikke hente FedEx bestillinger: ${escapeHtml(error.message || 'ukendt fejl')}</td></tr>`;
}
}
}
function bindEvents() {
document.getElementById('fedexSearchInput')?.addEventListener('input', applyFilters);
document.getElementById('fedexStatusFilter')?.addEventListener('change', applyFilters);
document.getElementById('fedexSortSelect')?.addEventListener('change', applyFilters);
document.getElementById('fedexClearBtn')?.addEventListener('click', () => {
document.getElementById('fedexSearchInput').value = '';
document.getElementById('fedexStatusFilter').value = 'all';
document.getElementById('fedexSortSelect').value = 'newest';
applyFilters();
});
document.getElementById('refreshFedexBtn')?.addEventListener('click', loadBookings);
}
document.addEventListener('DOMContentLoaded', () => {
bindEvents();
loadBookings();
});
})();
</script>
{% endblock %}