996 lines
32 KiB
HTML
996 lines
32 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Daily Mirror Dashboard - Quality Recticel{% endblock %}
|
|
|
|
{% block head %}
|
|
<!-- Chart.js Library -->
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
<style>
|
|
:root {
|
|
--primary-color: #007bff;
|
|
--success-color: #28a745;
|
|
--warning-color: #ffc107;
|
|
--danger-color: #dc3545;
|
|
--info-color: #17a2b8;
|
|
--dark-color: #343a40;
|
|
--light-bg: #f8f9fa;
|
|
}
|
|
|
|
.dashboard-header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 2rem;
|
|
border-radius: 10px;
|
|
margin-bottom: 2rem;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.dashboard-header h1 {
|
|
margin: 0;
|
|
font-size: 2rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.dashboard-header p {
|
|
margin: 0.5rem 0 0 0;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.summary-card {
|
|
background: white;
|
|
border-radius: 10px;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
cursor: pointer;
|
|
border-left: 4px solid var(--primary-color);
|
|
}
|
|
|
|
.summary-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.summary-card.orders {
|
|
border-left-color: #007bff;
|
|
}
|
|
|
|
.summary-card.production {
|
|
border-left-color: #28a745;
|
|
}
|
|
|
|
.summary-card.deliveries {
|
|
border-left-color: #ffc107;
|
|
}
|
|
|
|
.summary-card.customers {
|
|
border-left-color: #17a2b8;
|
|
}
|
|
|
|
.summary-card-icon {
|
|
font-size: 2.5rem;
|
|
opacity: 0.2;
|
|
position: absolute;
|
|
right: 1.5rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
}
|
|
|
|
.summary-card-title {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
text-transform: uppercase;
|
|
font-weight: 600;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.summary-card-value {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
color: #343a40;
|
|
margin: 0;
|
|
}
|
|
|
|
.summary-card-subtitle {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.chart-container {
|
|
background: white;
|
|
border-radius: 10px;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
position: relative;
|
|
}
|
|
|
|
.chart-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
padding-bottom: 1rem;
|
|
border-bottom: 2px solid #e9ecef;
|
|
}
|
|
|
|
.chart-title {
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
color: #343a40;
|
|
margin: 0;
|
|
}
|
|
|
|
.chart-toggle-btn {
|
|
background: var(--primary-color);
|
|
color: white;
|
|
border: none;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
font-size: 0.875rem;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.chart-toggle-btn:hover {
|
|
background: #0056b3;
|
|
}
|
|
|
|
.chart-toggle-btn i {
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
.data-table-container {
|
|
display: none;
|
|
margin-top: 1.5rem;
|
|
animation: slideDown 0.3s ease-out;
|
|
}
|
|
|
|
.data-table-container.active {
|
|
display: block;
|
|
}
|
|
|
|
@keyframes slideDown {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.data-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.data-table thead {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
}
|
|
|
|
.data-table th {
|
|
padding: 0.75rem;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.data-table tbody tr {
|
|
border-bottom: 1px solid #e9ecef;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.data-table tbody tr:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.data-table td {
|
|
padding: 0.75rem;
|
|
color: #495057;
|
|
}
|
|
|
|
.data-table td.number {
|
|
text-align: right;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 3px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.badge-success {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.badge-warning {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
}
|
|
|
|
.badge-info {
|
|
background: #d1ecf1;
|
|
color: #0c5460;
|
|
}
|
|
|
|
.loading-spinner {
|
|
text-align: center;
|
|
padding: 3rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.loading-spinner i {
|
|
font-size: 3rem;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.filter-section {
|
|
background: white;
|
|
border-radius: 10px;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.filter-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: #343a40;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.pivot-table-container {
|
|
overflow-x: auto;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.pivot-table {
|
|
min-width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.813rem;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.pivot-table thead th {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 0.75rem 0.5rem;
|
|
text-align: center;
|
|
font-weight: 600;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
border-right: 1px solid rgba(255,255,255,0.2);
|
|
}
|
|
|
|
.pivot-table thead th:first-child {
|
|
text-align: left;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.pivot-table tbody td {
|
|
padding: 0.75rem 0.5rem;
|
|
border: 1px solid #e9ecef;
|
|
text-align: center;
|
|
}
|
|
|
|
.pivot-table tbody td:first-child {
|
|
font-weight: 600;
|
|
background: #f8f9fa;
|
|
text-align: left;
|
|
position: sticky;
|
|
left: 0;
|
|
z-index: 5;
|
|
}
|
|
|
|
.pivot-table tbody tr:hover {
|
|
background: #f1f3f5;
|
|
}
|
|
|
|
.pivot-cell-orders {
|
|
color: #007bff;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.pivot-cell-production {
|
|
color: #28a745;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.pivot-cell-deliveries {
|
|
color: #ffc107;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.no-data-message {
|
|
text-align: center;
|
|
padding: 3rem;
|
|
color: #6c757d;
|
|
font-style: italic;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Floating Help Button -->
|
|
<div class="floating-help-btn">
|
|
<a href="{{ url_for('main.help', page='daily_mirror') }}" target="_blank" title="Ajutor - Daily Mirror">
|
|
📖
|
|
</a>
|
|
</div>
|
|
|
|
<div class="container-fluid">
|
|
<!-- Dashboard Header -->
|
|
<div class="dashboard-header">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h1><i class="fas fa-chart-line"></i> Daily Mirror Dashboard</h1>
|
|
<p>Comprehensive production, orders, and delivery analytics</p>
|
|
</div>
|
|
<div>
|
|
<a href="{{ url_for('daily_mirror.daily_mirror_history_route') }}" class="btn btn-light">
|
|
<i class="fas fa-history"></i> History
|
|
</a>
|
|
<a href="{{ url_for('daily_mirror.daily_mirror_main_route') }}" class="btn btn-light">
|
|
<i class="fas fa-arrow-left"></i> Back
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Section -->
|
|
<div class="filter-section">
|
|
<div class="filter-title"><i class="fas fa-filter"></i> Time Range Filter</div>
|
|
<div class="row align-items-end">
|
|
<div class="col-md-3">
|
|
<label for="weeksBack" class="form-label">Weeks to Display:</label>
|
|
<select id="weeksBack" class="form-select">
|
|
<option value="4">Last 4 Weeks</option>
|
|
<option value="8">Last 8 Weeks</option>
|
|
<option value="12" selected>Last 12 Weeks</option>
|
|
<option value="16">Last 16 Weeks</option>
|
|
<option value="24">Last 24 Weeks</option>
|
|
<option value="52">Last Year</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<button type="button" class="btn btn-primary" onclick="loadDashboardData()">
|
|
<i class="fas fa-sync-alt"></i> Refresh Data
|
|
</button>
|
|
</div>
|
|
<div class="col-md-6 text-end">
|
|
<small class="text-muted" id="dateRangeDisplay">Loading...</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary Cards -->
|
|
<div class="row" id="summaryCardsContainer">
|
|
<div class="col-xl-3 col-md-6">
|
|
<div class="summary-card orders" onclick="scrollToChart('ordersChart')">
|
|
<i class="fas fa-shopping-cart summary-card-icon"></i>
|
|
<div class="summary-card-title">Customer Orders</div>
|
|
<div class="summary-card-value" id="totalOrders">-</div>
|
|
<div class="summary-card-subtitle"><span id="totalOrderQty">-</span> units ordered</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6">
|
|
<div class="summary-card production" onclick="scrollToChart('productionChart')">
|
|
<i class="fas fa-cogs summary-card-icon"></i>
|
|
<div class="summary-card-title">Production Finished</div>
|
|
<div class="summary-card-value" id="totalFinished">-</div>
|
|
<div class="summary-card-subtitle"><span id="totalApproved">-</span> approved</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6">
|
|
<div class="summary-card deliveries" onclick="scrollToChart('deliveriesChart')">
|
|
<i class="fas fa-truck summary-card-icon"></i>
|
|
<div class="summary-card-title">Deliveries</div>
|
|
<div class="summary-card-value" id="totalDeliveries">-</div>
|
|
<div class="summary-card-subtitle"><span id="totalDelivered">-</span> units delivered</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6">
|
|
<div class="summary-card customers" onclick="scrollToChart('customerPivotTable')">
|
|
<i class="fas fa-users summary-card-icon"></i>
|
|
<div class="summary-card-title">Active Customers</div>
|
|
<div class="summary-card-value" id="uniqueCustomers">-</div>
|
|
<div class="summary-card-subtitle">unique customers</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Orders Chart -->
|
|
<div class="chart-container" id="ordersChart">
|
|
<div class="chart-header">
|
|
<h3 class="chart-title"><i class="fas fa-shopping-cart"></i> Customer Orders by Week</h3>
|
|
<button class="chart-toggle-btn" onclick="toggleDataTable('ordersTable')">
|
|
<i class="fas fa-table"></i> Show Data Table
|
|
</button>
|
|
</div>
|
|
<canvas id="ordersChartCanvas" height="80"></canvas>
|
|
<div class="data-table-container" id="ordersTable">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Week</th>
|
|
<th>Customer</th>
|
|
<th>Orders</th>
|
|
<th>Total Quantity</th>
|
|
<th>Open</th>
|
|
<th>Completed</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="ordersTableBody">
|
|
<tr><td colspan="6" class="text-center">No data available</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Production Chart -->
|
|
<div class="chart-container" id="productionChart">
|
|
<div class="chart-header">
|
|
<h3 class="chart-title"><i class="fas fa-cogs"></i> Production Finished by Week</h3>
|
|
<button class="chart-toggle-btn" onclick="toggleDataTable('productionTable')">
|
|
<i class="fas fa-table"></i> Show Data Table
|
|
</button>
|
|
</div>
|
|
<canvas id="productionChartCanvas" height="80"></canvas>
|
|
<div class="data-table-container" id="productionTable">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Week</th>
|
|
<th>Finished Orders</th>
|
|
<th>Approved Quantity</th>
|
|
<th>Rejected Quantity</th>
|
|
<th>Quality Rate</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="productionTableBody">
|
|
<tr><td colspan="5" class="text-center">No data available</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Deliveries Chart -->
|
|
<div class="chart-container" id="deliveriesChart">
|
|
<div class="chart-header">
|
|
<h3 class="chart-title"><i class="fas fa-truck"></i> Deliveries by Week</h3>
|
|
<button class="chart-toggle-btn" onclick="toggleDataTable('deliveriesTable')">
|
|
<i class="fas fa-table"></i> Show Data Table
|
|
</button>
|
|
</div>
|
|
<canvas id="deliveriesChartCanvas" height="80"></canvas>
|
|
<div class="data-table-container" id="deliveriesTable">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Week</th>
|
|
<th>Customer</th>
|
|
<th>Delivery Count</th>
|
|
<th>Total Delivered</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="deliveriesTableBody">
|
|
<tr><td colspan="4" class="text-center">No data available</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Customer Pivot Table -->
|
|
<div class="chart-container" id="customerPivotTable">
|
|
<div class="chart-header">
|
|
<h3 class="chart-title"><i class="fas fa-table"></i> Customer Orders Pivot Table</h3>
|
|
<div>
|
|
<select id="pivotMetric" class="form-select form-select-sm d-inline-block w-auto me-2" onchange="updatePivotTable()">
|
|
<option value="orders">Orders Count</option>
|
|
<option value="quantity" selected>Total Quantity</option>
|
|
<option value="open">Open Quantity</option>
|
|
<option value="completed">Completed Quantity</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="pivot-table-container">
|
|
<table class="pivot-table" id="pivotTableElement">
|
|
<thead id="pivotTableHead">
|
|
<tr><th>Customer</th></tr>
|
|
</thead>
|
|
<tbody id="pivotTableBody">
|
|
<tr><td colspan="100%" class="text-center">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading Overlay -->
|
|
<div class="loading-spinner" id="loadingSpinner" style="display: none;">
|
|
<i class="fas fa-spinner"></i>
|
|
<p>Loading dashboard data...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let dashboardData = null;
|
|
let ordersChart = null;
|
|
let productionChart = null;
|
|
let deliveriesChart = null;
|
|
|
|
// Load dashboard data on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadDashboardData();
|
|
});
|
|
|
|
function loadDashboardData() {
|
|
const weeksBack = document.getElementById('weeksBack').value;
|
|
document.getElementById('loadingSpinner').style.display = 'block';
|
|
|
|
fetch(`/daily_mirror/api/dashboard_data?weeks_back=${weeksBack}`)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
return response.json().then(err => {
|
|
throw new Error(err.error || `HTTP ${response.status}: ${response.statusText}`);
|
|
});
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log('Dashboard data received:', data);
|
|
if (data.error) {
|
|
throw new Error(data.error);
|
|
}
|
|
dashboardData = data;
|
|
updateSummaryCards(data.summary);
|
|
updateDateRange(data.date_range);
|
|
renderOrdersChart(data.customer_orders_by_week);
|
|
renderProductionChart(data.finished_orders_by_week);
|
|
renderDeliveriesChart(data.deliveries_by_week);
|
|
updatePivotTable();
|
|
document.getElementById('loadingSpinner').style.display = 'none';
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading dashboard data:', error);
|
|
document.getElementById('loadingSpinner').style.display = 'none';
|
|
alert('Error loading dashboard data: ' + error.message + '\n\nPlease make sure you are logged in and have Daily Mirror access.');
|
|
});
|
|
}
|
|
|
|
function updateSummaryCards(summary) {
|
|
document.getElementById('totalOrders').textContent = summary.total_orders.toLocaleString();
|
|
document.getElementById('totalOrderQty').textContent = summary.total_quantity.toLocaleString();
|
|
document.getElementById('totalFinished').textContent = summary.total_finished.toLocaleString();
|
|
document.getElementById('totalApproved').textContent = summary.total_approved.toLocaleString();
|
|
document.getElementById('totalDeliveries').textContent = summary.total_deliveries.toLocaleString();
|
|
document.getElementById('totalDelivered').textContent = summary.total_delivered.toLocaleString();
|
|
document.getElementById('uniqueCustomers').textContent = summary.unique_customers.toLocaleString();
|
|
}
|
|
|
|
function updateDateRange(dateRange) {
|
|
document.getElementById('dateRangeDisplay').textContent =
|
|
`Data from ${dateRange.start} to ${dateRange.end}`;
|
|
}
|
|
|
|
function renderOrdersChart(data) {
|
|
// Aggregate by week
|
|
const weekMap = {};
|
|
data.forEach(item => {
|
|
const week = item.week_display;
|
|
if (!weekMap[week]) {
|
|
weekMap[week] = { total: 0, open: 0, completed: 0 };
|
|
}
|
|
weekMap[week].total += item.total_quantity || 0;
|
|
weekMap[week].open += item.open_quantity || 0;
|
|
weekMap[week].completed += item.completed_quantity || 0;
|
|
});
|
|
|
|
const weeks = Object.keys(weekMap).sort();
|
|
const totals = weeks.map(w => weekMap[w].total);
|
|
const opens = weeks.map(w => weekMap[w].open);
|
|
const completed = weeks.map(w => weekMap[w].completed);
|
|
|
|
const ctx = document.getElementById('ordersChartCanvas').getContext('2d');
|
|
|
|
if (ordersChart) {
|
|
ordersChart.destroy();
|
|
}
|
|
|
|
ordersChart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: weeks,
|
|
datasets: [
|
|
{
|
|
label: 'Total Quantity',
|
|
data: totals,
|
|
backgroundColor: 'rgba(0, 123, 255, 0.6)',
|
|
borderColor: 'rgba(0, 123, 255, 1)',
|
|
borderWidth: 2
|
|
},
|
|
{
|
|
label: 'Open',
|
|
data: opens,
|
|
backgroundColor: 'rgba(255, 193, 7, 0.6)',
|
|
borderColor: 'rgba(255, 193, 7, 1)',
|
|
borderWidth: 2
|
|
},
|
|
{
|
|
label: 'Completed',
|
|
data: completed,
|
|
backgroundColor: 'rgba(40, 167, 69, 0.6)',
|
|
borderColor: 'rgba(40, 167, 69, 1)',
|
|
borderWidth: 2
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top'
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
return context.dataset.label + ': ' + context.parsed.y.toLocaleString();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: function(value) {
|
|
return value.toLocaleString();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update table
|
|
const tbody = document.getElementById('ordersTableBody');
|
|
if (data.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="text-center">No data available</td></tr>';
|
|
} else {
|
|
tbody.innerHTML = data.map(item => `
|
|
<tr>
|
|
<td>${item.week_display || '-'}</td>
|
|
<td>${item.customer_name || 'Unknown'}</td>
|
|
<td class="number">${(item.order_count || 0).toLocaleString()}</td>
|
|
<td class="number">${(item.total_quantity || 0).toLocaleString()}</td>
|
|
<td class="number">${(item.open_quantity || 0).toLocaleString()}</td>
|
|
<td class="number">${(item.completed_quantity || 0).toLocaleString()}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
}
|
|
|
|
function renderProductionChart(data) {
|
|
const weeks = data.map(item => item.week_display);
|
|
const finished = data.map(item => item.finished_count || 0);
|
|
const approved = data.map(item => item.approved_qty || 0);
|
|
const rejected = data.map(item => item.rejected_qty || 0);
|
|
|
|
const ctx = document.getElementById('productionChartCanvas').getContext('2d');
|
|
|
|
if (productionChart) {
|
|
productionChart.destroy();
|
|
}
|
|
|
|
productionChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: weeks,
|
|
datasets: [
|
|
{
|
|
label: 'Finished Orders',
|
|
data: finished,
|
|
backgroundColor: 'rgba(40, 167, 69, 0.2)',
|
|
borderColor: 'rgba(40, 167, 69, 1)',
|
|
borderWidth: 3,
|
|
fill: true,
|
|
tension: 0.4
|
|
},
|
|
{
|
|
label: 'Approved Qty',
|
|
data: approved,
|
|
backgroundColor: 'rgba(23, 162, 184, 0.2)',
|
|
borderColor: 'rgba(23, 162, 184, 1)',
|
|
borderWidth: 2,
|
|
fill: false,
|
|
tension: 0.4
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top'
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
return context.dataset.label + ': ' + context.parsed.y.toLocaleString();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: function(value) {
|
|
return value.toLocaleString();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update table
|
|
const tbody = document.getElementById('productionTableBody');
|
|
if (data.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center">No data available</td></tr>';
|
|
} else {
|
|
tbody.innerHTML = data.map(item => {
|
|
const qualityRate = item.approved_qty ?
|
|
((item.approved_qty / (item.approved_qty + item.rejected_qty)) * 100).toFixed(1) : 0;
|
|
return `
|
|
<tr>
|
|
<td>${item.week_display || '-'}</td>
|
|
<td class="number">${(item.finished_count || 0).toLocaleString()}</td>
|
|
<td class="number">${(item.approved_qty || 0).toLocaleString()}</td>
|
|
<td class="number">${(item.rejected_qty || 0).toLocaleString()}</td>
|
|
<td class="number"><span class="badge badge-success">${qualityRate}%</span></td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
}
|
|
|
|
function renderDeliveriesChart(data) {
|
|
// Aggregate by week
|
|
const weekMap = {};
|
|
data.forEach(item => {
|
|
const week = item.week_display;
|
|
if (!weekMap[week]) {
|
|
weekMap[week] = { count: 0, quantity: 0 };
|
|
}
|
|
weekMap[week].count += item.delivery_count || 0;
|
|
weekMap[week].quantity += item.total_delivered || 0;
|
|
});
|
|
|
|
const weeks = Object.keys(weekMap).sort();
|
|
const counts = weeks.map(w => weekMap[w].count);
|
|
const quantities = weeks.map(w => weekMap[w].quantity);
|
|
|
|
const ctx = document.getElementById('deliveriesChartCanvas').getContext('2d');
|
|
|
|
if (deliveriesChart) {
|
|
deliveriesChart.destroy();
|
|
}
|
|
|
|
deliveriesChart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: weeks,
|
|
datasets: [
|
|
{
|
|
label: 'Delivery Count',
|
|
data: counts,
|
|
backgroundColor: 'rgba(255, 193, 7, 0.6)',
|
|
borderColor: 'rgba(255, 193, 7, 1)',
|
|
borderWidth: 2,
|
|
yAxisID: 'y'
|
|
},
|
|
{
|
|
label: 'Quantity Delivered',
|
|
data: quantities,
|
|
backgroundColor: 'rgba(220, 53, 69, 0.6)',
|
|
borderColor: 'rgba(220, 53, 69, 1)',
|
|
borderWidth: 2,
|
|
type: 'line',
|
|
yAxisID: 'y1',
|
|
tension: 0.4
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top'
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'left',
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: 'Delivery Count'
|
|
}
|
|
},
|
|
y1: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'right',
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: 'Quantity'
|
|
},
|
|
grid: {
|
|
drawOnChartArea: false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update table
|
|
const tbody = document.getElementById('deliveriesTableBody');
|
|
if (data.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="text-center">No data available</td></tr>';
|
|
} else {
|
|
tbody.innerHTML = data.map(item => `
|
|
<tr>
|
|
<td>${item.week_display || '-'}</td>
|
|
<td>${item.customer_name || 'Unknown'}</td>
|
|
<td class="number">${(item.delivery_count || 0).toLocaleString()}</td>
|
|
<td class="number">${(item.total_delivered || 0).toLocaleString()}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
}
|
|
|
|
function updatePivotTable() {
|
|
if (!dashboardData || !dashboardData.customer_orders_by_week) {
|
|
return;
|
|
}
|
|
|
|
const metric = document.getElementById('pivotMetric').value;
|
|
const data = dashboardData.customer_orders_by_week;
|
|
|
|
// Get unique weeks and customers
|
|
const weeks = [...new Set(data.map(item => item.week_display))].sort();
|
|
const customers = [...new Set(data.map(item => item.customer_name))].sort();
|
|
|
|
// Build pivot data structure
|
|
const pivotData = {};
|
|
customers.forEach(customer => {
|
|
pivotData[customer] = {};
|
|
weeks.forEach(week => {
|
|
pivotData[customer][week] = 0;
|
|
});
|
|
});
|
|
|
|
data.forEach(item => {
|
|
const customer = item.customer_name;
|
|
const week = item.week_display;
|
|
let value = 0;
|
|
|
|
switch(metric) {
|
|
case 'orders':
|
|
value = item.order_count || 0;
|
|
break;
|
|
case 'quantity':
|
|
value = item.total_quantity || 0;
|
|
break;
|
|
case 'open':
|
|
value = item.open_quantity || 0;
|
|
break;
|
|
case 'completed':
|
|
value = item.completed_quantity || 0;
|
|
break;
|
|
}
|
|
|
|
if (pivotData[customer] && pivotData[customer][week] !== undefined) {
|
|
pivotData[customer][week] = value;
|
|
}
|
|
});
|
|
|
|
// Render table header
|
|
const thead = document.getElementById('pivotTableHead');
|
|
thead.innerHTML = `
|
|
<tr>
|
|
<th>Customer</th>
|
|
${weeks.map(week => `<th>${week}</th>`).join('')}
|
|
<th>Total</th>
|
|
</tr>
|
|
`;
|
|
|
|
// Render table body
|
|
const tbody = document.getElementById('pivotTableBody');
|
|
if (customers.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="100%" class="text-center">No data available</td></tr>';
|
|
} else {
|
|
tbody.innerHTML = customers.map(customer => {
|
|
const rowTotal = weeks.reduce((sum, week) => sum + (pivotData[customer][week] || 0), 0);
|
|
return `
|
|
<tr>
|
|
<td>${customer}</td>
|
|
${weeks.map(week => {
|
|
const value = pivotData[customer][week] || 0;
|
|
return `<td class="pivot-cell-orders">${value > 0 ? value.toLocaleString() : '-'}</td>`;
|
|
}).join('')}
|
|
<td class="number pivot-cell-orders"><strong>${rowTotal.toLocaleString()}</strong></td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
// Add totals row
|
|
const totalRow = `
|
|
<tr style="background: #f8f9fa; font-weight: bold;">
|
|
<td>Total</td>
|
|
${weeks.map(week => {
|
|
const total = customers.reduce((sum, customer) => sum + (pivotData[customer][week] || 0), 0);
|
|
return `<td class="pivot-cell-orders">${total.toLocaleString()}</td>`;
|
|
}).join('')}
|
|
<td class="number pivot-cell-orders">
|
|
${customers.reduce((sum, customer) => {
|
|
return sum + weeks.reduce((s, week) => s + (pivotData[customer][week] || 0), 0);
|
|
}, 0).toLocaleString()}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
tbody.innerHTML += totalRow;
|
|
}
|
|
}
|
|
|
|
function toggleDataTable(tableId) {
|
|
const table = document.getElementById(tableId);
|
|
const btn = event.target.closest('.chart-toggle-btn');
|
|
|
|
if (table.classList.contains('active')) {
|
|
table.classList.remove('active');
|
|
btn.innerHTML = '<i class="fas fa-table"></i> Show Data Table';
|
|
} else {
|
|
table.classList.add('active');
|
|
btn.innerHTML = '<i class="fas fa-chart-line"></i> Hide Data Table';
|
|
}
|
|
}
|
|
|
|
function scrollToChart(chartId) {
|
|
const element = document.getElementById(chartId);
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|