Przejdź do głównej zawartości

Calendar

Na tej stronie
KomendaOpisTyp
create_appointmentTworzy wizytę w kalendarzuwrite
update_appointmentAktualizuje wizytęwrite
get_appointmentsPobiera wizyty wg filtraquery
cancel_appointmentAnuluje wizytęwrite
complete_appointmentOznacza jako zrealizowanąwrite
delete_appointmentUsuwa wizytęwrite
KomendaOpisTyp
create_time_blockTworzy blokadę czasowąwrite
update_time_blockAktualizuje blokadęwrite
get_time_blocksPobiera blokady wg filtraquery
delete_time_blockUsuwa blokadęwrite

erDiagram
APPOINTMENT {
string appointment_id PK
string patient_id FK
string veterinarian_id FK
string visit_id FK "Opcjonalnie - po realizacji"
string parent_appointment_id FK "Dla serii"
date scheduled_date "YYYY-MM-DD"
time scheduled_time "HH:MM"
int duration_minutes
string appointment_type "consultation, vaccination, surgery..."
string status "scheduled, completed, cancelled, no_show"
text notes
boolean is_recurring
string recurrence_pattern "weekly, monthly..."
date recurrence_end_date
boolean reminder_enabled
boolean reminder_email_sent
timestamp reminder_email_sent_at
string[] additional_staff_ids "JSON array"
timestamp created_at
timestamp updated_at
string created_by
string updated_by
}
PATIENT ||--o{ APPOINTMENT : "has"
USER ||--o{ APPOINTMENT : "conducts"
VISIT ||--o| APPOINTMENT : "realized as"

stateDiagram-v2
[*] --> scheduled: Utworzenie
scheduled --> completed: Realizacja
scheduled --> cancelled: Anulowanie
scheduled --> no_show: Niestawienie się
completed --> [*]
cancelled --> [*]
no_show --> scheduled: Przełożenie
note right of scheduled: Można edytować
note right of completed: Powiązana z Visit

flowchart TB
subgraph CalendarView["CalendarView"]
MONTH[📅 Month View]
WEEK[📆 Week View]
DAY[📋 Day View]
end
subgraph Data["Źródła danych"]
APPTS[Appointments]
BLOCKS[Time Blocks]
PATIENTS[Patients]
VETS[Veterinarians]
end
subgraph Hooks["Hooki"]
useAppointmentsList
useCalendarData
useCalendarState
useCalendarMutations
end
CalendarView --> Hooks
Hooks --> Data

Tworzy nową wizytę w kalendarzu:

// Frontend (src/services/appointmentsService.ts)
interface CreateAppointmentRequest {
patient_id: string;
veterinarian_id: string;
scheduled_date: string; // 'YYYY-MM-DD'
scheduled_time: string; // 'HH:MM'
duration_minutes: number; // default: 30
appointment_type: string; // 'consultation', 'vaccination', etc.
notes?: string;
reminder_enabled?: boolean; // default: true
additional_staff_ids?: string[];
// Dla serii
is_recurring?: boolean;
recurrence_pattern?: 'weekly' | 'biweekly' | 'monthly';
recurrence_end_date?: string;
}
const appointment = await appointmentsService.createAppointment(
request,
userId,
sessionId
);
// Wewnętrznie:
// safeInvoke('create_appointment', withSessionPayload({ args: { request, user_id } }, sessionId))

Pobiera wizyty z filtrami:

interface GetAppointmentsFilter {
veterinarian_id?: string;
patient_id?: string;
start_date?: string; // 'YYYY-MM-DD'
end_date?: string;
status?: 'scheduled' | 'completed' | 'cancelled' | 'no_show';
user_id?: string; // Dla filtrowania po zalogowanym
is_admin?: boolean; // Admin widzi wszystkie
}
const appointments = await appointmentsService.getAppointments(
filter,
sessionId
);
// Response
interface Appointment {
appointment_id: string;
patient_id: string;
veterinarian_id: string;
visit_id?: string;
scheduled_date: string;
scheduled_time: string;
duration_minutes: number;
appointment_type: string;
status: 'scheduled' | 'completed' | 'cancelled' | 'no_show';
notes?: string;
is_recurring: boolean;
recurrence_pattern?: string;
reminder_enabled: boolean;
// Joined fields
patient_name?: string;
veterinarian_name?: string;
owner_phone?: string;
additional_staff_names?: string[];
}

Aktualizuje wizytę (np. przesunięcie, zmiana lekarza):

interface UpdateAppointmentRequest {
appointment_id: string;
scheduled_date?: string;
scheduled_time?: string;
duration_minutes?: number;
veterinarian_id?: string;
appointment_type?: string;
notes?: string;
status?: 'scheduled' | 'completed' | 'cancelled' | 'no_show';
additional_staff_ids?: string[];
}
await appointmentsService.updateAppointment(request, userId, sessionId);

Anuluje wizytę:

await appointmentsService.cancelAppointment(
appointmentId,
userId,
sessionId
);
// status → 'cancelled'

Oznacza wizytę jako zrealizowaną i powiązuje z Visit:

await appointmentsService.completeAppointment(
appointmentId,
visitId, // Powiązana wizyta medyczna
userId,
sessionId
);
// status → 'completed'
// visit_id → visitId

Usuwa wizytę (hard delete):

await appointmentsService.deleteAppointment(appointmentId, sessionId);

Blokady czasowe służą do oznaczania niedostępności lekarza (urlop, spotkanie, przerwa):

erDiagram
TIME_BLOCK {
string time_block_id PK
string user_id FK "Lekarz"
string title
date start_date
date end_date
time start_time
time end_time
string reason
boolean recurring
string[] recurring_days "JSON: ['monday', 'wednesday']"
timestamp created_at
timestamp updated_at
string created_by
}
USER ||--o{ TIME_BLOCK : "has"

interface CreateTimeBlockRequest {
title: string;
start_date: string; // 'YYYY-MM-DD'
end_date: string;
start_time: string; // 'HH:MM'
end_time: string;
reason?: string;
recurring?: boolean;
recurring_days?: ('monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday')[];
}
const timeBlock = await timeBlockService.createTimeBlock(
request,
userId
);

interface TimeBlockFilter {
start_date?: string;
end_date?: string;
date_range?: { start: string; end: string };
}
const timeBlocks = await timeBlockService.getTimeBlocks(filter);
// Cache lokalny (TTL 30s) dla getTimeBlocksForDate

interface UpdateTimeBlockRequest {
time_block_id: string;
title?: string;
start_date?: string;
end_date?: string;
start_time?: string;
end_time?: string;
reason?: string;
recurring?: boolean;
recurring_days?: string[];
}
await timeBlockService.updateTimeBlock(request, userId);

await timeBlockService.deleteTimeBlock(timeBlockId, userId);

Serwis sprawdza konflikty przed tworzeniem wizyty:

src/services/appointmentsService.ts
const conflicts = await appointmentsService.checkForConflicts(
veterinarianId,
date,
time,
durationMinutes,
excludeAppointmentId // Opcjonalne - przy edycji
);
// Sprawdza:
// 1. Istniejące appointments w tym samym czasie
// 2. Time blocks (niedostępność)
// Zwraca: Appointment[] - lista konfliktujących
// Sprawdza czy slot jest zablokowany
const isBlocked = await timeBlockService.isTimeSlotBlocked(
veterinarianId,
date,
startTime,
endTime
);
// Sync wersja (z cache)
const isBlockedSync = timeBlockService.isTimeSlotBlockedSync(
veterinarianId,
date,
startTime,
endTime
);

src/hooks/appointments/useAppointmentsList.ts
const {
appointments,
loading,
error,
refreshAppointments
} = useAppointmentsList();
// Automatycznie:
// - Filtruje po user_id (lub wszystkie dla admina)
// - Mapuje ServiceAppointment → UI Appointment
// - Dodaje title = appointment_type + patient_name

src/components/calendar/hooks/useCalendarData.ts
const {
patients, // CalendarDataPatient[]
veterinarians, // CalendarDataUser[]
loading
} = useCalendarData();
// Pobiera:
// - patientService.getAllPatients()
// - userService.getActiveVeterinarians()

src/components/calendar/hooks/useCalendarState.ts
const {
view, // 'month' | 'week' | 'day'
currentDate,
selectedDay,
selectedAppointment,
navigatePrevious,
navigateNext,
navigateToday,
handleViewChange,
handleDayClick,
handleAppointmentClick,
} = useCalendarState({ onDayClick, onAppointmentClick, onViewChange });

src/components/calendar/hooks/useCalendarMutations.ts
const {
deleteAppointment,
exportCalendar,
updateAppointment,
} = useCalendarMutations(userId, appointments);

Kalendarz obsługuje przeciąganie wizyt:

CalendarView.tsx
const handleAppointmentMove = async (
appointmentId: string,
newDate: Date,
newTime: string
) => {
if (!user) {
notify('calendar:userNotAuthenticated');
return;
}
const formattedDate = formatSafe(newDate, 'yyyy-MM-dd');
await appointmentsService.updateAppointment(
{
appointment_id: appointmentId,
scheduled_date: formattedDate,
scheduled_time: newTime,
},
user.user_id
);
notify('calendar:appointmentMovedSuccess');
refreshAppointments();
};

Tworzenie serii wizyt:

CalendarView.tsx
const handleRecurringAppointmentCreate = async (
newAppointments: Partial<Appointment>[]
) => {
for (const apt of newAppointments) {
await appointmentsService.createAppointment(
{
patient_id: apt.patient_id!,
veterinarian_id: apt.veterinarian_id!,
scheduled_date: apt.scheduled_date!,
scheduled_time: apt.scheduled_time!,
duration_minutes: apt.duration_minutes!,
appointment_type: apt.appointment_type!,
notes: apt.notes,
is_recurring: true,
recurrence_pattern: apt.recurrence_pattern,
reminder_enabled: true,
},
user.user_id
);
}
notify('calendar:recurringAppointmentsCreatedSuccess');
refreshAppointments();
};

Eksport kalendarza do formatu iCal:

src/services/exportService.ts
await exportService.exportCalendarToICal(appointments);
// Generuje plik .ics z VEVENT dla każdego spotkania:
// - DTSTART, DTEND
// - SUMMARY (appointment_type + patient_name)
// - DESCRIPTION (notes)
// - STATUS
// Zapisuje przez Tauri dialog (saveFile)

Dialog do tworzenia wizyt:

CalendarView.tsx
<AppointmentScheduler
patients={patients}
veterinarians={veterinarians}
existingAppointments={appointments.map(a => ({
date: a.scheduled_date,
duration: a.duration_minutes
}))}
prefilledDate={schedulerState.date}
prefilledTime={schedulerState.time}
onSchedule={handleAppointmentCreated}
onCancel={() => setSchedulerOpen(false)}
onPatientCreated={handlePatientCreated}
/>

KodOpisRozwiązanie
appointment_not_foundWizyta nie istniejeSprawdź ID
time_slot_conflictKonflikt czasowyWybierz inny termin
time_slot_blockedLekarz niedostępnySprawdź blokady
patient_not_foundPacjent nie istniejeSprawdź patient_id
veterinarian_not_foundLekarz nie istniejeSprawdź veterinarian_id
invalid_date_formatNieprawidłowy format datyUżyj YYYY-MM-DD
invalid_time_formatNieprawidłowy format czasuUżyj HH:MM
no_such_columnBrak migracji DBZaktualizuj bazę danych


Appointment może być konwertowany na pełną wizytę:

sequenceDiagram
participant User
participant Calendar as CalendarView
participant Hook as useAppointmentForm
participant API as appointmentApi
participant VisitAPI as visitApi
participant DB as SQLite
User->>Calendar: Click appointment
Calendar->>Hook: openAppointment(id)
Hook->>API: getAppointment(id)
API->>DB: SELECT * FROM appointments
DB-->>API: Appointment data
API-->>Hook: appointment
User->>Calendar: Start Visit
Calendar->>VisitAPI: createVisit({patient_id, appointment_id})
VisitAPI->>DB: INSERT INTO visits
DB-->>VisitAPI: visit_id
VisitAPI->>API: updateAppointment({visit_id, status: 'in_progress'})
API->>DB: UPDATE appointments SET visit_id = ?
DB-->>API: OK
VisitAPI-->>Calendar: Visit created
Calendar->>Calendar: Navigate to VisitEditor

MetricValue
Files~25
LOC~15,000
Tauri Commands10
Key HookuseAppointmentForm

HookLOCOpis
useAppointmentForm~400CRUD operations, validation
useAppointmentsList~200List & filtering
useTimeBlocks~150Time block management
useCalendarDragDrop~300Drag & drop logic