BharathTrucks MVP - 6 sprints complete

- Govt-app styled freight marketplace
- Role-based auth (driver/shipper/broker/admin)
- Load board with bidding system
- Trip tracking with status flow
- In-app messaging
- Admin panel
- Mobile bottom nav + PWA
- Docker + Coolify ready
This commit is contained in:
Vivek 2026-05-31 06:21:13 +00:00
commit 394117dd74
60 changed files with 6276 additions and 0 deletions

88
README.md Normal file
View file

@ -0,0 +1,88 @@
# 🚛 BharathTrucks — India's National Freight Marketplace
> राष्ट्रीय माल परिवहन मंच — ट्रक ड्राइवर, शिपर और ब्रोकर के लिए
## Quick Start
```bash
cd webapp
npm install
cp .env.example .env # Add your Supabase credentials
npm start # http://localhost:3000
```
## Database Setup
1. Create a Supabase project at [supabase.com](https://supabase.com)
2. Go to SQL Editor → paste contents of `supabase-FULL-migration.sql` → Run
3. Copy your project URL and anon key to `.env`
**Default admin:** username=`admin`, password=`admin123`
## Deploy to Production (Coolify + Hostinger VPS)
1. Push code to GitHub/GitLab
2. In Coolify: New Resource → Docker → point to repo
3. Set environment variables (from `.env.example`)
4. Domain: bharathtrucks.com → point DNS to VPS IP
5. Done — auto-deploys on push to main
## Tech Stack
| Layer | Technology |
|-------|-----------|
| Backend | Node.js + Express |
| Views | EJS (server-rendered) |
| Database | Supabase (PostgreSQL) |
| Auth | Username + Password (bcrypt) |
| Styles | Custom CSS (govt-app theme) |
| Deployment | Docker + Coolify |
| PWA | Service Worker + Manifest |
## Features
- **Load Board** — Shippers post loads, drivers browse and bid
- **Bidding** — Drivers bid on loads, shippers accept best bid
- **Trip Tracking** — Status flow: confirmed → picked up → in transit → delivered
- **Messaging** — Direct chat between users
- **Dashboards** — Role-specific (driver/shipper/broker) with real stats
- **Admin Panel** — User management, platform metrics, load overview
- **WhatsApp Share** — Share loads via WhatsApp
- **Mobile-First** — Bottom nav, responsive, PWA installable
- **Govt-App Design** — Tricolor, navy theme, Hindi-first, trust signals
## User Roles
| Role | Username | Features |
|------|----------|----------|
| Driver | Vehicle number (e.g. MH31AB1234) | Browse loads, bid, track trips, earnings |
| Shipper | Choose any username | Post loads, review bids, accept, track shipments |
| Broker | Choose any username | Post loads for clients, manage deals |
| Admin | `admin` | User management, platform metrics |
## Project Structure
```
webapp/
├── src/
│ ├── server.js # Express app entry
│ ├── config/ # env.js, constants.js
│ ├── middleware/ # auth.js
│ ├── routes/ # auth, loads, trips, admin, messages
│ ├── services/ # supabase.js
│ ├── views/pages/ # All EJS pages
│ ├── views/partials/ # header, footer, bottom-nav
│ └── public/ # CSS, JS, manifest, SW
├── Dockerfile
├── package.json
└── supabase-FULL-migration.sql
```
## Environment Variables
```
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your-anon-key
SESSION_SECRET=random-64-char-string
PORT=3000
```

12
docker/docker-compose.yml Normal file
View file

@ -0,0 +1,12 @@
version: '3.8'
services:
app:
build:
context: ./webapp
dockerfile: Dockerfile
ports:
- "3000:3000"
env_file:
- ./webapp/.env
restart: unless-stopped

284
docs/api/API_DESIGN.md Normal file
View file

@ -0,0 +1,284 @@
# BharathTrucks — API Design Document
**Version:** 1.0
**Date:** 2026-05-31
**Base URL:** `https://bharathtrucks.com`
---
## 1. API Architecture
This is a **server-rendered application** (EJS), so most routes return HTML pages. However, some endpoints return JSON for AJAX interactions (bidding, messaging, notifications).
### Route Naming Convention
- Pages: `GET /resource` → renders HTML
- Actions: `POST /resource` → form submission, redirects
- API (JSON): `GET|POST /api/resource` → returns JSON
### Authentication
All protected routes use session middleware. Supabase JWT stored in httpOnly cookie.
---
## 2. Public Routes (No Auth)
| Method | Path | Description | Response |
|--------|------|-------------|----------|
| GET | `/` | Landing page | HTML |
| GET | `/loadboard` | Public load board (read-only) | HTML |
| GET | `/login` | Login page | HTML |
| POST | `/login` | Send OTP | Redirect |
| GET | `/verify-otp` | OTP input page | HTML |
| POST | `/verify-otp` | Verify OTP, create session | Redirect to dashboard |
| GET | `/register` | Registration page | HTML |
| POST | `/register` | Create account + send OTP | Redirect |
| GET | `/about` | About page | HTML |
| GET | `/contact` | Contact page | HTML |
---
## 3. Auth Routes
| Method | Path | Description | Response |
|--------|------|-------------|----------|
| POST | `/auth/send-otp` | Send OTP to phone | JSON `{success, message}` |
| POST | `/auth/verify-otp` | Verify OTP | JSON `{success, redirect}` |
| POST | `/auth/logout` | Destroy session | Redirect to `/` |
| GET | `/auth/onboarding` | Role-specific profile setup | HTML |
| POST | `/auth/onboarding` | Save profile details | Redirect to dashboard |
---
## 4. Load Routes
### Pages
| Method | Path | Description | Auth | Role |
|--------|------|-------------|------|------|
| GET | `/loads` | Load board (authenticated, with filters) | ✅ | All |
| GET | `/loads/new` | Post new load form | ✅ | Shipper, Broker |
| GET | `/loads/:id` | Load detail + bids | ✅ | All |
| GET | `/loads/:id/edit` | Edit load form | ✅ | Owner |
### Actions
| Method | Path | Description | Auth | Role |
|--------|------|-------------|------|------|
| POST | `/loads` | Create new load | ✅ | Shipper, Broker |
| POST | `/loads/:id` | Update load | ✅ | Owner |
| POST | `/loads/:id/cancel` | Cancel load | ✅ | Owner |
### API (JSON)
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| GET | `/api/loads` | Load list with filters | ✅ |
| GET | `/api/loads/search` | Search by city/route | Optional |
#### Query Parameters for `/api/loads`
```
?origin=Mumbai
&destination=Delhi
&truck_type=open
&min_weight=5
&max_weight=20
&pickup_date=2026-06-01
&status=open
&page=1
&limit=20
```
#### Response Format
```json
{
"success": true,
"data": {
"loads": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 45,
"pages": 3
}
}
}
```
---
## 5. Bid Routes
### Actions
| Method | Path | Description | Auth | Role |
|--------|------|-------------|------|------|
| POST | `/loads/:id/bid` | Place bid on load | ✅ | Driver |
| POST | `/bids/:id/accept` | Accept a bid | ✅ | Load owner |
| POST | `/bids/:id/reject` | Reject a bid | ✅ | Load owner |
| POST | `/bids/:id/withdraw` | Withdraw own bid | ✅ | Bidder |
### API (JSON)
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| GET | `/api/loads/:id/bids` | Get all bids for a load | ✅ (owner/bidder) |
| GET | `/api/bids/my` | Get my bid history | ✅ |
#### Bid Request Body
```json
{
"amount": 42000,
"estimated_delivery": "2026-06-05",
"note": "Can pick up by evening"
}
```
#### Bid Rate Limiting
- Free users: 5 bids/day → returns `429` with message after limit
- Premium users: unlimited
---
## 6. Dashboard Routes
### Driver
| Method | Path | Description |
|--------|------|-------------|
| GET | `/driver` | Driver dashboard home |
| GET | `/driver/trips` | My trips list |
| GET | `/driver/trips/:id` | Trip detail |
| POST | `/driver/trips/:id/status` | Update trip status |
| GET | `/driver/earnings` | Earnings summary |
| GET | `/driver/profile` | Edit profile/truck |
| POST | `/driver/profile` | Update profile |
| POST | `/driver/availability` | Toggle availability |
### Shipper
| Method | Path | Description |
|--------|------|-------------|
| GET | `/shipper` | Shipper dashboard home |
| GET | `/shipper/loads` | My posted loads |
| GET | `/shipper/shipments` | Active shipments |
| GET | `/shipper/payments` | Payment history |
| POST | `/shipper/rate/:tripId` | Rate a driver |
### Broker
| Method | Path | Description |
|--------|------|-------------|
| GET | `/broker` | Broker dashboard home |
| GET | `/broker/network` | Driver network |
| POST | `/broker/network/add` | Add driver to network |
| GET | `/broker/clients` | Shipper clients |
| GET | `/broker/commissions` | Commission ledger |
| POST | `/broker/commissions/:id/received` | Mark commission received |
| GET | `/broker/loads` | Loads I've posted |
---
## 7. Messaging Routes
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| GET | `/messages` | Inbox | ✅ |
| GET | `/messages/:userId` | Conversation with user | ✅ |
| POST | `/api/messages` | Send message | ✅ |
| GET | `/api/messages/:userId` | Get conversation (JSON) | ✅ |
| POST | `/api/messages/read/:id` | Mark as read | ✅ |
#### Message Request Body
```json
{
"receiver_id": "uuid",
"load_id": "uuid",
"content": "Is this load still available?"
}
```
---
## 8. Notification Routes
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| GET | `/api/notifications` | Get unread notifications | ✅ |
| POST | `/api/notifications/read` | Mark all as read | ✅ |
| POST | `/api/notifications/:id/read` | Mark one as read | ✅ |
---
## 9. Admin Routes
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| GET | `/admin` | Admin dashboard | ✅ Admin |
| GET | `/admin/users` | User management | ✅ Admin |
| POST | `/admin/users/:id/suspend` | Suspend user | ✅ Admin |
| POST | `/admin/users/:id/verify` | Verify user | ✅ Admin |
| GET | `/admin/loads` | All loads | ✅ Admin |
| GET | `/admin/metrics` | Platform metrics | ✅ Admin |
| POST | `/admin/broadcast` | Send announcement | ✅ Admin |
---
## 10. Shared API Patterns
### Success Response
```json
{
"success": true,
"data": { ... },
"message": "Load created successfully"
}
```
### Error Response
```json
{
"success": false,
"error": {
"code": "BID_LIMIT_REACHED",
"message": "Daily bid limit reached. Upgrade to premium for unlimited bids."
}
}
```
### HTTP Status Codes Used
| Code | Meaning |
|------|---------|
| 200 | Success |
| 201 | Created |
| 302 | Redirect (after form submit) |
| 400 | Bad request / validation error |
| 401 | Not authenticated |
| 403 | Not authorized (wrong role) |
| 404 | Not found |
| 429 | Rate limited |
| 500 | Server error |
---
## 11. Middleware Stack
```
Request → Compression → Helmet → Cookie Parser → Session Check
→ Rate Limiter → Role Check → Controller → Response
```
| Middleware | Purpose |
|-----------|---------|
| `compression` | Gzip responses |
| `helmet` | Security headers |
| `cookie-parser` | Parse auth cookies |
| `authMiddleware` | Validate JWT, attach user to req |
| `roleMiddleware(roles)` | Check user role access |
| `rateLimiter` | Global rate limit (100 req/min) |
| `bidLimiter` | Bid-specific limit (5/day free) |
| `errorHandler` | Catch-all error formatting |
---
## 12. WhatsApp Share (No API needed)
Load sharing via `wa.me` links with pre-formatted text:
```
https://wa.me/?text=🚛 New Load Available!%0A📍 Mumbai → Delhi%0A🏋 20 Ton Open Body%0A💰 ₹45,000%0A📅 2 Jun 2026%0A%0AView: https://bharathtrucks.com/loads/abc123
```
---
*All routes follow RESTful conventions. Server-rendered pages for core flows, JSON APIs for dynamic interactions.*

View file

@ -0,0 +1,365 @@
# BharathTrucks — Database Schema Design
**Version:** 1.0
**Date:** 2026-05-31
**Database:** Supabase PostgreSQL
---
## 1. Schema Overview
```
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Users │────▶│ Profiles │────▶│ Loads │
│(Supabase)│ │ │ │ │
└──────────┘ └──────────┘ └────┬─────┘
│ │
│ ┌────▼─────┐
│ │ Bids │
│ └────┬─────┘
│ │
┌────▼─────┐ ┌────▼─────┐
│ Trucks │ │ Trips │
└──────────┘ └────┬─────┘
┌────▼─────┐
│ Payments │
└──────────┘
```
---
## 2. Tables
### 2.1 profiles
Extends Supabase auth.users with app-specific data.
```sql
CREATE TABLE profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('driver', 'shipper', 'broker', 'admin')),
full_name TEXT NOT NULL,
phone TEXT UNIQUE NOT NULL,
email TEXT,
avatar_url TEXT,
language TEXT DEFAULT 'hi' CHECK (language IN ('en', 'hi')),
city TEXT,
state TEXT,
is_verified BOOLEAN DEFAULT FALSE,
is_premium BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
onboarding_complete BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_profiles_role ON profiles(role);
CREATE INDEX idx_profiles_city ON profiles(city);
```
### 2.2 driver_profiles
Additional driver-specific info.
```sql
CREATE TABLE driver_profiles (
id UUID PRIMARY KEY REFERENCES profiles(id) ON DELETE CASCADE,
license_number TEXT,
license_expiry DATE,
experience_years INTEGER DEFAULT 0,
routes_preferred TEXT[], -- ['Mumbai-Delhi', 'Chennai-Bangalore']
truck_id UUID REFERENCES trucks(id),
availability_status TEXT DEFAULT 'available' CHECK (availability_status IN ('available', 'on_trip', 'offline')),
total_trips INTEGER DEFAULT 0,
rating NUMERIC(2,1) DEFAULT 0.0,
bids_today INTEGER DEFAULT 0,
bids_today_date DATE DEFAULT CURRENT_DATE
);
```
### 2.3 shipper_profiles
```sql
CREATE TABLE shipper_profiles (
id UUID PRIMARY KEY REFERENCES profiles(id) ON DELETE CASCADE,
business_name TEXT,
gst_number TEXT,
business_type TEXT,
shipping_frequency TEXT CHECK (shipping_frequency IN ('daily', 'weekly', 'monthly', 'occasional')),
total_loads_posted INTEGER DEFAULT 0,
rating NUMERIC(2,1) DEFAULT 0.0
);
```
### 2.4 broker_profiles
```sql
CREATE TABLE broker_profiles (
id UUID PRIMARY KEY REFERENCES profiles(id) ON DELETE CASCADE,
agency_name TEXT,
experience_years INTEGER DEFAULT 0,
network_size INTEGER DEFAULT 0,
operating_regions TEXT[],
total_deals INTEGER DEFAULT 0,
total_commission NUMERIC(12,2) DEFAULT 0,
rating NUMERIC(2,1) DEFAULT 0.0
);
```
### 2.5 trucks
```sql
CREATE TABLE trucks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
registration_number TEXT UNIQUE NOT NULL,
truck_type TEXT NOT NULL CHECK (truck_type IN ('open', 'closed', 'container', 'flatbed', 'tanker', 'refrigerated', 'mini')),
capacity_tons NUMERIC(5,1) NOT NULL,
make TEXT, -- Tata, Ashok Leyland, etc.
model TEXT,
year INTEGER,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_trucks_owner ON trucks(owner_id);
CREATE INDEX idx_trucks_type ON trucks(truck_type);
```
### 2.6 loads
```sql
CREATE TABLE loads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
posted_by UUID NOT NULL REFERENCES profiles(id),
shipper_id UUID REFERENCES profiles(id), -- if broker posts on behalf
origin_city TEXT NOT NULL,
origin_state TEXT NOT NULL,
destination_city TEXT NOT NULL,
destination_state TEXT NOT NULL,
weight_tons NUMERIC(5,1) NOT NULL,
truck_type_required TEXT NOT NULL,
material_type TEXT,
budget NUMERIC(10,2),
pickup_date DATE NOT NULL,
description TEXT,
is_urgent BOOLEAN DEFAULT FALSE,
status TEXT DEFAULT 'open' CHECK (status IN ('open', 'booked', 'in_transit', 'delivered', 'cancelled')),
bid_count INTEGER DEFAULT 0,
accepted_bid_id UUID,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_loads_status ON loads(status);
CREATE INDEX idx_loads_origin ON loads(origin_city);
CREATE INDEX idx_loads_destination ON loads(destination_city);
CREATE INDEX idx_loads_posted_by ON loads(posted_by);
CREATE INDEX idx_loads_pickup_date ON loads(pickup_date);
CREATE INDEX idx_loads_truck_type ON loads(truck_type_required);
```
### 2.7 bids
```sql
CREATE TABLE bids (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
load_id UUID NOT NULL REFERENCES loads(id) ON DELETE CASCADE,
driver_id UUID NOT NULL REFERENCES profiles(id),
amount NUMERIC(10,2) NOT NULL,
estimated_delivery DATE,
note TEXT,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'rejected', 'withdrawn')),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(load_id, driver_id) -- one bid per driver per load
);
CREATE INDEX idx_bids_load ON bids(load_id);
CREATE INDEX idx_bids_driver ON bids(driver_id);
CREATE INDEX idx_bids_status ON bids(status);
```
### 2.8 trips
```sql
CREATE TABLE trips (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
load_id UUID NOT NULL REFERENCES loads(id),
driver_id UUID NOT NULL REFERENCES profiles(id),
shipper_id UUID NOT NULL REFERENCES profiles(id),
bid_id UUID NOT NULL REFERENCES bids(id),
status TEXT DEFAULT 'confirmed' CHECK (status IN ('confirmed', 'picked_up', 'in_transit', 'delivered', 'cancelled')),
picked_up_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ,
driver_rating INTEGER CHECK (driver_rating BETWEEN 1 AND 5),
shipper_rating INTEGER CHECK (shipper_rating BETWEEN 1 AND 5),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_trips_driver ON trips(driver_id);
CREATE INDEX idx_trips_shipper ON trips(shipper_id);
CREATE INDEX idx_trips_status ON trips(status);
```
### 2.9 messages
```sql
CREATE TABLE messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
load_id UUID REFERENCES loads(id),
sender_id UUID NOT NULL REFERENCES profiles(id),
receiver_id UUID NOT NULL REFERENCES profiles(id),
content TEXT NOT NULL,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_messages_receiver ON messages(receiver_id, is_read);
CREATE INDEX idx_messages_load ON messages(load_id);
```
### 2.10 payments
```sql
CREATE TABLE payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
trip_id UUID NOT NULL REFERENCES trips(id),
payer_id UUID NOT NULL REFERENCES profiles(id),
payee_id UUID NOT NULL REFERENCES profiles(id),
amount NUMERIC(10,2) NOT NULL,
method TEXT DEFAULT 'upi' CHECK (method IN ('upi', 'cash', 'bank_transfer')),
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'completed', 'disputed')),
upi_reference TEXT,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_payments_trip ON payments(trip_id);
CREATE INDEX idx_payments_payee ON payments(payee_id);
```
### 2.11 broker_commissions
```sql
CREATE TABLE broker_commissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
broker_id UUID NOT NULL REFERENCES profiles(id),
trip_id UUID NOT NULL REFERENCES trips(id),
load_id UUID NOT NULL REFERENCES loads(id),
amount NUMERIC(10,2) NOT NULL,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'received')),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_commissions_broker ON broker_commissions(broker_id);
```
### 2.12 notifications
```sql
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES profiles(id),
type TEXT NOT NULL CHECK (type IN ('bid_received', 'bid_accepted', 'bid_rejected', 'trip_update', 'payment', 'system')),
title TEXT NOT NULL,
body TEXT,
reference_id UUID, -- load_id, bid_id, trip_id
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_notifications_user ON notifications(user_id, is_read);
```
---
## 3. Row Level Security (RLS) Policies
```sql
-- Profiles: users can read all, update own
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Public profiles readable" ON profiles FOR SELECT USING (true);
CREATE POLICY "Users update own profile" ON profiles FOR UPDATE USING (auth.uid() = id);
-- Loads: all can read open loads, owners can update
ALTER TABLE loads ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Open loads readable" ON loads FOR SELECT USING (true);
CREATE POLICY "Owners manage loads" ON loads FOR ALL USING (auth.uid() = posted_by);
-- Bids: load owner + bidder can see
ALTER TABLE bids ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Bid participants can view" ON bids FOR SELECT
USING (auth.uid() = driver_id OR auth.uid() IN (SELECT posted_by FROM loads WHERE id = load_id));
CREATE POLICY "Drivers create bids" ON bids FOR INSERT WITH CHECK (auth.uid() = driver_id);
-- Messages: sender and receiver only
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Message participants" ON messages FOR SELECT
USING (auth.uid() = sender_id OR auth.uid() = receiver_id);
CREATE POLICY "Users send messages" ON messages FOR INSERT WITH CHECK (auth.uid() = sender_id);
```
---
## 4. Database Functions
```sql
-- Auto-update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER profiles_updated_at BEFORE UPDATE ON profiles
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER loads_updated_at BEFORE UPDATE ON loads
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER bids_updated_at BEFORE UPDATE ON bids
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trips_updated_at BEFORE UPDATE ON trips
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- Increment bid count on load when new bid placed
CREATE OR REPLACE FUNCTION increment_bid_count()
RETURNS TRIGGER AS $$
BEGIN
UPDATE loads SET bid_count = bid_count + 1 WHERE id = NEW.load_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER bid_count_trigger AFTER INSERT ON bids
FOR EACH ROW EXECUTE FUNCTION increment_bid_count();
-- Reset daily bid count
CREATE OR REPLACE FUNCTION reset_daily_bids()
RETURNS void AS $$
BEGIN
UPDATE driver_profiles SET bids_today = 0, bids_today_date = CURRENT_DATE
WHERE bids_today_date < CURRENT_DATE;
END;
$$ LANGUAGE plpgsql;
```
---
## 5. Seed Data (Development)
```sql
-- Truck types reference
INSERT INTO trucks (owner_id, registration_number, truck_type, capacity_tons, make)
VALUES
-- Will be populated during testing
;
-- Sample cities for load testing
-- Mumbai, Delhi, Bangalore, Chennai, Hyderabad, Ahmedabad, Pune, Kolkata, Jaipur, Nagpur
```
---
*Schema designed for simplicity in Phase 1. Normalized where needed, denormalized (bid_count, total_trips) for read performance.*

View file

@ -0,0 +1,286 @@
# BharathTrucks — Deployment & Infrastructure
**Version:** 1.0
**Date:** 2026-05-31
---
## 1. Infrastructure Overview
```
┌─────────────────────────────────────────────────────────────┐
│ bharathtrucks.com │
│ (Cloudflare DNS/CDN) │
│ │
│ DNS: A record → VPS IP │
│ SSL: Cloudflare Full (Strict) │
│ Caching: Static assets (CSS/JS/images) │
└──────────────────────────┬──────────────────────────────────┘
┌──────────────────────────▼──────────────────────────────────┐
│ Hostinger VPS │
│ Ubuntu 22.04 LTS │
│ 4 vCPU / 8GB RAM / 200GB SSD │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Coolify │ │
│ │ (Self-hosted PaaS) │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ bharathtrucks (Docker Container) │ │ │
│ │ │ │ │ │
│ │ │ Node.js 20 + Express + EJS │ │ │
│ │ │ Port: 3000 (internal) │ │ │
│ │ │ Auto-restart: enabled │ │ │
│ │ │ Health check: /health │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Traefik (Reverse Proxy) → :443 → Container :3000 │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────▼──────────────────────────────────┐
│ Supabase Cloud │
│ │
│ Project: bharathtrucks │
│ Region: Mumbai (ap-south-1) │
│ Plan: Free → Pro (at 1000 users) │
└──────────────────────────────────────────────────────────────┘
```
---
## 2. Domain Setup (bharathtrucks.com)
### Cloudflare Configuration
1. Add domain to Cloudflare (free plan)
2. Update nameservers at registrar to Cloudflare's
3. DNS Records:
| Type | Name | Value | Proxy |
|------|------|-------|-------|
| A | @ | `<VPS_IP>` | Proxied ☁️ |
| A | www | `<VPS_IP>` | Proxied ☁️ |
| CNAME | api | @ | Proxied ☁️ |
4. SSL: Full (Strict) mode
5. Page Rules:
- `*.bharathtrucks.com/public/*` → Cache Everything, Edge TTL 1 month
- `bharathtrucks.com/` → Cache Level: Standard
---
## 3. Dockerfile
```dockerfile
FROM node:20-alpine
WORKDIR /app
COPY webapp/package*.json ./
RUN npm ci --only=production
COPY webapp/src ./src
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "src/server.js"]
```
---
## 4. Docker Compose (Local Development)
```yaml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
env_file:
- webapp/.env
volumes:
- ./webapp/src:/app/src
restart: unless-stopped
```
---
## 5. Coolify Deployment Steps
### Initial Setup
1. SSH into Hostinger VPS
2. Install Coolify: `curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash`
3. Access Coolify dashboard at `http://<VPS_IP>:8000`
4. Configure domain in Coolify settings
### App Deployment
1. **Source:** Connect GitHub/GitLab repo (or use Git URL)
2. **Build Pack:** Dockerfile
3. **Port:** 3000
4. **Domain:** bharathtrucks.com
5. **Environment Variables:** Add all from `.env.example`
6. **Health Check:** `/health`
7. **Auto Deploy:** On push to `main` branch
### Environment Variables in Coolify
```
NODE_ENV=production
PORT=3000
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_KEY=eyJ...
APP_URL=https://bharathtrucks.com
SESSION_SECRET=<generate-random-64-char>
RATE_LIMIT_BIDS_PER_DAY=5
```
---
## 6. Supabase Setup
### Project Configuration
1. Create project at supabase.com (region: Mumbai)
2. Note: Project URL + anon key + service role key
3. Enable Phone Auth (OTP provider)
4. Configure SMS provider (Twilio or MSG91)
### Auth Settings
- Phone OTP enabled
- OTP expiry: 5 minutes
- Rate limit: 5 OTP requests per hour per number
- Disable email confirmation (phone-first)
### Database Setup
- Run schema SQL from `docs/architecture/DATABASE_SCHEMA.md`
- Enable RLS on all tables
- Create indexes as specified
### Storage Buckets
| Bucket | Purpose | Public |
|--------|---------|--------|
| `avatars` | Profile photos | Yes |
| `documents` | License, RC uploads | No |
| `load-images` | Load/material photos | Yes |
---
## 7. CI/CD Pipeline
### GitHub Actions (Optional)
```yaml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Trigger Coolify Webhook
run: |
curl -X POST "${{ secrets.COOLIFY_WEBHOOK_URL }}"
```
### Simpler: Coolify Auto-Deploy
Coolify watches the repo and auto-deploys on push to `main`. No CI/CD config needed.
---
## 8. Monitoring & Logging
| Tool | Purpose | Cost |
|------|---------|------|
| Coolify Dashboard | Container status, resource usage | Free |
| Cloudflare Analytics | Traffic, cache hit rate | Free |
| Supabase Dashboard | DB metrics, auth logs | Free |
| UptimeRobot | Uptime monitoring, alerts | Free (50 monitors) |
### Health Check Endpoint
```javascript
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: Date.now() });
});
```
### Log Strategy
- Application logs: stdout (Docker captures)
- Access logs: Morgan middleware (combined format)
- Error logs: Structured JSON to stdout
- View in Coolify dashboard → Container logs
---
## 9. Backup Strategy
| What | How | Frequency |
|------|-----|-----------|
| Database | Supabase automatic backups | Daily (Pro plan) |
| Code | Git repository | Every push |
| Environment | Documented in `.env.example` | Manual |
| Uploads | Supabase Storage (managed) | Automatic |
---
## 10. Security Hardening
### VPS Level
- UFW firewall: allow 22, 80, 443 only
- Fail2ban for SSH brute-force protection
- SSH key-only auth (disable password)
- Automatic security updates
### Application Level
- Helmet.js security headers
- CORS restricted to bharathtrucks.com
- Rate limiting (express-rate-limit)
- Input sanitization
- httpOnly cookies for sessions
- CSP headers (Content Security Policy)
### Cloudflare Level
- DDoS protection (automatic)
- Bot management (free tier)
- WAF rules (basic)
- SSL enforcement
---
## 11. Scaling Triggers
| Metric | Threshold | Action |
|--------|-----------|--------|
| CPU | >80% sustained | Upgrade VPS |
| RAM | >85% | Upgrade VPS |
| Response time | >2s average | Add caching/optimize |
| Users | >5000 | Supabase Pro + Redis |
| Traffic | >10K req/min | Multiple containers |
---
## 12. Cost Estimate (Phase 1)
| Service | Plan | Monthly Cost |
|---------|------|-------------|
| Hostinger VPS | KVM 2 (4vCPU/8GB) | ~₹800/month |
| Supabase | Free tier | ₹0 |
| Cloudflare | Free plan | ₹0 |
| Domain | bharathtrucks.com | ~₹800/year |
| UptimeRobot | Free | ₹0 |
| **Total** | | **~₹900/month** |
---
*Infrastructure designed for minimal cost during growth phase, with clear upgrade paths as user base scales.*

View file

@ -0,0 +1,279 @@
# BharathTrucks — Technical Architecture Document
**Version:** 1.0
**Date:** 2026-05-31
---
## 1. Architecture Overview
```
┌─────────────────────────────────────────────────────────┐
│ bharathtrucks.com │
│ (Cloudflare DNS/CDN) │
└─────────────────────┬───────────────────────────────────┘
┌─────────────────────▼───────────────────────────────────┐
│ Hostinger VPS (Coolify) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Docker Container │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ Node.js + Express Server │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────┐ ┌──────────┐ ┌───────────┐ │ │ │
│ │ │ │ Routes │ │ Views │ │Middleware │ │ │ │
│ │ │ │(Express)│ │ (EJS) │ │(Auth/Rate)│ │ │ │
│ │ │ └────┬────┘ └──────────┘ └───────────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ┌────▼────────────────────────────────────┐ │ │ │
│ │ │ │ Controllers │ │ │ │
│ │ │ └────┬────────────────────────────────────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ┌────▼────────────────────────────────────┐ │ │ │
│ │ │ │ Service Layer │ │ │ │
│ │ │ └────┬────────────────────────────────────┘ │ │ │
│ │ └───────┼──────────────────────────────────────┘ │ │
│ └──────────┼─────────────────────────────────────────┘ │
└─────────────┼────────────────────────────────────────────┘
┌─────────────▼────────────────────────────────────────────┐
│ Supabase (Cloud) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │PostgreSQL│ │ Auth │ │ Storage │ │Realtime │ │
│ │(Database)│ │ (OTP) │ │ (Files) │ │ (WS) │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
└───────────────────────────────────────────────────────────┘
```
---
## 2. Technology Stack
| Layer | Technology | Justification |
|-------|-----------|---------------|
| **Runtime** | Node.js 20 LTS | Fast, async I/O, large ecosystem |
| **Framework** | Express.js 4.x | Minimal, flexible, well-documented |
| **Views** | EJS | Server-rendered (SEO, fast on 3G, no JS required) |
| **Database** | Supabase PostgreSQL | Managed, free tier, built-in auth |
| **Auth** | Supabase Auth (Phone OTP) | India-native phone-first auth |
| **File Storage** | Supabase Storage | Documents, profile photos |
| **Realtime** | Supabase Realtime | Live bid updates, notifications |
| **CSS** | Custom CSS | Full control for govt-app aesthetic |
| **PWA** | Service Worker + Manifest | Installable, offline caching |
| **Container** | Docker | Consistent deployment |
| **Orchestration** | Coolify | Self-hosted PaaS on Hostinger VPS |
| **CDN/DNS** | Cloudflare | Free SSL, caching, DDoS protection |
| **Domain** | bharathtrucks.com | Brand identity |
---
## 3. Application Structure
```
bharathtrucks/
├── webapp/
│ ├── src/
│ │ ├── app.js # Express app setup
│ │ ├── server.js # Server entry point
│ │ ├── routes/
│ │ │ ├── index.js # Route aggregator
│ │ │ ├── auth.routes.js # Login, register, OTP
│ │ │ ├── load.routes.js # Load CRUD, search
│ │ │ ├── bid.routes.js # Bidding operations
│ │ │ ├── driver.routes.js # Driver dashboard
│ │ │ ├── shipper.routes.js # Shipper dashboard
│ │ │ ├── broker.routes.js # Broker dashboard
│ │ │ ├── admin.routes.js # Admin panel
│ │ │ └── web.routes.js # Marketing pages
│ │ ├── controllers/
│ │ │ ├── auth.controller.js
│ │ │ ├── load.controller.js
│ │ │ ├── bid.controller.js
│ │ │ ├── driver.controller.js
│ │ │ ├── shipper.controller.js
│ │ │ ├── broker.controller.js
│ │ │ └── admin.controller.js
│ │ ├── services/
│ │ │ ├── supabase.js # Supabase client init
│ │ │ ├── auth.service.js # Auth logic
│ │ │ ├── load.service.js # Load business logic
│ │ │ ├── bid.service.js # Bid business logic
│ │ │ └── notification.service.js
│ │ ├── middleware/
│ │ │ ├── auth.middleware.js # Session validation
│ │ │ ├── role.middleware.js # Role-based access
│ │ │ ├── rate.middleware.js # Rate limiting (bids/day)
│ │ │ └── error.middleware.js
│ │ ├── views/
│ │ │ ├── layouts/
│ │ │ │ └── main.ejs # Base layout
│ │ │ ├── pages/
│ │ │ │ ├── landing.ejs
│ │ │ │ ├── login.ejs
│ │ │ │ ├── register.ejs
│ │ │ │ ├── loadboard.ejs
│ │ │ │ ├── driver-dashboard.ejs
│ │ │ │ ├── shipper-dashboard.ejs
│ │ │ │ ├── broker-dashboard.ejs
│ │ │ │ └── admin-dashboard.ejs
│ │ │ └── partials/
│ │ │ ├── header.ejs
│ │ │ ├── footer.ejs
│ │ │ ├── nav.ejs
│ │ │ └── load-card.ejs
│ │ ├── public/
│ │ │ ├── css/
│ │ │ │ ├── main.css # Global styles
│ │ │ │ ├── govt-theme.css # Govt-app aesthetic
│ │ │ │ └── components.css # Reusable components
│ │ │ ├── js/
│ │ │ │ ├── app.js # Client-side logic
│ │ │ │ └── sw.js # Service Worker
│ │ │ ├── images/
│ │ │ └── manifest.json # PWA manifest
│ │ └── config/
│ │ ├── env.js # Environment config
│ │ └── constants.js # App constants
│ ├── package.json
│ ├── Dockerfile
│ ├── .env.example
│ └── .dockerignore
├── docker/
│ └── docker-compose.yml
├── scripts/
│ └── seed.js # DB seed data
├── docs/
└── README.md
```
---
## 4. Authentication Flow
```
User (Phone) → Enter Number → Supabase OTP → Verify Code → Session Created
┌───────────────┼───────────────┐
│ │ │
New User? Existing User Admin?
│ │ │
Role Selection Dashboard Admin Panel
Profile Setup
Dashboard
```
- **Session:** Supabase JWT stored in httpOnly cookie
- **Role check:** Middleware reads user role from `profiles` table
- **Rate limiting:** Free users get 5 bids/day (tracked in DB)
---
## 5. Data Flow — Load Lifecycle
```
Shipper Posts Load → Load Board (visible to all)
Drivers Browse
Driver Bids (price + ETA)
Shipper Reviews Bids
Accepts Best Bid
┌───────────┼───────────┐
│ │
Driver Notified Load Status: "Booked"
Trip Starts → In Transit → Delivered
Payment (UPI link) → Confirmed → Complete
```
---
## 6. Security Architecture
| Layer | Measure |
|-------|---------|
| Transport | HTTPS via Cloudflare SSL |
| Auth | Supabase JWT + httpOnly cookies |
| Sessions | Server-side validation on every request |
| Input | Sanitization + validation (express-validator) |
| SQL | Parameterized queries via Supabase client |
| Rate Limiting | express-rate-limit (API) + DB-level (bids) |
| CORS | Restricted to bharathtrucks.com |
| Headers | Helmet.js security headers |
| File Upload | Type + size validation, Supabase Storage |
| Admin | Separate auth flow, IP whitelist optional |
---
## 7. Performance Strategy
| Technique | Implementation |
|-----------|---------------|
| Server-side rendering | EJS — no client-side framework overhead |
| Minimal JS | Progressive enhancement, no SPA |
| Image optimization | WebP, lazy loading, CDN-cached |
| Database | Indexed queries, connection pooling |
| Caching | Cloudflare page rules for static assets |
| PWA | Service Worker caches shell + recent loads |
| Compression | gzip via Express compression middleware |
| Bundle size | No framework = ~5KB JS total |
**Target:** < 3 second full page load on 3G connection
---
## 8. Scalability Plan
| Users | Infrastructure |
|-------|---------------|
| 01000 | Single Hostinger VPS (4GB RAM), Supabase free tier |
| 10005000 | Upgrade VPS (8GB), Supabase Pro |
| 500020000 | Add Redis caching, CDN optimization |
| 20000+ | Horizontal scaling (multiple containers), dedicated DB |
---
## 9. Third-Party Integrations
| Service | Purpose | Phase |
|---------|---------|-------|
| Supabase | DB + Auth + Storage + Realtime | Phase 1 |
| Cloudflare | CDN + DNS + SSL | Phase 1 |
| WhatsApp API (wa.me links) | Share loads | Phase 1 |
| SMS Gateway (MSG91/Twilio) | Critical notifications | Phase 2 |
| Razorpay/UPI | Payment processing | Phase 2 |
| Google Maps API | Route visualization | Phase 2 |
---
## 10. Environment Configuration
```env
# Server
NODE_ENV=production
PORT=3000
# Supabase
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_KEY=eyJ...
# App
APP_URL=https://bharathtrucks.com
SESSION_SECRET=xxx
RATE_LIMIT_BIDS_PER_DAY=5
# Optional (Phase 2)
SMS_API_KEY=xxx
RAZORPAY_KEY=xxx
```
---
*This architecture prioritizes simplicity, speed, and low-cost operation while supporting growth to 20K+ users.*

251
docs/bmad/MONETIZATION.md Normal file
View file

@ -0,0 +1,251 @@
# BharathTrucks — SaaS & Monetization Strategy
**Version:** 1.0
**Date:** 2026-05-31
---
## 1. Revenue Model Overview
### Phase 1: Growth (01000 users) — FREE
**Goal:** Acquire users, prove product-market fit, build trust.
Everything is free. No paywalls. No ads. Build the network effect.
### Phase 2: Monetization (1000+ users) — FREEMIUM
**Goal:** Convert power users to paid plans while keeping basic access free forever.
### Phase 3: Scale (10,000+ users) — PLATFORM FEES
**Goal:** Transaction-based revenue + subscriptions + value-added services.
---
## 2. Pricing Tiers
### Driver Plans
| Feature | Free (Muft) | Pro (₹299/mo) | Business (₹799/mo) |
|---------|-------------|---------------|---------------------|
| View loads | ✅ Unlimited | ✅ Unlimited | ✅ Unlimited |
| Place bids | 5/day | Unlimited | Unlimited |
| Profile visibility | Standard | Priority listing | Top listing + badge |
| Verified badge | ❌ | ✅ | ✅ |
| Bid analytics | ❌ | ✅ | ✅ |
| Trip history export | ❌ | ✅ | ✅ |
| Earnings reports | Basic | Detailed | Detailed + GST |
| Dedicated support | ❌ | ❌ | ✅ WhatsApp |
| Return load alerts | ❌ | ✅ (SMS) | ✅ (SMS + Call) |
### Shipper Plans
| Feature | Free (Muft) | Pro (₹499/mo) | Enterprise (₹1499/mo) |
|---------|-------------|---------------|------------------------|
| Post loads | 5/month | Unlimited | Unlimited |
| View bids | ✅ | ✅ | ✅ |
| Driver verification view | Basic | Full history | Full + documents |
| GPS tracking | ❌ | ✅ | ✅ |
| Invoice generation | ❌ | ✅ | ✅ + GST auto-file |
| Saved drivers | 5 | 50 | Unlimited |
| Priority support | ❌ | Email | WhatsApp + Phone |
| Bulk load posting | ❌ | ❌ | ✅ (CSV upload) |
| API access | ❌ | ❌ | ✅ |
### Broker Plans
| Feature | Free (Muft) | Pro (₹699/mo) | Agency (₹1999/mo) |
|---------|-------------|---------------|---------------------|
| Post loads | 10/month | Unlimited | Unlimited |
| Network size | 20 drivers | 200 drivers | Unlimited |
| Commission tracking | Basic | Full ledger | Full + reports |
| Client management | 5 clients | 50 clients | Unlimited |
| WhatsApp templates | ❌ | ✅ | ✅ + auto-send |
| Sub-broker accounts | ❌ | ❌ | 5 included |
| Branded profile page | ❌ | ✅ | ✅ + custom URL |
| Monthly reports | ❌ | ✅ | ✅ + PDF export |
---
## 3. Revenue Streams
### Stream 1: Subscriptions (Primary)
- Monthly recurring revenue from Pro/Business/Enterprise plans
- Annual plans at 20% discount (₹2,870 instead of ₹3,588 for Driver Pro yearly)
### Stream 2: Transaction Fees (Phase 3)
- 1-2% platform fee on payments processed through BharathTrucks
- Only when in-app payment is used (UPI direct remains free)
### Stream 3: Promoted Listings (Phase 2)
- Shippers pay ₹99-299 to boost a load to top of board for 24 hours
- Drivers pay ₹49 for "Featured Driver" badge for 7 days
### Stream 4: Verification Services (Phase 2)
- Aadhaar/PAN verification: ₹99 one-time
- Background check: ₹299 one-time
- Truck RC verification: ₹149 one-time
### Stream 5: Value-Added Services (Phase 3)
- Insurance partnerships (referral commission)
- Fuel card partnerships
- FASTag recharge (commission)
- Tyre/maintenance marketplace (listing fees)
---
## 4. Free-to-Paid Conversion Strategy
### Trigger Points (When users hit limits)
1. **Driver:** 6th bid attempt in a day → "Upgrade to Pro for unlimited bids"
2. **Shipper:** 6th load post in a month → "Upgrade for unlimited posting"
3. **Broker:** 21st driver added → "Expand your network with Pro"
### Conversion Tactics
| Tactic | Implementation |
|--------|---------------|
| Soft paywall | Show feature, explain it's premium, offer trial |
| Social proof | "500+ drivers upgraded this month" |
| Loss aversion | "You missed 3 loads matching your route today" |
| Free trial | 7-day Pro trial after 30 days of free usage |
| Referral bonus | Refer 3 users → get 1 month Pro free |
| Seasonal offers | Festival discounts (Diwali, Navratri) |
### Conversion Funnel
```
Free User → Hits Limit → Sees Upgrade Prompt → Trial/Pay
Dismisses → Reminder in 3 days
Still free → Monthly "what you missed" email
```
---
## 5. Payment Integration
### Supported Methods
| Method | Provider | Phase |
|--------|----------|-------|
| UPI | Razorpay | Phase 2 |
| Debit/Credit Card | Razorpay | Phase 2 |
| Net Banking | Razorpay | Phase 2 |
| Wallet (Paytm, PhonePe) | Razorpay | Phase 2 |
### Subscription Management
- Razorpay Subscriptions API for recurring billing
- Grace period: 3 days for failed payments
- Downgrade to free on cancellation (keep data)
- No lock-in, cancel anytime
---
## 6. Financial Projections
### Conservative Estimate (Year 1)
| Month | Users | Paid Users (5%) | MRR |
|-------|-------|-----------------|-----|
| 1-6 | 0-1000 | 0 | ₹0 |
| 7 | 1200 | 60 | ₹25,000 |
| 8 | 1500 | 75 | ₹32,000 |
| 9 | 2000 | 100 | ₹45,000 |
| 10 | 2500 | 125 | ₹55,000 |
| 11 | 3000 | 150 | ₹65,000 |
| 12 | 4000 | 200 | ₹85,000 |
**Year 1 Total Revenue:** ~₹3,00,000 (after free phase)
**Year 1 Costs:** ~₹1,50,000 (infra + SMS + domain)
**Year 1 Net:** ~₹1,50,000 profit
### Optimistic (Year 2)
- 15,000 users, 8% conversion = 1,200 paid users
- Average revenue per paid user: ₹500/month
- MRR: ₹6,00,000/month
- ARR: ₹72,00,000
---
## 7. Competitive Pricing Analysis
| Platform | Driver Cost | Shipper Cost | Our Advantage |
|----------|-------------|-------------|---------------|
| BlackBuck | Commission-based | Free | We're cheaper for drivers |
| Porter | Not for long-haul | Per-booking fee | We serve long-haul |
| Rivigo | Enterprise only | Enterprise only | We serve individuals |
| BharathTrucks | ₹299/mo (optional) | ₹499/mo (optional) | Free tier always available |
---
## 8. Feature Gating Implementation
### Technical Approach
```javascript
// middleware/premium.middleware.js
const checkFeature = (feature) => {
return async (req, res, next) => {
const user = req.user;
const limits = PLAN_LIMITS[user.plan || 'free'];
if (feature === 'bid' && !user.is_premium) {
const todayBids = await getBidsToday(user.id);
if (todayBids >= limits.bids_per_day) {
return res.render('upgrade', { feature: 'unlimited_bids' });
}
}
next();
};
};
```
### Plan Limits Config
```javascript
const PLAN_LIMITS = {
free: {
bids_per_day: 5,
loads_per_month: 5,
network_size: 20,
saved_drivers: 5
},
pro: {
bids_per_day: Infinity,
loads_per_month: Infinity,
network_size: 200,
saved_drivers: 50
},
business: {
bids_per_day: Infinity,
loads_per_month: Infinity,
network_size: Infinity,
saved_drivers: Infinity
}
};
```
---
## 9. Retention Strategy
| Strategy | Implementation |
|----------|---------------|
| Daily value | Load board updates, new loads notification |
| Weekly digest | "X loads matched your route this week" |
| Streak rewards | "7-day active streak — earn badge" |
| Community | Driver groups by route/region |
| Referral program | ₹100 credit per successful referral |
| Loyalty discount | 6-month users get 10% off annual plan |
---
## 10. Key Metrics to Track
| Metric | Target | Tool |
|--------|--------|------|
| Free → Paid conversion | 5-8% | Supabase + custom |
| Monthly churn rate | <5% | Subscription tracking |
| Average Revenue Per User (ARPU) | ₹450 | Razorpay dashboard |
| Customer Lifetime Value (LTV) | ₹5,400 (12 months) | Calculated |
| Customer Acquisition Cost (CAC) | <₹200 | Marketing spend / new users |
| LTV:CAC ratio | >10:1 | Calculated |
---
*Monetization starts only after proving value. Free users are never punished — they're future paying customers.*

239
docs/bmad/PRD.md Normal file
View file

@ -0,0 +1,239 @@
# BharathTrucks — Product Requirements Document (PRD)
**Version:** 1.0
**Date:** 2026-05-31
**Author:** BharathTrucks Team
**Status:** Draft
---
## 1. Product Vision
**BharathTrucks** is India's national freight marketplace — a government-styled, trust-first platform that connects truck drivers, shippers, and brokers in a unified digital ecosystem. The platform is designed to look and feel like an official government service, building instant trust with India's trucking community.
### Mission Statement
To digitize India's fragmented freight industry by providing a free, accessible, and trustworthy platform that empowers every stakeholder — from the single-truck driver to the large shipper.
---
## 2. Problem Statement
India's freight industry is:
- **Fragmented:** 75%+ trucks return empty after delivery (deadheading)
- **Unorganized:** Most deals happen via phone calls, chai-shop notice boards, and word-of-mouth
- **Trust-deficit:** Drivers get cheated on payments; shippers face unreliable delivery
- **Broker-dependent:** Brokers charge 5-10% commission with no transparency
- **Digitally excluded:** Most drivers are semi-literate, use basic smartphones
### Current Pain Points by Role
| Role | Pain Points |
|------|-------------|
| **Truck Drivers** | Empty return trips, delayed payments, no bargaining power, exploitative brokers |
| **Shippers** | Unreliable drivers, no tracking, price opacity, manual coordination |
| **Brokers** | Manual record-keeping, commission disputes, no CRM, losing business to apps |
---
## 3. Target Users (Phase 1)
### Primary Users
1. **Truck Drivers** — Owner-operators and employed drivers seeking loads
2. **Shippers** — Businesses and individuals needing goods transported
3. **Brokers (Transport Agents)** — Intermediaries who connect drivers and shippers
### Future Users (Phase 2+)
4. **Truck/Fleet Owners** — Multiple vehicle operators
5. **Packers & Movers** — Household and commercial relocation services
6. **House Shifting Services** — End-to-end relocation coordination
---
## 4. Product Strategy
### 4.1 Government-App Aesthetic (Key Differentiator)
The UI will deliberately mimic Indian government portals/apps to:
- Build **instant trust** with semi-literate users who trust "sarkari" (government) apps
- Create **perceived authority** — users treat it as an official service
- Drive **organic adoption** through word-of-mouth ("govt ne naya app nikala hai")
- Reduce **skepticism** that plagues private startup apps in this segment
Design elements:
- Ashoka Chakra blue/navy color palette
- Formal Hindi/English bilingual headers
- Official-looking seals, emblems, and certificate-style layouts
- "Bharat Sarkar" inspired typography (Noto Sans Devanagari)
- Tricolor accents (saffron, white, green)
### 4.2 SaaS & Monetization Model
**Phase 1: Free (01000 users)**
- All features free for all users
- Goal: Build user base, gather feedback, prove product-market fit
**Phase 2: Freemium (1000+ users)**
- Basic features remain free forever
- Premium features unlock via subscription
| Feature | Free | Premium |
|---------|------|---------|
| Post/View Loads | ✅ | ✅ |
| Bid on Loads | ✅ (5/day) | ✅ (Unlimited) |
| Basic Profile | ✅ | ✅ |
| Verified Badge | ❌ | ✅ |
| Priority Listing | ❌ | ✅ |
| Advanced Analytics | ❌ | ✅ |
| CRM Tools (Brokers) | Basic | Full |
| GPS Tracking | ❌ | ✅ |
| Invoice Generation | ❌ | ✅ |
| Dedicated Support | ❌ | ✅ |
### 4.3 Platform Type
- **Web-first** (responsive, mobile-optimized)
- **PWA** (installable, offline-capable)
- Future: Native Android app
---
## 5. Core Features (MVP — Phase 1)
### 5.1 Marketing Website
- Landing page with govt-app styling
- Feature showcase
- Trust signals (user count, loads moved, cities covered)
- Download/Install CTA
### 5.2 Authentication & Onboarding
- Role-based registration (Driver / Shipper / Broker)
- Phone number + OTP login (primary)
- Email as secondary
- KYC-lite: Aadhaar/PAN verification (future)
- Profile setup wizard per role
### 5.3 Load Board (Marketplace)
- Shippers post loads (origin, destination, weight, truck type, budget)
- Drivers browse and bid on loads
- Brokers can post on behalf of shippers
- Filters: route, truck type, weight, date, budget
- Real-time load count and activity
### 5.4 Bidding System
- Drivers submit bids with price and ETA
- Shippers review bids, accept/reject
- Counter-offer capability
- Bid history and status tracking
### 5.5 Role-Specific Dashboards
**Driver Dashboard:**
- Available loads (personalized)
- Active trips
- Earnings summary
- Trip history
- Profile & documents
**Shipper Dashboard:**
- Post new load
- Active shipments
- Bid management
- Payment history
- Saved drivers/brokers
**Broker Dashboard:**
- Load management (own + shipper loads)
- Driver network
- Commission tracking
- Quick-post tools
- Client management
### 5.6 Communication
- In-app messaging (driver ↔ shipper)
- WhatsApp share templates for loads
- Push notifications (PWA)
### 5.7 Basic Payments
- UPI payment links
- Payment status tracking
- Simple ledger per user
---
## 6. Non-Functional Requirements
| Requirement | Target |
|-------------|--------|
| Page Load Time | < 3 seconds on 3G |
| Mobile Responsiveness | 100% (320px1440px) |
| Offline Support | Load board caching, form drafts |
| Language Support | English, Hindi (Phase 1); Tamil, Telugu, Kannada (Phase 2) |
| Accessibility | WCAG 2.1 AA compliant |
| Uptime | 99.5% |
| Concurrent Users | 500 (Phase 1) |
| Data Security | Encrypted at rest + transit, GDPR-lite compliance |
---
## 7. Technical Constraints
| Constraint | Decision |
|-----------|----------|
| Backend | Node.js + Express |
| Views | EJS (server-rendered for SEO + low-bandwidth) |
| Database | Supabase (PostgreSQL + Auth + Storage) |
| Hosting | Hostinger VPS via Coolify |
| Domain | bharathtrucks.com |
| Containerization | Docker |
| CSS | Custom (no framework — govt-app aesthetic needs full control) |
| PWA | Service Worker + manifest |
---
## 8. Success Metrics
| Metric | Phase 1 Target |
|--------|---------------|
| Registered Users | 1000 |
| Daily Active Users | 100 |
| Loads Posted/Week | 50 |
| Successful Matches | 20/week |
| User Retention (30-day) | 40% |
| App Install Rate (PWA) | 30% of visitors |
---
## 9. Risks & Mitigations
| Risk | Impact | Mitigation |
|------|--------|-----------|
| Users discover it's not actually govt | High | Never explicitly claim govt; use "Bharat" branding which is legitimate |
| Low initial supply (loads/drivers) | High | Seed with broker partnerships; manual load posting |
| Trust issues with payments | Medium | UPI direct (no platform holding money in Phase 1) |
| Competition (Porter, BlackBuck) | Medium | Focus on tier-2/3 cities; free model; broker-friendly |
| Technical scalability | Low | Supabase handles scaling; Coolify makes deployment easy |
---
## 10. Out of Scope (Phase 1)
- Native mobile apps
- GPS live tracking
- In-app payment processing (escrow)
- AI-based load matching
- Fleet management tools
- Insurance integration
- Fuel card partnerships
---
## 11. Approval & Sign-off
| Role | Name | Date | Status |
|------|------|------|--------|
| Product Owner | — | — | Pending |
| Tech Lead | — | — | Pending |
| Design Lead | — | — | Pending |
---
*This is a living document. Updates will be versioned and tracked.*

245
docs/bmad/SPRINT_PLAN.md Normal file
View file

@ -0,0 +1,245 @@
# BharathTrucks — Sprint & Milestone Plan
**Version:** 1.0
**Date:** 2026-05-31
**Sprint Duration:** 1 week each
---
## Milestone Overview
```
M1 (Sprint 1-2) M2 (Sprint 3-4) M3 (Sprint 5-6) M4 (Sprint 7-8)
Foundation & Load Board & Dashboards & Polish &
Auth Bidding Communication Launch
────────────────────────────────────────────────────────────────────────────────►
🚀 LAUNCH
```
---
## Sprint 1: Foundation (Week 1)
### Goal: Project setup, infrastructure, landing page
| Task | Story | Hours |
|------|-------|-------|
| Initialize Node.js + Express project | — | 2 |
| Setup EJS templating + layouts | — | 2 |
| Create govt-theme CSS (design system) | — | 4 |
| Setup Supabase project + run schema SQL | — | 2 |
| Create Dockerfile + docker-compose | — | 1 |
| Deploy to Coolify (hello world) | — | 2 |
| Configure Cloudflare + domain | — | 1 |
| Build landing page | WEB-1, WEB-2 | 4 |
| Health check endpoint | — | 0.5 |
| PWA manifest + service worker (basic) | — | 2 |
**Deliverable:** bharathtrucks.com live with landing page
---
## Sprint 2: Authentication (Week 2)
### Goal: Phone OTP login, role-based registration, onboarding
| Task | Story | Hours |
|------|-------|-------|
| Supabase Auth integration (phone OTP) | AUTH-1 | 3 |
| Login page (phone + OTP flow) | AUTH-1, AUTH-6 | 3 |
| Registration page (role selection) | AUTH-2 | 3 |
| OTP verification page | AUTH-1 | 2 |
| Auth middleware (session validation) | — | 2 |
| Role middleware | — | 1 |
| Driver onboarding form | AUTH-3 | 2 |
| Shipper onboarding form | AUTH-4 | 2 |
| Broker onboarding form | AUTH-5 | 2 |
| Post-login redirect by role | AUTH-8 | 1 |
**Deliverable:** Users can register, login, complete profile
---
## Sprint 3: Load Board (Week 3)
### Goal: Shippers post loads, drivers browse and filter
| Task | Story | Hours |
|------|-------|-------|
| Load posting form (shipper) | LOAD-1 | 3 |
| Load board page (list view) | LOAD-2 | 4 |
| Load detail page | LOAD-3 | 2 |
| Filters (route, truck type, date) | LOAD-2 | 3 |
| Broker posts on behalf of shipper | LOAD-4 | 2 |
| Edit/cancel load | LOAD-5 | 2 |
| Public load board (read-only, no auth) | WEB-4 | 2 |
| Load card component (partial) | — | 1 |
| Load status badges | — | 1 |
**Deliverable:** Functional load board with posting and browsing
---
## Sprint 4: Bidding System (Week 4)
### Goal: Drivers bid, shippers accept/reject, rate limiting
| Task | Story | Hours |
|------|-------|-------|
| Bid submission form | BID-1 | 2 |
| Bid list on load detail page | BID-2 | 3 |
| Accept bid flow | BID-3 | 2 |
| Reject bid flow | BID-4 | 1 |
| Bid status notifications | BID-5 | 3 |
| Bid rate limiting (5/day free) | BID-8 | 2 |
| Bid history page (driver) | BID-7 | 2 |
| Withdraw bid | BID-6 | 1 |
| Trip creation on bid acceptance | — | 2 |
| Load status update on booking | — | 1 |
**Deliverable:** Complete bid lifecycle working
---
## Sprint 5: Dashboards (Week 5)
### Goal: Role-specific dashboards with key features
| Task | Story | Hours |
|------|-------|-------|
| Driver dashboard (active trips, loads) | DRV-1, DRV-2 | 4 |
| Trip status updates (driver) | DRV-3 | 2 |
| Shipper dashboard (my loads, bids) | SHP-1, SHP-2 | 4 |
| Broker dashboard (network, loads) | BRK-1, BRK-2 | 4 |
| Commission tracking (broker) | BRK-3 | 3 |
| Earnings summary (driver) | DRV-4 | 2 |
| Bottom navigation (mobile) | — | 1 |
**Deliverable:** All 3 role dashboards functional
---
## Sprint 6: Communication & Notifications (Week 6)
### Goal: In-app messaging, push notifications, WhatsApp share
| Task | Story | Hours |
|------|-------|-------|
| Messaging system (DB + UI) | COM-1 | 4 |
| Conversation view | COM-1 | 3 |
| Push notifications (PWA) | COM-2 | 3 |
| Notification bell + dropdown | COM-2 | 2 |
| WhatsApp share templates | COM-3 | 2 |
| Notification preferences | — | 1 |
| Unread message count | — | 1 |
| Real-time updates (Supabase Realtime) | — | 3 |
**Deliverable:** Users can communicate about loads
---
## Sprint 7: Admin & Payments (Week 7)
### Goal: Admin panel, basic payment tracking, ratings
| Task | Story | Hours |
|------|-------|-------|
| Admin dashboard (metrics) | ADM-1, ADM-2 | 4 |
| User management (list, suspend) | ADM-3 | 3 |
| UPI payment link generation | PAY-1 | 2 |
| Payment confirmation flow | PAY-2 | 2 |
| Transaction ledger | PAY-3 | 2 |
| Rating system (post-delivery) | SHP-4 | 3 |
| Feature flag system | ADM-4 | 2 |
| Admin login (separate) | — | 1 |
**Deliverable:** Admin can manage platform, basic payments work
---
## Sprint 8: Polish & Launch (Week 8)
### Goal: Bug fixes, performance, SEO, launch prep
| Task | Story | Hours |
|------|-------|-------|
| Performance optimization (images, caching) | — | 3 |
| SEO meta tags + sitemap | — | 2 |
| Error pages (404, 500) | — | 1 |
| Loading states + empty states | — | 2 |
| Hindi language strings | AUTH-7 | 3 |
| Mobile responsiveness audit | — | 3 |
| Security audit (headers, inputs) | — | 2 |
| End-to-end testing (manual) | — | 4 |
| Seed data for demo | — | 1 |
| Launch checklist completion | — | 2 |
**Deliverable:** 🚀 Production launch of bharathtrucks.com
---
## Post-Launch Sprints (Phase 2)
### Sprint 9-10: Growth Features
- Driver document management
- Shipper saved drivers
- Broker WhatsApp templates
- Search improvements
- Load recommendations
### Sprint 11-12: Monetization
- Razorpay subscription integration
- Premium feature gating
- Upgrade prompts
- Promoted listings
- Verification services
### Sprint 13-16: Scale
- GPS tracking integration
- SMS notifications
- Advanced analytics
- Fleet owner features
- Android PWA optimization
---
## Launch Checklist
| # | Item | Status |
|---|------|--------|
| 1 | Domain pointing to VPS | ⬜ |
| 2 | SSL working (Cloudflare) | ⬜ |
| 3 | Supabase schema deployed | ⬜ |
| 4 | Auth OTP working | ⬜ |
| 5 | All 3 roles can register + login | ⬜ |
| 6 | Load board functional | ⬜ |
| 7 | Bidding works end-to-end | ⬜ |
| 8 | Dashboards render correctly | ⬜ |
| 9 | Messaging works | ⬜ |
| 10 | Mobile responsive | ⬜ |
| 11 | PWA installable | ⬜ |
| 12 | Health check passing | ⬜ |
| 13 | Error handling in place | ⬜ |
| 14 | UptimeRobot configured | ⬜ |
| 15 | Backup strategy confirmed | ⬜ |
---
## Timeline Summary
| Week | Sprint | Milestone |
|------|--------|-----------|
| Week 1 | Sprint 1 | Infrastructure + Landing |
| Week 2 | Sprint 2 | Auth + Onboarding |
| Week 3 | Sprint 3 | Load Board |
| Week 4 | Sprint 4 | Bidding |
| Week 5 | Sprint 5 | Dashboards |
| Week 6 | Sprint 6 | Communication |
| Week 7 | Sprint 7 | Admin + Payments |
| Week 8 | Sprint 8 | Polish + Launch 🚀 |
**Total time to MVP: 8 weeks**
---
*Each sprint has a clear deliverable. No sprint depends on external factors. Ship weekly, iterate based on feedback.*

208
docs/bmad/USER_PERSONAS.md Normal file
View file

@ -0,0 +1,208 @@
# BharathTrucks — User Personas
**Version:** 1.0
**Date:** 2026-05-31
---
## Persona 1: Raju — The Truck Driver
### Demographics
| Field | Detail |
|-------|--------|
| Name | Raju Yadav |
| Age | 34 |
| Location | Nagpur, Maharashtra |
| Education | 10th pass |
| Language | Hindi, basic English |
| Phone | Android (₹8,000 Redmi), 4G |
| Income | ₹25,00040,000/month |
| Tech Comfort | WhatsApp, YouTube, basic apps |
### Context
- Owns one truck (Tata 407), still paying EMI
- Drives 1520 days/month, rest spent finding loads
- Currently depends on 2-3 brokers who take 8-10% commission
- Loses 3-5 days/month waiting for return loads (deadheading)
### Goals
- Find return loads quickly to avoid empty trips
- Get paid on time, full amount
- Reduce broker dependency
- Track earnings properly
### Frustrations
- Brokers delay payments by 15-30 days
- No bargaining power — "take it or leave it"
- Scam apps that charge registration but give no loads
- Complex apps with too many steps
### Behavior Patterns
- Checks phone at dhabas (truck stops) during breaks
- Trusts "sarkari" (government) things more than private companies
- Shares useful apps with other drivers at parking lots
- Prefers voice/Hindi over typing English
### What Success Looks Like
- Gets 2-3 return load options within hours of delivery
- Earns ₹5,000+ more per month by cutting broker fees
- Simple app he can explain to other drivers
---
## Persona 2: Priya — The Shipper
### Demographics
| Field | Detail |
|-------|--------|
| Name | Priya Mehta |
| Age | 42 |
| Location | Ahmedabad, Gujarat |
| Education | B.Com graduate |
| Language | Gujarati, Hindi, English |
| Phone | iPhone 13 |
| Income | Business owner (₹15L+ annual revenue) |
| Tech Comfort | High — uses Tally, WhatsApp Business, email |
### Context
- Runs a textile trading business, ships 8-12 loads/month
- Currently uses 2 brokers + direct driver contacts
- Spends 2-3 hours per shipment coordinating logistics
- Has been cheated twice — goods damaged, driver disappeared
### Goals
- Find reliable, verified drivers quickly
- Get competitive rates through bidding
- Track shipments without calling driver repeatedly
- Maintain records for GST compliance
### Frustrations
- No way to verify driver/truck before booking
- Rates vary wildly — no transparency
- Brokers add hidden charges
- No single platform to manage all shipments
### Behavior Patterns
- Posts requirements in morning, expects bids by afternoon
- Values reviews/ratings heavily before trusting
- Wants WhatsApp notifications, not just app notifications
- Will pay premium for reliability and tracking
### What Success Looks Like
- Posts a load, gets 5+ bids within 2 hours
- Books verified driver at market rate
- Tracks delivery without making phone calls
- Clean payment records for tax filing
---
## Persona 3: Suresh — The Broker (Transport Agent)
### Demographics
| Field | Detail |
|-------|--------|
| Name | Suresh Reddy |
| Age | 51 |
| Location | Hyderabad, Telangana |
| Education | 12th pass |
| Language | Telugu, Hindi, English |
| Phone | Samsung Galaxy A-series |
| Income | ₹50,0001,00,000/month (commission-based) |
| Tech Comfort | Medium — WhatsApp, basic Excel |
### Context
- 20+ years in transport brokerage
- Network of 200+ drivers and 50+ regular shippers
- Operates from a small office near truck terminal
- Manages everything via phone calls, WhatsApp groups, and a paper register
- Earns 5-8% commission per load
### Goals
- Manage his network digitally without losing personal touch
- Track commissions accurately (currently loses ₹10-15K/month to poor records)
- Expand network beyond his physical location
- Stay relevant as shippers move to apps
### Frustrations
- Apps like BlackBuck/Porter are cutting brokers out
- Younger brokers using tech are stealing his clients
- Can't remember which driver is where at any given time
- Commission disputes with no proof
### Behavior Patterns
- Makes 50+ calls/day coordinating loads
- Maintains relationships through personal touch
- Resistant to change but fears being left behind
- Will adopt tech if it makes him earn MORE, not less
### What Success Looks Like
- Digital register that replaces his paper notebook
- Posts loads for his shippers, earns commission transparently
- Drivers in his network see his loads first
- Earns 20% more by expanding reach digitally
---
## Persona 4 (Future): Vikram — The Fleet Owner
### Demographics
| Field | Detail |
|-------|--------|
| Name | Vikram Singh |
| Age | 45 |
| Location | Jaipur, Rajasthan |
| Trucks | 12 (mix of Tata, Ashok Leyland) |
| Income | ₹3-5L/month |
### Needs (Phase 2)
- Fleet dashboard — all trucks on one screen
- Driver assignment and performance tracking
- Maintenance scheduling
- Bulk load bidding
- Financial overview across fleet
---
## Persona 5 (Future): Anita — Packers & Movers Operator
### Demographics
| Field | Detail |
|-------|--------|
| Name | Anita Sharma |
| Age | 38 |
| Location | Bangalore, Karnataka |
| Business | "SafeShift Packers" — 5 employees |
### Needs (Phase 2+)
- List services on marketplace
- Get house-shifting leads
- Manage bookings and crew scheduling
- Customer reviews and portfolio
---
## User Persona Comparison Matrix
| Attribute | Raju (Driver) | Priya (Shipper) | Suresh (Broker) |
|-----------|---------------|-----------------|-----------------|
| Primary Goal | Find loads | Ship goods | Earn commission |
| Tech Level | Low | High | Medium |
| Trust Factor | Govt = trusted | Reviews = trusted | Relationships = trusted |
| Payment | Wants fast payment | Wants receipts | Wants commission tracking |
| Key Feature | Load board + bid | Post load + track | CRM + network |
| Engagement | Daily | 8-12x/month | Daily |
| Language | Hindi | English/Hindi | Regional + Hindi |
| Device | Budget Android | iPhone/mid-range | Mid-range Android |
| Onboarding Need | Minimal steps, voice | Professional, quick | Show ROI immediately |
---
## Design Implications
1. **For Raju:** Large buttons, Hindi-first, minimal text, voice input option, load board as homepage
2. **For Priya:** Clean dashboard, bid comparison table, payment history, professional tone
3. **For Suresh:** CRM-like interface, commission calculator visible, quick-post tools, contact management
---
*These personas inform all UX decisions, feature prioritization, and marketing messaging.*

157
docs/bmad/USER_STORIES.md Normal file
View file

@ -0,0 +1,157 @@
# BharathTrucks — User Stories & Epics
**Version:** 1.0
**Date:** 2026-05-31
---
## Epic 1: Authentication & Onboarding
| ID | Story | Role | Priority |
|----|-------|------|----------|
| AUTH-1 | As a user, I can register with my phone number and OTP so I don't need to remember passwords | All | P0 |
| AUTH-2 | As a user, I can select my role (Driver/Shipper/Broker) during registration | All | P0 |
| AUTH-3 | As a driver, I can complete my profile with truck details, license, and routes | Driver | P0 |
| AUTH-4 | As a shipper, I can complete my profile with business name, GST, and shipping frequency | Shipper | P0 |
| AUTH-5 | As a broker, I can complete my profile with experience, network size, and operating regions | Broker | P0 |
| AUTH-6 | As a user, I can log in with phone + OTP on any device | All | P0 |
| AUTH-7 | As a user, I can switch language (Hindi/English) during onboarding | All | P1 |
| AUTH-8 | As a user, I see a role-appropriate dashboard after login | All | P0 |
---
## Epic 2: Load Board (Marketplace)
| ID | Story | Role | Priority |
|----|-------|------|----------|
| LOAD-1 | As a shipper, I can post a load with origin, destination, weight, truck type, and budget | Shipper | P0 |
| LOAD-2 | As a driver, I can browse available loads filtered by route, truck type, and date | Driver | P0 |
| LOAD-3 | As a driver, I can view load details including shipper rating and payment terms | Driver | P0 |
| LOAD-4 | As a broker, I can post loads on behalf of my shipper clients | Broker | P0 |
| LOAD-5 | As a shipper, I can edit or cancel my posted load before bids are accepted | Shipper | P1 |
| LOAD-6 | As a user, I can search loads by city name or pincode | All | P1 |
| LOAD-7 | As a driver, I can save/bookmark loads to bid on later | Driver | P2 |
| LOAD-8 | As a user, I can see load count and recent activity on the board | All | P1 |
| LOAD-9 | As a shipper, I can mark a load as urgent for higher visibility | Shipper | P2 |
---
## Epic 3: Bidding System
| ID | Story | Role | Priority |
|----|-------|------|----------|
| BID-1 | As a driver, I can submit a bid with my price and estimated delivery time | Driver | P0 |
| BID-2 | As a shipper, I can view all bids on my load sorted by price/rating | Shipper | P0 |
| BID-3 | As a shipper, I can accept a bid and confirm booking | Shipper | P0 |
| BID-4 | As a shipper, I can reject bids with optional reason | Shipper | P1 |
| BID-5 | As a driver, I get notified when my bid is accepted/rejected | Driver | P0 |
| BID-6 | As a shipper, I can counter-offer a bid with a different price | Shipper | P2 |
| BID-7 | As a driver, I can see my bid history and success rate | Driver | P1 |
| BID-8 | As a free user, I can place up to 5 bids per day | Driver | P0 |
---
## Epic 4: Driver Dashboard & Tools
| ID | Story | Role | Priority |
|----|-------|------|----------|
| DRV-1 | As a driver, I can see personalized load recommendations based on my location and truck type | Driver | P1 |
| DRV-2 | As a driver, I can view my active trips and their status | Driver | P0 |
| DRV-3 | As a driver, I can mark trip milestones (picked up, in transit, delivered) | Driver | P0 |
| DRV-4 | As a driver, I can view my earnings summary (weekly/monthly) | Driver | P1 |
| DRV-5 | As a driver, I can manage my documents (license, RC, insurance) | Driver | P1 |
| DRV-6 | As a driver, I can update my availability status | Driver | P1 |
---
## Epic 5: Shipper Dashboard & Tools
| ID | Story | Role | Priority |
|----|-------|------|----------|
| SHP-1 | As a shipper, I can see all my posted loads and their bid status | Shipper | P0 |
| SHP-2 | As a shipper, I can track active shipments with status updates | Shipper | P0 |
| SHP-3 | As a shipper, I can view payment history and pending amounts | Shipper | P1 |
| SHP-4 | As a shipper, I can rate and review drivers after delivery | Shipper | P1 |
| SHP-5 | As a shipper, I can save preferred drivers for future loads | Shipper | P2 |
| SHP-6 | As a shipper, I can re-post a previous load with one click | Shipper | P2 |
---
## Epic 6: Broker Dashboard & CRM
| ID | Story | Role | Priority |
|----|-------|------|----------|
| BRK-1 | As a broker, I can manage my driver network (add, view, contact) | Broker | P0 |
| BRK-2 | As a broker, I can manage my shipper clients | Broker | P0 |
| BRK-3 | As a broker, I can track commissions per load with proof | Broker | P0 |
| BRK-4 | As a broker, I can quick-post loads using templates | Broker | P1 |
| BRK-5 | As a broker, I can see which drivers are available and where | Broker | P1 |
| BRK-6 | As a broker, I can share load details via WhatsApp to my network | Broker | P1 |
| BRK-7 | As a broker, I can view my monthly earnings report | Broker | P1 |
---
## Epic 7: Communication & Notifications
| ID | Story | Role | Priority |
|----|-------|------|----------|
| COM-1 | As a user, I can message another user about a specific load | All | P0 |
| COM-2 | As a user, I receive push notifications for bid updates | All | P0 |
| COM-3 | As a user, I can share load details via WhatsApp | All | P1 |
| COM-4 | As a user, I receive SMS notifications for critical updates (bid accepted) | All | P2 |
---
## Epic 8: Payments & Ledger
| ID | Story | Role | Priority |
|----|-------|------|----------|
| PAY-1 | As a shipper, I can generate a UPI payment link for the driver | Shipper | P1 |
| PAY-2 | As a driver, I can mark payment as received | Driver | P1 |
| PAY-3 | As a user, I can view my transaction ledger | All | P1 |
| PAY-4 | As a broker, I can record commission received per transaction | Broker | P1 |
---
## Epic 9: Marketing Website
| ID | Story | Role | Priority |
|----|-------|------|----------|
| WEB-1 | As a visitor, I can understand what BharathTrucks does from the landing page | Visitor | P0 |
| WEB-2 | As a visitor, I can see trust signals (user count, loads moved) | Visitor | P0 |
| WEB-3 | As a visitor, I can register directly from the landing page | Visitor | P0 |
| WEB-4 | As a visitor, I can view the load board without registering (read-only) | Visitor | P1 |
---
## Epic 10: Admin Panel
| ID | Story | Role | Priority |
|----|-------|------|----------|
| ADM-1 | As an admin, I can view all users and their roles | Admin | P0 |
| ADM-2 | As an admin, I can view platform metrics (users, loads, bids) | Admin | P0 |
| ADM-3 | As an admin, I can suspend/ban users | Admin | P1 |
| ADM-4 | As an admin, I can manage feature flags (free/premium) | Admin | P1 |
| ADM-5 | As an admin, I can broadcast announcements to all users | Admin | P2 |
---
## Priority Legend
| Priority | Meaning | Sprint Target |
|----------|---------|---------------|
| P0 | Must have for launch | Sprint 1-2 |
| P1 | Important, needed soon after launch | Sprint 3-4 |
| P2 | Nice to have, can wait | Sprint 5+ |
---
## MVP Scope Summary
**Launch with:** AUTH (all), LOAD (1-4), BID (1-5, 8), DRV (2-3), SHP (1-2), BRK (1-3), COM (1-2), WEB (1-3), ADM (1-2)
**Total MVP stories:** ~30 stories across 10 epics
---
*Stories will be refined and estimated during sprint planning.*

View file

@ -0,0 +1,284 @@
# BharathTrucks — UI/UX Design System
**Version:** 1.0
**Date:** 2026-05-31
---
## 1. Design Philosophy
**"Sarkari Trust, Modern Usability"**
The design mimics Indian government portals (DigiLocker, UMANG, CoWIN) to build instant trust with users who associate government branding with legitimacy. However, the UX is modern and mobile-first — unlike actual govt apps.
### Design Principles
1. **Trust First** — Look official, feel reliable
2. **Simplicity** — Minimum steps, maximum clarity
3. **Accessibility** — Works for semi-literate users on budget phones
4. **Mobile-First** — 90%+ users will be on mobile
5. **Performance** — Light CSS, no heavy frameworks
---
## 2. Color Palette
### Primary Colors (Government-Inspired)
| Name | Hex | Usage |
|------|-----|-------|
| **Navy Blue** | `#1a237e` | Headers, primary buttons, nav |
| **Ashoka Blue** | `#0d47a1` | Links, active states |
| **Saffron** | `#ff6f00` | Accents, CTAs, urgency |
| **White** | `#ffffff` | Backgrounds, cards |
| **National Green** | `#2e7d32` | Success states, verified badges |
### Secondary Colors
| Name | Hex | Usage |
|------|-----|-------|
| **Light Grey** | `#f5f5f5` | Page backgrounds |
| **Border Grey** | `#e0e0e0` | Card borders, dividers |
| **Text Dark** | `#212121` | Body text |
| **Text Muted** | `#616161` | Secondary text |
| **Error Red** | `#c62828` | Errors, warnings |
| **Gold** | `#f9a825` | Premium badges, highlights |
### Gradient (Header Bar)
```css
background: linear-gradient(135deg, #1a237e 0%, #0d47a1 100%);
```
---
## 3. Typography
### Font Stack
```css
/* Primary — Official Devanagari + Latin */
font-family: 'Noto Sans', 'Noto Sans Devanagari', -apple-system, BlinkMacSystemFont, sans-serif;
/* Monospace (data, numbers) */
font-family: 'JetBrains Mono', 'Courier New', monospace;
```
### Scale
| Element | Size | Weight | Usage |
|---------|------|--------|-------|
| H1 | 24px / 1.5rem | 700 | Page titles |
| H2 | 20px / 1.25rem | 600 | Section headers |
| H3 | 16px / 1rem | 600 | Card titles |
| Body | 14px / 0.875rem | 400 | Default text |
| Small | 12px / 0.75rem | 400 | Captions, meta |
| Button | 14px / 0.875rem | 600 | Button labels |
### Hindi Text
- All headings bilingual: English on top, Hindi below (smaller)
- Example: "Load Board" / "लोड बोर्ड"
---
## 4. Layout System
### Grid
- Mobile: Single column, 16px padding
- Tablet: 2-column grid, 24px gutter
- Desktop: Max-width 1200px, centered, 3-column for dashboards
### Spacing Scale
```css
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
--space-2xl: 48px;
```
### Page Structure
```
┌──────────────────────────────────┐
│ Top Bar (Navy, Emblem + Title) │ ← Govt-style header
├──────────────────────────────────┤
│ Navigation (Role-based) │
├──────────────────────────────────┤
│ │
│ Content Area │
│ │
├──────────────────────────────────┤
│ Bottom Nav (Mobile only) │ ← 4-5 icons
└──────────────────────────────────┘
```
---
## 5. Components
### 5.1 Header Bar (Govt-Style)
```
┌─────────────────────────────────────────┐
│ 🏛️ भारत ट्रक्स | BharathTrucks ☰ │
│ राष्ट्रीय माल परिवहन मंच │
└─────────────────────────────────────────┘
```
- Navy gradient background
- Ashoka-style emblem/icon on left
- Bilingual title
- Subtitle: "राष्ट्रीय माल परिवहन मंच" (National Freight Transport Platform)
### 5.2 Buttons
| Type | Style |
|------|-------|
| Primary | Navy bg, white text, 8px radius, slight shadow |
| Secondary | White bg, navy border, navy text |
| CTA/Urgent | Saffron bg, white text |
| Success | Green bg, white text |
| Danger | Red bg, white text |
```css
.btn-primary {
background: #1a237e;
color: #fff;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-weight: 600;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
```
### 5.3 Cards (Load Card)
```
┌─────────────────────────────────┐
│ 📍 Mumbai → Delhi │
│ 🚛 20 Ton | Open Body │
│ 💰 ₹45,000 | 📅 2 Jun 2026 │
│ ┌─────────────────────────────┐ │
│ │ [Bid Now] [View Details] │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────┘
```
- White background, 1px border (#e0e0e0)
- 12px border-radius
- Subtle shadow on hover
- Left color accent bar (saffron for urgent, blue for normal)
### 5.4 Form Inputs
```css
.form-input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.2s;
}
.form-input:focus {
border-color: #0d47a1;
outline: none;
box-shadow: 0 0 0 3px rgba(13, 71, 161, 0.1);
}
```
### 5.5 Trust Badges
```
┌──────────────────┐
│ ✓ सत्यापित │ (Verified)
│ Verified User │
└──────────────────┘
```
- Green border + green checkmark
- Certificate-style rounded badge
- Used for verified drivers/shippers
### 5.6 Bottom Navigation (Mobile)
```
┌─────┬─────┬─────┬─────┬─────┐
│ 🏠 │ 📋 │ │ 💬 │ 👤 │
│Home │Loads│Post │Chat │ Me │
└─────┴─────┴─────┴─────┴─────┘
```
### 5.7 Status Badges
| Status | Color | Label |
|--------|-------|-------|
| Open | Blue | खुला / Open |
| Booked | Saffron | बुक / Booked |
| In Transit | Navy | रास्ते में / In Transit |
| Delivered | Green | पहुँचा / Delivered |
| Cancelled | Red | रद्द / Cancelled |
---
## 6. Iconography
- Style: Outlined, 24px, 2px stroke
- Source: Lucide Icons (open source, lightweight)
- Key icons: truck, package, map-pin, phone, rupee-sign, user, shield-check
---
## 7. Government Trust Elements
### Must-Have Visual Cues
1. **Emblem-style logo** — Circular, with Ashoka-inspired motif
2. **Tricolor stripe** — Thin saffron-white-green bar at very top of page
3. **"Digital India" style footer** — Links, helpline number, official-looking layout
4. **Certificate-style cards** — For verified profiles, completed trips
5. **Formal language** — "आवेदन" (application) instead of "form", "पंजीकरण" (registration) instead of "signup"
6. **Seal/stamp graphics** — For verified badges, premium status
### Footer Pattern
```
┌─────────────────────────────────────────────┐
│ भारत ट्रक्स | BharathTrucks │
│ राष्ट्रीय माल परिवहन मंच │
│ │
│ सहायता: 1800-XXX-XXXX (टोल फ्री) │
│ ईमेल: support@bharathtrucks.com │
│ │
│ © 2026 BharathTrucks. सर्वाधिकार सुरक्षित। │
└─────────────────────────────────────────────┘
```
---
## 8. Responsive Breakpoints
```css
/* Mobile first */
@media (min-width: 480px) { /* Large phone */ }
@media (min-width: 768px) { /* Tablet */ }
@media (min-width: 1024px) { /* Desktop */ }
@media (min-width: 1200px) { /* Wide desktop */ }
```
---
## 9. Accessibility
| Requirement | Implementation |
|-------------|---------------|
| Color contrast | 4.5:1 minimum (WCAG AA) |
| Touch targets | 44px minimum |
| Font size | Never below 12px |
| Focus states | Visible blue outline |
| Alt text | All images and icons |
| Semantic HTML | Proper headings, landmarks, labels |
| Language | `lang="hi"` / `lang="en"` attributes |
---
## 10. Animation & Interaction
- **Minimal animations** — respect `prefers-reduced-motion`
- Page transitions: none (server-rendered)
- Button hover: subtle shadow increase
- Card hover: slight lift (2px translateY)
- Loading: Simple spinner with "कृपया प्रतीक्षा करें..." (Please wait)
- Toast notifications: Slide in from top, auto-dismiss 4s
---
*This design system ensures every screen feels official, trustworthy, and accessible to India's diverse trucking community.*

5
webapp/.dockerignore Normal file
View file

@ -0,0 +1,5 @@
node_modules
npm-debug.log
.env
.git
.gitignore

15
webapp/.env.example Normal file
View file

@ -0,0 +1,15 @@
# Server
NODE_ENV=development
PORT=3000
# Supabase
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your-supabase-anon-key
SUPABASE_SERVICE_KEY=your-supabase-service-role-key
# Session
SESSION_SECRET=change-this-to-a-random-64-char-string
# App
APP_URL=http://localhost:3000
RATE_LIMIT_BIDS_PER_DAY=5

2
webapp/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
.env

16
webapp/Dockerfile Normal file
View file

@ -0,0 +1,16 @@
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
COPY src ./src
ENV NODE_ENV=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "src/server.js"]

1317
webapp/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

26
webapp/package.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "bharathtrucks",
"version": "1.0.0",
"description": "BharathTrucks - India's National Freight Marketplace",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "node --watch src/server.js"
},
"keywords": ["freight", "marketplace", "india", "trucks", "logistics"],
"license": "ISC",
"dependencies": {
"@supabase/supabase-js": "2.45.0",
"bcryptjs": "2.4.3",
"compression": "1.7.4",
"cookie-parser": "1.4.6",
"dotenv": "16.4.5",
"ejs": "3.1.9",
"express": "4.18.2",
"express-rate-limit": "7.1.5",
"express-session": "1.18.0",
"helmet": "7.1.0",
"multer": "2.0.0",
"ws": "8.18.0"
}
}

