Skip to main content

Lesson Plans

Lesson Plans in Web/Admin cover planning therapy sessions, setting goals/activities, tracking progress, and generating sharable PDFs. This system manages therapy session planning with critical business logic for goal tracking, activity sequencing, and progress measurement.

Core Concepts

  • Goal-Based Planning: SMART goals with measurable outcomes and timelines
  • Activity Sequencing: Ordered activities with prerequisites and dependencies
  • Progress Tracking: Session notes, goal completion percentages, and milestone tracking
  • Status Flow: DRAFT → ACTIVE → COMPLETED → ARCHIVED
  • Therapist Assignment: Role-based access with specialization matching
  • Child-Specific Customization: Age-appropriate activities and difficulty scaling

Web/Admin Capabilities

1) List & Filter

Business Rules:

  • Filter by therapist specialization, child age group, and plan status
  • Show only plans accessible to current user's role and permissions
  • Display progress indicators and completion percentages
  • Sort by creation date, last activity, or completion status

Implementation:

// Filter logic with role-based access and progress calculation
const filtered = rows.filter((plan) => {
// Role-based access control
const hasAccess =
userRole === 'ADMIN' ||
(userRole === 'THERAPIST' && plan.therapist_id === currentUserId) ||
(userRole === 'PARENT' && plan.child_id === currentUserChildId);

// Status and therapist filtering
const matchesStatus = !statusFilter || plan.status === statusFilter;
const matchesTherapist =
!therapistFilter || plan.therapist_id === therapistFilter;

// Progress calculation for display
const progressPercentage = calculateProgress(plan.goals, plan.activities);
plan.progress = progressPercentage;

return hasAccess && matchesStatus && matchesTherapist;
});

// Progress calculation formula
const calculateProgress = (goals, activities) => {
const completedGoals = goals.filter(
(goal) => goal.status === 'COMPLETED',
).length;
const completedActivities = activities.filter(
(activity) => activity.is_completed,
).length;
const totalItems = goals.length + activities.length;

return totalItems > 0
? Math.round(((completedGoals + completedActivities) / totalItems) * 100)
: 0;
};

2) Create Lesson Plan (Admin/Therapist Only)

Critical Business Rules:

  • Child-Therapist Matching: Therapist must have specialization matching child's needs
  • Age Appropriateness: Activities must be suitable for child's developmental stage
  • Goal Validation: SMART goals with measurable outcomes and realistic timelines
  • Activity Sequencing: Activities must have proper prerequisites and dependencies
  • Session Duration: Total estimated time must not exceed child's attention span

Validation Formula:

// Child-therapist compatibility check
const validateTherapistMatch = (child, therapist) => {
const childNeeds = child.special_needs || [];
const therapistSpecializations = therapist.specializations || [];

return childNeeds.every((need) =>
therapistSpecializations.some((spec) => spec.includes(need)),
);
};

// Age-appropriate activity validation
const validateActivityAge = (activity, childAge) => {
const ageInMonths = childAge * 12;
return ageInMonths >= activity.min_age && ageInMonths <= activity.max_age;
};

// Session duration validation
const validateSessionDuration = (activities, childAge) => {
const totalDuration = activities.reduce(
(sum, activity) => sum + activity.estimated_duration,
0,
);
const maxDuration = childAge <= 3 ? 30 : childAge <= 6 ? 45 : 60; // minutes

return totalDuration <= maxDuration;
};

GraphQL Implementation:

import { gql, ApolloClient } from '@apollo/client';

const CREATE_PLAN = gql`
mutation CreatePlan($input: CreateLessonPlanInput!) {
createLessonPlan(input: $input) {
success
message
data {
id
status
estimated_duration
goals_count
activities_count
}
}
}
`;

async function createPlan(
client: ApolloClient<unknown>,
input: CreateLessonPlanInput,
) {
// Pre-validate business rules
const child = await getChildDetails(input.child_id);
const therapist = await getTherapistDetails(input.therapist_id);

if (!validateTherapistMatch(child, therapist)) {
throw new Error('Therapist specialization does not match child needs');
}

if (!validateSessionDuration(input.activities, child.age)) {
throw new Error("Session duration exceeds child's attention span");
}

// Validate each activity
for (const activity of input.activities) {
if (!validateActivityAge(activity, child.age)) {
throw new Error(
`Activity "${activity.title}" not suitable for child's age`,
);
}
}

const { data } = await client.mutate({
mutation: CREATE_PLAN,
variables: { input },
});

if (!data?.createLessonPlan?.success) {
throw new Error(data?.createLessonPlan?.message || 'Create failed');
}
return data.createLessonPlan.data;
}

