Autosave: 20260126-073515

This commit is contained in:
Flatlogic Bot 2026-01-26 07:35:15 +00:00
parent 88187c1cc8
commit 1e9f216ade
24 changed files with 1123 additions and 73 deletions

Binary file not shown.

Binary file not shown.

View File

@ -8,7 +8,6 @@ from django.shortcuts import render
from django.utils.html import format_html
from django.contrib import messages
from .whatsapp_utils import send_whatsapp_message_detailed
from django.core.mail import send_html_email
from django.conf import settings
from .mail import send_html_email
import logging

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-01-26 06:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0017_driverrating'),
]
operations = [
migrations.AlterField(
model_name='otpverification',
name='purpose',
field=models.CharField(choices=[('profile_update', 'Profile Update'), ('password_reset', 'Password Reset'), ('registration', 'Registration'), ('login', 'Login')], default='profile_update', max_length=20),
),
]

View File

@ -213,6 +213,7 @@ class OTPVerification(models.Model):
('profile_update', _('Profile Update')),
('password_reset', _('Password Reset')),
('registration', _('Registration')),
('login', _('Login')),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
code = models.CharField(max_length=6)
@ -269,4 +270,4 @@ class DriverRating(models.Model):
class Meta:
verbose_name = _('Driver Rating')
verbose_name_plural = _('Driver Ratings')
verbose_name_plural = _('Driver Ratings')

View File

@ -180,7 +180,7 @@
<span class="stat-title">{% trans "Total Revenue" %}</span>
<div class="stat-icon icon-revenue">💰</div>
</div>
<div class="stat-value">{{ stats.total_revenue|floatform:2 }} <span style="font-size: 1rem; color: #6b7280;">OMR</span></div>
<div class="stat-value">{{ stats.total_revenue|floatformat:3 }} <span style="font-size: 1rem; color: #6b7280;">OMR</span></div>
</div>
<div class="stat-card">

View File

@ -245,5 +245,38 @@
<!-- Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Chat Widget -->
<button id="masar-chat-toggle" class="d-flex align-items-center justify-content-center">
<i class="bi bi-chat-dots-fill fs-4"></i>
</button>
<div id="masar-chat-widget" class="d-none bg-white rounded-4 shadow overflow-hidden">
<div class="bg-dark text-white p-3 d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<i class="bi bi-robot me-2 fs-5"></i>
<h6 class="mb-0 fw-bold">MasarX AI</h6>
</div>
<button id="masar-chat-close" class="btn btn-sm btn-link text-white p-0">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div id="masar-chat-messages" class="flex-grow-1 p-3 bg-light overflow-auto">
<div class="d-flex mb-3 justify-content-start">
<div class="p-3 rounded-3 shadow-sm bg-light text-dark" style="max-width: 80%;">
{% trans "Hello! How can I help you with your shipments today?" %}
</div>
</div>
</div>
<div class="p-3 border-top bg-white">
<form id="masar-chat-form" class="d-flex">
<input type="text" id="masar-chat-input" class="form-control me-2" placeholder="{% trans "Type a message..." %}" autocomplete="off">
<button type="submit" class="btn btn-masarx-primary px-3">
<i class="bi bi-send-fill"></i>
</button>
</form>
</div>
</div>
<script src="{% static "js/chat.js" %}?v={{ deployment_timestamp }}"></script>
</body>
</html>

View File

