Skip to main content

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:

  1. Availability Setup: Therapist sets availability → Basic validation → Time zone conversion
  2. Block Management: Create blocks → Simple conflict detection → Manual resolution required
  3. Slot Generation: Server calculates slots → Availability + blocks + appointments → Real-time updates
  4. Visual Indicators: Dots shown only when dates have availability or appointments

GraphQL Operations with Business Rules:

ActionOperationBusiness Rules
Set AvailabilitysetAvailability(input)Basic overlap validation, time zone handling
Block WindowblockWindow(input)Simple conflict detection, manual resolution required
Unblock WindowunblockWindow(input)Availability restoration
List Slotsslots(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