From 04657b9f29802372a16f7cfb716d9bfc0bf3bdee Mon Sep 17 00:00:00 2001 From: FreightDesk Date: Mon, 8 Jun 2026 01:15:11 +0000 Subject: [PATCH] [OWL] WhatsApp parser v2 + mobile responsiveness + parser API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- webapp/src/public/css/style.css | 95 +++++++ webapp/src/public/js/app.js | 11 + webapp/src/routes/api.js | 34 +++ webapp/src/services/parser.js | 408 ++++++++++++++++++++------- webapp/src/views/partials/header.ejs | 4 + 5 files changed, 449 insertions(+), 103 deletions(-) diff --git a/webapp/src/public/css/style.css b/webapp/src/public/css/style.css index b9a5d98..f6b2b62 100644 --- a/webapp/src/public/css/style.css +++ b/webapp/src/public/css/style.css @@ -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 ============================================================ */ diff --git a/webapp/src/public/js/app.js b/webapp/src/public/js/app.js index 47ecc64..256aa5e 100644 --- a/webapp/src/public/js/app.js +++ b/webapp/src/public/js/app.js @@ -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 '—'; diff --git a/webapp/src/routes/api.js b/webapp/src/routes/api.js index 0be9633..769406e 100644 --- a/webapp/src/routes/api.js +++ b/webapp/src/routes/api.js @@ -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; diff --git a/webapp/src/services/parser.js b/webapp/src/services/parser.js index e45160f..4e37b07 100644 --- a/webapp/src/services/parser.js +++ b/webapp/src/services/parser.js @@ -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 }; diff --git a/webapp/src/views/partials/header.ejs b/webapp/src/views/partials/header.ejs index 839d996..8f2b78e 100644 --- a/webapp/src/views/partials/header.ejs +++ b/webapp/src/views/partials/header.ejs @@ -21,12 +21,16 @@
+ 👤 <%= user.username %> Logout
+ + +