@ -20,37 +20,132 @@
<div class="tab-content" id="pills-tabContent">
<!-- Available Shipments -->
<div class="tab-pane fade show active" id="pills-available" role="tabpanel">
{% if available_parcels %}
<div class="row g-4">
{% for parcel in available_parcels %}
<div class="col-md-6 col-lg-4">
<div class="card border-0 shadow-sm h-100" style="border-radius: 15px;">
<div class="card-body">
<h5 class="card-title">{{ parcel.description|truncatechars:30 }}</h5>
<p class="card-text mb-1 small"><strong>{% trans "Pickup" %}:</strong> {{ parcel.pickup_address }}</p>
<p class="card-text mb-3 small"><strong>{% trans "Delivery" %}:</strong> {{ parcel.delivery_address }}</p>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted small"><strong>{% trans "Weight" %}:</strong> {{ parcel.weight }} kg</span>
</div>
<!-- Bid/Price Highlight -->
<div class="bg-light p-2 rounded text-center mb-3 border border-primary border-opacity-25">
<small class="text-uppercase text-muted fw-bold" style="font-size: 0.7rem;">{% trans "Shipper's Offer (Bid)" %}</small>
<div class="text-primary fw-bold fs-4">{{ parcel.price }} <span class="fs-6">OMR</span></div>
</div>
<!-- Toolbar -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="mb-0 text-muted">{% trans "Browse Shipments" %}</h5>
<div class="btn-group" role="group" aria-label="View toggle">
<button type="button" class="btn btn-outline-primary active" id="btn-grid-view">
<i class="bi bi-grid"></i> {% trans "Grid" %}
</button>
<button type="button" class="btn btn-outline-primary" id="btn-list-view">
<i class="bi bi-list"></i> {% trans "List" %}
</button>
</div>
</div>
<form action="{% url 'accept_parcel' parcel.id %}" method="POST">
{% csrf_token %}
<button type="submit" class="btn btn-masarx-primary w-100">{% trans "Accept Shipment" %}</button>
</form>
{% if available_parcels %}
<!-- Grid View -->
<div id="view-grid" class="row g-4">
{% for parcel in available_parcels %}
<div class="col-md-6 col-lg-4">
<div class="card border-0 shadow-sm h-100" style="border-radius: 15px;">
<div class="card-body">
<h5 class="card-title">{{ parcel.description|truncatechars:30 }}</h5>
<p class="card-text mb-1 small"><strong>{% trans "Pickup" %}:</strong> {{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }}</p>
<p class="card-text mb-3 small"><strong>{% trans "Delivery" %}:</strong> {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}</p>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted small"><strong>{% trans "Weight" %}:</strong> {{ parcel.weight }} kg</span>
</div>
<!-- Bid/Price Highlight -->
<div class="bg-light p-2 rounded text-center mb-3 border border-primary border-opacity-25">
<small class="text-uppercase text-muted fw-bold" style="font-size: 0.7rem;">{% trans "Shipper's Offer (Bid)" %}</small>
<div class="text-primary fw-bold fs-4">{{ parcel.price }} <span class="fs-6">OMR</span></div>
</div>
<form action="{% url 'accept_parcel' parcel.id %}" method="POST">
{% csrf_token %}
<button type="submit" class="btn btn-masarx-primary w-100">{% trans "Accept Shipment" %}</button>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<!-- List View (Initially Hidden) -->
<div id="view-list" class="d-none">
<div class="d-flex flex-column gap-3">
{% for parcel in available_parcels %}
<div class="card border-0 shadow-sm rounded-3">
<div class="card-body p-3">
<div class="row align-items-center">
<div class="col-md-8 mb-3 mb-md-0">
<h5 class="card-title mb-2">{{ parcel.description|truncatechars:80 }}</h5>
<div class="d-flex flex-wrap gap-3 text-muted small">
<span class="d-flex align-items-center gap-1">
<i class="bi bi-geo-alt-fill text-primary"></i>
<strong>{% trans "From" %}:</strong> {{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }}
</span>
<span class="d-flex align-items-center gap-1">
<i class="bi bi-geo-alt-fill text-danger"></i>
<strong>{% trans "To" %}:</strong> {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}
</span>
<span class="d-flex align-items-center gap-1">
<i class="bi bi-box-seam"></i> {{ parcel.weight }} kg
</span>
</div>
</div>
<div class="col-md-4 text-md-end">
<div class="d-flex flex-column align-items-md-end gap-2">
<div class="text-primary fw-bold fs-5">{{ parcel.price }} OMR</div>
<form action="{% url 'accept_parcel' parcel.id %}" method="POST" class="w-100 w-md-auto">
{% csrf_token %}
<button type="submit" class="btn btn-masarx-primary btn-sm w-100">{% trans "Accept" %}</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Pagination -->
{% if available_parcels.has_other_pages %}
<nav aria-label="Page navigation" class="mt-5">
<ul class="pagination justify-content-center">
{% if available_parcels.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ available_parcels.previous_page_number }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link" aria-hidden="true">&laquo;</span>
</li>
{% endif %}
{% for i in available_parcels.paginator.page_range %}
{% if available_parcels.number == i %}
<li class="page-item active"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="?page={{ i }}">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if available_parcels.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ available_parcels.next_page_number }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link" aria-hidden="true">&raquo;</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<p class="text-center py-5">{% trans "No shipments available at the moment." %}</p>
<p class="text-center py-5">{% trans "No shipments available at the moment." %}</p>
{% endif %}
</div>
@ -67,7 +162,7 @@
<span class="badge bg-primary">{{ parcel.get_status_display }}</span>
</div>
<h5 class="card-title">{{ parcel.description|truncatechars:30 }}</h5>
<p class="card-text mb-1 small"><strong>{% trans "To" %}:</strong> {{ parcel.delivery_address }}</p>
<p class="card-text mb-1 small"><strong>{% trans "To" %}:</strong> {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}</p>
<p class="card-text mb-3 small"><strong>{% trans "Receiver" %}:</strong> {{ parcel.receiver_name }}</p>
<form action="{% url 'update_status' parcel.id %}" method="POST" class="d-flex gap-2">
@ -113,8 +208,8 @@
<tr>
<td class="ps-4">{{ parcel.created_at|date:"Y-m-d" }}</td>
<td><span class="badge bg-light text-dark">#{{ parcel.tracking_number }}</span></td>
<td>{{ parcel.pickup_city.name|default:parcel.pickup_address }}</td>
<td>{{ parcel.delivery_city.name|default:parcel.delivery_address }}</td>
<td>{{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }}</td>
<td>{{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}</td>
<td>{{ parcel.price }} OMR</td>
<td>
<span class="badge {% if parcel.status == 'delivered' %}bg-success{% else %}bg-danger{% endif %}">
@ -136,4 +231,39 @@
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const gridViewBtn = document.getElementById('btn-grid-view');
const listViewBtn = document.getElementById('btn-list-view');
const gridView = document.getElementById('view-grid');
const listView = document.getElementById('view-list');
function setView(view) {
if (view === 'list') {
if (gridView) gridView.classList.add('d-none');
if (listView) listView.classList.remove('d-none');
if (listViewBtn) listViewBtn.classList.add('active');
if (gridViewBtn) gridViewBtn.classList.remove('active');
localStorage.setItem('driverDashboardView', 'list');
} else {
if (listView) listView.classList.add('d-none');
if (gridView) gridView.classList.remove('d-none');
if (gridViewBtn) gridViewBtn.classList.add('active');
if (listViewBtn) listViewBtn.classList.remove('active');
localStorage.setItem('driverDashboardView', 'grid');
}
}
// Check local storage or default to grid
const savedView = localStorage.getItem('driverDashboardView');
if (savedView) {
setView(savedView);
}
// Bind events
if (gridViewBtn) gridViewBtn.addEventListener('click', () => setView('grid'));
if (listViewBtn) listViewBtn.addEventListener('click', () => setView('list'));
});
</script>
{% endblock %}

View File

