📚 Parent Lesson Plans
This document covers the comprehensive lesson plan system for parents in the Com DEALL mobile application, including lesson plan taking, resuming, retaking, progress tracking, and activity management.
🎯 Overview
The parent lesson plan system enables parents to access and complete lesson plans for their children, track progress, resume incomplete lessons, and retake lessons as needed. The system supports multiple lesson plan types and provides detailed progress tracking.
🏗️ Architecture
Lesson Plan System Components
Parent Lesson Plan System
├── Lesson Plan Access
├── Activity Completion
├── Progress Tracking
├── Resume Functionality
├── Retake Management
└── Progress Reports
Data Flow
Lesson Plan Selection → Activity Navigation → Completion Tracking → Progress Save → Report Generation
↓ ↓ ↓ ↓ ↓
Parent Action → Activity Flow → Local Storage → Database Update → Progress Update
📱 Mobile Implementation
Parent Lesson Plan Business Logic
Lesson Plan All Tab Logic
Lesson Plan Display Logic:
- Shows lesson plans with
assessor=parentand child age as filters - Displays lesson plans irrespective of child's subscription plan
- Implements subscription-based locking mechanism
- Maintains specific ordering: Screening → Case History → CDDC (GM, FM, ADL, RL, EL, Cog, Soc, Emo) → PLS → ORO-Motor → Others
Subscription Locking Logic:
- Active Subscription: Unlocks lesson plans linked to child subscription
- No Subscription: Locks all lesson plans except free ones
- Unlocked First: Unlocked lesson plans shown at top, locked ones below
- Real-time Updates: Updates lock status when subscription changes
// ParentLessonPlanAll.tsx - Lesson plan All tab with subscription locking
const ParentLessonPlanAll = ({ navigation, route }: ScreenProps) => {
const { childId } = route.params;
const [filter, setFilter] = useState<LessonPlanFilter>({
assessor: 'parent',
age: childAge,
category: 'ALL'
});
// Get child subscription details for locking logic
const { data: childSubscriptionData } = useGetChildSubscriptionQuery({
variables: { child_id: childId },
fetchPolicy: 'cache-and-network'
});
// Get all lesson plans with parent assessor and age filtering
const { data: lessonPlansData, loading, error, refetch } = useGetParentLessonPlansQuery({
variables: {
child_id: childId,
assessor: 'parent',
child_age: childAge,
category: filter.category
},
fetchPolicy: 'cache-and-network'
});
// Lesson plan locking logic with comprehensive subscription validation
const lessonPlanAllData = useMemo(() => {
const lessonPlans = lessonPlansData?.lesson_plans || [];
const subscription = childSubscriptionData?.child_subscription;
const isSubscriptionActive = subscription?.status === 'ACTIVE';
// Process lesson plans with subscription-based locking logic
const processedLessonPlans = lessonPlans.map(item => {
let isLocked = true;
// Always unlock free lesson plans
if (item?.type === LessonPlanType.Free) {
isLocked = false;
}
// Unlock subscribed lesson plans if subscription is active
if (item?.subscribed && isSubscriptionActive) {
isLocked = false;
}
return { ...item, isLocked };
});
// Sort lesson plans: unlocked first, then locked, maintaining category order
const categoryOrder = [
'Screening',
'Case History',
'CDDC_GM',
'CDDC_FM',
'CDDC_ADL',
'CDDC_RL',
'CDDC_EL',
'CDDC_Cog',
'CDDC_Soc',
'CDDC_Emo',
'PLS',
'ORO_Motor',
'Others'
];
const unlockedLessonPlans = processedLessonPlans
.filter(item => !item.isLocked)
.sort((a, b) => {
const aIndex = categoryOrder.indexOf(a.category);
const bIndex = categoryOrder.indexOf(b.category);
if (aIndex !== bIndex) {
return aIndex - bIndex;
}
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
});
const lockedLessonPlans = processedLessonPlans
.filter(item => item.isLocked)
.sort((a, b) => {
const aIndex = categoryOrder.indexOf(a.category);
const bIndex = categoryOrder.indexOf(b.category);
if (aIndex !== bIndex) {
return aIndex - bIndex;
}
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
});
return [...unlockedLessonPlans, ...lockedLessonPlans];
}, [lessonPlansData, childSubscriptionData]);
};
Lesson Plan Assigned Tab Logic
Assigned Lesson Plan Display Logic:
- Shows all lesson plans assigned to child by Therapist or Admin
- Everything is unlocked regardless of subscription status
- Ordered by assigned date with latest assigned at the top
- Displays assignment source and progress status
Assignment Source Tracking:
- Therapist Assignments: Shows lesson plans assigned by current or other therapists
- Admin Assignments: Shows lesson plans assigned by admin
- Assignment Date: Tracks when lesson plan was assigned
- Progress Status: Shows completion status and progress
// ParentLessonPlanAssigned.tsx - Assigned lesson plans with source tracking
const ParentLessonPlanAssigned = ({ navigation, route }: ScreenProps) => {
const { childId } = route.params;
// Get assigned lesson plans with comprehensive source tracking
const { data: assignedLessonPlans, loading, refetch } = useGetAssignedLessonPlansQuery({
variables: {
child_id: childId,
include_all_assignments: true
},
fetchPolicy: 'cache-and-network'
});
// Process assigned lesson plans with source identification and progress tracking
const lessonPlanAssignedData = useMemo(() => {
const lessonPlans = assignedLessonPlans?.assigned_lesson_plans || [];
return lessonPlans.map(item => {
const isCompleted = item?.lesson_plan_responses?.some(
response => response?.status === UserLessonPlanStatus.Completed,
);
const isInProgress = item?.lesson_plan_responses?.some(
response => response?.status === UserLessonPlanStatus.InProgress,
);
return {
...item,
isCompleted,
isInProgress,
canRetake: isCompleted && item?.allow_retake,
assignmentSource: getAssignmentSource(item.assigned_by),
assignedDate: item.assigned_at,
isUnlocked: true // All assigned lesson plans are unlocked
};
}).sort((a, b) =>
new Date(b.assignedDate).getTime() - new Date(a.assignedDate).getTime()
);
}, [assignedLessonPlans]);
// Assignment source identification logic
const getAssignmentSource = (assignedBy: string) => {
if (assignedBy === 'ADMIN') return 'Assigned by Admin';
if (assignedBy === 'THERAPIST') return 'Assigned by Therapist';
return 'Assigned by System';
};
};
Lesson Plan Linked Tab Logic
Auto-Linking Logic:
- Subscription Feature Check: Auto-links when user subscription plan features contain
Link Lesson Plan - Score-Based Linking: Links based on assessment scores (0,1) for ORO/PLS and (0,1,2,3) for CDDC
- Domain-Based Linking: Per domain/sub-domain, 3 lesson plans are unlocked for CDDC
- Completion Independence: Completing one lesson plan doesn't unlock others from same domain
- Re-assessment Requirement: Parent must re-take assessment to update scores and unlock new lesson plans
Unlinking Logic:
- Score Change Detection: Unlinks when score changes in re-assessment flow
- Notification System: Sends bell icon notification: "Skill is achieved, so the lesson plan is unlinked"
- Dynamic Updates: Real-time updates when scores change
Ordering and Display Logic:
- Category Ordering: Screening → Case History → CDDC (GM, FM, ADL, RL, EL, Cog, Soc, Emo) → PLS → ORO-Motor → Others
- Unlocked First: Unlocked lesson plans shown at top, locked ones below
- Chronological Sorting: Within categories, sorted by created_at in ascending order
// ParentLessonPlanLinked.tsx - Auto-linked lesson plans with score-based logic
const ParentLessonPlanLinked = ({ navigation, route }: ScreenProps) => {
const { childId } = route.params;
const [filter, setFilter] = useState<LessonPlanFilter>({
assessor: 'parent',
age: childAge,
category: 'ALL'
});
// Get child subscription details for linking feature check
const { data: childSubscriptionData } = useGetChildSubscriptionQuery({
variables: { child_id: childId },
fetchPolicy: 'cache-and-network'
});
// Get assessment scores for linking logic
const { data: assessmentScoresData } = useGetAssessmentScoresQuery({
variables: { child_id: childId },
fetchPolicy: 'cache-and-network'
});
// Get auto-linked lesson plans based on scores and subscription
const { data: linkedLessonPlansData, loading, refetch } = useGetLinkedLessonPlansQuery({
variables: {
child_id: childId,
subscription_features: childSubscriptionData?.child_subscription?.features || [],
assessment_scores: assessmentScoresData?.assessment_scores || []
},
fetchPolicy: 'cache-and-network'
});
// Auto-linking logic with comprehensive score validation
const lessonPlanLinkedData = useMemo(() => {
const lessonPlans = linkedLessonPlansData?.linked_lesson_plans || [];
const subscriptionFeatures = childSubscriptionData?.child_subscription?.features || [];
const assessmentScores = assessmentScoresData?.assessment_scores || [];
// Check if linking feature is available
const hasLinkingFeature = subscriptionFeatures.includes('Link Lesson Plan');
// Process lesson plans with auto-linking logic
const processedLessonPlans = lessonPlans.map(item => {
let isLinked = false;
let isUnlocked = false;
// Check if lesson plan should be auto-linked based on scores
if (hasLinkingFeature) {
isLinked = checkAutoLinkingCriteria(item, assessmentScores);
isUnlocked = isLinked;
}
return {
...item,
isLinked,
isUnlocked,
linkingReason: getLinkingReason(item, assessmentScores)
};
});
// Sort lesson plans: unlocked first, then locked, maintaining category order
const categoryOrder = [
'Screening',
'Case History',
'CDDC_GM',
'CDDC_FM',
'CDDC_ADL',
'CDDC_RL',
'CDDC_EL',
'CDDC_Cog',
'CDDC_Soc',
'CDDC_Emo',
'PLS',
'ORO_Motor',
'Others'
];
const unlockedLessonPlans = processedLessonPlans
.filter(item => item.isUnlocked)
.sort((a, b) => {
const aIndex = categoryOrder.indexOf(a.category);
const bIndex = categoryOrder.indexOf(b.category);
if (aIndex !== bIndex) {
return aIndex - bIndex;
}
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
});
const lockedLessonPlans = processedLessonPlans
.filter(item => !item.isUnlocked)
.sort((a, b) => {
const aIndex = categoryOrder.indexOf(a.category);
const bIndex = categoryOrder.indexOf(b.category);
if (aIndex !== bIndex) {
return aIndex - bIndex;
}
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
});
return [...unlockedLessonPlans, ...lockedLessonPlans];
}, [linkedLessonPlansData, childSubscriptionData, assessmentScoresData]);
// Auto-linking criteria validation with score-based logic
const checkAutoLinkingCriteria = (lessonPlan: LessonPlan, scores: AssessmentScore[]) => {
const domain = lessonPlan.domain;
const subDomain = lessonPlan.sub_domain;
// Find relevant assessment scores for this domain/sub-domain
const relevantScores = scores.filter(score =>
score.domain === domain && score.sub_domain === subDomain
);
if (relevantScores.length === 0) return false;
// Check linking criteria based on assessment type
if (domain === 'CDDC') {
// CDDC: scores (0,1,2,3) unlock lesson plans
return relevantScores.some(score =>
score.score >= 0 && score.score <= 3
);
} else if (domain === 'ORO' || domain === 'PLS') {
// ORO/PLS: scores (0,1) unlock lesson plans
return relevantScores.some(score =>
score.score >= 0 && score.score <= 1
);
}
return false;
};
// Get linking reason for display
const getLinkingReason = (lessonPlan: LessonPlan, scores: AssessmentScore[]) => {
const relevantScores = scores.filter(score =>
score.domain === lessonPlan.domain && score.sub_domain === lessonPlan.sub_domain
);
if (relevantScores.length > 0) {
const latestScore = relevantScores[0];
return `Linked based on ${lessonPlan.domain} score: ${latestScore.score}`;
}
return 'Linked based on assessment scores';
};
// Handle score change and unlinking
const handleScoreChange = async (newScores: AssessmentScore[]) => {
// Check for lesson plans that should be unlinked
const lessonPlansToUnlink = lessonPlanLinkedData.filter(lessonPlan => {
const shouldBeLinked = checkAutoLinkingCriteria(lessonPlan, newScores);
return lessonPlan.isLinked && !shouldBeLinked;
});
// Unlink lesson plans and send notifications
for (const lessonPlan of lessonPlansToUnlink) {
await unlinkLessonPlan({
variables: {
lesson_plan_id: lessonPlan.id,
child_id: childId
}
});
// Send notification about unlinking
await sendUnlinkNotification({
variables: {
child_id: childId,
lesson_plan_id: lessonPlan.id,
message: 'Skill is achieved, so the lesson plan is unlinked'
}
});
}
// Refresh data after unlinking
refetch();
};
};
Lesson Plan Home Screen
// LessonPlan.tsx - Main lesson plan screen
const LessonPlan = ({ navigation }: Props) => {
const { activeChildId, setActiveChildId } = useChildContext();
const [index, setIndex] = React.useState(0);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [filter, setFilter] = useState<LessonPlanFilterType>(initialFilterState);
const userData = useSelector(selectUserData);
const {
data,
loading,
error,
refetch: refetchChildData,
} = useGetActiveChildrenForLessonPlanQuery({
fetchPolicy: 'cache-and-network',
variables: {
parent_id: userData?.parentId,
},
skip: !userData?.parentId,
});
const filteredChildren = useMemo(
() =>
data?.child.map(c => {
return {
name: c.name,
age: calculateAgeInMonths(c?.dob),
profile_image: {
path: c.profile_image?.path,
},
id: c.id,
};
}) ?? [],
[data?.child],
);
const selectedChild = useMemo(
() => filteredChildren.find(c => c.id === activeChildId),
[filteredChildren, activeChildId],
);
const {
data: rawLessonPlanAllData,
loading: lessonPlanAllLoading,
error: lessonPlanAllError,
refetch: refetchAll,
fetchMore: fetchMoreLessonPlans,
} = useGetLessonPlansForAllQuery({
variables: {
child_id: selectedChild?.id,
limit: 50,
page: 1,
filter_type: AllLessonPlansFilterType.All,
},
skip: !selectedChild?.id || !selectedChild?.age,
fetchPolicy: 'cache-and-network',
notifyOnNetworkStatusChange: true,
});
const lessonPlanAllData = useMemo(() => {
const lessonPlans: LessonPlanData[] = (rawLessonPlanAllData
?.getAllLessonPlans?.data?.lesson_plans ?? []) as LessonPlanData[];
// Check subscription requirements
const allData =
lessonPlans?.map(item => {
let isLocked = true;
if (item?.type === LessonPlanType.Free || item?.subscribed) {
isLocked = false;
}
return { ...item, isLocked };
}) ?? [];
return allData;
}, [rawLessonPlanAllData?.getAllLessonPlans]);
const lessonPlanAssignedData = useMemo(() => {
const lessonPlans: LessonPlanData[] = (rawLessonPlanAssignedData
?.getLessonPlansForAssigned?.data?.lesson_plans ?? []) as LessonPlanData[];
return lessonPlans?.map(item => {
const isCompleted = item?.lesson_plan_responses?.some(
response => response?.status === UserLessonPlanStatus.Completed,
);
const isInProgress = item?.lesson_plan_responses?.some(
response => response?.status === UserLessonPlanStatus.InProgress,
);
return {
...item,
isCompleted,
isInProgress,
canRetake: isCompleted && item?.allow_retake,
};
});
}, [rawLessonPlanAssignedData?.getLessonPlansForAssigned]);
const tabs = [
{ key: 'all', title: 'All Lesson Plans', component: AllLessonPlans },
{ key: 'assigned', title: 'Assigned', component: AssignedLessonPlans },
];
// Lesson plan data processing with comprehensive filtering and subscription logic
const lessonPlanAllData = useMemo(() => {
const lessonPlans: LessonPlanData[] = (rawLessonPlanAllData
?.getAllLessonPlans?.data?.lesson_plans ?? []) as LessonPlanData[];
// Check subscription requirements and lock status
const allData =
lessonPlans?.map(item => {
let isLocked = true;
if (item?.type === LessonPlanType.Free || item?.subscribed) {
isLocked = false;
}
return { ...item, isLocked };
}) ?? [];
return allData;
}, [rawLessonPlanAllData?.getAllLessonPlans]);
// Assigned lesson plans processing with completion status
const lessonPlanAssignedData = useMemo(() => {
const lessonPlans: LessonPlanData[] = (rawLessonPlanAssignedData
?.getLessonPlansForAssigned?.data?.lesson_plans ?? []) as LessonPlanData[];
return lessonPlans?.map(item => {
const isCompleted = item?.lesson_plan_responses?.some(
response => response?.status === UserLessonPlanStatus.Completed,
);
const isInProgress = item?.lesson_plan_responses?.some(
response => response?.status === UserLessonPlanStatus.InProgress,
);
return {
...item,
isCompleted,
isInProgress,
canRetake: isCompleted && item?.allow_retake,
};
});
}, [rawLessonPlanAssignedData?.getLessonPlansForAssigned]);
};
Take Lesson Plan Screen
Lesson Plan Taking Logic:
- Activity Navigation: Manages current activity index and navigation between activities
- Progress Tracking: Tracks completed activities using Set data structure for efficient lookups
- Time Management: Monitors time spent on lesson plan for analytics and reporting
- Auto-Save Functionality: Automatically saves progress when activities are completed
- State Management: Handles submission and saving states to prevent duplicate operations
Activity Completion Logic:
- Activity Validation: Ensures all required activities are completed before submission
- Progress Calculation: Calculates completion percentage based on completed activities
- Navigation Control: Enables next/previous navigation with boundary checks
- Completion Tracking: Maintains list of completed activity IDs for progress persistence
Data Persistence Logic:
- Local State Management: Manages current activity index and completed activities in component state
- Server Synchronization: Syncs progress with server using GraphQL mutations
- Error Handling: Provides user feedback for save and submission operations
- Cache Management: Uses cache-and-network fetch policy for optimal data loading
// TakeLessonPlan.tsx - Lesson plan taking interface with comprehensive activity management
const TakeLessonPlan = ({ navigation, route }: ScreenProps) => {
const { lessonPlanId, childId } = route.params;
// Activity navigation state management
const [currentActivityIndex, setCurrentActivityIndex] = useState(0);
const [completedActivities, setCompletedActivities] = useState<Set<string>>(new Set());
// Operation state management for UI feedback
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [timeSpent, setTimeSpent] = useState(0);
// Fetch lesson plan details with comprehensive data loading
const { data: lessonPlanData, loading, error, refetch } = useGetLessonPlanDetailsQuery({
variables: { id: lessonPlanId },
fetchPolicy: 'cache-and-network'
});
// Extract lesson plan data and activities for processing
const lessonPlan = lessonPlanData?.lesson_plan_by_pk;
const activities = lessonPlan?.activities || [];
const currentActivity = activities[currentActivityIndex];
// Progress saving mutation with comprehensive error handling
const [saveProgress] = useSaveLessonPlanProgressMutation({
onCompleted: (data) => {
if (data.saveLessonPlanProgress.success) {
toast.show({ text: 'Progress saved successfully' });
}
},
onError: (error) => {
toast.show({ text: 'Failed to save progress' });
}
});
// Lesson plan submission mutation with success/failure handling
const [submitLessonPlan] = useSubmitLessonPlanMutation({
onCompleted: (data) => {
if (data.submitLessonPlan.success) {
toast.show({ text: 'Lesson plan completed successfully' });
navigation.navigate('parent/lesson-plan-results', {
lessonPlanId,
childId,
responseId: data.submitLessonPlan.response_id
});
} else {
toast.show({ text: data.submitLessonPlan.message || 'Failed to submit lesson plan' });
}
},
onError: (error) => {
toast.show({ text: error.message || 'Failed to submit lesson plan' });
}
});
const handleActivityComplete = (activityId: string) => {
setCompletedActivities(prev => new Set([...prev, activityId]));
// Auto-save progress
saveProgress({
variables: {
lesson_plan_id: lessonPlanId,
child_id: childId,
activity_id: activityId,
progress: (currentActivityIndex + 1) / activities.length,
time_spent: timeSpent
}
});
};
const handleNext = () => {
if (currentActivityIndex < activities.length - 1) {
setCurrentActivityIndex(prev => prev + 1);
} else {
handleSubmit();
}
};
const handlePrevious = () => {
if (currentActivityIndex > 0) {
setCurrentActivityIndex(prev => prev - 1);
}
};
const handleSubmit = async () => {
setIsSubmitting(true);
try {
await submitLessonPlan({
variables: {
lesson_plan_id: lessonPlanId,
child_id: childId,
completed_activities: Array.from(completedActivities),
completion_time: new Date().toISOString(),
total_time_spent: timeSpent
}
});
} catch (error) {
console.error('Submit lesson plan error:', error);
} finally {
setIsSubmitting(false);
}
};
const handleSaveAndExit = async () => {
setIsSaving(true);
try {
await saveProgress({
variables: {
lesson_plan_id: lessonPlanId,
child_id: childId,
completed_activities: Array.from(completedActivities),
progress: (currentActivityIndex + 1) / activities.length,
status: 'IN_PROGRESS',
time_spent: timeSpent
}
});
navigation.goBack();
} catch (error) {
console.error('Save progress error:', error);
} finally {
setIsSaving(false);
}
};
// Lesson plan taking logic with comprehensive progress tracking
const handleActivityComplete = (activityId: string) => {
setCompletedActivities(prev => new Set([...prev, activityId]));
// Auto-save progress
saveProgress({
variables: {
lesson_plan_id: lessonPlanId,
child_id: childId,
activity_id: activityId,
progress: (currentActivityIndex + 1) / activities.length,
time_spent: timeSpent
}
});
};
const handleNext = () => {
if (currentActivityIndex < activities.length - 1) {
setCurrentActivityIndex(prev => prev + 1);
} else {
handleSubmit();
}
};
const handlePrevious = () => {
if (currentActivityIndex > 0) {
setCurrentActivityIndex(prev => prev - 1);
}
};
const handleSubmit = async () => {
setIsSubmitting(true);
try {
await submitLessonPlan({
variables: {
lesson_plan_id: lessonPlanId,
child_id: childId,
completed_activities: Array.from(completedActivities),
completion_time: new Date().toISOString(),
total_time_spent: timeSpent
}
});
} catch (error) {
console.error('Submit lesson plan error:', error);
} finally {
setIsSubmitting(false);
}
};
const handleSaveAndExit = async () => {
setIsSaving(true);
try {
await saveProgress({
variables: {
lesson_plan_id: lessonPlanId,
child_id: childId,
completed_activities: Array.from(completedActivities),
progress: (currentActivityIndex + 1) / activities.length,
status: 'IN_PROGRESS',
time_spent: timeSpent
}
});
navigation.goBack();
} catch (error) {
console.error('Save progress error:', error);
} finally {
setIsSaving(false);
}
};
};
Resume Lesson Plan Screen
Resume Logic:
- Progress Restoration: Loads previously saved progress from server and restores user's position
- State Recovery: Restores completed activities, current activity index, and time spent
- Seamless Continuation: Allows user to continue from where they left off without losing progress
- Data Validation: Ensures saved progress is valid and activities are still available
Progress Loading Logic:
- Server Synchronization: Fetches saved progress data using response ID for accurate restoration
- Activity Index Calculation: Calculates current activity index based on saved progress percentage
- Completed Activities Restoration: Restores set of completed activity IDs for progress tracking
- Time Continuation: Continues time tracking from previously saved time spent
State Management Logic:
- Loading State: Shows loading indicator while fetching saved progress data
- Error Handling: Handles cases where saved progress is invalid or corrupted
- Data Validation: Validates that saved activities still exist in current lesson plan
- Fallback Logic: Provides fallback to beginning if saved progress is invalid
// ResumeLessonPlan.tsx - Resume incomplete lesson plan with comprehensive state restoration
const ResumeLessonPlan = ({ navigation, route }: ScreenProps) => {
const { lessonPlanId, childId, responseId } = route.params;
// State management for progress restoration
const [currentActivityIndex, setCurrentActivityIndex] = useState(0);
const [completedActivities, setCompletedActivities] = useState<Set<string>>(new Set());
const [isLoading, setIsLoading] = useState(true);
const [timeSpent, setTimeSpent] = useState(0);
// Fetch saved progress data with comprehensive error handling
const { data: progressData, loading, error } = useGetLessonPlanProgressQuery({
variables: {
lesson_plan_id: lessonPlanId,
child_id: childId,
response_id: responseId
},
fetchPolicy: 'cache-and-network'
});
// Extract lesson plan data and saved progress information
const lessonPlan = progressData?.lesson_plan_by_pk;
const activities = lessonPlan?.activities || [];
const savedProgress = progressData?.lesson_plan_response?.progress || 0;
const savedCompletedActivities = progressData?.lesson_plan_response?.completed_activities || [];
const savedTimeSpent = progressData?.lesson_plan_response?.time_spent || 0;
// Progress restoration logic with comprehensive state management
useEffect(() => {
if (progressData) {
// Restore completed activities from saved progress
setCompletedActivities(new Set(savedCompletedActivities));
// Calculate current activity index based on saved progress
setCurrentActivityIndex(Math.floor(savedProgress * activities.length));
// Restore time spent for accurate tracking
setTimeSpent(savedTimeSpent);
// Mark loading as complete
setIsLoading(false);
}
}, [progressData, savedProgress, savedCompletedActivities, savedTimeSpent, activities.length]);
// Progress saving mutation with success feedback
const [saveProgress] = useSaveLessonPlanProgressMutation({
onCompleted: (data) => {
if (data.saveLessonPlanProgress.success) {
toast.show({ text: 'Progress saved successfully' });
}
}
});
// Lesson plan submission mutation with navigation handling
const [submitLessonPlan] = useSubmitLessonPlanMutation({
onCompleted: (data) => {
if (data.submitLessonPlan.success) {
toast.show({ text: 'Lesson plan completed successfully' });
navigation.navigate('parent/lesson-plan-results', {
lessonPlanId,
childId,
responseId: data.submitLessonPlan.response_id
});
}
}
});
// Activity completion handling with progress tracking
const handleActivityComplete = (activityId: string) => {
setCompletedActivities(prev => new Set([...prev, activityId]));
// Auto-save progress with updated completion status
saveProgress({
variables: {
lesson_plan_id: lessonPlanId,
child_id: childId,
activity_id: activityId,
progress: (currentActivityIndex + 1) / activities.length,
time_spent: timeSpent
}
});
};
const handleNext = () => {
if (currentActivityIndex < activities.length - 1) {
setCurrentActivityIndex(prev => prev + 1);
} else {
handleSubmit();
}
};
const handlePrevious = () => {
if (currentActivityIndex > 0) {
setCurrentActivityIndex(prev => prev - 1);
}
};
const handleSubmit = async () => {
try {
await submitLessonPlan({
variables: {
lesson_plan_id: lessonPlanId,
child_id: childId,
completed_activities: Array.from(completedActivities),
completion_time: new Date().toISOString(),
total_time_spent: timeSpent
}
});
} catch (error) {
console.error('Submit lesson plan error:', error);
}
};
// Progress restoration logic for resume functionality
useEffect(() => {
if (progressData) {
setCompletedActivities(new Set(savedCompletedActivities));
setCurrentActivityIndex(Math.floor(savedProgress * activities.length));
setTimeSpent(savedTimeSpent);
setIsLoading(false);
}
}, [progressData, savedProgress, savedCompletedActivities, savedTimeSpent, activities.length]);
};
Retake Lesson Plan Screen
// RetakeLessonPlan.tsx - Retake completed lesson plan
const RetakeLessonPlan = ({ navigation, route }: ScreenProps) => {
const { lessonPlanId, childId } = route.params;
const [showConfirmation, setShowConfirmation] = useState(false);
const { data: lessonPlanData, loading } = useGetLessonPlanDetailsQuery({
variables: { id: lessonPlanId },
fetchPolicy: 'cache-and-network'
});
const lessonPlan = lessonPlanData?.lesson_plan_by_pk;
const previousResults = lessonPlan?.lesson_plan_responses?.filter(
response => response?.status === 'COMPLETED'
);
const [retakeLessonPlan] = useRetakeLessonPlanMutation({
onCompleted: (data) => {
if (data.retakeLessonPlan.success) {
toast.show({ text: 'Lesson plan retake initiated successfully' });
navigation.navigate('parent/take-lesson-plan', {
lessonPlanId,
childId,
responseId: data.retakeLessonPlan.response_id
});
} else {
toast.show({ text: data.retakeLessonPlan.message || 'Failed to retake lesson plan' });
}
},
onError: (error) => {
toast.show({ text: error.message || 'Failed to retake lesson plan' });
}
});
const handleRetake = async () => {
try {
await retakeLessonPlan({
variables: {
lesson_plan_id: lessonPlanId,
child_id: childId
}
});
} catch (error) {
console.error('Retake lesson plan error:', error);
}
};
const handleConfirmRetake = () => {
setShowConfirmation(true);
};
// Retake lesson plan logic with confirmation
const handleRetake = async () => {
try {
await retakeLessonPlan({
variables: {
lesson_plan_id: lessonPlanId,
child_id: childId
}
});
} catch (error) {
console.error('Retake lesson plan error:', error);
}
};
const handleConfirmRetake = () => {
setShowConfirmation(true);
};
};
Retake Lesson Plan Screen
Retake Logic:
- Permission Validation: Checks if lesson plan allows retaking based on
allow_retakeflag - Previous Response Handling: Manages previous completion data and creates new response record
- Confirmation Flow: Requires user confirmation before initiating retake process
- Fresh Start: Resets all progress and starts lesson plan from beginning
Retake Process Logic:
- Data Archival: Archives previous completion data for historical tracking
- New Response Creation: Creates new response record for retake attempt
- Progress Reset: Resets all progress indicators to initial state
- Navigation Handling: Navigates to fresh lesson plan taking experience
State Management Logic:
- Confirmation State: Manages confirmation dialog visibility and user interaction
- Loading State: Shows loading indicator during retake initiation process
- Error Handling: Handles retake failures and provides user feedback
- Success Handling: Navigates to lesson plan taking screen on successful retake initiation
// RetakeLessonPlan.tsx - Retake completed lesson plan with comprehensive validation
const RetakeLessonPlan = ({ navigation, route }: ScreenProps) => {
const { lessonPlanId, childId } = route.params;
const [showConfirmation, setShowConfirmation] = useState(false);
const [isRetaking, setIsRetaking] = useState(false);
// Get lesson plan details to check retake permissions
const { data: lessonPlanData, loading } = useGetLessonPlanDetailsQuery({
variables: { id: lessonPlanId },
fetchPolicy: 'cache-and-network'
});
const lessonPlan = lessonPlanData?.lesson_plan_by_pk;
const canRetake = lessonPlan?.allow_retake || false;
// Retake lesson plan mutation with comprehensive error handling
const [retakeLessonPlan, { loading: retakeLoading }] = useRetakeLessonPlanMutation({
onCompleted: (data) => {
if (data.retakeLessonPlan.success) {
toast.show({ text: 'Lesson plan retake initiated successfully' });
navigation.navigate('parent/take-lesson-plan', {
lessonPlanId,
childId,
responseId: data.retakeLessonPlan.response_id
});
} else {
toast.show({ text: data.retakeLessonPlan.message || 'Failed to retake lesson plan' });
}
},
onError: (error) => {
toast.show({ text: error.message || 'Failed to retake lesson plan' });
}
});
// Retake initiation logic with confirmation handling
const handleRetake = async () => {
if (!canRetake) {
toast.show({ text: 'This lesson plan does not allow retaking' });
return;
}
setIsRetaking(true);
try {
await retakeLessonPlan({
variables: {
lesson_plan_id: lessonPlanId,
child_id: childId
}
});
} catch (error) {
console.error('Retake lesson plan error:', error);
} finally {
setIsRetaking(false);
}
};
// Confirmation dialog handling
const handleConfirmRetake = () => {
setShowConfirmation(true);
};
const handleCancelRetake = () => {
setShowConfirmation(false);
};
const handleConfirmRetakeAction = () => {
setShowConfirmation(false);
handleRetake();
};
};
📊 Lesson Plan Progress Tracking
Progress Analytics
// Lesson plan progress analytics
const useLessonPlanProgress = (childId: string) => {
const { data: progressData } = useGetLessonPlanProgressAnalyticsQuery({
variables: {
child_id: childId,
date_range: {
start: format(subDays(new Date(), 30), 'YYYY-MM-DD'),
end: format(new Date(), 'YYYY-MM-DD')
}
}
});
const analytics = useMemo(() => {
if (!progressData) return null;
return {
totalLessonPlans: progressData.total_lesson_plans,
completedLessonPlans: progressData.completed_lesson_plans,
inProgressLessonPlans: progressData.in_progress_lesson_plans,
averageCompletionTime: progressData.average_completion_time,
improvementRate: progressData.improvement_rate,
strengths: progressData.strengths,
areasForImprovement: progressData.areas_for_improvement,
monthlyProgress: progressData.monthly_progress
};
}, [progressData]);
return analytics;
};
🔄 Real-time Updates
Lesson Plan Notifications
// Real-time lesson plan updates
const useLessonPlanSubscription = (childId: string) => {
const { data: subscriptionData } = useLessonPlanUpdatesSubscription({
variables: { child_id: childId }
});
useEffect(() => {
if (subscriptionData?.lesson_plan_updated) {
const update = subscriptionData.lesson_plan_updated;
// Update local cache
updateLessonPlanCache(update);
// Show notification
toast.show({
text: `Lesson plan ${update.status.toLowerCase()}`,
type: 'info'
});
}
}, [subscriptionData]);
};
📱 Mobile-Specific Features
Touch Interactions
// Lesson plan touch interactions
const useLessonPlanGestures = () => {
const handleSwipeLeft = (lessonPlan: LessonPlan) => {
// Swipe left to retake
showRetakeConfirmation(lessonPlan);
};
const handleSwipeRight = (lessonPlan: LessonPlan) => {
// Swipe right to resume
resumeLessonPlan(lessonPlan);
};
const handleLongPress = (lessonPlan: LessonPlan) => {
// Long press to show options
showLessonPlanOptions(lessonPlan);
};
return {
handleSwipeLeft,
handleSwipeRight,
handleLongPress
};
};
Offline Lesson Plan Management
// Offline lesson plan capabilities
const useOfflineLessonPlans = () => {
const [offlineLessonPlans, setOfflineLessonPlans] = useState([]);
const saveOfflineLessonPlan = (lessonPlan: LessonPlan) => {
const offlineLessonPlan = {
...lessonPlan,
id: `offline_${Date.now()}`,
isOffline: true,
createdAt: new Date().toISOString()
};
setOfflineLessonPlans(prev => [...prev, offlineLessonPlan]);
mmkvStorage.set('offline_lesson_plans', JSON.stringify([...offlineLessonPlans, offlineLessonPlan]));
};
const syncOfflineLessonPlans = async () => {
if (offlineLessonPlans.length === 0) return;
try {
for (const lessonPlan of offlineLessonPlans) {
await submitLessonPlan(lessonPlan);
}
setOfflineLessonPlans([]);
mmkvStorage.delete('offline_lesson_plans');
toast.show({ text: 'Offline lesson plans synced successfully' });
} catch (error) {
toast.show({ text: 'Failed to sync offline lesson plans' });
}
};
return {
saveOfflineLessonPlan,
syncOfflineLessonPlans,
offlineLessonPlans
};
};
🎨 UI Components
Lesson Plan Card
// LessonPlanCard.tsx
const LessonPlanCard = ({
lessonPlan,
onTake,
onResume,
onRetake,
onViewResults
}) => {
const { colors } = useTheme<Theme>();
// Lesson plan card logic with comprehensive status management
const getStatusColor = (status: string) => {
switch (status) {
case 'COMPLETED': return colors.success;
case 'IN_PROGRESS': return colors.warning;
case 'PENDING': return colors.info;
default: return colors.secondary;
}
};
const handleCardPress = () => {
if (lessonPlan.isCompleted) {
onViewResults(lessonPlan);
} else if (lessonPlan.isInProgress) {
onResume(lessonPlan);
} else {
onTake(lessonPlan);
}
};
};
🔧 GraphQL Integration
Lesson Plan Queries
# Get lesson plans for child
query GetLessonPlansForAll($child_id: uuid!, $limit: Int!, $page: Int!, $filter_type: String!) {
getAllLessonPlans(
child_id: $child_id
limit: $limit
page: $page
filter_type: $filter_type
) {
success
data {
lesson_plans {
id
title
description
type
difficulty
estimated_duration
activities {
id
title
description
activity_type
instructions
}
lesson_plan_responses {
id
status
score
progress
completed_activities
}
}
}
}
}
# Get lesson plan details
query GetLessonPlanDetails($id: uuid!) {
lesson_plan_by_pk(id: $id) {
id
title
description
type
difficulty
estimated_duration
activities {
id
title
description
activity_type
instructions
required
}
}
}
Lesson Plan Mutations
# Save lesson plan progress
mutation SaveLessonPlanProgress($lesson_plan_id: uuid!, $child_id: uuid!, $activity_id: uuid!, $progress: Float!, $time_spent: Int!) {
saveLessonPlanProgress(
lesson_plan_id: $lesson_plan_id
child_id: $child_id
activity_id: $activity_id
progress: $progress
time_spent: $time_spent
) {
success
message
}
}
# Submit lesson plan
mutation SubmitLessonPlan($lesson_plan_id: uuid!, $child_id: uuid!, $completed_activities: [uuid!]!, $completion_time: timestamptz!, $total_time_spent: Int!) {
submitLessonPlan(
lesson_plan_id: $lesson_plan_id
child_id: $child_id
completed_activities: $completed_activities
completion_time: $completion_time
total_time_spent: $total_time_spent
) {
success
message
response_id
}
}
# Retake lesson plan
mutation RetakeLessonPlan($lesson_plan_id: uuid!, $child_id: uuid!) {
retakeLessonPlan(
lesson_plan_id: $lesson_plan_id
child_id: $child_id
) {
success
message
response_id
}
}
🎯 Best Practices
Lesson Plan Management
- Progress Tracking: Save progress automatically
- Resume Functionality: Allow resuming incomplete lesson plans
- Retake Management: Enable retaking completed lesson plans
- Real-time Updates: Live progress synchronization
User Experience
- Visual Feedback: Clear progress indicators and status
- Touch Interactions: Intuitive lesson plan navigation
- Offline Support: Allow offline lesson plan taking
- Notifications: Timely lesson plan reminders
Performance
- Data Caching: Cache lesson plan data locally
- Lazy Loading: Load lesson plans on demand
- Optimistic Updates: Update UI before server confirmation
- Background Sync: Sync progress in background