| const prisma = require('../config/database'); |
|
|
| |
| |
| |
| |
| |
| exports.getOpportunities = async (req, res) => { |
| try { |
| const { truckId } = req.query; |
| const userId = req.user.id; |
|
|
| |
| let opportunities; |
|
|
| if (truckId) { |
| |
| const truck = await prisma.truck.findUnique({ |
| where: { id: truckId }, |
| include: { sourceHub: true } |
| }); |
|
|
| if (!truck || !truck.currentLat || !truck.currentLng) { |
| return res.status(404).json({ success: false, message: 'Truck or location not found' }); |
| } |
|
|
| |
| const nearbyShipments = await prisma.$queryRaw` |
| SELECT id, "pickupLocation", "pickupLat", "pickupLng", "dropLocation", "dropLat", "dropLng", "cargoType", "cargoWeight", "cargoVolume", |
| ( 6371 * acos( cos( radians(${truck.currentLat}) ) * cos( radians( "pickupLat" ) ) |
| * cos( radians( "pickupLng" ) - radians(${truck.currentLng}) ) + sin( radians(${truck.currentLat}) ) |
| * sin( radians( "pickupLat" ) ) ) ) AS distance |
| FROM "Shipment" |
| WHERE "isMarketplaceLoad" = true |
| AND status = 'PENDING' |
| AND (6371 * acos( cos( radians(${truck.currentLat}) ) * cos( radians( "pickupLat" ) ) |
| * cos( radians( "pickupLng" ) - radians(${truck.currentLng}) ) + sin( radians(${truck.currentLat}) ) |
| * sin( radians( "pickupLat" ) ) )) < 20 |
| ORDER BY distance; |
| `; |
| opportunities = nearbyShipments; |
| } else { |
| |
| opportunities = await prisma.backhaulPickup.findMany({ |
| where: { driverId: userId }, |
| include: { |
| destinationHub: true, |
| truck: true, |
| }, |
| orderBy: { proposedAt: 'desc' }, |
| }); |
| } |
|
|
| res.status(200).json({ |
| success: true, |
| data: opportunities |
| }); |
|
|
| } catch (error) { |
| console.error('Backhaul Error:', error); |
| res.status(500).json({ success: false, message: 'Internal server error' }); |
| } |
| }; |
|
|
| |
| |
| |
| exports.acceptBackhaul = async (req, res) => { |
| try { |
| const { id } = req.params; |
| const userId = req.user.id; |
| const io = req.app.get('io'); |
|
|
| const backhaul = await prisma.backhaulPickup.findUnique({ |
| where: { id }, |
| include: { driver: true, truck: true }, |
| }); |
|
|
| if (!backhaul) { |
| return res.status(404).json({ success: false, message: 'Backhaul not found' }); |
| } |
|
|
| if (backhaul.driverId !== userId) { |
| return res.status(403).json({ success: false, message: 'Not authorized' }); |
| } |
|
|
| if (backhaul.status !== 'PROPOSED') { |
| return res.status(400).json({ success: false, message: 'Cannot accept this backhaul' }); |
| } |
|
|
| const updated = await prisma.backhaulPickup.update({ |
| where: { id }, |
| data: { status: 'ACCEPTED' }, |
| }); |
|
|
| |
| io.emit('BACKHAUL_ACCEPTED', { |
| backhaulId: id, |
| driverName: backhaul.driver.name, |
| truckId: backhaul.truckId, |
| shipperName: backhaul.shipperName, |
| timestamp: new Date().toISOString(), |
| }); |
|
|
| res.status(200).json({ |
| success: true, |
| message: 'Backhaul accepted', |
| data: updated, |
| }); |
| } catch (error) { |
| console.error('Accept backhaul error:', error); |
| res.status(500).json({ success: false, message: 'Failed to accept backhaul' }); |
| } |
| }; |
|
|
| |
| |
| |
| exports.rejectBackhaul = async (req, res) => { |
| try { |
| const { id } = req.params; |
| const { reason } = req.body; |
| const userId = req.user.id; |
| const io = req.app.get('io'); |
|
|
| const backhaul = await prisma.backhaulPickup.findUnique({ |
| where: { id }, |
| include: { driver: true }, |
| }); |
|
|
| if (!backhaul) { |
| return res.status(404).json({ success: false, message: 'Backhaul not found' }); |
| } |
|
|
| if (backhaul.driverId !== userId) { |
| return res.status(403).json({ success: false, message: 'Not authorized' }); |
| } |
|
|
| const updated = await prisma.backhaulPickup.update({ |
| where: { id }, |
| data: { status: 'REJECTED' }, |
| }); |
|
|
| |
| io.emit('BACKHAUL_REJECTED', { |
| backhaulId: id, |
| driverName: backhaul.driver.name, |
| reason: reason || 'No reason provided', |
| timestamp: new Date().toISOString(), |
| }); |
|
|
| res.status(200).json({ |
| success: true, |
| message: 'Backhaul rejected', |
| data: updated, |
| }); |
| } catch (error) { |
| console.error('Reject backhaul error:', error); |
| res.status(500).json({ success: false, message: 'Failed to reject backhaul' }); |
| } |
| }; |
|
|
| |
| |
| |
| exports.startPickup = async (req, res) => { |
| try { |
| const { id } = req.params; |
| const userId = req.user.id; |
| const io = req.app.get('io'); |
|
|
| const backhaul = await prisma.backhaulPickup.findUnique({ |
| where: { id }, |
| include: { driver: true }, |
| }); |
|
|
| if (!backhaul) { |
| return res.status(404).json({ success: false, message: 'Backhaul not found' }); |
| } |
|
|
| if (backhaul.driverId !== userId) { |
| return res.status(403).json({ success: false, message: 'Not authorized' }); |
| } |
|
|
| if (backhaul.status !== 'ACCEPTED') { |
| return res.status(400).json({ success: false, message: 'Cannot start pickup' }); |
| } |
|
|
| const updated = await prisma.backhaulPickup.update({ |
| where: { id }, |
| data: { status: 'EN_ROUTE_TO_PICKUP' }, |
| }); |
|
|
| |
| io.emit('BACKHAUL_EN_ROUTE', { |
| backhaulId: id, |
| driverName: backhaul.driver.name, |
| shipperLocation: backhaul.shipperLocation, |
| timestamp: new Date().toISOString(), |
| }); |
|
|
| res.status(200).json({ |
| success: true, |
| message: 'Started heading to pickup', |
| data: updated, |
| }); |
| } catch (error) { |
| console.error('Start pickup error:', error); |
| res.status(500).json({ success: false, message: 'Failed to start pickup' }); |
| } |
| }; |
|
|
| |
| |
| |
| exports.confirmPickup = async (req, res) => { |
| try { |
| const { id } = req.params; |
| const { photos } = req.body; |
| const userId = req.user.id; |
| const io = req.app.get('io'); |
|
|
| const backhaul = await prisma.backhaulPickup.findUnique({ |
| where: { id }, |
| include: { driver: true, truck: true }, |
| }); |
|
|
| if (!backhaul) { |
| return res.status(404).json({ success: false, message: 'Backhaul not found' }); |
| } |
|
|
| if (backhaul.driverId !== userId) { |
| return res.status(403).json({ success: false, message: 'Not authorized' }); |
| } |
|
|
| if (backhaul.status !== 'EN_ROUTE_TO_PICKUP') { |
| return res.status(400).json({ success: false, message: 'Cannot confirm pickup' }); |
| } |
|
|
| const updated = await prisma.backhaulPickup.update({ |
| where: { id }, |
| data: { |
| status: 'PICKED_UP', |
| pickedUpAt: new Date(), |
| }, |
| }); |
|
|
| |
| await prisma.truck.update({ |
| where: { id: backhaul.truckId }, |
| data: { |
| currentWeight: { increment: backhaul.totalWeight }, |
| currentVolume: { increment: backhaul.totalVolume }, |
| }, |
| }); |
|
|
| |
| io.emit('BACKHAUL_PICKED_UP', { |
| backhaulId: id, |
| driverName: backhaul.driver.name, |
| shipperName: backhaul.shipperName, |
| packageCount: backhaul.packageCount, |
| totalWeight: backhaul.totalWeight, |
| timestamp: new Date().toISOString(), |
| }); |
|
|
| res.status(200).json({ |
| success: true, |
| message: 'Pickup confirmed', |
| data: updated, |
| }); |
| } catch (error) { |
| console.error('Confirm pickup error:', error); |
| res.status(500).json({ success: false, message: 'Failed to confirm pickup' }); |
| } |
| }; |
|
|
| |
| |
| |
| exports.completeBackhaul = async (req, res) => { |
| try { |
| const { id } = req.params; |
| const { photos } = req.body; |
| const userId = req.user.id; |
| const io = req.app.get('io'); |
|
|
| const backhaul = await prisma.backhaulPickup.findUnique({ |
| where: { id }, |
| include: { driver: true, truck: true, destinationHub: true }, |
| }); |
|
|
| if (!backhaul) { |
| return res.status(404).json({ success: false, message: 'Backhaul not found' }); |
| } |
|
|
| if (backhaul.driverId !== userId) { |
| return res.status(403).json({ success: false, message: 'Not authorized' }); |
| } |
|
|
| if (backhaul.status !== 'PICKED_UP') { |
| return res.status(400).json({ success: false, message: 'Cannot complete delivery' }); |
| } |
|
|
| |
| const [updated, transaction] = await prisma.$transaction([ |
| prisma.backhaulPickup.update({ |
| where: { id }, |
| data: { |
| status: 'DELIVERED', |
| deliveredAt: new Date(), |
| }, |
| }), |
| prisma.transaction.create({ |
| data: { |
| driverId: userId, |
| amount: 100, |
| type: 'BACKHAUL_BONUS', |
| description: `Backhaul delivery to ${backhaul.destinationHub?.name || 'Hub'}`, |
| route: `${backhaul.shipperLocation} → Hub`, |
| }, |
| }), |
| ]); |
|
|
| |
| await prisma.truck.update({ |
| where: { id: backhaul.truckId }, |
| data: { |
| currentWeight: { decrement: backhaul.totalWeight }, |
| currentVolume: { decrement: backhaul.totalVolume }, |
| }, |
| }); |
|
|
| |
| await prisma.user.update({ |
| where: { id: userId }, |
| data: { |
| totalEarnings: { increment: 100 }, |
| weeklyEarnings: { increment: 100 }, |
| }, |
| }); |
|
|
| |
| io.emit('BACKHAUL_COMPLETED', { |
| backhaulId: id, |
| driverName: backhaul.driver.name, |
| shipperName: backhaul.shipperName, |
| destinationHub: backhaul.destinationHub?.name, |
| carbonSaved: backhaul.carbonSavedKg, |
| timestamp: new Date().toISOString(), |
| }); |
|
|
| res.status(200).json({ |
| success: true, |
| message: 'Backhaul delivery completed!', |
| data: { |
| backhaul: updated, |
| transaction, |
| }, |
| }); |
| } catch (error) { |
| console.error('Complete backhaul error:', error); |
| res.status(500).json({ success: false, message: 'Failed to complete backhaul' }); |
| } |
| }; |
|
|
| |
| |
| |
| |
| exports.checkOpportunities = async (req, res) => { |
| try { |
| const { truckId, currentLat, currentLng, destinationLat, destinationLng } = req.body; |
| const userId = req.user.id; |
| const io = req.app.get('io'); |
|
|
| if (!truckId || !currentLat || !currentLng) { |
| return res.status(400).json({ success: false, message: 'Missing required fields' }); |
| } |
|
|
| |
| const truck = await prisma.truck.findUnique({ |
| where: { id: truckId }, |
| include: { owner: true, sourceHub: true }, |
| }); |
|
|
| if (!truck) { |
| return res.status(404).json({ success: false, message: 'Truck not found' }); |
| } |
|
|
| |
| const nearbyShipments = await prisma.$queryRaw` |
| SELECT id, "shipperId", "pickupLocation", "pickupLat", "pickupLng", |
| "dropLocation", "dropLat", "dropLng", "cargoType", "cargoWeight", "cargoVolume", |
| ( 6371 * acos( cos( radians(${currentLat}) ) * cos( radians( "pickupLat" ) ) |
| * cos( radians( "pickupLng" ) - radians(${currentLng}) ) + sin( radians(${currentLat}) ) |
| * sin( radians( "pickupLat" ) ) ) ) AS distance |
| FROM "Shipment" |
| WHERE "isMarketplaceLoad" = true |
| AND status = 'PENDING' |
| AND (6371 * acos( cos( radians(${currentLat}) ) * cos( radians( "pickupLat" ) ) |
| * cos( radians( "pickupLng" ) - radians(${currentLng}) ) + sin( radians(${currentLat}) ) |
| * sin( radians( "pickupLat" ) ) )) < 20 |
| ORDER BY distance |
| LIMIT 5; |
| `; |
|
|
| if (nearbyShipments.length === 0) { |
| return res.status(200).json({ |
| success: true, |
| message: 'No backhaul opportunities found', |
| data: [], |
| }); |
| } |
|
|
| |
| const backhaulRecords = []; |
| for (const shipment of nearbyShipments) { |
| |
| const shipper = await prisma.user.findUnique({ |
| where: { id: shipment.shipperId }, |
| }); |
|
|
| |
| const carbonSaved = shipment.distance * 0.1; |
|
|
| |
| const backhaul = await prisma.backhaulPickup.create({ |
| data: { |
| truckId: truck.id, |
| driverId: userId, |
| shipperId: shipment.shipperId, |
| shipperName: shipper?.name || 'Unknown Shipper', |
| shipperPhone: shipper?.phone || '', |
| shipperLocation: shipment.pickupLocation || 'Unknown', |
| shipperLat: shipment.pickupLat, |
| shipperLng: shipment.pickupLng, |
| destinationHubId: truck.sourceHubId || truck.sourceHub?.id, |
| packageCount: 1, |
| totalWeight: shipment.cargoWeight || 0, |
| totalVolume: shipment.cargoVolume || 0, |
| distanceKm: shipment.distance, |
| carbonSavedKg: carbonSaved, |
| status: 'PROPOSED', |
| }, |
| }); |
| backhaulRecords.push(backhaul); |
| } |
|
|
| |
| io.emit('BACKHAUL_OPPORTUNITIES_FOUND', { |
| truckId: truck.id, |
| driverName: truck.owner.name, |
| licensePlate: truck.licensePlate, |
| opportunityCount: backhaulRecords.length, |
| opportunities: backhaulRecords, |
| timestamp: new Date().toISOString(), |
| }); |
|
|
| |
| io.to(`driver_${userId}`).emit('BACKHAUL_AVAILABLE', { |
| message: `${backhaulRecords.length} backhaul opportunities found!`, |
| opportunities: backhaulRecords, |
| }); |
|
|
| res.status(200).json({ |
| success: true, |
| message: `Found ${backhaulRecords.length} backhaul opportunities`, |
| data: backhaulRecords, |
| }); |
| } catch (error) { |
| console.error('Check opportunities error:', error); |
| res.status(500).json({ success: false, message: 'Failed to check opportunities' }); |
| } |
| }; |