View file

@ -0,0 +1,11 @@
module.exports = {
ROLES: { DRIVER: 'driver', SHIPPER: 'shipper', BROKER: 'broker', ADMIN: 'admin' },
LOAD_STATUS: { OPEN: 'open', BOOKED: 'booked', IN_TRANSIT: 'in_transit', DELIVERED: 'delivered', CANCELLED: 'cancelled' },
BID_STATUS: { PENDING: 'pending', ACCEPTED: 'accepted', REJECTED: 'rejected', WITHDRAWN: 'withdrawn' },
TRUCK_TYPES: ['open', 'closed', 'container', 'flatbed', 'tanker', 'refrigerated', 'mini'],
PLAN_LIMITS: {
free: { bids_per_day: 5, loads_per_month: 5, network_size: 20 },
pro: { bids_per_day: Infinity, loads_per_month: Infinity, network_size: 200 },
business: { bids_per_day: Infinity, loads_per_month: Infinity, network_size: Infinity },
},
};

16
webapp/src/config/env.js Normal file
View file

@ -0,0 +1,16 @@
module.exports = {
port: process.env.PORT || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
appUrl: process.env.APP_URL || 'http://localhost:3000',
session: {
secret: process.env.SESSION_SECRET || 'dev-secret-change-in-production',
},
supabase: {
url: process.env.SUPABASE_URL,
key: process.env.SUPABASE_KEY,
serviceKey: process.env.SUPABASE_SERVICE_KEY,
},
limits: {
bidsPerDay: parseInt(process.env.RATE_LIMIT_BIDS_PER_DAY) || 5,
},
};