@ -86,6 +86,91 @@
</div>
</section>
<!-- Top Performers Section -->
<section class="py-5 bg-white border-top">
<div class="container py-5">
<div class="row g-5">
<!-- Top Drivers -->
<div class="col-lg-6">
<h3 class="mb-4 text-center">
<i class="bi bi-trophy text-warning me-2"></i>{% trans "Top Rated Drivers" %}
</h3>
<div class="list-group list-group-flush shadow-sm rounded-3 overflow-hidden">
{% for driver in top_drivers %}
<div class="list-group-item d-flex align-items-center p-3">
<div class="me-3 position-relative">
{% if driver.profile_picture %}
<img src="{{ driver.profile_picture.url }}" class="rounded-circle" width="50" height="50" style="object-fit: cover;">
{% else %}
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center text-muted fw-bold" style="width: 50px; height: 50px;">
{{ driver.user.first_name|first|upper }}
</div>
{% endif %}
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning text-dark border border-white">
{{ forloop.counter }}
</span>
</div>
<div class="flex-grow-1">
<h6 class="mb-0">{{ driver.user.first_name }} {{ driver.user.last_name|first }}.</h6>
<small class="text-muted">{% trans "Driver" %}</small>
</div>
<div class="text-end">
<div class="text-warning small mb-1">
<i class="bi bi-star-fill"></i> {{ driver.avg_rating|floatformat:1 }}
</div>
<small class="text-muted">({{ driver.rating_count }} {% trans "reviews" %})</small>
</div>
</div>
{% empty %}
<div class="list-group-item p-4 text-center text-muted">
{% trans "No ratings yet. Be the first!" %}
</div>
{% endfor %}
</div>
</div>
<!-- Top Shippers -->
<div class="col-lg-6">
<h3 class="mb-4 text-center">
<i class="bi bi-box-seam text-primary me-2"></i>{% trans "Top Shippers" %}
</h3>
<div class="list-group list-group-flush shadow-sm rounded-3 overflow-hidden">
{% for shipper in top_shippers %}
<div class="list-group-item d-flex align-items-center p-3">
<div class="me-3 position-relative">
{% if shipper.profile_picture %}
<img src="{{ shipper.profile_picture.url }}" class="rounded-circle" width="50" height="50" style="object-fit: cover;">
{% else %}
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center text-muted fw-bold" style="width: 50px; height: 50px;">
{{ shipper.user.first_name|first|upper }}
</div>
{% endif %}
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-primary text-white border border-white">
{{ forloop.counter }}
</span>
</div>
<div class="flex-grow-1">
<h6 class="mb-0">{{ shipper.user.first_name }} {{ shipper.user.last_name|first }}.</h6>
<small class="text-muted">{% trans "Shipper" %}</small>
</div>
<div class="text-end">
<div class="fw-bold text-primary mb-1">
{{ shipper.shipment_count }}
</div>
<small class="text-muted">{% trans "Shipments" %}</small>
</div>
</div>
{% empty %}
<div class="list-group-item p-4 text-center text-muted">
{% trans "No shipments yet. Start shipping now!" %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</section>
<!-- Testimonials Section -->
{% if testimonials %}
<section class="py-5 bg-light">

View File

@ -22,34 +22,81 @@
<p class="text-muted small">{% trans "Please login to your account" %}</p>
</div>
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label class="form-label fw-medium">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
<div class="text-danger small mt-1">{{ field.errors }}</div>
{% endif %}
</div>
{% endfor %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="form-check">
<!-- Optional: Remember Me logic could go here later -->
</div>
<a href="{% url 'password_reset' %}" class="text-decoration-none text-muted small hover-orange">
{% trans "Forgot Password?" %}
</a>
<!-- Login Method Tabs -->
<ul class="nav nav-pills nav-fill mb-4 p-1 bg-light rounded-3" id="loginTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active fw-bold rounded-3" id="password-tab" data-bs-toggle="tab" data-bs-target="#password-login" type="button" role="tab" aria-controls="password-login" aria-selected="true">{% trans "Password" %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link fw-bold rounded-3" id="otp-tab" data-bs-toggle="tab" data-bs-target="#otp-login" type="button" role="tab" aria-controls="otp-login" aria-selected="false">{% trans "OTP Login" %}</button>
</li>
</ul>
<div class="tab-content" id="loginTabContent">
<!-- Password Login Tab -->
<div class="tab-pane fade show active" id="password-login" role="tabpanel" aria-labelledby="password-tab">
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label class="form-label fw-medium">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
<div class="text-danger small mt-1">{{ field.errors }}</div>
{% endif %}
</div>
{% endfor %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="form-check">
<!-- Optional: Remember Me logic could go here later -->
</div>
<a href="{% url 'password_reset' %}" class="text-decoration-none text-muted small hover-orange">
{% trans "Forgot Password?" %}
</a>
</div>
<button type="submit" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3">{% trans "Login" %}</button>
</form>
</div>
<button type="submit" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3">{% trans "Login" %}</button>
<div class="text-center">
<span class="text-muted small">{% trans "Don't have an account?" %}</span>
<a href="{% url 'register' %}" class="text-masarx-orange text-decoration-none fw-bold ms-1">{% trans "Register" %}</a>
<!-- OTP Login Tab -->
<div class="tab-pane fade" id="otp-login" role="tabpanel" aria-labelledby="otp-tab">
<div id="otp-step-1">
<div class="mb-3">
<label for="otp-identifier" class="form-label fw-medium">{% trans "Email or Phone Number" %}</label>
<input type="text" class="form-control" id="otp-identifier" placeholder="{% trans 'e.g. user@example.com or 96812345678' %}">
<div id="otp-identifier-error" class="text-danger small mt-1 d-none"></div>
</div>
<button type="button" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3" id="btn-send-otp">
<span id="btn-send-otp-text">{% trans "Send OTP" %}</span>
<span id="btn-send-otp-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
</button>
</div>
<div id="otp-step-2" class="d-none">
<div class="alert alert-info py-2 small" id="otp-sent-msg"></div>
<div class="mb-3">
<label for="otp-code" class="form-label fw-medium">{% trans "Enter OTP Code" %}</label>
<input type="text" class="form-control text-center letter-spacing-2" id="otp-code" placeholder="123456" maxlength="6">
<div id="otp-code-error" class="text-danger small mt-1 d-none"></div>
</div>
<button type="button" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3" id="btn-verify-otp">
<span id="btn-verify-otp-text">{% trans "Verify & Login" %}</span>
<span id="btn-verify-otp-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
</button>
<div class="text-center">
<button type="button" class="btn btn-link text-muted small text-decoration-none" id="btn-back-otp">{% trans "Back" %}</button>
</div>
</div>
</div>
</form>
</div>
<div class="text-center border-top pt-3 mt-2">
<span class="text-muted small">{% trans "Don't have an account?" %}</span>
<a href="{% url 'register' %}" class="text-masarx-orange text-decoration-none fw-bold ms-1">{% trans "Register" %}</a>
</div>
</div>
</div>
</div>
@ -58,6 +105,15 @@
</section>
<style>
.nav-pills .nav-link {
color: #6c757d;
border-radius: 8px;
padding: 10px 15px;
}
.nav-pills .nav-link.active {
background-color: var(--accent-orange);
color: white;
}
.form-control {
border-radius: 8px;
padding: 12px 15px;
@ -85,5 +141,130 @@
.hover-orange:hover {
color: var(--accent-orange) !important;
}
.letter-spacing-2 {
letter-spacing: 4px;
font-size: 1.2rem;
}
</style>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const btnSendOtp = document.getElementById('btn-send-otp');
const btnVerifyOtp = document.getElementById('btn-verify-otp');
const btnBackOtp = document.getElementById('btn-back-otp');
const otpStep1 = document.getElementById('otp-step-1');
const otpStep2 = document.getElementById('otp-step-2');
const inputIdentifier = document.getElementById('otp-identifier');
const inputCode = document.getElementById('otp-code');
const errorIdentifier = document.getElementById('otp-identifier-error');
const errorCode = document.getElementById('otp-code-error');
const msgSent = document.getElementById('otp-sent-msg');
let userId = null;
function showLoading(btnId, show) {
const btn = document.getElementById(btnId);
const textSpan = document.getElementById(btnId + '-text');
const spinnerSpan = document.getElementById(btnId + '-spinner');
if (show) {
btn.disabled = true;
textSpan.classList.add('d-none');
spinnerSpan.classList.remove('d-none');
} else {
btn.disabled = false;
textSpan.classList.remove('d-none');
spinnerSpan.classList.add('d-none');
}
}
function showError(element, message) {
element.innerText = message;
element.classList.remove('d-none');
}
function clearErrors() {
errorIdentifier.classList.add('d-none');
errorCode.classList.add('d-none');
}
btnSendOtp.addEventListener('click', function() {
const identifier = inputIdentifier.value.trim();
if (!identifier) {
showError(errorIdentifier, "{% trans 'Please enter your email or phone number.' %}");
return;
}
clearErrors();
showLoading('btn-send-otp', true);
fetch("{% url 'request_login_otp' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': '{{ csrf_token }}'
},
body: 'identifier=' + encodeURIComponent(identifier)
})
.then(response => response.json())
.then(data => {
showLoading('btn-send-otp', false);
if (data.success) {
userId = data.user_id;
msgSent.innerText = data.message;
otpStep1.classList.add('d-none');
otpStep2.classList.remove('d-none');
} else {
showError(errorIdentifier, data.message);
}
})
.catch(error => {
showLoading('btn-send-otp', false);
showError(errorIdentifier, "{% trans 'An error occurred. Please try again.' %}");
console.error('Error:', error);
});
});
btnVerifyOtp.addEventListener('click', function() {
const code = inputCode.value.trim();
if (!code) {
showError(errorCode, "{% trans 'Please enter the code.' %}");
return;
}
clearErrors();
showLoading('btn-verify-otp', true);
fetch("{% url 'verify_login_otp' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': '{{ csrf_token }}'
},
body: 'user_id=' + encodeURIComponent(userId) + '&code=' + encodeURIComponent(code)
})
.then(response => response.json())
.then(data => {
showLoading('btn-verify-otp', false);
if (data.success) {
window.location.href = data.redirect_url;
} else {
showError(errorCode, data.message);
}
})
.catch(error => {
showLoading('btn-verify-otp', false);
showError(errorCode, "{% trans 'An error occurred. Please try again.' %}");
console.error('Error:', error);
});
});
btnBackOtp.addEventListener('click', function() {
otpStep2.classList.add('d-none');
otpStep1.classList.remove('d-none');
inputCode.value = '';
clearErrors();
});
});
</script>
{% endblock %}

