This commit is contained in:
Marwan Alwali 2025-05-03 15:51:01 +03:00
parent 239ea2e66e
commit 3267630578
10 changed files with 107 additions and 174 deletions

View File

@ -22,7 +22,7 @@ from django.conf import settings
from django.db import transaction from django.db import transaction
from django.db.models import Func from django.db.models import Func
from django.contrib import messages from django.contrib import messages
from django.http import JsonResponse from django.http import JsonResponse, HttpResponseForbidden
from django.forms import HiddenInput, ValidationError from django.forms import HiddenInput, ValidationError
from django.shortcuts import HttpResponse from django.shortcuts import HttpResponse
from django.db.models import Sum, F, Count from django.db.models import Sum, F, Count
@ -362,6 +362,8 @@ class ManagerDashboard(LoginRequiredMixin, TemplateView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated: if not request.user.is_authenticated:
return redirect("welcome") return redirect("welcome")
if not getattr(request.user, 'dealer', False):
return HttpResponseForbidden("You are not authorized to view this dashboard.")
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):

View File

@ -28,3 +28,25 @@
.rtl .fa-chevron-right { .rtl .fa-chevron-right {
transform: scaleX(-1); transform: scaleX(-1);
} }
.spinner-container {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
color: #555;
font-size: 14px;
}
.spinner {
width: 18px;
height: 18px;
border: 3px solid #ccc;
border-top-color: #333;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}

View File

@ -120,85 +120,7 @@
/* Echarts Total Sales */ /* Echarts Total Sales */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
const contactsBySourceChartInit = () => {
const { getColor, getData, toggleColor } = window.phoenix.utils;
const chartElContainer = document.querySelector(
'.echart-contact-by-source-container'
);
const chartEl = chartElContainer.querySelector('.echart-contact-by-source');
const chartLabel = chartElContainer.querySelector('[data-label]');
if (chartEl) {
const userOptions = getData(chartEl, 'echarts');
const chart = window.echarts.init(chartEl);
const data = [
{ value: 80, name: 'Organic Search' },
{ value: 65, name: 'Paid Search' },
{ value: 40, name: 'Direct Traffic' },
{ value: 220, name: 'Social Media' },
{ value: 120, name: 'Referrals' },
{ value: 35, name: 'Others Campaigns' }
];
const totalSource = data.reduce((acc, val) => val.value + acc, 0);
if (chartLabel) {
chartLabel.innerHTML = totalSource;
}
const getDefaultOptions = () => ({
color: [
getColor('primary'),
getColor('success'),
getColor('info'),
getColor('info-light'),
toggleColor(getColor('danger-lighter'), getColor('danger-darker')),
toggleColor(getColor('warning-light'), getColor('warning-dark'))
],
tooltip: {
trigger: 'item',
borderWidth: 0,
position: (...params) => handleTooltipPosition(params),
extraCssText: 'z-index: 1000'
},
responsive: true,
maintainAspectRatio: false,
series: [
{
name: 'Contacts by Source',
type: 'pie',
radius: ['55%', '90%'],
startAngle: 90,
avoidLabelOverlap: false,
itemStyle: {
borderColor: getColor('body-bg'),
borderWidth: 3
},
label: {
show: false
},
emphasis: {
label: {
show: false
}
},
labelLine: {
show: false
},
data
}
],
grid: {
bottom: 0,
top: 0,
left: 0,
right: 0,
containLabel: false
}
});
echartSetOption(chart, userOptions, getDefaultOptions);
}
};
// dayjs.extend(advancedFormat); // dayjs.extend(advancedFormat);
@ -1338,7 +1260,6 @@
const { docReady } = window.phoenix.utils; const { docReady } = window.phoenix.utils;
docReady(contactsBySourceChartInit);
docReady(contactsCreatedChartInit); docReady(contactsCreatedChartInit);
docReady(newUsersChartsInit); docReady(newUsersChartsInit);
docReady(newLeadsChartsInit); docReady(newLeadsChartsInit);

View File

@ -60,6 +60,7 @@
} }
</style>--> </style>-->
{% block customCSS %} {% block customCSS %}
{% endblock %} {% endblock %}
</head> </head>
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'> <body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>

View File

@ -1,4 +1,3 @@
{% extends 'base.html' %}
{% load i18n static custom_filters django_ledger%} {% load i18n static custom_filters django_ledger%}
{% block content %} {% block content %}

View File

@ -1,4 +1,3 @@
{% extends 'base.html' %}
{% load i18n static custom_filters django_ledger%} {% load i18n static custom_filters django_ledger%}
{% block content %} {% block content %}

View File