3) Activity Management & Sequencing

Activity Business Rules:

  • Prerequisite Validation: Activities must have completed prerequisites before starting
  • Difficulty Progression: Activities should follow logical difficulty progression
  • Resource Requirements: Materials and equipment must be available
  • Time Estimation: Realistic duration estimates based on child's abilities
  • Adaptive Difficulty: Activities can be scaled up/down based on child's progress

Activity Sequencing Logic:

// Prerequisite validation
const validatePrerequisites = (activity, completedActivities) => {
if (!activity.prerequisites || activity.prerequisites.length === 0) {
return true; // No prerequisites
}

return activity.prerequisites.every((prereq) =>
completedActivities.some((completed) => completed.id === prereq.id),
);
};

// Difficulty progression validation
const validateDifficultyProgression = (newActivity, existingActivities) => {
const maxDifficulty = Math.max(
...existingActivities.map((a) => a.difficulty_level || 0),
);
const minDifficulty = Math.min(
...existingActivities.map((a) => a.difficulty_level || 0),
);

// New activity should be within reasonable range of existing activities
return (
newActivity.difficulty_level >= minDifficulty - 1 &&
newActivity.difficulty_level <= maxDifficulty + 1
);
};

// Resource availability check
const validateResourceAvailability = (activity, availableResources) => {
const requiredResources = activity.required_materials || [];
return requiredResources.every((resource) =>
availableResources.some((available) => available.id === resource.id),
);
};

GraphQL Implementation:

const UPSERT_ACTIVITY = gql`
mutation UpsertActivity($input: UpsertActivityInput!) {
upsertActivity(input: $input) {
success
message
data {
id
title
difficulty_level
estimated_duration
prerequisites_met
resource_availability
}
}
}
`;

async function upsertActivity(
client: ApolloClient<unknown>,
planId: string,
activity: UpsertActivityInput,
) {
// Get current plan state for validation
const plan = await getLessonPlanDetails(planId);
const completedActivities = plan.activities.filter((a) => a.is_completed);
const availableResources = await getAvailableResources();

// Validate prerequisites
if (!validatePrerequisites(activity, completedActivities)) {
throw new Error('Activity prerequisites not met');
}

// Validate difficulty progression
if (!validateDifficultyProgression(activity, plan.activities)) {
throw new Error('Activity difficulty does not follow logical progression');
}

// Validate resource availability
if (!validateResourceAvailability(activity, availableResources)) {
throw new Error('Required materials not available');
}

const { data } = await client.mutate({
mutation: UPSERT_ACTIVITY,
variables: { input: { plan_id: planId, ...activity } },
});

if (!data?.upsertActivity?.success) {
throw new Error(data?.upsertActivity?.message || 'Save failed');
}
return data.upsertActivity.data;
}

4) Progress Tracking & Session Notes

Progress Tracking Business Rules:

  • Goal Completion: Track progress toward SMART goals with measurable milestones
  • Activity Completion: Mark activities as completed with performance ratings
  • Session Notes: Detailed observations and recommendations for future sessions
  • Progress Analytics: Calculate completion rates and identify areas needing attention
  • Parent Communication: Generate progress reports for parent review

Progress Calculation Formula:

// Goal progress calculation
const calculateGoalProgress = (goal, completedActivities) => {
const relevantActivities = completedActivities.filter((activity) =>
goal.related_activities.includes(activity.id),
);

if (goal.measurement_type === 'COUNT') {
return Math.min((relevantActivities.length / goal.target_count) * 100, 100);
} else if (goal.measurement_type === 'PERCENTAGE') {
const avgPerformance =
relevantActivities.reduce(
(sum, activity) => sum + (activity.performance_rating || 0),
0,
) / relevantActivities.length;
return Math.min((avgPerformance / 5) * 100, 100); // Assuming 5-point scale
}
return 0;
};

// Overall plan progress
const calculatePlanProgress = (plan) => {
const goalProgress = plan.goals.map((goal) =>
calculateGoalProgress(goal, plan.completed_activities),
);
const activityProgress =
(plan.completed_activities.length / plan.total_activities) * 100;

return Math.round(
(goalProgress.reduce((sum, progress) => sum + progress, 0) +
activityProgress) /
(plan.goals.length + 1),
);
};

GraphQL Implementation:

const ADD_NOTE = gql`
mutation AddNote($input: AddLessonPlanNoteInput!) {
addLessonPlanNote(input: $input) {
success
message
data {
id
note
session_date
progress_indicators {
goal_progress
activity_completion
overall_progress
}
}
}
}
`;