View File

@ -20,10 +20,26 @@
<div class="tab-content" id="pills-tabContent">
<!-- Active Shipments (Card View) -->
<!-- Active Shipments -->
<div class="tab-pane fade show active" id="pills-active" role="tabpanel">
<!-- Toolbar -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="mb-0 text-muted">{% trans "Current Shipments" %}</h5>
<div class="btn-group" role="group" aria-label="View toggle">
<button type="button" class="btn btn-outline-primary active" id="btn-grid-view">
<i class="bi bi-grid"></i> {% trans "Grid" %}
</button>
<button type="button" class="btn btn-outline-primary" id="btn-list-view">
<i class="bi bi-list"></i> {% trans "List" %}
</button>
</div>
</div>
{% if active_parcels %}
<div class="row g-4">
<!-- Grid View -->
<div id="view-grid" class="row g-4">
{% for parcel in active_parcels %}
<div class="col-md-6 col-lg-4">
<div class="card border-0 shadow-sm h-100" style="border-radius: 15px;">
@ -35,8 +51,8 @@
</span>
</div>
<h5 class="card-title">{{ parcel.description|truncatechars:30 }}</h5>
<p class="card-text mb-1 small text-muted"><i class="fas fa-map-marker-alt"></i> <strong>{% trans "From" %}:</strong> {{ parcel.pickup_address }}</p>
<p class="card-text mb-3 small text-muted"><i class="fas fa-flag-checkered"></i> <strong>{% trans "To" %}:</strong> {{ parcel.delivery_address }}</p>
<p class="card-text mb-1 small text-muted"><i class="fas fa-map-marker-alt"></i> <strong>{% trans "From" %}:</strong> {{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }}</p>
<p class="card-text mb-3 small text-muted"><i class="fas fa-flag-checkered"></i> <strong>{% trans "To" %}:</strong> {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}</p>
<div class="d-flex justify-content-between align-items-center mb-3">
<span class="text-primary fw-bold">{{ parcel.price }} OMR</span>
@ -61,6 +77,100 @@
</div>
{% endfor %}
</div>
<!-- List View (Initially Hidden) -->
<div id="view-list" class="d-none">
<div class="d-flex flex-column gap-3">
{% for parcel in active_parcels %}
<div class="card border-0 shadow-sm rounded-3">
<div class="card-body p-3">
<div class="row align-items-center">
<div class="col-md-7 mb-3 mb-md-0">
<div class="d-flex align-items-center gap-2 mb-2">
<h5 class="card-title mb-0">{{ parcel.description|truncatechars:60 }}</h5>
<span class="badge bg-light text-dark small">#{{ parcel.tracking_number }}</span>
</div>
<div class="d-flex flex-wrap gap-3 text-muted small">
<span class="d-flex align-items-center gap-1">
<i class="bi bi-geo-alt-fill text-primary"></i>
<strong>{% trans "From" %}:</strong> {{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }}
</span>
<span class="d-flex align-items-center gap-1">
<i class="bi bi-geo-alt-fill text-danger"></i>
<strong>{% trans "To" %}:</strong> {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}
</span>
<span class="d-flex align-items-center gap-1">
<i class="bi bi-truck"></i>
{% if parcel.carrier %}{{ parcel.carrier.get_full_name|default:parcel.carrier.username }}{% else %}{% trans "Waiting" %}{% endif %}
</span>
</div>
</div>
<div class="col-md-5 text-md-end">
<div class="d-flex flex-column align-items-md-end gap-2">
<div class="d-flex align-items-center gap-2">
<span class="text-primary fw-bold fs-5">{{ parcel.price }} OMR</span>
<span class="badge {% if parcel.status == 'delivered' %}bg-success{% elif parcel.status == 'cancelled' %}bg-danger{% else %}bg-warning{% endif %}">
{{ parcel.get_status_display }}
</span>
</div>
{% if parcel.payment_status == 'pending' and payments_enabled %}
<a href="{% url 'initiate_payment' parcel.id %}" class="btn btn-sm btn-outline-primary w-100 w-md-auto">
<i class="fas fa-credit-card me-1"></i> {% trans "Pay Now" %}
</a>
{% else %}
<span class="badge {% if parcel.payment_status == 'paid' %}bg-success{% else %}bg-secondary{% endif %}">
{{ parcel.get_payment_status_display }}
</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Pagination -->
{% if active_parcels.has_other_pages %}
<nav aria-label="Page navigation" class="mt-5">
<ul class="pagination justify-content-center">
{% if active_parcels.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ active_parcels.previous_page_number }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link" aria-hidden="true">&laquo;</span>
</li>
{% endif %}
{% for i in active_parcels.paginator.page_range %}
{% if active_parcels.number == i %}
<li class="page-item active"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="?page={{ i }}">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if active_parcels.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ active_parcels.next_page_number }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link" aria-hidden="true">&raquo;</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<p class="lead">{% trans "You have no active shipments." %}</p>
@ -135,4 +245,39 @@
</div>
</div>
</div>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const gridViewBtn = document.getElementById('btn-grid-view');
const listViewBtn = document.getElementById('btn-list-view');
const gridView = document.getElementById('view-grid');
const listView = document.getElementById('view-list');
function setView(view) {
if (view === 'list') {
if (gridView) gridView.classList.add('d-none');
if (listView) listView.classList.remove('d-none');
if (listViewBtn) listViewBtn.classList.add('active');
if (gridViewBtn) gridViewBtn.classList.remove('active');
localStorage.setItem('shipperDashboardView', 'list');
} else {
if (listView) listView.classList.add('d-none');
if (gridView) gridView.classList.remove('d-none');
if (gridViewBtn) gridViewBtn.classList.add('active');
if (listViewBtn) listViewBtn.classList.remove('active');
localStorage.setItem('shipperDashboardView', 'grid');
}
}
// Check local storage or default to grid
const savedView = localStorage.getItem('shipperDashboardView');
if (savedView) {
setView(savedView);
}
// Bind events
if (gridViewBtn) gridViewBtn.addEventListener('click', () => setView('grid'));
if (listViewBtn) listViewBtn.addEventListener('click', () => setView('list'));
});
</script>
{% endblock %}

