@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,677 @@
|
|
|
1
|
+
// Sales table component for displaying detailed sales information
|
|
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
|
+
CircularProgress
|
|
23
|
+
} from '@mui/material';
|
|
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 styles from '../styles/sales-portal.module.scss';
|
|
30
|
+
import { TableRowCells, getCellValue } from './table-row-cells';
|
|
31
|
+
import { COLUMNS } from './table-columns';
|
|
32
|
+
import { DataGrid } from '@mui/x-data-grid';
|
|
33
|
+
|
|
34
|
+
export function SalesTable({ sales = [], designer = {}, admin = false, loading = false, date = new Date(), updateLoadingState = () => {} }) {
|
|
35
|
+
const [anchorEl, setAnchorEl] = useState(null);
|
|
36
|
+
const [selectedColumns, setSelectedColumns] = useState([
|
|
37
|
+
'orderNumber',
|
|
38
|
+
'total',
|
|
39
|
+
'amountDiscounted',
|
|
40
|
+
'taxAmount',
|
|
41
|
+
'preTaxTotal',
|
|
42
|
+
'date',
|
|
43
|
+
'description',
|
|
44
|
+
'typeface',
|
|
45
|
+
'refund'
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
// Add sortModel state to manage sorting
|
|
49
|
+
const [sortModel, setSortModel] = useState([
|
|
50
|
+
{
|
|
51
|
+
field: 'date',
|
|
52
|
+
sort: 'desc'
|
|
53
|
+
}
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
// Filter model state for implementing filtering
|
|
57
|
+
const [filterModel, setFilterModel] = useState({
|
|
58
|
+
items: [],
|
|
59
|
+
quickFilterValues: []
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Reconciliation state
|
|
63
|
+
const [reconciliationData, setReconciliationData] = useState({
|
|
64
|
+
isLoading: false,
|
|
65
|
+
isReconciled: false,
|
|
66
|
+
totalBalanceChange: 0,
|
|
67
|
+
grossSales: 0,
|
|
68
|
+
difference: 0,
|
|
69
|
+
error: null
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const handleMenuClick = (event) => {
|
|
73
|
+
setAnchorEl(event.currentTarget);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleMenuClose = () => {
|
|
77
|
+
setAnchorEl(null);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleColumnToggle = (columnId) => {
|
|
81
|
+
setSelectedColumns(prev => {
|
|
82
|
+
if (prev.includes(columnId)) {
|
|
83
|
+
return prev.filter(id => id !== columnId);
|
|
84
|
+
} else {
|
|
85
|
+
return [...prev, columnId];
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleSelectAll = () => {
|
|
91
|
+
setSelectedColumns(COLUMNS.filter(col => !col.adminOnly || admin).map(col => col.id));
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleSelectNone = () => {
|
|
95
|
+
setSelectedColumns([]);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Memoize the columns configuration
|
|
99
|
+
const memoizedColumns = useMemo(() =>
|
|
100
|
+
COLUMNS
|
|
101
|
+
.filter(column => selectedColumns.includes(column.id) && (!column.adminOnly || admin))
|
|
102
|
+
.map(column => ({
|
|
103
|
+
field: column.id,
|
|
104
|
+
headerName: column.label || column.headerName,
|
|
105
|
+
flex: 1,
|
|
106
|
+
minWidth: 120,
|
|
107
|
+
// Explicitly enable sorting
|
|
108
|
+
sortable: true,
|
|
109
|
+
// Add custom comparator for currency and date values
|
|
110
|
+
sortComparator: (v1, v2, param1, param2) => {
|
|
111
|
+
// Extract the actual values from complex cell data
|
|
112
|
+
const getValue = (params) => {
|
|
113
|
+
if (!params.row) return '';
|
|
114
|
+
const result = getCellValue({
|
|
115
|
+
column,
|
|
116
|
+
sale: params.row.sale || null,
|
|
117
|
+
designer: params.row.designer || designer,
|
|
118
|
+
admin: params.row.admin || admin,
|
|
119
|
+
refund: params.row.refund || null,
|
|
120
|
+
isNoData: !params.row.sale,
|
|
121
|
+
color: params.row.color || ''
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Handle React elements (like links)
|
|
125
|
+
if (typeof result.value === 'object' && React.isValidElement(result.value)) {
|
|
126
|
+
return result.value.props.children || '';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result.value;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Get the actual values to compare
|
|
133
|
+
const val1 = getValue({ row: param1.api.getRow(param1.id) });
|
|
134
|
+
const val2 = getValue({ row: param2.api.getRow(param2.id) });
|
|
135
|
+
|
|
136
|
+
// For currency values, extract the number for comparison
|
|
137
|
+
if (column.id === 'total' || column.id === 'amountDiscounted' ||
|
|
138
|
+
column.id === 'preTaxTotal' || column.id === 'taxAmount' ||
|
|
139
|
+
column.id === 'stripeFees') {
|
|
140
|
+
// Extract numbers from currency strings like "$1,234.56"
|
|
141
|
+
const extractNumber = (str) => {
|
|
142
|
+
if (typeof str !== 'string') return str;
|
|
143
|
+
const match = str.replace(/[^0-9.-]+/g, '');
|
|
144
|
+
return match ? parseFloat(match) : 0;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return extractNumber(val1) - extractNumber(val2);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// For dates
|
|
151
|
+
if (column.id === 'date') {
|
|
152
|
+
const date1 = new Date(val1);
|
|
153
|
+
const date2 = new Date(val2);
|
|
154
|
+
return date1 - date2;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Default string comparison
|
|
158
|
+
if (typeof val1 === 'string' && typeof val2 === 'string') {
|
|
159
|
+
return val1.localeCompare(val2);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Fallback to default comparison
|
|
163
|
+
return v1 < v2 ? -1 : v1 > v2 ? 1 : 0;
|
|
164
|
+
},
|
|
165
|
+
renderCell: (params) => {
|
|
166
|
+
if (!params.row) return '';
|
|
167
|
+
|
|
168
|
+
const result = getCellValue({
|
|
169
|
+
column,
|
|
170
|
+
sale: params.row.sale || null,
|
|
171
|
+
designer: params.row.designer || designer,
|
|
172
|
+
admin: params.row.admin || admin,
|
|
173
|
+
refund: params.row.refund || null,
|
|
174
|
+
isNoData: !params.row.sale,
|
|
175
|
+
color: params.row.color || ''
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return <div style={{ color: params.row.color || undefined }}>{result.value}</div>;
|
|
179
|
+
}
|
|
180
|
+
}))
|
|
181
|
+
, [selectedColumns, admin, designer]);
|
|
182
|
+
|
|
183
|
+
// Calculate gross sales from the table's Gross Total column
|
|
184
|
+
function calculateGrossSales() {
|
|
185
|
+
let grossSales = 0;
|
|
186
|
+
sales.forEach(sale => {
|
|
187
|
+
sale?.refunds?.map((refund, i) => {
|
|
188
|
+
grossSales -= (refund.adjustedTotal || 0);
|
|
189
|
+
})
|
|
190
|
+
grossSales += sale?.shippingProvision ? sale?.total : sale?.totalWithTax;
|
|
191
|
+
});
|
|
192
|
+
return grossSales;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Fetch balance transactions from Stripe
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (!designer?.user || !designer?.password || !date || !admin || !sales) return;
|
|
198
|
+
|
|
199
|
+
const fetchBalanceTransactions = async () => {
|
|
200
|
+
setReconciliationData(prev => ({ ...prev, isLoading: true, error: null }));
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const response = await fetch('/api/sales-portal/getBalanceTransactions', {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
headers: { 'Content-Type': 'application/json' },
|
|
206
|
+
body: JSON.stringify({
|
|
207
|
+
user: designer?.user,
|
|
208
|
+
password: designer?.password,
|
|
209
|
+
date,
|
|
210
|
+
admin: designer?.admin
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const data = await response.json();
|
|
215
|
+
|
|
216
|
+
if (data.success) {
|
|
217
|
+
const totalBalanceChange = data.data.totalBalanceChange;
|
|
218
|
+
const grossSales = calculateGrossSales();
|
|
219
|
+
|
|
220
|
+
setReconciliationData(prev => ({
|
|
221
|
+
...prev,
|
|
222
|
+
totalBalanceChange,
|
|
223
|
+
grossSales,
|
|
224
|
+
difference: totalBalanceChange - grossSales,
|
|
225
|
+
isReconciled: Math.abs(totalBalanceChange - grossSales) < 1, // Allow for rounding differences
|
|
226
|
+
isLoading: false
|
|
227
|
+
}));
|
|
228
|
+
} else {
|
|
229
|
+
setReconciliationData(prev => ({
|
|
230
|
+
...prev,
|
|
231
|
+
error: data.message || 'Failed to fetch balance transactions',
|
|
232
|
+
isLoading: false
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error('Error fetching balance transactions:', error);
|
|
237
|
+
setReconciliationData(prev => ({
|
|
238
|
+
...prev,
|
|
239
|
+
error: error.message || 'Failed to fetch balance transactions',
|
|
240
|
+
isLoading: false
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
fetchBalanceTransactions();
|
|
246
|
+
}, [designer?.user, designer?.password, designer?.admin, date, admin, sales]);
|
|
247
|
+
|
|
248
|
+
// Early return if no designer data is available
|
|
249
|
+
if (!designer?.user || !designer?.password) {
|
|
250
|
+
return (
|
|
251
|
+
<Grid container sx={{ mt: 16 }} className={styles.salesSection}>
|
|
252
|
+
<Grid item xs={12}>
|
|
253
|
+
<Typography variant="h6" sx={{ color: 'var(--red, red)' }}>
|
|
254
|
+
Error: Designer credentials not available
|
|
255
|
+
</Typography>
|
|
256
|
+
</Grid>
|
|
257
|
+
</Grid>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Downloads sales data as CSV file
|
|
263
|
+
* @param {string} separator - CSV separator character
|
|
264
|
+
* @param {Object} designer - Designer information
|
|
265
|
+
* @param {Date} date - Current date
|
|
266
|
+
*/
|
|
267
|
+
const downloadSalesData = (separator = ',', designer, date, selectedCols = selectedColumns) => {
|
|
268
|
+
updateLoadingState('csvDownload', true);
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
// Get filtered columns
|
|
272
|
+
const filteredColumns = COLUMNS
|
|
273
|
+
.filter(column => selectedCols.includes(column.id) && (!column.adminOnly || admin));
|
|
274
|
+
|
|
275
|
+
// Generate row data programmatically instead of relying on DOM
|
|
276
|
+
const rows = !sales?.length
|
|
277
|
+
? [{
|
|
278
|
+
sale: null,
|
|
279
|
+
designer,
|
|
280
|
+
admin,
|
|
281
|
+
isNoData: true,
|
|
282
|
+
color: "red"
|
|
283
|
+
}]
|
|
284
|
+
: sales.flatMap(sale => {
|
|
285
|
+
const rowsData = [];
|
|
286
|
+
|
|
287
|
+
// Add refund rows if any
|
|
288
|
+
if (sale?.refunds?.length > 0) {
|
|
289
|
+
sale.refunds.forEach((refund) => {
|
|
290
|
+
rowsData.push({
|
|
291
|
+
sale,
|
|
292
|
+
designer,
|
|
293
|
+
admin,
|
|
294
|
+
refund,
|
|
295
|
+
color: "red"
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Add original sale row
|
|
301
|
+
rowsData.push({
|
|
302
|
+
sale,
|
|
303
|
+
designer,
|
|
304
|
+
admin,
|
|
305
|
+
color: sale?.disputed ? "red!important" : "inherit"
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return rowsData;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Generate CSV header row
|
|
312
|
+
const headerRow = filteredColumns.map(column => `"${column.label}"`).join(separator);
|
|
313
|
+
|
|
314
|
+
// Generate CSV data rows
|
|
315
|
+
const dataRows = rows.map(row => {
|
|
316
|
+
return filteredColumns.map(column => {
|
|
317
|
+
const result = getCellValue({
|
|
318
|
+
column,
|
|
319
|
+
sale: row.sale,
|
|
320
|
+
designer: row.designer,
|
|
321
|
+
admin: row.admin,
|
|
322
|
+
refund: row.refund,
|
|
323
|
+
isNoData: row.isNoData,
|
|
324
|
+
color: row.color
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Format the value for CSV
|
|
328
|
+
let value = result.value;
|
|
329
|
+
if (typeof value === 'object' && React.isValidElement(value)) {
|
|
330
|
+
// If value is a React element (like a link), extract its text
|
|
331
|
+
value = value.props.children || '';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Clean and format cell data
|
|
335
|
+
let data = String(value)
|
|
336
|
+
.replace(/(\r\n|\n|\r)/gm, '')
|
|
337
|
+
.replace(/(\s\s)/gm, ' ')
|
|
338
|
+
.replace(/"/g, '""');
|
|
339
|
+
|
|
340
|
+
return `"${data}"`;
|
|
341
|
+
}).join(separator);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Combine header and data rows
|
|
345
|
+
const csv_string = [headerRow, ...dataRows].join('\n');
|
|
346
|
+
|
|
347
|
+
// Download it
|
|
348
|
+
const filename = `${slugify(designer.user, { lower: true, remove: /[*/+~.()'"!:@]/g, strict: true })}-${slugify(new Date(date).toUTCString(), { lower: true, remove: /[*/+~.()'"!:@]/g, strict: true })}.csv`;
|
|
349
|
+
const link = document.createElement('a');
|
|
350
|
+
link.style.display = 'none';
|
|
351
|
+
link.setAttribute('target', '_blank');
|
|
352
|
+
link.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv_string));
|
|
353
|
+
link.setAttribute('download', filename);
|
|
354
|
+
document.body.appendChild(link);
|
|
355
|
+
link.click();
|
|
356
|
+
document.body.removeChild(link);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error('Error downloading sales data:', error);
|
|
359
|
+
} finally {
|
|
360
|
+
updateLoadingState('csvDownload', false);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<Grid
|
|
366
|
+
container
|
|
367
|
+
data-disabled={loading}
|
|
368
|
+
data-loading={loading}
|
|
369
|
+
className='salesTable-section'
|
|
370
|
+
>
|
|
371
|
+
{/* Reconciliation Check - Only show for admin users */}
|
|
372
|
+
{admin && !reconciliationData.error && sales.length > 0 && (
|
|
373
|
+
<Grid item xs={12} sx={{ mb: 2 }}>
|
|
374
|
+
<Box
|
|
375
|
+
sx={{
|
|
376
|
+
display: 'inline-block',
|
|
377
|
+
gap: 2,
|
|
378
|
+
p: 2,
|
|
379
|
+
mt: "20px",
|
|
380
|
+
borderRadius: '4px',
|
|
381
|
+
border: reconciliationData.isReconciled
|
|
382
|
+
? '2px solid var(--green, green)'
|
|
383
|
+
: '2px solid var(--red, red)',
|
|
384
|
+
color: reconciliationData.isReconciled
|
|
385
|
+
? 'var(--green, green)'
|
|
386
|
+
: 'var(--red, red)',
|
|
387
|
+
}}
|
|
388
|
+
>
|
|
389
|
+
{reconciliationData.isLoading ? (
|
|
390
|
+
<Typography variant="body1"><CircularProgress size={24} /> Checking reconciliation...</Typography>
|
|
391
|
+
) : reconciliationData.isReconciled ? (
|
|
392
|
+
<Typography variant="body1" sx={{ fontWeight: 'bold' }}>
|
|
393
|
+
<CheckCircleIcon /> Reconciled
|
|
394
|
+
</Typography>
|
|
395
|
+
) : (
|
|
396
|
+
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
|
397
|
+
<Tooltip title="The difference between Stripe balance change and gross sales">
|
|
398
|
+
<Typography variant="body1" sx={{ fontWeight: 'bold', display: 'flex', alignItems: 'center' }}>
|
|
399
|
+
<WarningIcon/> Needs Reconciliation (Difference: ${(Math.abs(reconciliationData.difference) / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })})
|
|
400
|
+
</Typography>
|
|
401
|
+
</Tooltip>
|
|
402
|
+
<Tooltip
|
|
403
|
+
title={
|
|
404
|
+
<React.Fragment>
|
|
405
|
+
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>Possible causes:</Typography>
|
|
406
|
+
<Typography variant="body2">• This could be due to a sale being refunded in another month. Check for the same difference either the prior or next month.</Typography>
|
|
407
|
+
<Typography variant="body2">• If a payment was approved after the invoice was finalized, there might be a delayed payment that shows up next month.</Typography>
|
|
408
|
+
</React.Fragment>
|
|
409
|
+
}
|
|
410
|
+
placement="right"
|
|
411
|
+
arrow
|
|
412
|
+
>
|
|
413
|
+
<IconButton size="small" sx={{ ml: 1, color: 'inherit' }}>
|
|
414
|
+
<InfoIcon fontSize="small" />
|
|
415
|
+
</IconButton>
|
|
416
|
+
</Tooltip>
|
|
417
|
+
</Box>
|
|
418
|
+
)}
|
|
419
|
+
|
|
420
|
+
{!reconciliationData.isLoading && (
|
|
421
|
+
<>
|
|
422
|
+
<Typography variant="body1">
|
|
423
|
+
<strong>Stripe Balance Change:</strong> ${(reconciliationData.totalBalanceChange / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}
|
|
424
|
+
</Typography>
|
|
425
|
+
<Typography variant="body1">
|
|
426
|
+
<strong>Gross Sales:</strong> ${(reconciliationData.grossSales / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}
|
|
427
|
+
</Typography>
|
|
428
|
+
</>
|
|
429
|
+
)}
|
|
430
|
+
|
|
431
|
+
</Box>
|
|
432
|
+
</Grid>
|
|
433
|
+
)}
|
|
434
|
+
|
|
435
|
+
{/* Sales Table */}
|
|
436
|
+
<Grid item xs={12}
|
|
437
|
+
textAlign={"right"}
|
|
438
|
+
sx={{
|
|
439
|
+
opacity: !!sales?.length ? 1 : 0.25,
|
|
440
|
+
pointerEvents: !!sales?.length ? "" : "none",
|
|
441
|
+
display: "flex",
|
|
442
|
+
justifyContent: "flex-end",
|
|
443
|
+
alignItems: "flex-end",
|
|
444
|
+
}}
|
|
445
|
+
>
|
|
446
|
+
<Box
|
|
447
|
+
className='buttonStyle'
|
|
448
|
+
sx={{
|
|
449
|
+
position: "relative",
|
|
450
|
+
borderRadius: "4px 4px 0px 0px ",
|
|
451
|
+
pointerEvents: "none",
|
|
452
|
+
backgroundColor: 'primary.main',
|
|
453
|
+
color: 'primary.contrastText',
|
|
454
|
+
display: 'inline-flex',
|
|
455
|
+
alignItems: 'center',
|
|
456
|
+
justifyContent: 'center',
|
|
457
|
+
padding: '6px 16px',
|
|
458
|
+
fontWeight: 500,
|
|
459
|
+
}}
|
|
460
|
+
>
|
|
461
|
+
<Grid container alignItems="center" spacing={1}>
|
|
462
|
+
<Grid item>
|
|
463
|
+
<Tooltip title="Select columns">
|
|
464
|
+
<IconButton
|
|
465
|
+
size="small"
|
|
466
|
+
onClick={handleMenuClick}
|
|
467
|
+
sx={{
|
|
468
|
+
color: 'var(--white, white)',
|
|
469
|
+
pointerEvents: "auto",
|
|
470
|
+
'&:hover': {
|
|
471
|
+
opacity: 0.8
|
|
472
|
+
}
|
|
473
|
+
}}
|
|
474
|
+
>
|
|
475
|
+
<TuneIcon fontSize="small" />
|
|
476
|
+
</IconButton>
|
|
477
|
+
</Tooltip>
|
|
478
|
+
</Grid>
|
|
479
|
+
<Grid item>
|
|
480
|
+
<Tooltip title="Download CSV">
|
|
481
|
+
<IconButton
|
|
482
|
+
size="small"
|
|
483
|
+
onClick={() => downloadSalesData(',', designer, date, selectedColumns)}
|
|
484
|
+
sx={{
|
|
485
|
+
color: 'var(--white, white)',
|
|
486
|
+
pointerEvents: "auto",
|
|
487
|
+
'&:hover': {
|
|
488
|
+
opacity: 0.8
|
|
489
|
+
}
|
|
490
|
+
}}
|
|
491
|
+
>
|
|
492
|
+
<DownloadIcon fontSize="small" />
|
|
493
|
+
</IconButton>
|
|
494
|
+
</Tooltip>
|
|
495
|
+
</Grid>
|
|
496
|
+
<Grid item ml={1}>
|
|
497
|
+
<strong style={{ color: 'var(--white, white)' }}>CSV</strong>
|
|
498
|
+
</Grid>
|
|
499
|
+
</Grid>
|
|
500
|
+
|
|
501
|
+
<Menu
|
|
502
|
+
anchorEl={anchorEl}
|
|
503
|
+
open={Boolean(anchorEl)}
|
|
504
|
+
onClose={handleMenuClose}
|
|
505
|
+
dense={true}
|
|
506
|
+
PaperProps={{
|
|
507
|
+
sx: {
|
|
508
|
+
maxHeight: 300,
|
|
509
|
+
width: 250,
|
|
510
|
+
borderRadius: '4px',
|
|
511
|
+
}
|
|
512
|
+
}}
|
|
513
|
+
>
|
|
514
|
+
<Grid container>
|
|
515
|
+
<Grid item xs={6}>
|
|
516
|
+
<Button
|
|
517
|
+
onClick={handleSelectNone}
|
|
518
|
+
size='small'
|
|
519
|
+
variant="contained"
|
|
520
|
+
elevation={0}
|
|
521
|
+
sx={{
|
|
522
|
+
borderRadius: 0,
|
|
523
|
+
width: "100%",
|
|
524
|
+
boxShadow: 0
|
|
525
|
+
}}
|
|
526
|
+
>None</Button>
|
|
527
|
+
</Grid>
|
|
528
|
+
<Grid item xs={6}>
|
|
529
|
+
<Button
|
|
530
|
+
onClick={handleSelectAll}
|
|
531
|
+
size='small'
|
|
532
|
+
variant="contained"
|
|
533
|
+
elevation={0}
|
|
534
|
+
sx={{
|
|
535
|
+
borderRadius: 0,
|
|
536
|
+
width: "100%",
|
|
537
|
+
boxShadow: 0
|
|
538
|
+
}}
|
|
539
|
+
>All</Button>
|
|
540
|
+
</Grid>
|
|
541
|
+
</Grid>
|
|
542
|
+
{COLUMNS
|
|
543
|
+
.filter(col => !col.adminOnly || admin)
|
|
544
|
+
.map((column) => (
|
|
545
|
+
<MenuItem
|
|
546
|
+
key={column.id}
|
|
547
|
+
onClick={() => handleColumnToggle(column.id)}
|
|
548
|
+
sx={{
|
|
549
|
+
"&:hover": {
|
|
550
|
+
backgroundColor: "rgba(0,0,0,0.1)"
|
|
551
|
+
}
|
|
552
|
+
}}
|
|
553
|
+
>
|
|
554
|
+
<Checkbox
|
|
555
|
+
checked={selectedColumns.includes(column.id)}
|
|
556
|
+
size="small"
|
|
557
|
+
/>
|
|
558
|
+
<ListItemText
|
|
559
|
+
primary={column.label}
|
|
560
|
+
primaryTypographyProps={{
|
|
561
|
+
variant: 'body2',
|
|
562
|
+
style: {
|
|
563
|
+
whiteSpace: 'nowrap',
|
|
564
|
+
overflow: 'hidden',
|
|
565
|
+
textOverflow: 'ellipsis',
|
|
566
|
+
}
|
|
567
|
+
}}
|
|
568
|
+
/>
|
|
569
|
+
</MenuItem>
|
|
570
|
+
))}
|
|
571
|
+
</Menu>
|
|
572
|
+
</Box>
|
|
573
|
+
</Grid>
|
|
574
|
+
|
|
575
|
+
<Grid
|
|
576
|
+
item
|
|
577
|
+
xs={12}
|
|
578
|
+
data-disabled={loading}
|
|
579
|
+
sx={{
|
|
580
|
+
opacity: !!sales?.length ? 1 : 0.25,
|
|
581
|
+
pointerEvents: !!sales?.length ? "" : "none"
|
|
582
|
+
}}
|
|
583
|
+
>
|
|
584
|
+
<DataGrid
|
|
585
|
+
id={`${slugify(designer.user, { lower: true, remove: /[*/+~.()'"!:@]/g, strict: true })}-${slugify(new Date(date).toUTCString(), { lower: true, remove: /[*/+~.()'"!:@]/g, strict: true })}`}
|
|
586
|
+
rows={(() => {
|
|
587
|
+
if (!sales?.length) {
|
|
588
|
+
// Return array with single no-data row
|
|
589
|
+
return [{
|
|
590
|
+
id: 'no-data',
|
|
591
|
+
sale: null,
|
|
592
|
+
designer,
|
|
593
|
+
admin,
|
|
594
|
+
color: "red"
|
|
595
|
+
}];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Process sales data
|
|
599
|
+
return sales.flatMap(sale => {
|
|
600
|
+
const rows = [];
|
|
601
|
+
|
|
602
|
+
// Create a unique ID for this sale using orderNumber if available
|
|
603
|
+
const saleUniqueId = sale.orderNumber
|
|
604
|
+
? `order-${sale.orderNumber}-${sale.id}`
|
|
605
|
+
: sale.id;
|
|
606
|
+
|
|
607
|
+
// Add refund rows if any
|
|
608
|
+
if (sale?.refunds?.length > 0) {
|
|
609
|
+
sale.refunds.forEach((refund, i) => {
|
|
610
|
+
rows.push({
|
|
611
|
+
// Create truly unique ID for each refund row
|
|
612
|
+
id: `${saleUniqueId}-refund-${i}-${refund.id}`,
|
|
613
|
+
sale,
|
|
614
|
+
designer,
|
|
615
|
+
admin,
|
|
616
|
+
refund,
|
|
617
|
+
color: "red"
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Add original sale row with enhanced uniqueness
|
|
623
|
+
rows.push({
|
|
624
|
+
id: `${saleUniqueId}-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
|
|
625
|
+
sale,
|
|
626
|
+
designer,
|
|
627
|
+
admin,
|
|
628
|
+
color: sale?.disputed ? "red!important" : "inherit"
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
return rows;
|
|
632
|
+
});
|
|
633
|
+
})()}
|
|
634
|
+
columns={memoizedColumns}
|
|
635
|
+
columnHeaderHeight={36}
|
|
636
|
+
rowHeight={28}
|
|
637
|
+
pagination
|
|
638
|
+
pageSizeOptions={[100]}
|
|
639
|
+
sortModel={sortModel}
|
|
640
|
+
onSortModelChange={(newSortModel) => setSortModel(newSortModel)}
|
|
641
|
+
filterModel={filterModel}
|
|
642
|
+
onFilterModelChange={(newFilterModel) => setFilterModel(newFilterModel)}
|
|
643
|
+
disableColumnMenu={true}
|
|
644
|
+
componentsProps={{
|
|
645
|
+
columnHeaders: {
|
|
646
|
+
sx: {
|
|
647
|
+
// Style for the header cells
|
|
648
|
+
'& .MuiDataGrid-columnHeader--sorted': {
|
|
649
|
+
backgroundColor: 'rgba(25, 118, 210, 0.12)',
|
|
650
|
+
'&:hover': {
|
|
651
|
+
backgroundColor: 'rgba(25, 118, 210, 0.18)'
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}}
|
|
657
|
+
sx={{
|
|
658
|
+
backgroundColor: 'rgba(var(--blackRGB, 0,0,0), 0.06)',
|
|
659
|
+
maxHeight: 'max(300px, 50vh)',
|
|
660
|
+
maxWidth: 'initial!important',
|
|
661
|
+
borderTopRightRadius: 0,
|
|
662
|
+
border: "none",
|
|
663
|
+
'--DataGrid-overlayHeight': '300px',
|
|
664
|
+
// Add specific styling for sorted column headers
|
|
665
|
+
'& .MuiDataGrid-columnHeader--sorted': {
|
|
666
|
+
backgroundColor: 'rgba(25, 118, 210, 0.12)'
|
|
667
|
+
},
|
|
668
|
+
// Optional: add hover effect for sorted headers
|
|
669
|
+
'& .MuiDataGrid-columnHeader--sorted:hover': {
|
|
670
|
+
backgroundColor: 'rgba(25, 118, 210, 0.18)'
|
|
671
|
+
}
|
|
672
|
+
}}
|
|
673
|
+
/>
|
|
674
|
+
</Grid>
|
|
675
|
+
</Grid>
|
|
676
|
+
);
|
|
677
|
+
}
|