const prisma = require('../config/database'); const Joi = require('joi'); /** * GET /api/deliveries/assigned - Driver's active/pending deliveries */ const getAssignedDeliveries = async (req, res) => { try { const deliveries = await prisma.delivery.findMany({ where: { driverId: req.user.id, status: { not: 'COMPLETED', }, }, include: { shipment: true, truck: true, }, orderBy: { createdAt: 'desc', }, }); res.status(200).json({ success: true, data: deliveries, }); } catch (error) { console.error('Get assigned deliveries error:', error); res.status(500).json({ success: false, message: 'Failed to fetch assigned deliveries', }); } }; /** * POST /api/deliveries/:id/accept - Driver accepts delivery */ const acceptDelivery = async (req, res) => { try { const { id } = req.params; const delivery = await prisma.delivery.findUnique({ where: { id }, include: { shipment: true }, }); if (!delivery || delivery.driverId !== req.user.id) { return res.status(404).json({ success: false, message: 'Delivery not found', }); } if (delivery.status !== 'PENDING') { return res.status(400).json({ success: false, message: 'Delivery is already accepted or in progress', }); } // Update delivery and shipment status const updatedDelivery = await prisma.$transaction([ prisma.delivery.update({ where: { id }, data: { status: 'EN_ROUTE_TO_PICKUP' }, }), prisma.shipment.update({ where: { id: delivery.shipmentId }, data: { status: 'DRIVER_ACCEPTED' }, }), ]); res.status(200).json({ success: true, message: 'Delivery accepted', data: updatedDelivery[0], }); } catch (error) { console.error('Accept delivery error:', error); res.status(500).json({ success: false, message: 'Failed to accept delivery', }); } }; /** * POST /api/deliveries/:id/reject - Driver rejects delivery */ const rejectDelivery = async (req, res) => { try { const { id } = req.params; const delivery = await prisma.delivery.findUnique({ where: { id }, }); if (!delivery || delivery.driverId !== req.user.id) { return res.status(404).json({ success: false, message: 'Delivery not found', }); } // Rejection usually means unassigning the driver or marking as cancelled await prisma.$transaction([ prisma.delivery.update({ where: { id }, data: { status: 'CANCELLED' }, }), prisma.shipment.update({ where: { id: delivery.shipmentId }, data: { status: 'DRIVER_REJECTED' }, }), ]); res.status(200).json({ success: true, message: 'Delivery rejected', }); } catch (error) { console.error('Reject delivery error:', error); res.status(500).json({ success: false, message: 'Failed to reject delivery', }); } }; /** * POST /api/deliveries/:id/start - Start navigation to pickup */ const startDelivery = async (req, res) => { try { const { id } = req.params; const delivery = await prisma.delivery.findUnique({ where: { id }, }); if (!delivery || delivery.driverId !== req.user.id) { return res.status(404).json({ success: false, message: 'Delivery not found', }); } const updatedDelivery = await prisma.delivery.update({ where: { id }, data: { status: 'IN_TRANSIT' }, }); // Also update shipment status await prisma.shipment.update({ where: { id: delivery.shipmentId }, data: { status: 'IN_TRANSIT' }, }); res.status(200).json({ success: true, message: 'Delivery started', data: updatedDelivery, }); } catch (error) { console.error('Start delivery error:', error); res.status(500).json({ success: false, message: 'Failed to start delivery', }); } }; /** * POST /api/deliveries/:id/pickup - Cargo loaded */ const pickupCargo = async (req, res) => { try { const { id } = req.params; const delivery = await prisma.delivery.findUnique({ where: { id }, }); if (!delivery || delivery.driverId !== req.user.id) { return res.status(404).json({ success: false, message: 'Delivery not found', }); } const updatedDelivery = await prisma.delivery.update({ where: { id }, data: { status: 'CARGO_LOADED', pickupTime: new Date(), }, }); res.status(200).json({ success: true, message: 'Cargo marked as picked up', data: updatedDelivery, }); } catch (error) { console.error('Pickup cargo error:', error); res.status(500).json({ success: false, message: 'Failed to mark cargo as picked up', }); } }; /** * POST /api/deliveries/:id/complete - Delivery complete */ const completeDelivery = async (req, res) => { try { const { id } = req.params; const delivery = await prisma.delivery.findUnique({ where: { id }, include: { shipment: true }, }); if (!delivery || delivery.driverId !== req.user.id) { return res.status(404).json({ success: false, message: 'Delivery not found', }); } // Update delivery status const updatedDelivery = await prisma.delivery.update({ where: { id }, data: { status: 'COMPLETED', completedAt: new Date(), dropTime: new Date(), }, }); // Update shipment if exists if (delivery.shipmentId) { await prisma.shipment.update({ where: { id: delivery.shipmentId }, data: { status: 'COMPLETED' }, }); } // Create transaction record const transaction = await prisma.transaction.create({ data: { driverId: req.user.id, deliveryId: id, amount: delivery.totalEarnings || 0, type: 'BASE_DELIVERY', description: `Payment for delivery ${id}`, route: `${delivery.pickupLocation} → ${delivery.dropLocation}`, }, }); // Update driver's total earnings await prisma.user.update({ where: { id: req.user.id }, data: { totalEarnings: { increment: delivery.totalEarnings || 0 }, weeklyEarnings: { increment: delivery.totalEarnings || 0 }, deliveriesCount: { increment: 1 }, totalDistanceKm: { increment: delivery.distanceKm || 0 }, }, }); res.status(200).json({ success: true, message: 'Delivery completed successfully', data: { delivery: updatedDelivery, transaction, }, }); } catch (error) { console.error('Complete delivery error:', error); res.status(500).json({ success: false, message: 'Failed to complete delivery', }); } }; /** * POST /api/deliveries/:id/upload-photos - Upload photos (4 angles) */ const uploadPhotos = async (req, res) => { try { const { id } = req.params; const { photos } = req.body; // Expecting array of URLs if (!photos || !Array.isArray(photos)) { return res.status(400).json({ success: false, message: 'Photos are required as an array of URLs', }); } const delivery = await prisma.delivery.findUnique({ where: { id }, }); if (!delivery || delivery.driverId !== req.user.id) { return res.status(404).json({ success: false, message: 'Delivery not found', }); } // In a real app, we'd handle file uploads here. // For now, we update the relay node or delivery record if applicable. // Assuming delivery has a way to store photos or it's linked to a relay node. res.status(200).json({ success: true, message: 'Photos uploaded successfully (simulated)', count: photos.length, }); } catch (error) { console.error('Upload photos error:', error); res.status(500).json({ success: false, message: 'Failed to upload photos', }); } }; /** * POST /api/deliveries/create - Create new delivery from package form */ const createDelivery = async (req, res) => { try { const { pickupLocation, pickupLat, pickupLng, pickupTime, deliveryLocation, deliveryLat, deliveryLng, deliveryTime, cargoType, cargoWeight, cargoVolumeLtrs, cargoValue, specialInstructions, courierCompanyId, dispatcherId, distanceKm, postalCode, timeWindowStart, timeWindowEnd } = req.body; // Validate required fields if (!pickupLocation || !deliveryLocation || !cargoType || !cargoWeight || !cargoVolumeLtrs) { return res.status(400).json({ success: false, message: 'Missing required fields: pickupLocation, deliveryLocation, cargoType, cargoWeight, cargoVolumeLtrs' }); } // Validate coordinates if (!pickupLat || !pickupLng || !deliveryLat || !deliveryLng) { return res.status(400).json({ success: false, message: 'Missing required coordinates: pickupLat, pickupLng, deliveryLat, deliveryLng' }); } // Validate dispatcherId const finalDispatcherId = dispatcherId || (req.user ? req.user.id : null); if (!finalDispatcherId) { return res.status(400).json({ success: false, message: 'dispatcherId is required' }); } // Generate unique package ID const packageId = `PKG-${Date.now()}-${Math.random().toString(36).substr(2, 9).toUpperCase()}`; // Create delivery record const delivery = await prisma.delivery.create({ data: { packageId, pickupLocation, pickupLat: parseFloat(pickupLat), pickupLng: parseFloat(pickupLng), pickupTime: pickupTime ? new Date(pickupTime) : null, dropLocation: deliveryLocation, dropLat: parseFloat(deliveryLat), dropLng: parseFloat(deliveryLng), dropTime: deliveryTime ? new Date(deliveryTime) : null, cargoType, cargoWeight: parseFloat(cargoWeight), cargoVolumeLtrs: parseFloat(cargoVolumeLtrs), distanceKm: distanceKm ? parseFloat(distanceKm) : null, postalCode: postalCode || null, timeWindowStart: timeWindowStart ? new Date(timeWindowStart) : null, timeWindowEnd: timeWindowEnd ? new Date(timeWindowEnd) : null, // specialInstructions: specialInstructions || null, courierCompanyId: courierCompanyId || null, status: 'PENDING', dispatcherId: finalDispatcherId } }); res.status(201).json({ success: true, message: 'Delivery created successfully', data: delivery }); } catch (error) { console.error('Create delivery error:', error); res.status(500).json({ success: false, message: 'Failed to create delivery', error: error.message }); } }; /** * GET /api/deliveries/unassigned - Dispatcher's list of unassigned pending deliveries */ const getUnassignedDeliveries = async (req, res) => { try { const { courierCompanyId } = req.query; if (!courierCompanyId) { return res.status(400).json({ success: false, message: 'courierCompanyId is required', }); } const whereClause = { status: 'PENDING', driverId: null, truckId: null, optimizedRouteId: null, OR: [ { courierCompanyId: courierCompanyId }, { courierCompanyId: null } ] }; const [count, deliveries] = await Promise.all([ prisma.delivery.count({ where: whereClause }), prisma.delivery.findMany({ where: whereClause, include: { shipment: true, }, orderBy: { createdAt: 'desc', }, }), ]); res.status(200).json({ success: true, data: { count, deliveries, }, }); } catch (error) { console.error('Get unassigned deliveries error:', error); res.status(500).json({ success: false, message: 'Failed to fetch unassigned deliveries', error: error.message, }); } }; module.exports = { getAssignedDeliveries, acceptDelivery, rejectDelivery, startDelivery, pickupCargo, completeDelivery, uploadPhotos, createDelivery, getUnassignedDeliveries, };