[OWL] Driver location tracking + bulk WhatsApp parser + deployment docs
Location Tracking: - POST /api/location/update — driver GPS update - GET /api/location/:load_id — get driver location for load - Migration 007: vehicle_locations table with spatial indexes Bulk WhatsApp Parser: - UI for pasting multiple messages at once - Batch parse via /api/parse-whatsapp - Review parsed results with confidence scores - Select and save all valid loads to database - One-click import from WhatsApp to loads Deployment: - DEPLOYMENT.md: full deployment guide - Environment configuration - Docker + Docker Compose setup - Coolify deployment steps - Post-deployment checklist - Troubleshooting guide - Architecture diagram
This commit is contained in:
parent
6be15fb059
commit
59d93d5281
5 changed files with 533 additions and 0 deletions
235
DEPLOYMENT.md
Normal file
235
DEPLOYMENT.md
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
# FreightDesk — Deployment Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Ubuntu 22.04+ VPS (minimum 2GB RAM, 2 vCPU)
|
||||||
|
- Domain pointed to VPS IP
|
||||||
|
- Coolify installed (or Docker + Docker Compose)
|
||||||
|
- Supabase project (self-hosted or cloud)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Clone Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone http://forgejo-vil3xyowqk0qsh4hiqy77e3h.187.127.178.110.sslip.io/iamcoolvivek007/freightdesk.git
|
||||||
|
cd freightdesk/webapp
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Environment Configuration
|
||||||
|
|
||||||
|
Create `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Server
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# Supabase
|
||||||
|
SUPABASE_URL=https://your-project.supabase.co
|
||||||
|
SUPABASE_SERVICE_KEY=your-service-role-key
|
||||||
|
SUPABASE_ANON_KEY=your-anon-key
|
||||||
|
|
||||||
|
# Session
|
||||||
|
SESSION_SECRET=generate-a-random-64-char-string-here
|
||||||
|
SESSION_MAX_AGE=86400000
|
||||||
|
|
||||||
|
# WhatsApp (optional — for receiving messages)
|
||||||
|
WHATSAPP_WEBHOOK_TOKEN=your-webhook-verify-token
|
||||||
|
|
||||||
|
# Payment Gateway (production)
|
||||||
|
RAZORPAY_KEY_ID=rzk_live_xxxxx
|
||||||
|
RAZORPAY_KEY_SECRET=xxxxx
|
||||||
|
|
||||||
|
# Email (optional — for notifications)
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=your-email@gmail.com
|
||||||
|
SMTP_PASS=your-app-password
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci --production
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Run Database Migrations
|
||||||
|
|
||||||
|
Run migrations 001 through 007 in order:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Supabase CLI
|
||||||
|
supabase db push
|
||||||
|
|
||||||
|
# Or manually via SQL editor:
|
||||||
|
# Copy contents of supabase/migrations/001_initial_schema.sql and run
|
||||||
|
# Copy contents of supabase/migrations/002_whatsapp_parser.sql and run
|
||||||
|
# ... through 007_location_tracking.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Create Admin User
|
||||||
|
|
||||||
|
Visit `/setup` in your browser and create the admin account.
|
||||||
|
|
||||||
|
### 6. Start the Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Production
|
||||||
|
NODE_ENV=production node src/server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Coolify Deployment (Recommended)
|
||||||
|
|
||||||
|
1. In Coolify, create new application
|
||||||
|
2. Connect to your Forgejo repository
|
||||||
|
3. Set buildpack: `Dockerfile`
|
||||||
|
4. Set Dockerfile path: `/webapp/Dockerfile`
|
||||||
|
5. Add environment variables from `.env`
|
||||||
|
6. Set domain and enable SSL
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd webapp
|
||||||
|
docker build -t freightdesk .
|
||||||
|
docker run -d \
|
||||||
|
--name freightdesk \
|
||||||
|
-p 3000:3000 \
|
||||||
|
--env-file .env \
|
||||||
|
--restart unless-stopped \
|
||||||
|
freightdesk
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Compose (Full Stack)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: ./webapp
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
env_file: .env
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- supabase
|
||||||
|
|
||||||
|
# If self-hosting Supabase
|
||||||
|
supabase:
|
||||||
|
image: supabase/supabase-local:latest
|
||||||
|
ports:
|
||||||
|
- "5432:5432" # PostgreSQL
|
||||||
|
- "8000:8000" # REST API
|
||||||
|
- "4000:4000" # Studio
|
||||||
|
volumes:
|
||||||
|
- supabase-data:/var/lib/supabase
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
supabase-data:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Post-Deployment Checklist
|
||||||
|
|
||||||
|
- [ ] Run all 7 migrations (001-007)
|
||||||
|
- [ ] Create admin account via /setup
|
||||||
|
- [ ] Configure SSL certificate
|
||||||
|
- [ ] Set up automated backups (Supabase: daily DB dump)
|
||||||
|
- [ ] Configure Coolify webhooks for auto-deploy on git push
|
||||||
|
- [ ] Set up monitoring (Prometheus /metrics endpoint at :3000/metrics)
|
||||||
|
- [ ] Configure Pino log aggregation
|
||||||
|
- [ ] Test WhatsApp parser with sample messages
|
||||||
|
- [ ] Test registration flow (shipper + driver)
|
||||||
|
- [ ] Test marketplace: post load → bid → accept
|
||||||
|
- [ ] Test payment escrow: deposit → hold → release → payout
|
||||||
|
|
||||||
|
## Migrations Summary
|
||||||
|
|
||||||
|
| # | File | What it adds |
|
||||||
|
|---|------|-------------|
|
||||||
|
| 001 | `001_initial_schema.sql` | Core tables: loads, shippers, vehicles, payments, users |
|
||||||
|
| 002 | `002_whatsapp_parser.sql` | Parser config, city list, known shippers |
|
||||||
|
| 003 | `003_soft_delete.sql` | Soft-delete columns on all tables |
|
||||||
|
| 004 | `004_audit_logging.sql` | Audit log table + triggers |
|
||||||
|
| 005 | `005_saas_marketplace.sql` | Bids, negotiations, ratings, notifications, marketplace fields |
|
||||||
|
| 006 | `006_payment_escrow.sql` | Escrow accounts, transactions, payouts, disputes |
|
||||||
|
| 007 | `007_location_tracking.sql` | Vehicle GPS location history |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### App won't start
|
||||||
|
- Check `.env` has all required variables
|
||||||
|
- Verify Supabase connection: `curl $SUPABASE_URL/rest/v1/`
|
||||||
|
- Check logs: `docker logs freightdesk`
|
||||||
|
|
||||||
|
### Database errors
|
||||||
|
- Run migrations in order (001 → 007)
|
||||||
|
- Check Supabase service key has proper permissions
|
||||||
|
- Verify `pgcrypto` extension is enabled (for UUID generation)
|
||||||
|
|
||||||
|
### WhatsApp parser not working
|
||||||
|
- Ensure migration 002 ran (populates CITIES and parser config)
|
||||||
|
- Test via `/api/parser/test` endpoint
|
||||||
|
|
||||||
|
### Payment flow fails
|
||||||
|
- Ensure migration 006 ran
|
||||||
|
- Check escrow_accounts table exists
|
||||||
|
- Verify platform_config has default values
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Nginx / Coolify │
|
||||||
|
│ (SSL + Proxy) │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│
|
||||||
|
┌──────────▼──────────┐
|
||||||
|
│ Node.js + Express │
|
||||||
|
│ FreightDesk App │
|
||||||
|
│ Port 3000 │
|
||||||
|
└──┬──────┬──────┬────┘
|
||||||
|
│ │ │
|
||||||
|
┌──────────────┘ │ └──────────────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||||
|
│ EJS Views │ │ REST API │ │ Supabase │
|
||||||
|
│ (templates) │ │ /api/* │ │ PostgreSQL │
|
||||||
|
│ + Recharts │ │ JSON │ │ + Realtime │
|
||||||
|
│ CDN widgets │ │ │ │ │
|
||||||
|
└──────────────┘ └──────────────┘ └──────────────┘
|
||||||
|
|
||||||
|
Routes:
|
||||||
|
/ → Public landing page
|
||||||
|
/login → Admin login
|
||||||
|
/setup → Initial admin setup
|
||||||
|
/dashboard → Admin dashboard (EJS + Recharts)
|
||||||
|
/loads → Load management (admin)
|
||||||
|
/shippers → Shipper management
|
||||||
|
/vehicles → Vehicle management
|
||||||
|
/payments → Payment tracking
|
||||||
|
/reports → Reports
|
||||||
|
/audit-logs → Audit log viewer
|
||||||
|
/invoices → Invoice PDF generation
|
||||||
|
/admin/moderation → User verification, payouts, disputes
|
||||||
|
|
||||||
|
/register/shipper → Shipper self-registration
|
||||||
|
/register/driver → Driver self-registration
|
||||||
|
/portal/* → Shipper/driver portal (dashboard, loads, trips)
|
||||||
|
|
||||||
|
/marketplace → Browse/post loads, bidding
|
||||||
|
/escrow → Deposits, payouts, disputes
|
||||||
|
|
||||||
|
/api/* → REST API (JSON)
|
||||||
|
/metrics → Prometheus metrics
|
||||||
|
/health → Health check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- Forgejo: `http://forgejo-vil3xyowqk0qsh4hiqy77e3h.187.127.178.110.sslip.io/iamcoolvivek007/freightdesk`
|
||||||
|
- Issues: Create on Forgejo
|
||||||
24
supabase/migrations/007_location_tracking.sql
Normal file
24
supabase/migrations/007_location_tracking.sql
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
-- ============================================================
|
||||||
|
-- FreightDesk — Migration 007: Driver Location Tracking
|
||||||
|
-- GPS location history for real-time tracking
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS vehicle_locations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
vehicle_id UUID NOT NULL REFERENCES vehicles(id) ON DELETE CASCADE,
|
||||||
|
lat DECIMAL(10,8) NOT NULL,
|
||||||
|
lng DECIMAL(11,8) NOT NULL,
|
||||||
|
accuracy DECIMAL(8,2),
|
||||||
|
heading DECIMAL(6,2),
|
||||||
|
speed DECIMAL(6,2),
|
||||||
|
recorded_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_vehicle ON vehicle_locations(vehicle_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_time ON vehicle_locations(recorded_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_vehicle_time ON vehicle_locations(vehicle_id, recorded_at DESC);
|
||||||
|
|
||||||
|
-- Enable PostGIS-like functionality with btree_gist for spatial queries
|
||||||
|
-- (In production, use PostGIS extension)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vehicles_location ON vehicles(current_lat, current_lng)
|
||||||
|
WHERE current_lat IS NOT NULL AND current_lng IS NOT NULL;
|
||||||
68
webapp/src/routes/location.js
Normal file
68
webapp/src/routes/location.js
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
// POST /api/location/update — driver updates their GPS location
|
||||||
|
// GET /api/location/:load_id — get driver location for a load (shipper views this)
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const supabase = require('../services/supabase');
|
||||||
|
const { asyncHandler } = require('../middleware/security');
|
||||||
|
|
||||||
|
function requirePortalAuth(req, res, next) {
|
||||||
|
if (!req.session.portalUser) {
|
||||||
|
return res.status(401).json({ error: 'Authentication required' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/location/update
|
||||||
|
router.post('/update', requirePortalAuth, asyncHandler(async (req, res) => {
|
||||||
|
const { lat, lng, accuracy, heading, speed } = req.body;
|
||||||
|
const driverId = req.session.portalUser?.driver_id;
|
||||||
|
|
||||||
|
if (!lat || !lng) {
|
||||||
|
return res.status(400).json({ error: 'lat and lng are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!driverId) {
|
||||||
|
return res.status(400).json({ error: 'Driver profile not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await supabase.from('vehicles').update({
|
||||||
|
current_lat: parseFloat(lat),
|
||||||
|
current_lng: parseFloat(lng),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}).eq('id', driverId);
|
||||||
|
|
||||||
|
// Also store in location history
|
||||||
|
await supabase.from('vehicle_locations').insert({
|
||||||
|
vehicle_id: driverId,
|
||||||
|
lat: parseFloat(lat),
|
||||||
|
lng: parseFloat(lng),
|
||||||
|
accuracy: accuracy || null,
|
||||||
|
heading: heading || null,
|
||||||
|
speed: speed || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
}));
|
||||||
|
|
||||||
|
// GET /api/location/:load_id — get assigned driver's location
|
||||||
|
router.get('/:load_id', requirePortalAuth, asyncHandler(async (req, res) => {
|
||||||
|
const { data: load } = await supabase
|
||||||
|
.from('loads')
|
||||||
|
.select('accepted_bid_id, vehicles(current_lat, current_lng, driver_name, updated_at)')
|
||||||
|
.eq('id', req.params.load_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (!load?.vehicles) {
|
||||||
|
return res.json({ error: 'No driver assigned or location not available' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
driver_name: load.vehicles.driver_name,
|
||||||
|
lat: load.vehicles.current_lat,
|
||||||
|
lng: load.vehicles.current_lng,
|
||||||
|
last_updated: load.vehicles.updated_at,
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
@ -210,6 +210,7 @@ app.use('/portal', require('./routes/portal'));
|
||||||
app.use('/invoices', require('./routes/invoices'));
|
app.use('/invoices', require('./routes/invoices'));
|
||||||
app.use('/portal-users', require('./routes/portal-users'));
|
app.use('/portal-users', require('./routes/portal-users'));
|
||||||
app.use('/api', require('./routes/api'));
|
app.use('/api', require('./routes/api'));
|
||||||
|
app.use('/api/location', require('./routes/location'));
|
||||||
app.use('/marketplace', require('./routes/marketplace'));
|
app.use('/marketplace', require('./routes/marketplace'));
|
||||||
app.use('/escrow', require('./routes/payments'));
|
app.use('/escrow', require('./routes/payments'));
|
||||||
app.use('/admin/moderation', require('./routes/admin-moderation'));
|
app.use('/admin/moderation', require('./routes/admin-moderation'));
|
||||||
|
|
|
||||||
205
webapp/src/views/pages/marketplace/bulk-parser.ejs
Normal file
205
webapp/src/views/pages/marketplace/bulk-parser.ejs
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
<%- include('../partials/portal-header', { activeMenu: 'parser' }) %>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<h1 class="page-title">📱 Bulk WhatsApp Parser</h1>
|
||||||
|
<p class="page-subtitle">Paste multiple WhatsApp messages at once to create loads in bulk</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><h3 class="card-title">Paste Messages</h3></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted" style="font-size:13px;margin-bottom:12px;">
|
||||||
|
Paste multiple WhatsApp messages (one per line or separated by blank lines).
|
||||||
|
Each message will be parsed and you can review before saving.
|
||||||
|
</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea id="bulkInput" class="form-input" rows="12" placeholder="Paste WhatsApp messages here...
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Kahn Transport KL01AB1234 Bangalore to Chennai freight 50000 loaded
|
||||||
|
Agarwal MH12CD5678 Delhi to Mumbai 75000 advance 30000 in transit
|
||||||
|
TN09EF9012 Coimbatore to Hyderabad 45000 delivered"></textarea>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px;">
|
||||||
|
<button type="button" class="btn btn-primary" onclick="parseBulk()">📱 Parse All Messages</button>
|
||||||
|
<button type="button" class="btn btn-outline" onclick="clearAll()">❌ Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Parsed Results <span id="parseCount" class="badge badge-primary" style="display:none;"></span></h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" style="padding:0;">
|
||||||
|
<div id="bulkResults">
|
||||||
|
<div class="empty-state" style="padding:48px;">
|
||||||
|
<div class="empty-icon">📱</div>
|
||||||
|
<h3>No messages parsed yet</h3>
|
||||||
|
<p>Paste WhatsApp messages and click Parse</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Review & Save Section -->
|
||||||
|
<div id="reviewSection" class="card mt-3" style="display:none;">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Review & Save Loads</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="reviewList"></div>
|
||||||
|
<div style="display:flex;gap:8px;margin-top:16px;">
|
||||||
|
<button type="button" class="btn btn-success" onclick="saveAll()">💾 Save All Valid Loads</button>
|
||||||
|
<button type="button" class="btn btn-outline" onclick="selectAllToggle()">☑ Select All</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let parsedMessages = [];
|
||||||
|
|
||||||
|
async function parseBulk() {
|
||||||
|
const input = document.getElementById('bulkInput').value.trim();
|
||||||
|
if (!input) return alert('Paste some messages first');
|
||||||
|
|
||||||
|
// Split by double newlines or single newlines (each line = one message)
|
||||||
|
const messages = input.split(/\n\s*\n|\n/).filter(m => m.trim().length > 0);
|
||||||
|
|
||||||
|
if (messages.length === 0) return alert('No messages found');
|
||||||
|
|
||||||
|
parsedMessages = [];
|
||||||
|
let html = '<div style="padding:16px;">';
|
||||||
|
|
||||||
|
for (let i = 0; i < messages.length; i++) {
|
||||||
|
const msg = messages[i].trim();
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/parse-whatsapp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ message: msg })
|
||||||
|
});
|
||||||
|
const parsed = await res.json();
|
||||||
|
parsedMessages.push({ original: msg, parsed, index: i });
|
||||||
|
|
||||||
|
const confidenceColor = parsed.confidence === 'high' ? '#2e7d32' : parsed.confidence === 'medium' ? '#f59e0b' : '#666';
|
||||||
|
const fields = parsed.parsed_fields?.join(', ') || 'none';
|
||||||
|
|
||||||
|
html += `<div style="padding:12px;border-bottom:1px solid #f0ede5;">
|
||||||
|
<div style="display:flex;justify-content:space-between;margin-bottom:6px;">
|
||||||
|
<strong>Message ${i + 1}</strong>
|
||||||
|
<span class="badge" style="background:${confidenceColor}20;color:${confidenceColor};">${parsed.confidence}</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px;color:#666;margin-bottom:8px;white-space:pre-wrap;">${msg.substring(0, 100)}${msg.length > 100 ? '...' : ''}</div>
|
||||||
|
<div style="font-size:13px;">
|
||||||
|
<strong>${parsed.shipper || 'Unknown'}</strong>
|
||||||
|
${parsed.from_city ? parsed.from_city + ' → ' + (parsed.to_city || '?') : ''}
|
||||||
|
${parsed.freight_charged ? ' · ₹' + parsed.freight_charged.toLocaleString('en-IN') : ''}
|
||||||
|
${parsed.vehicle ? ' · ' + parsed.vehicle : ''}
|
||||||
|
<br><small style="color:#999;">Fields: ${fields}</small>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
} catch (e) {
|
||||||
|
parsedMessages.push({ original: msg, parsed: null, index: i, error: e.message });
|
||||||
|
html += `<div style="padding:12px;border-bottom:1px solid #f0ede5;">
|
||||||
|
<strong>Message ${i + 1}</strong>
|
||||||
|
<span class="badge badge-danger">Error</span>
|
||||||
|
<div style="font-size:12px;color:#666;">${msg.substring(0, 80)}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
document.getElementById('bulkResults').innerHTML = html;
|
||||||
|
document.getElementById('parseCount').textContent = `${messages.length} parsed`;
|
||||||
|
document.getElementById('parseCount').style.display = 'inline';
|
||||||
|
|
||||||
|
// Show review section for valid ones
|
||||||
|
const validMessages = parsedMessages.filter(m => m.parsed && m.parsed.confidence !== 'low');
|
||||||
|
if (validMessages.length > 0) {
|
||||||
|
showReview(validMessages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showReview(messages) {
|
||||||
|
const section = document.getElementById('reviewSection');
|
||||||
|
const list = document.getElementById('reviewList');
|
||||||
|
|
||||||
|
let html = '<table class="table"><thead><tr><th>Select</th><th>Shipper</th><th>Route</th><th>Freight</th><th>Vehicle</th><th>Status</th></tr></thead><tbody>';
|
||||||
|
|
||||||
|
messages.forEach((m, i) => {
|
||||||
|
const p = m.parsed;
|
||||||
|
html += `<tr>
|
||||||
|
<td><input type="checkbox" class="load-checkbox" data-index="${m.index}" checked></td>
|
||||||
|
<td>${p.shipper || '<span style="color:#999;">Unknown</span>'}</td>
|
||||||
|
<td>${p.from_city || '?'} → ${p.to_city || '?'}</td>
|
||||||
|
<td>₹${(p.freight_charged || 0).toLocaleString('en-IN')}</td>
|
||||||
|
<td>${p.vehicle || '-'}</td>
|
||||||
|
<td><span class="badge badge-gray">${p.status || 'pending'}</span></td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table>';
|
||||||
|
list.innerHTML = html;
|
||||||
|
section.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveAll() {
|
||||||
|
const checkboxes = document.querySelectorAll('.load-checkbox:checked');
|
||||||
|
if (checkboxes.length === 0) return alert('Select at least one load');
|
||||||
|
|
||||||
|
let saved = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const cb of checkboxes) {
|
||||||
|
const index = parseInt(cb.dataset.index);
|
||||||
|
const { parsed } = parsedMessages[index];
|
||||||
|
if (!parsed) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/loads', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
shipper: parsed.shipper,
|
||||||
|
from_city: parsed.from_city,
|
||||||
|
to_city: parsed.to_city,
|
||||||
|
vehicle: parsed.vehicle,
|
||||||
|
freight_charged: parsed.freight_charged,
|
||||||
|
advance_received: parsed.advance_received,
|
||||||
|
status: parsed.status || 'pending lead',
|
||||||
|
notes: parsed.notes || '',
|
||||||
|
source: 'whatsapp_bulk',
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (res.ok) saved++;
|
||||||
|
else failed++;
|
||||||
|
} catch (e) {
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(`Saved ${saved} loads. ${failed} failed.`);
|
||||||
|
if (saved > 0) window.location.href = '/loads';
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllToggle() {
|
||||||
|
const boxes = document.querySelectorAll('.load-checkbox');
|
||||||
|
const allChecked = Array.from(boxes).every(b => b.checked);
|
||||||
|
boxes.forEach(b => b.checked = !allChecked);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
document.getElementById('bulkInput').value = '';
|
||||||
|
document.getElementById('bulkResults').innerHTML = '<div class="empty-state" style="padding:48px;"><div class="empty-icon">📱</div><h3>No messages parsed yet</h3><p>Paste WhatsApp messages and click Parse</p></div>';
|
||||||
|
document.getElementById('parseCount').style.display = 'none';
|
||||||
|
document.getElementById('reviewSection').style.display = 'none';
|
||||||
|
parsedMessages = [];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<%- include('../partials/portal-footer') %>
|
||||||
Loading…
Reference in a new issue