commit 1a4eaaa0404769896ed1b597ee828011d824f3a1 Author: FreightDesk Date: Sun Jun 7 18:57:24 2026 +0000 Initial commit: FreightDesk v1.0 - Express + EJS server-rendered app - Supabase PostgreSQL database - Auth: username/password with bcrypt - Dashboard with business stats - Load CRUD with filters - WhatsApp message parser - Payment tracking - Shipper & vehicle management - Reports (monthly, top shippers, routes) - Government-app aesthetic (tricolor theme) - Dark mode support - Docker + Coolify deployment ready - Seed data from existing business ledger (88 loads, 41 shippers, 70 vehicles) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..98c7c5a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + freightdesk: + build: + context: ./webapp + dockerfile: Dockerfile + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - PORT=3000 + - APP_URL=http://localhost:3000 + - SUPABASE_URL=${SUPABASE_URL} + - SUPABASE_KEY=${SUPABASE_KEY} + - SUPABASE_SERVICE_KEY=${SUPABASE_SERVICE_KEY} + - SESSION_SECRET=${SESSION_SECRET} + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"] + interval: 30s + timeout: 5s + retries: 3 diff --git a/supabase/migrations/001_initial_schema.sql b/supabase/migrations/001_initial_schema.sql new file mode 100644 index 0000000..4e50503 --- /dev/null +++ b/supabase/migrations/001_initial_schema.sql @@ -0,0 +1,175 @@ +-- ============================================================ +-- FreightDesk — Supabase Migration +-- Initial schema for freight forwarding commission agent +-- ============================================================ + +-- Enable UUID extension +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- ============================================================ +-- SHIPPERS +-- ============================================================ +CREATE TABLE shippers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + phone TEXT, + email TEXT, + city TEXT DEFAULT 'Thiruvananthapuram', + state TEXT DEFAULT 'Kerala', + gst TEXT, + total_freight NUMERIC(12,2) DEFAULT 0, + total_commission NUMERIC(12,2) DEFAULT 0, + pending_amount NUMERIC(12,2) DEFAULT 0, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- VEHICLES +-- ============================================================ +CREATE TABLE vehicles ( + id TEXT PRIMARY KEY, + number TEXT NOT NULL UNIQUE, + type TEXT DEFAULT 'open', + capacity_ton NUMERIC(6,2), + city TEXT DEFAULT 'Thiruvananthapuram', + state TEXT DEFAULT 'Kerala', + owner_name TEXT, + owner_phone TEXT, + is_active BOOLEAN DEFAULT true, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- LOADS (core table) +-- ============================================================ +CREATE TABLE loads ( + id TEXT PRIMARY KEY, + date DATE, + vehicle_id TEXT REFERENCES vehicles(id) ON DELETE SET NULL, + from_city TEXT, + via TEXT, + to_city TEXT, + shipper_id TEXT REFERENCES shippers(id) ON DELETE SET NULL, + load_type TEXT, + item TEXT, + freight_charged NUMERIC(12,2), + advance_received NUMERIC(12,2), + paid_to_driver NUMERIC(12,2), + commission NUMERIC(12,2), + driver_freight NUMERIC(12,2), + pending_from_shipper NUMERIC(12,2), + pending_to_driver NUMERIC(12,2), + status TEXT DEFAULT 'partial', + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- PAYMENTS (individual payment transactions) +-- ============================================================ +CREATE TABLE payments ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + load_id TEXT REFERENCES loads(id) ON DELETE CASCADE, + type TEXT NOT NULL CHECK (type IN ('advance', 'balance', 'commission', 'driver_payment', 'other')), + direction TEXT NOT NULL CHECK (direction IN ('in', 'out')), + amount NUMERIC(12,2) NOT NULL, + method TEXT DEFAULT 'bank_transfer', + payment_date DATE, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- PORTAL USERS (for shipper/driver portal access) +-- ============================================================ +CREATE TABLE portal_users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + role TEXT NOT NULL CHECK (role IN ('shipper', 'driver', 'admin')), + entity_id TEXT, -- links to shippers.id or vehicles.id + is_active BOOLEAN DEFAULT true, + last_login TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- COMMISSION INVOICES +-- ============================================================ +CREATE TABLE commission_invoices ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + invoice_no TEXT NOT NULL UNIQUE, + shipper_id TEXT REFERENCES shippers(id) ON DELETE SET NULL, + period_from DATE, + period_to DATE, + total_commission NUMERIC(12,2) NOT NULL DEFAULT 0, + status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'sent', 'paid', 'overdue')), + due_date DATE, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- APP SETTINGS +-- ============================================================ +CREATE TABLE app_settings ( + key TEXT PRIMARY KEY, + value TEXT, + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- INDEXES +-- ============================================================ +CREATE INDEX idx_loads_date ON loads(date); +CREATE INDEX idx_loads_status ON loads(status); +CREATE INDEX idx_loads_shipper ON loads(shipper_id); +CREATE INDEX idx_loads_vehicle ON loads(vehicle_id); +CREATE INDEX idx_loads_from_to ON loads(from_city, to_city); +CREATE INDEX idx_payments_load ON payments(load_id); +CREATE INDEX idx_payments_date ON payments(payment_date); +CREATE INDEX idx_vehicles_number ON vehicles(number); +CREATE INDEX idx_shippers_name ON shippers(name); + +-- ============================================================ +-- UPDATED_AT TRIGGER +-- ============================================================ +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER shippers_updated_at BEFORE UPDATE ON shippers + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER vehicles_updated_at BEFORE UPDATE ON vehicles + 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(); + +-- ============================================================ +-- DEFAULT ADMIN USER (password: admin123) +-- bcrypt hash for 'admin123' with 10 rounds +-- ============================================================ +INSERT INTO portal_users (username, password_hash, role, is_active) +VALUES ('admin', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy', 'admin', true); + +-- ============================================================ +-- DEFAULT SETTINGS +-- ============================================================ +INSERT INTO app_settings (key, value) VALUES + ('app_name', 'FreightDesk'), + ('app_name_hi', 'फ्रेटडेस्क'), + ('currency', 'INR'), + ('invoice_prefix', 'FD'), + ('default_city', 'Thiruvananthapuram'), + ('owner_name', ''), + ('owner_phone', ''), + ('owner_upi', ''); diff --git a/supabase/migrations/002_seed_data.sql b/supabase/migrations/002_seed_data.sql new file mode 100644 index 0000000..1c150da --- /dev/null +++ b/supabase/migrations/002_seed_data.sql @@ -0,0 +1,19 @@ +-- ============================================================ +-- FreightDesk — Seed Data Migration +-- Imports existing business data from JSON ledger +-- ============================================================ + +-- Note: Run this after 001_initial_schema.sql +-- This inserts the seed data from seed_data.json + +-- The actual seed data will be loaded via the seed.js script +-- which reads supabase/seed_data.json and inserts via Supabase client + +-- This file is a placeholder for any SQL-level seed operations +-- For example, updating computed fields after data load: + +-- Update shipper totals after loads are inserted +-- UPDATE shippers SET +-- total_freight = (SELECT COALESCE(SUM(freight_charged), 0) FROM loads WHERE shipper_id = shippers.id), +-- total_commission = (SELECT COALESCE(SUM(commission), 0) FROM loads WHERE shipper_id = shippers.id), +-- pending_amount = (SELECT COALESCE(SUM(pending_from_shipper), 0) FROM loads WHERE shipper_id = shippers.id); diff --git a/supabase/seed_data.json b/supabase/seed_data.json new file mode 100644 index 0000000..9122bb3 --- /dev/null +++ b/supabase/seed_data.json @@ -0,0 +1,2779 @@ +{ + "shippers": [ + { + "id": "agarwal_packers_and_movers", + "name": "Agarwal Packers and Movers", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 196500.0, + "total_commission": 8950.0, + "pending_amount": 13700.0, + "is_active": true + }, + { + "id": "superstar_packers", + "name": "Superstar Packers", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 57000.0, + "total_commission": 2700.0, + "pending_amount": 2000.0, + "is_active": true + }, + { + "id": "century_polymers", + "name": "Century Polymers", + "city": "Kollam", + "state": null, + "phone": null, + "total_freight": 67000.0, + "total_commission": 3400.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "kahn_transport", + "name": "Kahn Transport", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 313000.0, + "total_commission": 12000.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "sahara_packers", + "name": "Sahara Packers", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 26000.0, + "total_commission": 1000.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "dryfish_john", + "name": "Dryfish John", + "city": "Kollam", + "state": null, + "phone": null, + "total_freight": 7500.0, + "total_commission": 500.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "chips", + "name": "Chips", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 27000.0, + "total_commission": 2100.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "drs", + "name": "DRS", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 49000.0, + "total_commission": 2575.0, + "pending_amount": 2000.0, + "is_active": true + }, + { + "id": "hirosh", + "name": "Hirosh", + "city": "Kollam", + "state": null, + "phone": null, + "total_freight": 10500.0, + "total_commission": 600.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "sun_packers", + "name": "Sun Packers", + "city": "Changanassery", + "state": null, + "phone": null, + "total_freight": 12000.0, + "total_commission": 750.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "thangavel", + "name": "Thangavel", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 12000.0, + "total_commission": 700.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "ambika_packers", + "name": "Ambika Packers", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 19000.0, + "total_commission": 1750.0, + "pending_amount": 1000.0, + "is_active": true + }, + { + "id": "nafees_alappuzha", + "name": "Nafees Alappuzha", + "city": "Perumathura", + "state": null, + "phone": null, + "total_freight": 9500.0, + "total_commission": 500.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "superstar", + "name": "Superstar", + "city": "Marthandam", + "state": null, + "phone": null, + "total_freight": 30500.0, + "total_commission": 1600.0, + "pending_amount": 1500.0, + "is_active": true + }, + { + "id": "shivaprasad", + "name": "Shivaprasad", + "city": "Thiruvalla", + "state": null, + "phone": null, + "total_freight": 17000.0, + "total_commission": 1020.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "filatex", + "name": "Filatex", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 14500.0, + "total_commission": 820.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "atc", + "name": "ATC", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 28000.0, + "total_commission": 1600.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "indian_cbe", + "name": "Indian CBE", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 7000.0, + "total_commission": 500.0, + "pending_amount": 500.0, + "is_active": true + }, + { + "id": "agarwal", + "name": "Agarwal", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 38000.0, + "total_commission": 0, + "pending_amount": 5500.0, + "is_active": true + }, + { + "id": "balmer_thuni", + "name": "Balmer Thuni", + "city": "Kottarakara", + "state": null, + "phone": null, + "total_freight": 21000.0, + "total_commission": 0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "jinu_coin", + "name": "Jinu Coin", + "city": "Trivandrum", + "state": null, + "phone": null, + "total_freight": 24000.0, + "total_commission": 2300.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "pasupathy", + "name": "Pasupathy", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 9000.0, + "total_commission": 0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "drs_agarwal", + "name": "DRS Agarwal", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 16500.0, + "total_commission": 0, + "pending_amount": 2000.0, + "is_active": true + }, + { + "id": "krs", + "name": "KRS", + "city": "Kollam", + "state": null, + "phone": null, + "total_freight": 11000.0, + "total_commission": 0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "agarwal_packers", + "name": "Agarwal Packers", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 9000.0, + "total_commission": 500.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "indian_cbe_shipper", + "name": "Indian CBE Shipper", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 7000.0, + "total_commission": 500.0, + "pending_amount": 500.0, + "is_active": true + }, + { + "id": "vehicle_available_/_looking_for_load", + "name": "Vehicle available / looking for load", + "city": "Thirumala, Trivandrum", + "state": null, + "phone": null, + "total_freight": 0, + "total_commission": 0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "gem", + "name": "Gem", + "city": null, + "state": null, + "phone": null, + "total_freight": 3000.0, + "total_commission": 0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "tci", + "name": "TCI", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 17000.0, + "total_commission": 1200.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "ktc", + "name": "KTC", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 50000.0, + "total_commission": 1000.0, + "pending_amount": 5000.0, + "is_active": true + }, + { + "id": "contact_lead", + "name": "Contact lead", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 0, + "total_commission": 0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "sulphi_baddest", + "name": "Sulphi Baddest", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 12000.0, + "total_commission": 700.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "vehicle_lead", + "name": "Vehicle lead", + "city": "Kollam", + "state": null, + "phone": null, + "total_freight": 0, + "total_commission": 0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "chipps", + "name": "Chipps", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 16500.0, + "total_commission": 500.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "aero_rubber", + "name": "Aero Rubber", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 8000.0, + "total_commission": 500.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "e20_packers_and_movers", + "name": "E20 Packers and Movers", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 0, + "total_commission": 0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "crt_transport", + "name": "CRT Transport", + "city": "Kollam", + "state": null, + "phone": null, + "total_freight": 0, + "total_commission": 0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "mohamed_anas", + "name": "Mohamed Anas", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 25000.0, + "total_commission": 1200.0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "hirosh_roadways", + "name": "Hirosh Roadways", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 15000.0, + "total_commission": 0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "nair", + "name": "Nair", + "city": "Thiruvananthapuram", + "state": null, + "phone": null, + "total_freight": 0, + "total_commission": 0, + "pending_amount": 0, + "is_active": true + }, + { + "id": "silverstar", + "name": "Silverstar", + "city": "Hyderabad", + "state": null, + "phone": null, + "total_freight": 36000.0, + "total_commission": 2100.0, + "pending_amount": 3000.0, + "is_active": true + } + ], + "vehicles": [ + { + "id": "tn57cu6379", + "number": "TN57CU6379", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "up11dt3778", + "number": "UP11DT3778", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "ka52c2983", + "number": "KA52C2983", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn20da3719", + "number": "TN20DA3719", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn39cc0878", + "number": "TN39CC0878", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "ka19ae9954", + "number": "KA19AE9954", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "ka526819", + "number": "KA52 6819", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn06aj6045", + "number": "TN06AJ6045", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn29cs7440", + "number": "TN29CS7440", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "kl17y8979", + "number": "KL17Y8979", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "mh03es6156", + "number": "MH03ES6156", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn70z8400", + "number": "TN70Z8400", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn95b6705", + "number": "TN95B6705", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "ka01ad5075", + "number": "KA01AD5075", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "ka22ab0718", + "number": "KA22AB0718", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "mh04ly921", + "number": "MH04LY921", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "mh13ep1517", + "number": "MH13EP1517", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "mh48dc1206", + "number": "MH48DC1206", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "mh48dc4853", + "number": "MH48DC4853", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn99ac1128", + "number": "TN99AC1128", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn99ad7220", + "number": "TN99AD7220", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "mh44u3556", + "number": "MH44U3556", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn30aw7836", + "number": "TN30AW7836", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn39du6743", + "number": "TN39DU6743", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn99ac1990", + "number": "TN99AC1990", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "mh04mr3503", + "number": "MH04MR3503", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "mh12sx9830", + "number": "MH12SX9830", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "mh24au7933", + "number": "MH24AU7933", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn41bf8178", + "number": "TN41BF8178", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "ka53ac1668", + "number": "KA53AC1668", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn56u6984", + "number": "TN56U6984", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn48bd4858", + "number": "TN48BD4858", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "ka06ba2739", + "number": "KA06BA2739", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "kl07bp2609", + "number": "KL07BP2609", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn25ca9552", + "number": "TN25CA9552", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn37bs7431", + "number": "TN37BS7431", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn41dc5854", + "number": "TN41DC5854", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "hr38ac0945", + "number": "HR38AC0945", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "ka02ae4084", + "number": "KA02AE4084", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "ka02ak2189", + "number": "KA02AK2189", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "ka04ag7476", + "number": "KA04AG7476", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "ka25ac1629", + "number": "KA25AC1629", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "ka63a7003", + "number": "KA63A7003", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn29dw7303", + "number": "TN29DW7303", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn30gy8205", + "number": "TN30GY8205", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn55bj9487", + "number": "TN55BJ9487", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn13ah3364", + "number": "TN13AH3364", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "badadosthopen", + "number": "BADADOSTH OPEN", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tg12t4862", + "number": "TG12T4862", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn32bh7891", + "number": "TN32BH7891", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn29cd6955", + "number": "TN29CD6955", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn24au8565", + "number": "TN24AU8565", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn65l3982", + "number": "TN65L3982", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn95p5495", + "number": "TN95P5495", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "mh03fc1829", + "number": "MH03FC1829", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn43w8330", + "number": "TN43W8330", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn39ah9902", + "number": "TN39AH9902", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn39dv8142", + "number": "TN39DV8142", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "ka01al8839", + "number": "KA01AL8839", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "mh02gh3248", + "number": "MH02GH3248", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "mh46bb0041", + "number": "MH46BB0041", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn30cw9849", + "number": "TN30CW9849", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn66am1928", + "number": "TN66AM1928", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn66am3928", + "number": "TN66AM3928", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn77f3427", + "number": "TN77F3427", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn83md7328", + "number": "TN83MD7328", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn93e6166", + "number": "TN93E6166", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "mh12qw4555", + "number": "MH12QW4555", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn16f0565", + "number": "TN16F0565", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + }, + { + "id": "tn24ah6898", + "number": "TN24AH6898", + "type": "open", + "city": null, + "state": "Kerala", + "is_active": true + } + ], + "loads": [ + { + "id": "load_001", + "date": "2026-06-02", + "shipper_id": "agarwal_packers_and_movers", + "vehicle_id": "tn57cu6379", + "from_city": "Thiruvananthapuram", + "to_city": "Noida sector 33", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 76000.0, + "advance_received": 69000.0, + "paid_to_driver": 64250.0, + "driver_freight": 75000.0, + "commission": 3750.0, + "pending_from_shipper": 7000.0, + "pending_to_driver": 7000.0, + "status": "loaded / in transit", + "notes": "Agarwal Packers and Movers load from Thiruvananthapuram to Noida sector 33. Vehicle TN57CU6379, contact 9286755775. Freight: 76000. Driver freight: 75000. Commission: 3750 (5% of driver freight 75000, deducted from driver payment). Agarwal paid: 69000. Pending from Agarwal: 7000. Paid driver: 20000 advance + 1000 diesel + 30000 + 13250 = 64250 total. Driver entitled: 71250 (75000 - 3750 commission). Pending to driver: 7000." + }, + { + "id": "load_002", + "date": "2026-06-02", + "shipper_id": "superstar_packers", + "vehicle_id": "up11dt3778", + "from_city": "Thiruvananthapuram", + "to_city": "Visakhapatnam", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 45000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 2000.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "assigned", + "notes": "Superstar Packers load from Thiruvananthapuram to Visakhapatnam. Vehicle UP11DT3778, contact 9627574805. Freight: 45000. Payments managed by shipper and driver directly. Commission: 2000 paid by driver in cash." + }, + { + "id": "load_003", + "date": null, + "shipper_id": null, + "vehicle_id": null, + "from_city": null, + "to_city": "Pune", + "via": null, + "load_type": "Part load", + "item": null, + "freight_charged": 15000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "pending lead", + "notes": "User shared contact 08089002727 for a Pune part-load worth 15000. Origin, vehicle, and date were not provided yet." + }, + { + "id": "load_004", + "date": null, + "shipper_id": null, + "vehicle_id": null, + "from_city": "Technopark, Thiruvananthapuram", + "to_city": null, + "via": null, + "load_type": "17 ft open vehicle", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "available vehicle", + "notes": "Vehicle lead from user: contact 09952764728, 17 ft open vehicle in Technopark, Thiruvananthapuram. Route/load details not yet provided." + }, + { + "id": "load_005", + "date": null, + "shipper_id": null, + "vehicle_id": null, + "from_city": "Thiruvananthapuram", + "to_city": null, + "via": null, + "load_type": "20 ft open vehicle", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "available vehicle", + "notes": "Vehicle lead from user: owner Mayandi, 20 ft open vehicle in Thiruvananthapuram. Contact number not provided yet." + }, + { + "id": "load_006", + "date": null, + "shipper_id": null, + "vehicle_id": "ka52c2983", + "from_city": "Thiruvananthapuram", + "to_city": null, + "via": null, + "load_type": "Available vehicle", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "available vehicle", + "notes": "Vehicle lead from user: KA52C2983, contact 8861687221, near Tollgate, Thiruvananthapuram. Route/load details not yet provided." + }, + { + "id": "load_007", + "date": null, + "shipper_id": null, + "vehicle_id": "tn20da3719", + "from_city": "Thiruvananthapuram", + "to_city": null, + "via": null, + "load_type": "9 ft container", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "available vehicle", + "notes": "Vehicle lead from user: TN20DA3719, contact 9342894569, parked in Thiruvananthapuram. User said it is a 9 ft container vehicle. Route/load details not yet provided." + }, + { + "id": "load_008", + "date": null, + "shipper_id": null, + "vehicle_id": "tn39cc0878", + "from_city": "Thiruvananthapuram", + "to_city": null, + "via": null, + "load_type": "20 ft container", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "available vehicle", + "notes": "Vehicle lead from user: TN39CC0878, contact 9940750878, 20 ft container in parking at Thiruvananthapuram. Route/load details not yet provided." + }, + { + "id": "load_009", + "date": "2026-05-01", + "shipper_id": "century_polymers", + "vehicle_id": "ka19ae9954", + "from_city": "Kollam", + "to_city": "Bangalore", + "via": null, + "load_type": "Mixed / multi-drop", + "item": null, + "freight_charged": 12000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 900.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "User provided previous-month data: KA19AE9954 from Kollam to Palakkad and Bangalore with 3 deliveries RK Nagar, Bomasandra, HSR. Shipper Century Polymers, hire 12000, shipper managed payment, user received 900 cash commission." + }, + { + "id": "load_010", + "date": "2026-05-01", + "shipper_id": "kahn_transport", + "vehicle_id": "ka526819", + "from_city": "Thiruvananthapuram", + "to_city": "Mumbai", + "via": null, + "load_type": "Full load", + "item": null, + "freight_charged": 40000.0, + "advance_received": 35000.0, + "paid_to_driver": 33000.0, + "driver_freight": null, + "commission": 2000.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User said freight was 40000 and same freight to driver. Received 35000 advance from Kahn Transport and paid driver 33000 advance. User also mentioned deducting 2000 for Kahn Transport load. User confirmed this Kahn Transport load is settled." + }, + { + "id": "load_011", + "date": "2026-05-01", + "shipper_id": "sahara_packers", + "vehicle_id": "ka526819", + "from_city": "Thiruvananthapuram", + "to_city": "Mumbai", + "via": null, + "load_type": "Part load", + "item": null, + "freight_charged": 9000.0, + "advance_received": 9000.0, + "paid_to_driver": 8500.0, + "driver_freight": null, + "commission": 500.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "Part-load on the same vehicle. User received 5000 advance from Sahara and later received the 4000 balance from Sahara. User paid 8500 after unloading. Profit/commission taken as 500." + }, + { + "id": "load_012", + "date": "2026-05-01", + "shipper_id": "dryfish_john", + "vehicle_id": "tn06aj6045", + "from_city": "Kollam", + "to_city": "Tiruchirappalli", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 7500.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 500.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "User clarified the commission was received as profit, and payment was managed by the shipper." + }, + { + "id": "load_013", + "date": "2026-05-01", + "shipper_id": "chips", + "vehicle_id": "tn29cs7440", + "from_city": "Thiruvananthapuram", + "to_city": "Palakkad", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 6000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 500.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission adjusted", + "notes": "User said this vehicle was loaded for Chips from Thiruvananthapuram to Palakkad on 1 May. Hire 6000, commission 500. User also had to pay a previous KTC load balance of 500, and matched it against this commission." + }, + { + "id": "load_014", + "date": "2026-05-02", + "shipper_id": "drs", + "vehicle_id": "kl17y8979", + "from_city": "Thiruvananthapuram", + "to_city": "Coimbatore", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 9500.0, + "advance_received": 9500.0, + "paid_to_driver": 8900.0, + "driver_freight": null, + "commission": 600.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User said on 2 May, KL17Y8979 ran from Thiruvananthapuram to Coimbatore for DRS. Freight was 9500. Full payment was received from DRS. User paid 7500 and 1400, total 8900, leaving 600 as profit/commission." + }, + { + "id": "load_015", + "date": "2026-05-02", + "shipper_id": "kahn_transport", + "vehicle_id": "mh03es6156", + "from_city": "Thiruvananthapuram", + "to_city": "Mumbai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 40000.0, + "advance_received": 35000.0, + "paid_to_driver": 33000.0, + "driver_freight": null, + "commission": 2000.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User said on 2 May, MH03ES6156 ran from Thiruvananthapuram to Mumbai for Kahn Transport. Freight was 40000. Advance 35000 received, driver paid 33000 with 2000 debited as commission, and the remaining 5000 was received and paid to the driver after unloading. User confirmed this Kahn Transport load is settled." + }, + { + "id": "load_016", + "date": "2026-05-02", + "shipper_id": "hirosh", + "vehicle_id": "tn70z8400", + "from_city": "Kollam", + "to_city": "Coimbatore", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 10500.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 600.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "User said on 2 May, TN70Z8400 ran from Kollam to Coimbatore for shipper Hirosh. Hire was 10500 and user received 600 commission via Google Pay." + }, + { + "id": "load_017", + "date": "2026-05-02", + "shipper_id": "sun_packers", + "vehicle_id": "tn95b6705", + "from_city": "Changanassery", + "to_city": "Coimbatore", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 12000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 750.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "User said on 2 May, TN95B6705 ran from Changanassery to Coimbatore for Sun Packers. Freight was 12000, payment was managed by the shipper, and user received 750 as commission." + }, + { + "id": "load_018", + "date": "2026-05-03", + "shipper_id": "thangavel", + "vehicle_id": "ka01ad5075", + "from_city": "Thiruvananthapuram", + "to_city": "Erode", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 12000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 700.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "User said on 3 May, KA01AD5075 ran from Thiruvananthapuram to Erode for shipper Thangavel. Freight was 12000, payment was managed by the shipper, and user received 700 commission." + }, + { + "id": "load_019", + "date": "2026-05-03", + "shipper_id": "ambika_packers", + "vehicle_id": "ka22ab0718", + "from_city": "Thiruvananthapuram", + "to_city": "Kalpetta", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 9000.0, + "advance_received": 7500.0, + "paid_to_driver": 9000.0, + "driver_freight": null, + "commission": 750.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User said on 3 May, KA22AB0718 ran from Thiruvananthapuram to Kalpetta for Ambika Packers. Freight was 9000. User received 7500 advance, paid 7500 to the driver, received 750 cash commission from the driver, then received the remaining 1500 balance from the shipper and paid it to the driver." + }, + { + "id": "load_020", + "date": "2026-05-03", + "shipper_id": "kahn_transport", + "vehicle_id": "mh04ly921", + "from_city": "Thiruvananthapuram", + "to_city": "Mumbai", + "via": null, + "load_type": "33 ft container", + "item": null, + "freight_charged": 55000.0, + "advance_received": 50000.0, + "paid_to_driver": 53000.0, + "driver_freight": null, + "commission": 2000.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User said on 3 May, MH04LY921 33 ft container ran to Mumbai for Kahn Transport. Freight was 55000, advance received was 50000, commission deducted 2000, and the balance was settled too. User confirmed balance received and load settled." + }, + { + "id": "load_021", + "date": "2026-05-03", + "shipper_id": "nafees_alappuzha", + "vehicle_id": "mh13ep1517", + "from_city": "Perumathura", + "to_city": "Tiruchirappalli", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 9500.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 500.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "User said on 3 May, MH13EP1517 ran from Perumathura to Tiruchirappalli for shipper Nafees Alappuzha. Freight was 9500, payment was managed by the shipper, and user received 500 commission." + }, + { + "id": "load_022", + "date": "2026-05-03", + "shipper_id": "kahn_transport", + "vehicle_id": "mh48dc1206", + "from_city": "Thiruvananthapuram", + "to_city": "Mumbai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 41000.0, + "advance_received": 35000.0, + "paid_to_driver": 39000.0, + "driver_freight": null, + "commission": 2000.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User said this is the same Kahn Transport vehicle on 3 May. Advance received was 35000. Commission deducted 2000. Assumed driver settlement was completed, with balance pending from shipper. User confirmed balance received and load settled." + }, + { + "id": "load_023", + "date": "2026-05-03", + "shipper_id": "drs", + "vehicle_id": "mh48dc4853", + "from_city": "Thiruvananthapuram", + "to_city": "Mumbai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 39500.0, + "advance_received": 37500.0, + "paid_to_driver": 35525.0, + "driver_freight": null, + "commission": 1975.0, + "pending_from_shipper": 2000.0, + "pending_to_driver": null, + "status": "pending collection", + "notes": "User said on 3 May, MH48DC4853 (Sahaniji vehicle) ran from Thiruvananthapuram to Mumbai for DRS. Freight was 39500. User received 37500 advance and paid vehicle advance after deducting 1975 as commission, so vehicle advance is recorded as 35525." + }, + { + "id": "load_024", + "date": "2026-05-03", + "shipper_id": "superstar", + "vehicle_id": "tn99ac1128", + "from_city": "Marthandam", + "to_city": "Chennai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 10500.0, + "advance_received": 9000.0, + "paid_to_driver": 9900.0, + "driver_freight": null, + "commission": 600.0, + "pending_from_shipper": 1500.0, + "pending_to_driver": null, + "status": "pending collection", + "notes": "User said on 3 May, TN99AC1128 ran from Marthandam to Chennai for Superstar. Freight was 10500. User received 9000 advance and paid vehicle 8400 advance plus 1500 balance, so total paid to driver is recorded as 9900. Pending from shipper is 1500." + }, + { + "id": "load_025", + "date": "2026-05-03", + "shipper_id": "shivaprasad", + "vehicle_id": "tn99ad7220", + "from_city": "Thiruvalla", + "to_city": "Tanjavur", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 17000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 1020.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "User said on 3 May, TN99AD7220 ran from Thiruvalla to Tanjavur for shipper Shivaprasad. Freight was 17000, payment was managed by the shipper, and user received 1020 commission." + }, + { + "id": "load_026", + "date": "2026-05-04", + "shipper_id": "kahn_transport", + "vehicle_id": "mh44u3556", + "from_city": "Cochin", + "to_city": "Mumbai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 35000.0, + "advance_received": null, + "paid_to_driver": 34000.0, + "driver_freight": 34000.0, + "commission": 1000.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User said on 4 May, MH44U3556 ran from Cochin to Mumbai for Kahn Transport. Freight was 35000, driver freight 34000, and 1000 was collected as commission." + }, + { + "id": "load_027", + "date": "2026-05-04", + "shipper_id": "superstar", + "vehicle_id": "tn30aw7836", + "from_city": "Pathanamthitta", + "to_city": "Cochin", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 7000.0, + "advance_received": 7000.0, + "paid_to_driver": 6000.0, + "driver_freight": 6000.0, + "commission": 1000.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User said on 4 May, TN30AW7836 ran from Pathanamthitta to Cochin for Superstar. Freight was 7000, full payment was received from Superstar, and driver was paid 6000." + }, + { + "id": "load_028", + "date": "2026-05-04", + "shipper_id": "ambika_packers", + "vehicle_id": "tn39du6743", + "from_city": "Thiruvananthapuram", + "to_city": "Cochin", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 4500.0, + "advance_received": 4000.0, + "paid_to_driver": 4000.0, + "driver_freight": null, + "commission": 500.0, + "pending_from_shipper": 500.0, + "pending_to_driver": null, + "status": "pending collection", + "notes": "User said on 4 May, TN39DU6743 ran from Thiruvananthapuram to Cochin for Ambika Packers. Freight was 4500. User received 4000 from Ambika Packers, paid driver 4000, and 500 remains pending from Ambika Packers as commission." + }, + { + "id": "load_029", + "date": "2026-05-04", + "shipper_id": "chips", + "vehicle_id": "tn99ac1990", + "from_city": "Thiruvananthapuram", + "to_city": "Palakkad", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 10000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": 9000.0, + "commission": 1000.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "User said on 4 May, TN99AC1990 ran from Thiruvananthapuram to Palakkad for Chips. Freight was 10000, driver freight 9000, payment managed by shipper, and user received 1000." + }, + { + "id": "load_030", + "date": "2026-05-05", + "shipper_id": "kahn_transport", + "vehicle_id": "mh04mr3503", + "from_city": "Cochin", + "to_city": "Mumbai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 34000.0, + "advance_received": null, + "paid_to_driver": 33000.0, + "driver_freight": 33000.0, + "commission": 1000.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User said on 5 May, MH04MR3503 ran from Cochin to Mumbai for Kahn Transport. Freight was 34000, driver freight was 33000, and user received 1000 commission." + }, + { + "id": "load_031", + "date": "2026-05-05", + "shipper_id": "kahn_transport", + "vehicle_id": "mh12sx9830", + "from_city": "Cochin", + "to_city": "Mumbai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 34000.0, + "advance_received": null, + "paid_to_driver": 33000.0, + "driver_freight": 33000.0, + "commission": 1000.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User said on 5 May, MH12SX9830 ran from Cochin to Mumbai for Kahn Transport. Freight was 34000, driver freight was 33000, and user received 1000 commission." + }, + { + "id": "load_032", + "date": "2026-05-05", + "shipper_id": "kahn_transport", + "vehicle_id": "mh24au7933", + "from_city": "Cochin", + "to_city": "Mumbai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 34000.0, + "advance_received": null, + "paid_to_driver": 33000.0, + "driver_freight": 33000.0, + "commission": 1000.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User said on 5 May, MH24AU7933 ran from Cochin to Mumbai for Kahn Transport. Freight was 34000, driver freight was 33000, and user received 1000 commission." + }, + { + "id": "load_033", + "date": "2026-05-05", + "shipper_id": "filatex", + "vehicle_id": "tn41bf8178", + "from_city": "Thiruvananthapuram", + "to_city": "Palakkad", + "via": "['Ukkadam', 'Ganapati']", + "load_type": null, + "item": null, + "freight_charged": 14500.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 820.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "User said on 5 May, TN41BF8178 ran from Thiruvananthapuram to Palakkad via Ukkadam and Ganapati for shipper Filatex. Freight was 14500, payment was managed by the shipper, and user received 820 commission." + }, + { + "id": "load_034", + "date": "2026-05-06", + "shipper_id": "atc", + "vehicle_id": "ka53ac1668", + "from_city": "Thiruvananthapuram", + "to_city": "Mumbai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 28000.0, + "advance_received": 25000.0, + "paid_to_driver": 26400.0, + "driver_freight": null, + "commission": 1600.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User said on 6 May, KA53AC1668 ran from Thiruvananthapuram to Mumbai for ATC. Freight was 28000, 25000 was received as advance on 6 May, 3000 was received on 15 May, and user deducted 1600 as commission before paying the driver. User confirmed ATC Thiruvananthapuram to Mumbai is received and settled to driver." + }, + { + "id": "load_035", + "date": "2026-05-06", + "shipper_id": "indian_cbe", + "vehicle_id": "tn56u6984", + "from_city": "Thiruvananthapuram", + "to_city": "Coimbatore", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 7000.0, + "advance_received": 6500.0, + "paid_to_driver": 5500.0, + "driver_freight": 6000.0, + "commission": 500.0, + "pending_from_shipper": 500.0, + "pending_to_driver": null, + "status": "pending collection", + "notes": "User said on 6 May, TN56U6984 ran from Thiruvananthapuram to Coimbatore for Indian CBE. Freight was 7000, driver freight 6000, advance received 6500, and user paid driver 4500 advance plus 1000 balance. Commission deducted was 500." + }, + { + "id": "load_036", + "date": "2026-05-12", + "shipper_id": "agarwal_packers_and_movers", + "vehicle_id": "mh48dc1206", + "from_city": "Trivandrum", + "to_city": "Mumbai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 41000.0, + "advance_received": 38000.0, + "paid_to_driver": 40000.0, + "driver_freight": null, + "commission": 2000.0, + "pending_from_shipper": 3000.0, + "pending_to_driver": null, + "status": "reconciled", + "notes": "User first shared total 41000, advance 38000, balance 3000. Later clarified they received 38000, paid 20000 + 15000, and the driver freight was 40000 with 2000 commission." + }, + { + "id": "load_037", + "date": "2026-05-13", + "shipper_id": "agarwal_packers_and_movers", + "vehicle_id": "tn48bd4858", + "from_city": "Thiruvananthapuram", + "to_city": "Thirupathy", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 23000.0, + "advance_received": 23000.0, + "paid_to_driver": 21700.0, + "driver_freight": null, + "commission": 1300.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User later confirmed the pending 2000 was also paid, so the load is settled." + }, + { + "id": "load_038", + "date": "2026-05-14", + "shipper_id": "agarwal", + "vehicle_id": "ka06ba2739", + "from_city": "Thiruvananthapuram", + "to_city": "Hyderabad", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 38000.0, + "advance_received": 32500.0, + "paid_to_driver": 33500.0, + "driver_freight": null, + "commission": null, + "pending_from_shipper": 5500.0, + "pending_to_driver": 4500.0, + "status": "fully pending from shipper", + "notes": "User said they did not receive any payment from Agarwal for this load. User paid driver 30000 and 3500. Updated per user: advance received revised." + }, + { + "id": "load_039", + "date": "2026-05-14", + "shipper_id": "sahara_packers", + "vehicle_id": "kl07bp2609", + "from_city": "Adoor", + "to_city": "Cochin", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 7000.0, + "advance_received": 7000.0, + "paid_to_driver": 6500.0, + "driver_freight": null, + "commission": 500.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User clarified Sahara paid 7000 total, user paid 5500 advance and 1000 balance to truck, leaving 500 profit." + }, + { + "id": "load_040", + "date": "2026-05-15", + "shipper_id": "century_polymers", + "vehicle_id": "tn25ca9552", + "from_city": "Kollam", + "to_city": "Chennai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 18000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 1000.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "Payment managed by Century Polymers. User received 1000 cash commission from driver." + }, + { + "id": "load_041", + "date": "2026-05-15", + "shipper_id": "ambika_packers", + "vehicle_id": "tn37bs7431", + "from_city": "Thiruvananthapuram", + "to_city": "Thrissur", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 5500.0, + "advance_received": 5000.0, + "paid_to_driver": 5000.0, + "driver_freight": null, + "commission": 500.0, + "pending_from_shipper": 500.0, + "pending_to_driver": 500.0, + "status": "partially pending", + "notes": "User said 500 will be their commission. There is also 500 still due from the shipper based on the numbers given." + }, + { + "id": "load_042", + "date": "2026-05-15", + "shipper_id": "chips", + "vehicle_id": "tn41dc5854", + "from_city": "Thiruvananthapuram", + "to_city": "Palakkad", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 11000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 600.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission due", + "notes": "User said commission due is 600." + }, + { + "id": "load_043", + "date": "2026-05-17", + "shipper_id": "balmer_thuni", + "vehicle_id": null, + "from_city": "Kottarakara", + "to_city": "Bangalore", + "via": "['Mysore road']", + "load_type": "10 ton", + "item": null, + "freight_charged": 21000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "assigned vehicle", + "notes": "User said Balmer Thuni is looking for 2 vehicles today. One truck freight was negotiated and fixed at 21000. Driver number +91 98457 36339 is on the way to loading." + }, + { + "id": "load_044", + "date": "2026-05-17", + "shipper_id": "jinu_coin", + "vehicle_id": null, + "from_city": "Trivandrum", + "to_city": "Chennai", + "via": "['Shabarimala']", + "load_type": null, + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "partial", + "notes": "User first said shipper was Minutes Coin, then corrected to Jinu Coin. No amounts were provided." + }, + { + "id": "load_045", + "date": "2026-05-17", + "shipper_id": "balmer_thuni", + "vehicle_id": null, + "from_city": "Kottarakara", + "to_city": "Bangalore", + "via": "['Mysore road']", + "load_type": "10 ton", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "partial", + "notes": "User shared a lead for 10 ton old clothes from Kottarakara to Bangalore via Mysore road by Balmer Thuni. No freight or vehicle details provided yet." + }, + { + "id": "load_046", + "date": "2026-05-17", + "shipper_id": "pasupathy", + "vehicle_id": null, + "from_city": "Thiruvananthapuram", + "to_city": "Coimbatore", + "via": null, + "load_type": "Household", + "item": null, + "freight_charged": 9000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "partial", + "notes": "User shared a lead for household goods from Thiruvananthapuram to Coimbatore for 14ft/17ft, expected freight 9000, by Pasupathy. No vehicle or payment details provided yet." + }, + { + "id": "load_047", + "date": "2026-05-17", + "shipper_id": "agarwal_packers_and_movers", + "vehicle_id": "hr38ac0945", + "from_city": "Trivandrum", + "to_city": "Chennai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 19500.0, + "advance_received": 17800.0, + "paid_to_driver": 15000.0, + "driver_freight": null, + "commission": null, + "pending_from_shipper": 1700.0, + "pending_to_driver": null, + "status": "pending collection", + "notes": "Manager Vikas paid driver 15000 as advance. Balance of 4500 was pending. Updated per user: advance received revised." + }, + { + "id": "load_048", + "date": "2026-05-17", + "shipper_id": "jinu_coin", + "vehicle_id": "ka02ae4084", + "from_city": "Thiruvananthapuram", + "to_city": "Triplicane, Chennai", + "via": "['Shabarimala']", + "load_type": "19 ft container", + "item": null, + "freight_charged": 24000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": 23000.0, + "commission": 2300.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "loaded / in transit", + "notes": "User confirmed the lead converted into business. Vehicle was assigned yesterday, currently at Shabarimala and loaded. Shipper made full payment directly to the driver account. User received 2300 total from driver, including 1000 crossing amount and 1300 commission." + }, + { + "id": "load_049", + "date": "2026-05-17", + "shipper_id": null, + "vehicle_id": "ka02ak2189", + "from_city": "Thiruvananthapuram", + "to_city": null, + "via": null, + "load_type": "22 ft container", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "partial", + "notes": "User shared a 22 ft container vehicle KA02AK2189 located in Thiruvananthapuram. Contact number provided: 8660525007." + }, + { + "id": "load_050", + "date": "2026-05-17", + "shipper_id": "drs_agarwal", + "vehicle_id": "ka04ag7476", + "from_city": "Thiruvananthapuram", + "to_city": "Mysore", + "via": null, + "load_type": "Household", + "item": null, + "freight_charged": 16500.0, + "advance_received": 14500.0, + "paid_to_driver": 14000.0, + "driver_freight": null, + "commission": null, + "pending_from_shipper": 2000.0, + "pending_to_driver": null, + "status": "pending collection", + "notes": "User said DRS paid 14500 advance. User paid driver 10000 advance and 4000 balance. Later user clarified DRS was charged 16500 and 2000 is still due." + }, + { + "id": "load_051", + "date": "2026-05-17", + "shipper_id": "krs", + "vehicle_id": "ka04ag7476", + "from_city": "Kollam", + "to_city": "Bangalore", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 11000.0, + "advance_received": 5000.0, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "KRS handled payment directly. Driver received 5000 advance from KRS and 6000 on delivery. User said this part-load does not create pending for them. User confirmed the KRS load is settled." + }, + { + "id": "load_052", + "date": "2026-05-17", + "shipper_id": "agarwal_packers", + "vehicle_id": "ka25ac1629", + "from_city": "Thiruvananthapuram", + "to_city": "Theni", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 9000.0, + "advance_received": 8500.0, + "paid_to_driver": 8500.0, + "driver_freight": 8500.0, + "commission": 500.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "User clarified total freight was 9000, advance received was 8500, and the 500 balance was also received; there was additional money received beyond the 500 balance, but the exact extra amount was not specified." + }, + { + "id": "load_053", + "date": "2026-05-17", + "shipper_id": "indian_cbe_shipper", + "vehicle_id": "ka63a7003", + "from_city": "Thiruvananthapuram", + "to_city": "Coimbatore", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 7000.0, + "advance_received": 6500.0, + "paid_to_driver": 5000.0, + "driver_freight": 6000.0, + "commission": 500.0, + "pending_from_shipper": 500.0, + "pending_to_driver": 500.0, + "status": "partially pending", + "notes": "User said they received 6500 advance, paid 5000, and will charge the driver another 500 as commission." + }, + { + "id": "load_054", + "date": "2026-05-17", + "shipper_id": "vehicle_available_/_looking_for_load", + "vehicle_id": "tn29dw7303", + "from_city": "Thirumala, Trivandrum", + "to_city": null, + "via": null, + "load_type": "22 ft open", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "partial", + "notes": "Vehicle available in Thirumala, Trivandrum. Contact +91 95669 25640 for TN29DW7303, 22 ft open." + }, + { + "id": "load_055", + "date": "2026-05-17", + "shipper_id": "vehicle_available_/_looking_for_load", + "vehicle_id": "tn30gy8205", + "from_city": "Thirumala, Trivandrum", + "to_city": null, + "via": null, + "load_type": "22 ft open", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "partial", + "notes": "Vehicle available in Thirumala, Trivandrum. Contact +91 95669 25640 for TN30GY8205, 22 ft open." + }, + { + "id": "load_056", + "date": "2026-05-17", + "shipper_id": "vehicle_available_/_looking_for_load", + "vehicle_id": "tn55bj9487", + "from_city": "Trivandrum", + "to_city": null, + "via": null, + "load_type": "29 ft container", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "partial", + "notes": "User shared vehicle TN55BJ9487, 29 ft container, available in Trivandrum, contact 7092073729, looking for load." + }, + { + "id": "load_057", + "date": "2026-05-18", + "shipper_id": "gem", + "vehicle_id": null, + "from_city": null, + "to_city": "Bangalore", + "via": null, + "load_type": "Part load", + "item": null, + "freight_charged": 3000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "handled directly by shipper", + "notes": "User said yesterday there was another part load shipper Gem to Bangalore for 3000. Payment was managed by shipper. Driver paid user 1200 total commission across the TCI/Gem arrangement." + }, + { + "id": "load_058", + "date": "2026-05-18", + "shipper_id": "tci", + "vehicle_id": "ka02ak2189", + "from_city": "Thiruvananthapuram", + "to_city": "Bangalore", + "via": null, + "load_type": "22 ft container", + "item": null, + "freight_charged": 17000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 1200.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "User said yesterday KA02AK2189 22 ft container with contact 8660525007 was given to TCI for Bangalore. Freight 17000, payment managed by shipper, and driver paid 1200 commission to user." + }, + { + "id": "load_059", + "date": "2026-05-18", + "shipper_id": "ktc", + "vehicle_id": "tn13ah3364", + "from_city": "Thiruvananthapuram", + "to_city": "Coimbatore", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 7000.0, + "advance_received": 6000.0, + "paid_to_driver": 6000.0, + "driver_freight": 6500.0, + "commission": 500.0, + "pending_from_shipper": 1000.0, + "pending_to_driver": null, + "status": "settled", + "notes": "User clarified the driver settlement for the 18 May KTC load: 500 paid yesterday, 500 on fuel, 4500 to owner account, and 500 to driver account. Total paid to driver side is 6000. Advance received was 6000 and the load is now settled. Pending from shipper remains 1000." + }, + { + "id": "load_060", + "date": "2026-05-19", + "shipper_id": "contact_lead", + "vehicle_id": "badadosthopen", + "from_city": "Thiruvananthapuram", + "to_city": "Vellore", + "via": null, + "load_type": "household", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "pending lead", + "notes": "Loading Thiruvananthapuram, unloading Vellore, household material, 1 ton. Freight mentioned as 0. Contact numbers: 9447114970 / 8848001730. Location: Thiruvananthapuram, Kerala." + }, + { + "id": "load_061", + "date": "2026-05-19", + "shipper_id": "ktc", + "vehicle_id": "tg12t4862", + "from_city": "Thiruvananthapuram", + "to_city": "Hyderabad", + "via": null, + "load_type": "14 ft open", + "item": null, + "freight_charged": 28000.0, + "advance_received": 25000.0, + "paid_to_driver": 23500.0, + "driver_freight": null, + "commission": null, + "pending_from_shipper": 3000.0, + "pending_to_driver": null, + "status": "pending collection", + "notes": "User shared a KTC load today from Thiruvananthapuram to Hyderabad with freight 28000. Vehicle, advance, and settlement details were not provided yet. User assigned TG12T4862 (14 ft open), contact +91 70932 00452, to this Hyderabad load. User clarified payment received for TG12T4862: 15000 yesterday and 10000 today, total 25000. User clarified driver payment: 15000 and 8500 (total 23500)." + }, + { + "id": "load_062", + "date": "2026-05-19", + "shipper_id": "sulphi_baddest", + "vehicle_id": "tn32bh7891", + "from_city": "Thiruvananthapuram", + "to_city": "Chennai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 12000.0, + "advance_received": 10000.0, + "paid_to_driver": 10000.0, + "driver_freight": 1300.0, + "commission": 700.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "completed", + "notes": "Advance paid 10,000; today received vehicle payment 3,000, vehicle charge 2,000, and Sulphi charge 1,000. Paid driver 10,000 and driver freight 1,300 after deducting 700 commission." + }, + { + "id": "load_063", + "date": "2026-05-20", + "shipper_id": "sahara_packers", + "vehicle_id": "ka01ad5075", + "from_city": "Thiruvananthapuram", + "to_city": "Palakkad", + "via": null, + "load_type": "household", + "item": null, + "freight_charged": 10000.0, + "advance_received": 8000.0, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "pending collection", + "notes": "17 ft vehicle required. KA01AD5075 assigned for Sahara Packers. Date still not provided. Advance received: Rs 8,000 on 20 May 2026. Waiting for balance and driver payment details." + }, + { + "id": "load_064", + "date": "2026-05-20", + "shipper_id": "vehicle_lead", + "vehicle_id": "tn29cd6955", + "from_city": "Kollam", + "to_city": null, + "via": null, + "load_type": "20 ft container", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "available vehicle", + "notes": "Contact 9731776897. Vehicle in Kollam. 20 ft container." + }, + { + "id": "load_065", + "date": "2026-05-21", + "shipper_id": "chipps", + "vehicle_id": "tn24au8565", + "from_city": "Thiruvananthapuram", + "to_city": "Palakkad", + "via": null, + "load_type": "10 ft container", + "item": null, + "freight_charged": 7000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "assigned vehicle", + "notes": "10 ft container assigned. Contact: 9363509628. Freight: 7000." + }, + { + "id": "load_066", + "date": "2026-05-21", + "shipper_id": "chipps", + "vehicle_id": "tn65l3982", + "from_city": "Thiruvananthapuram", + "to_city": "Palakkad", + "via": null, + "load_type": "17 ft", + "item": null, + "freight_charged": 9500.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": 9000.0, + "commission": 500.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "17 ft vehicle assigned. Contact: 9384417088. Freight: 9500. Payment managed by shipper. Commission received from driver: \u20b9500." + }, + { + "id": "load_067", + "date": "2026-05-21", + "shipper_id": "ktc", + "vehicle_id": "tn95p5495", + "from_city": "Thiruvananthapuram", + "to_city": "Thirupur", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 7500.0, + "advance_received": 6000.0, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "pending lead", + "notes": "User mentioned today KTC load from Thiruvananthapuram to Thirupur. Freight, vehicle, and payment details not yet provided. Vehicle TN95P5495 (contact 9344989007) assigned for 7500 freight. Vehicle advance paid by user: Rs 6,000" + }, + { + "id": "load_068", + "date": "2026-05-23", + "shipper_id": "aero_rubber", + "vehicle_id": "mh03fc1829", + "from_city": "Thiruvananthapuram", + "to_city": "Madurai", + "via": null, + "load_type": "10 ft container", + "item": null, + "freight_charged": 8000.0, + "advance_received": 8000.0, + "paid_to_driver": 7500.0, + "driver_freight": 7500.0, + "commission": 500.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "Aero Rubber load from Thiruvananthapuram to Madurai. Vehicle MH03FC1829 assigned, contact 9892355292, 10ft container. 2.6 ton load. Vehicle placed on Saturday May 23, unloaded today May 25. Received 4000 on Saturday and 4000 today (total 8000). Paid driver 6500 on Saturday and 1000 today (total 7500). Commission: 500. Load settled." + }, + { + "id": "load_069", + "date": "2026-05-24", + "shipper_id": "e20_packers_and_movers", + "vehicle_id": null, + "from_city": "Thiruvananthapuram", + "to_city": "Marapalam", + "via": "['Mangalore']", + "load_type": null, + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "pending lead", + "notes": "E20 Packers and Movers (contact: 099618 52004) have a load from Thiruvananthapuram to Marapalam to Mangalore. 14ft or 17ft vehicle required. Route: Thiruvananthapuram \u2192 Marapalam \u2192 Mangalore." + }, + { + "id": "load_070", + "date": "2026-05-24", + "shipper_id": "agarwal_packers_and_movers", + "vehicle_id": "tn43w8330", + "from_city": "Thiruvananthapuram", + "to_city": "Guruvayoor", + "via": null, + "load_type": "10 ft open", + "item": null, + "freight_charged": 7500.0, + "advance_received": 7500.0, + "paid_to_driver": 6700.0, + "driver_freight": 6700.0, + "commission": 800.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "Agarwal Packers and Movers load from Thiruvananthapuram to Guruvayoor. Vehicle TN43W8330, contact 9943536599, 10ft open vehicle. Vehicle reached and unloaded. Full payment received from Agarwal: 7500. Paid driver: 5500 advance + 1200 extra for height load = 6700 total. Commission: 800. Load settled." + }, + { + "id": "load_071", + "date": "2026-05-25", + "shipper_id": "superstar_packers", + "vehicle_id": "tn39ah9902", + "from_city": "Thiruvananthapuram", + "to_city": "Coimbatore", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 12000.0, + "advance_received": 10000.0, + "paid_to_driver": 12000.0, + "driver_freight": 12000.0, + "commission": 700.0, + "pending_from_shipper": 2000.0, + "pending_to_driver": null, + "status": "pending collection", + "notes": "Superstar Packers load from Thiruvananthapuram to Coimbatore. Vehicle TN39AH9902, contact 9786720476. Freight: 12000. Superstar paid 10000 advance, transferred to driver. Driver paid 700 commission in cash before loading. Delivery completed. Paid driver 2000 balance. Driver fully settled. Pending from Superstar: 2000." + }, + { + "id": "load_072", + "date": "2026-05-31", + "shipper_id": "agarwal_packers_and_movers", + "vehicle_id": "tn39dv8142", + "from_city": "Thiruvananthapuram", + "to_city": "Bangalore", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 19000.0, + "advance_received": 17000.0, + "paid_to_driver": 17900.0, + "driver_freight": 17900.0, + "commission": 1100.0, + "pending_from_shipper": 2000.0, + "pending_to_driver": null, + "status": "delivered / pending collection", + "notes": "Agarwal Packers and Movers load from Thiruvananthapuram to Bangalore. Vehicle TN39DV8142, contact 7402215672. Freight: 19000. Agarwal advance: 17000. Load delivered. Driver paid: 15900 advance + 2000 balance = 17900 total. Driver fully settled. Commission: 1100. Pending from Agarwal: 2000." + }, + { + "id": "load_073", + "date": "2026-06-01", + "shipper_id": "crt_transport", + "vehicle_id": null, + "from_city": "Kollam", + "to_city": "Rajahmundry", + "via": null, + "load_type": null, + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "pending lead", + "notes": "CRT Transport load lead: Kollam \u2192 Rajahmundry, 5 ton machinery. No vehicle assigned yet. Awaiting freight details and vehicle assignment." + }, + { + "id": "load_074", + "date": "2026-06-01", + "shipper_id": "agarwal_packers_and_movers", + "vehicle_id": "ka01al8839", + "from_city": "Thiruvananthapuram", + "to_city": "Coimbatore", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 10500.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "assigned vehicle", + "notes": "Agarwal Packers and Movers load from Thiruvananthapuram to Coimbatore. Vehicle KA01AL8839 assigned, contact 9380018905. Freight: 10500. Vehicle assigned, awaiting loading and payment details." + }, + { + "id": "load_075", + "date": "2026-06-01", + "shipper_id": "superstar", + "vehicle_id": "mh02gh3248", + "from_city": "Thiruvananthapuram", + "to_city": "Bangalore", + "via": "['Cochin', 'Kozhikode']", + "load_type": "10 ft open", + "item": null, + "freight_charged": 13000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "assigned vehicle", + "notes": "Superstar load from Thiruvananthapuram to Bangalore via Cochin and Kozhikode (multi-drop). Vehicle MH02GH3248 assigned, 10ft open, contact 9284187529. Freight: 13000. Vehicle assigned, awaiting loading and payment details." + }, + { + "id": "load_076", + "date": "2026-06-01", + "shipper_id": "century_polymers", + "vehicle_id": "mh46bb0041", + "from_city": "Thiruvananthapuram", + "to_city": "Hyderabad", + "via": "['Gaganpahad', 'Tolichoki', 'Himayathnagar', 'Sheikpet']", + "load_type": null, + "item": null, + "freight_charged": 37000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 1500.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "Century Polymers load TVPM to Hyderabad multi-drop. MH46BB0041. Freight 37000. Commission 1500." + }, + { + "id": "load_077", + "date": "2026-06-01", + "shipper_id": "vehicle_available_/_looking_for_load", + "vehicle_id": "tn30cw9849", + "from_city": "Thiruvananthapuram", + "to_city": null, + "via": null, + "load_type": "9 ft open", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "available vehicle", + "notes": "Vehicle lead: TN30CW9849, 9ft open, in TVPM parking, contact 9944266246. Looking for load." + }, + { + "id": "load_078", + "date": "2026-06-01", + "shipper_id": "vehicle_available_/_looking_for_load", + "vehicle_id": "tn66am1928", + "from_city": "Thiruvananthapuram", + "to_city": null, + "via": null, + "load_type": "10 ft open", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "available vehicle", + "notes": "Vehicle lead: TN66AM1928, 10ft open, in TVPM parking, contact 9600028862. Looking for load." + }, + { + "id": "load_079", + "date": "2026-06-01", + "shipper_id": "ktc", + "vehicle_id": "tn66am3928", + "from_city": "Thiruvananthapuram", + "to_city": "Tirur", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 7500.0, + "advance_received": 6500.0, + "paid_to_driver": 6000.0, + "driver_freight": 7000.0, + "commission": 500.0, + "pending_from_shipper": 1000.0, + "pending_to_driver": 1000.0, + "status": "loaded / in transit", + "notes": "KTC load from Thiruvananthapuram to Tirur. Vehicle TN66AM3928, contact 9600028862. Freight: 7500. Advance received from KTC: 6500. Driver freight: 7000. Paid driver advance: 6000. Commission: 500. Pending from KTC: 1000. Pending to driver: 1000." + }, + { + "id": "load_080", + "date": "2026-06-01", + "shipper_id": "mohamed_anas", + "vehicle_id": "tn77f3427", + "from_city": "Thiruvananthapuram", + "to_city": "Madurai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 12000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 500.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "Mohamed Anas load from Thiruvananthapuram to Madurai. Vehicle TN77F3427, contact 9488151108. Freight: 12000. Vehicle reached and settled. Commission received: 500 cash from driver." + }, + { + "id": "load_081", + "date": "2026-06-01", + "shipper_id": "hirosh_roadways", + "vehicle_id": "tn83md7328", + "from_city": "Thiruvananthapuram", + "to_city": "Hyderabad", + "via": "['Gundoor']", + "load_type": null, + "item": null, + "freight_charged": 15000.0, + "advance_received": null, + "paid_to_driver": 15000.0, + "driver_freight": 15000.0, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "settled", + "notes": "Hirosh Roadways load from Thiruvananthapuram to Hyderabad via Gundoor. Vehicle TN83MD7328 (Mayandi), contact 9488559705. Freight: 15000. Paid driver: 15000. Commission: 0 (commission handled separately or no commission). Load settled." + }, + { + "id": "load_082", + "date": "2026-06-01", + "shipper_id": "vehicle_available_/_looking_for_load", + "vehicle_id": "tn93e6166", + "from_city": "Thiruvananthapuram", + "to_city": null, + "via": null, + "load_type": "24 ft open, 12 ton capacity", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "available vehicle", + "notes": "Vehicle lead: TN93E6166, 24ft open, 12 ton capacity, in TVPM parking, contact 8848672650. Looking for load." + }, + { + "id": "load_083", + "date": "2026-06-01", + "shipper_id": "mohamed_anas", + "vehicle_id": "tn99ac1128", + "from_city": "Thiruvananthapuram", + "to_city": "Chennai", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 13000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 700.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "loaded / in transit", + "notes": "Mohamed Anas load from Thiruvananthapuram to Chennai. Vehicle TN99AC1128 assigned, contact 9344685852. Freight: 13000. Driver paid 700 commission in cash after loading. Commission received. Awaiting POD and balance collection from shipper." + }, + { + "id": "load_084", + "date": "2026-06-01", + "shipper_id": null, + "vehicle_id": null, + "from_city": "Changanassery", + "to_city": "Salem", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 16000.0, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": 900.0, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "commission received", + "notes": "Load from Changanassery to Salem. Vehicle number not remembered. Freight: 16000. Driver paid 900 cash commission. Commission received, shipper handled driver payment." + }, + { + "id": "load_085", + "date": "2026-06-02", + "shipper_id": "nair", + "vehicle_id": null, + "from_city": "Thiruvananthapuram", + "to_city": "Udupi", + "via": null, + "load_type": "20 ft container", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "pending lead", + "notes": "Nair load from Thiruvananthapuram to Udupi. 20ft container, household goods, 3 tons. Freight mentioned as 0. No vehicle assigned yet. Awaiting freight details, vehicle assignment, and shipper confirmation." + }, + { + "id": "load_086", + "date": "2026-06-02", + "shipper_id": "silverstar", + "vehicle_id": "mh12qw4555", + "from_city": "Hyderabad", + "to_city": "Thiruvananthapuram", + "via": null, + "load_type": null, + "item": null, + "freight_charged": 36000.0, + "advance_received": 33000.0, + "paid_to_driver": 30900.0, + "driver_freight": 35400.0, + "commission": 2100.0, + "pending_from_shipper": 3000.0, + "pending_to_driver": null, + "status": "delivered / pending collection", + "notes": "Silverstar load from Hyderabad to Thiruvananthapuram (multi-drop: Tvm 1 item, Mannar 3 items). Vehicle MH12QW4555, contact 9591071127. Freight: 36000. Loading/unloading charges: 600 (debited to shipper). Driver freight: 35400. Total received from shipper: 33000 (32400 + 600 charges). Pending from shipper: 3000. Driver fully paid: 30900. Driver settled. Commission: 2100." + }, + { + "id": "load_087", + "date": "2026-06-02", + "shipper_id": "vehicle_available_/_looking_for_load", + "vehicle_id": "tn16f0565", + "from_city": "Thiruvananthapuram", + "to_city": null, + "via": null, + "load_type": "8 ft open", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "available vehicle", + "notes": "Vehicle lead: TN16F0565, 8ft open, in TVPM parking, contact 9894040247. Looking for load." + }, + { + "id": "load_088", + "date": "2026-06-02", + "shipper_id": "vehicle_available_/_looking_for_load", + "vehicle_id": "tn24ah6898", + "from_city": "Thiruvananthapuram", + "to_city": null, + "via": null, + "load_type": "20 ft open", + "item": null, + "freight_charged": null, + "advance_received": null, + "paid_to_driver": null, + "driver_freight": null, + "commission": null, + "pending_from_shipper": null, + "pending_to_driver": null, + "status": "available vehicle", + "notes": "Vehicle lead: TN24AH6898, 20ft open, in TVPM parking, contact 9986953098. Looking for load." + } + ] +} \ No newline at end of file diff --git a/webapp/.env.example b/webapp/.env.example new file mode 100644 index 0000000..23a28b6 --- /dev/null +++ b/webapp/.env.example @@ -0,0 +1,9 @@ +NODE_ENV=development +PORT=3000 +APP_URL=http://localhost:3000 + +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_KEY=your-anon-key +SUPABASE_SERVICE_KEY=your-service-role-key + +SESSION_SECRET=change-this-to-a-random-string-in-production diff --git a/webapp/Dockerfile b/webapp/Dockerfile new file mode 100644 index 0000000..4113f8b --- /dev/null +++ b/webapp/Dockerfile @@ -0,0 +1,24 @@ +FROM node:20-alpine + +WORKDIR /app + +# Install dependencies +COPY package*.json ./ +RUN npm ci --only=production + +# Copy source +COPY . . + +# Create non-root user +RUN addgroup -g 1001 -S appgroup && \ + adduser -S appuser -u 1001 -G appgroup && \ + chown -R appuser:appgroup /app + +USER appuser + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD wget -qO- http://localhost:3000/health || exit 1 + +CMD ["node", "src/server.js"] diff --git a/webapp/package.json b/webapp/package.json new file mode 100644 index 0000000..24108ee --- /dev/null +++ b/webapp/package.json @@ -0,0 +1,25 @@ +{ + "name": "freightdesk", + "version": "1.0.0", + "description": "FreightDesk — Freight Forwarding Commission Agent Management", + "main": "src/server.js", + "scripts": { + "start": "node src/server.js", + "dev": "node --watch src/server.js", + "seed": "node seed.js" + }, + "keywords": ["freight", "logistics", "commission", "agent", "india"], + "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" + } +} diff --git a/webapp/seed.js b/webapp/seed.js new file mode 100644 index 0000000..db570e0 --- /dev/null +++ b/webapp/seed.js @@ -0,0 +1,122 @@ +#!/usr/bin/env node +/** + * FreightDesk Seed Script + * Reads supabase/seed_data.json and inserts into Supabase + * Also runs the schema migrations + */ + +require('dotenv').config(); +const { createClient } = require('@supabase/supabase-js'); +const fs = require('fs'); +const path = require('path'); + +const supabaseUrl = process.env.SUPABASE_URL; +const supabaseKey = process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_KEY; + +if (!supabaseUrl || !supabaseKey) { + console.error('Missing SUPABASE_URL or SUPABASE_KEY in .env'); + process.exit(1); +} + +const supabase = createClient(supabaseUrl, supabaseKey); + +async function runMigrations() { + const migrationsDir = path.join(__dirname, '..', 'supabase', 'migrations'); + const files = fs.readdirSync(migrationsDir).filter(f => f.endsWith('.sql')).sort(); + + for (const file of files) { + if (file.includes('seed')) continue; // Skip seed placeholder + console.log(`Running migration: ${file}`); + const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf8'); + + // Split by semicolons and execute each statement + const statements = sql.split(';').filter(s => s.trim() && !s.trim().startsWith('--')); + for (const stmt of statements) { + if (stmt.trim()) { + const { error } = await supabase.rpc('exec_sql', { sql: stmt.trim() }); + if (error && !error.message.includes('already exists')) { + console.warn(` Warning: ${error.message}`); + } + } + } + console.log(` Done.`); + } +} + +async function seedData() { + const seedFile = path.join(__dirname, '..', 'supabase', 'seed_data.json'); + if (!fs.existsSync(seedFile)) { + console.log('No seed_data.json found, skipping seed.'); + return; + } + + const seed = JSON.parse(fs.readFileSync(seedFile, 'utf-8')); + + // Insert shippers + if (seed.shippers && seed.shippers.length > 0) { + console.log(`Inserting ${seed.shippers.length} shippers...`); + const { error } = await supabase.from('shippers').upsert(seed.shippers, { onConflict: 'id' }); + if (error) console.error('Shippers error:', error.message); + else console.log(' Done.'); + } + + // Insert vehicles + if (seed.vehicles && seed.vehicles.length > 0) { + console.log(`Inserting ${seed.vehicles.length} vehicles...`); + const { error } = await supabase.from('vehicles').upsert(seed.vehicles, { onConflict: 'id' }); + if (error) console.error('Vehicles error:', error.message); + else console.log(' Done.'); + } + + // Insert loads in batches + if (seed.loads && seed.loads.length > 0) { + console.log(`Inserting ${seed.loads.length} loads...`); + const batchSize = 20; + for (let i = 0; i < seed.loads.length; i += batchSize) { + const batch = seed.loads.slice(i, i + batchSize); + const { error } = await supabase.from('loads').upsert(batch, { onConflict: 'id' }); + if (error) console.error(`Loads batch error (${i}-${i + batchSize}):`, error.message); + else console.log(` Batch ${Math.floor(i / batchSize) + 1} done.`); + } + } + + // Update shipper totals + console.log('Updating shipper totals...'); + for (const shipper of seed.shippers) { + const shipperLoads = seed.loads.filter(l => l.shipper_id === shipper.id); + const totals = shipperLoads.reduce((acc, l) => ({ + freight: acc.freight + (l.freight_charged || 0), + commission: acc.commission + (l.commission || 0), + pending: acc.pending + (l.pending_from_shipper || 0), + }), { freight: 0, commission: 0, pending: 0 }); + + await supabase.from('shippers').update({ + total_freight: totals.freight, + total_commission: totals.commission, + pending_amount: totals.pending, + }).eq('id', shipper.id); + } + console.log(' Done.'); +} + +async function main() { + console.log('=== FreightDesk Seeder ===\n'); + + try { + console.log('Step 1: Running migrations...'); + // Note: For actual migration execution, use Supabase CLI or Dashboard SQL editor + // This is a simplified approach - run migrations manually in Supabase SQL Editor + console.log(' Please run the SQL migrations in Supabase SQL Editor first:'); + console.log(' - supabase/migrations/001_initial_schema.sql\n'); + + console.log('Step 2: Seeding data...'); + await seedData(); + + console.log('\n=== Seed complete! ==='); + } catch (err) { + console.error('Fatal error:', err); + process.exit(1); + } +} + +main(); diff --git a/webapp/src/config/constants.js b/webapp/src/config/constants.js new file mode 100644 index 0000000..767f5bf --- /dev/null +++ b/webapp/src/config/constants.js @@ -0,0 +1,52 @@ +module.exports = { + LOAD_STATUSES: [ + 'pending lead', + 'assigned vehicle', + 'assigned', + 'loaded / in transit', + 'delivered / pending collection', + 'pending collection', + 'partially pending', + 'fully pending from shipper', + 'settled', + 'commission received', + 'commission adjusted', + 'commission due', + 'reconciled', + 'completed', + 'handled directly by shipper', + 'partial', + 'available vehicle', + 'cancelled', + ], + VEHICLE_TYPES: ['open', 'closed', 'container', 'flatbed', 'tanker', 'refrigerated', 'mini', '10ft', '17ft', '20ft', '24ft'], + LOAD_TYPES: ['Full load', 'Part load', 'Mixed', 'Household', 'Container'], + PAYMENT_METHODS: ['bank_transfer', 'upi', 'cash', 'cheque', 'online'], + PAYMENT_TYPES: [ + { value: 'advance', label: 'Advance' }, + { value: 'balance', label: 'Balance' }, + { value: 'commission', label: 'Commission' }, + { value: 'driver_payment', label: 'Driver Payment' }, + { value: 'other', label: 'Other' }, + ], + CITIES: [ + 'Thiruvananthapuram', 'Kollam', 'Kochi', 'Cochin', 'Thrissur', 'Palakkad', + 'Kozhikode', 'Kannur', 'Mangalore', 'Bangalore', 'Chennai', 'Coimbatore', + 'Madurai', 'Salem', 'Erode', 'Tirupur', 'Vellore', 'Tirupati', + 'Hyderabad', 'Vijayawada', 'Visakhapatnam', 'Mumbai', 'Pune', 'Nagpur', + 'Delhi', 'Noida', 'Gurugram', 'Ahmedabad', 'Surat', 'Jaipur', 'Lucknow', + 'Kolkata', 'Bhopal', 'Indore', 'Patna', 'Ranchi', 'Guwahati', + 'Guruvayoor', 'Kalpetta', 'Theni', 'Tiruchirappalli', 'Tanjavur', + 'Thiruvalla', 'Pathanamthitta', 'Adoor', 'Kottarakara', 'Changanassery', + 'Perumathura', 'Marapalam', 'Udupi', 'Mysore', 'Hubli', 'Nashik', + 'Rajahmundry', 'Gundoor', 'Gaganpahad', 'Tolichoki', 'Himayathnagar', + 'Sheikpet', 'Ukkadam', 'Ganapati', 'Shabarimala', 'Triplicane', + ], + STATES: { + KL: 'Kerala', TN: 'Tamil Nadu', KA: 'Karnataka', AP: 'Andhra Pradesh', + TS: 'Telangana', MH: 'Maharashtra', GJ: 'Gujarat', RJ: 'Rajasthan', + UP: 'Uttar Pradesh', DL: 'Delhi', HR: 'Haryana', PB: 'Punjab', + WB: 'West Bengal', MP: 'Madhya Pradesh', CG: 'Chhattisgarh', + JH: 'Jharkhand', BR: 'Bihar', OR: 'Odisha', GA: 'Goa', + }, +}; diff --git a/webapp/src/config/env.js b/webapp/src/config/env.js new file mode 100644 index 0000000..b15caa6 --- /dev/null +++ b/webapp/src/config/env.js @@ -0,0 +1,13 @@ +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, + }, +}; diff --git a/webapp/src/lib/india.js b/webapp/src/lib/india.js new file mode 100644 index 0000000..bed3efd --- /dev/null +++ b/webapp/src/lib/india.js @@ -0,0 +1,92 @@ +// India-specific utilities for FreightDesk + +const STATES = { + KL:'Kerala',TN:'Tamil Nadu',KA:'Karnataka',AP:'Andhra Pradesh',TS:'Telangana', + MH:'Maharashtra',GJ:'Gujarat',RJ:'Rajasthan',UP:'Uttar Pradesh',DL:'Delhi', + HR:'Haryana',PB:'Punjab',WB:'West Bengal',MP:'Madhya Pradesh',GA:'Goa', +}; + +function formatINR(n) { + if (n === null || n === undefined || isNaN(n)) return '—'; + return '₹' + parseFloat(n).toLocaleString('en-IN'); +} + +function formatINRShort(n) { + if (n === null || n === undefined || isNaN(n)) return '—'; + const num = parseFloat(n); + if (num >= 100000) return '₹' + (num / 100000).toFixed(1) + 'L'; + if (num >= 1000) return '₹' + (num / 1000).toFixed(1) + 'K'; + return '₹' + num.toLocaleString('en-IN'); +} + +function formatDate(dateStr) { + if (!dateStr) return '—'; + const d = new Date(dateStr); + if (isNaN(d.getTime())) return dateStr; + return d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' }); +} + +function formatDateShort(dateStr) { + if (!dateStr) return '—'; + const d = new Date(dateStr); + if (isNaN(d.getTime())) return dateStr; + return d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short' }); +} + +function validateVehicleNumber(number) { + if (!number) return { valid: false }; + const cleaned = number.replace(/[\s\-\.]/g, '').toUpperCase(); + if (!/^[A-Z]{2}\d{1,2}[A-Z]{1,3}\d{4}$/.test(cleaned)) return { valid: false }; + const sc = cleaned.substring(0, 2); + return { + valid: true, + state_code: sc, + state_name: STATES[sc] || 'Unknown', + formatted: `${cleaned.substring(0,2)} ${cleaned.substring(2,4)} ${cleaned.substring(4,cleaned.length-4)} ${cleaned.slice(-4)}`, + }; +} + +function getStatusColor(status) { + const colors = { + 'settled': 'green', + 'completed': 'green', + 'commission received': 'green', + 'reconciled': 'green', + 'handled directly by shipper': 'green', + 'loaded / in transit': 'blue', + 'assigned': 'blue', + 'assigned vehicle': 'blue', + 'pending collection': 'orange', + 'partially pending': 'orange', + 'fully pending from shipper': 'orange', + 'delivered / pending collection': 'orange', + 'commission due': 'orange', + 'pending lead': 'gray', + 'partial': 'gray', + 'available vehicle': 'gray', + 'commission adjusted': 'purple', + 'cancelled': 'red', + }; + return colors[status] || 'gray'; +} + +function calcCommission(freight, paidToDriver) { + if (!freight || !paidToDriver) return null; + return freight - paidToDriver; +} + +function calcPendingFromShipper(freight, advance) { + if (!freight) return null; + return freight - (advance || 0); +} + +function calcPendingToDriver(driverFreight, paid) { + if (!driverFreight) return null; + return driverFreight - (paid || 0); +} + +module.exports = { + STATES, formatINR, formatINRShort, formatDate, formatDateShort, + validateVehicleNumber, getStatusColor, + calcCommission, calcPendingFromShipper, calcPendingToDriver, +}; diff --git a/webapp/src/middleware/auth.js b/webapp/src/middleware/auth.js new file mode 100644 index 0000000..75776a1 --- /dev/null +++ b/webapp/src/middleware/auth.js @@ -0,0 +1,30 @@ +function requireAuth(req, res, next) { + if (req.session && req.session.user) { + res.locals.user = req.session.user; + return next(); + } + if (req.accepts('html')) { + res.redirect('/login'); + } else { + res.status(401).json({ error: 'Authentication required' }); + } +} + +function requireRole(...roles) { + return (req, res, next) => { + if (!req.session || !req.session.user) { + if (req.accepts('html')) return res.redirect('/login'); + return res.status(401).json({ error: 'Authentication required' }); + } + if (roles.includes(req.session.user.role) || req.session.user.role === 'admin') { + return next(); + } + if (req.accepts('html')) { + res.status(403).render('pages/403'); + } else { + res.status(403).json({ error: 'Forbidden' }); + } + }; +} + +module.exports = { requireAuth, requireRole }; diff --git a/webapp/src/middleware/security.js b/webapp/src/middleware/security.js new file mode 100644 index 0000000..28ecd86 --- /dev/null +++ b/webapp/src/middleware/security.js @@ -0,0 +1,62 @@ +const crypto = require('crypto'); + +function generateCSRFToken() { + return crypto.randomBytes(32).toString('hex'); +} + +function setupCSRF(req, res, next) { + if (!req.session._csrf) { + req.session._csrf = generateCSRFToken(); + } + res.locals._csrf = req.session._csrf; + next(); +} + +function validateCSRF(req, res, next) { + if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next(); + const token = req.body?._csrf || req.headers['x-csrf-token'] || req.query._csrf; + if (!token || token !== req.session._csrf) { + return res.status(403).send('Invalid CSRF token. Please go back and try again.'); + } + next(); +} + +function sanitizeInput(str) { + if (typeof str !== 'string') return str; + return str + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); +} + +function sanitizeBody(req, res, next) { + if (req.body && typeof req.body === 'object') { + for (const key of Object.keys(req.body)) { + if (typeof req.body[key] === 'string') { + req.body[key] = sanitizeInput(req.body[key]).trim(); + } + } + } + next(); +} + +function requestLogger(req, res, next) { + const start = Date.now(); + res.on('finish', () => { + const duration = Date.now() - start; + const status = res.statusCode; + const icon = status >= 500 ? '❌' : status >= 400 ? '⚠️' : status >= 300 ? '↪️' : '✓'; + console.log(`${icon} ${req.method} ${req.url} ${status} ${duration}ms`); + }); + next(); +} + +function asyncHandler(fn) { + return (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +} + +module.exports = { setupCSRF, validateCSRF, sanitizeBody, requestLogger, asyncHandler, generateCSRFToken }; diff --git a/webapp/src/public/css/style.css b/webapp/src/public/css/style.css new file mode 100644 index 0000000..b9a5d98 --- /dev/null +++ b/webapp/src/public/css/style.css @@ -0,0 +1,767 @@ +/* ============================================================ + FreightDesk — Government App Aesthetic + Tricolor: Saffron #FF9933 | White #FFFFFF | Green #138808 + Navy: #000080 (Ashoka Chakra blue) + ============================================================ */ + +:root { + /* Colors */ + --saffron: #FF9933; + --saffron-dark: #E68A2E; + --green: #138808; + --green-dark: #0F6B06; + --navy: #000080; + --navy-light: #1a1a9a; + --white: #FFFFFF; + --bg: #f5f5f0; + --bg-card: #FFFFFF; + --bg-sidebar: #000080; + --text: #1a1a2e; + --text-muted: #666; + --text-light: #999; + --border: #e0ddd5; + --danger: #dc3545; + --warning: #f0ad4e; + --success: #138808; + --info: #17a2b8; + --shadow: 0 2px 8px rgba(0,0,0,0.08); + --shadow-lg: 0 4px 16px rgba(0,0,0,0.12); + --radius: 8px; + --radius-lg: 12px; + --font-hi: 'Noto Sans Devanagari', sans-serif; + --font-en: 'Inter', -apple-system, sans-serif; +} + +[data-theme="dark"] { + --bg: #1a1a2e; + --bg-card: #252540; + --bg-sidebar: #0a0a2e; + --text: #e8e8f0; + --text-muted: #a0a0b0; + --text-light: #707080; + --border: #3a3a55; + --shadow: 0 2px 8px rgba(0,0,0,0.3); + --shadow-lg: 0 4px 16px rgba(0,0,0,0.4); +} + +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: var(--font-en); + background: var(--bg); + color: var(--text); + line-height: 1.6; + min-height: 100vh; +} + +/* ============================================================ + TOPBAR + ============================================================ */ +.topbar { + background: var(--navy); + color: var(--white); + padding: 0 24px; + height: 64px; + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; + z-index: 100; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); +} + +.topbar::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 3px; + background: linear-gradient(90deg, var(--saffron) 33%, var(--white) 33%, var(--white) 66%, var(--green) 66%); +} + +.topbar-brand { + display: flex; + align-items: center; + gap: 12px; +} + +.emblem { + font-size: 28px; + line-height: 1; +} + +.brand-text { + display: flex; + flex-direction: column; +} + +.brand-hi { + font-family: var(--font-hi); + font-size: 16px; + font-weight: 700; + line-height: 1.2; +} + +.brand-en { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + opacity: 0.9; +} + +.brand-tagline { + font-size: 9px; + opacity: 0.7; + letter-spacing: 0.3px; +} + +.topbar-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.user-name { + font-size: 14px; + opacity: 0.9; +} + +/* ============================================================ + LAYOUT + ============================================================ */ +.layout { + display: flex; + min-height: calc(100vh - 64px); +} + +.sidebar { + width: 240px; + background: var(--bg-sidebar); + color: var(--white); + padding: 20px 0; + flex-shrink: 0; + min-height: calc(100vh - 64px); +} + +.sidebar-section { + margin-bottom: 24px; +} + +.sidebar-title { + display: block; + padding: 8px 20px; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1px; + opacity: 0.5; + font-weight: 600; +} + +.sidebar-link { + display: block; + padding: 10px 20px; + color: rgba(255,255,255,0.8); + text-decoration: none; + font-size: 14px; + transition: all 0.2s; + border-left: 3px solid transparent; +} + +.sidebar-link:hover { + background: rgba(255,255,255,0.1); + color: var(--white); +} + +.sidebar-link.active { + background: rgba(255,255,255,0.15); + color: var(--white); + border-left-color: var(--saffron); + font-weight: 600; +} + +.content { + flex: 1; + padding: 24px; + max-width: 100%; +} + +/* ============================================================ + CARDS + ============================================================ */ +.card { + background: var(--bg-card); + border-radius: var(--radius-lg); + box-shadow: var(--shadow); + border: 1px solid var(--border); + overflow: hidden; +} + +.card-header { + padding: 16px 20px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; +} + +.card-title { + font-size: 16px; + font-weight: 600; +} + +.card-body { + padding: 20px; +} + +/* ============================================================ + STATS + ============================================================ */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.stat-card { + background: var(--bg-card); + border-radius: var(--radius-lg); + padding: 20px; + display: flex; + align-items: center; + gap: 16px; + box-shadow: var(--shadow); + border: 1px solid var(--border); + border-left: 4px solid var(--navy); +} + +.stat-card.stat-primary { border-left-color: var(--navy); } +.stat-card.stat-success { border-left-color: var(--green); } +.stat-card.stat-warning { border-left-color: var(--saffron); } +.stat-card.stat-info { border-left-color: var(--info); } + +.stat-icon { + font-size: 32px; + line-height: 1; +} + +.stat-info { + display: flex; + flex-direction: column; +} + +.stat-value { + font-size: 22px; + font-weight: 700; + line-height: 1.2; +} + +.stat-label { + font-size: 12px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* ============================================================ + TABLES + ============================================================ */ +.table-responsive { + overflow-x: auto; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th { + text-align: left; + padding: 12px 16px; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); + border-bottom: 2px solid var(--border); + font-weight: 600; +} + +.table td { + padding: 12px 16px; + border-bottom: 1px solid var(--border); + font-size: 14px; +} + +.table tr:hover td { + background: rgba(0,0,128,0.02); +} + +/* ============================================================ + BADGES + ============================================================ */ +.badge { + display: inline-block; + padding: 3px 10px; + border-radius: 20px; + font-size: 11px; + font-weight: 600; + text-transform: capitalize; +} + +.badge-green { background: rgba(19,136,8,0.1); color: var(--green); } +.badge-blue { background: rgba(0,0,128,0.1); color: var(--navy); } +.badge-orange { background: rgba(255,153,51,0.15); color: #c47000; } +.badge-gray { background: rgba(128,128,128,0.1); color: #666; } +.badge-red { background: rgba(220,53,69,0.1); color: var(--danger); } +.badge-purple { background: rgba(128,0,128,0.1); color: #800080; } +.badge-success { background: rgba(19,136,8,0.1); color: var(--green); } +.badge-warning { background: rgba(240,173,78,0.15); color: #c47000; } +.badge-danger { background: rgba(220,53,69,0.1); color: var(--danger); } +.badge-info { background: rgba(23,162,184,0.1); color: var(--info); } + +/* ============================================================ + BUTTONS + ============================================================ */ +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: var(--radius); + font-size: 14px; + font-weight: 500; + text-decoration: none; + border: none; + cursor: pointer; + transition: all 0.2s; + font-family: var(--font-en); +} + +.btn-primary { + background: var(--navy); + color: var(--white); +} +.btn-primary:hover { background: var(--navy-light); } + +.btn-secondary { + background: var(--saffron); + color: var(--white); +} +.btn-secondary:hover { background: var(--saffron-dark); } + +.btn-outline { + background: transparent; + color: var(--navy); + border: 1px solid var(--navy); +} +.btn-outline:hover { background: rgba(0,0,128,0.05); } + +.btn-danger { + background: var(--danger); + color: var(--white); +} + +.btn-sm { + padding: 5px 12px; + font-size: 12px; +} + +.btn-block { + width: 100%; + justify-content: center; +} + +.btn-icon { + background: none; + border: none; + font-size: 20px; + cursor: pointer; + padding: 4px; + border-radius: 4px; +} + +/* ============================================================ + FORMS + ============================================================ */ +.form-group { + margin-bottom: 16px; + flex: 1; +} + +.form-label { + display: block; + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.form-input { + width: 100%; + padding: 10px 14px; + border: 1px solid var(--border); + border-radius: var(--radius); + font-size: 14px; + font-family: var(--font-en); + background: var(--bg-card); + color: var(--text); + transition: border-color 0.2s; +} + +.form-input:focus { + outline: none; + border-color: var(--navy); + box-shadow: 0 0 0 3px rgba(0,0,128,0.1); +} + +.form-row { + display: flex; + gap: 12px; + align-items: flex-end; +} + +.form-actions { + display: flex; + gap: 12px; + margin-top: 20px; +} + +.section-title { + font-size: 14px; + font-weight: 600; + color: var(--navy); + margin: 20px 0 12px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border); +} + +/* ============================================================ + FILTER BAR + ============================================================ */ +.filter-bar { + display: flex; + gap: 12px; + align-items: flex-end; + flex-wrap: wrap; +} + +.filter-bar .form-group { + margin-bottom: 0; +} + +/* ============================================================ + PAGE HEADER + ============================================================ */ +.page-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; + flex-wrap: wrap; + gap: 12px; +} + +.page-title { + font-size: 24px; + font-weight: 700; +} + +.page-subtitle { + font-size: 14px; + color: var(--text-muted); + margin-top: 2px; +} + +.page-actions { + display: flex; + gap: 8px; +} + +/* ============================================================ + GRID + ============================================================ */ +.grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; +} + +@media (max-width: 900px) { + .grid-2 { grid-template-columns: 1fr; } + .sidebar { display: none; } + .stats-grid { grid-template-columns: 1fr 1fr; } +} + +@media (max-width: 600px) { + .stats-grid { grid-template-columns: 1fr; } + .form-row { flex-direction: column; } + .filter-bar { flex-direction: column; } +} + +/* ============================================================ + DETAIL LIST + ============================================================ */ +.detail-list { + display: grid; + grid-template-columns: 140px 1fr; + gap: 8px 16px; +} + +.detail-list dt { + font-size: 12px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.3px; + font-weight: 600; +} + +.detail-list dd { + font-size: 14px; +} + +/* ============================================================ + STATUS GRID + ============================================================ */ +.status-grid { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.status-item { + display: flex; + align-items: center; + gap: 8px; +} + +.status-label { + font-size: 13px; + color: var(--text-muted); +} + +/* ============================================================ + ALERTS + ============================================================ */ +.alert { + padding: 12px 16px; + border-radius: var(--radius); + margin-bottom: 16px; + font-size: 14px; +} + +.alert-error { + background: rgba(220,53,69,0.1); + color: var(--danger); + border: 1px solid rgba(220,53,69,0.2); +} + +.alert-success { + background: rgba(19,136,8,0.1); + color: var(--green); + border: 1px solid rgba(19,136,8,0.2); +} + +/* ============================================================ + EMPTY STATE + ============================================================ */ +.empty-state { + text-align: center; + padding: 40px 20px; + color: var(--text-muted); + font-size: 14px; +} + +/* ============================================================ + LOGIN PAGE + ============================================================ */ +.auth-page { + background: linear-gradient(135deg, var(--navy) 0%, #1a1a4a 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +.login-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: 20px; +} + +.login-container { + background: var(--bg-card); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + width: 100%; + max-width: 420px; + overflow: hidden; +} + +.login-header { + background: var(--navy); + color: var(--white); + padding: 32px 24px 24px; + text-align: center; + position: relative; +} + +.login-header::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 4px; + background: linear-gradient(90deg, var(--saffron) 33%, var(--white) 33%, var(--white) 66%, var(--green) 66%); +} + +.login-emblem { + font-size: 48px; + margin-bottom: 12px; +} + +.login-title-hi { + font-family: var(--font-hi); + font-size: 24px; + font-weight: 700; +} + +.login-title-en { + font-size: 14px; + font-weight: 600; + opacity: 0.9; + text-transform: uppercase; + letter-spacing: 1px; +} + +.login-tagline { + font-family: var(--font-hi); + font-size: 13px; + opacity: 0.8; + margin-top: 8px; +} + +.login-tagline-en { + font-size: 11px; + opacity: 0.6; + margin-top: 4px; +} + +.login-form { + padding: 24px; +} + +.login-footer { + padding: 16px 24px; + text-align: center; + border-top: 1px solid var(--border); + font-size: 11px; + color: var(--text-muted); +} + +/* ============================================================ + ERROR PAGES + ============================================================ */ +.error-page { + text-align: center; + padding: 80px 20px; +} + +.error-code { + font-size: 120px; + font-weight: 700; + color: var(--navy); + line-height: 1; + opacity: 0.3; +} + +.error-page h1 { + font-size: 24px; + margin: 16px 0 8px; +} + +.error-page p { + color: var(--text-muted); +} + +/* ============================================================ + FOOTER + ============================================================ */ +.govt-footer { + text-align: center; + padding: 24px; + border-top: 1px solid var(--border); + margin-top: 40px; + font-size: 12px; + color: var(--text-muted); +} + +.footer-tricolor { + display: flex; + height: 4px; + border-radius: 2px; + overflow: hidden; + margin: 0 auto 16px; + max-width: 200px; +} + +.footer-tricolor span:nth-child(1) { background: var(--saffron); flex: 1; } +.footer-tricolor span:nth-child(2) { background: var(--white); flex: 1; border-left: 1px solid #ddd; border-right: 1px solid #ddd; } +.footer-tricolor span:nth-child(3) { background: var(--green); flex: 1; } + +.footer-muted { + opacity: 0.6; + margin-top: 4px; +} + +/* ============================================================ + PARSE RESULT + ============================================================ */ +.parse-result { + background: rgba(0,0,128,0.03); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px; +} + +.parse-fields { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin-top: 12px; +} + +.parse-field { + display: flex; + justify-content: space-between; + padding: 6px 10px; + background: var(--bg-card); + border-radius: 4px; + font-size: 13px; +} + +.parse-key { + color: var(--text-muted); + font-weight: 500; +} + +.parse-val { + font-weight: 600; +} + +/* ============================================================ + UTILITIES + ============================================================ */ +.mt-2 { margin-top: 8px; } +.mt-3 { margin-top: 12px; } +.mt-4 { margin-top: 16px; } +.mb-4 { margin-bottom: 16px; } +.text-bold { font-weight: 700; } +.text-success { color: var(--green); } +.text-danger { color: var(--danger); } +.text-warning { color: #c47000; } +.text-muted { color: var(--text-muted); } +.text-center { text-align: center; } diff --git a/webapp/src/public/js/app.js b/webapp/src/public/js/app.js new file mode 100644 index 0000000..47ecc64 --- /dev/null +++ b/webapp/src/public/js/app.js @@ -0,0 +1,59 @@ +// FreightDesk — Client-side JavaScript + +// Theme toggle +function toggleTheme() { + const html = document.documentElement; + const current = html.getAttribute('data-theme'); + const next = current === 'dark' ? 'light' : 'dark'; + html.setAttribute('data-theme', next); + localStorage.setItem('fd-theme', next); +} + +// Restore theme +(function() { + const saved = localStorage.getItem('fd-theme'); + if (saved) document.documentElement.setAttribute('data-theme', saved); +})(); + +// Auto-hide alerts after 5 seconds +document.addEventListener('DOMContentLoaded', function() { + const alerts = document.querySelectorAll('.alert'); + alerts.forEach(function(alert) { + setTimeout(function() { + alert.style.opacity = '0'; + alert.style.transition = 'opacity 0.5s'; + setTimeout(function() { alert.remove(); }, 500); + }, 5000); + }); +}); + +// Confirm delete actions +document.querySelectorAll('form[onsubmit]').forEach(function(form) { + form.addEventListener('submit', function(e) { + const msg = form.getAttribute('onsubmit'); + if (msg && msg.includes('confirm')) { + const question = msg.match(/confirm\('(.+?)'\)/); + if (question && !confirm(question[1])) { + e.preventDefault(); + } + } + }); +}); + +// WhatsApp parser (inline function for form page) +// parseWhatsApp() and applyParsed() are defined inline in the form view + +// Format number as INR +function formatINR(num) { + if (num === null || num === undefined || isNaN(num)) return '—'; + return '₹' + parseFloat(num).toLocaleString('en-IN'); +} + +// Debounce helper +function debounce(fn, ms) { + let timer; + return function(...args) { + clearTimeout(timer); + timer = setTimeout(() => fn.apply(this, args), ms); + }; +} diff --git a/webapp/src/routes/dashboard.js b/webapp/src/routes/dashboard.js new file mode 100644 index 0000000..6566d10 --- /dev/null +++ b/webapp/src/routes/dashboard.js @@ -0,0 +1,67 @@ +const express = require('express'); +const router = express.Router(); +const supabase = require('../services/supabase'); +const { requireAuth } = require('../middleware/auth'); +const { asyncHandler } = require('../middleware/security'); +const { formatINR, getStatusColor } = require('../lib/india'); + +// GET / — Dashboard +router.get('/', requireAuth, asyncHandler(async (req, res) => { + // Fetch summary stats + const { data: loads } = await supabase.from('loads').select('*'); + const allLoads = loads || []; + + const totalFreight = allLoads.reduce((s, l) => s + (l.freight_charged || 0), 0); + const totalCommission = allLoads.reduce((s, l) => s + (l.commission || 0), 0); + const totalPendingShipper = allLoads.reduce((s, l) => s + (l.pending_from_shipper || 0), 0); + const totalPendingDriver = allLoads.reduce((s, l) => s + (l.pending_to_driver || 0), 0); + const settledCount = allLoads.filter(l => ['settled', 'completed', 'commission received', 'reconciled'].includes(l.status)).length; + + // Recent loads (last 10) + const recentLoads = allLoads + .filter(l => l.date) + .sort((a, b) => new Date(b.date) - new Date(a.date)) + .slice(0, 10); + + // Status breakdown + const statusCounts = {}; + for (const l of allLoads) { + const s = l.status || 'unknown'; + statusCounts[s] = (statusCounts[s] || 0) + 1; + } + + // Monthly data (last 6 months) + const monthlyData = {}; + for (const l of allLoads) { + if (!l.date) continue; + const d = new Date(l.date); + const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; + if (!monthlyData[key]) monthlyData[key] = { freight: 0, commission: 0, count: 0 }; + monthlyData[key].freight += l.freight_charged || 0; + monthlyData[key].commission += l.commission || 0; + monthlyData[key].count++; + } + + // Recent payments needed + const pendingCollection = allLoads + .filter(l => ['pending collection', 'partially pending', 'fully pending from shipper', 'delivered / pending collection'].includes(l.status)) + .slice(0, 5); + + res.render('pages/dashboard', { + stats: { + totalFreight, + totalCommission, + totalPendingShipper, + totalPendingDriver, + totalLoads: allLoads.length, + settledCount, + }, + recentLoads, + statusCounts, + monthlyData, + pendingCollection, + getStatusColor, + }); +})); + +module.exports = router; diff --git a/webapp/src/routes/loads.js b/webapp/src/routes/loads.js new file mode 100644 index 0000000..9de711a --- /dev/null +++ b/webapp/src/routes/loads.js @@ -0,0 +1,187 @@ +const express = require('express'); +const router = express.Router(); +const supabase = require('../services/supabase'); +const { requireAuth } = require('../middleware/auth'); +const { asyncHandler } = require('../middleware/security'); +const { formatINR, formatDate, getStatusColor, calcCommission, calcPendingFromShipper, calcPendingToDriver } = require('../lib/india'); +const { CITIES, LOAD_STATUSES, LOAD_TYPES } = require('../config/constants'); + +// GET /loads — List all loads +router.get('/', requireAuth, asyncHandler(async (req, res) => { + const { status, shipper, search, sort } = req.query; + + let query = supabase.from('loads').select('*, shipper:shippers(name), vehicle:vehicles(number)'); + + if (status) query = query.eq('status', status); + if (shipper) query = query.eq('shipper_id', shipper); + if (search) { + query = query.or(`from_city.ilike.%${search}%,to_city.ilike.%${search}%,notes.ilike.%${search}%`); + } + + const sortField = sort === 'freight' ? 'freight_charged' : 'date'; + query = query.order(sortField, { ascending: false, nullsFirst: false }).limit(100); + + const { data: loads } = await query; + + // Get all shippers for filter dropdown + const { data: shippers } = await supabase.from('shippers').select('id, name').order('name'); + + res.render('pages/loads/list', { + loads: loads || [], + shippers: shippers || [], + filters: { status, shipper, search, sort }, + LOAD_STATUSES, + getStatusColor, + }); +})); + +// GET /loads/new — New load form +router.get('/new', requireAuth, asyncHandler(async (req, res) => { + const { data: shippers } = await supabase.from('shippers').select('*').order('name'); + const { data: vehicles } = await supabase.from('vehicles').select('*').eq('is_active', true).order('number'); + res.render('pages/loads/form', { + load: {}, + shippers: shippers || [], + vehicles: vehicles || [], + LOAD_STATUSES, + LOAD_TYPES, + CITIES, + isEdit: false, + }); +})); + +// POST /loads — Create load +router.post('/', requireAuth, asyncHandler(async (req, res) => { + const body = { ...req.body }; + + // Auto-calculate + const freight = parseFloat(body.freight_charged) || 0; + const advance = parseFloat(body.advance_received) || 0; + const paid = parseFloat(body.paid_to_driver) || 0; + const driverFreight = parseFloat(body.driver_freight) || 0; + + if (!body.commission && freight && paid) { + body.commission = freight - paid; + } + if (!body.pending_from_shipper && freight) { + body.pending_from_shipper = freight - advance; + } + if (!body.pending_to_driver && driverFreight) { + body.pending_to_driver = driverFreight - paid; + } + + // Clean empty strings to null + for (const key of Object.keys(body)) { + if (body[key] === '') body[key] = null; + } + + const { data, error } = await supabase.from('insert_load').insert(body).select().single(); + if (error) { + console.error('Insert error:', error); + const { data: shippers } = await supabase.from('shippers').select('*').order('name'); + const { data: vehicles } = await supabase.from('vehicles').select('*').eq('is_active', true).order('number'); + return res.render('pages/loads/form', { + load: body, shippers: shippers || [], vehicles: vehicles || [], + LOAD_STATUSES, LOAD_TYPES, CITIES, isEdit: false, error: 'Failed to save load', + }); + } + + // Update shipper totals + if (body.shipper_id) { + await updateShipperTotals(body.shipper_id); + } + + res.redirect('/loads/' + encodeURIComponent(data.id)); +})); + +// GET /loads/:id — Load detail +router.get('/:id', requireAuth, asyncHandler(async (req, res) => { + const { data: load } = await supabase.from('loads') + .select('*, shipper:shippers(*), vehicle:vehicles(*)') + .eq('id', req.params.id) + .single(); + + if (!load) return res.status(404).render('pages/404'); + + const { data: payments } = await supabase.from('payments') + .select('*').eq('load_id', load.id).order('payment_date', { ascending: false }); + + res.render('pages/loads/detail', { + load, + payments: payments || [], + getStatusColor, + }); +})); + +// GET /loads/:id/edit — Edit form +router.get('/:id/edit', requireAuth, asyncHandler(async (req, res) => { + const { data: load } = await supabase.from('loads').select('*').eq('id', req.params.id).single(); + if (!load) return res.status(404).render('pages/404'); + + const { data: shippers } = await supabase.from('shippers').select('*').order('name'); + const { data: vehicles } = await supabase.from('vehicles').select('*').order('number'); + + res.render('pages/loads/form', { + load, + shippers: shippers || [], + vehicles: vehicles || [], + LOAD_STATUSES, + LOAD_TYPES, + CITIES, + isEdit: true, + }); +})); + +// POST /loads/:id — Update load +router.post('/:id', requireAuth, asyncHandler(async (req, res) => { + const body = { ...req.body }; + + const freight = parseFloat(body.freight_charged) || 0; + const advance = parseFloat(body.advance_received) || 0; + const paid = parseFloat(body.paid_to_driver) || 0; + const driverFreight = parseFloat(body.driver_freight) || 0; + + if (body.commission === '') body.commission = (freight && paid) ? freight - paid : null; + if (body.pending_from_shipper === '') body.pending_from_shipper = freight ? freight - advance : null; + if (body.pending_to_driver === '') body.pending_to_driver = driverFreight ? driverFreight - paid : null; + + for (const key of Object.keys(body)) { + if (body[key] === '') body[key] = null; + } + delete body._csrf; + + const { error } = await supabase.from('loads').update(body).eq('id', req.params.id); + if (error) console.error('Update error:', error); + + if (body.shipper_id) await updateShipperTotals(body.shipper_id); + + res.redirect('/loads/' + encodeURIComponent(req.params.id)); +})); + +// POST /loads/:id/delete — Delete load +router.post('/:id/delete', requireAuth, asyncHandler(async (req, res) => { + await supabase.from('loads').delete().eq('id', req.params.id); + res.redirect('/loads'); +})); + +// Helper: Update shipper totals +async function updateShipperTotals(shipperId) { + const { data: shipperLoads } = await supabase.from('loads') + .select('freight_charged, commission, pending_from_shipper') + .eq('shipper_id', shipperId); + + if (!shipperLoads) return; + const totals = shipperLoads.reduce((acc, l) => ({ + freight: acc.freight + (l.freight_charged || 0), + commission: acc.commission + (l.commission || 0), + pending: acc.pending + (l.pending_from_shipper || 0), + }), { freight: 0, commission: 0, pending: 0 }); + + await supabase.from('shippers').update({ + total_freight: totals.freight, + total_commission: totals.commission, + pending_amount: totals.pending, + }).eq('id', shipperId); +} + +module.exports = router; diff --git a/webapp/src/routes/payments.js b/webapp/src/routes/payments.js new file mode 100644 index 0000000..7b9e863 --- /dev/null +++ b/webapp/src/routes/payments.js @@ -0,0 +1,37 @@ +const express = require('express'); +const router = express.Router(); +const supabase = require('../services/supabase'); +const { requireAuth } = require('../middleware/auth'); +const { asyncHandler } = require('../middleware/security'); +const { PAYMENT_METHODS } = require('../config/constants'); + +// GET /payments — Payment ledger +router.get('/', requireAuth, asyncHandler(async (req, res) => { + const { data: payments } = await supabase + .from('payments') + .select('*, load:loads(from_city, to_city, shipper:shippers(name))') + .order('payment_date', { ascending: false, nullsFirst: false }) + .limit(50); + + res.render('pages/payments/list', { + payments: payments || [], + PAYMENT_METHODS, + }); +})); + +// POST /payments — Record a payment +router.post('/', requireAuth, asyncHandler(async (req, res) => { + const { load_id, type, direction, amount, method, payment_date, notes } = req.body; + + await supabase.from('payments').insert({ + load_id, type, direction, + amount: parseFloat(amount) || 0, + method: method || 'bank_transfer', + payment_date: payment_date || null, + notes: notes || null, + }); + + res.redirect(req.get('Referer') || '/payments'); +})); + +module.exports = router; diff --git a/webapp/src/routes/reports.js b/webapp/src/routes/reports.js new file mode 100644 index 0000000..cd7318b --- /dev/null +++ b/webapp/src/routes/reports.js @@ -0,0 +1,50 @@ +const express = require('express'); +const router = express.Router(); +const supabase = require('../services/supabase'); +const { requireAuth } = require('../middleware/auth'); +const { asyncHandler } = require('../middleware/security'); + +// GET /reports — Reports dashboard +router.get('/', requireAuth, asyncHandler(async (req, res) => { + const { data: loads } = await supabase.from('loads').select('*'); + const allLoads = loads || []; + + // Monthly breakdown + const monthly = {}; + for (const l of allLoads) { + if (!l.date) continue; + const d = new Date(l.date); + const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; + if (!monthly[key]) monthly[key] = { label: d.toLocaleDateString('en-IN', { month: 'short', year: 'numeric' }), freight: 0, commission: 0, count: 0, pending: 0 }; + monthly[key].freight += l.freight_charged || 0; + monthly[key].commission += l.commission || 0; + monthly[key].count++; + monthly[key].pending += l.pending_from_shipper || 0; + } + const monthlySorted = Object.entries(monthly).sort(([a], [b]) => a.localeCompare(b)); + + // Top shippers + const { data: shippers } = await supabase + .from('shippers').select('*').order('total_freight', { ascending: false }).limit(10); + + // Route analysis + const routes = {}; + for (const l of allLoads) { + if (!l.from_city || !l.to_city) continue; + const key = `${l.from_city} → ${l.to_city}`; + if (!routes[key]) routes[key] = { route: key, count: 0, total_freight: 0, total_commission: 0 }; + routes[key].count++; + routes[key].total_freight += l.freight_charged || 0; + routes[key].total_commission += l.commission || 0; + } + const topRoutes = Object.values(routes).sort((a, b) => b.total_commission - a.total_commission).slice(10); + + res.render('pages/reports/index', { + monthly: monthlySorted, + shippers: shippers || [], + routes: topRoutes, + totalLoads: allLoads.length, + }); +})); + +module.exports = router; diff --git a/webapp/src/routes/shippers.js b/webapp/src/routes/shippers.js new file mode 100644 index 0000000..e675e94 --- /dev/null +++ b/webapp/src/routes/shippers.js @@ -0,0 +1,54 @@ +const express = require('express'); +const router = express.Router(); +const supabase = require('../services/supabase'); +const { requireAuth } = require('../middleware/auth'); +const { asyncHandler } = require('../middleware/security'); + +// GET /shippers — List all shippers +router.get('/', requireAuth, asyncHandler(async (req, res) => { + const { data: shippers } = await supabase + .from('shippers') + .select('*') + .order('total_freight', { ascending: false }); + + res.render('pages/shippers/list', { shippers: shippers || [] }); +})); + +// GET /shippers/:id — Shipper detail with loads +router.get('/:id', requireAuth, asyncHandler(async (req, res) => { + const { data: shipper } = await supabase + .from('shippers').select('*').eq('id', req.params.id).single(); + if (!shipper) return res.status(404).render('pages/404'); + + const { data: loads } = await supabase + .from('loads') + .select('*, vehicle:vehicles(number)') + .eq('shipper_id', req.params.id) + .order('date', { ascending: false, nullsFirst: false }); + + res.render('pages/shippers/detail', { + shipper, + loads: loads || [], + }); +})); + +// POST /shippers — Create shipper +router.post('/', requireAuth, asyncHandler(async (req, res) => { + const { name, phone, email, city, state, notes } = req.body; + const id = name.toLowerCase().replace(/&/g, 'and').replace(/'/g, '').replace(/\s+/g, '_'); + + await supabase.from('shippers').upsert({ id, name, phone: phone || null, email: email || null, city: city || 'Thiruvananthapuram', state: state || 'Kerala', notes: notes || null }); + res.redirect('/shippers/' + encodeURIComponent(id)); +})); + +// POST /shippers/:id — Update shipper +router.post('/:id', requireAuth, asyncHandler(async (req, res) => { + const { name, phone, email, city, state, notes } = req.body; + await supabase.from('shippers').update({ + name, phone: phone || null, email: email || null, + city: city || 'Thiruvananthapuram', state: state || 'Kerala', notes: notes || null, + }).eq('id', req.params.id); + res.redirect('/shippers/' + encodeURIComponent(req.params.id)); +})); + +module.exports = router; diff --git a/webapp/src/routes/vehicles.js b/webapp/src/routes/vehicles.js new file mode 100644 index 0000000..d9c8306 --- /dev/null +++ b/webapp/src/routes/vehicles.js @@ -0,0 +1,49 @@ +const express = require('express'); +const router = express.Router(); +const supabase = require('../services/supabase'); +const { requireAuth } = require('../middleware/auth'); +const { asyncHandler } = require('../middleware/security'); + +// GET /vehicles — List all vehicles +router.get('/', requireAuth, asyncHandler(async (req, res) => { + const { data: vehicles } = await supabase + .from('vehicles') + .select('*') + .order('number'); + + res.render('pages/vehicles/list', { vehicles: vehicles || [] }); +})); + +// GET /vehicles/:id — Vehicle detail with loads +router.get('/:id', requireAuth, asyncHandler(async (req, res) => { + const { data: vehicle } = await supabase + .from('vehicles').select('*').eq('id', req.params.id).single(); + if (!vehicle) return res.status(404).render('pages/404'); + + const { data: loads } = await supabase + .from('loads') + .select('*, shipper:shippers(name)') + .eq('vehicle_id', req.params.id) + .order('date', { ascending: false, nullsFirst: false }); + + res.render('pages/vehicles/detail', { + vehicle, + loads: loads || [], + }); +})); + +// POST /vehicles — Create/update vehicle +router.post('/', requireAuth, asyncHandler(async (req, res) => { + const { number, type, city, state, owner_name, owner_phone, is_active } = req.body; + const id = number.replace(/\s/g, '').toLowerCase(); + + await supabase.from('vehicles').upsert({ + id, number: number.toUpperCase().replace(/\s/g, ''), + type: type || 'open', city: city || 'Thiruvananthapuram', state: state || 'Kerala', + owner_name: owner_name || null, owner_phone: owner_phone || null, + is_active: is_active === 'true' || is_active === 'on', + }); + res.redirect('/vehicles/' + encodeURIComponent(id)); +})); + +module.exports = router; diff --git a/webapp/src/server.js b/webapp/src/server.js new file mode 100644 index 0000000..557ba75 --- /dev/null +++ b/webapp/src/server.js @@ -0,0 +1,251 @@ +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 cookieParser = require('cookie-parser'); +const rateLimit = require('express-rate-limit'); +const bcrypt = require('bcryptjs'); +const config = require('./config/env'); +const supabase = require('./services/supabase'); +const { setupCSRF, validateCSRF, sanitizeBody, requestLogger, asyncHandler } = require('./middleware/security'); +const { requireAuth } = require('./middleware/auth'); +const { formatINR, getStatusColor } = require('./lib/india'); + +const app = express(); + +// Trust proxy +app.set('trust proxy', 1); + +// Security headers +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdn.jsdelivr.net", "https://unpkg.com"], + fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdn.jsdelivr.net"], + imgSrc: ["'self'", "data:", "https:"], + scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://react.dev"], + connectSrc: ["'self'"], + }, + }, + crossOriginEmbedderPolicy: false, +})); + +app.use(compression()); +app.use(requestLogger); + +// Rate limiting +app.use(rateLimit({ + windowMs: 15 * 60 * 1000, + max: 200, + standardHeaders: true, + legacyHeaders: false, + message: 'Too many requests, please try again later.', +})); + +// Body parsing +app.use(express.json({ limit: '1mb' })); +app.use(express.urlencoded({ extended: true, limit: '1mb' })); +app.use(cookieParser()); + +// Static files +app.use(express.static(path.join(__dirname, 'public'), { + maxAge: config.nodeEnv === 'production' ? '1d' : 0, + etag: true, +})); + +// 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', + httpOnly: true, + sameSite: 'lax', + maxAge: 24 * 60 * 60 * 1000, + }, + name: 'fd.sid', +})); + +// CSRF +app.use(setupCSRF); +app.use(sanitizeBody); + +// Make helpers available to all views +app.use((req, res, next) => { + res.locals.user = req.session.user || null; + res.locals.appName = 'FreightDesk'; + res.locals.appNameHi = 'फ्रेटडेस्क'; + res.locals.formatINR = formatINR; + res.locals.getStatusColor = getStatusColor; + res.locals.year = new Date().getFullYear(); + res.locals._csrf = req.session._csrf; + next(); +}); + +// CSRF validation for POST/PUT/DELETE +app.use(validateCSRF); + +// ============================================================ +// AUTH ROUTES (public) +// ============================================================ + +app.get('/login', (req, res) => { + if (req.session.user) return res.redirect('/'); + res.render('pages/login', { error: null }); +}); + +app.post('/login', asyncHandler(async (req, res) => { + const { username, password } = req.body; + if (!username || !password) { + return res.render('pages/login', { error: 'Username and password required' }); + } + + const { data: user } = await supabase + .from('portal_users') + .select('*') + .eq('username', username) + .eq('is_active', true) + .single(); + + if (!user) { + return res.render('pages/login', { error: 'Invalid username or password' }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return res.render('pages/login', { error: 'Invalid username or password' }); + } + + req.session.user = { + id: user.id, + username: user.username, + role: user.role, + entity_id: user.entity_id, + }; + + await supabase.from('portal_users').update({ last_login: new Date().toISOString() }).eq('id', user.id); + + // Redirect based on role + if (user.role === 'admin') return res.redirect('/'); + return res.redirect('/'); +})); + +app.get('/logout', (req, res) => { + req.session.destroy(); + res.redirect('/login'); +}); + +app.get('/setup', asyncHandler(async (req, res) => { + // Check if admin exists + const { count } = await supabase + .from('portal_users') + .select('*', { count: 'exact', head: true }) + .eq('username', 'admin'); + + if (count > 0) { + return res.redirect('/login'); + } + + // Create default admin + const hash = await bcrypt.hash('admin123', 10); + await supabase.from('portal_users').insert({ + username: 'admin', + password_hash: hash, + role: 'admin', + is_active: true, + }); + + res.send('