@ -5,55 +5,8 @@
<div class="collapse navbar-collapse" id="navbarVerticalCollapse"> <div class="collapse navbar-collapse" id="navbarVerticalCollapse">
<div class="navbar-vertical-content"> <div class="navbar-vertical-content">
<ul class="navbar-nav flex-column" id="navbarVerticalNav"> <ul class="navbar-nav flex-column" id="navbarVerticalNav">
<li class="nav-item">
<div class="nav-item-wrapper">
<a class="nav-link dropdown-indicator label-1" href="#nv-dashboards" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-dashboards">
<div class="d-flex align-items-center">
<div class="dropdown-indicator-icon-wrapper"><span class="fas fa-caret-right dropdown-indicator-icon"></span></div>
<span class="nav-link-icon">
<span data-feather="pie-chart"></span>
</span>
<span class="nav-link-text">{{ _("Dashboards") }}</span>
</div>
</a>
<div class="parent-wrapper label-1">
<ul class="nav collapse parent" data-bs-parent="#navbarVerticalCollapse" id="nv-dashboards">
<li class="collapsed-nav-item-title d-none">{{ _("Dashboards") }}</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'manager_dashboard' %}">
<div class="d-flex align-items-center"><span class="nav-link-text">{{ _("Manager") }}</span></div>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<div class="d-flex align-items-center"><span class="nav-link-text">{{ _("Inventory") }}</span></div>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'sales_dashboard' %}">
<div class="d-flex align-items-center"><span class="nav-link-text">{{ _("Sales") }}</span></div>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<div class="d-flex align-items-center"><span class="nav-link-text">{{ _("CRM") }}</span></div>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<div class="d-flex align-items-center"><span class="nav-link-text">{{ _("Financials") }}</span></div>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<div class="d-flex align-items-center"><span class="nav-link-text">{{ _("Reports") }}</span></div>
</a>
</li>
</ul>
</div>
</div>
</li>
<hr class="my-0" />
<li class="nav-item"> <li class="nav-item">
<p class="navbar-vertical-label">Apps</p> <p class="navbar-vertical-label">Apps</p>
<hr class="navbar-vertical-line" /> <hr class="navbar-vertical-line" />

View File

