[OWL] WhatsApp parser v2 + mobile responsiveness + parser API
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

WhatsApp Parser v2:
- Pre-processing: normalize whitespace, expand abbreviations (frt→freight, adv→advance, etc.)
- Number format normalization: 1.5L→150000, 50K→50000, 2.5lakhs→250000
- Context-aware amount classification (freight vs advance vs commission vs driver pay)
- Multiple route patterns: 'X to Y', 'X → Y', 'From X to Y', 'X - Y'
- Vehicle number normalization with flexible spacing
- Date extraction (DD/MM/YYYY, DD-MM-YY, 15 Jan 2026)
- Material type and weight extraction
- Auto-calculate commission (5% default or freight - driver rate)
- Auto-calculate pending amounts
- Confidence scoring (high/medium/low)
- POST /api/parse-whatsapp endpoint
- GET /api/parser/test with sample messages

Mobile Responsiveness:
- Hamburger menu button on mobile
- Slide-in sidebar overlay
- Responsive stats grid (4→2→1 columns)
- Stacked filters and form rows on small screens
- Full-width action buttons
- Smaller login container on mobile
- Horizontal scroll for tables
- Pagination stacking on small screens
This commit is contained in:
FreightDesk 2026-06-08 01:15:11 +00:00
parent 07c025e698
commit 04657b9f29
5 changed files with 449 additions and 103 deletions

View file

