mirror of
http://forgejo-oa09toasww4dgii9cj3gpzda.187.127.164.61.sslip.io/iamcoolvivek007/bharath.git
synced 2026-06-11 00:06:51 +00:00
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:
commit
394117dd74
60 changed files with 6276 additions and 0 deletions
88
README.md
Normal file
88
README.md
Normal 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
12
docker/docker-compose.yml
Normal 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
284
docs/api/API_DESIGN.md
Normal 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.*
|
||||
365
docs/architecture/DATABASE_SCHEMA.md
Normal file
365
docs/architecture/DATABASE_SCHEMA.md
Normal 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.*
|
||||
286
docs/architecture/DEPLOYMENT.md
Normal file
286
docs/architecture/DEPLOYMENT.md
Normal 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.*
|
||||
279
docs/architecture/TECHNICAL_ARCHITECTURE.md
Normal file
279
docs/architecture/TECHNICAL_ARCHITECTURE.md
Normal 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 |
|
||||
|-------|---------------|
|
||||
| 0–1000 | Single Hostinger VPS (4GB RAM), Supabase free tier |
|
||||
| 1000–5000 | Upgrade VPS (8GB), Supabase Pro |
|
||||
| 5000–20000 | 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
251
docs/bmad/MONETIZATION.md
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
# BharathTrucks — SaaS & Monetization Strategy
|
||||
|
||||
**Version:** 1.0
|
||||
**Date:** 2026-05-31
|
||||
|
||||
---
|
||||
|
||||
## 1. Revenue Model Overview
|
||||
|
||||
### Phase 1: Growth (0–1000 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
239
docs/bmad/PRD.md
Normal 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 (0–1000 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% (320px–1440px) |
|
||||
| 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
245
docs/bmad/SPRINT_PLAN.md
Normal 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
208
docs/bmad/USER_PERSONAS.md
Normal 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,000–40,000/month |
|
||||
| Tech Comfort | WhatsApp, YouTube, basic apps |
|
||||
|
||||
### Context
|
||||
- Owns one truck (Tata 407), still paying EMI
|
||||
- Drives 15–20 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,000–1,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
157
docs/bmad/USER_STORIES.md
Normal 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.*
|
||||
284
docs/design/DESIGN_SYSTEM.md
Normal file
284
docs/design/DESIGN_SYSTEM.md
Normal 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
5
webapp/.dockerignore
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
npm-debug.log
|
||||
.env
|
||||
.git
|
||||
.gitignore
|
||||
15
webapp/.env.example
Normal file
15
webapp/.env.example
Normal 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
2
webapp/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
.env
|
||||
16
webapp/Dockerfile
Normal file
16
webapp/Dockerfile
Normal 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
1317
webapp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
webapp/package.json
Normal file
26
webapp/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
11
webapp/src/config/constants.js
Normal file
11
webapp/src/config/constants.js
Normal 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
16
webapp/src/config/env.js
Normal 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,
|
||||
},
|
||||
};
|
||||
24
webapp/src/middleware/auth.js
Normal file
24
webapp/src/middleware/auth.js
Normal 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 };
|
||||
359
webapp/src/public/css/govt-theme.css
Normal file
359
webapp/src/public/css/govt-theme.css
Normal 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; } }
|
||||
4
webapp/src/public/images/favicon.svg
Normal file
4
webapp/src/public/images/favicon.svg
Normal 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 |
4
webapp/src/public/js/app.js
Normal file
4
webapp/src/public/js/app.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// BharathTrucks — Client-side JS
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
||||
}
|
||||
14
webapp/src/public/manifest.json
Normal file
14
webapp/src/public/manifest.json
Normal 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
32
webapp/src/public/sw.js
Normal 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))
|
||||
);
|
||||
});
|
||||
50
webapp/src/routes/admin.js
Normal file
50
webapp/src/routes/admin.js
Normal 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
102
webapp/src/routes/auth.js
Normal 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
120
webapp/src/routes/loads.js
Normal 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;
|
||||
62
webapp/src/routes/messages.js
Normal file
62
webapp/src/routes/messages.js
Normal 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;
|
||||
39
webapp/src/routes/trips.js
Normal file
39
webapp/src/routes/trips.js
Normal 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
138
webapp/src/server.js
Normal 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}`);
|
||||
});
|
||||
16
webapp/src/services/supabase.js
Normal file
16
webapp/src/services/supabase.js
Normal 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;
|
||||
34
webapp/src/views/layouts/main.ejs
Normal file
34
webapp/src/views/layouts/main.ejs
Normal 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>
|
||||
9
webapp/src/views/pages/404.ejs
Normal file
9
webapp/src/views/pages/404.ejs
Normal 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') %>
|
||||
9
webapp/src/views/pages/500.ejs
Normal file
9
webapp/src/views/pages/500.ejs
Normal 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') %>
|
||||
46
webapp/src/views/pages/admin-dashboard.ejs
Normal file
46
webapp/src/views/pages/admin-dashboard.ejs
Normal 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') %>
|
||||
30
webapp/src/views/pages/admin-loads.ejs
Normal file
30
webapp/src/views/pages/admin-loads.ejs
Normal 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') %>
|
||||
44
webapp/src/views/pages/admin-users.ejs
Normal file
44
webapp/src/views/pages/admin-users.ejs
Normal 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') %>
|
||||
37
webapp/src/views/pages/broker-dashboard.ejs
Normal file
37
webapp/src/views/pages/broker-dashboard.ejs
Normal 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') %>
|
||||
29
webapp/src/views/pages/chat.ejs
Normal file
29
webapp/src/views/pages/chat.ejs
Normal 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') %>
|
||||
37
webapp/src/views/pages/driver-dashboard.ejs
Normal file
37
webapp/src/views/pages/driver-dashboard.ejs
Normal 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') %>
|
||||
77
webapp/src/views/pages/landing.ejs
Normal file
77
webapp/src/views/pages/landing.ejs
Normal 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') %>
|
||||
89
webapp/src/views/pages/load-detail.ejs
Normal file
89
webapp/src/views/pages/load-detail.ejs
Normal 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') %>
|
||||
76
webapp/src/views/pages/loadboard.ejs
Normal file
76
webapp/src/views/pages/loadboard.ejs
Normal 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') %>
|
||||
34
webapp/src/views/pages/login.ejs
Normal file
34
webapp/src/views/pages/login.ejs
Normal 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') %>
|
||||
34
webapp/src/views/pages/messages.ejs
Normal file
34
webapp/src/views/pages/messages.ejs
Normal 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') %>
|
||||
75
webapp/src/views/pages/post-load.ejs
Normal file
75
webapp/src/views/pages/post-load.ejs
Normal 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') %>
|
||||
51
webapp/src/views/pages/profile.ejs
Normal file
51
webapp/src/views/pages/profile.ejs
Normal 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') %>
|
||||
93
webapp/src/views/pages/register.ejs
Normal file
93
webapp/src/views/pages/register.ejs
Normal 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') %>
|
||||
37
webapp/src/views/pages/shipper-dashboard.ejs
Normal file
37
webapp/src/views/pages/shipper-dashboard.ejs
Normal 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') %>
|
||||
46
webapp/src/views/pages/trips.ejs
Normal file
46
webapp/src/views/pages/trips.ejs
Normal 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') %>
|
||||
13
webapp/src/views/partials/bottom-nav.ejs
Normal file
13
webapp/src/views/partials/bottom-nav.ejs
Normal 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>
|
||||
<% } %>
|
||||
19
webapp/src/views/partials/footer.ejs
Normal file
19
webapp/src/views/partials/footer.ejs
Normal 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>
|
||||
20
webapp/src/views/partials/header.ejs
Normal file
20
webapp/src/views/partials/header.ejs
Normal 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>
|
||||
122
webapp/supabase-FULL-migration.sql
Normal file
122
webapp/supabase-FULL-migration.sql
Normal 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
|
||||
-- ============================================================
|
||||
72
webapp/supabase-loads-migration.sql
Normal file
72
webapp/supabase-loads-migration.sql
Normal 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);
|
||||
19
webapp/supabase-messages-migration.sql
Normal file
19
webapp/supabase-messages-migration.sql
Normal 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);
|
||||
30
webapp/supabase-migration.sql
Normal file
30
webapp/supabase-migration.sql
Normal 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.
|
||||
-- ============================================================
|
||||
24
webapp/supabase-trips-migration.sql
Normal file
24
webapp/supabase-trips-migration.sql
Normal 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);
|
||||
Loading…
Reference in a new issue