Schedule
This document describes the Schedule feature in the Web/Admin application and how it maps to the backend Schedule Module. Use this as a guide for product behavior, UI flows, and the underlying API interactions with critical business logic for availability management, conflict detection, and appointment scheduling constraints.
The admin/web app provides operational tooling for:
- Managing therapist availability windows and time blocks with simple date-based rules
- Viewing calendar slots with real-time availability status and conflict indicators
- Blocking/unblocking time periods for vacations, emergencies, and maintenance
- Generating available slots based on existing appointments and availability windows
The backend reference for all rules and endpoints is documented in the backend guide: "Schedule Module." The web layer consumes these via the SDK/GraphQL and/or HTTP endpoints exposed by the backend.
Core Concepts (from Backend)
- Availability Windows: Simple date-based availability blocks without complex policies
- Slot Generation: Basic slot creation based on availability and existing appointments
- Conflict Detection: Simple validation against existing appointments and blocks
- Time Zone Management: Automatic conversion between therapist and system time zones
- Block Management: Vacation blocks, emergency blocks, and maintenance periods
- Visual Indicators: Dots shown only when dates have availability or appointments
Web/Admin Capabilities
1) Calendar View & Filters
Business Rules:
- Simple Filtering: Filter by therapist and date range only
- Visual Indicators: Dots shown only when dates have availability or appointments
- Slot Status: Slots are either available (no appointment) or booked (has appointment)
- No Complex Policies: No buffer times, break periods, or complex conflict detection
Implementation:
// Simple filter logic without complex policies
const filtered = slots.filter((slot) => {
// Basic filtering
const matchesTherapist = !therapistId || slot.therapistId === therapistId;
const matchesDateRange =
!dateRange ||
(slot.start_time >= dateRange.start && slot.start_time <= dateRange.end);
// Simple availability check
slot.isAvailable = !slot.hasAppointment;
slot.isBooked = slot.hasAppointment;
return matchesTherapist && matchesDateRange;
});
// Simple availability calculation
const calculateAvailability = (slot) => {
// Check if slot is within therapist's availability window
const withinAvailability = slot.therapist.availability_windows.some(
(window) => isTimeWithinWindow(slot.start_time, window),
);
// Check if slot has existing appointment
const hasAppointment = slot.existing_appointments.length > 0;
// Check if slot is blocked
const isBlocked = slot.blocks.some((block) =>
isTimeWithinBlock(slot.start_time, block),
);
return withinAvailability && !hasAppointment && !isBlocked;
};
// Simple conflict detection - only check for appointment overlap
const detectConflicts = (slot) => {
const conflicts = [];
// Only check for overlapping appointments
const overlappingAppointments = slot.existing_appointments.filter(
(appointment) =>
isTimeOverlapping(
slot.start_time,
slot.end_time,
appointment.start_time,
appointment.end_time,
),
);
if (overlappingAppointments.length > 0) {
conflicts.push({
type: 'APPOINTMENT_OVERLAP',
appointments: overlappingAppointments,
});
}
return conflicts;
};
// Visual indicator logic - show dots only when dates have content
const shouldShowDot = (date) => {
const hasAvailability = date.availability_windows.length > 0;
const hasAppointments = date.appointments.length > 0;
const hasBlocks = date.blocks.length > 0;
return hasAvailability || hasAppointments || hasBlocks;
};
2) Define Availability (GraphQL)
Availability Business Rules:
- Simple Date-Based: Basic availability windows for specific dates and times
- No Complex Policies: No minimum duration, maximum daily hours, or buffer time requirements
- Time Zone Handling: Automatic conversion between therapist and system time zones
- Basic Overlap Check: Simple validation to prevent overlapping availability windows
Availability Validation Logic:
// Simple availability window validation
const validateAvailabilityWindow = (availability, existingWindows) => {
const errors = [];
// Basic time validation
if (availability.start_time >= availability.end_time) {
errors.push('Start time must be before end time');
}
// Check for overlaps with existing windows
const overlaps = existingWindows.filter((window) =>
isTimeOverlapping(
availability.start_time,
availability.end_time,
window.start_time,
window.end_time,
),
);
if (overlaps.length > 0) {
errors.push('Availability window overlaps with existing schedule');
}
// Check time zone consistency
if (!isValidTimeZone(availability.timezone)) {
errors.push('Invalid time zone specified');
}
return errors;
};
// Simple time overlap check
const isTimeOverlapping = (start1, end1, start2, end2) => {
return start1 < end2 && start2 < end1;
};
// Basic duration calculation
const calculateDuration = (startTime, endTime) => {
return (new Date(endTime) - new Date(startTime)) / (1000 * 60); // minutes
};
GraphQL Implementation:
import { gql, ApolloClient } from '@apollo/client';
const SET_AVAILABILITY = gql`
mutation SetAvailability($input: SetAvailabilityInput!) {
setAvailability(input: $input) {
success
message
data {
id
start_time
end_time
timezone
conflicts_detected
}
}
}
`;
async function setAvailability(
client: ApolloClient<unknown>,
input: SetAvailabilityInput,
) {
// Simple pre-validation
const existingWindows = await getTherapistAvailability(input.therapist_id);
const validationErrors = validateAvailabilityWindow(input, existingWindows);
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join(', ')}`);
}
// Convert time zone if necessary
const processedInput = {
...input,
start_time: convertToSystemTimezone(input.start_time, input.timezone),
end_time: convertToSystemTimezone(input.end_time, input.timezone),
};
const { data } = await client.mutate({
mutation: SET_AVAILABILITY,
variables: { input: processedInput },
});
if (!data?.setAvailability?.success) {
throw new Error(
data?.setAvailability?.message || 'Save availability failed',
);
}
return data.setAvailability.data;
}
3) Block/Unblock Management (Vacations, Exceptions)
Block Management Business Rules:
- Simple Block Types: Vacation, emergency, personal, maintenance
- Basic Conflict Handling: Simple notification when blocking time with existing appointments
- No Complex Resolution: No automatic rescheduling or compensation logic
- Manual Intervention: Admin/therapist handles appointment conflicts manually
Block Conflict Detection Logic:
// Simple conflict detection when blocking time
const detectBlockConflicts = async (blockInput, existingAppointments) => {
const conflicts = existingAppointments.filter((appointment) =>
isTimeOverlapping(
blockInput.start_time,
blockInput.end_time,
appointment.start_time,
appointment.end_time,
),
);
return conflicts.map((appointment) => ({
appointment_id: appointment.id,
appointment_time: appointment.start_time,
conflict_type: 'TIME_OVERLAP',
requires_manual_resolution: true,
}));
};
// Simple block validation
const validateBlock = (blockInput) => {
const errors = [];
// Basic time validation
if (blockInput.start_time >= blockInput.end_time) {
errors.push('Start time must be before end time');
}
// Check if block is in the past
if (new Date(blockInput.start_time) < new Date()) {
errors.push('Cannot block time in the past');
}
return errors;
};
GraphQL Implementation:
const BLOCK = gql`
mutation BlockWindow($input: BlockWindowInput!) {
blockWindow(input: $input) {
success
message
data {
id
conflicts_detected
affected_appointments {
id
appointment_time
requires_manual_resolution
}
}
}
}
`;
const UNBLOCK = gql`
mutation UnblockWindow($input: UnblockWindowInput!) {
unblockWindow(input: $input) {
success
message
data {
id
availability_restored
}
}
}
`;
async function blockWindow(
client: ApolloClient<unknown>,
input: BlockWindowInput,
) {
// Simple pre-check for conflicts
const existingAppointments = await getAppointmentsInTimeRange(
input.therapist_id,
input.start_time,
input.end_time,
);
const conflicts = await detectBlockConflicts(input, existingAppointments);
const { data } = await client.mutate({
mutation: BLOCK,
variables: {
input: {
...input,
conflicts_detected: conflicts,
},
},
});
if (!data?.blockWindow?.success) {
throw new Error(data?.blockWindow?.message || 'Block failed');
}
return data.blockWindow.data;
}
async function unblockWindow(client: ApolloClient<unknown>, blockId: string) {
const { data } = await client.mutate({
mutation: UNBLOCK,
variables: { input: { block_id: blockId } },
});
if (!data?.unblockWindow?.success) {
throw new Error(data?.unblockWindow?.message || 'Unblock failed');
}
return data.unblockWindow.data;
}
4) Slot Generation (Simple Algorithm)
Slot Generation Business Rules:
- Simple Generation: Basic slot creation based on availability windows and existing appointments
- Real-time Updates: Slots reflect current availability and appointment status
- Time Zone Handling: Slots displayed in user's local time zone
- Visual Indicators: Dots shown only when dates have availability or appointments
Slot Generation Algorithm:
// Simple slot generation logic
const generateSlots = (therapist, dateRange, slotDuration = 30) => {
const slots = [];
const availabilityWindows = getAvailabilityWindows(therapist.id, dateRange);
const existingAppointments = getAppointments(therapist.id, dateRange);
const blocks = getBlocks(therapist.id, dateRange);
for (const window of availabilityWindows) {
const windowSlots = generateSlotsInWindow(window, slotDuration);
for (const slot of windowSlots) {
// Simple availability check
const isAvailable = isSlotAvailable(slot, existingAppointments, blocks);
if (isAvailable) {
// Add time zone information
slot.localTime = convertToUserTimezone(slot.start_time, slot.timezone);
slot.isBooked = false;
slots.push(slot);
}
}
}
return slots.sort((a, b) => a.start_time.localeCompare(b.start_time));
};
// Simple slot availability check
const isSlotAvailable = (slot, appointments, blocks) => {
// Check for appointment conflicts
const hasAppointmentConflict = appointments.some((appointment) =>
isTimeOverlapping(
slot.start_time,
slot.end_time,
appointment.start_time,
appointment.end_time,
),
);
// Check for block conflicts
const hasBlockConflict = blocks.some((block) =>
isTimeOverlapping(
slot.start_time,
slot.end_time,
block.start_time,
block.end_time,
),
);
return !hasAppointmentConflict && !hasBlockConflict;
};
// Generate slots within a time window
const generateSlotsInWindow = (window, slotDuration) => {
const slots = [];
const startTime = new Date(window.start_time);
const endTime = new Date(window.end_time);
let currentTime = new Date(startTime);
while (currentTime < endTime) {
const slotEndTime = new Date(currentTime.getTime() + slotDuration * 60000);
if (slotEndTime <= endTime) {
slots.push({
start_time: currentTime.toISOString(),
end_time: slotEndTime.toISOString(),
duration: slotDuration,
});
}
currentTime = slotEndTime;
}
return slots;
};
GraphQL Implementation:
const LIST_SLOTS = gql`
query Slots(
$therapistId: uuid!
$from: timestamptz!
$to: timestamptz!
$slotDuration: Int
) {
slots(
therapist_id: $therapistId
from: $from
to: $to
slot_duration: $slotDuration
) {
id
start_time
end_time
isBooked
local_time
timezone
has_appointment
}
}
`;
async function fetchSlots(
client: ApolloClient<unknown>,
therapistId: string,
fromISO: string,
toISO: string,
slotDuration: number = 30,
) {
const { data } = await client.query({
query: LIST_SLOTS,
variables: {
therapistId,
from: fromISO,
to: toISO,
slotDuration,
},
fetchPolicy: 'cache-and-network', // Real-time updates
});
// Simple processing - no complex logic
const processedSlots = data.slots.map((slot) => ({
...slot,
isAvailable: !slot.isBooked && !slot.has_appointment,
displayTime: formatSlotTime(slot.local_time, slot.timezone),
}));
return processedSlots;
}
5) Export & Analytics
Export Business Rules:
- Simple Schedule Reports: Basic availability, blocks, and appointment data
- Basic Utilization: Simple calculation of available vs booked hours
- No Complex Analytics: No advanced conflict analysis or pattern recognition
- Basic Time Zone Support: Simple time zone conversion for reports
Export Implementation:
// Generate simple schedule report
const generateScheduleReport = (therapist, dateRange) => {
const availability = getAvailabilityWindows(therapist.id, dateRange);
const appointments = getAppointments(therapist.id, dateRange);
const blocks = getBlocks(therapist.id, dateRange);
// Simple utilization calculation
const totalAvailableHours = calculateTotalHours(availability);
const totalBookedHours = calculateTotalHours(appointments);
const utilizationRate =
totalAvailableHours > 0
? (totalBookedHours / totalAvailableHours) * 100
: 0;
return {
therapist_name: therapist.name,
date_range: dateRange,
availability_summary: {
total_hours: totalAvailableHours,
booked_hours: totalBookedHours,
utilization_rate: Math.round(utilizationRate * 100) / 100,
},
blocks_summary: blocks.map((block) => ({
type: block.type,
duration: calculateDuration(block.start_time, block.end_time),
reason: block.reason,
})),
appointments_count: appointments.length,
availability_windows_count: availability.length,
};
};
// Simple CSV export
const csv = toCsv(generateScheduleReport(therapist, dateRange));
downloadCsv('therapist-schedule-report.csv', csv);
Data Flow (Web ↔ Backend)
Critical Business Logic Flow:
- Availability Setup: Therapist sets availability → Basic validation → Time zone conversion
- Block Management: Create blocks → Simple conflict detection → Manual resolution required
- Slot Generation: Server calculates slots → Availability + blocks + appointments → Real-time updates
- Visual Indicators: Dots shown only when dates have availability or appointments
GraphQL Operations with Business Rules:
| Action | Operation | Business Rules |
|---|---|---|
| Set Availability | setAvailability(input) | Basic overlap validation, time zone handling |
| Block Window | blockWindow(input) | Simple conflict detection, manual resolution required |
| Unblock Window | unblockWindow(input) | Availability restoration |
| List Slots | slots(args) | Simple availability calculation, no complex policies |
Error Handling & Validation:
- Overlap Detection: "Availability window overlaps with existing schedule"
- Time Zone Error: "Invalid time zone specified"
- Block Conflict: "Cannot block time with existing appointments"
- Past Time Error: "Cannot block time in the past"
Security & Access Control:
- Therapist Access: Therapists can only manage their own availability and blocks
- Admin Override: Admins can manage any therapist's schedule for operational needs
- Audit Trail: All schedule changes are logged with timestamps and user information
- Data Privacy: Schedule data protected by role-based permissions
- Simple Conflict Handling: Basic conflict detection with manual resolution required