View File

@ -43,6 +43,7 @@ urlpatterns = [
path('article/1/', views.article_detail, name='article_detail'),
path('ajax/get-governates/', views.get_governates, name='get_governates'),
path('ajax/get-cities/', views.get_cities, name='get_cities'),
path('ajax/chatbot/', views.chatbot, name='chatbot'),
path('privacy-policy/', views.privacy_policy, name='privacy_policy'),
path('terms-conditions/', views.terms_conditions, name='terms_conditions'),
path('contact/', views.contact, name='contact'),
@ -50,4 +51,8 @@ urlpatterns = [
path('profile/', views.profile_view, name='profile'),
path('profile/edit/', views.edit_profile, name='edit_profile'),
path('profile/verify-otp/', views.verify_otp_view, name='verify_otp'),
# OTP Login
path('login/request-otp/', views.request_login_otp, name='request_login_otp'),
path('login/verify-otp/', views.verify_login_otp, name='verify_login_otp'),
]

View File

@ -13,6 +13,9 @@ from django.urls import reverse
from .payment_utils import ThawaniPay
from django.conf import settings
from django.core.mail import send_mail
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.views.decorators.http import require_POST
from django.db.models import Avg, Count
import random
import string
from .whatsapp_utils import (
@ -23,6 +26,8 @@ from .whatsapp_utils import (
send_whatsapp_message
)
from .mail import send_contact_message, send_html_email
import json
from ai.local_ai_api import LocalAIApi
def index(request):
tracking_id = request.GET.get('tracking_id')
@ -36,11 +41,24 @@ def index(request):
testimonials = Testimonial.objects.filter(is_active=True)
# Top 5 Drivers (by Average Rating)
top_drivers = Profile.objects.filter(role='car_owner').annotate(
avg_rating=Avg('user__received_ratings__rating'),
rating_count=Count('user__received_ratings')
).filter(rating_count__gt=0).order_by('-avg_rating')[:5]
# Top 5 Shippers (by Shipment Count)
top_shippers = Profile.objects.filter(role='shipper').annotate(
shipment_count=Count('user__sent_parcels')
).order_by('-shipment_count')[:5]
return render(request, 'core/index.html', {
'parcel': parcel,
'error': error,
'tracking_id': tracking_id,
'testimonials': testimonials
'testimonials': testimonials,
'top_drivers': top_drivers,
'top_shippers': top_shippers
})
def register(request):
@ -58,14 +76,16 @@ def register(request):
# Send OTP
method = form.cleaned_data.get('verification_method', 'email')
otp_msg = _("Your Masar Verification Code is %(code)s") % {'code': code}
if method == 'whatsapp':
phone = user.profile.phone_number
send_whatsapp_message(phone, f"Your verification code is: {code}")
send_whatsapp_message(phone, otp_msg)
messages.info(request, _("Verification code sent to WhatsApp."))
else:
send_html_email(
subject=_('Verification Code'),
message=f'Your verification code is: {code}',
message=otp_msg,
recipient_list=[user.email],
title=_('Welcome to Masar!'),
request=request
@ -124,9 +144,20 @@ def dashboard(request):
if profile.role == 'shipper':
all_parcels = Parcel.objects.filter(shipper=request.user).order_by('-created_at')
active_parcels = all_parcels.exclude(status__in=['delivered', 'cancelled'])
active_parcels_list = all_parcels.exclude(status__in=['delivered', 'cancelled'])
history_parcels = all_parcels.filter(status__in=['delivered', 'cancelled'])
# Pagination for Active Shipments
page = request.GET.get('page', 1)
paginator = Paginator(active_parcels_list, 9) # Show 9 parcels per page
try:
active_parcels = paginator.page(page)
except PageNotAnInteger:
active_parcels = paginator.page(1)
except EmptyPage:
active_parcels = paginator.page(paginator.num_pages)
platform_profile = PlatformProfile.objects.first()
payments_enabled = platform_profile.enable_payment if platform_profile else True
@ -142,9 +173,20 @@ def dashboard(request):
payments_enabled = platform_profile.enable_payment if platform_profile else True
if payments_enabled:
available_parcels = Parcel.objects.filter(status='pending', payment_status='paid').order_by('-created_at')
available_parcels_list = Parcel.objects.filter(status='pending', payment_status='paid').order_by('-created_at')
else:
available_parcels = Parcel.objects.filter(status='pending').order_by('-created_at')
available_parcels_list = Parcel.objects.filter(status='pending').order_by('-created_at')
# Pagination for Available Shipments
page = request.GET.get('page', 1)
paginator = Paginator(available_parcels_list, 9) # Show 9 parcels per page
try:
available_parcels = paginator.page(page)
except PageNotAnInteger:
available_parcels = paginator.page(1)
except EmptyPage:
available_parcels = paginator.page(paginator.num_pages)
# Active: Picked up or In Transit
my_parcels = Parcel.objects.filter(carrier=request.user).exclude(status__in=['delivered', 'cancelled']).order_by('-created_at')
@ -360,10 +402,12 @@ def edit_profile(request):
# 4. Send OTP
method = data.get('otp_method', 'email')
otp_msg = _("Your Masar Update Code is %(code)s") % {'code': code}
if method == 'whatsapp':
# Use current phone if available, else new phone
phone = request.user.profile.phone_number or data['phone_number']
send_whatsapp_message(phone, f"Your verification code is: {code}")
send_whatsapp_message(phone, otp_msg)
messages.info(request, _("Verification code sent to WhatsApp."))
else:
# Default to email
@ -371,7 +415,7 @@ def edit_profile(request):
target_email = data['email']
send_html_email(
subject=_('Verification Code'),
message=f'Your verification code is: {code}',
message=otp_msg,
recipient_list=[target_email],
title=_('Profile Update Verification'),
request=request
@ -473,3 +517,136 @@ def rate_driver(request, parcel_id):
'form': form,
'parcel': parcel
})
@require_POST
def request_login_otp(request):
identifier = request.POST.get('identifier')
if not identifier:
return JsonResponse({'success': False, 'message': _('Please enter an email or phone number.')})
# Clean identifier
identifier = identifier.strip()
user = None
method = 'email'
# Try to find user by email
user = User.objects.filter(email__iexact=identifier).first()
# If not found, try by phone number
if not user:
profile = Profile.objects.filter(phone_number=identifier).first()
if profile:
user = profile.user
method = 'whatsapp'
else:
# Fallback: maybe they entered a phone without country code or with?
# For now, simplistic search
pass
if not user:
# Don't reveal if user exists or not for security, but for UX on this project we can be a bit more helpful
return JsonResponse({'success': False, 'message': _('User not found with this email or phone number.')})
if not user.is_active:
return JsonResponse({'success': False, 'message': _('Account is inactive. Please verify registration first.')})
# Generate OTP
code = ''.join(random.choices(string.digits, k=6))
OTPVerification.objects.create(user=user, code=code, purpose='login')
# Send OTP
otp_msg = _("Your Masar Login Code is %(code)s. Do not share this code.") % {'code': code}
try:
if method == 'whatsapp':
phone = user.profile.phone_number
send_whatsapp_message(phone, otp_msg)
message_sent = _("OTP sent to your WhatsApp.")
else:
send_html_email(
subject=_('Login OTP'),
message=otp_msg,
recipient_list=[user.email],
title=_('Login Verification'),
request=request
)
message_sent = _("OTP sent to your email.")
return JsonResponse({'success': True, 'message': message_sent, 'user_id': user.id})
except Exception as e:
return JsonResponse({'success': False, 'message': _('Failed to send OTP. Please try again.')})
@require_POST
def verify_login_otp(request):
user_id = request.POST.get('user_id')
code = request.POST.get('code')
if not user_id or not code:
return JsonResponse({'success': False, 'message': _('Invalid request.')})
try:
user = User.objects.get(id=user_id)
otp = OTPVerification.objects.filter(
user=user,
code=code,
purpose='login',
is_verified=False
).latest('created_at')
if otp.is_valid():
# Cleanup
otp.is_verified = True
otp.save()
# Login
login(request, user)
return JsonResponse({'success': True, 'redirect_url': reverse('dashboard')})
else:
return JsonResponse({'success': False, 'message': _('Invalid or expired OTP.')})
except (User.DoesNotExist, OTPVerification.DoesNotExist):
return JsonResponse({'success': False, 'message': _('Invalid OTP.')})
@require_POST
def chatbot(request):
try:
data = json.loads(request.body)
user_message = data.get("message", "")
language = data.get("language", "en")
if not user_message:
return JsonResponse({"success": False, "error": "Empty message"})
system_prompt = (
"You are MasarX AI, a helpful and professional assistant for the Masar logistics platform. "
"The platform connects shippers with drivers for small parcel deliveries. "
"Answer the user's questions about shipping, tracking, becoming a driver, or general support. "
"If the user speaks Arabic, reply in Arabic. If English, reply in English. "
"Keep responses concise and helpful."
)
if language == "ar":
system_prompt += " The user is currently browsing in Arabic."
else:
system_prompt += " The user is currently browsing in English."
response = LocalAIApi.create_response({
"input": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message},
]
})
if response.get("success"):
text = LocalAIApi.extract_text(response)
return JsonResponse({"success": True, "response": text})
else:
return JsonResponse({"success": False, "error": response.get("error", "AI Error")})
except json.JSONDecodeError:
return JsonResponse({"success": False, "error": "Invalid JSON"})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)})