View file

@ -0,0 +1,24 @@
const { ROLES } = require('../config/constants');
function requireAuth(req, res, next) {
if (req.session && req.session.user) {
res.locals.user = req.session.user;
return next();
}
res.redirect('/login');
}
function requireRole(...roles) {
return (req, res, next) => {
if (!req.session || !req.session.user) return res.redirect('/login');
if (roles.includes(req.session.user.role) || req.session.user.role === ROLES.ADMIN) return next();
res.redirect('/');
};
}
const requireDriver = requireRole(ROLES.DRIVER);
const requireShipper = requireRole(ROLES.SHIPPER);
const requireBroker = requireRole(ROLES.BROKER);
const requireAdmin = requireRole(ROLES.ADMIN);
module.exports = { requireAuth, requireRole, requireDriver, requireShipper, requireBroker, requireAdmin };

View file

@ -0,0 +1,359 @@
/* ============================================
BharathTrucks Government Theme CSS
Design System: Sarkari Trust, Modern Usability
============================================ */
/* --- CSS Variables --- */
:root {
--navy: #1a237e;
--navy-light: #283593;
--ashoka-blue: #0d47a1;
--saffron: #ff6f00;
--saffron-light: #ff8f00;
--green: #2e7d32;
--green-light: #388e3c;
--white: #ffffff;
--gray-50: #fafafa;
--gray-100: #f5f5f5;
--gray-200: #eeeeee;
--gray-300: #e0e0e0;
--gray-500: #9e9e9e;
--gray-700: #616161;
--gray-900: #212121;
--red: #c62828;
--gold: #f9a825;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
--shadow-md: 0 4px 12px rgba(0,0,0,0.1);
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
--space-2xl: 48px;
}
/* --- Reset --- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 16px; -webkit-text-size-adjust: 100%; }
body {
font-family: 'Noto Sans', 'Noto Sans Devanagari', -apple-system, BlinkMacSystemFont, sans-serif;
color: var(--gray-900);
background: var(--gray-100);
line-height: 1.6;
min-height: 100vh;
}
a { color: var(--ashoka-blue); text-decoration: none; }
a:hover { text-decoration: underline; }
img { max-width: 100%; height: auto; }
button, input, select, textarea { font-family: inherit; font-size: inherit; }
/* --- Tricolor Strip --- */
.tricolor-strip { display: flex; height: 4px; }
.tricolor-saffron { flex: 1; background: #ff9933; }
.tricolor-white { flex: 1; background: #ffffff; }
.tricolor-green { flex: 1; background: #138808; }
/* --- Header --- */
.govt-header {
background: linear-gradient(135deg, var(--navy) 0%, var(--ashoka-blue) 100%);
color: var(--white);
padding: var(--space-md);
box-shadow: var(--shadow-md);
}
.header-inner {
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-md);
}
.header-brand { display: flex; align-items: center; gap: var(--space-sm); }
.header-emblem { font-size: 2rem; }
.header-titles { line-height: 1.2; }
.header-title-hi { font-size: 1.25rem; font-weight: 700; margin: 0; }
.header-subtitle { font-size: 0.7rem; opacity: 0.85; margin: 0; }
.header-nav { display: flex; align-items: center; gap: var(--space-md); }
.header-link { color: rgba(255,255,255,0.9); font-size: 0.85rem; }
.header-link:hover { color: var(--white); text-decoration: none; }
.header-user { font-size: 0.8rem; opacity: 0.8; }
.btn-header-cta {
background: var(--saffron);
color: var(--white);
padding: 8px 16px;
border-radius: var(--radius-sm);
font-weight: 600;
font-size: 0.8rem;
}
.btn-header-cta:hover { background: var(--saffron-light); text-decoration: none; }
/* --- Main Content --- */
.main-content { min-height: calc(100vh - 200px); }
/* --- Footer --- */
.govt-footer {
background: var(--navy);
color: rgba(255,255,255,0.8);
padding: var(--space-xl) var(--space-md);
margin-top: var(--space-2xl);
}
.footer-inner { max-width: 1200px; margin: 0 auto; text-align: center; }
.footer-brand { margin-bottom: var(--space-md); }
.footer-brand strong { color: var(--white); font-size: 1rem; }
.footer-brand p { font-size: 0.75rem; opacity: 0.7; margin-top: 2px; }
.footer-links { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-md); margin-bottom: var(--space-md); }
.footer-links a { color: rgba(255,255,255,0.7); font-size: 0.8rem; }
.footer-links a:hover { color: var(--white); }
.footer-contact { font-size: 0.75rem; margin-bottom: var(--space-sm); }
.footer-copy { font-size: 0.7rem; opacity: 0.5; }
/* --- Buttons --- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-sm);
padding: 12px 24px;
border: none;
border-radius: var(--radius-md);
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
text-decoration: none;
}
.btn:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); text-decoration: none; }
.btn:active { transform: translateY(0); }
.btn-primary { background: var(--navy); color: var(--white); }
.btn-cta { background: var(--saffron); color: var(--white); box-shadow: 0 4px 16px rgba(255,111,0,0.3); }
.btn-success { background: var(--green); color: var(--white); }
.btn-outline { background: transparent; border: 2px solid var(--navy); color: var(--navy); }
.btn-lg { padding: 16px 32px; font-size: 1rem; border-radius: var(--radius-lg); }
.btn-sm { padding: 8px 16px; font-size: 0.8rem; }
.btn-block { width: 100%; }
/* --- Cards --- */
.card {
background: var(--white);
border: 1px solid var(--gray-300);
border-radius: var(--radius-lg);
padding: var(--space-lg);
box-shadow: var(--shadow-sm);
}
.card-accent { border-left: 4px solid var(--ashoka-blue); }
.card-urgent { border-left: 4px solid var(--saffron); }
/* --- Forms --- */
.form-group { margin-bottom: var(--space-md); }
.form-label { display: block; font-size: 0.85rem; font-weight: 600; margin-bottom: var(--space-xs); color: var(--gray-700); }
.form-input {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--gray-300);
border-radius: var(--radius-md);
font-size: 0.9rem;
transition: border-color 0.2s;
}
.form-input:focus { border-color: var(--ashoka-blue); outline: none; box-shadow: 0 0 0 3px rgba(13,71,161,0.1); }
.form-select { appearance: none; background: var(--white) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23616161' d='M6 8L1 3h10z'/%3E%3C/svg%3E") no-repeat right 16px center; padding-right: 40px; }
/* --- Badges --- */
.badge { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 20px; font-size: 0.7rem; font-weight: 600; }
.badge-open { background: #e3f2fd; color: var(--ashoka-blue); }
.badge-booked { background: #fff3e0; color: var(--saffron); }
.badge-transit { background: #e8eaf6; color: var(--navy); }
.badge-delivered { background: #e8f5e9; color: var(--green); }
.badge-cancelled { background: #ffebee; color: var(--red); }
.badge-verified { background: #e8f5e9; color: var(--green); border: 1px solid var(--green); }
/* --- Trust Signals --- */
.trust-seal {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: #f1f8e9;
border: 1px solid #c8e6c9;
border-radius: var(--radius-sm);
font-size: 0.75rem;
font-weight: 600;
color: var(--green);
}
/* --- Stats Grid --- */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--space-md); }
.stat-card {
background: var(--white);
border: 1px solid var(--gray-200);
border-radius: var(--radius-md);
padding: var(--space-md);
text-align: center;
}
.stat-value { font-size: 1.5rem; font-weight: 700; color: var(--navy); }
.stat-label { font-size: 0.75rem; color: var(--gray-700); margin-top: 2px; }
/* --- Container --- */
.container { max-width: 1200px; margin: 0 auto; padding: 0 var(--space-md); }
/* --- Section --- */
.section { padding: var(--space-2xl) var(--space-md); }
.section-title { font-size: 1.5rem; font-weight: 700; text-align: center; margin-bottom: var(--space-sm); }
.section-subtitle { text-align: center; color: var(--gray-700); font-size: 0.9rem; margin-bottom: var(--space-xl); }
/* --- Landing Hero --- */
.hero {
background: linear-gradient(135deg, var(--navy) 0%, var(--ashoka-blue) 100%);
color: var(--white);
padding: var(--space-2xl) var(--space-md);
text-align: center;
}
.hero-badge {
display: inline-block;
background: rgba(255,255,255,0.12);
padding: 6px 16px;
border-radius: 20px;
font-size: 0.75rem;
margin-bottom: var(--space-md);
border: 1px solid rgba(255,255,255,0.2);
}
.hero h1 { font-size: 2rem; font-weight: 700; line-height: 1.3; margin-bottom: var(--space-sm); }
.hero h1 .highlight { color: var(--gold); }
.hero-sub { font-size: 0.95rem; opacity: 0.9; max-width: 500px; margin: 0 auto var(--space-lg); }
.hero-ctas { display: flex; flex-direction: column; gap: var(--space-sm); align-items: center; }
.hero-stats { display: flex; justify-content: center; gap: var(--space-xl); margin-top: var(--space-lg); }
.hero-stat { text-align: center; }
.hero-stat-num { font-size: 1.3rem; font-weight: 700; }
.hero-stat-label { font-size: 0.65rem; opacity: 0.7; text-transform: uppercase; }
/* --- Role Cards --- */
.roles-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-md); }
.role-card {
border: 2px solid var(--gray-200);
border-radius: var(--radius-lg);
padding: var(--space-lg);
text-align: center;
transition: border-color 0.2s, box-shadow 0.2s;
}
.role-card:hover { box-shadow: var(--shadow-md); }
.role-card-driver { border-color: var(--green); background: #f1f8e9; }
.role-card-shipper { border-color: var(--saffron); background: #fff8e1; }
.role-card-broker { border-color: var(--ashoka-blue); background: #e3f2fd; }
.role-icon { font-size: 2.5rem; margin-bottom: var(--space-sm); }
.role-card h3 { font-size: 1rem; font-weight: 700; margin-bottom: var(--space-sm); }
.role-card ul { list-style: none; text-align: left; font-size: 0.8rem; }
.role-card li { padding: 3px 0; }
.role-card li::before { content: '✓ '; color: var(--green); font-weight: 700; }
/* --- How It Works --- */
.steps-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-md); counter-reset: step; }
.step-card {
background: var(--white);
border: 1px solid var(--gray-200);
border-radius: var(--radius-md);
padding: var(--space-lg);
position: relative;
counter-increment: step;
}
.step-card::before {
content: counter(step);
position: absolute;
top: var(--space-md);
left: var(--space-md);
width: 28px;
height: 28px;
background: var(--navy);
color: var(--white);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
}
.step-card h4 { margin-left: 40px; font-size: 0.9rem; font-weight: 700; }
.step-card p { margin-left: 40px; font-size: 0.8rem; color: var(--gray-700); margin-top: 4px; }
/* --- Error Pages --- */
.error-page { text-align: center; padding: var(--space-2xl); }
.error-page h1 { font-size: 3rem; color: var(--navy); }
.error-page p { color: var(--gray-700); margin: var(--space-md) 0; }
/* --- Responsive --- */
@media (min-width: 480px) {
.hero-ctas { flex-direction: row; justify-content: center; }
}
@media (min-width: 768px) {
.roles-grid { grid-template-columns: 1fr 1fr 1fr; }
.steps-grid { grid-template-columns: 1fr 1fr; }
.hero h1 { font-size: 2.5rem; }
}
@media (min-width: 1024px) {
.header-subtitle { font-size: 0.8rem; }
}
/* --- Utility --- */
.text-center { text-align: center; }
.mt-md { margin-top: var(--space-md); }
.mt-lg { margin-top: var(--space-lg); }
.mb-md { margin-bottom: var(--space-md); }
.hidden { display: none; }
/* --- Auth Pages --- */
.alert-error {
background: #ffebee;
color: var(--red);
border: 1px solid #ffcdd2;
border-radius: var(--radius-sm);
padding: 10px 14px;
font-size: 0.8rem;
margin-bottom: var(--space-md);
}
.role-select-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--space-sm); }
.role-option input { display: none; }
.role-option-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 12px 8px;
border: 2px solid var(--gray-300);
border-radius: var(--radius-md);
cursor: pointer;
font-size: 0.75rem;
font-weight: 600;
transition: border-color 0.2s, box-shadow 0.2s;
}
.role-option input:checked + .role-option-card { border-color: var(--navy); box-shadow: 0 0 0 3px rgba(26,35,126,0.15); }
.role-option-card .role-icon { font-size: 1.5rem; }
/* --- Bottom Nav --- */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--white);
border-top: 1px solid var(--gray-300);
display: flex;
justify-content: space-around;
padding: 6px 0 env(safe-area-inset-bottom, 6px);
z-index: 1000;
}
.bnav-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
font-size: 0.6rem;
color: var(--gray-700);
text-decoration: none;
padding: 4px 8px;
}
.bnav-item:hover { color: var(--navy); text-decoration: none; }
.bnav-icon { font-size: 1.2rem; }
.bnav-add .bnav-icon { background: var(--saffron); color: #fff; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-top: -12px; font-size: 1rem; }
body { padding-bottom: 70px; }
@media (min-width: 768px) { .bottom-nav { display: none; } body { padding-bottom: 0; } }

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<circle cx="32" cy="32" r="30" fill="#1a237e"/>
<text x="32" y="40" text-anchor="middle" font-size="24" fill="white" font-family="sans-serif" font-weight="bold">BT</text>
</svg>

After

Width:  |  Height:  |  Size: 266 B

View file

@ -0,0 +1,4 @@
// BharathTrucks — Client-side JS
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch(() => {});
}

