@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,773 @@
1
+ // Sales dashboard component displaying sales data, charts, and detailed transaction information for designers and administrators
2
+ import React, { useState, useEffect, useMemo, useCallback } from 'react';
3
+ import { Grid, Typography, Box, IconButton, Tooltip } from '@mui/material';
4
+ import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
5
+ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
6
+ import PrintIcon from '@mui/icons-material/Print';
7
+ import TrendingUpIcon from '@mui/icons-material/TrendingUp';
8
+ import TrendingDownIcon from '@mui/icons-material/TrendingDown';
9
+ import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
10
+ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
11
+ import { DatePicker } from '@mui/x-date-pickers/DatePicker';
12
+ import { LocalizationProvider } from '@mui/x-date-pickers';
13
+ import styles from '../styles/sales-portal.module.scss';
14
+ import dayjs from 'dayjs';
15
+ import utc from 'dayjs/plugin/utc';
16
+ import { processChartData, processTypefaceData, processDesignersData, generateSeriesData, processLicenseTypeData } from '../utils/salesDataProcessing';
17
+ import countryCodes from '../data/countryCode.json';
18
+ import { SalesTable } from './SalesTable';
19
+ import { DateRangeSalesTable } from './DateRangeSalesTable';
20
+ import SalesChart from './SalesChart';
21
+ import TopPerformers from './TopPerformers';
22
+ import TypefaceList from './TypefaceList';
23
+ import SummaryCards from './SummaryCards';
24
+ import LicenseTypeList from './LicenseTypeList';
25
+
26
+ dayjs.extend(utc);
27
+
28
+ /**
29
+ * Sales dashboard component
30
+ */
31
+ export default function Sales(props) {
32
+ const { month, year, designer, updateDate, categories = false, admin = false } = props;
33
+
34
+ // UI State
35
+ const [loadingStates, setLoadingStates] = useState({
36
+ designersList: false,
37
+ salesData: false,
38
+ previousSalesData: false,
39
+ salesProcessing: false,
40
+ designersProcessing: false,
41
+ chartProcessing: false,
42
+ csvDownload: false,
43
+ dateRangeSalesData: false
44
+ });
45
+ const [message, setMessage] = useState('');
46
+ const [previousSalesError, setPreviousSalesError] = useState('');
47
+ const [date, setDate] = useState(null);
48
+ const [displayLosses, setDisplayLosses] = useState(false);
49
+
50
+ // Helper to update individual loading states
51
+ const updateLoadingState = useCallback((key, value) => {
52
+ setLoadingStates(prev => ({
53
+ ...prev,
54
+ [key]: value
55
+ }));
56
+ }, []);
57
+
58
+ // Computed overall loading state
59
+ // but ignore dateRangeSalesData as it is not used in the loading overlay
60
+ const loading = useMemo(() =>
61
+ Object.values(loadingStates).some(value => value) && !loadingStates.dateRangeSalesData,
62
+ [loadingStates]
63
+ );
64
+
65
+ // Sales Data State
66
+ const [sales, setSales] = useState([]);
67
+ const [previousSales, setPreviousSales] = useState([]);
68
+ const [total, setTotal] = useState(0);
69
+ const [revenueChangePercent, setRevenueChangePercent] = useState(0);
70
+ const [typefaceData, setTypefaceData] = useState([]);
71
+ const [designersData, setDesignersData] = useState([]);
72
+ const [designers, setDesigners] = useState(null);
73
+ const [locationData, setLocationData] = useState({
74
+ topLocation: '',
75
+ topLocationRevenue: 0,
76
+ topLocationPercentage: 0,
77
+ previousTopLocation: '',
78
+ previousTopLocationRevenue: 0,
79
+ previousTopLocationPercentage: 0,
80
+ locationChange: null
81
+ });
82
+ const [licenseTypeData, setLicenseTypeData] = useState([]);
83
+
84
+ // Chart Data State
85
+ const [chartState, setChartState] = useState({
86
+ xAxis: [],
87
+ discountLostData: [],
88
+ salesData: [],
89
+ totalSalesData: [],
90
+ salesMax: 0,
91
+ orderData: [],
92
+ orderMax: 0,
93
+ regularOrderData: [],
94
+ regularOrderMax: 0,
95
+ discountData: [],
96
+ discountMax: 0,
97
+ discountFirstOrderData: [],
98
+ firstOrderDiscountMax: 0,
99
+ firstOrderData: [],
100
+ firstOrderMax: 0,
101
+ refundData: [],
102
+ refundMax: 0,
103
+ refundTotal: 0,
104
+ changeMax: 0,
105
+ taxData: [],
106
+ shippingData: [],
107
+ shippedOrdersData: [],
108
+ shippedOrdersMax: 0
109
+ });
110
+
111
+ const [seriesData, setSeriesData] = useState([]);
112
+
113
+ // Load designers list for admin
114
+ useEffect(() => {
115
+ if (!admin) return;
116
+
117
+ updateLoadingState('designersList', true);
118
+ fetch('/api/sales-portal/getDesigners', {
119
+ method: 'GET',
120
+ headers: { 'Content-Type': 'application/json' },
121
+ })
122
+ .then((res) => res.json())
123
+ .then((res) => {
124
+ if (res.success) setDesigners(res.data);
125
+ else setMessage('Error retrieving designers list');
126
+ updateLoadingState('designersList', false);
127
+ })
128
+ .catch((err) => {
129
+ setMessage('Error retrieving designers list');
130
+ console.error(err);
131
+ updateLoadingState('designersList', false);
132
+ });
133
+ }, [admin]);
134
+
135
+ // Set initial date
136
+ useEffect(() => {
137
+ if (year !== '' && month !== '') {
138
+ var date = new Date(Date.UTC(year, month, 1));
139
+ setDate(date);
140
+ }
141
+ }, [month, year]);
142
+
143
+ // Update date for admin
144
+ useEffect(() => {
145
+ if (designer?.admin && date && (new Date(date)?.getUTCMonth() !== month || new Date(date)?.getUTCFullYear() !== year)) {
146
+ updateDate(date);
147
+ }
148
+ }, [date, designer?.admin, month, year, updateDate]);
149
+
150
+ // Fetch current period sales data
151
+ async function fetchSales(){
152
+ if (!designer?.user || !designer?.password || !date) return;
153
+ updateLoadingState('salesData', true);
154
+ try {
155
+ const response = await fetch('/api/sales-portal/getSales', {
156
+ method: 'POST',
157
+ headers: { 'Content-Type': 'application/json' },
158
+ body: JSON.stringify({
159
+ user: designer?.user,
160
+ password: designer?.password,
161
+ date,
162
+ admin: designer?.admin
163
+ }),
164
+ });
165
+ const data = await response.json();
166
+
167
+ if (data.success) {
168
+ setSales(data.data);
169
+ } else {
170
+ setMessage('Error retrieving sales data');
171
+ }
172
+ } catch (err) {
173
+ setMessage('Error retrieving sales data');
174
+ console.error(err);
175
+ } finally {
176
+ updateLoadingState('salesData', false);
177
+ }
178
+ }
179
+
180
+ // Fetch previous period sales data for comparison (previous month)
181
+ async function fetchPreviousSales(){
182
+ if (!designer?.user || !designer?.password || !date) {
183
+ return;
184
+ }
185
+
186
+ updateLoadingState('previousSalesData', true);
187
+ setPreviousSalesError(''); // Clear any previous errors
188
+
189
+ // Calculate previous month date with more robust handling
190
+ const previousDate = new Date(date.getTime()); // Create a copy
191
+ const currentMonth = previousDate.getUTCMonth();
192
+ const currentYear = previousDate.getUTCFullYear();
193
+
194
+ // Handle edge cases for month boundaries
195
+ if (currentMonth === 0) {
196
+ // January -> December of previous year
197
+ previousDate.setUTCFullYear(currentYear - 1);
198
+ previousDate.setUTCMonth(11);
199
+ } else {
200
+ previousDate.setUTCMonth(currentMonth - 1);
201
+ }
202
+
203
+ try {
204
+ const response = await fetch('/api/sales-portal/getSales', {
205
+ method: 'POST',
206
+ headers: { 'Content-Type': 'application/json' },
207
+ body: JSON.stringify({
208
+ user: designer?.user,
209
+ password: designer?.password,
210
+ date: previousDate,
211
+ admin: designer?.admin
212
+ }),
213
+ });
214
+
215
+ if (!response.ok) {
216
+ throw new Error(`HTTP error! status: ${response.status}`);
217
+ }
218
+
219
+ const data = await response.json();
220
+
221
+ if (data.success) {
222
+ setPreviousSales(data.data || []);
223
+ } else {
224
+ const errorMsg = `Failed to load previous sales data: ${data.error || 'Unknown error'}`;
225
+ console.warn(errorMsg);
226
+ setPreviousSalesError(errorMsg);
227
+ setPreviousSales([]); // Clear any stale data
228
+ }
229
+ } catch (err) {
230
+ const errorMsg = `Error retrieving previous sales data: ${err.message}`;
231
+ console.error(errorMsg, err);
232
+ setPreviousSalesError(errorMsg);
233
+ setPreviousSales([]); // Clear any stale data
234
+ } finally {
235
+ updateLoadingState('previousSalesData', false);
236
+ }
237
+ }
238
+
239
+ useEffect(() => {
240
+ fetchSales();
241
+ fetchPreviousSales();
242
+ }, [designer?.user, designer?.password, designer?.admin, date]);
243
+
244
+ // Process sales data
245
+ useEffect(() => {
246
+ if (!sales || !date) return;
247
+
248
+ updateLoadingState('salesProcessing', true);
249
+ try {
250
+
251
+ // Calculate chart data
252
+ const processedData = processChartData(sales, date);
253
+ setChartState(processedData);
254
+
255
+ // Process typeface data
256
+ const processedTypefaceData = processTypefaceData(sales, designers);
257
+ setTypefaceData(processedTypefaceData);
258
+
259
+ // Process license type data
260
+ const processedLicenseTypeData = processLicenseTypeData(sales);
261
+ setLicenseTypeData(processedLicenseTypeData);
262
+
263
+ // Set total sales (sum of all sales minus tax and shipping)
264
+ const totalRevenue = (processedData.salesMax - processedData.taxData.at(-1) - processedData.shippingData.at(-1)) || 0;
265
+ setTotal(totalRevenue);
266
+
267
+ // Calculate revenue change percentage compared to previous period
268
+ let revChangePercent = 0;
269
+ let previousPeriodTotal = 0;
270
+
271
+ if (previousSales && previousSales.length > 0) {
272
+ previousPeriodTotal = previousSales.reduce((sum, sale) => {
273
+ const saleTotal = sale.total || 0;
274
+ const saleTax = sale.taxAmount || 0;
275
+ const saleShipping = sale.shippingCost || 0;
276
+ return sum + (saleTotal - saleTax - saleShipping) / 100; // Convert cents to dollars
277
+ }, 0);
278
+
279
+ if (previousPeriodTotal > 0) {
280
+ revChangePercent = ((totalRevenue / previousPeriodTotal) - 1) * 100;
281
+ setRevenueChangePercent(revChangePercent);
282
+ }
283
+ }
284
+
285
+ // Process location data
286
+ const locationStats = {
287
+ topLocation: 'Unknown',
288
+ topLocationRevenue: 0,
289
+ topLocationPercentage: 0,
290
+ previousTopLocation: '',
291
+ previousTopLocationRevenue: 0,
292
+ previousTopLocationPercentage: 0,
293
+ locationChange: null
294
+ };
295
+
296
+ // Create a map of country codes to full country names
297
+ const countryCodeMap = {};
298
+ countryCodes.forEach(country => {
299
+ countryCodeMap[country.code] = country.label;
300
+ });
301
+
302
+ // Helper function to get full country name from country code
303
+ const getCountryName = (countryCode) => {
304
+ if (!countryCode) return 'Unknown';
305
+ // If it's already a full name (more than 2 chars), return as is
306
+ if (countryCode.length > 2) return countryCode;
307
+ // Otherwise, look up the code in our map
308
+ return countryCodeMap[countryCode] || countryCode;
309
+ };
310
+
311
+ // Group sales by location
312
+ const locationSales = {};
313
+ sales.forEach(sale => {
314
+ // Get location using payment method origin for more consistent data
315
+ // Fall back to customer address if payment method origin not available
316
+ const countryCode = sale.paymentMethod?.origin?.country ||
317
+ sale.customerAddress?.country ||
318
+ sale.billingAddress?.country ||
319
+ 'Unknown';
320
+ // let location = getCountryName(countryCode);
321
+ let location = countryCode;
322
+
323
+ // Create location entry if doesn't exist
324
+ if (!locationSales[location]) {
325
+ locationSales[location] = {
326
+ totalRevenue: 0,
327
+ orders: 0
328
+ };
329
+ }
330
+
331
+ const saleRevenue = (sale.total - (sale.taxAmount || 0) - (sale.shippingCost || 0)) / 100;
332
+ locationSales[location].totalRevenue += saleRevenue;
333
+ locationSales[location].orders += 1;
334
+ });
335
+
336
+ // Find location with highest revenue
337
+ let maxRevenue = 0;
338
+ Object.entries(locationSales).forEach(([location, data]) => {
339
+ if (data.totalRevenue > maxRevenue) {
340
+ maxRevenue = data.totalRevenue;
341
+ locationStats.topLocation = location;
342
+ locationStats.topLocationRevenue = data.totalRevenue;
343
+ locationStats.topLocationPercentage = totalRevenue > 0 ? (data.totalRevenue / totalRevenue) * 100 : 0;
344
+ }
345
+ });
346
+
347
+ // Process previous period location data if available
348
+ if (previousSales && previousSales.length > 0) {
349
+ const prevLocationSales = {};
350
+ let prevTotalRevenue = 0;
351
+
352
+ previousSales.forEach(sale => {
353
+ // Get location using payment method origin for more consistent data
354
+ // Fall back to customer address if payment method origin not available
355
+ const countryCode = sale.paymentMethod?.origin?.country ||
356
+ sale.customerAddress?.country ||
357
+ sale.billingAddress?.country ||
358
+ 'Unknown';
359
+ let location = getCountryName(countryCode);
360
+
361
+ if (!prevLocationSales[location]) {
362
+ prevLocationSales[location] = {
363
+ totalRevenue: 0,
364
+ orders: 0
365
+ };
366
+ }
367
+
368
+ const saleRevenue = (sale.total - (sale.taxAmount || 0) - (sale.shippingCost || 0)) / 100;
369
+ prevLocationSales[location].totalRevenue += saleRevenue;
370
+ prevLocationSales[location].orders += 1;
371
+ prevTotalRevenue += saleRevenue;
372
+ });
373
+
374
+ // Find previous period location with highest revenue
375
+ let prevMaxRevenue = 0;
376
+ Object.entries(prevLocationSales).forEach(([location, data]) => {
377
+ if (data.totalRevenue > prevMaxRevenue) {
378
+ prevMaxRevenue = data.totalRevenue;
379
+ locationStats.previousTopLocation = location;
380
+ locationStats.previousTopLocationRevenue = data.totalRevenue;
381
+ locationStats.previousTopLocationPercentage = prevTotalRevenue > 0 ?
382
+ (data.totalRevenue / prevTotalRevenue) * 100 : 0;
383
+ }
384
+ });
385
+
386
+ // Calculate location change when top location is the same
387
+ if (locationStats.topLocation === locationStats.previousTopLocation && locationStats.previousTopLocationPercentage > 0) {
388
+ locationStats.locationChange = ((locationStats.topLocationPercentage / locationStats.previousTopLocationPercentage) - 1) * 100;
389
+ }
390
+ }
391
+
392
+ setLocationData(locationStats);
393
+ } catch (error) {
394
+ console.error('Error processing sales data:', error);
395
+ setMessage('Error processing sales data');
396
+ } finally {
397
+ updateLoadingState('salesProcessing', false);
398
+ }
399
+ }, [sales, designers, date, previousSales]);
400
+
401
+ // Process designers data
402
+ useEffect(() => {
403
+ if (!typefaceData) return;
404
+
405
+ updateLoadingState('designersProcessing', true);
406
+ try {
407
+ const processedDesignersData = processDesignersData(typefaceData);
408
+ setDesignersData(processedDesignersData);
409
+ } catch (error) {
410
+ console.error('Error processing designers data:', error);
411
+ setMessage('Error processing designers data');
412
+ } finally {
413
+ updateLoadingState('designersProcessing', false);
414
+ }
415
+ }, [typefaceData]);
416
+
417
+ // Update series data for charts
418
+ useEffect(() => {
419
+ if (!chartState.xAxis.length) return;
420
+
421
+ updateLoadingState('chartProcessing', true);
422
+ try {
423
+ const newSeriesData = generateSeriesData(chartState, displayLosses);
424
+ const filteredSeries = newSeriesData.filter(value => value);
425
+ // Ensure all series data arrays match xAxis length
426
+ const validSeries = filteredSeries.every(series => series.data.length === chartState.xAxis.length);
427
+ if (validSeries) {
428
+ setSeriesData(filteredSeries);
429
+ updateLoadingState('chartProcessing', false);
430
+ }
431
+ } catch (error) {
432
+ console.error('Error generating series data:', error);
433
+ setMessage('Error generating chart data');
434
+ }
435
+ }, [chartState, displayLosses]);
436
+
437
+ // Helper function to get trend icon based on percentage change
438
+ const getTrendIcon = (change) => {
439
+ if (change === null || change === undefined) return null;
440
+
441
+ if (Math.abs(change) < 1) return <TrendingFlatIcon sx={{ fontSize: '1em' }} />;
442
+
443
+ if (change > 0) {
444
+ return <TrendingUpIcon sx={{ fontSize: '1em', color: 'var(--green, green)' }} />;
445
+ } else {
446
+ return <TrendingDownIcon sx={{ fontSize: '1em', color: 'var(--red, red)' }} />;
447
+ }
448
+ };
449
+
450
+ // Memoize the header section to prevent unnecessary re-renders
451
+ const HeaderSection = useMemo(() => (
452
+ <Grid container className={`sales-header-section ${styles.salesSection}`}>
453
+ {/* Header Section */}
454
+ <Grid item xs={8} data-disabled={loading} data-loading={loading}
455
+ sx={{
456
+ ".show-hover": { display: "none" },
457
+ "&:hover .show-hover": { display: "inline", opacity: "0.5" },
458
+ }}
459
+ >
460
+ <Typography variant='h3' className={styles.pageTitle}
461
+ sx={{
462
+ borderRadius: "4px",
463
+ marginLeft: "-10px",
464
+ display: "inline-block",
465
+ }}
466
+ >
467
+ <Box
468
+ component={"span"}
469
+ sx={{
470
+ background: (sales?.reduce((acc, curr) => acc + curr.total, 0) / 100) > 0 ? 'rgba(var(--greenRGB, 0, 255, 0), 0.25)' : "",
471
+ padding: "5px 10px 0 10px",
472
+ }}
473
+ >
474
+ {sales ? <Box component="span" sx={{ fontVariationSettings: '"wght" 300' }}><Box component="span" sx={{ display: { xs: 'none', md: 'inherit' }, opacity: "0.5" }}>USD</Box>$ </Box> : ``}
475
+ {sales ? (
476
+ <>
477
+ <span className={`sales-total`} style={{ fontVariationSettings: '"wght" 900' }}>
478
+ {(total).toLocaleString('en-US', { minimumFractionDigits: 2 })}
479
+ </span>
480
+ </>
481
+ ) : `0.00`}
482
+ </Box>
483
+ {previousSales && previousSales.length > 0 && revenueChangePercent && (
484
+ <Tooltip
485
+ title={`Net Sales (after tax and shipping): ${previousSales && previousSales.length > 0 ? revenueChangePercent.toFixed(1) + '% vs prior month' : 'No prior month data'}`}
486
+ placement="top"
487
+ arrow
488
+ >
489
+ <Box
490
+ component="span"
491
+ sx={{
492
+ marginLeft: '8px',
493
+ cursor: 'help',
494
+ display: { xs: 'none', md: 'inherit' }
495
+ }}>
496
+ {getTrendIcon(revenueChangePercent)}
497
+ </Box>
498
+ </Tooltip>
499
+ )}
500
+ </Typography>
501
+ <Typography variant='h6'>
502
+ <strong>{designer?.firstName} {designer?.lastName}{!designer?.firstName && !designer?.lastName ? "Unknown" : ""}'s</strong> <span className="show-hover">[{designer?.user}]</span> pre-commission sales.
503
+ </Typography>
504
+ </Grid>
505
+
506
+ {/* Date Picker and View Controls */}
507
+ <Grid item xs={4}
508
+ sx={{
509
+ justifyContent: "end",
510
+ display: "flex",
511
+ }}
512
+ data-disabled={loading}
513
+ disabled={loading}
514
+ >
515
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
516
+ <IconButton
517
+ onClick={() => {
518
+ const newDate = new Date(date);
519
+ newDate.setUTCMonth(date.getUTCMonth() - 1);
520
+ setDate(newDate);
521
+ }}
522
+ size="small"
523
+ sx={{ display: { xs: 'none', md: 'inherit' } }}
524
+ >
525
+ <ChevronLeftIcon />
526
+ </IconButton>
527
+ <DatePicker
528
+ label='Month/Year (UTC)'
529
+ views={['month', 'year']}
530
+ format="MMM YYYY"
531
+ formatDensity="dense"
532
+ slotProps={{ textField: { variant: "filled" } }}
533
+ sx={{ "& *": { borderRadius: "4px" } }}
534
+ value={dayjs.utc(date)}
535
+ onChange={(newValue) => setDate(newValue.toDate())}
536
+ />
537
+ <IconButton
538
+ onClick={() => {
539
+ const newDate = new Date(date);
540
+ newDate.setUTCMonth(date.getUTCMonth() + 1);
541
+ setDate(newDate);
542
+ }}
543
+ size="small"
544
+ sx={{ display: { xs: 'none', md: 'inherit' } }}
545
+ >
546
+ <ChevronRightIcon />
547
+ </IconButton>
548
+ {!!sales.length && (
549
+ <Tooltip
550
+ title="Print to PDF"
551
+ sx={{ display: { xs: 'none', md: 'inherit' } }}
552
+ >
553
+ <IconButton
554
+ className='print-button'
555
+ onClick={() => {
556
+ // Add print-specific styles to the document
557
+ const style = document.createElement('style');
558
+ style.id = 'print-style';
559
+ // Find the current salesPortal containing the clicked print button
560
+ const printButton = document.querySelector('.print-button:hover');
561
+ const currentPortal = printButton.closest('.salesPortal');
562
+ const allPortals = Array.from(document.querySelectorAll('.salesPortal'));
563
+ const portalIndex = allPortals.indexOf(currentPortal) + 1;
564
+
565
+ style.textContent = `
566
+ @media print {
567
+ @page {
568
+ size: letter portrait;
569
+ margin: 0.5in;
570
+ }
571
+ header, #navBar, footer, #footer, #titleContainer, .exportSection,
572
+ .salesPortal:not(:nth-child(${portalIndex})) {
573
+ display: none!important;
574
+ }
575
+ .salesPortal, .salesPortalWrap{
576
+ background: white;
577
+ }
578
+ body {
579
+ -webkit-print-color-adjust: exact !important;
580
+ print-color-adjust: exact !important;
581
+ }
582
+ .MuiGrid-container{
583
+ min-height: 0;
584
+ }
585
+ .salesPortal {
586
+ padding: 0 !important;
587
+ }
588
+ .MuiGrid-root {
589
+ page-break-inside: avoid;
590
+ }
591
+ /* Hide non-printable elements */
592
+ button, .MuiIconButton-root, .DatePicker {
593
+ display: none !important;
594
+ }
595
+ /* Ensure charts and tables fit on page */
596
+ .salesSection {
597
+ width: 100% !important;
598
+ margin: 20px 0 !important;
599
+ }
600
+ /* Ensure text is readable */
601
+ .MuiTypography-root {
602
+ color: black !important;
603
+ }
604
+ }
605
+ `;
606
+ document.head.appendChild(style);
607
+
608
+ // Trigger print dialog
609
+ window.print();
610
+
611
+ // Remove print styles after printing
612
+ setTimeout(() => {
613
+ const printStyle = document.getElementById('print-style');
614
+ if (printStyle) {
615
+ printStyle.remove();
616
+ }
617
+ }, 1000);
618
+ }}
619
+ size="small"
620
+ >
621
+ <PrintIcon />
622
+ </IconButton>
623
+ </Tooltip>
624
+ )}
625
+ </Box>
626
+ </Grid>
627
+
628
+ {/* Error Messages */}
629
+ {!!(message !== '') &&
630
+ <Grid item xs={12} pb={8}>
631
+ <Typography variant='body2' sx={{ color: 'var(--red, red)', mt: 2 }}>{message}</Typography>
632
+ </Grid>
633
+ }
634
+ {!!(previousSalesError !== '') &&
635
+ <Grid item xs={12} pb={4}>
636
+ <Typography variant='body2' sx={{ color: 'var(--red, red)', mt: 1, opacity: 0.8 }}>
637
+ ⚠️ Previous sales data: {previousSalesError}
638
+ </Typography>
639
+ </Grid>
640
+ }
641
+ </Grid>
642
+ ), [loading, sales, total, designer, date, message, revenueChangePercent, previousSales]);
643
+
644
+ return (
645
+ <LocalizationProvider dateAdapter={AdapterDayjs}>
646
+ <Grid className={`salesPortal ${styles.salesPortal}`} item xs={12} p={0}
647
+ sx={{
648
+
649
+ pt: { xs: 8, md: 16 },
650
+ '&[data-loading="true"]': {
651
+ position: 'relative',
652
+ '&:after': {
653
+ content: '""',
654
+ position: 'absolute',
655
+ top: 0,
656
+ left: 0,
657
+ right: 0,
658
+ bottom: 0,
659
+ backgroundColor: 'rgba(255,255,255,0.7)',
660
+ zIndex: 1
661
+ }
662
+ }
663
+ }}
664
+ >
665
+ <Grid container>
666
+ {HeaderSection}
667
+
668
+ {/* Sales Data Display */}
669
+ {!!sales.length && (
670
+ <>
671
+
672
+ <Grid
673
+ item
674
+ xs={12}
675
+ data-disabled={loading}
676
+ className={`sales-data-section ${styles.salesSection}`}
677
+ sx={{
678
+ opacity: !!sales.length ? 1 : 0.25,
679
+ pointerEvents: !!sales.length ? "" : "none",
680
+ position: "relative",
681
+ }}
682
+ >
683
+
684
+ {/* Chart Section */}
685
+ <Box className="sales-chart-wrapper">
686
+ <SalesChart
687
+ sales={sales}
688
+ chartState={chartState}
689
+ seriesData={seriesData}
690
+ displayLosses={displayLosses}
691
+ setDisplayLosses={setDisplayLosses}
692
+ date={date}
693
+ loading={loading}
694
+ />
695
+ </Box>
696
+
697
+ {/* Summary Dashboard */}
698
+ <Box className="summary-cards-wrapper">
699
+ <SummaryCards
700
+ sales={sales}
701
+ previousSales={previousSales}
702
+ loading={loading}
703
+ chartState={chartState}
704
+ date={date}
705
+ locationData={locationData}
706
+ />
707
+ </Box>
708
+
709
+ {/* Top Performers Section */}
710
+ {!!(categories && typefaceData && typefaceData.length) && (
711
+ <Box className="top-performers-wrapper">
712
+ <TopPerformers
713
+ typefaceData={typefaceData}
714
+ designersData={designersData}
715
+ total={total}
716
+ chartState={chartState}
717
+ sales={sales}
718
+ loading={loading}
719
+ admin={admin}
720
+ />
721
+ </Box>
722
+ )}
723
+
724
+ {/* Typeface List */}
725
+ <Box className="typeface-list-wrapper">
726
+ <TypefaceList
727
+ typefaceData={typefaceData}
728
+ sales={sales}
729
+ loading={loading}
730
+ admin={admin}
731
+ />
732
+ </Box>
733
+
734
+ {/* License Type List */}
735
+ <Box className="license-type-list-wrapper">
736
+ <LicenseTypeList
737
+ licenseTypeData={licenseTypeData}
738
+ sales={sales}
739
+ loading={loading}
740
+ admin={admin}
741
+ />
742
+ </Box>
743
+
744
+ {/* Sales Table Section */}
745
+ {designer && (
746
+ <Box className="sales-table-wrapper">
747
+ <SalesTable
748
+ sales={sales}
749
+ designer={designer}
750
+ admin={admin}
751
+ loading={loading}
752
+ date={date}
753
+ updateLoadingState={updateLoadingState}
754
+ />
755
+ </Box>
756
+ )}
757
+ </Grid>
758
+
759
+ {/* Date Range Sales Table Section */}
760
+ <DateRangeSalesTable
761
+ designer={designer}
762
+ admin={admin}
763
+ loading={loadingStates.dateRangeSalesData}
764
+ updateLoadingState={updateLoadingState}
765
+ />
766
+ </>
767
+ )}
768
+
769
+ </Grid>
770
+ </Grid>
771
+ </LocalizationProvider>
772
+ );
773
+ }