async function addSessionNote(
client: ApolloClient<unknown>,
planId: string,
note: string,
sessionData: SessionData,
) {
// Calculate progress before adding note
const plan = await getLessonPlanDetails(planId);
const progressIndicators = {
goal_progress: calculateGoalProgress(
plan.current_goal,
plan.completed_activities,
),
activity_completion:
(plan.completed_activities.length / plan.total_activities) * 100,
overall_progress: calculatePlanProgress(plan),
};

const { data } = await client.mutate({
mutation: ADD_NOTE,
variables: {
input: {
plan_id: planId,
note,
session_date: sessionData.date,
activities_completed: sessionData.completed_activities,
performance_ratings: sessionData.performance_ratings,
},
},
});

if (!data?.addLessonPlanNote?.success) {
throw new Error(data?.addLessonPlanNote?.message || 'Add note failed');
}
return data.addLessonPlanNote.data;
}

5) Export & Reporting

Export Business Rules:

  • Progress Reports: Include goal progress, activity completion, and session notes
  • Parent Reports: Generate child-friendly progress summaries
  • Therapist Reports: Detailed analytics for therapy planning
  • Compliance Reports: Track session attendance and goal achievement

Export Implementation:

// Generate comprehensive lesson plan report
const generateLessonPlanReport = (plans: LessonPlan[]) => {
return plans.map((plan) => ({
id: plan.id,
child_name: plan.child.name,
therapist_name: plan.therapist.name,
start_date: plan.start_date,
current_progress: calculatePlanProgress(plan),
goals_summary: plan.goals.map((goal) => ({
title: goal.title,
progress: calculateGoalProgress(goal, plan.completed_activities),
status: goal.status,
})),
activities_summary: {
total: plan.total_activities,
completed: plan.completed_activities.length,
completion_rate:
(plan.completed_activities.length / plan.total_activities) * 100,
},
last_session_date: plan.last_session_date,
next_session_date: plan.next_session_date,
}));
};

const csv = toCsv(generateLessonPlanReport(rows));
downloadCsv('lesson-plans-progress-report.csv', csv);

Data Flow (Web ↔ Backend)

Critical Business Logic Flow:

  1. Creation: Admin/Therapist creates plan → Child-therapist matching → Age validation → Goal setting
  2. Activity Management: Add activities → Prerequisite validation → Resource check → Sequencing
  3. Progress Tracking: Session notes → Progress calculation → Goal updates → Parent communication
  4. Reporting: Generate reports → Export data → Analytics → Compliance tracking

GraphQL Operations with Business Rules:

ActionOperationBusiness Rules
Create PlancreateLessonPlan(input)Child-therapist matching, age appropriateness, session duration
Upsert ActivityupsertActivity(input)Prerequisite validation, difficulty progression, resource availability
Add NoteaddLessonPlanNote(input)Progress calculation, goal tracking, session analytics
List/FilterlessonPlans(filter)Role-based access, progress indicators, completion status
ExportexportLessonPlanReport(input)Progress analytics, compliance tracking, parent communication

Error Handling & Validation:

  • Therapist Mismatch: "Therapist specialization does not match child needs"
  • Age Inappropriate: "Activity not suitable for child's developmental stage"
  • Prerequisites Not Met: "Activity prerequisites not completed"
  • Resource Unavailable: "Required materials not available"
  • Session Too Long: "Session duration exceeds child's attention span"

Security & Access Control

  • Role-Based Access: Admins can access all plans, therapists only their assigned plans, parents only their child's plans
  • Data Privacy: Session notes and progress data protected by user permissions
  • Audit Trail: All plan modifications and progress updates are logged
  • Parent Communication: Controlled sharing of progress reports with parents
  • Compliance: Session tracking and goal achievement for regulatory requirements

Bulk Upload

Use the admin bulk uploader to create multiple lesson plans at once. A single template is supported.

Template

  • Required headers: title, description, min_age, max_age, language, section_name, assessors, subscription_plans, activity_1
  • Optional headers: activity_2activity_9

Validation

  • title, description required
  • min_age, max_age must be non-negative integers and max_age >= min_age
  • language must match reference languages
  • section_name must match a valid lesson type (enum derived from section_name)
  • assessors must map to known specialties; at least one
  • subscription_plans must map to known plans; at least one
  • At least one activity (activity_1) is required; up to 9 supported

