@liiift-studio/sales-portal 3.0.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/README.md +1 -1
- package/SETUP.md +1 -1
- package/api/utils/processors/invoiceProcessor.js +2 -2
- package/components/DateRangeSalesTable.js +9 -7
- package/components/{SummaryCards.js → Insights.js} +49 -64
- package/components/LicenseTypeList.js +12 -9
- package/components/LoginForm.js +3 -3
- package/components/Sales.js +130 -75
- package/components/SalesPortalPage.js +1 -1
- package/components/SalesTable.js +4 -2
- 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 +19 -12
- package/styles/sales-portal.theme.js +1 -1
package/README.md
CHANGED
package/SETUP.md
CHANGED
|
@@ -39,7 +39,7 @@ export default function SalesPortal() {
|
|
|
39
39
|
title: 'Sales Portal',
|
|
40
40
|
description: 'Track your typeface sales performance and revenue.',
|
|
41
41
|
subtitle: 'All sales are reviewed internally before payouts.',
|
|
42
|
-
dashboardTitle: '
|
|
42
|
+
dashboardTitle: 'Sales',
|
|
43
43
|
dashboardSubtitle: 'by designer'
|
|
44
44
|
}}
|
|
45
45
|
/>
|
|
@@ -17,8 +17,8 @@ import { isTestSale } from '../clients';
|
|
|
17
17
|
function findMatches({ invoice, line = {}, sanitySales = [] }) {
|
|
18
18
|
// Find matching order
|
|
19
19
|
const associatedOrder =
|
|
20
|
-
(invoice?.id && sanitySales.find(order => order
|
|
21
|
-
(invoice?.payment_intent && sanitySales.find(order => order
|
|
20
|
+
(invoice?.id && sanitySales.find(order => order?.orderStatus?.invoiceId === invoice.id)) ||
|
|
21
|
+
(invoice?.payment_intent && sanitySales.find(order => order?.orderStatus?.paymentIntentId === invoice.payment_intent)) ||
|
|
22
22
|
(invoice?.customer?.email && sanitySales.find(order => {
|
|
23
23
|
const orderDate = new Date(order._createdAt);
|
|
24
24
|
const invoiceDate = new Date(invoice.created * 1000);
|
|
@@ -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,14 +507,16 @@ 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)',
|
|
516
518
|
color: reconciliationData.isReconciled
|
|
517
|
-
? 'var(--
|
|
519
|
+
? 'var(--black, #1a1a1a)'
|
|
518
520
|
: 'var(--red, red)',
|
|
519
521
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
520
522
|
filter: 'invert(0) !important',
|
|
@@ -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);
|
|
@@ -203,6 +203,11 @@ export default function Sales(props) {
|
|
|
203
203
|
},
|
|
204
204
|
'Sales data'
|
|
205
205
|
);
|
|
206
|
+
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
throw new Error(`HTTP ${response.status}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
206
211
|
const data = await response.json();
|
|
207
212
|
|
|
208
213
|
if (data.success) {
|
|
@@ -211,8 +216,11 @@ export default function Sales(props) {
|
|
|
211
216
|
setMessage('Error retrieving sales data');
|
|
212
217
|
}
|
|
213
218
|
} catch (err) {
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
}
|
|
216
224
|
} finally {
|
|
217
225
|
updateLoadingState('salesData', false);
|
|
218
226
|
}
|
|
@@ -257,6 +265,10 @@ export default function Sales(props) {
|
|
|
257
265
|
'Previous sales data'
|
|
258
266
|
);
|
|
259
267
|
|
|
268
|
+
if (!response.ok) {
|
|
269
|
+
throw new Error(`HTTP ${response.status}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
260
272
|
const data = await response.json();
|
|
261
273
|
|
|
262
274
|
if (data.success) {
|
|
@@ -268,6 +280,8 @@ export default function Sales(props) {
|
|
|
268
280
|
setPreviousSales([]); // Clear any stale data
|
|
269
281
|
}
|
|
270
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;
|
|
271
285
|
const errorMsg = `Error retrieving previous sales data: ${err.message}`;
|
|
272
286
|
console.error(errorMsg, err);
|
|
273
287
|
setPreviousSalesError(errorMsg);
|
|
@@ -314,8 +328,27 @@ export default function Sales(props) {
|
|
|
314
328
|
const processedLicenseTypeData = processLicenseTypeData(sales);
|
|
315
329
|
setLicenseTypeData(processedLicenseTypeData);
|
|
316
330
|
|
|
317
|
-
// Set total sales
|
|
318
|
-
const
|
|
331
|
+
// Set total sales from unique invoice amounts (accounts for discounts correctly)
|
|
332
|
+
const seenInvoices = new Set();
|
|
333
|
+
let totalRevenue = 0;
|
|
334
|
+
sales.forEach(s => {
|
|
335
|
+
if (s.shippingProvision) return;
|
|
336
|
+
const sid = s.id;
|
|
337
|
+
if (sid && !seenInvoices.has(sid)) {
|
|
338
|
+
seenInvoices.add(sid);
|
|
339
|
+
totalRevenue += (s.invoiceTotal || s.total || 0) / 100;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
// Subtract refunds
|
|
343
|
+
const seenRefunds = new Set();
|
|
344
|
+
sales.forEach(s => {
|
|
345
|
+
s.refunds?.forEach(r => {
|
|
346
|
+
if (r.id && !seenRefunds.has(r.id)) {
|
|
347
|
+
seenRefunds.add(r.id);
|
|
348
|
+
totalRevenue -= (r.total || 0) / 100;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
});
|
|
319
352
|
setTotal(totalRevenue);
|
|
320
353
|
|
|
321
354
|
// Calculate revenue change percentage compared to previous period
|
|
@@ -506,7 +539,7 @@ export default function Sales(props) {
|
|
|
506
539
|
{/* Header Section */}
|
|
507
540
|
<Box data-disabled={loading} data-loading={loading}
|
|
508
541
|
sx={{
|
|
509
|
-
width: { xs: '
|
|
542
|
+
width: { xs: '100%', sm: '66.67%' },
|
|
510
543
|
".show-hover": { display: "none" },
|
|
511
544
|
"&:hover .show-hover": { display: "inline", opacity: "0.5" },
|
|
512
545
|
}}
|
|
@@ -560,50 +593,59 @@ export default function Sales(props) {
|
|
|
560
593
|
{/* Date Picker and View Controls */}
|
|
561
594
|
<Box
|
|
562
595
|
sx={{
|
|
563
|
-
width: { xs: '
|
|
564
|
-
justifyContent:
|
|
596
|
+
width: { xs: '100%', sm: '33.33%' },
|
|
597
|
+
justifyContent: { xs: 'flex-start', sm: 'flex-end' },
|
|
565
598
|
display: "flex",
|
|
599
|
+
mt: { xs: 2, sm: 0 },
|
|
566
600
|
}}
|
|
567
601
|
data-disabled={loading}
|
|
568
602
|
>
|
|
569
|
-
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
570
|
-
<IconButton
|
|
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' } }}>
|
|
604
|
+
<IconButton
|
|
571
605
|
onClick={() => {
|
|
572
606
|
const newDate = new Date(date);
|
|
573
|
-
|
|
607
|
+
if (viewMode === 'year') {
|
|
608
|
+
newDate.setUTCFullYear(date.getUTCFullYear() - 1);
|
|
609
|
+
} else {
|
|
610
|
+
newDate.setUTCMonth(date.getUTCMonth() - 1);
|
|
611
|
+
}
|
|
574
612
|
setDate(newDate);
|
|
575
613
|
}}
|
|
576
614
|
size="small"
|
|
577
|
-
sx={{ display: { xs: 'none',
|
|
615
|
+
sx={{ minWidth: '44px', minHeight: '44px', display: { xs: 'none', sm: 'flex' } }}
|
|
578
616
|
>
|
|
579
617
|
<ChevronLeftIcon />
|
|
580
618
|
</IconButton>
|
|
581
619
|
<DatePicker
|
|
582
|
-
label='Month/Year (UTC)'
|
|
583
|
-
views={['month', 'year']}
|
|
584
|
-
format=
|
|
620
|
+
label={viewMode === 'year' ? 'Year (UTC)' : 'Month/Year (UTC)'}
|
|
621
|
+
views={viewMode === 'year' ? ['year'] : ['month', 'year']}
|
|
622
|
+
format={viewMode === 'year' ? 'YYYY' : 'MMM YYYY'}
|
|
585
623
|
formatDensity="dense"
|
|
586
|
-
slotProps={{ textField: { variant: "filled" } }}
|
|
587
|
-
sx={{ "& *": { borderRadius: "4px" } }}
|
|
624
|
+
slotProps={{ textField: { variant: "filled", size: "small" } }}
|
|
625
|
+
sx={{ "& *": { borderRadius: "4px" }, maxWidth: { xs: '160px', sm: 'none' }, flex: { xs: 1, sm: 'none' } }}
|
|
588
626
|
value={dayjs.utc(date)}
|
|
589
627
|
onChange={(newValue) => setDate(newValue.toDate())}
|
|
590
628
|
/>
|
|
591
|
-
<IconButton
|
|
629
|
+
<IconButton
|
|
592
630
|
onClick={() => {
|
|
593
631
|
const newDate = new Date(date);
|
|
594
|
-
|
|
632
|
+
if (viewMode === 'year') {
|
|
633
|
+
newDate.setUTCFullYear(date.getUTCFullYear() + 1);
|
|
634
|
+
} else {
|
|
635
|
+
newDate.setUTCMonth(date.getUTCMonth() + 1);
|
|
636
|
+
}
|
|
595
637
|
setDate(newDate);
|
|
596
638
|
}}
|
|
597
639
|
size="small"
|
|
598
|
-
sx={{ display: { xs: 'none',
|
|
640
|
+
sx={{ minWidth: '44px', minHeight: '44px', display: { xs: 'none', sm: 'flex' } }}
|
|
599
641
|
>
|
|
600
642
|
<ChevronRightIcon />
|
|
601
643
|
</IconButton>
|
|
602
644
|
{!!sales.length && (
|
|
603
|
-
<Tooltip
|
|
645
|
+
<Tooltip
|
|
604
646
|
title="Print to PDF"
|
|
605
|
-
sx={{ display: { xs: 'none', md: 'inherit' } }}
|
|
606
647
|
>
|
|
648
|
+
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
|
|
607
649
|
<IconButton
|
|
608
650
|
className='print-button'
|
|
609
651
|
onClick={() => {
|
|
@@ -622,36 +664,50 @@ export default function Sales(props) {
|
|
|
622
664
|
size: letter portrait;
|
|
623
665
|
margin: 0.5in;
|
|
624
666
|
}
|
|
625
|
-
|
|
667
|
+
/* Hide everything except this sales portal */
|
|
668
|
+
header, nav, footer, #navBar, #footer, #titleContainer,
|
|
669
|
+
.exportSection, .date-range-sales-table-wrapper,
|
|
626
670
|
.salesPortal:not(:nth-child(${portalIndex})) {
|
|
627
|
-
display: none!important;
|
|
671
|
+
display: none !important;
|
|
628
672
|
}
|
|
629
|
-
.salesPortal, .salesPortalWrap{
|
|
630
|
-
background: white;
|
|
673
|
+
.salesPortal, .salesPortalWrap {
|
|
674
|
+
background: white !important;
|
|
631
675
|
}
|
|
632
676
|
body {
|
|
633
677
|
-webkit-print-color-adjust: exact !important;
|
|
634
678
|
print-color-adjust: exact !important;
|
|
635
679
|
}
|
|
680
|
+
/* Clean up layout */
|
|
636
681
|
.salesPortal {
|
|
637
682
|
padding: 0 !important;
|
|
683
|
+
width: 100% !important;
|
|
638
684
|
}
|
|
639
685
|
.salesPortal > * {
|
|
640
686
|
page-break-inside: avoid;
|
|
641
687
|
}
|
|
642
|
-
/* Hide
|
|
643
|
-
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 {
|
|
644
692
|
display: none !important;
|
|
645
693
|
}
|
|
646
|
-
/*
|
|
647
|
-
.
|
|
694
|
+
/* Full width sections */
|
|
695
|
+
.sales-header-section, .sales-data-section,
|
|
696
|
+
.insights-wrapper, .sales-table-wrapper {
|
|
648
697
|
width: 100% !important;
|
|
649
|
-
|
|
698
|
+
padding: 0 !important;
|
|
699
|
+
margin: 10px 0 !important;
|
|
650
700
|
}
|
|
651
|
-
/*
|
|
701
|
+
/* Readable text */
|
|
652
702
|
.MuiTypography-root {
|
|
653
703
|
color: black !important;
|
|
654
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
|
+
}
|
|
655
711
|
}
|
|
656
712
|
`;
|
|
657
713
|
document.head.appendChild(style);
|
|
@@ -671,6 +727,7 @@ export default function Sales(props) {
|
|
|
671
727
|
>
|
|
672
728
|
<PrintIcon />
|
|
673
729
|
</IconButton>
|
|
730
|
+
</Box>
|
|
674
731
|
</Tooltip>
|
|
675
732
|
)}
|
|
676
733
|
{/* Month/Year toggle */}
|
|
@@ -686,10 +743,10 @@ export default function Sales(props) {
|
|
|
686
743
|
key={mode}
|
|
687
744
|
onClick={() => setViewMode(mode)}
|
|
688
745
|
sx={{
|
|
689
|
-
px: 1.5,
|
|
690
|
-
py: 0.5,
|
|
746
|
+
px: { xs: 2, sm: 1.5 },
|
|
747
|
+
py: { xs: 1, sm: 0.5 },
|
|
691
748
|
cursor: 'pointer',
|
|
692
|
-
fontSize: '0.75rem',
|
|
749
|
+
fontSize: { xs: '0.85rem', sm: '0.75rem' },
|
|
693
750
|
fontWeight: viewMode === mode ? 'bold' : 'normal',
|
|
694
751
|
bgcolor: viewMode === mode ? 'var(--black, #1a1a1a)' : 'transparent',
|
|
695
752
|
color: viewMode === mode ? 'var(--white, white)' : 'inherit',
|
|
@@ -783,25 +840,9 @@ export default function Sales(props) {
|
|
|
783
840
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', width: '100%' }}>
|
|
784
841
|
{HeaderSection}
|
|
785
842
|
|
|
786
|
-
{/*
|
|
787
|
-
{
|
|
788
|
-
<Box sx={{ width: '100%', px: { xs: 2, md: 0 } }}>
|
|
789
|
-
<YearOverview
|
|
790
|
-
designer={designer}
|
|
791
|
-
year={date.getUTCFullYear()}
|
|
792
|
-
onMonthClick={(monthIndex) => {
|
|
793
|
-
const newDate = new Date(Date.UTC(date.getUTCFullYear(), monthIndex, 1));
|
|
794
|
-
setDate(newDate);
|
|
795
|
-
setViewMode('month');
|
|
796
|
-
}}
|
|
797
|
-
/>
|
|
798
|
-
</Box>
|
|
799
|
-
)}
|
|
800
|
-
|
|
801
|
-
{/* Monthly Sales Data Display */}
|
|
802
|
-
{viewMode === 'month' && !!sales.length && (
|
|
843
|
+
{/* Sales Data Display */}
|
|
844
|
+
{!!sales.length && (
|
|
803
845
|
<>
|
|
804
|
-
|
|
805
846
|
<Box
|
|
806
847
|
data-disabled={loading}
|
|
807
848
|
className={`sales-data-section ${styles.salesSection}`}
|
|
@@ -813,22 +854,36 @@ export default function Sales(props) {
|
|
|
813
854
|
}}
|
|
814
855
|
>
|
|
815
856
|
|
|
816
|
-
{/* Chart Section */}
|
|
817
|
-
|
|
818
|
-
<
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
857
|
+
{/* Chart Section — Year or Month */}
|
|
858
|
+
{viewMode === 'year' && date ? (
|
|
859
|
+
<Box className="year-overview-wrapper">
|
|
860
|
+
<YearOverview
|
|
861
|
+
designer={designer}
|
|
862
|
+
year={date.getUTCFullYear()}
|
|
863
|
+
onMonthClick={(monthIndex) => {
|
|
864
|
+
const newDate = new Date(Date.UTC(date.getUTCFullYear(), monthIndex, 1));
|
|
865
|
+
setDate(newDate);
|
|
866
|
+
setViewMode('month');
|
|
867
|
+
}}
|
|
868
|
+
/>
|
|
869
|
+
</Box>
|
|
870
|
+
) : (
|
|
871
|
+
<Box className="sales-chart-wrapper">
|
|
872
|
+
<SalesChart
|
|
873
|
+
sales={sales}
|
|
874
|
+
chartState={chartState}
|
|
875
|
+
seriesData={seriesData}
|
|
876
|
+
displayLosses={displayLosses}
|
|
877
|
+
setDisplayLosses={setDisplayLosses}
|
|
878
|
+
date={date}
|
|
879
|
+
loading={loading}
|
|
880
|
+
/>
|
|
881
|
+
</Box>
|
|
882
|
+
)}
|
|
828
883
|
|
|
829
|
-
{/*
|
|
830
|
-
<Box className="
|
|
831
|
-
<
|
|
884
|
+
{/* Insights */}
|
|
885
|
+
<Box className="insights-wrapper">
|
|
886
|
+
<Insights
|
|
832
887
|
sales={sales}
|
|
833
888
|
previousSales={previousSales}
|
|
834
889
|
loading={loading}
|
|
@@ -876,7 +931,7 @@ export default function Sales(props) {
|
|
|
876
931
|
{/* Sales Table Section */}
|
|
877
932
|
{designer && (
|
|
878
933
|
<Box className="sales-table-wrapper">
|
|
879
|
-
<SalesTable
|
|
934
|
+
<SalesTable
|
|
880
935
|
sales={sales}
|
|
881
936
|
designer={designer}
|
|
882
937
|
admin={admin}
|
|
@@ -889,12 +944,12 @@ export default function Sales(props) {
|
|
|
889
944
|
</Box>
|
|
890
945
|
|
|
891
946
|
{/* Date Range Sales Table Section */}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
947
|
+
<DateRangeSalesTable
|
|
948
|
+
designer={designer}
|
|
949
|
+
admin={admin}
|
|
950
|
+
loading={loadingStates.dateRangeSalesData}
|
|
951
|
+
updateLoadingState={updateLoadingState}
|
|
952
|
+
/>
|
|
898
953
|
</>
|
|
899
954
|
)}
|
|
900
955
|
|
package/components/SalesTable.js
CHANGED
|
@@ -166,14 +166,16 @@ 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)',
|
|
175
177
|
color: reconciliationData.isReconciled
|
|
176
|
-
? 'var(--
|
|
178
|
+
? 'var(--black, #1a1a1a)'
|
|
177
179
|
: 'var(--red, red)',
|
|
178
180
|
}}>
|
|
179
181
|
{reconciliationData.isLoading ? (
|
|
@@ -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
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
align-items: center;
|
|
48
48
|
white-space: nowrap;
|
|
49
49
|
:global(.MuiTypography-root){
|
|
50
|
-
font-family:
|
|
50
|
+
font-family: inherit;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -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);
|
|
@@ -27,7 +27,7 @@ let size = createTheme({
|
|
|
27
27
|
let typography = createTheme({
|
|
28
28
|
|
|
29
29
|
typography: {
|
|
30
|
-
fontFamily: '
|
|
30
|
+
fontFamily: 'inherit, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
31
31
|
h1: {
|
|
32
32
|
maxWidth: '95%',
|
|
33
33
|
fontStyle: 'normal',
|