Binary file not shown.

View File

@ -1315,4 +1315,18 @@ msgstr "شكراً لملاحظاتك!"
#~ msgstr "لم ترسل أي شحنات بعد."
#~ msgid "Find Loads"
#~ msgstr "البحث عن شحنات"
#~ msgstr "البحث عن شحنات"
msgid "Top Rated Drivers"
msgstr "أفضل السائقين تقييماً"
msgid "Top Shippers"
msgstr "أفضل الشاحنين"
msgid "No ratings yet. Be the first!"
msgstr "لا توجد تقييمات بعد. كن الأول!"
msgid "No shipments yet. Start shipping now!"
msgstr "لا توجد شحنات بعد. ابدأ الشحن الآن!"
msgid "Shipments"
msgstr "شحنات"

View File

@ -103,4 +103,62 @@ h1, h2, h3, h4, h5, h6 {
.status-pending { background: #FFE8CC; color: #D9480F; }
.status-picked_up { background: #E3FAFC; color: #0B7285; }
.status-in_transit { background: #E7F5FF; color: #1864AB; }
.status-delivered { background: #EBFBEE; color: #2B8A3E; }
.status-delivered { background: #EBFBEE; color: #2B8A3E; }
/* Chat Widget */
#masar-chat-widget {
position: fixed;
bottom: 90px;
right: 20px;
width: 350px;
height: 500px;
z-index: 9999;
display: flex;
flex-direction: column;
}
#masar-chat-toggle {
position: fixed;
bottom: 20px;
right: 20px;
width: 60px;
height: 60px;
border-radius: 50%;
z-index: 9999;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
background-color: var(--accent-orange);
color: white;
border: none;
transition: transform 0.2s;
}
#masar-chat-toggle:hover {
transform: scale(1.05);
}
/* RTL Support */
[dir="rtl"] #masar-chat-widget {
right: auto;
left: 20px;
}
[dir="rtl"] #masar-chat-toggle {
right: auto;
left: 20px;
}
.typing-dots span {
display: inline-block;
width: 8px;
height: 8px;
background-color: #adb5bd;
border-radius: 50%;
margin: 0 2px;
animation: typing 1s infinite;
}
.typing-dots span:nth-child(2) { animation-delay: 0.2s; }
.typing-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}

