380 lines
16 KiB
Python
380 lines
16 KiB
Python
from django.shortcuts import render, redirect, get_object_or_404
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.contrib.auth import login, authenticate, logout
|
|
from django.utils import timezone
|
|
from .models import Profile, Truck, Shipment, Bid, Message, OTPCode, Country, City
|
|
from .forms import TruckForm, ShipmentForm, BidForm, UserRegistrationForm, OTPVerifyForm, ShipperOfferForm
|
|
from django.contrib import messages
|
|
from django.utils.translation import gettext as _
|
|
from django.db.models import Q
|
|
from django.contrib.auth.models import User
|
|
from .whatsapp import send_whatsapp_message
|
|
from django.contrib.auth.forms import AuthenticationForm
|
|
|
|
def home(request):
|
|
"""Render the landing screen for MASAR CARGO."""
|
|
context = {
|
|
"deployment_timestamp": timezone.now().timestamp(),
|
|
}
|
|
return render(request, "core/index.html", context)
|
|
|
|
def register(request):
|
|
if request.method == 'POST':
|
|
form = UserRegistrationForm(request.POST)
|
|
if form.is_valid():
|
|
# Store data in session to be used after OTP verification
|
|
registration_data = {
|
|
'username': form.cleaned_data['username'],
|
|
'email': form.cleaned_data['email'],
|
|
'password': form.data['password1'], # We need raw password to create user later
|
|
'role': form.cleaned_data['role'],
|
|
'phone_number': form.cleaned_data['phone_number'],
|
|
'country_code': form.cleaned_data['country_code'],
|
|
}
|
|
request.session['registration_data'] = registration_data
|
|
|
|
# Send OTP
|
|
full_phone = f"{registration_data['country_code']}{registration_data['phone_number']}"
|
|
otp = OTPCode.generate_code(full_phone)
|
|
msg = _("Your verification code for MASAR CARGO is: %(code)s") % {"code": otp.code}
|
|
if send_whatsapp_message(full_phone, msg):
|
|
messages.info(request, _("A verification code has been sent to your WhatsApp."))
|
|
return redirect('verify_otp_registration')
|
|
else:
|
|
messages.error(request, _("Failed to send verification code. Please check your phone number."))
|
|
else:
|
|
messages.error(request, _("Please correct the errors below."))
|
|
else:
|
|
form = UserRegistrationForm()
|
|
return render(request, 'registration/register.html', {'form': form})
|
|
|
|
def verify_otp_registration(request):
|
|
registration_data = request.session.get('registration_data')
|
|
if not registration_data:
|
|
return redirect('register')
|
|
|
|
if request.method == 'POST':
|
|
form = OTPVerifyForm(request.POST)
|
|
if form.is_valid():
|
|
code = form.cleaned_data['otp_code']
|
|
full_phone = f"{registration_data['country_code']}{registration_data['phone_number']}"
|
|
otp_record = OTPCode.objects.filter(phone_number=full_phone, code=code, is_used=False).last()
|
|
|
|
if otp_record and otp_record.is_valid():
|
|
otp_record.is_used = True
|
|
otp_record.save()
|
|
|
|
# Create user
|
|
user = User.objects.create_user(
|
|
username=registration_data['username'],
|
|
email=registration_data['email'],
|
|
password=registration_data['password']
|
|
)
|
|
profile = user.profile
|
|
profile.role = registration_data['role']
|
|
profile.phone_number = registration_data['phone_number']
|
|
profile.country_code = registration_data['country_code']
|
|
profile.save()
|
|
|
|
login(request, user)
|
|
if 'registration_data' in request.session:
|
|
del request.session['registration_data']
|
|
messages.success(request, _("Registration successful. Welcome!"))
|
|
return redirect('dashboard')
|
|
else:
|
|
messages.error(request, _("Invalid or expired verification code."))
|
|
else:
|
|
form = OTPVerifyForm()
|
|
|
|
return render(request, 'registration/verify_otp.html', {'form': form, 'purpose': 'registration'})
|
|
|
|
def custom_login(request):
|
|
if request.method == 'POST':
|
|
form = AuthenticationForm(request, data=request.POST)
|
|
if form.is_valid():
|
|
user = form.get_user()
|
|
profile = user.profile
|
|
if not profile.phone_number:
|
|
messages.error(request, _("Your account does not have a phone number. Please contact admin."))
|
|
return redirect('login')
|
|
|
|
# Store user ID in session temporarily
|
|
request.session['pre_otp_user_id'] = user.id
|
|
|
|
# Send OTP
|
|
full_phone = profile.full_phone_number
|
|
otp = OTPCode.generate_code(full_phone)
|
|
msg = _("Your login verification code for MASAR CARGO is: %(code)s") % {"code": otp.code}
|
|
if send_whatsapp_message(full_phone, msg):
|
|
messages.info(request, _("A verification code has been sent to your WhatsApp."))
|
|
return redirect('verify_otp_login')
|
|
else:
|
|
# If WhatsApp fails, maybe allow login but warn? Or strictly enforce?
|
|
# For now, strictly enforce
|
|
messages.error(request, _("Failed to send verification code. Please check your connection."))
|
|
else:
|
|
messages.error(request, _("Invalid username or password."))
|
|
else:
|
|
form = AuthenticationForm()
|
|
return render(request, 'registration/login.html', {'form': form})
|
|
|
|
def verify_otp_login(request):
|
|
user_id = request.session.get('pre_otp_user_id')
|
|
if not user_id:
|
|
return redirect('login')
|
|
|
|
user = get_object_or_404(User, id=user_id)
|
|
profile = user.profile
|
|
|
|
if request.method == 'POST':
|
|
form = OTPVerifyForm(request.POST)
|
|
if form.is_valid():
|
|
code = form.cleaned_data['otp_code']
|
|
full_phone = profile.full_phone_number
|
|
otp_record = OTPCode.objects.filter(phone_number=full_phone, code=code, is_used=False).last()
|
|
|
|
if otp_record and otp_record.is_valid():
|
|
otp_record.is_used = True
|
|
otp_record.save()
|
|
|
|
login(request, user)
|
|
if 'pre_otp_user_id' in request.session:
|
|
del request.session['pre_otp_user_id']
|
|
messages.success(request, _("Logged in successfully!"))
|
|
return redirect('dashboard')
|
|
else:
|
|
messages.error(request, _("Invalid or expired verification code."))
|
|
else:
|
|
form = OTPVerifyForm()
|
|
|
|
return render(request, 'registration/verify_otp.html', {'form': form, 'purpose': 'login'})
|
|
|
|
@login_required
|
|
def dashboard(request):
|
|
profile, created = Profile.objects.get_or_create(user=request.user)
|
|
if profile.role == 'SHIPPER':
|
|
my_shipments = Shipment.objects.filter(shipper=request.user).order_by('-created_at')
|
|
# In the new flow, Shippers place bids.
|
|
my_bids = Bid.objects.filter(shipment__shipper=request.user).order_by('-created_at')
|
|
return render(request, 'core/shipper_dashboard.html', {
|
|
'shipments': my_shipments,
|
|
'bids': my_bids
|
|
})
|
|
elif profile.role == 'TRUCK_OWNER':
|
|
approved_trucks = Truck.objects.filter(owner=request.user, is_approved=True)
|
|
pending_trucks = Truck.objects.filter(owner=request.user, is_approved=False)
|
|
# Truck owners receive bids in the new flow
|
|
my_received_bids = Bid.objects.filter(truck_owner=request.user).order_by('-created_at')
|
|
return render(request, 'core/truck_owner_dashboard.html', {
|
|
'trucks': approved_trucks,
|
|
'pending_trucks': pending_trucks,
|
|
'bids': my_received_bids
|
|
})
|
|
elif profile.role == 'ADMIN' or request.user.is_superuser:
|
|
pending_trucks = Truck.objects.filter(is_approved=False).order_by('-created_at')
|
|
approved_trucks = Truck.objects.filter(is_approved=True).order_by('-created_at')
|
|
context = {
|
|
'total_users': User.objects.count(),
|
|
'total_trucks': Truck.objects.count(),
|
|
'total_shipments': Shipment.objects.count(),
|
|
'total_bids': Bid.objects.count(),
|
|
'pending_trucks': pending_trucks,
|
|
'approved_trucks': approved_trucks,
|
|
}
|
|
return render(request, 'core/admin_dashboard.html', context)
|
|
else:
|
|
return redirect('/')
|
|
|
|
@login_required
|
|
def truck_register(request):
|
|
if request.user.profile.role != 'TRUCK_OWNER':
|
|
return redirect('dashboard')
|
|
|
|
if request.method == 'POST':
|
|
form = TruckForm(request.POST, request.FILES)
|
|
if form.is_valid():
|
|
truck = form.save(commit=False)
|
|
truck.owner = request.user
|
|
truck.is_approved = False
|
|
truck.save()
|
|
messages.success(request, _("Truck registered successfully! It will be visible after admin approval."))
|
|
return redirect('dashboard')
|
|
else:
|
|
messages.error(request, _("There was an error in your registration. Please check the form."))
|
|
else:
|
|
form = TruckForm()
|
|
|
|
return render(request, 'core/truck_register.html', {'form': form})
|
|
|
|
@login_required
|
|
def edit_truck(request, truck_id):
|
|
truck = get_object_or_404(Truck, id=truck_id, owner=request.user)
|
|
|
|
if request.method == 'POST':
|
|
form = TruckForm(request.POST, request.FILES, instance=truck)
|
|
if form.is_valid():
|
|
truck = form.save(commit=False)
|
|
truck.is_approved = False
|
|
truck.save()
|
|
messages.success(request, _("Truck data updated successfully! It will be reviewed by admin again."))
|
|
return redirect('dashboard')
|
|
else:
|
|
messages.error(request, _("There was an error updating your truck. Please check the form."))
|
|
else:
|
|
form = TruckForm(instance=truck)
|
|
|
|
return render(request, 'core/truck_register.html', {'form': form, 'edit_mode': True, 'truck': truck})
|
|
|
|
@login_required
|
|
def approve_truck(request, truck_id):
|
|
if not (request.user.profile.role == 'ADMIN' or request.user.is_superuser):
|
|
return redirect('dashboard')
|
|
|
|
truck = get_object_or_404(Truck, id=truck_id)
|
|
truck.is_approved = True
|
|
truck.save()
|
|
|
|
owner_phone = getattr(truck.owner.profile, 'full_phone_number', None)
|
|
if owner_phone:
|
|
msg = _("Your truck (%(plate)s) has been approved! You can now receive offers for shipments.") % {"plate": truck.plate_no}
|
|
send_whatsapp_message(owner_phone, msg)
|
|
|
|
messages.success(request, _("Truck approved successfully!"))
|
|
return redirect('dashboard')
|
|
|
|
@login_required
|
|
def suspend_truck(request, truck_id):
|
|
if not (request.user.profile.role == 'ADMIN' or request.user.is_superuser):
|
|
return redirect('dashboard')
|
|
|
|
truck = get_object_or_404(Truck, id=truck_id)
|
|
truck.is_approved = False
|
|
truck.save()
|
|
messages.warning(request, _("Truck has been suspended."))
|
|
return redirect('dashboard')
|
|
|
|
@login_required
|
|
def post_shipment(request):
|
|
"""Note: This is used as the 'Add A Bid' / 'Add Post' action for Shippers."""
|
|
if request.user.profile.role != 'SHIPPER':
|
|
return redirect('dashboard')
|
|
|
|
if request.method == 'POST':
|
|
form = ShipmentForm(request.POST)
|
|
if form.is_valid():
|
|
shipment = form.save(commit=False)
|
|
shipment.shipper = request.user
|
|
shipment.save()
|
|
messages.success(request, _("Shipment posted successfully! It is now open for bids or you can browse trucks to send it as an offer."))
|
|
return redirect('dashboard')
|
|
else:
|
|
messages.error(request, _("Please correct the errors in the form."))
|
|
else:
|
|
form = ShipmentForm()
|
|
|
|
return render(request, 'core/post_shipment.html', {'form': form})
|
|
|
|
@login_required
|
|
def marketplace(request):
|
|
"""Shippers browse available trucks here."""
|
|
if request.user.profile.role != 'SHIPPER':
|
|
return redirect('dashboard')
|
|
|
|
trucks = Truck.objects.filter(is_approved=True).order_by('-created_at')
|
|
return render(request, 'core/marketplace.html', {'trucks': trucks})
|
|
|
|
@login_required
|
|
def place_bid(request, truck_id):
|
|
"""Shipper makes an offer to a specific truck."""
|
|
truck = get_object_or_404(Truck, id=truck_id, is_approved=True)
|
|
if request.user.profile.role != 'SHIPPER':
|
|
return redirect('dashboard')
|
|
|
|
if request.method == 'POST':
|
|
form = ShipperOfferForm(request.POST)
|
|
if form.is_valid():
|
|
# Create Shipment
|
|
shipment = Shipment.objects.create(
|
|
shipper=request.user,
|
|
description=form.cleaned_data['description'],
|
|
weight=form.cleaned_data['weight'],
|
|
origin_country=form.cleaned_data['origin_country'],
|
|
origin_city=form.cleaned_data['origin_city'],
|
|
destination_country=form.cleaned_data['destination_country'],
|
|
destination_city=form.cleaned_data['destination_city'],
|
|
delivery_date=form.cleaned_data['delivery_date'],
|
|
status='OPEN'
|
|
)
|
|
# Create Bid (Offer)
|
|
bid = Bid.objects.create(
|
|
shipment=shipment,
|
|
truck_owner=truck.owner,
|
|
truck=truck,
|
|
amount=form.cleaned_data['amount'],
|
|
comments=form.cleaned_data.get('comments', ''),
|
|
status='PENDING'
|
|
)
|
|
|
|
# Notify Truck Owner via WhatsApp
|
|
owner_phone = getattr(truck.owner.profile, 'full_phone_number', None)
|
|
if owner_phone:
|
|
msg = _("New offer received for your truck (%(plate)s)! Route: %(origin)s to %(dest)s. Amount: %(amount)s") % {"plate": truck.plate_no, "origin": shipment.display_origin, "dest": shipment.display_destination, "amount": bid.amount}
|
|
send_whatsapp_message(owner_phone, msg)
|
|
|
|
messages.success(request, _("Offer sent successfully!"))
|
|
return redirect('dashboard')
|
|
else:
|
|
messages.error(request, _("Error sending offer. Please check the form."))
|
|
else:
|
|
form = ShipperOfferForm()
|
|
|
|
return render(request, 'core/place_bid.html', {'form': form, 'truck': truck})
|
|
|
|
@login_required
|
|
def shipment_detail(request, shipment_id):
|
|
shipment = get_object_or_404(Shipment, id=shipment_id)
|
|
# Security: check if user is shipper, truck owner of a bid, or admin
|
|
is_involved = Bid.objects.filter(shipment=shipment, truck_owner=request.user).exists() or shipment.shipper == request.user
|
|
if not is_involved and not (request.user.profile.role == 'ADMIN' or request.user.is_superuser):
|
|
return redirect('dashboard')
|
|
|
|
bids = shipment.bids.all()
|
|
return render(request, 'core/shipment_detail.html', {'shipment': shipment, 'bids': bids})
|
|
|
|
@login_required
|
|
def accept_bid(request, bid_id):
|
|
"""Truck owner accepts an offer from a shipper."""
|
|
bid = get_object_or_404(Bid, id=bid_id)
|
|
if bid.truck_owner != request.user:
|
|
messages.error(request, _("You are not authorized to accept this offer."))
|
|
return redirect('dashboard')
|
|
|
|
# Accept this bid
|
|
bid.status = 'ACCEPTED'
|
|
bid.save()
|
|
|
|
# Update shipment
|
|
bid.shipment.status = 'IN_PROGRESS'
|
|
bid.shipment.assigned_truck = bid.truck
|
|
bid.shipment.save()
|
|
|
|
# Notify Shipper via WhatsApp
|
|
shipper_phone = getattr(bid.shipment.shipper.profile, 'full_phone_number', None)
|
|
if shipper_phone:
|
|
msg = _("Your offer for truck %(plate)s (%(origin)s to %(dest)s) has been accepted!") % {"plate": bid.truck.plate_no, "origin": bid.shipment.display_origin, "dest": bid.shipment.display_destination}
|
|
send_whatsapp_message(shipper_phone, msg)
|
|
|
|
messages.success(request, _("Offer accepted! Shipment is now in progress."))
|
|
return redirect('dashboard')
|
|
|
|
@login_required
|
|
def reject_bid(request, bid_id):
|
|
"""Truck owner rejects an offer."""
|
|
bid = get_object_or_404(Bid, id=bid_id)
|
|
if bid.truck_owner != request.user:
|
|
return redirect('dashboard')
|
|
|
|
bid.status = 'REJECTED'
|
|
bid.save()
|
|
messages.info(request, _("Offer rejected."))
|
|
return redirect('dashboard') |