@liiift-studio/sales-portal 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +461 -0
  2. package/SETUP.md +230 -0
  3. package/api/getAnalytics.d.ts +38 -0
  4. package/api/getAnalytics.js +346 -0
  5. package/api/getBalanceTransactions.d.ts +29 -0
  6. package/api/getBalanceTransactions.js +125 -0
  7. package/api/getDesignerInfo.d.ts +37 -0
  8. package/api/getDesignerInfo.js +98 -0
  9. package/api/getDesigners.d.ts +28 -0
  10. package/api/getDesigners.js +63 -0
  11. package/api/getPreviousSales.d.ts +23 -0
  12. package/api/getPreviousSales.js +82 -0
  13. package/api/getSales.d.ts +29 -0
  14. package/api/getSales.js +50 -0
  15. package/api/getSalesRange.d.ts +23 -0
  16. package/api/getSalesRange.js +58 -0
  17. package/api/utils/authMiddleware.js +84 -0
  18. package/api/utils/dateUtils.js +69 -0
  19. package/api/utils/feeCalculator.js +148 -0
  20. package/api/utils/processors/invoiceProcessor.js +337 -0
  21. package/api/utils/processors/paymentProcessor.js +462 -0
  22. package/api/utils/salesDataProcessing.js +596 -0
  23. package/api/utils/salesDataProcessor.js +224 -0
  24. package/api/utils/stripeFetcher.js +248 -0
  25. package/components/DateRangeSalesTable.js +1072 -0
  26. package/components/DebugValues.js +48 -0
  27. package/components/LicenseTypeList.js +193 -0
  28. package/components/LoginForm.js +219 -0
  29. package/components/PeriodComparison.js +501 -0
  30. package/components/Sales.js +773 -0
  31. package/components/SalesChart.js +307 -0
  32. package/components/SalesPortalPage.js +147 -0
  33. package/components/SalesTable.js +677 -0
  34. package/components/SummaryCards.js +345 -0
  35. package/components/TopPerformers.js +331 -0
  36. package/components/TypefaceList.js +154 -0
  37. package/components/table-columns.js +70 -0
  38. package/components/table-row-cells.js +295 -0
  39. package/data/countryCode.json +318 -0
  40. package/hooks/useSalesDateQuery.d.ts +20 -0
  41. package/hooks/useSalesDateQuery.js +71 -0
  42. package/index.d.ts +172 -0
  43. package/index.js +33 -0
  44. package/package.json +87 -0
  45. package/styles/sales-portal.module.scss +383 -0
  46. package/styles/sales-portal.theme.d.ts +5 -0
  47. package/styles/sales-portal.theme.js +799 -0
  48. package/utils/currencyUtils.d.ts +20 -0
  49. package/utils/currencyUtils.js +79 -0
  50. package/utils/salesDataProcessing.d.ts +44 -0
  51. package/utils/salesDataProcessing.js +596 -0
  52. package/utils/useSalesDateQuery.js +71 -0
