File size: 12,057 Bytes
fcf8749 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
// Calculate wellness score for a driver
function calculateWellnessScore(driver) {
const hoursToday = driver.hoursToday || 0;
const hoursSinceRest = driver.hoursSinceRest || 24;
const isIll = driver.isIll || false;
const totalHours7d = driver.totalHours7d || 0;
const fatigueFactor = Math.min(hoursToday / 12, 1.0) * 30;
const restFactor = Math.max(0, (1 - Math.min(hoursSinceRest / 10, 1.0))) * 25;
const illnessFactor = isIll ? 30 : 0;
const overworkFactor = Math.min(totalHours7d / 70, 1.0) * 15;
const rawScore = 100 - fatigueFactor - restFactor - illnessFactor - overworkFactor;
return Math.max(0, Math.min(100, Math.round(rawScore)));
}
// ====== COGNITIVE LOAD INDEX (CLI) ======
// 6-factor composite score grounded in cognitive psychology research.
// Higher CLI = MORE cognitive load = driver needs lighter routes.
function calculateCognitiveLoad(driver) {
// 1. Fatigue Load β hours driven β reaction time degrades
// Dawson & Reid (2000): 17h awake β 0.05% BAC
const fatigue = Math.min((driver.hoursToday || 0) / 14, 1.0) * 100;
// 2. Decision Fatigue β delivery stops deplete willpower
// Baumeister (1998): each decision drains self-regulation capacity
const stopsToday = driver.stopsToday || 0;
const decisionFatigue = Math.min(stopsToday / 20, 1.0) * 100;
// 3. Circadian Penalty β time-of-day alertness curve
// Monk (2005): alertness dips at 2β4 AM and 1β3 PM (post-lunch dip)
const hour = new Date().getHours();
const circadian = (hour >= 2 && hour <= 4) ? 90
: (hour >= 13 && hour <= 15) ? 50
: (hour >= 22 || hour <= 5) ? 70
: 10;
// 4. Monotony Index β same route repetition β vigilance decrement
// Mackworth (1948): sustained attention drops after 30 min of repetition
const routeRepeatCount = driver.routeRepeatCount || 0;
const monotony = Math.min(routeRepeatCount / 5, 1.0) * 100;
// 5. Route Complexity Stress β urban turns, traffic density
// Yerkes-Dodson (1908): optimal arousal curve; high stress = errors
const complexity = driver.lastRouteComplexity || 'MEDIUM';
const complexityScore = complexity === 'HIGH' ? 80
: complexity === 'MEDIUM' ? 40 : 15;
// 6. Recovery Deficit β sleep debt accumulation over 7 days
// Van Dongen (2003): sleep debt accumulates linearly
const avgRestPerDay = (driver.totalRestHours7d || 56) / 7;
const recoveryDeficit = Math.max(0, (8 - avgRestPerDay) / 8) * 100;
// Weighted composite (weights from cognitive science literature)
const CLI = Math.round(
fatigue * 0.25 +
decisionFatigue * 0.20 +
circadian * 0.15 +
monotony * 0.10 +
complexityScore * 0.15 +
recoveryDeficit * 0.15
);
// Cognitive state classification
const clampedCLI = Math.min(100, Math.max(0, CLI));
const state = clampedCLI <= 30 ? 'SHARP'
: clampedCLI <= 55 ? 'ALERT'
: clampedCLI <= 75 ? 'STRAINED'
: 'OVERLOADED';
return {
cognitiveLoadIndex: clampedCLI,
cognitiveState: state,
factors: {
fatigue: Math.round(fatigue),
decisionFatigue: Math.round(decisionFatigue),
circadian: Math.round(circadian),
monotony: Math.round(monotony),
complexityStress: Math.round(complexityScore),
recoveryDeficit: Math.round(recoveryDeficit),
},
maxSafeComplexity: clampedCLI <= 30 ? 'ANY'
: clampedCLI <= 55 ? 'HIGH'
: clampedCLI <= 75 ? 'MEDIUM'
: 'EASY_ONLY',
recommendation: clampedCLI > 75 ? 'β Rest recommended β cognitive overload detected'
: clampedCLI > 55 ? 'β οΈ Assign simple routes only β brain strain detected'
: clampedCLI > 30 ? 'β
Moderately alert β normal operations'
: 'π§ Peak cognitive readiness β ready for complex routes',
};
}
// Get all drivers with wellness data
exports.getDriversWithWellness = async (req, res) => {
try {
const drivers = await prisma.user.findMany({
where: { role: "DRIVER" },
select: {
id: true, name: true, phone: true, status: true,
vehicleType: true, totalDistanceKm: true, rating: true,
hoursToday: true, hoursSinceRest: true, isIll: true,
totalHours7d: true, wellnessScore: true, maxDifficulty: true,
gender: true, credits: true, totalCreditsEarned: true,
homeBaseCity: true, totalEarnings: true, weeklyEarnings: true,
},
});
const enriched = drivers.map(d => {
const wrs = calculateWellnessScore(d);
const cli = calculateCognitiveLoad(d);
return {
...d,
wellnessScore: wrs,
maxDifficulty: wrs < 40 ? 'EASY' : wrs < 70 ? 'MEDIUM' : 'ANY',
wellnessStatus: wrs < 40 ? 'FATIGUED' : wrs < 70 ? 'MODERATE' : 'FIT',
nightSafetyRequired: d.gender === 'F',
...cli,
};
});
res.json({ drivers: enriched, count: enriched.length });
} catch (error) {
console.error("Error fetching wellness data:", error);
res.status(500).json({ error: error.message });
}
};
// Update driver wellness status
exports.updateWellness = async (req, res) => {
try {
const { driverId } = req.params;
const { hoursToday, hoursSinceRest, isIll, totalHours7d } = req.body;
const updateData = {};
if (hoursToday !== undefined) updateData.hoursToday = hoursToday;
if (hoursSinceRest !== undefined) updateData.hoursSinceRest = hoursSinceRest;
if (isIll !== undefined) updateData.isIll = isIll;
if (totalHours7d !== undefined) updateData.totalHours7d = totalHours7d;
const driver = await prisma.user.update({
where: { id: driverId },
data: updateData,
});
const wrs = calculateWellnessScore(driver);
await prisma.user.update({
where: { id: driverId },
data: {
wellnessScore: wrs,
maxDifficulty: wrs < 40 ? 'EASY' : wrs < 70 ? 'MEDIUM' : 'ANY',
},
});
res.json({
driverId,
wellnessScore: wrs,
maxDifficulty: wrs < 40 ? 'EASY' : wrs < 70 ? 'MEDIUM' : 'ANY',
wellnessStatus: wrs < 40 ? 'FATIGUED' : wrs < 70 ? 'MODERATE' : 'FIT',
});
} catch (error) {
console.error("Error updating wellness:", error);
res.status(500).json({ error: error.message });
}
};
// ====== Credit Economy ======
exports.getDriverCredits = async (req, res) => {
try {
const { driverId } = req.params;
const driver = await prisma.user.findUnique({
where: { id: driverId },
select: { id: true, name: true, credits: true, totalCreditsEarned: true },
});
if (!driver) return res.status(404).json({ error: "Driver not found" });
res.json(driver);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
exports.addCredits = async (req, res) => {
try {
const { driverId } = req.params;
const { amount, reason } = req.body;
const driver = await prisma.user.update({
where: { id: driverId },
data: {
credits: { increment: amount },
totalCreditsEarned: { increment: Math.max(0, amount) },
},
});
// Log the credit transaction
await prisma.transaction.create({
data: {
driverId,
amount,
type: amount > 0 ? 'BONUS' : 'PENALTY',
description: reason || `Credit ${amount > 0 ? 'earned' : 'spent'}: ${Math.abs(amount)} credits`,
},
});
res.json({
driverId,
credits: driver.credits,
totalCreditsEarned: driver.totalCreditsEarned,
change: amount,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
};
// Get wellness summary for fleet
exports.getFleetWellnessSummary = async (req, res) => {
try {
const drivers = await prisma.user.findMany({
where: { role: "DRIVER" },
select: {
hoursToday: true, hoursSinceRest: true, isIll: true,
totalHours7d: true, gender: true, status: true,
},
});
const scores = drivers.map(d => calculateWellnessScore(d));
const fitCount = scores.filter(s => s >= 70).length;
const moderateCount = scores.filter(s => s >= 40 && s < 70).length;
const fatiguedCount = scores.filter(s => s < 40).length;
const femaleDrivers = drivers.filter(d => d.gender === 'F').length;
const illDrivers = drivers.filter(d => d.isIll).length;
res.json({
totalDrivers: drivers.length,
fit: fitCount,
moderate: moderateCount,
fatigued: fatiguedCount,
averageWellness: Math.round(scores.reduce((a, b) => a + b, 0) / (scores.length || 1)),
femaleDrivers,
illDrivers,
nightSafetyEligible: femaleDrivers,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
};
// ====== COGNITIVE LOAD ENDPOINTS ======
// Get cognitive load for a single driver
exports.getDriverCognitive = async (req, res) => {
try {
const { driverId } = req.params;
const driver = await prisma.user.findUnique({
where: { id: driverId },
select: {
id: true, name: true, phone: true,
hoursToday: true, hoursSinceRest: true, isIll: true,
totalHours7d: true, vehicleType: true, gender: true,
},
});
if (!driver) return res.status(404).json({ error: "Driver not found" });
const wellness = calculateWellnessScore(driver);
const cognitive = calculateCognitiveLoad(driver);
res.json({
...driver,
wellnessScore: wellness,
...cognitive,
});
} catch (error) {
console.error("Error fetching cognitive data:", error);
res.status(500).json({ error: error.message });
}
};
// Get fleet-wide cognitive summary
exports.getFleetCognitiveSummary = async (req, res) => {
try {
const drivers = await prisma.user.findMany({
where: { role: "DRIVER" },
select: {
hoursToday: true, hoursSinceRest: true, isIll: true,
totalHours7d: true, gender: true, status: true, name: true,
},
});
const results = drivers.map(d => {
const cli = calculateCognitiveLoad(d);
return { name: d.name, ...cli };
});
const scores = results.map(r => r.cognitiveLoadIndex);
const sharpCount = results.filter(r => r.cognitiveState === 'SHARP').length;
const alertCount = results.filter(r => r.cognitiveState === 'ALERT').length;
const strainedCount = results.filter(r => r.cognitiveState === 'STRAINED').length;
const overloadedCount = results.filter(r => r.cognitiveState === 'OVERLOADED').length;
res.json({
totalDrivers: drivers.length,
averageCLI: Math.round(scores.reduce((a, b) => a + b, 0) / (scores.length || 1)),
sharp: sharpCount,
alert: alertCount,
strained: strainedCount,
overloaded: overloadedCount,
drivers: results,
});
} catch (error) {
console.error("Error fetching fleet cognitive summary:", error);
res.status(500).json({ error: error.message });
}
};
|