View file

@ -0,0 +1,14 @@
{
"name": "भारत ट्रक्स - राष्ट्रीय माल परिवहन मंच",
"short_name": "भारत ट्रक्स",
"description": "India's National Freight Transport Platform",
"start_url": "/",
"display": "standalone",
"background_color": "#1a237e",
"theme_color": "#1a237e",
"orientation": "portrait",
"icons": [
{ "src": "/images/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/images/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}

32
webapp/src/public/sw.js Normal file
View file

@ -0,0 +1,32 @@
const CACHE_NAME = 'bharathtrucks-v1';
const STATIC_ASSETS = [
'/',
'/css/govt-theme.css',
'/js/app.js',
'/manifest.json',
];
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(CACHE_NAME).then(c => c.addAll(STATIC_ASSETS)));
self.skipWaiting();
});
self.addEventListener('activate', (e) => {
e.waitUntil(caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
));
self.clients.claim();
});
self.addEventListener('fetch', (e) => {
if (e.request.method !== 'GET') return;
e.respondWith(
fetch(e.request).then(res => {
if (res.ok && e.request.url.includes('/css/') || e.request.url.includes('/js/')) {
const clone = res.clone();
caches.open(CACHE_NAME).then(c => c.put(e.request, clone));
}
return res;
}).catch(() => caches.match(e.request))
);
});

