@liiift-studio/sales-portal 2.3.0 → 3.0.0
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/api/getYearSales.js +105 -0
- package/components/Sales.js +51 -3
- package/components/YearOverview.js +243 -0
- package/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// API endpoint for fetching a full year of sales data in a single request
|
|
2
|
+
import { authenticateDesigner, processSalesData } from './utils/salesDataProcessor';
|
|
3
|
+
import { sendError, requirePost } from './utils/apiResponse';
|
|
4
|
+
|
|
5
|
+
export const config = { maxDuration: 300 };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fetches 12 months of sales data, processes each month, and returns
|
|
9
|
+
* the results grouped by month with typeface breakdowns.
|
|
10
|
+
* This avoids 12 separate API calls from the frontend.
|
|
11
|
+
*/
|
|
12
|
+
export default async function handler(req, res) {
|
|
13
|
+
if (!requirePost(req, res)) return;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const { user, password, year, admin } = req.body;
|
|
17
|
+
|
|
18
|
+
if (!year) {
|
|
19
|
+
return sendError(res, 400, 'Year is required');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const designer = await authenticateDesigner(user, password);
|
|
23
|
+
if (!designer) {
|
|
24
|
+
return sendError(res, 401, 'Looks like there was an issue finding the account.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const months = [];
|
|
28
|
+
|
|
29
|
+
// Process each month sequentially to avoid Stripe rate limiting
|
|
30
|
+
for (let month = 0; month < 12; month++) {
|
|
31
|
+
const monthDate = new Date(Date.UTC(year, month, 1));
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const sales = await processSalesData({
|
|
35
|
+
date: monthDate.toISOString(),
|
|
36
|
+
designer,
|
|
37
|
+
admin
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Group sales by typeface for the stacked chart
|
|
41
|
+
const typefaceMap = {};
|
|
42
|
+
let monthTotal = 0;
|
|
43
|
+
let shippingTotal = 0;
|
|
44
|
+
|
|
45
|
+
sales.forEach(sale => {
|
|
46
|
+
if (sale.shippingProvision) {
|
|
47
|
+
shippingTotal += sale.total || 0;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const typefaceName = sale.typeface?.title || sale.description?.split(' (')[0] || 'Other';
|
|
52
|
+
if (!typefaceMap[typefaceName]) {
|
|
53
|
+
typefaceMap[typefaceName] = { total: 0, count: 0 };
|
|
54
|
+
}
|
|
55
|
+
typefaceMap[typefaceName].total += sale.total || 0;
|
|
56
|
+
typefaceMap[typefaceName].count += 1;
|
|
57
|
+
monthTotal += sale.total || 0;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
months.push({
|
|
61
|
+
month,
|
|
62
|
+
year,
|
|
63
|
+
total: monthTotal,
|
|
64
|
+
shippingTotal,
|
|
65
|
+
salesCount: sales.length,
|
|
66
|
+
currency: sales[0]?.currency || 'usd',
|
|
67
|
+
typefaces: Object.entries(typefaceMap)
|
|
68
|
+
.map(([name, data]) => ({ name, ...data }))
|
|
69
|
+
.sort((a, b) => b.total - a.total),
|
|
70
|
+
});
|
|
71
|
+
} catch (monthError) {
|
|
72
|
+
// If a single month fails, include it with zero data rather than failing the whole request
|
|
73
|
+
console.error(`Error processing month ${month + 1}/${year}:`, monthError.message);
|
|
74
|
+
months.push({
|
|
75
|
+
month,
|
|
76
|
+
year,
|
|
77
|
+
total: 0,
|
|
78
|
+
shippingTotal: 0,
|
|
79
|
+
salesCount: 0,
|
|
80
|
+
currency: 'usd',
|
|
81
|
+
typefaces: [],
|
|
82
|
+
error: monthError.message,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Build a list of all typefaces across the year for consistent chart series
|
|
88
|
+
const allTypefaces = new Set();
|
|
89
|
+
months.forEach(m => m.typefaces.forEach(t => allTypefaces.add(t.name)));
|
|
90
|
+
|
|
91
|
+
res.status(200).json({
|
|
92
|
+
success: true,
|
|
93
|
+
data: {
|
|
94
|
+
year,
|
|
95
|
+
months,
|
|
96
|
+
allTypefaces: [...allTypefaces].sort(),
|
|
97
|
+
yearTotal: months.reduce((sum, m) => sum + m.total, 0),
|
|
98
|
+
yearShipping: months.reduce((sum, m) => sum + m.shippingTotal, 0),
|
|
99
|
+
currency: months.find(m => m.currency)?.currency || 'usd',
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return sendError(res, 500, 'Failed to fetch year sales data', error);
|
|
104
|
+
}
|
|
105
|
+
}
|
package/components/Sales.js
CHANGED
|
@@ -18,6 +18,7 @@ import countryCodes from '../data/countryCode.json';
|
|
|
18
18
|
import { getCurrencySymbol, getCurrencyLabel } from '../utils/currencyUtils';
|
|
19
19
|
import { SalesTable } from './SalesTable';
|
|
20
20
|
import { DateRangeSalesTable } from './DateRangeSalesTable';
|
|
21
|
+
import YearOverview from './YearOverview';
|
|
21
22
|
import SalesChart from './SalesChart';
|
|
22
23
|
import TopPerformers from './TopPerformers';
|
|
23
24
|
import TypefaceList from './TypefaceList';
|
|
@@ -47,6 +48,7 @@ export default function Sales(props) {
|
|
|
47
48
|
const [previousSalesError, setPreviousSalesError] = useState('');
|
|
48
49
|
const [retryInfo, setRetryInfo] = useState({ retrying: false, attempt: 0, label: '' });
|
|
49
50
|
const [date, setDate] = useState(null);
|
|
51
|
+
const [viewMode, setViewMode] = useState('month'); // 'month' | 'year'
|
|
50
52
|
const [displayLosses, setDisplayLosses] = useState(false);
|
|
51
53
|
|
|
52
54
|
// Max retry attempts for failed API calls
|
|
@@ -671,6 +673,37 @@ export default function Sales(props) {
|
|
|
671
673
|
</IconButton>
|
|
672
674
|
</Tooltip>
|
|
673
675
|
)}
|
|
676
|
+
{/* Month/Year toggle */}
|
|
677
|
+
<Box sx={{
|
|
678
|
+
display: 'flex',
|
|
679
|
+
ml: 1,
|
|
680
|
+
border: '1px solid var(--black, #1a1a1a)',
|
|
681
|
+
borderRadius: '4px',
|
|
682
|
+
overflow: 'hidden',
|
|
683
|
+
}}>
|
|
684
|
+
{['month', 'year'].map(mode => (
|
|
685
|
+
<Box
|
|
686
|
+
key={mode}
|
|
687
|
+
onClick={() => setViewMode(mode)}
|
|
688
|
+
sx={{
|
|
689
|
+
px: 1.5,
|
|
690
|
+
py: 0.5,
|
|
691
|
+
cursor: 'pointer',
|
|
692
|
+
fontSize: '0.75rem',
|
|
693
|
+
fontWeight: viewMode === mode ? 'bold' : 'normal',
|
|
694
|
+
bgcolor: viewMode === mode ? 'var(--black, #1a1a1a)' : 'transparent',
|
|
695
|
+
color: viewMode === mode ? 'var(--white, white)' : 'inherit',
|
|
696
|
+
textTransform: 'uppercase',
|
|
697
|
+
letterSpacing: '0.05em',
|
|
698
|
+
'&:hover': {
|
|
699
|
+
bgcolor: viewMode === mode ? 'var(--black, #1a1a1a)' : 'rgba(0,0,0,0.05)',
|
|
700
|
+
},
|
|
701
|
+
}}
|
|
702
|
+
>
|
|
703
|
+
{mode === 'month' ? 'Mo' : 'Yr'}
|
|
704
|
+
</Box>
|
|
705
|
+
))}
|
|
706
|
+
</Box>
|
|
674
707
|
</Box>
|
|
675
708
|
</Box>
|
|
676
709
|
|
|
@@ -724,7 +757,7 @@ export default function Sales(props) {
|
|
|
724
757
|
</Box>
|
|
725
758
|
}
|
|
726
759
|
</Box>
|
|
727
|
-
), [loading, sales, total, designer, date, message, revenueChangePercent, previousSales, retryInfo]);
|
|
760
|
+
), [loading, sales, total, designer, date, message, revenueChangePercent, previousSales, retryInfo, viewMode]);
|
|
728
761
|
|
|
729
762
|
return (
|
|
730
763
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
|
@@ -750,8 +783,23 @@ export default function Sales(props) {
|
|
|
750
783
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', width: '100%' }}>
|
|
751
784
|
{HeaderSection}
|
|
752
785
|
|
|
753
|
-
{/*
|
|
754
|
-
{
|
|
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 && (
|
|
755
803
|
<>
|
|
756
804
|
|
|
757
805
|
<Box
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Year overview component showing 12-month stacked bar chart by typeface
|
|
2
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import { Box, Typography, CircularProgress, Tooltip } from '@mui/material';
|
|
4
|
+
import { ResponsiveChartContainer } from '@mui/x-charts/ResponsiveChartContainer';
|
|
5
|
+
import { BarPlot } from '@mui/x-charts/BarChart';
|
|
6
|
+
import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis';
|
|
7
|
+
import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis';
|
|
8
|
+
import { ChartsTooltip } from '@mui/x-charts/ChartsTooltip';
|
|
9
|
+
import { ChartsAxisHighlight } from '@mui/x-charts/ChartsAxisHighlight';
|
|
10
|
+
import { getCurrencySymbol } from '../utils/currencyUtils';
|
|
11
|
+
|
|
12
|
+
const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
13
|
+
|
|
14
|
+
// Color palette for typeface segments
|
|
15
|
+
const COLORS = [
|
|
16
|
+
'var(--black, #1a1a1a)',
|
|
17
|
+
'var(--green, #4caf50)',
|
|
18
|
+
'#2196f3',
|
|
19
|
+
'#ff9800',
|
|
20
|
+
'#9c27b0',
|
|
21
|
+
'#f44336',
|
|
22
|
+
'#00bcd4',
|
|
23
|
+
'#795548',
|
|
24
|
+
'#607d8b',
|
|
25
|
+
'#e91e63',
|
|
26
|
+
'#3f51b5',
|
|
27
|
+
'#cddc39',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Year overview component
|
|
32
|
+
* @param {Object} props
|
|
33
|
+
* @param {Object} props.designer - Designer data with user/password
|
|
34
|
+
* @param {number} props.year - Year to display
|
|
35
|
+
* @param {Function} props.onMonthClick - Called when a bar is clicked, receives month index
|
|
36
|
+
*/
|
|
37
|
+
export default function YearOverview({ designer, year, onMonthClick }) {
|
|
38
|
+
const [data, setData] = useState(null);
|
|
39
|
+
const [loading, setLoading] = useState(false);
|
|
40
|
+
const [error, setError] = useState('');
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!designer?.user || !designer?.password || !year) return;
|
|
44
|
+
|
|
45
|
+
setLoading(true);
|
|
46
|
+
setError('');
|
|
47
|
+
|
|
48
|
+
fetch('/api/sales-portal/getYearSales', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
user: designer.user,
|
|
53
|
+
password: designer.password,
|
|
54
|
+
year,
|
|
55
|
+
admin: designer.admin || false,
|
|
56
|
+
}),
|
|
57
|
+
})
|
|
58
|
+
.then(res => res.json())
|
|
59
|
+
.then(res => {
|
|
60
|
+
if (res.success) {
|
|
61
|
+
setData(res.data);
|
|
62
|
+
} else {
|
|
63
|
+
setError(res.message || 'Failed to load year data');
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.catch(err => setError(err.message))
|
|
67
|
+
.finally(() => setLoading(false));
|
|
68
|
+
}, [designer?.user, designer?.password, designer?.admin, year]);
|
|
69
|
+
|
|
70
|
+
// Build chart series from typeface data
|
|
71
|
+
const { series, xAxisData, topTypefaces } = useMemo(() => {
|
|
72
|
+
if (!data?.months) return { series: [], xAxisData: [], topTypefaces: [] };
|
|
73
|
+
|
|
74
|
+
// Get top typefaces by total revenue across the year, cap at 12 for readability
|
|
75
|
+
const typefaceTotals = {};
|
|
76
|
+
data.months.forEach(m => {
|
|
77
|
+
m.typefaces.forEach(t => {
|
|
78
|
+
typefaceTotals[t.name] = (typefaceTotals[t.name] || 0) + t.total;
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const sorted = Object.entries(typefaceTotals)
|
|
83
|
+
.sort((a, b) => b[1] - a[1]);
|
|
84
|
+
|
|
85
|
+
// Top N get individual colors, rest grouped as "Other"
|
|
86
|
+
const MAX_SERIES = 10;
|
|
87
|
+
const topNames = sorted.slice(0, MAX_SERIES).map(([name]) => name);
|
|
88
|
+
const hasOther = sorted.length > MAX_SERIES;
|
|
89
|
+
|
|
90
|
+
const chartSeries = topNames.map((name, i) => ({
|
|
91
|
+
type: 'bar',
|
|
92
|
+
data: data.months.map(m => {
|
|
93
|
+
const tf = m.typefaces.find(t => t.name === name);
|
|
94
|
+
return (tf?.total || 0) / 100;
|
|
95
|
+
}),
|
|
96
|
+
label: name,
|
|
97
|
+
stack: 'total',
|
|
98
|
+
color: COLORS[i % COLORS.length],
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
if (hasOther) {
|
|
102
|
+
chartSeries.push({
|
|
103
|
+
type: 'bar',
|
|
104
|
+
data: data.months.map(m => {
|
|
105
|
+
const otherTotal = m.typefaces
|
|
106
|
+
.filter(t => !topNames.includes(t.name))
|
|
107
|
+
.reduce((sum, t) => sum + t.total, 0);
|
|
108
|
+
return otherTotal / 100;
|
|
109
|
+
}),
|
|
110
|
+
label: 'Other',
|
|
111
|
+
stack: 'total',
|
|
112
|
+
color: '#e0e0e0',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
series: chartSeries,
|
|
118
|
+
xAxisData: MONTH_LABELS,
|
|
119
|
+
topTypefaces: sorted.slice(0, MAX_SERIES),
|
|
120
|
+
};
|
|
121
|
+
}, [data]);
|
|
122
|
+
|
|
123
|
+
if (loading) {
|
|
124
|
+
return (
|
|
125
|
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: 300, gap: 2 }}>
|
|
126
|
+
<CircularProgress size={24} />
|
|
127
|
+
<Typography variant="body1">Loading {year} sales data...</Typography>
|
|
128
|
+
</Box>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (error) {
|
|
133
|
+
return (
|
|
134
|
+
<Box sx={{ p: 4, textAlign: 'center' }}>
|
|
135
|
+
<Typography variant="body1" sx={{ color: 'var(--red, red)' }}>{error}</Typography>
|
|
136
|
+
</Box>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!data) return null;
|
|
141
|
+
|
|
142
|
+
const symbol = getCurrencySymbol(data.currency);
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Box sx={{ width: '100%' }}>
|
|
146
|
+
{/* Year total */}
|
|
147
|
+
<Typography variant="h6" sx={{ mb: 2, opacity: 0.7 }}>
|
|
148
|
+
{year} Total: {symbol}{(data.yearTotal / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}
|
|
149
|
+
{data.yearShipping > 0 && (
|
|
150
|
+
<span style={{ opacity: 0.5, fontSize: '0.8em' }}>
|
|
151
|
+
{' '}(+ {symbol}{(data.yearShipping / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })} shipping)
|
|
152
|
+
</span>
|
|
153
|
+
)}
|
|
154
|
+
</Typography>
|
|
155
|
+
|
|
156
|
+
{/* Stacked bar chart */}
|
|
157
|
+
{series.length > 0 ? (
|
|
158
|
+
<Box sx={{ width: '100%', height: { xs: 250, sm: 350 } }}>
|
|
159
|
+
<ResponsiveChartContainer
|
|
160
|
+
series={series}
|
|
161
|
+
xAxis={[{
|
|
162
|
+
data: xAxisData,
|
|
163
|
+
scaleType: 'band',
|
|
164
|
+
id: 'months',
|
|
165
|
+
}]}
|
|
166
|
+
yAxis={[{
|
|
167
|
+
id: 'revenue',
|
|
168
|
+
valueFormatter: (v) => `${symbol}${v?.toLocaleString() || 0}`,
|
|
169
|
+
}]}
|
|
170
|
+
>
|
|
171
|
+
<BarPlot
|
|
172
|
+
onItemClick={(event, barData) => {
|
|
173
|
+
if (onMonthClick && barData?.dataIndex !== undefined) {
|
|
174
|
+
onMonthClick(barData.dataIndex);
|
|
175
|
+
}
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
<ChartsXAxis axisId="months" />
|
|
179
|
+
<ChartsYAxis axisId="revenue" />
|
|
180
|
+
<ChartsTooltip />
|
|
181
|
+
<ChartsAxisHighlight x="band" />
|
|
182
|
+
</ResponsiveChartContainer>
|
|
183
|
+
</Box>
|
|
184
|
+
) : (
|
|
185
|
+
<Typography variant="body2" sx={{ textAlign: 'center', py: 4, opacity: 0.5 }}>
|
|
186
|
+
No sales data for {year}
|
|
187
|
+
</Typography>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{/* Legend */}
|
|
191
|
+
{topTypefaces.length > 0 && (
|
|
192
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mt: 2 }}>
|
|
193
|
+
{topTypefaces.map(([name, total], i) => (
|
|
194
|
+
<Tooltip key={name} title={`${symbol}${(total / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}`}>
|
|
195
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, cursor: 'default' }}>
|
|
196
|
+
<Box sx={{
|
|
197
|
+
width: 12,
|
|
198
|
+
height: 12,
|
|
199
|
+
borderRadius: '2px',
|
|
200
|
+
bgcolor: COLORS[i % COLORS.length],
|
|
201
|
+
}} />
|
|
202
|
+
<Typography variant="caption">{name}</Typography>
|
|
203
|
+
</Box>
|
|
204
|
+
</Tooltip>
|
|
205
|
+
))}
|
|
206
|
+
</Box>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
{/* Monthly breakdown table */}
|
|
210
|
+
<Box sx={{ mt: 4, opacity: 0.8 }}>
|
|
211
|
+
{data.months.map((m, i) => (
|
|
212
|
+
<Box
|
|
213
|
+
key={i}
|
|
214
|
+
onClick={() => onMonthClick && onMonthClick(i)}
|
|
215
|
+
sx={{
|
|
216
|
+
display: 'flex',
|
|
217
|
+
justifyContent: 'space-between',
|
|
218
|
+
py: 0.75,
|
|
219
|
+
px: 1,
|
|
220
|
+
borderBottom: '1px solid rgba(0,0,0,0.06)',
|
|
221
|
+
cursor: 'pointer',
|
|
222
|
+
'&:hover': { bgcolor: 'rgba(0,0,0,0.03)' },
|
|
223
|
+
borderRadius: '4px',
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<Typography variant="body2">
|
|
227
|
+
{MONTH_LABELS[i]} {year}
|
|
228
|
+
{m.error && <span style={{ color: 'var(--red, red)', marginLeft: 8 }}>error</span>}
|
|
229
|
+
</Typography>
|
|
230
|
+
<Box sx={{ display: 'flex', gap: 3 }}>
|
|
231
|
+
<Typography variant="body2" sx={{ opacity: 0.5 }}>
|
|
232
|
+
{m.salesCount} sale{m.salesCount !== 1 ? 's' : ''}
|
|
233
|
+
</Typography>
|
|
234
|
+
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>
|
|
235
|
+
{symbol}{(m.total / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}
|
|
236
|
+
</Typography>
|
|
237
|
+
</Box>
|
|
238
|
+
</Box>
|
|
239
|
+
))}
|
|
240
|
+
</Box>
|
|
241
|
+
</Box>
|
|
242
|
+
);
|
|
243
|
+
}
|
package/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { default as Sales } from './components/Sales.js';
|
|
|
10
10
|
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
|
+
export { default as YearOverview } from './components/YearOverview.js';
|
|
13
14
|
export { default as SummaryCards } from './components/SummaryCards.js';
|
|
14
15
|
export { default as TopPerformers } from './components/TopPerformers.js';
|
|
15
16
|
export { default as TypefaceList } from './components/TypefaceList.js';
|