General behavior

  • Header matching ignores spaces/special chars (case-insensitive)
  • Preview lists row number and validation errors; only valid rows are submitted
  • Backend payload includes resolved ids for language, assessors, subscription_plans, and structured activities

Implementation (TypeScript)

// Header normalization and mapping
const clean = (s: string) =>
s
.replace(/\s+/g, '')
.replace(/[^a-zA-Z0-9]/g, '')
.toLowerCase();

function validateAndMapHeaders(headers: string[]) {
const cleanedExpected = Headers.map((h) => ({
original: h,
cleaned: clean(h),
})); // from constants
const headerMap: Record<string, string> = {};
const unmapped: string[] = [];

headers.forEach((h) => {
const m = cleanedExpected.find((e) => e.cleaned === clean(h));
m ? (headerMap[h] = m.original) : unmapped.push(h);
});

const missingRequired = RequiredHeaders.filter(
(r) => !headers.map(clean).includes(clean(r)),
);
return {
isValid: missingRequired.length === 0,
missingRequired,
unmappedHeaders: unmapped,
headerMap,
};
}

// Row validation + transform
function validateAndTransformData(rows: CSVRow[], ref: ReferenceData) {
const display: DisplayLessonPlan[] = [];
const backend: BackendLessonPlan[] = [];

const toEnum = (s: string) =>
s
.toUpperCase()
.replace(/[^A-Z0-9]/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '');

rows.forEach((row, idx) => {
const errors: string[] = [];

if (!row.title?.trim()) errors.push('Title is required');
if (!row.description?.trim()) errors.push('Description is required');

const minAge = parseInt(row.min_age);
const maxAge = parseInt(row.max_age);
if (isNaN(minAge) || minAge < 0) errors.push('Min age must be >= 0');
if (isNaN(maxAge) || maxAge < 0) errors.push('Max age must be >= 0');
if (!isNaN(minAge) && !isNaN(maxAge) && maxAge < minAge)
errors.push('Max age must be >= Min age');

const language = ref.languages.find(
(l) => l.title.toLowerCase() === row.language?.toLowerCase(),
);
if (!language) errors.push('Invalid language');

const sectionEnum = toEnum(row.section_name || '');
const lessonType = ref.lessonTypes.find((lt) => lt.name === sectionEnum);
if (!row.section_name?.trim()) errors.push('Section name is required');
if (row.section_name?.trim() && !lessonType)
errors.push('Invalid section name');

const assessorTitles =
row.assessors
?.split(',')
.map((s) => s.trim())
.filter(Boolean) || [];
const assessorIds = assessorTitles
.map(
(t) =>
ref.specialities.find(
(s) => s.title.toLowerCase() === t.toLowerCase(),
)?.id,
)
.filter((id): id is string => !!id);
if (assessorIds.length !== assessorTitles.length)
errors.push('Invalid assessors');
if (assessorIds.length === 0) errors.push('At least one assessor required');

const subTitles =
row.subscription_plans
?.split(',')
.map((s) => s.trim())
.filter(Boolean) || [];
const subIds = subTitles
.map(
(t) =>
ref.subscriptionPlans.find(
(p) => p.name.toLowerCase() === t.toLowerCase(),
)?.id,
)
.filter((id): id is string => !!id);
if (subIds.length !== subTitles.length)
errors.push('Invalid subscription plans');
if (subIds.length === 0)
errors.push('At least one subscription plan required');

const activities: Activity[] = [];
let counter = 0;
for (let i = 1; i <= 9; i++) {
const v = row[`activity_${i}` as keyof CSVRow]?.trim();
if (v) {
counter++;
activities.push({ heading: `Activity ${counter}`, description: v });
}
}
if (activities.length === 0)
errors.push('At least one activity is required');

display.push({
title: row.title?.trim() || '',
description: row.description?.trim() || '',
min_age: minAge,
max_age: maxAge,
language_title: language?.title || row.language || '',
language_id: language?.id || '',
section_name: row.section_name?.trim() || '',
lesson_type: lessonType?.name || 'OTHERS',
assessor_titles: assessorTitles,
assessor_ids: assessorIds,
subscription_titles: subTitles,
subscription_ids: subIds,
activities: activities.map((a) => a.description),
row_number: idx + 2,
validation_errors: errors,
});

if (errors.length === 0) {
backend.push({
name: row.title!.trim(),
description: row.description!.trim(),
min_age: minAge,
max_age: maxAge,
language_id: language!.id,
lesson_type: lessonType!.name,
lesson_plan_specialties: assessorIds,
lesson_plan_subscriptions: subIds,
activities,
});
}
});

return { display, backend };
}