@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 CHANGED
@@ -72,7 +72,7 @@ export default function SalesPortal() {
72
72
  <SalesPortalPage
73
73
  texts={{
74
74
  title: 'Sales Portal',
75
- dashboardTitle: 'Monthly Sales',
75
+ dashboardTitle: ' Sales',
76
76
  dashboardSubtitle: 'by designer'
77
77
  }}
78
78
  />
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: 'Monthly Sales',
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.orderStatus.invoiceId === invoice.id)) ||
21
- (invoice?.payment_intent && sanitySales.find(order => order.orderStatus.paymentIntentId === invoice.payment_intent)) ||
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 SummaryCards from './SummaryCards';
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(--green, green)'
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
- {/* Summary Cards */}
622
- <SummaryCards
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
- // Summary dashboard cards displaying key sales metrics at a glance with country code tooltips
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
- * Summary dashboard component showing key sales metrics with period-over-period comparison
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} Summary cards component
44
+ * @returns {JSX.Element} Insights component
45
45
  */
46
- export default function SummaryCards({
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: 'var(--black, black)',
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: 'var(--black, black)',
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: 'var(--black, black)',
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: 'var(--black, black)',
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: 'var(--black, black)',
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: 'var(--black, black)',
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,.45)', cursor: 'help' }}>{code}</span>
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
- <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"}
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
- xs: '1.1rem',
310
- sm: '1.3rem',
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
- </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,
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
- card.change < 0 ? 'var(--red, #f44336)' :
332
- 'rgba(255, 255, 255, 0.6)'
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>&nbsp;</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={2} alignItems="center">
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
  >
@@ -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': {
@@ -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 SummaryCards from './SummaryCards';
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
- setMessage('Error retrieving sales data');
215
- console.error(err);
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 (sum of all sales minus tax and shipping)
318
- const totalRevenue = (processedData.salesMax - processedData.taxData.at(-1) - processedData.shippingData.at(-1)) || 0;
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: '66.67%', md: '66.67%' },
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: '33.33%', md: '33.33%' },
564
- justifyContent: "end",
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
- newDate.setUTCMonth(date.getUTCMonth() - 1);
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', md: 'inherit' } }}
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="MMM YYYY"
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
- newDate.setUTCMonth(date.getUTCMonth() + 1);
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', md: 'inherit' } }}
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
- header, #navBar, footer, #footer, #titleContainer, .exportSection,
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 non-printable elements */
643
- button, .MuiIconButton-root, .DatePicker {
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
- /* Ensure charts and tables fit on page */
647
- .salesSection {
694
+ /* Full width sections */
695
+ .sales-header-section, .sales-data-section,
696
+ .insights-wrapper, .sales-table-wrapper {
648
697
  width: 100% !important;
649
- margin: 20px 0 !important;
698
+ padding: 0 !important;
699
+ margin: 10px 0 !important;
650
700
  }
651
- /* Ensure text is readable */
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
- {/* Year Overview */}
787
- {viewMode === 'year' && date && (
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
- <Box className="sales-chart-wrapper">
818
- <SalesChart
819
- sales={sales}
820
- chartState={chartState}
821
- seriesData={seriesData}
822
- displayLosses={displayLosses}
823
- setDisplayLosses={setDisplayLosses}
824
- date={date}
825
- loading={loading}
826
- />
827
- </Box>
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
- {/* Summary Dashboard */}
830
- <Box className="summary-cards-wrapper">
831
- <SummaryCards
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
- <DateRangeSalesTable
893
- designer={designer}
894
- admin={admin}
895
- loading={loadingStates.dateRangeSalesData}
896
- updateLoadingState={updateLoadingState}
897
- />
947
+ <DateRangeSalesTable
948
+ designer={designer}
949
+ admin={admin}
950
+ loading={loadingStates.dateRangeSalesData}
951
+ updateLoadingState={updateLoadingState}
952
+ />
898
953
  </>
899
954
  )}
900
955
 
@@ -60,7 +60,7 @@ export default function SalesPortalPage({
60
60
 
61
61
  // Default page title text
62
62
  const {
63
- dashboardTitle = 'Monthly Sales',
63
+ dashboardTitle = 'Sales',
64
64
  dashboardSubtitle = 'by designer'
65
65
  } = texts;
66
66
 
@@ -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(--green, green)'
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={2} alignItems="center">
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={2} alignItems="center">
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={2} alignItems="center">
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: 250, sm: 350 } }}>
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
- }, [designer?.user, designer?.password, designer?.admin, date, dateRange, admin, sales]);
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/SummaryCards.js';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liiift-studio/sales-portal",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "Centralized sales portal package for Liiift Studio projects",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -47,7 +47,7 @@
47
47
  align-items: center;
48
48
  white-space: nowrap;
49
49
  :global(.MuiTypography-root){
50
- font-family: 'Tomato Grotesk', sans-serif;
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: 28px;
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: -50%;
133
- width: 200%;
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
- /* Summary Cards Styling */
227
- .summaryCardsContainer {
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
- .summaryCard {
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: '"Ordinary", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
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',