[OWL] Bug fixes + seed data + bulk parser route
Some checks are pending
FreightDesk CI/CD / Lint & Test (push) Waiting to run
FreightDesk CI/CD / Build Docker Image (push) Blocked by required conditions
FreightDesk CI/CD / Deploy to Coolify (push) Blocked by required conditions

Fixes:
- Negotiate route: added auth check (only shipper or bidder can negotiate)
- Negotiate route: added notification to other party
- All payment views: removed /100 division (amounts stored in rupees, not paise)
- Migration 006: updated platform_config seed values to rupees
- Migration 007: added current_lat/current_lng columns to vehicles table
- Added bulk-parser route to marketplace.js
- Added Bulk WhatsApp Parser link to portal sidebar

Seed Data:
- scripts/seed-demo.js: 5 shippers, 5 drivers, 8 loads, sample bids
- Idempotent: skips if data already exists
This commit is contained in:
FreightDesk 2026-06-08 02:16:02 +00:00
parent 59d93d5281
commit 9b5e568e72
9 changed files with 196 additions and 15 deletions

View file

@ -83,13 +83,13 @@ CREATE TABLE IF NOT EXISTS platform_config (
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
); );
-- Default fee settings -- Default fee settings (amounts in rupees)
INSERT INTO platform_config (key, value, description) VALUES INSERT INTO platform_config (key, value, description) VALUES
('escrow.platform_fee_percent', '5', 'Platform commission percentage'), ('escrow.platform_fee_percent', '5', 'Platform commission percentage'),
('escrow.min_deposit_amount', '10000', 'Minimum deposit amount in paise (₹100)'), ('escrow.min_deposit_amount', '100', 'Minimum deposit in rupees'),
('escrow.hold_period_hours', '72', 'Hours to hold funds after delivery before auto-release'), ('escrow.hold_period_hours', '72', 'Hours to hold funds after delivery before auto-release'),
('escrow.payout_min_amount', '50000', 'Minimum payout request in paise (₹500)'), ('escrow.payout_min_amount', '500', 'Minimum payout request in rupees'),
('escrow.payout_fee', '0', 'Payout processing fee in paise') ('escrow.payout_fee', '0', 'Payout processing fee in rupees')
ON CONFLICT (key) DO NOTHING; ON CONFLICT (key) DO NOTHING;
-- ============================================================ -- ============================================================

View file

@ -18,6 +18,10 @@ CREATE INDEX IF NOT EXISTS idx_vehicle_locations_vehicle ON vehicle_locations(ve
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_time ON vehicle_locations(recorded_at); 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); CREATE INDEX IF NOT EXISTS idx_vehicle_locations_vehicle_time ON vehicle_locations(vehicle_id, recorded_at DESC);
-- Add current location columns to vehicles
ALTER TABLE vehicles ADD COLUMN IF NOT EXISTS current_lat DECIMAL(10,8);
ALTER TABLE vehicles ADD COLUMN IF NOT EXISTS current_lng DECIMAL(11,8);
-- Enable PostGIS-like functionality with btree_gist for spatial queries -- Enable PostGIS-like functionality with btree_gist for spatial queries
-- (In production, use PostGIS extension) -- (In production, use PostGIS extension)
CREATE INDEX IF NOT EXISTS idx_vehicles_location ON vehicles(current_lat, current_lng) CREATE INDEX IF NOT EXISTS idx_vehicles_location ON vehicles(current_lat, current_lng)

138
webapp/scripts/seed-demo.js Normal file
View file

