Przejdź do głównej zawartości

Analytics Commands

Analytics API dostarcza dane do dashboardu, wykresów i raportów. Wszystkie komendy wymagają aktywnej sesji i obsługują deduplikację zapytań (cache).

Service: src/services/analyticsService.ts Types: src/types/analytics.ts Hook: src/components/analytics/dashboard/hooks/useAnalyticsData.ts


Każde zapytanie analityczne wymaga AnalyticsScope:

interface AnalyticsScope {
dateRange: {
start: Date;
end: Date;
};
clinicId?: string; // Z user.clinic_id
period?: AnalyticsPeriod; // 'week' | 'month' | 'year'
}
// FE → Backend payload
toScopePayload(scope) → {
clinic_id: scope.clinicId,
date_range: {
start: scope.dateRange.start.toISOString(),
end: scope.dateRange.end.toISOString()
},
period: scope.period // optional
}
// Cache key dla deduplikacji
analyticsScopeKey(payload) → "clinic:range:period"

// get_dashboard_stats
analyticsService.getDashboardStats(scope: AnalyticsScope): Promise<DashboardStats>
// Response
interface DashboardStats {
totalVisits: number;
totalPatients: number;
avgVisitDuration: number;
completionRate: number;
// ...
}

Usage: Dashboard główny (ostatnie 30 dni), zakładka Overview.


// get_visit_trends
analyticsService.getVisitTrends(
scope: AnalyticsScope & { period: AnalyticsPeriod }
): Promise<VisitTrend[]>
// Response
interface VisitTrend {
date: string;
count: number;
}

Aggregation: Trendy wizyt z agregacją wg period (tydzień/miesiąc/rok).


// get_visit_trends_period_compare
// Porównanie dwóch okresów z tą samą periodyzacją
analyticsService.getVisitTrendsPeriodCompare(
scope: AnalyticsScope & { period: AnalyticsPeriod }
): Promise<VisitTrendsCompare>
// get_visit_trends_compare
// Porównanie bez periodyzacji
analyticsService.getVisitTrendsCompare(
scope: AnalyticsScope
): Promise<VisitTrendsCompare>
// Response
interface VisitTrendsCompare {
current: VisitTrend[];
previous: VisitTrend[];
}

// get_visit_type_distribution
analyticsService.getVisitTypeDistribution(
scope: AnalyticsScope
): Promise<VisitTypeDistribution[]>
// Response (z kolorem dodanym przez FE)
interface VisitTypeDistribution {
type: string;
count: number;
percentage: number;
color: string; // Dodane przez getVisitTypeColor()
}

FE przypisuje kolory na podstawie typu wizyty:

const VISIT_TYPE_COLORS = {
consultation: 'var(--color-blue)',
vaccination: 'var(--color-green)',
surgery: 'var(--color-red)',
emergency: 'var(--color-orange)',
other: 'var(--color-gray)',
};
// Fuzzy mapping dla PL nazw
getVisitTypeColor('pierwsza wizyta') → VISIT_TYPE_COLORS.consultation
getVisitTypeColor('szczepienie') → VISIT_TYPE_COLORS.vaccination
getVisitTypeColor('usg') → VISIT_TYPE_COLORS.other

// get_staff_performance
analyticsService.getStaffPerformance(
scope: AnalyticsScope
): Promise<StaffPerformance[]>
// Response
interface StaffPerformance {
staffId: string;
staffName: string;
totalVisits: number;
avgDuration: number;
completionRate: number;
}

// get_staff_time_distribution
analyticsService.getStaffTimeDistribution(
scope: AnalyticsScope
): Promise<StaffTimeDistribution[]>
// Response
interface StaffTimeDistribution {
staffId: string;
staffName: string;
visitTypes: Array<{
type: string;
minutes: number;
}>;
}

// get_patient_demographics
analyticsService.getPatientDemographics(
scope: AnalyticsScope
): Promise<PatientDemographics[]>
// Response
interface PatientDemographics {
species: string;
count: number;
percentage: number;
}

// get_time_slot_analysis
analyticsService.getTimeSlotAnalysis(
scope: AnalyticsScope
): Promise<TimeSlotAnalysis[]>
// Response
interface TimeSlotAnalysis {
hour: number; // 0-23
count: number;
avgDuration: number;
}

// get_day_hour_heatmap
analyticsService.getDayHourHeatmap(
scope: AnalyticsScope
): Promise<DayHourHeatCell[]>
// Response
interface DayHourHeatCell {
day: number; // 0-6 (Sunday-Saturday)
hour: number; // 0-23
count: number;
}

// get_overview_bundle
// Zbiorczy pakiet do szybkiego ładowania zakładki Overview
analyticsService.getOverviewBundle(
scope: AnalyticsScope
): Promise<OverviewBundle>
// Response
interface OverviewBundle {
stats: DashboardStats;
compare: {
current: VisitTrend[];
previous: VisitTrend[];
};
}

// get_revenue_data
// Jedyna komenda bez AnalyticsScope
analyticsService.getRevenueData(months: number = 12): Promise<RevenueData[]>
// Response
interface RevenueData {
month: string; // "2024-01", "2024-02", ...
revenue: number;
visitCount: number;
}

Plik: src/components/analytics/dashboard/hooks/useAnalyticsData.ts

useAnalyticsData({
dateRange: { start: Date; end: Date },
activeTab: 'overview' | 'visits' | 'patients' | 'performance'
})
flowchart TB
subgraph "Per Tab Loading"
overview[Overview Tab] --> getOverviewBundle
visits[Visits Tab] --> getVisitTypeDistribution
visits --> getTimeSlotAnalysis
patients[Patients Tab] --> getPatientDemographics
performance[Performance Tab] --> getStaffPerformance
performance --> getDayHourHeatmap
performance --> getStaffTimeDistribution
end
DataTransformation
visitTypestranslateVisitType(item.type, t)
patientDemographicstranslateSpecies(item.species)
staffTimeDistributionLokalizacja visitTypes[].type
// Helpery do generowania insightów
getPeakHourInsight(timeSlots) // "Najwięcej wizyt o 10:00"
getBestDayHourInsight(heatmap) // "Najlepszy dzień: Poniedziałek 9:00-11:00"

Wszystkie komendy używają dedupeGet z kluczem cache:

// Pattern
analytics:<sessionId>:<endpoint>:<scopeKey>
// Przykłady
analytics:abc123:dashboard:clinic1:2024-01-01:2024-01-31:month
analytics:abc123:visitTypeDistribution:clinic1:2024-01-01:2024-01-31
analytics:abc123:revenue:12

try {
const stats = await analyticsService.getDashboardStats(scope);
} catch (error) {
if (error instanceof SessionNotReadyError) {
// Propagowane dalej
throw error;
}
// Inne błędy logowane i rzucane
secureLogger.error('[Analytics] getDashboardStats failed', error);
throw error;
}

ComponentData Source
Dashboard.tsxgetDashboardStats (30 dni)
AnalyticsDashboard.tsxuseAnalyticsData
KpiSummary.tsxstats z useAnalyticsData
PerformanceTablePanel.tsxstaffPerformance