View file

@ -0,0 +1,50 @@
const express = require('express');
const router = express.Router();
const supabase = require('../services/supabase');
const { requireAdmin } = require('../middleware/auth');
router.use(requireAdmin);
// GET /admin — dashboard
router.get('/', async (req, res) => {
const { count: userCount } = await supabase.from('app_users').select('*', { count: 'exact', head: true });
const { count: loadCount } = await supabase.from('loads').select('*', { count: 'exact', head: true });
const { count: bidCount } = await supabase.from('bids').select('*', { count: 'exact', head: true });
const { count: tripCount } = await supabase.from('trips').select('*', { count: 'exact', head: true });
const { data: roleStats } = await supabase.from('app_users').select('role');
const roles = { driver: 0, shipper: 0, broker: 0 };
(roleStats || []).forEach(u => { if (roles[u.role] !== undefined) roles[u.role]++; });
const { data: recentUsers } = await supabase.from('app_users').select('id, name, username, role, created_at').order('created_at', { ascending: false }).limit(5);
res.render('pages/admin-dashboard', {
stats: { users: userCount || 0, loads: loadCount || 0, bids: bidCount || 0, trips: tripCount || 0 },
roles, recentUsers: recentUsers || [],
});
});
// GET /admin/users
router.get('/users', async (req, res) => {
const { role, search } = req.query;
let query = supabase.from('app_users').select('*').order('created_at', { ascending: false });
if (role && role !== 'all') query = query.eq('role', role);
if (search) query = query.or(`name.ilike.%${search}%,username.ilike.%${search}%`);
const { data: users } = await query.limit(100);
res.render('pages/admin-users', { users: users || [], filters: req.query });
});
// POST /admin/users/:id/suspend
router.post('/users/:id/suspend', async (req, res) => {
const { data: user } = await supabase.from('app_users').select('is_active').eq('id', req.params.id).single();
if (user) await supabase.from('app_users').update({ is_active: !user.is_active }).eq('id', req.params.id);
res.redirect('/admin/users');
});
// GET /admin/loads
router.get('/loads', async (req, res) => {
const { data: loads } = await supabase.from('loads').select('*, poster:posted_by(name)').order('created_at', { ascending: false }).limit(50);
res.render('pages/admin-loads', { loads: loads || [] });
});
module.exports = router;

