@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.
- package/README.md +461 -0
- package/SETUP.md +230 -0
- package/api/getAnalytics.d.ts +38 -0
- package/api/getAnalytics.js +346 -0
- package/api/getBalanceTransactions.d.ts +29 -0
- package/api/getBalanceTransactions.js +125 -0
- package/api/getDesignerInfo.d.ts +37 -0
- package/api/getDesignerInfo.js +98 -0
- package/api/getDesigners.d.ts +28 -0
- package/api/getDesigners.js +63 -0
- package/api/getPreviousSales.d.ts +23 -0
- package/api/getPreviousSales.js +82 -0
- package/api/getSales.d.ts +29 -0
- package/api/getSales.js +50 -0
- package/api/getSalesRange.d.ts +23 -0
- package/api/getSalesRange.js +58 -0
- package/api/utils/authMiddleware.js +84 -0
- package/api/utils/dateUtils.js +69 -0
- package/api/utils/feeCalculator.js +148 -0
- package/api/utils/processors/invoiceProcessor.js +337 -0
- package/api/utils/processors/paymentProcessor.js +462 -0
- package/api/utils/salesDataProcessing.js +596 -0
- package/api/utils/salesDataProcessor.js +224 -0
- package/api/utils/stripeFetcher.js +248 -0
- package/components/DateRangeSalesTable.js +1072 -0
- package/components/DebugValues.js +48 -0
- package/components/LicenseTypeList.js +193 -0
- package/components/LoginForm.js +219 -0
- package/components/PeriodComparison.js +501 -0
- package/components/Sales.js +773 -0
- package/components/SalesChart.js +307 -0
- package/components/SalesPortalPage.js +147 -0
- package/components/SalesTable.js +677 -0
- package/components/SummaryCards.js +345 -0
- package/components/TopPerformers.js +331 -0
- package/components/TypefaceList.js +154 -0
- package/components/table-columns.js +70 -0
- package/components/table-row-cells.js +295 -0
- package/data/countryCode.json +318 -0
- package/hooks/useSalesDateQuery.d.ts +20 -0
- package/hooks/useSalesDateQuery.js +71 -0
- package/index.d.ts +172 -0
- package/index.js +33 -0
- package/package.json +87 -0
- package/styles/sales-portal.module.scss +383 -0
- package/styles/sales-portal.theme.d.ts +5 -0
- package/styles/sales-portal.theme.js +799 -0
- package/utils/currencyUtils.d.ts +20 -0
- package/utils/currencyUtils.js +79 -0
- package/utils/salesDataProcessing.d.ts +44 -0
- package/utils/salesDataProcessing.js +596 -0
- 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
|
+
}
|