@ -1,8 +1,19 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n static %} {% load i18n static custom_filters django_ledger %}
{% block content %} {% block content %}
{% if request.user.is_authenticated %}
<div
id="dashboard-content"
hx-get="{% if request.user.dealer %}{% url 'manager_dashboard' %}{% else %}{% url 'sales_dashboard' %}{% endif %}"
hx-trigger="load"
hx-target="#dashboard-content"
hx-swap="innerHTML"
>
<div class="spinner-container">
<div class="spinner"></div>
<p>Loading dashboard...</p>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -546,39 +546,64 @@
const specificationsContent = document.getElementById("specificationsContent"); const specificationsContent = document.getElementById("specificationsContent");
showSpecificationButton.addEventListener("click", function () { showSpecificationButton.addEventListener("click", function () {
specificationsContent.innerHTML = ""; specificationsContent.innerHTML = "";
fetch(`${ajaxUrl}?action=get_specifications&trim_id={{ car.id_car_trim.id_car_trim }}`, { fetch(`${ajaxUrl}?action=get_specifications&trim_id={{ car.id_car_trim.id_car_trim }}`, {
headers: { headers: {
"X-Requested-With": "XMLHttpRequest", "X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrftoken, "X-CSRFToken": csrftoken,
}, },
}) })
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
if (data.length > 0) { if (data.length > 0) {
data.forEach(function (parent) { data.forEach(function (parent) {
const parentSpec = document.createElement("div"); // Create a section container
parentSpec.classList.add("mb-2"); const section = document.createElement("div");
parentSpec.innerHTML = `<strong>${parent.parent_name}</strong>`; section.classList.add("mb-4", "p-3", "border", "rounded");
parent.specifications.forEach(function (specification) { // Add section title
const specificationDiv = document.createElement("div"); const sectionTitle = document.createElement("h4");
specificationDiv.classList.add("ms-3"); sectionTitle.classList.add("mb-3", "fw-bold");
specificationDiv.innerHTML = `&#183; ${specification.s_name}: ${specification.s_value} ${specification.s_unit}`; sectionTitle.textContent = parent.parent_name;
parentSpec.appendChild(specificationDiv); section.appendChild(sectionTitle);
});
specificationsContent.appendChild(parentSpec); // Create a table for the specifications
}); const specsTable = document.createElement("div");
} else { specsTable.classList.add("row");
specificationsContent.innerHTML = '<p>{% trans "No specifications available." %}</p>';
} parent.specifications.forEach(function (specification) {
}) // Create a row for each specification
.catch((error) => { const specRow = document.createElement("div");
specificationsContent.innerHTML = '<p>{% trans "Error loading specifications." %}</p>'; specRow.classList.add("row", "mb-2");
console.error("Error fetching specifications:", error);
// Left Column: Spec name
const specName = document.createElement("div");
specName.classList.add("col-6", "text-muted");
specName.textContent = specification.s_name;
// Right Column: Spec value + unit
const specValue = document.createElement("div");
specValue.classList.add("col-6", "text-end");
specValue.textContent = `${specification.s_value} ${specification.s_unit || ""}`;
specRow.appendChild(specName);
specRow.appendChild(specValue);
specsTable.appendChild(specRow);
});
section.appendChild(specsTable);
specificationsContent.appendChild(section);
}); });
} else {
specificationsContent.innerHTML = '<p>{% trans "No specifications available." %}</p>';
}
})
.catch((error) => {
specificationsContent.innerHTML = '<p>{% trans "Error loading specifications." %}</p>';
console.error("Error fetching specifications:", error);
}); });
});
document.querySelectorAll(".reserve-btn").forEach((button) => { document.querySelectorAll(".reserve-btn").forEach((button) => {
button.addEventListener("click", async function () { button.addEventListener("click", async function () {

View File

@ -34,11 +34,11 @@
hx-on::before-request="on_before_request()" hx-on::before-request="on_before_request()"
hx-on::after-request="on_after_request()" hx-on::after-request="on_after_request()"
> >
<li class="nav-item"><a class="nav-link px-2 py-1 active" aria-current="page" href="{% url 'car_list' %}"><span>All</span><span class="text-body-tertiary fw-semibold">({{stats.all}})</span></a></li> <li class="nav-item"><a class="nav-link px-2 py-1 active" aria-current="page" href="{% url 'car_list' %}"><span>{{ _("All") }}</span><span class="text-body-tertiary fw-semibold">({{stats.all}})</span></a></li>
<li class="nav-item"><a class="nav-link px-2 py-1" href="{% url 'car_list' %}?status=available"><span>Available</span><span class="text-body-tertiary fw-semibold">({{stats.available}})</span></a></li> <li class="nav-item"><a class="nav-link px-2 py-1" href="{% url 'car_list' %}?status=available"><span>{{ _("Available") }}</span><span class="text-body-tertiary fw-semibold">({{stats.available}})</span></a></li>
<li class="nav-item"><a class="nav-link px-2 py-1" href="{% url 'car_list' %}?status=reserved"><span>Reserved</span><span class="text-body-tertiary fw-semibold">({{stats.reserved}})</span></a></li> <li class="nav-item"><a class="nav-link px-2 py-1" href="{% url 'car_list' %}?status=reserved"><span>{{ _("Reserved") }}</span><span class="text-body-tertiary fw-semibold">({{stats.reserved}})</span></a></li>
<li class="nav-item"><a class="nav-link px-2 py-1" href="{% url 'car_list' %}?status=transfer"><span>Transfer</span><span class="text-body-tertiary fw-semibold">({{stats.transfer}})</span></a></li> <li class="nav-item"><a class="nav-link px-2 py-1" href="{% url 'car_list' %}?status=transfer"><span>{{ _("Transfer") }}</span><span class="text-body-tertiary fw-semibold">({{stats.transfer}})</span></a></li>
<li class="nav-item"><a class="nav-link px-2 py-1" href="{% url 'car_list' %}?status=sold"><span>Sold</span><span class="text-body-tertiary fw-semibold">({{stats.sold}})</span></a></li> <li class="nav-item"><a class="nav-link px-2 py-1" href="{% url 'car_list' %}?status=sold"><span>{{ _("Sold") }}</span><span class="text-body-tertiary fw-semibold">({{stats.sold}})</span></a></li>
<li class="nav-item"><button hx-on:click="toggle_filter()" class="btn btn-sm btn-primary px-2 py-1"><span><span class="fa fa-filter me-1"></span>{{ _("Filter") }}</span><span class="fas fa-caret-down fs-9 ms-1 filter-icon"></span></button></li> <li class="nav-item"><button hx-on:click="toggle_filter()" class="btn btn-sm btn-primary px-2 py-1"><span><span class="fa fa-filter me-1"></span>{{ _("Filter") }}</span><span class="fas fa-caret-down fs-9 ms-1 filter-icon"></span></button></li>
</ul> </ul>
</div> </div>
@ -151,13 +151,13 @@
<td class="align-middle white-space-nowrap statuses"> <td class="align-middle white-space-nowrap statuses">
{% if car.status == "available" %} {% if car.status == "available" %}
<span class="badge badge-phoenix fs-11 badge-phoenix-success">{{car.status}}</span> <span class="badge badge-phoenix fs-11 badge-phoenix-success">{{ _("Available") }}</span>
{% elif car.status == "reserved" %} {% elif car.status == "reserved" %}
<span class="badge badge-phoenix fs-11 badge-phoenix-danger">{{car.status}}</span> <span class="badge badge-phoenix fs-11 badge-phoenix-danger">{{ _("Reserved") }}</span>
{% elif car.status == "sold" %} {% elif car.status == "sold" %}
<span class="badge badge-phoenix fs-11 badge-phoenix-info">{{car.status}}</span> <span class="badge badge-phoenix fs-11 badge-phoenix-info">{{ _("Sold") }}</span>
{% elif car.status == "transfer" %} {% elif car.status == "transfer" %}
<span class="badge badge-phoenix fs-11 badge-phoenix-warning">{{car.status}}</span> <span class="badge badge-phoenix fs-11 badge-phoenix-warning">{{ _("Transfer") }}</span>
{% endif %} {% endif %}
</td> </td>
<td class="align-middle text-end white-space-nowrap pe-0 action"> <td class="align-middle text-end white-space-nowrap pe-0 action">