118
static/js/chat.js Normal file
View File

@ -0,0 +1,118 @@
document.addEventListener('DOMContentLoaded', function() {
const chatWidget = document.getElementById('masar-chat-widget');
const chatToggle = document.getElementById('masar-chat-toggle');
const chatClose = document.getElementById('masar-chat-close');
const chatForm = document.getElementById('masar-chat-form');
const chatInput = document.getElementById('masar-chat-input');
const chatMessages = document.getElementById('masar-chat-messages');
if (!chatWidget) return;
// Toggle Chat
function toggleChat() {
if (chatWidget.classList.contains('d-none')) {
chatWidget.classList.remove('d-none');
setTimeout(() => chatInput.focus(), 100);
} else {
chatWidget.classList.add('d-none');
}
}
chatToggle.addEventListener('click', toggleChat);
chatClose.addEventListener('click', toggleChat);
// Send Message
chatForm.addEventListener('submit', function(e) {
e.preventDefault();
const message = chatInput.value.trim();
if (!message) return;
// Add User Message
addMessage(message, 'user');
chatInput.value = '';
// Show Typing Indicator
const typingId = addTypingIndicator();
// Send to Backend
fetch('/ajax/chatbot/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({
message: message,
language: document.documentElement.lang || 'en'
})
})
.then(response => response.json())
.then(data => {
removeMessage(typingId);
if (data.success) {
addMessage(data.response, 'bot');
} else {
addMessage('Sorry, I encountered an error.', 'bot');
}
})
.catch(error => {
removeMessage(typingId);
addMessage('Sorry, connection error.', 'bot');
console.error('Error:', error);
});
});
function addMessage(text, sender) {
const div = document.createElement('div');
div.className = `d-flex mb-3 ${sender === 'user' ? 'justify-content-end' : 'justify-content-start'}`;
const bubble = document.createElement('div');
bubble.className = `p-3 rounded-3 shadow-sm ${sender === 'user' ? 'bg-primary text-white' : 'bg-light text-dark'}`;
bubble.style.maxWidth = '80%';
bubble.style.wordWrap = 'break-word';
// Convert newlines to <br> for basic formatting
bubble.innerHTML = text.replace(/\n/g, '<br>');
div.appendChild(bubble);
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
return div.id;
}
function addTypingIndicator() {
const id = 'typing-' + Date.now();
const div = document.createElement('div');
div.id = id;
div.className = 'd-flex mb-3 justify-content-start';
div.innerHTML = ""
+ "<div class=\"bg-light p-3 rounded-3 shadow-sm\">"
+ " <div class=\"typing-dots\">"
+ " <span></span><span></span><span></span>"
+ " </div>"
+ "</div>"
;
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
return id;
}
function removeMessage(id) {
const el = document.getElementById(id);
if (el) el.remove();
}
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
});

