@embedreach/components 0.1.12 → 0.1.13

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.
@@ -1,2535 +0,0 @@
1
- import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
- import { useNavigate } from "react-router-dom";
3
- import { useTranslation } from "react-i18next";
4
- import { c as calculateDateRange, f as formatDateToString, s as styles$9, S as SettingsSheet } from "./styles.module-B1IY0-9d.js";
5
- import { B as Button, c as createDataContext, P as Popover, a as PopoverTrigger, b as PopoverContent, S as Select, d as SelectTrigger, e as SelectValue, f as SelectContent, g as SelectItem, C as Calendar, h as baseRequest, G as GOOGLE_ADS_METRICS_PATH, u as useAdAccount, i as CACHE_STANDARD, M as META_ADS_METRICS_PATH, D as DropdownMenu, j as DropdownMenuTrigger, k as DropdownMenuContent, l as GoogleIcon, m as MetaIcon, n as cn, o as Card, p as Skeleton, q as CardHeader, r as CardTitle, s as CardContent, t as PARTNER_USER_METRICS_PATH, T as Table, v as TableHeader, w as TableRow, x as TableHead, y as TableBody, z as TableCell, A as Tabs, E as TabsList, F as TabsTrigger, H as TabsContent, I as AlertTitle, J as AlertDescription, K as Alert, L as useBusiness, N as useAdAccounts, O as SpinLoader, Q as H2, R as PATHS } from "./index-OHOroL_D.js";
6
- import * as React from "react";
7
- import React__default, { useContext, useMemo, useState, useRef } from "react";
8
- import { format } from "date-fns";
9
- import { CalendarIcon, FilterIcon, TvMinimalPlayIcon, UserPlus, HelpCircle, ChevronLeft, ChevronRight, Loader, Info, CheckCircle, AlertTriangle, AlertCircle } from "lucide-react";
10
- import { AnimatePresence, motion } from "framer-motion";
11
- import { useQuery } from "@tanstack/react-query";
12
- import * as TooltipPrimitive from "@radix-ui/react-tooltip";
13
- import * as RechartsPrimitive from "recharts";
14
- import { AreaChart, CartesianGrid, XAxis, YAxis, Tooltip as Tooltip$1, Area } from "recharts";
15
- import { flushSync } from "react-dom";
16
- import { Slot } from "@radix-ui/react-slot";
17
- import { cva } from "class-variance-authority";
18
- const container$4 = "reach-styles-module__container___GuRla";
19
- const title = "reach-styles-module__title___3HRHQ";
20
- const description = "reach-styles-module__description___RAI1V";
21
- const button = "reach-styles-module__button___5HKgK";
22
- const styles$8 = {
23
- container: container$4,
24
- title,
25
- description,
26
- button
27
- };
28
- const EmptyStateLayout = ({
29
- title: title2,
30
- description: description2,
31
- buttonText,
32
- onButtonClick,
33
- className
34
- }) => {
35
- return /* @__PURE__ */ jsxs("div", { className: `${styles$8.container} ${className}`, children: [
36
- /* @__PURE__ */ jsx("p", { className: styles$8.title, children: title2 }),
37
- /* @__PURE__ */ jsx("p", { className: styles$8.description, children: description2 }),
38
- buttonText && onButtonClick && /* @__PURE__ */ jsx(Button, { size: "lg", onClick: onButtonClick, className: styles$8.button, children: buttonText })
39
- ] });
40
- };
41
- const dashboardFilterReducer = (state, action) => {
42
- switch (action.type) {
43
- case "SET_DATE_RANGE":
44
- return { ...state, dateRange: action.payload };
45
- case "SET_CHANNEL":
46
- return { ...state, channel: action.payload };
47
- case "SET_CAMPAIGN_ID":
48
- return { ...state, campaignId: action.payload };
49
- default:
50
- return state;
51
- }
52
- };
53
- const updateDateRange = (dispatch) => (dateRange) => {
54
- dispatch({ type: "SET_DATE_RANGE", payload: dateRange });
55
- };
56
- const updateChannel = (dispatch) => (channel) => {
57
- dispatch({ type: "SET_CHANNEL", payload: channel });
58
- };
59
- const updateCampaignId = (dispatch) => (campaignId) => {
60
- dispatch({ type: "SET_CAMPAIGN_ID", payload: campaignId });
61
- };
62
- const defaultState = {
63
- dateRange: calculateDateRange("last-month"),
64
- channel: "all",
65
- campaignId: void 0
66
- };
67
- const { Context, Provider } = createDataContext(
68
- dashboardFilterReducer,
69
- {
70
- updateDateRange,
71
- updateChannel,
72
- updateCampaignId
73
- },
74
- defaultState
75
- );
76
- const useDashboardFilterContext = () => {
77
- const context = useContext(Context);
78
- if (context === void 0) {
79
- throw new Error(
80
- "useDashboardFilterContext must be used within an DashboardFilterContext"
81
- );
82
- }
83
- return context;
84
- };
85
- const container$3 = "reach-styles-module__container___vdVjp";
86
- const controlsGroup$1 = "reach-styles-module__controlsGroup___xeRAT";
87
- const dateButton = "reach-styles-module__dateButton___VYYgL";
88
- const icon = "reach-styles-module__icon___yfRDZ";
89
- const presetSection = "reach-styles-module__presetSection___C912U";
90
- const channelItem = "reach-styles-module__channelItem___UGH3Z";
91
- const channelLabel = "reach-styles-module__channelLabel___bP5D0";
92
- const channelLabelDisabled = "reach-styles-module__channelLabelDisabled___C6Psj";
93
- const comingSoonBadge = "reach-styles-module__comingSoonBadge___3wQWV";
94
- const popoverContent = "reach-styles-module__popoverContent___QIr4Q";
95
- const dropdownContent = "reach-styles-module__dropdownContent___3Hhp1";
96
- const styles$7 = {
97
- container: container$3,
98
- controlsGroup: controlsGroup$1,
99
- dateButton,
100
- icon,
101
- presetSection,
102
- channelItem,
103
- channelLabel,
104
- channelLabelDisabled,
105
- comingSoonBadge,
106
- popoverContent,
107
- dropdownContent
108
- };
109
- const DateRangePicker = () => {
110
- var _a, _b;
111
- const { state, updateDateRange: updateDateRange2 } = useDashboardFilterContext();
112
- const [preset, setPreset] = React__default.useState("last-month");
113
- const { t } = useTranslation();
114
- const handlePresetChange = (value) => {
115
- const newRange = calculateDateRange(value);
116
- setPreset(value);
117
- updateDateRange2(newRange);
118
- };
119
- return /* @__PURE__ */ jsx("div", { className: styles$7.container, children: /* @__PURE__ */ jsx("div", { className: styles$7.controlsGroup, children: /* @__PURE__ */ jsxs(Popover, { children: [
120
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", children: [
121
- /* @__PURE__ */ jsx(CalendarIcon, { className: "h-4 w-4 mr-2" }),
122
- ((_a = state.dateRange) == null ? void 0 : _a.from) ? state.dateRange.to ? /* @__PURE__ */ jsxs(Fragment, { children: [
123
- format(state.dateRange.from, "LLL dd, y"),
124
- " -",
125
- " ",
126
- format(state.dateRange.to, "LLL dd, y")
127
- ] }) : format(state.dateRange.from, "LLL dd, y") : t("select_dates")
128
- ] }) }),
129
- /* @__PURE__ */ jsxs(PopoverContent, { className: "w-auto p-0", align: "start", children: [
130
- /* @__PURE__ */ jsx("div", { className: "border-b p-3", children: /* @__PURE__ */ jsxs(Select, { onValueChange: handlePresetChange, value: preset, children: [
131
- /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select preset" }) }),
132
- /* @__PURE__ */ jsxs(SelectContent, { children: [
133
- /* @__PURE__ */ jsx(SelectItem, { value: "last-week", children: t("dates.last_week") }),
134
- /* @__PURE__ */ jsx(SelectItem, { value: "last-month", children: t("dates.last_month") }),
135
- /* @__PURE__ */ jsx(SelectItem, { value: "last-quarter", children: t("dates.last_quarter") }),
136
- /* @__PURE__ */ jsx(SelectItem, { value: "last-year", children: t("dates.last_year") })
137
- ] })
138
- ] }) }),
139
- /* @__PURE__ */ jsx(
140
- Calendar,
141
- {
142
- initialFocus: true,
143
- mode: "range",
144
- defaultMonth: (_b = state.dateRange) == null ? void 0 : _b.from,
145
- selected: state.dateRange,
146
- onSelect: updateDateRange2,
147
- numberOfMonths: 2
148
- }
149
- )
150
- ] })
151
- ] }) }) });
152
- };
153
- const filterButton = "reach-styles-module__filterButton___H-U-Z";
154
- const filterIcon = "reach-styles-module__filterIcon___PegbQ";
155
- const content$1 = "reach-styles-module__content___EEhv3";
156
- const container$2 = "reach-styles-module__container___eaADr";
157
- const header$2 = "reach-styles-module__header___qIzzs";
158
- const options = "reach-styles-module__options___o57N9";
159
- const option = "reach-styles-module__option___BEaPP";
160
- const checkbox = "reach-styles-module__checkbox___1s-NG";
161
- const channelIcon = "reach-styles-module__channelIcon___wR2Oe";
162
- const channelName = "reach-styles-module__channelName___6h5In";
163
- const comingSoon = "reach-styles-module__comingSoon___yc5ZE";
164
- const styles$6 = {
165
- filterButton,
166
- filterIcon,
167
- content: content$1,
168
- container: container$2,
169
- header: header$2,
170
- options,
171
- option,
172
- checkbox,
173
- channelIcon,
174
- channelName,
175
- comingSoon
176
- };
177
- const googleAdMetricsKeys = {
178
- all: ["googleAdMetrics"],
179
- aggregatedByBusiness: (startDate, endDate) => [
180
- ...googleAdMetricsKeys.all,
181
- "aggregated",
182
- "business",
183
- startDate,
184
- endDate
185
- ],
186
- aggregatedByCampaign: (campaignId, startDate, endDate) => [
187
- ...googleAdMetricsKeys.all,
188
- "aggregated",
189
- "campaign",
190
- campaignId,
191
- startDate,
192
- endDate
193
- ],
194
- timeSeriesByBusiness: (startDate, endDate) => [
195
- ...googleAdMetricsKeys.all,
196
- "timeseries",
197
- "business",
198
- startDate,
199
- endDate
200
- ]
201
- };
202
- const getAggregatedMetricsByBusiness$1 = async (startDate, endDate) => {
203
- const formattedStartDate = formatDateToString(startDate);
204
- const formattedEndDate = formatDateToString(endDate);
205
- const queryParams = new URLSearchParams({
206
- startDate: formattedStartDate,
207
- endDate: formattedEndDate
208
- }).toString();
209
- const response = await baseRequest(
210
- `${GOOGLE_ADS_METRICS_PATH}/aggregatedByBusinessId?${queryParams}`
211
- );
212
- return response.data;
213
- };
214
- const getTimeSeriesMetricsByBusiness$1 = async (startDate, endDate) => {
215
- const formattedStartDate = formatDateToString(startDate);
216
- const formattedEndDate = formatDateToString(endDate);
217
- const queryParams = new URLSearchParams({
218
- startDate: formattedStartDate,
219
- endDate: formattedEndDate
220
- }).toString();
221
- const response = await baseRequest(
222
- `${GOOGLE_ADS_METRICS_PATH}/timeseriesbybusinessid?${queryParams}`
223
- );
224
- return response.data;
225
- };
226
- const useAggregatedMetricsByBusiness$1 = (dateRange) => {
227
- const { account } = useAdAccount("google");
228
- const isEnabled = Boolean(
229
- (account == null ? void 0 : account.status) === "connected" && (dateRange == null ? void 0 : dateRange.from) instanceof Date && (dateRange == null ? void 0 : dateRange.to) instanceof Date
230
- );
231
- return useQuery({
232
- queryKey: isEnabled ? googleAdMetricsKeys.aggregatedByBusiness(
233
- formatDateToString(dateRange.from),
234
- formatDateToString(dateRange.to)
235
- ) : googleAdMetricsKeys.all,
236
- queryFn: () => {
237
- if (!(dateRange == null ? void 0 : dateRange.from) || !(dateRange == null ? void 0 : dateRange.to)) {
238
- throw new Error("Date range is required");
239
- }
240
- return getAggregatedMetricsByBusiness$1(dateRange.from, dateRange.to);
241
- },
242
- ...CACHE_STANDARD,
243
- enabled: isEnabled,
244
- refetchOnMount: true,
245
- refetchOnWindowFocus: false,
246
- staleTime: 0
247
- // Force refetch when dates change
248
- });
249
- };
250
- const useTimeSeriesMetrics$1 = (dateRange) => {
251
- const { account } = useAdAccount("google");
252
- const isEnabled = Boolean(
253
- (account == null ? void 0 : account.status) === "connected" && (dateRange == null ? void 0 : dateRange.from) instanceof Date && (dateRange == null ? void 0 : dateRange.to) instanceof Date
254
- );
255
- return useQuery({
256
- queryKey: isEnabled ? googleAdMetricsKeys.timeSeriesByBusiness(
257
- formatDateToString(dateRange.from),
258
- formatDateToString(dateRange.to)
259
- ) : googleAdMetricsKeys.all,
260
- queryFn: () => {
261
- if (!(dateRange == null ? void 0 : dateRange.from) || !(dateRange == null ? void 0 : dateRange.to)) {
262
- throw new Error("Date range is required");
263
- }
264
- return getTimeSeriesMetricsByBusiness$1(dateRange.from, dateRange.to);
265
- },
266
- ...CACHE_STANDARD,
267
- enabled: isEnabled,
268
- refetchOnMount: true,
269
- refetchOnWindowFocus: false,
270
- staleTime: 0
271
- // Force refetch when dates change
272
- });
273
- };
274
- const metaAdMetricsKeys = {
275
- all: ["metaAdMetrics"],
276
- aggregatedByBusiness: (startDate, endDate) => [
277
- ...metaAdMetricsKeys.all,
278
- "aggregated",
279
- "business",
280
- startDate,
281
- endDate
282
- ],
283
- aggregatedByCampaign: (campaignId, startDate, endDate) => [
284
- ...metaAdMetricsKeys.all,
285
- "aggregated",
286
- "campaign",
287
- campaignId,
288
- startDate,
289
- endDate
290
- ],
291
- timeSeriesByBusiness: (startDate, endDate) => [
292
- ...metaAdMetricsKeys.all,
293
- "timeseries",
294
- "business",
295
- startDate,
296
- endDate
297
- ]
298
- };
299
- const getAggregatedMetricsByBusiness = async (startDate, endDate) => {
300
- const formattedStartDate = formatDateToString(startDate);
301
- const formattedEndDate = formatDateToString(endDate);
302
- const queryParams = new URLSearchParams({
303
- startDate: formattedStartDate,
304
- endDate: formattedEndDate
305
- }).toString();
306
- const response = await baseRequest(
307
- `${META_ADS_METRICS_PATH}/aggregatedbybusinessid?${queryParams}`
308
- );
309
- return response.data;
310
- };
311
- const getTimeSeriesMetricsByBusiness = async (startDate, endDate) => {
312
- const formattedStartDate = formatDateToString(startDate);
313
- const formattedEndDate = formatDateToString(endDate);
314
- const queryParams = new URLSearchParams({
315
- startDate: formattedStartDate,
316
- endDate: formattedEndDate
317
- }).toString();
318
- const response = await baseRequest(
319
- `${META_ADS_METRICS_PATH}/timeseriesbybusinessid?${queryParams}`
320
- );
321
- return response.data;
322
- };
323
- const useAggregatedMetricsByBusiness = (dateRange) => {
324
- const { account } = useAdAccount("meta");
325
- const isEnabled = Boolean(
326
- (account == null ? void 0 : account.status) === "connected" && (dateRange == null ? void 0 : dateRange.from) instanceof Date && (dateRange == null ? void 0 : dateRange.to) instanceof Date
327
- );
328
- return useQuery({
329
- queryKey: isEnabled ? metaAdMetricsKeys.aggregatedByBusiness(
330
- formatDateToString(dateRange.from),
331
- formatDateToString(dateRange.to)
332
- ) : metaAdMetricsKeys.all,
333
- queryFn: () => {
334
- if (!(dateRange == null ? void 0 : dateRange.from) || !(dateRange == null ? void 0 : dateRange.to)) {
335
- throw new Error("Date range is required");
336
- }
337
- return getAggregatedMetricsByBusiness(dateRange.from, dateRange.to);
338
- },
339
- ...CACHE_STANDARD,
340
- enabled: isEnabled,
341
- refetchOnMount: true,
342
- refetchOnWindowFocus: false,
343
- staleTime: 0
344
- // Force refetch when dates change
345
- });
346
- };
347
- const useTimeSeriesMetrics = (dateRange) => {
348
- const { account } = useAdAccount("meta");
349
- const isEnabled = Boolean(
350
- (account == null ? void 0 : account.status) === "connected" && (dateRange == null ? void 0 : dateRange.from) instanceof Date && (dateRange == null ? void 0 : dateRange.to) instanceof Date
351
- );
352
- return useQuery({
353
- queryKey: isEnabled ? metaAdMetricsKeys.timeSeriesByBusiness(
354
- formatDateToString(dateRange.from),
355
- formatDateToString(dateRange.to)
356
- ) : metaAdMetricsKeys.all,
357
- queryFn: () => {
358
- if (!(dateRange == null ? void 0 : dateRange.from) || !(dateRange == null ? void 0 : dateRange.to)) {
359
- throw new Error("Date range is required");
360
- }
361
- return getTimeSeriesMetricsByBusiness(dateRange.from, dateRange.to);
362
- },
363
- ...CACHE_STANDARD,
364
- enabled: isEnabled,
365
- refetchOnMount: true,
366
- refetchOnWindowFocus: false,
367
- staleTime: 0
368
- // Force refetch when dates change
369
- });
370
- };
371
- const safeParseFloat = (value) => {
372
- if (!value) return 0;
373
- const parsed = parseFloat(value);
374
- return isNaN(parsed) ? 0 : parsed;
375
- };
376
- const calculateRoas = (revenue, spend) => {
377
- const revenueNum = safeParseFloat(revenue);
378
- const spendNum = safeParseFloat(spend);
379
- if (spendNum === 0) return "0";
380
- return (revenueNum / spendNum).toFixed(2);
381
- };
382
- const useCombinedMetrics = (dateRange, channel) => {
383
- const {
384
- data: googleData,
385
- isLoading: isGoogleLoading,
386
- error: googleError
387
- } = useAggregatedMetricsByBusiness$1(dateRange);
388
- const {
389
- data: metaData,
390
- isLoading: isMetaLoading,
391
- error: metaError
392
- } = useAggregatedMetricsByBusiness(dateRange);
393
- const availableChannels = useMemo(() => {
394
- const channels = [];
395
- if (googleData) channels.push("google");
396
- if (metaData) channels.push("meta");
397
- if (channels.length > 1) channels.push("all");
398
- return channels;
399
- }, [googleData, metaData]);
400
- const sources = useMemo(() => {
401
- const activeSources = [];
402
- if (channel === "all") {
403
- if (googleData) activeSources.push("google");
404
- if (metaData) activeSources.push("meta");
405
- } else if (channel === "google" && googleData) {
406
- activeSources.push("google");
407
- } else if (channel === "meta" && metaData) {
408
- activeSources.push("meta");
409
- }
410
- return activeSources;
411
- }, [channel, googleData, metaData]);
412
- const data = useMemo(() => {
413
- if (channel === "google") return googleData;
414
- if (channel === "meta") return metaData;
415
- if (channel === "all" && googleData && metaData) {
416
- const combinedAdsSpend = safeParseFloat(googleData.ads_spend) + safeParseFloat(metaData.ads_spend);
417
- const combinedMeasuredRevenue = safeParseFloat(googleData.measured_revenue) + safeParseFloat(metaData.measured_revenue);
418
- return {
419
- ...googleData,
420
- ads_spend: combinedAdsSpend.toString(),
421
- clicks: (googleData.clicks || 0) + (metaData.clicks || 0),
422
- impressions: (googleData.impressions || 0) + (metaData.impressions || 0),
423
- measured_revenue: combinedMeasuredRevenue.toString(),
424
- roas: calculateRoas(
425
- combinedMeasuredRevenue.toString(),
426
- combinedAdsSpend.toString()
427
- ),
428
- campaign_ids: [
429
- ...googleData.campaign_ids || [],
430
- ...metaData.campaign_ids || []
431
- ]
432
- };
433
- }
434
- return googleData || metaData || null;
435
- }, [channel, googleData, metaData]);
436
- return {
437
- data,
438
- isLoading: isGoogleLoading || isMetaLoading,
439
- error: googleError || metaError || null,
440
- sources,
441
- availableChannels
442
- };
443
- };
444
- const ChannelFilter = () => {
445
- const { state, updateChannel: updateChannel2 } = useDashboardFilterContext();
446
- const { availableChannels } = useCombinedMetrics(
447
- state.dateRange,
448
- state.channel
449
- );
450
- const getChannelNameAndIcon = (channel) => {
451
- if (channel === "google")
452
- return {
453
- name: "Google Ads",
454
- icon: /* @__PURE__ */ jsx(GoogleIcon, { className: styles$6.channelIcon })
455
- };
456
- if (channel === "meta")
457
- return {
458
- name: "Meta Ads",
459
- icon: /* @__PURE__ */ jsx(MetaIcon, { className: styles$6.channelIcon })
460
- };
461
- return {
462
- name: "All Channels",
463
- icon: /* @__PURE__ */ jsx(TvMinimalPlayIcon, { className: styles$6.channelIcon })
464
- };
465
- };
466
- return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
467
- /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: styles$6.filterButton, children: /* @__PURE__ */ jsx(FilterIcon, { className: styles$6.filterIcon }) }) }),
468
- /* @__PURE__ */ jsx(AnimatePresence, { children: /* @__PURE__ */ jsx(DropdownMenuContent, { sideOffset: 8, className: styles$6.content, children: /* @__PURE__ */ jsxs(
469
- motion.div,
470
- {
471
- className: styles$6.container,
472
- initial: { opacity: 0, y: -8 },
473
- animate: { opacity: 1, y: 0 },
474
- exit: { opacity: 0, y: -8 },
475
- transition: { duration: 0.15 },
476
- children: [
477
- /* @__PURE__ */ jsx("div", { className: styles$6.header, children: "Channel Selection" }),
478
- /* @__PURE__ */ jsx("div", { className: styles$6.options, children: availableChannels.map((channel) => {
479
- const { name: channelName2, icon: channelIcon2 } = getChannelNameAndIcon(channel);
480
- return /* @__PURE__ */ jsxs(
481
- motion.div,
482
- {
483
- className: styles$6.option,
484
- whileHover: { backgroundColor: "var(--hover-bg)" },
485
- whileTap: { scale: 0.98 },
486
- onClick: () => updateChannel2(channel),
487
- children: [
488
- /* @__PURE__ */ jsx(
489
- "input",
490
- {
491
- type: "checkbox",
492
- checked: state.channel === channel || state.channel === "all",
493
- className: styles$6.checkbox,
494
- onChange: () => {
495
- }
496
- }
497
- ),
498
- channelIcon2,
499
- /* @__PURE__ */ jsx("span", { className: styles$6.channelName, children: channelName2 })
500
- ]
501
- },
502
- channel
503
- );
504
- }) })
505
- ]
506
- }
507
- ) }) })
508
- ] });
509
- };
510
- const container$1 = "reach-styles-module__container___7dvcd";
511
- const controlsGroup = "reach-styles-module__controlsGroup___zKKiD";
512
- const styles$5 = {
513
- container: container$1,
514
- controlsGroup
515
- };
516
- const ActionBar = () => {
517
- return /* @__PURE__ */ jsx("div", { className: styles$5.container, children: /* @__PURE__ */ jsxs("div", { className: styles$5.controlsGroup, children: [
518
- /* @__PURE__ */ jsx(DateRangePicker, {}),
519
- /* @__PURE__ */ jsx(ChannelFilter, {})
520
- ] }) });
521
- };
522
- const useMetricsConfig = () => {
523
- const { t } = useTranslation();
524
- return useMemo(
525
- () => [
526
- {
527
- id: "ads_spend",
528
- title: t("dashboard.metrics.ads_spend"),
529
- description: t("dashboard.metrics.ads_spend_description"),
530
- dataKey: "ads_spend",
531
- formatter: (value) => value ? `$${Number(value).toFixed(2)}` : "0"
532
- },
533
- {
534
- id: "measured_revenue",
535
- title: t("dashboard.metrics.measured_revenue"),
536
- description: t("dashboard.metrics.measured_revenue_description"),
537
- dataKey: "measured_revenue",
538
- formatter: (value) => value ? `$${Number(value).toFixed(2)}` : "0"
539
- },
540
- {
541
- id: "measured_conversions",
542
- title: t("dashboard.metrics.measured_conversions"),
543
- description: t("dashboard.metrics.measured_conversions_description"),
544
- dataKey: "measured_conversions",
545
- formatter: (value) => value ? `${Number(value).toFixed(0)}` : "0"
546
- },
547
- {
548
- id: "roas",
549
- title: t("dashboard.metrics.roas"),
550
- description: t("dashboard.metrics.roas_description"),
551
- dataKey: "roas",
552
- formatter: (value) => value ? `${value}x` : "0"
553
- },
554
- {
555
- id: "impressions",
556
- title: t("dashboard.metrics.impressions"),
557
- description: t("dashboard.metrics.impressions_description"),
558
- dataKey: "impressions",
559
- formatter: (value) => value ? value.toLocaleString() : "0"
560
- },
561
- {
562
- id: "clicks",
563
- title: t("dashboard.metrics.clicks"),
564
- description: t("dashboard.metrics.clicks_description"),
565
- dataKey: "clicks",
566
- formatter: (value) => value ? value.toLocaleString() : "0"
567
- },
568
- {
569
- id: "website_visitors",
570
- title: t("dashboard.metrics.website_visitors"),
571
- description: t("dashboard.metrics.website_visitors_description"),
572
- dataKey: "website_visitors",
573
- formatter: (value) => value ? value.toLocaleString() : "0"
574
- },
575
- {
576
- id: "new_leads",
577
- title: t("dashboard.metrics.new_leads"),
578
- description: t("dashboard.metrics.new_leads_description"),
579
- dataKey: "new_leads",
580
- formatter: (value) => value ? value.toLocaleString() : "0"
581
- },
582
- {
583
- id: "customer_ltv",
584
- title: t("dashboard.metrics.customer_ltv"),
585
- description: t("dashboard.metrics.customer_ltv_description"),
586
- dataKey: "customer_ltv",
587
- formatter: (value) => value ? `$${Number(value).toFixed(2)}` : "0"
588
- }
589
- ],
590
- [t]
591
- );
592
- };
593
- const TooltipProvider = TooltipPrimitive.Provider;
594
- const Tooltip = TooltipPrimitive.Root;
595
- const TooltipTrigger = TooltipPrimitive.Trigger;
596
- const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx(TooltipPrimitive.Portal, { children: /* @__PURE__ */ jsx(
597
- TooltipPrimitive.Content,
598
- {
599
- ref,
600
- sideOffset,
601
- className: cn(
602
- "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
603
- className
604
- ),
605
- ...props
606
- }
607
- ) }));
608
- TooltipContent.displayName = TooltipPrimitive.Content.displayName;
609
- const card = "reach-styles-module__card___16dhP";
610
- const content = "reach-styles-module__content___pYRpR";
611
- const header$1 = "reach-styles-module__header___Yxq4J";
612
- const titleRow = "reach-styles-module__titleRow___AdYCN";
613
- const titleContainer = "reach-styles-module__titleContainer___4Sg-1";
614
- const rightColumn = "reach-styles-module__rightColumn___Qm7Pr";
615
- const cardTitle = "reach-styles-module__cardTitle___pgJm7";
616
- const infoIcon = "reach-styles-module__infoIcon___xQWFv";
617
- const platformIcon = "reach-styles-module__platformIcon___K2TuN";
618
- const tooltipText = "reach-styles-module__tooltipText___jQMnv";
619
- const valueContainer = "reach-styles-module__valueContainer___VBnjC";
620
- const metricValue = "reach-styles-module__metricValue___6vxdH";
621
- const footer = "reach-styles-module__footer___HPW6-";
622
- const footerContent = "reach-styles-module__footerContent___znjhg";
623
- const positive = "reach-styles-module__positive___-z57U";
624
- const negative = "reach-styles-module__negative___oWYJQ";
625
- const trendIcon = "reach-styles-module__trendIcon___Bn2Kv";
626
- const footerText = "reach-styles-module__footerText___8661C";
627
- const titleSkeleton = "reach-styles-module__titleSkeleton___EHM42";
628
- const infoIconSkeleton = "reach-styles-module__infoIconSkeleton___Xqch0";
629
- const badgeSkeleton = "reach-styles-module__badgeSkeleton___zfXXM";
630
- const valueSkeleton = "reach-styles-module__valueSkeleton___GuhQN";
631
- const footerSkeleton = "reach-styles-module__footerSkeleton___aeBcZ";
632
- const styles$4 = {
633
- card,
634
- content,
635
- header: header$1,
636
- titleRow,
637
- titleContainer,
638
- rightColumn,
639
- cardTitle,
640
- infoIcon,
641
- platformIcon,
642
- tooltipText,
643
- valueContainer,
644
- metricValue,
645
- footer,
646
- footerContent,
647
- positive,
648
- negative,
649
- trendIcon,
650
- footerText,
651
- titleSkeleton,
652
- infoIconSkeleton,
653
- badgeSkeleton,
654
- valueSkeleton,
655
- footerSkeleton
656
- };
657
- const MetricCard = ({
658
- title: title2,
659
- value,
660
- description: description2,
661
- isLoading,
662
- className,
663
- sources
664
- }) => {
665
- if (isLoading) {
666
- return /* @__PURE__ */ jsx(Card, { className: `${styles$4.card} ${className}`, children: /* @__PURE__ */ jsxs("div", { className: styles$4.content, children: [
667
- /* @__PURE__ */ jsx("div", { className: styles$4.header, children: /* @__PURE__ */ jsxs("div", { className: styles$4.titleRow, children: [
668
- /* @__PURE__ */ jsx(
669
- Skeleton,
670
- {
671
- className: styles$4.titleSkeleton,
672
- "data-testid": "title-skeleton"
673
- }
674
- ),
675
- /* @__PURE__ */ jsxs("div", { className: styles$4.rightColumn, children: [
676
- /* @__PURE__ */ jsx(
677
- Skeleton,
678
- {
679
- className: styles$4.infoIconSkeleton,
680
- "data-testid": "info-icon-skeleton"
681
- }
682
- ),
683
- /* @__PURE__ */ jsx(
684
- Skeleton,
685
- {
686
- className: styles$4.badgeSkeleton,
687
- "data-testid": "badge-skeleton"
688
- }
689
- )
690
- ] })
691
- ] }) }),
692
- /* @__PURE__ */ jsx(
693
- Skeleton,
694
- {
695
- className: styles$4.valueSkeleton,
696
- "data-testid": "value-skeleton"
697
- }
698
- )
699
- ] }) });
700
- }
701
- const getSourceIconAndTooltip = (sources2) => {
702
- if ((sources2 == null ? void 0 : sources2.length) === 1) {
703
- if (sources2[0].type === "google")
704
- return {
705
- icon: /* @__PURE__ */ jsx(GoogleIcon, { className: styles$4.platformIcon }),
706
- tooltip: `${description2} (Google Ads)`
707
- };
708
- if (sources2[0].type === "meta")
709
- return {
710
- icon: /* @__PURE__ */ jsx(MetaIcon, { className: styles$4.platformIcon }),
711
- tooltip: `${description2} (Meta Ads)`
712
- };
713
- }
714
- return {
715
- icon: /* @__PURE__ */ jsx(
716
- TvMinimalPlayIcon,
717
- {
718
- className: styles$4.platformIcon,
719
- "data-testid": "all-channels-icon"
720
- }
721
- ),
722
- tooltip: `${description2} (All Channels)`
723
- };
724
- };
725
- const { icon: icon2, tooltip } = getSourceIconAndTooltip(sources);
726
- return /* @__PURE__ */ jsx(Card, { className: cn(styles$4.card, className), children: /* @__PURE__ */ jsxs("div", { className: styles$4.content, children: [
727
- /* @__PURE__ */ jsx("div", { className: styles$4.header, children: /* @__PURE__ */ jsxs("div", { className: styles$4.titleRow, children: [
728
- /* @__PURE__ */ jsx("div", { className: styles$4.titleContainer, children: /* @__PURE__ */ jsx("span", { className: styles$4.cardTitle, children: title2 }) }),
729
- /* @__PURE__ */ jsx("div", { className: styles$4.rightColumn, children: /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [
730
- /* @__PURE__ */ jsx(TooltipTrigger, { children: icon2 }),
731
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { className: styles$4.tooltipText, children: tooltip }) })
732
- ] }) }) })
733
- ] }) }),
734
- /* @__PURE__ */ jsx("div", { className: styles$4.valueContainer, children: /* @__PURE__ */ jsx("div", { className: styles$4.metricValue, children: value != null ? value : "0" }) })
735
- ] }) });
736
- };
737
- const metricsContainer$1 = "reach-styles-module__metricsContainer___sLSU8";
738
- const styles$3 = {
739
- metricsContainer: metricsContainer$1
740
- };
741
- const MetricsRow = () => {
742
- const { state } = useDashboardFilterContext();
743
- const { data, isLoading, sources } = useCombinedMetrics(
744
- state.dateRange,
745
- state.channel
746
- );
747
- const metricsConfig = useMetricsConfig();
748
- return /* @__PURE__ */ jsx("div", { className: styles$3.metricsContainer, children: metricsConfig.map((metric) => /* @__PURE__ */ jsx(
749
- MetricCard,
750
- {
751
- title: metric.title,
752
- value: metric.formatter(data == null ? void 0 : data[metric.dataKey]),
753
- description: metric.description,
754
- isLoading,
755
- sources: sources.map((source) => ({
756
- icon: source === "google" ? /* @__PURE__ */ jsx(GoogleIcon, {}) : /* @__PURE__ */ jsx(MetaIcon, {}),
757
- type: source
758
- }))
759
- },
760
- metric.id
761
- )) });
762
- };
763
- const THEMES = { light: "", dark: ".dark" };
764
- const ChartContext = React.createContext(null);
765
- function useChart() {
766
- const context = React.useContext(ChartContext);
767
- if (!context) {
768
- throw new Error("useChart must be used within a <ChartContainer />");
769
- }
770
- return context;
771
- }
772
- const ChartContainer = React.forwardRef(({ id, className, children, config, ...props }, ref) => {
773
- const uniqueId = React.useId();
774
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
775
- return /* @__PURE__ */ jsx(ChartContext.Provider, { value: { config }, children: /* @__PURE__ */ jsxs(
776
- "div",
777
- {
778
- "data-chart": chartId,
779
- ref,
780
- className: cn(
781
- "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
782
- className
783
- ),
784
- ...props,
785
- children: [
786
- /* @__PURE__ */ jsx(ChartStyle, { id: chartId, config }),
787
- /* @__PURE__ */ jsx(RechartsPrimitive.ResponsiveContainer, { children })
788
- ]
789
- }
790
- ) });
791
- });
792
- ChartContainer.displayName = "Chart";
793
- const ChartStyle = ({ id, config }) => {
794
- const colorConfig = Object.entries(config).filter(
795
- ([, config2]) => config2.theme || config2.color
796
- );
797
- if (!colorConfig.length) {
798
- return null;
799
- }
800
- return /* @__PURE__ */ jsx(
801
- "style",
802
- {
803
- dangerouslySetInnerHTML: {
804
- __html: Object.entries(THEMES).map(
805
- ([theme, prefix]) => `
806
- ${prefix} [data-chart=${id}] {
807
- ${colorConfig.map(([key, itemConfig]) => {
808
- var _a;
809
- const color = ((_a = itemConfig.theme) == null ? void 0 : _a[theme]) || itemConfig.color;
810
- return color ? ` --color-${key}: ${color};` : null;
811
- }).join("\n")}
812
- }
813
- `
814
- ).join("\n")
815
- }
816
- }
817
- );
818
- };
819
- const ChartTooltipContent = React.forwardRef(
820
- ({
821
- active,
822
- payload,
823
- className,
824
- indicator = "dot",
825
- hideLabel = false,
826
- hideIndicator = false,
827
- label,
828
- labelFormatter,
829
- labelClassName,
830
- formatter,
831
- color,
832
- nameKey,
833
- labelKey
834
- }, ref) => {
835
- const { config } = useChart();
836
- const tooltipLabel2 = React.useMemo(() => {
837
- var _a;
838
- if (hideLabel || !(payload == null ? void 0 : payload.length)) {
839
- return null;
840
- }
841
- const [item] = payload;
842
- const key = `${labelKey || item.dataKey || item.name || "value"}`;
843
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
844
- const value = !labelKey && typeof label === "string" ? ((_a = config[label]) == null ? void 0 : _a.label) || label : itemConfig == null ? void 0 : itemConfig.label;
845
- if (labelFormatter) {
846
- return /* @__PURE__ */ jsx("div", { className: cn("font-medium", labelClassName), children: labelFormatter(value, payload) });
847
- }
848
- if (!value) {
849
- return null;
850
- }
851
- return /* @__PURE__ */ jsx("div", { className: cn("font-medium", labelClassName), children: value });
852
- }, [
853
- label,
854
- labelFormatter,
855
- payload,
856
- hideLabel,
857
- labelClassName,
858
- config,
859
- labelKey
860
- ]);
861
- if (!active || !(payload == null ? void 0 : payload.length)) {
862
- return null;
863
- }
864
- const nestLabel = payload.length === 1 && indicator !== "dot";
865
- return /* @__PURE__ */ jsxs(
866
- "div",
867
- {
868
- ref,
869
- className: cn(
870
- "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
871
- className
872
- ),
873
- children: [
874
- !nestLabel ? tooltipLabel2 : null,
875
- /* @__PURE__ */ jsx("div", { className: "grid gap-1.5", children: payload.map((item, index) => {
876
- const key = `${nameKey || item.name || item.dataKey || "value"}`;
877
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
878
- const indicatorColor = color || item.payload.fill || item.color;
879
- return /* @__PURE__ */ jsx(
880
- "div",
881
- {
882
- className: cn(
883
- "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
884
- indicator === "dot" && "items-center"
885
- ),
886
- children: formatter && (item == null ? void 0 : item.value) !== void 0 && item.name ? formatter(item.value, item.name, item, index, item.payload) : /* @__PURE__ */ jsxs(Fragment, { children: [
887
- (itemConfig == null ? void 0 : itemConfig.icon) ? /* @__PURE__ */ jsx(itemConfig.icon, {}) : !hideIndicator && /* @__PURE__ */ jsx(
888
- "div",
889
- {
890
- className: cn(
891
- "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
892
- {
893
- "h-2.5 w-2.5": indicator === "dot",
894
- "w-1": indicator === "line",
895
- "w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
896
- "my-0.5": nestLabel && indicator === "dashed"
897
- }
898
- ),
899
- style: {
900
- "--color-bg": indicatorColor,
901
- "--color-border": indicatorColor
902
- }
903
- }
904
- ),
905
- /* @__PURE__ */ jsxs(
906
- "div",
907
- {
908
- className: cn(
909
- "flex flex-1 justify-between leading-none",
910
- nestLabel ? "items-end" : "items-center"
911
- ),
912
- children: [
913
- /* @__PURE__ */ jsxs("div", { className: "grid gap-1.5", children: [
914
- nestLabel ? tooltipLabel2 : null,
915
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: (itemConfig == null ? void 0 : itemConfig.label) || item.name })
916
- ] }),
917
- item.value && /* @__PURE__ */ jsx("span", { className: "font-mono font-medium tabular-nums text-foreground", children: item.value.toLocaleString() })
918
- ]
919
- }
920
- )
921
- ] })
922
- },
923
- item.dataKey
924
- );
925
- }) })
926
- ]
927
- }
928
- );
929
- }
930
- );
931
- ChartTooltipContent.displayName = "ChartTooltip";
932
- const ChartLegendContent = React.forwardRef(
933
- ({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
934
- const { config } = useChart();
935
- if (!(payload == null ? void 0 : payload.length)) {
936
- return null;
937
- }
938
- return /* @__PURE__ */ jsx(
939
- "div",
940
- {
941
- ref,
942
- className: cn(
943
- "flex items-center justify-center gap-4",
944
- verticalAlign === "top" ? "pb-3" : "pt-3",
945
- className
946
- ),
947
- children: payload.map((item) => {
948
- const key = `${nameKey || item.dataKey || "value"}`;
949
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
950
- return /* @__PURE__ */ jsxs(
951
- "div",
952
- {
953
- className: cn(
954
- "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
955
- ),
956
- children: [
957
- (itemConfig == null ? void 0 : itemConfig.icon) && !hideIcon ? /* @__PURE__ */ jsx(itemConfig.icon, {}) : /* @__PURE__ */ jsx(
958
- "div",
959
- {
960
- className: "h-2 w-2 shrink-0 rounded-[2px]",
961
- style: {
962
- backgroundColor: item.color
963
- }
964
- }
965
- ),
966
- itemConfig == null ? void 0 : itemConfig.label
967
- ]
968
- },
969
- item.value
970
- );
971
- })
972
- }
973
- );
974
- }
975
- );
976
- ChartLegendContent.displayName = "ChartLegend";
977
- function getPayloadConfigFromPayload(config, payload, key) {
978
- if (typeof payload !== "object" || payload === null) {
979
- return void 0;
980
- }
981
- const payloadPayload = "payload" in payload && typeof payload.payload === "object" && payload.payload !== null ? payload.payload : void 0;
982
- let configLabelKey = key;
983
- if (key in payload && typeof payload[key] === "string") {
984
- configLabelKey = payload[key];
985
- } else if (payloadPayload && key in payloadPayload && typeof payloadPayload[key] === "string") {
986
- configLabelKey = payloadPayload[key];
987
- }
988
- return configLabelKey in config ? config[configLabelKey] : config[key];
989
- }
990
- const chartCard = "reach-styles-module__chartCard___uu68R";
991
- const chartHeader = "reach-styles-module__chartHeader___djnhO";
992
- const chartTitleSection = "reach-styles-module__chartTitleSection___T142a";
993
- const chartTitle = "reach-styles-module__chartTitle___hEksh";
994
- const chartDateRange = "reach-styles-module__chartDateRange___LZWCQ";
995
- const dateContainer = "reach-styles-module__dateContainer___ZZOzN";
996
- const dateIcon = "reach-styles-module__dateIcon___DqCwb";
997
- const dateText = "reach-styles-module__dateText___rqjcQ";
998
- const chartContent = "reach-styles-module__chartContent___1DOVs";
999
- const chartContainer = "reach-styles-module__chartContainer___wysu3";
1000
- const skeletonContainer$1 = "reach-styles-module__skeletonContainer___Fv6Gl";
1001
- const chartSkeleton = "reach-styles-module__chartSkeleton___VRRcE";
1002
- const tooltipContainer = "reach-styles-module__tooltipContainer___yNPN6";
1003
- const tooltipGrid = "reach-styles-module__tooltipGrid___M69Z-";
1004
- const tooltipLabel = "reach-styles-module__tooltipLabel___k90L-";
1005
- const tooltipValue = "reach-styles-module__tooltipValue___76dDZ";
1006
- const areaGradientSpend = "reach-styles-module__areaGradientSpend___428-Z";
1007
- const areaGradientSpendEnd = "reach-styles-module__areaGradientSpendEnd___EOpKD";
1008
- const areaGradientRevenue = "reach-styles-module__areaGradientRevenue___fB5a0";
1009
- const areaGradientRevenueEnd = "reach-styles-module__areaGradientRevenueEnd___X2rf-";
1010
- const legendSpend = "reach-styles-module__legendSpend___-MU-z";
1011
- const legendRevenue = "reach-styles-module__legendRevenue___kWA1z";
1012
- const styles$2 = {
1013
- chartCard,
1014
- chartHeader,
1015
- chartTitleSection,
1016
- chartTitle,
1017
- chartDateRange,
1018
- dateContainer,
1019
- dateIcon,
1020
- dateText,
1021
- chartContent,
1022
- chartContainer,
1023
- skeletonContainer: skeletonContainer$1,
1024
- chartSkeleton,
1025
- tooltipContainer,
1026
- tooltipGrid,
1027
- tooltipLabel,
1028
- tooltipValue,
1029
- areaGradientSpend,
1030
- areaGradientSpendEnd,
1031
- areaGradientRevenue,
1032
- areaGradientRevenueEnd,
1033
- legendSpend,
1034
- legendRevenue
1035
- };
1036
- const ROASChartLegend = () => {
1037
- const { t } = useTranslation();
1038
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center justify-center gap-2 mt-1", children: [
1039
- /* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center justify-between", children: [
1040
- /* @__PURE__ */ jsx("div", { className: `w-2 h-2 rounded-full ${styles$2.legendSpend}` }),
1041
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium ml-1", children: t("dashboard.channel_performance.spend") })
1042
- ] }),
1043
- /* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center justify-between", children: [
1044
- /* @__PURE__ */ jsx("div", { className: `w-2 h-2 rounded-full ${styles$2.legendRevenue}` }),
1045
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium ml-1", children: t("dashboard.channel_performance.revenue") })
1046
- ] })
1047
- ] });
1048
- };
1049
- const useCombinedTimeSeriesMetrics = (dateRange, channel) => {
1050
- const {
1051
- data: googleData,
1052
- isLoading: googleIsLoading,
1053
- error: googleError
1054
- } = useTimeSeriesMetrics$1(dateRange);
1055
- const {
1056
- data: metaData,
1057
- isLoading: metaIsLoading,
1058
- error: metaError
1059
- } = useTimeSeriesMetrics(dateRange);
1060
- const data = useMemo(() => {
1061
- if (!googleData && !metaData) return [];
1062
- const dateMap = /* @__PURE__ */ new Map();
1063
- if (channel === "google" || channel === "all") {
1064
- googleData == null ? void 0 : googleData.forEach((point) => {
1065
- dateMap.set(point.date, {
1066
- date: point.date,
1067
- ads_spend: point.ads_spend || "0",
1068
- measured_revenue: point.measured_revenue || "0"
1069
- });
1070
- });
1071
- }
1072
- if (channel === "meta" || channel === "all") {
1073
- metaData == null ? void 0 : metaData.forEach((point) => {
1074
- const existing = dateMap.get(point.date);
1075
- dateMap.set(point.date, {
1076
- date: point.date,
1077
- ads_spend: ((Number(existing == null ? void 0 : existing.ads_spend) || 0) + (Number(point.ads_spend) || 0)).toString(),
1078
- measured_revenue: ((Number(existing == null ? void 0 : existing.measured_revenue) || 0) + (Number(point.measured_revenue) || 0)).toString()
1079
- });
1080
- });
1081
- }
1082
- const combinedData = Array.from(dateMap.values()).sort(
1083
- (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
1084
- );
1085
- let spendTotal = 0;
1086
- let revenueTotal = 0;
1087
- return combinedData.map((point) => {
1088
- spendTotal += Number(point.ads_spend) || 0;
1089
- revenueTotal += Number(point.measured_revenue) || 0;
1090
- const roasValue = spendTotal > 0 ? Number((revenueTotal / spendTotal).toFixed(2)) : 0;
1091
- return {
1092
- ...point,
1093
- cumulative_spend: spendTotal,
1094
- cumulative_revenue: revenueTotal,
1095
- cumulative_roas: roasValue
1096
- };
1097
- });
1098
- }, [googleData, metaData, channel]);
1099
- return {
1100
- data,
1101
- isLoading: googleIsLoading || metaIsLoading,
1102
- error: googleError || metaError || null
1103
- };
1104
- };
1105
- const formatCurrency = (amount) => new Intl.NumberFormat("en-US", {
1106
- style: "currency",
1107
- currency: "USD",
1108
- minimumFractionDigits: 2,
1109
- maximumFractionDigits: 2
1110
- }).format(amount);
1111
- const ROASChart = ({ height }) => {
1112
- const { state } = useDashboardFilterContext();
1113
- const { data: cumulativeData, isLoading } = useCombinedTimeSeriesMetrics(
1114
- state.dateRange,
1115
- state.channel
1116
- );
1117
- const { t } = useTranslation();
1118
- const chartConfig = {
1119
- spend: {
1120
- label: t("total_ads_spend"),
1121
- color: "var(--reach-destructive)"
1122
- },
1123
- revenue: {
1124
- label: t("revenue"),
1125
- color: "var(--reach-primary)"
1126
- }
1127
- };
1128
- const maxValue = useMemo(() => {
1129
- if (!cumulativeData.length) return 0;
1130
- return Math.max(
1131
- cumulativeData[cumulativeData.length - 1].cumulative_spend,
1132
- cumulativeData[cumulativeData.length - 1].cumulative_revenue
1133
- );
1134
- }, [cumulativeData]);
1135
- const yAxisMax = useMemo(() => {
1136
- if (maxValue <= 0) return 10;
1137
- const magnitude = Math.pow(10, Math.floor(Math.log10(maxValue)));
1138
- return Math.ceil(maxValue / magnitude) * magnitude;
1139
- }, [maxValue]);
1140
- return /* @__PURE__ */ jsxs(Card, { className: styles$2.chartCard, children: [
1141
- /* @__PURE__ */ jsx(CardHeader, { className: styles$2.chartHeader, children: /* @__PURE__ */ jsxs("div", { className: styles$2.chartTitleSection, children: [
1142
- /* @__PURE__ */ jsx(CardTitle, { className: styles$2.chartTitle, children: t("dashboard.roas_chart.title") }),
1143
- /* @__PURE__ */ jsx(ROASChartLegend, {})
1144
- ] }) }),
1145
- /* @__PURE__ */ jsx(CardContent, { className: styles$2.chartContent, children: isLoading ? /* @__PURE__ */ jsx("div", { className: styles$2.chartContainer, children: /* @__PURE__ */ jsx(Skeleton, { className: styles$2.chartSkeleton }) }) : /* @__PURE__ */ jsx(
1146
- ChartContainer,
1147
- {
1148
- config: chartConfig,
1149
- className: cn(
1150
- `aspect-auto h-[${height}px] w-full`,
1151
- styles$2.chartContainer
1152
- ),
1153
- children: /* @__PURE__ */ jsxs(AreaChart, { data: cumulativeData, children: [
1154
- /* @__PURE__ */ jsxs("defs", { children: [
1155
- /* @__PURE__ */ jsxs("linearGradient", { id: "fillSpend", x1: "0", y1: "0", x2: "0", y2: "1", children: [
1156
- /* @__PURE__ */ jsx("stop", { offset: "5%", className: styles$2.areaGradientSpend }),
1157
- /* @__PURE__ */ jsx("stop", { offset: "95%", className: styles$2.areaGradientSpendEnd })
1158
- ] }),
1159
- /* @__PURE__ */ jsxs("linearGradient", { id: "fillRevenue", x1: "0", y1: "0", x2: "0", y2: "1", children: [
1160
- /* @__PURE__ */ jsx("stop", { offset: "5%", className: styles$2.areaGradientRevenue }),
1161
- /* @__PURE__ */ jsx(
1162
- "stop",
1163
- {
1164
- offset: "95%",
1165
- className: styles$2.areaGradientRevenueEnd
1166
- }
1167
- )
1168
- ] })
1169
- ] }),
1170
- /* @__PURE__ */ jsx(CartesianGrid, { vertical: false }),
1171
- /* @__PURE__ */ jsx(
1172
- XAxis,
1173
- {
1174
- dataKey: "date",
1175
- tickLine: false,
1176
- axisLine: false,
1177
- tickMargin: 8,
1178
- minTickGap: 32,
1179
- stroke: "#888888",
1180
- fontSize: 12
1181
- }
1182
- ),
1183
- /* @__PURE__ */ jsx(
1184
- YAxis,
1185
- {
1186
- stroke: "#888888",
1187
- fontSize: 12,
1188
- tickLine: false,
1189
- axisLine: false,
1190
- tickFormatter: formatCurrency,
1191
- domain: [0, yAxisMax]
1192
- }
1193
- ),
1194
- /* @__PURE__ */ jsx(
1195
- Tooltip$1,
1196
- {
1197
- content: ({ active, payload }) => {
1198
- if (active && payload && payload.length) {
1199
- const spendValue = payload[0].value;
1200
- const revenueValue = payload[1].value;
1201
- return /* @__PURE__ */ jsx("div", { className: styles$2.tooltipContainer, children: /* @__PURE__ */ jsxs("div", { className: styles$2.tooltipGrid, children: [
1202
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
1203
- /* @__PURE__ */ jsx("span", { className: styles$2.tooltipLabel, children: t("date") }),
1204
- /* @__PURE__ */ jsx("span", { className: styles$2.tooltipValue, children: payload[0].payload.date })
1205
- ] }),
1206
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
1207
- /* @__PURE__ */ jsx("span", { className: styles$2.tooltipLabel, children: t("total_ads_spend") }),
1208
- /* @__PURE__ */ jsx("span", { className: styles$2.tooltipValue, children: formatCurrency(spendValue) })
1209
- ] }),
1210
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
1211
- /* @__PURE__ */ jsx("span", { className: styles$2.tooltipLabel, children: t("revenue") }),
1212
- /* @__PURE__ */ jsx("span", { className: styles$2.tooltipValue, children: formatCurrency(revenueValue) })
1213
- ] }),
1214
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
1215
- /* @__PURE__ */ jsx("span", { className: styles$2.tooltipLabel, children: t("roas") }),
1216
- /* @__PURE__ */ jsxs("span", { className: styles$2.tooltipValue, children: [
1217
- Number(revenueValue / spendValue).toFixed(2),
1218
- "x"
1219
- ] })
1220
- ] })
1221
- ] }) });
1222
- }
1223
- return null;
1224
- }
1225
- }
1226
- ),
1227
- /* @__PURE__ */ jsx(
1228
- Area,
1229
- {
1230
- type: "monotone",
1231
- dataKey: "cumulative_spend",
1232
- stroke: "#FF6B6B",
1233
- fill: "url(#fillSpend)",
1234
- strokeWidth: 2,
1235
- name: t("total_ads_spend")
1236
- }
1237
- ),
1238
- /* @__PURE__ */ jsx(
1239
- Area,
1240
- {
1241
- type: "monotone",
1242
- dataKey: "cumulative_revenue",
1243
- stroke: "#4ECDC4",
1244
- fill: "url(#fillRevenue)",
1245
- strokeWidth: 2,
1246
- name: t("revenue")
1247
- }
1248
- )
1249
- ] })
1250
- }
1251
- ) })
1252
- ] });
1253
- };
1254
- function memo(getDeps, fn, opts) {
1255
- var _a;
1256
- let deps = (_a = opts.initialDeps) != null ? _a : [];
1257
- let result;
1258
- return () => {
1259
- var _a2, _b, _c, _d;
1260
- let depTime;
1261
- if (opts.key && ((_a2 = opts.debug) == null ? void 0 : _a2.call(opts))) depTime = Date.now();
1262
- const newDeps = getDeps();
1263
- const depsChanged = newDeps.length !== deps.length || newDeps.some((dep, index) => deps[index] !== dep);
1264
- if (!depsChanged) {
1265
- return result;
1266
- }
1267
- deps = newDeps;
1268
- let resultTime;
1269
- if (opts.key && ((_b = opts.debug) == null ? void 0 : _b.call(opts))) resultTime = Date.now();
1270
- result = fn(...newDeps);
1271
- if (opts.key && ((_c = opts.debug) == null ? void 0 : _c.call(opts))) {
1272
- const depEndTime = Math.round((Date.now() - depTime) * 100) / 100;
1273
- const resultEndTime = Math.round((Date.now() - resultTime) * 100) / 100;
1274
- const resultFpsPercentage = resultEndTime / 16;
1275
- const pad = (str, num) => {
1276
- str = String(str);
1277
- while (str.length < num) {
1278
- str = " " + str;
1279
- }
1280
- return str;
1281
- };
1282
- console.info(
1283
- `%c⏱ ${pad(resultEndTime, 5)} /${pad(depEndTime, 5)} ms`,
1284
- `
1285
- font-size: .6rem;
1286
- font-weight: bold;
1287
- color: hsl(${Math.max(
1288
- 0,
1289
- Math.min(120 - 120 * resultFpsPercentage, 120)
1290
- )}deg 100% 31%);`,
1291
- opts == null ? void 0 : opts.key
1292
- );
1293
- }
1294
- (_d = opts == null ? void 0 : opts.onChange) == null ? void 0 : _d.call(opts, result);
1295
- return result;
1296
- };
1297
- }
1298
- function notUndefined(value, msg) {
1299
- if (value === void 0) {
1300
- throw new Error(`Unexpected undefined${""}`);
1301
- } else {
1302
- return value;
1303
- }
1304
- }
1305
- const approxEqual = (a, b) => Math.abs(a - b) < 1;
1306
- const debounce = (targetWindow, fn, ms) => {
1307
- let timeoutId;
1308
- return function(...args) {
1309
- targetWindow.clearTimeout(timeoutId);
1310
- timeoutId = targetWindow.setTimeout(() => fn.apply(this, args), ms);
1311
- };
1312
- };
1313
- const defaultKeyExtractor = (index) => index;
1314
- const defaultRangeExtractor = (range) => {
1315
- const start = Math.max(range.startIndex - range.overscan, 0);
1316
- const end = Math.min(range.endIndex + range.overscan, range.count - 1);
1317
- const arr = [];
1318
- for (let i = start; i <= end; i++) {
1319
- arr.push(i);
1320
- }
1321
- return arr;
1322
- };
1323
- const observeElementRect = (instance, cb) => {
1324
- const element = instance.scrollElement;
1325
- if (!element) {
1326
- return;
1327
- }
1328
- const targetWindow = instance.targetWindow;
1329
- if (!targetWindow) {
1330
- return;
1331
- }
1332
- const handler = (rect) => {
1333
- const { width, height } = rect;
1334
- cb({ width: Math.round(width), height: Math.round(height) });
1335
- };
1336
- handler(element.getBoundingClientRect());
1337
- if (!targetWindow.ResizeObserver) {
1338
- return () => {
1339
- };
1340
- }
1341
- const observer = new targetWindow.ResizeObserver((entries) => {
1342
- const run = () => {
1343
- const entry = entries[0];
1344
- if (entry == null ? void 0 : entry.borderBoxSize) {
1345
- const box = entry.borderBoxSize[0];
1346
- if (box) {
1347
- handler({ width: box.inlineSize, height: box.blockSize });
1348
- return;
1349
- }
1350
- }
1351
- handler(element.getBoundingClientRect());
1352
- };
1353
- instance.options.useAnimationFrameWithResizeObserver ? requestAnimationFrame(run) : run();
1354
- });
1355
- observer.observe(element, { box: "border-box" });
1356
- return () => {
1357
- observer.unobserve(element);
1358
- };
1359
- };
1360
- const addEventListenerOptions = {
1361
- passive: true
1362
- };
1363
- const supportsScrollend = typeof window == "undefined" ? true : "onscrollend" in window;
1364
- const observeElementOffset = (instance, cb) => {
1365
- const element = instance.scrollElement;
1366
- if (!element) {
1367
- return;
1368
- }
1369
- const targetWindow = instance.targetWindow;
1370
- if (!targetWindow) {
1371
- return;
1372
- }
1373
- let offset = 0;
1374
- const fallback = instance.options.useScrollendEvent && supportsScrollend ? () => void 0 : debounce(
1375
- targetWindow,
1376
- () => {
1377
- cb(offset, false);
1378
- },
1379
- instance.options.isScrollingResetDelay
1380
- );
1381
- const createHandler = (isScrolling) => () => {
1382
- const { horizontal, isRtl } = instance.options;
1383
- offset = horizontal ? element["scrollLeft"] * (isRtl && -1 || 1) : element["scrollTop"];
1384
- fallback();
1385
- cb(offset, isScrolling);
1386
- };
1387
- const handler = createHandler(true);
1388
- const endHandler = createHandler(false);
1389
- endHandler();
1390
- element.addEventListener("scroll", handler, addEventListenerOptions);
1391
- const registerScrollendEvent = instance.options.useScrollendEvent && supportsScrollend;
1392
- if (registerScrollendEvent) {
1393
- element.addEventListener("scrollend", endHandler, addEventListenerOptions);
1394
- }
1395
- return () => {
1396
- element.removeEventListener("scroll", handler);
1397
- if (registerScrollendEvent) {
1398
- element.removeEventListener("scrollend", endHandler);
1399
- }
1400
- };
1401
- };
1402
- const measureElement = (element, entry, instance) => {
1403
- if (entry == null ? void 0 : entry.borderBoxSize) {
1404
- const box = entry.borderBoxSize[0];
1405
- if (box) {
1406
- const size = Math.round(
1407
- box[instance.options.horizontal ? "inlineSize" : "blockSize"]
1408
- );
1409
- return size;
1410
- }
1411
- }
1412
- return Math.round(
1413
- element.getBoundingClientRect()[instance.options.horizontal ? "width" : "height"]
1414
- );
1415
- };
1416
- const elementScroll = (offset, {
1417
- adjustments = 0,
1418
- behavior
1419
- }, instance) => {
1420
- var _a, _b;
1421
- const toOffset = offset + adjustments;
1422
- (_b = (_a = instance.scrollElement) == null ? void 0 : _a.scrollTo) == null ? void 0 : _b.call(_a, {
1423
- [instance.options.horizontal ? "left" : "top"]: toOffset,
1424
- behavior
1425
- });
1426
- };
1427
- class Virtualizer {
1428
- constructor(opts) {
1429
- this.unsubs = [];
1430
- this.scrollElement = null;
1431
- this.targetWindow = null;
1432
- this.isScrolling = false;
1433
- this.scrollToIndexTimeoutId = null;
1434
- this.measurementsCache = [];
1435
- this.itemSizeCache = /* @__PURE__ */ new Map();
1436
- this.pendingMeasuredCacheIndexes = [];
1437
- this.scrollRect = null;
1438
- this.scrollOffset = null;
1439
- this.scrollDirection = null;
1440
- this.scrollAdjustments = 0;
1441
- this.elementsCache = /* @__PURE__ */ new Map();
1442
- this.observer = /* @__PURE__ */ (() => {
1443
- let _ro = null;
1444
- const get = () => {
1445
- if (_ro) {
1446
- return _ro;
1447
- }
1448
- if (!this.targetWindow || !this.targetWindow.ResizeObserver) {
1449
- return null;
1450
- }
1451
- return _ro = new this.targetWindow.ResizeObserver((entries) => {
1452
- entries.forEach((entry) => {
1453
- const run = () => {
1454
- this._measureElement(entry.target, entry);
1455
- };
1456
- this.options.useAnimationFrameWithResizeObserver ? requestAnimationFrame(run) : run();
1457
- });
1458
- });
1459
- };
1460
- return {
1461
- disconnect: () => {
1462
- var _a;
1463
- (_a = get()) == null ? void 0 : _a.disconnect();
1464
- _ro = null;
1465
- },
1466
- observe: (target) => {
1467
- var _a;
1468
- return (_a = get()) == null ? void 0 : _a.observe(target, { box: "border-box" });
1469
- },
1470
- unobserve: (target) => {
1471
- var _a;
1472
- return (_a = get()) == null ? void 0 : _a.unobserve(target);
1473
- }
1474
- };
1475
- })();
1476
- this.range = null;
1477
- this.setOptions = (opts2) => {
1478
- Object.entries(opts2).forEach(([key, value]) => {
1479
- if (typeof value === "undefined") delete opts2[key];
1480
- });
1481
- this.options = {
1482
- debug: false,
1483
- initialOffset: 0,
1484
- overscan: 1,
1485
- paddingStart: 0,
1486
- paddingEnd: 0,
1487
- scrollPaddingStart: 0,
1488
- scrollPaddingEnd: 0,
1489
- horizontal: false,
1490
- getItemKey: defaultKeyExtractor,
1491
- rangeExtractor: defaultRangeExtractor,
1492
- onChange: () => {
1493
- },
1494
- measureElement,
1495
- initialRect: { width: 0, height: 0 },
1496
- scrollMargin: 0,
1497
- gap: 0,
1498
- indexAttribute: "data-index",
1499
- initialMeasurementsCache: [],
1500
- lanes: 1,
1501
- isScrollingResetDelay: 150,
1502
- enabled: true,
1503
- isRtl: false,
1504
- useScrollendEvent: true,
1505
- useAnimationFrameWithResizeObserver: false,
1506
- ...opts2
1507
- };
1508
- };
1509
- this.notify = (sync) => {
1510
- var _a, _b;
1511
- (_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a, this, sync);
1512
- };
1513
- this.maybeNotify = memo(
1514
- () => {
1515
- this.calculateRange();
1516
- return [
1517
- this.isScrolling,
1518
- this.range ? this.range.startIndex : null,
1519
- this.range ? this.range.endIndex : null
1520
- ];
1521
- },
1522
- (isScrolling) => {
1523
- this.notify(isScrolling);
1524
- },
1525
- {
1526
- key: process.env.NODE_ENV !== "production" && "maybeNotify",
1527
- debug: () => this.options.debug,
1528
- initialDeps: [
1529
- this.isScrolling,
1530
- this.range ? this.range.startIndex : null,
1531
- this.range ? this.range.endIndex : null
1532
- ]
1533
- }
1534
- );
1535
- this.cleanup = () => {
1536
- this.unsubs.filter(Boolean).forEach((d) => d());
1537
- this.unsubs = [];
1538
- this.observer.disconnect();
1539
- this.scrollElement = null;
1540
- this.targetWindow = null;
1541
- };
1542
- this._didMount = () => {
1543
- return () => {
1544
- this.cleanup();
1545
- };
1546
- };
1547
- this._willUpdate = () => {
1548
- var _a2;
1549
- var _a;
1550
- const scrollElement = this.options.enabled ? this.options.getScrollElement() : null;
1551
- if (this.scrollElement !== scrollElement) {
1552
- this.cleanup();
1553
- if (!scrollElement) {
1554
- this.maybeNotify();
1555
- return;
1556
- }
1557
- this.scrollElement = scrollElement;
1558
- if (this.scrollElement && "ownerDocument" in this.scrollElement) {
1559
- this.targetWindow = this.scrollElement.ownerDocument.defaultView;
1560
- } else {
1561
- this.targetWindow = (_a2 = (_a = this.scrollElement) == null ? void 0 : _a.window) != null ? _a2 : null;
1562
- }
1563
- this.elementsCache.forEach((cached) => {
1564
- this.observer.observe(cached);
1565
- });
1566
- this._scrollToOffset(this.getScrollOffset(), {
1567
- adjustments: void 0,
1568
- behavior: void 0
1569
- });
1570
- this.unsubs.push(
1571
- this.options.observeElementRect(this, (rect) => {
1572
- this.scrollRect = rect;
1573
- this.maybeNotify();
1574
- })
1575
- );
1576
- this.unsubs.push(
1577
- this.options.observeElementOffset(this, (offset, isScrolling) => {
1578
- this.scrollAdjustments = 0;
1579
- this.scrollDirection = isScrolling ? this.getScrollOffset() < offset ? "forward" : "backward" : null;
1580
- this.scrollOffset = offset;
1581
- this.isScrolling = isScrolling;
1582
- this.maybeNotify();
1583
- })
1584
- );
1585
- }
1586
- };
1587
- this.getSize = () => {
1588
- var _a;
1589
- if (!this.options.enabled) {
1590
- this.scrollRect = null;
1591
- return 0;
1592
- }
1593
- this.scrollRect = (_a = this.scrollRect) != null ? _a : this.options.initialRect;
1594
- return this.scrollRect[this.options.horizontal ? "width" : "height"];
1595
- };
1596
- this.getScrollOffset = () => {
1597
- var _a;
1598
- if (!this.options.enabled) {
1599
- this.scrollOffset = null;
1600
- return 0;
1601
- }
1602
- this.scrollOffset = (_a = this.scrollOffset) != null ? _a : typeof this.options.initialOffset === "function" ? this.options.initialOffset() : this.options.initialOffset;
1603
- return this.scrollOffset;
1604
- };
1605
- this.getFurthestMeasurement = (measurements, index) => {
1606
- const furthestMeasurementsFound = /* @__PURE__ */ new Map();
1607
- const furthestMeasurements = /* @__PURE__ */ new Map();
1608
- for (let m = index - 1; m >= 0; m--) {
1609
- const measurement = measurements[m];
1610
- if (furthestMeasurementsFound.has(measurement.lane)) {
1611
- continue;
1612
- }
1613
- const previousFurthestMeasurement = furthestMeasurements.get(
1614
- measurement.lane
1615
- );
1616
- if (previousFurthestMeasurement == null || measurement.end > previousFurthestMeasurement.end) {
1617
- furthestMeasurements.set(measurement.lane, measurement);
1618
- } else if (measurement.end < previousFurthestMeasurement.end) {
1619
- furthestMeasurementsFound.set(measurement.lane, true);
1620
- }
1621
- if (furthestMeasurementsFound.size === this.options.lanes) {
1622
- break;
1623
- }
1624
- }
1625
- return furthestMeasurements.size === this.options.lanes ? Array.from(furthestMeasurements.values()).sort((a, b) => {
1626
- if (a.end === b.end) {
1627
- return a.index - b.index;
1628
- }
1629
- return a.end - b.end;
1630
- })[0] : void 0;
1631
- };
1632
- this.getMeasurementOptions = memo(
1633
- () => [
1634
- this.options.count,
1635
- this.options.paddingStart,
1636
- this.options.scrollMargin,
1637
- this.options.getItemKey,
1638
- this.options.enabled
1639
- ],
1640
- (count, paddingStart, scrollMargin, getItemKey, enabled) => {
1641
- this.pendingMeasuredCacheIndexes = [];
1642
- return {
1643
- count,
1644
- paddingStart,
1645
- scrollMargin,
1646
- getItemKey,
1647
- enabled
1648
- };
1649
- },
1650
- {
1651
- key: false
1652
- }
1653
- );
1654
- this.getMeasurements = memo(
1655
- () => [this.getMeasurementOptions(), this.itemSizeCache],
1656
- ({ count, paddingStart, scrollMargin, getItemKey, enabled }, itemSizeCache) => {
1657
- if (!enabled) {
1658
- this.measurementsCache = [];
1659
- this.itemSizeCache.clear();
1660
- return [];
1661
- }
1662
- if (this.measurementsCache.length === 0) {
1663
- this.measurementsCache = this.options.initialMeasurementsCache;
1664
- this.measurementsCache.forEach((item) => {
1665
- this.itemSizeCache.set(item.key, item.size);
1666
- });
1667
- }
1668
- const min = this.pendingMeasuredCacheIndexes.length > 0 ? Math.min(...this.pendingMeasuredCacheIndexes) : 0;
1669
- this.pendingMeasuredCacheIndexes = [];
1670
- const measurements = this.measurementsCache.slice(0, min);
1671
- for (let i = min; i < count; i++) {
1672
- const key = getItemKey(i);
1673
- const furthestMeasurement = this.options.lanes === 1 ? measurements[i - 1] : this.getFurthestMeasurement(measurements, i);
1674
- const start = furthestMeasurement ? furthestMeasurement.end + this.options.gap : paddingStart + scrollMargin;
1675
- const measuredSize = itemSizeCache.get(key);
1676
- const size = typeof measuredSize === "number" ? measuredSize : this.options.estimateSize(i);
1677
- const end = start + size;
1678
- const lane = furthestMeasurement ? furthestMeasurement.lane : i % this.options.lanes;
1679
- measurements[i] = {
1680
- index: i,
1681
- start,
1682
- size,
1683
- end,
1684
- key,
1685
- lane
1686
- };
1687
- }
1688
- this.measurementsCache = measurements;
1689
- return measurements;
1690
- },
1691
- {
1692
- key: process.env.NODE_ENV !== "production" && "getMeasurements",
1693
- debug: () => this.options.debug
1694
- }
1695
- );
1696
- this.calculateRange = memo(
1697
- () => [this.getMeasurements(), this.getSize(), this.getScrollOffset()],
1698
- (measurements, outerSize, scrollOffset) => {
1699
- return this.range = measurements.length > 0 && outerSize > 0 ? calculateRange({
1700
- measurements,
1701
- outerSize,
1702
- scrollOffset
1703
- }) : null;
1704
- },
1705
- {
1706
- key: process.env.NODE_ENV !== "production" && "calculateRange",
1707
- debug: () => this.options.debug
1708
- }
1709
- );
1710
- this.getVirtualIndexes = memo(
1711
- () => {
1712
- let startIndex = null;
1713
- let endIndex = null;
1714
- const range = this.calculateRange();
1715
- if (range) {
1716
- startIndex = range.startIndex;
1717
- endIndex = range.endIndex;
1718
- }
1719
- return [
1720
- this.options.rangeExtractor,
1721
- this.options.overscan,
1722
- this.options.count,
1723
- startIndex,
1724
- endIndex
1725
- ];
1726
- },
1727
- (rangeExtractor, overscan, count, startIndex, endIndex) => {
1728
- return startIndex === null || endIndex === null ? [] : rangeExtractor({
1729
- startIndex,
1730
- endIndex,
1731
- overscan,
1732
- count
1733
- });
1734
- },
1735
- {
1736
- key: process.env.NODE_ENV !== "production" && "getVirtualIndexes",
1737
- debug: () => this.options.debug
1738
- }
1739
- );
1740
- this.indexFromElement = (node) => {
1741
- const attributeName = this.options.indexAttribute;
1742
- const indexStr = node.getAttribute(attributeName);
1743
- if (!indexStr) {
1744
- console.warn(
1745
- `Missing attribute name '${attributeName}={index}' on measured element.`
1746
- );
1747
- return -1;
1748
- }
1749
- return parseInt(indexStr, 10);
1750
- };
1751
- this._measureElement = (node, entry) => {
1752
- const index = this.indexFromElement(node);
1753
- const item = this.measurementsCache[index];
1754
- if (!item) {
1755
- return;
1756
- }
1757
- const key = item.key;
1758
- const prevNode = this.elementsCache.get(key);
1759
- if (prevNode !== node) {
1760
- if (prevNode) {
1761
- this.observer.unobserve(prevNode);
1762
- }
1763
- this.observer.observe(node);
1764
- this.elementsCache.set(key, node);
1765
- }
1766
- if (node.isConnected) {
1767
- this.resizeItem(index, this.options.measureElement(node, entry, this));
1768
- }
1769
- };
1770
- this.resizeItem = (index, size) => {
1771
- var _a;
1772
- const item = this.measurementsCache[index];
1773
- if (!item) {
1774
- return;
1775
- }
1776
- const itemSize = (_a = this.itemSizeCache.get(item.key)) != null ? _a : item.size;
1777
- const delta = size - itemSize;
1778
- if (delta !== 0) {
1779
- if (this.shouldAdjustScrollPositionOnItemSizeChange !== void 0 ? this.shouldAdjustScrollPositionOnItemSizeChange(item, delta, this) : item.start < this.getScrollOffset() + this.scrollAdjustments) {
1780
- if (process.env.NODE_ENV !== "production" && this.options.debug) {
1781
- console.info("correction", delta);
1782
- }
1783
- this._scrollToOffset(this.getScrollOffset(), {
1784
- adjustments: this.scrollAdjustments += delta,
1785
- behavior: void 0
1786
- });
1787
- }
1788
- this.pendingMeasuredCacheIndexes.push(item.index);
1789
- this.itemSizeCache = new Map(this.itemSizeCache.set(item.key, size));
1790
- this.notify(false);
1791
- }
1792
- };
1793
- this.measureElement = (node) => {
1794
- if (!node) {
1795
- this.elementsCache.forEach((cached, key) => {
1796
- if (!cached.isConnected) {
1797
- this.observer.unobserve(cached);
1798
- this.elementsCache.delete(key);
1799
- }
1800
- });
1801
- return;
1802
- }
1803
- this._measureElement(node, void 0);
1804
- };
1805
- this.getVirtualItems = memo(
1806
- () => [this.getVirtualIndexes(), this.getMeasurements()],
1807
- (indexes, measurements) => {
1808
- const virtualItems = [];
1809
- for (let k = 0, len = indexes.length; k < len; k++) {
1810
- const i = indexes[k];
1811
- const measurement = measurements[i];
1812
- virtualItems.push(measurement);
1813
- }
1814
- return virtualItems;
1815
- },
1816
- {
1817
- key: process.env.NODE_ENV !== "production" && "getVirtualItems",
1818
- debug: () => this.options.debug
1819
- }
1820
- );
1821
- this.getVirtualItemForOffset = (offset) => {
1822
- const measurements = this.getMeasurements();
1823
- if (measurements.length === 0) {
1824
- return void 0;
1825
- }
1826
- return notUndefined(
1827
- measurements[findNearestBinarySearch(
1828
- 0,
1829
- measurements.length - 1,
1830
- (index) => notUndefined(measurements[index]).start,
1831
- offset
1832
- )]
1833
- );
1834
- };
1835
- this.getOffsetForAlignment = (toOffset, align) => {
1836
- const size = this.getSize();
1837
- const scrollOffset = this.getScrollOffset();
1838
- if (align === "auto") {
1839
- if (toOffset >= scrollOffset + size) {
1840
- align = "end";
1841
- }
1842
- }
1843
- if (align === "end") {
1844
- toOffset -= size;
1845
- }
1846
- const scrollSizeProp = this.options.horizontal ? "scrollWidth" : "scrollHeight";
1847
- const scrollSize = this.scrollElement ? "document" in this.scrollElement ? this.scrollElement.document.documentElement[scrollSizeProp] : this.scrollElement[scrollSizeProp] : 0;
1848
- const maxOffset = scrollSize - size;
1849
- return Math.max(Math.min(maxOffset, toOffset), 0);
1850
- };
1851
- this.getOffsetForIndex = (index, align = "auto") => {
1852
- index = Math.max(0, Math.min(index, this.options.count - 1));
1853
- const item = this.measurementsCache[index];
1854
- if (!item) {
1855
- return void 0;
1856
- }
1857
- const size = this.getSize();
1858
- const scrollOffset = this.getScrollOffset();
1859
- if (align === "auto") {
1860
- if (item.end >= scrollOffset + size - this.options.scrollPaddingEnd) {
1861
- align = "end";
1862
- } else if (item.start <= scrollOffset + this.options.scrollPaddingStart) {
1863
- align = "start";
1864
- } else {
1865
- return [scrollOffset, align];
1866
- }
1867
- }
1868
- const centerOffset = item.start - this.options.scrollPaddingStart + (item.size - size) / 2;
1869
- switch (align) {
1870
- case "center":
1871
- return [this.getOffsetForAlignment(centerOffset, align), align];
1872
- case "end":
1873
- return [
1874
- this.getOffsetForAlignment(
1875
- item.end + this.options.scrollPaddingEnd,
1876
- align
1877
- ),
1878
- align
1879
- ];
1880
- default:
1881
- return [
1882
- this.getOffsetForAlignment(
1883
- item.start - this.options.scrollPaddingStart,
1884
- align
1885
- ),
1886
- align
1887
- ];
1888
- }
1889
- };
1890
- this.isDynamicMode = () => this.elementsCache.size > 0;
1891
- this.cancelScrollToIndex = () => {
1892
- if (this.scrollToIndexTimeoutId !== null && this.targetWindow) {
1893
- this.targetWindow.clearTimeout(this.scrollToIndexTimeoutId);
1894
- this.scrollToIndexTimeoutId = null;
1895
- }
1896
- };
1897
- this.scrollToOffset = (toOffset, { align = "start", behavior } = {}) => {
1898
- this.cancelScrollToIndex();
1899
- if (behavior === "smooth" && this.isDynamicMode()) {
1900
- console.warn(
1901
- "The `smooth` scroll behavior is not fully supported with dynamic size."
1902
- );
1903
- }
1904
- this._scrollToOffset(this.getOffsetForAlignment(toOffset, align), {
1905
- adjustments: void 0,
1906
- behavior
1907
- });
1908
- };
1909
- this.scrollToIndex = (index, { align: initialAlign = "auto", behavior } = {}) => {
1910
- index = Math.max(0, Math.min(index, this.options.count - 1));
1911
- this.cancelScrollToIndex();
1912
- if (behavior === "smooth" && this.isDynamicMode()) {
1913
- console.warn(
1914
- "The `smooth` scroll behavior is not fully supported with dynamic size."
1915
- );
1916
- }
1917
- const offsetAndAlign = this.getOffsetForIndex(index, initialAlign);
1918
- if (!offsetAndAlign) return;
1919
- const [offset, align] = offsetAndAlign;
1920
- this._scrollToOffset(offset, { adjustments: void 0, behavior });
1921
- if (behavior !== "smooth" && this.isDynamicMode() && this.targetWindow) {
1922
- this.scrollToIndexTimeoutId = this.targetWindow.setTimeout(() => {
1923
- this.scrollToIndexTimeoutId = null;
1924
- const elementInDOM = this.elementsCache.has(
1925
- this.options.getItemKey(index)
1926
- );
1927
- if (elementInDOM) {
1928
- const [latestOffset] = notUndefined(
1929
- this.getOffsetForIndex(index, align)
1930
- );
1931
- if (!approxEqual(latestOffset, this.getScrollOffset())) {
1932
- this.scrollToIndex(index, { align, behavior });
1933
- }
1934
- } else {
1935
- this.scrollToIndex(index, { align, behavior });
1936
- }
1937
- });
1938
- }
1939
- };
1940
- this.scrollBy = (delta, { behavior } = {}) => {
1941
- this.cancelScrollToIndex();
1942
- if (behavior === "smooth" && this.isDynamicMode()) {
1943
- console.warn(
1944
- "The `smooth` scroll behavior is not fully supported with dynamic size."
1945
- );
1946
- }
1947
- this._scrollToOffset(this.getScrollOffset() + delta, {
1948
- adjustments: void 0,
1949
- behavior
1950
- });
1951
- };
1952
- this.getTotalSize = () => {
1953
- var _a2;
1954
- var _a;
1955
- const measurements = this.getMeasurements();
1956
- let end;
1957
- if (measurements.length === 0) {
1958
- end = this.options.paddingStart;
1959
- } else {
1960
- end = this.options.lanes === 1 ? (_a2 = (_a = measurements[measurements.length - 1]) == null ? void 0 : _a.end) != null ? _a2 : 0 : Math.max(
1961
- ...measurements.slice(-this.options.lanes).map((m) => m.end)
1962
- );
1963
- }
1964
- return Math.max(
1965
- end - this.options.scrollMargin + this.options.paddingEnd,
1966
- 0
1967
- );
1968
- };
1969
- this._scrollToOffset = (offset, {
1970
- adjustments,
1971
- behavior
1972
- }) => {
1973
- this.options.scrollToFn(offset, { behavior, adjustments }, this);
1974
- };
1975
- this.measure = () => {
1976
- this.itemSizeCache = /* @__PURE__ */ new Map();
1977
- this.notify(false);
1978
- };
1979
- this.setOptions(opts);
1980
- }
1981
- }
1982
- const findNearestBinarySearch = (low, high, getCurrentValue, value) => {
1983
- while (low <= high) {
1984
- const middle = (low + high) / 2 | 0;
1985
- const currentValue = getCurrentValue(middle);
1986
- if (currentValue < value) {
1987
- low = middle + 1;
1988
- } else if (currentValue > value) {
1989
- high = middle - 1;
1990
- } else {
1991
- return middle;
1992
- }
1993
- }
1994
- if (low > 0) {
1995
- return low - 1;
1996
- } else {
1997
- return 0;
1998
- }
1999
- };
2000
- function calculateRange({
2001
- measurements,
2002
- outerSize,
2003
- scrollOffset
2004
- }) {
2005
- const count = measurements.length - 1;
2006
- const getOffset = (index) => measurements[index].start;
2007
- const startIndex = findNearestBinarySearch(0, count, getOffset, scrollOffset);
2008
- let endIndex = startIndex;
2009
- while (endIndex < count && measurements[endIndex].end < scrollOffset + outerSize) {
2010
- endIndex++;
2011
- }
2012
- return { startIndex, endIndex };
2013
- }
2014
- const useIsomorphicLayoutEffect = typeof document !== "undefined" ? React.useLayoutEffect : React.useEffect;
2015
- function useVirtualizerBase(options2) {
2016
- const rerender = React.useReducer(() => ({}), {})[1];
2017
- const resolvedOptions = {
2018
- ...options2,
2019
- onChange: (instance2, sync) => {
2020
- var _a;
2021
- if (sync) {
2022
- flushSync(rerender);
2023
- } else {
2024
- rerender();
2025
- }
2026
- (_a = options2.onChange) == null ? void 0 : _a.call(options2, instance2, sync);
2027
- }
2028
- };
2029
- const [instance] = React.useState(
2030
- () => new Virtualizer(resolvedOptions)
2031
- );
2032
- instance.setOptions(resolvedOptions);
2033
- useIsomorphicLayoutEffect(() => {
2034
- return instance._didMount();
2035
- }, []);
2036
- useIsomorphicLayoutEffect(() => {
2037
- return instance._willUpdate();
2038
- });
2039
- return instance;
2040
- }
2041
- function useVirtualizer(options2) {
2042
- return useVirtualizerBase({
2043
- observeElementRect,
2044
- observeElementOffset,
2045
- scrollToFn: elementScroll,
2046
- ...options2
2047
- });
2048
- }
2049
- const partnerResourcesKeys = {
2050
- all: ["partnerResources"],
2051
- byClientIds: (ids) => [...partnerResourcesKeys.all, "byClientIds", ...ids]
2052
- };
2053
- const getPartnerResourcesByClientIds = async (userIds, startDate, endDate) => {
2054
- if (!userIds.length || !startDate || !endDate) return [];
2055
- const formattedStartDate = formatDateToString(startDate);
2056
- const formattedEndDate = formatDateToString(endDate);
2057
- const queryParams = new URLSearchParams({
2058
- userIds: userIds.join(","),
2059
- startDate: formattedStartDate,
2060
- endDate: formattedEndDate
2061
- });
2062
- const response = await baseRequest(
2063
- `${PARTNER_USER_METRICS_PATH}/aggregate-spend-for-user-ids?${queryParams.toString()}`
2064
- );
2065
- return response.data;
2066
- };
2067
- const usePartnerResourcesByClientIds = (userIds = [], startDate, endDate) => {
2068
- return useQuery({
2069
- queryKey: partnerResourcesKeys.byClientIds(userIds),
2070
- queryFn: () => getPartnerResourcesByClientIds(userIds, startDate, endDate),
2071
- ...CACHE_STANDARD,
2072
- enabled: (userIds == null ? void 0 : userIds.length) > 0 && !!startDate && !!endDate
2073
- });
2074
- };
2075
- const SourceIcon = ({ source }) => {
2076
- switch (source.toLowerCase()) {
2077
- case "google":
2078
- return /* @__PURE__ */ jsx(GoogleIcon, { className: "h-4 w-4" });
2079
- case "meta":
2080
- return /* @__PURE__ */ jsx(MetaIcon, { className: "h-4 w-4" });
2081
- case "direct":
2082
- return /* @__PURE__ */ jsx(UserPlus, { className: "h-4 w-4" });
2083
- default:
2084
- return null;
2085
- }
2086
- };
2087
- const SourceCell = ({ sources }) => /* @__PURE__ */ jsx("div", { className: "space-y-1", children: sources.map((source) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2088
- /* @__PURE__ */ jsx(SourceIcon, { source }),
2089
- /* @__PURE__ */ jsx("span", { children: source[0].toUpperCase() + source.slice(1) })
2090
- ] }, source)) });
2091
- const SpendCell = ({ amount }) => /* @__PURE__ */ jsx("span", { className: "font-semibold text-gray-900", children: formatCurrency(Number(amount != null ? amount : 0)) });
2092
- const StatusCell = ({
2093
- statusMap
2094
- }) => {
2095
- var _a;
2096
- const { t } = useTranslation();
2097
- const paidAmount = (_a = statusMap["Paid"]) != null ? _a : 0;
2098
- const unpaidAmount = Object.entries(statusMap).reduce(
2099
- (acc, [status, amount]) => status !== "Paid" ? acc + amount : acc,
2100
- 0
2101
- );
2102
- const isPaid = paidAmount > unpaidAmount;
2103
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2104
- /* @__PURE__ */ jsx(
2105
- "span",
2106
- {
2107
- className: `px-2 py-1 rounded-full text-sm ${isPaid ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}`,
2108
- children: isPaid ? t("dashboard.client_acquisition.paid") : t("dashboard.client_acquisition.unpaid")
2109
- }
2110
- ),
2111
- /* @__PURE__ */ jsxs(Tooltip, { children: [
2112
- /* @__PURE__ */ jsx(TooltipTrigger, { children: /* @__PURE__ */ jsx(HelpCircle, { className: "h-4 w-4 text-gray-400 cursor-help" }) }),
2113
- /* @__PURE__ */ jsx(
2114
- TooltipContent,
2115
- {
2116
- side: "top",
2117
- className: "bg-white border border-gray-200 shadow-lg p-3 rounded-lg",
2118
- children: /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2119
- /* @__PURE__ */ jsxs("div", { className: "flex justify-between gap-4", children: [
2120
- /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-600", children: t("dashboard.client_acquisition.paid") }),
2121
- /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-emerald-600", children: formatCurrency(paidAmount) })
2122
- ] }),
2123
- /* @__PURE__ */ jsxs("div", { className: "flex justify-between gap-4", children: [
2124
- /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-600", children: t("dashboard.client_acquisition.unpaid") }),
2125
- /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-gray-900", children: formatCurrency(unpaidAmount) })
2126
- ] })
2127
- ] })
2128
- }
2129
- )
2130
- ] })
2131
- ] });
2132
- };
2133
- const tableCard = "reach-styles-module__tableCard___8xy0b";
2134
- const skeletonContainer = "reach-styles-module__skeletonContainer___8EjUC";
2135
- const platformBadge = "reach-styles-module__platformBadge___mth-X";
2136
- const rightAligned = "reach-styles-module__rightAligned___Ou-Ub";
2137
- const statusCell = "reach-styles-module__statusCell___32iSB";
2138
- const statusPaid = "reach-styles-module__statusPaid___z3i6-";
2139
- const statusUnpaid = "reach-styles-module__statusUnpaid___LSUt4";
2140
- const badgeContainer = "reach-styles-module__badgeContainer___lZ3yD";
2141
- const dateBadge = "reach-styles-module__dateBadge___otHgz";
2142
- const noDataContainer = "reach-styles-module__noDataContainer___-1C3o";
2143
- const styles$1 = {
2144
- tableCard,
2145
- skeletonContainer,
2146
- platformBadge,
2147
- rightAligned,
2148
- statusCell,
2149
- statusPaid,
2150
- statusUnpaid,
2151
- badgeContainer,
2152
- dateBadge,
2153
- noDataContainer
2154
- };
2155
- const ITEMS_PER_PAGE = 10;
2156
- const ROW_HEIGHT = 75;
2157
- const COLUMN_WIDTHS = {
2158
- client: "30%",
2159
- source: "25%",
2160
- totalSpent: "25%",
2161
- paymentStatus: "20%"
2162
- };
2163
- const ClientAcquisitionTable = ({ height }) => {
2164
- var _a, _b, _c, _d;
2165
- const { t } = useTranslation();
2166
- const [page, setPage] = useState(1);
2167
- const { state } = useDashboardFilterContext();
2168
- const parentRef = useRef(null);
2169
- const { data: metrics, isLoading: isLoadingMetrics } = useCombinedMetrics(
2170
- state.dateRange,
2171
- state.channel
2172
- );
2173
- const { data: resources, isLoading: isLoadingResources } = usePartnerResourcesByClientIds(
2174
- metrics == null ? void 0 : metrics.converted_users_ids,
2175
- (_a = state.dateRange) == null ? void 0 : _a.from,
2176
- (_b = state.dateRange) == null ? void 0 : _b.to
2177
- );
2178
- const isLoading = isLoadingMetrics || isLoadingResources;
2179
- const startIndex = (page - 1) * ITEMS_PER_PAGE;
2180
- const paginatedResources = resources == null ? void 0 : resources.slice(
2181
- startIndex,
2182
- startIndex + ITEMS_PER_PAGE
2183
- );
2184
- const totalPages = Math.ceil(((_c = resources == null ? void 0 : resources.length) != null ? _c : 0) / ITEMS_PER_PAGE);
2185
- const virtualizer = useVirtualizer({
2186
- count: (_d = paginatedResources == null ? void 0 : paginatedResources.length) != null ? _d : 0,
2187
- getScrollElement: () => parentRef.current,
2188
- estimateSize: () => ROW_HEIGHT,
2189
- overscan: 5
2190
- });
2191
- if (isLoading) {
2192
- return /* @__PURE__ */ jsxs(Card, { className: styles$1.tableCard, children: [
2193
- /* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { children: t("dashboard.client_acquisition.title") }) }),
2194
- /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx("div", { className: styles$1.skeletonContainer, children: /* @__PURE__ */ jsx(Skeleton, { className: "w-full h-full" }) }) })
2195
- ] });
2196
- }
2197
- return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(Card, { className: styles$1.tableCard, children: [
2198
- /* @__PURE__ */ jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [
2199
- /* @__PURE__ */ jsx(CardTitle, { children: t("dashboard.client_acquisition.title") }),
2200
- totalPages > 1 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2201
- /* @__PURE__ */ jsx(
2202
- "button",
2203
- {
2204
- onClick: () => setPage((p) => Math.max(1, p - 1)),
2205
- disabled: page === 1,
2206
- className: "p-1 hover:bg-gray-100 rounded disabled:opacity-50 disabled:hover:bg-transparent",
2207
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
2208
- }
2209
- ),
2210
- /* @__PURE__ */ jsxs("span", { className: "text-sm text-gray-600", children: [
2211
- page,
2212
- " / ",
2213
- totalPages
2214
- ] }),
2215
- /* @__PURE__ */ jsx(
2216
- "button",
2217
- {
2218
- onClick: () => setPage((p) => Math.min(totalPages, p + 1)),
2219
- disabled: page === totalPages,
2220
- className: "p-1 hover:bg-gray-100 rounded disabled:opacity-50 disabled:hover:bg-transparent",
2221
- children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
2222
- }
2223
- )
2224
- ] })
2225
- ] }),
2226
- /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
2227
- /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
2228
- /* @__PURE__ */ jsx(TableHead, { style: { width: COLUMN_WIDTHS.client }, children: t("dashboard.client_acquisition.client") }),
2229
- /* @__PURE__ */ jsx(TableHead, { style: { width: COLUMN_WIDTHS.source }, children: t("dashboard.client_acquisition.source") }),
2230
- /* @__PURE__ */ jsx(TableHead, { style: { width: COLUMN_WIDTHS.totalSpent }, children: t("dashboard.client_acquisition.total_spent") }),
2231
- /* @__PURE__ */ jsx(TableHead, { style: { width: COLUMN_WIDTHS.paymentStatus }, children: t("dashboard.client_acquisition.payment_status") })
2232
- ] }) }) }),
2233
- /* @__PURE__ */ jsx(
2234
- "div",
2235
- {
2236
- ref: parentRef,
2237
- style: { height: `${height - 120}px`, overflowY: "auto" },
2238
- children: /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsx(TableBody, { children: virtualizer.getVirtualItems().map((virtualRow) => {
2239
- const resource = paginatedResources == null ? void 0 : paginatedResources[virtualRow.index];
2240
- if (!resource) return null;
2241
- return /* @__PURE__ */ jsxs(
2242
- TableRow,
2243
- {
2244
- className: "hover:bg-gray-50/50 transition-colors",
2245
- children: [
2246
- /* @__PURE__ */ jsx(TableCell, { style: { width: COLUMN_WIDTHS.client }, children: resource.spend.userName }),
2247
- /* @__PURE__ */ jsx(TableCell, { style: { width: COLUMN_WIDTHS.source }, children: /* @__PURE__ */ jsx(
2248
- SourceCell,
2249
- {
2250
- sources: Object.keys(
2251
- resource.spend.adSourceToAmountMap
2252
- )
2253
- }
2254
- ) }),
2255
- /* @__PURE__ */ jsx(TableCell, { style: { width: COLUMN_WIDTHS.totalSpent }, children: /* @__PURE__ */ jsx(SpendCell, { amount: resource.spend.totalAmount }) }),
2256
- /* @__PURE__ */ jsx(
2257
- TableCell,
2258
- {
2259
- style: { width: COLUMN_WIDTHS.paymentStatus },
2260
- children: /* @__PURE__ */ jsx(
2261
- StatusCell,
2262
- {
2263
- statusMap: resource.spend.amountToStatusMap
2264
- }
2265
- )
2266
- }
2267
- )
2268
- ]
2269
- },
2270
- resource.userId
2271
- );
2272
- }) }) })
2273
- }
2274
- )
2275
- ] }) })
2276
- ] }) });
2277
- };
2278
- const ChannelPerformance = ({ dateRange }) => {
2279
- var _a, _b;
2280
- const { t } = useTranslation();
2281
- const { state } = useDashboardFilterContext();
2282
- const { data: googleData, isLoading: isGoogleLoading } = useAggregatedMetricsByBusiness$1(
2283
- state.dateRange
2284
- );
2285
- const { data: metaData, isLoading: isMetaLoading } = useAggregatedMetricsByBusiness(
2286
- state.dateRange
2287
- );
2288
- const isLoading = isGoogleLoading || isMetaLoading;
2289
- const channelData = [
2290
- googleData && {
2291
- channel: "Google Ads",
2292
- id: "google",
2293
- icon: /* @__PURE__ */ jsx(GoogleIcon, { className: "h-4 w-4" }),
2294
- spend: googleData == null ? void 0 : googleData.ads_spend,
2295
- revenue: googleData == null ? void 0 : googleData.measured_revenue,
2296
- roas: googleData == null ? void 0 : googleData.roas,
2297
- newCustomers: googleData == null ? void 0 : googleData.new_customers
2298
- },
2299
- metaData && {
2300
- channel: "Meta Ads",
2301
- id: "meta",
2302
- icon: /* @__PURE__ */ jsx(MetaIcon, { className: "h-4 w-4" }),
2303
- spend: metaData == null ? void 0 : metaData.ads_spend,
2304
- revenue: metaData == null ? void 0 : metaData.measured_revenue,
2305
- roas: metaData == null ? void 0 : metaData.roas,
2306
- newCustomers: metaData == null ? void 0 : metaData.new_customers
2307
- }
2308
- ].filter(Boolean);
2309
- const getFilteredChannelData = () => {
2310
- if (state.channel === "all") {
2311
- return channelData;
2312
- }
2313
- return channelData.filter((channel) => channel.id === state.channel);
2314
- };
2315
- return /* @__PURE__ */ jsxs(Card, { children: [
2316
- /* @__PURE__ */ jsxs(CardHeader, { children: [
2317
- /* @__PURE__ */ jsx(CardTitle, { children: t("dashboard.channel_performance.title") }),
2318
- dateRange && /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
2319
- (_a = dateRange.from) == null ? void 0 : _a.toLocaleDateString(),
2320
- " -",
2321
- " ",
2322
- (_b = dateRange.to) == null ? void 0 : _b.toLocaleDateString()
2323
- ] })
2324
- ] }),
2325
- /* @__PURE__ */ jsx(CardContent, { children: isLoading ? /* @__PURE__ */ jsx(Skeleton, { className: "w-full h-[400px]" }) : /* @__PURE__ */ jsxs(Table, { children: [
2326
- /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
2327
- /* @__PURE__ */ jsx(TableHead, { children: t("dashboard.channel_performance.channel") }),
2328
- /* @__PURE__ */ jsx(TableHead, { className: "text-right", children: t("dashboard.channel_performance.spend") }),
2329
- /* @__PURE__ */ jsx(TableHead, { className: "text-right", children: t("dashboard.channel_performance.revenue") }),
2330
- /* @__PURE__ */ jsx(TableHead, { className: "text-right", children: t("dashboard.channel_performance.roas") }),
2331
- /* @__PURE__ */ jsx(TableHead, { className: "text-right", children: t("dashboard.channel_performance.new_customers") })
2332
- ] }) }),
2333
- /* @__PURE__ */ jsx(TableBody, { children: getFilteredChannelData().map((row) => /* @__PURE__ */ jsxs(TableRow, { children: [
2334
- /* @__PURE__ */ jsxs(TableCell, { className: "font-medium flex gap-2 items-center", children: [
2335
- row.icon,
2336
- row.channel
2337
- ] }),
2338
- /* @__PURE__ */ jsx(TableCell, { className: "text-right", children: formatCurrency(Number(row.spend)) }),
2339
- /* @__PURE__ */ jsx(TableCell, { className: "text-right", children: formatCurrency(Number(row.revenue)) }),
2340
- /* @__PURE__ */ jsx(TableCell, { className: "text-right", children: `${Number(row.roas).toFixed(1)}x` }),
2341
- /* @__PURE__ */ jsx(TableCell, { className: "text-right", children: row.newCustomers || 0 })
2342
- ] }, row.channel)) })
2343
- ] }) })
2344
- ] });
2345
- };
2346
- const container = "reach-styles-module__container___MU7iK";
2347
- const header = "reach-styles-module__header___yz14d";
2348
- const actionBarContainer = "reach-styles-module__actionBarContainer___G5hpa";
2349
- const overviewContent = "reach-styles-module__overviewContent___UZVfK";
2350
- const metricsContainer = "reach-styles-module__metricsContainer___23m-t";
2351
- const metricsWrapper = "reach-styles-module__metricsWrapper___DKNuR";
2352
- const chartsGrid = "reach-styles-module__chartsGrid___ovwxR";
2353
- const channelContent = "reach-styles-module__channelContent___Vgz1F";
2354
- const reengagementContent = "reach-styles-module__reengagementContent___rEqt7";
2355
- const styles = {
2356
- container,
2357
- header,
2358
- actionBarContainer,
2359
- overviewContent,
2360
- metricsContainer,
2361
- metricsWrapper,
2362
- chartsGrid,
2363
- channelContent,
2364
- reengagementContent
2365
- };
2366
- const CHART_HEIGHT = 250;
2367
- const DashboardTabs = () => {
2368
- const { t } = useTranslation();
2369
- return /* @__PURE__ */ jsxs(Tabs, { defaultValue: "overview", className: styles.container, children: [
2370
- /* @__PURE__ */ jsxs("div", { className: styles.header, children: [
2371
- /* @__PURE__ */ jsxs(TabsList, { children: [
2372
- /* @__PURE__ */ jsx(TabsTrigger, { value: "overview", children: t("dashboard.tabs.overview") }),
2373
- /* @__PURE__ */ jsx(TabsTrigger, { value: "channel-performance", children: t("dashboard.tabs.channel_performance") })
2374
- ] }),
2375
- /* @__PURE__ */ jsx("div", { className: styles.actionBarContainer, children: /* @__PURE__ */ jsx(ActionBar, {}) })
2376
- ] }),
2377
- /* @__PURE__ */ jsx(TabsContent, { value: "overview", className: styles.overviewContent, children: /* @__PURE__ */ jsxs("div", { className: styles.metricsContainer, children: [
2378
- /* @__PURE__ */ jsx("div", { className: styles.metricsWrapper, children: /* @__PURE__ */ jsx(MetricsRow, {}) }),
2379
- /* @__PURE__ */ jsxs("div", { className: styles.chartsGrid, children: [
2380
- /* @__PURE__ */ jsx(ROASChart, { height: CHART_HEIGHT }),
2381
- /* @__PURE__ */ jsx(ClientAcquisitionTable, { height: CHART_HEIGHT })
2382
- ] })
2383
- ] }) }),
2384
- /* @__PURE__ */ jsx(
2385
- TabsContent,
2386
- {
2387
- value: "channel-performance",
2388
- className: styles.channelContent,
2389
- children: /* @__PURE__ */ jsx(ChannelPerformance, {})
2390
- }
2391
- )
2392
- ] });
2393
- };
2394
- const bannerVariants = cva("w-full border", {
2395
- variants: {
2396
- variant: {
2397
- default: "bg-slate-50 border-slate-200 text-slate-700",
2398
- primary: "bg-blue-50 border-blue-200 text-blue-700",
2399
- success: "bg-green-50 border-green-200 text-green-700",
2400
- warning: "bg-amber-50 border-amber-200 text-amber-700",
2401
- error: "bg-red-50 border-red-200 text-red-700"
2402
- },
2403
- size: {
2404
- default: "py-1",
2405
- sm: "py-0.5 text-xs",
2406
- lg: "py-2"
2407
- },
2408
- position: {
2409
- default: "my-4 rounded-lg",
2410
- top: "rounded-none border-t border-b fixed top-0 left-0 right-0 z-50",
2411
- bottom: "rounded-none border-t border-b fixed bottom-0 left-0 right-0 z-50",
2412
- inline: "my-2 rounded-lg"
2413
- }
2414
- },
2415
- defaultVariants: {
2416
- variant: "default",
2417
- size: "default",
2418
- position: "default"
2419
- }
2420
- });
2421
- const iconMap = {
2422
- default: /* @__PURE__ */ jsx(Info, { className: "h-4 w-4" }),
2423
- primary: /* @__PURE__ */ jsx(Info, { className: "h-4 w-4" }),
2424
- success: /* @__PURE__ */ jsx(CheckCircle, { className: "h-4 w-4" }),
2425
- warning: /* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4" }),
2426
- error: /* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4" })
2427
- };
2428
- const Banner = React.forwardRef(
2429
- ({
2430
- className,
2431
- variant = "default",
2432
- size = "default",
2433
- position = "default",
2434
- loading = false,
2435
- title: title2,
2436
- description: description2,
2437
- asChild = false,
2438
- children,
2439
- ...props
2440
- }, ref) => {
2441
- const Comp = asChild ? Slot : Alert;
2442
- return /* @__PURE__ */ jsx(
2443
- Comp,
2444
- {
2445
- ref,
2446
- className: cn(bannerVariants({ variant, size, position, className })),
2447
- ...props,
2448
- children: /* @__PURE__ */ jsxs("div", { className: "container mx-auto flex items-center py-1", children: [
2449
- loading ? /* @__PURE__ */ jsx(
2450
- Loader,
2451
- {
2452
- "data-testid": "loading-icon",
2453
- className: "h-4 w-4 animate-spin mr-3"
2454
- }
2455
- ) : /* @__PURE__ */ jsx("div", { className: "mr-3", children: iconMap[variant] }),
2456
- /* @__PURE__ */ jsxs("div", { children: [
2457
- title2 && /* @__PURE__ */ jsx(AlertTitle, { className: "text-sm font-medium", children: title2 }),
2458
- description2 && /* @__PURE__ */ jsx(AlertDescription, { className: "text-sm", children: description2 }),
2459
- children
2460
- ] })
2461
- ] })
2462
- }
2463
- );
2464
- }
2465
- );
2466
- Banner.displayName = "Banner";
2467
- const AdDashboard = ({ children }) => {
2468
- const navigate = useNavigate();
2469
- const { t } = useTranslation();
2470
- const { isLoading: isBusinessLoading, error: businessError } = useBusiness();
2471
- const { data: accounts, isLoading: isAccountsLoading } = useAdAccounts();
2472
- const { state } = useDashboardFilterContext();
2473
- const { data: combinedMetrics } = useCombinedMetrics(
2474
- state.dateRange,
2475
- state.channel
2476
- );
2477
- const isLoading = isBusinessLoading || isAccountsLoading;
2478
- if (isLoading) {
2479
- return /* @__PURE__ */ jsx("div", { className: "flex h-screen items-center justify-center", children: /* @__PURE__ */ jsx(SpinLoader, { text: ["Fetching your data", "Finishing up"] }) });
2480
- }
2481
- if (businessError) {
2482
- return /* @__PURE__ */ jsx("div", { children: "Error loading business data" });
2483
- }
2484
- const showNoAccounts = (accounts == null ? void 0 : accounts.length) === 0 || (accounts == null ? void 0 : accounts.every((account) => account.status === "draft"));
2485
- const showMetricsBanner = combinedMetrics && (!(combinedMetrics == null ? void 0 : combinedMetrics.ads_spend) || (combinedMetrics == null ? void 0 : combinedMetrics.ads_spend) == "0");
2486
- const showRevokedAccountsBanner = accounts == null ? void 0 : accounts.every(
2487
- (account) => account.status === "revoked"
2488
- );
2489
- if (children) {
2490
- console.log("rendering in component mode");
2491
- return /* @__PURE__ */ jsx(Provider, { children: /* @__PURE__ */ jsxs("div", { className: styles$9.container, children: [
2492
- /* @__PURE__ */ jsx(DashboardTabs, {}),
2493
- children
2494
- ] }) });
2495
- }
2496
- console.log("rendering in iframe default mode");
2497
- return /* @__PURE__ */ jsx(Provider, { children: /* @__PURE__ */ jsx("div", { className: styles$9.container, children: /* @__PURE__ */ jsxs("div", { className: styles$9.innerContainer, children: [
2498
- /* @__PURE__ */ jsxs("div", { className: styles$9.header, children: [
2499
- /* @__PURE__ */ jsx(H2, { children: t("dashboard.title") }),
2500
- !showNoAccounts && /* @__PURE__ */ jsx(SettingsSheet, {})
2501
- ] }),
2502
- showMetricsBanner && /* @__PURE__ */ jsx(
2503
- Banner,
2504
- {
2505
- variant: "default",
2506
- title: t("dashboard.banners.metrics_crunching.title"),
2507
- description: t("dashboard.banners.metrics_crunching.description")
2508
- }
2509
- ),
2510
- showRevokedAccountsBanner && /* @__PURE__ */ jsx(
2511
- Banner,
2512
- {
2513
- variant: "error",
2514
- title: t("dashboard.banners.revoked_accounts.title"),
2515
- description: t("dashboard.banners.revoked_accounts.description")
2516
- }
2517
- ),
2518
- /* @__PURE__ */ jsxs("div", { className: "relative", children: [
2519
- /* @__PURE__ */ jsx(DashboardTabs, {}),
2520
- showNoAccounts && /* @__PURE__ */ jsx(
2521
- EmptyStateLayout,
2522
- {
2523
- title: t("empty_state.analytics.title"),
2524
- description: t("empty_state.analytics.description"),
2525
- buttonText: t("empty_state.analytics.button_text"),
2526
- onButtonClick: () => navigate(PATHS.MEASURE_SETUP)
2527
- }
2528
- )
2529
- ] })
2530
- ] }) }) });
2531
- };
2532
- export {
2533
- AdDashboard as default
2534
- };
2535
- //# sourceMappingURL=AdDashboard-BEWUbw3u.js.map