Admin created!

Username: admin

Password: admin123

Go to login

'); +})); + +// ============================================================ +// API ROUTES (for React dashboard + WhatsApp parser) +// ============================================================ + +// WhatsApp parser API +app.post('/api/parse-whatsapp', requireAuth, asyncHandler(async (req, res) => { + const { parseWhatsAppMessage } = require('./services/parser'); + const { message } = req.body; + if (!message) return res.json({ error: 'No message provided' }); + const result = parseWhatsAppMessage(message); + res.json(result); +})); + +// Dashboard stats API +app.get('/api/stats', requireAuth, asyncHandler(async (req, res) => { + const { data: loads } = await supabase.from('loads').select('*'); + const allLoads = loads || []; + + const monthly = {}; + for (const l of allLoads) { + if (!l.date) continue; + const d = new Date(l.date); + const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; + if (!monthly[key]) monthly[key] = { freight: 0, commission: 0, count: 0 }; + monthly[key].freight += l.freight_charged || 0; + monthly[key].commission += l.commission || 0; + monthly[key].count++; + } + + res.json({ + totalFreight: allLoads.reduce((s, l) => s + (l.freight_charged || 0), 0), + totalCommission: allLoads.reduce((s, l) => s + (l.commission || 0), 0), + totalPending: allLoads.reduce((s, l) => s + (l.pending_from_shipper || 0), 0), + settledCount: allLoads.filter(l => ['settled', 'completed', 'commission received', 'reconciled'].includes(l.status)).length, + totalLoads: allLoads.length, + monthly: Object.entries(monthly).sort(([a], [b]) => a.localeCompare(b)), + }); +})); + +// ============================================================ +// PAGE ROUTES (protected) +// ============================================================ + +app.use('/', require('./routes/dashboard')); +app.use('/loads', require('./routes/loads')); +app.use('/shippers', require('./routes/shippers')); +app.use('/vehicles', require('./routes/vehicles')); +app.use('/payments', require('./routes/payments')); +app.use('/reports', require('./routes/reports')); + +// Health check +app.get('/health', (req, res) => res.json({ status: 'ok', ts: Date.now() })); + +// 404 +app.use((req, res) => { + res.status(404); + res.render('pages/404'); +}); + +// Error handler +app.use((err, req, res, next) => { + console.error(`[ERROR] ${req.method} ${req.url}:`, err.message); + if (config.nodeEnv === 'development') console.error(err.stack); + res.status(err.status || 500); + res.render('pages/500', { error: config.nodeEnv === 'development' ? err.message : null }); +}); + +const server = app.listen(config.port, '::', () => { + console.log(`\n🚛 FreightDesk running at http://localhost:${config.port}`); + console.log(` Environment: ${config.nodeEnv}`); + console.log(` Press Ctrl+C to stop\n`); +}); + +process.on('SIGTERM', () => { + console.log('SIGTERM received, shutting down gracefully...'); + server.close(() => { + console.log('Server closed.'); + process.exit(0); + }); +}); + +module.exports = app; diff --git a/webapp/src/services/parser.js b/webapp/src/services/parser.js new file mode 100644 index 0000000..e45160f --- /dev/null +++ b/webapp/src/services/parser.js @@ -0,0 +1,190 @@ +// WhatsApp message parser for FreightDesk +// Parses natural language freight messages into structured data + +const { CITIES } = require('../config/constants'); + +// Known shipper names (from existing data) +const KNOWN_SHIPPERS = [ + 'Kahn Transport', 'Agarwal Packers and Movers', 'Agarwal', 'Sahara Packers', + 'Ambika Packers', 'Century Polymers', 'DRS', 'Superstar', 'Superstar Packers', + 'Chips', 'Chipps', 'Indian CBE', 'Indian CBE Shipper', 'KTC', 'ATC', 'TCI', + 'Filatex', 'Dryfish John', 'Sun Packers', 'Thangavel', 'Nafees Alappuzha', + 'Shivaprasad', 'Jinu Coin', 'Balmer Thuni', 'Pasupathy', 'Sulphi Baddest', + 'KRS', 'Hirosh', 'Hirosh Roadways', 'Aero Rubber', 'Silverstar', + 'DRS Agarwal', 'Gem', 'E20 Packers and Movers', 'CRT Transport', + 'Mohamed Anas', 'Nair', 'Badadosth', +]; + +// Status keywords mapping +const STATUS_KEYWORDS = { + 'pending lead': ['pending lead', 'lead', 'enquiry', 'enquiry'], + 'assigned vehicle': ['assigned vehicle', 'vehicle assigned'], + 'assigned': ['assigned', 'allotted'], + 'loaded / in transit': ['loaded', 'in transit', 'on the way', 'dispatched', 'started'], + 'delivered / pending collection': ['delivered', 'delivery done'], + 'pending collection': ['pending collection', 'collection pending', 'to collect'], + 'partially pending': ['partially pending', 'partial pending'], + 'fully pending from shipper': ['fully pending', 'no payment'], + 'settled': ['settled', 'complete', 'completed', 'closed'], + 'commission received': ['commission received', 'comm received'], + 'commission adjusted': ['commission adjusted', 'comm adjusted'], + 'commission due': ['commission due', 'comm due'], + 'reconciled': ['reconciled'], + 'completed': ['completed', 'done'], + 'handled directly by shipper': ['directly by shipper', 'handled directly'], + 'available vehicle': ['available', 'vehicle available'], + 'partial': ['partial'], +}; + +function parseWhatsAppMessage(text) { + const result = { + shipper: null, + vehicle: null, + from_city: null, + to_city: null, + via: null, + status: null, + freight_charged: null, + advance_received: null, + paid_to_driver: null, + commission: null, + driver_freight: null, + pending_from_shipper: null, + pending_to_driver: null, + notes: text, + confidence: 'low', + parsed_fields: [], + }; + + const lower = text.toLowerCase(); + + // 1. Parse shipper + for (const shipper of KNOWN_SHIPPERS) { + if (lower.includes(shipper.toLowerCase())) { + result.shipper = shipper; + result.parsed_fields.push('shipper'); + break; + } + } + + // 2. Parse vehicle number (Indian format: XX00XX0000) + const vehicleMatch = text.match(/\b([A-Z]{2}\s*\d{1,2}\s*[A-Z]{1,3}\s*\d{4})\b/i); + if (vehicleMatch) { + result.vehicle = vehicleMatch[1].replace(/\s/g, '').toUpperCase(); + result.parsed_fields.push('vehicle'); + } + + // 3. Parse cities (from → to pattern) + const cityPattern = CITIES.map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'); + const routeMatch = text.match(new RegExp(`(${cityPattern})\\s*(?:to|→|-|via)\\s*(${cityPattern})`, 'i')); + if (routeMatch) { + result.from_city = routeMatch[1]; + result.to_city = routeMatch[2]; + result.parsed_fields.push('from_city', 'to_city'); + } else { + // Try to find any known city + for (const city of CITIES) { + if (lower.includes(city.toLowerCase())) { + if (!result.to_city) { + result.to_city = city; + result.parsed_fields.push('to_city'); + } else if (!result.from_city) { + result.from_city = city; + result.parsed_fields.push('from_city'); + } + } + } + } + + // 4. Parse via + const viaMatch = text.match(/via\s+([A-Za-z\s,]+?)(?:\s*(?:to|→|-|loaded|freight|₹|\d{4,}))/i); + if (viaMatch) { + result.via = viaMatch[1].trim(); + result.parsed_fields.push('via'); + } + + // 5. Parse status + for (const [status, keywords] of Object.entries(STATUS_KEYWORDS)) { + for (const kw of keywords) { + if (lower.includes(kw)) { + result.status = status; + result.parsed_fields.push('status'); + break; + } + } + if (result.status) break; + } + + // 6. Parse amounts + // Freight: look for "freight", "charged", "total" followed by number + const freightMatch = text.match(/(?:freight|charged|total|amount|bill)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i); + if (freightMatch) { + result.freight_charged = parseInt(freightMatch[1].replace(/,/g, '')); + result.parsed_fields.push('freight_charged'); + } else { + // Try standalone large numbers (4-6 digits) that could be freight + const amountMatches = text.match(/₹?\s*(\d{4,6})\b/g); + if (amountMatches) { + const amounts = amountMatches.map(m => parseInt(m.replace(/[₹,\s]/g, ''))); + if (amounts.length > 0) { + result.freight_charged = Math.max(...amounts); + result.parsed_fields.push('freight_charged'); + } + } + } + + // Advance received + const advanceMatch = text.match(/(?:advance|received|paid by shipper)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i); + if (advanceMatch) { + result.advance_received = parseInt(advanceMatch[1].replace(/,/g, '')); + result.parsed_fields.push('advance_received'); + } + + // Paid to driver + const driverPaidMatch = text.match(/(?:paid to driver|driver advance|driver paid|to driver)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i); + if (driverPaidMatch) { + result.paid_to_driver = parseInt(driverPaidMatch[1].replace(/,/g, '')); + result.parsed_fields.push('paid_to_driver'); + } + + // Commission + const commissionMatch = text.match(/(?:commission|comm)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i); + if (commissionMatch) { + result.commission = parseInt(commissionMatch[1].replace(/,/g, '')); + result.parsed_fields.push('commission'); + } + + // Driver freight + const driverFreightMatch = text.match(/(?:driver freight|driver rate|driver amount)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i); + if (driverFreightMatch) { + result.driver_freight = parseInt(driverFreightMatch[1].replace(/,/g, '')); + result.parsed_fields.push('driver_freight'); + } + + // Auto-calculate commission if not parsed + if (!result.commission && result.freight_charged && result.paid_to_driver) { + result.commission = result.freight_charged - result.paid_to_driver; + result.parsed_fields.push('commission (auto)'); + } + + // Auto-calculate pending from shipper + if (!result.pending_from_shipper && result.freight_charged) { + result.pending_from_shipper = result.freight_charged - (result.advance_received || 0); + if (result.pending_from_shipper > 0) result.parsed_fields.push('pending_from_shipper (auto)'); + } + + // Auto-calculate pending to driver + if (!result.pending_to_driver && result.driver_freight) { + result.pending_to_driver = result.driver_freight - (result.paid_to_driver || 0); + if (result.pending_to_driver > 0) result.parsed_fields.push('pending_to_driver (auto)'); + } + + // Confidence based on how many fields were parsed + const fieldCount = result.parsed_fields.length; + if (fieldCount >= 6) result.confidence = 'high'; + else if (fieldCount >= 3) result.confidence = 'medium'; + + return result; +} + +module.exports = { parseWhatsAppMessage, KNOWN_SHIPPERS }; diff --git a/webapp/src/services/supabase.js b/webapp/src/services/supabase.js new file mode 100644 index 0000000..2c92612 --- /dev/null +++ b/webapp/src/services/supabase.js @@ -0,0 +1,14 @@ +const { createClient } = require('@supabase/supabase-js'); +const config = require('../config/env'); + +const supabaseUrl = config.supabase.url; +const supabaseKey = config.supabase.key; + +if (!supabaseUrl || !supabaseKey) { + console.error('Missing SUPABASE_URL or SUPABASE_KEY. Check .env file.'); + process.exit(1); +} + +const supabase = createClient(supabaseUrl, supabaseKey); + +module.exports = supabase; diff --git a/webapp/src/views/layouts/main.ejs b/webapp/src/views/layouts/main.ejs new file mode 100644 index 0000000..9139600 --- /dev/null +++ b/webapp/src/views/layouts/main.ejs @@ -0,0 +1,68 @@ + + + + + + <%= typeof title !== 'undefined' ? title + ' — ' : '' %><%= appName %> · <%= appNameHi %> + + + + + <% if (typeof extraCss !== 'undefined') { <% for (const css of extraCss) { %> <% } %> <% } %> + + + <% if (typeof user !== 'undefined' && user) { %> + + +
+ + +
+ <%- content %> +
+
+ + + + <% } else { %> + <%- content %> + <% } %> + + + <% if (typeof extraJs !== 'undefined') { <% for (const js of extraJs) { %><% } %> <% } %> + + diff --git a/webapp/src/views/layouts/page.ejs b/webapp/src/views/layouts/page.ejs new file mode 100644 index 0000000..368908f --- /dev/null +++ b/webapp/src/views/layouts/page.ejs @@ -0,0 +1,9 @@ +<% + // Simple layout helper — pages set these variables before including + var _body = ''; + var _title = ''; + var _activeMenu = ''; + var _extraJs = []; +%> + +<%- include('../layouts/main') %> diff --git a/webapp/src/views/pages/403.ejs b/webapp/src/views/pages/403.ejs new file mode 100644 index 0000000..f05d9ff --- /dev/null +++ b/webapp/src/views/pages/403.ejs @@ -0,0 +1,18 @@ + + + + + + 403 - <%= appName %> + + + + +
+
403
+

