diff --git a/multi-language-support-903482.md b/multi-language-support-903482.md
new file mode 100644
index 0000000..012bf09
--- /dev/null
+++ b/multi-language-support-903482.md
@@ -0,0 +1,44 @@
+# Multi-Language Support with Icon-Based UI
+
+## Overview
+Add Hindi, English, Tamil, and Telugu language support to BharathTrucks with large icon-based buttons designed for low-literacy users.
+
+## Requirements
+1. Support 4 languages: Hindi (default), English, Tamil, Telugu
+2. Language switcher in header — persisted in session
+3. All navigation and action buttons use large icons with minimal text labels
+4. Bottom nav and dashboard buttons are icon-first (large emoji/SVG + short label)
+5. Low-literacy friendly: icons convey meaning without reading
+
+## Technical Design
+
+### i18n Approach
+- Simple JSON translation files in `src/i18n/{lang}.json`
+- Middleware reads `req.session.lang` (default: `hi`) and attaches `t()` helper to `res.locals`
+- No external i18n library needed — lightweight custom implementation
+
+### Language Switcher
+- `GET /lang/:code` route sets `req.session.lang` and redirects back
+- Header shows 4 flag/label buttons: हिंदी | EN | தமிழ் | తెలుగు
+
+### Icon-Based UI for Low-Literacy Users
+- Bottom nav: larger icons (40px), short label below
+- Dashboard action buttons: icon-button class with 48px emoji + label
+- All key actions identifiable by icon alone (🚛 truck, 📋 loads, 💰 money, etc.)
+
+## Files Changed
+- `src/i18n/hi.json` — Hindi translations
+- `src/i18n/en.json` — English translations
+- `src/i18n/ta.json` — Tamil translations
+- `src/i18n/te.json` — Telugu translations
+- `src/middleware/i18n.js` — language detection middleware
+- `src/server.js` — add i18n middleware + `/lang/:code` route
+- `src/views/partials/header.ejs` — language switcher
+- `src/views/partials/bottom-nav.ejs` — icon-first nav
+- `src/views/pages/driver-dashboard.ejs` — icon-based action buttons
+- `src/public/css/govt-theme.css` — icon-button styles
+
+## Testing
+- Switch language via header buttons, verify all labels change
+- Verify icons are large and clear on mobile
+- Verify session persists language across page navigations
diff --git a/webapp/src/i18n/en.json b/webapp/src/i18n/en.json
new file mode 100644
index 0000000..059a355
--- /dev/null
+++ b/webapp/src/i18n/en.json
@@ -0,0 +1,143 @@
+{
+ "nav": {
+ "home": "Home",
+ "loads": "Loads",
+ "post": "Post",
+ "trips": "Trips",
+ "messages": "Messages",
+ "profile": "Profile"
+ },
+ "actions": {
+ "viewLoads": "View Loads",
+ "myTrips": "My Trips",
+ "earnings": "Earnings",
+ "postLoad": "Post Load",
+ "bid": "Place Bid",
+ "search": "Search",
+ "login": "Login",
+ "register": "Register",
+ "logout": "Logout"
+ },
+ "dashboard": {
+ "hello": "Hello",
+ "totalTrips": "Total Trips",
+ "activeBids": "Active Bids",
+ "earnings": "Earnings",
+ "activeTrips": "Active Trips",
+ "shipperTitle": "Shipper Dashboard",
+ "brokerTitle": "Broker Dashboard",
+ "myLoads": "My Loads",
+ "openLoads": "Open Loads",
+ "activeShipments": "Active Shipments",
+ "recentLoads": "Recent Loads",
+ "loadsPosted": "Loads Posted",
+ "deals": "Deals"
+ },
+ "common": {
+ "appName": "Bharath Trucks",
+ "subtitle": "National Freight Platform",
+ "noLoads": "No loads available",
+ "urgent": "Urgent",
+ "from": "From",
+ "to": "To",
+ "truckType": "Truck Type",
+ "all": "All",
+ "loadboard": "Load Board",
+ "tons": "tons",
+ "bids": "bids"
+ },
+ "auth": {
+ "loginTitle": "Login",
+ "loginSubtitle": "Enter your username and password",
+ "registerTitle": "Register",
+ "registerSubtitle": "Create a free account",
+ "username": "Username",
+ "password": "Password",
+ "confirmPassword": "Confirm Password",
+ "fullName": "Full Name",
+ "phone": "Phone Number",
+ "yourRole": "Your Role",
+ "driver": "Driver",
+ "shipper": "Shipper",
+ "broker": "Broker",
+ "noAccount": "New here?",
+ "hasAccount": "Already have an account?",
+ "registerBtn": "Register Free",
+ "vehicleNumber": "Vehicle Number",
+ "vehicleHint": "Your vehicle number will be your username"
+ },
+ "trips": {
+ "noTrips": "No trips yet",
+ "pickedUp": "Picked Up",
+ "inTransit": "In Transit",
+ "delivered": "Delivered"
+ },
+ "postLoad": {
+ "weight": "Weight (tons)",
+ "material": "Material Type",
+ "budget": "Budget (₹)",
+ "pickupDate": "Pickup Date",
+ "notes": "Notes",
+ "select": "Select"
+ },
+ "profile": {
+ "name": "Name",
+ "phone": "Phone",
+ "city": "City",
+ "state": "State",
+ "update": "Update Profile",
+ "updated": "Profile updated",
+ "myLevel": "My Level"
+ },
+ "messages": {
+ "noMessages": "No messages yet",
+ "typeHere": "Type message..."
+ },
+ "loadDetail": {
+ "yourPrice": "Your Price",
+ "updateBid": "Update Bid",
+ "bidsReceived": "Bids Received"
+ },
+ "landing": {
+ "badge": "Government Registered Platform 🇮🇳",
+ "heroTitle": "Truck Drivers. Shippers. Brokers.",
+ "heroHighlight": "Free for everyone.",
+ "heroSub": "India's National Freight Platform — Post loads, bid, earn. No fees ever.",
+ "free": "Free",
+ "forever": "Forever",
+ "seconds": "seconds",
+ "minutes": "minutes",
+ "firstBid": "First Bid",
+ "onePlatform": "One Platform. Three Users.",
+ "onePlatformSub": "Whether you ship goods, drive trucks, or broker deals — Bharath Trucks is for you.",
+ "driverF1": "Find loads and place bids",
+ "driverF2": "Avoid empty returns",
+ "driverF3": "Track your earnings",
+ "driverF4": "Connect directly with shippers",
+ "shipperF1": "Post loads, get bids",
+ "shipperF2": "Choose verified drivers",
+ "shipperF3": "Track shipment status",
+ "shipperF4": "Keep payment records",
+ "brokerF1": "Digitize your network",
+ "brokerF2": "Track commissions",
+ "brokerF3": "Post loads for shippers",
+ "brokerF4": "Grow driver network",
+ "howTitle": "How does it work?",
+ "howSub": "Just 4 easy steps",
+ "step1": "Register",
+ "step1Desc": "Create a free account with your phone number. Choose your role.",
+ "step2": "Post / Find Loads",
+ "step2Desc": "Shippers post loads. Drivers browse available loads.",
+ "step3": "Bid / Accept",
+ "step3Desc": "Drivers quote their price. Shippers pick the best bid.",
+ "step4": "Deliver & Get Paid",
+ "step4Desc": "Complete the trip. Get paid directly via UPI.",
+ "whyTitle": "Why Bharath Trucks?",
+ "noFee": "No fees",
+ "secure": "Secure platform",
+ "mobile": "Works on mobile",
+ "madeInIndia": "Made for India",
+ "ctaTitle": "Start today — completely free!",
+ "ctaSub": "All features free for 1000+ users. No credit card needed."
+ }
+}
diff --git a/webapp/src/i18n/hi.json b/webapp/src/i18n/hi.json
new file mode 100644
index 0000000..318155c
--- /dev/null
+++ b/webapp/src/i18n/hi.json
@@ -0,0 +1,143 @@
+{
+ "nav": {
+ "home": "होम",
+ "loads": "लोड",
+ "post": "पोस्ट",
+ "trips": "ट्रिप",
+ "messages": "संदेश",
+ "profile": "प्रोफ़ाइल"
+ },
+ "actions": {
+ "viewLoads": "लोड देखें",
+ "myTrips": "मेरी ट्रिप",
+ "earnings": "कमाई",
+ "postLoad": "लोड पोस्ट करें",
+ "bid": "बोली लगाएं",
+ "search": "खोजें",
+ "login": "लॉगिन",
+ "register": "पंजीकरण",
+ "logout": "लॉगआउट"
+ },
+ "dashboard": {
+ "hello": "नमस्ते",
+ "totalTrips": "कुल ट्रिप",
+ "activeBids": "सक्रिय बोलियाँ",
+ "earnings": "कमाई",
+ "activeTrips": "सक्रिय ट्रिप",
+ "shipperTitle": "शिपर डैशबोर्ड",
+ "brokerTitle": "ब्रोकर डैशबोर्ड",
+ "myLoads": "मेरे लोड",
+ "openLoads": "खुले लोड",
+ "activeShipments": "सक्रिय शिपमेंट",
+ "recentLoads": "हाल के लोड",
+ "loadsPosted": "लोड पोस्ट",
+ "deals": "सौदे"
+ },
+ "common": {
+ "appName": "भारत ट्रक्स",
+ "subtitle": "राष्ट्रीय माल परिवहन मंच",
+ "noLoads": "कोई लोड उपलब्ध नहीं",
+ "urgent": "अर्जेंट",
+ "from": "कहाँ से",
+ "to": "कहाँ तक",
+ "truckType": "ट्रक प्रकार",
+ "all": "सभी",
+ "loadboard": "लोड बोर्ड",
+ "tons": "टन",
+ "bids": "बोली"
+ },
+ "auth": {
+ "loginTitle": "लॉगिन | Login",
+ "loginSubtitle": "अपना यूज़रनेम और पासवर्ड दर्ज करें",
+ "registerTitle": "पंजीकरण | Register",
+ "registerSubtitle": "मुफ्त खाता बनाएं",
+ "username": "यूज़रनेम",
+ "password": "पासवर्ड",
+ "confirmPassword": "पासवर्ड पुष्टि",
+ "fullName": "पूरा नाम",
+ "phone": "फोन नंबर",
+ "yourRole": "आप कौन हैं?",
+ "driver": "ड्राइवर",
+ "shipper": "शिपर",
+ "broker": "ब्रोकर",
+ "noAccount": "नया खाता?",
+ "hasAccount": "पहले से खाता है?",
+ "registerBtn": "मुफ्त पंजीकरण करें",
+ "vehicleNumber": "गाड़ी नंबर",
+ "vehicleHint": "आपका गाड़ी नंबर ही आपका यूज़रनेम होगा"
+ },
+ "trips": {
+ "noTrips": "कोई ट्रिप नहीं",
+ "pickedUp": "पिकअप किया",
+ "inTransit": "रास्ते में",
+ "delivered": "पहुँचा दिया"
+ },
+ "postLoad": {
+ "weight": "वज़न (टन)",
+ "material": "माल का प्रकार",
+ "budget": "बजट (₹)",
+ "pickupDate": "पिकअप तारीख",
+ "notes": "विवरण",
+ "select": "चुनें"
+ },
+ "profile": {
+ "name": "नाम",
+ "phone": "फोन नंबर",
+ "city": "शहर",
+ "state": "राज्य",
+ "update": "प्रोफ़ाइल अपडेट करें",
+ "updated": "प्रोफ़ाइल अपडेट हो गई",
+ "myLevel": "मेरा लेवल"
+ },
+ "messages": {
+ "noMessages": "कोई संदेश नहीं",
+ "typeHere": "संदेश लिखें..."
+ },
+ "loadDetail": {
+ "yourPrice": "आपकी कीमत",
+ "updateBid": "बोली अपडेट करें",
+ "bidsReceived": "बोलियाँ प्राप्त"
+ },
+ "landing": {
+ "badge": "भारत सरकार पंजीकृत मंच | Registered Platform",
+ "heroTitle": "ट्रक ड्राइवर। शिपर। ब्रोकर।",
+ "heroHighlight": "सबके लिए मुफ्त।",
+ "heroSub": "भारत का राष्ट्रीय माल परिवहन मंच — लोड पोस्ट करें, बोली लगाएं, कमाई करें। बिना किसी शुल्क के।",
+ "free": "मुफ्त",
+ "forever": "हमेशा के लिए",
+ "seconds": "सेकंड",
+ "minutes": "मिनट",
+ "firstBid": "पहली बोली",
+ "onePlatform": "एक मंच। तीन उपयोगकर्ता।",
+ "onePlatformSub": "चाहे आप माल भेजें, ट्रक चलाएं, या सौदे कराएं — भारत ट्रक्स आपके लिए है।",
+ "driverF1": "लोड खोजें और बोली लगाएं",
+ "driverF2": "खाली वापसी से बचें",
+ "driverF3": "कमाई का हिसाब रखें",
+ "driverF4": "सीधे शिपर से जुड़ें",
+ "shipperF1": "लोड पोस्ट करें, बोली पाएं",
+ "shipperF2": "सत्यापित ड्राइवर चुनें",
+ "shipperF3": "माल की स्थिति जानें",
+ "shipperF4": "भुगतान का रिकॉर्ड रखें",
+ "brokerF1": "अपने नेटवर्क को डिजिटल करें",
+ "brokerF2": "कमीशन ट्रैक करें",
+ "brokerF3": "शिपर के लिए लोड पोस्ट करें",
+ "brokerF4": "ड्राइवर नेटवर्क बढ़ाएं",
+ "howTitle": "कैसे काम करता है?",
+ "howSub": "सिर्फ 4 आसान कदम",
+ "step1": "पंजीकरण करें",
+ "step1Desc": "फोन नंबर से मुफ्त अकाउंट बनाएं। अपनी भूमिका चुनें।",
+ "step2": "लोड पोस्ट / खोजें",
+ "step2Desc": "शिपर लोड पोस्ट करें। ड्राइवर उपलब्ध लोड देखें।",
+ "step3": "बोली लगाएं / स्वीकार करें",
+ "step3Desc": "ड्राइवर अपनी कीमत बताएं। शिपर सबसे अच्छी बोली चुनें।",
+ "step4": "माल पहुँचाएं, भुगतान पाएं",
+ "step4Desc": "ट्रिप पूरी करें। UPI से सीधे भुगतान पाएं।",
+ "whyTitle": "क्यों भारत ट्रक्स?",
+ "noFee": "कोई शुल्क नहीं",
+ "secure": "सुरक्षित मंच",
+ "mobile": "मोबाइल पर चलता है",
+ "madeInIndia": "भारत के लिए बना",
+ "ctaTitle": "आज ही शुरू करें — बिल्कुल मुफ्त!",
+ "ctaSub": "1000+ उपयोगकर्ताओं तक सभी सुविधाएं मुफ्त। कोई क्रेडिट कार्ड नहीं चाहिए।"
+ }
+}
diff --git a/webapp/src/i18n/ta.json b/webapp/src/i18n/ta.json
new file mode 100644
index 0000000..050dea7
--- /dev/null
+++ b/webapp/src/i18n/ta.json
@@ -0,0 +1,143 @@
+{
+ "nav": {
+ "home": "முகப்பு",
+ "loads": "சரக்கு",
+ "post": "பதிவு",
+ "trips": "பயணம்",
+ "messages": "செய்தி",
+ "profile": "சுயவிவரம்"
+ },
+ "actions": {
+ "viewLoads": "சரக்கு பார்க்க",
+ "myTrips": "என் பயணங்கள்",
+ "earnings": "வருமானம்",
+ "postLoad": "சரக்கு பதிவு",
+ "bid": "ஏலம்",
+ "search": "தேடு",
+ "login": "உள்நுழை",
+ "register": "பதிவு செய்",
+ "logout": "வெளியேறு"
+ },
+ "dashboard": {
+ "hello": "வணக்கம்",
+ "totalTrips": "மொத்த பயணம்",
+ "activeBids": "செயலில் ஏலங்கள்",
+ "earnings": "வருமானம்",
+ "activeTrips": "செயலில் பயணம்",
+ "shipperTitle": "அனுப்புநர் டாஷ்போர்டு",
+ "brokerTitle": "தரகர் டாஷ்போர்டு",
+ "myLoads": "என் சரக்கு",
+ "openLoads": "திறந்த சரக்கு",
+ "activeShipments": "செயலில் ஷிப்மென்ட்",
+ "recentLoads": "சமீபத்திய சரக்கு",
+ "loadsPosted": "பதிவு செய்தவை",
+ "deals": "ஒப்பந்தங்கள்"
+ },
+ "common": {
+ "appName": "பாரத் டிரக்ஸ்",
+ "subtitle": "தேசிய சரக்கு போக்குவரத்து தளம்",
+ "noLoads": "சரக்கு இல்லை",
+ "urgent": "அவசரம்",
+ "from": "எங்கிருந்து",
+ "to": "எங்கு",
+ "truckType": "லாரி வகை",
+ "all": "அனைத்தும்",
+ "loadboard": "சரக்கு பலகை",
+ "tons": "டன்",
+ "bids": "ஏலங்கள்"
+ },
+ "auth": {
+ "loginTitle": "உள்நுழை",
+ "loginSubtitle": "உங்கள் பயனர்பெயர் மற்றும் கடவுச்சொல்லை உள்ளிடவும்",
+ "registerTitle": "பதிவு செய்",
+ "registerSubtitle": "இலவச கணக்கு உருவாக்கவும்",
+ "username": "பயனர்பெயர்",
+ "password": "கடவுச்சொல்",
+ "confirmPassword": "கடவுச்சொல் உறுதி",
+ "fullName": "முழு பெயர்",
+ "phone": "தொலைபேசி எண்",
+ "yourRole": "நீங்கள் யார்?",
+ "driver": "டிரைவர்",
+ "shipper": "அனுப்புநர்",
+ "broker": "தரகர்",
+ "noAccount": "புதிய கணக்கு?",
+ "hasAccount": "ஏற்கனவே கணக்கு உள்ளதா?",
+ "registerBtn": "இலவச பதிவு",
+ "vehicleNumber": "வாகன எண்",
+ "vehicleHint": "உங்கள் வாகன எண் உங்கள் பயனர்பெயர் ஆகும்"
+ },
+ "trips": {
+ "noTrips": "பயணங்கள் இல்லை",
+ "pickedUp": "எடுக்கப்பட்டது",
+ "inTransit": "வழியில்",
+ "delivered": "வழங்கப்பட்டது"
+ },
+ "postLoad": {
+ "weight": "எடை (டன்)",
+ "material": "பொருள் வகை",
+ "budget": "பட்ஜெட் (₹)",
+ "pickupDate": "பிக்அப் தேதி",
+ "notes": "குறிப்புகள்",
+ "select": "தேர்வு"
+ },
+ "profile": {
+ "name": "பெயர்",
+ "phone": "தொலைபேசி",
+ "city": "நகரம்",
+ "state": "மாநிலம்",
+ "update": "புரொஃபைல் புதுப்பி",
+ "updated": "புரொஃபைல் புதுப்பிக்கப்பட்டது",
+ "myLevel": "என் நிலை"
+ },
+ "messages": {
+ "noMessages": "செய்திகள் இல்லை",
+ "typeHere": "செய்தி எழுதுங்கள்..."
+ },
+ "loadDetail": {
+ "yourPrice": "உங்கள் விலை",
+ "updateBid": "ஏலம் புதுப்பி",
+ "bidsReceived": "ஏலங்கள் பெறப்பட்டன"
+ },
+ "landing": {
+ "badge": "அரசு பதிவு செய்யப்பட்ட தளம் 🇮🇳",
+ "heroTitle": "டிரக் டிரைவர். அனுப்புநர். தரகர்.",
+ "heroHighlight": "அனைவருக்கும் இலவசம்.",
+ "heroSub": "இந்தியாவின் தேசிய சரக்கு தளம் — சரக்கு பதிவு செய்யுங்கள், ஏலம் விடுங்கள், சம்பாதியுங்கள். கட்டணம் இல்லை.",
+ "free": "இலவசம்",
+ "forever": "எப்போதும்",
+ "seconds": "வினாடி",
+ "minutes": "நிமிடம்",
+ "firstBid": "முதல் ஏலம்",
+ "onePlatform": "ஒரு தளம். மூன்று பயனர்கள்.",
+ "onePlatformSub": "நீங்கள் சரக்கு அனுப்பினாலும், லாரி ஓட்டினாலும், தரகு செய்தாலும் — பாரத் டிரக்ஸ் உங்களுக்கானது.",
+ "driverF1": "சரக்கு கண்டுபிடித்து ஏலம் விடுங்கள்",
+ "driverF2": "வெற்று திரும்புதலை தவிர்க்கவும்",
+ "driverF3": "வருமானத்தை கணக்கிடுங்கள்",
+ "driverF4": "நேரடியாக அனுப்புநருடன் இணையுங்கள்",
+ "shipperF1": "சரக்கு பதிவு செய்யுங்கள், ஏலம் பெறுங்கள்",
+ "shipperF2": "சரிபார்க்கப்பட்ட டிரைவரை தேர்வு செய்யுங்கள்",
+ "shipperF3": "சரக்கு நிலையை அறியுங்கள்",
+ "shipperF4": "பணம் செலுத்தல் பதிவு வைக்கவும்",
+ "brokerF1": "உங்கள் நெட்வொர்க்கை டிஜிட்டல் ஆக்குங்கள்",
+ "brokerF2": "கமிஷன் கண்காணிக்கவும்",
+ "brokerF3": "அனுப்புநருக்காக சரக்கு பதிவு செய்யுங்கள்",
+ "brokerF4": "டிரைவர் நெட்வொர்க் வளர்க்கவும்",
+ "howTitle": "எப்படி வேலை செய்கிறது?",
+ "howSub": "வெறும் 4 எளிய படிகள்",
+ "step1": "பதிவு செய்யுங்கள்",
+ "step1Desc": "தொலைபேசி எண்ணுடன் இலவச கணக்கு உருவாக்கவும். உங்கள் பங்கை தேர்வு செய்யுங்கள்.",
+ "step2": "சரக்கு பதிவு / தேடு",
+ "step2Desc": "அனுப்புநர் சரக்கு பதிவு செய்யுங்கள். டிரைவர் கிடைக்கும் சரக்கை பாருங்கள்.",
+ "step3": "ஏலம் / ஏற்றுக்கொள்",
+ "step3Desc": "டிரைவர் விலை சொல்லுங்கள். அனுப்புநர் சிறந்த ஏலத்தை தேர்வு செய்யுங்கள்.",
+ "step4": "டெலிவரி & பணம் பெறுங்கள்",
+ "step4Desc": "பயணத்தை முடியுங்கள். UPI மூலம் நேரடியாக பணம் பெறுங்கள்.",
+ "whyTitle": "ஏன் பாரத் டிரக்ஸ்?",
+ "noFee": "கட்டணம் இல்லை",
+ "secure": "பாதுகாப்பான தளம்",
+ "mobile": "மொபைலில் இயங்கும்",
+ "madeInIndia": "இந்தியாவுக்காக உருவாக்கப்பட்டது",
+ "ctaTitle": "இன்றே தொடங்குங்கள் — முற்றிலும் இலவசம்!",
+ "ctaSub": "1000+ பயனர்களுக்கு அனைத்து வசதிகளும் இலவசம். கிரெடிட் கார்டு தேவையில்லை."
+ }
+}
diff --git a/webapp/src/i18n/te.json b/webapp/src/i18n/te.json
new file mode 100644
index 0000000..f813ee8
--- /dev/null
+++ b/webapp/src/i18n/te.json
@@ -0,0 +1,143 @@
+{
+ "nav": {
+ "home": "హోమ్",
+ "loads": "లోడ్లు",
+ "post": "పోస్ట్",
+ "trips": "ట్రిప్",
+ "messages": "సందేశాలు",
+ "profile": "ప్రొఫైల్"
+ },
+ "actions": {
+ "viewLoads": "లోడ్లు చూడండి",
+ "myTrips": "నా ట్రిప్లు",
+ "earnings": "ఆదాయం",
+ "postLoad": "లోడ్ పోస్ట్",
+ "bid": "బిడ్ వేయండి",
+ "search": "వెతకండి",
+ "login": "లాగిన్",
+ "register": "నమోదు",
+ "logout": "లాగ్అవుట్"
+ },
+ "dashboard": {
+ "hello": "నమస్కారం",
+ "totalTrips": "మొత్తం ట్రిప్లు",
+ "activeBids": "యాక్టివ్ బిడ్లు",
+ "earnings": "ఆదాయం",
+ "activeTrips": "యాక్టివ్ ట్రిప్లు",
+ "shipperTitle": "షిప్పర్ డాష్బోర్డ్",
+ "brokerTitle": "బ్రోకర్ డాష్బోర్డ్",
+ "myLoads": "నా లోడ్లు",
+ "openLoads": "ఓపెన్ లోడ్లు",
+ "activeShipments": "యాక్టివ్ షిప్మెంట్లు",
+ "recentLoads": "ఇటీవలి లోడ్లు",
+ "loadsPosted": "పోస్ట్ చేసిన లోడ్లు",
+ "deals": "డీల్స్"
+ },
+ "common": {
+ "appName": "భారత్ ట్రక్స్",
+ "subtitle": "జాతీయ సరుకు రవాణా వేదిక",
+ "noLoads": "లోడ్లు లేవు",
+ "urgent": "అత్యవసరం",
+ "from": "ఎక్కడ నుండి",
+ "to": "ఎక్కడికి",
+ "truckType": "ట్రక్ రకం",
+ "all": "అన్నీ",
+ "loadboard": "లోడ్ బోర్డ్",
+ "tons": "టన్నులు",
+ "bids": "బిడ్లు"
+ },
+ "auth": {
+ "loginTitle": "లాగిన్",
+ "loginSubtitle": "మీ యూజర్నేమ్ మరియు పాస్వర్డ్ నమోదు చేయండి",
+ "registerTitle": "నమోదు",
+ "registerSubtitle": "ఉచిత ఖాతా సృష్టించండి",
+ "username": "యూజర్నేమ్",
+ "password": "పాస్వర్డ్",
+ "confirmPassword": "పాస్వర్డ్ నిర్ధారణ",
+ "fullName": "పూర్తి పేరు",
+ "phone": "ఫోన్ నంబర్",
+ "yourRole": "మీరు ఎవరు?",
+ "driver": "డ్రైవర్",
+ "shipper": "షిప్పర్",
+ "broker": "బ్రోకర్",
+ "noAccount": "కొత్త ఖాతా?",
+ "hasAccount": "ఇప్పటికే ఖాతా ఉందా?",
+ "registerBtn": "ఉచిత నమోదు",
+ "vehicleNumber": "వాహన నంబర్",
+ "vehicleHint": "మీ వాహన నంబర్ మీ యూజర్నేమ్ అవుతుంది"
+ },
+ "trips": {
+ "noTrips": "ట్రిప్లు లేవు",
+ "pickedUp": "పికప్ చేసారు",
+ "inTransit": "దారిలో",
+ "delivered": "డెలివరీ అయింది"
+ },
+ "postLoad": {
+ "weight": "బరువు (టన్నులు)",
+ "material": "మెటీరియల్ రకం",
+ "budget": "బడ్జెట్ (₹)",
+ "pickupDate": "పికప్ తేదీ",
+ "notes": "నోట్స్",
+ "select": "ఎంచుకోండి"
+ },
+ "profile": {
+ "name": "పేరు",
+ "phone": "ఫోన్",
+ "city": "నగరం",
+ "state": "రాష్ట్రం",
+ "update": "ప్రొఫైల్ అప్డేట్",
+ "updated": "ప్రొఫైల్ అప్డేట్ అయింది",
+ "myLevel": "నా లెవెల్"
+ },
+ "messages": {
+ "noMessages": "సందేశాలు లేవు",
+ "typeHere": "సందేశం రాయండి..."
+ },
+ "loadDetail": {
+ "yourPrice": "మీ ధర",
+ "updateBid": "బిడ్ అప్డేట్",
+ "bidsReceived": "బిడ్లు వచ్చాయి"
+ },
+ "landing": {
+ "badge": "ప్రభుత్వ నమోదిత వేదిక 🇮🇳",
+ "heroTitle": "ట్రక్ డ్రైవర్లు. షిప్పర్లు. బ్రోకర్లు.",
+ "heroHighlight": "అందరికీ ఉచితం.",
+ "heroSub": "భారతదేశ జాతీయ సరుకు రవాణా వేదిక — లోడ్ పోస్ట్ చేయండి, బిడ్ వేయండి, సంపాదించండి. ఎటువంటి ఫీజు లేదు.",
+ "free": "ఉచితం",
+ "forever": "ఎప్పటికీ",
+ "seconds": "సెకన్లు",
+ "minutes": "నిమిషాలు",
+ "firstBid": "మొదటి బిడ్",
+ "onePlatform": "ఒక వేదిక. ముగ్గురు వినియోగదారులు.",
+ "onePlatformSub": "మీరు సరుకు పంపినా, ట్రక్ నడిపినా, డీల్స్ చేసినా — భారత్ ట్రక్స్ మీ కోసం.",
+ "driverF1": "లోడ్లు కనుగొని బిడ్ వేయండి",
+ "driverF2": "ఖాళీ తిరుగు ప్రయాణం నివారించండి",
+ "driverF3": "ఆదాయం లెక్కించండి",
+ "driverF4": "నేరుగా షిప్పర్తో కనెక్ట్ అవ్వండి",
+ "shipperF1": "లోడ్ పోస్ట్ చేయండి, బిడ్లు పొందండి",
+ "shipperF2": "వెరిఫైడ్ డ్రైవర్ను ఎంచుకోండి",
+ "shipperF3": "సరుకు స్థితి తెలుసుకోండి",
+ "shipperF4": "చెల్లింపు రికార్డ్ ఉంచండి",
+ "brokerF1": "మీ నెట్వర్క్ను డిజిటల్ చేయండి",
+ "brokerF2": "కమీషన్ ట్రాక్ చేయండి",
+ "brokerF3": "షిప్పర్ కోసం లోడ్ పోస్ట్ చేయండి",
+ "brokerF4": "డ్రైవర్ నెట్వర్క్ పెంచండి",
+ "howTitle": "ఎలా పని చేస్తుంది?",
+ "howSub": "కేవలం 4 సులభ అడుగులు",
+ "step1": "నమోదు చేయండి",
+ "step1Desc": "ఫోన్ నంబర్తో ఉచిత ఖాతా సృష్టించండి. మీ పాత్ర ఎంచుకోండి.",
+ "step2": "లోడ్ పోస్ట్ / వెతకండి",
+ "step2Desc": "షిప్పర్ లోడ్ పోస్ట్ చేయండి. డ్రైవర్ అందుబాటులో ఉన్న లోడ్లు చూడండి.",
+ "step3": "బిడ్ / అంగీకరించండి",
+ "step3Desc": "డ్రైవర్ మీ ధర చెప్పండి. షిప్పర్ ఉత్తమ బిడ్ ఎంచుకోండి.",
+ "step4": "డెలివరీ & చెల్లింపు పొందండి",
+ "step4Desc": "ట్రిప్ పూర్తి చేయండి. UPI ద్వారా నేరుగా చెల్లింపు పొందండి.",
+ "whyTitle": "ఎందుకు భారత్ ట్రక్స్?",
+ "noFee": "ఫీజు లేదు",
+ "secure": "సురక్షిత వేదిక",
+ "mobile": "మొబైల్లో పని చేస్తుంది",
+ "madeInIndia": "భారతదేశం కోసం తయారు",
+ "ctaTitle": "ఈ రోజే ప్రారంభించండి — పూర్తిగా ఉచితం!",
+ "ctaSub": "1000+ వినియోగదారులకు అన్ని ఫీచర్లు ఉచితం. క్రెడిట్ కార్డ్ అవసరం లేదు."
+ }
+}
diff --git a/webapp/src/lib/gamification.js b/webapp/src/lib/gamification.js
new file mode 100644
index 0000000..591e6ea
--- /dev/null
+++ b/webapp/src/lib/gamification.js
@@ -0,0 +1,46 @@
+// Gamification Engine: XP, Levels, Achievements, Badges, Streaks
+
+const XP_REWARDS = {
+ signup: 50, add_phone: 30, select_role: 25, add_vehicle: 40, complete_onboarding: 100,
+ first_login_today: 10, post_load: 30, place_bid: 20, win_bid: 50, complete_trip: 40,
+ add_ledger_entry: 15, settle_payment: 25, share_load_whatsapp: 15,
+ invite_friend: 40, friend_joined: 60, receive_rating: 20, give_rating: 10,
+ safety_checkin: 5, log_toll: 5, add_reminder: 10,
+ first_load_posted: 75, first_bid_placed: 75, first_bid_won: 100, first_trip_completed: 100,
+ login_streak_3: 50, login_streak_7: 150, login_streak_30: 500,
+};
+
+const LEVELS = [
+ { level: 1, xp: 0, title: 'Beginner', title_hi: 'शुरुआत', icon: '🌱' },
+ { level: 2, xp: 100, title: 'Starter', title_hi: 'नया', icon: '🌿' },
+ { level: 3, xp: 300, title: 'Active', title_hi: 'सक्रिय', icon: '🌳' },
+ { level: 4, xp: 600, title: 'Regular', title_hi: 'नियमित', icon: '⭐' },
+ { level: 5, xp: 1000, title: 'Pro', title_hi: 'प्रो', icon: '🌟' },
+ { level: 6, xp: 1500, title: 'Expert', title_hi: 'विशेषज्ञ', icon: '💫' },
+ { level: 7, xp: 2500, title: 'Master', title_hi: 'मास्टर', icon: '🏆' },
+ { level: 8, xp: 4000, title: 'Legend', title_hi: 'लीजेंड', icon: '👑' },
+];
+
+const ACHIEVEMENTS = [
+ { id: 'first_load', title: 'First Load', icon: '📦', xp: 75, condition: 'post_load >= 1' },
+ { id: 'first_bid', title: 'First Bid', icon: '🏷️', xp: 75, condition: 'place_bid >= 1' },
+ { id: 'first_trip', title: 'First Trip', icon: '🚛', xp: 100, condition: 'complete_trip >= 1' },
+ { id: 'five_trips', title: '5 Trips', icon: '🎯', xp: 150, condition: 'complete_trip >= 5' },
+ { id: 'ten_trips', title: '10 Trips', icon: '🔥', xp: 250, condition: 'complete_trip >= 10' },
+ { id: 'social_butterfly', title: 'Social', icon: '🦋', xp: 100, condition: 'share_load_whatsapp >= 5' },
+ { id: 'safe_driver', title: 'Safe Driver', icon: '🛡️', xp: 100, condition: 'safety_checkin >= 7' },
+ { id: 'streak_week', title: '7 Day Streak', icon: '🔥', xp: 150, condition: 'login_streak >= 7' },
+ { id: 'referrer', title: 'Referrer', icon: '🤝', xp: 200, condition: 'invite_friend >= 3' },
+];
+
+function getLevelForXP(xp) {
+ let current = LEVELS[0];
+ for (const l of LEVELS) { if (xp >= l.xp) current = l; else break; }
+ const next = LEVELS[current.level] || null;
+ const progress = next ? Math.round(((xp - current.xp) / (next.xp - current.xp)) * 100) : 100;
+ return { ...current, xp_current: xp, xp_next: next ? next.xp : current.xp, progress };
+}
+
+function getXPReward(action) { return XP_REWARDS[action] || 0; }
+
+module.exports = { XP_REWARDS, LEVELS, ACHIEVEMENTS, getLevelForXP, getXPReward };
diff --git a/webapp/src/lib/india.js b/webapp/src/lib/india.js
new file mode 100644
index 0000000..8297fcf
--- /dev/null
+++ b/webapp/src/lib/india.js
@@ -0,0 +1,96 @@
+// India-specific utilities: UPI, vehicle validation, toll calc, fuel, WhatsApp templates
+
+const STATES = {
+ KL:'Kerala',TN:'Tamil Nadu',KA:'Karnataka',AP:'Andhra Pradesh',TS:'Telangana',
+ MH:'Maharashtra',GJ:'Gujarat',RJ:'Rajasthan',UP:'Uttar Pradesh',DL:'Delhi',
+ HR:'Haryana',PB:'Punjab',WB:'West Bengal',MP:'Madhya Pradesh',CG:'Chhattisgarh',
+ JH:'Jharkhand',BR:'Bihar',OR:'Odisha',GA:'Goa',HP:'Himachal Pradesh',
+};
+
+const DIESEL_PRICES = {KL:95.5,TN:92.8,KA:92.4,MH:92.1,TS:95.6,AP:95.2,GJ:92.3,DL:90.6,UP:92.8,RJ:93.1,DEFAULT:92.5};
+
+const FREIGHT_CITIES = [
+ 'Mumbai','Delhi','Bangalore','Chennai','Hyderabad','Pune','Ahmedabad','Kolkata',
+ 'Jaipur','Lucknow','Nagpur','Indore','Surat','Nashik','Coimbatore','Madurai',
+ 'Vijayawada','Visakhapatnam','Kochi','Thiruvananthapuram','Mangalore','Hubli',
+ 'Salem','Erode','Vellore','Tirupati','Guntur','Rajkot','Vadodara','Bhopal',
+];
+
+const ROUTE_DB = {
+ 'mumbai_delhi':{km:1400,hours:22,tolls:12},'mumbai_pune':{km:150,hours:3,tolls:2},
+ 'mumbai_ahmedabad':{km:530,hours:8,tolls:4},'mumbai_bangalore':{km:980,hours:16,tolls:8},
+ 'delhi_jaipur':{km:280,hours:5,tolls:3},'delhi_lucknow':{km:560,hours:9,tolls:5},
+ 'delhi_kolkata':{km:1500,hours:24,tolls:12},'bangalore_chennai':{km:350,hours:6,tolls:3},
+ 'bangalore_hyderabad':{km:570,hours:9,tolls:5},'chennai_hyderabad':{km:630,hours:10,tolls:5},
+ 'chennai_mumbai':{km:1330,hours:22,tolls:10},'kolkata_delhi':{km:1500,hours:24,tolls:12},
+ 'pune_nagpur':{km:720,hours:12,tolls:6},'ahmedabad_mumbai':{km:530,hours:8,tolls:4},
+ 'hyderabad_bangalore':{km:570,hours:9,tolls:5},'indore_mumbai':{km:590,hours:10,tolls:5},
+ 'jaipur_delhi':{km:280,hours:5,tolls:3},'lucknow_delhi':{km:560,hours:9,tolls:5},
+ 'surat_mumbai':{km:300,hours:5,tolls:3},'nagpur_mumbai':{km:840,hours:14,tolls:7},
+ 'coimbatore_chennai':{km:500,hours:8,tolls:4},'kochi_bangalore':{km:550,hours:10,tolls:4},
+ 'vijayawada_hyderabad':{km:270,hours:5,tolls:3},'madurai_chennai':{km:460,hours:8,tolls:4},
+};
+
+function validateVehicleNumber(number) {
+ if (!number) return { valid: false, error: 'Required' };
+ const cleaned = number.replace(/[\s\-\.]/g, '').toUpperCase();
+ if (!/^[A-Z]{2}\d{1,2}[A-Z]{1,3}\d{4}$/.test(cleaned)) return { valid: false, error: 'Invalid format' };
+ const sc = cleaned.substring(0, 2);
+ return { valid: true, state_code: sc, state_name: STATES[sc] || 'Unknown', formatted: `${cleaned.substring(0,2)} ${cleaned.substring(2,4)} ${cleaned.substring(4,cleaned.length-4)} ${cleaned.slice(-4)}` };
+}
+
+function formatIndianPhone(phone) {
+ if (!phone) return null;
+ let c = phone.replace(/[\s\-\(\)\+]/g, '');
+ if (c.startsWith('0')) c = c.substring(1);
+ if (c.startsWith('91') && c.length === 12) c = c.substring(2);
+ if (c.length !== 10) return { valid: false };
+ return { valid: true, formatted: `+91 ${c.substring(0,5)} ${c.substring(5)}`, whatsapp: `91${c}`, raw: c };
+}
+
+function generateUPILink(opts) {
+ if (!opts.upi_id) return null;
+ const p = new URLSearchParams({ pa: opts.upi_id, pn: opts.name || 'BharathTrucks', am: String(opts.amount || ''), cu: 'INR', tn: opts.note || 'Freight Payment' });
+ return { upi_intent: `upi://pay?${p}`, gpay: `tez://upi/pay?${p}`, phonepe: `phonepe://pay?${p}` };
+}
+
+function formatINR(n) { return '₹' + (parseFloat(n) || 0).toLocaleString('en-IN'); }
+
+function estimateToll(distanceKm, vehicleType = 'truck') {
+ const rates = { lcv: 1.5, truck: 2.5, multi_axle: 3.5 };
+ return Math.round(distanceKm * 0.6 * (rates[vehicleType] || 2.5));
+}
+
+function calculateTripCost(params) {
+ const { distance_km, vehicle_type, origin_state, dest_state, freight_charged, mileage } = params;
+ const m = mileage || 4;
+ const avgDiesel = ((DIESEL_PRICES[origin_state] || DIESEL_PRICES.DEFAULT) + (DIESEL_PRICES[dest_state] || DIESEL_PRICES.DEFAULT)) / 2;
+ const fuelLitres = Math.round(distance_km / m);
+ const fuelCost = Math.round(fuelLitres * avgDiesel);
+ const toll = estimateToll(distance_km, vehicle_type);
+ const driverBata = Math.round(distance_km * 1.5);
+ const misc = 800;
+ const total = fuelCost + toll + driverBata + misc;
+ const profit = (freight_charged || 0) - total;
+ return { fuel: { litres: fuelLitres, price: avgDiesel, cost: fuelCost }, toll, driver_bata: driverBata, misc, total, profit, margin: freight_charged ? Math.round((profit / freight_charged) * 100) : 0, viable: profit > 0 };
+}
+
+function getRouteInfo(origin, destination) {
+ const o = origin.toLowerCase().trim().replace(/\s+/g, '');
+ const d = destination.toLowerCase().trim().replace(/\s+/g, '');
+ return ROUTE_DB[`${o}_${d}`] || ROUTE_DB[`${d}_${o}`] || null;
+}
+
+function getStateFromCity(city) {
+ const map = { mumbai:'MH',pune:'MH',nagpur:'MH',nashik:'MH',delhi:'DL',jaipur:'RJ',lucknow:'UP',bangalore:'KA',chennai:'TN',hyderabad:'TS',kolkata:'WB',ahmedabad:'GJ',surat:'GJ',indore:'MP',kochi:'KL',coimbatore:'TN',vijayawada:'AP',madurai:'TN' };
+ return map[city.toLowerCase().trim()] || 'MH';
+}
+
+const WHATSAPP_TEMPLATES = {
+ load_available: (d) => `🚛 *लोड उपलब्ध*\n\n📍 ${d.origin} → ${d.destination}\n💰 ₹${d.budget}\n📦 ${d.truck_type || 'ट्रक'}\n⚖️ ${d.weight || ''} टन\n\n👉 ${d.link}`,
+ payment_reminder: (d) => `नमस्ते ${d.name || ''} जी,\n\nभाड़ा भुगतान रिमाइंडर:\n📍 ${d.origin} → ${d.destination}\n💰 बकाया: ₹${d.amount}\n\nकृपया भुगतान करें। धन्यवाद! 🙏`,
+ safety_checkin: (d) => `✅ *सुरक्षा अपडेट*\n\n${d.message || 'मैं सुरक्षित हूँ।'}\n📍 ${d.location || ''}\n🕐 ${new Date().toLocaleString('en-IN', { timeZone: 'Asia/Kolkata' })}`,
+ sos: (d) => `🆘 *आपातकालीन अलर्ट*\n\n⚠️ ${d.type || 'Emergency'}\n📍 ${d.location || ''}\n🕐 ${new Date().toLocaleString('en-IN', { timeZone: 'Asia/Kolkata' })}\n\nकृपया तुरंत कॉल करें!`,
+};
+
+module.exports = { STATES, DIESEL_PRICES, FREIGHT_CITIES, ROUTE_DB, validateVehicleNumber, formatIndianPhone, generateUPILink, formatINR, estimateToll, calculateTripCost, getRouteInfo, getStateFromCity, WHATSAPP_TEMPLATES };
diff --git a/webapp/src/middleware/i18n.js b/webapp/src/middleware/i18n.js
new file mode 100644
index 0000000..38a378b
--- /dev/null
+++ b/webapp/src/middleware/i18n.js
@@ -0,0 +1,27 @@
+const path = require('path');
+const fs = require('fs');
+
+const LANGS = ['hi', 'en', 'ta', 'te'];
+const translations = {};
+
+LANGS.forEach(lang => {
+ translations[lang] = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'i18n', `${lang}.json`), 'utf8'));
+});
+
+function i18n(req, res, next) {
+ const lang = (req.session && req.session.lang && LANGS.includes(req.session.lang)) ? req.session.lang : 'en';
+ const strings = translations[lang];
+
+ res.locals.lang = lang;
+ res.locals.t = (key) => {
+ const parts = key.split('.');
+ let val = strings;
+ for (const p of parts) {
+ val = val && val[p];
+ }
+ return val || key;
+ };
+ next();
+}
+
+module.exports = { i18n, LANGS };
diff --git a/webapp/src/public/css/govt-theme.css b/webapp/src/public/css/govt-theme.css
index e2cab54..5abff3f 100644
--- a/webapp/src/public/css/govt-theme.css
+++ b/webapp/src/public/css/govt-theme.css
@@ -357,3 +357,41 @@ button, input, select, textarea { font-family: inherit; font-size: inherit; }
.bnav-add .bnav-icon { background: var(--saffron); color: #fff; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-top: -12px; font-size: 1rem; }
body { padding-bottom: 70px; }
@media (min-width: 768px) { .bottom-nav { display: none; } body { padding-bottom: 0; } }
+
+/* --- Language Switcher --- */
+.lang-switcher { display: flex; gap: 4px; margin-right: 12px; }
+.lang-btn {
+ display: inline-flex; align-items: center; justify-content: center;
+ width: 28px; height: 28px; border-radius: 50%;
+ background: rgba(255,255,255,0.15); color: #fff;
+ font-size: 0.7rem; font-weight: 700; text-decoration: none;
+ border: 2px solid transparent; transition: background 0.2s, border-color 0.2s;
+}
+.lang-btn:hover { background: rgba(255,255,255,0.3); text-decoration: none; }
+.lang-btn.active { border-color: var(--saffron); background: rgba(255,255,255,0.25); }
+
+/* --- Large Icon Nav (low-literacy) --- */
+.bnav-icon-lg { font-size: 1.8rem; line-height: 1; }
+.bnav-label { font-size: 0.65rem; font-weight: 600; }
+
+/* --- Icon Action Buttons (dashboard) --- */
+.icon-action-btn {
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
+ gap: 6px; padding: 20px 12px;
+ background: var(--white); border: 2px solid var(--gray-300);
+ border-radius: var(--radius-md); text-decoration: none; color: var(--navy);
+ transition: border-color 0.2s, box-shadow 0.2s;
+}
+.icon-action-btn:hover { border-color: var(--navy); box-shadow: 0 2px 8px rgba(26,35,126,0.12); text-decoration: none; }
+.icon-action-emoji { font-size: 2.5rem; line-height: 1; }
+.icon-action-label { font-size: 0.85rem; font-weight: 700; text-align: center; }
+
+/* --- Voice Input Button --- */
+.voice-btn {
+ position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
+ background: none; border: none; font-size: 1.2rem; cursor: pointer;
+ padding: 4px; border-radius: 50%; transition: background 0.2s;
+}
+.voice-btn:hover { background: var(--gray-100); }
+.voice-btn.voice-active { animation: pulse 1s infinite; }
+@keyframes pulse { 0%,100%{transform:translateY(-50%) scale(1)} 50%{transform:translateY(-50%) scale(1.2)} }
diff --git a/webapp/src/public/js/voice.js b/webapp/src/public/js/voice.js
new file mode 100644
index 0000000..1a536dc
--- /dev/null
+++ b/webapp/src/public/js/voice.js
@@ -0,0 +1,34 @@
+// Voice Input - Web Speech API for low-literacy users
+(function() {
+ if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) return;
+
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
+
+ document.querySelectorAll('.form-input[type="text"], .form-input[type="tel"], .form-input[type="number"]').forEach(input => {
+ if (input.closest('.no-voice')) return;
+ const btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'voice-btn';
+ btn.innerHTML = '🎤';
+ btn.title = 'Voice input';
+ btn.onclick = function(e) {
+ e.preventDefault();
+ const recognition = new SpeechRecognition();
+ recognition.lang = document.documentElement.lang === 'ta' ? 'ta-IN' : document.documentElement.lang === 'te' ? 'te-IN' : document.documentElement.lang === 'en' ? 'en-IN' : 'hi-IN';
+ recognition.interimResults = false;
+ btn.classList.add('voice-active');
+ btn.innerHTML = '🔴';
+ recognition.start();
+ recognition.onresult = function(ev) {
+ const text = ev.results[0][0].transcript;
+ if (input.type === 'number') input.value = text.replace(/[^\d]/g, '');
+ else input.value = text;
+ input.dispatchEvent(new Event('input'));
+ };
+ recognition.onend = function() { btn.classList.remove('voice-active'); btn.innerHTML = '🎤'; };
+ recognition.onerror = function() { btn.classList.remove('voice-active'); btn.innerHTML = '🎤'; };
+ };
+ input.parentNode.style.position = 'relative';
+ input.parentNode.appendChild(btn);
+ });
+})();
diff --git a/webapp/src/routes/auth.js b/webapp/src/routes/auth.js
index ab2bd7e..07bf3ae 100644
--- a/webapp/src/routes/auth.js
+++ b/webapp/src/routes/auth.js
@@ -91,6 +91,10 @@ router.post('/register', async (req, res) => {
id: user.id, username: user.username, name: user.name,
role: user.role, phone: user.phone,
};
+
+ // Award signup XP
+ await supabase.from('user_gamification').insert([{ user_id: user.id, xp: 50, login_streak: 1 }]).catch(() => {});
+
res.redirect('/');
});
diff --git a/webapp/src/routes/bank.js b/webapp/src/routes/bank.js
new file mode 100644
index 0000000..b9f0655
--- /dev/null
+++ b/webapp/src/routes/bank.js
@@ -0,0 +1,25 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+
+router.get('/', requireAuth, async (req, res) => {
+ const { data: accounts } = await supabase.from('bank_accounts').select('*').eq('user_id', req.session.user.id);
+ res.render('pages/bank', { accounts: accounts || [] });
+});
+
+router.post('/add', requireAuth, async (req, res) => {
+ const { bank_name, account_number, ifsc, upi_id, account_holder } = req.body;
+ await supabase.from('bank_accounts').insert([{
+ user_id: req.session.user.id, bank_name, account_number: account_number || null,
+ ifsc: ifsc || null, upi_id: upi_id || null, account_holder: account_holder || null,
+ }]);
+ res.redirect('/bank');
+});
+
+router.post('/delete/:id', requireAuth, async (req, res) => {
+ await supabase.from('bank_accounts').delete().eq('id', req.params.id).eq('user_id', req.session.user.id);
+ res.redirect('/bank');
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/challenges.js b/webapp/src/routes/challenges.js
new file mode 100644
index 0000000..f3dd6b7
--- /dev/null
+++ b/webapp/src/routes/challenges.js
@@ -0,0 +1,42 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+
+const DAILY_CHALLENGES = [
+ { id: 'login', title: 'Open App', title_hi: 'ऐप खोलें', icon: '📱', xp: 10, type: 'easy' },
+ { id: 'view_loads', title: 'View 3 Loads', title_hi: '3 लोड देखें', icon: '📋', xp: 15, type: 'easy' },
+ { id: 'share_load', title: 'Share a Load', title_hi: 'लोड शेयर करें', icon: '📤', xp: 20, type: 'medium' },
+ { id: 'place_bid', title: 'Place a Bid', title_hi: 'बोली लगाएं', icon: '🏷️', xp: 25, type: 'medium' },
+ { id: 'safety_checkin', title: 'Safety Check-in', title_hi: 'सुरक्षा चेक-इन', icon: '🛡️', xp: 15, type: 'easy' },
+ { id: 'add_ledger', title: 'Log a Trip', title_hi: 'ट्रिप लॉग करें', icon: '📒', xp: 20, type: 'medium' },
+ { id: 'invite_friend', title: 'Invite a Friend', title_hi: 'दोस्त को बुलाएं', icon: '🤝', xp: 40, type: 'hard' },
+];
+
+router.get('/', requireAuth, async (req, res) => {
+ const userId = req.session.user.id;
+ const today = new Date().toISOString().split('T')[0];
+ const { data: progress } = await supabase.from('challenge_progress').select('challenge_id').eq('user_id', userId).eq('completed_date', today);
+ const completed = (progress || []).map(p => p.challenge_id);
+ // Pick 3 daily challenges (rotate by day)
+ const dayIndex = new Date().getDay();
+ const todayChallenges = [DAILY_CHALLENGES[dayIndex % 7], DAILY_CHALLENGES[(dayIndex + 2) % 7], DAILY_CHALLENGES[(dayIndex + 4) % 7]];
+ const challenges = todayChallenges.map(c => ({ ...c, completed: completed.includes(c.id) }));
+ const streak = completed.length;
+ res.render('pages/challenges', { challenges, streak, completedCount: completed.length });
+});
+
+router.post('/complete', requireAuth, async (req, res) => {
+ const { challenge_id } = req.body;
+ const userId = req.session.user.id;
+ const today = new Date().toISOString().split('T')[0];
+ await supabase.from('challenge_progress').upsert([{ user_id: userId, challenge_id, completed_date: today }], { onConflict: 'user_id,challenge_id,completed_date' });
+ const challenge = DAILY_CHALLENGES.find(c => c.id === challenge_id);
+ if (challenge) {
+ const { data: gam } = await supabase.from('user_gamification').select('xp').eq('user_id', userId).single();
+ await supabase.from('user_gamification').upsert([{ user_id: userId, xp: (gam?.xp || 0) + challenge.xp }], { onConflict: 'user_id' });
+ }
+ res.redirect('/challenges');
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/classifieds.js b/webapp/src/routes/classifieds.js
new file mode 100644
index 0000000..3ed047a
--- /dev/null
+++ b/webapp/src/routes/classifieds.js
@@ -0,0 +1,33 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+
+router.get('/', async (req, res) => {
+ const { category } = req.query;
+ let query = supabase.from('classifieds').select('*').eq('status', 'active').order('created_at', { ascending: false }).limit(30);
+ if (category) query = query.eq('category', category);
+ const { data: listings } = await query;
+ res.render('pages/classifieds', { listings: listings || [], category: category || 'all' });
+});
+
+router.get('/post', requireAuth, (req, res) => {
+ res.render('pages/classifieds-post');
+});
+
+router.post('/post', requireAuth, async (req, res) => {
+ const { title, description, price, category, location, contact_phone } = req.body;
+ await supabase.from('classifieds').insert([{
+ user_id: req.session.user.id, title, description: description || null,
+ price: parseFloat(price) || 0, category: category || 'truck',
+ location: location || null, contact_phone: contact_phone || null, status: 'active',
+ }]);
+ res.redirect('/classifieds');
+});
+
+router.post('/delete/:id', requireAuth, async (req, res) => {
+ await supabase.from('classifieds').update({ status: 'deleted' }).eq('id', req.params.id).eq('user_id', req.session.user.id);
+ res.redirect('/classifieds');
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/documents.js b/webapp/src/routes/documents.js
new file mode 100644
index 0000000..99dcc1a
--- /dev/null
+++ b/webapp/src/routes/documents.js
@@ -0,0 +1,27 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+
+router.get('/', requireAuth, async (req, res) => {
+ const userId = req.session.user.id;
+ const { data: docs } = await supabase.from('vehicle_documents').select('*').eq('user_id', userId).order('created_at', { ascending: false });
+ res.render('pages/documents', { documents: docs || [] });
+});
+
+router.post('/add', requireAuth, async (req, res) => {
+ const { doc_type, doc_number, vehicle_number, expiry_date, notes } = req.body;
+ await supabase.from('vehicle_documents').insert([{
+ user_id: req.session.user.id, doc_type, doc_number: doc_number || null,
+ vehicle_number: (vehicle_number || '').toUpperCase().trim(),
+ expiry_date: expiry_date || null, notes: notes || null, status: 'uploaded',
+ }]);
+ res.redirect('/documents');
+});
+
+router.post('/delete/:id', requireAuth, async (req, res) => {
+ await supabase.from('vehicle_documents').delete().eq('id', req.params.id).eq('user_id', req.session.user.id);
+ res.redirect('/documents');
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/driver-ledger.js b/webapp/src/routes/driver-ledger.js
new file mode 100644
index 0000000..25366fb
--- /dev/null
+++ b/webapp/src/routes/driver-ledger.js
@@ -0,0 +1,41 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth, requireDriver } = require('../middleware/auth');
+
+// Driver ledger - personal earnings tracker
+router.get('/ledger', requireAuth, requireDriver, async (req, res) => {
+ const userId = req.session.user.id;
+ const { data: entries } = await supabase.from('driver_ledger').select('*').eq('user_id', userId).order('trip_date', { ascending: false }).limit(50);
+ const all = entries || [];
+ const stats = {
+ total_trips: all.length,
+ total_earned: all.reduce((s, e) => s + (parseFloat(e.freight_received) || 0), 0),
+ total_expenses: all.reduce((s, e) => s + (parseFloat(e.fuel_cost) || 0) + (parseFloat(e.toll_cost) || 0) + (parseFloat(e.other_expense) || 0), 0),
+ };
+ stats.net_profit = stats.total_earned - stats.total_expenses;
+ res.render('pages/driver-ledger', { entries: all, stats });
+});
+
+// Add trip to ledger
+router.get('/ledger/add', requireAuth, requireDriver, (req, res) => {
+ res.render('pages/driver-ledger-add');
+});
+
+router.post('/ledger/add', requireAuth, requireDriver, async (req, res) => {
+ const { origin, destination, trip_date, freight_received, fuel_cost, toll_cost, other_expense, notes } = req.body;
+ await supabase.from('driver_ledger').insert([{
+ user_id: req.session.user.id, origin, destination, trip_date: trip_date || new Date().toISOString().split('T')[0],
+ freight_received: parseFloat(freight_received) || 0, fuel_cost: parseFloat(fuel_cost) || 0,
+ toll_cost: parseFloat(toll_cost) || 0, other_expense: parseFloat(other_expense) || 0, notes: notes || null,
+ }]);
+ res.redirect('/driver/ledger');
+});
+
+// Delete entry
+router.post('/ledger/delete/:id', requireAuth, requireDriver, async (req, res) => {
+ await supabase.from('driver_ledger').delete().eq('id', req.params.id).eq('user_id', req.session.user.id);
+ res.redirect('/driver/ledger');
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/fastag.js b/webapp/src/routes/fastag.js
new file mode 100644
index 0000000..4219204
--- /dev/null
+++ b/webapp/src/routes/fastag.js
@@ -0,0 +1,37 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+const { generateUPILink } = require('../lib/india');
+
+router.get('/', requireAuth, async (req, res) => {
+ const userId = req.session.user.id;
+ const { data: fastag } = await supabase.from('fastag_accounts').select('*').eq('user_id', userId).single();
+ const { data: history } = await supabase.from('toll_history').select('*').eq('user_id', userId).order('created_at', { ascending: false }).limit(30);
+ const thisMonth = new Date().toISOString().slice(0, 7);
+ const monthSpend = (history || []).filter(h => h.type === 'toll' && (h.created_at || '').startsWith(thisMonth)).reduce((s, h) => s + (parseFloat(h.amount) || 0), 0);
+ res.render('pages/fastag', { fastag, history: history || [], stats: { balance: fastag?.balance || 0, month_spend: monthSpend } });
+});
+
+router.post('/register', requireAuth, async (req, res) => {
+ const { fastag_number, vehicle_number, issuer_bank } = req.body;
+ await supabase.from('fastag_accounts').upsert([{ user_id: req.session.user.id, fastag_number: (fastag_number || '').trim(), vehicle_number: (vehicle_number || '').toUpperCase().trim(), issuer_bank: issuer_bank || null, balance: 0 }], { onConflict: 'user_id' });
+ res.redirect('/fastag');
+});
+
+router.post('/log-toll', requireAuth, async (req, res) => {
+ const { plaza_name, amount } = req.body;
+ const userId = req.session.user.id;
+ await supabase.from('toll_history').insert([{ user_id: userId, type: 'toll', plaza_name, amount: parseFloat(amount) || 0, status: 'completed' }]);
+ const { data: account } = await supabase.from('fastag_accounts').select('balance').eq('user_id', userId).single();
+ if (account) await supabase.from('fastag_accounts').update({ balance: Math.max(0, (account.balance || 0) - (parseFloat(amount) || 0)) }).eq('user_id', userId);
+ res.redirect('/fastag');
+});
+
+router.post('/recharge', requireAuth, async (req, res) => {
+ const { amount } = req.body;
+ const upi = generateUPILink({ upi_id: process.env.FASTAG_UPI_ID || 'bharathtrucks@upi', amount: parseInt(amount) || 500, note: 'FASTag Recharge' });
+ res.json({ ...upi, amount: parseInt(amount) || 500 });
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/feed.js b/webapp/src/routes/feed.js
new file mode 100644
index 0000000..9ee9f72
--- /dev/null
+++ b/webapp/src/routes/feed.js
@@ -0,0 +1,17 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+
+router.get('/', requireAuth, async (req, res) => {
+ const { data: events } = await supabase.from('feed_events').select('*').order('created_at', { ascending: false }).limit(30);
+ res.render('pages/feed', { events: events || [] });
+});
+
+// Utility to log feed events (called from other routes)
+async function logFeedEvent(type, data) {
+ await supabase.from('feed_events').insert([{ event_type: type, data, created_at: new Date().toISOString() }]).catch(() => {});
+}
+
+module.exports = router;
+module.exports.logFeedEvent = logFeedEvent;
diff --git a/webapp/src/routes/fleet.js b/webapp/src/routes/fleet.js
new file mode 100644
index 0000000..9d96bbe
--- /dev/null
+++ b/webapp/src/routes/fleet.js
@@ -0,0 +1,33 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+
+router.get('/', requireAuth, async (req, res) => {
+ const userId = req.session.user.id;
+ const { data: vehicles } = await supabase.from('fleet_vehicles').select('*').eq('owner_id', userId).order('created_at', { ascending: false });
+ res.render('pages/fleet', { vehicles: vehicles || [] });
+});
+
+router.post('/add', requireAuth, async (req, res) => {
+ const { vehicle_number, vehicle_type, driver_name, driver_phone, capacity_tons } = req.body;
+ await supabase.from('fleet_vehicles').insert([{
+ owner_id: req.session.user.id, vehicle_number: (vehicle_number || '').toUpperCase().trim(),
+ vehicle_type: vehicle_type || 'open', driver_name: driver_name || null,
+ driver_phone: driver_phone || null, capacity_tons: parseFloat(capacity_tons) || 0, status: 'available',
+ }]);
+ res.redirect('/fleet');
+});
+
+router.post('/status/:id', requireAuth, async (req, res) => {
+ const { status } = req.body;
+ await supabase.from('fleet_vehicles').update({ status }).eq('id', req.params.id).eq('owner_id', req.session.user.id);
+ res.redirect('/fleet');
+});
+
+router.post('/delete/:id', requireAuth, async (req, res) => {
+ await supabase.from('fleet_vehicles').delete().eq('id', req.params.id).eq('owner_id', req.session.user.id);
+ res.redirect('/fleet');
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/gamification.js b/webapp/src/routes/gamification.js
new file mode 100644
index 0000000..5fc0dce
--- /dev/null
+++ b/webapp/src/routes/gamification.js
@@ -0,0 +1,40 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+const { getLevelForXP, ACHIEVEMENTS, XP_REWARDS } = require('../lib/gamification');
+
+// Profile score / gamification dashboard
+router.get('/', requireAuth, async (req, res) => {
+ const userId = req.session.user.id;
+ const { data: gam } = await supabase.from('user_gamification').select('*').eq('user_id', userId).single();
+ const xp = gam?.xp || 0;
+ const level = getLevelForXP(xp);
+ const { data: achievements } = await supabase.from('user_achievements').select('achievement_id').eq('user_id', userId);
+ const earned = (achievements || []).map(a => a.achievement_id);
+ const allAchievements = ACHIEVEMENTS.map(a => ({ ...a, earned: earned.includes(a.id) }));
+ res.render('pages/gamification', { level, xp, achievements: allAchievements, streak: gam?.login_streak || 0 });
+});
+
+// Onboarding game
+router.get('/onboarding', requireAuth, async (req, res) => {
+ const userId = req.session.user.id;
+ const { data: gam } = await supabase.from('user_gamification').select('*').eq('user_id', userId).single();
+ if (!gam) await supabase.from('user_gamification').insert([{ user_id: userId, xp: XP_REWARDS.signup, login_streak: 1 }]);
+ res.render('pages/onboarding-game', { xp: gam?.xp || XP_REWARDS.signup, steps_completed: gam?.steps_completed || [] });
+});
+
+// Award XP (internal API)
+router.post('/award', requireAuth, async (req, res) => {
+ const { action } = req.body;
+ const userId = req.session.user.id;
+ const reward = XP_REWARDS[action] || 0;
+ if (!reward) return res.json({ success: false });
+ const { data: gam } = await supabase.from('user_gamification').select('xp').eq('user_id', userId).single();
+ const newXP = (gam?.xp || 0) + reward;
+ await supabase.from('user_gamification').upsert([{ user_id: userId, xp: newXP }], { onConflict: 'user_id' });
+ await supabase.from('xp_log').insert([{ user_id: userId, action, xp_earned: reward }]);
+ res.json({ success: true, xp_earned: reward, total_xp: newXP, level: getLevelForXP(newXP) });
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/invoice.js b/webapp/src/routes/invoice.js
new file mode 100644
index 0000000..fa54c38
--- /dev/null
+++ b/webapp/src/routes/invoice.js
@@ -0,0 +1,38 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+const { generateUPILink } = require('../lib/india');
+
+router.get('/', requireAuth, async (req, res) => {
+ const userId = req.session.user.id;
+ const { data: invoices } = await supabase.from('invoices').select('*').eq('user_id', userId).order('created_at', { ascending: false }).limit(20);
+ res.render('pages/invoices', { invoices: invoices || [] });
+});
+
+router.get('/create', requireAuth, (req, res) => {
+ res.render('pages/invoice-create');
+});
+
+router.post('/create', requireAuth, async (req, res) => {
+ const { client_name, origin, destination, amount, gst_rate, upi_id, notes } = req.body;
+ const amt = parseFloat(amount) || 0;
+ const gst = Math.round(amt * ((parseFloat(gst_rate) || 5) / 100));
+ const total = amt + gst;
+ const invNo = 'BT-' + Date.now().toString(36).toUpperCase();
+ const upi = upi_id ? generateUPILink({ upi_id, amount: total, name: client_name, note: `Invoice ${invNo}` }) : null;
+ await supabase.from('invoices').insert([{
+ user_id: req.session.user.id, invoice_number: invNo, client_name, origin, destination,
+ amount: amt, gst_amount: gst, total_amount: total, gst_rate: parseFloat(gst_rate) || 5,
+ upi_id: upi_id || null, upi_link: upi?.upi_intent || null, notes: notes || null, status: 'unpaid',
+ }]);
+ res.redirect('/invoice');
+});
+
+router.get('/:id', requireAuth, async (req, res) => {
+ const { data: invoice } = await supabase.from('invoices').select('*').eq('id', req.params.id).eq('user_id', req.session.user.id).single();
+ if (!invoice) return res.redirect('/invoice');
+ res.render('pages/invoice-view', { invoice });
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/leaderboard.js b/webapp/src/routes/leaderboard.js
new file mode 100644
index 0000000..8b13aae
--- /dev/null
+++ b/webapp/src/routes/leaderboard.js
@@ -0,0 +1,18 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+const { getLevelForXP } = require('../lib/gamification');
+
+router.get('/', requireAuth, async (req, res) => {
+ const { data: top } = await supabase.from('user_gamification').select('user_id, xp, login_streak').order('xp', { ascending: false }).limit(20);
+ const userIds = (top || []).map(t => t.user_id);
+ const { data: users } = userIds.length ? await supabase.from('app_users').select('id, name, username, role').in('id', userIds) : { data: [] };
+ const userMap = {};
+ (users || []).forEach(u => { userMap[u.id] = u; });
+ const leaderboard = (top || []).map((t, i) => ({ rank: i + 1, ...t, user: userMap[t.user_id] || {}, level: getLevelForXP(t.xp) }));
+ const myRank = leaderboard.findIndex(l => l.user_id === req.session.user.id) + 1;
+ res.render('pages/leaderboard', { leaderboard, myRank });
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/loads.js b/webapp/src/routes/loads.js
index b2c51b4..edaa98c 100644
--- a/webapp/src/routes/loads.js
+++ b/webapp/src/routes/loads.js
@@ -58,6 +58,12 @@ router.post('/post', requireAuth, requireRole(ROLES.SHIPPER, ROLES.BROKER), asyn
if (error) {
return res.render('pages/post-load', { error: 'लोड पोस्ट करने में त्रुटि', truckTypes: TRUCK_TYPES });
}
+
+ // Award XP for posting load
+ await supabase.from('user_gamification').upsert([{ user_id: req.session.user.id, xp: 30 }], { onConflict: 'user_id', ignoreDuplicates: false }).catch(() => {});
+ const { data: gam } = await supabase.from('user_gamification').select('xp').eq('user_id', req.session.user.id).single().catch(() => ({}));
+ if (gam) await supabase.from('user_gamification').update({ xp: (gam.xp || 0) + 30 }).eq('user_id', req.session.user.id).catch(() => {});
+
res.redirect('/loadboard');
});
@@ -95,6 +101,10 @@ router.post('/:id/bid', requireAuth, requireRole(ROLES.DRIVER), async (req, res)
note: note || null,
}, { onConflict: 'load_id,driver_id' });
+ // Award XP for placing bid
+ const { data: gam } = await supabase.from('user_gamification').select('xp').eq('user_id', req.session.user.id).single();
+ if (gam) await supabase.from('user_gamification').update({ xp: (gam.xp || 0) + 20 }).eq('user_id', req.session.user.id).catch(() => {});
+
res.redirect(`/loadboard/${req.params.id}`);
});
diff --git a/webapp/src/routes/maintenance.js b/webapp/src/routes/maintenance.js
new file mode 100644
index 0000000..5244656
--- /dev/null
+++ b/webapp/src/routes/maintenance.js
@@ -0,0 +1,36 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+
+router.get('/', requireAuth, async (req, res) => {
+ const userId = req.session.user.id;
+ const { data: reminders } = await supabase.from('vehicle_reminders').select('*').eq('user_id', userId).order('expiry_date', { ascending: true });
+ const today = new Date();
+ const enriched = (reminders || []).map(r => {
+ const daysLeft = Math.ceil((new Date(r.expiry_date) - today) / 86400000);
+ const urgency = daysLeft < 0 ? 'expired' : daysLeft <= 7 ? 'critical' : daysLeft <= 30 ? 'warning' : 'valid';
+ return { ...r, days_left: daysLeft, urgency };
+ });
+ const stats = { total: enriched.length, expired: enriched.filter(r => r.urgency === 'expired').length, expiring: enriched.filter(r => r.urgency === 'critical' || r.urgency === 'warning').length };
+ res.render('pages/maintenance', { reminders: enriched, stats });
+});
+
+router.post('/add', requireAuth, async (req, res) => {
+ const { vehicle_number, doc_type, expiry_date, notes } = req.body;
+ await supabase.from('vehicle_reminders').insert([{ user_id: req.session.user.id, vehicle_number: (vehicle_number || '').toUpperCase().trim(), doc_type: doc_type || 'insurance', expiry_date, notes: notes || null, status: 'active' }]);
+ res.redirect('/maintenance');
+});
+
+router.post('/delete/:id', requireAuth, async (req, res) => {
+ await supabase.from('vehicle_reminders').delete().eq('id', req.params.id).eq('user_id', req.session.user.id);
+ res.redirect('/maintenance');
+});
+
+router.post('/renew/:id', requireAuth, async (req, res) => {
+ const { new_expiry_date } = req.body;
+ await supabase.from('vehicle_reminders').update({ expiry_date: new_expiry_date }).eq('id', req.params.id).eq('user_id', req.session.user.id);
+ res.redirect('/maintenance');
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/minigames.js b/webapp/src/routes/minigames.js
new file mode 100644
index 0000000..eada05e
--- /dev/null
+++ b/webapp/src/routes/minigames.js
@@ -0,0 +1,52 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+const { ROUTE_DB, FREIGHT_CITIES } = require('../lib/india');
+
+router.get('/', requireAuth, (req, res) => {
+ res.render('pages/games-hub');
+});
+
+// Rate Guesser - guess the freight rate for a route
+router.get('/rate-guesser', requireAuth, async (req, res) => {
+ const { data: loads } = await supabase.from('loads').select('origin_city, destination_city, budget, weight_tons').not('budget', 'is', null).gt('budget', 0).limit(50);
+ const pool = (loads || []).filter(l => l.budget > 5000);
+ const load = pool.length > 0 ? pool[Math.floor(Math.random() * pool.length)] : { origin_city: 'Mumbai', destination_city: 'Delhi', budget: 45000, weight_tons: 15 };
+ res.render('pages/games-rate-guesser', { load, revealed: false });
+});
+
+router.post('/rate-guesser/guess', requireAuth, async (req, res) => {
+ const { guess, actual, origin, destination, weight } = req.body;
+ const g = parseInt(guess) || 0;
+ const a = parseInt(actual) || 0;
+ const diff = Math.abs(g - a);
+ const accuracy = a > 0 ? Math.max(0, 100 - Math.round((diff / a) * 100)) : 0;
+ let xpEarned = accuracy >= 90 ? 25 : accuracy >= 70 ? 15 : accuracy >= 50 ? 10 : 5;
+ const { data: gam } = await supabase.from('user_gamification').select('xp').eq('user_id', req.session.user.id).single();
+ if (gam) await supabase.from('user_gamification').update({ xp: (gam.xp || 0) + xpEarned }).eq('user_id', req.session.user.id);
+ res.render('pages/games-rate-guesser', { load: { origin_city: origin, destination_city: destination, budget: a, weight_tons: weight }, revealed: true, guess: g, accuracy, xpEarned });
+});
+
+// Route Quiz - guess distance between cities
+router.get('/route-quiz', requireAuth, (req, res) => {
+ const keys = Object.keys(ROUTE_DB);
+ const key = keys[Math.floor(Math.random() * keys.length)];
+ const [o, d] = key.split('_');
+ const route = ROUTE_DB[key];
+ res.render('pages/games-route-quiz', { origin: o.charAt(0).toUpperCase() + o.slice(1), destination: d.charAt(0).toUpperCase() + d.slice(1), actual_km: route.km, revealed: false });
+});
+
+router.post('/route-quiz/guess', requireAuth, async (req, res) => {
+ const { guess, actual_km, origin, destination } = req.body;
+ const g = parseInt(guess) || 0;
+ const a = parseInt(actual_km) || 0;
+ const diff = Math.abs(g - a);
+ const accuracy = a > 0 ? Math.max(0, 100 - Math.round((diff / a) * 100)) : 0;
+ let xpEarned = accuracy >= 90 ? 20 : accuracy >= 70 ? 12 : accuracy >= 50 ? 8 : 3;
+ const { data: gam } = await supabase.from('user_gamification').select('xp').eq('user_id', req.session.user.id).single();
+ if (gam) await supabase.from('user_gamification').update({ xp: (gam.xp || 0) + xpEarned }).eq('user_id', req.session.user.id);
+ res.render('pages/games-route-quiz', { origin, destination, actual_km: a, revealed: true, guess: g, accuracy, xpEarned });
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/news.js b/webapp/src/routes/news.js
new file mode 100644
index 0000000..09e8f8c
--- /dev/null
+++ b/webapp/src/routes/news.js
@@ -0,0 +1,19 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+
+router.get('/', requireAuth, async (req, res) => {
+ const { data: news } = await supabase.from('trucker_news').select('*').eq('status', 'published').order('created_at', { ascending: false }).limit(20);
+ res.render('pages/news', { news: news || [] });
+});
+
+// Admin post news
+router.post('/post', requireAuth, async (req, res) => {
+ if (req.session.user.role !== 'admin') return res.redirect('/news');
+ const { title, content, category } = req.body;
+ await supabase.from('trucker_news').insert([{ title, content, category: category || 'general', status: 'published', posted_by: req.session.user.id }]);
+ res.redirect('/news');
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/notifications.js b/webapp/src/routes/notifications.js
new file mode 100644
index 0000000..f7532da
--- /dev/null
+++ b/webapp/src/routes/notifications.js
@@ -0,0 +1,37 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+
+router.get('/', requireAuth, async (req, res) => {
+ const userId = req.session.user.id;
+ const notifications = [];
+ const today = new Date();
+
+ // Overdue payments (for shippers/brokers)
+ if (req.session.user.role === 'shipper' || req.session.user.role === 'broker') {
+ const { data: trips } = await supabase.from('trips').select('*, load:load_id(origin_city, destination_city, budget)').eq('shipper_id', userId).eq('status', 'delivered');
+ (trips || []).forEach(t => {
+ const days = Math.floor((today - new Date(t.created_at)) / 86400000);
+ if (days >= 3) notifications.push({ type: 'payment', icon: '💰', title: `Payment pending ${days} days`, subtitle: `${t.load?.origin_city} → ${t.load?.destination_city} • ₹${t.amount}`, priority: days >= 7 ? 'high' : 'medium', url: `/trips` });
+ });
+ }
+
+ // Bid updates (for drivers)
+ if (req.session.user.role === 'driver') {
+ const { data: bids } = await supabase.from('bids').select('*, load:load_id(origin_city, destination_city, status)').eq('driver_id', userId).eq('status', 'accepted').limit(5);
+ (bids || []).forEach(b => { notifications.push({ type: 'bid', icon: '✅', title: `Bid accepted!`, subtitle: `${b.load?.origin_city} → ${b.load?.destination_city}`, priority: 'high', url: `/loadboard/${b.load_id}` }); });
+ }
+
+ // Maintenance reminders
+ const { data: reminders } = await supabase.from('vehicle_reminders').select('*').eq('user_id', userId).eq('status', 'active').lte('expiry_date', new Date(Date.now() + 15 * 86400000).toISOString().split('T')[0]);
+ (reminders || []).forEach(r => {
+ const days = Math.ceil((new Date(r.expiry_date) - today) / 86400000);
+ notifications.push({ type: 'maintenance', icon: days < 0 ? '🔴' : '🟡', title: `${r.doc_type} ${days < 0 ? 'expired' : 'expiring in ' + days + ' days'}`, subtitle: r.vehicle_number, priority: days < 0 ? 'high' : 'medium', url: '/maintenance' });
+ });
+
+ notifications.sort((a, b) => ({ high: 0, medium: 1, low: 2 }[a.priority] || 2) - ({ high: 0, medium: 1, low: 2 }[b.priority] || 2));
+ res.render('pages/notifications', { notifications });
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/rates.js b/webapp/src/routes/rates.js
new file mode 100644
index 0000000..bb843de
--- /dev/null
+++ b/webapp/src/routes/rates.js
@@ -0,0 +1,21 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+
+router.get('/', requireAuth, async (req, res) => {
+ const { origin, destination } = req.query;
+ let rates = null;
+ if (origin && destination) {
+ const { data: loads } = await supabase.from('loads').select('budget, weight_tons, created_at')
+ .ilike('origin_city', `%${origin}%`).ilike('destination_city', `%${destination}%`)
+ .not('budget', 'is', null).order('created_at', { ascending: false }).limit(20);
+ if (loads && loads.length > 0) {
+ const budgets = loads.map(l => parseFloat(l.budget)).filter(b => b > 0);
+ rates = { origin, destination, count: budgets.length, avg: Math.round(budgets.reduce((a, b) => a + b, 0) / budgets.length), min: Math.min(...budgets), max: Math.max(...budgets), per_ton_avg: Math.round(budgets.reduce((a, b) => a + b, 0) / budgets.length / (loads[0].weight_tons || 1)) };
+ }
+ }
+ res.render('pages/rates', { rates, origin: origin || '', destination: destination || '' });
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/referral.js b/webapp/src/routes/referral.js
new file mode 100644
index 0000000..7f9b1ab
--- /dev/null
+++ b/webapp/src/routes/referral.js
@@ -0,0 +1,25 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+
+router.get('/', requireAuth, async (req, res) => {
+ const userId = req.session.user.id;
+ const code = req.session.user.username || userId.slice(0, 8);
+ const { data: referrals } = await supabase.from('referrals').select('*').eq('referrer_id', userId).order('created_at', { ascending: false });
+ const stats = { total: (referrals || []).length, joined: (referrals || []).filter(r => r.status === 'joined').length };
+ const shareMsg = `🚛 भारत ट्रक्स पर आओ! मुफ्त लोड बोर्ड, ट्रिप ट्रैकर, FASTag। मेरा कोड: ${code}\n👉 https://bharathtrucks.com/register?ref=${code}`;
+ res.render('pages/referral', { code, referrals: referrals || [], stats, shareMsg });
+});
+
+router.post('/track', async (req, res) => {
+ const { referrer_code, new_user_id } = req.body;
+ if (!referrer_code || !new_user_id) return res.json({ success: false });
+ const { data: referrer } = await supabase.from('app_users').select('id').or(`username.eq.${referrer_code},id.ilike.${referrer_code}%`).single();
+ if (referrer) {
+ await supabase.from('referrals').insert([{ referrer_id: referrer.id, referred_user_id: new_user_id, referral_code: referrer_code, status: 'joined' }]);
+ }
+ res.json({ success: true });
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/reports.js b/webapp/src/routes/reports.js
new file mode 100644
index 0000000..79c047c
--- /dev/null
+++ b/webapp/src/routes/reports.js
@@ -0,0 +1,33 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+
+router.get('/', requireAuth, async (req, res) => {
+ const userId = req.session.user.id;
+ const thisMonth = new Date().toISOString().slice(0, 7);
+ const { data: trips } = await supabase.from('trips').select('amount, status, created_at').or(`driver_id.eq.${userId},shipper_id.eq.${userId}`);
+ const { data: ledger } = await supabase.from('driver_ledger').select('freight_received, fuel_cost, toll_cost, other_expense, trip_date').eq('user_id', userId);
+ const allTrips = trips || [];
+ const allLedger = ledger || [];
+ const monthTrips = allTrips.filter(t => (t.created_at || '').startsWith(thisMonth));
+ const monthLedger = allLedger.filter(l => (l.trip_date || '').startsWith(thisMonth));
+ const stats = {
+ total_trips: allTrips.length, month_trips: monthTrips.length,
+ total_revenue: allLedger.reduce((s, l) => s + (parseFloat(l.freight_received) || 0), 0),
+ month_revenue: monthLedger.reduce((s, l) => s + (parseFloat(l.freight_received) || 0), 0),
+ total_expenses: allLedger.reduce((s, l) => s + (parseFloat(l.fuel_cost) || 0) + (parseFloat(l.toll_cost) || 0) + (parseFloat(l.other_expense) || 0), 0),
+ };
+ stats.profit = stats.total_revenue - stats.total_expenses;
+ res.render('pages/reports', { stats, ledger: allLedger });
+});
+
+router.get('/export', requireAuth, async (req, res) => {
+ const userId = req.session.user.id;
+ const { data: ledger } = await supabase.from('driver_ledger').select('*').eq('user_id', userId).order('trip_date', { ascending: false });
+ let csv = 'Date,From,To,Freight,Fuel,Toll,Other,Notes\n';
+ (ledger || []).forEach(l => { csv += `${l.trip_date},${l.origin},${l.destination},${l.freight_received},${l.fuel_cost},${l.toll_cost},${l.other_expense},${(l.notes||'').replace(/,/g,' ')}\n`; });
+ res.set({ 'Content-Type': 'text/csv', 'Content-Disposition': 'attachment; filename=bharathtrucks-report.csv' }).send(csv);
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/returnload.js b/webapp/src/routes/returnload.js
new file mode 100644
index 0000000..fbbb341
--- /dev/null
+++ b/webapp/src/routes/returnload.js
@@ -0,0 +1,30 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth, requireDriver } = require('../middleware/auth');
+
+// View return load page
+router.get('/', requireAuth, requireDriver, async (req, res) => {
+ const userId = req.session.user.id;
+ const { data: availability } = await supabase.from('available_for_return').select('*').eq('user_id', userId).single();
+ const { data: suggestions } = availability ? await supabase.from('loads').select('*').eq('status', 'open').ilike('origin_city', `%${availability.current_city}%`).order('created_at', { ascending: false }).limit(10) : { data: [] };
+ res.render('pages/return-load', { availability, suggestions: suggestions || [] });
+});
+
+// Mark available for return load
+router.post('/available', requireAuth, requireDriver, async (req, res) => {
+ const { current_city, home_city, vehicle_type } = req.body;
+ await supabase.from('available_for_return').upsert([{
+ user_id: req.session.user.id, current_city, home_city: home_city || null,
+ vehicle_type: vehicle_type || null, status: 'looking', updated_at: new Date().toISOString(),
+ }], { onConflict: 'user_id' });
+ res.redirect('/returnload');
+});
+
+// Cancel availability
+router.post('/cancel', requireAuth, requireDriver, async (req, res) => {
+ await supabase.from('available_for_return').update({ status: 'inactive' }).eq('user_id', req.session.user.id);
+ res.redirect('/returnload');
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/safety.js b/webapp/src/routes/safety.js
new file mode 100644
index 0000000..741c99e
--- /dev/null
+++ b/webapp/src/routes/safety.js
@@ -0,0 +1,49 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth, requireDriver } = require('../middleware/auth');
+const { WHATSAPP_TEMPLATES } = require('../lib/india');
+
+router.get('/', requireAuth, requireDriver, async (req, res) => {
+ const userId = req.session.user.id;
+ const { data: contacts } = await supabase.from('safety_contacts').select('*').eq('user_id', userId);
+ const { data: checkins } = await supabase.from('safety_checkins').select('*').eq('user_id', userId).order('created_at', { ascending: false }).limit(10);
+ res.render('pages/safety', { contacts: contacts || [], checkins: checkins || [] });
+});
+
+// Add emergency contact
+router.post('/contacts/add', requireAuth, requireDriver, async (req, res) => {
+ const { contact_name, contact_phone, relationship } = req.body;
+ await supabase.from('safety_contacts').insert([{ user_id: req.session.user.id, contact_name, contact_phone: (contact_phone || '').replace(/\s/g, ''), relationship: relationship || 'family' }]);
+ res.redirect('/safety');
+});
+
+// Delete contact
+router.post('/contacts/delete/:id', requireAuth, async (req, res) => {
+ await supabase.from('safety_contacts').delete().eq('id', req.params.id).eq('user_id', req.session.user.id);
+ res.redirect('/safety');
+});
+
+// Check-in (generates WhatsApp links)
+router.post('/checkin', requireAuth, requireDriver, async (req, res) => {
+ const { location, message } = req.body;
+ const userId = req.session.user.id;
+ const { data: contacts } = await supabase.from('safety_contacts').select('*').eq('user_id', userId);
+ const msg = WHATSAPP_TEMPLATES.safety_checkin({ message: message || 'मैं सुरक्षित हूँ', location });
+ await supabase.from('safety_checkins').insert([{ user_id: userId, location, message: message || 'Safe', is_sos: false }]);
+ const links = (contacts || []).map(c => ({ name: c.contact_name, url: `https://wa.me/${c.contact_phone.replace(/\D/g, '')}?text=${encodeURIComponent(msg)}` }));
+ res.render('pages/safety-sent', { links, message: msg, is_sos: false });
+});
+
+// SOS Emergency
+router.post('/sos', requireAuth, requireDriver, async (req, res) => {
+ const { location, emergency_type } = req.body;
+ const userId = req.session.user.id;
+ const { data: contacts } = await supabase.from('safety_contacts').select('*').eq('user_id', userId);
+ const msg = WHATSAPP_TEMPLATES.sos({ type: emergency_type || 'Emergency', location });
+ await supabase.from('safety_checkins').insert([{ user_id: userId, location, message: `SOS: ${emergency_type}`, is_sos: true }]);
+ const links = (contacts || []).map(c => ({ name: c.contact_name, url: `https://wa.me/${c.contact_phone.replace(/\D/g, '')}?text=${encodeURIComponent(msg)}`, call: `tel:${c.contact_phone}` }));
+ res.render('pages/safety-sent', { links, message: msg, is_sos: true });
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/search.js b/webapp/src/routes/search.js
new file mode 100644
index 0000000..c9d0048
--- /dev/null
+++ b/webapp/src/routes/search.js
@@ -0,0 +1,19 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+const { requireAuth } = require('../middleware/auth');
+
+router.get('/', requireAuth, async (req, res) => {
+ const { q } = req.query;
+ let results = { loads: [], users: [], classifieds: [] };
+ if (q && q.length >= 2) {
+ const term = `%${q}%`;
+ const { data: loads } = await supabase.from('loads').select('id, origin_city, destination_city, budget, status').or(`origin_city.ilike.${term},destination_city.ilike.${term}`).limit(10);
+ const { data: users } = await supabase.from('app_users').select('id, name, username, role').or(`name.ilike.${term},username.ilike.${term}`).limit(10);
+ const { data: classifieds } = await supabase.from('classifieds').select('id, title, price, category').ilike('title', term).eq('status', 'active').limit(10);
+ results = { loads: loads || [], users: users || [], classifieds: classifieds || [] };
+ }
+ res.render('pages/search', { q: q || '', results });
+});
+
+module.exports = router;
diff --git a/webapp/src/routes/sitemap.js b/webapp/src/routes/sitemap.js
new file mode 100644
index 0000000..94571ff
--- /dev/null
+++ b/webapp/src/routes/sitemap.js
@@ -0,0 +1,21 @@
+const express = require('express');
+const router = express.Router();
+const supabase = require('../services/supabase');
+
+router.get('/sitemap.xml', async (req, res) => {
+ const base = `${req.protocol}://${req.get('host')}`;
+ const { data: loads } = await supabase.from('loads').select('id, created_at').eq('status', 'open').order('created_at', { ascending: false }).limit(200);
+ let xml = '
कुछ गलत हो गया। कृपया बाद में पुनः प्रयास करें। | Something went wrong.
- मुख्य पृष्ठ पर जाएं -| रूट | वज़न | बजट | बोली | स्थिति | पोस्टर |
|---|---|---|---|---|---|
| <%= l.origin_city %> → <%= l.destination_city %> | -<%= l.weight_tons %>T | -<%= l.budget ? '₹' + Number(l.budget).toLocaleString('en-IN') : '-' %> | -<%= l.bid_count %> | -<%= l.status %> | -<%= l.poster ? l.poster.name : '-' %> | -
| नाम | यूज़रनेम | भूमिका | स्थिति | कार्रवाई |
|---|---|---|---|---|
| <%= u.name %> | -<%= u.username %> | -<%= u.role %> | -<%= u.is_active ? 'सक्रिय' : 'निलंबित' %> | -- - | -
Complete tasks to earn XP! (<%= completedCount %>/3 done today)
+📭 No entries yet. Add your first trip!
No activity yet. Be the first!
Guess the freight rate:
+⚖️ <%= load.weight_tons %> tons
+ <% if (!revealed) { %> + + <% } else { %> +Your guess: ₹<%= guess.toLocaleString('en-IN') %>
+Actual rate: ₹<%= load.budget.toLocaleString('en-IN') %>
++<%= xpEarned %> XP earned! 🎉
+Guess the distance (km):
+Your guess: <%= guess %> km
+Actual: <%= actual_km %> km
++<%= xpEarned %> XP earned! 🎉
+<%= xp %> XP
+ +<%= level.progress %>% to Level <%= level.level + 1 %>
+🔥 Streak: <%= streak %> days
+<%= invoice.invoice_number %>
📝 <%= invoice.notes %>
<% } %> +No invoices yet.
भारत का राष्ट्रीय माल परिवहन मंच — लोड पोस्ट करें, बोली लगाएं, कमाई करें। बिना किसी शुल्क के।
+<%= t('landing.heroSub') %>
चाहे आप माल भेजें, ट्रक चलाएं, या सौदे कराएं — भारत ट्रक्स आपके लिए है।
+<%= t('landing.onePlatformSub') %>
सिर्फ 4 आसान कदम
+<%= t('landing.howSub') %>
फोन नंबर से मुफ्त अकाउंट बनाएं। अपनी भूमिका चुनें।
शिपर लोड पोस्ट करें। ड्राइवर उपलब्ध लोड देखें।
ड्राइवर अपनी कीमत बताएं। शिपर सबसे अच्छी बोली चुनें।
ट्रिप पूरी करें। UPI से सीधे भुगतान पाएं।
<%= t('landing.step1Desc') %>
<%= t('landing.step2Desc') %>
<%= t('landing.step3Desc') %>
<%= t('landing.step4Desc') %>
1000+ उपयोगकर्ताओं तक सभी सुविधाएं मुफ्त। कोई क्रेडिट कार्ड नहीं चाहिए।
- अभी पंजीकरण करें → +<%= t('landing.ctaSub') %>
+ <%= t('auth.registerBtn') %> →Your Rank: #<%= myRank %>
<% } %> + <% leaderboard.forEach(l => { %> +🏷️ <%= bids.length %> बोली प्राप्त
+ <% if (user && (user.role === 'shipper' || user.role === 'broker') && load.posted_by === user.id && bids && bids.length > 0) { %> +📭
-कोई लोड उपलब्ध नहीं
+<%= t('common.noLoads') %>
अपना यूज़रनेम और पासवर्ड दर्ज करें
+<%= t('auth.loginSubtitle') %>
<% if (error) { %>- नया खाता? पंजीकरण करें + <%= t('auth.noAccount') %> <%= t('actions.register') %>
कोई संदेश नहीं
+<%= t('messages.noMessages') %>
No news yet. Check back later!
<%= n.content %>
<% } %> +✅ All clear! No pending actions.
Complete steps to level up!
+Based on <%= rates.count %> recent loads
+No rate data for this route yet.
मुफ्त खाता बनाएं
+<%= t('auth.registerSubtitle') %>
<% if (error) { %>- पहले से खाता है? लॉगिन करें + <%= t('auth.hasAccount') %> <%= t('actions.login') %>
📭 No loads from your city yet. We'll notify you!
Tap to send via WhatsApp:
+No results for "<%= q %>"
कोई ट्रिप नहीं
+<%= t('trips.noTrips') %>