Task Management Implementation Plan
Current System Integration Strategyβ
This document outlines how the task management system will integrate with the existing AttuneLogic architecture.
1. Current System Analysisβ
Existing Components We'll Leverage:β
- β
Legs API:
/api/legs- Will extend to include task data - β LoadDetails Component: Current UI will be enhanced with task display
- β MongoDB/Mongoose: Will add task schemas to existing models
- β RTK Query: Will extend legs API for task operations
- β
Hook System: Will use existing
useAuthanduseCustomerConfig - β Navigation: Expo Router for task checklist navigation
Integration Points:β
- π LoadDetails.tsx: Already has task UI mockup - will make functional
- ποΈ Legs Model: Will embed tasks following Option A approach
- ποΈ Customer Config: Task templates will be configurable per org
- π± Mobile Navigation: Task checklist as modal/screen
2. Implementation Phasesβ
Phase 1A: Backend Foundation (API)β
Target: Add basic task support to existing legs system
1. Extend Legs Model (/src/models/legs.js)β
// Add to existing Legs schema
const taskSchema = new mongoose.Schema({
id: { type: String, required: true },
templateId: { type: String, required: true },
name: { type: String, required: true },
description: String,
status: {
type: String,
enum: ["PENDING", "IN_PROGRESS", "COMPLETED", "SKIPPED", "FAILED"],
default: "PENDING",
},
priority: {
type: String,
enum: ["LOW", "NORMAL", "HIGH"],
default: "NORMAL",
},
// Progress tracking
steps: [
{
id: String,
title: String,
type: { type: String, enum: ["checkbox", "photo", "signature", "text"] },
required: Boolean,
completed: { type: Boolean, default: false },
completedAt: Date,
data: mongoose.Schema.Types.Mixed, // Photos, notes, etc.
},
],
// Timing
assignedAt: { type: Date, default: Date.now },
dueDate: Date,
startedAt: Date,
completedAt: Date,
// Metadata
assignedTo: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
completionNotes: String,
// Audit
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
});
// Add to existing Legs schema
const legsSchema = new mongoose.Schema({
// ... existing fields ...
// New task fields
tasks: [taskSchema],
currentTaskId: String, // Reference to active task
taskProgress: {
total: { type: Number, default: 0 },
completed: { type: Number, default: 0 },
percentage: { type: Number, default: 0 },
},
});
2. Extend Legs Controller (/src/controllers/legs/index.js)β
// Add task management methods to existing controller
// Get tasks for a leg
getTasks: async (req, res) => {
try {
const { user } = req.user;
const leg = await Leg.findById(req.params.id).populate('tasks.assignedTo');
if (!leg) {
return res.status(404).json({ message: 'Leg not found' });
}
res.json({ tasks: leg.tasks, progress: leg.taskProgress });
} catch (error) {
res.status(500).json({ error: error.message });
}
},
// Update task progress
updateTask: async (req, res) => {
try {
const { taskId, stepId, data } = req.body;
const { user } = req.user;
const leg = await Leg.findById(req.params.id);
const task = leg.tasks.id(taskId);
const step = task.steps.id(stepId);
// Update step
step.completed = data.completed;
step.completedAt = data.completed ? new Date() : null;
step.data = { ...step.data, ...data.stepData };
// Update task status
const completedSteps = task.steps.filter(s => s.completed).length;
const totalSteps = task.steps.length;
if (completedSteps === totalSteps) {
task.status = 'COMPLETED';
task.completedAt = new Date();
} else if (completedSteps > 0) {
task.status = 'IN_PROGRESS';
if (!task.startedAt) task.startedAt = new Date();
}
// Update leg task progress
leg.taskProgress = calculateTaskProgress(leg.tasks);
await leg.save();
res.json({ task, progress: leg.taskProgress });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
3. Add Task Routes (/src/routes/api/v1/legs.js)β
// Extend existing legs routes
router.get("/:id/tasks", verifyToken, verifyParent, legsController.getTasks);
router.put(
"/:id/tasks/:taskId",
verifyToken,
verifyParent,
legsController.updateTask
);
router.post("/:id/tasks", verifyToken, verifyParent, legsController.createTask);
Phase 1B: Frontend Foundation (Mobile)β
1. Extend RTK Query (/store/api/legs/api.js)β
// Add to existing legs API slice
export const legsApi = createApi({
// ... existing config ...
endpoints: (builder) => ({
// ... existing endpoints ...
// New task endpoints
getLegTasks: builder.query({
query: ({ id }) => `legs/${id}/tasks`,
providesTags: (result, error, { id }) => [
{ type: "Leg", id },
{ type: "Task", id: "LIST" },
],
}),
updateTask: builder.mutation({
query: ({ legId, taskId, ...data }) => ({
url: `legs/${legId}/tasks/${taskId}`,
method: "PUT",
body: data,
}),
invalidatesTags: (result, error, { legId }) => [
{ type: "Leg", id: legId },
{ type: "Task", id: "LIST" },
],
}),
}),
});
export const {
useGetLegByIdQuery, // existing
useGetLegTasksQuery, // new
useUpdateTaskMutation, // new
} = legsApi;
2. Create Task Hook (/hooks/useTasks.tsx)β
import {
useGetLegTasksQuery,
useUpdateTaskMutation,
} from "@/store/api/legs/api";
export const useTasks = (legId: string) => {
const {
data: tasksData,
isLoading,
refetch,
} = useGetLegTasksQuery({ id: legId }, { skip: !legId });
const [updateTask] = useUpdateTaskMutation();
const currentTask = tasksData?.tasks?.find(
(task) => task.status === "IN_PROGRESS" || task.status === "PENDING"
);
const completeStep = async (taskId: string, stepId: string, data: any) => {
try {
await updateTask({
legId,
taskId,
stepId,
data: {
completed: true,
stepData: data,
},
}).unwrap();
} catch (error) {
console.error("Failed to complete step:", error);
throw error;
}
};
return {
tasks: tasksData?.tasks || [],
progress: tasksData?.progress || { total: 0, completed: 0, percentage: 0 },
currentTask,
isLoading,
refetch,
completeStep,
};
};
3. Update LoadDetails Component (/features/Load/LoadDetails.tsx)β
// Replace mock data with real data
import { useTasks } from '@/hooks/useTasks';
import { useRouter } from 'expo-router';
export const LoadDetails: React.FC<LoadDetailsProps> = ({ id }) => {
const router = useRouter();
const { currentTask, progress } = useTasks(id);
// Replace the mock current task section
{/* Current Task Section */}
<View style={tw`mt-4 pt-4 border-t border-gray-200`}>
<Text style={tw`text-sm font-medium text-gray-700 mb-3`}>
Current Task
</Text>
{currentTask ? (
<TouchableOpacity
style={tw`bg-blue-50 p-3 rounded-lg border border-blue-200 flex-row items-center justify-between`}
onPress={() => {
router.push({
pathname: '/load/[id]/tasks',
params: { id }
});
}}
>
<View style={tw`flex-row items-center flex-1`}>
<View style={tw`w-8 h-8 bg-blue-500 rounded-full items-center justify-center mr-3`}>
<Ionicons
name={currentTask.status === 'COMPLETED' ? 'checkmark-circle' : 'time'}
size={16}
color="white"
/>
</View>
<View style={tw`flex-1`}>
<Text style={tw`text-sm font-medium text-blue-900`}>
{currentTask.name}
</Text>
<Text style={tw`text-xs text-blue-700 mt-1`}>
{progress.completed} of {progress.total} tasks completed
</Text>
</View>
</View>
<Ionicons
name="chevron-forward"
size={16}
color={tw.color("blue-600")}
/>
</TouchableOpacity>
) : (
<View style={tw`bg-gray-50 p-3 rounded-lg border border-gray-200`}>
<Text style={tw`text-sm text-gray-600 text-center`}>
No active tasks
</Text>
</View>
)}
</View>
4. Create Task Checklist Screen (/app/(app)/(tabs)/(home)/load/[id]/tasks.tsx)β
import React from "react";
import { View, Text, ScrollView, TouchableOpacity, Modal } from "react-native";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useTasks } from "@/hooks/useTasks";
import { TaskChecklistItem } from "@/components/TaskChecklistItem";
import tw from "@/theme";
export default function TasksScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
const { tasks, progress, completeStep } = useTasks(id);
return (
<ScrollView style={tw`flex-1 bg-gray-100`}>
{/* Header */}
<View style={tw`bg-white p-4 border-b border-gray-200`}>
<Text style={tw`text-lg font-semibold text-gray-900`}>Load Tasks</Text>
<Text style={tw`text-sm text-gray-600`}>
{progress.completed} of {progress.total} completed (
{progress.percentage}%)
</Text>
</View>
{/* Progress Bar */}
<View style={tw`bg-white mx-4 mt-4 p-4 rounded-lg`}>
<View style={tw`bg-gray-200 rounded-full h-2`}>
<View
style={[
tw`bg-blue-500 h-2 rounded-full`,
{ width: `${progress.percentage}%` },
]}
/>
</View>
</View>
{/* Task List */}
<View style={tw`p-4`}>
{tasks.map((task, index) => (
<TaskChecklistItem
key={task.id}
task={task}
onStepComplete={completeStep}
isExpanded={task.status === "IN_PROGRESS"}
/>
))}
</View>
</ScrollView>
);
}
5. Create Task Checklist Component (/components/TaskChecklistItem.tsx)β
import React, { useState } from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import tw from "@/theme";
interface TaskChecklistItemProps {
task: any;
onStepComplete: (taskId: string, stepId: string, data: any) => void;
isExpanded?: boolean;
}
export const TaskChecklistItem: React.FC<TaskChecklistItemProps> = ({
task,
onStepComplete,
isExpanded = false,
}) => {
const [expanded, setExpanded] = useState(isExpanded);
const getStatusColor = (status: string) => {
switch (status) {
case "COMPLETED":
return "green";
case "IN_PROGRESS":
return "blue";
case "FAILED":
return "red";
default:
return "gray";
}
};
return (
<View style={tw`bg-white rounded-lg mb-4 shadow-sm`}>
{/* Task Header */}
<TouchableOpacity
style={tw`p-4 flex-row items-center justify-between`}
onPress={() => setExpanded(!expanded)}
>
<View style={tw`flex-row items-center flex-1`}>
<View
style={tw`w-8 h-8 bg-${getStatusColor(
task.status
)}-500 rounded-full items-center justify-center mr-3`}
>
<Ionicons
name={task.status === "COMPLETED" ? "checkmark" : "time"}
size={16}
color="white"
/>
</View>
<View style={tw`flex-1`}>
<Text style={tw`text-base font-medium text-gray-900`}>
{task.name}
</Text>
<Text style={tw`text-sm text-gray-600`}>
{task.steps.filter((s) => s.completed).length} of{" "}
{task.steps.length} steps
</Text>
</View>
</View>
<Ionicons
name={expanded ? "chevron-up" : "chevron-down"}
size={16}
color={tw.color("gray-400")}
/>
</TouchableOpacity>
{/* Task Steps */}
{expanded && (
<View style={tw`px-4 pb-4 border-t border-gray-100`}>
{task.steps.map((step, index) => (
<TouchableOpacity
key={step.id}
style={tw`flex-row items-center py-3 ${
index > 0 ? "border-t border-gray-50" : ""
}`}
onPress={() => {
if (!step.completed) {
onStepComplete(task.id, step.id, { notes: "" });
}
}}
disabled={step.completed}
>
<View
style={tw`w-6 h-6 rounded-full border-2 mr-3 items-center justify-center ${
step.completed
? "bg-green-500 border-green-500"
: "border-gray-300"
}`}
>
{step.completed && (
<Ionicons name="checkmark" size={12} color="white" />
)}
</View>
<Text
style={tw`text-sm ${
step.completed
? "text-gray-500 line-through"
: "text-gray-900"
}`}
>
{step.title}
</Text>
</TouchableOpacity>
))}
</View>
)}
</View>
);
};
3. Customer Configuration Integrationβ
Add Task Templates to Customer Config (/services/config/default-configs/index.js)β
taskManagement: {
enabled: true,
templates: {
trucking: [
{
id: 'pre-delivery-inspection',
name: 'Pre-delivery Inspection',
category: 'safety',
steps: [
{ id: 'check_tires', title: 'Check tire pressure and condition', type: 'checkbox' },
{ id: 'check_lights', title: 'Verify all lights working', type: 'checkbox' },
{ id: 'check_brakes', title: 'Test brake system', type: 'checkbox' },
{ id: 'take_photos', title: 'Take vehicle photos', type: 'photo' }
],
autoAssign: {
trigger: 'status_change',
from: 'assigned',
to: 'in_transit'
}
}
],
service_repair: [
{
id: 'site-assessment',
name: 'Site Assessment',
category: 'evaluation',
steps: [
{ id: 'property_photos', title: 'Take property photos', type: 'photo' },
{ id: 'damage_assessment', title: 'Assess damage/issues', type: 'text' },
{ id: 'customer_signature', title: 'Get customer signature', type: 'signature' }
]
}
]
},
notifications: {
enabled: true,
reminderHours: [4, 1], // Hours before deadline
escalationEnabled: false
}
}
4. Navigation Setupβ
Add Task Route (/app/(app)/(tabs)/(home)/_layout.tsx)β
// Add the tasks route to the existing layout
5. Testing Strategyβ
Phase 1 Testingβ
- Unit Tests: Task model validation, controller methods
- Integration Tests: API endpoints with existing legs
- UI Tests: Task checklist component functionality
- E2E Tests: Complete task workflow from assignment to completion
Test Data Setupβ
// Add to existing seed files
const testTasks = [
{
id: "task-1",
templateId: "pre-delivery-inspection",
name: "Pre-delivery Inspection",
status: "IN_PROGRESS",
steps: [
{ id: "step-1", title: "Check tires", completed: true },
{ id: "step-2", title: "Check lights", completed: false },
],
},
];
6. Migration Strategyβ
Database Migrationβ
// Migration script to add tasks to existing legs
const migrateLegsTasks = async () => {
const legs = await Leg.find({});
for (const leg of legs) {
if (!leg.tasks) {
leg.tasks = [];
leg.taskProgress = { total: 0, completed: 0, percentage: 0 };
await leg.save();
}
}
};
7. Rollout Planβ
Week 1-2: Backend Implementationβ
- Extend legs model with task schema
- Add task controller methods
- Create API endpoints
- Write unit tests
Week 3-4: Frontend Implementationβ
- Create task hooks and RTK queries
- Update LoadDetails with real task data
- Build task checklist screen
- Create task checklist components
Week 5: Integration & Testingβ
- Connect frontend to backend
- E2E testing
- Customer config integration
- Performance testing
Week 6: Deployment & Monitoringβ
- Deploy to staging
- User acceptance testing
- Production deployment
- Monitor usage and performance
Phase 2A: Task Templates & Rule Engineβ
Target: Add automatic task assignment and template management
1. Enhanced TaskTemplate Operations (/src/controllers/taskTemplates/index.js)β
// Task Template Controller - extends existing Task.js model
// Get available templates for organization/industry
getTemplates: async (req, res) => {
try {
const { user } = req.user;
const { industry, category } = req.query;
const query = {
$or: [
{ organizationId: user.organizationId },
{ organizationId: null } // System templates
],
isActive: true
};
if (industry) query.industry = industry;
if (category) query.category = category;
const templates = await TaskTemplate.find(query);
res.json({ data: templates });
} catch (error) {
res.status(500).json({ error: error.message });
}
},
// Create template from existing task
createTemplate: async (req, res) => {
try {
const { name, description, industry, category, steps, assignmentRules } = req.body;
const { user } = req.user;
const template = new TaskTemplate({
name,
description,
industry,
category,
steps,
assignmentRules,
organizationId: user.organizationId,
createdBy: user._id
});
await template.save();
res.status(201).json({ data: template });
} catch (error) {
res.status(500).json({ error: error.message });
}
},
// Apply template to leg
applyTemplate: async (req, res) => {
try {
const { templateId } = req.body;
const legId = req.params.id;
const { user } = req.user;
const template = await TaskTemplate.findById(templateId);
const leg = await Leg.findById(legId);
if (!template || !leg) {
return res.status(404).json({ message: 'Template or leg not found' });
}
// Create task instance from template
const taskInstance = template.createTaskInstance({
assignedTo: user._id
});
leg.tasks.push(taskInstance);
leg.updateTaskProgress();
await leg.save();
res.json({ data: taskInstance, progress: leg.taskProgress });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
2. Rule Engine Service (/src/services/taskRuleEngine.js)β
const TaskTemplate = require("../models/Task").TaskTemplate;
const Leg = require("../models/Leg");
class TaskRuleEngine {
// Evaluate if templates should be auto-assigned
static async evaluateStatusChange(leg, oldStatus, newStatus) {
try {
const applicableTemplates = await TaskTemplate.find({
"assignmentRules.triggers": `status_change:${newStatus}`,
$or: [{ organizationId: leg.organizationId }, { organizationId: null }],
isActive: true,
});
const assignedTasks = [];
for (const template of applicableTemplates) {
const shouldAssign = await this.evaluateConditions(
leg,
template.assignmentRules.conditions
);
if (shouldAssign) {
const taskInstance = template.createTaskInstance({
assignedTo: leg.assignedDriver || leg.assignedTo,
});
leg.tasks.push(taskInstance);
assignedTasks.push(taskInstance);
}
}
if (assignedTasks.length > 0) {
leg.updateTaskProgress();
await leg.save();
}
return assignedTasks;
} catch (error) {
console.error("Rule engine evaluation failed:", error);
return [];
}
}
// Evaluate template conditions against leg data
static async evaluateConditions(leg, conditions) {
if (!conditions) return true;
// Equipment type condition
if (conditions.equipmentType && leg.equipment) {
if (!conditions.equipmentType.includes(leg.equipment.type)) {
return false;
}
}
// Load type condition
if (conditions.loadType && leg.loadType) {
if (!conditions.loadType.includes(leg.loadType)) {
return false;
}
}
// Load value condition
if (conditions.loadValue && leg.rate) {
if (conditions.loadValue.min && leg.rate < conditions.loadValue.min) {
return false;
}
if (conditions.loadValue.max && leg.rate > conditions.loadValue.max) {
return false;
}
}
// Distance condition
if (conditions.distance && leg.miles) {
if (conditions.distance.min && leg.miles < conditions.distance.min) {
return false;
}
if (conditions.distance.max && leg.miles > conditions.distance.max) {
return false;
}
}
return true;
}
// Process equipment-based assignment
static async evaluateEquipmentChange(leg, equipmentType) {
const applicableTemplates = await TaskTemplate.find({
"assignmentRules.triggers": `equipment_type:${equipmentType}`,
isActive: true,
});
// Similar logic to status change evaluation
return this.processTemplateAssignment(leg, applicableTemplates);
}
// Common template assignment logic
static async processTemplateAssignment(leg, templates) {
const assignedTasks = [];
for (const template of templates) {
const shouldAssign = await this.evaluateConditions(
leg,
template.assignmentRules.conditions
);
if (shouldAssign) {
// Check if task from this template already exists
const existingTask = leg.tasks.find(
(task) => task.templateId === template._id.toString()
);
if (!existingTask) {
const taskInstance = template.createTaskInstance();
leg.tasks.push(taskInstance);
assignedTasks.push(taskInstance);
}
}
}
return assignedTasks;
}
}
module.exports = TaskRuleEngine;
3. Integration with Existing Legs Controllerβ
// Extend existing updateLegStatus method
updateLegStatus: async (req, res) => {
try {
const leg = await Leg.findById(req.params.id);
const oldStatus = leg.status;
// Existing status update logic...
leg.status = req.body.status;
await leg.save();
// NEW: Trigger rule engine for automatic task assignment
const TaskRuleEngine = require("../services/taskRuleEngine");
const assignedTasks = await TaskRuleEngine.evaluateStatusChange(
leg,
oldStatus,
leg.status
);
res.json({
data: leg,
assignedTasks: assignedTasks.length,
message:
assignedTasks.length > 0
? `${assignedTasks.length} tasks automatically assigned`
: undefined,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
};
4. Add Task Template Routes (/src/routes/api/v1/taskTemplates.js)β
const express = require("express");
const router = express.Router();
const { verifyToken, verifyParent } = require("../../middlewares");
const taskTemplateController = require("../../controllers/taskTemplates");
// Template management routes
router.get("/", verifyToken, verifyParent, taskTemplateController.getTemplates);
router.post(
"/",
verifyToken,
verifyParent,
taskTemplateController.createTemplate
);
router.get(
"/:id",
verifyToken,
verifyParent,
taskTemplateController.getTemplate
);
router.put(
"/:id",
verifyToken,
verifyParent,
taskTemplateController.updateTemplate
);
router.delete(
"/:id",
verifyToken,
verifyParent,
taskTemplateController.deleteTemplate
);
// Template application routes
router.post(
"/:id/apply-to-leg/:legId",
verifyToken,
verifyParent,
taskTemplateController.applyTemplate
);
module.exports = router;
5. Extend Legs Routes (/src/routes/api/v1/legs.js)β
// Add to existing legs routes
router.get(
"/:id/available-templates",
verifyToken,
verifyParent,
legsController.getAvailableTemplates
);
router.post(
"/:id/apply-template",
verifyToken,
verifyParent,
legsController.applyTemplate
);
6. Customer Configuration Integrationβ
// Add to existing default-configs/index.js
taskManagement: {
enabled: true,
autoAssignment: {
enabled: true,
triggers: {
statusChanges: true,
equipmentChanges: true,
loadTypeChanges: true
}
},
templates: {
// Industry-specific defaults will be seeded as TaskTemplate documents
defaultIndustry: "trucking", // or "service_repair"
customTemplatesEnabled: true,
systemTemplatesEnabled: true
},
notifications: {
newTaskAssigned: true,
taskOverdue: true,
reminderHours: [24, 4, 1]
}
}
7. System Template Seeding (/seeds/taskTemplates.js)β
const TaskTemplate = require("../src/models/Task").TaskTemplate;
const systemTemplates = [
{
name: "Pre-Delivery Inspection",
description: "Standard safety inspection before delivery",
industry: "trucking",
category: "safety",
organizationId: null, // System template
assignmentRules: {
triggers: ["status_change:en_route"],
conditions: {
loadValue: { min: 5000 },
},
timing: {
relativeToStatus: "en_route",
offsetHours: -2,
},
},
steps: [
{
id: "check_tires",
title: "Check tire pressure and condition",
type: "checkbox",
required: true,
instructions:
"Inspect all tires for proper pressure and visible damage",
},
{
id: "check_lights",
title: "Verify all lights working",
type: "checkbox",
required: true,
instructions:
"Test headlights, taillights, turn signals, and hazard lights",
},
{
id: "check_brakes",
title: "Test brake system",
type: "checkbox",
required: true,
instructions: "Check brake pedal feel and parking brake operation",
},
{
id: "vehicle_photos",
title: "Take vehicle exterior photos",
type: "photo",
required: true,
instructions:
"Capture all four sides of vehicle and any existing damage",
},
],
documentsRequired: false,
estimatedDuration: 15,
deadline: {
type: "relative",
value: 4, // 4 hours from assignment
required: false,
},
},
{
name: "Hazmat Safety Check",
description: "Required safety procedures for hazardous materials",
industry: "trucking",
category: "safety",
organizationId: null,
assignmentRules: {
triggers: ["status_change:assigned"],
conditions: {
loadType: ["hazmat", "chemical"],
},
},
steps: [
{
id: "placards_check",
title: "Verify proper placards installed",
type: "photo",
required: true,
instructions: "Photograph all required hazmat placards",
},
{
id: "emergency_kit",
title: "Confirm emergency response kit",
type: "checkbox",
required: true,
instructions: "Verify spill kit and emergency equipment present",
},
{
id: "documentation",
title: "Check shipping papers and permits",
type: "checkbox",
required: true,
instructions: "Ensure all required documentation is complete",
},
],
documentsRequired: true,
statusUpdateTrigger: "ready_for_pickup",
},
];
// Seed function
const seedTaskTemplates = async () => {
try {
for (const templateData of systemTemplates) {
const existing = await TaskTemplate.findOne({
name: templateData.name,
organizationId: null,
});
if (!existing) {
await TaskTemplate.create(templateData);
console.log(`Created template: ${templateData.name}`);
}
}
} catch (error) {
console.error("Template seeding failed:", error);
}
};
module.exports = { seedTaskTemplates, systemTemplates };
Phase 2B: Frontend Template Management (Mobile)β
1. Task Template API Integration (/store/api/taskTemplates/api.js)β
export const taskTemplatesApi = rootApi.injectEndpoints({
endpoints: (builder) => ({
getTaskTemplates: builder.query({
query: ({ industry, category } = {}) => {
const params = new URLSearchParams();
if (industry) params.append("industry", industry);
if (category) params.append("category", category);
return `task-templates?${params}`;
},
transformResponse: (response) => response.data,
providesTags: ["TaskTemplates"],
}),
applyTemplate: builder.mutation({
query: ({ legId, templateId }) => ({
url: `legs/${legId}/apply-template`,
method: "POST",
body: { templateId },
}),
invalidatesTags: (result, error, { legId }) => [
{ type: "Legs", id: legId },
{ type: "Tasks", id: "LIST" },
],
}),
getAvailableTemplates: builder.query({
query: ({ legId }) => `legs/${legId}/available-templates`,
transformResponse: (response) => response.data,
providesTags: (result, error, { legId }) => [
{ type: "Legs", id: legId },
"TaskTemplates",
],
}),
}),
});
export const {
useGetTaskTemplatesQuery,
useApplyTemplateMutation,
useGetAvailableTemplatesQuery,
} = taskTemplatesApi;
2. Template Management Hook (/hooks/useTaskTemplates.tsx)β
import {
useGetTaskTemplatesQuery,
useApplyTemplateMutation,
useGetAvailableTemplatesQuery,
} from "@/store/api/taskTemplates/api";
import { Alert } from "react-native";
export const useTaskTemplates = (legId?: string) => {
const {
data: allTemplates,
isLoading: templatesLoading,
refetch: refetchTemplates,
} = useGetTaskTemplatesQuery({});
const { data: availableTemplates, isLoading: availableLoading } =
useGetAvailableTemplatesQuery({ legId: legId! }, { skip: !legId });
const [applyTemplate, { isLoading: isApplying }] = useApplyTemplateMutation();
const applyTemplateToLeg = async (templateId: string) => {
if (!legId) return;
try {
const result = await applyTemplate({ legId, templateId }).unwrap();
Alert.alert("Success", "Tasks added from template successfully!");
return result;
} catch (error) {
console.error("Failed to apply template:", error);
Alert.alert("Error", "Failed to apply template. Please try again.");
throw error;
}
};
const getTemplatesByCategory = (category: string) => {
return (
allTemplates?.filter((template: any) => template.category === category) ||
[]
);
};
const getTemplatesByIndustry = (industry: string) => {
return (
allTemplates?.filter((template: any) => template.industry === industry) ||
[]
);
};
return {
// Data
allTemplates: allTemplates || [],
availableTemplates: availableTemplates || [],
// State
templatesLoading,
availableLoading,
isApplying,
// Actions
applyTemplateToLeg,
refetchTemplates,
// Utilities
getTemplatesByCategory,
getTemplatesByIndustry,
};
};
3. Admin Template Selection Screen (/app/(app)/(tabs)/(home)/load/[id]/add-tasks.tsx)β
import React, { useState } from "react";
import { View, Text, ScrollView, TouchableOpacity } from "react-native";
import { useLocalSearchParams, useRouter } from "expo-router";
import { SafeAreaView } from "react-native-safe-area-context";
import { Ionicons } from "@expo/vector-icons";
import tw from "@/theme";
import { useTaskTemplates } from "@/hooks/useTaskTemplates";
export default function AddTasksScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const {
availableTemplates,
availableLoading,
applyTemplateToLeg,
isApplying,
} = useTaskTemplates(id);
const categories = [
{ key: "all", label: "All Templates" },
{ key: "safety", label: "Safety" },
{ key: "documentation", label: "Documentation" },
{ key: "maintenance", label: "Maintenance" },
{ key: "delivery", label: "Delivery" },
{ key: "inspection", label: "Inspection" },
];
const filteredTemplates =
selectedCategory === "all"
? availableTemplates
: availableTemplates.filter((t: any) => t.category === selectedCategory);
const handleApplyTemplate = async (templateId: string) => {
try {
await applyTemplateToLeg(templateId);
router.back();
} catch (error) {
// Error handled in hook
}
};
return (
<SafeAreaView style={tw`flex-1 bg-gray-50`}>
{/* Header */}
<View style={tw`bg-white px-4 py-3 border-b border-gray-200`}>
<View style={tw`flex-row items-center justify-between`}>
<TouchableOpacity onPress={() => router.back()}>
<Ionicons
name="arrow-back"
size={24}
color={tw.color("gray-600")}
/>
</TouchableOpacity>
<Text style={tw`text-lg font-semibold text-gray-900`}>Add Tasks</Text>
<View style={tw`w-6`} />
</View>
</View>
{/* Category Filter */}
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={tw`bg-white border-b border-gray-200`}
contentContainerStyle={tw`px-4 py-3`}
>
{categories.map((category) => (
<TouchableOpacity
key={category.key}
style={tw`mr-3 px-4 py-2 rounded-full ${
selectedCategory === category.key ? "bg-blue-500" : "bg-gray-100"
}`}
onPress={() => setSelectedCategory(category.key)}
>
<Text
style={tw`text-sm font-medium ${
selectedCategory === category.key
? "text-white"
: "text-gray-700"
}`}
>
{category.label}
</Text>
</TouchableOpacity>
))}
</ScrollView>
{/* Templates List */}
<ScrollView style={tw`flex-1`} contentContainerStyle={tw`p-4`}>
{availableLoading ? (
<View style={tw`flex-1 justify-center items-center py-20`}>
<Ionicons name="sync" size={32} color={tw.color("blue-500")} />
<Text style={tw`text-gray-600 mt-4`}>Loading templates...</Text>
</View>
) : filteredTemplates.length === 0 ? (
<View style={tw`flex-1 justify-center items-center py-20`}>
<Ionicons name="clipboard" size={64} color={tw.color("gray-400")} />
<Text style={tw`text-lg font-medium text-gray-900 mt-4`}>
No Templates Available
</Text>
<Text style={tw`text-sm text-gray-600 mt-2 text-center`}>
No templates found for the selected category.
</Text>
</View>
) : (
filteredTemplates.map((template: any) => (
<TouchableOpacity
key={template._id}
style={tw`bg-white rounded-lg p-4 mb-4 shadow-sm border border-gray-200`}
onPress={() => handleApplyTemplate(template._id)}
disabled={isApplying}
>
<View style={tw`flex-row items-start justify-between`}>
<View style={tw`flex-1`}>
<Text style={tw`text-base font-medium text-gray-900 mb-2`}>
{template.name}
</Text>
{template.description && (
<Text style={tw`text-sm text-gray-600 mb-3`}>
{template.description}
</Text>
)}
<View style={tw`flex-row items-center mb-2`}>
<View style={tw`bg-blue-100 px-2 py-1 rounded mr-2`}>
<Text
style={tw`text-xs font-medium text-blue-800 capitalize`}
>
{template.category}
</Text>
</View>
<View style={tw`bg-gray-100 px-2 py-1 rounded`}>
<Text
style={tw`text-xs font-medium text-gray-700 capitalize`}
>
{template.industry}
</Text>
</View>
</View>
<Text style={tw`text-xs text-gray-500`}>
{template.steps?.length || 0} steps
{template.estimatedDuration &&
` β’ ~${template.estimatedDuration} min`}
</Text>
</View>
{isApplying ? (
<Ionicons
name="sync"
size={20}
color={tw.color("blue-500")}
/>
) : (
<Ionicons
name="add-circle"
size={24}
color={tw.color("blue-500")}
/>
)}
</View>
</TouchableOpacity>
))
)}
</ScrollView>
</SafeAreaView>
);
}
Phase 2C: Testing & Migrationβ
1. Template Seeding Script (/scripts/seed-templates.js)β
const mongoose = require("mongoose");
const { seedTaskTemplates } = require("../seeds/taskTemplates");
require("dotenv").config();
const seedDatabase = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI);
console.log("Connected to MongoDB");
await seedTaskTemplates();
console.log("Template seeding completed");
await mongoose.disconnect();
console.log("Disconnected from MongoDB");
} catch (error) {
console.error("Seeding failed:", error);
process.exit(1);
}
};
seedDatabase();
2. Integration Testing Script (/scripts/test-automation.js)β
const Leg = require("../src/models/Leg");
const TaskRuleEngine = require("../src/services/taskRuleEngine");
const testAutomation = async () => {
// Create test leg
const testLeg = await Leg.create({
loadNumber: "TEST-AUTO-001",
status: "assigned",
loadType: "hazmat",
equipment: { type: "flatbed" },
rate: 15000,
miles: 500,
});
console.log("Created test leg:", testLeg.loadNumber);
// Test status change automation
const assignedTasks = await TaskRuleEngine.evaluateStatusChange(
testLeg,
"assigned",
"en_route"
);
console.log(`Assigned ${assignedTasks.length} tasks automatically`);
// Verify tasks were added
const updatedLeg = await Leg.findById(testLeg._id);
console.log(`Leg now has ${updatedLeg.tasks.length} tasks`);
return updatedLeg;
};
Phase 3: Advanced Featuresβ
Target: Add conditional logic, time-based triggers, analytics, and notifications
1. Enhanced Rule Engine (/src/services/advancedRuleEngine.js)β
const TaskRuleEngine = require("./taskRuleEngine");
const NotificationService = require("./notificationService");
class AdvancedRuleEngine extends TaskRuleEngine {
// Time-based task assignment
static async evaluateTimeBasedTriggers() {
try {
const now = new Date();
// Find legs with pending time-based triggers
const legsWithTimeTriggers = await Leg.aggregate([
{
$lookup: {
from: "tasktemplates",
let: { legId: "$_id" },
pipeline: [
{
$match: {
"assignmentRules.timing": { $exists: true },
isActive: true,
},
},
],
as: "availableTemplates",
},
},
{
$match: {
"availableTemplates.0": { $exists: true },
},
},
]);
const assignedTasks = [];
for (const leg of legsWithTimeTriggers) {
for (const template of leg.availableTemplates) {
const shouldTrigger = await this.evaluateTimeTrigger(
leg,
template,
now
);
if (shouldTrigger) {
const taskInstance = template.createTaskInstance({
assignedTo: leg.assignedDriver || leg.assignedTo,
triggeredBy: "time_based",
});
await Leg.findByIdAndUpdate(leg._id, {
$push: { tasks: taskInstance },
});
assignedTasks.push({ legId: leg._id, task: taskInstance });
}
}
}
return assignedTasks;
} catch (error) {
console.error("Time-based trigger evaluation failed:", error);
return [];
}
}
// Evaluate time-based trigger conditions
static async evaluateTimeTrigger(leg, template, currentTime) {
const timing = template.assignmentRules.timing;
if (!timing || !timing.relativeToStatus) return false;
// Check if leg is in the target status
if (leg.status !== timing.relativeToStatus) return false;
// Calculate trigger time
const statusUpdateTime =
leg.statusHistory?.find((h) => h.status === timing.relativeToStatus)
?.timestamp || leg.updatedAt;
const triggerTime = new Date(statusUpdateTime);
triggerTime.setHours(triggerTime.getHours() + (timing.offsetHours || 0));
// Check if it's time to trigger
return currentTime >= triggerTime;
}
// Conditional task assignment with complex logic
static async evaluateConditionalRules(leg, event) {
try {
const conditionalTemplates = await TaskTemplate.find({
"assignmentRules.conditionalLogic": { $exists: true },
isActive: true,
});
const assignedTasks = [];
for (const template of conditionalTemplates) {
const shouldAssign = await this.evaluateConditionalLogic(
leg,
template.assignmentRules.conditionalLogic,
event
);
if (shouldAssign) {
const taskInstance = template.createTaskInstance({
assignedTo: leg.assignedDriver || leg.assignedTo,
triggeredBy: "conditional_logic",
});
leg.tasks.push(taskInstance);
assignedTasks.push(taskInstance);
}
}
if (assignedTasks.length > 0) {
leg.updateTaskProgress();
await leg.save();
}
return assignedTasks;
} catch (error) {
console.error("Conditional rule evaluation failed:", error);
return [];
}
}
// Evaluate complex conditional logic
static async evaluateConditionalLogic(leg, logic, event) {
if (!logic) return false;
switch (logic.type) {
case "AND":
return logic.conditions.every((condition) =>
this.evaluateSingleCondition(leg, condition, event)
);
case "OR":
return logic.conditions.some((condition) =>
this.evaluateSingleCondition(leg, condition, event)
);
case "NOT":
return !this.evaluateSingleCondition(leg, logic.condition, event);
default:
return this.evaluateSingleCondition(leg, logic, event);
}
}
// Evaluate single condition
static evaluateSingleCondition(leg, condition, event) {
switch (condition.field) {
case "loadValue":
return this.evaluateNumericCondition(leg.rate, condition);
case "distance":
return this.evaluateNumericCondition(leg.miles, condition);
case "equipmentAge":
const equipmentAge = leg.equipment?.year
? new Date().getFullYear() - leg.equipment.year
: 0;
return this.evaluateNumericCondition(equipmentAge, condition);
case "driverExperience":
// Would need to lookup driver data
return true; // Placeholder
case "weatherConditions":
// Would integrate with weather API
return true; // Placeholder
case "timeOfDay":
const hour = new Date().getHours();
return this.evaluateNumericCondition(hour, condition);
default:
return false;
}
}
// Helper for numeric conditions
static evaluateNumericCondition(value, condition) {
if (condition.operator === "gt") return value > condition.value;
if (condition.operator === "lt") return value < condition.value;
if (condition.operator === "eq") return value === condition.value;
if (condition.operator === "gte") return value >= condition.value;
if (condition.operator === "lte") return value <= condition.value;
if (condition.operator === "between") {
return value >= condition.min && value <= condition.max;
}
return false;
}
}
module.exports = AdvancedRuleEngine;
2. Notification Service (/src/services/notificationService.js)β
const PushNotification = require("./pushNotificationService");
const EmailService = require("./emailService");
const SMSService = require("./smsService");
class NotificationService {
// Send task assignment notification
static async notifyTaskAssigned(task, leg, user) {
try {
const notification = {
title: "New Task Assigned",
body: `${task.name} has been assigned to load ${leg.loadNumber}`,
data: {
type: "task_assigned",
taskId: task.id,
legId: leg._id,
loadNumber: leg.loadNumber,
},
};
// Send push notification
if (user.pushNotificationToken) {
await PushNotification.send(user.pushNotificationToken, notification);
}
// Send email if enabled
if (user.notificationPreferences?.email?.taskAssigned) {
await EmailService.sendTaskAssignedEmail(user.email, task, leg);
}
// Send SMS if enabled and urgent
if (
task.priority === "URGENT" &&
user.phone &&
user.notificationPreferences?.sms?.urgent
) {
await SMSService.sendTaskUrgentSMS(user.phone, task, leg);
}
} catch (error) {
console.error("Failed to send task assignment notification:", error);
}
}
// Send task overdue notification
static async notifyTaskOverdue(task, leg, user) {
try {
const notification = {
title: "Task Overdue",
body: `${task.name} is overdue for load ${leg.loadNumber}`,
data: {
type: "task_overdue",
taskId: task.id,
legId: leg._id,
loadNumber: leg.loadNumber,
},
};
await PushNotification.send(user.pushNotificationToken, notification);
// Also notify dispatcher/supervisor
const supervisors = await User.find({
organizationId: user.organizationId,
role: { $in: ["dispatcher", "supervisor", "admin"] },
});
for (const supervisor of supervisors) {
if (supervisor.pushNotificationToken) {
await PushNotification.send(supervisor.pushNotificationToken, {
...notification,
body: `${task.name} is overdue for ${user.name} on load ${leg.loadNumber}`,
});
}
}
} catch (error) {
console.error("Failed to send overdue notification:", error);
}
}
// Send task reminder
static async sendTaskReminder(task, leg, user, reminderType) {
try {
const timeText =
reminderType === "1_hour"
? "1 hour"
: reminderType === "4_hours"
? "4 hours"
: "24 hours";
const notification = {
title: "Task Reminder",
body: `${task.name} is due in ${timeText} for load ${leg.loadNumber}`,
data: {
type: "task_reminder",
taskId: task.id,
legId: leg._id,
reminderType,
},
};
await PushNotification.send(user.pushNotificationToken, notification);
} catch (error) {
console.error("Failed to send task reminder:", error);
}
}
// Batch process reminder notifications
static async processScheduledReminders() {
try {
const now = new Date();
// Find tasks with upcoming deadlines
const legsWithTasks = await Leg.find({
"tasks.dueDate": { $exists: true },
"tasks.status": { $in: ["PENDING", "IN_PROGRESS"] },
}).populate("assignedDriver assignedTo");
for (const leg of legsWithTasks) {
for (const task of leg.tasks) {
if (!task.dueDate || task.status === "COMPLETED") continue;
const dueDate = new Date(task.dueDate);
const timeDiff = dueDate.getTime() - now.getTime();
const hoursUntilDue = timeDiff / (1000 * 60 * 60);
const user = leg.assignedDriver || leg.assignedTo;
if (!user) continue;
// Send reminders at 24h, 4h, 1h before due
if (Math.abs(hoursUntilDue - 24) < 0.5) {
await this.sendTaskReminder(task, leg, user, "24_hours");
} else if (Math.abs(hoursUntilDue - 4) < 0.5) {
await this.sendTaskReminder(task, leg, user, "4_hours");
} else if (Math.abs(hoursUntilDue - 1) < 0.5) {
await this.sendTaskReminder(task, leg, user, "1_hour");
} else if (hoursUntilDue < 0) {
// Task is overdue
await this.notifyTaskOverdue(task, leg, user);
}
}
}
} catch (error) {
console.error("Failed to process scheduled reminders:", error);
}
}
}
module.exports = NotificationService;
3. Analytics Service (/src/services/taskAnalytics.js)β
class TaskAnalytics {
// Generate task completion analytics
static async generateCompletionReport(organizationId, dateRange) {
try {
const { startDate, endDate } = dateRange;
const pipeline = [
{
$match: {
organizationId,
"tasks.completedAt": {
$gte: startDate,
$lte: endDate,
},
},
},
{
$unwind: "$tasks",
},
{
$match: {
"tasks.completedAt": {
$gte: startDate,
$lte: endDate,
},
},
},
{
$group: {
_id: {
category: "$tasks.category",
status: "$tasks.status",
},
count: { $sum: 1 },
avgDuration: { $avg: "$tasks.actualDuration" },
totalTasks: { $sum: 1 },
},
},
];
const results = await Leg.aggregate(pipeline);
return this.formatCompletionReport(results);
} catch (error) {
console.error("Failed to generate completion report:", error);
return null;
}
}
// Generate overdue task analytics
static async generateOverdueReport(organizationId) {
try {
const now = new Date();
const pipeline = [
{
$match: {
organizationId,
"tasks.dueDate": { $lt: now },
"tasks.status": { $in: ["PENDING", "IN_PROGRESS"] },
},
},
{
$unwind: "$tasks",
},
{
$match: {
"tasks.dueDate": { $lt: now },
"tasks.status": { $in: ["PENDING", "IN_PROGRESS"] },
},
},
{
$group: {
_id: {
category: "$tasks.category",
assignedTo: "$tasks.assignedTo",
},
count: { $sum: 1 },
avgOverdueDays: {
$avg: {
$divide: [
{ $subtract: [now, "$tasks.dueDate"] },
1000 * 60 * 60 * 24,
],
},
},
},
},
];
const results = await Leg.aggregate(pipeline);
return this.formatOverdueReport(results);
} catch (error) {
console.error("Failed to generate overdue report:", error);
return null;
}
}
// Generate driver performance analytics
static async generateDriverPerformanceReport(organizationId, dateRange) {
try {
const { startDate, endDate } = dateRange;
const pipeline = [
{
$match: {
organizationId,
"tasks.completedAt": {
$gte: startDate,
$lte: endDate,
},
},
},
{
$unwind: "$tasks",
},
{
$match: {
"tasks.completedAt": {
$gte: startDate,
$lte: endDate,
},
},
},
{
$group: {
_id: "$tasks.assignedTo",
totalTasks: { $sum: 1 },
completedTasks: {
$sum: {
$cond: [{ $eq: ["$tasks.status", "COMPLETED"] }, 1, 0],
},
},
avgCompletionTime: { $avg: "$tasks.actualDuration" },
overdueCount: {
$sum: {
$cond: [
{
$and: [
{ $ne: ["$tasks.dueDate", null] },
{ $gt: ["$tasks.completedAt", "$tasks.dueDate"] },
],
},
1,
0,
],
},
},
},
},
{
$lookup: {
from: "users",
localField: "_id",
foreignField: "_id",
as: "user",
},
},
];
const results = await Leg.aggregate(pipeline);
return this.formatDriverPerformanceReport(results);
} catch (error) {
console.error("Failed to generate driver performance report:", error);
return null;
}
}
// Format reports
static formatCompletionReport(results) {
const summary = {
totalTasks: 0,
completedTasks: 0,
byCategory: {},
completionRate: 0,
avgDuration: 0,
};
results.forEach((item) => {
const category = item._id.category;
const status = item._id.status;
if (!summary.byCategory[category]) {
summary.byCategory[category] = {
total: 0,
completed: 0,
failed: 0,
skipped: 0,
avgDuration: 0,
};
}
summary.totalTasks += item.count;
summary.byCategory[category].total += item.count;
if (status === "COMPLETED") {
summary.completedTasks += item.count;
summary.byCategory[category].completed += item.count;
} else if (status === "FAILED") {
summary.byCategory[category].failed += item.count;
} else if (status === "SKIPPED") {
summary.byCategory[category].skipped += item.count;
}
if (item.avgDuration) {
summary.byCategory[category].avgDuration = item.avgDuration;
}
});
summary.completionRate =
summary.totalTasks > 0
? (summary.completedTasks / summary.totalTasks) * 100
: 0;
return summary;
}
static formatOverdueReport(results) {
return {
totalOverdue: results.reduce((sum, item) => sum + item.count, 0),
byCategory: results.reduce((acc, item) => {
const category = item._id.category;
if (!acc[category]) {
acc[category] = { count: 0, avgOverdueDays: 0 };
}
acc[category].count += item.count;
acc[category].avgOverdueDays = item.avgOverdueDays;
return acc;
}, {}),
byDriver: results.reduce((acc, item) => {
const driverId = item._id.assignedTo;
if (!acc[driverId]) {
acc[driverId] = { count: 0, avgOverdueDays: 0 };
}
acc[driverId].count += item.count;
acc[driverId].avgOverdueDays = item.avgOverdueDays;
return acc;
}, {}),
};
}
static formatDriverPerformanceReport(results) {
return results.map((item) => ({
driverId: item._id,
driverName: item.user[0]?.name || "Unknown",
totalTasks: item.totalTasks,
completedTasks: item.completedTasks,
completionRate: (item.completedTasks / item.totalTasks) * 100,
avgCompletionTime: item.avgCompletionTime,
overdueCount: item.overdueCount,
overdueRate: (item.overdueCount / item.totalTasks) * 100,
}));
}
}
module.exports = TaskAnalytics;
4. Enhanced Task Templates with Advanced Rulesβ
// Add to existing TaskTemplate schema in Task.js
const advancedTaskTemplateSchema = new Schema({
// ... existing fields ...
// Advanced assignment rules
assignmentRules: {
triggers: [String],
conditions: Schema.Types.Mixed,
timing: {
relativeToStatus: String,
offsetHours: Number,
recurringInterval: String, // "daily", "weekly", "monthly"
recurringUntil: Date,
},
conditionalLogic: {
type: { type: String, enum: ["AND", "OR", "NOT", "SIMPLE"] },
conditions: [Schema.Types.Mixed],
condition: Schema.Types.Mixed, // for NOT type
},
},
// Analytics tracking
analytics: {
totalAssignments: { type: Number, default: 0 },
completionRate: { type: Number, default: 0 },
avgCompletionTime: { type: Number, default: 0 },
lastUsed: Date,
},
// Notification settings
notifications: {
onAssignment: { type: Boolean, default: true },
reminders: [
{
beforeDeadlineHours: Number,
message: String,
channels: [{ type: String, enum: ["push", "email", "sms"] }],
},
],
escalation: {
enabled: { type: Boolean, default: false },
afterHours: Number,
escalateTo: [{ type: Schema.Types.ObjectId, ref: "User" }],
},
},
});
Phase 4: AI Enhancementβ
Target: Add predictive features, completion estimation, optimization, and smart recommendations
1. AI Prediction Service (/src/services/aiPredictionService.js)β
const tf = require("@tensorflow/tfjs-node");
const TaskAnalytics = require("./taskAnalytics");
class AIPredictionService {
// Predict task completion time
static async predictCompletionTime(task, leg, driver) {
try {
// Gather historical data
const historicalData = await this.getHistoricalTaskData(
task.category,
driver._id,
leg.organizationId
);
if (historicalData.length < 10) {
// Not enough data, return template estimate
return task.estimatedDuration || 30;
}
// Feature engineering
const features = this.extractFeatures(task, leg, driver, historicalData);
// Load or create prediction model
const model = await this.getCompletionTimeModel();
// Make prediction
const prediction = model.predict(tf.tensor2d([features]));
const estimatedMinutes = await prediction.data();
return Math.round(estimatedMinutes[0]);
} catch (error) {
console.error("Failed to predict completion time:", error);
return task.estimatedDuration || 30;
}
}
// Predict task assignment recommendations
static async recommendTaskAssignments(leg) {
try {
// Get available templates
const availableTemplates = await TaskTemplate.find({
$or: [{ organizationId: leg.organizationId }, { organizationId: null }],
isActive: true,
});
const recommendations = [];
for (const template of availableTemplates) {
const relevanceScore = await this.calculateRelevanceScore(
leg,
template
);
const urgencyScore = await this.calculateUrgencyScore(leg, template);
const successProbability = await this.predictSuccessProbability(
leg,
template
);
if (relevanceScore > 0.6) {
recommendations.push({
template,
relevanceScore,
urgencyScore,
successProbability,
combinedScore:
(relevanceScore + urgencyScore + successProbability) / 3,
reasoning: this.generateRecommendationReasoning(leg, template, {
relevanceScore,
urgencyScore,
successProbability,
}),
});
}
}
// Sort by combined score
return recommendations.sort((a, b) => b.combinedScore - a.combinedScore);
} catch (error) {
console.error("Failed to generate task recommendations:", error);
return [];
}
}
// Predict optimal task sequencing
static async optimizeTaskSequence(tasks, leg, driver) {
try {
if (tasks.length <= 1) return tasks;
// Calculate dependency matrix
const dependencies = this.calculateTaskDependencies(tasks);
// Calculate efficiency scores for different sequences
const possibleSequences = this.generatePossibleSequences(
tasks,
dependencies
);
let bestSequence = tasks;
let bestScore = 0;
for (const sequence of possibleSequences) {
const score = await this.calculateSequenceScore(sequence, leg, driver);
if (score > bestScore) {
bestScore = score;
bestSequence = sequence;
}
}
return bestSequence;
} catch (error) {
console.error("Failed to optimize task sequence:", error);
return tasks;
}
}
// Smart reminder timing
static async optimizeReminderTiming(task, driver) {
try {
// Analyze driver's historical response patterns
const responsePatterns = await this.getDriverResponsePatterns(driver._id);
// Predict optimal reminder times based on:
// - Driver's typical work schedule
// - Historical response times to reminders
// - Task urgency and complexity
// - Current workload
const optimalReminders = [];
if (task.dueDate) {
const dueDate = new Date(task.dueDate);
const hoursUntilDue =
(dueDate.getTime() - Date.now()) / (1000 * 60 * 60);
// Predict best reminder times
if (hoursUntilDue > 48) {
optimalReminders.push({
hours: 24,
probability: 0.85,
message: "Upcoming task reminder",
});
}
if (hoursUntilDue > 8) {
optimalReminders.push({
hours: 4,
probability: 0.92,
message: "Task due soon",
});
}
optimalReminders.push({
hours: 1,
probability: 0.95,
message: "Final reminder - task due in 1 hour",
});
}
return optimalReminders;
} catch (error) {
console.error("Failed to optimize reminder timing:", error);
return [{ hours: 4, probability: 0.8, message: "Task reminder" }];
}
}
// Feature extraction for ML models
static extractFeatures(task, leg, driver, historicalData) {
return [
// Task features
task.steps?.length || 0,
task.priority === "HIGH" ? 1 : 0,
task.priority === "URGENT" ? 1 : 0,
task.documentsRequired ? 1 : 0,
// Leg features
leg.miles || 0,
leg.rate || 0,
leg.loadType === "hazmat" ? 1 : 0,
// Driver features
driver.experience || 0,
this.calculateDriverEfficiency(driver._id, historicalData),
// Time features
new Date().getHours(), // Hour of day
new Date().getDay(), // Day of week
// Historical averages
this.calculateAvgCompletionTime(historicalData),
historicalData.length, // Sample size
];
}
// Calculate task relevance score
static async calculateRelevanceScore(leg, template) {
let score = 0.5; // Base score
// Check load type match
if (template.assignmentRules.conditions?.loadType) {
if (template.assignmentRules.conditions.loadType.includes(leg.loadType)) {
score += 0.3;
}
}
// Check equipment type match
if (template.assignmentRules.conditions?.equipmentType) {
if (
template.assignmentRules.conditions.equipmentType.includes(
leg.equipment?.type
)
) {
score += 0.2;
}
}
// Check industry match
if (template.industry === leg.organization?.industry) {
score += 0.2;
}
// Check historical usage
const usageHistory = await this.getTemplateUsageHistory(
template._id,
leg.organizationId
);
if (usageHistory.successRate > 0.8) {
score += 0.1;
}
return Math.min(score, 1.0);
}
// Helper methods
static async getHistoricalTaskData(category, driverId, organizationId) {
return await Leg.aggregate([
{
$match: {
organizationId,
"tasks.category": category,
"tasks.assignedTo": driverId,
"tasks.status": "COMPLETED",
},
},
{ $unwind: "$tasks" },
{
$match: {
"tasks.category": category,
"tasks.assignedTo": driverId,
"tasks.status": "COMPLETED",
},
},
{
$project: {
duration: "$tasks.actualDuration",
estimatedDuration: "$tasks.estimatedDuration",
completedOnTime: {
$cond: [
{
$and: [
{ $ne: ["$tasks.dueDate", null] },
{ $lte: ["$tasks.completedAt", "$tasks.dueDate"] },
],
},
1,
0,
],
},
},
},
]);
}
static calculateDriverEfficiency(driverId, historicalData) {
if (historicalData.length === 0) return 0.5;
const onTimeCount = historicalData.filter((d) => d.completedOnTime).length;
return onTimeCount / historicalData.length;
}
static calculateAvgCompletionTime(historicalData) {
if (historicalData.length === 0) return 30;
const total = historicalData.reduce((sum, d) => sum + (d.duration || 0), 0);
return total / historicalData.length;
}
static generateRecommendationReasoning(leg, template, scores) {
const reasons = [];
if (scores.relevanceScore > 0.8) {
reasons.push(`Highly relevant for ${leg.loadType} loads`);
}
if (scores.urgencyScore > 0.8) {
reasons.push("Time-sensitive based on delivery schedule");
}
if (scores.successProbability > 0.8) {
reasons.push("High success rate with similar loads");
}
return reasons.join("; ");
}
}
module.exports = AIPredictionService;
2. Performance Optimization Service (/src/services/performanceOptimizer.js)β
class PerformanceOptimizer {
// Optimize database queries for task operations
static async optimizeTaskQueries() {
try {
// Ensure proper indexes exist
await this.ensureTaskIndexes();
// Cache frequently accessed templates
await this.cachePopularTemplates();
// Optimize aggregation pipelines
await this.optimizeAggregationPipelines();
} catch (error) {
console.error("Failed to optimize task queries:", error);
}
}
// Smart caching for task templates
static async cachePopularTemplates() {
const popularTemplates = await TaskTemplate.find({
"analytics.totalAssignments": { $gte: 100 },
isActive: true,
})
.sort({ "analytics.totalAssignments": -1 })
.limit(20);
// Cache in Redis or memory
for (const template of popularTemplates) {
await this.cacheTemplate(template);
}
}
// Batch processing for notifications
static async optimizeBatchNotifications() {
// Group notifications by user to reduce API calls
const pendingNotifications = await this.getPendingNotifications();
const groupedByUser = this.groupNotificationsByUser(pendingNotifications);
for (const [userId, notifications] of Object.entries(groupedByUser)) {
await this.sendBatchNotifications(userId, notifications);
}
}
// Predictive pre-loading of task data
static async preloadTaskData(legId) {
try {
// Predict which tasks might be assigned next
const leg = await Leg.findById(legId);
const predictions = await AIPredictionService.recommendTaskAssignments(
leg
);
// Pre-load template data for top predictions
const topTemplates = predictions.slice(0, 5).map((p) => p.template);
for (const template of topTemplates) {
await this.preloadTemplateData(template);
}
} catch (error) {
console.error("Failed to preload task data:", error);
}
}
}
module.exports = PerformanceOptimizer;
3. AI Analytics Dashboard API (/src/controllers/aiAnalytics/index.js)β
const AIPredictionService = require("../../services/aiPredictionService");
const TaskAnalytics = require("../../services/taskAnalytics");
const aiAnalyticsController = {
// Get AI-powered insights
getInsights: async (req, res) => {
try {
const { organizationId } = req.user;
const { timeframe = "30d" } = req.query;
const dateRange = getDateRange(timeframe);
// Generate various insights
const insights = await Promise.all([
TaskAnalytics.generateCompletionReport(organizationId, dateRange),
TaskAnalytics.generateOverdueReport(organizationId),
TaskAnalytics.generateDriverPerformanceReport(
organizationId,
dateRange
),
generateEfficiencyInsights(organizationId, dateRange),
generatePredictiveInsights(organizationId),
]);
res.json({
data: {
completion: insights[0],
overdue: insights[1],
driverPerformance: insights[2],
efficiency: insights[3],
predictive: insights[4],
},
});
} catch (error) {
res.status(500).json({ error: error.message });
}
},
// Get task recommendations for a leg
getTaskRecommendations: async (req, res) => {
try {
const { id } = req.params;
const leg = await Leg.findById(id);
if (!leg) {
return res.status(404).json({ message: "Leg not found" });
}
const recommendations =
await AIPredictionService.recommendTaskAssignments(leg);
res.json({ data: recommendations });
} catch (error) {
res.status(500).json({ error: error.message });
}
},
// Get completion time predictions
getCompletionPredictions: async (req, res) => {
try {
const { legId, taskIds } = req.body;
const leg = await Leg.findById(legId).populate("assignedDriver");
const predictions = [];
for (const taskId of taskIds) {
const task = leg.tasks.id(taskId);
if (task) {
const prediction = await AIPredictionService.predictCompletionTime(
task,
leg,
leg.assignedDriver
);
predictions.push({
taskId,
estimatedMinutes: prediction,
confidence: 0.85, // Would come from model
});
}
}
res.json({ data: predictions });
} catch (error) {
res.status(500).json({ error: error.message });
}
},
};
// Helper functions
function getDateRange(timeframe) {
const now = new Date();
const days = timeframe === "7d" ? 7 : timeframe === "30d" ? 30 : 90;
return {
startDate: new Date(now.getTime() - days * 24 * 60 * 60 * 1000),
endDate: now,
};
}
async function generateEfficiencyInsights(organizationId, dateRange) {
// Calculate efficiency metrics
const totalTasks = await countTotalTasks(organizationId, dateRange);
const automatedTasks = await countAutomatedTasks(organizationId, dateRange);
const manualTasks = totalTasks - automatedTasks;
return {
automationRate: totalTasks > 0 ? (automatedTasks / totalTasks) * 100 : 0,
manualTasks,
automatedTasks,
recommendations: [
{
type: "automation",
message: "Consider creating templates for recurring manual tasks",
impact: "Could save 2-3 hours per week",
},
],
};
}
async function generatePredictiveInsights(organizationId) {
return {
upcomingBottlenecks: [
{
area: "safety_inspections",
predictedDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
severity: "medium",
recommendation: "Schedule additional safety personnel",
},
],
capacityForecast: {
nextWeek: "85% capacity",
nextMonth: "92% capacity",
recommendations: ["Consider hiring additional drivers"],
},
};
}
module.exports = aiAnalyticsController;
Next Stepsβ
- Phase 1: β COMPLETED - Basic task management foundation
- Phase 2: π DOCUMENTED - Templates and automation
- Phase 3: π DOCUMENTED - Advanced features and analytics
- Phase 4: π DOCUMENTED - AI enhancement and optimization
- Implementation: Ready to begin Phase 2A implementation
- Stakeholder Review: Present complete roadmap for approval
This comprehensive plan now covers the entire task management system evolution from basic functionality through advanced AI-powered features, providing a clear path for implementation over multiple development cycles.