mirror of
http://forgejo-oa09toasww4dgii9cj3gpzda.187.127.164.61.sslip.io/iamcoolvivek007/bharath.git
synced 2026-06-11 00:06:51 +00:00
feat: add 35+ features - i18n, voice input, gamification, driver tools, marketplace
- Multi-language support (English, Hindi, Tamil, Telugu) with icon-based UI - Voice input (Web Speech API) for low-literacy users - Driver tools: Ledger, Trip Planner, Return Load, Safety, Maintenance, FASTag - Marketplace: WhatsApp share, Rate Intelligence, Classifieds, Fleet - Engagement: Gamification (XP/Levels), Challenges, Leaderboard, Referrals, Feed - Business: Invoice (GST+UPI), Reports+CSV, Notifications, Documents, Bank - Games: Rate Guesser, Route Quiz - SEO: Sitemap, public load share pages with OG tags - India utilities: vehicle validation, UPI links, toll/fuel calculator - 29 routes, 54 templates, 4 languages, 3 migration files
This commit is contained in:
parent
60415a02fa
commit
ed320e82c1
94 changed files with 3164 additions and 447 deletions
44
multi-language-support-903482.md
Normal file
44
multi-language-support-903482.md
Normal file
|
|
@ -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
|
||||||
143
webapp/src/i18n/en.json
Normal file
143
webapp/src/i18n/en.json
Normal file
|
|
@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
143
webapp/src/i18n/hi.json
Normal file
143
webapp/src/i18n/hi.json
Normal file
|
|
@ -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+ उपयोगकर्ताओं तक सभी सुविधाएं मुफ्त। कोई क्रेडिट कार्ड नहीं चाहिए।"
|
||||||
|
}
|
||||||
|
}
|
||||||
143
webapp/src/i18n/ta.json
Normal file
143
webapp/src/i18n/ta.json
Normal file
|
|
@ -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+ பயனர்களுக்கு அனைத்து வசதிகளும் இலவசம். கிரெடிட் கார்டு தேவையில்லை."
|
||||||
|
}
|
||||||
|
}
|
||||||
143
webapp/src/i18n/te.json
Normal file
143
webapp/src/i18n/te.json
Normal file
|
|
@ -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+ వినియోగదారులకు అన్ని ఫీచర్లు ఉచితం. క్రెడిట్ కార్డ్ అవసరం లేదు."
|
||||||
|
}
|
||||||
|
}
|
||||||
46
webapp/src/lib/gamification.js
Normal file
46
webapp/src/lib/gamification.js
Normal file
|
|
@ -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 };
|
||||||
96
webapp/src/lib/india.js
Normal file
96
webapp/src/lib/india.js
Normal file
|
|
@ -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 };
|
||||||
27
webapp/src/middleware/i18n.js
Normal file
27
webapp/src/middleware/i18n.js
Normal file
|
|
@ -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 };
|
||||||
|
|
@ -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; }
|
.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; }
|
body { padding-bottom: 70px; }
|
||||||
@media (min-width: 768px) { .bottom-nav { display: none; } body { padding-bottom: 0; } }
|
@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)} }
|
||||||
|
|
|
||||||
34
webapp/src/public/js/voice.js
Normal file
34
webapp/src/public/js/voice.js
Normal file
|
|
@ -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);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
@ -91,6 +91,10 @@ router.post('/register', async (req, res) => {
|
||||||
id: user.id, username: user.username, name: user.name,
|
id: user.id, username: user.username, name: user.name,
|
||||||
role: user.role, phone: user.phone,
|
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('/');
|
res.redirect('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
25
webapp/src/routes/bank.js
Normal file
25
webapp/src/routes/bank.js
Normal file
|
|
@ -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;
|
||||||
42
webapp/src/routes/challenges.js
Normal file
42
webapp/src/routes/challenges.js
Normal file
|
|
@ -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;
|
||||||
33
webapp/src/routes/classifieds.js
Normal file
33
webapp/src/routes/classifieds.js
Normal file
|
|
@ -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;
|
||||||
27
webapp/src/routes/documents.js
Normal file
27
webapp/src/routes/documents.js
Normal file
|
|
@ -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;
|
||||||
41
webapp/src/routes/driver-ledger.js
Normal file
41
webapp/src/routes/driver-ledger.js
Normal file
|
|
@ -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;
|
||||||
37
webapp/src/routes/fastag.js
Normal file
37
webapp/src/routes/fastag.js
Normal file
|
|
@ -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;
|
||||||
17
webapp/src/routes/feed.js
Normal file
17
webapp/src/routes/feed.js
Normal file
|
|
@ -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;
|
||||||
33
webapp/src/routes/fleet.js
Normal file
33
webapp/src/routes/fleet.js
Normal file
|
|
@ -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;
|
||||||
40
webapp/src/routes/gamification.js
Normal file
40
webapp/src/routes/gamification.js
Normal file
|
|
@ -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;
|
||||||
38
webapp/src/routes/invoice.js
Normal file
38
webapp/src/routes/invoice.js
Normal file
|
|
@ -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;
|
||||||
18
webapp/src/routes/leaderboard.js
Normal file
18
webapp/src/routes/leaderboard.js
Normal file
|
|
@ -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;
|
||||||
|
|
@ -58,6 +58,12 @@ router.post('/post', requireAuth, requireRole(ROLES.SHIPPER, ROLES.BROKER), asyn
|
||||||
if (error) {
|
if (error) {
|
||||||
return res.render('pages/post-load', { error: 'लोड पोस्ट करने में त्रुटि', truckTypes: TRUCK_TYPES });
|
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');
|
res.redirect('/loadboard');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -95,6 +101,10 @@ router.post('/:id/bid', requireAuth, requireRole(ROLES.DRIVER), async (req, res)
|
||||||
note: note || null,
|
note: note || null,
|
||||||
}, { onConflict: 'load_id,driver_id' });
|
}, { 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}`);
|
res.redirect(`/loadboard/${req.params.id}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
36
webapp/src/routes/maintenance.js
Normal file
36
webapp/src/routes/maintenance.js
Normal file
|
|
@ -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;
|
||||||
52
webapp/src/routes/minigames.js
Normal file
52
webapp/src/routes/minigames.js
Normal file
|
|
@ -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;
|
||||||
19
webapp/src/routes/news.js
Normal file
19
webapp/src/routes/news.js
Normal file
|
|
@ -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;
|
||||||
37
webapp/src/routes/notifications.js
Normal file
37
webapp/src/routes/notifications.js
Normal file
|
|
@ -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;
|
||||||
21
webapp/src/routes/rates.js
Normal file
21
webapp/src/routes/rates.js
Normal file
|
|
@ -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;
|
||||||
25
webapp/src/routes/referral.js
Normal file
25
webapp/src/routes/referral.js
Normal file
|
|
@ -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;
|
||||||
33
webapp/src/routes/reports.js
Normal file
33
webapp/src/routes/reports.js
Normal file
|
|
@ -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;
|
||||||
30
webapp/src/routes/returnload.js
Normal file
30
webapp/src/routes/returnload.js
Normal file
|
|
@ -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;
|
||||||
49
webapp/src/routes/safety.js
Normal file
49
webapp/src/routes/safety.js
Normal file
|
|
@ -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;
|
||||||
19
webapp/src/routes/search.js
Normal file
19
webapp/src/routes/search.js
Normal file
|
|
@ -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;
|
||||||
21
webapp/src/routes/sitemap.js
Normal file
21
webapp/src/routes/sitemap.js
Normal file
|
|
@ -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 version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
|
||||||
|
xml += `<url><loc>${base}/</loc><priority>1.0</priority></url>`;
|
||||||
|
xml += `<url><loc>${base}/loadboard</loc><changefreq>hourly</changefreq><priority>0.9</priority></url>`;
|
||||||
|
xml += `<url><loc>${base}/register</loc><priority>0.8</priority></url>`;
|
||||||
|
(loads || []).forEach(l => { xml += `<url><loc>${base}/loadboard/share/${l.id}</loc><lastmod>${l.created_at?.split('T')[0]}</lastmod></url>`; });
|
||||||
|
xml += '</urlset>';
|
||||||
|
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;
|
||||||
27
webapp/src/routes/tripplanner.js
Normal file
27
webapp/src/routes/tripplanner.js
Normal file
|
|
@ -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;
|
||||||
|
|
@ -33,6 +33,12 @@ router.post('/:id/status', async (req, res) => {
|
||||||
if (trip) await supabase.from('loads').update({ status }).eq('id', trip.load_id);
|
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');
|
res.redirect('/trips');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
22
webapp/src/routes/whatsapp.js
Normal file
22
webapp/src/routes/whatsapp.js
Normal file
|
|
@ -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;
|
||||||
|
|
@ -38,8 +38,8 @@ app.set('views', path.join(__dirname, 'views'));
|
||||||
// Session
|
// Session
|
||||||
app.use(session({
|
app.use(session({
|
||||||
secret: config.session.secret,
|
secret: config.session.secret,
|
||||||
resave: false,
|
resave: true,
|
||||||
saveUninitialized: false,
|
saveUninitialized: true,
|
||||||
cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 },
|
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.user = req.session.user || null;
|
||||||
res.locals.appName = 'भारत ट्रक्स';
|
res.locals.appName = 'भारत ट्रक्स';
|
||||||
res.locals.appNameEn = 'BharathTrucks';
|
res.locals.appNameEn = 'BharathTrucks';
|
||||||
|
res.locals.formatINR = require('./lib/india').formatINR;
|
||||||
next();
|
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
|
// Routes
|
||||||
const authRoutes = require('./routes/auth');
|
const authRoutes = require('./routes/auth');
|
||||||
const loadRoutes = require('./routes/loads');
|
const loadRoutes = require('./routes/loads');
|
||||||
const tripRoutes = require('./routes/trips');
|
const tripRoutes = require('./routes/trips');
|
||||||
const adminRoutes = require('./routes/admin');
|
const adminRoutes = require('./routes/admin');
|
||||||
const messageRoutes = require('./routes/messages');
|
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('/', authRoutes);
|
||||||
app.use('/loadboard', loadRoutes);
|
app.use('/loadboard', loadRoutes);
|
||||||
|
app.use('/loadboard', whatsappRoutes);
|
||||||
app.use('/trips', tripRoutes);
|
app.use('/trips', tripRoutes);
|
||||||
app.use('/admin', adminRoutes);
|
app.use('/admin', adminRoutes);
|
||||||
app.use('/messages', messageRoutes);
|
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 { requireAuth, requireDriver, requireShipper, requireBroker } = require('./middleware/auth');
|
||||||
const supabase = require('./services/supabase');
|
const supabase = require('./services/supabase');
|
||||||
|
|
||||||
app.get('/health', (req, res) => res.json({ status: 'ok', ts: Date.now() }));
|
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) => {
|
app.get('/', (req, res) => {
|
||||||
if (req.session && req.session.user) {
|
if (req.session && req.session.user) {
|
||||||
const { ROLES } = require('./config/constants');
|
const { ROLES } = require('./config/constants');
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
<% var title = '404 - पृष्ठ नहीं मिला'; %>
|
<% var title = '404'; %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<section class="section text-center" style="padding-top:60px">
|
||||||
<div class="error-page">
|
<div class="container"><h1 style="font-size:3rem;color:var(--navy)">404</h1><p style="color:var(--gray-700);margin:var(--space-md) 0">Page not found</p><a href="/" class="btn btn-primary">🏠 Home</a></div>
|
||||||
<h1>404</h1>
|
</section>
|
||||||
<p>यह पृष्ठ उपलब्ध नहीं है। | Page not found.</p>
|
|
||||||
<a href="/" class="btn btn-primary">मुख्य पृष्ठ पर जाएं</a>
|
|
||||||
</div>
|
|
||||||
<%- include('../partials/footer') %>
|
<%- include('../partials/footer') %>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
<% var title = '500 - सर्वर त्रुटि'; %>
|
<% var title = 'Error'; %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<section class="section text-center" style="padding-top:60px">
|
||||||
<div class="error-page">
|
<div class="container"><h1 style="font-size:3rem;color:var(--navy)">500</h1><p style="color:var(--gray-700);margin:var(--space-md) 0">Something went wrong</p><a href="/" class="btn btn-primary">🏠 Home</a></div>
|
||||||
<h1>500</h1>
|
</section>
|
||||||
<p>कुछ गलत हो गया। कृपया बाद में पुनः प्रयास करें। | Something went wrong.</p>
|
|
||||||
<a href="/" class="btn btn-primary">मुख्य पृष्ठ पर जाएं</a>
|
|
||||||
</div>
|
|
||||||
<%- include('../partials/footer') %>
|
<%- include('../partials/footer') %>
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,21 @@
|
||||||
<% var title = 'एडमिन पैनल'; %>
|
<% var title = 'Admin Dashboard'; %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="section" style="padding-top:var(--space-lg)">
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🏛️ एडमिन पैनल</h2>
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🏛️ Admin Dashboard</h2>
|
||||||
|
<div class="stats-grid" style="margin-bottom:var(--space-lg)">
|
||||||
<div class="stats-grid">
|
<div class="stat-card"><div class="stat-value"><%= stats.totalUsers %></div><div class="stat-label">👤 Users</div></div>
|
||||||
<div class="stat-card"><div class="stat-value"><%= stats.users %></div><div class="stat-label">कुल उपयोगकर्ता</div></div>
|
<div class="stat-card"><div class="stat-value"><%= stats.totalLoads %></div><div class="stat-label">📦 Loads</div></div>
|
||||||
<div class="stat-card"><div class="stat-value"><%= stats.loads %></div><div class="stat-label">कुल लोड</div></div>
|
<div class="stat-card"><div class="stat-value"><%= stats.totalTrips %></div><div class="stat-label">🚛 Trips</div></div>
|
||||||
<div class="stat-card"><div class="stat-value"><%= stats.bids %></div><div class="stat-label">कुल बोलियाँ</div></div>
|
<div class="stat-card"><div class="stat-value"><%= stats.totalBids %></div><div class="stat-label">🏷️ Bids</div></div>
|
||||||
<div class="stat-card"><div class="stat-value"><%= stats.trips %></div><div class="stat-label">कुल ट्रिप</div></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
||||||
<div class="stats-grid" style="margin-top:var(--space-md)">
|
<a href="/admin/users" class="icon-action-btn"><span class="icon-action-emoji">👥</span><span class="icon-action-label">Users</span></a>
|
||||||
<div class="stat-card"><div class="stat-value"><%= roles.driver %></div><div class="stat-label">🚛 ड्राइवर</div></div>
|
<a href="/admin/loads" class="icon-action-btn"><span class="icon-action-emoji">📦</span><span class="icon-action-label">Loads</span></a>
|
||||||
<div class="stat-card"><div class="stat-value"><%= roles.shipper %></div><div class="stat-label">📦 शिपर</div></div>
|
<a href="/news" class="icon-action-btn"><span class="icon-action-emoji">📰</span><span class="icon-action-label">News</span></a>
|
||||||
<div class="stat-card"><div class="stat-value"><%= roles.broker %></div><div class="stat-label">🤝 ब्रोकर</div></div>
|
<a href="/reports" class="icon-action-btn"><span class="icon-action-emoji">📊</span><span class="icon-action-label">Reports</span></a>
|
||||||
</div>
|
|
||||||
|
|
||||||
<% if (recentUsers.length > 0) { %>
|
|
||||||
<h3 style="font-size:1rem;margin-top:var(--space-lg);margin-bottom:var(--space-sm)">नए उपयोगकर्ता</h3>
|
|
||||||
<div class="card" style="padding:0;overflow:hidden">
|
|
||||||
<table style="width:100%;border-collapse:collapse;font-size:0.8rem">
|
|
||||||
<tr style="background:var(--gray-100)"><th style="padding:8px;text-align:left">नाम</th><th>यूज़रनेम</th><th>भूमिका</th><th>तारीख</th></tr>
|
|
||||||
<% recentUsers.forEach(u => { %>
|
|
||||||
<tr style="border-top:1px solid var(--gray-200)">
|
|
||||||
<td style="padding:8px"><%= u.name %></td>
|
|
||||||
<td style="padding:8px"><%= u.username %></td>
|
|
||||||
<td style="padding:8px"><span class="badge badge-open"><%= u.role %></span></td>
|
|
||||||
<td style="padding:8px"><%= new Date(u.created_at).toLocaleDateString('hi-IN') %></td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<div style="margin-top:var(--space-lg);display:grid;grid-template-columns:1fr 1fr;gap:var(--space-sm)">
|
|
||||||
<a href="/admin/users" class="btn btn-primary btn-block">👥 उपयोगकर्ता</a>
|
|
||||||
<a href="/admin/loads" class="btn btn-outline btn-block">📋 लोड</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<%- include('../partials/footer') %>
|
<%- include('../partials/footer') %>
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,17 @@
|
||||||
<% var title = 'सभी लोड — एडमिन'; %>
|
<% var title = 'Admin - Loads'; %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="section" style="padding-top:var(--space-lg)">
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-md)">
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">📦 All Loads (<%= loads.length %>)</h2>
|
||||||
<h2 style="font-size:1.3rem">📋 सभी लोड (<%= loads.length %>)</h2>
|
|
||||||
<a href="/admin" style="font-size:0.8rem">← एडमिन</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" style="padding:0;overflow-x:auto">
|
|
||||||
<table style="width:100%;border-collapse:collapse;font-size:0.8rem;min-width:600px">
|
|
||||||
<tr style="background:var(--gray-100)"><th style="padding:8px;text-align:left">रूट</th><th>वज़न</th><th>बजट</th><th>बोली</th><th>स्थिति</th><th>पोस्टर</th></tr>
|
|
||||||
<% loads.forEach(l => { %>
|
<% loads.forEach(l => { %>
|
||||||
<tr style="border-top:1px solid var(--gray-200)">
|
<a href="/loadboard/<%= l.id %>" class="card card-accent" style="display:block;text-decoration:none;color:inherit;margin-bottom:var(--space-sm)">
|
||||||
<td style="padding:8px"><a href="/loadboard/<%= l.id %>"><%= l.origin_city %> → <%= l.destination_city %></a></td>
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
<td style="padding:8px"><%= l.weight_tons %>T</td>
|
<div><strong><%= l.origin_city %> → <%= l.destination_city %></strong><div style="font-size:0.8rem;color:var(--gray-700)"><%= l.weight_tons %> tons | <%= l.truck_type %></div></div>
|
||||||
<td style="padding:8px"><%= l.budget ? '₹' + Number(l.budget).toLocaleString('en-IN') : '-' %></td>
|
<span class="badge badge-<%= l.status==='open'?'open':l.status==='booked'?'booked':'delivered' %>"><%= l.status %></span>
|
||||||
<td style="padding:8px"><%= l.bid_count %></td>
|
|
||||||
<td style="padding:8px"><span class="badge badge-<%= l.status === 'open' ? 'open' : l.status === 'booked' ? 'booked' : 'delivered' %>"><%= l.status %></span></td>
|
|
||||||
<td style="padding:8px"><%= l.poster ? l.poster.name : '-' %></td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
|
<% }) %>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<%- include('../partials/footer') %>
|
<%- include('../partials/footer') %>
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,15 @@
|
||||||
<% var title = 'उपयोगकर्ता प्रबंधन'; %>
|
<% var title = 'Admin - Users'; %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="section" style="padding-top:var(--space-lg)">
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-md)">
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">👥 All Users (<%= users.length %>)</h2>
|
||||||
<h2 style="font-size:1.3rem">👥 उपयोगकर्ता (<%= users.length %>)</h2>
|
|
||||||
<a href="/admin" style="font-size:0.8rem">← एडमिन</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="GET" action="/admin/users" style="display:flex;gap:var(--space-sm);margin-bottom:var(--space-md)">
|
|
||||||
<input type="text" name="search" class="form-input" placeholder="नाम या यूज़रनेम खोजें" value="<%= filters.search || '' %>" style="padding:8px 12px">
|
|
||||||
<select name="role" class="form-input form-select" style="width:auto;padding:8px 12px">
|
|
||||||
<option value="all">सभी</option>
|
|
||||||
<option value="driver" <%= filters.role === 'driver' ? 'selected' : '' %>>ड्राइवर</option>
|
|
||||||
<option value="shipper" <%= filters.role === 'shipper' ? 'selected' : '' %>>शिपर</option>
|
|
||||||
<option value="broker" <%= filters.role === 'broker' ? 'selected' : '' %>>ब्रोकर</option>
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-primary btn-sm">खोजें</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="card" style="padding:0;overflow-x:auto">
|
|
||||||
<table style="width:100%;border-collapse:collapse;font-size:0.8rem;min-width:500px">
|
|
||||||
<tr style="background:var(--gray-100)"><th style="padding:8px;text-align:left">नाम</th><th>यूज़रनेम</th><th>भूमिका</th><th>स्थिति</th><th>कार्रवाई</th></tr>
|
|
||||||
<% users.forEach(u => { %>
|
<% users.forEach(u => { %>
|
||||||
<tr style="border-top:1px solid var(--gray-200)">
|
<div class="card" style="padding:12px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center">
|
||||||
<td style="padding:8px"><%= u.name %></td>
|
<div><strong><%= u.name || u.username %></strong><div style="font-size:0.75rem;color:var(--gray-700)">@<%= u.username %> | <%= u.phone || '' %></div></div>
|
||||||
<td style="padding:8px;color:var(--gray-700)"><%= u.username %></td>
|
<span class="badge badge-<%= u.role==='driver'?'transit':u.role==='shipper'?'open':'booked' %>"><%= u.role %></span>
|
||||||
<td style="padding:8px"><span class="badge badge-open"><%= u.role %></span></td>
|
|
||||||
<td style="padding:8px"><span class="badge badge-<%= u.is_active ? 'delivered' : 'cancelled' %>"><%= u.is_active ? 'सक्रिय' : 'निलंबित' %></span></td>
|
|
||||||
<td style="padding:8px">
|
|
||||||
<form method="POST" action="/admin/users/<%= u.id %>/suspend" style="display:inline">
|
|
||||||
<button class="btn btn-sm" style="padding:4px 8px;font-size:0.7rem;background:<%= u.is_active ? 'var(--red)' : 'var(--green)' %>;color:#fff"><%= u.is_active ? 'निलंबित' : 'सक्रिय' %></button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
<% }) %>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<%- include('../partials/footer') %>
|
<%- include('../partials/footer') %>
|
||||||
|
|
|
||||||
26
webapp/src/views/pages/bank.ejs
Normal file
26
webapp/src/views/pages/bank.ejs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<% var title = 'Bank'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🏦 Bank Accounts</h2>
|
||||||
|
<% accounts.forEach(a => { %>
|
||||||
|
<div class="card" style="padding:12px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div><strong><%= a.bank_name %></strong><div style="font-size:0.75rem;color:var(--gray-700)"><%= a.account_holder || '' %> | <%= a.upi_id || a.account_number || '' %></div></div>
|
||||||
|
<form method="POST" action="/bank/delete/<%= a.id %>" style="margin:0"><button class="btn btn-sm" style="color:red">✕</button></form>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<form method="POST" action="/bank/add" class="card" style="padding:var(--space-md);margin-top:var(--space-lg)">
|
||||||
|
<h4 style="margin-bottom:var(--space-sm)">➕ Add Account</h4>
|
||||||
|
<div style="display:grid;gap:var(--space-sm)">
|
||||||
|
<input type="text" name="bank_name" class="form-input" placeholder="🏦 Bank Name" required>
|
||||||
|
<input type="text" name="account_holder" class="form-input" placeholder="👤 Account Holder">
|
||||||
|
<input type="text" name="account_number" class="form-input" placeholder="Account Number">
|
||||||
|
<input type="text" name="ifsc" class="form-input" placeholder="IFSC Code">
|
||||||
|
<input type="text" name="upi_id" class="form-input" placeholder="📱 UPI ID (name@upi)">
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
<% var title = 'ब्रोकर डैशबोर्ड'; %>
|
<% var title = t('dashboard.brokerTitle'); %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="section" style="padding-top:var(--space-lg)">
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🤝 नमस्ते, <%= user.name %>!</h2>
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🤝 <%= t('dashboard.hello') %>, <%= user.name %>!</h2>
|
||||||
|
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-card"><div class="stat-value"><%= stats.totalLoads %></div><div class="stat-label">लोड पोस्ट</div></div>
|
<div class="stat-card"><div class="stat-value"><%= stats.totalLoads %></div><div class="stat-label"><%= t('dashboard.loadsPosted') %></div></div>
|
||||||
<div class="stat-card"><div class="stat-value"><%= stats.bookedLoads %></div><div class="stat-label">सौदे</div></div>
|
<div class="stat-card"><div class="stat-value"><%= stats.bookedLoads %></div><div class="stat-label"><%= t('dashboard.deals') %></div></div>
|
||||||
<div class="stat-card"><div class="stat-value"><%= stats.activeTrips %></div><div class="stat-label">सक्रिय</div></div>
|
<div class="stat-card"><div class="stat-value"><%= stats.activeTrips %></div><div class="stat-label"><%= t('dashboard.activeTrips') %></div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if (recentLoads.length > 0) { %>
|
<% if (recentLoads.length > 0) { %>
|
||||||
<h3 style="font-size:1rem;margin-top:var(--space-lg);margin-bottom:var(--space-sm)">📋 हाल के लोड</h3>
|
<h3 style="font-size:1rem;margin-top:var(--space-lg);margin-bottom:var(--space-sm)">📋 <%= t('dashboard.recentLoads') %></h3>
|
||||||
<% recentLoads.forEach(load => { %>
|
<% recentLoads.forEach(load => { %>
|
||||||
<a href="/loadboard/<%= load.id %>" class="card card-accent" style="display:block;text-decoration:none;color:inherit;margin-bottom:var(--space-sm)">
|
<a href="/loadboard/<%= load.id %>" class="card card-accent" style="display:block;text-decoration:none;color:inherit;margin-bottom:var(--space-sm)">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
<div>
|
<div>
|
||||||
<strong><%= load.origin_city %> → <%= load.destination_city %></strong>
|
<strong><%= load.origin_city %> → <%= load.destination_city %></strong>
|
||||||
<div style="font-size:0.8rem;color:var(--gray-700)"><%= load.weight_tons %> टन | 🏷️ <%= load.bid_count %> बोली</div>
|
<div style="font-size:0.8rem;color:var(--gray-700)"><%= load.weight_tons %> <%= t('common.tons') %> | 🏷️ <%= load.bid_count %> <%= t('common.bids') %></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge badge-<%= load.status === 'open' ? 'open' : 'booked' %>"><%= load.status %></span>
|
<span class="badge badge-<%= load.status === 'open' ? 'open' : 'booked' %>"><%= load.status %></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -27,9 +27,39 @@
|
||||||
<% }) %>
|
<% }) %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<div style="margin-top:var(--space-lg);display:grid;gap:var(--space-sm)">
|
<div style="margin-top:var(--space-lg);display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
||||||
<a href="/loadboard/post" class="btn btn-cta btn-block">+ लोड पोस्ट करें</a>
|
<a href="/loadboard/post" class="icon-action-btn">
|
||||||
<a href="/loadboard" class="btn btn-outline btn-block">📋 लोड बोर्ड</a>
|
<span class="icon-action-emoji">➕</span>
|
||||||
|
<span class="icon-action-label"><%= t('actions.postLoad') %></span>
|
||||||
|
</a>
|
||||||
|
<a href="/loadboard" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">📋</span>
|
||||||
|
<span class="icon-action-label"><%= t('actions.viewLoads') %></span>
|
||||||
|
</a>
|
||||||
|
<a href="/invoice" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🧾</span>
|
||||||
|
<span class="icon-action-label">Invoice</span>
|
||||||
|
</a>
|
||||||
|
<a href="/rates" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">📊</span>
|
||||||
|
<span class="icon-action-label">Rates</span>
|
||||||
|
</a>
|
||||||
|
<a href="/fleet" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🚛</span>
|
||||||
|
<span class="icon-action-label">Fleet</span>
|
||||||
|
</a>
|
||||||
|
<a href="/classifieds" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🛒</span>
|
||||||
|
<span class="icon-action-label">Buy/Sell</span>
|
||||||
|
</a>
|
||||||
|
<a href="/referral" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🤝</span>
|
||||||
|
<span class="icon-action-label">Referral</span>
|
||||||
|
</a>
|
||||||
|
<a href="/notifications" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🔔</span>
|
||||||
|
<span class="icon-action-label">Alerts</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
30
webapp/src/views/pages/challenges.ejs
Normal file
30
webapp/src/views/pages/challenges.ejs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<% var title = 'Challenges'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🎯 Daily Challenges</h2>
|
||||||
|
<p style="color:var(--gray-700);font-size:0.85rem;margin-bottom:var(--space-md)">Complete tasks to earn XP! (<%= completedCount %>/3 done today)</p>
|
||||||
|
<div style="display:grid;gap:var(--space-sm)">
|
||||||
|
<% challenges.forEach(c => { %>
|
||||||
|
<div class="card card-accent" style="padding:16px;<%= c.completed ? 'opacity:0.6;border-left-color:var(--green)' : '' %>">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div style="display:flex;gap:12px;align-items:center">
|
||||||
|
<span style="font-size:1.5rem"><%= c.icon %></span>
|
||||||
|
<div>
|
||||||
|
<strong><%= c.title_hi %></strong>
|
||||||
|
<div style="font-size:0.75rem;color:var(--gray-700)"><%= c.title %> • +<%= c.xp %> XP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% if (c.completed) { %>
|
||||||
|
<span style="color:green;font-size:1.2rem">✅</span>
|
||||||
|
<% } else { %>
|
||||||
|
<form method="POST" action="/challenges/complete" style="margin:0"><input type="hidden" name="challenge_id" value="<%= c.id %>"><button class="btn btn-sm btn-primary">Done</button></form>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
|
|
@ -1,29 +1,24 @@
|
||||||
<% var title = otherUser.name + ' — चैट'; %>
|
<% var title = t('nav.messages'); %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg);padding-bottom:80px">
|
||||||
<section class="section" style="padding-top:var(--space-lg)">
|
<div class="container" style="max-width:600px">
|
||||||
<div class="container" style="max-width:500px">
|
<a href="/messages" style="font-size:0.8rem;color:var(--gray-700)">← <%= t('nav.messages') %></a>
|
||||||
<div style="display:flex;align-items:center;gap:var(--space-sm);margin-bottom:var(--space-md)">
|
<h3 style="margin:8px 0 var(--space-md)">💬 <%= otherUser.name || otherUser.username %></h3>
|
||||||
<a href="/messages" style="font-size:1.2rem">←</a>
|
<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:var(--space-lg)">
|
||||||
<strong><%= otherUser.name %></strong>
|
<% messages.forEach(m => { const isMine = m.sender_id === user.id; %>
|
||||||
<span style="font-size:0.75rem;color:var(--gray-700)">@<%= otherUser.username %></span>
|
<div style="align-self:<%= isMine ? 'flex-end' : 'flex-start' %>;background:<%= isMine ? 'var(--navy)' : 'var(--gray-100)' %>;color:<%= isMine ? '#fff' : 'inherit' %>;padding:10px 14px;border-radius:12px;max-width:80%;font-size:0.85rem">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:var(--space-sm);margin-bottom:var(--space-md);max-height:400px;overflow-y:auto">
|
|
||||||
<% messages.forEach(m => { %>
|
|
||||||
<div style="align-self:<%= m.sender_id === user.id ? 'flex-end' : 'flex-start' %>;max-width:80%;padding:8px 12px;border-radius:12px;font-size:0.85rem;background:<%= m.sender_id === user.id ? 'var(--navy)' : 'var(--gray-200)' %>;color:<%= m.sender_id === user.id ? '#fff' : 'var(--gray-900)' %>">
|
|
||||||
<%= m.content %>
|
<%= m.content %>
|
||||||
<div style="font-size:0.6rem;opacity:0.7;margin-top:2px"><%= new Date(m.created_at).toLocaleTimeString('hi-IN', {hour:'2-digit',minute:'2-digit'}) %></div>
|
<div style="font-size:0.65rem;opacity:0.7;margin-top:4px"><%= new Date(m.created_at).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'}) %></div>
|
||||||
</div>
|
</div>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</div>
|
</div>
|
||||||
|
<form method="POST" action="/messages/<%= otherUser.id %>" style="position:fixed;bottom:70px;left:0;right:0;padding:8px 16px;background:#fff;border-top:1px solid var(--gray-200)">
|
||||||
<form method="POST" action="/messages/<%= otherId %>" style="display:flex;gap:var(--space-sm)">
|
<div style="display:grid;grid-template-columns:1fr auto;gap:8px">
|
||||||
<input type="text" name="content" class="form-input" placeholder="संदेश लिखें..." required autofocus style="flex:1;padding:10px 14px">
|
<input type="text" name="content" class="form-input" placeholder="<%= t('messages.typeHere') %>" required autofocus>
|
||||||
<button type="submit" class="btn btn-primary">भेजें</button>
|
<button type="submit" class="btn btn-primary">📤</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<%- include('../partials/footer') %>
|
<%- include('../partials/footer') %>
|
||||||
|
|
|
||||||
22
webapp/src/views/pages/classifieds-post.ejs
Normal file
22
webapp/src/views/pages/classifieds-post.ejs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<% var title = 'Post Ad'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container" style="max-width:500px">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">📝 Post Ad</h2>
|
||||||
|
<form method="POST" action="/classifieds/post" class="card" style="padding:var(--space-md)">
|
||||||
|
<div style="display:grid;gap:var(--space-sm)">
|
||||||
|
<input type="text" name="title" class="form-input" placeholder="Title (e.g. Tata 407 for sale)" required>
|
||||||
|
<textarea name="description" class="form-input" placeholder="Description" rows="3"></textarea>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-sm)">
|
||||||
|
<input type="number" name="price" class="form-input" placeholder="₹ Price" required>
|
||||||
|
<select name="category" class="form-input form-select"><option value="truck">🚛 Truck</option><option value="parts">🔧 Parts</option><option value="tyres">⭕ Tyres</option><option value="other">📦 Other</option></select>
|
||||||
|
</div>
|
||||||
|
<input type="text" name="location" class="form-input" placeholder="📍 Location">
|
||||||
|
<input type="tel" name="contact_phone" class="form-input" placeholder="📱 Contact Phone">
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">Post Ad</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
28
webapp/src/views/pages/classifieds.ejs
Normal file
28
webapp/src/views/pages/classifieds.ejs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<% var title = 'Buy/Sell'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-md)">
|
||||||
|
<h2 style="font-size:1.3rem">🛒 Buy / Sell</h2>
|
||||||
|
<a href="/classifieds/post" class="btn btn-cta btn-sm">+ Post</a>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px;margin-bottom:var(--space-md);flex-wrap:wrap">
|
||||||
|
<a href="/classifieds" class="btn btn-sm <%= category==='all'?'btn-primary':'btn-outline' %>">All</a>
|
||||||
|
<a href="/classifieds?category=truck" class="btn btn-sm <%= category==='truck'?'btn-primary':'btn-outline' %>">🚛 Trucks</a>
|
||||||
|
<a href="/classifieds?category=parts" class="btn btn-sm <%= category==='parts'?'btn-primary':'btn-outline' %>">🔧 Parts</a>
|
||||||
|
<a href="/classifieds?category=tyres" class="btn btn-sm <%= category==='tyres'?'btn-primary':'btn-outline' %>">⭕ Tyres</a>
|
||||||
|
</div>
|
||||||
|
<% if (listings.length === 0) { %>
|
||||||
|
<div class="card text-center" style="padding:var(--space-2xl)"><p>No listings yet.</p></div>
|
||||||
|
<% } else { listings.forEach(l => { %>
|
||||||
|
<div class="card card-accent" style="margin-bottom:var(--space-sm)">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:start">
|
||||||
|
<div><strong><%= l.title %></strong><div style="font-size:0.8rem;color:var(--gray-700)">📍 <%= l.location || '' %> | <%= l.category %></div></div>
|
||||||
|
<div style="font-weight:700;color:var(--navy)">₹<%= (l.price||0).toLocaleString('en-IN') %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
25
webapp/src/views/pages/documents.ejs
Normal file
25
webapp/src/views/pages/documents.ejs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<% var title = 'Documents'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">📄 Document Vault</h2>
|
||||||
|
<% documents.forEach(d => { %>
|
||||||
|
<div class="card" style="padding:12px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div><strong><%= d.doc_type.toUpperCase() %></strong> — <%= d.vehicle_number %><div style="font-size:0.75rem;color:var(--gray-700)"><%= d.doc_number || '' %> <% if(d.expiry_date){%>| Exp: <%= d.expiry_date %><%}%></div></div>
|
||||||
|
<form method="POST" action="/documents/delete/<%= d.id %>" style="margin:0"><button class="btn btn-sm" style="color:red">✕</button></form>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<form method="POST" action="/documents/add" class="card" style="padding:var(--space-md);margin-top:var(--space-lg)">
|
||||||
|
<h4 style="margin-bottom:var(--space-sm)">➕ Add Document</h4>
|
||||||
|
<div style="display:grid;gap:var(--space-sm)">
|
||||||
|
<select name="doc_type" class="form-input form-select" required><option value="rc">RC</option><option value="insurance">Insurance</option><option value="permit">Permit</option><option value="license">License</option><option value="puc">PUC</option><option value="fitness">Fitness</option></select>
|
||||||
|
<input type="text" name="vehicle_number" class="form-input" placeholder="🚛 Vehicle Number" required>
|
||||||
|
<input type="text" name="doc_number" class="form-input" placeholder="Document Number">
|
||||||
|
<input type="date" name="expiry_date" class="form-input">
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
<% var title = 'ड्राइवर डैशबोर्ड'; %>
|
<% var title = 'Driver Dashboard'; %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="section" style="padding-top:var(--space-lg)">
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🚛 नमस्ते, <%= user.name %>!</h2>
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🚛 <%= t('dashboard.hello') %>, <%= user.name %>!</h2>
|
||||||
|
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-card"><div class="stat-value"><%= stats.totalTrips %></div><div class="stat-label">कुल ट्रिप</div></div>
|
<div class="stat-card"><div class="stat-value"><%= stats.totalTrips %></div><div class="stat-label"><%= t('dashboard.totalTrips') %></div></div>
|
||||||
<div class="stat-card"><div class="stat-value"><%= stats.activeBids %></div><div class="stat-label">सक्रिय बोलियाँ</div></div>
|
<div class="stat-card"><div class="stat-value"><%= stats.activeBids %></div><div class="stat-label"><%= t('dashboard.activeBids') %></div></div>
|
||||||
<div class="stat-card"><div class="stat-value">₹<%= stats.earnings.toLocaleString('en-IN') %></div><div class="stat-label">कमाई</div></div>
|
<div class="stat-card"><div class="stat-value">₹<%= stats.earnings.toLocaleString('en-IN') %></div><div class="stat-label"><%= t('dashboard.earnings') %></div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if (activeTrips.length > 0) { %>
|
<% if (activeTrips.length > 0) { %>
|
||||||
<h3 style="font-size:1rem;margin-top:var(--space-lg);margin-bottom:var(--space-sm)">🔄 सक्रिय ट्रिप</h3>
|
<h3 style="font-size:1rem;margin-top:var(--space-lg);margin-bottom:var(--space-sm)">🔄 <%= t('dashboard.activeTrips') %></h3>
|
||||||
<% activeTrips.forEach(trip => { %>
|
<% activeTrips.forEach(trip => { %>
|
||||||
<div class="card card-accent" style="margin-bottom:var(--space-sm)">
|
<div class="card card-accent" style="margin-bottom:var(--space-sm)">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
|
@ -27,9 +27,39 @@
|
||||||
<% }) %>
|
<% }) %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<div style="margin-top:var(--space-lg);display:grid;gap:var(--space-sm)">
|
<div style="margin-top:var(--space-lg);display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
||||||
<a href="/loadboard" class="btn btn-primary btn-block">📋 लोड बोर्ड देखें</a>
|
<a href="/loadboard" class="icon-action-btn">
|
||||||
<a href="/trips" class="btn btn-outline btn-block">🚚 मेरी ट्रिप</a>
|
<span class="icon-action-emoji">📋</span>
|
||||||
|
<span class="icon-action-label"><%= t('actions.viewLoads') %></span>
|
||||||
|
</a>
|
||||||
|
<a href="/driver/ledger" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">📒</span>
|
||||||
|
<span class="icon-action-label">Ledger</span>
|
||||||
|
</a>
|
||||||
|
<a href="/trip-planner" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🧮</span>
|
||||||
|
<span class="icon-action-label">Trip Cost</span>
|
||||||
|
</a>
|
||||||
|
<a href="/returnload" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🔄</span>
|
||||||
|
<span class="icon-action-label">Return Load</span>
|
||||||
|
</a>
|
||||||
|
<a href="/safety" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🛡️</span>
|
||||||
|
<span class="icon-action-label">Safety</span>
|
||||||
|
</a>
|
||||||
|
<a href="/maintenance" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🔧</span>
|
||||||
|
<span class="icon-action-label">Reminders</span>
|
||||||
|
</a>
|
||||||
|
<a href="/fastag" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🏷️</span>
|
||||||
|
<span class="icon-action-label">FASTag</span>
|
||||||
|
</a>
|
||||||
|
<a href="/notifications" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🔔</span>
|
||||||
|
<span class="icon-action-label">Alerts</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
25
webapp/src/views/pages/driver-ledger-add.ejs
Normal file
25
webapp/src/views/pages/driver-ledger-add.ejs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<% var title = 'Add Trip'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container" style="max-width:500px">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">➕ Add Trip to Ledger</h2>
|
||||||
|
<form method="POST" action="/driver/ledger/add" class="card" style="padding:var(--space-md)">
|
||||||
|
<div style="display:grid;gap:var(--space-sm)">
|
||||||
|
<div><label class="form-label">📍 From</label><input type="text" name="origin" class="form-input" placeholder="City" required></div>
|
||||||
|
<div><label class="form-label">📍 To</label><input type="text" name="destination" class="form-input" placeholder="City" required></div>
|
||||||
|
<div><label class="form-label">📅 Date</label><input type="date" name="trip_date" class="form-input" value="<%= new Date().toISOString().split('T')[0] %>"></div>
|
||||||
|
<div><label class="form-label">💰 Freight Received ₹</label><input type="number" name="freight_received" class="form-input" placeholder="25000" required></div>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-sm)">
|
||||||
|
<div><label class="form-label">⛽ Fuel ₹</label><input type="number" name="fuel_cost" class="form-input" placeholder="8000"></div>
|
||||||
|
<div><label class="form-label">🚧 Toll ₹</label><input type="number" name="toll_cost" class="form-input" placeholder="3000"></div>
|
||||||
|
</div>
|
||||||
|
<div><label class="form-label">📦 Other ₹</label><input type="number" name="other_expense" class="form-input" placeholder="1000"></div>
|
||||||
|
<div><label class="form-label">📝 Notes</label><input type="text" name="notes" class="form-input" placeholder="Optional"></div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block btn-lg">💾 Save Trip</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<a href="/driver/ledger" class="btn btn-outline btn-block" style="margin-top:var(--space-sm)">← Back</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
34
webapp/src/views/pages/driver-ledger.ejs
Normal file
34
webapp/src/views/pages/driver-ledger.ejs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<% var title = 'My Ledger'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-md)">
|
||||||
|
<h2 style="font-size:1.3rem">📒 My Ledger</h2>
|
||||||
|
<a href="/driver/ledger/add" class="btn btn-cta btn-sm">+ Add Trip</a>
|
||||||
|
</div>
|
||||||
|
<div class="stats-grid" style="margin-bottom:var(--space-md)">
|
||||||
|
<div class="stat-card"><div class="stat-value"><%= stats.total_trips %></div><div class="stat-label">🚛 Trips</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value">₹<%= stats.total_earned.toLocaleString('en-IN') %></div><div class="stat-label">💰 Earned</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value">₹<%= stats.total_expenses.toLocaleString('en-IN') %></div><div class="stat-label">💸 Expenses</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value" style="color:<%= stats.net_profit >= 0 ? 'green' : 'red' %>">₹<%= stats.net_profit.toLocaleString('en-IN') %></div><div class="stat-label">📊 Profit</div></div>
|
||||||
|
</div>
|
||||||
|
<% if (entries.length === 0) { %>
|
||||||
|
<div class="card text-center" style="padding:var(--space-2xl)"><p>📭 No entries yet. Add your first trip!</p></div>
|
||||||
|
<% } else { entries.forEach(e => { %>
|
||||||
|
<div class="card card-accent" style="margin-bottom:var(--space-sm)">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:start">
|
||||||
|
<div>
|
||||||
|
<strong>📍 <%= e.origin %> → <%= e.destination %></strong>
|
||||||
|
<div style="font-size:0.8rem;color:var(--gray-700);margin-top:4px">📅 <%= e.trip_date %> | ⛽ ₹<%= (e.fuel_cost||0).toLocaleString('en-IN') %> | 🚧 ₹<%= (e.toll_cost||0).toLocaleString('en-IN') %></div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:right">
|
||||||
|
<div style="font-weight:700;color:green">+₹<%= (e.freight_received||0).toLocaleString('en-IN') %></div>
|
||||||
|
<form method="POST" action="/driver/ledger/delete/<%= e.id %>" style="margin:0"><button class="btn btn-sm" style="color:red;font-size:0.7rem">✕</button></form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
40
webapp/src/views/pages/fastag.ejs
Normal file
40
webapp/src/views/pages/fastag.ejs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<% var title = 'FASTag'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🏷️ FASTag & Tolls</h2>
|
||||||
|
<% if (!fastag) { %>
|
||||||
|
<form method="POST" action="/fastag/register" class="card" style="padding:var(--space-md)">
|
||||||
|
<h4 style="margin-bottom:var(--space-sm)">Register FASTag</h4>
|
||||||
|
<div style="display:grid;gap:var(--space-sm)">
|
||||||
|
<input type="text" name="fastag_number" class="form-input" placeholder="FASTag Number" required>
|
||||||
|
<input type="text" name="vehicle_number" class="form-input" placeholder="🚛 Vehicle Number" required>
|
||||||
|
<input type="text" name="issuer_bank" class="form-input" placeholder="🏦 Bank (optional)">
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">Register</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="stats-grid" style="margin-bottom:var(--space-md)">
|
||||||
|
<div class="stat-card"><div class="stat-value">₹<%= stats.balance.toLocaleString('en-IN') %></div><div class="stat-label">💳 Balance</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value">₹<%= stats.month_spend.toLocaleString('en-IN') %></div><div class="stat-label">📅 This Month</div></div>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="/fastag/log-toll" class="card" style="padding:var(--space-md);margin-bottom:var(--space-md)">
|
||||||
|
<h4 style="margin-bottom:var(--space-sm)">🚧 Log Toll</h4>
|
||||||
|
<div style="display:grid;grid-template-columns:2fr 1fr auto;gap:var(--space-sm);align-items:end">
|
||||||
|
<input type="text" name="plaza_name" class="form-input" placeholder="Toll Name">
|
||||||
|
<input type="number" name="amount" class="form-input" placeholder="₹" required>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">+</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<h3 style="font-size:1rem;margin-bottom:var(--space-sm)">📋 Recent</h3>
|
||||||
|
<% history.slice(0,15).forEach(h => { %>
|
||||||
|
<div class="card" style="padding:10px;margin-bottom:6px;display:flex;justify-content:space-between">
|
||||||
|
<div><strong><%= h.type==='toll'?'🚧':'💳' %> <%= h.plaza_name || h.type %></strong><div style="font-size:0.7rem;color:var(--gray-700)"><%= new Date(h.created_at).toLocaleDateString('en-IN') %></div></div>
|
||||||
|
<span style="font-weight:700;color:<%= h.type==='toll'?'red':'green' %>"><%= h.type==='toll'?'-':'+' %>₹<%= (h.amount||0).toLocaleString('en-IN') %></span>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
23
webapp/src/views/pages/feed.ejs
Normal file
23
webapp/src/views/pages/feed.ejs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<% var title = 'Activity Feed'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">📰 Activity Feed</h2>
|
||||||
|
<% if (events.length === 0) { %>
|
||||||
|
<div class="card text-center" style="padding:var(--space-2xl)"><p>No activity yet. Be the first!</p></div>
|
||||||
|
<% } else { events.forEach(e => { const d = e.data || {}; %>
|
||||||
|
<div class="card" style="padding:12px;margin-bottom:8px">
|
||||||
|
<div style="display:flex;gap:10px;align-items:start">
|
||||||
|
<span style="font-size:1.3rem"><%= e.event_type==='bid_placed'?'🏷️':e.event_type==='load_posted'?'📦':e.event_type==='trip_completed'?'✅':'📣' %></span>
|
||||||
|
<div>
|
||||||
|
<strong style="font-size:0.85rem"><%= d.title || e.event_type %></strong>
|
||||||
|
<% if (d.subtitle) { %><div style="font-size:0.75rem;color:var(--gray-700)"><%= d.subtitle %></div><% } %>
|
||||||
|
<div style="font-size:0.7rem;color:var(--gray-500);margin-top:2px"><%= new Date(e.created_at).toLocaleString('en-IN') %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
33
webapp/src/views/pages/fleet.ejs
Normal file
33
webapp/src/views/pages/fleet.ejs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<% var title = 'Fleet'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🚛 My Fleet</h2>
|
||||||
|
<% vehicles.forEach(v => { %>
|
||||||
|
<div class="card card-accent" style="margin-bottom:var(--space-sm)">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div>
|
||||||
|
<strong><%= v.vehicle_number %></strong> — <%= v.vehicle_type %>
|
||||||
|
<div style="font-size:0.8rem;color:var(--gray-700)"><%= v.driver_name || 'No driver' %> | <%= v.capacity_tons %> tons</div>
|
||||||
|
</div>
|
||||||
|
<span class="badge badge-<%= v.status==='available'?'open':v.status==='on_trip'?'transit':'booked' %>"><%= v.status %></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<form method="POST" action="/fleet/add" class="card" style="padding:var(--space-md);margin-top:var(--space-lg)">
|
||||||
|
<h4 style="margin-bottom:var(--space-sm)">➕ Add Vehicle</h4>
|
||||||
|
<div style="display:grid;gap:var(--space-sm)">
|
||||||
|
<input type="text" name="vehicle_number" class="form-input" placeholder="🚛 Vehicle Number" required>
|
||||||
|
<select name="vehicle_type" class="form-input form-select"><option value="open">Open</option><option value="container">Container</option><option value="trailer">Trailer</option><option value="tanker">Tanker</option></select>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-sm)">
|
||||||
|
<input type="text" name="driver_name" class="form-input" placeholder="👤 Driver Name">
|
||||||
|
<input type="tel" name="driver_phone" class="form-input" placeholder="📱 Phone">
|
||||||
|
</div>
|
||||||
|
<input type="number" name="capacity_tons" class="form-input" placeholder="⚖️ Capacity (tons)">
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">Add Vehicle</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
15
webapp/src/views/pages/games-hub.ejs
Normal file
15
webapp/src/views/pages/games-hub.ejs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<% var title = 'Games'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🎮 Mini Games</h2>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
||||||
|
<a href="/games/rate-guesser" class="icon-action-btn"><span class="icon-action-emoji">💰</span><span class="icon-action-label">Rate Guesser</span></a>
|
||||||
|
<a href="/games/route-quiz" class="icon-action-btn"><span class="icon-action-emoji">🗺️</span><span class="icon-action-label">Route Quiz</span></a>
|
||||||
|
<a href="/challenges" class="icon-action-btn"><span class="icon-action-emoji">🎯</span><span class="icon-action-label">Challenges</span></a>
|
||||||
|
<a href="/leaderboard" class="icon-action-btn"><span class="icon-action-emoji">🏆</span><span class="icon-action-label">Leaderboard</span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
32
webapp/src/views/pages/games-rate-guesser.ejs
Normal file
32
webapp/src/views/pages/games-rate-guesser.ejs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<% var title = 'Rate Guesser'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container" style="max-width:500px;text-align:center">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">💰 Rate Guesser</h2>
|
||||||
|
<div class="card" style="padding:var(--space-lg)">
|
||||||
|
<p style="font-size:0.85rem;color:var(--gray-700)">Guess the freight rate:</p>
|
||||||
|
<h3 style="margin:12px 0">📍 <%= load.origin_city %> → <%= load.destination_city %></h3>
|
||||||
|
<p style="font-size:0.9rem">⚖️ <%= load.weight_tons %> tons</p>
|
||||||
|
<% if (!revealed) { %>
|
||||||
|
<form method="POST" action="/games/rate-guesser/guess" style="margin-top:var(--space-md)">
|
||||||
|
<input type="hidden" name="actual" value="<%= load.budget %>">
|
||||||
|
<input type="hidden" name="origin" value="<%= load.origin_city %>">
|
||||||
|
<input type="hidden" name="destination" value="<%= load.destination_city %>">
|
||||||
|
<input type="hidden" name="weight" value="<%= load.weight_tons %>">
|
||||||
|
<input type="number" name="guess" class="form-input" placeholder="₹ Your guess" required style="text-align:center;font-size:1.2rem">
|
||||||
|
<button type="submit" class="btn btn-primary btn-block" style="margin-top:var(--space-sm)">Submit Guess</button>
|
||||||
|
</form>
|
||||||
|
<% } else { %>
|
||||||
|
<div style="margin-top:var(--space-md)">
|
||||||
|
<p>Your guess: <strong>₹<%= guess.toLocaleString('en-IN') %></strong></p>
|
||||||
|
<p>Actual rate: <strong style="color:var(--navy)">₹<%= load.budget.toLocaleString('en-IN') %></strong></p>
|
||||||
|
<div style="margin:12px 0;font-size:1.5rem;font-weight:700;color:<%= accuracy >= 70 ? 'green' : accuracy >= 50 ? 'orange' : 'red' %>"><%= accuracy %>% accurate</div>
|
||||||
|
<p style="color:var(--gray-700)">+<%= xpEarned %> XP earned! 🎉</p>
|
||||||
|
</div>
|
||||||
|
<a href="/games/rate-guesser" class="btn btn-primary btn-block" style="margin-top:var(--space-md)">Play Again</a>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
30
webapp/src/views/pages/games-route-quiz.ejs
Normal file
30
webapp/src/views/pages/games-route-quiz.ejs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<% var title = 'Route Quiz'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container" style="max-width:500px;text-align:center">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🗺️ Route Quiz</h2>
|
||||||
|
<div class="card" style="padding:var(--space-lg)">
|
||||||
|
<p style="font-size:0.85rem;color:var(--gray-700)">Guess the distance (km):</p>
|
||||||
|
<h3 style="margin:12px 0">📍 <%= origin %> → <%= destination %></h3>
|
||||||
|
<% if (!revealed) { %>
|
||||||
|
<form method="POST" action="/games/route-quiz/guess" style="margin-top:var(--space-md)">
|
||||||
|
<input type="hidden" name="actual_km" value="<%= actual_km %>">
|
||||||
|
<input type="hidden" name="origin" value="<%= origin %>">
|
||||||
|
<input type="hidden" name="destination" value="<%= destination %>">
|
||||||
|
<input type="number" name="guess" class="form-input" placeholder="Distance in km" required style="text-align:center;font-size:1.2rem">
|
||||||
|
<button type="submit" class="btn btn-primary btn-block" style="margin-top:var(--space-sm)">Submit</button>
|
||||||
|
</form>
|
||||||
|
<% } else { %>
|
||||||
|
<div style="margin-top:var(--space-md)">
|
||||||
|
<p>Your guess: <strong><%= guess %> km</strong></p>
|
||||||
|
<p>Actual: <strong style="color:var(--navy)"><%= actual_km %> km</strong></p>
|
||||||
|
<div style="margin:12px 0;font-size:1.5rem;font-weight:700;color:<%= accuracy >= 70 ? 'green' : accuracy >= 50 ? 'orange' : 'red' %>"><%= accuracy %>% accurate</div>
|
||||||
|
<p style="color:var(--gray-700)">+<%= xpEarned %> XP earned! 🎉</p>
|
||||||
|
</div>
|
||||||
|
<a href="/games/route-quiz" class="btn btn-primary btn-block" style="margin-top:var(--space-md)">Play Again</a>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
28
webapp/src/views/pages/gamification.ejs
Normal file
28
webapp/src/views/pages/gamification.ejs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<% var title = 'My Level'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<div class="card" style="padding:var(--space-lg);text-align:center;margin-bottom:var(--space-md)">
|
||||||
|
<div style="font-size:3rem"><%= level.icon %></div>
|
||||||
|
<h2 style="margin:8px 0 4px">Level <%= level.level %> — <%= level.title %></h2>
|
||||||
|
<p style="color:var(--gray-700);font-size:0.85rem"><%= xp %> XP</p>
|
||||||
|
<div style="background:var(--gray-200);border-radius:20px;height:12px;margin-top:12px;overflow:hidden">
|
||||||
|
<div style="background:linear-gradient(90deg,var(--saffron),var(--ashoka-blue));height:100%;width:<%= level.progress %>%;border-radius:20px"></div>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:0.75rem;color:var(--gray-700);margin-top:4px"><%= level.progress %>% to Level <%= level.level + 1 %></p>
|
||||||
|
<p style="font-size:0.85rem;margin-top:8px">🔥 Streak: <%= streak %> days</p>
|
||||||
|
</div>
|
||||||
|
<h3 style="font-size:1rem;margin-bottom:var(--space-sm)">🏆 Achievements</h3>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:var(--space-sm)">
|
||||||
|
<% achievements.forEach(a => { %>
|
||||||
|
<div class="card text-center" style="padding:12px;opacity:<%= a.earned ? 1 : 0.4 %>">
|
||||||
|
<div style="font-size:1.5rem"><%= a.icon %></div>
|
||||||
|
<div style="font-size:0.7rem;font-weight:700;margin-top:4px"><%= a.title %></div>
|
||||||
|
<div style="font-size:0.65rem;color:var(--gray-700)">+<%= a.xp %> XP</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
25
webapp/src/views/pages/invoice-create.ejs
Normal file
25
webapp/src/views/pages/invoice-create.ejs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<% var title = 'New Invoice'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container" style="max-width:500px">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🧾 Create Invoice</h2>
|
||||||
|
<form method="POST" action="/invoice/create" class="card" style="padding:var(--space-md)">
|
||||||
|
<div style="display:grid;gap:var(--space-sm)">
|
||||||
|
<div><label class="form-label">👤 Client Name</label><input type="text" name="client_name" class="form-input" required></div>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-sm)">
|
||||||
|
<div><label class="form-label">📍 From</label><input type="text" name="origin" class="form-input" required></div>
|
||||||
|
<div><label class="form-label">📍 To</label><input type="text" name="destination" class="form-input" required></div>
|
||||||
|
</div>
|
||||||
|
<div style="display:grid;grid-template-columns:2fr 1fr;gap:var(--space-sm)">
|
||||||
|
<div><label class="form-label">💰 Amount ₹</label><input type="number" name="amount" class="form-input" required></div>
|
||||||
|
<div><label class="form-label">GST %</label><input type="number" name="gst_rate" class="form-input" value="5"></div>
|
||||||
|
</div>
|
||||||
|
<div><label class="form-label">📱 UPI ID (for payment link)</label><input type="text" name="upi_id" class="form-input" placeholder="name@upi"></div>
|
||||||
|
<div><label class="form-label">📝 Notes</label><input type="text" name="notes" class="form-input"></div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block btn-lg">Generate Invoice</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
25
webapp/src/views/pages/invoice-view.ejs
Normal file
25
webapp/src/views/pages/invoice-view.ejs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<% var title = 'Invoice ' + invoice.invoice_number; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container" style="max-width:500px">
|
||||||
|
<div class="card" style="padding:var(--space-lg)">
|
||||||
|
<div style="text-align:center;margin-bottom:var(--space-md)"><h3>🧾 INVOICE</h3><p style="font-size:0.8rem;color:var(--gray-700)"><%= invoice.invoice_number %></p></div>
|
||||||
|
<div style="display:grid;gap:8px;font-size:0.9rem">
|
||||||
|
<div><strong>Client:</strong> <%= invoice.client_name %></div>
|
||||||
|
<div><strong>Route:</strong> <%= invoice.origin %> → <%= invoice.destination %></div>
|
||||||
|
<hr style="border:none;border-top:1px solid var(--gray-200)">
|
||||||
|
<div style="display:flex;justify-content:space-between"><span>Amount:</span><span>₹<%= (invoice.amount||0).toLocaleString('en-IN') %></span></div>
|
||||||
|
<div style="display:flex;justify-content:space-between"><span>GST (<%= invoice.gst_rate %>%):</span><span>₹<%= (invoice.gst_amount||0).toLocaleString('en-IN') %></span></div>
|
||||||
|
<hr style="border:none;border-top:2px solid var(--navy)">
|
||||||
|
<div style="display:flex;justify-content:space-between;font-size:1.1rem;font-weight:700"><span>Total:</span><span>₹<%= (invoice.total_amount||0).toLocaleString('en-IN') %></span></div>
|
||||||
|
</div>
|
||||||
|
<% if (invoice.upi_link) { %>
|
||||||
|
<a href="<%= invoice.upi_link %>" class="btn btn-primary btn-block" style="margin-top:var(--space-lg)">💳 Pay via UPI</a>
|
||||||
|
<% } %>
|
||||||
|
<% if (invoice.notes) { %><p style="font-size:0.8rem;color:var(--gray-700);margin-top:var(--space-md)">📝 <%= invoice.notes %></p><% } %>
|
||||||
|
</div>
|
||||||
|
<a href="/invoice" class="btn btn-outline btn-block" style="margin-top:var(--space-sm)">← Back</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
28
webapp/src/views/pages/invoices.ejs
Normal file
28
webapp/src/views/pages/invoices.ejs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<% var title = 'Invoices'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-md)">
|
||||||
|
<h2 style="font-size:1.3rem">🧾 Invoices</h2>
|
||||||
|
<a href="/invoice/create" class="btn btn-cta btn-sm">+ New</a>
|
||||||
|
</div>
|
||||||
|
<% if (invoices.length === 0) { %>
|
||||||
|
<div class="card text-center" style="padding:var(--space-2xl)"><p>No invoices yet.</p></div>
|
||||||
|
<% } else { invoices.forEach(inv => { %>
|
||||||
|
<a href="/invoice/<%= inv.id %>" class="card card-accent" style="display:block;text-decoration:none;color:inherit;margin-bottom:var(--space-sm)">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div>
|
||||||
|
<strong><%= inv.invoice_number %></strong> — <%= inv.client_name %>
|
||||||
|
<div style="font-size:0.8rem;color:var(--gray-700)"><%= inv.origin %> → <%= inv.destination %></div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:right">
|
||||||
|
<div style="font-weight:700">₹<%= (inv.total_amount||0).toLocaleString('en-IN') %></div>
|
||||||
|
<span class="badge badge-<%= inv.status==='paid'?'open':'transit' %>"><%= inv.status %></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<% }) } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
|
|
@ -1,41 +1,41 @@
|
||||||
<% var title = 'राष्ट्रीय माल परिवहन मंच'; %>
|
<% var title = t('common.subtitle'); %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="hero-badge">🇮🇳 भारत सरकार पंजीकृत मंच | Registered Platform</div>
|
<div class="hero-badge">🇮🇳 <%= t('landing.badge') %></div>
|
||||||
<h1>ट्रक ड्राइवर। शिपर। ब्रोकर।<br><span class="highlight">सबके लिए मुफ्त।</span></h1>
|
<h1><%= t('landing.heroTitle') %><br><span class="highlight"><%= t('landing.heroHighlight') %></span></h1>
|
||||||
<p class="hero-sub">भारत का राष्ट्रीय माल परिवहन मंच — लोड पोस्ट करें, बोली लगाएं, कमाई करें। बिना किसी शुल्क के।</p>
|
<p class="hero-sub"><%= t('landing.heroSub') %></p>
|
||||||
<div class="hero-ctas">
|
<div class="hero-ctas">
|
||||||
<a href="/register" class="btn btn-cta btn-lg">मुफ्त पंजीकरण करें</a>
|
<a href="/register" class="btn btn-cta btn-lg"><%= t('auth.registerBtn') %></a>
|
||||||
<a href="/loadboard" class="btn btn-outline btn-lg" style="border-color:rgba(255,255,255,0.5);color:#fff">लोड बोर्ड देखें</a>
|
<a href="/loadboard" class="btn btn-outline btn-lg" style="border-color:rgba(255,255,255,0.5);color:#fff"><%= t('actions.viewLoads') %></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-stats">
|
<div class="hero-stats">
|
||||||
<div class="hero-stat"><div class="hero-stat-num">मुफ्त</div><div class="hero-stat-label">हमेशा के लिए</div></div>
|
<div class="hero-stat"><div class="hero-stat-num"><%= t('landing.free') %></div><div class="hero-stat-label"><%= t('landing.forever') %></div></div>
|
||||||
<div class="hero-stat"><div class="hero-stat-num">30 सेकंड</div><div class="hero-stat-label">पंजीकरण</div></div>
|
<div class="hero-stat"><div class="hero-stat-num">30 <%= t('landing.seconds') %></div><div class="hero-stat-label"><%= t('actions.register') %></div></div>
|
||||||
<div class="hero-stat"><div class="hero-stat-num">5 मिनट</div><div class="hero-stat-label">पहली बोली</div></div>
|
<div class="hero-stat"><div class="hero-stat-num">5 <%= t('landing.minutes') %></div><div class="hero-stat-label"><%= t('landing.firstBid') %></div></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">एक मंच। तीन उपयोगकर्ता।</h2>
|
<h2 class="section-title"><%= t('landing.onePlatform') %></h2>
|
||||||
<p class="section-subtitle">चाहे आप माल भेजें, ट्रक चलाएं, या सौदे कराएं — भारत ट्रक्स आपके लिए है।</p>
|
<p class="section-subtitle"><%= t('landing.onePlatformSub') %></p>
|
||||||
<div class="roles-grid">
|
<div class="roles-grid">
|
||||||
<div class="role-card role-card-driver">
|
<div class="role-card role-card-driver">
|
||||||
<div class="role-icon">🚛</div>
|
<div class="role-icon">🚛</div>
|
||||||
<h3>ट्रक ड्राइवर</h3>
|
<h3><%= t('auth.driver') %></h3>
|
||||||
<ul><li>लोड खोजें और बोली लगाएं</li><li>खाली वापसी से बचें</li><li>कमाई का हिसाब रखें</li><li>सीधे शिपर से जुड़ें</li></ul>
|
<ul><li><%= t('landing.driverF1') %></li><li><%= t('landing.driverF2') %></li><li><%= t('landing.driverF3') %></li><li><%= t('landing.driverF4') %></li></ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="role-card role-card-shipper">
|
<div class="role-card role-card-shipper">
|
||||||
<div class="role-icon">📦</div>
|
<div class="role-icon">📦</div>
|
||||||
<h3>शिपर / माल भेजने वाले</h3>
|
<h3><%= t('auth.shipper') %></h3>
|
||||||
<ul><li>लोड पोस्ट करें, बोली पाएं</li><li>सत्यापित ड्राइवर चुनें</li><li>माल की स्थिति जानें</li><li>भुगतान का रिकॉर्ड रखें</li></ul>
|
<ul><li><%= t('landing.shipperF1') %></li><li><%= t('landing.shipperF2') %></li><li><%= t('landing.shipperF3') %></li><li><%= t('landing.shipperF4') %></li></ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="role-card role-card-broker">
|
<div class="role-card role-card-broker">
|
||||||
<div class="role-icon">🤝</div>
|
<div class="role-icon">🤝</div>
|
||||||
<h3>ब्रोकर / एजेंट</h3>
|
<h3><%= t('auth.broker') %></h3>
|
||||||
<ul><li>अपने नेटवर्क को डिजिटल करें</li><li>कमीशन ट्रैक करें</li><li>शिपर के लिए लोड पोस्ट करें</li><li>ड्राइवर नेटवर्क बढ़ाएं</li></ul>
|
<ul><li><%= t('landing.brokerF1') %></li><li><%= t('landing.brokerF2') %></li><li><%= t('landing.brokerF3') %></li><li><%= t('landing.brokerF4') %></li></ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -43,34 +43,34 @@
|
||||||
|
|
||||||
<section class="section" style="background:var(--gray-50)">
|
<section class="section" style="background:var(--gray-50)">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">कैसे काम करता है?</h2>
|
<h2 class="section-title"><%= t('landing.howTitle') %></h2>
|
||||||
<p class="section-subtitle">सिर्फ 4 आसान कदम</p>
|
<p class="section-subtitle"><%= t('landing.howSub') %></p>
|
||||||
<div class="steps-grid">
|
<div class="steps-grid">
|
||||||
<div class="step-card"><h4>पंजीकरण करें</h4><p>फोन नंबर से मुफ्त अकाउंट बनाएं। अपनी भूमिका चुनें।</p></div>
|
<div class="step-card"><h4><%= t('landing.step1') %></h4><p><%= t('landing.step1Desc') %></p></div>
|
||||||
<div class="step-card"><h4>लोड पोस्ट / खोजें</h4><p>शिपर लोड पोस्ट करें। ड्राइवर उपलब्ध लोड देखें।</p></div>
|
<div class="step-card"><h4><%= t('landing.step2') %></h4><p><%= t('landing.step2Desc') %></p></div>
|
||||||
<div class="step-card"><h4>बोली लगाएं / स्वीकार करें</h4><p>ड्राइवर अपनी कीमत बताएं। शिपर सबसे अच्छी बोली चुनें।</p></div>
|
<div class="step-card"><h4><%= t('landing.step3') %></h4><p><%= t('landing.step3Desc') %></p></div>
|
||||||
<div class="step-card"><h4>माल पहुँचाएं, भुगतान पाएं</h4><p>ट्रिप पूरी करें। UPI से सीधे भुगतान पाएं।</p></div>
|
<div class="step-card"><h4><%= t('landing.step4') %></h4><p><%= t('landing.step4Desc') %></p></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">क्यों भारत ट्रक्स?</h2>
|
<h2 class="section-title"><%= t('landing.whyTitle') %></h2>
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-card"><div class="stat-value">₹0</div><div class="stat-label">कोई शुल्क नहीं</div></div>
|
<div class="stat-card"><div class="stat-value">₹0</div><div class="stat-label"><%= t('landing.noFee') %></div></div>
|
||||||
<div class="stat-card"><div class="stat-value">🔒</div><div class="stat-label">सुरक्षित मंच</div></div>
|
<div class="stat-card"><div class="stat-value">🔒</div><div class="stat-label"><%= t('landing.secure') %></div></div>
|
||||||
<div class="stat-card"><div class="stat-value">📱</div><div class="stat-label">मोबाइल पर चलता है</div></div>
|
<div class="stat-card"><div class="stat-value">📱</div><div class="stat-label"><%= t('landing.mobile') %></div></div>
|
||||||
<div class="stat-card"><div class="stat-value">🇮🇳</div><div class="stat-label">भारत के लिए बना</div></div>
|
<div class="stat-card"><div class="stat-value">🇮🇳</div><div class="stat-label"><%= t('landing.madeInIndia') %></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section" style="background:linear-gradient(135deg, var(--navy), var(--ashoka-blue)); color:var(--white); text-align:center;">
|
<section class="section" style="background:linear-gradient(135deg, var(--navy), var(--ashoka-blue)); color:var(--white); text-align:center;">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 style="color:var(--white); font-size:1.5rem; margin-bottom:var(--space-sm);">आज ही शुरू करें — बिल्कुल मुफ्त!</h2>
|
<h2 style="color:var(--white); font-size:1.5rem; margin-bottom:var(--space-sm);"><%= t('landing.ctaTitle') %></h2>
|
||||||
<p style="opacity:0.85; margin-bottom:var(--space-lg);">1000+ उपयोगकर्ताओं तक सभी सुविधाएं मुफ्त। कोई क्रेडिट कार्ड नहीं चाहिए।</p>
|
<p style="opacity:0.85; margin-bottom:var(--space-lg);"><%= t('landing.ctaSub') %></p>
|
||||||
<a href="/register" class="btn btn-cta btn-lg">अभी पंजीकरण करें →</a>
|
<a href="/register" class="btn btn-cta btn-lg"><%= t('auth.registerBtn') %> →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
20
webapp/src/views/pages/leaderboard.ejs
Normal file
20
webapp/src/views/pages/leaderboard.ejs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<% var title = 'Leaderboard'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🏆 Leaderboard</h2>
|
||||||
|
<% if (myRank) { %><p style="color:var(--navy);font-weight:700;margin-bottom:var(--space-md)">Your Rank: #<%= myRank %></p><% } %>
|
||||||
|
<% leaderboard.forEach(l => { %>
|
||||||
|
<div class="card" style="padding:12px;margin-bottom:8px;display:flex;align-items:center;gap:12px;<%= l.user_id === user.id ? 'border:2px solid var(--navy)' : '' %>">
|
||||||
|
<div style="font-size:1.2rem;font-weight:700;width:30px;text-align:center"><%= l.rank <= 3 ? ['🥇','🥈','🥉'][l.rank-1] : '#'+l.rank %></div>
|
||||||
|
<div style="flex:1">
|
||||||
|
<strong><%= l.user?.name || l.user?.username || 'User' %></strong>
|
||||||
|
<div style="font-size:0.75rem;color:var(--gray-700)"><%= l.level.icon %> Level <%= l.level.level %> • <%= l.user?.role || '' %></div>
|
||||||
|
</div>
|
||||||
|
<div style="font-weight:700;color:var(--navy)"><%= l.xp %> XP</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
|
|
@ -1,89 +1,65 @@
|
||||||
<% var title = load.origin_city + ' → ' + load.destination_city; %>
|
<% var title = load.origin_city + ' → ' + load.destination_city; %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="section" style="padding-top:var(--space-lg)">
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
<div class="container" style="max-width:600px">
|
<div class="container" style="max-width:600px">
|
||||||
<a href="/loadboard" style="font-size:0.8rem;color:var(--gray-700)">← लोड बोर्ड पर वापस</a>
|
<a href="/loadboard" style="font-size:0.8rem;color:var(--gray-700)">← <%= t('actions.viewLoads') %></a>
|
||||||
|
|
||||||
<div class="card" style="margin-top:var(--space-md);<%= load.is_urgent ? 'border-left:4px solid var(--saffron)' : 'border-left:4px solid var(--ashoka-blue)' %>">
|
<div class="card" style="margin-top:var(--space-md);<%= load.is_urgent ? 'border-left:4px solid var(--saffron)' : 'border-left:4px solid var(--ashoka-blue)' %>">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:start">
|
<div style="display:flex;justify-content:space-between;align-items:start">
|
||||||
<h2 style="font-size:1.2rem">📍 <%= load.origin_city %> → <%= load.destination_city %></h2>
|
<h2 style="font-size:1.2rem">📍 <%= load.origin_city %> → <%= load.destination_city %></h2>
|
||||||
<span class="badge badge-<%= load.status === 'open' ? 'open' : load.status === 'booked' ? 'booked' : 'delivered' %>"><%= load.status %></span>
|
<span class="badge badge-<%= load.status === 'open' ? 'open' : load.status === 'booked' ? 'booked' : 'delivered' %>"><%= load.status %></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md);margin-top:var(--space-md)">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md);margin-top:var(--space-md)">
|
||||||
<div><small style="color:var(--gray-700)">वज़न</small><br><strong><%= load.weight_tons %> टन</strong></div>
|
<div><small style="color:var(--gray-700)">⚖️ <%= t('postLoad.weight') %></small><br><strong><%= load.weight_tons %> <%= t('common.tons') %></strong></div>
|
||||||
<div><small style="color:var(--gray-700)">ट्रक</small><br><strong><%= load.truck_type %></strong></div>
|
<div><small style="color:var(--gray-700)">🚛 <%= t('common.truckType') %></small><br><strong><%= load.truck_type %></strong></div>
|
||||||
<div><small style="color:var(--gray-700)">पिकअप</small><br><strong><%= new Date(load.pickup_date).toLocaleDateString('hi-IN') %></strong></div>
|
<div><small style="color:var(--gray-700)">📅 <%= t('postLoad.pickupDate') %></small><br><strong><%= new Date(load.pickup_date).toLocaleDateString('en-IN') %></strong></div>
|
||||||
<div><small style="color:var(--gray-700)">बजट</small><br><strong><%= load.budget ? '₹' + Number(load.budget).toLocaleString('en-IN') : 'बताया नहीं' %></strong></div>
|
<div><small style="color:var(--gray-700)">💰 <%= t('postLoad.budget') %></small><br><strong><%= load.budget ? '₹' + Number(load.budget).toLocaleString('en-IN') : '—' %></strong></div>
|
||||||
</div>
|
</div>
|
||||||
|
<% if (load.material_type) { %><div style="margin-top:var(--space-sm)"><small style="color:var(--gray-700)">📦 <%= t('postLoad.material') %>:</small> <%= load.material_type %></div><% } %>
|
||||||
<% if (load.material_type) { %>
|
<% if (load.description) { %><div style="margin-top:var(--space-sm)"><small style="color:var(--gray-700)">📝:</small> <%= load.description %></div><% } %>
|
||||||
<div style="margin-top:var(--space-sm)"><small style="color:var(--gray-700)">माल:</small> <%= load.material_type %></div>
|
|
||||||
<% } %>
|
|
||||||
<% if (load.description) { %>
|
|
||||||
<div style="margin-top:var(--space-sm)"><small style="color:var(--gray-700)">विवरण:</small> <%= load.description %></div>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<div style="margin-top:var(--space-md);padding-top:var(--space-md);border-top:1px solid var(--gray-200);font-size:0.8rem;color:var(--gray-700);display:flex;justify-content:space-between;align-items:center">
|
<div style="margin-top:var(--space-md);padding-top:var(--space-md);border-top:1px solid var(--gray-200);font-size:0.8rem;color:var(--gray-700);display:flex;justify-content:space-between;align-items:center">
|
||||||
<span>पोस्ट किया: <%= load.poster ? load.poster.name : 'Unknown' %> | <%= new Date(load.created_at).toLocaleDateString('hi-IN') %></span>
|
<span><%= load.poster ? load.poster.name : '' %> | <%= new Date(load.created_at).toLocaleDateString('en-IN') %></span>
|
||||||
<a href="https://wa.me/?text=🚛 *लोड उपलब्ध*%0A📍 <%= load.origin_city %> → <%= load.destination_city %>%0A🏋️ <%= load.weight_tons %> टन | <%= load.truck_type %><%= load.budget ? '%0A💰 ₹' + Number(load.budget).toLocaleString('en-IN') : '' %>%0A📅 <%= load.pickup_date %>%0A%0Ahttps://bharathtrucks.com/loadboard/<%= load.id %>" target="_blank" class="btn btn-sm" style="background:#25d366;color:#fff;padding:6px 12px;font-size:0.7rem">WhatsApp शेयर</a>
|
<a href="/loadboard/whatsapp/<%= load.id %>" target="_blank" class="btn btn-sm" style="background:#25d366;color:#fff;padding:6px 12px;font-size:0.7rem">📱 WhatsApp</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bid Form (drivers only, open loads) -->
|
|
||||||
<% if (user && user.role === 'driver' && load.status === 'open') { %>
|
<% if (user && user.role === 'driver' && load.status === 'open') { %>
|
||||||
<div class="card" style="margin-top:var(--space-md)">
|
<div class="card" style="margin-top:var(--space-md)">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-md)">
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-md)">
|
||||||
<h3 style="font-size:1rem">🏷️ <%= myBid ? 'अपनी बोली अपडेट करें' : 'बोली लगाएं' %></h3>
|
<h3 style="font-size:1rem">🏷️ <%= myBid ? t('loadDetail.updateBid') : t('actions.bid') %></h3>
|
||||||
<a href="/messages/<%= load.posted_by %>" class="btn btn-sm btn-outline" style="font-size:0.7rem">💬 शिपर से बात करें</a>
|
<a href="/messages/<%= load.posted_by %>" class="btn btn-sm btn-outline" style="font-size:0.7rem">💬 Chat</a>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="/loadboard/<%= load.id %>/bid">
|
<form method="POST" action="/loadboard/<%= load.id %>/bid">
|
||||||
<div style="display:grid;grid-template-columns:1fr auto;gap:var(--space-sm);align-items:end">
|
<div style="display:grid;grid-template-columns:1fr auto;gap:var(--space-sm);align-items:end">
|
||||||
<div class="form-group" style="margin:0">
|
<div class="form-group" style="margin:0">
|
||||||
<label class="form-label">आपकी कीमत (₹)</label>
|
<label class="form-label">💰 <%= t('loadDetail.yourPrice') %> (₹)</label>
|
||||||
<input type="number" name="amount" class="form-input" placeholder="42000" value="<%= myBid ? myBid.amount : '' %>" required>
|
<input type="number" name="amount" class="form-input" value="<%= myBid ? myBid.amount : '' %>" placeholder="<%= load.budget || '40000' %>" required>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-success">बोली लगाएं</button>
|
<button type="submit" class="btn btn-cta"><%= t('actions.bid') %></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-top:var(--space-sm)">
|
<div class="form-group" style="margin-top:var(--space-sm)">
|
||||||
<input type="text" name="note" class="form-input" placeholder="कोई संदेश (वैकल्पिक)" value="<%= myBid ? myBid.note || '' : '' %>" style="padding:8px 12px;font-size:0.8rem">
|
<input type="text" name="note" class="form-input" placeholder="📝 Note" value="<%= myBid ? myBid.note || '' : '' %>">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<!-- Bids List (visible to load owner) -->
|
<% if (user && (user.role === 'shipper' || user.role === 'broker') && load.posted_by === user.id && bids && bids.length > 0) { %>
|
||||||
<% if (bids.length > 0 && user && (user.id === load.posted_by || user.role === 'admin')) { %>
|
|
||||||
<div class="card" style="margin-top:var(--space-md)">
|
<div class="card" style="margin-top:var(--space-md)">
|
||||||
<h3 style="font-size:1rem;margin-bottom:var(--space-md)">📊 बोलियाँ (<%= bids.length %>)</h3>
|
<h3 style="font-size:1rem;margin-bottom:var(--space-md)">🏷️ <%= t('loadDetail.bidsReceived') %> (<%= bids.length %>)</h3>
|
||||||
<% bids.forEach(bid => { %>
|
<% bids.forEach(bid => { %>
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid var(--gray-200)">
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid var(--gray-200)">
|
||||||
<div>
|
<div><strong><%= bid.driver ? bid.driver.name : 'Driver' %></strong><% if (bid.note) { %><div style="font-size:0.75rem;color:var(--gray-700)"><%= bid.note %></div><% } %></div>
|
||||||
<strong style="font-size:0.9rem"><%= bid.driver ? bid.driver.name : 'Driver' %></strong>
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
<div style="font-size:0.75rem;color:var(--gray-700)"><%= bid.driver ? bid.driver.username : '' %> <% if (bid.note) { %>| <%= bid.note %><% } %></div>
|
|
||||||
</div>
|
|
||||||
<div style="text-align:right">
|
|
||||||
<strong style="color:var(--navy)">₹<%= Number(bid.amount).toLocaleString('en-IN') %></strong>
|
<strong style="color:var(--navy)">₹<%= Number(bid.amount).toLocaleString('en-IN') %></strong>
|
||||||
<% if (load.status === 'open' && bid.status === 'pending') { %>
|
<% if (load.status === 'open' && bid.status === 'pending') { %>
|
||||||
<form method="POST" action="/loadboard/<%= load.id %>/accept-bid" style="margin-top:4px">
|
<form method="POST" action="/loadboard/<%= load.id %>/accept-bid" style="margin:0"><input type="hidden" name="bid_id" value="<%= bid.id %>"><button class="btn btn-sm btn-cta">✓</button></form>
|
||||||
<input type="hidden" name="bid_id" value="<%= bid.id %>">
|
<% } else { %><span class="badge badge-<%= bid.status %>"><%= bid.status %></span><% } %>
|
||||||
<button type="submit" class="btn btn-success btn-sm" style="padding:4px 10px;font-size:0.7rem">स्वीकार</button>
|
|
||||||
</form>
|
|
||||||
<% } else { %>
|
|
||||||
<div><span class="badge badge-<%= bid.status === 'accepted' ? 'delivered' : 'cancelled' %>"><%= bid.status %></span></div>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</div>
|
</div>
|
||||||
<% } else if (bids.length > 0) { %>
|
|
||||||
<div class="card" style="margin-top:var(--space-md)">
|
|
||||||
<p style="font-size:0.85rem;color:var(--gray-700)">🏷️ <%= bids.length %> बोली प्राप्त</p>
|
|
||||||
</div>
|
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<%- include('../partials/footer') %>
|
<%- include('../partials/footer') %>
|
||||||
|
|
|
||||||
30
webapp/src/views/pages/load-share.ejs
Normal file
30
webapp/src/views/pages/load-share.ejs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="hi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= load.origin_city %> → <%= load.destination_city %> | BharathTrucks</title>
|
||||||
|
<meta property="og:title" content="🚛 Load: <%= load.origin_city %> → <%= load.destination_city %>">
|
||||||
|
<meta property="og:description" content="<%= load.weight_tons %> tons | ₹<%= (load.budget||0).toLocaleString('en-IN') %> | <%= load.truck_type %>">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta name="description" content="Freight load from <%= load.origin_city %> to <%= load.destination_city %> - <%= load.weight_tons %> tons">
|
||||||
|
<link rel="stylesheet" href="/css/govt-theme.css">
|
||||||
|
</head>
|
||||||
|
<body style="padding:20px">
|
||||||
|
<div class="container" style="max-width:500px;margin:0 auto">
|
||||||
|
<div class="card" style="padding:var(--space-lg)">
|
||||||
|
<div style="text-align:center;margin-bottom:var(--space-md)"><span style="font-size:2rem">🚛</span><h2>BharathTrucks</h2></div>
|
||||||
|
<h3 style="margin-bottom:var(--space-sm)">📍 <%= load.origin_city %> → <%= load.destination_city %></h3>
|
||||||
|
<div style="display:grid;gap:8px;font-size:0.9rem">
|
||||||
|
<div>⚖️ Weight: <strong><%= load.weight_tons %> tons</strong></div>
|
||||||
|
<div>🚛 Truck: <strong><%= load.truck_type %></strong></div>
|
||||||
|
<% if (load.budget) { %><div>💰 Budget: <strong>₹<%= Number(load.budget).toLocaleString('en-IN') %></strong></div><% } %>
|
||||||
|
<% if (load.pickup_date) { %><div>📅 Pickup: <strong><%= new Date(load.pickup_date).toLocaleDateString('en-IN') %></strong></div><% } %>
|
||||||
|
<% if (load.material_type) { %><div>📦 Material: <strong><%= load.material_type %></strong></div><% } %>
|
||||||
|
</div>
|
||||||
|
<a href="/loadboard/<%= load.id %>" class="btn btn-cta btn-block btn-lg" style="margin-top:var(--space-lg)">Bid on this Load →</a>
|
||||||
|
<a href="/register" class="btn btn-outline btn-block" style="margin-top:var(--space-sm)">Join Free</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,45 +1,43 @@
|
||||||
<% var title = 'लोड बोर्ड'; %>
|
<% var title = t('common.loadboard'); %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="section" style="padding-top:var(--space-lg)">
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-md)">
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-md)">
|
||||||
<h2 style="font-size:1.3rem">📋 लोड बोर्ड</h2>
|
<h2 style="font-size:1.3rem">📋 <%= t('common.loadboard') %></h2>
|
||||||
<% if (user && (user.role === 'shipper' || user.role === 'broker')) { %>
|
<% if (user && (user.role === 'shipper' || user.role === 'broker')) { %>
|
||||||
<a href="/loadboard/post" class="btn btn-cta btn-sm">+ लोड पोस्ट करें</a>
|
<a href="/loadboard/post" class="btn btn-cta btn-sm">+ <%= t('actions.postLoad') %></a>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
|
||||||
<form method="GET" action="/loadboard" class="card" style="padding:var(--space-md);margin-bottom:var(--space-md)">
|
<form method="GET" action="/loadboard" class="card" style="padding:var(--space-md);margin-bottom:var(--space-md)">
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:var(--space-sm);align-items:end">
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:var(--space-sm);align-items:end">
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label">कहाँ से</label>
|
<label class="form-label"><%= t('common.from') %></label>
|
||||||
<input type="text" name="origin" class="form-input" placeholder="शहर" value="<%= filters.origin || '' %>" style="padding:8px 12px">
|
<input type="text" name="origin" class="form-input" placeholder="📍" value="<%= filters.origin || '' %>" style="padding:8px 12px">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label">कहाँ तक</label>
|
<label class="form-label"><%= t('common.to') %></label>
|
||||||
<input type="text" name="destination" class="form-input" placeholder="शहर" value="<%= filters.destination || '' %>" style="padding:8px 12px">
|
<input type="text" name="destination" class="form-input" placeholder="📍" value="<%= filters.destination || '' %>" style="padding:8px 12px">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label">ट्रक प्रकार</label>
|
<label class="form-label"><%= t('common.truckType') %></label>
|
||||||
<select name="truck_type" class="form-input form-select" style="padding:8px 12px">
|
<select name="truck_type" class="form-input form-select" style="padding:8px 12px">
|
||||||
<option value="all">सभी</option>
|
<option value="all"><%= t('common.all') %></option>
|
||||||
<% truckTypes.forEach(t => { %>
|
<% truckTypes.forEach(t_type => { %>
|
||||||
<option value="<%= t %>" <%= filters.truck_type === t ? 'selected' : '' %>><%= t %></option>
|
<option value="<%= t_type %>" <%= filters.truck_type === t_type ? 'selected' : '' %>><%= t_type %></option>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary btn-sm">खोजें</button>
|
<button type="submit" class="btn btn-primary btn-sm">🔍 <%= t('actions.search') %></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Load List -->
|
|
||||||
<% if (loads.length === 0) { %>
|
<% if (loads.length === 0) { %>
|
||||||
<div class="card text-center" style="padding:var(--space-2xl)">
|
<div class="card text-center" style="padding:var(--space-2xl)">
|
||||||
<p style="font-size:1.2rem">📭</p>
|
<p style="font-size:1.2rem">📭</p>
|
||||||
<p style="color:var(--gray-700)">कोई लोड उपलब्ध नहीं</p>
|
<p style="color:var(--gray-700)"><%= t('common.noLoads') %></p>
|
||||||
</div>
|
</div>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<div style="display:grid;gap:var(--space-md)">
|
<div style="display:grid;gap:var(--space-md)">
|
||||||
|
|
@ -49,12 +47,12 @@
|
||||||
<div>
|
<div>
|
||||||
<strong style="font-size:0.95rem">📍 <%= load.origin_city %> → <%= load.destination_city %></strong>
|
<strong style="font-size:0.95rem">📍 <%= load.origin_city %> → <%= load.destination_city %></strong>
|
||||||
<div style="font-size:0.8rem;color:var(--gray-700);margin-top:4px">
|
<div style="font-size:0.8rem;color:var(--gray-700);margin-top:4px">
|
||||||
🚛 <%= load.weight_tons %> टन | <%= load.truck_type %>
|
🚛 <%= load.weight_tons %> <%= t('common.tons') %> | <%= load.truck_type %>
|
||||||
<% if (load.material_type) { %> | <%= load.material_type %><% } %>
|
<% if (load.material_type) { %> | <%= load.material_type %><% } %>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:0.8rem;color:var(--gray-700);margin-top:2px">
|
<div style="font-size:0.8rem;color:var(--gray-700);margin-top:2px">
|
||||||
📅 <%= new Date(load.pickup_date).toLocaleDateString('hi-IN') %>
|
📅 <%= 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') %><% } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:right">
|
<div style="text-align:right">
|
||||||
|
|
@ -62,7 +60,7 @@
|
||||||
<div style="font-size:1rem;font-weight:700;color:var(--navy)">₹<%= Number(load.budget).toLocaleString('en-IN') %></div>
|
<div style="font-size:1rem;font-weight:700;color:var(--navy)">₹<%= Number(load.budget).toLocaleString('en-IN') %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
<% if (load.is_urgent) { %>
|
<% if (load.is_urgent) { %>
|
||||||
<span class="badge badge-booked">अर्जेंट</span>
|
<span class="badge badge-booked"><%= t('common.urgent') %></span>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<% var title = 'लॉगिन'; %>
|
<% var title = t('actions.login'); %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container" style="max-width:400px">
|
<div class="container" style="max-width:400px">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="text-center" style="margin-bottom:4px">लॉगिन | Login</h2>
|
<h2 class="text-center" style="margin-bottom:4px"><%= t('auth.loginTitle') %></h2>
|
||||||
<p class="text-center" style="color:var(--gray-700);font-size:0.8rem;margin-bottom:var(--space-lg)">अपना यूज़रनेम और पासवर्ड दर्ज करें</p>
|
<p class="text-center" style="color:var(--gray-700);font-size:0.8rem;margin-bottom:var(--space-lg)"><%= t('auth.loginSubtitle') %></p>
|
||||||
|
|
||||||
<% if (error) { %>
|
<% if (error) { %>
|
||||||
<div class="alert-error"><%= error %></div>
|
<div class="alert-error"><%= error %></div>
|
||||||
|
|
@ -14,18 +14,18 @@
|
||||||
|
|
||||||
<form method="POST" action="/login">
|
<form method="POST" action="/login">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">यूज़रनेम (ड्राइवर: गाड़ी नंबर)</label>
|
<label class="form-label">👤 <%= t('auth.username') %></label>
|
||||||
<input type="text" name="username" class="form-input" placeholder="MH31AB1234 या अपना यूज़रनेम" required autofocus>
|
<input type="text" name="username" class="form-input" placeholder="MH31AB1234" required autofocus>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">पासवर्ड</label>
|
<label class="form-label">🔒 <%= t('auth.password') %></label>
|
||||||
<input type="password" name="password" class="form-input" placeholder="••••" required>
|
<input type="password" name="password" class="form-input" placeholder="••••" required>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary btn-block btn-lg">लॉगिन करें →</button>
|
<button type="submit" class="btn btn-primary btn-block btn-lg"><%= t('actions.login') %> →</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="text-center" style="margin-top:var(--space-lg);font-size:0.8rem">
|
<p class="text-center" style="margin-top:var(--space-lg);font-size:0.8rem">
|
||||||
नया खाता? <a href="/register">पंजीकरण करें</a>
|
<%= t('auth.noAccount') %> <a href="/register"><%= t('actions.register') %></a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
37
webapp/src/views/pages/maintenance.ejs
Normal file
37
webapp/src/views/pages/maintenance.ejs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<% var title = 'Maintenance'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🔧 Vehicle Reminders</h2>
|
||||||
|
<div class="stats-grid" style="margin-bottom:var(--space-md)">
|
||||||
|
<div class="stat-card"><div class="stat-value"><%= stats.total %></div><div class="stat-label">Total</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value" style="color:red"><%= stats.expired %></div><div class="stat-label">🔴 Expired</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value" style="color:orange"><%= stats.expiring %></div><div class="stat-label">🟡 Expiring</div></div>
|
||||||
|
</div>
|
||||||
|
<% reminders.forEach(r => { %>
|
||||||
|
<div class="card card-accent" style="margin-bottom:var(--space-sm);border-left-color:<%= r.urgency==='expired'||r.urgency==='critical'?'red':r.urgency==='warning'?'orange':'var(--green)' %>">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div>
|
||||||
|
<strong><%= r.doc_type.toUpperCase() %></strong> — <%= r.vehicle_number %>
|
||||||
|
<div style="font-size:0.8rem;color:var(--gray-700)">📅 <%= r.expiry_date %> | <span style="color:<%= r.days_left<0?'red':'orange' %>"><%= r.days_left < 0 ? Math.abs(r.days_left) + ' days overdue' : r.days_left + ' days left' %></span></div>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="/maintenance/delete/<%= r.id %>" style="margin:0"><button class="btn btn-sm" style="color:red">✕</button></form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<form method="POST" action="/maintenance/add" class="card" style="padding:var(--space-md);margin-top:var(--space-lg)">
|
||||||
|
<h4 style="margin-bottom:var(--space-sm)">➕ Add Reminder</h4>
|
||||||
|
<div style="display:grid;gap:var(--space-sm)">
|
||||||
|
<input type="text" name="vehicle_number" class="form-input" placeholder="🚛 Vehicle Number" required>
|
||||||
|
<select name="doc_type" class="form-input form-select" required>
|
||||||
|
<option value="insurance">🛡️ Insurance</option><option value="fitness">🏋️ Fitness</option>
|
||||||
|
<option value="permit">📄 Permit</option><option value="puc">💨 PUC</option><option value="service">🔧 Service</option>
|
||||||
|
</select>
|
||||||
|
<input type="date" name="expiry_date" class="form-input" required>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
|
|
@ -1,34 +1,19 @@
|
||||||
<% var title = 'संदेश'; %>
|
<% var title = t('nav.messages'); %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="section" style="padding-top:var(--space-lg)">
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
<div class="container" style="max-width:500px">
|
<div class="container">
|
||||||
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">💬 संदेश</h2>
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">💬 <%= t('nav.messages') %></h2>
|
||||||
|
|
||||||
<% if (conversations.length === 0) { %>
|
<% if (conversations.length === 0) { %>
|
||||||
<div class="card text-center" style="padding:var(--space-2xl)">
|
<div class="card text-center" style="padding:var(--space-2xl)"><p><%= t('messages.noMessages') %></p></div>
|
||||||
<p>कोई संदेश नहीं</p>
|
<% } else { conversations.forEach(c => { %>
|
||||||
</div>
|
<a href="/messages/<%= c.other_user_id %>" class="card" style="display:block;text-decoration:none;color:inherit;padding:14px;margin-bottom:8px">
|
||||||
<% } else { %>
|
|
||||||
<div style="display:grid;gap:2px">
|
|
||||||
<% conversations.forEach(c => { %>
|
|
||||||
<a href="/messages/<%= c.lastMsg.sender_id === user.id ? c.lastMsg.receiver_id : c.lastMsg.sender_id %>" class="card" style="text-decoration:none;color:inherit;padding:12px var(--space-md)">
|
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
<div>
|
<div><strong><%= c.other_user_name || 'User' %></strong><div style="font-size:0.8rem;color:var(--gray-700)"><%= c.last_message || '' %></div></div>
|
||||||
<strong style="font-size:0.9rem"><%= c.user ? c.user.name : 'User' %></strong>
|
<span style="font-size:0.7rem;color:var(--gray-500)"><%= c.last_at ? new Date(c.last_at).toLocaleDateString('en-IN') : '' %></span>
|
||||||
<div style="font-size:0.75rem;color:var(--gray-700);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px"><%= c.lastMsg.content %></div>
|
|
||||||
</div>
|
|
||||||
<div style="text-align:right">
|
|
||||||
<% if (c.unread > 0) { %><span class="badge badge-booked"><%= c.unread %></span><% } %>
|
|
||||||
<div style="font-size:0.65rem;color:var(--gray-500)"><%= new Date(c.lastMsg.created_at).toLocaleDateString('hi-IN') %></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<% }) %>
|
<% }) } %>
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<%- include('../partials/footer') %>
|
<%- include('../partials/footer') %>
|
||||||
|
|
|
||||||
34
webapp/src/views/pages/more.ejs
Normal file
34
webapp/src/views/pages/more.ejs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<% var title = 'More'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">📱 All Features</h2>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:var(--space-md)">
|
||||||
|
<a href="/games" class="icon-action-btn"><span class="icon-action-emoji">🎮</span><span class="icon-action-label">Games</span></a>
|
||||||
|
<a href="/news" class="icon-action-btn"><span class="icon-action-emoji">📰</span><span class="icon-action-label">News</span></a>
|
||||||
|
<a href="/documents" class="icon-action-btn"><span class="icon-action-emoji">📄</span><span class="icon-action-label">Docs</span></a>
|
||||||
|
<a href="/bank" class="icon-action-btn"><span class="icon-action-emoji">🏦</span><span class="icon-action-label">Bank</span></a>
|
||||||
|
<a href="/reports" class="icon-action-btn"><span class="icon-action-emoji">📊</span><span class="icon-action-label">Reports</span></a>
|
||||||
|
<a href="/search" class="icon-action-btn"><span class="icon-action-emoji">🔍</span><span class="icon-action-label">Search</span></a>
|
||||||
|
<a href="/classifieds" class="icon-action-btn"><span class="icon-action-emoji">🛒</span><span class="icon-action-label">Buy/Sell</span></a>
|
||||||
|
<a href="/fleet" class="icon-action-btn"><span class="icon-action-emoji">🚛</span><span class="icon-action-label">Fleet</span></a>
|
||||||
|
<a href="/invoice" class="icon-action-btn"><span class="icon-action-emoji">🧾</span><span class="icon-action-label">Invoice</span></a>
|
||||||
|
<a href="/rates" class="icon-action-btn"><span class="icon-action-emoji">💹</span><span class="icon-action-label">Rates</span></a>
|
||||||
|
<a href="/referral" class="icon-action-btn"><span class="icon-action-emoji">🤝</span><span class="icon-action-label">Referral</span></a>
|
||||||
|
<a href="/gamification" class="icon-action-btn"><span class="icon-action-emoji">🏆</span><span class="icon-action-label">Level</span></a>
|
||||||
|
<a href="/leaderboard" class="icon-action-btn"><span class="icon-action-emoji">🥇</span><span class="icon-action-label">Rank</span></a>
|
||||||
|
<a href="/challenges" class="icon-action-btn"><span class="icon-action-emoji">🎯</span><span class="icon-action-label">Daily</span></a>
|
||||||
|
<a href="/feed" class="icon-action-btn"><span class="icon-action-emoji">📰</span><span class="icon-action-label">Feed</span></a>
|
||||||
|
<% if (user.role === 'driver') { %>
|
||||||
|
<a href="/driver/ledger" class="icon-action-btn"><span class="icon-action-emoji">📒</span><span class="icon-action-label">Ledger</span></a>
|
||||||
|
<a href="/trip-planner" class="icon-action-btn"><span class="icon-action-emoji">🧮</span><span class="icon-action-label">Trip Cost</span></a>
|
||||||
|
<a href="/returnload" class="icon-action-btn"><span class="icon-action-emoji">🔄</span><span class="icon-action-label">Return</span></a>
|
||||||
|
<a href="/safety" class="icon-action-btn"><span class="icon-action-emoji">🛡️</span><span class="icon-action-label">Safety</span></a>
|
||||||
|
<a href="/maintenance" class="icon-action-btn"><span class="icon-action-emoji">🔧</span><span class="icon-action-label">Reminders</span></a>
|
||||||
|
<a href="/fastag" class="icon-action-btn"><span class="icon-action-emoji">🏷️</span><span class="icon-action-label">FASTag</span></a>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
23
webapp/src/views/pages/news.ejs
Normal file
23
webapp/src/views/pages/news.ejs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<% var title = 'News'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">📰 Trucker News</h2>
|
||||||
|
<% if (news.length === 0) { %>
|
||||||
|
<div class="card text-center" style="padding:var(--space-2xl)"><p>No news yet. Check back later!</p></div>
|
||||||
|
<% } else { news.forEach(n => { %>
|
||||||
|
<div class="card" style="padding:var(--space-md);margin-bottom:var(--space-sm)">
|
||||||
|
<div style="display:flex;gap:8px;align-items:start">
|
||||||
|
<span style="font-size:1.3rem"><%= n.category==='diesel'?'⛽':n.category==='toll'?'🚧':n.category==='alert'?'⚠️':'📰' %></span>
|
||||||
|
<div>
|
||||||
|
<strong><%= n.title %></strong>
|
||||||
|
<% if (n.content) { %><p style="font-size:0.85rem;color:var(--gray-700);margin-top:4px"><%= n.content %></p><% } %>
|
||||||
|
<div style="font-size:0.7rem;color:var(--gray-500);margin-top:4px"><%= new Date(n.created_at).toLocaleDateString('en-IN') %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
22
webapp/src/views/pages/notifications.ejs
Normal file
22
webapp/src/views/pages/notifications.ejs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<% var title = 'Notifications'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🔔 Notifications</h2>
|
||||||
|
<% if (notifications.length === 0) { %>
|
||||||
|
<div class="card text-center" style="padding:var(--space-2xl)"><p>✅ All clear! No pending actions.</p></div>
|
||||||
|
<% } else { notifications.forEach(n => { %>
|
||||||
|
<a href="<%= n.url || '#' %>" class="card card-accent" style="display:block;text-decoration:none;color:inherit;margin-bottom:var(--space-sm);border-left-color:<%= n.priority==='high'?'red':n.priority==='medium'?'orange':'var(--gray-300)' %>">
|
||||||
|
<div style="display:flex;gap:var(--space-sm);align-items:start">
|
||||||
|
<span style="font-size:1.3rem"><%= n.icon %></span>
|
||||||
|
<div>
|
||||||
|
<strong style="font-size:0.9rem"><%= n.title %></strong>
|
||||||
|
<div style="font-size:0.8rem;color:var(--gray-700)"><%= n.subtitle || '' %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<% }) } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
18
webapp/src/views/pages/onboarding-game.ejs
Normal file
18
webapp/src/views/pages/onboarding-game.ejs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<% var title = 'Welcome'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container text-center">
|
||||||
|
<div style="font-size:3rem;margin-bottom:var(--space-sm)">🎮</div>
|
||||||
|
<h2 style="font-size:1.3rem">Welcome! You earned <%= xp %> XP</h2>
|
||||||
|
<p style="color:var(--gray-700);margin-bottom:var(--space-lg)">Complete steps to level up!</p>
|
||||||
|
<div style="display:grid;gap:var(--space-sm);text-align:left">
|
||||||
|
<a href="/profile" class="card card-accent" style="padding:14px;text-decoration:none;color:inherit"><span style="font-size:1.2rem">👤</span> Complete your profile (+30 XP)</a>
|
||||||
|
<a href="/loadboard" class="card card-accent" style="padding:14px;text-decoration:none;color:inherit"><span style="font-size:1.2rem">📋</span> Browse loads (+10 XP)</a>
|
||||||
|
<a href="/safety" class="card card-accent" style="padding:14px;text-decoration:none;color:inherit"><span style="font-size:1.2rem">🛡️</span> Add safety contact (+20 XP)</a>
|
||||||
|
<a href="/referral" class="card card-accent" style="padding:14px;text-decoration:none;color:inherit"><span style="font-size:1.2rem">🤝</span> Invite a friend (+40 XP)</a>
|
||||||
|
</div>
|
||||||
|
<a href="/<%= user.role %>" class="btn btn-primary btn-block" style="margin-top:var(--space-lg)">Go to Dashboard →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
|
|
@ -1,75 +1,32 @@
|
||||||
<% var title = 'लोड पोस्ट करें'; %>
|
<% var title = t('actions.postLoad'); %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="section" style="padding-top:var(--space-lg)">
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
<div class="container" style="max-width:500px">
|
<div class="container" style="max-width:500px">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 style="font-size:1.2rem;margin-bottom:var(--space-md)">📦 नया लोड पोस्ट करें</h2>
|
<h2 style="font-size:1.2rem;margin-bottom:var(--space-md)">📦 <%= t('actions.postLoad') %></h2>
|
||||||
|
<% if (error) { %><div class="alert-error"><%= error %></div><% } %>
|
||||||
<% if (error) { %>
|
|
||||||
<div class="alert-error"><%= error %></div>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<form method="POST" action="/loadboard/post">
|
<form method="POST" action="/loadboard/post">
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
||||||
<div class="form-group">
|
<div class="form-group"><label class="form-label">📍 <%= t('common.from') %> *</label><input type="text" name="origin_city" class="form-input" placeholder="Mumbai" required></div>
|
||||||
<label class="form-label">कहाँ से / Origin *</label>
|
<div class="form-group"><label class="form-label">📍 <%= t('common.to') %> *</label><input type="text" name="destination_city" class="form-input" placeholder="Delhi" required></div>
|
||||||
<input type="text" name="origin_city" class="form-input" placeholder="मुंबई" required>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">कहाँ तक / Destination *</label>
|
|
||||||
<input type="text" name="destination_city" class="form-input" placeholder="दिल्ली" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
||||||
<div class="form-group">
|
<div class="form-group"><label class="form-label">⚖️ <%= t('postLoad.weight') %> *</label><input type="number" name="weight_tons" class="form-input" placeholder="20" step="0.5" required></div>
|
||||||
<label class="form-label">वज़न (टन) *</label>
|
<div class="form-group"><label class="form-label">🚛 <%= t('common.truckType') %> *</label>
|
||||||
<input type="number" name="weight_tons" class="form-input" placeholder="20" step="0.5" required>
|
<select name="truck_type" class="form-input form-select" required><option value=""><%= t('postLoad.select') %></option><% truckTypes.forEach(tt => { %><option value="<%= tt %>"><%= tt %></option><% }) %></select>
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">ट्रक प्रकार *</label>
|
|
||||||
<select name="truck_type" class="form-input form-select" required>
|
|
||||||
<option value="">चुनें</option>
|
|
||||||
<% truckTypes.forEach(t => { %>
|
|
||||||
<option value="<%= t %>"><%= t %></option>
|
|
||||||
<% }) %>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
||||||
<div class="form-group">
|
<div class="form-group"><label class="form-label">📦 <%= t('postLoad.material') %></label><input type="text" name="material_type" class="form-input" placeholder="Cement, Steel..."></div>
|
||||||
<label class="form-label">माल का प्रकार</label>
|
<div class="form-group"><label class="form-label">💰 <%= t('postLoad.budget') %></label><input type="number" name="budget" class="form-input" placeholder="45000"></div>
|
||||||
<input type="text" name="material_type" class="form-input" placeholder="सीमेंट, स्टील...">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group"><label class="form-label">📅 <%= t('postLoad.pickupDate') %> *</label><input type="date" name="pickup_date" class="form-input" required></div>
|
||||||
<label class="form-label">बजट (₹)</label>
|
<div class="form-group"><label class="form-label">📝 <%= t('postLoad.notes') %></label><textarea name="description" class="form-input" rows="2" placeholder=""></textarea></div>
|
||||||
<input type="number" name="budget" class="form-input" placeholder="45000">
|
<div class="form-group"><label style="display:flex;align-items:center;gap:8px;font-size:0.85rem;cursor:pointer"><input type="checkbox" name="is_urgent"> 🔴 <%= t('common.urgent') %></label></div>
|
||||||
</div>
|
<button type="submit" class="btn btn-primary btn-block btn-lg"><%= t('actions.postLoad') %> →</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">पिकअप तारीख *</label>
|
|
||||||
<input type="date" name="pickup_date" class="form-input" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">विवरण / Notes</label>
|
|
||||||
<textarea name="description" class="form-input" rows="2" placeholder="अतिरिक्त जानकारी..."></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label style="display:flex;align-items:center;gap:8px;font-size:0.85rem;cursor:pointer">
|
|
||||||
<input type="checkbox" name="is_urgent"> 🔴 अर्जेंट लोड
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-block btn-lg">लोड पोस्ट करें →</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<%- include('../partials/footer') %>
|
<%- include('../partials/footer') %>
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,29 @@
|
||||||
<% var title = 'मेरी प्रोफ़ाइल'; %>
|
<% var title = t('nav.profile'); %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="section" style="padding-top:var(--space-lg)">
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
<div class="container" style="max-width:500px">
|
<div class="container" style="max-width:500px">
|
||||||
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">👤 मेरी प्रोफ़ाइल</h2>
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">👤 <%= t('nav.profile') %></h2>
|
||||||
|
<% if (success) { %><div style="background:#e8f5e9;color:green;border-radius:var(--radius-sm);padding:10px 14px;font-size:0.8rem;margin-bottom:var(--space-md)">✓ <%= t('profile.updated') %></div><% } %>
|
||||||
<% if (success) { %>
|
|
||||||
<div style="background:#e8f5e9;color:var(--green);border:1px solid #c8e6c9;border-radius:var(--radius-sm);padding:10px 14px;font-size:0.8rem;margin-bottom:var(--space-md)">✓ प्रोफ़ाइल अपडेट हो गई</div>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div style="display:flex;align-items:center;gap:var(--space-md);margin-bottom:var(--space-lg)">
|
<div style="display:flex;align-items:center;gap:var(--space-md);margin-bottom:var(--space-lg)">
|
||||||
<div style="width:50px;height:50px;background:var(--navy);color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.2rem;font-weight:700"><%= profile.name ? profile.name.charAt(0).toUpperCase() : '?' %></div>
|
<div style="width:50px;height:50px;background:var(--navy);color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.2rem;font-weight:700"><%= profile.name ? profile.name.charAt(0).toUpperCase() : '?' %></div>
|
||||||
<div>
|
<div><strong><%= profile.name %></strong><div style="font-size:0.8rem;color:var(--gray-700)">@<%= profile.username %> | <span class="badge badge-open"><%= profile.role %></span></div></div>
|
||||||
<strong><%= profile.name %></strong>
|
|
||||||
<div style="font-size:0.8rem;color:var(--gray-700)">@<%= profile.username %> | <span class="badge badge-open"><%= profile.role %></span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="POST" action="/profile">
|
<form method="POST" action="/profile">
|
||||||
<div class="form-group">
|
<div class="form-group"><label class="form-label">👤 <%= t('profile.name') %></label><input type="text" name="name" class="form-input" value="<%= profile.name %>" required></div>
|
||||||
<label class="form-label">नाम / Name</label>
|
<div class="form-group"><label class="form-label">📱 <%= t('profile.phone') %></label><input type="tel" name="phone" class="form-input" value="<%= profile.phone || '' %>" placeholder="9876543210"></div>
|
||||||
<input type="text" name="name" class="form-input" value="<%= profile.name %>" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">फोन नंबर</label>
|
|
||||||
<input type="tel" name="phone" class="form-input" value="<%= profile.phone || '' %>" placeholder="9876543210">
|
|
||||||
</div>
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
||||||
<div class="form-group">
|
<div class="form-group"><label class="form-label">🏙️ <%= t('profile.city') %></label><input type="text" name="city" class="form-input" value="<%= profile.city || '' %>"></div>
|
||||||
<label class="form-label">शहर</label>
|
<div class="form-group"><label class="form-label">🗺️ <%= t('profile.state') %></label><input type="text" name="state" class="form-input" value="<%= profile.state || '' %>"></div>
|
||||||
<input type="text" name="city" class="form-input" value="<%= profile.city || '' %>">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<button type="submit" class="btn btn-primary btn-block"><%= t('profile.update') %></button>
|
||||||
<label class="form-label">राज्य</label>
|
|
||||||
<input type="text" name="state" class="form-input" value="<%= profile.state || '' %>">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary btn-block">प्रोफ़ाइल अपडेट करें</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div style="margin-top:var(--space-lg);padding-top:var(--space-md);border-top:1px solid var(--gray-200)">
|
<div style="margin-top:var(--space-lg);padding-top:var(--space-md);border-top:1px solid var(--gray-200)">
|
||||||
<a href="/logout" class="btn btn-outline btn-block" style="color:var(--red);border-color:var(--red)">लॉगआउट</a>
|
<a href="/gamification" class="btn btn-outline btn-block" style="margin-bottom:8px">🏆 <%= t('profile.myLevel') %></a>
|
||||||
|
<a href="/logout" class="btn btn-outline btn-block" style="color:red;border-color:red"><%= t('actions.logout') %></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<%- include('../partials/footer') %>
|
<%- include('../partials/footer') %>
|
||||||
|
|
|
||||||
29
webapp/src/views/pages/rates.ejs
Normal file
29
webapp/src/views/pages/rates.ejs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<% var title = 'Rate Check'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">📊 Rate Intelligence</h2>
|
||||||
|
<form method="GET" action="/rates" class="card" style="padding:var(--space-md);margin-bottom:var(--space-md)">
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr auto;gap:var(--space-sm);align-items:end">
|
||||||
|
<div><label class="form-label">📍 From</label><input type="text" name="origin" class="form-input" value="<%= origin %>" placeholder="City" required></div>
|
||||||
|
<div><label class="form-label">📍 To</label><input type="text" name="destination" class="form-input" value="<%= destination %>" placeholder="City" required></div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">🔍</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<% if (rates) { %>
|
||||||
|
<div class="card" style="padding:var(--space-md)">
|
||||||
|
<h3 style="font-size:1rem;margin-bottom:var(--space-sm)"><%= rates.origin %> → <%= rates.destination %></h3>
|
||||||
|
<p style="font-size:0.8rem;color:var(--gray-700);margin-bottom:var(--space-md)">Based on <%= rates.count %> recent loads</p>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card"><div class="stat-value">₹<%= rates.avg.toLocaleString('en-IN') %></div><div class="stat-label">Average</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value">₹<%= rates.min.toLocaleString('en-IN') %></div><div class="stat-label">Min</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value">₹<%= rates.max.toLocaleString('en-IN') %></div><div class="stat-label">Max</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } else if (origin && destination) { %>
|
||||||
|
<div class="card text-center" style="padding:var(--space-xl)"><p>No rate data for this route yet.</p></div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
26
webapp/src/views/pages/referral.ejs
Normal file
26
webapp/src/views/pages/referral.ejs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<% var title = 'Referral'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🤝 Invite & Earn</h2>
|
||||||
|
<div class="card" style="padding:var(--space-lg);text-align:center;margin-bottom:var(--space-md)">
|
||||||
|
<p style="font-size:0.85rem;color:var(--gray-700)">Your Referral Code:</p>
|
||||||
|
<div style="font-size:1.5rem;font-weight:700;color:var(--navy);margin:8px 0"><%= code %></div>
|
||||||
|
<a href="https://wa.me/?text=<%= encodeURIComponent(shareMsg) %>" target="_blank" class="btn btn-block" style="background:#25d366;color:#fff">📱 Share on WhatsApp</a>
|
||||||
|
</div>
|
||||||
|
<div class="stats-grid" style="margin-bottom:var(--space-md)">
|
||||||
|
<div class="stat-card"><div class="stat-value"><%= stats.total %></div><div class="stat-label">Invited</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value"><%= stats.joined %></div><div class="stat-label">Joined</div></div>
|
||||||
|
</div>
|
||||||
|
<% if (referrals.length > 0) { %>
|
||||||
|
<h3 style="font-size:1rem;margin-bottom:var(--space-sm)">Recent Referrals</h3>
|
||||||
|
<% referrals.slice(0,10).forEach(r => { %>
|
||||||
|
<div class="card" style="padding:10px;margin-bottom:6px;display:flex;justify-content:space-between">
|
||||||
|
<span><%= r.referral_code %></span>
|
||||||
|
<span class="badge badge-<%= r.status === 'joined' ? 'open' : 'transit' %>"><%= r.status %></span>
|
||||||
|
</div>
|
||||||
|
<% }) } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<% var title = 'पंजीकरण'; %>
|
<% var title = t('actions.register'); %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container" style="max-width:440px">
|
<div class="container" style="max-width:440px">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="text-center" style="margin-bottom:4px">पंजीकरण | Register</h2>
|
<h2 class="text-center" style="margin-bottom:4px"><%= t('auth.registerTitle') %></h2>
|
||||||
<p class="text-center" style="color:var(--gray-700);font-size:0.8rem;margin-bottom:var(--space-lg)">मुफ्त खाता बनाएं</p>
|
<p class="text-center" style="color:var(--gray-700);font-size:0.8rem;margin-bottom:var(--space-lg)"><%= t('auth.registerSubtitle') %></p>
|
||||||
|
|
||||||
<% if (error) { %>
|
<% if (error) { %>
|
||||||
<div class="alert-error"><%= error %></div>
|
<div class="alert-error"><%= error %></div>
|
||||||
|
|
@ -14,55 +14,55 @@
|
||||||
|
|
||||||
<form method="POST" action="/register" id="registerForm">
|
<form method="POST" action="/register" id="registerForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">आप कौन हैं? / Your Role *</label>
|
<label class="form-label"><%= t('auth.yourRole') %> *</label>
|
||||||
<div class="role-select-grid">
|
<div class="role-select-grid">
|
||||||
<label class="role-option">
|
<label class="role-option">
|
||||||
<input type="radio" name="role" value="driver" <%= role === 'driver' ? 'checked' : '' %> required>
|
<input type="radio" name="role" value="driver" <%= role === 'driver' ? 'checked' : '' %> required>
|
||||||
<div class="role-option-card"><span class="role-icon">🚛</span><span>ड्राइवर</span></div>
|
<div class="role-option-card"><span class="role-icon">🚛</span><span><%= t('auth.driver') %></span></div>
|
||||||
</label>
|
</label>
|
||||||
<label class="role-option">
|
<label class="role-option">
|
||||||
<input type="radio" name="role" value="shipper" <%= role === 'shipper' ? 'checked' : '' %>>
|
<input type="radio" name="role" value="shipper" <%= role === 'shipper' ? 'checked' : '' %>>
|
||||||
<div class="role-option-card"><span class="role-icon">📦</span><span>शिपर</span></div>
|
<div class="role-option-card"><span class="role-icon">📦</span><span><%= t('auth.shipper') %></span></div>
|
||||||
</label>
|
</label>
|
||||||
<label class="role-option">
|
<label class="role-option">
|
||||||
<input type="radio" name="role" value="broker" <%= role === 'broker' ? 'checked' : '' %>>
|
<input type="radio" name="role" value="broker" <%= role === 'broker' ? 'checked' : '' %>>
|
||||||
<div class="role-option-card"><span class="role-icon">🤝</span><span>ब्रोकर</span></div>
|
<div class="role-option-card"><span class="role-icon">🤝</span><span><%= t('auth.broker') %></span></div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">पूरा नाम / Full Name *</label>
|
<label class="form-label">👤 <%= t('auth.fullName') %> *</label>
|
||||||
<input type="text" name="name" class="form-input" placeholder="अपना नाम" required>
|
<input type="text" name="name" class="form-input" placeholder="<%= t('auth.fullName') %>" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" id="usernameLabel">यूज़रनेम *</label>
|
<label class="form-label" id="usernameLabel">📝 <%= t('auth.username') %> *</label>
|
||||||
<input type="text" name="username" class="form-input" id="usernameInput" placeholder="यूज़रनेम चुनें" required>
|
<input type="text" name="username" class="form-input" id="usernameInput" placeholder="<%= t('auth.username') %>" required>
|
||||||
<small id="usernameHint" style="color:var(--gray-700);font-size:0.7rem"></small>
|
<small id="usernameHint" style="color:var(--gray-700);font-size:0.7rem"></small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">फोन नंबर (वैकल्पिक)</label>
|
<label class="form-label">📱 <%= t('auth.phone') %></label>
|
||||||
<input type="tel" name="phone" class="form-input" placeholder="9876543210" maxlength="10">
|
<input type="tel" name="phone" class="form-input" placeholder="9876543210" maxlength="10">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">पासवर्ड *</label>
|
<label class="form-label">🔒 <%= t('auth.password') %> *</label>
|
||||||
<input type="password" name="password" class="form-input" placeholder="••••" required minlength="4">
|
<input type="password" name="password" class="form-input" placeholder="••••" required minlength="4">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">पासवर्ड पुष्टि *</label>
|
<label class="form-label">🔒 <%= t('auth.confirmPassword') %> *</label>
|
||||||
<input type="password" name="password_confirm" class="form-input" placeholder="••••" required minlength="4">
|
<input type="password" name="password_confirm" class="form-input" placeholder="••••" required minlength="4">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-cta btn-block btn-lg">मुफ्त पंजीकरण करें →</button>
|
<button type="submit" class="btn btn-cta btn-block btn-lg"><%= t('auth.registerBtn') %> →</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="text-center" style="margin-top:var(--space-lg);font-size:0.8rem">
|
<p class="text-center" style="margin-top:var(--space-lg);font-size:0.8rem">
|
||||||
पहले से खाता है? <a href="/login">लॉगिन करें</a>
|
<%= t('auth.hasAccount') %> <a href="/login"><%= t('actions.login') %></a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -75,17 +75,16 @@ document.querySelectorAll('input[name="role"]').forEach(r => {
|
||||||
const input = document.getElementById('usernameInput');
|
const input = document.getElementById('usernameInput');
|
||||||
const hint = document.getElementById('usernameHint');
|
const hint = document.getElementById('usernameHint');
|
||||||
if (this.value === 'driver') {
|
if (this.value === 'driver') {
|
||||||
label.textContent = 'गाड़ी नंबर / Vehicle Number *';
|
label.innerHTML = '🚛 <%= t("auth.vehicleNumber") %> *';
|
||||||
input.placeholder = 'MH31AB1234';
|
input.placeholder = 'MH31AB1234';
|
||||||
hint.textContent = 'आपका गाड़ी नंबर ही आपका यूज़रनेम होगा';
|
hint.textContent = '<%= t("auth.vehicleHint") %>';
|
||||||
} else {
|
} else {
|
||||||
label.textContent = 'यूज़रनेम *';
|
label.innerHTML = '📝 <%= t("auth.username") %> *';
|
||||||
input.placeholder = 'अपना यूज़रनेम चुनें';
|
input.placeholder = '<%= t("auth.username") %>';
|
||||||
hint.textContent = '';
|
hint.textContent = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// Trigger on load if role pre-selected
|
|
||||||
const checked = document.querySelector('input[name="role"]:checked');
|
const checked = document.querySelector('input[name="role"]:checked');
|
||||||
if (checked) checked.dispatchEvent(new Event('change'));
|
if (checked) checked.dispatchEvent(new Event('change'));
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
22
webapp/src/views/pages/reports.ejs
Normal file
22
webapp/src/views/pages/reports.ejs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<% var title = 'Reports'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-md)">
|
||||||
|
<h2 style="font-size:1.3rem">📊 Reports</h2>
|
||||||
|
<a href="/reports/export" class="btn btn-sm btn-outline">📥 CSV Export</a>
|
||||||
|
</div>
|
||||||
|
<div class="stats-grid" style="margin-bottom:var(--space-md)">
|
||||||
|
<div class="stat-card"><div class="stat-value"><%= stats.total_trips %></div><div class="stat-label">Total Trips</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value"><%= stats.month_trips %></div><div class="stat-label">This Month</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value">₹<%= stats.total_revenue.toLocaleString('en-IN') %></div><div class="stat-label">💰 Revenue</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value">₹<%= stats.total_expenses.toLocaleString('en-IN') %></div><div class="stat-label">💸 Expenses</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="card card-accent" style="border-left-color:<%= stats.profit >= 0 ? 'var(--green)' : 'red' %>;padding:var(--space-md);text-align:center">
|
||||||
|
<div style="font-size:0.85rem;color:var(--gray-700)">Net Profit</div>
|
||||||
|
<div style="font-size:1.5rem;font-weight:700;color:<%= stats.profit >= 0 ? 'green' : 'red' %>">₹<%= stats.profit.toLocaleString('en-IN') %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
40
webapp/src/views/pages/return-load.ejs
Normal file
40
webapp/src/views/pages/return-load.ejs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<% var title = 'Return Load'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🔄 Return Load</h2>
|
||||||
|
<% if (availability && availability.status === 'looking') { %>
|
||||||
|
<div class="card card-accent" style="border-left-color:var(--green);margin-bottom:var(--space-md);padding:var(--space-md)">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div><strong>✅ You're visible in <%= availability.current_city %></strong><div style="font-size:0.8rem;color:var(--gray-700)">Shippers can find you</div></div>
|
||||||
|
<form method="POST" action="/returnload/cancel" style="margin:0"><button class="btn btn-sm btn-outline">Cancel</button></form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<form method="POST" action="/returnload/available" class="card" style="padding:var(--space-md);margin-bottom:var(--space-md)">
|
||||||
|
<h4 style="margin-bottom:var(--space-sm)">📍 Where are you now?</h4>
|
||||||
|
<div style="display:grid;gap:var(--space-sm)">
|
||||||
|
<input type="text" name="current_city" class="form-input" placeholder="📍 Current City" required>
|
||||||
|
<input type="text" name="home_city" class="form-input" placeholder="🏠 Home City (optional)">
|
||||||
|
<select name="vehicle_type" class="form-input form-select">
|
||||||
|
<option value="open">Open Body</option><option value="container">Container</option><option value="trailer">Trailer</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">🔍 Find Return Loads</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<% } %>
|
||||||
|
<% if (suggestions.length > 0) { %>
|
||||||
|
<h3 style="font-size:1rem;margin-bottom:var(--space-sm)">📋 Available Loads</h3>
|
||||||
|
<% suggestions.forEach(load => { %>
|
||||||
|
<a href="/loadboard/<%= load.id %>" class="card card-accent" style="display:block;text-decoration:none;color:inherit;margin-bottom:var(--space-sm)">
|
||||||
|
<strong>📍 <%= load.origin_city %> → <%= load.destination_city %></strong>
|
||||||
|
<div style="font-size:0.8rem;color:var(--gray-700)"><%= load.weight_tons %> tons | ₹<%= (load.budget||0).toLocaleString('en-IN') %></div>
|
||||||
|
</a>
|
||||||
|
<% }) %>
|
||||||
|
<% } else if (availability) { %>
|
||||||
|
<div class="card text-center" style="padding:var(--space-xl)"><p>📭 No loads from your city yet. We'll notify you!</p></div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
18
webapp/src/views/pages/safety-sent.ejs
Normal file
18
webapp/src/views/pages/safety-sent.ejs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<% var title = is_sos ? 'SOS' : 'Check-in'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container text-center">
|
||||||
|
<div style="font-size:3rem;margin-bottom:var(--space-md)"><%= is_sos ? '🆘' : '✅' %></div>
|
||||||
|
<h2 style="font-size:1.2rem;margin-bottom:var(--space-md)"><%= is_sos ? 'SOS Alert Ready' : 'Check-in Ready' %></h2>
|
||||||
|
<p style="color:var(--gray-700);font-size:0.85rem;margin-bottom:var(--space-lg)">Tap to send via WhatsApp:</p>
|
||||||
|
<div style="display:grid;gap:var(--space-sm)">
|
||||||
|
<% links.forEach(l => { %>
|
||||||
|
<a href="<%= l.url %>" target="_blank" class="btn btn-block <%= is_sos ? '' : 'btn-primary' %>" style="<%= is_sos ? 'background:red;color:#fff' : '' %>">💬 <%= l.name %></a>
|
||||||
|
<% if (l.call) { %><a href="<%= l.call %>" class="btn btn-outline btn-block btn-sm">📞 Call <%= l.name %></a><% } %>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
<a href="/safety" class="btn btn-outline btn-block" style="margin-top:var(--space-lg)">← Back</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
49
webapp/src/views/pages/safety.ejs
Normal file
49
webapp/src/views/pages/safety.ejs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<% var title = 'Safety'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🛡️ Safety</h2>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md);margin-bottom:var(--space-lg)">
|
||||||
|
<form method="POST" action="/safety/checkin" style="margin:0">
|
||||||
|
<input type="hidden" name="message" value="I am safe.">
|
||||||
|
<button type="submit" class="icon-action-btn" style="width:100%;border-color:var(--green)">
|
||||||
|
<span class="icon-action-emoji">✅</span><span class="icon-action-label">I'm Safe</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/safety/sos" style="margin:0">
|
||||||
|
<input type="hidden" name="emergency_type" value="Emergency">
|
||||||
|
<button type="submit" class="icon-action-btn" style="width:100%;border-color:red;color:red">
|
||||||
|
<span class="icon-action-emoji">🆘</span><span class="icon-action-label">SOS</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="padding:var(--space-md);margin-bottom:var(--space-md);background:#fff3e0">
|
||||||
|
<strong>📞 Emergency:</strong>
|
||||||
|
<div style="display:flex;gap:var(--space-sm);margin-top:8px;flex-wrap:wrap">
|
||||||
|
<a href="tel:100" class="btn btn-sm btn-outline">🚔 100</a>
|
||||||
|
<a href="tel:108" class="btn btn-sm btn-outline">🚑 108</a>
|
||||||
|
<a href="tel:1033" class="btn btn-sm btn-outline">🛣️ 1033</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 style="font-size:1rem;margin-bottom:var(--space-sm)">👨👩👧 Family Contacts</h3>
|
||||||
|
<% contacts.forEach(c => { %>
|
||||||
|
<div class="card" style="padding:12px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div><strong><%= c.contact_name %></strong><div style="font-size:0.75rem;color:var(--gray-700)"><%= c.contact_phone %> • <%= c.relationship %></div></div>
|
||||||
|
<form method="POST" action="/safety/contacts/delete/<%= c.id %>" style="margin:0"><button class="btn btn-sm" style="color:red">✕</button></form>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<form method="POST" action="/safety/contacts/add" class="card" style="padding:var(--space-md);margin-top:var(--space-md)">
|
||||||
|
<h4 style="margin-bottom:var(--space-sm)">➕ Add Contact</h4>
|
||||||
|
<div style="display:grid;gap:var(--space-sm)">
|
||||||
|
<input type="text" name="contact_name" class="form-input" placeholder="👤 Name" required>
|
||||||
|
<input type="tel" name="contact_phone" class="form-input" placeholder="📱 Phone" required maxlength="10">
|
||||||
|
<select name="relationship" class="form-input form-select">
|
||||||
|
<option value="family">👨👩👧 Family</option><option value="spouse">💑 Spouse</option><option value="friend">🤝 Friend</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
24
webapp/src/views/pages/search.ejs
Normal file
24
webapp/src/views/pages/search.ejs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<% var title = 'Search'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🔍 Search</h2>
|
||||||
|
<form method="GET" action="/search" style="margin-bottom:var(--space-md)">
|
||||||
|
<div style="display:grid;grid-template-columns:1fr auto;gap:var(--space-sm)">
|
||||||
|
<input type="text" name="q" class="form-input" placeholder="Search loads, users, ads..." value="<%= q %>" autofocus>
|
||||||
|
<button type="submit" class="btn btn-primary">🔍</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<% if (q) { %>
|
||||||
|
<% if (results.loads.length) { %><h3 style="font-size:1rem;margin:var(--space-md) 0 var(--space-sm)">📋 Loads (<%= results.loads.length %>)</h3>
|
||||||
|
<% results.loads.forEach(l => { %><a href="/loadboard/<%= l.id %>" class="card" style="display:block;padding:10px;margin-bottom:6px;text-decoration:none;color:inherit"><strong><%= l.origin_city %> → <%= l.destination_city %></strong> <% if(l.budget){%>| ₹<%= l.budget.toLocaleString('en-IN') %><%}%></a><% }) } %>
|
||||||
|
<% if (results.users.length) { %><h3 style="font-size:1rem;margin:var(--space-md) 0 var(--space-sm)">👤 Users (<%= results.users.length %>)</h3>
|
||||||
|
<% results.users.forEach(u => { %><div class="card" style="padding:10px;margin-bottom:6px"><strong><%= u.name || u.username %></strong> <span class="badge"><%= u.role %></span></div><% }) } %>
|
||||||
|
<% if (results.classifieds.length) { %><h3 style="font-size:1rem;margin:var(--space-md) 0 var(--space-sm)">🛒 Classifieds (<%= results.classifieds.length %>)</h3>
|
||||||
|
<% results.classifieds.forEach(c => { %><div class="card" style="padding:10px;margin-bottom:6px"><strong><%= c.title %></strong> | ₹<%= (c.price||0).toLocaleString('en-IN') %></div><% }) } %>
|
||||||
|
<% if (!results.loads.length && !results.users.length && !results.classifieds.length) { %><div class="card text-center" style="padding:var(--space-xl)"><p>No results for "<%= q %>"</p></div><% } %>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
<% var title = 'शिपर डैशबोर्ड'; %>
|
<% var title = t('dashboard.shipperTitle'); %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="section" style="padding-top:var(--space-lg)">
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">📦 नमस्ते, <%= user.name %>!</h2>
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">📦 <%= t('dashboard.hello') %>, <%= user.name %>!</h2>
|
||||||
|
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-card"><div class="stat-value"><%= stats.totalLoads %></div><div class="stat-label">मेरे लोड</div></div>
|
<div class="stat-card"><div class="stat-value"><%= stats.totalLoads %></div><div class="stat-label"><%= t('dashboard.myLoads') %></div></div>
|
||||||
<div class="stat-card"><div class="stat-value"><%= stats.openLoads %></div><div class="stat-label">खुले लोड</div></div>
|
<div class="stat-card"><div class="stat-value"><%= stats.openLoads %></div><div class="stat-label"><%= t('dashboard.openLoads') %></div></div>
|
||||||
<div class="stat-card"><div class="stat-value"><%= stats.activeTrips %></div><div class="stat-label">सक्रिय शिपमेंट</div></div>
|
<div class="stat-card"><div class="stat-value"><%= stats.activeTrips %></div><div class="stat-label"><%= t('dashboard.activeShipments') %></div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if (recentLoads.length > 0) { %>
|
<% if (recentLoads.length > 0) { %>
|
||||||
<h3 style="font-size:1rem;margin-top:var(--space-lg);margin-bottom:var(--space-sm)">📋 हाल के लोड</h3>
|
<h3 style="font-size:1rem;margin-top:var(--space-lg);margin-bottom:var(--space-sm)">📋 <%= t('dashboard.recentLoads') %></h3>
|
||||||
<% recentLoads.forEach(load => { %>
|
<% recentLoads.forEach(load => { %>
|
||||||
<a href="/loadboard/<%= load.id %>" class="card card-accent" style="display:block;text-decoration:none;color:inherit;margin-bottom:var(--space-sm)">
|
<a href="/loadboard/<%= load.id %>" class="card card-accent" style="display:block;text-decoration:none;color:inherit;margin-bottom:var(--space-sm)">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
<div>
|
<div>
|
||||||
<strong><%= load.origin_city %> → <%= load.destination_city %></strong>
|
<strong><%= load.origin_city %> → <%= load.destination_city %></strong>
|
||||||
<div style="font-size:0.8rem;color:var(--gray-700)"><%= load.weight_tons %> टन | 🏷️ <%= load.bid_count %> बोली</div>
|
<div style="font-size:0.8rem;color:var(--gray-700)"><%= load.weight_tons %> <%= t('common.tons') %> | 🏷️ <%= load.bid_count %> <%= t('common.bids') %></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge badge-<%= load.status === 'open' ? 'open' : 'booked' %>"><%= load.status %></span>
|
<span class="badge badge-<%= load.status === 'open' ? 'open' : 'booked' %>"><%= load.status %></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -27,9 +27,39 @@
|
||||||
<% }) %>
|
<% }) %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<div style="margin-top:var(--space-lg);display:grid;gap:var(--space-sm)">
|
<div style="margin-top:var(--space-lg);display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
|
||||||
<a href="/loadboard/post" class="btn btn-cta btn-block">+ नया लोड पोस्ट करें</a>
|
<a href="/loadboard/post" class="icon-action-btn">
|
||||||
<a href="/trips" class="btn btn-outline btn-block">🚚 मेरी शिपमेंट</a>
|
<span class="icon-action-emoji">➕</span>
|
||||||
|
<span class="icon-action-label"><%= t('actions.postLoad') %></span>
|
||||||
|
</a>
|
||||||
|
<a href="/trips" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🚚</span>
|
||||||
|
<span class="icon-action-label"><%= t('dashboard.activeShipments') %></span>
|
||||||
|
</a>
|
||||||
|
<a href="/invoice" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🧾</span>
|
||||||
|
<span class="icon-action-label">Invoice</span>
|
||||||
|
</a>
|
||||||
|
<a href="/rates" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">📊</span>
|
||||||
|
<span class="icon-action-label">Rates</span>
|
||||||
|
</a>
|
||||||
|
<a href="/fleet" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🚛</span>
|
||||||
|
<span class="icon-action-label">Fleet</span>
|
||||||
|
</a>
|
||||||
|
<a href="/classifieds" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🛒</span>
|
||||||
|
<span class="icon-action-label">Buy/Sell</span>
|
||||||
|
</a>
|
||||||
|
<a href="/messages" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">💬</span>
|
||||||
|
<span class="icon-action-label"><%= t('nav.messages') %></span>
|
||||||
|
</a>
|
||||||
|
<a href="/notifications" class="icon-action-btn">
|
||||||
|
<span class="icon-action-emoji">🔔</span>
|
||||||
|
<span class="icon-action-label">Alerts</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
43
webapp/src/views/pages/trip-planner.ejs
Normal file
43
webapp/src/views/pages/trip-planner.ejs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<% var title = 'Trip Planner'; %>
|
||||||
|
<%- include('../partials/header') %>
|
||||||
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🧮 Trip Cost Calculator</h2>
|
||||||
|
<form method="POST" action="/trip-planner/calculate" class="card" style="padding:var(--space-md)">
|
||||||
|
<div style="display:grid;gap:var(--space-sm)">
|
||||||
|
<div><label class="form-label">📍 From</label><input type="text" name="origin" class="form-input" list="cities" placeholder="Mumbai" required></div>
|
||||||
|
<div><label class="form-label">📍 To</label><input type="text" name="destination" class="form-input" list="cities" placeholder="Delhi" required></div>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-sm)">
|
||||||
|
<div><label class="form-label">⛽ Mileage (km/l)</label><input type="number" name="mileage" class="form-input" value="4" step="0.5"></div>
|
||||||
|
<div><label class="form-label">💰 Freight ₹</label><input type="number" name="freight_charged" class="form-input" placeholder="Optional"></div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">🧮 Calculate</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<datalist id="cities"><% cities.forEach(c => { %><option value="<%= c %>"><% }) %></datalist>
|
||||||
|
<% if (result && !result.error) { %>
|
||||||
|
<div class="card" style="margin-top:var(--space-md);padding:var(--space-md)">
|
||||||
|
<h3 style="font-size:1rem;margin-bottom:var(--space-sm)">📊 <%= result.origin %> → <%= result.destination %></h3>
|
||||||
|
<div style="font-size:0.85rem;color:var(--gray-700);margin-bottom:var(--space-md)">📏 <%= result.distance_km %> km | ⏱️ ~<%= result.hours %> hrs | 🚧 <%= result.toll_plazas %> tolls</div>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card"><div class="stat-value">₹<%= result.fuel.cost.toLocaleString('en-IN') %></div><div class="stat-label">⛽ Fuel</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value">₹<%= result.toll.toLocaleString('en-IN') %></div><div class="stat-label">🚧 Toll</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value">₹<%= result.driver_bata.toLocaleString('en-IN') %></div><div class="stat-label">🍽️ Bata</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value">₹<%= result.total.toLocaleString('en-IN') %></div><div class="stat-label">💸 Total</div></div>
|
||||||
|
</div>
|
||||||
|
<% if (result.profit !== undefined && result.profit !== 0) { %>
|
||||||
|
<div class="card card-accent" style="margin-top:var(--space-md);border-left-color:<%= result.viable ? 'var(--green)' : 'red' %>">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<span style="font-weight:700"><%= result.viable ? '✅ Profitable' : '❌ Loss' %></span>
|
||||||
|
<span style="font-size:1.2rem;font-weight:700;color:<%= result.viable ? 'green' : 'red' %>">₹<%= result.profit.toLocaleString('en-IN') %> (<%= result.margin %>%)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<% } else if (result && result.error) { %>
|
||||||
|
<div class="card" style="margin-top:var(--space-md);padding:var(--space-md);text-align:center;color:var(--gray-700)">⚠️ <%= result.error %></div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<% var title = 'मेरी ट्रिप'; %>
|
<% var title = t('actions.myTrips'); %>
|
||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
<div class="tricolor-strip"><div class="tricolor-saffron"></div><div class="tricolor-white"></div><div class="tricolor-green"></div></div>
|
||||||
|
|
||||||
<section class="section" style="padding-top:var(--space-lg)">
|
<section class="section" style="padding-top:var(--space-lg)">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🚚 मेरी ट्रिप</h2>
|
<h2 style="font-size:1.3rem;margin-bottom:var(--space-md)">🚚 <%= t('actions.myTrips') %></h2>
|
||||||
|
|
||||||
<% if (trips.length === 0) { %>
|
<% if (trips.length === 0) { %>
|
||||||
<div class="card text-center" style="padding:var(--space-2xl)">
|
<div class="card text-center" style="padding:var(--space-2xl)">
|
||||||
<p>कोई ट्रिप नहीं</p>
|
<p><%= t('trips.noTrips') %></p>
|
||||||
</div>
|
</div>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<div style="display:grid;gap:var(--space-md)">
|
<div style="display:grid;gap:var(--space-md)">
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<strong>📍 <%= trip.load ? trip.load.origin_city + ' → ' + trip.load.destination_city : 'N/A' %></strong>
|
<strong>📍 <%= trip.load ? trip.load.origin_city + ' → ' + trip.load.destination_city : 'N/A' %></strong>
|
||||||
<div style="font-size:0.8rem;color:var(--gray-700);margin-top:4px">
|
<div style="font-size:0.8rem;color:var(--gray-700);margin-top:4px">
|
||||||
₹<%= Number(trip.amount).toLocaleString('en-IN') %>
|
₹<%= 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 %><% } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge badge-<%= trip.status === 'delivered' ? 'delivered' : trip.status === 'cancelled' ? 'cancelled' : 'transit' %>"><%= trip.status %></span>
|
<span class="badge badge-<%= trip.status === 'delivered' ? 'delivered' : trip.status === 'cancelled' ? 'cancelled' : 'transit' %>"><%= trip.status %></span>
|
||||||
|
|
@ -28,11 +28,11 @@
|
||||||
<% if (user.role === 'driver' && trip.status !== 'delivered' && trip.status !== 'cancelled') { %>
|
<% if (user.role === 'driver' && trip.status !== 'delivered' && trip.status !== 'cancelled') { %>
|
||||||
<div style="margin-top:var(--space-md);display:flex;gap:var(--space-sm)">
|
<div style="margin-top:var(--space-md);display:flex;gap:var(--space-sm)">
|
||||||
<% if (trip.status === 'confirmed') { %>
|
<% if (trip.status === 'confirmed') { %>
|
||||||
<form method="POST" action="/trips/<%= trip.id %>/status"><input type="hidden" name="status" value="picked_up"><button class="btn btn-primary btn-sm">पिकअप किया</button></form>
|
<form method="POST" action="/trips/<%= trip.id %>/status"><input type="hidden" name="status" value="picked_up"><button class="btn btn-primary btn-sm">📦 <%= t('trips.pickedUp') %></button></form>
|
||||||
<% } else if (trip.status === 'picked_up') { %>
|
<% } else if (trip.status === 'picked_up') { %>
|
||||||
<form method="POST" action="/trips/<%= trip.id %>/status"><input type="hidden" name="status" value="in_transit"><button class="btn btn-primary btn-sm">रास्ते में</button></form>
|
<form method="POST" action="/trips/<%= trip.id %>/status"><input type="hidden" name="status" value="in_transit"><button class="btn btn-primary btn-sm">🚛 <%= t('trips.inTransit') %></button></form>
|
||||||
<% } else if (trip.status === 'in_transit') { %>
|
<% } else if (trip.status === 'in_transit') { %>
|
||||||
<form method="POST" action="/trips/<%= trip.id %>/status"><input type="hidden" name="status" value="delivered"><button class="btn btn-success btn-sm">पहुँचा दिया ✓</button></form>
|
<form method="POST" action="/trips/<%= trip.id %>/status"><input type="hidden" name="status" value="delivered"><button class="btn btn-success btn-sm">✅ <%= t('trips.delivered') %></button></form>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,31 @@
|
||||||
<% if (user) { %>
|
<% if (user) { %>
|
||||||
<nav class="bottom-nav">
|
<nav class="bottom-nav" aria-label="Main navigation">
|
||||||
<a href="/<%= user.role %>" class="bnav-item"><span class="bnav-icon">🏠</span><span>होम</span></a>
|
<a href="/<%= user.role %>" class="bnav-item">
|
||||||
<a href="/loadboard" class="bnav-item"><span class="bnav-icon">📋</span><span>लोड</span></a>
|
<span class="bnav-icon-lg">🏠</span>
|
||||||
|
<span class="bnav-label"><%= t('nav.home') %></span>
|
||||||
|
</a>
|
||||||
|
<a href="/loadboard" class="bnav-item">
|
||||||
|
<span class="bnav-icon-lg">📋</span>
|
||||||
|
<span class="bnav-label"><%= t('nav.loads') %></span>
|
||||||
|
</a>
|
||||||
<% if (user.role === 'shipper' || user.role === 'broker') { %>
|
<% if (user.role === 'shipper' || user.role === 'broker') { %>
|
||||||
<a href="/loadboard/post" class="bnav-item bnav-add"><span class="bnav-icon">➕</span><span>पोस्ट</span></a>
|
<a href="/loadboard/post" class="bnav-item bnav-add">
|
||||||
|
<span class="bnav-icon-lg">➕</span>
|
||||||
|
<span class="bnav-label"><%= t('nav.post') %></span>
|
||||||
|
</a>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a href="/trips" class="bnav-item"><span class="bnav-icon">🚚</span><span>ट्रिप</span></a>
|
<a href="/search" class="bnav-item">
|
||||||
|
<span class="bnav-icon-lg">🔍</span>
|
||||||
|
<span class="bnav-label">Search</span>
|
||||||
|
</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
<a href="/messages" class="bnav-item"><span class="bnav-icon">💬</span><span>संदेश</span></a>
|
<a href="/notifications" class="bnav-item">
|
||||||
<a href="/profile" class="bnav-item"><span class="bnav-icon">👤</span><span>प्रोफ़ाइल</span></a>
|
<span class="bnav-icon-lg">🔔</span>
|
||||||
|
<span class="bnav-label">Alerts</span>
|
||||||
|
</a>
|
||||||
|
<a href="/more" class="bnav-item">
|
||||||
|
<span class="bnav-icon-lg">⋯</span>
|
||||||
|
<span class="bnav-label">More</span>
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,13 @@
|
||||||
<footer class="govt-footer">
|
<footer class="govt-footer">
|
||||||
<div class="footer-inner">
|
<div class="footer-inner">
|
||||||
<div class="footer-brand">
|
<div class="footer-brand">
|
||||||
<strong>भारत ट्रक्स | BharathTrucks</strong>
|
<strong><%= t('common.appName') %> | BharathTrucks</strong>
|
||||||
<p>राष्ट्रीय माल परिवहन मंच</p>
|
<p><%= t('common.subtitle') %></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-links">
|
<p class="footer-copy">© 2026 BharathTrucks.</p>
|
||||||
<a href="/about">हमारे बारे में</a>
|
|
||||||
<a href="/contact">संपर्क करें</a>
|
|
||||||
</div>
|
|
||||||
<p class="footer-copy">© 2026 BharathTrucks. सर्वाधिकार सुरक्षित।</p>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
|
<script src="/js/voice.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
<meta name="theme-color" content="#1a237e">
|
<meta name="theme-color" content="#1a237e">
|
||||||
<title><%= typeof title !== 'undefined' ? title + ' | भारत ट्रक्स' : 'भारत ट्रक्स' %></title>
|
<title><%= typeof title !== 'undefined' ? title + ' | ' + t('common.appName') : t('common.appName') %></title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;600;700&family=Noto+Sans+Devanagari:wght@400;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;600;700&family=Noto+Sans+Devanagari:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
@ -18,17 +18,23 @@
|
||||||
<div class="header-brand">
|
<div class="header-brand">
|
||||||
<div class="header-emblem">🏛️</div>
|
<div class="header-emblem">🏛️</div>
|
||||||
<div class="header-titles">
|
<div class="header-titles">
|
||||||
<h1 class="header-title-hi">भारत ट्रक्स</h1>
|
<h1 class="header-title-hi"><%= t('common.appName') %></h1>
|
||||||
<p class="header-subtitle">राष्ट्रीय माल परिवहन मंच | National Freight Transport Platform</p>
|
<p class="header-subtitle"><%= t('common.subtitle') %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav class="header-nav">
|
<nav class="header-nav">
|
||||||
|
<div class="lang-switcher">
|
||||||
|
<a href="/lang/hi" class="lang-btn <%= lang === 'hi' ? 'active' : '' %>" title="हिंदी">हि</a>
|
||||||
|
<a href="/lang/en" class="lang-btn <%= lang === 'en' ? 'active' : '' %>" title="English">EN</a>
|
||||||
|
<a href="/lang/ta" class="lang-btn <%= lang === 'ta' ? 'active' : '' %>" title="தமிழ்">த</a>
|
||||||
|
<a href="/lang/te" class="lang-btn <%= lang === 'te' ? 'active' : '' %>" title="తెలుగు">తె</a>
|
||||||
|
</div>
|
||||||
<% if (user) { %>
|
<% if (user) { %>
|
||||||
<span class="header-user"><%= user.name || user.username %></span>
|
<span class="header-user"><%= user.name || user.username %></span>
|
||||||
<a href="/logout" class="header-link">लॉगआउट</a>
|
<a href="/logout" class="header-link"><%= t('actions.logout') %></a>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a href="/login" class="header-link">लॉगिन</a>
|
<a href="/login" class="header-link"><%= t('actions.login') %></a>
|
||||||
<a href="/register" class="btn-header-cta">पंजीकरण</a>
|
<a href="/register" class="btn-header-cta"><%= t('actions.register') %></a>
|
||||||
<% } %>
|
<% } %>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
106
webapp/supabase-phase1-migration.sql
Normal file
106
webapp/supabase-phase1-migration.sql
Normal file
|
|
@ -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);
|
||||||
91
webapp/supabase-phase2-migration.sql
Normal file
91
webapp/supabase-phase2-migration.sql
Normal file
|
|
@ -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);
|
||||||
1
webapp/supabase-phase3-migration.sql
Normal file
1
webapp/supabase-phase3-migration.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
Loading…
Reference in a new issue