@ -0,0 +1,138 @@
#!/usr/bin/env node
/**
* FreightDesk Demo Seed Data Script
* Run: node scripts/seed-demo.js
*
* Creates demo shippers, drivers, loads, and bids for testing.
* Requires SUPABASE_URL and SUPABASE_SERVICE_KEY in environment.
*/
const supabase = require('../src/services/supabase');
const DEMO_SHIPPERS = [
{ name: 'Kahn Transport', phone: '+919876543210', email: 'kahn@example.com', company_name: 'Kahn Transport Pvt Ltd', city: 'Kochi', state: 'Kerala', address: 'MG Road, Ernakulam', is_verified: true },
{ name: 'Agarwal Logistics', phone: '+918765432109', email: 'agarwal@example.com', company_name: 'Agarwal Trading Co', city: 'Bangalore', state: 'Karnataka', address: 'KR Market', is_verified: true },
{ name: 'Rajesh Freight', phone: '+917654321098', email: 'rajesh@example.com', city: 'Chennai', state: 'Tamil Nadu', address: 'T Nagar', is_verified: true },
{ name: 'Sharma Carriers', phone: '+916543210987', email: 'sharma@example.com', company_name: 'Sharma & Sons', city: 'Mumbai', state: 'Maharashtra', address: 'Andheri', is_verified: true },
{ name: 'VIP Logistics', phone: '+915432109876', email: 'vip@example.com', city: 'Hyderabad', state: 'Telangana', address: 'Banjara Hills', is_verified: false },
];
const DEMO_DRIVERS = [
{ driver_name: 'Suresh Kumar', phone: '+919988776655', vehicle_number: 'KL 01 AB 1234', vehicle_type: '14ft', capacity_tons: 7, city: 'Kochi', state: 'Kerala', is_verified: true },
{ driver_name: 'Ramesh Singh', phone: '+918877665544', vehicle_number: 'KA 05 CD 5678', vehicle_type: '17ft', capacity_tons: 9, city: 'Bangalore', state: 'Karnataka', is_verified: true },
{ driver_name: 'Abdul Rahman', phone: '+917766554433', vehicle_number: 'TN 09 EF 9012', vehicle_type: '19ft', capacity_tons: 12, city: 'Chennai', state: 'Tamil Nadu', is_verified: true },
{ driver_name: 'Prakash Yadav', phone: '+916655443322', vehicle_number: 'MH 12 GH 3456', vehicle_type: '20ft', capacity_tons: 14, city: 'Mumbai', state: 'Maharashtra', is_verified: true },
{ driver_name: 'Venkat Rao', phone: '+915544332211', vehicle_number: 'TS 08 IJ 7890', vehicle_type: '17ft', capacity_tons: 9, city: 'Hyderabad', state: 'Telangana', is_verified: false },
];
const DEMO_LOADS = [
{ from_city: 'Bangalore', to_city: 'Kochi', load_type: 'ftl', weight_kg: 8000, material_type: 'Electronics', budget_min: 25000, budget_max: 35000, pickup_date: '2026-02-15', delivery_date: '2026-02-17' },
{ from_city: 'Chennai', to_city: 'Mumbai', load_type: 'ftl', weight_kg: 12000, material_type: 'Machine Parts', budget_min: 45000, budget_max: 55000, pickup_date: '2026-02-16', delivery_date: '2026-02-19' },
{ from_city: 'Mumbai', to_city: 'Hyderabad', load_type: 'ftl', weight_kg: 10000, material_type: 'Chemicals', budget_min: 30000, budget_max: 40000, pickup_date: '2026-02-17', delivery_date: '2026-02-20' },
{ from_city: 'Hyderabad', to_city: 'Bangalore', load_type: 'ftl', weight_kg: 9000, material_type: 'Textiles', budget_min: 20000, budget_max: 28000, pickup_date: '2026-02-18', delivery_date: '2026-02-21' },
{ from_city: 'Kochi', to_city: 'Chennai', load_type: 'ftl', weight_kg: 7000, material_type: 'Spices', budget_min: 18000, budget_max: 25000, pickup_date: '2026-02-19', delivery_date: '2026-02-22' },
{ from_city: 'Bangalore', to_city: 'Mumbai', load_type: 'ptl', weight_kg: 3000, material_type: 'Auto Parts', budget_min: 12000, budget_max: 18000, pickup_date: '2026-02-20', delivery_date: '2026-02-23' },
{ from_city: 'Delhi', to_city: 'Bangalore', load_type: 'ftl', weight_kg: 15000, material_type: 'Furniture', budget_min: 55000, budget_max: 70000, pickup_date: '2026-02-21', delivery_date: '2026-02-25' },
{ from_city: 'Chennai', to_city: 'Kochi', load_type: 'ftl', weight_kg: 6000, material_type: 'Tea', budget_min: 15000, budget_max: 22000, pickup_date: '2026-02-22', delivery_date: '2026-02-24' },
];
async function seed() {
console.log('🌱 Seeding FreightDesk demo data...\n');
// Check if already seeded
const { count: existingLoads } = await supabase.from('loads').select('*', { count: 'exact', head: true });
if (existingLoads > 0) {
console.log(`⚠️ Found ${existingLoads} existing loads. Skipping seed. (Delete manually to re-seed)`);
process.exit(0);
}
// Seed shippers
console.log('📦 Creating shippers...');
const { data: shippers, error: shipperError } = await supabase.from('shippers').insert(DEMO_SHIPPERS).select();
if (shipperError) { console.error('Shipper error:', shipperError); process.exit(1); }
console.log(`${shippers.length} shippers created`);
// Seed vehicles (drivers)
console.log('🚛 Creating drivers/vehicles...');
const driverRecords = DEMO_DRIVERS.map(d => ({
number: d.vehicle_number,
vehicle_type: d.vehicle_type,
capacity_tons: d.capacity_tons,
city: d.city,
state: d.state,
is_verified: d.is_verified,
driver_name: d.driver_name,
phone: d.phone,
}));
const { data: vehicles, error: vehicleError } = await supabase.from('vehicles').insert(driverRecords).select();
if (vehicleError) { console.error('Vehicle error:', vehicleError); process.exit(1); }
console.log(`${vehicles.length} drivers/vehicles created`);
// Seed loads
console.log('📋 Creating marketplace loads...');
const loadRecords = DEMO_LOADS.map((l, i) => ({
...l,
shipper_id: shippers[i % shippers.length]?.id,
status: 'pending lead',
is_open: true,
expires_at: new Date(Date.now() + 7 * 86400000).toISOString(),
views: Math.floor(Math.random() * 50),
pickup_address: 'Pickup location TBD',
delivery_address: 'Delivery location TBD',
}));
const { data: loads, error: loadError } = await supabase.from('loads').insert(loadRecords).select();
if (loadError) { console.error('Load error:', loadError); process.exit(1); }
console.log(`${loads.length} loads created`);
// Seed some bids
console.log('💰 Creating sample bids...');
const bidRecords = [];
for (const load of loads.slice(0, 4)) {
const numBids = Math.floor(Math.random() * 3) + 1;
for (let i = 0; i < numBids; i++) {
const vehicle = vehicles[i % vehicles.length];
const baseLoad = DEMO_LOADS[loads.indexOf(load)];
const bidAmount = baseLoad.budget_min + Math.floor(Math.random() * (baseLoad.budget_max - baseLoad.budget_min) * 0.3);
bidRecords.push({
load_id: load.id,
shipper_id: load.shipper_id,
driver_id: vehicle.id,
amount: bidAmount,
message: `Available for immediate pickup. ${vehicle.vehicle_type} truck. Contact: ${vehicle.phone}`,
status: 'pending',
});
}
}
if (bidRecords.length > 0) {
const { error: bidError } = await supabase.from('bids').insert(bidRecords);
if (bidError) { console.error('Bid error:', bidError); process.exit(1); }
console.log(`${bidRecords.length} bids created`);
}
// Ensure platform config
console.log('⚙️ Setting platform config...');
await supabase.from('platform_config').upsert([
{ key: 'escrow.platform_fee_percent', value: '5', description: 'Platform commission percentage' },
{ key: 'escrow.min_deposit_amount', value: '100', description: 'Minimum deposit in rupees' },
{ key: 'escrow.hold_period_hours', value: '72', description: 'Hours to hold funds after delivery' },
{ key: 'escrow.payout_min_amount', value: '500', description: 'Minimum payout in rupees' },
], { onConflict: 'key' });
console.log('\n✅ Seed complete!');
console.log('\n📊 Demo data:');
console.log(` ${shippers.length} shippers (${shippers.filter(s => s.is_verified).length} verified)`);
console.log(` ${vehicles.length} drivers (${vehicles.filter(v => v.is_verified).length} verified)`);
console.log(` ${loads.length} marketplace loads`);
console.log(` ${bidRecords.length} bids`);
console.log('\n🌐 Access the app:');
console.log(' Landing: http://localhost:3000/');
console.log(' Admin: http://localhost:3000/login');
console.log(' Marketplace: http://localhost:3000/marketplace');
console.log(' Portal: http://localhost:3000/portal');
process.exit(0);
}
seed().catch(err => {
console.error('❌ Seed failed:', err);
process.exit(1);
});