@ -493,12 +493,67 @@ body {
.grid-2 { grid-template-columns: 1fr; } .grid-2 { grid-template-columns: 1fr; }
.sidebar { display: none; } .sidebar { display: none; }
.stats-grid { grid-template-columns: 1fr 1fr; } .stats-grid { grid-template-columns: 1fr 1fr; }
.mobile-menu-btn { display: flex; }
.main-content { padding: 12px; }
} }
@media (max-width: 600px) { @media (max-width: 600px) {
.stats-grid { grid-template-columns: 1fr; } .stats-grid { grid-template-columns: 1fr; }
.form-row { flex-direction: column; } .form-row { flex-direction: column; }
.filter-bar { flex-direction: column; } .filter-bar { flex-direction: column; }
.filter-bar .form-group { width: 100%; }
.page-header { flex-direction: column; gap: 12px; align-items: flex-start; }
.page-actions { width: 100%; display: flex; gap: 8px; }
.page-actions .btn { flex: 1; text-align: center; }
.card-header { flex-direction: column; gap: 8px; }
.table-responsive { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.topbar { padding: 0 12px; }
.brand-hi { font-size: 13px; }
.brand-en { font-size: 9px; }
.login-container { margin: 16px; padding: 24px 20px; }
.detail-grid { grid-template-columns: 1fr; }
.pagination { flex-direction: column; gap: 8px; text-align: center; }
}
/* Mobile menu toggle button */
.mobile-menu-btn {
display: none;
background: none;
border: none;
color: var(--white);
font-size: 24px;
cursor: pointer;
padding: 4px 8px;
}
/* Mobile sidebar overlay */
.sidebar-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 998;
}
.sidebar-overlay.active { display: block; }
@media (max-width: 900px) {
.sidebar.mobile-open {
display: block;
position: fixed;
top: 64px;
left: 0;
bottom: 0;
z-index: 999;
width: 260px;
box-shadow: 4px 0 16px rgba(0,0,0,0.3);
animation: slideIn 0.2s ease-out;
}
}
@keyframes slideIn {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
} }
/* ============================================================ /* ============================================================
@ -564,6 +619,46 @@ body {
border: 1px solid rgba(19,136,8,0.2); border: 1px solid rgba(19,136,8,0.2);
} }
/* ============================================================
WHATSAPP PARSER
============================================================ */
.parse-fields {
display: grid;
grid-template-columns: 140px 1fr;
gap: 6px 12px;
margin: 12px 0;
}
.parse-field {
display: contents;
}
.parse-key {
font-size: 12px;
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
}
.parse-val {
font-size: 14px;
color: var(--text);
}
.parse-result {
background: rgba(0,0,128,0.04);
border: 1px solid rgba(0,0,128,0.15);
border-radius: var(--radius);
padding: 16px;
}
.parse-result h4 {
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
/* ============================================================ /* ============================================================
EMPTY STATE EMPTY STATE
============================================================ */ ============================================================ */

View file

@ -43,6 +43,17 @@ document.querySelectorAll('form[onsubmit]').forEach(function(form) {
// WhatsApp parser (inline function for form page) // WhatsApp parser (inline function for form page)
// parseWhatsApp() and applyParsed() are defined inline in the form view // parseWhatsApp() and applyParsed() are defined inline in the form view
// Mobile menu toggle
function toggleMobileMenu() {
const sidebar = document.querySelector('.sidebar');
const overlay = document.getElementById('sidebarOverlay');
if (sidebar && overlay) {
sidebar.classList.toggle('mobile-open');
overlay.classList.toggle('active');
document.body.style.overflow = sidebar.classList.contains('mobile-open') ? 'hidden' : '';
}
}
// Format number as INR // Format number as INR
function formatINR(num) { function formatINR(num) {
if (num === null || num === undefined || isNaN(num)) return '—'; if (num === null || num === undefined || isNaN(num)) return '—';

View file

@ -212,4 +212,38 @@ router.get('/stats', asyncHandler(async (req, res) => {
}); });
})); }));
// ============================================================
// WHATSAPP PARSER API
// ============================================================
// POST /api/parse-whatsapp — parse a WhatsApp message
router.post('/parse-whatsapp', asyncHandler(async (req, res) => {
const { message } = req.body;
if (!message || typeof message !== 'string') {
return res.status(400).json({ error: 'Message is required' });
}
const { parseWhatsAppMessage } = require('../services/parser');
const result = parseWhatsAppMessage(message);
res.json(result);
}));
// GET /api/parser/test — test parser with sample messages (dev only)
router.get('/parser/test', requireRole('admin'), asyncHandler(async (req, res) => {
const { parseWhatsAppMessage } = require('../services/parser');
const samples = [
'Kahn Transport KL01AB1234 Bangalore to Chennai freight 50000 advance 20000 loaded',
'Vehicle: KL 05 XY 6789 From Mumbai to Kochi via Goa. Freight: ₹75,000/- Driver rate: ₹60,000. Commission: ₹15,000. Delivered.',
'Agarwal Packers MH12CD5678 Delhi to Trivandrum. ₹1.5L freight. ₹50K advance received. In transit.',
'TN09EF9012 Coimbatore to Hyderabad goods: electronics wt: 500kg ₹45000/= assigned vehicle',
];
const results = samples.map(msg => ({
input: msg,
parsed: parseWhatsAppMessage(msg),
}));
res.json(results);
}));
module.exports = router; module.exports = router;

View file

