@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,1072 @@
1
+ // Sales table component for displaying sales data within a selected date range
2
+ import React, { useState, useEffect, useMemo } from 'react';
3
+ import { useRouter } from 'next/router';
4
+ var slugify = require('slugify');
5
+ import {
6
+ Grid,
7
+ Button,
8
+ TableRow,
9
+ TableHead,
10
+ TableContainer,
11
+ TableCell,
12
+ TableBody,
13
+ Table,
14
+ Typography,
15
+ Menu,
16
+ MenuItem,
17
+ Checkbox,
18
+ ListItemText,
19
+ IconButton,
20
+ Tooltip,
21
+ Box
22
+ } from '@mui/material';
23
+ import { DataGrid } from '@mui/x-data-grid';
24
+ import DownloadIcon from '@mui/icons-material/Download';
25
+ import TuneIcon from '@mui/icons-material/Tune';
26
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
27
+ import WarningIcon from '@mui/icons-material/Warning';
28
+ import InfoIcon from '@mui/icons-material/Info';
29
+ import CircularProgress from '@mui/material/CircularProgress';
30
+ import { DatePicker } from '@mui/x-date-pickers/DatePicker';
31
+ import dayjs from 'dayjs';
32
+ import utc from 'dayjs/plugin/utc';
33
+ dayjs.extend(utc);
34
+ import styles from '../styles/sales-portal.module.scss';
35
+ import { TableRowCells, getCellValue } from './table-row-cells';
36
+ import { COLUMNS } from './table-columns';
37
+ import SummaryCards from './SummaryCards';
38
+ import TopPerformers from './TopPerformers';
39
+ import TypefaceList from './TypefaceList';
40
+ import LicenseTypeList from './LicenseTypeList';
41
+ import { processChartData, processTypefaceData, processDesignersData, processLicenseTypeData } from '../utils/salesDataProcessing';
42
+
43
+ /**
44
+ * Date range sales table component for displaying detailed sales information between two dates
45
+ * @param {Object} props - Component props
46
+ * @param {Array} props.sales - Sales data to display
47
+ * @param {Object} props.designer - Designer information
48
+ * @param {boolean} props.admin - Whether user is admin
49
+ * @param {boolean} props.loading - Loading state
50
+ * @param {Function} props.updateLoadingState - Function to update loading states
51
+ */
52
+ export function DateRangeSalesTable({ designer, admin, loading, updateLoadingState }) {
53
+ // Early return if no designer data is available - MOVED TO TOP before any hooks
54
+ if (!designer?.user || !designer?.password) {
55
+ return (
56
+ <Grid container sx={{ mt: 16 }}>
57
+ <Grid item xs={12}>
58
+ <Typography variant="h6" sx={{ color: 'var(--red, red)' }}>
59
+ Error: Designer credentials not available
60
+ </Typography>
61
+ </Grid>
62
+ </Grid>
63
+ );
64
+ }
65
+
66
+ const [anchorEl, setAnchorEl] = useState(null);
67
+ const [selectedColumns, setSelectedColumns] = useState([
68
+ 'orderNumber',
69
+ 'total',
70
+ 'amountDiscounted',
71
+ 'taxAmount',
72
+ 'preTaxTotal',
73
+ 'date',
74
+ 'description',
75
+ 'typeface',
76
+ 'refund',
77
+ 'testSales',
78
+ ]);
79
+
80
+ // Add sortModel state to manage sorting
81
+ const [sortModel, setSortModel] = useState([
82
+ {
83
+ field: 'date',
84
+ sort: 'desc'
85
+ }
86
+ ]);
87
+
88
+ // Filter model state for implementing filtering
89
+ const [filterModel, setFilterModel] = useState({
90
+ items: [],
91
+ quickFilterValues: []
92
+ });
93
+
94
+ // Initialize dates
95
+ const [startDate, setStartDate] = useState(null);
96
+ const [endDate, setEndDate] = useState(null);
97
+ const [dateRangeSales, setDateRangeSales] = useState([]);
98
+ const [error, setError] = useState('');
99
+
100
+ // Reconciliation state
101
+ const [reconciliationData, setReconciliationData] = useState({
102
+ isLoading: false,
103
+ isReconciled: false,
104
+ totalBalanceChange: 0,
105
+ grossSales: 0,
106
+ difference: 0,
107
+ error: null
108
+ });
109
+
110
+ // Dashboard states for summary data
111
+ const [chartState, setChartState] = useState({
112
+ xAxis: [],
113
+ discountLostData: [],
114
+ salesData: [],
115
+ totalSalesData: [],
116
+ salesMax: 0,
117
+ orderData: [],
118
+ orderMax: 0,
119
+ regularOrderData: [],
120
+ regularOrderMax: 0,
121
+ discountData: [],
122
+ discountMax: 0,
123
+ discountFirstOrderData: [],
124
+ firstOrderDiscountMax: 0,
125
+ firstOrderData: [],
126
+ firstOrderMax: 0,
127
+ refundData: [],
128
+ refundMax: 0,
129
+ refundTotal: 0,
130
+ changeMax: 0,
131
+ taxData: [],
132
+ shippingData: [],
133
+ shippedOrdersData: [],
134
+ shippedOrdersMax: 0
135
+ });
136
+ const [typefaceData, setTypefaceData] = useState([]);
137
+ const [designersData, setDesignersData] = useState([]);
138
+ const [licenseTypeData, setLicenseTypeData] = useState([]);
139
+ const [total, setTotal] = useState(0);
140
+ const [locationData, setLocationData] = useState({
141
+ topLocation: '',
142
+ topLocationRevenue: 0,
143
+ topLocationPercentage: 0,
144
+ previousTopLocation: '',
145
+ previousTopLocationRevenue: 0,
146
+ previousTopLocationPercentage: 0,
147
+ locationChange: null
148
+ });
149
+
150
+ const handleMenuClick = (event) => {
151
+ setAnchorEl(event.currentTarget);
152
+ };
153
+
154
+ const handleMenuClose = () => {
155
+ setAnchorEl(null);
156
+ };
157
+
158
+ const handleColumnToggle = (columnId) => {
159
+ setSelectedColumns(prev => {
160
+ if (prev.includes(columnId)) {
161
+ return prev.filter(id => id !== columnId);
162
+ } else {
163
+ return [...prev, columnId];
164
+ }
165
+ });
166
+ };
167
+
168
+ const handleSelectAll = () => {
169
+ setSelectedColumns(COLUMNS.filter(col => !col.adminOnly || admin).map(col => col.id));
170
+ };
171
+
172
+ const handleSelectNone = () => {
173
+ setSelectedColumns([]);
174
+ };
175
+
176
+ // Memoize the columns configuration
177
+ const memoizedColumns = useMemo(() =>
178
+ COLUMNS
179
+ .filter(column => selectedColumns.includes(column.id) && (!column.adminOnly || admin))
180
+ .map(column => ({
181
+ field: column.id,
182
+ headerName: column.label || column.headerName,
183
+ flex: 1,
184
+ minWidth: 120,
185
+ sortable: true,
186
+ sortComparator: (v1, v2, param1, param2) => {
187
+ // Extract the actual values from complex cell data
188
+ const getValue = (params) => {
189
+ if (!params.row) return '';
190
+ const result = getCellValue({
191
+ column,
192
+ sale: params.row.sale || null,
193
+ designer: params.row.designer || designer,
194
+ admin: params.row.admin || admin,
195
+ refund: params.row.refund || null,
196
+ isNoData: !params.row.sale,
197
+ color: params.row.color || ''
198
+ });
199
+
200
+ // Handle React elements (like links)
201
+ if (typeof result.value === 'object' && React.isValidElement(result.value)) {
202
+ return result.value.props.children || '';
203
+ }
204
+
205
+ return result.value;
206
+ };
207
+
208
+ // Get the actual values to compare
209
+ const val1 = getValue({ row: param1.api.getRow(param1.id) });
210
+ const val2 = getValue({ row: param2.api.getRow(param2.id) });
211
+
212
+ // For currency values, extract the number for comparison
213
+ if (column.id === 'total' || column.id === 'amountDiscounted' ||
214
+ column.id === 'preTaxTotal' || column.id === 'taxAmount' ||
215
+ column.id === 'stripeFees') {
216
+ // Extract numbers from currency strings like "$1,234.56"
217
+ const extractNumber = (str) => {
218
+ if (typeof str !== 'string') return str;
219
+ const match = str.replace(/[^0-9.-]+/g, '');
220
+ return match ? parseFloat(match) : 0;
221
+ };
222
+
223
+ return extractNumber(val1) - extractNumber(val2);
224
+ }
225
+
226
+ // For dates
227
+ if (column.id === 'date') {
228
+ const date1 = new Date(val1);
229
+ const date2 = new Date(val2);
230
+ return date1 - date2;
231
+ }
232
+
233
+ // Default string comparison
234
+ if (typeof val1 === 'string' && typeof val2 === 'string') {
235
+ return val1.localeCompare(val2);
236
+ }
237
+
238
+ // Fallback to default comparison
239
+ return v1 < v2 ? -1 : v1 > v2 ? 1 : 0;
240
+ },
241
+ renderCell: (params) => {
242
+ if (!params.row) return '';
243
+
244
+ const result = getCellValue({
245
+ column,
246
+ sale: params.row.sale || null,
247
+ designer: params.row.designer || designer,
248
+ admin: params.row.admin || admin,
249
+ refund: params.row.refund || null,
250
+ isNoData: !params.row.sale,
251
+ color: params.row.color || ''
252
+ });
253
+
254
+ return <div style={{ color: params.row.color || undefined }}>{result.value}</div>;
255
+ }
256
+ }))
257
+ , [selectedColumns, admin, designer]);
258
+
259
+ // Calculate gross sales from the sales data
260
+ function calculateGrossSales() {
261
+ let grossSales = 0;
262
+ dateRangeSales.forEach(sale => {
263
+ sale?.refunds?.map((refund, i) => {
264
+ grossSales -= (refund.adjustedTotal || 0);
265
+ })
266
+ grossSales += sale?.shippingProvision ? sale?.total : sale?.totalWithTax;
267
+ });
268
+ return grossSales;
269
+ }
270
+
271
+ // Handle start date change with UTC consistency
272
+ const handleStartDateChange = (newValue) => {
273
+ if (!newValue) {
274
+ setStartDate(null);
275
+ return;
276
+ }
277
+
278
+ // Set to start of day in UTC
279
+ const utcDate = dayjs.utc(newValue).startOf('day');
280
+ setStartDate(utcDate.toDate());
281
+
282
+ // If end date exists and is before new start date, clear it
283
+ if (endDate && utcDate.toDate() > endDate) {
284
+ setEndDate(null);
285
+ }
286
+ };
287
+
288
+ // Fetch sales data for date range
289
+ useEffect(() => {
290
+ if (!startDate || !endDate) return;
291
+
292
+ const fetchDateRangeSales = async () => {
293
+ updateLoadingState('dateRangeSalesData', true);
294
+ setError('');
295
+
296
+ try {
297
+ const response = await fetch('/api/sales-portal/getSales', {
298
+ method: 'POST',
299
+ headers: { 'Content-Type': 'application/json' },
300
+ body: JSON.stringify({
301
+ user: designer?.user,
302
+ password: designer?.password,
303
+ date: startDate,
304
+ dateRange: {
305
+ start: startDate.getTime(),
306
+ end: endDate.getTime()
307
+ },
308
+ admin: designer?.admin
309
+ }),
310
+ });
311
+ const data = await response.json();
312
+
313
+ if (data.success) {
314
+ setDateRangeSales(data.data);
315
+ } else {
316
+ setError('Error retrieving sales data');
317
+ setDateRangeSales([]);
318
+ }
319
+ } catch (err) {
320
+ console.error('Error fetching date range sales:', err);
321
+ setError('Error retrieving sales data');
322
+ setDateRangeSales([]);
323
+ } finally {
324
+ updateLoadingState('dateRangeSalesData', false);
325
+ }
326
+ };
327
+
328
+ fetchDateRangeSales();
329
+ }, [startDate, endDate, designer?.user, designer?.password, designer?.admin, updateLoadingState]);
330
+
331
+ // Process sales data for dashboard
332
+ useEffect(() => {
333
+ if (!dateRangeSales || !dateRangeSales.length) return;
334
+
335
+ try {
336
+ // For chart state, we'll create a mock date object since processChartData expects one
337
+ // but will adapt to use all data from the date range
338
+ const mockDate = startDate ? new Date(startDate) : new Date();
339
+
340
+ // Process chart data
341
+ const processedData = processChartData(dateRangeSales, mockDate);
342
+ setChartState(processedData);
343
+
344
+ // Process typeface data
345
+ const processedTypefaceData = processTypefaceData(dateRangeSales);
346
+ setTypefaceData(processedTypefaceData);
347
+
348
+ // Process license type data
349
+ const processedLicenseTypeData = processLicenseTypeData(dateRangeSales);
350
+ setLicenseTypeData(processedLicenseTypeData);
351
+
352
+ // Set total sales (sum of all sales minus tax and shipping)
353
+ const totalRevenue = (processedData.salesMax - processedData.taxData.at(-1) - processedData.shippingData.at(-1)) || 0;
354
+ setTotal(totalRevenue);
355
+
356
+ // Process designers data
357
+ const processedDesignersData = processDesignersData(processedTypefaceData);
358
+ setDesignersData(processedDesignersData);
359
+
360
+ // Process location data
361
+ const countries = {};
362
+ let topLocation = 'Unknown';
363
+ let topLocationRevenue = 0;
364
+ let topLocationPercentage = 0;
365
+
366
+ dateRangeSales.forEach(sale => {
367
+ // Get location from various possible sources
368
+ const countryCode = sale.paymentMethod?.origin?.country ||
369
+ sale.customerAddress?.country ||
370
+ sale.billingAddress?.country ||
371
+ 'Unknown';
372
+
373
+ if (!countries[countryCode]) {
374
+ countries[countryCode] = { revenue: 0, count: 0 };
375
+ }
376
+
377
+ // Calculate sale revenue (excluding tax and shipping)
378
+ const saleRevenue = (sale.total - (sale.taxAmount || 0) - (sale.shippingCost || 0)) / 100;
379
+ countries[countryCode].revenue += saleRevenue;
380
+ countries[countryCode].count++;
381
+ });
382
+
383
+ // Find top location
384
+ Object.entries(countries).forEach(([code, data]) => {
385
+ if (data.revenue > topLocationRevenue) {
386
+ topLocation = code;
387
+ topLocationRevenue = data.revenue;
388
+ }
389
+ });
390
+
391
+ // Calculate percentage of total
392
+ if (totalRevenue > 0 && topLocationRevenue > 0) {
393
+ topLocationPercentage = (topLocationRevenue / totalRevenue) * 100;
394
+ }
395
+
396
+ // Set location data (without comparison to previous since this is a custom date range)
397
+ setLocationData({
398
+ topLocation,
399
+ topLocationRevenue,
400
+ topLocationPercentage,
401
+ previousTopLocation: '',
402
+ previousTopLocationRevenue: 0,
403
+ previousTopLocationPercentage: 0,
404
+ locationChange: null
405
+ });
406
+ } catch (error) {
407
+ console.error('Error processing date range sales data:', error);
408
+ setError('Error processing sales data');
409
+ }
410
+ }, [dateRangeSales, startDate]);
411
+
412
+ // Fetch balance transactions from Stripe for the date range
413
+ useEffect(() => {
414
+ if (!designer?.user || !designer?.password || !startDate || !endDate || !admin || !dateRangeSales.length) {
415
+ console.log('Skipping balance transactions fetch due to missing data:', {
416
+ hasDesignerUser: !!designer?.user,
417
+ hasDesignerPassword: !!designer?.password,
418
+ hasStartDate: !!startDate,
419
+ hasEndDate: !!endDate,
420
+ isAdmin: !!admin,
421
+ salesCount: dateRangeSales.length
422
+ });
423
+ return;
424
+ }
425
+
426
+ console.log('Fetching balance transactions for date range:', {
427
+ start: new Date(startDate).toISOString(),
428
+ end: new Date(endDate).toISOString()
429
+ });
430
+
431
+ const fetchBalanceTransactions = async () => {
432
+ setReconciliationData(prev => ({ ...prev, isLoading: true, error: null }));
433
+
434
+ try {
435
+ const response = await fetch('/api/sales-portal/getBalanceTransactions', {
436
+ method: 'POST',
437
+ headers: { 'Content-Type': 'application/json' },
438
+ body: JSON.stringify({
439
+ user: designer?.user,
440
+ password: designer?.password,
441
+ dateRange: {
442
+ start: startDate.getTime(),
443
+ end: endDate.getTime()
444
+ },
445
+ admin: designer?.admin
446
+ }),
447
+ });
448
+
449
+ const data = await response.json();
450
+
451
+ if (data.success) {
452
+ const totalBalanceChange = data.data.totalBalanceChange;
453
+ const grossSales = calculateGrossSales();
454
+
455
+ setReconciliationData(prev => ({
456
+ ...prev,
457
+ totalBalanceChange,
458
+ grossSales,
459
+ difference: totalBalanceChange - grossSales,
460
+ isReconciled: Math.abs(totalBalanceChange - grossSales) < 1, // Allow for rounding differences
461
+ isLoading: false
462
+ }));
463
+ } else {
464
+ setReconciliationData(prev => ({
465
+ ...prev,
466
+ error: data.message || 'Failed to fetch balance transactions',
467
+ isLoading: false
468
+ }));
469
+ }
470
+ } catch (error) {
471
+ console.error('Error fetching balance transactions:', error);
472
+ setReconciliationData(prev => ({
473
+ ...prev,
474
+ error: error.message || 'Failed to fetch balance transactions',
475
+ isLoading: false
476
+ }));
477
+ }
478
+ };
479
+
480
+ fetchBalanceTransactions();
481
+ }, [designer?.user, designer?.password, designer?.admin, startDate, endDate, admin, dateRangeSales]);
482
+
483
+ /**
484
+ * Downloads sales data as CSV file
485
+ * @param {string} separator - CSV separator character
486
+ * @param {Object} designer - Designer information
487
+ * @param {Array} selectedCols - Selected columns to include in CSV
488
+ */
489
+ const downloadSalesData = (separator = ',', designer, selectedCols = selectedColumns) => {
490
+ updateLoadingState('csvDownload', true);
491
+
492
+ try {
493
+ // Create CSV header row using selected columns
494
+ const visibleColumns = COLUMNS
495
+ .filter(col => selectedCols.includes(col.id) && (!col.adminOnly || admin));
496
+
497
+ const headerRow = visibleColumns.map(col => `"${col.label || col.headerName}"`);
498
+ const csv = [headerRow.join(separator)];
499
+
500
+ // Add data rows
501
+ dateRangeSales.forEach(sale => {
502
+ // Add original sale row
503
+ const rowData = [];
504
+ visibleColumns.forEach(column => {
505
+ const cellData = getCellValue({
506
+ column,
507
+ sale,
508
+ designer,
509
+ admin,
510
+ refund: null,
511
+ isNoData: false
512
+ });
513
+
514
+ // Clean and format cell data
515
+ let data = (cellData.value?.toString() || '')
516
+ .replace(/(\r\n|\n|\r)/gm, '')
517
+ .replace(/(\s\s)/gm, ' ')
518
+ .replace(/"/g, '""');
519
+
520
+ rowData.push(`"${data}"`);
521
+ });
522
+ csv.push(rowData.join(separator));
523
+
524
+ // Add refund rows if any
525
+ if (sale?.refunds?.length > 0) {
526
+ sale.refunds.forEach(refund => {
527
+ const refundRowData = [];
528
+ visibleColumns.forEach(column => {
529
+ const cellData = getCellValue({
530
+ column,
531
+ sale,
532
+ designer,
533
+ admin,
534
+ refund,
535
+ isNoData: false
536
+ });
537
+
538
+ // Clean and format cell data
539
+ let data = (cellData.value?.toString() || '')
540
+ .replace(/(\r\n|\n|\r)/gm, '')
541
+ .replace(/(\s\s)/gm, ' ')
542
+ .replace(/"/g, '""');
543
+
544
+ refundRowData.push(`"${data}"`);
545
+ });
546
+ csv.push(refundRowData.join(separator));
547
+ });
548
+ }
549
+ });
550
+
551
+ var csv_string = csv.join('\n');
552
+
553
+ // Download it
554
+ var filename = `${slugify(designer.user, { lower: true, remove: /[*/+~.()'"!:@]/g, strict: true })}-${slugify(startDate.toUTCString(), { lower: true, remove: /[*/+~.()'"!:@]/g, strict: true })}-to-${slugify(endDate.toUTCString(), { lower: true, remove: /[*/+~.()'"!:@]/g, strict: true })}.csv`;
555
+ var link = document.createElement('a');
556
+ link.style.display = 'none';
557
+ link.setAttribute('target', '_blank');
558
+ link.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv_string));
559
+ link.setAttribute('download', filename);
560
+ document.body.appendChild(link);
561
+ link.click();
562
+ document.body.removeChild(link);
563
+ } catch (error) {
564
+ console.error('Error downloading sales data:', error);
565
+ } finally {
566
+ updateLoadingState('csvDownload', false);
567
+ }
568
+ };
569
+
570
+ return (
571
+ <Grid
572
+ container
573
+ data-disabled={loading}
574
+ data-loading={loading}
575
+ className={`date-range-sales-table-wrapper exportSection ${styles.exportSection} ${styles.salesSection}`}
576
+ sx={{
577
+ mt: 32,
578
+ pt: 16,
579
+ pb: 8,
580
+ mb: '0!important',
581
+ background: "rgba(0,0,0,.85)",
582
+ "& > *": {filter: "invert(1)"}
583
+ }}
584
+ >
585
+ <Grid id='titleContainer' item xs={12} sm={6} md={8} >
586
+ <Typography variant='h3'>
587
+ Export {designer?.firstName ? `${designer.firstName}'s`: designer?.name ? `${designer.name}'s` : ''} Sales
588
+ </Typography>
589
+ </Grid>
590
+
591
+ {/* Date Range Pickers */}
592
+ <Grid item xs={12} sm={6} md={4} sx={{ display: 'flex', gap: 2, mb: 8, justifyContent: 'flex-end', alignItems: 'center' }}>
593
+ {!startDate && !endDate && (
594
+ <>
595
+ <Button
596
+ variant="outlined"
597
+ onClick={() => {
598
+ // Set to January 1st 00:00:00 UTC of two years ago
599
+ const year = dayjs().subtract(2, 'year').year();
600
+ const startOfYear = dayjs.utc(`${year}-01-01`).startOf('day');
601
+ const endOfYear = dayjs.utc(`${year}-12-31`).endOf('day');
602
+ setStartDate(startOfYear.toDate());
603
+ setEndDate(endOfYear.toDate());
604
+ }}
605
+ sx={{
606
+ height: 'fit-content',
607
+ whiteSpace: 'nowrap',
608
+ mr: 2,
609
+ borderWidth: '0!important',
610
+ color: 'var(--black, black)',
611
+ py: 4,
612
+ opacity: 0.5,
613
+ '&:hover': {
614
+ opacity: 1
615
+ }
616
+ }}
617
+ >
618
+ {dayjs().subtract(2, 'year').year()}
619
+ </Button>
620
+ <Button
621
+ variant="outlined"
622
+ onClick={() => {
623
+ // Set to January 1st 00:00:00 UTC of last year
624
+ const year = dayjs().subtract(1, 'year').year();
625
+ const startOfYear = dayjs.utc(`${year}-01-01`).startOf('day');
626
+ const endOfYear = dayjs.utc(`${year}-12-31`).endOf('day');
627
+ setStartDate(startOfYear.toDate());
628
+ setEndDate(endOfYear.toDate());
629
+ }}
630
+ sx={{
631
+ height: 'fit-content',
632
+ whiteSpace: 'nowrap',
633
+ mr: 2,
634
+ borderWidth: '0!important',
635
+ color: 'var(--black, black)',
636
+ py: 4,
637
+ opacity: 0.5,
638
+ '&:hover': {
639
+ opacity: 1
640
+ }
641
+ }}
642
+ >
643
+ {dayjs().subtract(1, 'year').year()}
644
+ </Button>
645
+ </>
646
+ )}
647
+
648
+ <DatePicker
649
+ label="Start (UTC)"
650
+ value={startDate ? dayjs.utc(startDate) : null}
651
+ onChange={handleStartDateChange}
652
+ views={['year', 'month', 'day']}
653
+ format="DD-MM-YYYY"
654
+ slotProps={{ textField: { variant: "filled" } }}
655
+ sx={{ "& *": { borderRadius: "4px" } }}
656
+ />
657
+
658
+ <DatePicker
659
+ label="End (UTC)"
660
+ value={endDate ? dayjs.utc(endDate) : null}
661
+ onChange={(newValue) => {
662
+ if (!newValue) {
663
+ setEndDate(null);
664
+ return;
665
+ }
666
+ // Set to end of day in UTC
667
+ const utcDate = dayjs.utc(newValue).endOf('day');
668
+ setEndDate(utcDate.toDate());
669
+ }}
670
+ minDate={startDate ? dayjs.utc(startDate) : null}
671
+ disabled={!startDate}
672
+ views={['year', 'month', 'day']}
673
+ format="DD-MM-YYYY"
674
+ slotProps={{ textField: { variant: "filled" } }}
675
+ sx={{
676
+ transition: "all 0.5s",
677
+ maxWidth: startDate ? "initial" : "0",
678
+ opacity: startDate ? "" : "0",
679
+ "& *": {
680
+ borderRadius: "4px",
681
+ color: startDate ? "" : "transparent",
682
+ }
683
+ }}
684
+ />
685
+
686
+ {(startDate && endDate) && (
687
+ <Tooltip title="Clear date range">
688
+ <IconButton
689
+ size="small"
690
+ onClick={() => {
691
+ setStartDate(null);
692
+ setEndDate(null);
693
+ }}
694
+ sx={{
695
+ color: 'var(--black, black)',
696
+ opacity: 0.5,
697
+ '&:hover': {
698
+ opacity: 1
699
+ }
700
+ }}
701
+ >
702
+
703
+ </IconButton>
704
+ </Tooltip>
705
+ )}
706
+ </Grid>
707
+
708
+ {/* Error Message */}
709
+ {error && (
710
+ <Grid item xs={12}>
711
+ <Typography variant="body2" sx={{ color: 'var(--red, red)', mb: 2 }}>{error}</Typography>
712
+ </Grid>
713
+ )}
714
+
715
+ {/* Reconciliation Check - Only show for admin users */}
716
+ {admin && dateRangeSales.length > 0 && (
717
+ <Grid item xs={12} sx={{ mb: 2 }}>
718
+ <Box
719
+ sx={{
720
+ display: 'inline-block',
721
+ gap: 2,
722
+ p: 2,
723
+ ml: -2,
724
+ borderRadius: '4px',
725
+ border: reconciliationData.isReconciled
726
+ ? '2px solid var(--green, green)'
727
+ : '2px solid var(--red, red)',
728
+ color: reconciliationData.isReconciled
729
+ ? 'var(--green, green)'
730
+ : 'var(--red, red)',
731
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
732
+ filter: 'invert(0) !important',
733
+ '& *': { filter: 'invert(0) !important' },
734
+ }}
735
+ >
736
+
737
+ {reconciliationData.error ? (
738
+ <Typography variant="body1" sx={{ color: 'var(--red, red)' }}>
739
+ <WarningIcon fontSize="small" sx={{ mr: 1 }} />
740
+ Error: {reconciliationData.error}
741
+ </Typography>
742
+ ) : reconciliationData.isLoading ? (
743
+ <Typography variant="body1"><CircularProgress size={24} sx={{ mr: 1 }} /> Checking reconciliation...</Typography>
744
+ ) : reconciliationData.isReconciled ? (
745
+ <Typography variant="body1" sx={{ fontWeight: 'bold'}}>
746
+ <CheckCircleIcon /> Reconciled
747
+ </Typography>
748
+ ) : (
749
+ <Box sx={{ display: 'flex', alignItems: 'center' }}>
750
+ <Tooltip title="The difference between Stripe balance change and gross sales">
751
+ <Typography variant="body1" sx={{ fontWeight: 'bold', display: 'flex', alignItems: 'center' }}>
752
+ <WarningIcon/> Needs Reconciliation (Difference: ${(Math.abs(reconciliationData.difference) / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })})
753
+ </Typography>
754
+ </Tooltip>
755
+ <Tooltip
756
+ title={
757
+ <React.Fragment>
758
+ <Typography variant="body2" sx={{ fontWeight: 'bold' }}>Possible causes:</Typography>
759
+ <Typography variant="body2">• This could be due to a sale being refunded outside your selected date range. Check for the same difference in other periods.</Typography>
760
+ </React.Fragment>
761
+ }
762
+ placement="right"
763
+ arrow
764
+ >
765
+ <IconButton size="small" sx={{ ml: 1, color: 'inherit' }}>
766
+ <InfoIcon fontSize="small" />
767
+ </IconButton>
768
+ </Tooltip>
769
+ </Box>
770
+ )}
771
+
772
+ {!reconciliationData.isLoading && (
773
+ <>
774
+ <Typography variant="body1">
775
+ <strong>Stripe Balance Change:</strong> ${(reconciliationData.totalBalanceChange / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}
776
+ </Typography>
777
+ <Typography variant="body1">
778
+ <strong>Gross Sales:</strong> ${(reconciliationData.grossSales / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}
779
+ </Typography>
780
+ </>
781
+ )}
782
+ </Box>
783
+ </Grid>
784
+ )}
785
+
786
+ {/* Summary Dashboard and Analysis Components - only show when data is loaded */}
787
+ {!!dateRangeSales.length && (
788
+ <>
789
+ {/* Summary Cards */}
790
+ <SummaryCards
791
+ sales={dateRangeSales}
792
+ previousSales={[]} // No previous data in date range view
793
+ loading={loading}
794
+ chartState={chartState}
795
+ date={startDate}
796
+ locationData={locationData}
797
+ />
798
+
799
+ {/* Top Performers */}
800
+ {typefaceData?.length > 0 && (
801
+ <TopPerformers
802
+ typefaceData={typefaceData}
803
+ designersData={designersData}
804
+ total={total}
805
+ chartState={chartState}
806
+ sales={dateRangeSales}
807
+ loading={loading}
808
+ admin={admin}
809
+ />
810
+ )}
811
+
812
+ {/* Typeface List */}
813
+ {typefaceData?.length > 0 && (
814
+ <TypefaceList
815
+ typefaceData={typefaceData}
816
+ sales={dateRangeSales}
817
+ loading={loading}
818
+ admin={admin}
819
+ />
820
+ )}
821
+
822
+ {/* License Type List */}
823
+ {licenseTypeData?.length > 0 && (
824
+ <LicenseTypeList
825
+ licenseTypeData={licenseTypeData}
826
+ sales={dateRangeSales}
827
+ loading={loading}
828
+ admin={admin}
829
+ />
830
+ )}
831
+ </>
832
+ )}
833
+
834
+ {/* Sales Table */}
835
+ {!!dateRangeSales.length && (
836
+ <Grid container sx={{pb: 24}}>
837
+ <Grid item xs={12}
838
+ textAlign={"right"}
839
+ sx={{
840
+ opacity: !!dateRangeSales.length ? 1 : 0.25,
841
+ pointerEvents: !!dateRangeSales.length ? "" : "none",
842
+ display: "flex",
843
+ justifyContent: "flex-end",
844
+ alignItems: "flex-end",
845
+ }}
846
+ >
847
+ <Box
848
+ className='buttonStyle'
849
+ sx={{
850
+ position: "relative",
851
+ borderRadius: "4px 4px 0px 0px ",
852
+ pointerEvents: "none",
853
+ backgroundColor: 'primary.main',
854
+ color: 'primary.contrastText',
855
+ display: 'inline-flex',
856
+ alignItems: 'center',
857
+ justifyContent: 'center',
858
+ padding: '6px 16px',
859
+ fontWeight: 500,
860
+ }}
861
+ >
862
+ <Grid container alignItems="center" spacing={1}>
863
+ <Grid item>
864
+ <Tooltip title="Select columns">
865
+ <IconButton
866
+ size="small"
867
+ onClick={handleMenuClick}
868
+ sx={{
869
+ color: 'var(--white, white)',
870
+ pointerEvents: "auto",
871
+ '&:hover': {
872
+ opacity: 0.8
873
+ }
874
+ }}
875
+ >
876
+ <TuneIcon fontSize="small" />
877
+ </IconButton>
878
+ </Tooltip>
879
+ </Grid>
880
+ <Grid item>
881
+ <Tooltip title="Download CSV">
882
+ <IconButton
883
+ size="small"
884
+ onClick={() => downloadSalesData(',', designer, selectedColumns)}
885
+ sx={{
886
+ color: 'var(--white, white)',
887
+ pointerEvents: "auto",
888
+ '&:hover': {
889
+ opacity: 0.8
890
+ }
891
+ }}
892
+ >
893
+ <DownloadIcon fontSize="small" />
894
+ </IconButton>
895
+ </Tooltip>
896
+ </Grid>
897
+ <Grid item ml={1}>
898
+ <strong style={{ color: 'var(--white, white)' }}>CSV</strong>
899
+ </Grid>
900
+ </Grid>
901
+
902
+ <Menu
903
+ anchorEl={anchorEl}
904
+ open={Boolean(anchorEl)}
905
+ onClose={handleMenuClose}
906
+ dense={true}
907
+ PaperProps={{
908
+ sx: {
909
+ maxHeight: 300,
910
+ width: 250,
911
+ borderRadius: '4px',
912
+ }
913
+ }}
914
+ >
915
+ <Grid container>
916
+ <Grid item xs={6}>
917
+ <Button
918
+ onClick={handleSelectNone}
919
+ size='small'
920
+ variant="contained"
921
+ elevation={0}
922
+ sx={{
923
+ borderRadius: 0,
924
+ width: "100%",
925
+ boxShadow: 0
926
+ }}
927
+ >None</Button>
928
+ </Grid>
929
+ <Grid item xs={6}>
930
+ <Button
931
+ onClick={handleSelectAll}
932
+ size='small'
933
+ variant="contained"
934
+ elevation={0}
935
+ sx={{
936
+ borderRadius: 0,
937
+ width: "100%",
938
+ boxShadow: 0
939
+ }}
940
+ >All</Button>
941
+ </Grid>
942
+ </Grid>
943
+ {COLUMNS
944
+ .filter(col => !col.adminOnly || admin)
945
+ .map((column) => (
946
+ <MenuItem
947
+ key={column.id}
948
+ onClick={() => handleColumnToggle(column.id)}
949
+ sx={{
950
+ "&:hover": {
951
+ backgroundColor: "rgba(0,0,0,0.1)"
952
+ }
953
+ }}
954
+ >
955
+ <Checkbox
956
+ checked={selectedColumns.includes(column.id)}
957
+ size="small"
958
+ />
959
+ <ListItemText
960
+ primary={column.label}
961
+ primaryTypographyProps={{
962
+ variant: 'body2',
963
+ style: {
964
+ whiteSpace: 'nowrap',
965
+ overflow: 'hidden',
966
+ textOverflow: 'ellipsis',
967
+ }
968
+ }}
969
+ />
970
+ </MenuItem>
971
+ ))}
972
+ </Menu>
973
+ </Box>
974
+ </Grid>
975
+
976
+ <Grid item xs={12} data-disabled={loading} sx={{opacity: !!dateRangeSales.length ? 1 : 0.25, pointerEvents: !!dateRangeSales.length ? "" : "none"}}>
977
+ <DataGrid
978
+ id={`${slugify(designer.user, { lower: true, remove: /[*/+~.()'"!:@]/g, strict: true })}-date-range`}
979
+ rows={(() => {
980
+ if (!dateRangeSales?.length) {
981
+ // Return array with single no-data row
982
+ return [{
983
+ id: 'no-data',
984
+ sale: null,
985
+ designer,
986
+ admin,
987
+ color: "red"
988
+ }];
989
+ }
990
+
991
+ // Process sales data
992
+ return dateRangeSales.flatMap(sale => {
993
+ const rows = [];
994
+
995
+ // Add refund rows if any
996
+ // Create a unique ID for this sale using orderNumber if available
997
+ const saleUniqueId = sale.orderNumber
998
+ ? `order-${sale.orderNumber}-${sale.lineId || sale.id}`
999
+ : (sale.lineId || sale.id);
1000
+
1001
+ if (sale?.refunds?.length > 0) {
1002
+ sale.refunds.forEach((refund, i) => {
1003
+ rows.push({
1004
+ // Create truly unique ID for each refund row
1005
+ id: `${saleUniqueId}-refund-${i}-${refund.id}-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
1006
+ sale,
1007
+ designer,
1008
+ admin,
1009
+ refund,
1010
+ color: "red"
1011
+ });
1012
+ });
1013
+ }
1014
+
1015
+ // Add original sale row with enhanced uniqueness
1016
+ rows.push({
1017
+ id: `${saleUniqueId}-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
1018
+ sale,
1019
+ designer,
1020
+ admin,
1021
+ color: sale?.disputed ? "red!important" : "inherit"
1022
+ });
1023
+
1024
+ return rows;
1025
+ });
1026
+ })()}
1027
+ columns={memoizedColumns}
1028
+ columnHeaderHeight={36}
1029
+ rowHeight={28}
1030
+ pagination
1031
+ pageSizeOptions={[100]}
1032
+ sortModel={sortModel}
1033
+ onSortModelChange={(newSortModel) => setSortModel(newSortModel)}
1034
+ filterModel={filterModel}
1035
+ onFilterModelChange={(newFilterModel) => setFilterModel(newFilterModel)}
1036
+ disableColumnMenu={true}
1037
+ componentsProps={{
1038
+ columnHeaders: {
1039
+ sx: {
1040
+ // Style for the header cells
1041
+ '& .MuiDataGrid-columnHeader--sorted': {
1042
+ backgroundColor: 'rgba(25, 118, 210, 0.12)',
1043
+ '&:hover': {
1044
+ backgroundColor: 'rgba(25, 118, 210, 0.18)'
1045
+ }
1046
+ }
1047
+ }
1048
+ }
1049
+ }}
1050
+ sx={{
1051
+ backgroundColor: 'rgba(var(--blackRGB, 0,0,0), 0.06)',
1052
+ maxHeight: 'max(300px, 50vh)',
1053
+ maxWidth: 'initial!important',
1054
+ borderTopRightRadius: 0,
1055
+ border: "none",
1056
+ '--DataGrid-overlayHeight': '300px',
1057
+ // Add specific styling for sorted column headers
1058
+ '& .MuiDataGrid-columnHeader--sorted': {
1059
+ backgroundColor: 'rgba(25, 118, 210, 0.12)'
1060
+ },
1061
+ // Optional: add hover effect for sorted headers
1062
+ '& .MuiDataGrid-columnHeader--sorted:hover': {
1063
+ backgroundColor: 'rgba(25, 118, 210, 0.18)'
1064
+ }
1065
+ }}
1066
+ />
1067
+ </Grid>
1068
+ </Grid>
1069
+ )}
1070
+ </Grid>
1071
+ );
1072
+ }