पृनरावेशन निषेद़ / Forbidden

+

You don't have permission to access this page.

+ Go to Dashboard +
+ + diff --git a/webapp/src/views/pages/404.ejs b/webapp/src/views/pages/404.ejs new file mode 100644 index 0000000..f6d15e7 --- /dev/null +++ b/webapp/src/views/pages/404.ejs @@ -0,0 +1,32 @@ +<% var _title = '404 Not Found'; %> + + + + + + 404 - <%= appName %> + + + + + + +
+
404
+

रिक्त पेज / Page Not Found

+

The page you're looking for doesn't exist.

+ Go to Dashboard +
+ + diff --git a/webapp/src/views/pages/500.ejs b/webapp/src/views/pages/500.ejs new file mode 100644 index 0000000..4d81c61 --- /dev/null +++ b/webapp/src/views/pages/500.ejs @@ -0,0 +1,28 @@ + + + + + + 500 - <%= appName %> + + + + + +
+
500
+

सर्वर त्रुटि / Server Error

+

Something went wrong. Please try again later.

+ <% if (typeof error !== 'undefined' && error) { %>

<%= error %>

<% } %> + Go to Dashboard +
+ + diff --git a/webapp/src/views/pages/dashboard.ejs b/webapp/src/views/pages/dashboard.ejs new file mode 100644 index 0000000..653b6ab --- /dev/null +++ b/webapp/src/views/pages/dashboard.ejs @@ -0,0 +1,132 @@ + +<%- include('../partials/header', { activeMenu: 'dashboard' }) %> + + + + +
+
+
💰
+
+ <%= formatINR(stats.totalFreight) %> + Total Freight +
+
+
+
+
+ <%= formatINR(stats.totalCommission) %> + Commission Earned +
+
+
+
+
+ <%= formatINR(stats.totalPendingShipper) %> + Pending Collection +
+
+
+
🚚
+
+ <%= stats.totalLoads %> + Total Loads (<%= stats.settledCount %> settled) +
+
+
+ +
+ +
+
+

