from datetime import timedelta 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, AppSetting, Banner, HomeSection, Transaction from .forms import TruckForm, ShipmentForm, BidForm, UserRegistrationForm, OTPVerifyForm, ShipperOfferForm, RenewSubscriptionForm 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 from django.core.mail import send_mail from django.conf import settings import json def home(request): """Render the landing screen for MASAR CARGO.""" banners = Banner.objects.filter(is_active=True) home_sections = HomeSection.objects.filter(is_active=True).order_by('order') context = { "deployment_timestamp": timezone.now().timestamp(), "banners": banners, "home_sections": home_sections, } return render(request, "core/index.html", context) def register(request): app_settings = AppSetting.objects.first() subscription_enabled = app_settings.subscription_enabled if app_settings else False # Simplified fees dictionary for JS # Ensuring keys are exactly as they appear in Profile.ROLE_CHOICES fees = { 'SHIPPER': { 'MONTHLY': str(app_settings.shipper_monthly_fee) if app_settings else "0.00", 'ANNUAL': str(app_settings.shipper_annual_fee) if app_settings else "0.00", }, 'TRUCK_OWNER': { 'MONTHLY': str(app_settings.truck_owner_monthly_fee) if app_settings else "0.00", 'ANNUAL': str(app_settings.truck_owner_annual_fee) if app_settings else "0.00", } } 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'], 'subscription_plan': form.cleaned_data.get('subscription_plan', 'NONE'), } 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, 'subscription_enabled': subscription_enabled, 'fees_json': json.dumps(fees) }) 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.subscription_plan = registration_data.get('subscription_plan', 'NONE') if profile.subscription_plan != 'NONE': profile.is_subscription_active = True if profile.subscription_plan == 'MONTHLY': profile.subscription_expiry = timezone.now().date() + timedelta(days=30) elif profile.subscription_plan == 'ANNUAL': profile.subscription_expiry = timezone.now().date() + timedelta(days=365) 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') # Subscription stats today = timezone.now().date() total_profiles = Profile.objects.exclude(role='ADMIN') active_subscriptions = 0 expired_subscriptions = 0 for p in total_profiles: if p.is_expired(): expired_subscriptions += 1 else: active_subscriptions += 1 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, 'active_subscriptions': active_subscriptions, 'expired_subscriptions': expired_subscriptions, } 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') def privacy_policy(request): app_settings = AppSetting.objects.first() context = { 'article': { 'title': _('Privacy Policy'), 'content': app_settings.privacy_policy if app_settings else _("Privacy policy is coming soon.") } } return render(request, 'core/article_detail.html', context) def terms_of_service(request): app_settings = AppSetting.objects.first() context = { 'article': { 'title': _('Terms of Service'), 'content': app_settings.terms_of_service if app_settings else _("Terms of service are soon.") } } return render(request, 'core/article_detail.html', context) @login_required def subscription_expired(request): profile = request.user.profile if not profile.is_expired(): return redirect('dashboard') app_settings = AppSetting.objects.first() form = RenewSubscriptionForm() # Simplified fees dictionary for JS fees = { 'SHIPPER': { 'MONTHLY': str(app_settings.shipper_monthly_fee) if app_settings else "0.00", 'ANNUAL': str(app_settings.shipper_annual_fee) if app_settings else "0.00", }, 'TRUCK_OWNER': { 'MONTHLY': str(app_settings.truck_owner_monthly_fee) if app_settings else "0.00", 'ANNUAL': str(app_settings.truck_owner_annual_fee) if app_settings else "0.00", } } return render(request, 'core/subscription_expired.html', { 'profile': profile, 'app_settings': app_settings, 'form': form, 'fees_json': json.dumps(fees) }) @login_required def renew_subscription(request): if request.method == 'POST': form = RenewSubscriptionForm(request.POST) if form.is_valid(): profile = request.user.profile plan = form.cleaned_data['subscription_plan'] # Calculate amount based on role and plan app_settings = AppSetting.objects.first() amount = 0 if profile.role == 'SHIPPER': amount = app_settings.shipper_monthly_fee if plan == 'MONTHLY' else app_settings.shipper_annual_fee elif profile.role == 'TRUCK_OWNER': amount = app_settings.truck_owner_monthly_fee if plan == 'MONTHLY' else app_settings.truck_owner_annual_fee profile.subscription_plan = plan profile.is_subscription_active = True if plan == 'MONTHLY': profile.subscription_expiry = timezone.now().date() + timedelta(days=30) elif plan == 'ANNUAL': profile.subscription_expiry = timezone.now().date() + timedelta(days=365) profile.save() # Create Transaction record Transaction.objects.create( user=request.user, amount=amount, transaction_type='PAYMENT', status='COMPLETED', description=f"Subscription Renewal: {plan}", payment_method="Online Payment" ) # Notifications expiry_date = profile.subscription_expiry.strftime('%Y-%m-%d') msg = _("Your subscription for MASAR CARGO has been successfully renewed! Your new expiry date is %(date)s. Thank you for using our service.") % {"date": expiry_date} # WhatsApp if profile.full_phone_number: send_whatsapp_message(profile.full_phone_number, msg) # Email if request.user.email: send_mail( _("Subscription Renewed - MASAR CARGO"), msg, settings.DEFAULT_FROM_EMAIL, [request.user.email], fail_silently=True, ) messages.success(request, _("Subscription renewed successfully!")) return redirect('dashboard') return redirect('subscription_expired') @login_required def financial_history(request): transactions = Transaction.objects.filter(user=request.user) return render(request, 'core/financial_history.html', {'transactions': transactions}) @login_required def transaction_receipt(request, receipt_number): transaction = get_object_or_404(Transaction, receipt_number=receipt_number, user=request.user) app_settings = AppSetting.objects.first() return render(request, 'core/receipt.html', { 'transaction': transaction, 'app_settings': app_settings }) @login_required def admin_financials(request): if request.user.profile.role != 'ADMIN': return redirect('dashboard') transactions = Transaction.objects.all() total_revenue = sum(t.amount for t in transactions if t.transaction_type == 'PAYMENT' and t.status == 'COMPLETED') return render(request, 'core/admin_financials.html', { 'transactions': transactions, 'total_revenue': total_revenue }) @login_required def issue_refund(request, receipt_number): if request.user.profile.role != 'ADMIN': return redirect('dashboard') transaction = get_object_or_404(Transaction, receipt_number=receipt_number) if transaction.transaction_type == 'REFUND': messages.error(request, _("This is already a refund transaction.")) return redirect('admin_financials') # Create a refund transaction refund = Transaction.objects.create( user=transaction.user, amount=transaction.amount, transaction_type='REFUND', status='COMPLETED', description=f"Refund for Receipt: {transaction.receipt_number}", payment_method=transaction.payment_method ) messages.success(request, _("Refund issued successfully! Receipt: %(receipt)s") % {'receipt': refund.receipt_number}) return redirect('admin_financials')