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 = ''; + xml += `${base}/1.0`; + xml += `${base}/loadboardhourly0.9`; + xml += `${base}/register0.8`; + (loads || []).forEach(l => { xml += `${base}/loadboard/share/${l.id}${l.created_at?.split('T')[0]}`; }); + xml += ''; + res.set('Content-Type', 'application/xml').send(xml); +}); + +router.get('/robots.txt', (req, res) => { + res.type('text/plain').send(`User-agent: *\nAllow: /\nSitemap: ${req.protocol}://${req.get('host')}/sitemap.xml`); +}); + +module.exports = router; diff --git a/webapp/src/routes/tripplanner.js b/webapp/src/routes/tripplanner.js new file mode 100644 index 0000000..6d56465 --- /dev/null +++ b/webapp/src/routes/tripplanner.js @@ -0,0 +1,27 @@ +const express = require('express'); +const router = express.Router(); +const { calculateTripCost, getRouteInfo, getStateFromCity, FREIGHT_CITIES } = require('../lib/india'); + +router.get('/', (req, res) => { + res.render('pages/trip-planner', { result: null, cities: FREIGHT_CITIES }); +}); + +router.post('/calculate', (req, res) => { + const { origin, destination, vehicle_type, mileage, freight_charged } = req.body; + if (!origin || !destination) return res.render('pages/trip-planner', { result: { error: 'Origin and destination required' }, cities: FREIGHT_CITIES }); + + const route = getRouteInfo(origin, destination); + if (!route) return res.render('pages/trip-planner', { result: { error: 'Route not in database. Try major cities.' }, cities: FREIGHT_CITIES }); + + const result = calculateTripCost({ + distance_km: route.km, vehicle_type: vehicle_type || 'truck', + origin_state: getStateFromCity(origin), dest_state: getStateFromCity(destination), + freight_charged: parseFloat(freight_charged) || 0, mileage: parseFloat(mileage) || 4, + }); + result.origin = origin; result.destination = destination; + result.distance_km = route.km; result.hours = route.hours; result.toll_plazas = route.tolls; + + res.render('pages/trip-planner', { result, cities: FREIGHT_CITIES }); +}); + +module.exports = router; diff --git a/webapp/src/routes/trips.js b/webapp/src/routes/trips.js index c4b9f5d..fe2db53 100644 --- a/webapp/src/routes/trips.js +++ b/webapp/src/routes/trips.js @@ -33,6 +33,12 @@ router.post('/:id/status', async (req, res) => { if (trip) await supabase.from('loads').update({ status }).eq('id', trip.load_id); } + // Award XP on delivery + if (status === 'delivered') { + 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) + 40 }).eq('user_id', req.session.user.id).catch(() => {}); + } + res.redirect('/trips'); }); diff --git a/webapp/src/routes/whatsapp.js b/webapp/src/routes/whatsapp.js new file mode 100644 index 0000000..8d579d2 --- /dev/null +++ b/webapp/src/routes/whatsapp.js @@ -0,0 +1,22 @@ +const express = require('express'); +const router = express.Router(); +const supabase = require('../services/supabase'); +const { WHATSAPP_TEMPLATES } = require('../lib/india'); + +// Public share page with OG tags for WhatsApp preview +router.get('/share/:id', async (req, res) => { + const { data: load } = await supabase.from('loads').select('*').eq('id', req.params.id).single(); + if (!load) return res.status(404).render('pages/404'); + res.render('pages/load-share', { load, layout: false }); +}); + +// Generate WhatsApp share link +router.get('/whatsapp/:id', async (req, res) => { + const { data: load } = await supabase.from('loads').select('*').eq('id', req.params.id).single(); + if (!load) return res.status(404).json({ error: 'Not found' }); + const link = `${req.protocol}://${req.get('host')}/loadboard/share/${load.id}`; + const msg = WHATSAPP_TEMPLATES.load_available({ origin: load.origin_city, destination: load.destination_city, budget: load.budget, truck_type: load.truck_type, weight: load.weight_tons, link }); + res.redirect(`https://wa.me/?text=${encodeURIComponent(msg)}`); +}); + +module.exports = router; diff --git a/webapp/src/server.js b/webapp/src/server.js index f96450d..eecf229 100644 --- a/webapp/src/server.js +++ b/webapp/src/server.js @@ -38,8 +38,8 @@ app.set('views', path.join(__dirname, 'views')); // Session app.use(session({ secret: config.session.secret, - resave: false, - saveUninitialized: false, + resave: true, + saveUninitialized: true, cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 }, })); @@ -48,25 +48,95 @@ app.use((req, res, next) => { res.locals.user = req.session.user || null; res.locals.appName = 'भारत ट्रक्स'; res.locals.appNameEn = 'BharathTrucks'; + res.locals.formatINR = require('./lib/india').formatINR; next(); }); +// i18n +const { i18n, LANGS } = require('./middleware/i18n'); +app.use(i18n); +app.get('/lang/:code', (req, res) => { + const code = req.params.code; + if (LANGS.includes(code)) req.session.lang = code; + req.session.save(() => { + res.redirect(req.get('Referer') || '/'); + }); +}); + // Routes const authRoutes = require('./routes/auth'); const loadRoutes = require('./routes/loads'); const tripRoutes = require('./routes/trips'); const adminRoutes = require('./routes/admin'); const messageRoutes = require('./routes/messages'); + +// Phase 1 routes +const whatsappRoutes = require('./routes/whatsapp'); +const driverLedgerRoutes = require('./routes/driver-ledger'); +const tripplannerRoutes = require('./routes/tripplanner'); +const returnloadRoutes = require('./routes/returnload'); +const safetyRoutes = require('./routes/safety'); +const maintenanceRoutes = require('./routes/maintenance'); +const fastagRoutes = require('./routes/fastag'); +const notificationsRoutes = require('./routes/notifications'); + +// Phase 2 routes +const gamificationRoutes = require('./routes/gamification'); +const referralRoutes = require('./routes/referral'); +const feedRoutes = require('./routes/feed'); +const leaderboardRoutes = require('./routes/leaderboard'); +const challengesRoutes = require('./routes/challenges'); +const invoiceRoutes = require('./routes/invoice'); +const ratesRoutes = require('./routes/rates'); +const sitemapRoutes = require('./routes/sitemap'); + app.use('/', authRoutes); app.use('/loadboard', loadRoutes); +app.use('/loadboard', whatsappRoutes); app.use('/trips', tripRoutes); app.use('/admin', adminRoutes); app.use('/messages', messageRoutes); +app.use('/driver', driverLedgerRoutes); +app.use('/trip-planner', tripplannerRoutes); +app.use('/returnload', returnloadRoutes); +app.use('/safety', safetyRoutes); +app.use('/maintenance', maintenanceRoutes); +app.use('/fastag', fastagRoutes); +app.use('/notifications', notificationsRoutes); + +// Phase 2 +app.use('/gamification', gamificationRoutes); +app.use('/referral', referralRoutes); +app.use('/feed', feedRoutes); +app.use('/leaderboard', leaderboardRoutes); +app.use('/challenges', challengesRoutes); +app.use('/invoice', invoiceRoutes); +app.use('/rates', ratesRoutes); +app.use('/', sitemapRoutes); + +// Phase 3 +const minigamesRoutes = require('./routes/minigames'); +const fleetRoutes = require('./routes/fleet'); +const classifiedsRoutes = require('./routes/classifieds'); +const documentsRoutes = require('./routes/documents'); +const bankRoutes = require('./routes/bank'); +const searchRoutes = require('./routes/search'); +const reportsRoutes = require('./routes/reports'); +const newsRoutes = require('./routes/news'); +app.use('/games', minigamesRoutes); +app.use('/fleet', fleetRoutes); +app.use('/classifieds', classifiedsRoutes); +app.use('/documents', documentsRoutes); +app.use('/bank', bankRoutes); +app.use('/search', searchRoutes); +app.use('/reports', reportsRoutes); +app.use('/news', newsRoutes); const { requireAuth, requireDriver, requireShipper, requireBroker } = require('./middleware/auth'); const supabase = require('./services/supabase'); app.get('/health', (req, res) => res.json({ status: 'ok', ts: Date.now() })); +app.get('/more', requireAuth, (req, res) => res.render('pages/more')); app.get('/', (req, res) => { if (req.session && req.session.user) { const { ROLES } = require('./config/constants'); diff --git a/webapp/src/views/pages/404.ejs b/webapp/src/views/pages/404.ejs index 8970631..9ca6252 100644 --- a/webapp/src/views/pages/404.ejs +++ b/webapp/src/views/pages/404.ejs @@ -1,9 +1,6 @@ -<% var title = '404 - पृष्ठ नहीं मिला'; %> +<% var title = '404'; %> <%- include('../partials/header') %> -
-
-

404

-

यह पृष्ठ उपलब्ध नहीं है। | Page not found.

- मुख्य पृष्ठ पर जाएं -
+
+

404

Page not found

🏠 Home
+
<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/500.ejs b/webapp/src/views/pages/500.ejs index 74cc930..c97a255 100644 --- a/webapp/src/views/pages/500.ejs +++ b/webapp/src/views/pages/500.ejs @@ -1,9 +1,6 @@ -<% var title = '500 - सर्वर त्रुटि'; %> +<% var title = 'Error'; %> <%- include('../partials/header') %> -
-
-

500

-

कुछ गलत हो गया। कृपया बाद में पुनः प्रयास करें। | Something went wrong.

- मुख्य पृष्ठ पर जाएं -
+
+

500

Something went wrong

🏠 Home
+
<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/admin-dashboard.ejs b/webapp/src/views/pages/admin-dashboard.ejs index a0f1837..df98ada 100644 --- a/webapp/src/views/pages/admin-dashboard.ejs +++ b/webapp/src/views/pages/admin-dashboard.ejs @@ -1,46 +1,21 @@ -<% var title = 'एडमिन पैनल'; %> +<% var title = 'Admin Dashboard'; %> <%- include('../partials/header') %>
-
-

🏛️ एडमिन पैनल

- -
-
<%= stats.users %>
कुल उपयोगकर्ता
-
<%= stats.loads %>
कुल लोड
-
<%= stats.bids %>
कुल बोलियाँ
-
<%= stats.trips %>
कुल ट्रिप
+

🏛️ Admin Dashboard

+
+
<%= stats.totalUsers %>
👤 Users
+
<%= stats.totalLoads %>
📦 Loads
+
<%= stats.totalTrips %>
🚛 Trips
+
<%= stats.totalBids %>
🏷️ Bids
- -
-
<%= roles.driver %>
🚛 ड्राइवर
-
<%= roles.shipper %>
📦 शिपर
-
<%= roles.broker %>
🤝 ब्रोकर
-
- - <% if (recentUsers.length > 0) { %> -

नए उपयोगकर्ता

-
- - - <% recentUsers.forEach(u => { %> - - - - - - - <% }) %> -
नामयूज़रनेमभूमिकातारीख
<%= u.name %><%= u.username %><%= u.role %><%= new Date(u.created_at).toLocaleDateString('hi-IN') %>
-
- <% } %> - -
- 👥 उपयोगकर्ता - 📋 लोड +
+ 👥Users + 📦Loads + 📰News + 📊Reports
- <%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/admin-loads.ejs b/webapp/src/views/pages/admin-loads.ejs index 63ccd3e..6039a38 100644 --- a/webapp/src/views/pages/admin-loads.ejs +++ b/webapp/src/views/pages/admin-loads.ejs @@ -1,30 +1,17 @@ -<% var title = 'सभी लोड — एडमिन'; %> +<% var title = 'Admin - Loads'; %> <%- include('../partials/header') %>
-
-
-

📋 सभी लोड (<%= loads.length %>)

- ← एडमिन -
- -
- - - <% loads.forEach(l => { %> - - - - - - - - - <% }) %> -
रूटवज़नबजटबोलीस्थितिपोस्टर
<%= 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 : '-' %>
-
+

📦 All Loads (<%= loads.length %>)

+ <% loads.forEach(l => { %> + +
+
<%= l.origin_city %> → <%= l.destination_city %>
<%= l.weight_tons %> tons | <%= l.truck_type %>
+ <%= l.status %> +
+
+ <% }) %>
- <%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/admin-users.ejs b/webapp/src/views/pages/admin-users.ejs index 8464ab6..49a73ec 100644 --- a/webapp/src/views/pages/admin-users.ejs +++ b/webapp/src/views/pages/admin-users.ejs @@ -1,44 +1,15 @@ -<% var title = 'उपयोगकर्ता प्रबंधन'; %> +<% var title = 'Admin - Users'; %> <%- include('../partials/header') %>
-
-
-

👥 उपयोगकर्ता (<%= users.length %>)

- ← एडमिन -
- -
- - - -
- -
- - - <% users.forEach(u => { %> - - - - - - - - <% }) %> -
नामयूज़रनेमभूमिकास्थितिकार्रवाई
<%= u.name %><%= u.username %><%= u.role %><%= u.is_active ? 'सक्रिय' : 'निलंबित' %> -
- -
-
+

👥 All Users (<%= users.length %>)

+ <% users.forEach(u => { %> +
+
<%= u.name || u.username %>
@<%= u.username %> | <%= u.phone || '' %>
+ <%= u.role %>
+ <% }) %>
- <%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/bank.ejs b/webapp/src/views/pages/bank.ejs new file mode 100644 index 0000000..e447db8 --- /dev/null +++ b/webapp/src/views/pages/bank.ejs @@ -0,0 +1,26 @@ +<% var title = 'Bank'; %> +<%- include('../partials/header') %> +
+
+
+

🏦 Bank Accounts

+ <% accounts.forEach(a => { %> +
+
<%= a.bank_name %>
<%= a.account_holder || '' %> | <%= a.upi_id || a.account_number || '' %>
+
+
+ <% }) %> +
+

➕ Add Account

+
+ + + + + + +
+
+
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/broker-dashboard.ejs b/webapp/src/views/pages/broker-dashboard.ejs index 371d550..75abb58 100644 --- a/webapp/src/views/pages/broker-dashboard.ejs +++ b/webapp/src/views/pages/broker-dashboard.ejs @@ -1,25 +1,25 @@ -<% var title = 'ब्रोकर डैशबोर्ड'; %> +<% var title = t('dashboard.brokerTitle'); %> <%- include('../partials/header') %>
-

🤝 नमस्ते, <%= user.name %>!

+

🤝 <%= t('dashboard.hello') %>, <%= user.name %>!

-
<%= stats.totalLoads %>
लोड पोस्ट
-
<%= stats.bookedLoads %>
सौदे
-
<%= stats.activeTrips %>
सक्रिय
+
<%= stats.totalLoads %>
<%= t('dashboard.loadsPosted') %>
+
<%= stats.bookedLoads %>
<%= t('dashboard.deals') %>
+
<%= stats.activeTrips %>
<%= t('dashboard.activeTrips') %>
<% if (recentLoads.length > 0) { %> -

📋 हाल के लोड

+

📋 <%= t('dashboard.recentLoads') %>

<% recentLoads.forEach(load => { %>
<%= load.origin_city %> → <%= load.destination_city %> -
<%= load.weight_tons %> टन | 🏷️ <%= load.bid_count %> बोली
+
<%= load.weight_tons %> <%= t('common.tons') %> | 🏷️ <%= load.bid_count %> <%= t('common.bids') %>
<%= load.status %>
@@ -27,9 +27,39 @@ <% }) %> <% } %> -
- + लोड पोस्ट करें - 📋 लोड बोर्ड +
+ + + <%= t('actions.postLoad') %> + + + 📋 + <%= t('actions.viewLoads') %> + + + 🧾 + Invoice + + + 📊 + Rates + + + 🚛 + Fleet + + + 🛒 + Buy/Sell + + + 🤝 + Referral + + + 🔔 + Alerts +
diff --git a/webapp/src/views/pages/challenges.ejs b/webapp/src/views/pages/challenges.ejs new file mode 100644 index 0000000..4acb0f2 --- /dev/null +++ b/webapp/src/views/pages/challenges.ejs @@ -0,0 +1,30 @@ +<% var title = 'Challenges'; %> +<%- include('../partials/header') %> +
+
+
+

🎯 Daily Challenges

+

Complete tasks to earn XP! (<%= completedCount %>/3 done today)

+
+ <% challenges.forEach(c => { %> +
+
+
+ <%= c.icon %> +
+ <%= c.title_hi %> +
<%= c.title %> • +<%= c.xp %> XP
+
+
+ <% if (c.completed) { %> + + <% } else { %> +
+ <% } %> +
+
+ <% }) %> +
+
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/chat.ejs b/webapp/src/views/pages/chat.ejs index 68a298a..cd3ce4d 100644 --- a/webapp/src/views/pages/chat.ejs +++ b/webapp/src/views/pages/chat.ejs @@ -1,29 +1,24 @@ -<% var title = otherUser.name + ' — चैट'; %> +<% var title = t('nav.messages'); %> <%- include('../partials/header') %>
- -
-
-
- - <%= otherUser.name %> - @<%= otherUser.username %> -
- -
- <% messages.forEach(m => { %> -
- <%= m.content %> -
<%= new Date(m.created_at).toLocaleTimeString('hi-IN', {hour:'2-digit',minute:'2-digit'}) %>
-
+
+
+ ← <%= t('nav.messages') %> +

💬 <%= otherUser.name || otherUser.username %>

+
+ <% messages.forEach(m => { const isMine = m.sender_id === user.id; %> +
+ <%= m.content %> +
<%= new Date(m.created_at).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'}) %>
+
<% }) %>
- -
- - + +
+ + +
- <%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/classifieds-post.ejs b/webapp/src/views/pages/classifieds-post.ejs new file mode 100644 index 0000000..11b8993 --- /dev/null +++ b/webapp/src/views/pages/classifieds-post.ejs @@ -0,0 +1,22 @@ +<% var title = 'Post Ad'; %> +<%- include('../partials/header') %> +
+
+
+

📝 Post Ad

+
+
+ + +
+ + +
+ + + +
+
+
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/classifieds.ejs b/webapp/src/views/pages/classifieds.ejs new file mode 100644 index 0000000..fbbd9bb --- /dev/null +++ b/webapp/src/views/pages/classifieds.ejs @@ -0,0 +1,28 @@ +<% var title = 'Buy/Sell'; %> +<%- include('../partials/header') %> +
+
+
+
+

🛒 Buy / Sell

+ + Post +
+ + <% if (listings.length === 0) { %> +

No listings yet.

+ <% } else { listings.forEach(l => { %> +
+
+
<%= l.title %>
📍 <%= l.location || '' %> | <%= l.category %>
+
₹<%= (l.price||0).toLocaleString('en-IN') %>
+
+
+ <% }) } %> +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/documents.ejs b/webapp/src/views/pages/documents.ejs new file mode 100644 index 0000000..1751621 --- /dev/null +++ b/webapp/src/views/pages/documents.ejs @@ -0,0 +1,25 @@ +<% var title = 'Documents'; %> +<%- include('../partials/header') %> +
+
+
+

📄 Document Vault

+ <% documents.forEach(d => { %> +
+
<%= d.doc_type.toUpperCase() %> — <%= d.vehicle_number %>
<%= d.doc_number || '' %> <% if(d.expiry_date){%>| Exp: <%= d.expiry_date %><%}%>
+
+
+ <% }) %> +
+

➕ Add Document

+
+ + + + + +
+
+
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/driver-dashboard.ejs b/webapp/src/views/pages/driver-dashboard.ejs index 5c9da54..eb5684e 100644 --- a/webapp/src/views/pages/driver-dashboard.ejs +++ b/webapp/src/views/pages/driver-dashboard.ejs @@ -1,19 +1,19 @@ -<% var title = 'ड्राइवर डैशबोर्ड'; %> +<% var title = 'Driver Dashboard'; %> <%- include('../partials/header') %>
-

🚛 नमस्ते, <%= user.name %>!

+

🚛 <%= t('dashboard.hello') %>, <%= user.name %>!

-
<%= stats.totalTrips %>
कुल ट्रिप
-
<%= stats.activeBids %>
सक्रिय बोलियाँ
-
₹<%= stats.earnings.toLocaleString('en-IN') %>
कमाई
+
<%= stats.totalTrips %>
<%= t('dashboard.totalTrips') %>
+
<%= stats.activeBids %>
<%= t('dashboard.activeBids') %>
+
₹<%= stats.earnings.toLocaleString('en-IN') %>
<%= t('dashboard.earnings') %>
<% if (activeTrips.length > 0) { %> -

🔄 सक्रिय ट्रिप

+

🔄 <%= t('dashboard.activeTrips') %>

<% activeTrips.forEach(trip => { %>
diff --git a/webapp/src/views/pages/driver-ledger-add.ejs b/webapp/src/views/pages/driver-ledger-add.ejs new file mode 100644 index 0000000..e6e3e9e --- /dev/null +++ b/webapp/src/views/pages/driver-ledger-add.ejs @@ -0,0 +1,25 @@ +<% var title = 'Add Trip'; %> +<%- include('../partials/header') %> +
+
+
+

➕ Add Trip to Ledger

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ ← Back +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/driver-ledger.ejs b/webapp/src/views/pages/driver-ledger.ejs new file mode 100644 index 0000000..44e6e90 --- /dev/null +++ b/webapp/src/views/pages/driver-ledger.ejs @@ -0,0 +1,34 @@ +<% var title = 'My Ledger'; %> +<%- include('../partials/header') %> +
+
+
+
+

📒 My Ledger

+ + Add Trip +
+
+
<%= stats.total_trips %>
🚛 Trips
+
₹<%= stats.total_earned.toLocaleString('en-IN') %>
💰 Earned
+
₹<%= stats.total_expenses.toLocaleString('en-IN') %>
💸 Expenses
+
₹<%= stats.net_profit.toLocaleString('en-IN') %>
📊 Profit
+
+ <% if (entries.length === 0) { %> +

📭 No entries yet. Add your first trip!

+ <% } else { entries.forEach(e => { %> +
+
+
+ 📍 <%= e.origin %> → <%= e.destination %> +
📅 <%= e.trip_date %> | ⛽ ₹<%= (e.fuel_cost||0).toLocaleString('en-IN') %> | 🚧 ₹<%= (e.toll_cost||0).toLocaleString('en-IN') %>
+
+
+
+₹<%= (e.freight_received||0).toLocaleString('en-IN') %>
+
+
+
+
+ <% }) } %> +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/fastag.ejs b/webapp/src/views/pages/fastag.ejs new file mode 100644 index 0000000..02b3196 --- /dev/null +++ b/webapp/src/views/pages/fastag.ejs @@ -0,0 +1,40 @@ +<% var title = 'FASTag'; %> +<%- include('../partials/header') %> +
+
+
+

🏷️ FASTag & Tolls

+ <% if (!fastag) { %> +
+

Register FASTag

+
+ + + + +
+
+ <% } else { %> +
+
₹<%= stats.balance.toLocaleString('en-IN') %>
💳 Balance
+
₹<%= stats.month_spend.toLocaleString('en-IN') %>
📅 This Month
+
+
+

🚧 Log Toll

+
+ + + +
+
+

📋 Recent

+ <% history.slice(0,15).forEach(h => { %> +
+
<%= h.type==='toll'?'🚧':'💳' %> <%= h.plaza_name || h.type %>
<%= new Date(h.created_at).toLocaleDateString('en-IN') %>
+ <%= h.type==='toll'?'-':'+' %>₹<%= (h.amount||0).toLocaleString('en-IN') %> +
+ <% }) %> + <% } %> +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/feed.ejs b/webapp/src/views/pages/feed.ejs new file mode 100644 index 0000000..d1a1833 --- /dev/null +++ b/webapp/src/views/pages/feed.ejs @@ -0,0 +1,23 @@ +<% var title = 'Activity Feed'; %> +<%- include('../partials/header') %> +
+
+
+

📰 Activity Feed

+ <% if (events.length === 0) { %> +

No activity yet. Be the first!

+ <% } else { events.forEach(e => { const d = e.data || {}; %> +
+
+ <%= e.event_type==='bid_placed'?'🏷️':e.event_type==='load_posted'?'📦':e.event_type==='trip_completed'?'✅':'📣' %> +
+ <%= d.title || e.event_type %> + <% if (d.subtitle) { %>
<%= d.subtitle %>
<% } %> +
<%= new Date(e.created_at).toLocaleString('en-IN') %>
+
+
+
+ <% }) } %> +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/fleet.ejs b/webapp/src/views/pages/fleet.ejs new file mode 100644 index 0000000..4f0212f --- /dev/null +++ b/webapp/src/views/pages/fleet.ejs @@ -0,0 +1,33 @@ +<% var title = 'Fleet'; %> +<%- include('../partials/header') %> +
+
+
+

🚛 My Fleet

+ <% vehicles.forEach(v => { %> +
+
+
+ <%= v.vehicle_number %> — <%= v.vehicle_type %> +
<%= v.driver_name || 'No driver' %> | <%= v.capacity_tons %> tons
+
+ <%= v.status %> +
+
+ <% }) %> +
+

➕ Add Vehicle

+
+ + +
+ + +
+ + +
+
+
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/games-hub.ejs b/webapp/src/views/pages/games-hub.ejs new file mode 100644 index 0000000..642f0d3 --- /dev/null +++ b/webapp/src/views/pages/games-hub.ejs @@ -0,0 +1,15 @@ +<% var title = 'Games'; %> +<%- include('../partials/header') %> +
+
+ +
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/games-rate-guesser.ejs b/webapp/src/views/pages/games-rate-guesser.ejs new file mode 100644 index 0000000..33d56ef --- /dev/null +++ b/webapp/src/views/pages/games-rate-guesser.ejs @@ -0,0 +1,32 @@ +<% var title = 'Rate Guesser'; %> +<%- include('../partials/header') %> +
+
+
+

💰 Rate Guesser

+
+

Guess the freight rate:

+

📍 <%= load.origin_city %> → <%= load.destination_city %>

+

⚖️ <%= load.weight_tons %> tons

+ <% if (!revealed) { %> +
+ + + + + + +
+ <% } else { %> +
+

Your guess: ₹<%= guess.toLocaleString('en-IN') %>

+

Actual rate: ₹<%= load.budget.toLocaleString('en-IN') %>

+
<%= accuracy %>% accurate
+

+<%= xpEarned %> XP earned! 🎉

+
+ Play Again + <% } %> +
+
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/games-route-quiz.ejs b/webapp/src/views/pages/games-route-quiz.ejs new file mode 100644 index 0000000..7f19e46 --- /dev/null +++ b/webapp/src/views/pages/games-route-quiz.ejs @@ -0,0 +1,30 @@ +<% var title = 'Route Quiz'; %> +<%- include('../partials/header') %> +
+
+
+

🗺️ Route Quiz

+
+

Guess the distance (km):

+

📍 <%= origin %> → <%= destination %>

+ <% if (!revealed) { %> +
+ + + + + +
+ <% } else { %> +
+

Your guess: <%= guess %> km

+

Actual: <%= actual_km %> km

+
<%= accuracy %>% accurate
+

+<%= xpEarned %> XP earned! 🎉

+
+ Play Again + <% } %> +
+
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/gamification.ejs b/webapp/src/views/pages/gamification.ejs new file mode 100644 index 0000000..2142647 --- /dev/null +++ b/webapp/src/views/pages/gamification.ejs @@ -0,0 +1,28 @@ +<% var title = 'My Level'; %> +<%- include('../partials/header') %> +
+
+
+
+
<%= level.icon %>
+

Level <%= level.level %> — <%= level.title %>

+

<%= xp %> XP

+
+
+
+

<%= level.progress %>% to Level <%= level.level + 1 %>

+

🔥 Streak: <%= streak %> days

+
+

🏆 Achievements

+
+ <% achievements.forEach(a => { %> +
+
<%= a.icon %>
+
<%= a.title %>
+
+<%= a.xp %> XP
+
+ <% }) %> +
+
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/invoice-create.ejs b/webapp/src/views/pages/invoice-create.ejs new file mode 100644 index 0000000..454b7af --- /dev/null +++ b/webapp/src/views/pages/invoice-create.ejs @@ -0,0 +1,25 @@ +<% var title = 'New Invoice'; %> +<%- include('../partials/header') %> +
+
+
+

🧾 Create Invoice

+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/invoice-view.ejs b/webapp/src/views/pages/invoice-view.ejs new file mode 100644 index 0000000..1ff640b --- /dev/null +++ b/webapp/src/views/pages/invoice-view.ejs @@ -0,0 +1,25 @@ +<% var title = 'Invoice ' + invoice.invoice_number; %> +<%- include('../partials/header') %> +
+
+
+
+

🧾 INVOICE

<%= invoice.invoice_number %>

+
+
Client: <%= invoice.client_name %>
+
Route: <%= invoice.origin %> → <%= invoice.destination %>
+
+
Amount:₹<%= (invoice.amount||0).toLocaleString('en-IN') %>
+
GST (<%= invoice.gst_rate %>%):₹<%= (invoice.gst_amount||0).toLocaleString('en-IN') %>
+
+
Total:₹<%= (invoice.total_amount||0).toLocaleString('en-IN') %>
+
+ <% if (invoice.upi_link) { %> + 💳 Pay via UPI + <% } %> + <% if (invoice.notes) { %>

📝 <%= invoice.notes %>

<% } %> +
+ ← Back +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/invoices.ejs b/webapp/src/views/pages/invoices.ejs new file mode 100644 index 0000000..7fed828 --- /dev/null +++ b/webapp/src/views/pages/invoices.ejs @@ -0,0 +1,28 @@ +<% var title = 'Invoices'; %> +<%- include('../partials/header') %> +
+
+
+
+

🧾 Invoices

+ + New +
+ <% if (invoices.length === 0) { %> +

No invoices yet.

+ <% } else { invoices.forEach(inv => { %> + +
+
+ <%= inv.invoice_number %> — <%= inv.client_name %> +
<%= inv.origin %> → <%= inv.destination %>
+
+
+
₹<%= (inv.total_amount||0).toLocaleString('en-IN') %>
+ <%= inv.status %> +
+
+
+ <% }) } %> +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/landing.ejs b/webapp/src/views/pages/landing.ejs index 0c40ea9..e61d01a 100644 --- a/webapp/src/views/pages/landing.ejs +++ b/webapp/src/views/pages/landing.ejs @@ -1,41 +1,41 @@ -<% var title = 'राष्ट्रीय माल परिवहन मंच'; %> +<% var title = t('common.subtitle'); %> <%- include('../partials/header') %>
-
🇮🇳 भारत सरकार पंजीकृत मंच | Registered Platform
-

ट्रक ड्राइवर। शिपर। ब्रोकर।
सबके लिए मुफ्त।

-

भारत का राष्ट्रीय माल परिवहन मंच — लोड पोस्ट करें, बोली लगाएं, कमाई करें। बिना किसी शुल्क के।

+
🇮🇳 <%= t('landing.badge') %>
+

<%= t('landing.heroTitle') %>
<%= t('landing.heroHighlight') %>

+

<%= t('landing.heroSub') %>

-
मुफ्त
हमेशा के लिए
-
30 सेकंड
पंजीकरण
-
5 मिनट
पहली बोली
+
<%= t('landing.free') %>
<%= t('landing.forever') %>
+
30 <%= t('landing.seconds') %>
<%= t('actions.register') %>
+
5 <%= t('landing.minutes') %>
<%= t('landing.firstBid') %>
-

एक मंच। तीन उपयोगकर्ता।

-

चाहे आप माल भेजें, ट्रक चलाएं, या सौदे कराएं — भारत ट्रक्स आपके लिए है।

+

<%= t('landing.onePlatform') %>

+

<%= t('landing.onePlatformSub') %>

🚛
-

ट्रक ड्राइवर

-
  • लोड खोजें और बोली लगाएं
  • खाली वापसी से बचें
  • कमाई का हिसाब रखें
  • सीधे शिपर से जुड़ें
+

<%= t('auth.driver') %>

+
  • <%= t('landing.driverF1') %>
  • <%= t('landing.driverF2') %>
  • <%= t('landing.driverF3') %>
  • <%= t('landing.driverF4') %>
📦
-

शिपर / माल भेजने वाले

-
  • लोड पोस्ट करें, बोली पाएं
  • सत्यापित ड्राइवर चुनें
  • माल की स्थिति जानें
  • भुगतान का रिकॉर्ड रखें
+

<%= t('auth.shipper') %>

+
  • <%= t('landing.shipperF1') %>
  • <%= t('landing.shipperF2') %>
  • <%= t('landing.shipperF3') %>
  • <%= t('landing.shipperF4') %>
🤝
-

ब्रोकर / एजेंट

-
  • अपने नेटवर्क को डिजिटल करें
  • कमीशन ट्रैक करें
  • शिपर के लिए लोड पोस्ट करें
  • ड्राइवर नेटवर्क बढ़ाएं
+

<%= t('auth.broker') %>

+
  • <%= t('landing.brokerF1') %>
  • <%= t('landing.brokerF2') %>
  • <%= t('landing.brokerF3') %>
  • <%= t('landing.brokerF4') %>
@@ -43,34 +43,34 @@
-

कैसे काम करता है?

-

सिर्फ 4 आसान कदम

+

<%= t('landing.howTitle') %>

+

<%= t('landing.howSub') %>

-

पंजीकरण करें

फोन नंबर से मुफ्त अकाउंट बनाएं। अपनी भूमिका चुनें।

-

लोड पोस्ट / खोजें

शिपर लोड पोस्ट करें। ड्राइवर उपलब्ध लोड देखें।

-

बोली लगाएं / स्वीकार करें

ड्राइवर अपनी कीमत बताएं। शिपर सबसे अच्छी बोली चुनें।

-

माल पहुँचाएं, भुगतान पाएं

ट्रिप पूरी करें। UPI से सीधे भुगतान पाएं।

+

<%= t('landing.step1') %>

<%= t('landing.step1Desc') %>

+

<%= t('landing.step2') %>

<%= t('landing.step2Desc') %>

+

<%= t('landing.step3') %>

<%= t('landing.step3Desc') %>

+

<%= t('landing.step4') %>

<%= t('landing.step4Desc') %>

-

क्यों भारत ट्रक्स?

+

<%= t('landing.whyTitle') %>

-
₹0
कोई शुल्क नहीं
-
🔒
सुरक्षित मंच
-
📱
मोबाइल पर चलता है
-
🇮🇳
भारत के लिए बना
+
₹0
<%= t('landing.noFee') %>
+
🔒
<%= t('landing.secure') %>
+
📱
<%= t('landing.mobile') %>
+
🇮🇳
<%= t('landing.madeInIndia') %>
-

आज ही शुरू करें — बिल्कुल मुफ्त!

-

1000+ उपयोगकर्ताओं तक सभी सुविधाएं मुफ्त। कोई क्रेडिट कार्ड नहीं चाहिए।

- अभी पंजीकरण करें → +

<%= t('landing.ctaTitle') %>

+

<%= t('landing.ctaSub') %>

+ <%= t('auth.registerBtn') %> →
diff --git a/webapp/src/views/pages/leaderboard.ejs b/webapp/src/views/pages/leaderboard.ejs new file mode 100644 index 0000000..594ccc6 --- /dev/null +++ b/webapp/src/views/pages/leaderboard.ejs @@ -0,0 +1,20 @@ +<% var title = 'Leaderboard'; %> +<%- include('../partials/header') %> +
+
+
+

🏆 Leaderboard

+ <% if (myRank) { %>

Your Rank: #<%= myRank %>

<% } %> + <% leaderboard.forEach(l => { %> +
+
<%= l.rank <= 3 ? ['🥇','🥈','🥉'][l.rank-1] : '#'+l.rank %>
+
+ <%= l.user?.name || l.user?.username || 'User' %> +
<%= l.level.icon %> Level <%= l.level.level %> • <%= l.user?.role || '' %>
+
+
<%= l.xp %> XP
+
+ <% }) %> +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/load-detail.ejs b/webapp/src/views/pages/load-detail.ejs index 19164f4..25ec181 100644 --- a/webapp/src/views/pages/load-detail.ejs +++ b/webapp/src/views/pages/load-detail.ejs @@ -1,89 +1,65 @@ <% var title = load.origin_city + ' → ' + load.destination_city; %> <%- include('../partials/header') %>
-
- ← लोड बोर्ड पर वापस - + ← <%= t('actions.viewLoads') %>

📍 <%= load.origin_city %> → <%= load.destination_city %>

<%= load.status %>
-
-
वज़न
<%= load.weight_tons %> टन
-
ट्रक
<%= load.truck_type %>
-
पिकअप
<%= new Date(load.pickup_date).toLocaleDateString('hi-IN') %>
-
बजट
<%= load.budget ? '₹' + Number(load.budget).toLocaleString('en-IN') : 'बताया नहीं' %>
+
⚖️ <%= t('postLoad.weight') %>
<%= load.weight_tons %> <%= t('common.tons') %>
+
🚛 <%= t('common.truckType') %>
<%= load.truck_type %>
+
📅 <%= t('postLoad.pickupDate') %>
<%= new Date(load.pickup_date).toLocaleDateString('en-IN') %>
+
💰 <%= t('postLoad.budget') %>
<%= load.budget ? '₹' + Number(load.budget).toLocaleString('en-IN') : '—' %>
- - <% if (load.material_type) { %> -
माल: <%= load.material_type %>
- <% } %> - <% if (load.description) { %> -
विवरण: <%= load.description %>
- <% } %> - + <% if (load.material_type) { %>
📦 <%= t('postLoad.material') %>: <%= load.material_type %>
<% } %> + <% if (load.description) { %>
📝: <%= load.description %>
<% } %>
- पोस्ट किया: <%= load.poster ? load.poster.name : 'Unknown' %> | <%= new Date(load.created_at).toLocaleDateString('hi-IN') %> - WhatsApp शेयर + <%= load.poster ? load.poster.name : '' %> | <%= new Date(load.created_at).toLocaleDateString('en-IN') %> + 📱 WhatsApp
- <% if (user && user.role === 'driver' && load.status === 'open') { %> -
-
-

🏷️ <%= myBid ? 'अपनी बोली अपडेट करें' : 'बोली लगाएं' %>

- 💬 शिपर से बात करें -
-
-
-
- - -
- -
-
- -
-
+
+
+

🏷️ <%= myBid ? t('loadDetail.updateBid') : t('actions.bid') %>

+ 💬 Chat
+
+
+
+ + +
+ +
+
+ +
+
+
<% } %> - - <% if (bids.length > 0 && user && (user.id === load.posted_by || user.role === 'admin')) { %> -
-

📊 बोलियाँ (<%= bids.length %>)

- <% bids.forEach(bid => { %> -
-
- <%= bid.driver ? bid.driver.name : 'Driver' %> -
<%= bid.driver ? bid.driver.username : '' %> <% if (bid.note) { %>| <%= bid.note %><% } %>
-
-
- ₹<%= Number(bid.amount).toLocaleString('en-IN') %> - <% if (load.status === 'open' && bid.status === 'pending') { %> -
- - -
- <% } else { %> -
<%= bid.status %>
- <% } %> -
-
- <% }) %> -
- <% } else if (bids.length > 0) { %> -
-

🏷️ <%= bids.length %> बोली प्राप्त

+ <% if (user && (user.role === 'shipper' || user.role === 'broker') && load.posted_by === user.id && bids && bids.length > 0) { %> +
+

🏷️ <%= t('loadDetail.bidsReceived') %> (<%= bids.length %>)

+ <% bids.forEach(bid => { %> +
+
<%= bid.driver ? bid.driver.name : 'Driver' %><% if (bid.note) { %>
<%= bid.note %>
<% } %>
+
+ ₹<%= Number(bid.amount).toLocaleString('en-IN') %> + <% if (load.status === 'open' && bid.status === 'pending') { %> +
+ <% } else { %><%= bid.status %><% } %> +
+ <% }) %> +
<% } %>
- <%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/load-share.ejs b/webapp/src/views/pages/load-share.ejs new file mode 100644 index 0000000..ac4b0d1 --- /dev/null +++ b/webapp/src/views/pages/load-share.ejs @@ -0,0 +1,30 @@ + + + + + + <%= load.origin_city %> → <%= load.destination_city %> | BharathTrucks + + + + + + + +
+
+
🚛

BharathTrucks

+

📍 <%= load.origin_city %> → <%= load.destination_city %>

+
+
⚖️ Weight: <%= load.weight_tons %> tons
+
🚛 Truck: <%= load.truck_type %>
+ <% if (load.budget) { %>
💰 Budget: ₹<%= Number(load.budget).toLocaleString('en-IN') %>
<% } %> + <% if (load.pickup_date) { %>
📅 Pickup: <%= new Date(load.pickup_date).toLocaleDateString('en-IN') %>
<% } %> + <% if (load.material_type) { %>
📦 Material: <%= load.material_type %>
<% } %> +
+ Bid on this Load → + Join Free +
+
+ + diff --git a/webapp/src/views/pages/loadboard.ejs b/webapp/src/views/pages/loadboard.ejs index d593ea1..146b08f 100644 --- a/webapp/src/views/pages/loadboard.ejs +++ b/webapp/src/views/pages/loadboard.ejs @@ -1,45 +1,43 @@ -<% var title = 'लोड बोर्ड'; %> +<% var title = t('common.loadboard'); %> <%- include('../partials/header') %>
-

📋 लोड बोर्ड

+

📋 <%= t('common.loadboard') %>

<% if (user && (user.role === 'shipper' || user.role === 'broker')) { %> - + लोड पोस्ट करें + + <%= t('actions.postLoad') %> <% } %>
-
- - + +
- - + +
- +
- +
- <% if (loads.length === 0) { %>

📭

-

कोई लोड उपलब्ध नहीं

+

<%= t('common.noLoads') %>

<% } else { %>
@@ -49,12 +47,12 @@
📍 <%= load.origin_city %> → <%= load.destination_city %>
- 🚛 <%= load.weight_tons %> टन | <%= load.truck_type %> + 🚛 <%= load.weight_tons %> <%= t('common.tons') %> | <%= load.truck_type %> <% if (load.material_type) { %> | <%= load.material_type %><% } %>
📅 <%= new Date(load.pickup_date).toLocaleDateString('hi-IN') %> - <% if (load.bid_count > 0) { %> | 🏷️ <%= load.bid_count %> बोली<% } %> + <% if (load.bid_count > 0) { %> | 🏷️ <%= load.bid_count %> <%= t('common.bids') %><% } %>
@@ -62,7 +60,7 @@
₹<%= Number(load.budget).toLocaleString('en-IN') %>
<% } %> <% if (load.is_urgent) { %> - अर्जेंट + <%= t('common.urgent') %> <% } %>
diff --git a/webapp/src/views/pages/login.ejs b/webapp/src/views/pages/login.ejs index 742482f..e3cf02e 100644 --- a/webapp/src/views/pages/login.ejs +++ b/webapp/src/views/pages/login.ejs @@ -1,12 +1,12 @@ -<% var title = 'लॉगिन'; %> +<% var title = t('actions.login'); %> <%- include('../partials/header') %>
-

लॉगिन | Login

-

अपना यूज़रनेम और पासवर्ड दर्ज करें

+

<%= t('auth.loginTitle') %>

+

<%= t('auth.loginSubtitle') %>

<% if (error) { %>
<%= error %>
@@ -14,18 +14,18 @@
- - + +
- +
- +

- नया खाता? पंजीकरण करें + <%= t('auth.noAccount') %> <%= t('actions.register') %>

diff --git a/webapp/src/views/pages/maintenance.ejs b/webapp/src/views/pages/maintenance.ejs new file mode 100644 index 0000000..fa5dd1c --- /dev/null +++ b/webapp/src/views/pages/maintenance.ejs @@ -0,0 +1,37 @@ +<% var title = 'Maintenance'; %> +<%- include('../partials/header') %> +
+
+
+

🔧 Vehicle Reminders

+
+
<%= stats.total %>
Total
+
<%= stats.expired %>
🔴 Expired
+
<%= stats.expiring %>
🟡 Expiring
+
+ <% reminders.forEach(r => { %> +
+
+
+ <%= r.doc_type.toUpperCase() %> — <%= r.vehicle_number %> +
📅 <%= r.expiry_date %> | <%= r.days_left < 0 ? Math.abs(r.days_left) + ' days overdue' : r.days_left + ' days left' %>
+
+
+
+
+ <% }) %> +
+

➕ Add Reminder

+
+ + + + +
+
+
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/messages.ejs b/webapp/src/views/pages/messages.ejs index 5ab0bf2..6b03af0 100644 --- a/webapp/src/views/pages/messages.ejs +++ b/webapp/src/views/pages/messages.ejs @@ -1,34 +1,19 @@ -<% var title = 'संदेश'; %> +<% var title = t('nav.messages'); %> <%- include('../partials/header') %>
-
-
- <%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/more.ejs b/webapp/src/views/pages/more.ejs new file mode 100644 index 0000000..9164bd7 --- /dev/null +++ b/webapp/src/views/pages/more.ejs @@ -0,0 +1,34 @@ +<% var title = 'More'; %> +<%- include('../partials/header') %> +
+
+ +
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/news.ejs b/webapp/src/views/pages/news.ejs new file mode 100644 index 0000000..4ec8134 --- /dev/null +++ b/webapp/src/views/pages/news.ejs @@ -0,0 +1,23 @@ +<% var title = 'News'; %> +<%- include('../partials/header') %> +
+
+
+

📰 Trucker News

+ <% if (news.length === 0) { %> +

No news yet. Check back later!

+ <% } else { news.forEach(n => { %> +
+
+ <%= n.category==='diesel'?'⛽':n.category==='toll'?'🚧':n.category==='alert'?'⚠️':'📰' %> +
+ <%= n.title %> + <% if (n.content) { %>

<%= n.content %>

<% } %> +
<%= new Date(n.created_at).toLocaleDateString('en-IN') %>
+
+
+
+ <% }) } %> +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/notifications.ejs b/webapp/src/views/pages/notifications.ejs new file mode 100644 index 0000000..2af24b1 --- /dev/null +++ b/webapp/src/views/pages/notifications.ejs @@ -0,0 +1,22 @@ +<% var title = 'Notifications'; %> +<%- include('../partials/header') %> +
+
+
+

🔔 Notifications

+ <% if (notifications.length === 0) { %> +

✅ All clear! No pending actions.

+ <% } else { notifications.forEach(n => { %> + +
+ <%= n.icon %> +
+ <%= n.title %> +
<%= n.subtitle || '' %>
+
+
+
+ <% }) } %> +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/onboarding-game.ejs b/webapp/src/views/pages/onboarding-game.ejs new file mode 100644 index 0000000..cf54ecc --- /dev/null +++ b/webapp/src/views/pages/onboarding-game.ejs @@ -0,0 +1,18 @@ +<% var title = 'Welcome'; %> +<%- include('../partials/header') %> +
+
+ +
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/post-load.ejs b/webapp/src/views/pages/post-load.ejs index 22b8279..f95817e 100644 --- a/webapp/src/views/pages/post-load.ejs +++ b/webapp/src/views/pages/post-load.ejs @@ -1,75 +1,32 @@ -<% var title = 'लोड पोस्ट करें'; %> +<% var title = t('actions.postLoad'); %> <%- include('../partials/header') %>
-
-

📦 नया लोड पोस्ट करें

- - <% if (error) { %> -
<%= error %>
- <% } %> - +

📦 <%= t('actions.postLoad') %>

+ <% if (error) { %>
<%= error %>
<% } %>
-
- - -
-
- - -
+
+
-
-
- - -
-
- - +
+
+
-
-
- - -
-
- - -
+
+
- -
- - -
- -
- - -
- -
- -
- - +
+
+
+
- <%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/profile.ejs b/webapp/src/views/pages/profile.ejs index 3907724..28fce7b 100644 --- a/webapp/src/views/pages/profile.ejs +++ b/webapp/src/views/pages/profile.ejs @@ -1,51 +1,29 @@ -<% var title = 'मेरी प्रोफ़ाइल'; %> +<% var title = t('nav.profile'); %> <%- include('../partials/header') %>
-
-

👤 मेरी प्रोफ़ाइल

- - <% if (success) { %> -
✓ प्रोफ़ाइल अपडेट हो गई
- <% } %> - +

👤 <%= t('nav.profile') %>

+ <% if (success) { %>
✓ <%= t('profile.updated') %>
<% } %>
<%= profile.name ? profile.name.charAt(0).toUpperCase() : '?' %>
-
- <%= profile.name %> -
@<%= profile.username %> | <%= profile.role %>
-
+
<%= profile.name %>
@<%= profile.username %> | <%= profile.role %>
-
-
- - -
-
- - -
+
+
-
- - -
-
- - -
+
+
- +
-
- <%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/rates.ejs b/webapp/src/views/pages/rates.ejs new file mode 100644 index 0000000..738f6f6 --- /dev/null +++ b/webapp/src/views/pages/rates.ejs @@ -0,0 +1,29 @@ +<% var title = 'Rate Check'; %> +<%- include('../partials/header') %> +
+
+
+

📊 Rate Intelligence

+
+
+
+
+ +
+
+ <% if (rates) { %> +
+

<%= rates.origin %> → <%= rates.destination %>

+

Based on <%= rates.count %> recent loads

+
+
₹<%= rates.avg.toLocaleString('en-IN') %>
Average
+
₹<%= rates.min.toLocaleString('en-IN') %>
Min
+
₹<%= rates.max.toLocaleString('en-IN') %>
Max
+
+
+ <% } else if (origin && destination) { %> +

No rate data for this route yet.

+ <% } %> +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/referral.ejs b/webapp/src/views/pages/referral.ejs new file mode 100644 index 0000000..0b350d5 --- /dev/null +++ b/webapp/src/views/pages/referral.ejs @@ -0,0 +1,26 @@ +<% var title = 'Referral'; %> +<%- include('../partials/header') %> +
+
+
+

🤝 Invite & Earn

+
+

Your Referral Code:

+
<%= code %>
+ 📱 Share on WhatsApp +
+
+
<%= stats.total %>
Invited
+
<%= stats.joined %>
Joined
+
+ <% if (referrals.length > 0) { %> +

Recent Referrals

+ <% referrals.slice(0,10).forEach(r => { %> +
+ <%= r.referral_code %> + <%= r.status %> +
+ <% }) } %> +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/register.ejs b/webapp/src/views/pages/register.ejs index 91872fd..cf85964 100644 --- a/webapp/src/views/pages/register.ejs +++ b/webapp/src/views/pages/register.ejs @@ -1,12 +1,12 @@ -<% var title = 'पंजीकरण'; %> +<% var title = t('actions.register'); %> <%- include('../partials/header') %>
-

पंजीकरण | Register

-

मुफ्त खाता बनाएं

+

<%= t('auth.registerTitle') %>

+

<%= t('auth.registerSubtitle') %>

<% if (error) { %>
<%= error %>
@@ -14,55 +14,55 @@
- +
- - + +
- - + +
- +
- +
- +
- +

- पहले से खाता है? लॉगिन करें + <%= t('auth.hasAccount') %> <%= t('actions.login') %>

@@ -75,17 +75,16 @@ document.querySelectorAll('input[name="role"]').forEach(r => { const input = document.getElementById('usernameInput'); const hint = document.getElementById('usernameHint'); if (this.value === 'driver') { - label.textContent = 'गाड़ी नंबर / Vehicle Number *'; + label.innerHTML = '🚛 <%= t("auth.vehicleNumber") %> *'; input.placeholder = 'MH31AB1234'; - hint.textContent = 'आपका गाड़ी नंबर ही आपका यूज़रनेम होगा'; + hint.textContent = '<%= t("auth.vehicleHint") %>'; } else { - label.textContent = 'यूज़रनेम *'; - input.placeholder = 'अपना यूज़रनेम चुनें'; + label.innerHTML = '📝 <%= t("auth.username") %> *'; + input.placeholder = '<%= t("auth.username") %>'; hint.textContent = ''; } }); }); -// Trigger on load if role pre-selected const checked = document.querySelector('input[name="role"]:checked'); if (checked) checked.dispatchEvent(new Event('change')); diff --git a/webapp/src/views/pages/reports.ejs b/webapp/src/views/pages/reports.ejs new file mode 100644 index 0000000..730f86d --- /dev/null +++ b/webapp/src/views/pages/reports.ejs @@ -0,0 +1,22 @@ +<% var title = 'Reports'; %> +<%- include('../partials/header') %> +
+
+
+
+

📊 Reports

+ 📥 CSV Export +
+
+
<%= stats.total_trips %>
Total Trips
+
<%= stats.month_trips %>
This Month
+
₹<%= stats.total_revenue.toLocaleString('en-IN') %>
💰 Revenue
+
₹<%= stats.total_expenses.toLocaleString('en-IN') %>
💸 Expenses
+
+
+
Net Profit
+
₹<%= stats.profit.toLocaleString('en-IN') %>
+
+
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/return-load.ejs b/webapp/src/views/pages/return-load.ejs new file mode 100644 index 0000000..68cdfb0 --- /dev/null +++ b/webapp/src/views/pages/return-load.ejs @@ -0,0 +1,40 @@ +<% var title = 'Return Load'; %> +<%- include('../partials/header') %> +
+
+
+

🔄 Return Load

+ <% if (availability && availability.status === 'looking') { %> +
+
+
✅ You're visible in <%= availability.current_city %>
Shippers can find you
+
+
+
+ <% } else { %> +
+

📍 Where are you now?

+
+ + + + +
+
+ <% } %> + <% if (suggestions.length > 0) { %> +

📋 Available Loads

+ <% suggestions.forEach(load => { %> + + 📍 <%= load.origin_city %> → <%= load.destination_city %> +
<%= load.weight_tons %> tons | ₹<%= (load.budget||0).toLocaleString('en-IN') %>
+
+ <% }) %> + <% } else if (availability) { %> +

📭 No loads from your city yet. We'll notify you!

+ <% } %> +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/safety-sent.ejs b/webapp/src/views/pages/safety-sent.ejs new file mode 100644 index 0000000..2dfe5c3 --- /dev/null +++ b/webapp/src/views/pages/safety-sent.ejs @@ -0,0 +1,18 @@ +<% var title = is_sos ? 'SOS' : 'Check-in'; %> +<%- include('../partials/header') %> +
+
+
+
<%= is_sos ? '🆘' : '✅' %>
+

<%= is_sos ? 'SOS Alert Ready' : 'Check-in Ready' %>

+

Tap to send via WhatsApp:

+
+ <% links.forEach(l => { %> + 💬 <%= l.name %> + <% if (l.call) { %>📞 Call <%= l.name %><% } %> + <% }) %> +
+ ← Back +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/safety.ejs b/webapp/src/views/pages/safety.ejs new file mode 100644 index 0000000..24dbe13 --- /dev/null +++ b/webapp/src/views/pages/safety.ejs @@ -0,0 +1,49 @@ +<% var title = 'Safety'; %> +<%- include('../partials/header') %> +
+
+
+

🛡️ Safety

+
+
+ + +
+
+ + +
+
+
+ 📞 Emergency: + +
+

👨‍👩‍👧 Family Contacts

+ <% contacts.forEach(c => { %> +
+
<%= c.contact_name %>
<%= c.contact_phone %> • <%= c.relationship %>
+
+
+ <% }) %> +
+

➕ Add Contact

+
+ + + + +
+
+
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/search.ejs b/webapp/src/views/pages/search.ejs new file mode 100644 index 0000000..fd6751d --- /dev/null +++ b/webapp/src/views/pages/search.ejs @@ -0,0 +1,24 @@ +<% var title = 'Search'; %> +<%- include('../partials/header') %> +
+
+
+

🔍 Search

+
+
+ + +
+
+ <% if (q) { %> + <% if (results.loads.length) { %>

📋 Loads (<%= results.loads.length %>)

+ <% results.loads.forEach(l => { %><%= l.origin_city %> → <%= l.destination_city %> <% if(l.budget){%>| ₹<%= l.budget.toLocaleString('en-IN') %><%}%><% }) } %> + <% if (results.users.length) { %>

👤 Users (<%= results.users.length %>)

+ <% results.users.forEach(u => { %>
<%= u.name || u.username %> <%= u.role %>
<% }) } %> + <% if (results.classifieds.length) { %>

🛒 Classifieds (<%= results.classifieds.length %>)

+ <% results.classifieds.forEach(c => { %>
<%= c.title %> | ₹<%= (c.price||0).toLocaleString('en-IN') %>
<% }) } %> + <% if (!results.loads.length && !results.users.length && !results.classifieds.length) { %>

No results for "<%= q %>"

<% } %> + <% } %> +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/shipper-dashboard.ejs b/webapp/src/views/pages/shipper-dashboard.ejs index 0be5209..3cc4b07 100644 --- a/webapp/src/views/pages/shipper-dashboard.ejs +++ b/webapp/src/views/pages/shipper-dashboard.ejs @@ -1,25 +1,25 @@ -<% var title = 'शिपर डैशबोर्ड'; %> +<% var title = t('dashboard.shipperTitle'); %> <%- include('../partials/header') %>
-

📦 नमस्ते, <%= user.name %>!

+

📦 <%= t('dashboard.hello') %>, <%= user.name %>!

-
<%= stats.totalLoads %>
मेरे लोड
-
<%= stats.openLoads %>
खुले लोड
-
<%= stats.activeTrips %>
सक्रिय शिपमेंट
+
<%= stats.totalLoads %>
<%= t('dashboard.myLoads') %>
+
<%= stats.openLoads %>
<%= t('dashboard.openLoads') %>
+
<%= stats.activeTrips %>
<%= t('dashboard.activeShipments') %>
<% if (recentLoads.length > 0) { %> -

📋 हाल के लोड

+

📋 <%= t('dashboard.recentLoads') %>

<% recentLoads.forEach(load => { %>
<%= load.origin_city %> → <%= load.destination_city %> -
<%= load.weight_tons %> टन | 🏷️ <%= load.bid_count %> बोली
+
<%= load.weight_tons %> <%= t('common.tons') %> | 🏷️ <%= load.bid_count %> <%= t('common.bids') %>
<%= load.status %>
@@ -27,9 +27,39 @@ <% }) %> <% } %> -
diff --git a/webapp/src/views/pages/trip-planner.ejs b/webapp/src/views/pages/trip-planner.ejs new file mode 100644 index 0000000..a7a25c0 --- /dev/null +++ b/webapp/src/views/pages/trip-planner.ejs @@ -0,0 +1,43 @@ +<% var title = 'Trip Planner'; %> +<%- include('../partials/header') %> +
+
+
+

🧮 Trip Cost Calculator

+
+
+
+
+
+
+
+
+ +
+
+ <% cities.forEach(c => { %> + <% if (result && !result.error) { %> +
+

📊 <%= result.origin %> → <%= result.destination %>

+
📏 <%= result.distance_km %> km | ⏱️ ~<%= result.hours %> hrs | 🚧 <%= result.toll_plazas %> tolls
+
+
₹<%= result.fuel.cost.toLocaleString('en-IN') %>
⛽ Fuel
+
₹<%= result.toll.toLocaleString('en-IN') %>
🚧 Toll
+
₹<%= result.driver_bata.toLocaleString('en-IN') %>
🍽️ Bata
+
₹<%= result.total.toLocaleString('en-IN') %>
💸 Total
+
+ <% if (result.profit !== undefined && result.profit !== 0) { %> +
+
+ <%= result.viable ? '✅ Profitable' : '❌ Loss' %> + ₹<%= result.profit.toLocaleString('en-IN') %> (<%= result.margin %>%) +
+
+ <% } %> +
+ <% } else if (result && result.error) { %> +
⚠️ <%= result.error %>
+ <% } %> +
+
+<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/trips.ejs b/webapp/src/views/pages/trips.ejs index db1a741..37da0a2 100644 --- a/webapp/src/views/pages/trips.ejs +++ b/webapp/src/views/pages/trips.ejs @@ -1,14 +1,14 @@ -<% var title = 'मेरी ट्रिप'; %> +<% var title = t('actions.myTrips'); %> <%- include('../partials/header') %>
-

🚚 मेरी ट्रिप

+

🚚 <%= t('actions.myTrips') %>

<% if (trips.length === 0) { %>
-

कोई ट्रिप नहीं

+

<%= t('trips.noTrips') %>

<% } else { %>
@@ -19,7 +19,7 @@ 📍 <%= trip.load ? trip.load.origin_city + ' → ' + trip.load.destination_city : 'N/A' %>
₹<%= Number(trip.amount).toLocaleString('en-IN') %> - <% if (trip.load) { %> | <%= trip.load.weight_tons %> टन | <%= trip.load.truck_type %><% } %> + <% if (trip.load) { %> | <%= trip.load.weight_tons %> <%= t('common.tons') %> | <%= trip.load.truck_type %><% } %>
<%= trip.status %> @@ -28,11 +28,11 @@ <% if (user.role === 'driver' && trip.status !== 'delivered' && trip.status !== 'cancelled') { %>
<% if (trip.status === 'confirmed') { %> -
+
<% } else if (trip.status === 'picked_up') { %> -
+
<% } else if (trip.status === 'in_transit') { %> -
+
<% } %>
<% } %> diff --git a/webapp/src/views/partials/bottom-nav.ejs b/webapp/src/views/partials/bottom-nav.ejs index e0e5aaa..ff614e7 100644 --- a/webapp/src/views/partials/bottom-nav.ejs +++ b/webapp/src/views/partials/bottom-nav.ejs @@ -1,13 +1,31 @@ <% if (user) { %> -
diff --git a/webapp/supabase-phase1-migration.sql b/webapp/supabase-phase1-migration.sql new file mode 100644 index 0000000..f897a6b --- /dev/null +++ b/webapp/supabase-phase1-migration.sql @@ -0,0 +1,106 @@ +-- BharathTrucks Phase 1 Migration: Driver tools, Safety, Maintenance, FASTag, Notifications +-- Run this AFTER supabase-FULL-migration.sql + +-- Driver personal ledger (replaces paper diary) +CREATE TABLE IF NOT EXISTS driver_ledger ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID REFERENCES app_users(id) ON DELETE CASCADE, + origin TEXT, + destination TEXT, + trip_date DATE DEFAULT CURRENT_DATE, + freight_received NUMERIC(10,2) DEFAULT 0, + fuel_cost NUMERIC(10,2) DEFAULT 0, + toll_cost NUMERIC(10,2) DEFAULT 0, + other_expense NUMERIC(10,2) DEFAULT 0, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX idx_driver_ledger_user ON driver_ledger(user_id); + +-- Return load availability +CREATE TABLE IF NOT EXISTS available_for_return ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID REFERENCES app_users(id) ON DELETE CASCADE UNIQUE, + current_city TEXT NOT NULL, + home_city TEXT, + vehicle_type TEXT, + status TEXT DEFAULT 'looking', + updated_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX idx_return_city ON available_for_return(current_city, status); + +-- Safety contacts +CREATE TABLE IF NOT EXISTS safety_contacts ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID REFERENCES app_users(id) ON DELETE CASCADE, + contact_name TEXT NOT NULL, + contact_phone TEXT NOT NULL, + relationship TEXT DEFAULT 'family', + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX idx_safety_contacts_user ON safety_contacts(user_id); + +-- Safety check-ins log +CREATE TABLE IF NOT EXISTS safety_checkins ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID REFERENCES app_users(id) ON DELETE CASCADE, + location TEXT, + message TEXT, + is_sos BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Vehicle maintenance reminders +CREATE TABLE IF NOT EXISTS vehicle_reminders ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID REFERENCES app_users(id) ON DELETE CASCADE, + vehicle_number TEXT, + doc_type TEXT NOT NULL, -- insurance, fitness, permit, puc, service + expiry_date DATE NOT NULL, + notes TEXT, + status TEXT DEFAULT 'active', + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX idx_reminders_user ON vehicle_reminders(user_id); +CREATE INDEX idx_reminders_expiry ON vehicle_reminders(expiry_date, status); + +-- FASTag accounts +CREATE TABLE IF NOT EXISTS fastag_accounts ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID REFERENCES app_users(id) ON DELETE CASCADE UNIQUE, + fastag_number TEXT, + vehicle_number TEXT, + issuer_bank TEXT, + balance NUMERIC(10,2) DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Toll history +CREATE TABLE IF NOT EXISTS toll_history ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID REFERENCES app_users(id) ON DELETE CASCADE, + type TEXT DEFAULT 'toll', -- toll, recharge + plaza_name TEXT, + amount NUMERIC(10,2) DEFAULT 0, + status TEXT DEFAULT 'completed', + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX idx_toll_user ON toll_history(user_id, created_at DESC); + +-- Enable RLS +ALTER TABLE driver_ledger ENABLE ROW LEVEL SECURITY; +ALTER TABLE available_for_return ENABLE ROW LEVEL SECURITY; +ALTER TABLE safety_contacts ENABLE ROW LEVEL SECURITY; +ALTER TABLE safety_checkins ENABLE ROW LEVEL SECURITY; +ALTER TABLE vehicle_reminders ENABLE ROW LEVEL SECURITY; +ALTER TABLE fastag_accounts ENABLE ROW LEVEL SECURITY; +ALTER TABLE toll_history ENABLE ROW LEVEL SECURITY; + +-- RLS policies (allow all for service role, restrict for anon) +CREATE POLICY "Users manage own ledger" ON driver_ledger FOR ALL USING (true); +CREATE POLICY "Users manage own return" ON available_for_return FOR ALL USING (true); +CREATE POLICY "Users manage own contacts" ON safety_contacts FOR ALL USING (true); +CREATE POLICY "Users manage own checkins" ON safety_checkins FOR ALL USING (true); +CREATE POLICY "Users manage own reminders" ON vehicle_reminders FOR ALL USING (true); +CREATE POLICY "Users manage own fastag" ON fastag_accounts FOR ALL USING (true); +CREATE POLICY "Users manage own tolls" ON toll_history FOR ALL USING (true); diff --git a/webapp/supabase-phase2-migration.sql b/webapp/supabase-phase2-migration.sql new file mode 100644 index 0000000..22452b0 --- /dev/null +++ b/webapp/supabase-phase2-migration.sql @@ -0,0 +1,91 @@ +-- BharathTrucks Phase 2 Migration: Gamification, Referral, Feed, Challenges, Invoice +-- Run AFTER supabase-phase1-migration.sql + +CREATE TABLE IF NOT EXISTS user_gamification ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID REFERENCES app_users(id) ON DELETE CASCADE UNIQUE, + xp INTEGER DEFAULT 0, + login_streak INTEGER DEFAULT 0, + last_login_date DATE, + steps_completed JSONB DEFAULT '[]', + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS user_achievements ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID REFERENCES app_users(id) ON DELETE CASCADE, + achievement_id TEXT NOT NULL, + earned_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(user_id, achievement_id) +); + +CREATE TABLE IF NOT EXISTS xp_log ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID REFERENCES app_users(id) ON DELETE CASCADE, + action TEXT NOT NULL, + xp_earned INTEGER DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX idx_xp_log_user ON xp_log(user_id, created_at DESC); + +CREATE TABLE IF NOT EXISTS challenge_progress ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID REFERENCES app_users(id) ON DELETE CASCADE, + challenge_id TEXT NOT NULL, + completed_date DATE DEFAULT CURRENT_DATE, + UNIQUE(user_id, challenge_id, completed_date) +); + +CREATE TABLE IF NOT EXISTS referrals ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + referrer_id UUID REFERENCES app_users(id) ON DELETE CASCADE, + referred_user_id UUID, + referral_code TEXT, + status TEXT DEFAULT 'pending', + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX idx_referrals_referrer ON referrals(referrer_id); + +CREATE TABLE IF NOT EXISTS feed_events ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + event_type TEXT NOT NULL, + data JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX idx_feed_created ON feed_events(created_at DESC); + +CREATE TABLE IF NOT EXISTS invoices ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID REFERENCES app_users(id) ON DELETE CASCADE, + invoice_number TEXT UNIQUE, + client_name TEXT, + origin TEXT, + destination TEXT, + amount NUMERIC(10,2) DEFAULT 0, + gst_rate NUMERIC(4,2) DEFAULT 5, + gst_amount NUMERIC(10,2) DEFAULT 0, + total_amount NUMERIC(10,2) DEFAULT 0, + upi_id TEXT, + upi_link TEXT, + notes TEXT, + status TEXT DEFAULT 'unpaid', + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX idx_invoices_user ON invoices(user_id, created_at DESC); + +-- RLS +ALTER TABLE user_gamification ENABLE ROW LEVEL SECURITY; +ALTER TABLE user_achievements ENABLE ROW LEVEL SECURITY; +ALTER TABLE xp_log ENABLE ROW LEVEL SECURITY; +ALTER TABLE challenge_progress ENABLE ROW LEVEL SECURITY; +ALTER TABLE referrals ENABLE ROW LEVEL SECURITY; +ALTER TABLE feed_events ENABLE ROW LEVEL SECURITY; +ALTER TABLE invoices ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "all_access" ON user_gamification FOR ALL USING (true); +CREATE POLICY "all_access" ON user_achievements FOR ALL USING (true); +CREATE POLICY "all_access" ON xp_log FOR ALL USING (true); +CREATE POLICY "all_access" ON challenge_progress FOR ALL USING (true); +CREATE POLICY "all_access" ON referrals FOR ALL USING (true); +CREATE POLICY "all_access" ON feed_events FOR ALL USING (true); +CREATE POLICY "all_access" ON invoices FOR ALL USING (true); diff --git a/webapp/supabase-phase3-migration.sql b/webapp/supabase-phase3-migration.sql new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/webapp/supabase-phase3-migration.sql @@ -0,0 +1 @@ +