Recent Loads

+ View All +
+
+ <% if (recentLoads.length === 0) { %> +

No loads yet. Add your first load

+ <% } else { %> +
+ + + + + + + + + + + <% for (const load of recentLoads) { %> + + + + + + + <% } %> + +
DateRouteFreightStatus
<%= load.date || '—' %><%= load.from_city || '?' %> → <%= load.to_city || '?' %><%= formatINR(load.freight_charged) %><%= load.status %>
+
+ <% } %> +
+
+ + +
+
+

⏰ Pending Collections

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

No pending collections. Great job!

+ <% } else { %> +
+ + + + + + + + + + <% for (const load of pendingCollection) { %> + + + + + + <% } %> + +
ShipperRoutePending
<%= load.shipper_id || '—' %><%= load.from_city || '?' %> → <%= load.to_city || '?' %><%= formatINR(load.pending_from_shipper) %>
+
+ <% } %> +
+
+
+ + +
+
+

Status Breakdown

+
+
+
+ <% for (const [status, count] of Object.entries(statusCounts)) { %> +
+ <%= count %> + <%= status %> +
+ <% } %> +
+
+
+ +<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/loads/detail.ejs b/webapp/src/views/pages/loads/detail.ejs new file mode 100644 index 0000000..fa8e996 --- /dev/null +++ b/webapp/src/views/pages/loads/detail.ejs @@ -0,0 +1,111 @@ +<%- include('../partials/header', { activeMenu: 'loads' }) %> + + + +
+
+