102
webapp/src/routes/auth.js Normal file
View file

@ -0,0 +1,102 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const router = express.Router();
const supabase = require('../services/supabase');
const { ROLES } = require('../config/constants');
// GET /login
router.get('/login', (req, res) => {
if (req.session.user) return res.redirect('/');
res.render('pages/login', { error: null });
});
// POST /login
router.post('/login', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.render('pages/login', { error: 'यूज़रनेम और पासवर्ड आवश्यक है' });
}
const { data: user, error } = await supabase
.from('app_users')
.select('*')
.eq('username', username.toLowerCase().trim())
.single();
if (error || !user) {
return res.render('pages/login', { error: 'गलत यूज़रनेम या पासवर्ड' });
}
const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) {
return res.render('pages/login', { error: 'गलत यूज़रनेम या पासवर्ड' });
}
req.session.user = {
id: user.id, username: user.username, name: user.name,
role: user.role, phone: user.phone,
};
res.redirect('/');
});
// GET /register
router.get('/register', (req, res) => {
if (req.session.user) return res.redirect('/');
res.render('pages/register', { error: null, role: req.query.role || '' });
});
// POST /register
router.post('/register', async (req, res) => {
const { name, username, password, password_confirm, role, phone } = req.body;
if (!name || !username || !password || !role) {
return res.render('pages/register', { error: 'सभी फ़ील्ड भरें', role });
}
if (password.length < 4) {
return res.render('pages/register', { error: 'पासवर्ड कम से कम 4 अक्षर का होना चाहिए', role });
}
if (password !== password_confirm) {
return res.render('pages/register', { error: 'पासवर्ड मेल नहीं खाता', role });
}
if (![ROLES.DRIVER, ROLES.SHIPPER, ROLES.BROKER].includes(role)) {
return res.render('pages/register', { error: 'कृपया भूमिका चुनें', role });
}
const cleanUsername = username.toLowerCase().trim().replace(/\s/g, '');
// Check existing
const { data: existing } = await supabase
.from('app_users')
.select('id')
.eq('username', cleanUsername)
.single();
if (existing) {
return res.render('pages/register', { error: 'यह यूज़रनेम पहले से लिया हुआ है', role });
}
const password_hash = await bcrypt.hash(password, 10);
const { data: user, error } = await supabase
.from('app_users')
.insert([{ username: cleanUsername, name: name.trim(), password_hash, role, phone: phone || null }])
.select()
.single();
if (error) {
return res.render('pages/register', { error: 'पंजीकरण विफल: ' + error.message, role });
}
req.session.user = {
id: user.id, username: user.username, name: user.name,
role: user.role, phone: user.phone,
};
res.redirect('/');
});
// GET /logout
router.get('/logout', (req, res) => {
req.session.destroy(() => res.redirect('/'));
});
module.exports = router;

120
webapp/src/routes/loads.js Normal file
View file

@ -0,0 +1,120 @@
const express = require('express');
const router = express.Router();
const supabase = require('../services/supabase');
const { requireAuth, requireRole } = require('../middleware/auth');
const { ROLES, TRUCK_TYPES } = require('../config/constants');
// GET /loadboard — public browse
router.get('/', async (req, res) => {
try {
const { origin, destination, truck_type, sort } = req.query;
let query = supabase.from('loads').select('*, poster:posted_by(name, username)').eq('status', 'open');
if (origin) query = query.ilike('origin_city', `%${origin}%`);
if (destination) query = query.ilike('destination_city', `%${destination}%`);
if (truck_type && truck_type !== 'all') query = query.eq('truck_type', truck_type);
if (sort === 'budget_high') query = query.order('budget', { ascending: false });
else if (sort === 'budget_low') query = query.order('budget', { ascending: true });
else query = query.order('created_at', { ascending: false });
const { data: loads } = await query.limit(50);
res.render('pages/loadboard', {
loads: loads || [], filters: req.query, truckTypes: TRUCK_TYPES,
});
} catch (err) {
console.error('Loadboard error:', err);
res.render('pages/loadboard', { loads: [], filters: {}, truckTypes: TRUCK_TYPES });
}
});
// GET /loadboard/post — form
router.get('/post', requireAuth, requireRole(ROLES.SHIPPER, ROLES.BROKER), (req, res) => {
res.render('pages/post-load', { error: null, truckTypes: TRUCK_TYPES });
});
// POST /loadboard/post — create load
router.post('/post', requireAuth, requireRole(ROLES.SHIPPER, ROLES.BROKER), async (req, res) => {
const { origin_city, destination_city, weight_tons, truck_type, material_type, budget, pickup_date, description, is_urgent } = req.body;
if (!origin_city || !destination_city || !weight_tons || !truck_type || !pickup_date) {
return res.render('pages/post-load', { error: 'सभी आवश्यक फ़ील्ड भरें', truckTypes: TRUCK_TYPES });
}
const { error } = await supabase.from('loads').insert({
posted_by: req.session.user.id,
origin_city: origin_city.trim(),
destination_city: destination_city.trim(),
weight_tons: parseFloat(weight_tons),
truck_type,
material_type: material_type || null,
budget: parseFloat(budget) || null,
pickup_date,
description: description || null,
is_urgent: is_urgent === 'on',
});
if (error) {
return res.render('pages/post-load', { error: 'लोड पोस्ट करने में त्रुटि', truckTypes: TRUCK_TYPES });
}
res.redirect('/loadboard');
});
// GET /loadboard/:id — detail
router.get('/:id', async (req, res) => {
const { data: load } = await supabase
.from('loads')
.select('*, poster:posted_by(name, username)')
.eq('id', req.params.id)
.single();
if (!load) return res.redirect('/loadboard');
const { data: bids } = await supabase
.from('bids')
.select('*, driver:driver_id(name, username)')
.eq('load_id', req.params.id)
.order('amount', { ascending: true });
const user = req.session.user || null;
const myBid = user ? (bids || []).find(b => b.driver_id === user.id) : null;
res.render('pages/load-detail', { load, bids: bids || [], myBid, user });
});
// POST /loadboard/:id/bid — place bid
router.post('/:id/bid', requireAuth, requireRole(ROLES.DRIVER), async (req, res) => {
const { amount, note } = req.body;
if (!amount || parseFloat(amount) <= 0) return res.redirect(`/loadboard/${req.params.id}`);
await supabase.from('bids').upsert({
load_id: req.params.id,
driver_id: req.session.user.id,
amount: parseFloat(amount),
note: note || null,
}, { onConflict: 'load_id,driver_id' });
res.redirect(`/loadboard/${req.params.id}`);
});
// POST /loadboard/:id/accept-bid — shipper accepts + create trip
router.post('/:id/accept-bid', requireAuth, requireRole(ROLES.SHIPPER, ROLES.BROKER), async (req, res) => {
const { bid_id } = req.body;
const { data: bid } = await supabase.from('bids').select('*').eq('id', bid_id).single();
if (!bid) return res.redirect(`/loadboard/${req.params.id}`);
await supabase.from('bids').update({ status: 'accepted' }).eq('id', bid_id);
await supabase.from('bids').update({ status: 'rejected' }).eq('load_id', req.params.id).neq('id', bid_id).eq('status', 'pending');
await supabase.from('loads').update({ status: 'booked', accepted_bid_id: bid_id }).eq('id', req.params.id);
// Create trip
await supabase.from('trips').insert({
load_id: req.params.id, driver_id: bid.driver_id,
shipper_id: req.session.user.id, bid_id, amount: bid.amount,
});
res.redirect(`/loadboard/${req.params.id}`);
});
module.exports = router;

View file

@ -0,0 +1,62 @@
const express = require('express');
const router = express.Router();
const supabase = require('../services/supabase');
const { requireAuth } = require('../middleware/auth');
router.use(requireAuth);
// GET /messages — inbox (conversations)
router.get('/', async (req, res) => {
const userId = req.session.user.id;
// Get distinct conversations
const { data: msgs } = await supabase.from('messages')
.select('*, sender:sender_id(name, username), receiver:receiver_id(name, username)')
.or(`sender_id.eq.${userId},receiver_id.eq.${userId}`)
.order('created_at', { ascending: false })
.limit(50);
// Group by other user
const convos = {};
(msgs || []).forEach(m => {
const otherId = m.sender_id === userId ? m.receiver_id : m.sender_id;
const other = m.sender_id === userId ? m.receiver : m.sender;
if (!convos[otherId]) convos[otherId] = { user: other, lastMsg: m, unread: 0 };
if (m.receiver_id === userId && !m.is_read) convos[otherId].unread++;
});
res.render('pages/messages', { conversations: Object.values(convos) });
});
// GET /messages/:userId — conversation thread
router.get('/:userId', async (req, res) => {
const userId = req.session.user.id;
const otherId = req.params.userId;
const { data: otherUser } = await supabase.from('app_users').select('name, username').eq('id', otherId).single();
const { data: msgs } = await supabase.from('messages')
.select('*')
.or(`and(sender_id.eq.${userId},receiver_id.eq.${otherId}),and(sender_id.eq.${otherId},receiver_id.eq.${userId})`)
.order('created_at', { ascending: true });
// Mark as read
await supabase.from('messages').update({ is_read: true }).eq('receiver_id', userId).eq('sender_id', otherId);
res.render('pages/chat', { otherUser: otherUser || { name: 'User', username: '' }, messages: msgs || [], otherId });
});
// POST /messages/:userId — send message
router.post('/:userId', async (req, res) => {
const { content, load_id } = req.body;
if (!content || !content.trim()) return res.redirect(`/messages/${req.params.userId}`);
await supabase.from('messages').insert({
sender_id: req.session.user.id,
receiver_id: req.params.userId,
content: content.trim(),
load_id: load_id || null,
});
res.redirect(`/messages/${req.params.userId}`);
});
module.exports = router;

View file

@ -0,0 +1,39 @@
const express = require('express');
const router = express.Router();
const supabase = require('../services/supabase');
const { requireAuth } = require('../middleware/auth');
router.use(requireAuth);
// GET /trips — my trips
router.get('/', async (req, res) => {
const userId = req.session.user.id;
const role = req.session.user.role;
let query = supabase.from('trips').select('*, load:load_id(origin_city, destination_city, truck_type, weight_tons)');
if (role === 'driver') query = query.eq('driver_id', userId);
else query = query.eq('shipper_id', userId);
const { data: trips } = await query.order('created_at', { ascending: false });
res.render('pages/trips', { trips: trips || [] });
});
// POST /trips/:id/status — update trip status
router.post('/:id/status', async (req, res) => {
const { status } = req.body;
const updates = { status };
if (status === 'picked_up') updates.picked_up_at = new Date().toISOString();
if (status === 'delivered') updates.delivered_at = new Date().toISOString();
await supabase.from('trips').update(updates).eq('id', req.params.id);
// Also update load status
if (status === 'in_transit' || status === 'delivered') {
const { data: trip } = await supabase.from('trips').select('load_id').eq('id', req.params.id).single();
if (trip) await supabase.from('loads').update({ status }).eq('id', trip.load_id);
}
res.redirect('/trips');
});
module.exports = router;

138
webapp/src/server.js Normal file
View file

@ -0,0 +1,138 @@
require('dotenv').config();
const express = require('express');
const path = require('path');
const helmet = require('helmet');
const compression = require('compression');
const session = require('express-session');
const rateLimit = require('express-rate-limit');
const config = require('./config/env');
const app = express();
// Security
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "https:"],
scriptSrc: ["'self'", "'unsafe-inline'"],
},
},
}));
app.use(compression());
app.use(rateLimit({ windowMs: 60 * 1000, max: 100 }));
// Body parsing
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Static files
app.use(express.static(path.join(__dirname, 'public')));
// View engine
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// Session
app.use(session({
secret: config.session.secret,
resave: false,
saveUninitialized: false,
cookie: { secure: config.nodeEnv === 'production', maxAge: 24 * 60 * 60 * 1000 },
}));
// Make user available to all views
app.use((req, res, next) => {
res.locals.user = req.session.user || null;
res.locals.appName = 'भारत ट्रक्स';
res.locals.appNameEn = 'BharathTrucks';
next();
});
// Routes
const authRoutes = require('./routes/auth');
const loadRoutes = require('./routes/loads');
const tripRoutes = require('./routes/trips');
const adminRoutes = require('./routes/admin');
const messageRoutes = require('./routes/messages');
app.use('/', authRoutes);
app.use('/loadboard', loadRoutes);
app.use('/trips', tripRoutes);
app.use('/admin', adminRoutes);
app.use('/messages', messageRoutes);
const { requireAuth, requireDriver, requireShipper, requireBroker } = require('./middleware/auth');
const supabase = require('./services/supabase');
app.get('/health', (req, res) => res.json({ status: 'ok', ts: Date.now() }));
app.get('/', (req, res) => {
if (req.session && req.session.user) {
const { ROLES } = require('./config/constants');
if (req.session.user.role === ROLES.DRIVER) return res.redirect('/driver');
if (req.session.user.role === ROLES.SHIPPER) return res.redirect('/shipper');
if (req.session.user.role === ROLES.BROKER) return res.redirect('/broker');
}
res.render('pages/landing');
});
// Dashboards
app.get('/profile', requireAuth, async (req, res) => {
const { data: profile } = await supabase.from('app_users').select('*').eq('id', req.session.user.id).single();
res.render('pages/profile', { profile: profile || req.session.user, success: req.query.ok });
});
app.post('/profile', requireAuth, async (req, res) => {
const { name, phone, city, state } = req.body;
await supabase.from('app_users').update({ name: name.trim(), phone: phone || null, city: city || null, state: state || null }).eq('id', req.session.user.id);
req.session.user.name = name.trim();
res.redirect('/profile?ok=1');
});
app.get('/driver', requireAuth, requireDriver, async (req, res) => {
const userId = req.session.user.id;
const { data: bids } = await supabase.from('bids').select('status').eq('driver_id', userId);
const { data: trips } = await supabase.from('trips').select('*, load:load_id(origin_city, destination_city)').eq('driver_id', userId).order('created_at', { ascending: false });
const activeTrips = (trips || []).filter(t => !['delivered', 'cancelled'].includes(t.status));
const delivered = (trips || []).filter(t => t.status === 'delivered');
const earnings = delivered.reduce((s, t) => s + (parseFloat(t.amount) || 0), 0);
res.render('pages/driver-dashboard', {
stats: { totalTrips: (trips || []).length, activeBids: (bids || []).filter(b => b.status === 'pending').length, earnings },
activeTrips,
});
});
app.get('/shipper', requireAuth, requireShipper, async (req, res) => {
const userId = req.session.user.id;
const { data: loads } = await supabase.from('loads').select('*').eq('posted_by', userId).order('created_at', { ascending: false }).limit(10);
const { data: trips } = await supabase.from('trips').select('status').eq('shipper_id', userId);
const allLoads = loads || [];
res.render('pages/shipper-dashboard', {
stats: { totalLoads: allLoads.length, openLoads: allLoads.filter(l => l.status === 'open').length, activeTrips: (trips || []).filter(t => !['delivered', 'cancelled'].includes(t.status)).length },
recentLoads: allLoads.slice(0, 5),
});
});
app.get('/broker', requireAuth, requireBroker, async (req, res) => {
const userId = req.session.user.id;
const { data: loads } = await supabase.from('loads').select('*').eq('posted_by', userId).order('created_at', { ascending: false }).limit(10);
const { data: trips } = await supabase.from('trips').select('status').eq('shipper_id', userId);
const allLoads = loads || [];
res.render('pages/broker-dashboard', {
stats: { totalLoads: allLoads.length, bookedLoads: allLoads.filter(l => l.status === 'booked').length, activeTrips: (trips || []).filter(t => !['delivered', 'cancelled'].includes(t.status)).length },
recentLoads: allLoads.slice(0, 5),
});
});
// 404
app.use((req, res) => res.status(404).render('pages/404'));
// Error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).render('pages/500');
});
app.listen(config.port, '0.0.0.0', () => {
console.log(`BharathTrucks running at http://localhost:${config.port}`);
});