@@ -0,0 +1,345 @@
1
+ // Summary dashboard cards displaying key sales metrics at a glance with country code tooltips
2
+ import React from 'react';
3
+ import {
4
+ Grid,
5
+ Box,
6
+ Typography,
7
+ Paper,
8
+ Tooltip,
9
+ useMediaQuery,
10
+ useTheme
11
+ } from '@mui/material';
12
+ import TrendingUpIcon from '@mui/icons-material/TrendingUp';
13
+ import TrendingDownIcon from '@mui/icons-material/TrendingDown';
14
+ import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
15
+ import styles from '../styles/sales-portal.module.scss';
16
+ import DebugValues from './DebugValues';
17
+ import { formatCurrency, calculatePercentageChange, formatPercentage } from '../utils/currencyUtils';
18
+ import countryCodes from '../data/countryCode.json';
19
+
20
+ /**
21
+ * Gets the full country name from a country code
22
+ * @param {string} code - The country code (e.g., 'US', 'GB')
23
+ * @returns {string} The full country name or the original code if not found
24
+ */
25
+ const getCountryName = (code) => {
26
+ if (!code || code === 'Unknown') return 'Unknown';
27
+
28
+ // If it's already a full name (more than 2 chars), return as is
29
+ if (code.length > 2) return code;
30
+
31
+ // Find the country in the countryCodes array
32
+ const country = countryCodes.find(c => c.code === code);
33
+ return country ? country.label : code;
34
+ };
35
+
36
+ /**
37
+ * Summary dashboard component showing key sales metrics with period-over-period comparison
38
+ * @param {Object} props - Component props
39
+ * @param {Array} props.sales - Current period sales data
40
+ * @param {Array} props.previousSales - Previous period sales data for comparison
41
+ * @param {boolean} props.loading - Loading state
42
+ * @param {Object} props.chartState - Chart data state
43
+ * @param {Date} props.date - Current date being displayed
44
+ * @param {Object} props.locationData - Data about sales by location
45
+ * @returns {JSX.Element} Summary cards component
46
+ */
47
+ export default function SummaryCards({
48
+ sales = [],
49
+ previousSales = [],
50
+ loading = false,
51
+ chartState,
52
+ date,
53
+ locationData = {}
54
+ }) {
55
+ if (!sales || !chartState) {
56
+ return null;
57
+ }
58
+
59
+ // Get month name for display
60
+ const monthName = new Date(date).toLocaleString("en-US", { timeZone: 'UTC', month: "long" });
61
+ const year = new Date(date).getUTCFullYear();
62
+
63
+ // NOTE: Values from chartState are transformed in the processChartData function in salesDataProcessing.js
64
+ // Values from the API are in cents, but processChartData divides them by 100 for display
65
+
66
+ // Net revenue (excluding tax and shipping)
67
+ const netRevenue = (chartState.salesMax - chartState.taxData.at(-1) - chartState.shippingData.at(-1)) || 0;
68
+
69
+ // Gross revenue (including tax and shipping)
70
+ const grossRevenue = chartState.salesMax || 0;
71
+
72
+ const taxTotal = chartState.taxData.at(-1) || 0;
73
+ const shippingTotal = chartState.shippingData.at(-1) || 0;
74
+
75
+ const orderCount = sales.length;
76
+ const averageOrderValue = orderCount ? netRevenue / orderCount : 0;
77
+ // Sum all daily discount amounts to get total discounts for the period
78
+ const discountTotal = chartState.discountLostData.reduce((sum, val) => sum + val, 0) || 0;
79
+ const discountRate = netRevenue > 0 ? (discountTotal / (netRevenue + discountTotal)) * 100 : 0;
80
+ const refundTotal = chartState.refundTotal || 0;
81
+ const refundRate = netRevenue > 0 ? (refundTotal / (netRevenue + refundTotal)) * 100 : 0;
82
+
83
+ // Location data
84
+ const {
85
+ topLocation = 'Unknown',
86
+ topLocationPercentage = 0,
87
+ previousTopLocation = '',
88
+ previousTopLocationPercentage = 0,
89
+ locationChange = null
90
+ } = locationData;
91
+
92
+ // Calculate previous period metrics (if available)
93
+ let previousRevenue = 0;
94
+ let previousOrderCount = 0;
95
+ let previousAvgOrderValue = 0;
96
+ let previousRefundTotal = 0;
97
+ let hasPreviousData = false;
98
+
99
+ if (previousSales && previousSales.length > 0) {
100
+ hasPreviousData = true;
101
+ previousOrderCount = previousSales.length;
102
+
103
+ // Important: Raw sale data is in cents, but after applying processChartData,
104
+ // the previous period data would be in dollars.
105
+ // Here we're calculating manually, so we need to convert the values ourselves
106
+ previousRevenue = previousSales.reduce((sum, sale) => {
107
+ const saleTotal = sale.total || 0;
108
+ const saleTax = sale.taxAmount || 0;
109
+ const saleShipping = sale.shippingCost || 0;
110
+ return sum + (saleTotal - saleTax - saleShipping) / 100; // Convert cents to dollars
111
+ }, 0);
112
+
113
+ previousRefundTotal = previousSales.reduce((sum, sale) => {
114
+ if (sale.refunds && sale.refunds.length > 0) {
115
+ return sum + sale.refunds.reduce((refundSum, refund) => {
116
+ return refundSum + (refund.adjustedTotal || 0);
117
+ }, 0) / 100; // Convert cents to dollars
118
+ }
119
+ return sum;
120
+ }, 0);
121
+
122
+ previousAvgOrderValue = previousOrderCount ? previousRevenue / previousOrderCount : 0;
123
+ }
124
+
125
+ // Calculate period-over-period changes with our utility function
126
+ const revenueChange = calculatePercentageChange(grossRevenue, previousRevenue);
127
+ const orderCountChange = calculatePercentageChange(orderCount, previousOrderCount);
128
+ const avgOrderValueChange = calculatePercentageChange(averageOrderValue, previousAvgOrderValue);
129
+ const refundChange = calculatePercentageChange(refundTotal, previousRefundTotal);
130
+
131
+ // Helper function to get trend icon
132
+ const getTrendIcon = (change) => {
133
+ if (change === null) return null;
134
+
135
+ // For refund metrics, lower is better
136
+ const isRefundMetric = false; // Set to true for refund metrics
137
+
138
+ if (Math.abs(change) < 1) return <TrendingFlatIcon sx={{ mx: 1 }} />;
139
+
140
+ if ((change > 0 && !isRefundMetric) || (change < 0 && isRefundMetric)) {
141
+ return <TrendingUpIcon sx={{ mx: 1, fontSize: '1em', verticalAlign: 'middle', color: 'var(--green)' }} />;
142
+ } else {
143
+ return <TrendingDownIcon sx={{ mx: 1, fontSize: '1em', verticalAlign: 'middle', color: 'var(--red)' }} />;
144
+ }
145
+ };
146
+
147
+
148
+ // Define metric cards
149
+ const metricCards = [
150
+ {
151
+ title: 'Gross Sales',
152
+ value: formatCurrency(grossRevenue),
153
+ change: revenueChange,
154
+ tooltip: hasPreviousData ? `Previous: ${formatCurrency(previousRevenue)}` : 'No prior data available',
155
+ bgcolor: 'var(--black, black)',
156
+ },
157
+ {
158
+ title: 'Orders',
159
+ value: orderCount,
160
+ change: orderCountChange,
161
+ tooltip: hasPreviousData ? `Previous: ${previousOrderCount}` : 'No prior data available',
162
+ bgcolor: 'var(--black, black)',
163
+ },
164
+ {
165
+ title: 'Avg. Order Value',
166
+ value: formatCurrency(averageOrderValue),
167
+ change: avgOrderValueChange,
168
+ tooltip: hasPreviousData ? `Previous: ${formatCurrency(previousAvgOrderValue)}` : 'No prior data available',
169
+ bgcolor: 'var(--black, black)',
170
+ },
171
+ {
172
+ title: 'Discount Rate',
173
+ value: `${discountRate.toFixed(1)}%`,
174
+ tooltip: `Total discounts: ${formatCurrency(discountTotal)}`,
175
+ bgcolor: 'var(--black, black)',
176
+ },
177
+ {
178
+ title: 'Refund Rate',
179
+ value: `${refundRate.toFixed(1)}%`,
180
+ change: refundChange,
181
+ tooltip: `Total refunds: ${formatCurrency(refundTotal)}`,
182
+ bgcolor: 'var(--black, black)',
183
+ },
184
+ {
185
+ title: 'Top Location',
186
+ value: topLocation !== 'Unknown' ? `${topLocationPercentage.toFixed(0)}% ${topLocation}` : 'Unknown',
187
+ change: locationChange,
188
+ tooltip: previousTopLocation ?
189
+ `Previous top location: ${previousTopLocation} (${previousTopLocationPercentage.toFixed(1)}%)` :
190
+ 'No prior data available',
191
+ bgcolor: 'var(--black, black)',
192
+ isLocationCard: true,
193
+ locationCode: topLocation,
194
+ }
195
+ ];
196
+
197
+ // Use MUI's useMediaQuery hook for responsive design
198
+ const theme = useTheme();
199
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
200
+ const isTablet = useMediaQuery(theme.breakpoints.down('md'));
201
+
202
+ // Function to render the location card value with tooltip
203
+ const renderLocationValue = (locationCode, value) => {
204
+ if (locationCode === 'Unknown' || !locationCode) {
205
+ return value;
206
+ }
207
+
208
+ // If the location code is 2 characters (standard ISO country code)
209
+ if (locationCode.length === 2) {
210
+ // Extract percentage and code parts
211
+ const parts = value.split(' ');
212
+ const percentage = parts[0];
213
+ const code = parts[1];
214
+
215
+ const countryName = getCountryName(code);
216
+
217
+ return (
218
+ <>
219
+ {percentage}{' '}
220
+ <Tooltip
221
+ title={`${code}: ${countryName}`}
222
+ placement="top"
223
+ arrow
224
+ >
225
+ <span style={{ color: "var(--white, white)", borderBottom: '2px solid rgba(255,255,255,.45)', cursor: 'help' }}>{code}</span>
226
+ </Tooltip>
227
+ </>
228
+ );
229
+ }
230
+
231
+ return value;
232
+ };
233
+
234
+ return (
235
+ <Grid
236
+ container
237
+ spacing={isMobile ? 1 : 2}
238
+ sx={{
239
+ mt: 2,
240
+ position: 'relative',
241
+ '&[data-loading="true"]': {
242
+ opacity: 0.7,
243
+ pointerEvents: 'none'
244
+ }
245
+ }}
246
+ data-disabled={loading}
247
+ data-loading={loading}
248
+ >
249
+ {/* Debug component */}
250
+ <DebugValues
251
+ totalRevenue={netRevenue}
252
+ grossRevenue={grossRevenue}
253
+ previousRevenue={previousRevenue}
254
+ revenueChange={revenueChange}
255
+ chartState={chartState}
256
+ />
257
+
258
+ {/* Period header - removing CircularProgress as we use loading attribute instead */}
259
+
260
+ {/* Metric cards */}
261
+ {metricCards.map((card, index) => (
262
+ <Grid key={`metric-${index}`} item xs={6} md={4}>
263
+ <Tooltip
264
+ title={card.tooltip}
265
+ placement="top"
266
+ arrow
267
+ >
268
+ <Paper
269
+ elevation={0}
270
+ sx={{
271
+ p: 4,
272
+ textAlign: 'center',
273
+ borderRadius: '4px',
274
+ border: '1px solid rgba(255, 255, 255, 0.12)',
275
+ backgroundColor: card.bgcolor,
276
+ '&:hover': {
277
+ boxShadow: '0 4px 8px rgba(0, 0, 0, 0.3)',
278
+ transform: 'translateY(-2px)',
279
+ transition: 'all 0.3s ease'
280
+ }
281
+ }}
282
+ >
283
+ <Box sx={{
284
+ display: 'flex',
285
+ alignItems: 'center',
286
+ justifyContent: 'center',
287
+ }}>
288
+ <Typography
289
+ variant="h6" color="rgba(255, 255, 255, 0.7)"
290
+ sx={{
291
+ whiteSpace: 'nowrap',
292
+ textOverflow: 'ellipsis',
293
+ overflow: 'hidden',
294
+ }}
295
+ >
296
+ {card.title}
297
+ </Typography>
298
+ </Box>
299
+ <Box sx={{
300
+ display: 'flex',
301
+ alignItems: 'center',
302
+ justifyContent: 'center',
303
+ }}>
304
+ <Typography
305
+ variant={isMobile ? "h6" : "h5"}
306
+ fontWeight="bold"
307
+ sx={{
308
+ fontSize: {
309
+ xs: '1.1rem',
310
+ sm: '1.3rem',
311
+ md: '1.5rem'
312
+ },
313
+ color: 'rgba(255, 255, 255, 0.95)'
314
+ }}
315
+ >
316
+ {card.isLocationCard ? renderLocationValue(card.locationCode, card.value) : card.value}
317
+ </Typography>
318
+ </Box>
319
+
320
+ <Box sx={{
321
+ display: 'flex',
322
+ alignItems: 'center',
323
+ justifyContent: 'center',
324
+ }}>
325
+ <Typography
326
+ variant="caption"
327
+ sx={{
328
+ mt: 0.5,
329
+ opacity: (card.change !== null && card.change !== undefined) ? 1 : 0.25,
330
+ color: card.change > 0 ? 'var(--green, #4caf50)' :
331
+ card.change < 0 ? 'var(--red, #f44336)' :
332
+ 'rgba(255, 255, 255, 0.6)'
333
+ }}
334
+ >
335
+ {(card.change !== null && card.change !== undefined) ? `${formatPercentage(card.change)} vs prior` : <span>&nbsp;</span>}
336
+ {card.change !== undefined && getTrendIcon(card.change)}
337
+ </Typography>
338
+ </Box>
339
+ </Paper>
340
+ </Tooltip>
341
+ </Grid>
342
+ ))}
343
+ </Grid>
344
+ );
345
+ }
@@ -0,0 +1,331 @@
1
+ // Top performers component displaying top performing typefaces and designers
2
+ import React, { useState } from 'react';
3
+ import { Grid, Typography, Box, Tooltip, Select, MenuItem, FormControl, InputLabel, Stack } from '@mui/material';
4
+ import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
5
+ import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
6
+ import SortIcon from '@mui/icons-material/Sort';
7
+ // Top performers component displaying top performing typefaces and designers
8
+ import styles from '../styles/sales-portal.module.scss';
9
+
10
+ /**
11
+ * Top performers section showing top typefaces and designers with sortable data
12
+ */
13
+ export default function TopPerformers({
14
+ typefaceData,
15
+ designersData,
16
+ total,
17
+ chartState,
18
+ sales,
19
+ loading,
20
+ admin
21
+ }) {
22
+ // State for sorting typefaces
23
+ const [typefaceSortKey, setTypefaceSortKey] = useState('percentage'); // 'percentage', 'orders', 'title'
24
+ const [typefaceSortDirection, setTypefaceSortDirection] = useState('desc'); // 'asc', 'desc'
25
+
26
+ // State for sorting designers
27
+ const [designerSortKey, setDesignerSortKey] = useState('percentage'); // 'percentage', 'orders', 'name'
28
+ const [designerSortDirection, setDesignerSortDirection] = useState('desc'); // 'asc', 'desc'
29
+
30
+ // Sort typefaces based on the current sort config
31
+ const sortedTypefaces = React.useMemo(() => {
32
+ if (!typefaceData) return [];
33
+
34
+ const sortableData = [...typefaceData];
35
+
36
+ return sortableData.sort((a, b) => {
37
+ if (typefaceSortKey === 'percentage') {
38
+ // Sort by percentage of total sales
39
+ const valueA = a.total / total;
40
+ const valueB = b.total / total;
41
+ return typefaceSortDirection === 'asc' ? valueA - valueB : valueB - valueA;
42
+ }
43
+ else if (typefaceSortKey === 'orders') {
44
+ // Sort by order count
45
+ return typefaceSortDirection === 'asc' ? a.orders - b.orders : b.orders - a.orders;
46
+ }
47
+ else {
48
+ // Sort by title alphabetically
49
+ return typefaceSortDirection === 'asc'
50
+ ? a.title.localeCompare(b.title)
51
+ : b.title.localeCompare(a.title);
52
+ }
53
+ }).slice(0, 3); // Get top 3
54
+ }, [typefaceData, typefaceSortKey, typefaceSortDirection, total]);
55
+
56
+ // Sort designers based on the current sort config
57
+ const sortedDesigners = React.useMemo(() => {
58
+ if (!designersData) return [];
59
+
60
+ const sortableData = [...designersData];
61
+
62
+ return sortableData.sort((a, b) => {
63
+ if (designerSortKey === 'percentage') {
64
+ // Sort by percentage of total sales
65
+ const valueA = a.total / total;
66
+ const valueB = b.total / total;
67
+ return designerSortDirection === 'asc' ? valueA - valueB : valueB - valueA;
68
+ }
69
+ else if (designerSortKey === 'orders') {
70
+ // Sort by order count
71
+ return designerSortDirection === 'asc' ? a.orders - b.orders : b.orders - a.orders;
72
+ }
73
+ else {
74
+ // Sort by name alphabetically
75
+ const nameA = a?.firstName && a?.lastName ? `${a.firstName} ${a.lastName}` : a?._id || '';
76
+ const nameB = b?.firstName && b?.lastName ? `${b.firstName} ${b.lastName}` : b?._id || '';
77
+ return designerSortDirection === 'asc'
78
+ ? nameA.localeCompare(nameB)
79
+ : nameB.localeCompare(nameA);
80
+ }
81
+ }).slice(0, 3); // Get top 3
82
+ }, [designersData, designerSortKey, designerSortDirection, total]);
83
+
84
+ // Toggle sort direction
85
+ const toggleSortDirection = (currentType) => {
86
+ if (currentType === 'typeface') {
87
+ setTypefaceSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
88
+ } else {
89
+ setDesignerSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
90
+ }
91
+ };
92
+ return (
93
+ <>
94
+
95
+ {/* Top Performing Designers */}
96
+ <Grid container className='designers-section'>
97
+ <Grid
98
+ item
99
+ xs={12}
100
+ data-disabled={loading}
101
+ data-loading={loading}
102
+ sx={{
103
+ margin: "20px 0 10px",
104
+ borderRadius: "4px",
105
+ flex: "none",
106
+ opacity: !!sales.length ? 1 : 0.25,
107
+ pointerEvents: !!sales.length ? "" : "none",
108
+ border: "1px solid var(--black, black)",
109
+ '&:hover': { boxShadow: "0 0 8px rgba(0,0,0,0.25)" },
110
+ }}
111
+ >
112
+ <Box sx={{
113
+ background: "rgba(var(--blackRGB, 0,0,0), 1)",
114
+ color: "var(--white, white)",
115
+ padding: "5px 10px 10px",
116
+ display: "flex",
117
+ justifyContent: "space-between",
118
+ alignItems: "center"
119
+ }}>
120
+ <Typography variant='h5' sx={{color: "var(--white, white)"}}>
121
+ <strong>Designers</strong>
122
+ </Typography>
123
+ <Stack direction="row" spacing={2} alignItems="center">
124
+
125
+ <FormControl variant="filled" size="small" sx={{
126
+ minWidth: 120,
127
+ '& .MuiFilledInput-root': { color: "var(--white, white)" },
128
+ '& .MuiFormLabel-root': { color: 'rgba(255,255,255,0.7)' },
129
+ '& .MuiSelect-icon': { display: 'none' }
130
+ }}>
131
+ <InputLabel id="designer-sort-label">Sort by</InputLabel>
132
+ <Select
133
+ labelId="designer-sort-label"
134
+ value={designerSortKey}
135
+ onChange={(e) => setDesignerSortKey(e.target.value)}
136
+ label="Sort by"
137
+ >
138
+ <MenuItem value="percentage">Percentage</MenuItem>
139
+ <MenuItem value="orders">Orders</MenuItem>
140
+ <MenuItem value="name">Name</MenuItem>
141
+ </Select>
142
+ </FormControl>
143
+ <Tooltip
144
+ title={`Sort ${designerSortDirection === 'asc' ? 'Descending' : 'Ascending'}`}
145
+ >
146
+ <Box
147
+ onClick={() => toggleSortDirection('designer')}
148
+ sx={{
149
+ cursor: 'pointer',
150
+ display: { xs: 'none', sm: 'flex' },
151
+ alignItems: 'center',
152
+ justifyContent: 'center',
153
+ padding: '6px 10px',
154
+ color: "var(--white, white)",
155
+ }}
156
+ >
157
+ {designerSortDirection === 'asc' ? <ArrowUpwardIcon /> : <ArrowDownwardIcon />}
158
+ </Box>
159
+ </Tooltip>
160
+ </Stack>
161
+ </Box>
162
+ <Box sx={{
163
+ background: "rgba(var(--blackRGB, 0,0,0), 0.06)",
164
+ padding: "10px 10px 20px",
165
+ }}>
166
+ {
167
+ sortedDesigners.map((designer, i) => {
168
+ return (
169
+ <Box
170
+ key={`designer-total-${i}`}
171
+ sx={{
172
+ paddingBottom: "10px",
173
+ }}
174
+ >
175
+ <Typography
176
+ variant='h5'
177
+ sx={{
178
+ whiteSpace: "nowrap",
179
+ overflow: "hidden",
180
+ textOverflow: "ellipsis",
181
+ paddingBottom: "0.5em"
182
+ }}>
183
+ {designer?.firstName && designer?.lastName ?
184
+ `${designer.firstName} ${designer.lastName}`
185
+ :
186
+ designer?._id
187
+ }
188
+ <span className={styles.earningContainer}>
189
+ <strong> {(designer.total / (total)).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</strong>%
190
+ </span>
191
+ <span style={{ opacity: "0.5", textTransform: "none" }}> ({designer.orders} Orders for a total of </span>
192
+ <span style={{ opacity: "0.5" }}> USD</span>$<strong>{(designer.total / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}</strong>
193
+ <span style={{ opacity: "0.5", textTransform: "none" }}>)</span>
194
+ </Typography>
195
+
196
+ {/* Individual typefaces for this designer */}
197
+ {typefaceData
198
+ ?.filter(typeface => typeface.author._id === designer._id)
199
+ .map((typeface, j) => (
200
+ <Typography
201
+ key={`designer-typeface-${i}-${j}`}
202
+ variant='h6'
203
+ sx={{
204
+ whiteSpace: "nowrap",
205
+ overflow: "hidden",
206
+ textOverflow: "ellipsis",
207
+ }}>
208
+ {typeface.title}
209
+ <span className={styles.earningContainer}>
210
+ <strong> {(typeface.total / (total + chartState.refundTotal)).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</strong>%
211
+ </span>
212
+ <span style={{ opacity: "0.5", textTransform: "none" }}> ({typeface.orders} Orders for a total of </span>
213
+ <span style={{ opacity: "0.5" }}> USD</span>$<strong>{(typeface.total / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}</strong>
214
+ <span style={{ opacity: "0.5", textTransform: "none" }}>)</span>
215
+ </Typography>
216
+ ))}
217
+ </Box>
218
+ )
219
+ })
220
+ }
221
+ </Box>
222
+ </Grid>
223
+ </Grid>
224
+
225
+
226
+ {/* Top Performing products */}
227
+ <Grid container className='top-performers-section'>
228
+ <Grid
229
+ item
230
+ xs={12}
231
+ data-disabled={loading}
232
+ data-loading={loading}
233
+ sx={{
234
+ margin: "20px 0 10px",
235
+ borderRadius: "4px",
236
+ flex: "none",
237
+ opacity: !!sales.length ? 1 : 0.25,
238
+ pointerEvents: !!sales.length ? "" : "none",
239
+ border: "1px solid var(--black, black)",
240
+
241
+ '&:hover': { boxShadow: "0 0 8px rgba(0,0,0,0.25)" },
242
+ }}
243
+ >
244
+ <Box sx={{
245
+ background: "rgba(var(--blackRGB, 0,0,0), 1)",
246
+ color: "var(--white, white)",
247
+ padding: "5px 10px 10px",
248
+ display: "flex",
249
+ justifyContent: "space-between",
250
+ alignItems: "center"
251
+ }}>
252
+ <Typography variant='h5'>
253
+ <strong>Top Performers</strong>
254
+ </Typography>
255
+ <Stack direction="row" spacing={2} alignItems="center">
256
+
257
+ <FormControl
258
+ variant="filled"
259
+ size="small"
260
+ sx={{
261
+ minWidth: 120,
262
+ '& .MuiFilledInput-root': { color: "var(--white, white)" },
263
+ '& .MuiFormLabel-root': { color: 'rgba(255,255,255,0.7)' },
264
+ '& .MuiSelect-icon': { display: 'none' },
265
+ }}>
266
+ <InputLabel id="typeface-sort-label">Sort by</InputLabel>
267
+ <Select
268
+ labelId="typeface-sort-label"
269
+ value={typefaceSortKey}
270
+ onChange={(e) => setTypefaceSortKey(e.target.value)}
271
+ label="Sort by"
272
+
273
+ >
274
+ <MenuItem value="percentage">Percentage</MenuItem>
275
+ <MenuItem value="orders">Orders</MenuItem>
276
+ <MenuItem value="title">Name</MenuItem>
277
+ </Select>
278
+ </FormControl>
279
+ <Tooltip
280
+ title={`Sort ${typefaceSortDirection === 'asc' ? 'Descending' : 'Ascending'}`}
281
+ >
282
+ <Box
283
+ onClick={() => toggleSortDirection('typeface')}
284
+ sx={{
285
+ cursor: 'pointer',
286
+ display: { xs: 'none', sm: 'flex' },
287
+ alignItems: 'center',
288
+ justifyContent: 'center',
289
+ padding: '6px 10px',
290
+ color: "var(--white, white)",
291
+ }}
292
+ >
293
+ {typefaceSortDirection === 'asc' ? <ArrowUpwardIcon /> : <ArrowDownwardIcon />}
294
+ </Box>
295
+ </Tooltip>
296
+ </Stack>
297
+ </Box>
298
+ <Box sx={{
299
+ background: "rgba(var(--blackRGB, 0,0,0), 0.06)",
300
+ padding: "10px"
301
+ }}>
302
+ {
303
+ sortedTypefaces.map((typeface, i) => {
304
+ return (
305
+ <Typography
306
+ key={`typeface-total-${i}`}
307
+ variant='h5'
308
+ sx={{
309
+ whiteSpace: "nowrap",
310
+ overflow: "hidden",
311
+ textOverflow: "ellipsis",
312
+ paddingBottom: "0.5em"
313
+ }}>
314
+ {typeface.title}
315
+ <span className={styles.earningContainer}>
316
+ <strong> {(typeface.total / (total)).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</strong>%
317
+ </span>
318
+ <span style={{ opacity: "0.5", textTransform: "none" }}> ({typeface.orders} Orders for a total of </span>
319
+ <span style={{ opacity: "0.5" }}> USD</span>$<strong>{(typeface.total / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}</strong>
320
+ <span style={{ opacity: "0.5", textTransform: "none" }}>)</span>
321
+ </Typography>
322
+ )
323
+ })
324
+ }
325
+ </Box>
326
+ </Grid>
327
+ </Grid>
328
+
329
+ </>
330
+ );
331
+ }