Load Info

+
+
+
Date
<%= load.date || '—' %>
+
Shipper
<%= load.shipper ? load.shipper.name : (load.shipper_id || '—') %>
+
Route
<%= load.from_city || '?' %> → <%= load.via ? load.via + ' → ' : '' %><%= load.to_city || '?' %>
+
Vehicle
<%= load.vehicle ? load.vehicle.number : '—' %>
+
Load Type
<%= load.load_type || '—' %>
+
Item
<%= load.item || '—' %>
+
Status
<%= load.status %>
+
+
+
+ +
+

Financials

+
+
+
Freight Charged
<%= formatINR(load.freight_charged) %>
+
Advance Received
<%= formatINR(load.advance_received) %>
+
Paid to Driver
<%= formatINR(load.paid_to_driver) %>
+
Driver Freight
<%= formatINR(load.driver_freight) %>
+
Commission
<%= formatINR(load.commission) %>
+
Pending from Shipper
<%= formatINR(load.pending_from_shipper) %>
+
Pending to Driver
<%= formatINR(load.pending_to_driver) %>
+
+
+
+
+ +<% if (load.notes) { %> +
+

Notes

+

<%= load.notes %>

+
+<% } %> + + +
+
+

Payment History

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

No payments recorded yet.

+ <% } else { %> + + + + <% for (const p of payments) { %> + + + + + + + + + <% } %> + +
DateTypeDirectionAmountMethodNotes
<%= p.payment_date || '—' %><%= p.type %><%= p.direction === 'in' ? '⬆ In' : '⬇ Out' %><%= formatINR(p.amount) %><%= p.method %><%= p.notes || '' %>
+ <% } %> + + +