View file

@ -0,0 +1,16 @@
const { createClient } = require('@supabase/supabase-js');
const config = require('../config/env');
if (!config.supabase.url || !config.supabase.key) {
console.error('Missing SUPABASE_URL or SUPABASE_KEY. Check .env file.');
process.exit(1);
}
const options = {};
if (typeof globalThis.WebSocket === 'undefined') {
options.realtime = { transport: require('ws') };
}
const supabase = createClient(config.supabase.url, config.supabase.key, options);
module.exports = supabase;

View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="hi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#1a237e">
<meta name="description" content="भारत ट्रक्स - राष्ट्रीय माल परिवहन मंच। ट्रक ड्राइवर, शिपर और ब्रोकर के लिए मुफ्त लोड बोर्ड।">
<title><%= typeof title !== 'undefined' ? title + ' | भारत ट्रक्स' : 'भारत ट्रक्स - राष्ट्रीय माल परिवहन मंच' %></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<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 rel="stylesheet" href="/css/govt-theme.css">
<link rel="manifest" href="/manifest.json">
<link rel="icon" href="/images/favicon.svg" type="image/svg+xml">
</head>
<body>
<!-- Tricolor Strip -->
<div class="tricolor-strip">
<div class="tricolor-saffron"></div>
<div class="tricolor-white"></div>
<div class="tricolor-green"></div>
</div>
<%- include('../partials/header') %>
<main class="main-content">
<%- body %>
</main>
<%- include('../partials/footer') %>
<script src="/js/app.js"></script>
</body>
</html>

View file

@ -0,0 +1,9 @@
<% var title = '404 - पृष्ठ नहीं मिला'; %>
<%- 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="error-page">
<h1>404</h1>
<p>यह पृष्ठ उपलब्ध नहीं है। | Page not found.</p>
<a href="/" class="btn btn-primary">मुख्य पृष्ठ पर जाएं</a>
</div>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,9 @@
<% var title = '500 - सर्वर त्रुटि'; %>
<%- 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="error-page">
<h1>500</h1>
<p>कुछ गलत हो गया। कृपया बाद में पुनः प्रयास करें। | Something went wrong.</p>
<a href="/" class="btn btn-primary">मुख्य पृष्ठ पर जाएं</a>
</div>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,46 @@
<% var title = 'एडमिन पैनल'; %>
<%- 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)">🏛️ एडमिन पैनल</h2>
<div class="stats-grid">
<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.loads %></div><div class="stat-label">कुल लोड</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.trips %></div><div class="stat-label">कुल ट्रिप</div></div>
</div>
<div class="stats-grid" style="margin-top:var(--space-md)">
<div class="stat-card"><div class="stat-value"><%= roles.driver %></div><div class="stat-label">🚛 ड्राइवर</div></div>
<div class="stat-card"><div class="stat-value"><%= roles.shipper %></div><div class="stat-label">📦 शिपर</div></div>
<div class="stat-card"><div class="stat-value"><%= roles.broker %></div><div class="stat-label">🤝 ब्रोकर</div></div>
</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>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,30 @@
<% var title = 'सभी लोड — एडमिन'; %>
<%- 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">📋 सभी लोड (<%= 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 => { %>
<tr style="border-top:1px solid var(--gray-200)">
<td style="padding:8px"><a href="/loadboard/<%= l.id %>"><%= l.origin_city %> → <%= l.destination_city %></a></td>
<td style="padding:8px"><%= l.weight_tons %>T</td>
<td style="padding:8px"><%= l.budget ? '₹' + Number(l.budget).toLocaleString('en-IN') : '-' %></td>
<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>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,44 @@
<% var title = 'उपयोगकर्ता प्रबंधन'; %>
<%- 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">👥 उपयोगकर्ता (<%= 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 => { %>
<tr style="border-top:1px solid var(--gray-200)">
<td style="padding:8px"><%= u.name %></td>
<td style="padding:8px;color:var(--gray-700)"><%= u.username %></td>
<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>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,37 @@
<% var title = 'ब्रोकर डैशबोर्ड'; %>
<%- 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)">🤝 नमस्ते, <%= user.name %>!</h2>
<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.bookedLoads %></div><div class="stat-label">सौदे</div></div>
<div class="stat-card"><div class="stat-value"><%= stats.activeTrips %></div><div class="stat-label">सक्रिय</div></div>
</div>
<% if (recentLoads.length > 0) { %>
<h3 style="font-size:1rem;margin-top:var(--space-lg);margin-bottom:var(--space-sm)">📋 हाल के लोड</h3>
<% 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)">
<div style="display:flex;justify-content:space-between;align-items:center">
<div>
<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>
<span class="badge badge-<%= load.status === 'open' ? 'open' : 'booked' %>"><%= load.status %></span>
</div>
</a>
<% }) %>
<% } %>
<div style="margin-top:var(--space-lg);display:grid;gap:var(--space-sm)">
<a href="/loadboard/post" class="btn btn-cta btn-block">+ लोड पोस्ट करें</a>
<a href="/loadboard" class="btn btn-outline btn-block">📋 लोड बोर्ड</a>
</div>
</div>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,29 @@
<% var title = otherUser.name + ' — चैट'; %>
<%- 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 style="display:flex;align-items:center;gap:var(--space-sm);margin-bottom:var(--space-md)">
<a href="/messages" style="font-size:1.2rem">←</a>
<strong><%= otherUser.name %></strong>
<span style="font-size:0.75rem;color:var(--gray-700)">@<%= otherUser.username %></span>
</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 %>
<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>
<% }) %>
</div>
<form method="POST" action="/messages/<%= otherId %>" style="display:flex;gap:var(--space-sm)">
<input type="text" name="content" class="form-input" placeholder="संदेश लिखें..." required autofocus style="flex:1;padding:10px 14px">
<button type="submit" class="btn btn-primary">भेजें</button>
</form>
</div>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,37 @@
<% var title = 'ड्राइवर डैशबोर्ड'; %>
<%- 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)">🚛 नमस्ते, <%= user.name %>!</h2>
<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.activeBids %></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">कमाई</div></div>
</div>
<% if (activeTrips.length > 0) { %>
<h3 style="font-size:1rem;margin-top:var(--space-lg);margin-bottom:var(--space-sm)">🔄 सक्रिय ट्रिप</h3>
<% activeTrips.forEach(trip => { %>
<div class="card card-accent" style="margin-bottom:var(--space-sm)">
<div style="display:flex;justify-content:space-between;align-items:center">
<div>
<strong><%= trip.load ? trip.load.origin_city + ' → ' + trip.load.destination_city : '' %></strong>
<div style="font-size:0.8rem;color:var(--gray-700)">₹<%= Number(trip.amount).toLocaleString('en-IN') %></div>
</div>
<span class="badge badge-transit"><%= trip.status %></span>
</div>
</div>
<% }) %>
<% } %>
<div style="margin-top:var(--space-lg);display:grid;gap:var(--space-sm)">
<a href="/loadboard" class="btn btn-primary btn-block">📋 लोड बोर्ड देखें</a>
<a href="/trips" class="btn btn-outline btn-block">🚚 मेरी ट्रिप</a>
</div>
</div>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,77 @@
<% var title = 'राष्ट्रीय माल परिवहन मंच'; %>
<%- 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="hero">
<div class="hero-badge">🇮🇳 भारत सरकार पंजीकृत मंच | Registered Platform</div>
<h1>ट्रक ड्राइवर। शिपर। ब्रोकर।<br><span class="highlight">सबके लिए मुफ्त।</span></h1>
<p class="hero-sub">भारत का राष्ट्रीय माल परिवहन मंच — लोड पोस्ट करें, बोली लगाएं, कमाई करें। बिना किसी शुल्क के।</p>
<div class="hero-ctas">
<a href="/register" class="btn btn-cta btn-lg">मुफ्त पंजीकरण करें</a>
<a href="/loadboard" class="btn btn-outline btn-lg" style="border-color:rgba(255,255,255,0.5);color:#fff">लोड बोर्ड देखें</a>
</div>
<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">30 सेकंड</div><div class="hero-stat-label">पंजीकरण</div></div>
<div class="hero-stat"><div class="hero-stat-num">5 मिनट</div><div class="hero-stat-label">पहली बोली</div></div>
</div>
</section>
<section class="section">
<div class="container">
<h2 class="section-title">एक मंच। तीन उपयोगकर्ता।</h2>
<p class="section-subtitle">चाहे आप माल भेजें, ट्रक चलाएं, या सौदे कराएं — भारत ट्रक्स आपके लिए है।</p>
<div class="roles-grid">
<div class="role-card role-card-driver">
<div class="role-icon">🚛</div>
<h3>ट्रक ड्राइवर</h3>
<ul><li>लोड खोजें और बोली लगाएं</li><li>खाली वापसी से बचें</li><li>कमाई का हिसाब रखें</li><li>सीधे शिपर से जुड़ें</li></ul>
</div>
<div class="role-card role-card-shipper">
<div class="role-icon">📦</div>
<h3>शिपर / माल भेजने वाले</h3>
<ul><li>लोड पोस्ट करें, बोली पाएं</li><li>सत्यापित ड्राइवर चुनें</li><li>माल की स्थिति जानें</li><li>भुगतान का रिकॉर्ड रखें</li></ul>
</div>
<div class="role-card role-card-broker">
<div class="role-icon">🤝</div>
<h3>ब्रोकर / एजेंट</h3>
<ul><li>अपने नेटवर्क को डिजिटल करें</li><li>कमीशन ट्रैक करें</li><li>शिपर के लिए लोड पोस्ट करें</li><li>ड्राइवर नेटवर्क बढ़ाएं</li></ul>
</div>
</div>
</div>
</section>
<section class="section" style="background:var(--gray-50)">
<div class="container">
<h2 class="section-title">कैसे काम करता है?</h2>
<p class="section-subtitle">सिर्फ 4 आसान कदम</p>
<div class="steps-grid">
<div class="step-card"><h4>पंजीकरण करें</h4><p>फोन नंबर से मुफ्त अकाउंट बनाएं। अपनी भूमिका चुनें।</p></div>
<div class="step-card"><h4>लोड पोस्ट / खोजें</h4><p>शिपर लोड पोस्ट करें। ड्राइवर उपलब्ध लोड देखें।</p></div>
<div class="step-card"><h4>बोली लगाएं / स्वीकार करें</h4><p>ड्राइवर अपनी कीमत बताएं। शिपर सबसे अच्छी बोली चुनें।</p></div>
<div class="step-card"><h4>माल पहुँचाएं, भुगतान पाएं</h4><p>ट्रिप पूरी करें। UPI से सीधे भुगतान पाएं।</p></div>
</div>
</div>
</section>
<section class="section">
<div class="container">
<h2 class="section-title">क्यों भारत ट्रक्स?</h2>
<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">🔒</div><div class="stat-label">सुरक्षित मंच</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">भारत के लिए बना</div></div>
</div>
</div>
</section>
<section class="section" style="background:linear-gradient(135deg, var(--navy), var(--ashoka-blue)); color:var(--white); text-align:center;">
<div class="container">
<h2 style="color:var(--white); font-size:1.5rem; margin-bottom:var(--space-sm);">आज ही शुरू करें — बिल्कुल मुफ्त!</h2>
<p style="opacity:0.85; margin-bottom:var(--space-lg);">1000+ उपयोगकर्ताओं तक सभी सुविधाएं मुफ्त। कोई क्रेडिट कार्ड नहीं चाहिए।</p>
<a href="/register" class="btn btn-cta btn-lg">अभी पंजीकरण करें →</a>
</div>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,89 @@
<% var title = load.origin_city + ' → ' + load.destination_city; %>
<%- 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:600px">
<a href="/loadboard" style="font-size:0.8rem;color:var(--gray-700)">← लोड बोर्ड पर वापस</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 style="display:flex;justify-content:space-between;align-items:start">
<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>
</div>
<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)">ट्रक</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)">बजट</small><br><strong><%= load.budget ? '₹' + Number(load.budget).toLocaleString('en-IN') : 'बताया नहीं' %></strong></div>
</div>
<% if (load.material_type) { %>
<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">
<span>पोस्ट किया: <%= load.poster ? load.poster.name : 'Unknown' %> | <%= new Date(load.created_at).toLocaleDateString('hi-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>
</div>
</div>
<!-- Bid Form (drivers only, open loads) -->
<% if (user && user.role === 'driver' && load.status === 'open') { %>
<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)">
<h3 style="font-size:1rem">🏷️ <%= myBid ? 'अपनी बोली अपडेट करें' : 'बोली लगाएं' %></h3>
<a href="/messages/<%= load.posted_by %>" class="btn btn-sm btn-outline" style="font-size:0.7rem">💬 शिपर से बात करें</a>
</div>
<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 class="form-group" style="margin:0">
<label class="form-label">आपकी कीमत (₹)</label>
<input type="number" name="amount" class="form-input" placeholder="42000" value="<%= myBid ? myBid.amount : '' %>" required>
</div>
<button type="submit" class="btn btn-success">बोली लगाएं</button>
</div>
<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">
</div>
</form>
</div>
<% } %>
<!-- Bids List (visible to load owner) -->
<% if (bids.length > 0 && user && (user.id === load.posted_by || user.role === 'admin')) { %>
<div class="card" style="margin-top:var(--space-md)">
<h3 style="font-size:1rem;margin-bottom:var(--space-md)">📊 बोलियाँ (<%= bids.length %>)</h3>
<% 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>
<strong style="font-size:0.9rem"><%= bid.driver ? bid.driver.name : 'Driver' %></strong>
<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>
<% if (load.status === 'open' && bid.status === 'pending') { %>
<form method="POST" action="/loadboard/<%= load.id %>/accept-bid" style="margin-top:4px">
<input type="hidden" name="bid_id" value="<%= bid.id %>">
<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>
<% } 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>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,76 @@
<% var title = 'लोड बोर्ड'; %>
<%- 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">📋 लोड बोर्ड</h2>
<% if (user && (user.role === 'shipper' || user.role === 'broker')) { %>
<a href="/loadboard/post" class="btn btn-cta btn-sm">+ लोड पोस्ट करें</a>
<% } %>
</div>
<!-- Filters -->
<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>
<label class="form-label">कहाँ से</label>
<input type="text" name="origin" class="form-input" placeholder="शहर" value="<%= filters.origin || '' %>" style="padding:8px 12px">
</div>
<div>
<label class="form-label">कहाँ तक</label>
<input type="text" name="destination" class="form-input" placeholder="शहर" value="<%= filters.destination || '' %>" style="padding:8px 12px">
</div>
<div>
<label class="form-label">ट्रक प्रकार</label>
<select name="truck_type" class="form-input form-select" style="padding:8px 12px">
<option value="all">सभी</option>
<% truckTypes.forEach(t => { %>
<option value="<%= t %>" <%= filters.truck_type === t ? 'selected' : '' %>><%= t %></option>
<% }) %>
</select>
</div>
<button type="submit" class="btn btn-primary btn-sm">खोजें</button>
</div>
</form>
<!-- Load List -->
<% if (loads.length === 0) { %>
<div class="card text-center" style="padding:var(--space-2xl)">
<p style="font-size:1.2rem">📭</p>
<p style="color:var(--gray-700)">कोई लोड उपलब्ध नहीं</p>
</div>
<% } else { %>
<div style="display:grid;gap:var(--space-md)">
<% loads.forEach(load => { %>
<a href="/loadboard/<%= load.id %>" class="card card-accent" style="text-decoration:none;color:inherit;<%= load.is_urgent ? 'border-left-color:var(--saffron)' : '' %>">
<div style="display:flex;justify-content:space-between;align-items:start">
<div>
<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">
🚛 <%= load.weight_tons %> टन | <%= load.truck_type %>
<% if (load.material_type) { %> | <%= load.material_type %><% } %>
</div>
<div style="font-size:0.8rem;color:var(--gray-700);margin-top:2px">
📅 <%= new Date(load.pickup_date).toLocaleDateString('hi-IN') %>
<% if (load.bid_count > 0) { %> | 🏷️ <%= load.bid_count %> बोली<% } %>
</div>
</div>
<div style="text-align:right">
<% if (load.budget) { %>
<div style="font-size:1rem;font-weight:700;color:var(--navy)">₹<%= Number(load.budget).toLocaleString('en-IN') %></div>
<% } %>
<% if (load.is_urgent) { %>
<span class="badge badge-booked">अर्जेंट</span>
<% } %>
</div>
</div>
</a>
<% }) %>
</div>
<% } %>
</div>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,34 @@
<% var title = 'लॉगिन'; %>
<%- 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">
<div class="container" style="max-width:400px">
<div class="card">
<h2 class="text-center" style="margin-bottom:4px">लॉगिन | Login</h2>
<p class="text-center" style="color:var(--gray-700);font-size:0.8rem;margin-bottom:var(--space-lg)">अपना यूज़रनेम और पासवर्ड दर्ज करें</p>
<% if (error) { %>
<div class="alert-error"><%= error %></div>
<% } %>
<form method="POST" action="/login">
<div class="form-group">
<label class="form-label">यूज़रनेम (ड्राइवर: गाड़ी नंबर)</label>
<input type="text" name="username" class="form-input" placeholder="MH31AB1234 या अपना यूज़रनेम" required autofocus>
</div>
<div class="form-group">
<label class="form-label">पासवर्ड</label>
<input type="password" name="password" class="form-input" placeholder="••••" required>
</div>
<button type="submit" class="btn btn-primary btn-block btn-lg">लॉगिन करें →</button>
</form>
<p class="text-center" style="margin-top:var(--space-lg);font-size:0.8rem">
नया खाता? <a href="/register">पंजीकरण करें</a>
</p>
</div>
</div>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,34 @@
<% var title = 'संदेश'; %>
<%- 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)">💬 संदेश</h2>
<% if (conversations.length === 0) { %>
<div class="card text-center" style="padding:var(--space-2xl)">
<p>कोई संदेश नहीं</p>
</div>
<% } 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>
<strong style="font-size:0.9rem"><%= c.user ? c.user.name : 'User' %></strong>
<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>
</a>
<% }) %>
</div>
<% } %>
</div>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,75 @@
<% var title = 'लोड पोस्ट करें'; %>
<%- 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">
<h2 style="font-size:1.2rem;margin-bottom:var(--space-md)">📦 नया लोड पोस्ट करें</h2>
<% if (error) { %>
<div class="alert-error"><%= error %></div>
<% } %>
<form method="POST" action="/loadboard/post">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
<div class="form-group">
<label class="form-label">कहाँ से / Origin *</label>
<input type="text" name="origin_city" class="form-input" placeholder="मुंबई" required>
</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 class="form-group">
<label class="form-label">वज़न (टन) *</label>
<input type="number" name="weight_tons" class="form-input" placeholder="20" step="0.5" required>
</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 style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
<div class="form-group">
<label class="form-label">माल का प्रकार</label>
<input type="text" name="material_type" class="form-input" placeholder="सीमेंट, स्टील...">
</div>
<div class="form-group">
<label class="form-label">बजट (₹)</label>
<input type="number" name="budget" class="form-input" placeholder="45000">
</div>
</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>
</div>
</div>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,51 @@
<% var title = 'मेरी प्रोफ़ाइल'; %>
<%- 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)">👤 मेरी प्रोफ़ाइल</h2>
<% 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 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>
<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>
<form method="POST" action="/profile">
<div class="form-group">
<label class="form-label">नाम / Name</label>
<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 class="form-group">
<label class="form-label">शहर</label>
<input type="text" name="city" class="form-input" value="<%= profile.city || '' %>">
</div>
<div class="form-group">
<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>
<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>
</div>
</div>
</div>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,93 @@
<% var title = 'पंजीकरण'; %>
<%- 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">
<div class="container" style="max-width:440px">
<div class="card">
<h2 class="text-center" style="margin-bottom:4px">पंजीकरण | Register</h2>
<p class="text-center" style="color:var(--gray-700);font-size:0.8rem;margin-bottom:var(--space-lg)">मुफ्त खाता बनाएं</p>
<% if (error) { %>
<div class="alert-error"><%= error %></div>
<% } %>
<form method="POST" action="/register" id="registerForm">
<div class="form-group">
<label class="form-label">आप कौन हैं? / Your Role *</label>
<div class="role-select-grid">
<label class="role-option">
<input type="radio" name="role" value="driver" <%= role === 'driver' ? 'checked' : '' %> required>
<div class="role-option-card"><span class="role-icon">🚛</span><span>ड्राइवर</span></div>
</label>
<label class="role-option">
<input type="radio" name="role" value="shipper" <%= role === 'shipper' ? 'checked' : '' %>>
<div class="role-option-card"><span class="role-icon">📦</span><span>शिपर</span></div>
</label>
<label class="role-option">
<input type="radio" name="role" value="broker" <%= role === 'broker' ? 'checked' : '' %>>
<div class="role-option-card"><span class="role-icon">🤝</span><span>ब्रोकर</span></div>
</label>
</div>
</div>
<div class="form-group">
<label class="form-label">पूरा नाम / Full Name *</label>
<input type="text" name="name" class="form-input" placeholder="अपना नाम" required>
</div>
<div class="form-group">
<label class="form-label" id="usernameLabel">यूज़रनेम *</label>
<input type="text" name="username" class="form-input" id="usernameInput" placeholder="यूज़रनेम चुनें" required>
<small id="usernameHint" style="color:var(--gray-700);font-size:0.7rem"></small>
</div>
<div class="form-group">
<label class="form-label">फोन नंबर (वैकल्पिक)</label>
<input type="tel" name="phone" class="form-input" placeholder="9876543210" maxlength="10">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-md)">
<div class="form-group">
<label class="form-label">पासवर्ड *</label>
<input type="password" name="password" class="form-input" placeholder="••••" required minlength="4">
</div>
<div class="form-group">
<label class="form-label">पासवर्ड पुष्टि *</label>
<input type="password" name="password_confirm" class="form-input" placeholder="••••" required minlength="4">
</div>
</div>
<button type="submit" class="btn btn-cta btn-block btn-lg">मुफ्त पंजीकरण करें →</button>
</form>
<p class="text-center" style="margin-top:var(--space-lg);font-size:0.8rem">
पहले से खाता है? <a href="/login">लॉगिन करें</a>
</p>
</div>
</div>
</section>
<script>
document.querySelectorAll('input[name="role"]').forEach(r => {
r.addEventListener('change', function() {
const label = document.getElementById('usernameLabel');
const input = document.getElementById('usernameInput');
const hint = document.getElementById('usernameHint');
if (this.value === 'driver') {
label.textContent = 'गाड़ी नंबर / Vehicle Number *';
input.placeholder = 'MH31AB1234';
hint.textContent = 'आपका गाड़ी नंबर ही आपका यूज़रनेम होगा';
} else {
label.textContent = 'यूज़रनेम *';
input.placeholder = 'अपना यूज़रनेम चुनें';
hint.textContent = '';
}
});
});
// Trigger on load if role pre-selected
const checked = document.querySelector('input[name="role"]:checked');
if (checked) checked.dispatchEvent(new Event('change'));
</script>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,37 @@
<% var title = 'शिपर डैशबोर्ड'; %>
<%- 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)">📦 नमस्ते, <%= user.name %>!</h2>
<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.openLoads %></div><div class="stat-label">खुले लोड</div></div>
<div class="stat-card"><div class="stat-value"><%= stats.activeTrips %></div><div class="stat-label">सक्रिय शिपमेंट</div></div>
</div>
<% if (recentLoads.length > 0) { %>
<h3 style="font-size:1rem;margin-top:var(--space-lg);margin-bottom:var(--space-sm)">📋 हाल के लोड</h3>
<% 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)">
<div style="display:flex;justify-content:space-between;align-items:center">
<div>
<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>
<span class="badge badge-<%= load.status === 'open' ? 'open' : 'booked' %>"><%= load.status %></span>
</div>
</a>
<% }) %>
<% } %>
<div style="margin-top:var(--space-lg);display:grid;gap:var(--space-sm)">
<a href="/loadboard/post" class="btn btn-cta btn-block">+ नया लोड पोस्ट करें</a>
<a href="/trips" class="btn btn-outline btn-block">🚚 मेरी शिपमेंट</a>
</div>
</div>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,46 @@
<% var title = 'मेरी ट्रिप'; %>
<%- 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)">🚚 मेरी ट्रिप</h2>
<% if (trips.length === 0) { %>
<div class="card text-center" style="padding:var(--space-2xl)">
<p>कोई ट्रिप नहीं</p>
</div>
<% } else { %>
<div style="display:grid;gap:var(--space-md)">
<% trips.forEach(trip => { %>
<div class="card card-accent">
<div style="display:flex;justify-content:space-between;align-items:start">
<div>
<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">
₹<%= Number(trip.amount).toLocaleString('en-IN') %>
<% if (trip.load) { %> | <%= trip.load.weight_tons %> टन | <%= trip.load.truck_type %><% } %>
</div>
</div>
<span class="badge badge-<%= trip.status === 'delivered' ? 'delivered' : trip.status === 'cancelled' ? 'cancelled' : 'transit' %>"><%= trip.status %></span>
</div>
<% if (user.role === 'driver' && trip.status !== 'delivered' && trip.status !== 'cancelled') { %>
<div style="margin-top:var(--space-md);display:flex;gap:var(--space-sm)">
<% 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>
<% } 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>
<% } 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>
<% } %>
</div>
<% } %>
</div>
<% }) %>
</div>
<% } %>
</div>
</section>
<%- include('../partials/footer') %>

