- 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).
287 lines
13 KiB
HTML
287 lines
13 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Ticket Dashboard - BMC Hub{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
<!-- Header -->
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<h1 class="h3 mb-0">🎫 Support Dashboard</h1>
|
|
<p class="text-muted">Oversigt over alle support tickets og aktivitet</p>
|
|
</div>
|
|
<div class="col-auto">
|
|
<button class="btn btn-outline-primary me-2" onclick="window.location.href='/ticket/dashboard/technician'">
|
|
<i class="bi bi-tools"></i> Tekniker Dashboard (3 forslag)
|
|
</button>
|
|
<button class="btn btn-primary" onclick="window.location.href='/ticket/tickets/new'">
|
|
<i class="bi bi-plus-circle"></i> Ny Ticket
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Overview -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-2">
|
|
<div class="card border-0 shadow-sm h-100" style="cursor: pointer;" onclick="filterByStatus('open')">
|
|
<div class="card-body text-center">
|
|
<div class="rounded-circle bg-info bg-opacity-10 p-3 d-inline-flex mb-3">
|
|
<i class="bi bi-inbox-fill text-info fs-4"></i>
|
|
</div>
|
|
<h2 class="mb-1 text-info">{{ stats.open_count or 0 }}</h2>
|
|
<p class="text-muted small mb-0">Åbne</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card border-0 shadow-sm h-100" style="cursor: pointer;" onclick="filterByStatus('in_progress')">
|
|
<div class="card-body text-center">
|
|
<div class="rounded-circle bg-warning bg-opacity-10 p-3 d-inline-flex mb-3">
|
|
<i class="bi bi-hourglass-split text-warning fs-4"></i>
|
|
</div>
|
|
<h2 class="mb-1 text-warning">{{ stats.in_progress_count or 0 }}</h2>
|
|
<p class="text-muted small mb-0">I Gang</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card border-0 shadow-sm h-100" style="cursor: pointer;" onclick="filterByStatus('pending_customer')">
|
|
<div class="card-body text-center">
|
|
<div class="rounded-circle bg-secondary bg-opacity-10 p-3 d-inline-flex mb-3">
|
|
<i class="bi bi-clock-fill text-secondary fs-4"></i>
|
|
</div>
|
|
<h2 class="mb-1 text-secondary">{{ stats.pending_count or 0 }}</h2>
|
|
<p class="text-muted small mb-0">Afventer</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card border-0 shadow-sm h-100" style="cursor: pointer;" onclick="filterByStatus('resolved')">
|
|
<div class="card-body text-center">
|
|
<div class="rounded-circle bg-success bg-opacity-10 p-3 d-inline-flex mb-3">
|
|
<i class="bi bi-check-circle-fill text-success fs-4"></i>
|
|
</div>
|
|
<h2 class="mb-1 text-success">{{ stats.resolved_count or 0 }}</h2>
|
|
<p class="text-muted small mb-0">Løst</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card border-0 shadow-sm h-100" style="cursor: pointer;" onclick="filterByStatus('closed')">
|
|
<div class="card-body text-center">
|
|
<div class="rounded-circle bg-dark bg-opacity-10 p-3 d-inline-flex mb-3">
|
|
<i class="bi bi-archive-fill text-dark fs-4"></i>
|
|
</div>
|
|
<h2 class="mb-1 text-dark">{{ stats.closed_count or 0 }}</h2>
|
|
<p class="text-muted small mb-0">Lukket</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card border-0 shadow-sm h-100 bg-primary text-white">
|
|
<div class="card-body text-center">
|
|
<div class="rounded-circle bg-white bg-opacity-25 p-3 d-inline-flex mb-3">
|
|
<i class="bi bi-ticket-detailed-fill fs-4"></i>
|
|
</div>
|
|
<h2 class="mb-1">{{ stats.total_count or 0 }}</h2>
|
|
<p class="small mb-0 opacity-75">I Alt</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Worklog & Prepaid Overview -->
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-md-6">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white border-0 py-3">
|
|
<h5 class="mb-0">⏱️ Worklog Status</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-6 border-end">
|
|
<div class="text-center p-3">
|
|
<h3 class="text-warning mb-2">{{ worklog_stats.draft_count or 0 }}</h3>
|
|
<p class="text-muted small mb-1">Kladder</p>
|
|
<p class="mb-0"><strong>{{ "%.1f"|format(worklog_stats.draft_hours|float if worklog_stats.draft_hours else 0) }} timer</strong></p>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="text-center p-3">
|
|
<h3 class="text-success mb-2">{{ worklog_stats.billable_count or 0 }}</h3>
|
|
<p class="text-muted small mb-1">Fakturerbare</p>
|
|
<p class="mb-0"><strong>{{ "%.1f"|format(worklog_stats.billable_hours|float if worklog_stats.billable_hours else 0) }} timer</strong></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="text-center pt-3 border-top">
|
|
<a href="/ticket/worklog/review" class="btn btn-outline-primary btn-sm">
|
|
<i class="bi bi-check-square"></i> Godkend Worklog
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white border-0 py-3">
|
|
<h5 class="mb-0">💳 Prepaid Cards</h5>
|
|
</div>
|
|
<div class="card-body" id="prepaidStats">
|
|
<div class="text-center py-3">
|
|
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Tickets -->
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">📋 Seneste Tickets</h5>
|
|
<a href="/ticket/tickets" class="btn btn-sm btn-outline-secondary">
|
|
Se Alle <i class="bi bi-arrow-right"></i>
|
|
</a>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Ticket #</th>
|
|
<th>Emne</th>
|
|
<th>Kunde</th>
|
|
<th>Status</th>
|
|
<th>Prioritet</th>
|
|
<th>Oprettet</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% if recent_tickets %}
|
|
{% for ticket in recent_tickets %}
|
|
<tr onclick="window.location.href='/ticket/tickets/{{ ticket.id }}'" style="cursor: pointer;">
|
|
<td><strong>{{ ticket.ticket_number }}</strong></td>
|
|
<td>{{ ticket.subject }}</td>
|
|
<td>{{ ticket.customer_name or '-' }}</td>
|
|
<td>
|
|
{% if ticket.status == 'open' %}
|
|
<span class="badge bg-info">Åben</span>
|
|
{% elif ticket.status == 'in_progress' %}
|
|
<span class="badge bg-warning">I Gang</span>
|
|
{% elif ticket.status == 'pending_customer' %}
|
|
<span class="badge bg-secondary">Afventer</span>
|
|
{% elif ticket.status == 'resolved' %}
|
|
<span class="badge bg-success">Løst</span>
|
|
{% elif ticket.status == 'closed' %}
|
|
<span class="badge bg-dark">Lukket</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">{{ ticket.status }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if ticket.priority == 'urgent' %}
|
|
<span class="badge bg-danger">Akut</span>
|
|
{% elif ticket.priority == 'high' %}
|
|
<span class="badge bg-warning">Høj</span>
|
|
{% elif ticket.priority == 'normal' %}
|
|
<span class="badge bg-info">Normal</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">Lav</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ ticket.created_at.strftime('%d/%m/%Y %H:%M') if ticket.created_at else '-' }}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation(); window.location.href='/ticket/tickets/{{ ticket.id }}'">
|
|
<i class="bi bi-arrow-right"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="7" class="text-center text-muted py-5">
|
|
Ingen tickets endnu
|
|
</td>
|
|
</tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Load prepaid stats
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadPrepaidStats();
|
|
});
|
|
|
|
async function loadPrepaidStats() {
|
|
try {
|
|
const response = await fetch('/api/v1/prepaid-cards/stats/summary');
|
|
const stats = await response.json();
|
|
|
|
document.getElementById('prepaidStats').innerHTML = `
|
|
<div class="row text-center">
|
|
<div class="col-6 border-end">
|
|
<h4 class="text-success mb-1">${stats.active_count || 0}</h4>
|
|
<p class="text-muted small mb-0">Aktive Kort</p>
|
|
</div>
|
|
<div class="col-6">
|
|
<h4 class="text-primary mb-1">${parseFloat(stats.total_remaining_hours || 0).toFixed(1)} t</h4>
|
|
<p class="text-muted small mb-0">Timer Tilbage</p>
|
|
</div>
|
|
</div>
|
|
<div class="text-center pt-3 border-top">
|
|
<a href="/prepaid-cards" class="btn btn-outline-primary btn-sm">
|
|
<i class="bi bi-credit-card-2-front"></i> Se Alle Kort
|
|
</a>
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
console.error('Error loading prepaid stats:', error);
|
|
document.getElementById('prepaidStats').innerHTML = `
|
|
<p class="text-center text-muted mb-0">Kunne ikke indlæse data</p>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function filterByStatus(status) {
|
|
window.location.href = `/ticket/tickets?status=${status}`;
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.card {
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
|
|
}
|
|
|
|
.table th {
|
|
font-weight: 600;
|
|
font-size: 0.85rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: var(--bs-secondary);
|
|
}
|
|
|
|
.table tbody tr {
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.table tbody tr:hover {
|
|
background-color: rgba(15, 76, 117, 0.05);
|
|
}
|
|
</style>
|
|
{% endblock %}
|