@oneclick.dev/cms-core-modules 0.0.74 → 0.0.75

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.
@@ -0,0 +1,898 @@
1
+ import { createRouter as v, defineEventHandler as y, createError as h, getRouterParam as U, getQuery as f } from "h3";
2
+ import "vue";
3
+ function $(w) {
4
+ const { initFirebase: S, normalizeTimestamps: A } = w, l = v();
5
+ async function m(d) {
6
+ const { supabase: c, instanceId: g } = d.context.module, { data: e, error: o } = await c.from("project_modules").select("config").eq("id", g).single();
7
+ if (o || !e?.config)
8
+ throw h({ statusCode: 500, statusMessage: "Failed to load module config." });
9
+ const n = e.config, t = n.project, s = n.productCollection || "products";
10
+ if (!t)
11
+ throw h({ statusCode: 400, statusMessage: "Products module has no Firebase integration configured." });
12
+ return { firebase: await S(d, t), collection: s };
13
+ }
14
+ return l.get("/products", y(async (d) => {
15
+ try {
16
+ const { firebase: c, collection: g } = await m(d);
17
+ return (await c.firestore().collection(g).get()).docs.map((o) => A({
18
+ id: o.id,
19
+ ...o.data()
20
+ }));
21
+ } catch (c) {
22
+ throw console.error("Products handler — list error:", c), h({
23
+ statusCode: c.statusCode || 500,
24
+ statusMessage: c.statusMessage || "Failed to list products"
25
+ });
26
+ }
27
+ })), l.get("/products/:productId", y(async (d) => {
28
+ const c = U(d, "productId");
29
+ if (!c)
30
+ throw h({ statusCode: 400, statusMessage: "Product ID is required." });
31
+ try {
32
+ const { firebase: g, collection: e } = await m(d), o = await g.firestore().collection(e).doc(c).get();
33
+ if (!o.exists)
34
+ throw h({ statusCode: 404, statusMessage: "Product not found." });
35
+ return A({
36
+ id: o.id,
37
+ ...o.data()
38
+ });
39
+ } catch (g) {
40
+ throw console.error("Products handler — get error:", g), h({
41
+ statusCode: g.statusCode || 500,
42
+ statusMessage: g.statusMessage || "Failed to get product"
43
+ });
44
+ }
45
+ })), l.get("/empty-stock", y(async (d) => {
46
+ const g = f(d).category;
47
+ try {
48
+ const { firebase: e, collection: o } = await m(d);
49
+ let n = e.firestore().collection(o).where("stock", "==", 0);
50
+ g && (n = n.where("collections", "array-contains", g));
51
+ const s = (await n.get()).docs.map((a) => A({
52
+ id: a.id,
53
+ ...a.data()
54
+ }));
55
+ return {
56
+ count: s.length,
57
+ products: s.map((a) => ({
58
+ id: a.id,
59
+ title: a.title,
60
+ slug: a.slug,
61
+ stock: a.stock,
62
+ price: a.price,
63
+ currency: a.currency,
64
+ status: a.status
65
+ }))
66
+ };
67
+ } catch (e) {
68
+ throw console.error("Products handler — empty-stock error:", e), h({
69
+ statusCode: e.statusCode || 500,
70
+ statusMessage: e.statusMessage || "Failed to query empty stock"
71
+ });
72
+ }
73
+ })), l.handler;
74
+ }
75
+ function G(w) {
76
+ const { initFirebase: S, normalizeTimestamps: A } = w, l = v();
77
+ async function m(c) {
78
+ const { supabase: g, instanceId: e } = c.context.module, { data: o, error: n } = await g.from("project_modules").select("config").eq("id", e).single();
79
+ if (n || !o?.config)
80
+ throw h({ statusCode: 500, statusMessage: "Failed to load module config." });
81
+ const t = o.config, s = t.project, a = t.reservationsCollection || "bookings_orders", r = t.agendaCollection || "agendas";
82
+ if (!s)
83
+ throw h({ statusCode: 400, statusMessage: "Appointments module has no Firebase integration configured." });
84
+ return { firebase: await S(c, s), reservationsCollection: a, agendaCollection: r };
85
+ }
86
+ function d(c) {
87
+ return (c.reservations || []).map((e) => ({
88
+ orderId: c.id,
89
+ reservationId: e.id,
90
+ customerInfo: c.customerInfo,
91
+ date: e.date,
92
+ startTime: e.timeslot?.startTime,
93
+ endTime: e.timeslot?.endTime,
94
+ spots: e.spots,
95
+ status: c.status,
96
+ reservationStatus: e.status,
97
+ resourceId: e.resourceId,
98
+ reservationPrice: e.totalPrice,
99
+ reservationBasePrice: e.basePrice,
100
+ reservationAddOnsPrice: e.addOnsPrice,
101
+ pricingOption: e.pricingOption,
102
+ amountDue: c.amountDue,
103
+ amountPaid: c.amountPaid,
104
+ createdAt: c.createdAt
105
+ }));
106
+ }
107
+ return l.get("/appointments", y(async (c) => {
108
+ const g = f(c), e = Math.min(Number(g.quantity) || 20, 100);
109
+ try {
110
+ const { firebase: o, reservationsCollection: n } = await m(c), s = (await o.firestore().collection(n).orderBy("createdAt", "desc").limit(e).get()).docs.flatMap(
111
+ (a) => d(A({ id: a.id, ...a.data() }))
112
+ );
113
+ return {
114
+ count: s.length,
115
+ appointments: s
116
+ };
117
+ } catch (o) {
118
+ throw console.error("Appointments handler — list error:", o), h({
119
+ statusCode: o.statusCode || 500,
120
+ statusMessage: o.statusMessage || "Failed to list appointments"
121
+ });
122
+ }
123
+ })), l.get("/appointments/find", y(async (c) => {
124
+ const g = f(c), e = (g.name || "").toLowerCase().trim(), o = (g.email || "").toLowerCase().trim(), n = g.date;
125
+ try {
126
+ const { firebase: t, reservationsCollection: s } = await m(c);
127
+ let a = t.firestore().collection(s);
128
+ o && (a = a.where("customerInfo.email", "==", o));
129
+ let i = (await a.orderBy("createdAt", "desc").limit(200).get()).docs.flatMap(
130
+ (u) => d(A({ id: u.id, ...u.data() }))
131
+ );
132
+ return n && (i = i.filter((u) => u.date === n)), e && (i = i.filter((u) => {
133
+ const p = (u.customerInfo?.firstName || "").toLowerCase(), D = (u.customerInfo?.lastName || "").toLowerCase();
134
+ return p.includes(e) || D.includes(e) || `${p} ${D}`.includes(e);
135
+ })), {
136
+ count: i.length,
137
+ appointments: i
138
+ };
139
+ } catch (t) {
140
+ throw console.error("Appointments handler — find error:", t), h({
141
+ statusCode: t.statusCode || 500,
142
+ statusMessage: t.statusMessage || "Failed to find appointments"
143
+ });
144
+ }
145
+ })), l.get("/appointments/:appointmentId", y(async (c) => {
146
+ const g = U(c, "appointmentId");
147
+ if (!g)
148
+ throw h({ statusCode: 400, statusMessage: "Appointment ID is required." });
149
+ try {
150
+ const { firebase: e, reservationsCollection: o } = await m(c);
151
+ console.log("Fetching appointment with ID:", g, o);
152
+ const n = await e.firestore().collection(o).doc(g).get();
153
+ if (!n.exists)
154
+ throw h({ statusCode: 404, statusMessage: "Appointment not found." });
155
+ const t = A({ id: n.id, ...n.data() });
156
+ return {
157
+ ...t,
158
+ reservations: d(t)
159
+ };
160
+ } catch (e) {
161
+ throw console.error("Appointments handler — get error:", e), h({
162
+ statusCode: e.statusCode || 500,
163
+ statusMessage: e.statusMessage || "Failed to get appointment"
164
+ });
165
+ }
166
+ })), l.handler;
167
+ }
168
+ const M = "https://analyticsdata.googleapis.com/v1beta", F = [
169
+ "https://www.googleapis.com/auth/analytics.readonly",
170
+ "https://www.googleapis.com/auth/webmasters.readonly"
171
+ ], T = "https://searchconsole.googleapis.com/webmasters/v3";
172
+ function x(w) {
173
+ const { decrypt: S, getGoogleAccessToken: A } = w, l = v();
174
+ async function m(e) {
175
+ const { supabase: o, instanceId: n } = e.context.module, { data: t, error: s } = await o.from("project_modules").select("config").eq("id", n).single();
176
+ if (s || !t?.config)
177
+ throw h({ statusCode: 500, statusMessage: "Failed to load module config." });
178
+ const a = t.config, r = a.propertyId, i = a.serviceAccount, u = a.siteUrl || "";
179
+ if (!i)
180
+ throw h({ statusCode: 400, statusMessage: "No Google Service Account configured for this module." });
181
+ if (!r)
182
+ throw h({ statusCode: 400, statusMessage: "No GA4 Property ID configured for this module." });
183
+ const { data: p, error: D } = await o.from("integrations").select("config").eq("id", i).single();
184
+ let C = p?.config;
185
+ if (D || !C) {
186
+ const { data: q, error: b } = await o.from("agency_integrations").select("config").eq("id", i).single();
187
+ if (b || !q?.config)
188
+ throw h({ statusCode: 500, statusMessage: "Failed to load Google Service Account credentials." });
189
+ C = q.config;
190
+ }
191
+ const R = S(C.clientEmail), P = S(C.privateKey), k = S(C.projectId);
192
+ return { accessToken: await A(
193
+ { clientEmail: R, privateKey: P, projectId: k },
194
+ F
195
+ ), propertyId: r, siteUrl: u };
196
+ }
197
+ async function d(e, o, n) {
198
+ const t = await fetch(`${M}/properties/${o}:runReport`, {
199
+ method: "POST",
200
+ headers: {
201
+ Authorization: `Bearer ${e}`,
202
+ "Content-Type": "application/json"
203
+ },
204
+ body: JSON.stringify(n)
205
+ });
206
+ if (!t.ok) {
207
+ const s = await t.json().catch(() => ({}));
208
+ throw console.error("GA4 runReport failed:", s), h({
209
+ statusCode: t.status,
210
+ statusMessage: s?.error?.message || "GA4 API request failed"
211
+ });
212
+ }
213
+ return t.json();
214
+ }
215
+ l.get(
216
+ "/report",
217
+ y(async (e) => {
218
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = s.match(/^(\d+)daysAgo$/), i = r ? parseInt(r[1], 10) : 30, u = `${i * 2}daysAgo`, p = `${i + 1}daysAgo`, D = [
219
+ { name: "sessions" },
220
+ { name: "totalUsers" },
221
+ { name: "screenPageViews" },
222
+ { name: "bounceRate" },
223
+ { name: "averageSessionDuration" },
224
+ { name: "newUsers" },
225
+ { name: "engagementRate" },
226
+ { name: "sessionsPerUser" },
227
+ { name: "screenPageViewsPerSession" }
228
+ ];
229
+ let C, R = !0;
230
+ try {
231
+ C = await d(o, n, {
232
+ dateRanges: [
233
+ { startDate: s, endDate: a, name: "current" },
234
+ { startDate: u, endDate: p, name: "previous" }
235
+ ],
236
+ dimensions: [{ name: "date" }],
237
+ metrics: D,
238
+ metricAggregations: ["TOTAL"],
239
+ orderBys: [{ dimension: { dimensionName: "date" } }]
240
+ });
241
+ } catch {
242
+ R = !1, C = await d(o, n, {
243
+ dateRanges: [{ startDate: s, endDate: a }],
244
+ dimensions: [{ name: "date" }],
245
+ metrics: D,
246
+ metricAggregations: ["TOTAL"],
247
+ orderBys: [{ dimension: { dimensionName: "date" } }]
248
+ });
249
+ }
250
+ return V(C, R);
251
+ })
252
+ ), l.get(
253
+ "/realtime",
254
+ y(async (e) => {
255
+ const { accessToken: o, propertyId: n } = await m(e), t = await fetch(
256
+ `${M}/properties/${n}:runRealtimeReport`,
257
+ {
258
+ method: "POST",
259
+ headers: {
260
+ Authorization: `Bearer ${o}`,
261
+ "Content-Type": "application/json"
262
+ },
263
+ body: JSON.stringify({
264
+ dimensions: [{ name: "unifiedScreenName" }],
265
+ metrics: [{ name: "activeUsers" }],
266
+ orderBys: [{ metric: { metricName: "activeUsers" }, desc: !0 }],
267
+ limit: 5
268
+ })
269
+ }
270
+ );
271
+ if (!t.ok) {
272
+ const i = await t.json().catch(() => ({}));
273
+ throw h({ statusCode: t.status, statusMessage: i?.error?.message || "Realtime API failed" });
274
+ }
275
+ const s = await t.json(), a = (s?.rows || []).reduce(
276
+ (i, u) => i + parseInt(u.metricValues?.[0]?.value || "0", 10),
277
+ 0
278
+ ), r = (s?.rows || []).map((i) => ({
279
+ page: i.dimensionValues?.[0]?.value || "(not set)",
280
+ activeUsers: parseInt(i.metricValues?.[0]?.value || "0", 10)
281
+ }));
282
+ return { activeUsers: a, activePages: r };
283
+ })
284
+ ), l.get(
285
+ "/top-pages",
286
+ y(async (e) => {
287
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = parseInt(t.limit || "10", 10), i = await d(o, n, {
288
+ dateRanges: [{ startDate: s, endDate: a }],
289
+ dimensions: [{ name: "pagePath" }],
290
+ metrics: [
291
+ { name: "screenPageViews" },
292
+ { name: "totalUsers" },
293
+ { name: "averageSessionDuration" }
294
+ ],
295
+ orderBys: [{ metric: { metricName: "screenPageViews" }, desc: !0 }],
296
+ limit: r * 2
297
+ // fetch extra to account for duplicates before aggregation
298
+ }), u = I(i, "pagePath"), p = /* @__PURE__ */ new Map();
299
+ for (const C of u.rows) {
300
+ const R = C.pagePath.toLowerCase().replace(/\/+$/, ""), P = p.get(R);
301
+ if (P) {
302
+ P.screenPageViews += C.screenPageViews || 0, P.totalUsers += C.totalUsers || 0;
303
+ const k = P.screenPageViews;
304
+ k > 0 && (P.averageSessionDuration = (P.averageSessionDuration * (k - (C.screenPageViews || 0)) + (C.averageSessionDuration || 0) * (C.screenPageViews || 0)) / k);
305
+ } else
306
+ p.set(R, { ...C });
307
+ }
308
+ return { rows: [...p.values()].sort((C, R) => (R.screenPageViews || 0) - (C.screenPageViews || 0)).slice(0, r), rowCount: u.rowCount };
309
+ })
310
+ ), l.get(
311
+ "/top-sources",
312
+ y(async (e) => {
313
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = await d(o, n, {
314
+ dateRanges: [{ startDate: s, endDate: a }],
315
+ dimensions: [{ name: "sessionSource" }],
316
+ metrics: [
317
+ { name: "sessions" },
318
+ { name: "totalUsers" }
319
+ ],
320
+ orderBys: [{ metric: { metricName: "sessions" }, desc: !0 }],
321
+ limit: 10
322
+ });
323
+ return I(r, "sessionSource");
324
+ })
325
+ ), l.get(
326
+ "/devices",
327
+ y(async (e) => {
328
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = await d(o, n, {
329
+ dateRanges: [{ startDate: s, endDate: a }],
330
+ dimensions: [{ name: "deviceCategory" }],
331
+ metrics: [
332
+ { name: "sessions" },
333
+ { name: "totalUsers" }
334
+ ],
335
+ orderBys: [{ metric: { metricName: "sessions" }, desc: !0 }]
336
+ });
337
+ return I(r, "deviceCategory");
338
+ })
339
+ ), l.get(
340
+ "/countries",
341
+ y(async (e) => {
342
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = await d(o, n, {
343
+ dateRanges: [{ startDate: s, endDate: a }],
344
+ dimensions: [{ name: "country" }],
345
+ metrics: [
346
+ { name: "sessions" },
347
+ { name: "totalUsers" }
348
+ ],
349
+ orderBys: [{ metric: { metricName: "sessions" }, desc: !0 }],
350
+ limit: 10
351
+ });
352
+ return I(r, "country");
353
+ })
354
+ ), l.get(
355
+ "/acquisition/channels",
356
+ y(async (e) => {
357
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = await d(o, n, {
358
+ dateRanges: [{ startDate: s, endDate: a }],
359
+ dimensions: [{ name: "sessionDefaultChannelGroup" }],
360
+ metrics: [
361
+ { name: "sessions" },
362
+ { name: "totalUsers" },
363
+ { name: "newUsers" },
364
+ { name: "engagementRate" },
365
+ { name: "averageSessionDuration" },
366
+ { name: "screenPageViewsPerSession" },
367
+ { name: "conversions" }
368
+ ],
369
+ orderBys: [{ metric: { metricName: "sessions" }, desc: !0 }],
370
+ limit: 15
371
+ });
372
+ return I(r, "sessionDefaultChannelGroup");
373
+ })
374
+ ), l.get(
375
+ "/acquisition/source-medium",
376
+ y(async (e) => {
377
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = await d(o, n, {
378
+ dateRanges: [{ startDate: s, endDate: a }],
379
+ dimensions: [{ name: "sessionSourceMedium" }],
380
+ metrics: [
381
+ { name: "sessions" },
382
+ { name: "totalUsers" },
383
+ { name: "newUsers" },
384
+ { name: "bounceRate" },
385
+ { name: "averageSessionDuration" },
386
+ { name: "conversions" }
387
+ ],
388
+ orderBys: [{ metric: { metricName: "sessions" }, desc: !0 }],
389
+ limit: 20
390
+ });
391
+ return I(r, "sessionSourceMedium");
392
+ })
393
+ ), l.get(
394
+ "/acquisition/referrals",
395
+ y(async (e) => {
396
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = await d(o, n, {
397
+ dateRanges: [{ startDate: s, endDate: a }],
398
+ dimensions: [{ name: "sessionSource" }],
399
+ dimensionFilter: {
400
+ filter: {
401
+ fieldName: "sessionMedium",
402
+ stringFilter: { value: "referral" }
403
+ }
404
+ },
405
+ metrics: [
406
+ { name: "sessions" },
407
+ { name: "totalUsers" },
408
+ { name: "engagementRate" },
409
+ { name: "averageSessionDuration" }
410
+ ],
411
+ orderBys: [{ metric: { metricName: "sessions" }, desc: !0 }],
412
+ limit: 20
413
+ });
414
+ return I(r, "sessionSource");
415
+ })
416
+ ), l.get(
417
+ "/acquisition/campaigns",
418
+ y(async (e) => {
419
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = await d(o, n, {
420
+ dateRanges: [{ startDate: s, endDate: a }],
421
+ dimensions: [{ name: "sessionCampaignName" }],
422
+ dimensionFilter: {
423
+ notExpression: {
424
+ filter: {
425
+ fieldName: "sessionCampaignName",
426
+ stringFilter: { value: "(not set)" }
427
+ }
428
+ }
429
+ },
430
+ metrics: [
431
+ { name: "sessions" },
432
+ { name: "totalUsers" },
433
+ { name: "conversions" },
434
+ { name: "engagementRate" }
435
+ ],
436
+ orderBys: [{ metric: { metricName: "sessions" }, desc: !0 }],
437
+ limit: 20
438
+ });
439
+ return I(r, "sessionCampaignName");
440
+ })
441
+ ), l.get(
442
+ "/content/all-pages",
443
+ y(async (e) => {
444
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = parseInt(t.limit || "50", 10), i = await d(o, n, {
445
+ dateRanges: [{ startDate: s, endDate: a }],
446
+ dimensions: [{ name: "pagePath" }],
447
+ metrics: [
448
+ { name: "screenPageViews" },
449
+ { name: "totalUsers" },
450
+ { name: "averageSessionDuration" },
451
+ { name: "bounceRate" },
452
+ { name: "engagementRate" },
453
+ { name: "sessions" },
454
+ { name: "userEngagementDuration" }
455
+ ],
456
+ orderBys: [{ metric: { metricName: "screenPageViews" }, desc: !0 }],
457
+ limit: r
458
+ });
459
+ return I(i, "pagePath");
460
+ })
461
+ ), l.get(
462
+ "/content/landing-pages",
463
+ y(async (e) => {
464
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = await d(o, n, {
465
+ dateRanges: [{ startDate: s, endDate: a }],
466
+ dimensions: [{ name: "landingPagePlusQueryString" }],
467
+ metrics: [
468
+ { name: "sessions" },
469
+ { name: "totalUsers" },
470
+ { name: "bounceRate" },
471
+ { name: "averageSessionDuration" },
472
+ { name: "screenPageViews" },
473
+ { name: "engagementRate" }
474
+ ],
475
+ orderBys: [{ metric: { metricName: "sessions" }, desc: !0 }],
476
+ limit: 30
477
+ });
478
+ return I(r, "landingPagePlusQueryString");
479
+ })
480
+ ), l.get(
481
+ "/content/exit-pages",
482
+ y(async (e) => {
483
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = await d(o, n, {
484
+ dateRanges: [{ startDate: s, endDate: a }],
485
+ dimensions: [{ name: "pagePath" }],
486
+ metrics: [
487
+ { name: "sessions" },
488
+ { name: "screenPageViews" },
489
+ { name: "totalUsers" },
490
+ { name: "bounceRate" }
491
+ ],
492
+ orderBys: [{ metric: { metricName: "screenPageViews" }, desc: !0 }],
493
+ limit: 20
494
+ }), i = I(r, "pagePath");
495
+ return i.rows = i.rows.map((u) => ({
496
+ ...u,
497
+ exitRate: u.bounceRate || 0
498
+ })), i;
499
+ })
500
+ ), l.get(
501
+ "/content/search-terms",
502
+ y(async (e) => {
503
+ const { accessToken: o, propertyId: n, siteUrl: t } = await m(e), s = f(e), a = s.startDate || "30daysAgo", r = s.endDate || "today";
504
+ if (t)
505
+ try {
506
+ const p = `${T}/sites/${encodeURIComponent(t)}/searchAnalytics/query`;
507
+ console.log("[GA Module] Attempting Search Console query for site:", t);
508
+ const D = await fetch(
509
+ p,
510
+ {
511
+ method: "POST",
512
+ headers: {
513
+ Authorization: `Bearer ${o}`,
514
+ "Content-Type": "application/json"
515
+ },
516
+ body: JSON.stringify({
517
+ startDate: c(a),
518
+ endDate: c(r),
519
+ dimensions: ["query"],
520
+ rowLimit: 30
521
+ })
522
+ }
523
+ );
524
+ if (D.ok) {
525
+ const R = ((await D.json()).rows || []).map((P) => ({
526
+ query: P.keys[0],
527
+ clicks: P.clicks,
528
+ impressions: P.impressions,
529
+ ctr: P.ctr,
530
+ position: P.position
531
+ }));
532
+ if (R.length > 0)
533
+ return console.log(`[GA Module] Search Console returned ${R.length} keyword rows`), {
534
+ rows: R,
535
+ rowCount: R.length,
536
+ source: "search_console"
537
+ };
538
+ console.log("[GA Module] Search Console returned 0 rows, falling through");
539
+ } else {
540
+ const C = await D.text().catch(() => "");
541
+ console.error(`[GA Module] Search Console API error (${D.status}):`, C);
542
+ }
543
+ } catch (p) {
544
+ console.error("[GA Module] Search Console request failed:", p?.message || p);
545
+ }
546
+ else
547
+ console.log("[GA Module] No siteUrl configured, skipping Search Console");
548
+ try {
549
+ const p = await d(o, n, {
550
+ dateRanges: [{ startDate: a, endDate: r }],
551
+ dimensions: [{ name: "sessionGoogleAdsQuery" }],
552
+ metrics: [
553
+ { name: "sessions" },
554
+ { name: "totalUsers" },
555
+ { name: "engagementRate" }
556
+ ],
557
+ dimensionFilter: {
558
+ andGroup: {
559
+ expressions: [
560
+ {
561
+ notExpression: {
562
+ filter: {
563
+ fieldName: "sessionGoogleAdsQuery",
564
+ stringFilter: { value: "(not set)" }
565
+ }
566
+ }
567
+ },
568
+ {
569
+ notExpression: {
570
+ filter: {
571
+ fieldName: "sessionGoogleAdsQuery",
572
+ stringFilter: { value: "(not provided)" }
573
+ }
574
+ }
575
+ }
576
+ ]
577
+ }
578
+ },
579
+ orderBys: [{ metric: { metricName: "sessions" }, desc: !0 }],
580
+ limit: 30
581
+ }), D = I(p, "sessionGoogleAdsQuery");
582
+ if (D.rows.length > 0)
583
+ return { ...D, source: "google_ads" };
584
+ } catch {
585
+ }
586
+ const i = await d(o, n, {
587
+ dateRanges: [{ startDate: a, endDate: r }],
588
+ dimensions: [{ name: "landingPagePlusQueryString" }],
589
+ metrics: [
590
+ { name: "sessions" },
591
+ { name: "totalUsers" },
592
+ { name: "engagementRate" }
593
+ ],
594
+ dimensionFilter: {
595
+ andGroup: {
596
+ expressions: [
597
+ {
598
+ filter: {
599
+ fieldName: "sessionMedium",
600
+ stringFilter: {
601
+ matchType: "EXACT",
602
+ value: "organic"
603
+ }
604
+ }
605
+ },
606
+ {
607
+ notExpression: {
608
+ filter: {
609
+ fieldName: "landingPagePlusQueryString",
610
+ stringFilter: { value: "(not set)" }
611
+ }
612
+ }
613
+ }
614
+ ]
615
+ }
616
+ },
617
+ orderBys: [{ metric: { metricName: "sessions" }, desc: !0 }],
618
+ limit: 30
619
+ });
620
+ return { ...I(i, "landingPagePlusQueryString"), source: "organic_landing_pages" };
621
+ })
622
+ ), l.get(
623
+ "/audience/overview",
624
+ y(async (e) => {
625
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = await d(o, n, {
626
+ dateRanges: [{ startDate: s, endDate: a }],
627
+ dimensions: [{ name: "newVsReturning" }],
628
+ metrics: [
629
+ { name: "totalUsers" },
630
+ { name: "sessions" },
631
+ { name: "engagementRate" },
632
+ { name: "averageSessionDuration" },
633
+ { name: "screenPageViewsPerSession" }
634
+ ],
635
+ metricAggregations: ["TOTAL"]
636
+ });
637
+ return I(r, "newVsReturning");
638
+ })
639
+ ), l.get(
640
+ "/audience/technology",
641
+ y(async (e) => {
642
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = t.dimension || "browser", u = ["browser", "operatingSystem", "screenResolution"].includes(r) ? r : "browser", p = await d(o, n, {
643
+ dateRanges: [{ startDate: s, endDate: a }],
644
+ dimensions: [{ name: u }],
645
+ metrics: [
646
+ { name: "totalUsers" },
647
+ { name: "sessions" },
648
+ { name: "engagementRate" }
649
+ ],
650
+ orderBys: [{ metric: { metricName: "totalUsers" }, desc: !0 }],
651
+ limit: 10
652
+ });
653
+ return I(p, u);
654
+ })
655
+ ), l.get(
656
+ "/audience/languages",
657
+ y(async (e) => {
658
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = await d(o, n, {
659
+ dateRanges: [{ startDate: s, endDate: a }],
660
+ dimensions: [{ name: "language" }],
661
+ metrics: [
662
+ { name: "totalUsers" },
663
+ { name: "sessions" }
664
+ ],
665
+ orderBys: [{ metric: { metricName: "totalUsers" }, desc: !0 }],
666
+ limit: 15
667
+ });
668
+ return I(r, "language");
669
+ })
670
+ ), l.get(
671
+ "/audience/hours",
672
+ y(async (e) => {
673
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = await d(o, n, {
674
+ dateRanges: [{ startDate: s, endDate: a }],
675
+ dimensions: [{ name: "dayOfWeekName" }, { name: "hour" }],
676
+ metrics: [{ name: "sessions" }],
677
+ orderBys: [
678
+ { dimension: { dimensionName: "dayOfWeekName" } },
679
+ { dimension: { dimensionName: "hour" } }
680
+ ],
681
+ limit: 168
682
+ // 7 days × 24 hours
683
+ });
684
+ return N(r, ["dayOfWeekName", "hour"]);
685
+ })
686
+ ), l.get(
687
+ "/audience/cities",
688
+ y(async (e) => {
689
+ const { accessToken: o, propertyId: n } = await m(e), t = f(e), s = t.startDate || "30daysAgo", a = t.endDate || "today", r = await d(o, n, {
690
+ dateRanges: [{ startDate: s, endDate: a }],
691
+ dimensions: [{ name: "city" }, { name: "country" }],
692
+ metrics: [
693
+ { name: "totalUsers" },
694
+ { name: "sessions" }
695
+ ],
696
+ dimensionFilter: {
697
+ notExpression: {
698
+ filter: {
699
+ fieldName: "city",
700
+ stringFilter: { value: "(not set)" }
701
+ }
702
+ }
703
+ },
704
+ orderBys: [{ metric: { metricName: "totalUsers" }, desc: !0 }],
705
+ limit: 20
706
+ });
707
+ return N(r, ["city", "country"]);
708
+ })
709
+ );
710
+ function c(e) {
711
+ const o = e.match(/^(\d+)daysAgo$/);
712
+ if (o) {
713
+ const n = /* @__PURE__ */ new Date();
714
+ return n.setDate(n.getDate() - parseInt(o[1], 10)), n.toISOString().slice(0, 10);
715
+ }
716
+ if (e === "today") return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
717
+ if (e === "yesterday") {
718
+ const n = /* @__PURE__ */ new Date();
719
+ return n.setDate(n.getDate() - 1), n.toISOString().slice(0, 10);
720
+ }
721
+ return e;
722
+ }
723
+ async function g(e, o, n) {
724
+ const t = `${T}/sites/${encodeURIComponent(o)}/searchAnalytics/query`, s = await fetch(t, {
725
+ method: "POST",
726
+ headers: {
727
+ Authorization: `Bearer ${e}`,
728
+ "Content-Type": "application/json"
729
+ },
730
+ body: JSON.stringify(n)
731
+ });
732
+ if (!s.ok) {
733
+ const a = await s.json().catch(() => ({}));
734
+ throw h({
735
+ statusCode: s.status,
736
+ statusMessage: a?.error?.message || "Search Console API request failed"
737
+ });
738
+ }
739
+ return s.json();
740
+ }
741
+ return l.get(
742
+ "/seo/keywords",
743
+ y(async (e) => {
744
+ const { accessToken: o, siteUrl: n } = await m(e);
745
+ if (!n)
746
+ throw h({ statusCode: 400, statusMessage: "Search Console Site URL is not configured. Add it in module settings." });
747
+ const t = f(e), s = c(t.startDate || "30daysAgo"), a = c(t.endDate || "today"), r = Math.min(parseInt(t.limit) || 50, 100), i = await g(o, n, {
748
+ startDate: s,
749
+ endDate: a,
750
+ dimensions: ["query"],
751
+ rowLimit: r
752
+ });
753
+ return {
754
+ rows: (i.rows || []).map((u) => ({
755
+ query: u.keys[0],
756
+ clicks: u.clicks,
757
+ impressions: u.impressions,
758
+ ctr: u.ctr,
759
+ position: u.position
760
+ })),
761
+ rowCount: i.rows?.length || 0
762
+ };
763
+ })
764
+ ), l.get(
765
+ "/seo/pages",
766
+ y(async (e) => {
767
+ const { accessToken: o, siteUrl: n } = await m(e);
768
+ if (!n)
769
+ throw h({ statusCode: 400, statusMessage: "Search Console Site URL is not configured." });
770
+ const t = f(e), s = c(t.startDate || "30daysAgo"), a = c(t.endDate || "today"), r = Math.min(parseInt(t.limit) || 50, 100), i = await g(o, n, {
771
+ startDate: s,
772
+ endDate: a,
773
+ dimensions: ["page"],
774
+ rowLimit: r
775
+ });
776
+ return {
777
+ rows: (i.rows || []).map((u) => ({
778
+ page: u.keys[0],
779
+ clicks: u.clicks,
780
+ impressions: u.impressions,
781
+ ctr: u.ctr,
782
+ position: u.position
783
+ })),
784
+ rowCount: i.rows?.length || 0
785
+ };
786
+ })
787
+ ), l.get(
788
+ "/seo/trends",
789
+ y(async (e) => {
790
+ const { accessToken: o, siteUrl: n } = await m(e);
791
+ if (!n)
792
+ throw h({ statusCode: 400, statusMessage: "Search Console Site URL is not configured." });
793
+ const t = f(e), s = c(t.startDate || "30daysAgo"), a = c(t.endDate || "today"), i = ((await g(o, n, {
794
+ startDate: s,
795
+ endDate: a,
796
+ dimensions: ["date"],
797
+ rowLimit: 500
798
+ })).rows || []).map((p) => ({
799
+ date: p.keys[0],
800
+ clicks: p.clicks,
801
+ impressions: p.impressions,
802
+ ctr: p.ctr,
803
+ position: p.position
804
+ })).sort((p, D) => p.date.localeCompare(D.date)), u = i.reduce(
805
+ (p, D) => ({
806
+ clicks: p.clicks + D.clicks,
807
+ impressions: p.impressions + D.impressions
808
+ }),
809
+ { clicks: 0, impressions: 0 }
810
+ );
811
+ return u.ctr = u.impressions > 0 ? u.clicks / u.impressions : 0, u.avgPosition = i.length > 0 ? i.reduce((p, D) => p + D.position, 0) / i.length : 0, { rows: i, totals: u, rowCount: i.length };
812
+ })
813
+ ), l.get(
814
+ "/seo/query-pages",
815
+ y(async (e) => {
816
+ const { accessToken: o, siteUrl: n } = await m(e);
817
+ if (!n)
818
+ throw h({ statusCode: 400, statusMessage: "Search Console Site URL is not configured." });
819
+ const t = f(e), s = c(t.startDate || "30daysAgo"), a = c(t.endDate || "today"), r = await g(o, n, {
820
+ startDate: s,
821
+ endDate: a,
822
+ dimensions: ["query", "page"],
823
+ rowLimit: 100
824
+ });
825
+ return {
826
+ rows: (r.rows || []).map((i) => ({
827
+ query: i.keys[0],
828
+ page: i.keys[1],
829
+ clicks: i.clicks,
830
+ impressions: i.impressions,
831
+ ctr: i.ctr,
832
+ position: i.position
833
+ })),
834
+ rowCount: r.rows?.length || 0
835
+ };
836
+ })
837
+ ), l.handler;
838
+ }
839
+ function V(w, S = !0) {
840
+ const A = (w.metricHeaders || []).map((s) => s.name), m = (w.dimensionHeaders || []).map((s) => s.name).indexOf("date"), d = [];
841
+ (w.rows || []).forEach((s) => {
842
+ const a = m >= 0 ? s.dimensionValues[m]?.value : s.dimensionValues[0]?.value;
843
+ if (!a || a.length < 8) return;
844
+ const i = { date: `${a.slice(0, 4)}-${a.slice(4, 6)}-${a.slice(6, 8)}` };
845
+ A.forEach((u, p) => {
846
+ i[u] = parseFloat(s.metricValues[p]?.value || "0");
847
+ }), d.push(i);
848
+ });
849
+ const c = {}, g = {};
850
+ w.totals && w.totals.length >= 2 ? A.forEach((s, a) => {
851
+ c[s] = parseFloat(w.totals[0]?.metricValues?.[a]?.value || "0"), g[s] = parseFloat(w.totals[1]?.metricValues?.[a]?.value || "0");
852
+ }) : w.totals && w.totals.length === 1 && A.forEach((s, a) => {
853
+ c[s] = parseFloat(w.totals[0]?.metricValues?.[a]?.value || "0");
854
+ });
855
+ const e = {};
856
+ A.forEach((s) => {
857
+ const a = c[s] || 0, r = g[s];
858
+ r !== void 0 && r !== 0 ? e[s] = (a - r) / r * 100 : e[s] = null;
859
+ });
860
+ const o = /* @__PURE__ */ new Map();
861
+ for (const s of d)
862
+ o.has(s.date) || o.set(s.date, s);
863
+ const n = Array.from(o.values()).sort((s, a) => s.date.localeCompare(a.date)), t = S && w.totals && w.totals.length >= 2 ? n.slice(-Math.ceil(n.length / 2)) : n;
864
+ return {
865
+ rows: t,
866
+ totals: c,
867
+ previousTotals: g,
868
+ changes: e,
869
+ rowCount: t.length
870
+ };
871
+ }
872
+ function I(w, S) {
873
+ const A = (w.metricHeaders || []).map((m) => m.name), l = (w.rows || []).map((m) => {
874
+ const d = {
875
+ [S]: m.dimensionValues[0].value
876
+ };
877
+ return A.forEach((c, g) => {
878
+ d[c] = parseFloat(m.metricValues[g].value);
879
+ }), d;
880
+ });
881
+ return { rows: l, rowCount: w.rowCount || l.length };
882
+ }
883
+ function N(w, S) {
884
+ const A = (w.metricHeaders || []).map((m) => m.name), l = (w.rows || []).map((m) => {
885
+ const d = {};
886
+ return S.forEach((c, g) => {
887
+ d[c] = m.dimensionValues[g]?.value || "";
888
+ }), A.forEach((c, g) => {
889
+ d[c] = parseFloat(m.metricValues[g].value);
890
+ }), d;
891
+ });
892
+ return { rows: l, rowCount: w.rowCount || l.length };
893
+ }
894
+ export {
895
+ G as appointments,
896
+ x as googleAnalytics,
897
+ $ as products
898
+ };