Skip to main content

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 useAuth and useCustomerConfig
  • βœ… 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​

  1. Unit Tests: Task model validation, controller methods
  2. Integration Tests: API endpoints with existing legs
  3. UI Tests: Task checklist component functionality
  4. 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​

  1. Phase 1: βœ… COMPLETED - Basic task management foundation
  2. Phase 2: πŸ“‹ DOCUMENTED - Templates and automation
  3. Phase 3: πŸ“‹ DOCUMENTED - Advanced features and analytics
  4. Phase 4: πŸ“‹ DOCUMENTED - AI enhancement and optimization
  5. Implementation: Ready to begin Phase 2A implementation
  6. 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.