evaldoc 1.0.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/.env.staging.example +19 -0
- package/.firebaserc +6 -0
- package/.github/workflows/auto-merge-to-main.yml +28 -0
- package/.github/workflows/ci.yml +27 -0
- package/.github/workflows/npxhub-publish.yml +170 -0
- package/.prettierignore +4 -0
- package/.prettierrc.json +7 -0
- package/README.md +142 -0
- package/SRS.md +448 -0
- package/apphosting.staging.yaml +48 -0
- package/apphosting.yaml +50 -0
- package/bin/evaldoc.cjs +105 -0
- package/cors.json +8 -0
- package/docs/ARCHITECTURE.md +769 -0
- package/docs/ARCHITECTURE_SUMMARY.md +323 -0
- package/docs/DEPLOYMENT.md +86 -0
- package/docs/DEPLOYMENT_STEPS.md +373 -0
- package/docs/DEPLOY_FIRESTORE_RULES.md +239 -0
- package/docs/DEPLOY_STORAGE_RULES.md +82 -0
- package/docs/DOCTOR_TUTORIAL.md +467 -0
- package/docs/FIREBASE_STORAGE_CORS_FIX.md +131 -0
- package/docs/FIXES_APPLIED.md +128 -0
- package/docs/IMPLEMENTATION.md +498 -0
- package/docs/IMPLEMENTATION_COMPLETE.md +317 -0
- package/docs/LLM-SETUP.md +216 -0
- package/docs/MANUAL_TEST_GUIDE.md +327 -0
- package/docs/PATIENT_GUIDE.md +451 -0
- package/docs/PLATFORM_OVERVIEW.md +194 -0
- package/docs/QUICKSTART.md +252 -0
- package/docs/QUICK_REFERENCE.md +222 -0
- package/docs/READY_TO_TEST.md +383 -0
- package/docs/RUN.md +237 -0
- package/docs/START_TESTING.md +130 -0
- package/docs/STRIPE_CONNECT_SETUP.md +454 -0
- package/docs/SYSTEM_READY.md +345 -0
- package/docs/THREADS_IMPLEMENTATION_COMPLETE.md +431 -0
- package/docs/THREAD_IMPLEMENTATION_GUIDE.md +345 -0
- package/docs/firebasefiles.md +21 -0
- package/docs/homepage.md +1208 -0
- package/docs/payments.md +159 -0
- package/eslint.config.js +76 -0
- package/firebase.json +27 -0
- package/firestore.indexes.json +88 -0
- package/firestore.rules +125 -0
- package/functions/package-lock.json +2884 -0
- package/functions/package.json +20 -0
- package/functions/src/analytics.js +101 -0
- package/functions/src/audit.js +28 -0
- package/functions/src/auth.js +19 -0
- package/functions/src/drafts.js +128 -0
- package/functions/src/feedback.js +164 -0
- package/functions/src/firebase-init.js +29 -0
- package/functions/src/i18n.js +134 -0
- package/functions/src/limits.js +27 -0
- package/functions/src/llm.js +271 -0
- package/functions/src/ner.js +162 -0
- package/functions/src/notifications.js +273 -0
- package/functions/src/orgs.js +127 -0
- package/functions/src/patient-api.js +545 -0
- package/functions/src/payment.js +830 -0
- package/functions/src/safety.js +188 -0
- package/functions/src/stripe-config.js +33 -0
- package/functions/src/topics.js +94 -0
- package/functions/src/triage.js +191 -0
- package/functions/src/utils.js +28 -0
- package/functions/src/validate.js +177 -0
- package/functions/test-firebase.js +36 -0
- package/jest.config.js +18 -0
- package/package.json +52 -0
- package/public/404.html +93 -0
- package/public/app/analytics.html +336 -0
- package/public/app/analytics.js +177 -0
- package/public/app/app.css +1400 -0
- package/public/app/app.js +1754 -0
- package/public/app/config.js +14 -0
- package/public/app/inbox-threads.js +843 -0
- package/public/app/inbox.html +598 -0
- package/public/app/inbox.js +397 -0
- package/public/app/index.html +612 -0
- package/public/assets/logo.jpg +0 -0
- package/public/chat.html +137 -0
- package/public/css/chat.css +870 -0
- package/public/css/landing-enhance.css +298 -0
- package/public/css/styles.css +1198 -0
- package/public/favicon.svg +5 -0
- package/public/find-doctor.html +220 -0
- package/public/index.html +399 -0
- package/public/js/chat-threads.js +513 -0
- package/public/js/chat.js +832 -0
- package/public/js/search.js +41 -0
- package/public/privacy.html +101 -0
- package/public/robots.txt +6 -0
- package/public/site.webmanifest +12 -0
- package/public/terms.html +110 -0
- package/scripts/create-test-provider.js +55 -0
- package/scripts/deploy-staging.sh +51 -0
- package/scripts/fix-provider.js +39 -0
- package/scripts/quick-test.sh +37 -0
- package/scripts/test-chat.sh +54 -0
- package/scripts/test-multi-turn.js +137 -0
- package/scripts/test-ner-simple.js +28 -0
- package/scripts/test-ner.js +60 -0
- package/scripts/test-stripe-connect.js +304 -0
- package/scripts/test-webhook-secret.sh +24 -0
- package/server.js +3864 -0
- package/storage.rules +22 -0
- package/test-api.json +1 -0
- package/tests/emulator/README.md +38 -0
- package/tests/emulator/counters.emulator.test.js +81 -0
- package/tests/integration/api-account-selfserve.test.js +312 -0
- package/tests/integration/api-admin-digest.test.js +202 -0
- package/tests/integration/api-chat-satisfaction.test.js +259 -0
- package/tests/integration/api-doctor-drafts.test.js +236 -0
- package/tests/integration/api-doctor-inbox.test.js +401 -0
- package/tests/integration/api-org.test.js +328 -0
- package/tests/integration/api-payment.test.js +664 -0
- package/tests/integration/api-provider-settings.test.js +475 -0
- package/tests/integration/api-qr.test.js +121 -0
- package/tests/integration/api-security.test.js +241 -0
- package/tests/integration/api-stripe-search.test.js +303 -0
- package/tests/integration/api-webhook-deletion.test.js +231 -0
- package/tests/mocks/firebase-admin.js +220 -0
- package/tests/unit/analytics.test.js +151 -0
- package/tests/unit/audit-bugs.test.js +203 -0
- package/tests/unit/chat-ui-logic.test.js +283 -0
- package/tests/unit/drafts.test.js +104 -0
- package/tests/unit/feedback.test.js +148 -0
- package/tests/unit/i18n.test.js +229 -0
- package/tests/unit/llm-retry.test.js +122 -0
- package/tests/unit/notifications.test.js +155 -0
- package/tests/unit/orgs.test.js +328 -0
- package/tests/unit/payment.test.js +889 -0
- package/tests/unit/safety-edge-cases.test.js +509 -0
- package/tests/unit/safety-srs-compliance.test.js +252 -0
- package/tests/unit/safety.test.js +291 -0
- package/tests/unit/server-helpers.test.js +774 -0
- package/tests/unit/topics-limits.test.js +158 -0
- package/tests/unit/triage.test.js +152 -0
- package/tests/unit/validate.test.js +408 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Analytics - EvalDoc Provider Console</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
13
|
+
<link rel="stylesheet" href="/app/app.css" />
|
|
14
|
+
<style>
|
|
15
|
+
.analytics-header {
|
|
16
|
+
margin-bottom: 2rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.analytics-header h1 {
|
|
20
|
+
margin-bottom: 0.5rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.stats-grid {
|
|
24
|
+
display: grid;
|
|
25
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
26
|
+
gap: 1.5rem;
|
|
27
|
+
margin-bottom: 2rem;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.stat-card {
|
|
31
|
+
background: var(--white);
|
|
32
|
+
padding: 1.5rem;
|
|
33
|
+
border-radius: var(--radius);
|
|
34
|
+
box-shadow: var(--shadow);
|
|
35
|
+
border: 1px solid var(--gray-200);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.stat-label {
|
|
39
|
+
font-size: 0.875rem;
|
|
40
|
+
color: var(--muted);
|
|
41
|
+
margin-bottom: 0.5rem;
|
|
42
|
+
text-transform: uppercase;
|
|
43
|
+
letter-spacing: 0.05em;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.stat-value {
|
|
47
|
+
font-size: 2.5rem;
|
|
48
|
+
font-weight: 700;
|
|
49
|
+
color: var(--primary);
|
|
50
|
+
margin-bottom: 0.25rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.stat-subtext {
|
|
54
|
+
font-size: 0.875rem;
|
|
55
|
+
color: var(--gray-600);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.stat-trend {
|
|
59
|
+
display: inline-block;
|
|
60
|
+
font-size: 0.875rem;
|
|
61
|
+
padding: 0.25rem 0.5rem;
|
|
62
|
+
border-radius: 4px;
|
|
63
|
+
margin-top: 0.5rem;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.stat-trend.up {
|
|
67
|
+
background: rgba(41, 123, 86, 0.1);
|
|
68
|
+
color: var(--primary);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.stat-trend.down {
|
|
72
|
+
background: rgba(220, 38, 38, 0.1);
|
|
73
|
+
color: var(--danger);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.doctors-table-container {
|
|
77
|
+
background: var(--white);
|
|
78
|
+
padding: 1.5rem;
|
|
79
|
+
border-radius: var(--radius);
|
|
80
|
+
box-shadow: var(--shadow);
|
|
81
|
+
border: 1px solid var(--gray-200);
|
|
82
|
+
overflow-x: auto;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.doctors-table {
|
|
86
|
+
width: 100%;
|
|
87
|
+
border-collapse: collapse;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.doctors-table thead {
|
|
91
|
+
background: var(--gray-50);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.doctors-table th {
|
|
95
|
+
padding: 1rem;
|
|
96
|
+
text-align: left;
|
|
97
|
+
font-size: 0.875rem;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
color: var(--gray-600);
|
|
100
|
+
text-transform: uppercase;
|
|
101
|
+
letter-spacing: 0.05em;
|
|
102
|
+
border-bottom: 2px solid var(--gray-200);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.doctors-table td {
|
|
106
|
+
padding: 1rem;
|
|
107
|
+
border-bottom: 1px solid var(--gray-200);
|
|
108
|
+
color: var(--gray-900);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.doctors-table tbody tr:hover {
|
|
112
|
+
background: var(--gray-50);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.doctor-name {
|
|
116
|
+
font-weight: 600;
|
|
117
|
+
color: var(--primary);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.doctor-specialty {
|
|
121
|
+
font-size: 0.875rem;
|
|
122
|
+
color: var(--gray-600);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.status-badge {
|
|
126
|
+
display: inline-block;
|
|
127
|
+
padding: 0.25rem 0.75rem;
|
|
128
|
+
border-radius: 999px;
|
|
129
|
+
font-size: 0.75rem;
|
|
130
|
+
font-weight: 600;
|
|
131
|
+
text-transform: uppercase;
|
|
132
|
+
letter-spacing: 0.05em;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.status-badge.published {
|
|
136
|
+
background: rgba(41, 123, 86, 0.1);
|
|
137
|
+
color: var(--primary);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.status-badge.draft {
|
|
141
|
+
background: rgba(156, 163, 175, 0.1);
|
|
142
|
+
color: var(--gray-600);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.status-badge.suspended {
|
|
146
|
+
background: rgba(220, 38, 38, 0.1);
|
|
147
|
+
color: var(--danger);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.metric-number {
|
|
151
|
+
font-weight: 600;
|
|
152
|
+
font-size: 1.125rem;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.chart-container {
|
|
156
|
+
background: var(--white);
|
|
157
|
+
padding: 1.5rem;
|
|
158
|
+
border-radius: var(--radius);
|
|
159
|
+
box-shadow: var(--shadow);
|
|
160
|
+
border: 1px solid var(--gray-200);
|
|
161
|
+
margin-bottom: 2rem;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.chart-title {
|
|
165
|
+
font-size: 1.125rem;
|
|
166
|
+
font-weight: 600;
|
|
167
|
+
margin-bottom: 1rem;
|
|
168
|
+
color: var(--gray-900);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.filter-bar {
|
|
172
|
+
display: flex;
|
|
173
|
+
gap: 1rem;
|
|
174
|
+
margin-bottom: 1.5rem;
|
|
175
|
+
align-items: center;
|
|
176
|
+
flex-wrap: wrap;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.filter-label {
|
|
180
|
+
font-size: 0.875rem;
|
|
181
|
+
font-weight: 600;
|
|
182
|
+
color: var(--gray-600);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.filter-select {
|
|
186
|
+
padding: 0.5rem 1rem;
|
|
187
|
+
border: 1px solid var(--gray-200);
|
|
188
|
+
border-radius: var(--radius-sm);
|
|
189
|
+
font-size: 0.875rem;
|
|
190
|
+
background: var(--white);
|
|
191
|
+
color: var(--gray-900);
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.empty-state {
|
|
196
|
+
text-align: center;
|
|
197
|
+
padding: 3rem;
|
|
198
|
+
color: var(--gray-400);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.refresh-btn {
|
|
202
|
+
padding: 0.5rem 1rem;
|
|
203
|
+
background: var(--primary);
|
|
204
|
+
color: white;
|
|
205
|
+
border: none;
|
|
206
|
+
border-radius: var(--radius-sm);
|
|
207
|
+
font-size: 0.875rem;
|
|
208
|
+
font-weight: 600;
|
|
209
|
+
cursor: pointer;
|
|
210
|
+
transition: var(--transition);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.refresh-btn:hover {
|
|
214
|
+
background: var(--primary-dark);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.loading {
|
|
218
|
+
text-align: center;
|
|
219
|
+
padding: 2rem;
|
|
220
|
+
color: var(--gray-600);
|
|
221
|
+
}
|
|
222
|
+
</style>
|
|
223
|
+
<!-- Google tag (gtag.js) -->
|
|
224
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPWYSPFSVG"></script>
|
|
225
|
+
<script>
|
|
226
|
+
window.dataLayer = window.dataLayer || [];
|
|
227
|
+
function gtag(){dataLayer.push(arguments);}
|
|
228
|
+
gtag("js", new Date());
|
|
229
|
+
|
|
230
|
+
gtag("config", "G-NPWYSPFSVG");
|
|
231
|
+
</script>
|
|
232
|
+
</head>
|
|
233
|
+
<body>
|
|
234
|
+
<div class="dashboard-bg"></div>
|
|
235
|
+
<header class="topbar">
|
|
236
|
+
<div class="brand">
|
|
237
|
+
<img src="/assets/logo.jpg" alt="EvalDoc Logo" class="logo-img" />
|
|
238
|
+
<span class="subtitle">Provider Console</span>
|
|
239
|
+
</div>
|
|
240
|
+
<button id="signOutBtn" class="ghost">Sign out</button>
|
|
241
|
+
</header>
|
|
242
|
+
|
|
243
|
+
<main class="container">
|
|
244
|
+
<div class="nav-links">
|
|
245
|
+
<a href="/app" class="nav-link">Dashboard</a>
|
|
246
|
+
<a href="/app/inbox" class="nav-link">Inbox</a>
|
|
247
|
+
<a href="/app/analytics" class="nav-link active">Analytics</a>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div id="authRequired" class="card hidden">
|
|
251
|
+
<h2>Authentication Required</h2>
|
|
252
|
+
<p>Please sign in to view analytics.</p>
|
|
253
|
+
<a href="/app" class="button">Go to Sign In</a>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div id="analyticsContainer" class="hidden">
|
|
257
|
+
<div class="analytics-header">
|
|
258
|
+
<h1>Platform Analytics</h1>
|
|
259
|
+
<p style="color: var(--muted);">Monitor doctor signups and user activity across the platform</p>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div class="filter-bar">
|
|
263
|
+
<span class="filter-label">Time Period:</span>
|
|
264
|
+
<select id="timePeriodFilter" class="filter-select">
|
|
265
|
+
<option value="all">All Time</option>
|
|
266
|
+
<option value="today">Today</option>
|
|
267
|
+
<option value="week">This Week</option>
|
|
268
|
+
<option value="month" selected>This Month</option>
|
|
269
|
+
<option value="quarter">This Quarter</option>
|
|
270
|
+
</select>
|
|
271
|
+
<button id="refreshBtn" class="refresh-btn">Refresh Data</button>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<!-- Summary Statistics -->
|
|
275
|
+
<div class="stats-grid">
|
|
276
|
+
<div class="stat-card">
|
|
277
|
+
<div class="stat-label">Total Doctors</div>
|
|
278
|
+
<div class="stat-value" id="totalDoctors">-</div>
|
|
279
|
+
<div class="stat-subtext">Registered providers</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<div class="stat-card">
|
|
283
|
+
<div class="stat-label">Active Doctors</div>
|
|
284
|
+
<div class="stat-value" id="activeDoctors">-</div>
|
|
285
|
+
<div class="stat-subtext">Published profiles</div>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div class="stat-card">
|
|
289
|
+
<div class="stat-label">Total Conversations</div>
|
|
290
|
+
<div class="stat-value" id="totalConversations">-</div>
|
|
291
|
+
<div class="stat-subtext">Across all doctors</div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div class="stat-card">
|
|
295
|
+
<div class="stat-label">Unique Patients</div>
|
|
296
|
+
<div class="stat-value" id="uniquePatients">-</div>
|
|
297
|
+
<div class="stat-subtext">Active users</div>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<!-- Doctors Table -->
|
|
302
|
+
<div class="doctors-table-container">
|
|
303
|
+
<h2 class="chart-title">Doctors Overview</h2>
|
|
304
|
+
|
|
305
|
+
<div id="doctorsTableLoading" class="loading">Loading doctors data...</div>
|
|
306
|
+
|
|
307
|
+
<table class="doctors-table" id="doctorsTable" style="display: none;">
|
|
308
|
+
<thead>
|
|
309
|
+
<tr>
|
|
310
|
+
<th>Doctor</th>
|
|
311
|
+
<th>Specialty</th>
|
|
312
|
+
<th>Handle</th>
|
|
313
|
+
<th>Status</th>
|
|
314
|
+
<th>Conversations</th>
|
|
315
|
+
<th>Unique Patients</th>
|
|
316
|
+
<th>Messages</th>
|
|
317
|
+
<th>Joined</th>
|
|
318
|
+
</tr>
|
|
319
|
+
</thead>
|
|
320
|
+
<tbody id="doctorsTableBody">
|
|
321
|
+
</tbody>
|
|
322
|
+
</table>
|
|
323
|
+
|
|
324
|
+
<div id="doctorsTableEmpty" class="empty-state" style="display: none;">
|
|
325
|
+
<p>No doctors found</p>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</main>
|
|
330
|
+
|
|
331
|
+
<script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-app-compat.js"></script>
|
|
332
|
+
<script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-auth-compat.js"></script>
|
|
333
|
+
<script src="/app/config.js"></script>
|
|
334
|
+
<script src="/app/analytics.js"></script>
|
|
335
|
+
</body>
|
|
336
|
+
</html>
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
const authRequired = document.getElementById('authRequired');
|
|
2
|
+
const analyticsContainer = document.getElementById('analyticsContainer');
|
|
3
|
+
const signOutBtn = document.getElementById('signOutBtn');
|
|
4
|
+
const refreshBtn = document.getElementById('refreshBtn');
|
|
5
|
+
const timePeriodFilter = document.getElementById('timePeriodFilter');
|
|
6
|
+
|
|
7
|
+
// Stat elements
|
|
8
|
+
const totalDoctorsEl = document.getElementById('totalDoctors');
|
|
9
|
+
const activeDoctorsEl = document.getElementById('activeDoctors');
|
|
10
|
+
const totalConversationsEl = document.getElementById('totalConversations');
|
|
11
|
+
const uniquePatientsEl = document.getElementById('uniquePatients');
|
|
12
|
+
|
|
13
|
+
// Table elements
|
|
14
|
+
const doctorsTable = document.getElementById('doctorsTable');
|
|
15
|
+
const doctorsTableBody = document.getElementById('doctorsTableBody');
|
|
16
|
+
const doctorsTableLoading = document.getElementById('doctorsTableLoading');
|
|
17
|
+
const doctorsTableEmpty = document.getElementById('doctorsTableEmpty');
|
|
18
|
+
|
|
19
|
+
let currentUser = null;
|
|
20
|
+
let analyticsData = null;
|
|
21
|
+
|
|
22
|
+
// Fetch with authentication
|
|
23
|
+
async function fetchWithAuth(path, options = {}) {
|
|
24
|
+
const user = firebase.auth().currentUser;
|
|
25
|
+
if (!user) throw new Error('Not authenticated');
|
|
26
|
+
|
|
27
|
+
const token = await user.getIdToken();
|
|
28
|
+
const headers = Object.assign({}, options.headers || {}, {
|
|
29
|
+
Authorization: `Bearer ${token}`
|
|
30
|
+
});
|
|
31
|
+
return fetch(path, { ...options, headers });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Format date
|
|
35
|
+
function formatDate(timestamp) {
|
|
36
|
+
if (!timestamp) return 'N/A';
|
|
37
|
+
const date = timestamp.toDate ? timestamp.toDate() : new Date(timestamp._seconds * 1000);
|
|
38
|
+
return date.toLocaleDateString('en-US', {
|
|
39
|
+
year: 'numeric',
|
|
40
|
+
month: 'short',
|
|
41
|
+
day: 'numeric'
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Format time ago
|
|
46
|
+
function formatTimeAgo(timestamp) {
|
|
47
|
+
if (!timestamp) return 'Unknown';
|
|
48
|
+
const date = timestamp.toDate ? timestamp.toDate() : new Date(timestamp._seconds * 1000);
|
|
49
|
+
const now = new Date();
|
|
50
|
+
const diff = now - date;
|
|
51
|
+
|
|
52
|
+
const days = Math.floor(diff / 86400000);
|
|
53
|
+
if (days === 0) return 'Today';
|
|
54
|
+
if (days === 1) return 'Yesterday';
|
|
55
|
+
if (days < 7) return `${days} days ago`;
|
|
56
|
+
if (days < 30) return `${Math.floor(days / 7)} weeks ago`;
|
|
57
|
+
if (days < 365) return `${Math.floor(days / 30)} months ago`;
|
|
58
|
+
return `${Math.floor(days / 365)} years ago`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Load analytics data
|
|
62
|
+
async function loadAnalytics() {
|
|
63
|
+
try {
|
|
64
|
+
const timePeriod = timePeriodFilter.value;
|
|
65
|
+
const res = await fetchWithAuth(`/api/analytics?period=${timePeriod}`);
|
|
66
|
+
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
if (res.status === 403) {
|
|
69
|
+
throw new Error('Access denied. Admin privileges required.');
|
|
70
|
+
}
|
|
71
|
+
throw new Error('Unable to load analytics');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
analyticsData = await res.json();
|
|
75
|
+
renderAnalytics();
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('Error loading analytics:', err);
|
|
78
|
+
alert(err.message || 'Failed to load analytics data');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Render analytics data
|
|
83
|
+
function renderAnalytics() {
|
|
84
|
+
if (!analyticsData) return;
|
|
85
|
+
|
|
86
|
+
// Update summary statistics
|
|
87
|
+
totalDoctorsEl.textContent = analyticsData.summary.totalDoctors || 0;
|
|
88
|
+
activeDoctorsEl.textContent = analyticsData.summary.activeDoctors || 0;
|
|
89
|
+
totalConversationsEl.textContent = analyticsData.summary.totalConversations || 0;
|
|
90
|
+
uniquePatientsEl.textContent = analyticsData.summary.uniquePatients || 0;
|
|
91
|
+
|
|
92
|
+
// Render doctors table
|
|
93
|
+
renderDoctorsTable(analyticsData.doctors);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Render doctors table
|
|
97
|
+
function renderDoctorsTable(doctors) {
|
|
98
|
+
doctorsTableLoading.style.display = 'none';
|
|
99
|
+
|
|
100
|
+
// Ensure doctors is an array
|
|
101
|
+
const doctorsList = Array.isArray(doctors) ? doctors : [];
|
|
102
|
+
|
|
103
|
+
if (doctorsList.length === 0) {
|
|
104
|
+
doctorsTable.style.display = 'none';
|
|
105
|
+
doctorsTableEmpty.style.display = 'block';
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
doctorsTable.style.display = 'table';
|
|
110
|
+
doctorsTableEmpty.style.display = 'none';
|
|
111
|
+
|
|
112
|
+
// Sort doctors by conversation count (descending)
|
|
113
|
+
const sortedDoctors = [...doctorsList].sort((a, b) =>
|
|
114
|
+
(b.conversationCount || 0) - (a.conversationCount || 0)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
doctorsTableBody.innerHTML = sortedDoctors.map(doctor => `
|
|
118
|
+
<tr>
|
|
119
|
+
<td>
|
|
120
|
+
<div class="doctor-name">${escapeHtml(doctor.displayName || 'Unnamed Doctor')}</div>
|
|
121
|
+
<div class="doctor-specialty">${escapeHtml(doctor.email || '')}</div>
|
|
122
|
+
</td>
|
|
123
|
+
<td>${escapeHtml(doctor.specialty || 'N/A')}</td>
|
|
124
|
+
<td>
|
|
125
|
+
<a href="/${doctor.handle}" target="_blank" style="color: var(--primary);">
|
|
126
|
+
/${doctor.handle || 'N/A'}
|
|
127
|
+
</a>
|
|
128
|
+
</td>
|
|
129
|
+
<td>
|
|
130
|
+
<span class="status-badge ${doctor.status}">${doctor.status || 'draft'}</span>
|
|
131
|
+
</td>
|
|
132
|
+
<td class="metric-number">${doctor.conversationCount || 0}</td>
|
|
133
|
+
<td class="metric-number">${doctor.uniquePatients || 0}</td>
|
|
134
|
+
<td class="metric-number">${doctor.messageCount || 0}</td>
|
|
135
|
+
<td>${formatTimeAgo(doctor.createdAt)}</td>
|
|
136
|
+
</tr>
|
|
137
|
+
`).join('');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Escape HTML to prevent XSS
|
|
141
|
+
function escapeHtml(text) {
|
|
142
|
+
const div = document.createElement('div');
|
|
143
|
+
div.textContent = text;
|
|
144
|
+
return div.innerHTML;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Sign out
|
|
148
|
+
async function signOut() {
|
|
149
|
+
try {
|
|
150
|
+
await firebase.auth().signOut();
|
|
151
|
+
window.location.href = '/app';
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.error('Sign out error:', err);
|
|
154
|
+
alert('Failed to sign out');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check authentication
|
|
159
|
+
firebase.auth().onAuthStateChanged(async (user) => {
|
|
160
|
+
if (!user) {
|
|
161
|
+
authRequired.classList.remove('hidden');
|
|
162
|
+
analyticsContainer.classList.add('hidden');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
currentUser = user;
|
|
167
|
+
authRequired.classList.add('hidden');
|
|
168
|
+
analyticsContainer.classList.remove('hidden');
|
|
169
|
+
|
|
170
|
+
// Load initial analytics data
|
|
171
|
+
await loadAnalytics();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Event listeners
|
|
175
|
+
signOutBtn.addEventListener('click', signOut);
|
|
176
|
+
refreshBtn.addEventListener('click', loadAnalytics);
|
|
177
|
+
timePeriodFilter.addEventListener('change', loadAnalytics);
|