50
update_base.py Normal file
View File

@ -0,0 +1,50 @@
import os
with open("core/templates/base.html", "r") as f:
content = f.read()
widget_html = """
<!-- Chat Widget -->
<button id="masar-chat-toggle" class="d-flex align-items-center justify-content-center">
<i class="bi bi-chat-dots-fill fs-4"></i>
</button>
<div id="masar-chat-widget" class="d-none bg-white rounded-4 shadow overflow-hidden">
<div class="bg-dark text-white p-3 d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<i class="bi bi-robot me-2 fs-5"></i>
<h6 class="mb-0 fw-bold">MasarX AI</h6>
</div>
<button id="masar-chat-close" class="btn btn-sm btn-link text-white p-0">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div id="masar-chat-messages" class="flex-grow-1 p-3 bg-light overflow-auto">
<div class="d-flex mb-3 justify-content-start">
<div class="p-3 rounded-3 shadow-sm bg-light text-dark" style="max-width: 80%;">
{% trans "Hello! How can I help you with your shipments today?" %}
</div>
</div>
</div>
<div class="p-3 border-top bg-white">
<form id="masar-chat-form" class="d-flex">
<input type="text" id="masar-chat-input" class="form-control me-2" placeholder="{% trans 'Type a message...' %}" autocomplete="off">
<button type="submit" class="btn btn-masarx-primary px-3">
<i class="bi bi-send-fill"></i>
</button>
</form>
</div>
</div>
<script src="{% static 'js/chat.js' %}?v={{ deployment_timestamp }}"></script>
"
if "masar-chat-toggle" not in content:
new_content = content.replace("</body>", widget_html + "\n</body>")
with open("core/templates/base.html", "w") as f:
f.write(new_content)
print("Updated base.html")
else:
print("Widget already exists")

36
update_base_v2.py Normal file
View File

@ -0,0 +1,36 @@
<!-- Chat Widget -->
<button id="masar-chat-toggle" class="d-flex align-items-center justify-content-center">
<i class="bi bi-chat-dots-fill fs-4"></i>
</button>
<div id="masar-chat-widget" class="d-none bg-white rounded-4 shadow overflow-hidden">
<div class="bg-dark text-white p-3 d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<i class="bi bi-robot me-2 fs-5"></i>
<h6 class="mb-0 fw-bold">MasarX AI</h6>
</div>
<button id="masar-chat-close" class="btn btn-sm btn-link text-white p-0">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div id="masar-chat-messages" class="flex-grow-1 p-3 bg-light overflow-auto">
<div class="d-flex mb-3 justify-content-start">
<div class="p-3 rounded-3 shadow-sm bg-light text-dark" style="max-width: 80%;">
{% trans "Hello! How can I help you with your shipments today?" %}
</div>
</div>
</div>
<div class="p-3 border-top bg-white">
<form id="masar-chat-form" class="d-flex">
<input type="text" id="masar-chat-input" class="form-control me-2" placeholder="{% trans 'Type a message...' %}" autocomplete="off">
<button type="submit" class="btn btn-masarx-primary px-3">
<i class="bi bi-send-fill"></i>
</button>
</form>
</div>
</div>
<script src="{% static 'js/chat.js' %}?v={{ deployment_timestamp }}"></script>
</body>