@liiift-studio/sales-portal 1.2.1

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.
Files changed (52) hide show
  1. package/README.md +461 -0
  2. package/SETUP.md +230 -0
  3. package/api/getAnalytics.d.ts +38 -0
  4. package/api/getAnalytics.js +346 -0
  5. package/api/getBalanceTransactions.d.ts +29 -0
  6. package/api/getBalanceTransactions.js +125 -0
  7. package/api/getDesignerInfo.d.ts +37 -0
  8. package/api/getDesignerInfo.js +98 -0
  9. package/api/getDesigners.d.ts +28 -0
  10. package/api/getDesigners.js +63 -0
  11. package/api/getPreviousSales.d.ts +23 -0
  12. package/api/getPreviousSales.js +82 -0
  13. package/api/getSales.d.ts +29 -0
  14. package/api/getSales.js +50 -0
  15. package/api/getSalesRange.d.ts +23 -0
  16. package/api/getSalesRange.js +58 -0
  17. package/api/utils/authMiddleware.js +84 -0
  18. package/api/utils/dateUtils.js +69 -0
  19. package/api/utils/feeCalculator.js +148 -0
  20. package/api/utils/processors/invoiceProcessor.js +337 -0
  21. package/api/utils/processors/paymentProcessor.js +462 -0
  22. package/api/utils/salesDataProcessing.js +596 -0
  23. package/api/utils/salesDataProcessor.js +224 -0
  24. package/api/utils/stripeFetcher.js +248 -0
  25. package/components/DateRangeSalesTable.js +1072 -0
  26. package/components/DebugValues.js +48 -0
  27. package/components/LicenseTypeList.js +193 -0
  28. package/components/LoginForm.js +219 -0
  29. package/components/PeriodComparison.js +501 -0
  30. package/components/Sales.js +773 -0
  31. package/components/SalesChart.js +307 -0
  32. package/components/SalesPortalPage.js +147 -0
  33. package/components/SalesTable.js +677 -0
  34. package/components/SummaryCards.js +345 -0
  35. package/components/TopPerformers.js +331 -0
  36. package/components/TypefaceList.js +154 -0
  37. package/components/table-columns.js +70 -0
  38. package/components/table-row-cells.js +295 -0
  39. package/data/countryCode.json +318 -0
  40. package/hooks/useSalesDateQuery.d.ts +20 -0
  41. package/hooks/useSalesDateQuery.js +71 -0
  42. package/index.d.ts +172 -0
  43. package/index.js +33 -0
  44. package/package.json +87 -0
  45. package/styles/sales-portal.module.scss +383 -0
  46. package/styles/sales-portal.theme.d.ts +5 -0
  47. package/styles/sales-portal.theme.js +799 -0
  48. package/utils/currencyUtils.d.ts +20 -0
  49. package/utils/currencyUtils.js +79 -0
  50. package/utils/salesDataProcessing.d.ts +44 -0
  51. package/utils/salesDataProcessing.js +596 -0
  52. package/utils/useSalesDateQuery.js +71 -0
