@blackcode_sa/metaestetics-api 1.12.68 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/index.d.mts +801 -2
- package/dist/admin/index.d.ts +801 -2
- package/dist/admin/index.js +2332 -153
- package/dist/admin/index.mjs +2321 -153
- package/dist/index.d.mts +1057 -2
- package/dist/index.d.ts +1057 -2
- package/dist/index.js +4150 -2117
- package/dist/index.mjs +3832 -1810
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +140 -0
- package/src/admin/analytics/analytics.admin.service.ts +278 -0
- package/src/admin/analytics/index.ts +2 -0
- package/src/admin/index.ts +6 -0
- package/src/backoffice/services/README.md +17 -0
- package/src/backoffice/services/analytics.service.proposal.md +863 -0
- package/src/backoffice/services/analytics.service.summary.md +143 -0
- package/src/services/analytics/ARCHITECTURE.md +199 -0
- package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -0
- package/src/services/analytics/GROUPED_ANALYTICS.md +501 -0
- package/src/services/analytics/QUICK_START.md +393 -0
- package/src/services/analytics/README.md +287 -0
- package/src/services/analytics/SUMMARY.md +141 -0
- package/src/services/analytics/USAGE_GUIDE.md +518 -0
- package/src/services/analytics/analytics-cloud.service.ts +222 -0
- package/src/services/analytics/analytics.service.ts +1632 -0
- package/src/services/analytics/index.ts +3 -0
- package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -0
- package/src/services/analytics/utils/cost-calculation.utils.ts +154 -0
- package/src/services/analytics/utils/grouping.utils.ts +394 -0
- package/src/services/analytics/utils/stored-analytics.utils.ts +347 -0
- package/src/services/analytics/utils/time-calculation.utils.ts +186 -0
- package/src/services/appointment/appointment.service.ts +50 -6
- package/src/services/index.ts +1 -0
- package/src/types/analytics/analytics.types.ts +500 -0
- package/src/types/analytics/grouped-analytics.types.ts +148 -0
- package/src/types/analytics/index.ts +4 -0
- package/src/types/analytics/stored-analytics.types.ts +137 -0
- package/src/types/index.ts +3 -0
- package/src/types/notifications/index.ts +21 -0
package/package.json
CHANGED
|
@@ -37,6 +37,7 @@ import { AppointmentMailingService } from '../../mailing/appointment/appointment
|
|
|
37
37
|
import { Logger } from '../../logger';
|
|
38
38
|
import { UserRole } from '../../../types';
|
|
39
39
|
import { CalendarEventStatus } from '../../../types/calendar';
|
|
40
|
+
import { NotificationType } from '../../../types/notifications';
|
|
40
41
|
|
|
41
42
|
// Mailgun client will be injected via constructor
|
|
42
43
|
|
|
@@ -540,6 +541,13 @@ export class AppointmentAggregationService {
|
|
|
540
541
|
await this.handleZonePhotosUpdate(before, after);
|
|
541
542
|
}
|
|
542
543
|
|
|
544
|
+
// Handle Recommended Procedures Added
|
|
545
|
+
const recommendationsChanged = this.hasRecommendationsChanged(before, after);
|
|
546
|
+
if (recommendationsChanged) {
|
|
547
|
+
Logger.info(`[AggService] Recommended procedures changed for appointment ${after.id}`);
|
|
548
|
+
await this.handleRecommendedProceduresUpdate(before, after, patientProfile);
|
|
549
|
+
}
|
|
550
|
+
|
|
543
551
|
// TODO: Handle Review Added
|
|
544
552
|
// const reviewAdded = !before.reviewInfo && after.reviewInfo;
|
|
545
553
|
// if (reviewAdded) { ... }
|
|
@@ -1841,4 +1849,136 @@ export class AppointmentAggregationService {
|
|
|
1841
1849
|
// Don't throw - this is a side effect and shouldn't break the main update flow
|
|
1842
1850
|
}
|
|
1843
1851
|
}
|
|
1852
|
+
|
|
1853
|
+
/**
|
|
1854
|
+
* Checks if recommended procedures have changed between two appointment states
|
|
1855
|
+
* @param before - The appointment state before update
|
|
1856
|
+
* @param after - The appointment state after update
|
|
1857
|
+
* @returns True if recommendations have changed, false otherwise
|
|
1858
|
+
*/
|
|
1859
|
+
private hasRecommendationsChanged(before: Appointment, after: Appointment): boolean {
|
|
1860
|
+
const beforeRecommendations = before.metadata?.recommendedProcedures || [];
|
|
1861
|
+
const afterRecommendations = after.metadata?.recommendedProcedures || [];
|
|
1862
|
+
|
|
1863
|
+
// If lengths differ, there's a change
|
|
1864
|
+
if (beforeRecommendations.length !== afterRecommendations.length) {
|
|
1865
|
+
return true;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// Compare each recommendation (simple comparison - if any differ, return true)
|
|
1869
|
+
// For simplicity, we compare by procedure ID and note
|
|
1870
|
+
for (let i = 0; i < afterRecommendations.length; i++) {
|
|
1871
|
+
const beforeRec = beforeRecommendations[i];
|
|
1872
|
+
const afterRec = afterRecommendations[i];
|
|
1873
|
+
|
|
1874
|
+
if (!beforeRec || !afterRec) {
|
|
1875
|
+
return true;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
if (
|
|
1879
|
+
beforeRec.procedure.procedureId !== afterRec.procedure.procedureId ||
|
|
1880
|
+
beforeRec.note !== afterRec.note ||
|
|
1881
|
+
beforeRec.timeframe.value !== afterRec.timeframe.value ||
|
|
1882
|
+
beforeRec.timeframe.unit !== afterRec.timeframe.unit
|
|
1883
|
+
) {
|
|
1884
|
+
return true;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
return false;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/**
|
|
1892
|
+
* Handles recommended procedures update - creates notifications for newly added recommendations
|
|
1893
|
+
* @param before - The appointment state before update
|
|
1894
|
+
* @param after - The appointment state after update
|
|
1895
|
+
* @param patientProfile - The patient profile (for expo tokens)
|
|
1896
|
+
*/
|
|
1897
|
+
private async handleRecommendedProceduresUpdate(
|
|
1898
|
+
before: Appointment,
|
|
1899
|
+
after: Appointment,
|
|
1900
|
+
patientProfile: PatientProfile | null,
|
|
1901
|
+
): Promise<void> {
|
|
1902
|
+
try {
|
|
1903
|
+
const beforeRecommendations = before.metadata?.recommendedProcedures || [];
|
|
1904
|
+
const afterRecommendations = after.metadata?.recommendedProcedures || [];
|
|
1905
|
+
|
|
1906
|
+
// Find newly added recommendations
|
|
1907
|
+
const newRecommendations = afterRecommendations.slice(beforeRecommendations.length);
|
|
1908
|
+
|
|
1909
|
+
if (newRecommendations.length === 0) {
|
|
1910
|
+
Logger.info(
|
|
1911
|
+
`[AggService] No new recommendations detected for appointment ${after.id}`,
|
|
1912
|
+
);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
Logger.info(
|
|
1917
|
+
`[AggService] Found ${newRecommendations.length} new recommendation(s) for appointment ${after.id}`,
|
|
1918
|
+
);
|
|
1919
|
+
|
|
1920
|
+
// Create notifications for each new recommendation
|
|
1921
|
+
for (let i = 0; i < newRecommendations.length; i++) {
|
|
1922
|
+
const recommendation = newRecommendations[i];
|
|
1923
|
+
const recommendationIndex = beforeRecommendations.length + i;
|
|
1924
|
+
const recommendationId = `${after.id}:${recommendationIndex}`;
|
|
1925
|
+
|
|
1926
|
+
// Format timeframe for display
|
|
1927
|
+
const timeframeText = `${recommendation.timeframe.value} ${recommendation.timeframe.unit}${recommendation.timeframe.value > 1 ? 's' : ''}`;
|
|
1928
|
+
|
|
1929
|
+
// Create notification
|
|
1930
|
+
const notificationPayload: Omit<
|
|
1931
|
+
any,
|
|
1932
|
+
'id' | 'createdAt' | 'updatedAt' | 'status' | 'isRead'
|
|
1933
|
+
> = {
|
|
1934
|
+
userId: after.patientId,
|
|
1935
|
+
userRole: UserRole.PATIENT,
|
|
1936
|
+
notificationType: NotificationType.PROCEDURE_RECOMMENDATION,
|
|
1937
|
+
notificationTime: admin.firestore.Timestamp.now(),
|
|
1938
|
+
notificationTokens: patientProfile?.expoTokens || [],
|
|
1939
|
+
title: 'New Procedure Recommendation',
|
|
1940
|
+
body: `${after.practitionerInfo?.name || 'Your doctor'} recommended "${recommendation.procedure.procedureName}" for you. Suggested timeframe: in ${timeframeText}`,
|
|
1941
|
+
appointmentId: after.id,
|
|
1942
|
+
recommendationId,
|
|
1943
|
+
procedureId: recommendation.procedure.procedureId,
|
|
1944
|
+
procedureName: recommendation.procedure.procedureName,
|
|
1945
|
+
practitionerName: after.practitionerInfo?.name || 'Unknown Practitioner',
|
|
1946
|
+
clinicName: after.clinicInfo?.name || 'Unknown Clinic',
|
|
1947
|
+
note: recommendation.note,
|
|
1948
|
+
timeframe: recommendation.timeframe,
|
|
1949
|
+
};
|
|
1950
|
+
|
|
1951
|
+
try {
|
|
1952
|
+
const notificationId = await this.notificationsAdmin.createNotification(
|
|
1953
|
+
notificationPayload as any,
|
|
1954
|
+
);
|
|
1955
|
+
|
|
1956
|
+
Logger.info(
|
|
1957
|
+
`[AggService] Created notification ${notificationId} for recommendation ${recommendationId}`,
|
|
1958
|
+
);
|
|
1959
|
+
|
|
1960
|
+
// Send push notification immediately if patient has tokens
|
|
1961
|
+
if (patientProfile?.expoTokens && patientProfile.expoTokens.length > 0) {
|
|
1962
|
+
const notification = await this.notificationsAdmin.getNotification(notificationId);
|
|
1963
|
+
if (notification) {
|
|
1964
|
+
await this.notificationsAdmin.sendPushNotification(notification);
|
|
1965
|
+
Logger.info(
|
|
1966
|
+
`[AggService] Sent push notification for recommendation ${recommendationId}`,
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
} catch (error) {
|
|
1971
|
+
Logger.error(
|
|
1972
|
+
`[AggService] Error creating notification for recommendation ${recommendationId}:`,
|
|
1973
|
+
error,
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
} catch (error) {
|
|
1978
|
+
Logger.error(
|
|
1979
|
+
`[AggService] Error handling recommended procedures update for appointment ${after.id}:`,
|
|
1980
|
+
error,
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1844
1984
|
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import * as admin from 'firebase-admin';
|
|
2
|
+
import { AnalyticsService } from '../../services/analytics/analytics.service';
|
|
3
|
+
import {
|
|
4
|
+
AnalyticsDateRange,
|
|
5
|
+
AnalyticsFilters,
|
|
6
|
+
EntityType,
|
|
7
|
+
} from '../../types/analytics';
|
|
8
|
+
import {
|
|
9
|
+
PractitionerAnalytics,
|
|
10
|
+
ProcedureAnalytics,
|
|
11
|
+
TimeEfficiencyMetrics,
|
|
12
|
+
CancellationMetrics,
|
|
13
|
+
NoShowMetrics,
|
|
14
|
+
RevenueMetrics,
|
|
15
|
+
ProductUsageMetrics,
|
|
16
|
+
PatientAnalytics,
|
|
17
|
+
ClinicAnalytics,
|
|
18
|
+
DashboardAnalytics,
|
|
19
|
+
GroupedRevenueMetrics,
|
|
20
|
+
GroupedProductUsageMetrics,
|
|
21
|
+
GroupedTimeEfficiencyMetrics,
|
|
22
|
+
GroupedPatientBehaviorMetrics,
|
|
23
|
+
} from '../../types/analytics';
|
|
24
|
+
import { Appointment } from '../../types/appointment';
|
|
25
|
+
import { APPOINTMENTS_COLLECTION } from '../../types/appointment';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Admin version of AnalyticsService that uses Firebase Admin SDK
|
|
29
|
+
* This is intended for use in Cloud Functions and server-side code
|
|
30
|
+
*/
|
|
31
|
+
export class AnalyticsAdminService {
|
|
32
|
+
private analyticsService: AnalyticsService;
|
|
33
|
+
private db: admin.firestore.Firestore;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates a new AnalyticsAdminService instance
|
|
37
|
+
*
|
|
38
|
+
* @param firestore - Admin Firestore instance (optional, defaults to admin.firestore())
|
|
39
|
+
*/
|
|
40
|
+
constructor(firestore?: admin.firestore.Firestore) {
|
|
41
|
+
this.db = firestore || admin.firestore();
|
|
42
|
+
|
|
43
|
+
// Create a mock Firebase App for client SDK compatibility
|
|
44
|
+
const mockApp = {
|
|
45
|
+
name: '[DEFAULT]',
|
|
46
|
+
options: {},
|
|
47
|
+
automaticDataCollectionEnabled: false,
|
|
48
|
+
} as any;
|
|
49
|
+
|
|
50
|
+
// Create mock Auth (not used by AnalyticsService)
|
|
51
|
+
const mockAuth = {} as any;
|
|
52
|
+
|
|
53
|
+
// Create AppointmentService adapter that uses admin SDK
|
|
54
|
+
const appointmentService = this.createAppointmentServiceAdapter();
|
|
55
|
+
|
|
56
|
+
// Initialize AnalyticsService with adapted dependencies
|
|
57
|
+
this.analyticsService = new AnalyticsService(
|
|
58
|
+
this.db as any, // Cast admin Firestore to client Firestore type
|
|
59
|
+
mockAuth,
|
|
60
|
+
mockApp,
|
|
61
|
+
appointmentService,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates an adapter for AppointmentService to work with admin SDK
|
|
67
|
+
*/
|
|
68
|
+
private createAppointmentServiceAdapter(): any {
|
|
69
|
+
return {
|
|
70
|
+
searchAppointments: async (params: any) => {
|
|
71
|
+
// Build query using admin SDK
|
|
72
|
+
let query: admin.firestore.Query = this.db.collection(APPOINTMENTS_COLLECTION);
|
|
73
|
+
|
|
74
|
+
if (params.clinicBranchId) {
|
|
75
|
+
query = query.where('clinicBranchId', '==', params.clinicBranchId);
|
|
76
|
+
}
|
|
77
|
+
if (params.practitionerId) {
|
|
78
|
+
query = query.where('practitionerId', '==', params.practitionerId);
|
|
79
|
+
}
|
|
80
|
+
if (params.procedureId) {
|
|
81
|
+
query = query.where('procedureId', '==', params.procedureId);
|
|
82
|
+
}
|
|
83
|
+
if (params.patientId) {
|
|
84
|
+
query = query.where('patientId', '==', params.patientId);
|
|
85
|
+
}
|
|
86
|
+
if (params.startDate) {
|
|
87
|
+
const startDate = params.startDate instanceof Date
|
|
88
|
+
? params.startDate
|
|
89
|
+
: params.startDate.toDate();
|
|
90
|
+
const startTimestamp = admin.firestore.Timestamp.fromDate(startDate);
|
|
91
|
+
query = query.where('appointmentStartTime', '>=', startTimestamp);
|
|
92
|
+
}
|
|
93
|
+
if (params.endDate) {
|
|
94
|
+
const endDate = params.endDate instanceof Date
|
|
95
|
+
? params.endDate
|
|
96
|
+
: params.endDate.toDate();
|
|
97
|
+
const endTimestamp = admin.firestore.Timestamp.fromDate(endDate);
|
|
98
|
+
query = query.where('appointmentStartTime', '<=', endTimestamp);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const snapshot = await query.get();
|
|
102
|
+
const appointments = snapshot.docs.map(doc => ({
|
|
103
|
+
id: doc.id,
|
|
104
|
+
...doc.data(),
|
|
105
|
+
})) as Appointment[];
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
appointments,
|
|
109
|
+
total: appointments.length,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Delegate all methods to the underlying AnalyticsService
|
|
116
|
+
// We expose them here so they can be called with admin SDK context
|
|
117
|
+
|
|
118
|
+
async getPractitionerAnalytics(
|
|
119
|
+
practitionerId: string,
|
|
120
|
+
dateRange?: AnalyticsDateRange,
|
|
121
|
+
options?: any,
|
|
122
|
+
): Promise<PractitionerAnalytics> {
|
|
123
|
+
return this.analyticsService.getPractitionerAnalytics(practitionerId, dateRange, options);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async getProcedureAnalytics(
|
|
127
|
+
procedureId?: string,
|
|
128
|
+
dateRange?: AnalyticsDateRange,
|
|
129
|
+
options?: any,
|
|
130
|
+
): Promise<ProcedureAnalytics | ProcedureAnalytics[]> {
|
|
131
|
+
return this.analyticsService.getProcedureAnalytics(procedureId, dateRange, options);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getTimeEfficiencyMetrics(
|
|
135
|
+
filters?: AnalyticsFilters,
|
|
136
|
+
dateRange?: AnalyticsDateRange,
|
|
137
|
+
options?: any,
|
|
138
|
+
): Promise<TimeEfficiencyMetrics> {
|
|
139
|
+
return this.analyticsService.getTimeEfficiencyMetrics(filters, dateRange, options);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async getTimeEfficiencyMetricsByEntity(
|
|
143
|
+
groupBy: EntityType,
|
|
144
|
+
dateRange?: AnalyticsDateRange,
|
|
145
|
+
filters?: AnalyticsFilters,
|
|
146
|
+
): Promise<GroupedTimeEfficiencyMetrics[]> {
|
|
147
|
+
return this.analyticsService.getTimeEfficiencyMetricsByEntity(groupBy, dateRange, filters);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async getCancellationMetrics(
|
|
151
|
+
groupBy: EntityType,
|
|
152
|
+
dateRange?: AnalyticsDateRange,
|
|
153
|
+
options?: any,
|
|
154
|
+
): Promise<CancellationMetrics | CancellationMetrics[]> {
|
|
155
|
+
return this.analyticsService.getCancellationMetrics(groupBy, dateRange, options);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async getNoShowMetrics(
|
|
159
|
+
groupBy: EntityType,
|
|
160
|
+
dateRange?: AnalyticsDateRange,
|
|
161
|
+
options?: any,
|
|
162
|
+
): Promise<NoShowMetrics | NoShowMetrics[]> {
|
|
163
|
+
return this.analyticsService.getNoShowMetrics(groupBy, dateRange, options);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async getRevenueMetrics(
|
|
167
|
+
filters?: AnalyticsFilters,
|
|
168
|
+
dateRange?: AnalyticsDateRange,
|
|
169
|
+
options?: any,
|
|
170
|
+
): Promise<RevenueMetrics> {
|
|
171
|
+
return this.analyticsService.getRevenueMetrics(filters, dateRange, options);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async getRevenueMetricsByEntity(
|
|
175
|
+
groupBy: EntityType,
|
|
176
|
+
dateRange?: AnalyticsDateRange,
|
|
177
|
+
filters?: AnalyticsFilters,
|
|
178
|
+
): Promise<GroupedRevenueMetrics[]> {
|
|
179
|
+
return this.analyticsService.getRevenueMetricsByEntity(groupBy, dateRange, filters);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async getProductUsageMetrics(
|
|
183
|
+
productId?: string,
|
|
184
|
+
dateRange?: AnalyticsDateRange,
|
|
185
|
+
): Promise<ProductUsageMetrics | ProductUsageMetrics[]> {
|
|
186
|
+
return this.analyticsService.getProductUsageMetrics(productId, dateRange);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async getProductUsageMetricsByEntity(
|
|
190
|
+
groupBy: EntityType,
|
|
191
|
+
dateRange?: AnalyticsDateRange,
|
|
192
|
+
filters?: AnalyticsFilters,
|
|
193
|
+
): Promise<GroupedProductUsageMetrics[]> {
|
|
194
|
+
return this.analyticsService.getProductUsageMetricsByEntity(groupBy, dateRange, filters);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async getPatientAnalytics(
|
|
198
|
+
patientId?: string,
|
|
199
|
+
dateRange?: AnalyticsDateRange,
|
|
200
|
+
): Promise<PatientAnalytics | PatientAnalytics[]> {
|
|
201
|
+
return this.analyticsService.getPatientAnalytics(patientId, dateRange);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async getPatientBehaviorMetricsByEntity(
|
|
205
|
+
groupBy: 'clinic' | 'practitioner' | 'procedure' | 'technology',
|
|
206
|
+
dateRange?: AnalyticsDateRange,
|
|
207
|
+
filters?: AnalyticsFilters,
|
|
208
|
+
): Promise<GroupedPatientBehaviorMetrics[]> {
|
|
209
|
+
return this.analyticsService.getPatientBehaviorMetricsByEntity(groupBy, dateRange, filters);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async getClinicAnalytics(
|
|
213
|
+
clinicBranchId: string,
|
|
214
|
+
dateRange?: AnalyticsDateRange,
|
|
215
|
+
): Promise<ClinicAnalytics | ClinicAnalytics[]> {
|
|
216
|
+
// Use getDashboardData to get clinic-level analytics
|
|
217
|
+
const dashboard = await this.analyticsService.getDashboardData(
|
|
218
|
+
{ clinicBranchId },
|
|
219
|
+
dateRange,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Get clinic info
|
|
223
|
+
const clinicDoc = await this.db.collection('clinics').doc(clinicBranchId).get();
|
|
224
|
+
const clinicData = clinicDoc.data();
|
|
225
|
+
const clinicName = clinicData?.name || 'Unknown';
|
|
226
|
+
|
|
227
|
+
// Convert DashboardAnalytics to ClinicAnalytics format
|
|
228
|
+
return {
|
|
229
|
+
clinicBranchId,
|
|
230
|
+
clinicName,
|
|
231
|
+
totalAppointments: dashboard.overview.totalAppointments,
|
|
232
|
+
completedAppointments: dashboard.overview.completedAppointments,
|
|
233
|
+
canceledAppointments: dashboard.overview.canceledAppointments,
|
|
234
|
+
noShowAppointments: dashboard.overview.noShowAppointments,
|
|
235
|
+
cancellationRate: dashboard.overview.cancellationRate,
|
|
236
|
+
noShowRate: dashboard.overview.noShowRate,
|
|
237
|
+
totalRevenue: dashboard.overview.totalRevenue,
|
|
238
|
+
averageRevenuePerAppointment: dashboard.overview.averageRevenuePerAppointment,
|
|
239
|
+
currency: dashboard.overview.currency,
|
|
240
|
+
practitionerCount: dashboard.overview.uniquePractitioners,
|
|
241
|
+
patientCount: dashboard.overview.uniquePatients,
|
|
242
|
+
procedureCount: dashboard.overview.uniqueProcedures,
|
|
243
|
+
topPractitioners: dashboard.practitionerMetrics.slice(0, 5).map(p => ({
|
|
244
|
+
practitionerId: p.practitionerId,
|
|
245
|
+
practitionerName: p.practitionerName,
|
|
246
|
+
appointmentCount: p.totalAppointments,
|
|
247
|
+
revenue: p.totalRevenue,
|
|
248
|
+
})),
|
|
249
|
+
topProcedures: dashboard.procedureMetrics.slice(0, 5).map(p => ({
|
|
250
|
+
procedureId: p.procedureId,
|
|
251
|
+
procedureName: p.procedureName,
|
|
252
|
+
appointmentCount: p.totalAppointments,
|
|
253
|
+
revenue: p.totalRevenue,
|
|
254
|
+
})),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async getDashboardData(
|
|
259
|
+
filters?: AnalyticsFilters,
|
|
260
|
+
dateRange?: AnalyticsDateRange,
|
|
261
|
+
options?: any,
|
|
262
|
+
): Promise<DashboardAnalytics> {
|
|
263
|
+
return this.analyticsService.getDashboardData(filters, dateRange, options);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Expose fetchAppointments for direct access if needed
|
|
268
|
+
* This method is used internally by AnalyticsService
|
|
269
|
+
*/
|
|
270
|
+
async fetchAppointments(
|
|
271
|
+
filters?: AnalyticsFilters,
|
|
272
|
+
dateRange?: AnalyticsDateRange,
|
|
273
|
+
): Promise<any[]> {
|
|
274
|
+
// Access the private method via the service
|
|
275
|
+
return (this.analyticsService as any).fetchAppointments(filters, dateRange);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
package/src/admin/index.ts
CHANGED
|
@@ -64,6 +64,7 @@ TimestampUtils.enableServerMode();
|
|
|
64
64
|
|
|
65
65
|
// Export all services and utilities from the admin sub-modules.
|
|
66
66
|
export * from './aggregation';
|
|
67
|
+
export * from './analytics';
|
|
67
68
|
export * from './booking';
|
|
68
69
|
export * from './calendar';
|
|
69
70
|
export * from './documentation-templates';
|
|
@@ -73,3 +74,8 @@ export * from './mailing';
|
|
|
73
74
|
export * from './notifications';
|
|
74
75
|
export * from './requirements';
|
|
75
76
|
export * from './users';
|
|
77
|
+
|
|
78
|
+
// Export analytics types for Cloud Functions
|
|
79
|
+
export type * from '../types/analytics';
|
|
80
|
+
export * from '../types/analytics';
|
|
81
|
+
export { CLINICS_COLLECTION } from '../types/clinic';
|
|
@@ -38,3 +38,20 @@ Manages administrative constants like treatment benefits and contraindications.
|
|
|
38
38
|
- **`addContraindication(contraindication)`**: Adds a new contraindication.
|
|
39
39
|
- **`updateContraindication(contraindication)`**: Updates an existing contraindication.
|
|
40
40
|
- **`deleteContraindication(contraindicationId)`**: Deletes a contraindication.
|
|
41
|
+
|
|
42
|
+
### `AnalyticsService` (Proposed)
|
|
43
|
+
|
|
44
|
+
Comprehensive financial and analytical intelligence service for the Clinic Admin app. Provides insights about doctors, procedures, appointments, patients, products, and clinic operations.
|
|
45
|
+
|
|
46
|
+
**Status**: Proposal phase - See [analytics.service.proposal.md](./analytics.service.proposal.md) for detailed design and implementation plan.
|
|
47
|
+
|
|
48
|
+
**Planned Features**:
|
|
49
|
+
- Practitioner performance analytics (appointments, cancellations, time efficiency, revenue)
|
|
50
|
+
- Procedure analytics (popularity, profitability, product usage)
|
|
51
|
+
- Appointment time analytics (booked vs actual time, efficiency metrics)
|
|
52
|
+
- Cancellation & no-show analytics (by clinic, practitioner, patient, procedure)
|
|
53
|
+
- Financial analytics (revenue, costs, payment status, trends)
|
|
54
|
+
- Product usage analytics (usage patterns, revenue contribution)
|
|
55
|
+
- Patient analytics (lifetime value, retention, appointment frequency)
|
|
56
|
+
- Clinic analytics (performance metrics, comparisons)
|
|
57
|
+
- Comprehensive dashboard data aggregation
|