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