428 lines
18 KiB
HTML
428 lines
18 KiB
HTML
{% extends 'base.html' %}
|
|
{% load i18n %}
|
|
{% load tenhal_tag %}
|
|
{% block title %}
|
|
{% trans "Dealership Dashboard"|capfirst %}
|
|
{% endblock title %}
|
|
{% block content %}
|
|
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
|
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
|
|
<h2 class="h3 fw-bold mb-3 mb-md-0">
|
|
{% if request.is_dealer %}
|
|
{% trans "Business Health Dashboard" %}
|
|
{% elif request.is_manger %}
|
|
{% trans "Manager Dashboard" %}
|
|
{% elif request.is_inventory %}
|
|
{% trans "Inventory Dashboard" %}
|
|
{% else %}
|
|
{% trans "Accountant Dashboard" %}
|
|
{% endif %}
|
|
<i class="fas fa-chart-area text-primary ms-2"></i>
|
|
</h2>
|
|
<form method="GET" class="date-filter-form">
|
|
<div class="row g-3">
|
|
<div class="col-12 col-md-4">
|
|
<label for="start-date" class="form-label">{% trans "Start Date" %}</label>
|
|
<input type="date"
|
|
class="form-control"
|
|
id="start-date"
|
|
name="start_date"
|
|
value="{{ start_date|date:'Y-m-d' }}"
|
|
required>
|
|
</div>
|
|
<div class="col-12 col-md-4">
|
|
<label for="end-date" class="form-label">{% trans "End Date" %}</label>
|
|
<input type="date"
|
|
class="form-control"
|
|
id="end-date"
|
|
name="end_date"
|
|
value="{{ end_date|date:'Y-m-d' }}"
|
|
required>
|
|
</div>
|
|
<div class="col-12 col-md-4 d-flex align-items-end">
|
|
<button type="submit" class="btn btn-primary w-100">{% trans "Apply Filter" %}</button>
|
|
</div>
|
|
</div>
|
|
<input type="hidden" name="make_sold" value="{{ selected_make_sales }}">
|
|
</form>
|
|
</div>
|
|
<div class="row g-4 mb-5">{% include 'dashboards/financial_data_cards.html' %}</div>
|
|
<div class="row g-4 mb-5">{% include 'dashboards/chart.html' %}</div>
|
|
</div>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
{% endblock content %}
|
|
{% block customJS %}
|
|
<script>
|
|
// Define a color palette that aligns with the Phoenix template
|
|
const primaryColor = '#7249b6';
|
|
const secondaryColor = '#8193a6';
|
|
const successColor = '#00d074';
|
|
const dangerColor = '#e63757';
|
|
const chartColors = [
|
|
'#7249b6', '#00d074', '#e63757', '#17a2b8', '#ffc107',
|
|
'#8193a6', '#28a745', '#6c757d', '#fd7e14', '#dc3545',
|
|
'#20c997', '#6f42c1', '#e83e8c', '#6610f2', '#007bff',
|
|
'#495057'
|
|
];
|
|
|
|
// Pass translated strings from Django to JavaScript
|
|
const translatedStrings = {
|
|
monthlyCarsSoldLabel: "{% trans 'Total Cars Sold' %}",
|
|
monthlyRevenueLabel: "{% trans 'Monthly Revenue' %}",
|
|
monthlyNetProfitLabel: "{% trans 'Monthly Net Profit' %}",
|
|
salesByMakeLabel: "{% trans 'Car Count by Make' %}",
|
|
salesByModelPrefix: "{% trans 'Cars Sold for' %}",
|
|
inventoryByMakeLabel: "{% trans 'Car Count by Make' %}",
|
|
inventoryByModelLabel: "{% trans 'Cars in Inventory' %}",
|
|
jan: "{% trans 'Jan' %}",
|
|
feb: "{% trans 'Feb' %}",
|
|
mar: "{% trans 'Mar' %}",
|
|
apr: "{% trans 'Apr' %}",
|
|
may: "{% trans 'May' %}",
|
|
jun: "{% trans 'Jun' %}",
|
|
jul: "{% trans 'Jul' %}",
|
|
aug: "{% trans 'Aug' %}",
|
|
sep: "{% trans 'Sep' %}",
|
|
oct: "{% trans 'Oct' %}",
|
|
nov: "{% trans 'Nov' %}",
|
|
dec: "{% trans 'Dec' %}",
|
|
cars: "{% trans 'cars' %}"
|
|
};
|
|
|
|
|
|
// Monthly Cars Sold (Bar Chart)
|
|
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d');
|
|
new Chart(ctx1, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: [
|
|
translatedStrings.jan, translatedStrings.feb, translatedStrings.mar, translatedStrings.apr,
|
|
translatedStrings.may, translatedStrings.jun, translatedStrings.jul, translatedStrings.aug,
|
|
translatedStrings.sep, translatedStrings.oct, translatedStrings.nov, translatedStrings.dec
|
|
],
|
|
datasets: [{
|
|
label: translatedStrings.monthlyCarsSoldLabel,
|
|
data: {{ monthly_cars_sold_json|safe }},
|
|
backgroundColor: primaryColor,
|
|
borderColor: primaryColor,
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: false }
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
|
ticks: {
|
|
color: secondaryColor,
|
|
callback: function(value) {
|
|
if (Number.isInteger(value)) {
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
x: {
|
|
grid: { display: false },
|
|
ticks: { color: secondaryColor }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Monthly Revenue & Profit (Line Chart)
|
|
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d');
|
|
new Chart(ctx2, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [
|
|
translatedStrings.jan, translatedStrings.feb, translatedStrings.mar, translatedStrings.apr,
|
|
translatedStrings.may, translatedStrings.jun, translatedStrings.jul, translatedStrings.aug,
|
|
translatedStrings.sep, translatedStrings.oct, translatedStrings.nov, translatedStrings.dec
|
|
],
|
|
datasets: [
|
|
{
|
|
label: translatedStrings.monthlyRevenueLabel,
|
|
data: {{ monthly_revenue_json|safe }},
|
|
borderColor: primaryColor,
|
|
backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency
|
|
tension: 0.4,
|
|
fill: true,
|
|
pointBackgroundColor: primaryColor,
|
|
pointRadius: 5,
|
|
pointHoverRadius: 8
|
|
},
|
|
{
|
|
label: translatedStrings.monthlyNetProfitLabel,
|
|
data: {{ monthly_net_profit_json|safe }},
|
|
borderColor: successColor,
|
|
backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency
|
|
tension: 0.4,
|
|
fill: true,
|
|
pointBackgroundColor: successColor,
|
|
pointRadius: 5,
|
|
pointHoverRadius: 8
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
labels: { color: '#495057', boxWidth: 20 }
|
|
},
|
|
tooltip: {
|
|
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
|
titleColor: 'white',
|
|
bodyColor: 'white',
|
|
padding: 10,
|
|
callbacks: {
|
|
label: function(context) {
|
|
let label = context.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
if (context.parsed.y !== null) {
|
|
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'SAR' }).format(context.parsed.y);
|
|
}
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
|
ticks: { color: secondaryColor },
|
|
border: { color: secondaryColor }
|
|
},
|
|
y: {
|
|
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
|
ticks: { color: secondaryColor },
|
|
border: { color: secondaryColor }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Sales by Make (Pie Chart)
|
|
function getChartColors(count) {
|
|
const colors = [];
|
|
for (let i = 0; i < count; i++) {
|
|
colors.push(chartColors[i % chartColors.length]);
|
|
}
|
|
return colors;
|
|
}
|
|
|
|
const ctx3 = document.getElementById('salesByBrandChart').getContext('2d');
|
|
new Chart(ctx3, {
|
|
type: 'pie',
|
|
data: {
|
|
labels: {{ sales_by_make_labels_json|safe }},
|
|
datasets: [{
|
|
label: translatedStrings.salesByMakeLabel,
|
|
data: {{ sales_by_make_counts_json|safe }},
|
|
backgroundColor: getChartColors({{ sales_by_make_counts_json|safe }}.length),
|
|
hoverOffset: 15,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'right',
|
|
labels: { color: '#343a40', font: { size: 14 } }
|
|
},
|
|
tooltip: {
|
|
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
|
titleColor: '#fff',
|
|
bodyColor: '#fff',
|
|
callbacks: {
|
|
label: function(context) {
|
|
const label = context.label || '';
|
|
const value = context.parsed || 0;
|
|
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
|
const percentage = ((value / total) * 100).toFixed(2);
|
|
return `${label}: ${value} ${translatedStrings.cars} (${percentage}%)`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 4. Sales by Model (Bar Chart)
|
|
// -----------------------------------------------------------
|
|
const salesDataByModel = JSON.parse('{{ sales_data_by_model_json|safe }}');
|
|
const canvasElementSales = document.getElementById('salesChartByModel');
|
|
let chartInstanceSales = null;
|
|
|
|
if (salesDataByModel.length > 0) {
|
|
const labels = salesDataByModel.map(item => item.id_car_model__name);
|
|
const counts = salesDataByModel.map(item => item.count);
|
|
const backgroundColor = labels.map((_, index) => getChartColors(labels.length)[index]);
|
|
|
|
chartInstanceSales = new Chart(canvasElementSales, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: `${translatedStrings.salesByModelPrefix} {{ selected_make_sales }}`,
|
|
data: counts,
|
|
backgroundColor: backgroundColor,
|
|
borderColor: backgroundColor,
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: function(value) {
|
|
if (Number.isInteger(value)) {
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
let label = context.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
label += Math.round(context.parsed.y);
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// -----------------------------------------------------------
|
|
// 5. Inventory by Make (Pie Chart)
|
|
// -----------------------------------------------------------
|
|
const ctxInventoryMake = document.getElementById('inventoryByMakeChart').getContext('2d');
|
|
new Chart(ctxInventoryMake, {
|
|
type: 'pie',
|
|
data: {
|
|
labels: {{ inventory_by_make_labels_json|safe }},
|
|
datasets: [{
|
|
label: translatedStrings.inventoryByMakeLabel,
|
|
data: {{ inventory_by_make_counts_json|safe }},
|
|
backgroundColor: getChartColors({{ inventory_by_make_counts_json|safe }}.length),
|
|
hoverOffset: 15,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'right',
|
|
labels: { color: '#343a40', font: { size: 14 } }
|
|
},
|
|
tooltip: {
|
|
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
|
titleColor: '#fff',
|
|
bodyColor: '#fff',
|
|
callbacks: {
|
|
label: function(context) {
|
|
const label = context.label || '';
|
|
const value = context.parsed || 0;
|
|
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
|
const percentage = ((value / total) * 100).toFixed(2);
|
|
return `${label}: ${value} ${translatedStrings.cars} (${percentage}%)`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 6. Inventory by Model (Bar Chart)
|
|
// -----------------------------------------------------------
|
|
const inventoryDataByModel = JSON.parse('{{ inventory_data_by_model_json|safe }}');
|
|
const canvasInventoryModel = document.getElementById('inventoryByModelChart');
|
|
const messageInventoryModel = document.getElementById('inventoryByModelMessage');
|
|
|
|
if (inventoryDataByModel.length > 0) {
|
|
canvasInventoryModel.style.display = 'block';
|
|
if (messageInventoryModel) {
|
|
messageInventoryModel.style.display = 'none';
|
|
}
|
|
|
|
const labels = inventoryDataByModel.map(item => item.id_car_model__name);
|
|
const counts = inventoryDataByModel.map(item => item.count);
|
|
const backgroundColor = getChartColors(labels.length);
|
|
|
|
new Chart(canvasInventoryModel, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: translatedStrings.inventoryByModelLabel,
|
|
data: counts,
|
|
backgroundColor: backgroundColor,
|
|
borderColor: backgroundColor,
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: function(value) {
|
|
if (Number.isInteger(value)) {
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
let label = context.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
label += Math.round(context.parsed.y);
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
canvasInventoryModel.style.display = 'none';
|
|
if (messageInventoryModel) {
|
|
messageInventoryModel.style.display = 'flex';
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|