[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; }
.sidebar { display: none; }
.stats-grid { grid-template-columns: 1fr 1fr; }
.mobile-menu-btn { display: flex; }
.main-content { padding: 12px; }
}
@media (max-width: 600px) {
.stats-grid { grid-template-columns: 1fr; }
.form-row { 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);
}
/* ============================================================
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
============================================================ */

View file

@ -43,6 +43,17 @@ document.querySelectorAll('form[onsubmit]').forEach(function(form) {
// WhatsApp parser (inline function for form page)
// 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
function formatINR(num) {
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;

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
// Handles common Kerala/India freight message formats
const { CITIES } = require('../config/constants');
// Known shipper names (from existing data)
// Known shipper names (from existing data + common Kerala names)
const KNOWN_SHIPPERS = [
'Kahn Transport', 'Agarwal Packers and Movers', 'Agarwal', 'Sahara Packers',
'Ambika Packers', 'Century Polymers', 'DRS', 'Superstar', 'Superstar Packers',
@ -15,27 +16,218 @@ const KNOWN_SHIPPERS = [
'Mohamed Anas', 'Nair', 'Badadosth',
];
// Status keywords mapping
// Status keywords mapping (ordered by specificity — most specific first)
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'],
'settled': ['settled', 'fully settled', 'payment received in full'],
'commission received': ['commission received', 'comm received', 'commission got'],
'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'],
'reconciled': ['reconciled', 'recon done'],
'completed': ['completed', 'fully completed'],
'delivered / pending collection': ['delivered', 'delivery done', 'reached', 'reached destination', 'delivered successfully'],
'pending collection': ['pending collection', 'collection pending', 'to collect', 'amount pending'],
'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'],
'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) {
const result = {
shipper: null,
@ -51,14 +243,19 @@ function parseWhatsAppMessage(text) {
driver_freight: null,
pending_from_shipper: null,
pending_to_driver: null,
date: null,
material: null,
weight: null,
notes: text,
confidence: 'low',
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) {
if (lower.includes(shipper.toLowerCase())) {
result.shipper = shipper;
@ -66,44 +263,46 @@ function parseWhatsAppMessage(text) {
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');
}
// If no known shipper, try to extract from patterns like "Shp: X" or "From: X (shipper)"
if (!result.shipper) {
const shipperPatterns = [
/(?:shp|shipper|from\s+shp|client)\s*[:\\-]\\s*([A-Za-z\s]+?)(?:\\s*(?:to|→|-|vehicle|loaded|freight|₹|\d{4,}|$))/i,
/(?:booking\\s+from|received\\s+from)\\s+([A-Za-z\s]+?)(?:\\s*(?:to|→|-|vehicle|loaded|freight|₹|\d{4,}|$))/i,
];
for (const pattern of shipperPatterns) {
const match = processed.match(pattern);
if (match) {
result.shipper = match[1].trim();
result.parsed_fields.push('shipper');
break;
}
}
}
// 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');
// 2. Parse vehicle number (Indian format with flexible spacing)
const vehiclePatterns = [
/\b([A-Z]{2}\s*\d{1,2}\s*[A-Z]{1,3}\s*\d{4})\b/i, // Standard: KL01AB1234
/\b([A-Z]{2}\s*\d{2}\s*[A-Z]{2}\s*\d{4})\b/i, // KL 01 AB 1234
/\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 kw of keywords) {
if (lower.includes(kw)) {
@ -115,76 +314,79 @@ function parseWhatsAppMessage(text) {
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');
}
// 5. Parse amounts with context-aware classification
const amounts = extractAmounts(processed);
const classified = classifyAmounts(amounts);
if (classified.freight_charged) { result.freight_charged = classified.freight_charged; result.parsed_fields.push('freight_charged'); }
if (classified.advance_received) { result.advance_received = classified.advance_received; result.parsed_fields.push('advance_received'); }
if (classified.paid_to_driver) { result.paid_to_driver = classified.paid_to_driver; result.parsed_fields.push('paid_to_driver'); }
if (classified.commission) { result.commission = classified.commission; result.parsed_fields.push('commission'); }
if (classified.driver_freight) { result.driver_freight = classified.driver_freight; result.parsed_fields.push('driver_freight'); }
// 6. Parse date (common formats in WhatsApp)
const datePatterns = [
/(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{2,4})/, // DD/MM/YYYY or DD-MM-YY
/(\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
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');
// 7. Parse material type
const materialPatterns = [
/(?:material|goods|load|items?)\s*[:\\-]?\s*([A-Za-z\s]+?)(?:\\s*(?:wt|weight|qty|quantity|₹|\d{4,}|$))/i,
/(furniture|electronics|machinery|food|grains|cement|steel|tiles|cement bags|sugar|rice|cotton|textile|plastic|chemical|hardware|auto parts|automobile)/i,
];
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
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');
// 8. Parse weight
const weightMatch = processed.match(/(?:wt|weight|w)\s*[:\\-]?\s*([\d.]+)\s*(?:kg|tons?|tonnes?|quintals?|qtl|MT|mt)/i);
if (weightMatch) {
result.weight = weightMatch[0].trim();
result.parsed_fields.push('weight');
}
// 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');
// 9. Auto-calculate derived fields
if (!result.commission && result.freight_charged && result.driver_freight) {
result.commission = result.freight_charged - result.driver_freight;
result.parsed_fields.push('commission (auto: freight - driver)');
}
if (!result.commission && result.freight_charged && !result.driver_freight) {
// Default 5% commission if only freight is known
result.commission = Math.round(result.freight_charged * 0.05);
result.parsed_fields.push('commission (auto: 5%)');
}
// 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) {
if (result.freight_charged && !result.pending_from_shipper) {
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) {
if (result.driver_freight && !result.pending_to_driver) {
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
// 10. Confidence score
const fieldCount = result.parsed_fields.length;
if (fieldCount >= 6) result.confidence = 'high';
else if (fieldCount >= 3) result.confidence = 'medium';
if (fieldCount >= 7) result.confidence = 'high';
else if (fieldCount >= 4) result.confidence = 'medium';
return result;
}
module.exports = { parseWhatsAppMessage, KNOWN_SHIPPERS };
module.exports = { parseWhatsAppMessage, KNOWN_SHIPPERS, preprocessMessage, extractAmounts };

View file

@ -21,12 +21,16 @@
</div>
</div>
<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>
<span class="user-name">&#128100; <%= user.username %></span>
<a href="/logout" class="btn btn-sm btn-outline">Logout</a>
</div>
</nav>
<!-- Mobile sidebar overlay -->
<div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleMobileMenu()"></div>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-section">