| const prisma = require('../config/database'); |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| async function dispatcherAcceptSynergy(req, res) { |
| try { |
| const { opportunityId, dispatcherId } = req.body; |
| const io = req.app.get('io'); |
|
|
| |
| const opportunity = await prisma.absorptionOpportunity.findUnique({ |
| where: { id: opportunityId }, |
| include: { |
| route1: { |
| include: { |
| truck: { include: { owner: true } }, |
| deliveries: true |
| } |
| }, |
| route2: { |
| include: { |
| truck: { include: { owner: true } }, |
| deliveries: true |
| } |
| }, |
| nearestHub: true |
| } |
| }); |
|
|
| if (!opportunity) { |
| return res.status(404).json({ |
| success: false, |
| message: 'Opportunity not found' |
| }); |
| } |
|
|
| if (opportunity.status !== 'PENDING') { |
| return res.status(400).json({ |
| success: false, |
| message: 'Opportunity already processed' |
| }); |
| } |
|
|
| |
| await prisma.absorptionOpportunity.update({ |
| where: { id: opportunityId }, |
| data: { |
| status: 'BOTH_ACCEPTED', |
| acceptedByRoute1At: new Date(), |
| acceptedByRoute2At: new Date() |
| } |
| }); |
|
|
| |
| const transfer = await prisma.absorptionTransfer.create({ |
| data: { |
| absorptionOpportunityId: opportunityId, |
| exporterDriverId: opportunity.route2.truck.owner.id, |
| importerDriverId: opportunity.route1.truck.owner.id, |
| hubId: opportunity.nearestHubId, |
| deliveryIdsToTransfer: opportunity.eligibleDeliveryIds, |
| spaceAvailableExporter: opportunity.truck2SpaceAvailable, |
| spaceAvailableImporter: opportunity.truck1SpaceAvailable, |
| distanceSavedKm: opportunity.totalDistanceSaved, |
| carbonSavedKg: opportunity.potentialCarbonSaved, |
| status: 'PENDING' |
| } |
| }); |
|
|
| |
| const handshakeData = { |
| transferId: transfer.id, |
| hubLat: opportunity.nearestHub.latitude, |
| hubLng: opportunity.nearestHub.longitude, |
| hubName: opportunity.nearestHub.name, |
| hubAddress: opportunity.nearestHub.address |
| }; |
|
|
| |
| io.to(`driver_${opportunity.route1.truck.owner.id}`).emit('HANDSHAKE_REQUIRED', { |
| ...handshakeData, |
| role: 'IMPORTER', |
| counterpartTruck: opportunity.route2.truck.licensePlate, |
| counterpartDriver: opportunity.route2.truck.owner.name |
| }); |
|
|
| |
| io.to(`driver_${opportunity.route2.truck.owner.id}`).emit('HANDSHAKE_REQUIRED', { |
| ...handshakeData, |
| role: 'EXPORTER', |
| counterpartTruck: opportunity.route1.truck.licensePlate, |
| counterpartDriver: opportunity.route1.truck.owner.name |
| }); |
|
|
| res.status(200).json({ |
| success: true, |
| message: 'Synergy accepted, drivers notified', |
| data: { |
| transferId: transfer.id, |
| hubCoordinates: { |
| lat: opportunity.nearestHub.latitude, |
| lng: opportunity.nearestHub.longitude |
| } |
| } |
| }); |
| } catch (error) { |
| console.error('Dispatcher accept error:', error); |
| res.status(500).json({ |
| success: false, |
| message: error.message || 'Failed to accept synergy' |
| }); |
| } |
| } |
|
|
| |
| |
| |
| async function generateQRCode(req, res) { |
| try { |
| const { transferId, truckId } = req.body; |
| const userId = req.user.id; |
|
|
| |
| const transfer = await prisma.absorptionTransfer.findUnique({ |
| where: { id: transferId }, |
| include: { |
| exporterDriver: true, |
| absorptionOpportunity: { |
| include: { |
| route2: { |
| include: { |
| deliveries: { |
| select: { |
| id: true, |
| packageId: true, |
| cargoType: true, |
| cargoWeight: true, |
| cargoVolumeLtrs: true, |
| pickupLocation: true, |
| dropLocation: true |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| }); |
|
|
| if (!transfer) { |
| return res.status(404).json({ |
| success: false, |
| message: 'Transfer not found' |
| }); |
| } |
|
|
| |
| if (transfer.exporterDriverId !== userId) { |
| return res.status(403).json({ |
| success: false, |
| message: 'Only exporter can generate QR code' |
| }); |
| } |
|
|
| |
| const deliveryIds = transfer.deliveryIdsToTransfer.split(','); |
| const deliveries = transfer.absorptionOpportunity.route2.deliveries.filter( |
| d => deliveryIds.includes(d.id) |
| ); |
|
|
| |
| const totalWeight = deliveries.reduce((sum, d) => sum + d.cargoWeight, 0); |
| const totalVolume = deliveries.reduce((sum, d) => sum + d.cargoVolumeLtrs, 0); |
|
|
| |
| const qrPayload = { |
| transferId: transfer.id, |
| deliveryIds: deliveries.map(d => d.id), |
| totalWeight, |
| totalVolume, |
| packageDetails: deliveries.map(d => ({ |
| packageId: d.packageId, |
| cargoType: d.cargoType, |
| weight: d.cargoWeight, |
| volume: d.cargoVolumeLtrs, |
| from: d.pickupLocation, |
| to: d.dropLocation |
| })), |
| timestamp: new Date().toISOString() |
| }; |
|
|
| const qrData = JSON.stringify(qrPayload); |
|
|
| |
| await prisma.absorptionTransfer.update({ |
| where: { id: transferId }, |
| data: { qrCodeData: qrData } |
| }); |
|
|
| res.status(200).json({ |
| success: true, |
| message: 'QR code generated', |
| data: { qrData } |
| }); |
| } catch (error) { |
| console.error('QR generation error:', error); |
| res.status(500).json({ |
| success: false, |
| message: error.message || 'Failed to generate QR code' |
| }); |
| } |
| } |
|
|
| |
| |
| |
| async function verifyQR(req, res) { |
| try { |
| const { transferId, qrData, currentLat, currentLng } = req.body; |
| const userId = req.user.id; |
| const io = req.app.get('io'); |
|
|
| |
| const transfer = await prisma.absorptionTransfer.findUnique({ |
| where: { id: transferId }, |
| include: { |
| importerDriver: true, |
| hub: true |
| } |
| }); |
|
|
| if (!transfer) { |
| return res.status(404).json({ |
| success: false, |
| message: 'Transfer not found' |
| }); |
| } |
|
|
| |
| if (transfer.importerDriverId !== userId) { |
| return res.status(403).json({ |
| success: false, |
| message: 'Only importer can verify QR code' |
| }); |
| } |
|
|
| |
| let scannedData; |
| try { |
| scannedData = JSON.parse(qrData); |
| } catch (e) { |
| return res.status(400).json({ |
| success: false, |
| message: 'Invalid QR code format' |
| }); |
| } |
|
|
| |
| if (scannedData.transferId !== transferId) { |
| return res.status(400).json({ |
| success: false, |
| message: 'QR code does not match this transfer' |
| }); |
| } |
|
|
| |
| const expectedIds = transfer.deliveryIdsToTransfer.split(',').sort(); |
| const scannedIds = scannedData.deliveryIds.sort(); |
|
|
| if (JSON.stringify(expectedIds) !== JSON.stringify(scannedIds)) { |
| return res.status(400).json({ |
| success: false, |
| message: 'Delivery IDs mismatch' |
| }); |
| } |
|
|
| |
| const checklistData = scannedData.packageDetails.map(pkg => ({ |
| packageId: pkg.packageId, |
| cargoType: pkg.cargoType, |
| weight: pkg.weight, |
| volume: pkg.volume, |
| from: pkg.from, |
| to: pkg.to, |
| verified: false |
| })); |
|
|
| |
| await prisma.absorptionTransfer.update({ |
| where: { id: transferId }, |
| data: { |
| status: 'QR_SCANNED', |
| qrCodeScanned: true, |
| scannedAt: new Date(), |
| checklistData: checklistData |
| } |
| }); |
|
|
| |
| io.emit('QR_SCANNED', { |
| transferId, |
| location: { lat: currentLat, lng: currentLng }, |
| timestamp: new Date().toISOString(), |
| importerDriver: transfer.importerDriver.name |
| }); |
|
|
| res.status(200).json({ |
| success: true, |
| message: 'QR code verified successfully', |
| data: { checklistData } |
| }); |
| } catch (error) { |
| console.error('QR verification error:', error); |
| res.status(500).json({ |
| success: false, |
| message: error.message || 'Failed to verify QR code' |
| }); |
| } |
| } |
|
|
| |
| |
| |
| async function completeHandover(req, res) { |
| try { |
| const { transferId, photos, checklistData } = req.body; |
| const userId = req.user.id; |
| const io = req.app.get('io'); |
|
|
| |
| const transfer = await prisma.absorptionTransfer.findUnique({ |
| where: { id: transferId }, |
| include: { |
| importerDriver: true, |
| exporterDriver: true, |
| absorptionOpportunity: { |
| include: { |
| route1: { include: { truck: true } }, |
| route2: { include: { truck: true } } |
| } |
| } |
| } |
| }); |
|
|
| if (!transfer) { |
| return res.status(404).json({ |
| success: false, |
| message: 'Transfer not found' |
| }); |
| } |
|
|
| |
| if (transfer.importerDriverId !== userId) { |
| return res.status(403).json({ |
| success: false, |
| message: 'Only importer can complete handover' |
| }); |
| } |
|
|
| |
| if (transfer.status !== 'QR_SCANNED' && transfer.status !== 'CHECKLIST_VERIFIED') { |
| return res.status(400).json({ |
| success: false, |
| message: 'QR code must be scanned first' |
| }); |
| } |
|
|
| const deliveryIds = transfer.deliveryIdsToTransfer.split(','); |
| const truckA = transfer.absorptionOpportunity.route1.truck; |
| const truckB = transfer.absorptionOpportunity.route2.truck; |
|
|
| |
| await prisma.$transaction(async (tx) => { |
| |
| await tx.absorptionTransfer.update({ |
| where: { id: transferId }, |
| data: { |
| photos: photos || [], |
| checklistData: checklistData, |
| status: 'COMPLETED', |
| completedAt: new Date() |
| } |
| }); |
|
|
| |
| const deliveries = await tx.delivery.findMany({ |
| where: { id: { in: deliveryIds } } |
| }); |
|
|
| const totalWeight = deliveries.reduce((sum, d) => sum + d.cargoWeight, 0); |
| const totalVolume = deliveries.reduce((sum, d) => sum + d.cargoVolumeLtrs, 0); |
|
|
| |
| await tx.eWayBill.updateMany({ |
| where: { |
| driverId: transfer.exporterDriverId, |
| status: 'ACTIVE' |
| }, |
| data: { |
| vehicleNo: truckA.licensePlate, |
| driverId: transfer.importerDriverId, |
| status: 'TRANSFERRED' |
| } |
| }); |
|
|
| |
| await tx.delivery.updateMany({ |
| where: { id: { in: deliveryIds } }, |
| data: { |
| truckId: truckA.id, |
| driverId: transfer.importerDriverId, |
| status: 'ABSORPTION_TRANSFERRED' |
| } |
| }); |
|
|
| |
| await tx.truck.update({ |
| where: { id: truckA.id }, |
| data: { |
| currentWeight: { increment: totalWeight }, |
| currentVolume: { increment: totalVolume } |
| } |
| }); |
|
|
| |
| await tx.truck.update({ |
| where: { id: truckB.id }, |
| data: { |
| currentWeight: { decrement: totalWeight }, |
| currentVolume: { decrement: totalVolume } |
| } |
| }); |
|
|
| |
| await tx.absorptionOpportunity.update({ |
| where: { id: transfer.absorptionOpportunityId }, |
| data: { status: 'COMPLETED' } |
| }); |
| }); |
|
|
| |
| io.emit('TRANSFER_COMPLETED', { |
| transferId, |
| deliveryIds, |
| newTruckId: truckA.id, |
| newTruckPlate: truckA.licensePlate, |
| importerDriver: transfer.importerDriver.name, |
| exporterDriver: transfer.exporterDriver.name, |
| timestamp: new Date().toISOString() |
| }); |
|
|
| |
| io.to(`driver_${transfer.importerDriverId}`).emit('TRANSFER_COMPLETED', { |
| message: 'Handover completed successfully', |
| role: 'IMPORTER' |
| }); |
|
|
| io.to(`driver_${transfer.exporterDriverId}`).emit('TRANSFER_COMPLETED', { |
| message: 'Handover completed successfully', |
| role: 'EXPORTER' |
| }); |
|
|
| res.status(200).json({ |
| success: true, |
| message: 'Handover completed successfully', |
| data: { |
| updatedDeliveries: deliveryIds.length, |
| updatedEWayBills: await prisma.eWayBill.count({ |
| where: { |
| driverId: transfer.importerDriverId, |
| status: 'TRANSFERRED' |
| } |
| }) |
| } |
| }); |
| } catch (error) { |
| console.error('Handover completion error:', error); |
| res.status(500).json({ |
| success: false, |
| message: error.message || 'Failed to complete handover' |
| }); |
| } |
| } |
|
|
| |
| |
| |
| async function searchSynergy(req, res) { |
| try { |
| const { truckId } = req.body; |
| const truck = await prisma.truck.findUnique({ where: { id: truckId } }); |
|
|
| if (!truck) { |
| return res.status(404).json({ message: 'Truck not found' }); |
| } |
|
|
| res.status(200).json({ |
| success: true, |
| message: 'Manual search deprecated. Use continuous monitoring service.' |
| }); |
| } catch (error) { |
| res.status(500).json({ error: error.message }); |
| } |
| } |
|
|
| async function acceptSynergy(req, res) { |
| |
| |
| try { |
| return res.status(403).json({ |
| success: false, |
| message: 'Driver acceptance not allowed. Synergy opportunities are approved by dispatcher only. Please wait for dispatcher approval.', |
| deprecated: true |
| }); |
| } catch (error) { |
| res.status(500).json({ error: error.message }); |
| } |
| } |
|
|
| |
| |
| |
| |
| async function dispatcherRejectSynergy(req, res) { |
| try { |
| const { opportunityId, dispatcherId, reason } = req.body; |
| const io = req.app.get('io'); |
|
|
| |
| const opportunity = await prisma.absorptionOpportunity.findUnique({ |
| where: { id: opportunityId }, |
| include: { |
| route1: { |
| include: { |
| truck: { include: { owner: true } } |
| } |
| }, |
| route2: { |
| include: { |
| truck: { include: { owner: true } } |
| } |
| } |
| } |
| }); |
|
|
| if (!opportunity) { |
| return res.status(404).json({ |
| success: false, |
| message: 'Opportunity not found' |
| }); |
| } |
|
|
| if (opportunity.status !== 'PENDING') { |
| return res.status(400).json({ |
| success: false, |
| message: 'Opportunity already processed' |
| }); |
| } |
|
|
| |
| await prisma.absorptionOpportunity.update({ |
| where: { id: opportunityId }, |
| data: { |
| status: 'REJECTED', |
| rejectedAt: new Date(), |
| rejectionReason: reason || 'Rejected by dispatcher' |
| } |
| }); |
|
|
| |
| const rejectionData = { |
| opportunityId, |
| reason: reason || 'Rejected by dispatcher', |
| timestamp: new Date().toISOString() |
| }; |
|
|
| io.to(`driver_${opportunity.route1.truck.owner.id}`).emit('SYNERGY_REJECTED', rejectionData); |
| io.to(`driver_${opportunity.route2.truck.owner.id}`).emit('SYNERGY_REJECTED', rejectionData); |
|
|
| res.status(200).json({ |
| success: true, |
| message: 'Synergy opportunity rejected', |
| data: { |
| opportunityId, |
| status: 'REJECTED' |
| } |
| }); |
| } catch (error) { |
| console.error('Dispatcher reject error:', error); |
| res.status(500).json({ |
| success: false, |
| message: error.message || 'Failed to reject synergy' |
| }); |
| } |
| } |
|
|
| async function handleHandshake(req, res) { |
| try { |
| res.status(200).json({ |
| success: true, |
| message: 'Use new QR-based handshake flow' |
| }); |
| } catch (error) { |
| res.status(500).json({ error: error.message }); |
| } |
| } |
|
|
| module.exports = { |
| |
| dispatcherAcceptSynergy, |
| dispatcherRejectSynergy, |
| generateQRCode, |
| verifyQR, |
| completeHandover, |
|
|
| |
| searchSynergy, |
| acceptSynergy, |
| handleHandshake |
| }; |
|
|