@@ -0,0 +1,501 @@
1
+ // Period comparison component for comparing sales data across different time periods
2
+ import React, { useState, useEffect } from 'react';
3
+ import {
4
+ Grid,
5
+ Typography,
6
+ Box,
7
+ ToggleButtonGroup,
8
+ ToggleButton,
9
+ FormControl,
10
+ InputLabel,
11
+ Select,
12
+ MenuItem,
13
+ CircularProgress,
14
+ Paper
15
+ } from '@mui/material';
16
+ import { ResponsiveChartContainer } from '@mui/x-charts/ResponsiveChartContainer';
17
+ import { BarPlot } from '@mui/x-charts/BarChart';
18
+ import { LinePlot } from '@mui/x-charts/LineChart';
19
+ import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis';
20
+ import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis';
21
+ import { ChartsTooltip } from '@mui/x-charts/ChartsTooltip';
22
+ import { ChartsLegend } from '@mui/x-charts/ChartsLegend';
23
+ import styles from '../styles/sales-portal.module.scss';
24
+
25
+ /**
26
+ * Process chart data from sales information
27
+ * @param {Array} sales - Sales data
28
+ * @param {Date} date - Date for the data
29
+ * @returns {Object} Processed chart data
30
+ */
31
+ const processChartData = (sales, date) => {
32
+ // Initialize arrays for each day of the month (1-31)
33
+ const daysInMonth = new Date(date.getUTCFullYear(), date.getUTCMonth() + 1, 0).getUTCDate();
34
+ const xAxis = Array.from({ length: daysInMonth }, (_, i) => i + 1);
35
+
36
+ // Initialize data arrays with zeros
37
+ const salesData = Array(daysInMonth).fill(0);
38
+ const orderData = Array(daysInMonth).fill(0);
39
+ const discountData = Array(daysInMonth).fill(0);
40
+ const refundData = Array(daysInMonth).fill(0);
41
+ const taxData = Array(daysInMonth).fill(0);
42
+ const shippingData = Array(daysInMonth).fill(0);
43
+
44
+ let refundTotal = 0;
45
+
46
+ // Process each sale
47
+ sales.forEach(sale => {
48
+ // Get the day of the month (1-31)
49
+ const saleDate = new Date(sale.date);
50
+ const day = saleDate.getUTCDate() - 1; // Adjust to 0-based index
51
+
52
+ // Skip if the day is out of range
53
+ if (day < 0 || day >= daysInMonth) return;
54
+
55
+ // Update cumulative data for the day and all subsequent days
56
+ for (let i = day; i < daysInMonth; i++) {
57
+ salesData[i] += sale.total || 0;
58
+ orderData[i] += 1;
59
+ discountData[i] += sale.amountDiscounted || 0;
60
+ taxData[i] += sale.taxAmount || 0;
61
+ shippingData[i] += sale.shippingCost || 0;
62
+ }
63
+
64
+ // Process refunds
65
+ if (sale.refunds && sale.refunds.length > 0) {
66
+ sale.refunds.forEach(refund => {
67
+ refundTotal += refund.adjustedTotal || 0;
68
+ // Get the day of the refund
69
+ const refundDate = new Date(refund.date);
70
+ const refundDay = refundDate.getUTCDate() - 1;
71
+
72
+ // Skip if the refund day is out of range
73
+ if (refundDay < 0 || refundDay >= daysInMonth) return;
74
+
75
+ // Update refund data for the day and all subsequent days
76
+ for (let i = refundDay; i < daysInMonth; i++) {
77
+ refundData[i] += refund.adjustedTotal || 0;
78
+ }
79
+ });
80
+ }
81
+ });
82
+
83
+ // Convert from cents to dollars for display
84
+ const salesDataInDollars = salesData.map(value => value / 100);
85
+ const discountDataInDollars = discountData.map(value => value / 100);
86
+ const refundDataInDollars = refundData.map(value => value / 100);
87
+ const taxDataInDollars = taxData.map(value => value / 100);
88
+ const shippingDataInDollars = shippingData.map(value => value / 100);
89
+
90
+ return {
91
+ xAxis,
92
+ salesData: salesDataInDollars,
93
+ salesMax: salesDataInDollars[daysInMonth - 1] || 0,
94
+ orderData,
95
+ orderMax: orderData[daysInMonth - 1] || 0,
96
+ discountData: discountDataInDollars,
97
+ discountMax: discountDataInDollars[daysInMonth - 1] || 0,
98
+ refundData: refundDataInDollars,
99
+ refundMax: refundDataInDollars[daysInMonth - 1] || 0,
100
+ refundTotal: refundTotal / 100,
101
+ taxData: taxDataInDollars,
102
+ shippingData: shippingDataInDollars
103
+ };
104
+ };
105
+
106
+ /**
107
+ * Format currency values in USD
108
+ * @param {number} value - Value to format as currency
109
+ * @returns {string} Formatted currency string
110
+ */
111
+ const formatCurrency = (value) => {
112
+ // Values are already in dollars, no need to divide by 100
113
+ return new Intl.NumberFormat('en-US', {
114
+ style: 'currency',
115
+ currency: 'USD',
116
+ minimumFractionDigits: 0,
117
+ maximumFractionDigits: 0,
118
+ }).format(value);
119
+ };
120
+
121
+ /**
122
+ * Period comparison component that allows comparing sales metrics across two time periods
123
+ * @param {Object} props - Component props
124
+ * @param {Array} props.currentSales - Current period sales data
125
+ * @param {Object} props.chartState - Current period chart data
126
+ * @param {Object} props.designer - Designer information
127
+ * @param {boolean} props.loading - Loading state
128
+ * @param {Date} props.date - Current date being displayed
129
+ * @param {Function} props.updateLoadingState - Function to update loading state
130
+ * @returns {JSX.Element} Period comparison component
131
+ */
132
+ export default function PeriodComparison({
133
+ currentSales = [],
134
+ chartState,
135
+ designer = {},
136
+ loading = false,
137
+ date,
138
+ updateLoadingState
139
+ }) {
140
+ // Comparison type: MoM (Month over Month) or YoY (Year over Year)
141
+ const [comparisonType, setComparisonType] = useState('MoM');
142
+
143
+ // Metric to compare: revenue, orders, avgOrderValue, etc.
144
+ const [metric, setMetric] = useState('revenue');
145
+
146
+ // Previous period data
147
+ const [previousSales, setPreviousSales] = useState([]);
148
+ const [previousChartState, setPreviousChartState] = useState(null);
149
+ const [loadingPrevious, setLoadingPrevious] = useState(false);
150
+
151
+ // Process chart data for the metric being compared
152
+ const [comparisonData, setComparisonData] = useState({
153
+ currentPeriodLabel: '',
154
+ previousPeriodLabel: '',
155
+ currentData: [],
156
+ previousData: [],
157
+ xAxisData: []
158
+ });
159
+
160
+ // Fetch previous period data when date, comparison type, or designer changes
161
+ useEffect(() => {
162
+ if (!date || !designer?.user || !designer?.password) {
163
+ return;
164
+ }
165
+
166
+ const fetchPreviousPeriodData = async () => {
167
+ setLoadingPrevious(true);
168
+
169
+ // Calculate the previous period date based on comparison type
170
+ let previousDate = new Date(date);
171
+ if (comparisonType === 'MoM') {
172
+ // Month over Month - go back 1 month
173
+ previousDate.setUTCMonth(previousDate.getUTCMonth() - 1);
174
+ } else {
175
+ // Year over Year - go back 1 year
176
+ previousDate.setUTCFullYear(previousDate.getUTCFullYear() - 1);
177
+ }
178
+
179
+ try {
180
+ const response = await fetch('/api/sales-portal/getSales', {
181
+ method: 'POST',
182
+ headers: { 'Content-Type': 'application/json' },
183
+ body: JSON.stringify({
184
+ user: designer?.user,
185
+ password: designer?.password,
186
+ date: previousDate,
187
+ admin: designer?.admin
188
+ }),
189
+ });
190
+
191
+ const data = await response.json();
192
+
193
+ if (data.success) {
194
+ setPreviousSales(data.data);
195
+
196
+ // Process previous period chart data
197
+ if (data.data && data.data.length > 0) {
198
+ // Process chart data with correct cents to dollars conversion
199
+ const processedChartData = processChartData(data.data, previousDate);
200
+ setPreviousChartState(processedChartData);
201
+ }
202
+ }
203
+ } catch (error) {
204
+ console.error('Error fetching previous period data:', error);
205
+ } finally {
206
+ setLoadingPrevious(false);
207
+ }
208
+ };
209
+
210
+ fetchPreviousPeriodData();
211
+ }, [date, comparisonType, designer?.user, designer?.password, designer?.admin]);
212
+
213
+ // Process comparison data based on selected metric when data changes
214
+ useEffect(() => {
215
+ if (!chartState || !date) {
216
+ return;
217
+ }
218
+
219
+ // Ensure we have valid chart state before processing
220
+ if (!chartState.xAxis || !chartState.salesData) {
221
+ return;
222
+ }
223
+
224
+ // Get current period label
225
+ const currentMonthName = new Date(date).toLocaleString("en-US", { timeZone: 'UTC', month: "long" });
226
+ const currentYear = new Date(date).getUTCFullYear();
227
+ const currentPeriodLabel = `${currentMonthName} ${currentYear}`;
228
+
229
+ // Get previous period label
230
+ let previousPeriodLabel = 'Previous Period';
231
+ if (previousChartState) {
232
+ let previousDate = new Date(date);
233
+ if (comparisonType === 'MoM') {
234
+ previousDate.setUTCMonth(previousDate.getUTCMonth() - 1);
235
+ } else {
236
+ previousDate.setUTCFullYear(previousDate.getUTCFullYear() - 1);
237
+ }
238
+
239
+ const previousMonthName = previousDate.toLocaleString("en-US", { timeZone: 'UTC', month: "long" });
240
+ const previousYear = previousDate.getUTCFullYear();
241
+ previousPeriodLabel = `${previousMonthName} ${previousYear}`;
242
+ }
243
+
244
+ // Generate x-axis data (days 1-31)
245
+ const daysInMonth = new Date(date.getUTCFullYear(), date.getUTCMonth() + 1, 0).getUTCDate();
246
+ const xAxisData = Array.from({ length: daysInMonth }, (_, i) => i + 1);
247
+
248
+ // Generate data arrays based on selected metric
249
+ let currentData = Array(daysInMonth).fill(0);
250
+ let previousData = Array(daysInMonth).fill(0);
251
+
252
+ // Check if we're comparing daily values or cumulative
253
+ const useCumulativeData = true; // Could be a toggle in the UI
254
+
255
+ switch (metric) {
256
+ case 'revenue':
257
+ if (useCumulativeData) {
258
+ currentData = chartState.salesData || Array(daysInMonth).fill(0);
259
+ previousData = previousChartState?.salesData || Array(daysInMonth).fill(0);
260
+ } else {
261
+ // For non-cumulative data, calculate daily values
262
+ currentData = (chartState.salesData || Array(daysInMonth).fill(0)).map((val, i, arr) =>
263
+ i === 0 ? val : val - arr[i-1]
264
+ );
265
+
266
+ // Process previous data safely
267
+ previousData = previousChartState?.salesData
268
+ ? previousChartState.salesData.map((val, i, arr) =>
269
+ i === 0 ? val : val - arr[i-1]
270
+ )
271
+ : Array(daysInMonth).fill(0);
272
+ }
273
+ break;
274
+
275
+ case 'orders':
276
+ if (useCumulativeData) {
277
+ currentData = chartState.orderData || Array(daysInMonth).fill(0);
278
+ previousData = previousChartState?.orderData || Array(daysInMonth).fill(0);
279
+ } else {
280
+ currentData = (chartState.orderData || Array(daysInMonth).fill(0)).map((val, i, arr) =>
281
+ i === 0 ? val : val - arr[i-1]
282
+ );
283
+
284
+ previousData = previousChartState?.orderData
285
+ ? previousChartState.orderData.map((val, i, arr) =>
286
+ i === 0 ? val : val - arr[i-1]
287
+ )
288
+ : Array(daysInMonth).fill(0);
289
+ }
290
+ break;
291
+
292
+ case 'discounts':
293
+ if (useCumulativeData) {
294
+ currentData = chartState.discountData || Array(daysInMonth).fill(0);
295
+ previousData = previousChartState?.discountData || Array(daysInMonth).fill(0);
296
+ } else {
297
+ currentData = (chartState.discountData || Array(daysInMonth).fill(0)).map((val, i, arr) =>
298
+ i === 0 ? val : val - arr[i-1]
299
+ );
300
+
301
+ previousData = previousChartState?.discountData
302
+ ? previousChartState.discountData.map((val, i, arr) =>
303
+ i === 0 ? val : val - arr[i-1]
304
+ )
305
+ : Array(daysInMonth).fill(0);
306
+ }
307
+ break;
308
+
309
+ case 'refunds':
310
+ if (useCumulativeData) {
311
+ currentData = chartState.refundData || Array(daysInMonth).fill(0);
312
+ previousData = previousChartState?.refundData || Array(daysInMonth).fill(0);
313
+ } else {
314
+ currentData = (chartState.refundData || Array(daysInMonth).fill(0)).map((val, i, arr) =>
315
+ i === 0 ? val : val - arr[i-1]
316
+ );
317
+
318
+ previousData = previousChartState?.refundData
319
+ ? previousChartState.refundData.map((val, i, arr) =>
320
+ i === 0 ? val : val - arr[i-1]
321
+ )
322
+ : Array(daysInMonth).fill(0);
323
+ }
324
+ break;
325
+
326
+ default:
327
+ currentData = [];
328
+ previousData = [];
329
+ }
330
+
331
+ // Calculate the current day (for limiting data display in current month)
332
+ const currentDate = new Date();
333
+ const isCurrentMonth = date.getUTCMonth() === currentDate.getUTCMonth() &&
334
+ date.getUTCFullYear() === currentDate.getUTCFullYear();
335
+ const currentDay = isCurrentMonth ? currentDate.getUTCDate() : daysInMonth;
336
+
337
+ // Limit the data to the current day if viewing the current month
338
+ if (isCurrentMonth) {
339
+ currentData = currentData.slice(0, currentDay);
340
+ previousData = previousData.slice(0, currentDay);
341
+ }
342
+
343
+ // Set the comparison data
344
+ setComparisonData({
345
+ currentPeriodLabel,
346
+ previousPeriodLabel,
347
+ currentData,
348
+ previousData,
349
+ xAxisData: xAxisData.slice(0, isCurrentMonth ? currentDay : daysInMonth)
350
+ });
351
+
352
+ }, [chartState, previousChartState, date, metric, comparisonType]);
353
+
354
+ // Handle comparison type change
355
+ const handleComparisonTypeChange = (event, newType) => {
356
+ if (newType) {
357
+ setComparisonType(newType);
358
+ }
359
+ };
360
+
361
+ // Handle metric change
362
+ const handleMetricChange = (event) => {
363
+ setMetric(event.target.value);
364
+ };
365
+
366
+ // Get y-axis label based on selected metric
367
+ const getYAxisLabel = () => {
368
+ switch (metric) {
369
+ case 'revenue':
370
+ return 'Revenue';
371
+ case 'orders':
372
+ return 'Order Count';
373
+ case 'discounts':
374
+ return 'Discount Amount';
375
+ case 'refunds':
376
+ return 'Refund Amount';
377
+ default:
378
+ return '';
379
+ }
380
+ };
381
+
382
+ // Skip rendering if no data
383
+ if (!chartState || currentSales.length === 0) {
384
+ return null;
385
+ }
386
+
387
+ return (
388
+ <Grid container className={styles.salesSection} sx={{ mt: 4 }}>
389
+ <Grid item xs={12} sx={{ mb: 2 }}>
390
+ <Paper elevation={0} sx={{ p: 2, borderRadius: '4px', border: '1px solid rgba(0, 0, 0, 0.12)' }}>
391
+ <Box sx={{
392
+ display: 'flex',
393
+ justifyContent: 'space-between',
394
+ alignItems: 'center',
395
+ flexWrap: 'wrap',
396
+ mb: 2
397
+ }}>
398
+ <Typography variant="h5" fontWeight="bold">
399
+ Period Comparison
400
+ {(loading || loadingPrevious) && <CircularProgress size={20} sx={{ ml: 2 }} />}
401
+ </Typography>
402
+
403
+ <Box sx={{
404
+ display: 'flex',
405
+ alignItems: 'center',
406
+ gap: 2
407
+ }}>
408
+ {/* Metric selection */}
409
+ <FormControl size="small" sx={{ minWidth: 150 }}>
410
+ <InputLabel id="metric-select-label">Metric</InputLabel>
411
+ <Select
412
+ labelId="metric-select-label"
413
+ id="metric-select"
414
+ value={metric}
415
+ label="Metric"
416
+ onChange={handleMetricChange}
417
+ >
418
+ <MenuItem value="revenue">Revenue</MenuItem>
419
+ <MenuItem value="orders">Orders</MenuItem>
420
+ <MenuItem value="discounts">Discounts</MenuItem>
421
+ <MenuItem value="refunds">Refunds</MenuItem>
422
+ </Select>
423
+ </FormControl>
424
+
425
+ {/* Comparison type toggle */}
426
+ <ToggleButtonGroup
427
+ value={comparisonType}
428
+ exclusive
429
+ onChange={handleComparisonTypeChange}
430
+ size="small"
431
+ >
432
+ <ToggleButton value="MoM">
433
+ Month over Month
434
+ </ToggleButton>
435
+ <ToggleButton value="YoY">
436
+ Year over Year
437
+ </ToggleButton>
438
+ </ToggleButtonGroup>
439
+ </Box>
440
+ </Box>
441
+
442
+ {/* Comparison chart */}
443
+ <ResponsiveChartContainer
444
+ xAxis={[{
445
+ data: comparisonData.xAxisData,
446
+ scaleType: 'band',
447
+ id: 'x-axis-id',
448
+ label: 'Day of Month',
449
+ }]}
450
+ yAxis={[{
451
+ scaleType: 'linear',
452
+ valueFormatter: (value) => {
453
+ if (metric === 'orders') {
454
+ return value;
455
+ }
456
+ return formatCurrency(value);
457
+ },
458
+ id: 'y-axis-id',
459
+ label: getYAxisLabel(),
460
+ }]}
461
+ series={[
462
+ {
463
+ type: 'line',
464
+ data: comparisonData.currentData,
465
+ label: comparisonData.currentPeriodLabel,
466
+ color: 'var(--blue, blue)',
467
+ valueFormatter: (value) => {
468
+ if (metric === 'orders') {
469
+ return value;
470
+ }
471
+ return formatCurrency(value);
472
+ }
473
+ },
474
+ {
475
+ type: 'line',
476
+ data: comparisonData.previousData,
477
+ label: comparisonData.previousPeriodLabel,
478
+ color: 'var(--grey800, grey)',
479
+ valueFormatter: (value) => {
480
+ if (metric === 'orders') {
481
+ return value;
482
+ }
483
+ return formatCurrency(value);
484
+ }
485
+ }
486
+ ]}
487
+ height={300}
488
+ margin={{ left: 70, right: 20, top: 50, bottom: 30 }}
489
+ >
490
+ <ChartsLegend position="top" />
491
+ <LinePlot />
492
+ <ChartsXAxis label="Day of Month" />
493
+ <ChartsYAxis label={getYAxisLabel()} />
494
+ <ChartsTooltip />
495
+ </ResponsiveChartContainer>
496
+
497
+ </Paper>
498
+ </Grid>
499
+ </Grid>
500
+ );
501
+ }