@ -1,9 +1,10 @@
// WhatsApp message parser for FreightDesk // WhatsApp message parser for FreightDesk v2
// Parses natural language freight messages into structured data // Parses natural language freight messages into structured data
// Handles common Kerala/India freight message formats
const { CITIES } = require('../config/constants'); const { CITIES } = require('../config/constants');
// Known shipper names (from existing data) // Known shipper names (from existing data + common Kerala names)
const KNOWN_SHIPPERS = [ const KNOWN_SHIPPERS = [
'Kahn Transport', 'Agarwal Packers and Movers', 'Agarwal', 'Sahara Packers', 'Kahn Transport', 'Agarwal Packers and Movers', 'Agarwal', 'Sahara Packers',
'Ambika Packers', 'Century Polymers', 'DRS', 'Superstar', 'Superstar Packers', 'Ambika Packers', 'Century Polymers', 'DRS', 'Superstar', 'Superstar Packers',
@ -15,27 +16,218 @@ const KNOWN_SHIPPERS = [
'Mohamed Anas', 'Nair', 'Badadosth', 'Mohamed Anas', 'Nair', 'Badadosth',
]; ];
// Status keywords mapping // Status keywords mapping (ordered by specificity — most specific first)
const STATUS_KEYWORDS = { const STATUS_KEYWORDS = {
'pending lead': ['pending lead', 'lead', 'enquiry', 'enquiry'], 'settled': ['settled', 'fully settled', 'payment received in full'],
'assigned vehicle': ['assigned vehicle', 'vehicle assigned'], 'commission received': ['commission received', 'comm received', 'commission got'],
'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 adjusted': ['commission adjusted', 'comm adjusted'],
'commission due': ['commission due', 'comm due'], 'reconciled': ['reconciled', 'recon done'],
'reconciled': ['reconciled'], 'completed': ['completed', 'fully completed'],
'completed': ['completed', 'done'], 'delivered / pending collection': ['delivered', 'delivery done', 'reached', 'reached destination', 'delivered successfully'],
'handled directly by shipper': ['directly by shipper', 'handled directly'], 'pending collection': ['pending collection', 'collection pending', 'to collect', 'amount pending'],
'available vehicle': ['available', 'vehicle available'], 'partially pending': ['partially pending', 'partial payment received'],
'fully pending from shipper': ['fully pending', 'no payment received', 'nothing received'],
'loaded / in transit': ['loaded', 'in transit', 'on the way', 'dispatched', 'started', 'left', 'moving', 'on route'],
'assigned vehicle': ['assigned vehicle', 'vehicle assigned', 'truck assigned'],
'assigned': ['assigned', 'allotted', 'booking confirmed'],
'pending lead': ['pending lead', 'lead', 'enquiry', 'just enquiry'],
'commission due': ['commission due', 'comm due', 'commission pending'],
'cancelled': ['cancelled', 'canceled', 'booking cancelled'],
'available vehicle': ['available', 'vehicle available', 'truck available'],
'partial': ['partial'], 'partial': ['partial'],
'handled directly by shipper': ['directly by shipper', 'handled directly', 'direct handling'],
}; };
// Common abbreviations in Kerala freight messages
const ABBREVIATIONS = {
'frt': 'freight',
'adv': 'advance',
'recd': 'received',
'pd': 'paid',
'coll': 'collection',
'del': 'delivered',
'trpt': 'transport',
'shpr': 'shipper',
'vhcl': 'vehicle',
'drv': 'driver',
'cmn': 'commission',
'amt': 'amount',
'qty': 'quantity',
'wt': 'weight',
'pcs': 'pieces',
'pkt': 'packet',
'ctn': 'carton',
'bdl': 'bundle',
};
/**
* Pre-process message: normalize whitespace, expand abbreviations
*/
function preprocessMessage(text) {
let processed = text.trim();
// Normalize whitespace (WhatsApp often has irregular spacing)
processed = processed.replace(/\r\n/g, '\n').replace(/\s+/g, ' ');
// Expand common abbreviations
for (const [abbr, full] of Object.entries(ABBREVIATIONS)) {
const regex = new RegExp(`\\b${abbr}\\b`, 'gi');
processed = processed.replace(regex, full);
}
// Normalize common number formats
// "1.5L" → "150000", "2.5lakhs" → "250000"
processed = processed.replace(/(\d+\.?\d*)\s*L\b/gi, (m, n) => String(Math.round(parseFloat(n) * 100000)));
processed = processed.replace(/(\d+\.?\d*)\s*(?:lakhs?|lacs?)\b/gi, (m, n) => String(Math.round(parseFloat(n) * 100000)));
// "50K" → "50000"
processed = processed.replace(/(\d+\.?\d*)\s*K\b/gi, (m, n) => String(Math.round(parseFloat(n) * 1000)));
// Normalize vehicle number spacing: "KL 01 AB 1234" → "KL01AB1234"
processed = processed.replace(/\b([A-Z]{2})\s*(\d{1,2})\s*([A-Z]{1,3})\s*(\d{4})\b/gi, '$1$2$3$4');
return processed;
}
/**
* Extract all currency amounts from text with context
*/
function extractAmounts(text) {
const amounts = [];
// Pattern: ₹X,XXX or Rs. X,XXX or X,XXX/-
const patterns = [
/₹\s*([\d,]+(?:\.\d{1,2})?)/g,
/Rs\.?\s*([\d,]+(?:\.\d{1,2})?)/gi,
/INR\s*([\d,]+(?:\.\d{1,2})?)/gi,
/([\d,]+(?:\.\d{1,2})?)\s*\/-(?!\d)/g,
];
for (const pattern of patterns) {
let match;
while ((match = pattern.exec(text)) !== null) {
const value = parseInt(match[1].replace(/,/g, ''));
if (value > 0) {
// Get surrounding context (20 chars before and after)
const start = Math.max(0, match.index - 20);
const end = Math.min(text.length, match.index + match[0].length + 20);
const context = text.substring(start, end).toLowerCase();
amounts.push({ value, context, raw: match[0] });
}
}
}
return amounts;
}
/**
* Determine which amount is which based on context
*/
function classifyAmounts(amounts) {
const classified = {
freight_charged: null,
advance_received: null,
paid_to_driver: null,
commission: null,
driver_freight: null,
};
const contextMap = [
{ field: 'freight_charged', keywords: ['freight', 'charged', 'total', 'amount', 'bill', 'rate', 'frt'] },
{ field: 'advance_received', keywords: ['advance', 'received', 'paid by shipper', 'adv', 'recd'] },
{ field: 'paid_to_driver', keywords: ['paid to driver', 'driver advance', 'driver paid', 'to driver', 'drv paid'] },
{ field: 'commission', keywords: ['commission', 'comm', 'cmn', 'my commission'] },
{ field: 'driver_freight', keywords: ['driver freight', 'driver rate', 'driver amount', 'to driver', 'drv rate'] },
];
for (const amount of amounts) {
let bestMatch = null;
let bestScore = 0;
for (const mapping of contextMap) {
for (const keyword of mapping.keywords) {
if (amount.context.includes(keyword)) {
const score = keyword.length; // longer keyword = more specific match
if (score > bestScore) {
bestScore = score;
bestMatch = mapping.field;
}
}
}
}
if (bestMatch && !classified[bestMatch]) {
classified[bestMatch] = amount.value;
}
}
// If we have amounts but no classification, use heuristics
const unclassified = amounts.filter(a => {
return !Object.values(classified).includes(a.value);
});
if (classified.freight_charged === null && amounts.length > 0) {
// Largest amount is usually freight
const sorted = [...amounts].sort((a, b) => b.value - a.value);
classified.freight_charged = sorted[0].value;
}
return classified;
}
/**
* Parse route with multiple patterns
*/
function parseRoute(text, lower) {
const cities = CITIES || [];
let from_city = null, to_city = null, via = null;
// Build city pattern (escape special regex chars)
const cityPattern = cities.map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
// Pattern 1: "From X to Y" / "X to Y" / "X → Y" / "X - Y"
const routePatterns = [
new RegExp(`(?:from\\s+)?(${cityPattern})\\s*(?:to|→|->||—|-)\\s*(${cityPattern})`, 'i'),
new RegExp(`(${cityPattern})\\s*(?:to|→|->||—|-)\\s*(${cityPattern})`, 'i'),
new RegExp(`(${cityPattern})\\s+to\\s+(${cityPattern})`, 'i'),
];
for (const pattern of routePatterns) {
const match = text.match(pattern);
if (match) {
from_city = match[1];
to_city = match[2];
break;
}
}
// Pattern 2: "via X" for intermediate stops
const viaMatch = text.match(/via\s+([A-Za-z\s]+?)(?:\s+(?:to|→|-|loaded|freight|₹|\d{4,}|$))/i);
if (viaMatch) {
via = viaMatch[1].trim();
}
// Pattern 3: If no route found, try to find any known cities
if (!from_city || !to_city) {
const found = [];
for (const city of cities) {
if (lower.includes(city.toLowerCase())) {
found.push(city);
}
}
if (found.length >= 2 && !from_city && !to_city) {
from_city = found[0];
to_city = found[1];
} else if (found.length === 1 && !to_city) {
to_city = found[0];
}
}
return { from_city, to_city, via };
}
/**
* Main parser function
*/
function parseWhatsAppMessage(text) { function parseWhatsAppMessage(text) {
const result = { const result = {
shipper: null, shipper: null,
@ -51,14 +243,19 @@ function parseWhatsAppMessage(text) {
driver_freight: null, driver_freight: null,
pending_from_shipper: null, pending_from_shipper: null,
pending_to_driver: null, pending_to_driver: null,
date: null,
material: null,
weight: null,
notes: text, notes: text,
confidence: 'low', confidence: 'low',
parsed_fields: [], parsed_fields: [],
}; };
const lower = text.toLowerCase(); // Pre-process message
const processed = preprocessMessage(text);
const lower = processed.toLowerCase();
// 1. Parse shipper // 1. Parse shipper (check known shippers first, then try to extract from context)
for (const shipper of KNOWN_SHIPPERS) { for (const shipper of KNOWN_SHIPPERS) {
if (lower.includes(shipper.toLowerCase())) { if (lower.includes(shipper.toLowerCase())) {
result.shipper = shipper; result.shipper = shipper;
@ -67,43 +264,45 @@ function parseWhatsAppMessage(text) {
} }
} }
// 2. Parse vehicle number (Indian format: XX00XX0000) // If no known shipper, try to extract from patterns like "Shp: X" or "From: X (shipper)"
const vehicleMatch = text.match(/\b([A-Z]{2}\s*\d{1,2}\s*[A-Z]{1,3}\s*\d{4})\b/i); if (!result.shipper) {
if (vehicleMatch) { const shipperPatterns = [
result.vehicle = vehicleMatch[1].replace(/\s/g, '').toUpperCase(); /(?:shp|shipper|from\s+shp|client)\s*[:\\-]\\s*([A-Za-z\s]+?)(?:\\s*(?:to|→|-|vehicle|loaded|freight|₹|\d{4,}|$))/i,
result.parsed_fields.push('vehicle'); /(?:booking\\s+from|received\\s+from)\\s+([A-Za-z\s]+?)(?:\\s*(?:to|→|-|vehicle|loaded|freight|₹|\d{4,}|$))/i,
} ];
for (const pattern of shipperPatterns) {
// 3. Parse cities (from → to pattern) const match = processed.match(pattern);
const cityPattern = CITIES.map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'); if (match) {
const routeMatch = text.match(new RegExp(`(${cityPattern})\\s*(?:to|→|-|via)\\s*(${cityPattern})`, 'i')); result.shipper = match[1].trim();
if (routeMatch) { result.parsed_fields.push('shipper');
result.from_city = routeMatch[1]; break;
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 // 2. Parse vehicle number (Indian format with flexible spacing)
const viaMatch = text.match(/via\s+([A-Za-z\s,]+?)(?:\s*(?:to|→|-|loaded|freight|₹|\d{4,}))/i); const vehiclePatterns = [
if (viaMatch) { /\b([A-Z]{2}\s*\d{1,2}\s*[A-Z]{1,3}\s*\d{4})\b/i, // Standard: KL01AB1234
result.via = viaMatch[1].trim(); /\b([A-Z]{2}\s*\d{2}\s*[A-Z]{2}\s*\d{4})\b/i, // KL 01 AB 1234
result.parsed_fields.push('via'); /\b(vehicle|truck|vhcl)\s*[:#]?\s*([A-Z]{2}\d{1,2}[A-Z]{1,3}\d{4})\b/i, // "Vehicle: KL01AB1234"
];
for (let i = 0; i < vehiclePatterns.length; i++) {
const match = processed.match(vehiclePatterns[i]);
if (match) {
result.vehicle = (match[2] || match[1]).replace(/\s/g, '').toUpperCase();
result.parsed_fields.push('vehicle');
break;
}
} }
// 5. Parse status // 3. Parse route
const route = parseRoute(processed, lower);
if (route.from_city) { result.from_city = route.from_city; result.parsed_fields.push('from_city'); }
if (route.to_city) { result.to_city = route.to_city; result.parsed_fields.push('to_city'); }
if (route.via) { result.via = route.via; result.parsed_fields.push('via'); }
// 4. Parse status (most specific first)
for (const [status, keywords] of Object.entries(STATUS_KEYWORDS)) { for (const [status, keywords] of Object.entries(STATUS_KEYWORDS)) {
for (const kw of keywords) { for (const kw of keywords) {
if (lower.includes(kw)) { if (lower.includes(kw)) {
@ -115,76 +314,79 @@ function parseWhatsAppMessage(text) {
if (result.status) break; if (result.status) break;
} }
// 6. Parse amounts // 5. Parse amounts with context-aware classification
// Freight: look for "freight", "charged", "total" followed by number const amounts = extractAmounts(processed);
const freightMatch = text.match(/(?:freight|charged|total|amount|bill)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i); const classified = classifyAmounts(amounts);
if (freightMatch) {
result.freight_charged = parseInt(freightMatch[1].replace(/,/g, '')); if (classified.freight_charged) { result.freight_charged = classified.freight_charged; result.parsed_fields.push('freight_charged'); }
result.parsed_fields.push('freight_charged'); if (classified.advance_received) { result.advance_received = classified.advance_received; result.parsed_fields.push('advance_received'); }
} else { if (classified.paid_to_driver) { result.paid_to_driver = classified.paid_to_driver; result.parsed_fields.push('paid_to_driver'); }
// Try standalone large numbers (4-6 digits) that could be freight if (classified.commission) { result.commission = classified.commission; result.parsed_fields.push('commission'); }
const amountMatches = text.match(/₹?\s*(\d{4,6})\b/g); if (classified.driver_freight) { result.driver_freight = classified.driver_freight; result.parsed_fields.push('driver_freight'); }
if (amountMatches) {
const amounts = amountMatches.map(m => parseInt(m.replace(/[₹,\s]/g, ''))); // 6. Parse date (common formats in WhatsApp)
if (amounts.length > 0) { const datePatterns = [
result.freight_charged = Math.max(...amounts); /(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{2,4})/, // DD/MM/YYYY or DD-MM-YY
result.parsed_fields.push('freight_charged'); /(\d{1,2})\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*\s+(\d{2,4})/i, // 15 Jan 2026
} ];
for (const pattern of datePatterns) {
const match = processed.match(pattern);
if (match) {
result.date = match[0];
result.parsed_fields.push('date');
break;
} }
} }
// Advance received // 7. Parse material type
const advanceMatch = text.match(/(?:advance|received|paid by shipper)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i); const materialPatterns = [
if (advanceMatch) { /(?:material|goods|load|items?)\s*[:\\-]?\s*([A-Za-z\s]+?)(?:\\s*(?:wt|weight|qty|quantity|₹|\d{4,}|$))/i,
result.advance_received = parseInt(advanceMatch[1].replace(/,/g, '')); /(furniture|electronics|machinery|food|grains|cement|steel|tiles|cement bags|sugar|rice|cotton|textile|plastic|chemical|hardware|auto parts|automobile)/i,
result.parsed_fields.push('advance_received'); ];
for (const pattern of materialPatterns) {
const match = processed.match(pattern);
if (match) {
result.material = match[1].trim();
result.parsed_fields.push('material');
break;
}
} }
// Paid to driver // 8. Parse weight
const driverPaidMatch = text.match(/(?:paid to driver|driver advance|driver paid|to driver)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i); const weightMatch = processed.match(/(?:wt|weight|w)\s*[:\\-]?\s*([\d.]+)\s*(?:kg|tons?|tonnes?|quintals?|qtl|MT|mt)/i);
if (driverPaidMatch) { if (weightMatch) {
result.paid_to_driver = parseInt(driverPaidMatch[1].replace(/,/g, '')); result.weight = weightMatch[0].trim();
result.parsed_fields.push('paid_to_driver'); result.parsed_fields.push('weight');
} }
// Commission // 9. Auto-calculate derived fields
const commissionMatch = text.match(/(?:commission|comm)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i); if (!result.commission && result.freight_charged && result.driver_freight) {
if (commissionMatch) { result.commission = result.freight_charged - result.driver_freight;
result.commission = parseInt(commissionMatch[1].replace(/,/g, '')); result.parsed_fields.push('commission (auto: freight - driver)');
result.parsed_fields.push('commission');
} }
// Driver freight if (!result.commission && result.freight_charged && !result.driver_freight) {
const driverFreightMatch = text.match(/(?:driver freight|driver rate|driver amount)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i); // Default 5% commission if only freight is known
if (driverFreightMatch) { result.commission = Math.round(result.freight_charged * 0.05);
result.driver_freight = parseInt(driverFreightMatch[1].replace(/,/g, '')); result.parsed_fields.push('commission (auto: 5%)');
result.parsed_fields.push('driver_freight');
} }
// Auto-calculate commission if not parsed if (result.freight_charged && !result.pending_from_shipper) {
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); 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)'); if (result.pending_from_shipper > 0) result.parsed_fields.push('pending_from_shipper (auto)');
} }
// Auto-calculate pending to driver if (result.driver_freight && !result.pending_to_driver) {
if (!result.pending_to_driver && result.driver_freight) {
result.pending_to_driver = result.driver_freight - (result.paid_to_driver || 0); 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)'); if (result.pending_to_driver > 0) result.parsed_fields.push('pending_to_driver (auto)');
} }
// Confidence based on how many fields were parsed // 10. Confidence score
const fieldCount = result.parsed_fields.length; const fieldCount = result.parsed_fields.length;
if (fieldCount >= 6) result.confidence = 'high'; if (fieldCount >= 7) result.confidence = 'high';
else if (fieldCount >= 3) result.confidence = 'medium'; else if (fieldCount >= 4) result.confidence = 'medium';
return result; return result;
} }
module.exports = { parseWhatsAppMessage, KNOWN_SHIPPERS }; module.exports = { parseWhatsAppMessage, KNOWN_SHIPPERS, preprocessMessage, extractAmounts };

View file

@ -21,12 +21,16 @@
</div> </div>
</div> </div>
<div class="topbar-actions"> <div class="topbar-actions">
<button class="mobile-menu-btn" onclick="toggleMobileMenu()" title="Menu">&#9776;</button>
<button onclick="toggleTheme()" class="btn-icon" title="Toggle theme">&#9728;</button> <button onclick="toggleTheme()" class="btn-icon" title="Toggle theme">&#9728;</button>
<span class="user-name">&#128100; <%= user.username %></span> <span class="user-name">&#128100; <%= user.username %></span>
<a href="/logout" class="btn btn-sm btn-outline">Logout</a> <a href="/logout" class="btn btn-sm btn-outline">Logout</a>
</div> </div>
</nav> </nav>
<!-- Mobile sidebar overlay -->
<div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleMobileMenu()"></div>
<div class="layout"> <div class="layout">
<aside class="sidebar"> <aside class="sidebar">
<div class="sidebar-section"> <div class="sidebar-section">