Skip to main content

📅 Therapist Schedule Management

This document covers the comprehensive schedule management system for therapists in the Com DEALL mobile application, including weekly and monthly schedule views, time slot management, and appointment constraints.

🎯 Overview

The therapist schedule management system allows therapists to set their availability, manage time slots, and optimize their schedule. The system supports both weekly and monthly views with real-time updates and appointment booking constraints.

🏗️ Architecture

Schedule Management Components

Schedule Management System
├── Weekly Schedule View (WeeklySchedule.tsx)
├── Monthly Schedule View (MonthlySchedule.tsx)
├── Time Slot Management
├── Month Picker Integration
├── Appointment Constraints
└── CRUD Operations (Add, Edit, Delete)
Profile Tab → Schedule Feature → Weekly/Monthly View Toggle

📱 Mobile Implementation

Weekly Schedule View

The weekly schedule view displays a horizontal slider of dates with orange dots indicating days that have scheduled time slots. Therapists can select dates and manage time slots for each day.

Key Features:

  • Date Slider: Horizontal scrollable week view with date selection
  • Orange Dots: Visual indicators for days with existing schedules
  • Month Picker: Tap to change month and view different weeks
  • Time Slot Management: Add, edit, and delete time slots for selected dates
  • Appointment Constraints: Cannot delete slots with booked appointments

Weekly Schedule Implementation:

// WeeklySchedule.tsx - Weekly schedule view
const WeeklySchedule = ({ navigation, toggleMonthlyView }) => {
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [schedules, setSchedules] = useState<TSchedule[]>();
const [startTime, setStartTime] = useState<Date | null>(null);
const [showNewSlotOption, setShowNewSlotOption] = useState(false);
const [isAddSchedule, setIsAddSchedule] = useState(false);
const [isEditSchedule, setIsEditSchedule] = useState(false);
const [isShowMonthPicker, setIsShowMonthPicker] = useState(false);
const [monthPickerDate, setMonthPickerDate] = useState<Date>(new Date());

const { therapistId } = useSelector(selectUserData);

// Fetch schedule data
const {
data: scheduleData,
loading: getScheduleLoading,
refetch,
error,
} = useGetSchedulesQuery({
variables: {
therapist_id: therapistId,
date: format(dayjs(today), 'YYYY-MM-DD'),
},
fetchPolicy: 'cache-and-network',
});

// Add schedule mutation
const [sendSchedule, { loading }] = useAddScheduleMutation({
errorPolicy: 'all',
onCompleted: data => {
if (data?.create_therapist_schedule[0]?.id) {
refetch();
toast.show({ text: t('createSuccess') });
setModalVisible(true);
setIsAddSchedule(false);
setIsEditSchedule(false);
}
},
onError: e => {
toast.show({ text: e.message });
},
});

Date Selection and Visual Indicators:

// Week slider with date selection and orange dots
const WeekSlider = () => {
const isDateSelected = dayjs(dateOfTheWeekInMonth).isSame(
dayjs(selectedDate),
'day',
);
const isDisabled = dayjs(dateOfTheWeekInMonth).isBefore(
dayjs(today),
'day',
);

const selectedDaySchedule = getSelectedDateSchedule(dateOfTheWeekInMonth);

const handleSelectedDate = (_date: Date) => {
if (isAddSchedule) {
setIsAddSchedule(false);
}
if (isEditSchedule) {
setIsEditSchedule(false);
}
setSelectedDate(_date);
setSchedules(cloneDeep(scheduleData?.schedule));
setShowNewSlotOption(false);
};

// Orange dot indicator logic for days with schedules
const hasSchedule = selectedDaySchedule?.timeSlots?.length > 0;
};

Time Slot Management (Add, Edit, Delete):

// Add new time slot
const onEndTimeSelect = (_date: Date) => {
const updatingSchedules = cloneDeep(schedules);

const indexOfSchedule = updatingSchedules.findIndex(
s => s.date === format(_date, 'YYYY-MM-DD'),
);

if (indexOfSchedule === -1) {
updatingSchedules.push({
date: format(_date, 'YYYY-MM-DD'),
Day: format(_date, 'dddd'),
timeSlots: [{ StartTime: startTime, EndTime: _date }],
isUpdated: true,
});
} else {
const schedule = updatingSchedules[indexOfSchedule];
updatingSchedules[indexOfSchedule] = {
...schedule,
timeSlots: [
...schedule.timeSlots,
{
StartTime: startTime,
EndTime: _date,
},
],
isUpdated: true,
};
}

setSchedules(updatingSchedules);
setShowNewSlotOption(false);
};

// Delete time slot (with appointment constraint)
const deleteSlot = (startTime: Date, endTime: Date) => {
const updatingSchedules = cloneDeep(schedules);

const indexOfSchedule = updatingSchedules.findIndex(
s => selectedDate && s.date === format(selectedDate, 'YYYY-MM-DD'),
);

const schedule = updatingSchedules[indexOfSchedule];
schedule.timeSlots = schedule?.timeSlots?.filter(
timeSlot =>
!(
dayjs(timeSlot?.StartTime).isSame(startTime) &&
dayjs(timeSlot?.EndTime).isSame(endTime)
),
);

updatingSchedules[indexOfSchedule] = {
...schedule,
isUpdated: true,
};

setSchedules(updatingSchedules);
};

// Submit schedule changes
const handleSubmit = () => {
const isValid = checkValidity(schedules);

if (isValid) {
const schedulesPayload = schedules
.filter(s => s?.isUpdated)
.map(schedule => ({
schedule_date: schedule.date,
day_of_week: schedule.Day,
time_slots: schedule?.timeSlots
?.filter(timeSlot => !timeSlot?.booking_count) // Only non-booked slots
.map(timeSlot => ({
start_time: timeSlot.StartTime,
end_time: timeSlot.EndTime,
time_slot_id: timeSlot?.id,
})),
}));

if (schedulesPayload?.length > 0) {
sendSchedule({
variables: {
input_therapist_id: therapistId!,
input_schedule_entries: schedulesPayload,
},
});
} else {
toast.show({ text: t('noSchedule') });
}
} else {
toast.show({ text: t('slotsNotValid') });
}
};

Monthly Schedule View

The monthly schedule view provides a full calendar interface with advanced selection capabilities including weekday selection and bulk operations.

Key Features:

  • Full Calendar View: Complete month view with date selection
  • Weekday Selection: Select all Mondays, Tuesdays, etc.
  • Bulk Operations: Select entire month or specific weekdays
  • CRUD Operations: Add, edit, and delete schedules for multiple dates
  • Appointment Constraints: Same booking constraints as weekly view

Monthly Schedule Implementation:

// MonthlySchedule.tsx - Monthly schedule view
const MonthlySchedule = ({ navigation, toggleMonthlyView }) => {
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [currentMonth, setCurrentMonth] = useState(new Date());
const [markedDates, setMarkedDates] = useState<MarkedDates>({});
const [selectedDates, setSelectDates] = useState<string[]>([]);
const [checkboxes, setCheckboxes] = useState(initialCheckboxes);
const [isSelectAll, setIsSelectAll] = useState(false);
const [isShowMonthPicker, setIsShowMonthPicker] = useState(false);
const [_date, setDate] = useState<Date>(new Date());

const { therapistId } = useSelector(selectUserData);

// Fetch schedule data
const {
data: scheduleData,
loading: getScheduleLoading,
refetch,
error,
} = useGetSchedulesQuery({
variables: {
therapist_id: therapistId,
date: format(dayjs(), _format),
},
fetchPolicy: 'cache-and-network',
});

Weekday Selection and Bulk Operations:

// Get weekdays for a specific day of the week
const getWeekdays = useCallback(
(day: number) => {
const d = new Date(currentMonth);
const days = [];
d.setDate(1);

// Get the first particular weekday in the month
while (d.getDay() !== day) {
d.setDate(d.getDate() + 1);
}

// Get all the other weekdays in the month
while (d.getMonth() === currentMonth.getMonth()) {
days.push(new Date(d.getTime()));
d.setDate(d.getDate() + 7);
}

return days
.map(item => format(item, _format))
.filter(item => {
return isDateSameOrAfterToday(item ?? '');
});
},
[currentMonth],
);

// Select all weekdays of a specific day
const onSelect = (item: CheckBoxProps) => {
const temp = checkboxes.map(box => {
if (item.name === box.name) {
box.value = !box.value;
}
return box;
});
setCheckboxes(temp);
const dates = getWeekdays(item.day);

if (item.value) {
checkboxSelectDays(dates);
} else {
checkboxDeselectDays(dates);
}
};

// Select all dates in the month
const onSelectAll = () => {
const isAllSelected = !isSelectAll;

const month = getFullMonth();

const updatedSelectedDates: string[] = [];
const updateMarkedDates = cloneDeep(markedDates);

if (isAllSelected) {
month.forEach(datesArray => {
updatedSelectedDates.push(...datesArray);
});

updatedSelectedDates.forEach(date => {
const markedDay = updateMarkedDates[date];
updateMarkedDates[date] = {
...markedDay,
selected: true,
customStyles: {
container: styles.selectedDate,
},
};
});
} else {
selectedDates.forEach(date => {
const markedDay = updateMarkedDates[date];
updateMarkedDates[date] = {
...markedDay,
selected: false,
customStyles: {
container: styles.unSelectedDate,
},
};
});
}

setIsSelectAll(!isSelectAll);
setSelectDates(updatedSelectedDates);
setMarkedDates(updateMarkedDates);
};

⏰ Time Slot Management

Time Slot Operations

Add Time Slot:

// Add new time slot for selected dates
const handleTimeSlots = (timeSlots: Schedule[]) => {
sheetRef.current?.close();
const schedule = timeSlots
.filter(s => s?.isUpdated)
.map(date => {
const slots = date.timeSlots
?.filter(timeSlot => !timeSlot?.booking_count) // Only non-booked slots
.map(s => {
const StartTime = formatTimeSlot(s?.StartTime, date?.date);
const EndTime = formatTimeSlot(s?.EndTime, date?.date);

return {
start_time: StartTime,
end_time: EndTime,
time_slot_id: s?.id,
};
});
return {
time_slots: slots,
day_of_week: date.Day as string,
schedule_date: date.date,
};
});

sendSchedule({
variables: {
input_therapist_id: therapistId!,
input_schedule_entries: schedule,
},
});
};

Edit Time Slot:

// Update existing time slot
const onUpdateSlotStart = (time: Date, rowIndex: number) => {
const updatingSchedules = cloneDeep(schedules);

const indexOfSchedule = updatingSchedules.findIndex(
s => s.date === format(time, 'YYYY-MM-DD'),
);

const schedule = updatingSchedules[indexOfSchedule];
schedule.timeSlots[rowIndex].StartTime = time;
updatingSchedules[indexOfSchedule] = {
...schedule,
isUpdated: true,
};

setSchedules(updatingSchedules);
};

const onUpdateSlotEnd = (time: Date, rowIndex: number) => {
const updatingSchedules = cloneDeep(schedules);

const indexOfSchedule = updatingSchedules.findIndex(
s => s.date === format(time, 'YYYY-MM-DD'),
);

const schedule = updatingSchedules[indexOfSchedule];
schedule.timeSlots[rowIndex].EndTime = time;
updatingSchedules[indexOfSchedule] = {
...schedule,
isUpdated: true,
};

setSchedules(updatingSchedules);
};

Delete Time Slot (with Appointment Constraint):

// Delete time slot - only if no appointments are booked
const deleteSlot = (startTime: Date, endTime: Date) => {
const updatingSchedules = cloneDeep(schedules);

const indexOfSchedule = updatingSchedules.findIndex(
s => selectedDate && s.date === format(selectedDate, 'YYYY-MM-DD'),
);

const schedule = updatingSchedules[indexOfSchedule];
schedule.timeSlots = schedule?.timeSlots?.filter(
timeSlot =>
!(
dayjs(timeSlot?.StartTime).isSame(startTime) &&
dayjs(timeSlot?.EndTime).isSame(endTime)
),
);

updatingSchedules[indexOfSchedule] = {
...schedule,
isUpdated: true,
};

setSchedules(updatingSchedules);
};

// Time slot rendering logic with booking constraint
const renderTimeSlots = () => {
return selectedDateSchedule?.timeSlots?.map((i, index) => {
const isBooked = i?.booking_count > 0;

return {
index,
startTime: i?.StartTime,
endTime: i?.EndTime,
isDisabled: isBooked,
opacity: isBooked ? 0.5 : 1,
onStartTimeChange: (date) => onUpdateSlotStart(date, index),
onEndTimeChange: (date) => onUpdateSlotEnd(date, index),
onDelete: () => deleteSlot(i.StartTime, i.EndTime)
};
});
};

🔄 Month Picker Integration

Month Navigation:

// Month picker for both weekly and monthly views
const handleMonthChange = (event: EventTypes, _date: Date) => {
setIsShowMonthPicker(false);
if (event === 'dateSetAction') {
setMonthPickerDate(_date);
setDatesOfTheWeeksInMonth(getDatesOfTheWeeksInMonth(_date));
}
};

// Month picker trigger logic
const handleMonthPickerToggle = () => {
setIsShowMonthPicker(!isShowMonthPicker);
};

// Month picker modal logic
const shouldShowMonthPicker = isShowMonthPicker;
const monthPickerConfig = {
onChange: handleMonthChange,
value: monthPickerDate,
minimumDate: new Date(),
okButton: "Confirm"
};

📱 View Toggle Functionality

Weekly to Monthly Toggle:

// View toggle logic
const handleViewToggle = () => {
toggleMonthlyView();
};

// Arrow button configurations
const downArrowConfig = {
style: styles.downArrowButton,
onPress: handleViewToggle,
icon: 'ChevronDown',
size: theme.spacing.xl,
color: theme.colors.secondary4D
};

const upArrowConfig = {
style: styles.upArrowButton,
onPress: handleViewToggle,
icon: 'ChevronUp',
size: theme.spacing.xl,
color: theme.colors.secondary4D
};

🔧 GraphQL Integration

Schedule Queries

// Get schedules for therapist
const {
data: scheduleData,
loading: getScheduleLoading,
refetch,
error,
} = useGetSchedulesQuery({
variables: {
therapist_id: therapistId,
date: format(dayjs(today), 'YYYY-MM-DD'),
},
fetchPolicy: 'cache-and-network',
});

Schedule Mutations

// Add schedule mutation
const [sendSchedule, { loading }] = useAddScheduleMutation({
errorPolicy: 'all',
onCompleted: data => {
if (data?.create_therapist_schedule[0]?.id) {
refetch();
toast.show({ text: t('createSuccess') });
setModalVisible(true);
setIsAddSchedule(false);
setIsEditSchedule(false);
}
},
onError: e => {
toast.show({ text: e.message });
},
});

🎨 UI Components

Time Picker Component

// TimePickerBox component configuration for time selection
const startTimePickerConfig = {
style: commonFlexStyles.FLEX,
label: t('startTime'),
onTimeSelect: (date) => onStartTimeSelect(date),
selectedDate: selectedDate
};

const endTimePickerConfig = {
style: commonFlexStyles.FLEX,
label: t('endTime'),
onTimeSelect: (date) => onEndTimeSelect(date),
isForEndTime: true,
selectedDate: startTime ?? selectedDate,
startTime: startTime,
isDisabled: !startTime
};

Success Modal

// Success modal configuration after schedule creation
const successModalConfig = {
modalVisible: modalVisible,
setModalVisible: setModalVisible,
title: t('scheduleCreated'),
subtitle: t('successSubtitle'),
ctaText: t('goToSchedule'),
ctaLink: "therapist/schedule",
image: 'Thumb',
navigation: navigation
};

🎯 Best Practices

Schedule Management

  • Appointment Constraints: Always check booking_count before allowing edits/deletes
  • Time Validation: Ensure end time is after start time
  • Date Selection: Disable past dates for schedule creation
  • Visual Indicators: Use orange dots to show days with existing schedules

User Experience

  • Month Navigation: Provide easy month picker for date navigation
  • View Toggle: Smooth transition between weekly and monthly views
  • Bulk Operations: Allow selection of multiple weekdays or entire month
  • Success Feedback: Show confirmation modals after successful operations

Performance

  • Data Fetching: Use cache-and-network policy for real-time updates
  • State Management: Use cloneDeep for immutable state updates
  • Error Handling: Provide user-friendly error messages
  • Loading States: Show loading indicators during operations

🔧 Key Implementation Details

Appointment Constraints

// Check if time slot has bookings before allowing modifications
isDisabled={i?.booking_count > 0}
disabled={i?.booking_count > 0}
style={{ opacity: i?.booking_count > 0 ? 0.5 : 1 }}

State Management

// Use cloneDeep for immutable updates
const updatingSchedules = cloneDeep(schedules);
schedule.timeSlots[rowIndex].StartTime = time;
updatingSchedules[indexOfSchedule] = {
...schedule,
isUpdated: true,
};
setSchedules(updatingSchedules);

Visual Feedback

// Orange dot indicator logic for days with schedules
const hasScheduleIndicator = selectedDaySchedule?.timeSlots?.length > 0;
const dotIndicatorConfig = {
backgroundColor: "primary36",
width: 5,
height: 5,
borderRadius: "full",
marginTop: "xxxxs"
};

🎯 Next Steps