Record Payment

+
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/loads/form.ejs b/webapp/src/views/pages/loads/form.ejs new file mode 100644 index 0000000..dd4e8bf --- /dev/null +++ b/webapp/src/views/pages/loads/form.ejs @@ -0,0 +1,234 @@ +<%- include('../partials/header', { activeMenu: 'loads' }) %> + + + +<% if (typeof error !== 'undefined' && error) { %> +
<%= error %>
+<% } %> + +
+ +
+

Load Details

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + + <% for (const c of CITIES) { %> + +
+
+ + +
+
+ + +
+
+ +

Financials

+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + Cancel +
+
+
+
+ + +
+

📱 WhatsApp Parser

+
+

Paste a WhatsApp message to auto-fill the form.

+
+ +
+ + + +
+
+
+ +<% if (isEdit) { %> +
+
+ + +
+
+<% } %> + + + +<%- include('../partials/footer', { extraJs: [] }) %> diff --git a/webapp/src/views/pages/loads/list.ejs b/webapp/src/views/pages/loads/list.ejs new file mode 100644 index 0000000..0e1ca1a --- /dev/null +++ b/webapp/src/views/pages/loads/list.ejs @@ -0,0 +1,81 @@ +<%- include('../partials/header', { activeMenu: 'loads' }) %> + + + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ <% if (loads.length === 0) { %> +

No loads found. Add your first load

+ <% } else { %> +
+ + + + + + + + + + + + + + + <% for (const load of loads) { %> + + + + + + + + + + + <% } %> + +
DateShipperRouteVehicleFreightCommissionStatusActions
<%= load.date || '—' %><%= load.shipper ? load.shipper.name : (load.shipper_id || '—') %><%= load.from_city || '?' %> → <%= load.to_city || '?' %><%= load.vehicle ? load.vehicle.number : '—' %><%= formatINR(load.freight_charged) %><%= formatINR(load.commission) %><%= load.status %> + View + Edit +
+
+ <% } %> +
+
+ +<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/login.ejs b/webapp/src/views/pages/login.ejs new file mode 100644 index 0000000..e9bb7d9 --- /dev/null +++ b/webapp/src/views/pages/login.ejs @@ -0,0 +1,48 @@ + + + + + + <%= typeof title !== 'undefined' ? title + ' — ' : '' %><%= appName %> · <%= appNameHi %> + + + + + + +
+ +
+ + + diff --git a/webapp/src/views/pages/payments/list.ejs b/webapp/src/views/pages/payments/list.ejs new file mode 100644 index 0000000..361e0c1 --- /dev/null +++ b/webapp/src/views/pages/payments/list.ejs @@ -0,0 +1,33 @@ +<%- include('../partials/header', { activeMenu: 'payments' }) %> + + + +
+
+
+ + + + <% for (const p of payments) { %> + + + + + + + + + + <% } %> + +
DateTypeDirectionAmountMethodNotesLoad
<%= p.payment_date || '—' %><%= p.type %><%= p.direction === 'in' ? 'In' : 'Out' %><%= formatINR(p.amount) %><%= p.method %><%= p.notes || '' %><%= p.load ? (p.load.from_city + ' → ' + p.load.to_city) : '—' %>
+
+
+
+ +<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/reports/index.ejs b/webapp/src/views/pages/reports/index.ejs new file mode 100644 index 0000000..48dab29 --- /dev/null +++ b/webapp/src/views/pages/reports/index.ejs @@ -0,0 +1,75 @@ +<%- include('../partials/header', { activeMenu: 'reports' }) %> + + + +
+

Monthly Summary

+
+
+ + + + <% for (const [key, m] of monthly) { %> + + + + + + + + <% } %> + +
MonthLoadsFreightCommissionPending
<%= m.label %><%= m.count %><%= formatINR(m.freight) %><%= formatINR(m.commission) %><%= formatINR(m.pending) %>
+
+
+
+ +
+
+

Top Shippers

+
+
+ + + + <% for (const s of shippers) { %> + + + + + + + <% } %> + +
ShipperLoadsFreightCommission
<%= s.name %><%= s.load_count || 0 %><%= formatINR(s.total_freight) %><%= formatINR(s.total_commission) %>
+
+
+
+ +
+

Top Routes

+
+
+ + + + <% for (const r of routes) { %> + + + + + + <% } %> + +
RouteLoadsCommission
<%= r.route %><%= r.count %><%= formatINR(r.total_commission) %>
+
+
+
+
+ +<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/shippers/detail.ejs b/webapp/src/views/pages/shippers/detail.ejs new file mode 100644 index 0000000..9e63a0d --- /dev/null +++ b/webapp/src/views/pages/shippers/detail.ejs @@ -0,0 +1,49 @@ +<%- include('../partials/header', { activeMenu: 'shippers' }) %> + + + +
+
+
<%= formatINR(shipper.total_freight) %>Total Freight
+
+
+
<%= formatINR(shipper.total_commission) %>Commission
+
+
+
<%= formatINR(shipper.pending_amount) %>Pending
+
+
+
<%= loads.length %>Loads
+
+
+ +
+

All Loads

+
+
+ + + + <% for (const l of loads) { %> + + + + + + + + + <% } %> + +
DateRouteVehicleFreightCommissionStatus
<%= l.date || '—' %><%= l.from_city || '?' %> → <%= l.to_city || '?' %><%= l.vehicle ? l.vehicle.number : '—' %><%= formatINR(l.freight_charged) %><%= formatINR(l.commission) %><%= l.status %>
+
+
+
+ +<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/shippers/list.ejs b/webapp/src/views/pages/shippers/list.ejs new file mode 100644 index 0000000..f1c89bd --- /dev/null +++ b/webapp/src/views/pages/shippers/list.ejs @@ -0,0 +1,65 @@ +<%- include('../partials/header', { activeMenu: 'shippers' }) %> + + + +
+
+
+ + + + + + + + + + + + + + <% for (const s of shippers) { %> + + + + + + + + + + <% } %> + +
NameCityLoadsTotal FreightCommissionPendingActions
<%= s.name %><%= s.city || '—' %><%= s.load_count || 0 %><%= formatINR(s.total_freight) %><%= formatINR(s.total_commission) %><%= formatINR(s.pending_amount) %>View
+
+
+
+ + +
+

Add New Shipper

+
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/vehicles/detail.ejs b/webapp/src/views/pages/vehicles/detail.ejs new file mode 100644 index 0000000..521b85d --- /dev/null +++ b/webapp/src/views/pages/vehicles/detail.ejs @@ -0,0 +1,33 @@ +<%- include('../partials/header', { activeMenu: 'vehicles' }) %> + + + +
+

Load History

+
+
+ + + + <% for (const l of loads) { %> + + + + + + + + <% } %> + +
DateShipperRouteFreightStatus
<%= l.date || '—' %><%= l.shipper ? l.shipper.name : '—' %><%= l.from_city || '?' %> → <%= l.to_city || '?' %><%= formatINR(l.freight_charged) %><%= l.status %>
+
+
+
+ +<%- include('../partials/footer') %> diff --git a/webapp/src/views/pages/vehicles/list.ejs b/webapp/src/views/pages/vehicles/list.ejs new file mode 100644 index 0000000..01d7aeb --- /dev/null +++ b/webapp/src/views/pages/vehicles/list.ejs @@ -0,0 +1,56 @@ +<%- include('../partials/header', { activeMenu: 'vehicles' }) %> + + + +
+
+
+ + + + <% for (const v of vehicles) { %> + + + + + + + + <% } %> + +
NumberTypeCityActiveActions
<%= v.number %><%= v.type || '—' %><%= v.city || '—' %><%= v.is_active ? 'Active' : 'Inactive' %>View
+
+
+
+ +
+

Add Vehicle

+
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +<%- include('../partials/footer') %> diff --git a/webapp/src/views/partials/footer.ejs b/webapp/src/views/partials/footer.ejs new file mode 100644 index 0000000..ac7eb03 --- /dev/null +++ b/webapp/src/views/partials/footer.ejs @@ -0,0 +1,17 @@ + + + + + + + <% if (typeof extraJs !== 'undefined') { %> + <% for (const js of extraJs) { %> + + <% } %> + <% } %> + + diff --git a/webapp/src/views/partials/header.ejs b/webapp/src/views/partials/header.ejs new file mode 100644 index 0000000..6c54305 --- /dev/null +++ b/webapp/src/views/partials/header.ejs @@ -0,0 +1,49 @@ + + + + + + + <%= typeof title !== 'undefined' ? title + ' — ' : '' %><%= appName %> · <%= appNameHi %> + + + + + + + + +
+ + +