View file

@ -0,0 +1,13 @@
<% if (user) { %>
<nav class="bottom-nav">
<a href="/<%= user.role %>" class="bnav-item"><span class="bnav-icon">🏠</span><span>होम</span></a>
<a href="/loadboard" class="bnav-item"><span class="bnav-icon">📋</span><span>लोड</span></a>
<% if (user.role === 'shipper' || user.role === 'broker') { %>
<a href="/loadboard/post" class="bnav-item bnav-add"><span class="bnav-icon"></span><span>पोस्ट</span></a>
<% } else { %>
<a href="/trips" class="bnav-item"><span class="bnav-icon">🚚</span><span>ट्रिप</span></a>
<% } %>
<a href="/messages" class="bnav-item"><span class="bnav-icon">💬</span><span>संदेश</span></a>
<a href="/profile" class="bnav-item"><span class="bnav-icon">👤</span><span>प्रोफ़ाइल</span></a>
</nav>
<% } %>

View file

@ -0,0 +1,19 @@
<%- include('./bottom-nav') %>
<footer class="govt-footer">
<div class="footer-inner">
<div class="footer-brand">
<strong>भारत ट्रक्स | BharathTrucks</strong>
<p>राष्ट्रीय माल परिवहन मंच</p>
</div>
<div class="footer-links">
<a href="/about">हमारे बारे में</a>
<a href="/contact">संपर्क करें</a>
<a href="/privacy">गोपनीयता नीति</a>
<a href="/terms">नियम एवं शर्तें</a>
</div>
<div class="footer-contact">
<p>सहायता: support@bharathtrucks.com</p>
</div>
<p class="footer-copy">© 2026 BharathTrucks. सर्वाधिकार सुरक्षित।</p>
</div>
</footer>

View file

@ -0,0 +1,20 @@
<header class="govt-header">
<div class="header-inner">
<div class="header-brand">
<div class="header-emblem">🏛️</div>
<div class="header-titles">
<h1 class="header-title-hi">भारत ट्रक्स</h1>
<p class="header-subtitle">राष्ट्रीय माल परिवहन मंच | National Freight Transport Platform</p>
</div>
</div>
<nav class="header-nav">
<% if (user) { %>
<span class="header-user"><%= user.name || user.username %></span>
<a href="/auth/logout" class="header-link">लॉगआउट</a>
<% } else { %>
<a href="/login" class="header-link">लॉगिन</a>
<a href="/register" class="btn-header-cta">पंजीकरण</a>
<% } %>
</nav>
</div>
</header>

View file

@ -0,0 +1,122 @@
-- ============================================================
-- BharathTrucks — FULL DATABASE SETUP
-- Run this ONCE in Supabase SQL Editor
-- ============================================================
-- 1. USERS
CREATE TABLE IF NOT EXISTS app_users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('driver', 'shipper', 'broker', 'admin')),
phone TEXT,
city TEXT,
state TEXT,
is_verified BOOLEAN DEFAULT FALSE,
is_premium BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_app_users_role ON app_users(role);
CREATE INDEX IF NOT EXISTS idx_app_users_username ON app_users(username);
-- 2. LOADS
CREATE TABLE IF NOT EXISTS loads (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
posted_by UUID NOT NULL REFERENCES app_users(id) ON DELETE CASCADE,
origin_city TEXT NOT NULL,
destination_city TEXT NOT NULL,
weight_tons NUMERIC(5,1) NOT NULL,
truck_type TEXT NOT NULL,
material_type TEXT,
budget NUMERIC(10,2),
pickup_date DATE NOT NULL,
description TEXT,
is_urgent BOOLEAN DEFAULT FALSE,
status TEXT DEFAULT 'open' CHECK (status IN ('open', 'booked', 'in_transit', 'delivered', 'cancelled')),
bid_count INTEGER DEFAULT 0,
accepted_bid_id UUID,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_loads_status ON loads(status);
CREATE INDEX IF NOT EXISTS idx_loads_origin ON loads(origin_city);
CREATE INDEX IF NOT EXISTS idx_loads_destination ON loads(destination_city);
CREATE INDEX IF NOT EXISTS idx_loads_posted_by ON loads(posted_by);
-- 3. BIDS
CREATE TABLE IF NOT EXISTS bids (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
load_id UUID NOT NULL REFERENCES loads(id) ON DELETE CASCADE,
driver_id UUID NOT NULL REFERENCES app_users(id) ON DELETE CASCADE,
amount NUMERIC(10,2) NOT NULL,
note TEXT,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'rejected', 'withdrawn')),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(load_id, driver_id)
);
CREATE INDEX IF NOT EXISTS idx_bids_load ON bids(load_id);
CREATE INDEX IF NOT EXISTS idx_bids_driver ON bids(driver_id);
-- 4. TRIPS
CREATE TABLE IF NOT EXISTS trips (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
load_id UUID NOT NULL REFERENCES loads(id),
driver_id UUID NOT NULL REFERENCES app_users(id),
shipper_id UUID NOT NULL REFERENCES app_users(id),
bid_id UUID NOT NULL REFERENCES bids(id),
amount NUMERIC(10,2) NOT NULL,
status TEXT DEFAULT 'confirmed' CHECK (status IN ('confirmed', 'picked_up', 'in_transit', 'delivered', 'cancelled')),
picked_up_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_trips_driver ON trips(driver_id);
CREATE INDEX IF NOT EXISTS idx_trips_shipper ON trips(shipper_id);
-- 5. MESSAGES
CREATE TABLE IF NOT EXISTS messages (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
sender_id UUID NOT NULL REFERENCES app_users(id),
receiver_id UUID NOT NULL REFERENCES app_users(id),
load_id UUID REFERENCES loads(id),
content TEXT NOT NULL,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_messages_receiver ON messages(receiver_id, is_read);
-- 6. TRIGGERS
CREATE OR REPLACE FUNCTION increment_bid_count()
RETURNS TRIGGER AS $$ BEGIN UPDATE loads SET bid_count = bid_count + 1 WHERE id = NEW.load_id; RETURN NEW; END; $$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS bid_count_trigger ON bids;
CREATE TRIGGER bid_count_trigger AFTER INSERT ON bids FOR EACH ROW EXECUTE FUNCTION increment_bid_count();
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS loads_updated_at ON loads;
CREATE TRIGGER loads_updated_at BEFORE UPDATE ON loads FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- 7. RLS (open for now — tighten later)
ALTER TABLE app_users ENABLE ROW LEVEL SECURITY;
ALTER TABLE loads ENABLE ROW LEVEL SECURITY;
ALTER TABLE bids ENABLE ROW LEVEL SECURITY;
ALTER TABLE trips ENABLE ROW LEVEL SECURITY;
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "open" ON app_users FOR ALL USING (true) WITH CHECK (true);
CREATE POLICY "open" ON loads FOR ALL USING (true) WITH CHECK (true);
CREATE POLICY "open" ON bids FOR ALL USING (true) WITH CHECK (true);
CREATE POLICY "open" ON trips FOR ALL USING (true) WITH CHECK (true);
CREATE POLICY "open" ON messages FOR ALL USING (true) WITH CHECK (true);
-- 8. SEED ADMIN USER (password: admin123)
INSERT INTO app_users (username, name, password_hash, role) VALUES
('admin', 'Admin', '$2a$10$8KzaNdKIMyOkASCBFOmKS.VbhOLar0sFAFJcXwauRfMRE8.xOP6Hy', 'admin')
ON CONFLICT (username) DO NOTHING;
-- ============================================================
-- DONE! Your database is ready.
-- Admin login: username=admin, password=admin123
-- ============================================================

View file

@ -0,0 +1,72 @@
-- ============================================================
-- BharathTrucks — Loads & Bids Migration
-- Run in Supabase SQL Editor AFTER app_users migration
-- ============================================================
CREATE TABLE IF NOT EXISTS loads (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
posted_by UUID NOT NULL REFERENCES app_users(id) ON DELETE CASCADE,
origin_city TEXT NOT NULL,
destination_city TEXT NOT NULL,
weight_tons NUMERIC(5,1) NOT NULL,
truck_type TEXT NOT NULL,
material_type TEXT,
budget NUMERIC(10,2),
pickup_date DATE NOT NULL,
description TEXT,
is_urgent BOOLEAN DEFAULT FALSE,
status TEXT DEFAULT 'open' CHECK (status IN ('open', 'booked', 'in_transit', 'delivered', 'cancelled')),
bid_count INTEGER DEFAULT 0,
accepted_bid_id UUID,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_loads_status ON loads(status);
CREATE INDEX IF NOT EXISTS idx_loads_origin ON loads(origin_city);
CREATE INDEX IF NOT EXISTS idx_loads_destination ON loads(destination_city);
CREATE INDEX IF NOT EXISTS idx_loads_posted_by ON loads(posted_by);
CREATE INDEX IF NOT EXISTS idx_loads_pickup_date ON loads(pickup_date);
CREATE TABLE IF NOT EXISTS bids (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
load_id UUID NOT NULL REFERENCES loads(id) ON DELETE CASCADE,
driver_id UUID NOT NULL REFERENCES app_users(id) ON DELETE CASCADE,
amount NUMERIC(10,2) NOT NULL,
note TEXT,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'rejected', 'withdrawn')),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(load_id, driver_id)
);
CREATE INDEX IF NOT EXISTS idx_bids_load ON bids(load_id);
CREATE INDEX IF NOT EXISTS idx_bids_driver ON bids(driver_id);
-- Auto increment bid_count
CREATE OR REPLACE FUNCTION increment_bid_count()
RETURNS TRIGGER AS $$
BEGIN
UPDATE loads SET bid_count = bid_count + 1 WHERE id = NEW.load_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS bid_count_trigger ON bids;
CREATE TRIGGER bid_count_trigger AFTER INSERT ON bids
FOR EACH ROW EXECUTE FUNCTION increment_bid_count();
-- updated_at trigger
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN NEW.updated_at = NOW(); RETURN NEW; END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS loads_updated_at ON loads;
CREATE TRIGGER loads_updated_at BEFORE UPDATE ON loads
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- RLS
ALTER TABLE loads ENABLE ROW LEVEL SECURITY;
ALTER TABLE bids ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow all on loads" ON loads FOR ALL USING (true) WITH CHECK (true);
CREATE POLICY "Allow all on bids" ON bids FOR ALL USING (true) WITH CHECK (true);

View file

@ -0,0 +1,19 @@
-- ============================================================
-- BharathTrucks — Messages Migration
-- ============================================================
CREATE TABLE IF NOT EXISTS messages (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
sender_id UUID NOT NULL REFERENCES app_users(id),
receiver_id UUID NOT NULL REFERENCES app_users(id),
load_id UUID REFERENCES loads(id),
content TEXT NOT NULL,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_messages_receiver ON messages(receiver_id, is_read);
CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender_id);
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow all on messages" ON messages FOR ALL USING (true) WITH CHECK (true);

View file

@ -0,0 +1,30 @@
-- ============================================================
-- BharathTrucks — App Users Migration (Username + Password)
-- Run in Supabase SQL Editor
-- ============================================================
CREATE TABLE IF NOT EXISTS app_users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('driver', 'shipper', 'broker', 'admin')),
phone TEXT,
city TEXT,
state TEXT,
is_verified BOOLEAN DEFAULT FALSE,
is_premium BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_app_users_role ON app_users(role);
CREATE INDEX IF NOT EXISTS idx_app_users_username ON app_users(username);
-- RLS
ALTER TABLE app_users ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow all on app_users" ON app_users FOR ALL USING (true) WITH CHECK (true);
-- ============================================================
-- Done! No Supabase Auth needed — app manages its own users.
-- ============================================================

View file

@ -0,0 +1,24 @@
-- ============================================================
-- BharathTrucks — Trips Migration
-- Run AFTER loads migration
-- ============================================================
CREATE TABLE IF NOT EXISTS trips (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
load_id UUID NOT NULL REFERENCES loads(id),
driver_id UUID NOT NULL REFERENCES app_users(id),
shipper_id UUID NOT NULL REFERENCES app_users(id),
bid_id UUID NOT NULL REFERENCES bids(id),
amount NUMERIC(10,2) NOT NULL,
status TEXT DEFAULT 'confirmed' CHECK (status IN ('confirmed', 'picked_up', 'in_transit', 'delivered', 'cancelled')),
picked_up_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_trips_driver ON trips(driver_id);
CREATE INDEX IF NOT EXISTS idx_trips_shipper ON trips(shipper_id);
CREATE INDEX IF NOT EXISTS idx_trips_status ON trips(status);
ALTER TABLE trips ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow all on trips" ON trips FOR ALL USING (true) WITH CHECK (true);