View file

@ -123,6 +123,11 @@ router.get('/post', requirePortalAuth, requireRole('shipper'), (req, res) => {
res.render('pages/marketplace/post', { error: null, formData: {} }); res.render('pages/marketplace/post', { error: null, formData: {} });
}); });
// Bulk WhatsApp parser page
router.get('/bulk-parser', requirePortalAuth, requireRole('shipper'), (req, res) => {
res.render('pages/marketplace/bulk-parser');
});
router.post('/post', requirePortalAuth, requireRole('shipper'), asyncHandler(async (req, res) => { router.post('/post', requirePortalAuth, requireRole('shipper'), asyncHandler(async (req, res) => {
const { const {
from_city, to_city, via, load_type, weight_kg, material_type, from_city, to_city, via, load_type, weight_kg, material_type,
@ -247,13 +252,43 @@ router.post('/bid/:bidId/negotiate', requirePortalAuth, asyncHandler(async (req,
const { proposed_amount, message } = req.body; const { proposed_amount, message } = req.body;
if (!proposed_amount || parseInt(proposed_amount) <= 0) return res.status(400).json({ error: 'Valid amount required' }); if (!proposed_amount || parseInt(proposed_amount) <= 0) return res.status(400).json({ error: 'Valid amount required' });
// Verify user is party to this bid
const { data: bid } = await supabase
.from('bids')
.select('*, loads(shipper_id)')
.eq('id', req.params.bidId)
.single();
if (!bid) return res.status(404).json({ error: 'Bid not found' });
const userId = req.session.portalUser.id;
const isShipper = req.session.portalUser.role === 'shipper';
const isDriver = req.session.portalUser.role === 'driver' && req.session.portalUser.driver_id === bid.driver_id;
if (!isShipper && !isDriver) {
return res.status(403).json({ error: 'Only the shipper or bidder can negotiate' });
}
const { error } = await supabase.from('negotiations').insert({ const { error } = await supabase.from('negotiations').insert({
bid_id: req.params.bidId, proposed_by: req.session.portalUser.id, bid_id: req.params.bidId, proposed_by: userId,
proposed_amount: parseInt(proposed_amount), message: message || null, proposed_amount: parseInt(proposed_amount), message: message || null,
}); });
if (error) return res.status(400).json({ error: error.message }); if (error) return res.status(400).json({ error: error.message });
await supabase.from('bids').update({ status: 'negotiating' }).eq('id', req.params.bidId); await supabase.from('bids').update({ status: 'negotiating' }).eq('id', req.params.bidId);
// Notify the other party
const notifyUserId = isShipper ? bid.driver_id : bid.loads?.shipper_id;
if (notifyUserId) {
await supabase.from('notifications').insert({
user_id: notifyUserId,
type: 'negotiation',
title: 'Counter Offer',
message: `${parseInt(proposed_amount).toLocaleString('en-IN')} counter offer on your bid`,
data: { bid_id: bid.id, load_id: bid.load_id },
});
}
res.json({ success: true }); res.json({ success: true });
})); }));

View file

@ -114,7 +114,7 @@
<strong><%= p.vehicles?.driver_name || 'N/A' %></strong> <strong><%= p.vehicles?.driver_name || 'N/A' %></strong>
<br><small style="color:#666;"><%= p.vehicles?.number || '' %></small> <br><small style="color:#666;"><%= p.vehicles?.number || '' %></small>
</td> </td>
<td style="font-weight:700;">&#8377; <%= (p.amount / 100).toLocaleString('en-IN') %></td> <td style="font-weight:700;">&#8377; <%= (p.amount).toLocaleString('en-IN') %></td>
<td><%= p.upi_id ? 'UPI' : 'Bank' %></td> <td><%= p.upi_id ? 'UPI' : 'Bank' %></td>
<td> <td>
<button class="btn btn-sm btn-success" onclick="processPayout('<%= p.id %>', 'approve')">&#10004; Process</button> <button class="btn btn-sm btn-success" onclick="processPayout('<%= p.id %>', 'approve')">&#10004; Process</button>

View file

@ -57,12 +57,12 @@
<div class="card-header"><h3 class="card-title">Current Balance</h3></div> <div class="card-header"><h3 class="card-title">Current Balance</h3></div>
<div class="card-body" style="text-align:center;padding:24px;"> <div class="card-body" style="text-align:center;padding:24px;">
<div style="font-size:32px;font-weight:700;color:#000080;"> <div style="font-size:32px;font-weight:700;color:#000080;">
&#8377; <%= ((account?.balance || 0) / 100).toLocaleString('en-IN') %> &#8377; <%= (account?.balance || 0).toLocaleString('en-IN') %>
</div> </div>
<div style="font-size:13px;color:#666;">Available</div> <div style="font-size:13px;color:#666;">Available</div>
<% if (account?.held_balance > 0) { %> <% if (account?.held_balance > 0) { %>
<div style="margin-top:8px;font-size:14px;color:#f59e0b;"> <div style="margin-top:8px;font-size:14px;color:#f59e0b;">
&#8377; <%= (account.held_balance / 100).toLocaleString('en-IN') %> in escrow &#8377; <%= (account.held_balance).toLocaleString('en-IN') %> in escrow
</div> </div>
<% } %> <% } %>
</div> </div>

View file

@ -13,12 +13,12 @@
<div class="card-header"><h3 class="card-title">Account Balance</h3></div> <div class="card-header"><h3 class="card-title">Account Balance</h3></div>
<div class="card-body" style="text-align:center;padding:32px;"> <div class="card-body" style="text-align:center;padding:32px;">
<div style="font-size:36px;font-weight:700;color:#000080;"> <div style="font-size:36px;font-weight:700;color:#000080;">
&#8377; <%= ((account?.balance || 0) / 100).toLocaleString('en-IN') %> &#8377; <%= (account?.balance || 0).toLocaleString('en-IN') %>
</div> </div>
<div style="font-size:13px;color:#666;margin-top:4px;">Available Balance</div> <div style="font-size:13px;color:#666;margin-top:4px;">Available Balance</div>
<% if (account?.held_balance > 0) { %> <% if (account?.held_balance > 0) { %>
<div style="margin-top:12px;font-size:14px;color:#f59e0b;"> <div style="margin-top:12px;font-size:14px;color:#f59e0b;">
&#8377; <%= (account.held_balance / 100).toLocaleString('en-IN') %> in escrow &#8377; <%= (account.held_balance).toLocaleString('en-IN') %> in escrow
</div> </div>
<% } %> <% } %>
<div style="margin-top:16px;display:flex;gap:8px;justify-content:center;"> <div style="margin-top:16px;display:flex;gap:8px;justify-content:center;">
@ -37,13 +37,13 @@
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div style="text-align:center;padding:12px;background:#e8f5e9;border-radius:8px;"> <div style="text-align:center;padding:12px;background:#e8f5e9;border-radius:8px;">
<div style="font-size:24px;font-weight:700;color:#2e7d32;"> <div style="font-size:24px;font-weight:700;color:#2e7d32;">
&#8377; <%= ((account?.total_deposited || 0) / 100).toLocaleString('en-IN') %> &#8377; <%= ((account?.total_deposited || 0)).toLocaleString('en-IN') %>
</div> </div>
<div style="font-size:12px;color:#666;">Total Deposited</div> <div style="font-size:12px;color:#666;">Total Deposited</div>
</div> </div>
<div style="text-align:center;padding:12px;background:#fff3e0;border-radius:8px;"> <div style="text-align:center;padding:12px;background:#fff3e0;border-radius:8px;">
<div style="font-size:24px;font-weight:700;color:#e65100;"> <div style="font-size:24px;font-weight:700;color:#e65100;">
&#8377; <%= ((account?.total_withdrawn || 0) / 100).toLocaleString('en-IN') %> &#8377; <%= ((account?.total_withdrawn || 0)).toLocaleString('en-IN') %>
</div> </div>
<div style="font-size:12px;color:#666;">Total Withdrawn</div> <div style="font-size:12px;color:#666;">Total Withdrawn</div>
</div> </div>
@ -81,7 +81,7 @@
</td> </td>
<td style="font-weight:600;"> <td style="font-weight:600;">
<%= tx.type === 'deposit' || tx.type === 'release' ? '+' : '-' %> <%= tx.type === 'deposit' || tx.type === 'release' ? '+' : '-' %>
&#8377; <%= (tx.amount / 100).toLocaleString('en-IN') %> &#8377; <%= (tx.amount).toLocaleString('en-IN') %>
</td> </td>
<td> <td>
<% if (tx.loads) { %> <% if (tx.loads) { %>

View file

@ -18,7 +18,7 @@
<div class="card-body"> <div class="card-body">
<div style="text-align:center;padding:16px;background:#e8f5e9;border-radius:8px;margin-bottom:16px;"> <div style="text-align:center;padding:16px;background:#e8f5e9;border-radius:8px;margin-bottom:16px;">
<div style="font-size:28px;font-weight:700;color:#2e7d32;"> <div style="font-size:28px;font-weight:700;color:#2e7d32;">
&#8377; <%= ((account?.balance || 0) / 100).toLocaleString('en-IN') %> &#8377; <%= (account?.balance || 0).toLocaleString('en-IN') %>
</div> </div>
<div style="font-size:12px;color:#666;">Available for withdrawal</div> <div style="font-size:12px;color:#666;">Available for withdrawal</div>
</div> </div>
@ -86,7 +86,7 @@
<tbody> <tbody>
<% for (const p of payouts) { %> <% for (const p of payouts) { %>
<tr> <tr>
<td style="font-weight:600;">&#8377; <%= (p.amount / 100).toLocaleString('en-IN') %></td> <td style="font-weight:600;">&#8377; <%= (p.amount).toLocaleString('en-IN') %></td>
<td> <td>
<% if (p.upi_id) { %> <% if (p.upi_id) { %>
UPI: <%= p.upi_id %> UPI: <%= p.upi_id %>

View file

@ -49,6 +49,10 @@
<% } %> <% } %>
<a href="/marketplace/notifications" class="sidebar-link <%= typeof activeMenu !== 'undefined' && activeMenu === 'notifications' ? 'active' : '' %>">&#128276; Notifications</a> <a href="/marketplace/notifications" class="sidebar-link <%= typeof activeMenu !== 'undefined' && activeMenu === 'notifications' ? 'active' : '' %>">&#128276; Notifications</a>
</div> </div>
<div class="sidebar-section">
<span class="sidebar-title">Tools</span>
<a href="/marketplace/bulk-parser" class="sidebar-link <%= typeof activeMenu !== 'undefined' && activeMenu === 'bulk-parser' ? 'active' : '' %>">&#128241; Bulk WhatsApp Parser</a>
</div>
<div class="sidebar-section" style="padding:12px 16px;border-top:1px solid #e0ddd5;"> <div class="sidebar-section" style="padding:12px 16px;border-top:1px solid #e0ddd5;">
<span class="sidebar-title">Quick Links</span> <span class="sidebar-title">Quick Links</span>
<a href="/" class="sidebar-link">&#127968; Main Site</a> <a href="/" class="sidebar-link">&#127968; Main Site</a>