Autosave: 20260128-000421
This commit is contained in:
parent
c728c4e116
commit
5f2219fc0f
BIN
assets/vm-shot-2026-01-27T10-44-32-354Z.jpg
Normal file
BIN
assets/vm-shot-2026-01-27T10-44-32-354Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
19
core/apps.py
19
core/apps.py
@ -1,6 +1,25 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class CoreConfig(AppConfig):
|
class CoreConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'core'
|
name = 'core'
|
||||||
|
verbose_name = _('Masar Express Management')
|
||||||
|
default = True
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
|
|
||||||
|
# Monkey-patch Permission.__str__ to show a VERY short name
|
||||||
|
# Standard was: "app_label | model | name" (e.g. core | country | Can add country)
|
||||||
|
# Previous fix: "Country | Can add country"
|
||||||
|
# New fix: "add Country", "change Country" (strips "Can " prefix)
|
||||||
|
|
||||||
|
def short_str(self):
|
||||||
|
name = str(self.name)
|
||||||
|
if name.startswith("Can "):
|
||||||
|
return name[4:]
|
||||||
|
return name
|
||||||
|
|
||||||
|
Permission.__str__ = short_str
|
||||||
@ -96,14 +96,44 @@ class UserRegistrationForm(forms.ModelForm):
|
|||||||
user.save()
|
user.save()
|
||||||
# Profile is created by signal, so we update it
|
# Profile is created by signal, so we update it
|
||||||
profile, created = Profile.objects.get_or_create(user=user)
|
profile, created = Profile.objects.get_or_create(user=user)
|
||||||
profile.role = self.cleaned_data['role']
|
# Handle role if it exists in cleaned_data (it might be excluded in subclasses)
|
||||||
|
if 'role' in self.cleaned_data:
|
||||||
|
profile.role = self.cleaned_data['role']
|
||||||
profile.phone_number = self.cleaned_data['phone_number']
|
profile.phone_number = self.cleaned_data['phone_number']
|
||||||
profile.country = self.cleaned_data['country']
|
profile.country = self.cleaned_data['country']
|
||||||
profile.governate = self.cleaned_data['governate']
|
profile.governate = self.cleaned_data['governate']
|
||||||
profile.city = self.cleaned_data['city']
|
profile.city = self.cleaned_data['city']
|
||||||
|
|
||||||
|
# Save extra driver fields if they exist
|
||||||
|
if 'profile_picture' in self.cleaned_data and self.cleaned_data['profile_picture']:
|
||||||
|
profile.profile_picture = self.cleaned_data['profile_picture']
|
||||||
|
if 'license_front_image' in self.cleaned_data and self.cleaned_data['license_front_image']:
|
||||||
|
profile.license_front_image = self.cleaned_data['license_front_image']
|
||||||
|
if 'license_back_image' in self.cleaned_data and self.cleaned_data['license_back_image']:
|
||||||
|
profile.license_back_image = self.cleaned_data['license_back_image']
|
||||||
|
if 'car_plate_number' in self.cleaned_data:
|
||||||
|
profile.car_plate_number = self.cleaned_data['car_plate_number']
|
||||||
|
|
||||||
profile.save()
|
profile.save()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
class ShipperRegistrationForm(UserRegistrationForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['role'].widget = forms.HiddenInput()
|
||||||
|
self.fields['role'].initial = 'shipper'
|
||||||
|
|
||||||
|
class DriverRegistrationForm(UserRegistrationForm):
|
||||||
|
profile_picture = forms.ImageField(label=_("Profile Picture (Webcam/Upload)"), required=True, widget=forms.FileInput(attrs={'class': 'form-control', 'capture': 'user', 'accept': 'image/*'}))
|
||||||
|
license_front_image = forms.ImageField(label=_("License Front Image"), required=True, widget=forms.FileInput(attrs={'class': 'form-control', 'accept': 'image/*'}))
|
||||||
|
license_back_image = forms.ImageField(label=_("License Back Image"), required=True, widget=forms.FileInput(attrs={'class': 'form-control', 'accept': 'image/*'}))
|
||||||
|
car_plate_number = forms.CharField(label=_("Car Plate Number"), max_length=20, required=True, widget=forms.TextInput(attrs={'class': 'form-control'}))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['role'].widget = forms.HiddenInput()
|
||||||
|
self.fields['role'].initial = 'car_owner'
|
||||||
|
|
||||||
class UserProfileForm(forms.ModelForm):
|
class UserProfileForm(forms.ModelForm):
|
||||||
first_name = forms.CharField(label=_("First Name"), max_length=150, widget=forms.TextInput(attrs={'class': 'form-control'}))
|
first_name = forms.CharField(label=_("First Name"), max_length=150, widget=forms.TextInput(attrs={'class': 'form-control'}))
|
||||||
last_name = forms.CharField(label=_("Last Name"), max_length=150, widget=forms.TextInput(attrs={'class': 'form-control'}))
|
last_name = forms.CharField(label=_("Last Name"), max_length=150, widget=forms.TextInput(attrs={'class': 'form-control'}))
|
||||||
@ -347,4 +377,4 @@ class DriverRatingForm(forms.ModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
# Reverse choices for CSS star rating logic (5 to 1) to ensure left-to-right filling
|
# Reverse choices for CSS star rating logic (5 to 1) to ensure left-to-right filling
|
||||||
self.fields['rating'].choices = [(i, str(i)) for i in range(5, 0, -1)]
|
self.fields['rating'].choices = [(i, str(i)) for i in range(5, 0, -1)]
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-01-27 23:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0018_alter_otpverification_purpose'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='car_plate_number',
|
||||||
|
field=models.CharField(blank=True, max_length=20, verbose_name='Car Plate Number'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='license_back_image',
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to='licenses/', verbose_name='License Back Image'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='license_front_image',
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to='licenses/', verbose_name='License Front Image'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
@ -73,6 +73,11 @@ class Profile(models.Model):
|
|||||||
profile_picture = models.ImageField(_('Profile Picture'), upload_to='profile_pics/', blank=True, null=True)
|
profile_picture = models.ImageField(_('Profile Picture'), upload_to='profile_pics/', blank=True, null=True)
|
||||||
address = models.CharField(_('Address'), max_length=255, blank=True)
|
address = models.CharField(_('Address'), max_length=255, blank=True)
|
||||||
|
|
||||||
|
# Driver specific fields
|
||||||
|
license_front_image = models.ImageField(_('License Front Image'), upload_to='licenses/', blank=True, null=True)
|
||||||
|
license_back_image = models.ImageField(_('License Back Image'), upload_to='licenses/', blank=True, null=True)
|
||||||
|
car_plate_number = models.CharField(_('Car Plate Number'), max_length=20, blank=True)
|
||||||
|
|
||||||
country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Country'))
|
country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Country'))
|
||||||
governate = models.ForeignKey(Governate, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Governate'))
|
governate = models.ForeignKey(Governate, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Governate'))
|
||||||
city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('City'))
|
city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('City'))
|
||||||
@ -270,4 +275,4 @@ class DriverRating(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('Driver Rating')
|
verbose_name = _('Driver Rating')
|
||||||
verbose_name_plural = _('Driver Ratings')
|
verbose_name_plural = _('Driver Ratings')
|
||||||
|
|||||||
73
core/templates/core/register_choice.html
Normal file
73
core/templates/core/register_choice.html
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Register" %} | masarX{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="py-5 bg-light" style="min-height: 80vh;">
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center text-center mb-5">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<h2 class="fw-bold mb-3">{% trans "Welcome to masarX" %}</h2>
|
||||||
|
<p class="text-muted lead">{% trans "Choose how you want to join us today." %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center g-4">
|
||||||
|
<!-- Shipper Card -->
|
||||||
|
<div class="col-md-5 col-lg-4">
|
||||||
|
<div class="card h-100 shadow-sm border-0 transition-hover">
|
||||||
|
<div class="card-body p-5 text-center">
|
||||||
|
<div class="mb-4 text-masarx-orange">
|
||||||
|
<i class="bi bi-box-seam display-4"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="h4 fw-bold mb-3">{% trans "I am a Shipper" %}</h3>
|
||||||
|
<p class="text-muted mb-4">{% trans "I want to send parcels and track my shipments easily." %}</p>
|
||||||
|
<a href="{% url 'register_shipper' %}" class="btn btn-masarx-primary w-100 py-2">{% trans "Register as Shipper" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Driver Card -->
|
||||||
|
<div class="col-md-5 col-lg-4">
|
||||||
|
<div class="card h-100 shadow-sm border-0 transition-hover">
|
||||||
|
<div class="card-body p-5 text-center">
|
||||||
|
<div class="mb-4 text-masarx-dark">
|
||||||
|
<i class="bi bi-truck display-4"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="h4 fw-bold mb-3">{% trans "I am a Car Owner" %}</h3>
|
||||||
|
<p class="text-muted mb-4">{% trans "I want to deliver parcels and earn money on my own schedule." %}</p>
|
||||||
|
<a href="{% url 'register_driver' %}" class="btn btn-outline-dark w-100 py-2">{% trans "Register as Car Owner" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-5">
|
||||||
|
<p class="text-muted">{% trans "Already have an account?" %} <a href="{% url 'login' %}" class="text-masarx-orange fw-semibold">{% trans "Login here" %}</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.transition-hover {
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
.transition-hover:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 1rem 3rem rgba(0,0,0,.175)!important;
|
||||||
|
}
|
||||||
|
.btn-masarx-primary {
|
||||||
|
background-color: var(--accent-orange);
|
||||||
|
border-color: var(--accent-orange);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.text-masarx-orange {
|
||||||
|
color: var(--accent-orange);
|
||||||
|
}
|
||||||
|
.text-masarx-dark {
|
||||||
|
color: var(--accent-dark);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
222
core/templates/core/register_driver.html
Normal file
222
core/templates/core/register_driver.html
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Register as Car Owner" %} | masarX{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="py-5 bg-light" style="min-height: 80vh;">
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-10 col-lg-8">
|
||||||
|
<!-- Back -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<a href="{% url 'register' %}" class="btn btn-link text-decoration-none text-muted ps-0">
|
||||||
|
<i class="bi {% if LANGUAGE_BIDI %}bi-arrow-right{% else %}bi-arrow-left{% endif %} me-2"></i>{% trans "Back" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm border-0">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<h2 class="fw-bold mb-4 text-center">{% trans "Car Owner Registration" %}</h2>
|
||||||
|
<form method="post" id="registrationForm" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="row">
|
||||||
|
{% for field in form %}
|
||||||
|
{% if field.is_hidden %}
|
||||||
|
{{ field }}
|
||||||
|
{% else %}
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold" for="{{ field.id_for_label }}">
|
||||||
|
{{ field.label }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{% if field.name == 'profile_picture' %}
|
||||||
|
<div class="input-group">
|
||||||
|
{{ field }}
|
||||||
|
<button type="button" class="btn btn-secondary" id="openWebcamBtn" title="{% trans 'Take photo with Webcam' %}">
|
||||||
|
<i class="bi bi-camera-video"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{{ field }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if field.help_text %}
|
||||||
|
<div class="form-text small">{{ field.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="text-danger small">{{ field.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info small">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
{% trans "Please provide clear photos of your license (front and back) and car plate number for verification." %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-dark w-100 py-2 mt-3">{% trans "Submit Application" %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Webcam Modal -->
|
||||||
|
<div class="modal fade" id="webcamModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">{% trans "Take Photo" %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center bg-dark p-0">
|
||||||
|
<video id="webcamVideo" style="width: 100%; max-height: 400px;" autoplay playsinline></video>
|
||||||
|
<canvas id="webcamCanvas" style="display:none;"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer justify-content-center">
|
||||||
|
<button type="button" class="btn btn-primary rounded-circle p-3" id="captureBtn">
|
||||||
|
<i class="bi bi-camera-fill fs-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Location Logic
|
||||||
|
const countrySelect = document.getElementById('id_country');
|
||||||
|
const governateSelect = document.getElementById('id_governate');
|
||||||
|
const citySelect = document.getElementById('id_city');
|
||||||
|
|
||||||
|
if (countrySelect && governateSelect && citySelect) {
|
||||||
|
countrySelect.addEventListener('change', function() {
|
||||||
|
const countryId = this.value;
|
||||||
|
governateSelect.innerHTML = '<option value="">{% trans "Select Governate" %}</option>';
|
||||||
|
citySelect.innerHTML = '<option value="">{% trans "Select City" %}</option>';
|
||||||
|
|
||||||
|
if (countryId) {
|
||||||
|
fetch(`{% url 'get_governates' %}?country_id=${countryId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
data.forEach(gov => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = gov.id;
|
||||||
|
option.textContent = gov.name;
|
||||||
|
governateSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
governateSelect.addEventListener('change', function() {
|
||||||
|
const governateId = this.value;
|
||||||
|
citySelect.innerHTML = '<option value="">{% trans "Select City" %}</option>';
|
||||||
|
|
||||||
|
if (governateId) {
|
||||||
|
fetch(`{% url 'get_cities' %}?governate_id=${governateId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
data.forEach(city => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = city.id;
|
||||||
|
option.textContent = city.name;
|
||||||
|
citySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webcam Logic
|
||||||
|
const openWebcamBtn = document.getElementById('openWebcamBtn');
|
||||||
|
const webcamModal = new bootstrap.Modal(document.getElementById('webcamModal'));
|
||||||
|
const video = document.getElementById('webcamVideo');
|
||||||
|
const canvas = document.getElementById('webcamCanvas');
|
||||||
|
const captureBtn = document.getElementById('captureBtn');
|
||||||
|
const profileInput = document.getElementById('id_profile_picture');
|
||||||
|
let stream = null;
|
||||||
|
|
||||||
|
if (openWebcamBtn) {
|
||||||
|
openWebcamBtn.addEventListener('click', function() {
|
||||||
|
webcamModal.show();
|
||||||
|
navigator.mediaDevices.getUserMedia({ video: true })
|
||||||
|
.then(function(s) {
|
||||||
|
stream = s;
|
||||||
|
video.srcObject = stream;
|
||||||
|
video.play();
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.error("An error occurred: " + err);
|
||||||
|
alert("{% trans 'Could not access webcam. Please check permissions.' %}");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('webcamModal').addEventListener('hidden.bs.modal', function () {
|
||||||
|
if (stream) {
|
||||||
|
stream.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
captureBtn.addEventListener('click', function() {
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
canvas.width = video.videoWidth;
|
||||||
|
canvas.height = video.videoHeight;
|
||||||
|
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
canvas.toBlob(function(blob) {
|
||||||
|
// Create a file from the blob
|
||||||
|
const file = new File([blob], "webcam_profile.jpg", { type: "image/jpeg" });
|
||||||
|
|
||||||
|
// Use DataTransfer to set the file input value
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
dataTransfer.items.add(file);
|
||||||
|
profileInput.files = dataTransfer.files;
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
webcamModal.hide();
|
||||||
|
|
||||||
|
// Visual feedback (optional)
|
||||||
|
// alert("{% trans 'Photo captured!' %}");
|
||||||
|
}, 'image/jpeg');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input:not([type=radio]):not([type=checkbox]), select {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
#id_verification_method {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
#id_verification_method li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.input-group > .form-control {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.input-group > .btn {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
125
core/templates/core/register_shipper.html
Normal file
125
core/templates/core/register_shipper.html
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Register as Shipper" %} | masarX{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="py-5 bg-light" style="min-height: 80vh;">
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-10 col-lg-8">
|
||||||
|
<!-- Back -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<a href="{% url 'register' %}" class="btn btn-link text-decoration-none text-muted ps-0">
|
||||||
|
<i class="bi {% if LANGUAGE_BIDI %}bi-arrow-right{% else %}bi-arrow-left{% endif %} me-2"></i>{% trans "Back" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm border-0">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<h2 class="fw-bold mb-4 text-center">{% trans "Shipper Registration" %}</h2>
|
||||||
|
<form method="post" id="registrationForm">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="row">
|
||||||
|
{% for field in form %}
|
||||||
|
{% if field.is_hidden %}
|
||||||
|
{{ field }}
|
||||||
|
{% else %}
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<div class="form-text small">{{ field.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="text-danger small">{{ field.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-masarx-primary w-100 py-2 mt-3">{% trans "Create Account" %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const countrySelect = document.getElementById('id_country');
|
||||||
|
const governateSelect = document.getElementById('id_governate');
|
||||||
|
const citySelect = document.getElementById('id_city');
|
||||||
|
|
||||||
|
if (countrySelect && governateSelect && citySelect) {
|
||||||
|
countrySelect.addEventListener('change', function() {
|
||||||
|
const countryId = this.value;
|
||||||
|
governateSelect.innerHTML = '<option value="">{% trans "Select Governate" %}</option>';
|
||||||
|
citySelect.innerHTML = '<option value="">{% trans "Select City" %}</option>';
|
||||||
|
|
||||||
|
if (countryId) {
|
||||||
|
fetch(`{% url 'get_governates' %}?country_id=${countryId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
data.forEach(gov => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = gov.id;
|
||||||
|
option.textContent = gov.name;
|
||||||
|
governateSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
governateSelect.addEventListener('change', function() {
|
||||||
|
const governateId = this.value;
|
||||||
|
citySelect.innerHTML = '<option value="">{% trans "Select City" %}</option>';
|
||||||
|
|
||||||
|
if (governateId) {
|
||||||
|
fetch(`{% url 'get_cities' %}?governate_id=${governateId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
data.forEach(city => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = city.id;
|
||||||
|
option.textContent = city.name;
|
||||||
|
citySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input:not([type=radio]):not([type=checkbox]), select {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
#id_verification_method {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
#id_verification_method li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.btn-masarx-primary {
|
||||||
|
background-color: var(--accent-orange);
|
||||||
|
border-color: var(--accent-orange);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
14
core/urls.py
14
core/urls.py
@ -7,7 +7,11 @@ urlpatterns = [
|
|||||||
path('', views.index, name='index'),
|
path('', views.index, name='index'),
|
||||||
path('login/', auth_views.LoginView.as_view(template_name='core/login.html'), name='login'),
|
path('login/', auth_views.LoginView.as_view(template_name='core/login.html'), name='login'),
|
||||||
path('logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
|
path('logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
|
||||||
|
|
||||||
|
# Registration Flow
|
||||||
path('register/', views.register, name='register'),
|
path('register/', views.register, name='register'),
|
||||||
|
path('register/shipper/', views.register_shipper, name='register_shipper'),
|
||||||
|
path('register/driver/', views.register_driver, name='register_driver'),
|
||||||
path('register/verify/', views.verify_registration, name='verify_registration'),
|
path('register/verify/', views.verify_registration, name='verify_registration'),
|
||||||
|
|
||||||
# Password Reset URLs
|
# Password Reset URLs
|
||||||
@ -66,10 +70,18 @@ urlpatterns = [
|
|||||||
path('login/request-otp/', views.request_login_otp, name='request_login_otp'),
|
path('login/request-otp/', views.request_login_otp, name='request_login_otp'),
|
||||||
path('login/verify-otp/', views.verify_login_otp, name='verify_login_otp'),
|
path('login/verify-otp/', views.verify_login_otp, name='verify_login_otp'),
|
||||||
|
|
||||||
# API Endpoints
|
# API Endpoints (Standard)
|
||||||
path('api/auth/token/', api_views.CustomAuthToken.as_view(), name='api_token_auth'),
|
path('api/auth/token/', api_views.CustomAuthToken.as_view(), name='api_token_auth'),
|
||||||
path('api/parcels/', api_views.ParcelListCreateView.as_view(), name='api_parcel_list'),
|
path('api/parcels/', api_views.ParcelListCreateView.as_view(), name='api_parcel_list'),
|
||||||
path('api/parcels/<int:pk>/', api_views.ParcelDetailView.as_view(), name='api_parcel_detail'),
|
path('api/parcels/<int:pk>/', api_views.ParcelDetailView.as_view(), name='api_parcel_detail'),
|
||||||
path('api/track/<str:tracking_number>/', api_views.PublicParcelTrackView.as_view(), name='api_track_parcel'),
|
path('api/track/<str:tracking_number>/', api_views.PublicParcelTrackView.as_view(), name='api_track_parcel'),
|
||||||
path('api/profile/', api_views.UserProfileView.as_view(), name='api_user_profile'),
|
path('api/profile/', api_views.UserProfileView.as_view(), name='api_user_profile'),
|
||||||
|
|
||||||
|
# Aliases for mobile app compatibility (API v1)
|
||||||
|
path('api/shipments/', api_views.ParcelListCreateView.as_view(), name='api_shipment_list'),
|
||||||
|
path('api/shipments/<int:pk>/', api_views.ParcelDetailView.as_view(), name='api_shipment_detail'),
|
||||||
|
|
||||||
|
# Root-level Aliases (for apps hardcoded to /shipments/)
|
||||||
|
path('shipments/', api_views.ParcelListCreateView.as_view(), name='root_shipment_list'),
|
||||||
|
path('shipments/<int:pk>/', api_views.ParcelDetailView.as_view(), name='root_shipment_detail'),
|
||||||
]
|
]
|
||||||
@ -4,7 +4,7 @@ from django.contrib.auth.forms import AuthenticationForm
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from .models import Parcel, Profile, Country, Governate, City, OTPVerification, PlatformProfile, Testimonial, DriverRating
|
from .models import Parcel, Profile, Country, Governate, City, OTPVerification, PlatformProfile, Testimonial, DriverRating
|
||||||
from .forms import UserRegistrationForm, ParcelForm, ContactForm, UserProfileForm, DriverRatingForm
|
from .forms import UserRegistrationForm, ParcelForm, ContactForm, UserProfileForm, DriverRatingForm, ShipperRegistrationForm, DriverRegistrationForm
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@ -77,8 +77,11 @@ def track_parcel(request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def register(request):
|
def register(request):
|
||||||
|
return render(request, 'core/register_choice.html')
|
||||||
|
|
||||||
|
def register_shipper(request):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = UserRegistrationForm(request.POST)
|
form = ShipperRegistrationForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
# Save user but inactive
|
# Save user but inactive
|
||||||
user = form.save(commit=True)
|
user = form.save(commit=True)
|
||||||
@ -110,8 +113,45 @@ def register(request):
|
|||||||
request.session['registration_user_id'] = user.id
|
request.session['registration_user_id'] = user.id
|
||||||
return redirect('verify_registration')
|
return redirect('verify_registration')
|
||||||
else:
|
else:
|
||||||
form = UserRegistrationForm()
|
form = ShipperRegistrationForm()
|
||||||
return render(request, 'core/register.html', {'form': form})
|
return render(request, 'core/register_shipper.html', {'form': form})
|
||||||
|
|
||||||
|
def register_driver(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = DriverRegistrationForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
# Save user but inactive
|
||||||
|
user = form.save(commit=True)
|
||||||
|
user.is_active = False
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
# Generate OTP
|
||||||
|
code = ''.join(random.choices(string.digits, k=6))
|
||||||
|
OTPVerification.objects.create(user=user, code=code, purpose='registration')
|
||||||
|
|
||||||
|
# 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, otp_msg)
|
||||||
|
messages.info(request, _("Verification code sent to WhatsApp."))
|
||||||
|
else:
|
||||||
|
send_html_email(
|
||||||
|
subject=_('Verification Code'),
|
||||||
|
message=otp_msg,
|
||||||
|
recipient_list=[user.email],
|
||||||
|
title=_('Welcome to Masar!'),
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
messages.info(request, _("Verification code sent to email."))
|
||||||
|
|
||||||
|
request.session['registration_user_id'] = user.id
|
||||||
|
return redirect('verify_registration')
|
||||||
|
else:
|
||||||
|
form = DriverRegistrationForm()
|
||||||
|
return render(request, 'core/register_driver.html', {'form': form})
|
||||||
|
|
||||||
def verify_registration(request):
|
def verify_registration(request):
|
||||||
if 'registration_user_id' not in request.session:
|
if 'registration_user_id' not in request.session:
|
||||||
@ -888,4 +928,4 @@ def cancel_parcel(request, parcel_id):
|
|||||||
parcel.save()
|
parcel.save()
|
||||||
messages.success(request, _("Shipment cancelled successfully."))
|
messages.success(request, _("Shipment cancelled successfully."))
|
||||||
|
|
||||||
return redirect('dashboard')
|
return redirect('dashboard')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user