@liiift-studio/sales-portal 3.1.0 → 3.1.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/components/DateRangeSalesTable.js +8 -6
- package/components/{SummaryCards.js → Insights.js} +49 -64
- package/components/LicenseTypeList.js +12 -9
- package/components/LoginForm.js +3 -3
- package/components/Sales.js +50 -29
- package/components/SalesTable.js +3 -1
- package/components/TopPerformers.js +23 -17
- package/components/TypefaceList.js +12 -9
- package/components/YearOverview.js +4 -4
- package/hooks/useReconciliation.js +5 -3
- package/index.js +1 -1
- package/package.json +1 -1
- package/styles/sales-portal.module.scss +18 -11
|
@@ -29,7 +29,7 @@ dayjs.extend(utc);
|
|
|
29
29
|
import styles from '../styles/sales-portal.module.scss';
|
|
30
30
|
import { getCellValue } from './table-row-cells';
|
|
31
31
|
import { COLUMNS } from './table-columns';
|
|
32
|
-
import
|
|
32
|
+
import Insights from './Insights';
|
|
33
33
|
import TopPerformers from './TopPerformers';
|
|
34
34
|
import TypefaceList from './TypefaceList';
|
|
35
35
|
import LicenseTypeList from './LicenseTypeList';
|
|
@@ -379,7 +379,7 @@ export function DateRangeSalesTable({ designer, admin, loading, updateLoadingSta
|
|
|
379
379
|
</Box>
|
|
380
380
|
|
|
381
381
|
{/* Date Range Pickers */}
|
|
382
|
-
<Box sx={{ width: { xs: '100%', sm: '50%', md: '33.33%' }, display: 'flex', gap: 2, mb: 8, justifyContent: 'flex-end', alignItems: 'center' }}>
|
|
382
|
+
<Box sx={{ width: { xs: '100%', sm: '50%', md: '33.33%' }, display: 'flex', flexWrap: 'wrap', gap: { xs: 1, sm: 2 }, mb: { xs: 4, sm: 8 }, justifyContent: { xs: 'flex-start', sm: 'flex-end' }, alignItems: 'center' }}>
|
|
383
383
|
{!startDate && !endDate && (
|
|
384
384
|
<>
|
|
385
385
|
<Button
|
|
@@ -507,9 +507,11 @@ export function DateRangeSalesTable({ designer, admin, loading, updateLoadingSta
|
|
|
507
507
|
<Box sx={{ width: '100%', mb: 2 }}>
|
|
508
508
|
<Box sx={{
|
|
509
509
|
display: 'inline-block',
|
|
510
|
-
p: 2,
|
|
511
|
-
ml: -2,
|
|
510
|
+
p: { xs: 1.5, sm: 2 },
|
|
511
|
+
ml: { xs: 0, sm: -2 },
|
|
512
512
|
borderRadius: '4px',
|
|
513
|
+
maxWidth: '100%',
|
|
514
|
+
boxSizing: 'border-box',
|
|
513
515
|
border: reconciliationData.isReconciled
|
|
514
516
|
? '2px solid var(--green, green)'
|
|
515
517
|
: '2px solid var(--red, red)',
|
|
@@ -618,8 +620,8 @@ export function DateRangeSalesTable({ designer, admin, loading, updateLoadingSta
|
|
|
618
620
|
{/* Summary Dashboard and Analysis Components - only show when data is loaded */}
|
|
619
621
|
{!!dateRangeSales.length && (
|
|
620
622
|
<>
|
|
621
|
-
{/*
|
|
622
|
-
<
|
|
623
|
+
{/* Insights */}
|
|
624
|
+
<Insights
|
|
623
625
|
sales={dateRangeSales}
|
|
624
626
|
previousSales={[]} // No previous data in date range view
|
|
625
627
|
loading={loading}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Insights cards displaying key sales metrics at a glance with country code tooltips
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import {
|
|
4
4
|
Box,
|
|
@@ -33,7 +33,7 @@ const getCountryName = (code) => {
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* Insights component showing key sales metrics with period-over-period comparison
|
|
37
37
|
* @param {Object} props - Component props
|
|
38
38
|
* @param {Array} props.sales - Current period sales data
|
|
39
39
|
* @param {Array} props.previousSales - Previous period sales data for comparison
|
|
@@ -41,9 +41,9 @@ const getCountryName = (code) => {
|
|
|
41
41
|
* @param {Object} props.chartState - Chart data state
|
|
42
42
|
* @param {Date} props.date - Current date being displayed
|
|
43
43
|
* @param {Object} props.locationData - Data about sales by location
|
|
44
|
-
* @returns {JSX.Element}
|
|
44
|
+
* @returns {JSX.Element} Insights component
|
|
45
45
|
*/
|
|
46
|
-
export default function
|
|
46
|
+
export default function Insights({
|
|
47
47
|
sales = [],
|
|
48
48
|
previousSales = [],
|
|
49
49
|
loading = false,
|
|
@@ -151,34 +151,34 @@ export default function SummaryCards({
|
|
|
151
151
|
value: formatCurrency(grossRevenue),
|
|
152
152
|
change: revenueChange,
|
|
153
153
|
tooltip: hasPreviousData ? `Previous: ${formatCurrency(previousRevenue)}` : 'No prior data available',
|
|
154
|
-
bgcolor: '
|
|
154
|
+
bgcolor: '#000000',
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
157
|
title: 'Orders',
|
|
158
158
|
value: orderCount,
|
|
159
159
|
change: orderCountChange,
|
|
160
160
|
tooltip: hasPreviousData ? `Previous: ${previousOrderCount}` : 'No prior data available',
|
|
161
|
-
bgcolor: '
|
|
161
|
+
bgcolor: '#000000',
|
|
162
162
|
},
|
|
163
163
|
{
|
|
164
164
|
title: 'Avg. Order Value',
|
|
165
165
|
value: formatCurrency(averageOrderValue),
|
|
166
166
|
change: avgOrderValueChange,
|
|
167
167
|
tooltip: hasPreviousData ? `Previous: ${formatCurrency(previousAvgOrderValue)}` : 'No prior data available',
|
|
168
|
-
bgcolor: '
|
|
168
|
+
bgcolor: '#000000',
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
title: 'Discount Rate',
|
|
172
172
|
value: `${discountRate.toFixed(1)}%`,
|
|
173
173
|
tooltip: `Total discounts: ${formatCurrency(discountTotal)}`,
|
|
174
|
-
bgcolor: '
|
|
174
|
+
bgcolor: '#000000',
|
|
175
175
|
},
|
|
176
176
|
{
|
|
177
177
|
title: 'Refund Rate',
|
|
178
178
|
value: `${refundRate.toFixed(1)}%`,
|
|
179
179
|
change: refundChange,
|
|
180
180
|
tooltip: `Total refunds: ${formatCurrency(refundTotal)}`,
|
|
181
|
-
bgcolor: '
|
|
181
|
+
bgcolor: '#000000',
|
|
182
182
|
},
|
|
183
183
|
{
|
|
184
184
|
title: 'Top Location',
|
|
@@ -187,7 +187,7 @@ export default function SummaryCards({
|
|
|
187
187
|
tooltip: previousTopLocation ?
|
|
188
188
|
`Previous top location: ${previousTopLocation} (${previousTopLocationPercentage.toFixed(1)}%)` :
|
|
189
189
|
'No prior data available',
|
|
190
|
-
bgcolor: '
|
|
190
|
+
bgcolor: '#000000',
|
|
191
191
|
isLocationCard: true,
|
|
192
192
|
locationCode: topLocation,
|
|
193
193
|
}
|
|
@@ -221,7 +221,7 @@ export default function SummaryCards({
|
|
|
221
221
|
placement="top"
|
|
222
222
|
arrow
|
|
223
223
|
>
|
|
224
|
-
<span style={{ color: "var(--white, white)", borderBottom: '2px solid rgba(255,255,255
|
|
224
|
+
<span style={{ color: "var(--white, white)", borderBottom: '2px solid rgba(var(--whiteRGB, 255, 255, 255), .45)', cursor: 'help' }}>{code}</span>
|
|
225
225
|
</Tooltip>
|
|
226
226
|
</>
|
|
227
227
|
);
|
|
@@ -235,7 +235,7 @@ export default function SummaryCards({
|
|
|
235
235
|
sx={{
|
|
236
236
|
display: 'flex',
|
|
237
237
|
flexWrap: 'wrap',
|
|
238
|
-
gap: isMobile ? 1 : 2,
|
|
238
|
+
gap: isMobile ? 1.5 : 2,
|
|
239
239
|
mt: 2,
|
|
240
240
|
position: 'relative',
|
|
241
241
|
'&[data-loading="true"]': {
|
|
@@ -259,77 +259,62 @@ export default function SummaryCards({
|
|
|
259
259
|
|
|
260
260
|
{/* Metric cards */}
|
|
261
261
|
{metricCards.map((card, index) => (
|
|
262
|
-
<Box key={`metric-${index}`} sx={{ width: { xs: 'calc(50% - 8px)', md: 'calc(33.33% - 11px)' } }}>
|
|
262
|
+
<Box key={`metric-${index}`} sx={{ width: { xs: '100%', sm: 'calc(50% - 8px)', md: 'calc(33.33% - 11px)' } }}>
|
|
263
263
|
<Tooltip
|
|
264
264
|
title={card.tooltip}
|
|
265
265
|
placement="top"
|
|
266
266
|
arrow
|
|
267
267
|
>
|
|
268
|
-
<Paper
|
|
269
|
-
elevation={0}
|
|
270
|
-
sx={{
|
|
271
|
-
p: 4,
|
|
272
|
-
textAlign: 'center',
|
|
268
|
+
<Paper
|
|
269
|
+
elevation={0}
|
|
270
|
+
sx={{
|
|
271
|
+
p: { xs: 1.5, sm: 4 },
|
|
272
|
+
textAlign: { xs: 'left', sm: 'center' },
|
|
273
273
|
borderRadius: '4px',
|
|
274
|
-
border: '1px solid rgba(255, 255, 255, 0.12)',
|
|
274
|
+
border: '1px solid rgba(var(--whiteRGB, 255, 255, 255), 0.12)',
|
|
275
275
|
backgroundColor: card.bgcolor,
|
|
276
|
+
display: { xs: 'flex', sm: 'block' },
|
|
277
|
+
alignItems: 'center',
|
|
278
|
+
justifyContent: 'space-between',
|
|
279
|
+
gap: 1,
|
|
276
280
|
'&:hover': {
|
|
277
|
-
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.3)',
|
|
278
|
-
transform: 'translateY(-2px)',
|
|
281
|
+
boxShadow: '0 4px 8px rgba(var(--blackRGB, 0, 0, 0), 0.3)',
|
|
282
|
+
transform: { sm: 'translateY(-2px)' },
|
|
279
283
|
transition: 'all 0.3s ease'
|
|
280
284
|
}
|
|
281
285
|
}}
|
|
282
286
|
>
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
</Typography>
|
|
298
|
-
</Box>
|
|
299
|
-
<Box sx={{
|
|
300
|
-
display: 'flex',
|
|
301
|
-
alignItems: 'center',
|
|
302
|
-
justifyContent: 'center',
|
|
303
|
-
}}>
|
|
304
|
-
<Typography
|
|
305
|
-
variant={isMobile ? "h6" : "h5"}
|
|
287
|
+
<Typography
|
|
288
|
+
variant="body2" color="rgba(var(--whiteRGB, 255, 255, 255), 0.7)"
|
|
289
|
+
sx={{
|
|
290
|
+
whiteSpace: 'nowrap',
|
|
291
|
+
textOverflow: 'ellipsis',
|
|
292
|
+
overflow: 'hidden',
|
|
293
|
+
fontSize: { xs: '0.8rem', sm: '1rem' },
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
{card.title}
|
|
297
|
+
</Typography>
|
|
298
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, justifyContent: { xs: 'flex-end', sm: 'center' } }}>
|
|
299
|
+
<Typography
|
|
300
|
+
variant="body1"
|
|
306
301
|
fontWeight="bold"
|
|
307
302
|
sx={{
|
|
308
|
-
fontSize: {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
md: '1.5rem'
|
|
312
|
-
},
|
|
313
|
-
color: 'rgba(255, 255, 255, 0.95)'
|
|
303
|
+
fontSize: { xs: '0.95rem', sm: '1.3rem', md: '1.5rem' },
|
|
304
|
+
color: 'rgba(var(--whiteRGB, 255, 255, 255), 0.95)',
|
|
305
|
+
whiteSpace: 'nowrap',
|
|
314
306
|
}}
|
|
315
307
|
>
|
|
316
308
|
{card.isLocationCard ? renderLocationValue(card.locationCode, card.value) : card.value}
|
|
317
309
|
</Typography>
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
alignItems: 'center',
|
|
323
|
-
justifyContent: 'center',
|
|
324
|
-
}}>
|
|
325
|
-
<Typography
|
|
326
|
-
variant="caption"
|
|
327
|
-
sx={{
|
|
328
|
-
mt: 0.5,
|
|
310
|
+
<Typography
|
|
311
|
+
variant="caption"
|
|
312
|
+
sx={{
|
|
313
|
+
display: { xs: 'none', sm: 'block' },
|
|
329
314
|
opacity: (card.change !== null && card.change !== undefined) ? 1 : 0.25,
|
|
330
|
-
color: card.change > 0 ? 'var(--green, #4caf50)' :
|
|
331
|
-
|
|
332
|
-
|
|
315
|
+
color: card.change > 0 ? 'var(--green, #4caf50)' :
|
|
316
|
+
card.change < 0 ? 'var(--red, #f44336)' :
|
|
317
|
+
'rgba(var(--whiteRGB, 255, 255, 255), 0.6)'
|
|
333
318
|
}}
|
|
334
319
|
>
|
|
335
320
|
{(card.change !== null && card.change !== undefined) ? `${formatPercentage(card.change)} vs prior` : <span> </span>}
|
|
@@ -76,17 +76,19 @@ export default function LicenseTypeList({ licenseTypeData, sales, loading, admin
|
|
|
76
76
|
<Box sx={{
|
|
77
77
|
background: "rgba(var(--blackRGB, 0,0,0), 1)",
|
|
78
78
|
color: "var(--white, white)",
|
|
79
|
-
padding: "5px 10px 10px",
|
|
79
|
+
padding: { xs: "8px 12px", sm: "5px 10px 10px" },
|
|
80
80
|
display: "flex",
|
|
81
81
|
justifyContent: "space-between",
|
|
82
|
-
alignItems: "center"
|
|
82
|
+
alignItems: "center",
|
|
83
|
+
flexWrap: { xs: 'wrap', sm: 'nowrap' },
|
|
84
|
+
gap: 1,
|
|
83
85
|
}}>
|
|
84
|
-
<Typography variant='h5'>
|
|
86
|
+
<Typography variant='h5' sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }}>
|
|
85
87
|
<strong>License Type</strong>
|
|
86
88
|
</Typography>
|
|
87
|
-
<Stack direction="row" spacing={
|
|
88
|
-
<FormControl variant="filled" size="small" sx={{
|
|
89
|
-
minWidth: 120,
|
|
89
|
+
<Stack direction="row" spacing={1} alignItems="center">
|
|
90
|
+
<FormControl variant="filled" size="small" sx={{
|
|
91
|
+
minWidth: { xs: 90, sm: 120 },
|
|
90
92
|
'& .MuiFilledInput-root': { color: "var(--white, white)"},
|
|
91
93
|
'& .MuiFormLabel-root': { color: 'rgba(255,255,255,0.7)' },
|
|
92
94
|
'& .MuiSelect-icon': { display: 'none' },
|
|
@@ -137,10 +139,11 @@ export default function LicenseTypeList({ licenseTypeData, sales, loading, admin
|
|
|
137
139
|
key={`license-total-${i}`}
|
|
138
140
|
variant='h5'
|
|
139
141
|
sx={{
|
|
140
|
-
color: 'var(--black, black)',
|
|
141
|
-
whiteSpace: "nowrap",
|
|
142
|
+
color: 'var(--black, black)',
|
|
143
|
+
whiteSpace: { xs: "normal", md: "nowrap" },
|
|
142
144
|
overflow: "hidden",
|
|
143
|
-
textOverflow: "ellipsis",
|
|
145
|
+
textOverflow: "ellipsis",
|
|
146
|
+
fontSize: { xs: '0.85rem', sm: '1rem', md: '1.25rem' },
|
|
144
147
|
paddingTop: licenseTypeData[i-1] && license.name[0] !== licenseTypeData[i-1].name[0] ? "0.5em" : "",
|
|
145
148
|
}}
|
|
146
149
|
>
|
package/components/LoginForm.js
CHANGED
|
@@ -104,7 +104,7 @@ export default function LoginForm({
|
|
|
104
104
|
<Box sx={{
|
|
105
105
|
width: '100%',
|
|
106
106
|
maxWidth: '860px',
|
|
107
|
-
borderRadius: '12px',
|
|
107
|
+
borderRadius: { xs: 0, sm: '12px' },
|
|
108
108
|
overflow: 'hidden',
|
|
109
109
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.12)',
|
|
110
110
|
bgcolor: 'var(--white, white)',
|
|
@@ -207,9 +207,9 @@ export default function LoginForm({
|
|
|
207
207
|
justifyContent: 'center',
|
|
208
208
|
alignItems: 'center',
|
|
209
209
|
bgcolor: 'var(--green)',
|
|
210
|
-
minHeight: '64px',
|
|
210
|
+
minHeight: { xs: '52px', sm: '64px' },
|
|
211
211
|
maxWidth: 'initial',
|
|
212
|
-
borderRadius: '0 0 0 12px',
|
|
212
|
+
borderRadius: { xs: '0 0 0 0', sm: '0 0 0 12px' },
|
|
213
213
|
opacity: 1,
|
|
214
214
|
flexShrink: 0,
|
|
215
215
|
'&:hover': {
|
package/components/Sales.js
CHANGED
|
@@ -22,7 +22,7 @@ import YearOverview from './YearOverview';
|
|
|
22
22
|
import SalesChart from './SalesChart';
|
|
23
23
|
import TopPerformers from './TopPerformers';
|
|
24
24
|
import TypefaceList from './TypefaceList';
|
|
25
|
-
import
|
|
25
|
+
import Insights from './Insights';
|
|
26
26
|
import LicenseTypeList from './LicenseTypeList';
|
|
27
27
|
|
|
28
28
|
dayjs.extend(utc);
|
|
@@ -216,8 +216,11 @@ export default function Sales(props) {
|
|
|
216
216
|
setMessage('Error retrieving sales data');
|
|
217
217
|
}
|
|
218
218
|
} catch (err) {
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
// Ignore aborted requests (e.g. from component unmount during resize)
|
|
220
|
+
if (err.name !== 'AbortError' && err.message !== 'Failed to fetch') {
|
|
221
|
+
setMessage('Error retrieving sales data');
|
|
222
|
+
console.error(err);
|
|
223
|
+
}
|
|
221
224
|
} finally {
|
|
222
225
|
updateLoadingState('salesData', false);
|
|
223
226
|
}
|
|
@@ -277,6 +280,8 @@ export default function Sales(props) {
|
|
|
277
280
|
setPreviousSales([]); // Clear any stale data
|
|
278
281
|
}
|
|
279
282
|
} catch (err) {
|
|
283
|
+
// Ignore aborted requests (e.g. from component unmount during resize)
|
|
284
|
+
if (err.name === 'AbortError' || err.message === 'Failed to fetch') return;
|
|
280
285
|
const errorMsg = `Error retrieving previous sales data: ${err.message}`;
|
|
281
286
|
console.error(errorMsg, err);
|
|
282
287
|
setPreviousSalesError(errorMsg);
|
|
@@ -534,7 +539,7 @@ export default function Sales(props) {
|
|
|
534
539
|
{/* Header Section */}
|
|
535
540
|
<Box data-disabled={loading} data-loading={loading}
|
|
536
541
|
sx={{
|
|
537
|
-
width: { xs: '
|
|
542
|
+
width: { xs: '100%', sm: '66.67%' },
|
|
538
543
|
".show-hover": { display: "none" },
|
|
539
544
|
"&:hover .show-hover": { display: "inline", opacity: "0.5" },
|
|
540
545
|
}}
|
|
@@ -588,13 +593,14 @@ export default function Sales(props) {
|
|
|
588
593
|
{/* Date Picker and View Controls */}
|
|
589
594
|
<Box
|
|
590
595
|
sx={{
|
|
591
|
-
width: { xs: '
|
|
592
|
-
justifyContent:
|
|
596
|
+
width: { xs: '100%', sm: '33.33%' },
|
|
597
|
+
justifyContent: { xs: 'flex-start', sm: 'flex-end' },
|
|
593
598
|
display: "flex",
|
|
599
|
+
mt: { xs: 2, sm: 0 },
|
|
594
600
|
}}
|
|
595
601
|
data-disabled={loading}
|
|
596
602
|
>
|
|
597
|
-
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
603
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 0.5, sm: 1 }, flexWrap: { xs: 'wrap', sm: 'nowrap' }, justifyContent: { xs: 'flex-start', sm: 'flex-end' } }}>
|
|
598
604
|
<IconButton
|
|
599
605
|
onClick={() => {
|
|
600
606
|
const newDate = new Date(date);
|
|
@@ -606,7 +612,7 @@ export default function Sales(props) {
|
|
|
606
612
|
setDate(newDate);
|
|
607
613
|
}}
|
|
608
614
|
size="small"
|
|
609
|
-
sx={{ display: { xs: 'none',
|
|
615
|
+
sx={{ minWidth: '44px', minHeight: '44px', display: { xs: 'none', sm: 'flex' } }}
|
|
610
616
|
>
|
|
611
617
|
<ChevronLeftIcon />
|
|
612
618
|
</IconButton>
|
|
@@ -615,8 +621,8 @@ export default function Sales(props) {
|
|
|
615
621
|
views={viewMode === 'year' ? ['year'] : ['month', 'year']}
|
|
616
622
|
format={viewMode === 'year' ? 'YYYY' : 'MMM YYYY'}
|
|
617
623
|
formatDensity="dense"
|
|
618
|
-
slotProps={{ textField: { variant: "filled" } }}
|
|
619
|
-
sx={{ "& *": { borderRadius: "4px" } }}
|
|
624
|
+
slotProps={{ textField: { variant: "filled", size: "small" } }}
|
|
625
|
+
sx={{ "& *": { borderRadius: "4px" }, maxWidth: { xs: '160px', sm: 'none' }, flex: { xs: 1, sm: 'none' } }}
|
|
620
626
|
value={dayjs.utc(date)}
|
|
621
627
|
onChange={(newValue) => setDate(newValue.toDate())}
|
|
622
628
|
/>
|
|
@@ -631,15 +637,15 @@ export default function Sales(props) {
|
|
|
631
637
|
setDate(newDate);
|
|
632
638
|
}}
|
|
633
639
|
size="small"
|
|
634
|
-
sx={{ display: { xs: 'none',
|
|
640
|
+
sx={{ minWidth: '44px', minHeight: '44px', display: { xs: 'none', sm: 'flex' } }}
|
|
635
641
|
>
|
|
636
642
|
<ChevronRightIcon />
|
|
637
643
|
</IconButton>
|
|
638
644
|
{!!sales.length && (
|
|
639
|
-
<Tooltip
|
|
645
|
+
<Tooltip
|
|
640
646
|
title="Print to PDF"
|
|
641
|
-
sx={{ display: { xs: 'none', md: 'inherit' } }}
|
|
642
647
|
>
|
|
648
|
+
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
|
|
643
649
|
<IconButton
|
|
644
650
|
className='print-button'
|
|
645
651
|
onClick={() => {
|
|
@@ -658,36 +664,50 @@ export default function Sales(props) {
|
|
|
658
664
|
size: letter portrait;
|
|
659
665
|
margin: 0.5in;
|
|
660
666
|
}
|
|
661
|
-
|
|
667
|
+
/* Hide everything except this sales portal */
|
|
668
|
+
header, nav, footer, #navBar, #footer, #titleContainer,
|
|
669
|
+
.exportSection, .date-range-sales-table-wrapper,
|
|
662
670
|
.salesPortal:not(:nth-child(${portalIndex})) {
|
|
663
|
-
display: none!important;
|
|
671
|
+
display: none !important;
|
|
664
672
|
}
|
|
665
|
-
.salesPortal, .salesPortalWrap{
|
|
666
|
-
background: white;
|
|
673
|
+
.salesPortal, .salesPortalWrap {
|
|
674
|
+
background: white !important;
|
|
667
675
|
}
|
|
668
676
|
body {
|
|
669
677
|
-webkit-print-color-adjust: exact !important;
|
|
670
678
|
print-color-adjust: exact !important;
|
|
671
679
|
}
|
|
680
|
+
/* Clean up layout */
|
|
672
681
|
.salesPortal {
|
|
673
682
|
padding: 0 !important;
|
|
683
|
+
width: 100% !important;
|
|
674
684
|
}
|
|
675
685
|
.salesPortal > * {
|
|
676
686
|
page-break-inside: avoid;
|
|
677
687
|
}
|
|
678
|
-
/* Hide
|
|
679
|
-
button, .MuiIconButton-root, .
|
|
688
|
+
/* Hide interactive elements */
|
|
689
|
+
button, .MuiIconButton-root, .MuiInput-root,
|
|
690
|
+
.MuiDateCalendar-root, .print-button,
|
|
691
|
+
.year-overview-wrapper, .sales-chart-wrapper {
|
|
680
692
|
display: none !important;
|
|
681
693
|
}
|
|
682
|
-
/*
|
|
683
|
-
.
|
|
694
|
+
/* Full width sections */
|
|
695
|
+
.sales-header-section, .sales-data-section,
|
|
696
|
+
.insights-wrapper, .sales-table-wrapper {
|
|
684
697
|
width: 100% !important;
|
|
685
|
-
|
|
698
|
+
padding: 0 !important;
|
|
699
|
+
margin: 10px 0 !important;
|
|
686
700
|
}
|
|
687
|
-
/*
|
|
701
|
+
/* Readable text */
|
|
688
702
|
.MuiTypography-root {
|
|
689
703
|
color: black !important;
|
|
690
704
|
}
|
|
705
|
+
/* Compact cards for print */
|
|
706
|
+
.MuiPaper-root {
|
|
707
|
+
box-shadow: none !important;
|
|
708
|
+
border: 1px solid #ddd !important;
|
|
709
|
+
page-break-inside: avoid;
|
|
710
|
+
}
|
|
691
711
|
}
|
|
692
712
|
`;
|
|
693
713
|
document.head.appendChild(style);
|
|
@@ -707,6 +727,7 @@ export default function Sales(props) {
|
|
|
707
727
|
>
|
|
708
728
|
<PrintIcon />
|
|
709
729
|
</IconButton>
|
|
730
|
+
</Box>
|
|
710
731
|
</Tooltip>
|
|
711
732
|
)}
|
|
712
733
|
{/* Month/Year toggle */}
|
|
@@ -722,10 +743,10 @@ export default function Sales(props) {
|
|
|
722
743
|
key={mode}
|
|
723
744
|
onClick={() => setViewMode(mode)}
|
|
724
745
|
sx={{
|
|
725
|
-
px: 1.5,
|
|
726
|
-
py: 0.5,
|
|
746
|
+
px: { xs: 2, sm: 1.5 },
|
|
747
|
+
py: { xs: 1, sm: 0.5 },
|
|
727
748
|
cursor: 'pointer',
|
|
728
|
-
fontSize: '0.75rem',
|
|
749
|
+
fontSize: { xs: '0.85rem', sm: '0.75rem' },
|
|
729
750
|
fontWeight: viewMode === mode ? 'bold' : 'normal',
|
|
730
751
|
bgcolor: viewMode === mode ? 'var(--black, #1a1a1a)' : 'transparent',
|
|
731
752
|
color: viewMode === mode ? 'var(--white, white)' : 'inherit',
|
|
@@ -860,9 +881,9 @@ export default function Sales(props) {
|
|
|
860
881
|
</Box>
|
|
861
882
|
)}
|
|
862
883
|
|
|
863
|
-
{/*
|
|
864
|
-
<Box className="
|
|
865
|
-
<
|
|
884
|
+
{/* Insights */}
|
|
885
|
+
<Box className="insights-wrapper">
|
|
886
|
+
<Insights
|
|
866
887
|
sales={sales}
|
|
867
888
|
previousSales={previousSales}
|
|
868
889
|
loading={loading}
|
package/components/SalesTable.js
CHANGED
|
@@ -166,9 +166,11 @@ export function SalesTable({ sales = [], designer = {}, admin = false, loading =
|
|
|
166
166
|
<Box sx={{ width: '100%', mb: 2 }}>
|
|
167
167
|
<Box sx={{
|
|
168
168
|
display: 'inline-block',
|
|
169
|
-
p: 2,
|
|
169
|
+
p: { xs: 1.5, sm: 2 },
|
|
170
170
|
mt: '20px',
|
|
171
171
|
borderRadius: '4px',
|
|
172
|
+
maxWidth: '100%',
|
|
173
|
+
boxSizing: 'border-box',
|
|
172
174
|
border: reconciliationData.isReconciled
|
|
173
175
|
? '2px solid var(--green, green)'
|
|
174
176
|
: '2px solid var(--red, red)',
|
|
@@ -110,18 +110,20 @@ export default function TopPerformers({
|
|
|
110
110
|
<Box sx={{
|
|
111
111
|
background: "rgba(var(--blackRGB, 0,0,0), 1)",
|
|
112
112
|
color: "var(--white, white)",
|
|
113
|
-
padding: "5px 10px 10px",
|
|
113
|
+
padding: { xs: "8px 12px", sm: "5px 10px 10px" },
|
|
114
114
|
display: "flex",
|
|
115
115
|
justifyContent: "space-between",
|
|
116
|
-
alignItems: "center"
|
|
116
|
+
alignItems: "center",
|
|
117
|
+
flexWrap: { xs: 'wrap', sm: 'nowrap' },
|
|
118
|
+
gap: 1,
|
|
117
119
|
}}>
|
|
118
|
-
<Typography variant='h5' sx={{color: "var(--white, white)"}}>
|
|
120
|
+
<Typography variant='h5' sx={{ color: "var(--white, white)", fontSize: { xs: '1rem', sm: '1.25rem' } }}>
|
|
119
121
|
<strong>Designers</strong>
|
|
120
122
|
</Typography>
|
|
121
|
-
<Stack direction="row" spacing={
|
|
122
|
-
|
|
123
|
-
<FormControl variant="filled" size="small" sx={{
|
|
124
|
-
minWidth: 120,
|
|
123
|
+
<Stack direction="row" spacing={1} alignItems="center">
|
|
124
|
+
|
|
125
|
+
<FormControl variant="filled" size="small" sx={{
|
|
126
|
+
minWidth: { xs: 90, sm: 120 },
|
|
125
127
|
'& .MuiFilledInput-root': { color: "var(--white, white)" },
|
|
126
128
|
'& .MuiFormLabel-root': { color: 'rgba(255,255,255,0.7)' },
|
|
127
129
|
'& .MuiSelect-icon': { display: 'none' }
|
|
@@ -159,7 +161,7 @@ export default function TopPerformers({
|
|
|
159
161
|
</Box>
|
|
160
162
|
<Box sx={{
|
|
161
163
|
background: "rgba(var(--blackRGB, 0,0,0), 0.06)",
|
|
162
|
-
padding: "10px 10px 20px",
|
|
164
|
+
padding: { xs: "8px 12px 16px", sm: "10px 10px 20px" },
|
|
163
165
|
}}>
|
|
164
166
|
{
|
|
165
167
|
sortedDesigners.map((designer, i) => {
|
|
@@ -173,10 +175,11 @@ export default function TopPerformers({
|
|
|
173
175
|
<Typography
|
|
174
176
|
variant='h5'
|
|
175
177
|
sx={{
|
|
176
|
-
whiteSpace: "nowrap",
|
|
178
|
+
whiteSpace: { xs: "normal", md: "nowrap" },
|
|
177
179
|
overflow: "hidden",
|
|
178
180
|
textOverflow: "ellipsis",
|
|
179
|
-
paddingBottom: "0.5em"
|
|
181
|
+
paddingBottom: "0.5em",
|
|
182
|
+
fontSize: { xs: '0.85rem', sm: '1rem', md: '1.25rem' },
|
|
180
183
|
}}>
|
|
181
184
|
{designer?.firstName && designer?.lastName ?
|
|
182
185
|
`${designer.firstName} ${designer.lastName}`
|
|
@@ -199,9 +202,10 @@ export default function TopPerformers({
|
|
|
199
202
|
key={`designer-typeface-${i}-${j}`}
|
|
200
203
|
variant='h6'
|
|
201
204
|
sx={{
|
|
202
|
-
whiteSpace: "nowrap",
|
|
205
|
+
whiteSpace: { xs: "normal", md: "nowrap" },
|
|
203
206
|
overflow: "hidden",
|
|
204
207
|
textOverflow: "ellipsis",
|
|
208
|
+
fontSize: { xs: '0.8rem', sm: '0.9rem', md: '1rem' },
|
|
205
209
|
}}>
|
|
206
210
|
{typeface.title}
|
|
207
211
|
<span className={styles.earningContainer}>
|
|
@@ -240,17 +244,19 @@ export default function TopPerformers({
|
|
|
240
244
|
<Box sx={{
|
|
241
245
|
background: "rgba(var(--blackRGB, 0,0,0), 1)",
|
|
242
246
|
color: "var(--white, white)",
|
|
243
|
-
padding: "5px 10px 10px",
|
|
247
|
+
padding: { xs: "8px 12px", sm: "5px 10px 10px" },
|
|
244
248
|
display: "flex",
|
|
245
249
|
justifyContent: "space-between",
|
|
246
|
-
alignItems: "center"
|
|
250
|
+
alignItems: "center",
|
|
251
|
+
flexWrap: { xs: 'wrap', sm: 'nowrap' },
|
|
252
|
+
gap: 1,
|
|
247
253
|
}}>
|
|
248
|
-
<Typography variant='h5'>
|
|
254
|
+
<Typography variant='h5' sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }}>
|
|
249
255
|
<strong>Top Performers</strong>
|
|
250
256
|
</Typography>
|
|
251
|
-
<Stack direction="row" spacing={
|
|
252
|
-
|
|
253
|
-
<FormControl
|
|
257
|
+
<Stack direction="row" spacing={1} alignItems="center">
|
|
258
|
+
|
|
259
|
+
<FormControl
|
|
254
260
|
variant="filled"
|
|
255
261
|
size="small"
|
|
256
262
|
sx={{
|
|
@@ -64,17 +64,19 @@ export default function TypefaceList({ typefaceData, sales, loading, admin }) {
|
|
|
64
64
|
<Box sx={{
|
|
65
65
|
background: "rgba(var(--blackRGB, 0,0,0), 1)",
|
|
66
66
|
color: "var(--white, white)",
|
|
67
|
-
padding: "5px 10px 10px",
|
|
67
|
+
padding: { xs: "8px 12px", sm: "5px 10px 10px" },
|
|
68
68
|
display: "flex",
|
|
69
69
|
justifyContent: "space-between",
|
|
70
|
-
alignItems: "center"
|
|
70
|
+
alignItems: "center",
|
|
71
|
+
flexWrap: { xs: 'wrap', sm: 'nowrap' },
|
|
72
|
+
gap: 1,
|
|
71
73
|
}}>
|
|
72
|
-
<Typography variant='h5'>
|
|
74
|
+
<Typography variant='h5' sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }}>
|
|
73
75
|
<strong>Products</strong>
|
|
74
76
|
</Typography>
|
|
75
|
-
<Stack direction="row" spacing={
|
|
76
|
-
<FormControl variant="filled" size="small" sx={{
|
|
77
|
-
minWidth: 120,
|
|
77
|
+
<Stack direction="row" spacing={1} alignItems="center">
|
|
78
|
+
<FormControl variant="filled" size="small" sx={{
|
|
79
|
+
minWidth: { xs: 90, sm: 120 },
|
|
78
80
|
'& .MuiFilledInput-root': { color: "var(--white, white)"},
|
|
79
81
|
'& .MuiFormLabel-root': { color: 'rgba(255,255,255,0.7)' },
|
|
80
82
|
'& .MuiSelect-icon': { display: 'none' },
|
|
@@ -118,10 +120,11 @@ export default function TypefaceList({ typefaceData, sales, loading, admin }) {
|
|
|
118
120
|
key={`typeface-total-${i}`}
|
|
119
121
|
variant='h5'
|
|
120
122
|
sx={{
|
|
121
|
-
color: 'var(--black, black)',
|
|
122
|
-
whiteSpace: "nowrap",
|
|
123
|
+
color: 'var(--black, black)',
|
|
124
|
+
whiteSpace: { xs: "normal", md: "nowrap" },
|
|
123
125
|
overflow: "hidden",
|
|
124
|
-
textOverflow: "ellipsis",
|
|
126
|
+
textOverflow: "ellipsis",
|
|
127
|
+
fontSize: { xs: '0.85rem', sm: '1rem', md: '1.25rem' },
|
|
125
128
|
paddingTop: typefaceData[i-1] && typeface.title[0] !== typefaceData[i-1].title[0] ? "0.5em" : "",
|
|
126
129
|
"& .designer": {display: "none"},
|
|
127
130
|
"&:hover .designer": {display: "inline"}
|
|
@@ -155,7 +155,7 @@ export default function YearOverview({ designer, year, onMonthClick }) {
|
|
|
155
155
|
|
|
156
156
|
{/* Stacked bar chart */}
|
|
157
157
|
{series.length > 0 ? (
|
|
158
|
-
<Box sx={{ width: '100%', height: { xs:
|
|
158
|
+
<Box sx={{ width: '100%', height: { xs: 200, sm: 300, md: 350 } }}>
|
|
159
159
|
<ResponsiveChartContainer
|
|
160
160
|
series={series}
|
|
161
161
|
xAxis={[{
|
|
@@ -215,8 +215,8 @@ export default function YearOverview({ designer, year, onMonthClick }) {
|
|
|
215
215
|
sx={{
|
|
216
216
|
display: 'flex',
|
|
217
217
|
justifyContent: 'space-between',
|
|
218
|
-
py: 0.75,
|
|
219
|
-
px: 1,
|
|
218
|
+
py: { xs: 1.5, sm: 0.75 },
|
|
219
|
+
px: { xs: 1.5, sm: 1 },
|
|
220
220
|
borderBottom: '1px solid rgba(0,0,0,0.06)',
|
|
221
221
|
cursor: 'pointer',
|
|
222
222
|
'&:hover': { bgcolor: 'rgba(0,0,0,0.03)' },
|
|
@@ -227,7 +227,7 @@ export default function YearOverview({ designer, year, onMonthClick }) {
|
|
|
227
227
|
{MONTH_LABELS[i]} {year}
|
|
228
228
|
{m.error && <span style={{ color: 'var(--red, red)', marginLeft: 8 }}>error</span>}
|
|
229
229
|
</Typography>
|
|
230
|
-
<Box sx={{ display: 'flex', gap: 3 }}>
|
|
230
|
+
<Box sx={{ display: 'flex', gap: { xs: 1.5, sm: 3 } }}>
|
|
231
231
|
<Typography variant="body2" sx={{ opacity: 0.5 }}>
|
|
232
232
|
{m.salesCount} sale{m.salesCount !== 1 ? 's' : ''}
|
|
233
233
|
</Typography>
|
|
@@ -12,7 +12,7 @@ export { calculateGrossSales };
|
|
|
12
12
|
*/
|
|
13
13
|
function buildChecks({ sales, diagnostics, grossSales, totalBalanceChange, difference, flaggedTransactions }) {
|
|
14
14
|
const checks = [];
|
|
15
|
-
const uniqueChargeIds = new Set(sales.map(s => s.id)).size;
|
|
15
|
+
const uniqueChargeIds = new Set(sales.filter(s => (s.chargeAmount || s.invoiceTotalWithTax || 0) > 0).map(s => s.id)).size;
|
|
16
16
|
const salesRefundIds = new Set();
|
|
17
17
|
sales.forEach(s => s.refunds?.forEach(r => r.id && salesRefundIds.add(r.id)));
|
|
18
18
|
const testSales = sales.filter(s => s.testSale);
|
|
@@ -20,7 +20,7 @@ function buildChecks({ sales, diagnostics, grossSales, totalBalanceChange, diffe
|
|
|
20
20
|
// 1. Transaction count match
|
|
21
21
|
const countMatch = uniqueChargeIds === diagnostics.chargeCount;
|
|
22
22
|
checks.push({
|
|
23
|
-
label: 'Transaction count',
|
|
23
|
+
label: 'Transaction count (>$0)',
|
|
24
24
|
status: countMatch ? 'pass' : 'fail',
|
|
25
25
|
detail: `${uniqueChargeIds} sales vs ${diagnostics.chargeCount} Stripe charges`,
|
|
26
26
|
tip: !countMatch ? 'Mismatch may indicate missing orders in Sanity or charges outside this period.' : null,
|
|
@@ -184,7 +184,9 @@ export function useReconciliation({ designer, admin, sales, date, dateRange }) {
|
|
|
184
184
|
};
|
|
185
185
|
|
|
186
186
|
fetchBalanceTransactions();
|
|
187
|
-
|
|
187
|
+
// Use sales.length instead of sales array to avoid infinite re-render loops
|
|
188
|
+
// when the array reference changes but content is the same
|
|
189
|
+
}, [designer?.user, designer?.password, designer?.admin, date, dateRange, admin, sales?.length]);
|
|
188
190
|
|
|
189
191
|
return reconciliationData;
|
|
190
192
|
}
|
package/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { SalesTable } from './components/SalesTable.js';
|
|
|
11
11
|
export { DateRangeSalesTable } from './components/DateRangeSalesTable.js';
|
|
12
12
|
export { default as SalesChart } from './components/SalesChart.js';
|
|
13
13
|
export { default as YearOverview } from './components/YearOverview.js';
|
|
14
|
-
export { default as SummaryCards } from './components/
|
|
14
|
+
export { default as SummaryCards } from './components/Insights.js';
|
|
15
15
|
export { default as TopPerformers } from './components/TopPerformers.js';
|
|
16
16
|
export { default as TypefaceList } from './components/TypefaceList.js';
|
|
17
17
|
export { default as LicenseTypeList } from './components/LicenseTypeList.js';
|
package/package.json
CHANGED
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
.earningContainer {
|
|
79
|
-
border-bottom: 1px solid rgba(0, 0, 0, .25);
|
|
79
|
+
border-bottom: 1px solid rgba(var(--blackRGB, 0, 0, 0), .25);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
.displayLossContainer{
|
|
@@ -115,8 +115,11 @@
|
|
|
115
115
|
.salesSection{
|
|
116
116
|
padding-left: var(--marginXMobile, 15px);
|
|
117
117
|
padding-right: var(--marginXMobile, 15px);
|
|
118
|
-
margin-bottom:
|
|
118
|
+
margin-bottom: 16px;
|
|
119
119
|
transition: all 0.3s ease;
|
|
120
|
+
@media screen and (min-width: 600px) {
|
|
121
|
+
margin-bottom: 28px;
|
|
122
|
+
}
|
|
120
123
|
@media screen and (min-width: 900px) {
|
|
121
124
|
padding-left: var(--marginX, 30px);
|
|
122
125
|
padding-right: var(--marginX, 30px);
|
|
@@ -129,10 +132,14 @@
|
|
|
129
132
|
pointer-events: none;
|
|
130
133
|
position: absolute;
|
|
131
134
|
top: 0;
|
|
132
|
-
left: -
|
|
133
|
-
width:
|
|
135
|
+
left: -10%;
|
|
136
|
+
width: 120%;
|
|
137
|
+
@media screen and (min-width: 600px) {
|
|
138
|
+
left: -50%;
|
|
139
|
+
width: 200%;
|
|
140
|
+
}
|
|
134
141
|
height: 100%;
|
|
135
|
-
box-shadow: inset 0 10px 10px 0px rgba(0, 0, 0, 1);
|
|
142
|
+
box-shadow: inset 0 10px 10px 0px rgba(var(--blackRGB, 0, 0, 0), 1);
|
|
136
143
|
}
|
|
137
144
|
}
|
|
138
145
|
.globeWrap {
|
|
@@ -162,7 +169,7 @@
|
|
|
162
169
|
width: 90%;
|
|
163
170
|
height: 90%;
|
|
164
171
|
border-radius: 100%;
|
|
165
|
-
background: radial-gradient(circle at top, white, rgba(255, 255, 255, 0) 58%);
|
|
172
|
+
background: radial-gradient(circle at top, var(--white, white), rgba(var(--whiteRGB, 255, 255, 255), 0) 58%);
|
|
166
173
|
filter: blur(5px);
|
|
167
174
|
z-index: 2;
|
|
168
175
|
}
|
|
@@ -223,14 +230,14 @@
|
|
|
223
230
|
|
|
224
231
|
/* New Dashboard Styles */
|
|
225
232
|
|
|
226
|
-
/*
|
|
227
|
-
.
|
|
233
|
+
/* Insights Styling */
|
|
234
|
+
.insightsContainer {
|
|
228
235
|
display: grid;
|
|
229
236
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
230
237
|
gap: 16px;
|
|
231
238
|
margin-bottom: 24px;
|
|
232
239
|
|
|
233
|
-
.
|
|
240
|
+
.insightCard {
|
|
234
241
|
background-color: rgba(var(--blackRGB, 0, 0, 0), 0.04);
|
|
235
242
|
padding: 16px;
|
|
236
243
|
border-radius: 4px;
|
|
@@ -239,7 +246,7 @@
|
|
|
239
246
|
|
|
240
247
|
&:hover {
|
|
241
248
|
transform: translateY(-2px);
|
|
242
|
-
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
249
|
+
box-shadow: 0 4px 8px rgba(var(--blackRGB, 0, 0, 0), 0.1);
|
|
243
250
|
}
|
|
244
251
|
|
|
245
252
|
.cardTitle {
|
|
@@ -320,7 +327,7 @@
|
|
|
320
327
|
.enhancedTable {
|
|
321
328
|
border-radius: 4px;
|
|
322
329
|
overflow: hidden;
|
|
323
|
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
330
|
+
box-shadow: 0 2px 4px rgba(var(--blackRGB, 0, 0, 0), 0.05);
|
|
324
331
|
|
|
325
332
|
.tableHeader {
|
|
326
333
|
background-color: rgba(var(--blackRGB, 0, 0, 0), 0.06);
|