@liiift-studio/sales-portal 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +461 -0
- package/SETUP.md +230 -0
- package/api/getAnalytics.d.ts +38 -0
- package/api/getAnalytics.js +346 -0
- package/api/getBalanceTransactions.d.ts +29 -0
- package/api/getBalanceTransactions.js +125 -0
- package/api/getDesignerInfo.d.ts +37 -0
- package/api/getDesignerInfo.js +98 -0
- package/api/getDesigners.d.ts +28 -0
- package/api/getDesigners.js +63 -0
- package/api/getPreviousSales.d.ts +23 -0
- package/api/getPreviousSales.js +82 -0
- package/api/getSales.d.ts +29 -0
- package/api/getSales.js +50 -0
- package/api/getSalesRange.d.ts +23 -0
- package/api/getSalesRange.js +58 -0
- package/api/utils/authMiddleware.js +84 -0
- package/api/utils/dateUtils.js +69 -0
- package/api/utils/feeCalculator.js +148 -0
- package/api/utils/processors/invoiceProcessor.js +337 -0
- package/api/utils/processors/paymentProcessor.js +462 -0
- package/api/utils/salesDataProcessing.js +596 -0
- package/api/utils/salesDataProcessor.js +224 -0
- package/api/utils/stripeFetcher.js +248 -0
- package/components/DateRangeSalesTable.js +1072 -0
- package/components/DebugValues.js +48 -0
- package/components/LicenseTypeList.js +193 -0
- package/components/LoginForm.js +219 -0
- package/components/PeriodComparison.js +501 -0
- package/components/Sales.js +773 -0
- package/components/SalesChart.js +307 -0
- package/components/SalesPortalPage.js +147 -0
- package/components/SalesTable.js +677 -0
- package/components/SummaryCards.js +345 -0
- package/components/TopPerformers.js +331 -0
- package/components/TypefaceList.js +154 -0
- package/components/table-columns.js +70 -0
- package/components/table-row-cells.js +295 -0
- package/data/countryCode.json +318 -0
- package/hooks/useSalesDateQuery.d.ts +20 -0
- package/hooks/useSalesDateQuery.js +71 -0
- package/index.d.ts +172 -0
- package/index.js +33 -0
- package/package.json +87 -0
- package/styles/sales-portal.module.scss +383 -0
- package/styles/sales-portal.theme.d.ts +5 -0
- package/styles/sales-portal.theme.js +799 -0
- package/utils/currencyUtils.d.ts +20 -0
- package/utils/currencyUtils.js +79 -0
- package/utils/salesDataProcessing.d.ts +44 -0
- package/utils/salesDataProcessing.js +596 -0
- package/utils/useSalesDateQuery.js +71 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process raw sales data into chart-ready format
|
|
3
|
+
* @param {Array} sales - Raw sales data
|
|
4
|
+
* @param {Date} date - Current date being displayed
|
|
5
|
+
* @returns {Object} Processed chart data
|
|
6
|
+
*/
|
|
7
|
+
export function processChartData(sales, date) {
|
|
8
|
+
const daysInMonth = new Date(date.getUTCFullYear(), date.getUTCMonth() + 1, 0).getDate();
|
|
9
|
+
|
|
10
|
+
// Initialize data arrays with actual days in month
|
|
11
|
+
let tmpXAxis = Array.from({ length: daysInMonth }, (_, i) => i + 1);
|
|
12
|
+
let tmpSalesData = Array(daysInMonth).fill(0);
|
|
13
|
+
let tmpDiscountLossData = Array(daysInMonth).fill(0);
|
|
14
|
+
let tmpTotalSalesData = Array(daysInMonth).fill(0);
|
|
15
|
+
let tmpOrderData = Array(daysInMonth).fill(0);
|
|
16
|
+
let tmpRegularOrderData = Array(daysInMonth).fill(0);
|
|
17
|
+
let tmpRefundData = Array(daysInMonth).fill(0);
|
|
18
|
+
let tmpRefundTotal = 0;
|
|
19
|
+
let tmpDiscountData = Array(daysInMonth).fill(0);
|
|
20
|
+
let tmpFirstOrderData = Array(daysInMonth).fill(0);
|
|
21
|
+
let tmpDiscountFirstOrderData = Array(daysInMonth).fill(0);
|
|
22
|
+
let tmpTaxData = Array(daysInMonth).fill(0);
|
|
23
|
+
let tmpShippingData = Array(daysInMonth).fill(0);
|
|
24
|
+
let tmpShippedOrdersData = Array(daysInMonth).fill(0);
|
|
25
|
+
let grandTotal = 0;
|
|
26
|
+
let grandTax = 0;
|
|
27
|
+
let grandShipping = 0;
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
// Process data day by day
|
|
31
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
32
|
+
// Skip future dates
|
|
33
|
+
if (
|
|
34
|
+
new Date(date).getUTCFullYear() > new Date().getUTCFullYear() ||
|
|
35
|
+
(new Date(date).getUTCFullYear() >= new Date().getUTCFullYear() &&
|
|
36
|
+
new Date(date).getUTCMonth() > new Date().getUTCMonth()) ||
|
|
37
|
+
(day > new Date().getDate() &&
|
|
38
|
+
new Date(date).getUTCMonth() >= new Date().getUTCMonth() &&
|
|
39
|
+
new Date(date).getUTCFullYear() >= new Date().getUTCFullYear())
|
|
40
|
+
) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Get day's sales
|
|
45
|
+
let todaySales = sales?.filter(
|
|
46
|
+
(item) => new Date(item.created * 1000).getUTCDate() === day
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
let total = 0;
|
|
50
|
+
let discountLost = 0;
|
|
51
|
+
let orders = 0;
|
|
52
|
+
let regularOrders = 0;
|
|
53
|
+
let refunds = 0;
|
|
54
|
+
let refundAmount = 0;
|
|
55
|
+
let discounts = 0;
|
|
56
|
+
let firstOrders = 0;
|
|
57
|
+
let firstOrderDiscounts = 0;
|
|
58
|
+
let tax = 0;
|
|
59
|
+
let shipping = 0;
|
|
60
|
+
let shippedOrders = 0;
|
|
61
|
+
|
|
62
|
+
// Create a map to track unique sales by ID to prevent duplicates
|
|
63
|
+
const processedSales = new Map();
|
|
64
|
+
|
|
65
|
+
// Process each sale
|
|
66
|
+
todaySales?.forEach((item) => {
|
|
67
|
+
// Create a unique identifier for this sale
|
|
68
|
+
const saleKey = `${item.saleType || ''}-${item.id}-${item.lineId || ''}`;
|
|
69
|
+
|
|
70
|
+
// Skip if we've already processed this exact sale
|
|
71
|
+
if (processedSales.has(saleKey)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Mark as processed
|
|
76
|
+
processedSales.set(saleKey, true);
|
|
77
|
+
|
|
78
|
+
if (item?.shippingProvision) {
|
|
79
|
+
shipping += item.total;
|
|
80
|
+
shippedOrders++;
|
|
81
|
+
} else {
|
|
82
|
+
total += item.total;
|
|
83
|
+
orders++;
|
|
84
|
+
regularOrders++;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Process tax
|
|
88
|
+
if (item?.taxAmounts?.length) {
|
|
89
|
+
item.taxAmounts.forEach((taxItem) => {
|
|
90
|
+
tax += taxItem.amount;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Process discounts
|
|
95
|
+
if (!item?.shippingProvision) {
|
|
96
|
+
if (item?.discountAmounts.length) {
|
|
97
|
+
item.discountAmounts.forEach((discount) => {
|
|
98
|
+
discountLost += discount.amount;
|
|
99
|
+
if (item?.customer?.next_invoice_sequence <= 2) firstOrderDiscounts++;
|
|
100
|
+
else discounts++;
|
|
101
|
+
regularOrders--;
|
|
102
|
+
});
|
|
103
|
+
} else if (item?.customer?.next_invoice_sequence <= 2) {
|
|
104
|
+
firstOrders++;
|
|
105
|
+
regularOrders--;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Process refunds and disputes
|
|
110
|
+
if (item?.disputed) {
|
|
111
|
+
refunds++;
|
|
112
|
+
const disputeAmount = item.totalWithTax - item.total;
|
|
113
|
+
refundAmount += disputeAmount;
|
|
114
|
+
total -= disputeAmount;
|
|
115
|
+
}
|
|
116
|
+
item?.refunds?.forEach((refund) => {
|
|
117
|
+
total -= refund.adjustedTotal;
|
|
118
|
+
refundAmount += refund.adjustedTotal;
|
|
119
|
+
refunds++;
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Update running totals
|
|
124
|
+
shipping = shipping / 100;
|
|
125
|
+
grandShipping += shipping;
|
|
126
|
+
tax = tax / 100;
|
|
127
|
+
grandTax += tax;
|
|
128
|
+
discountLost = discountLost / 100;
|
|
129
|
+
total = total / 100;
|
|
130
|
+
grandTotal += total;
|
|
131
|
+
|
|
132
|
+
// Update arrays at index day-1
|
|
133
|
+
const idx = day - 1;
|
|
134
|
+
tmpOrderData[idx] = orders;
|
|
135
|
+
tmpRegularOrderData[idx] = regularOrders;
|
|
136
|
+
tmpFirstOrderData[idx] = firstOrders;
|
|
137
|
+
tmpDiscountData[idx] = discounts;
|
|
138
|
+
tmpDiscountFirstOrderData[idx] = firstOrderDiscounts;
|
|
139
|
+
tmpRefundData[idx] = refunds;
|
|
140
|
+
tmpShippingData[idx] = grandShipping;
|
|
141
|
+
tmpShippedOrdersData[idx] = shippedOrders;
|
|
142
|
+
tmpTaxData[idx] = grandTax;
|
|
143
|
+
tmpDiscountLossData[idx] = discountLost;
|
|
144
|
+
tmpSalesData[idx] = total;
|
|
145
|
+
tmpTotalSalesData[idx] = grandTotal + grandShipping + grandTax;
|
|
146
|
+
tmpRefundTotal += refundAmount;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Calculate changeMax for bar chart scaling
|
|
150
|
+
const changeMax = Math.max(
|
|
151
|
+
Math.max(...tmpOrderData) || 0,
|
|
152
|
+
Math.max(...tmpRegularOrderData) || 0,
|
|
153
|
+
Math.max(...tmpFirstOrderData) || 0,
|
|
154
|
+
Math.max(...tmpDiscountData) || 0,
|
|
155
|
+
Math.max(...tmpDiscountFirstOrderData) || 0,
|
|
156
|
+
Math.max(...tmpShippedOrdersData) || 0,
|
|
157
|
+
Math.max(...tmpRefundData) || 0
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
xAxis: tmpXAxis,
|
|
162
|
+
discountLostData: tmpDiscountLossData,
|
|
163
|
+
salesData: tmpSalesData,
|
|
164
|
+
totalSalesData: tmpTotalSalesData,
|
|
165
|
+
salesMax: Math.max(...tmpTotalSalesData) || 0,
|
|
166
|
+
orderData: tmpOrderData,
|
|
167
|
+
orderMax: Math.max(...tmpOrderData) || 0,
|
|
168
|
+
regularOrderData: tmpRegularOrderData,
|
|
169
|
+
regularOrderMax: Math.max(...tmpRegularOrderData) || 0,
|
|
170
|
+
discountData: tmpDiscountData,
|
|
171
|
+
discountMax: Math.max(...tmpDiscountData) || 0,
|
|
172
|
+
discountFirstOrderData: tmpDiscountFirstOrderData,
|
|
173
|
+
firstOrderDiscountMax: Math.max(...tmpDiscountFirstOrderData) || 0,
|
|
174
|
+
firstOrderData: tmpFirstOrderData,
|
|
175
|
+
firstOrderMax: Math.max(...tmpFirstOrderData) || 0,
|
|
176
|
+
refundData: tmpRefundData,
|
|
177
|
+
refundMax: Math.max(...tmpRefundData) || 0,
|
|
178
|
+
refundTotal: tmpRefundTotal / 100,
|
|
179
|
+
taxData: tmpTaxData,
|
|
180
|
+
shippingData: tmpShippingData,
|
|
181
|
+
shippedOrdersData: tmpShippedOrdersData,
|
|
182
|
+
shippedOrdersMax: Math.max(...tmpShippedOrdersData) || 0,
|
|
183
|
+
changeMax: changeMax,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Process sales data into typeface-specific data with detailed financial breakdowns
|
|
189
|
+
* @param {Array} sales - Raw sales data
|
|
190
|
+
* @param {Array} designers - Designer information
|
|
191
|
+
* @returns {Array} Processed typeface data with various financial totals
|
|
192
|
+
*/
|
|
193
|
+
export function processTypefaceData(sales, designers) {
|
|
194
|
+
let typefaceData = [];
|
|
195
|
+
|
|
196
|
+
// Create a map to track unique sales by ID to prevent duplicates
|
|
197
|
+
const processedSales = new Map();
|
|
198
|
+
|
|
199
|
+
sales?.forEach((sale) => {
|
|
200
|
+
// Create a unique identifier for this sale
|
|
201
|
+
const saleKey = `${sale.saleType || ''}-${sale.id}-${sale.lineId || ''}`;
|
|
202
|
+
|
|
203
|
+
// Skip if we've already processed this exact sale
|
|
204
|
+
if (processedSales.has(saleKey)) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Mark as processed
|
|
209
|
+
processedSales.set(saleKey, true);
|
|
210
|
+
|
|
211
|
+
// Extract typeface title
|
|
212
|
+
var title = '';
|
|
213
|
+
if (sale.description) {
|
|
214
|
+
title = sale.description.substring(0, sale.description.indexOf(' ('));
|
|
215
|
+
if (title === '') title = sale.description.substring(0, sale.description.indexOf(' —'));
|
|
216
|
+
if (title === '') title = sale.description.substring(0, sale.description.indexOf('.'));
|
|
217
|
+
if (title === '') title = sale.description;
|
|
218
|
+
} else {
|
|
219
|
+
title = 'Unknown Typeface';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Calculate tax amounts for this sale
|
|
223
|
+
let saleTax = 0;
|
|
224
|
+
if (sale?.taxAmounts?.length) {
|
|
225
|
+
sale.taxAmounts.forEach((taxItem) => {
|
|
226
|
+
saleTax += taxItem.amount;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Calculate discount amounts for this sale
|
|
231
|
+
let saleDiscounts = 0;
|
|
232
|
+
if (sale?.discountAmounts?.length) {
|
|
233
|
+
sale.discountAmounts.forEach((discount) => {
|
|
234
|
+
saleDiscounts += discount.amount;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Calculate refund amounts for this sale
|
|
239
|
+
let saleRefunds = 0;
|
|
240
|
+
if (sale?.disputed) {
|
|
241
|
+
const disputeAmount = sale.totalWithTax - sale.total;
|
|
242
|
+
saleRefunds += disputeAmount;
|
|
243
|
+
}
|
|
244
|
+
if (sale?.refunds?.length) {
|
|
245
|
+
sale.refunds.forEach((refund) => {
|
|
246
|
+
saleRefunds += refund.adjustedTotal;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Find or create typeface entry
|
|
251
|
+
let existingTypeface = typefaceData.find(item => item.title === title);
|
|
252
|
+
if (existingTypeface) {
|
|
253
|
+
// Update existing typeface data
|
|
254
|
+
existingTypeface.grossTotal += sale.total;
|
|
255
|
+
existingTypeface.taxTotal += saleTax;
|
|
256
|
+
existingTypeface.discountTotal += saleDiscounts;
|
|
257
|
+
existingTypeface.refundTotal += saleRefunds;
|
|
258
|
+
existingTypeface.total += sale.total;
|
|
259
|
+
existingTypeface.orders += 1;
|
|
260
|
+
} else {
|
|
261
|
+
// Create new typeface entry with detailed financials
|
|
262
|
+
typefaceData.push({
|
|
263
|
+
title: title,
|
|
264
|
+
total: sale.total,
|
|
265
|
+
grossTotal: sale.total, // Total before any adjustments
|
|
266
|
+
taxTotal: saleTax,
|
|
267
|
+
discountTotal: saleDiscounts,
|
|
268
|
+
refundTotal: saleRefunds,
|
|
269
|
+
orders: 1,
|
|
270
|
+
author: designers && designers.find(designer => designer._id === sale.author || designer._id === sale?.author?._id) ?
|
|
271
|
+
designers.find(designer => (designer._id === sale.author || designer._id === sale?.author?._id))
|
|
272
|
+
:
|
|
273
|
+
{
|
|
274
|
+
firstName: undefined,
|
|
275
|
+
lastName: undefined,
|
|
276
|
+
_id: sale.author?._id || sale?.author,
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Apply discounts and refunds to the adjusted total
|
|
282
|
+
existingTypeface = typefaceData.find(item => item.title === title);
|
|
283
|
+
sale?.discountAmounts?.forEach(discountAmount => {
|
|
284
|
+
existingTypeface.total -= discountAmount.amount;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
sale?.refunds?.forEach(refund => {
|
|
288
|
+
existingTypeface.total -= refund.adjustedTotal;
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Calculate derived totals
|
|
293
|
+
typefaceData.forEach(typeface => {
|
|
294
|
+
// Total with discounts applied, but before tax and refunds
|
|
295
|
+
typeface.totalWithDiscounts = typeface.grossTotal - typeface.discountTotal;
|
|
296
|
+
|
|
297
|
+
// Total before tax, but after discounts and refunds
|
|
298
|
+
typeface.pretaxTotal = typeface.totalWithDiscounts - typeface.refundTotal;
|
|
299
|
+
|
|
300
|
+
// Net total after all adjustments (same as typeface.total)
|
|
301
|
+
typeface.netTotal = typeface.total;
|
|
302
|
+
|
|
303
|
+
// Convert cents to dollars for all monetary values
|
|
304
|
+
typeface.grossTotal = typeface.grossTotal;
|
|
305
|
+
typeface.taxTotal = typeface.taxTotal;
|
|
306
|
+
typeface.discountTotal = typeface.discountTotal;
|
|
307
|
+
typeface.refundTotal = typeface.refundTotal;
|
|
308
|
+
typeface.totalWithDiscounts = typeface.totalWithDiscounts;
|
|
309
|
+
typeface.pretaxTotal = typeface.pretaxTotal;
|
|
310
|
+
typeface.total = typeface.total;
|
|
311
|
+
typeface.netTotal = typeface.netTotal;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Sort by title
|
|
315
|
+
return typefaceData.sort((a, b) => a.title.localeCompare(b.title));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Process typeface data into designer-specific data
|
|
320
|
+
* @param {Array} typefaceData - Processed typeface data
|
|
321
|
+
* @returns {Array} Processed designer data
|
|
322
|
+
*/
|
|
323
|
+
/**
|
|
324
|
+
* Process sales data into license-type-specific data with financial breakdowns
|
|
325
|
+
* @param {Array} sales - Raw sales data
|
|
326
|
+
* @returns {Array} Processed license type data with financial totals
|
|
327
|
+
*/
|
|
328
|
+
export function processLicenseTypeData(sales) {
|
|
329
|
+
let licenseTypeData = [];
|
|
330
|
+
|
|
331
|
+
// Create a map to track unique sales by ID to prevent duplicates
|
|
332
|
+
const processedSales = new Map();
|
|
333
|
+
|
|
334
|
+
sales?.forEach((sale) => {
|
|
335
|
+
// Create a unique identifier for this sale
|
|
336
|
+
const saleKey = `${sale.saleType || ''}-${sale.id}-${sale.lineId || ''}`;
|
|
337
|
+
|
|
338
|
+
// Skip if we've already processed this exact sale
|
|
339
|
+
if (processedSales.has(saleKey)) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Mark as processed
|
|
344
|
+
processedSales.set(saleKey, true);
|
|
345
|
+
|
|
346
|
+
// Extract license type from description
|
|
347
|
+
let licenseName = 'Unknown';
|
|
348
|
+
|
|
349
|
+
// Try to extract license type from description using various patterns
|
|
350
|
+
// Pattern 1: "Typeface Name (License Type)"
|
|
351
|
+
const parenthesisMatch = sale.description && sale.description.match(/\((.*?)\)/);
|
|
352
|
+
if (parenthesisMatch && parenthesisMatch[1]) {
|
|
353
|
+
licenseName = parenthesisMatch[1].trim();
|
|
354
|
+
}
|
|
355
|
+
// Pattern 2: "Typeface Name — License Type"
|
|
356
|
+
else if (sale.description && sale.description.includes('—')) {
|
|
357
|
+
const parts = sale.description.split('—');
|
|
358
|
+
if (parts.length > 1) {
|
|
359
|
+
licenseName = parts[1].trim();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Pattern 3: If license is explicitly provided in a license property
|
|
363
|
+
else if (sale.license) {
|
|
364
|
+
licenseName = sale.license;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Calculate tax amounts for this sale
|
|
368
|
+
let saleTax = 0;
|
|
369
|
+
if (sale?.taxAmounts?.length) {
|
|
370
|
+
sale.taxAmounts.forEach((taxItem) => {
|
|
371
|
+
saleTax += taxItem.amount;
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Calculate discount amounts for this sale
|
|
376
|
+
let saleDiscounts = 0;
|
|
377
|
+
if (sale?.discountAmounts?.length) {
|
|
378
|
+
sale.discountAmounts.forEach((discount) => {
|
|
379
|
+
saleDiscounts += discount.amount;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Calculate refund amounts for this sale
|
|
384
|
+
let saleRefunds = 0;
|
|
385
|
+
if (sale?.disputed) {
|
|
386
|
+
const disputeAmount = sale.totalWithTax - sale.total;
|
|
387
|
+
saleRefunds += disputeAmount;
|
|
388
|
+
}
|
|
389
|
+
if (sale?.refunds?.length) {
|
|
390
|
+
sale.refunds.forEach((refund) => {
|
|
391
|
+
saleRefunds += refund.adjustedTotal;
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Find or create license type entry
|
|
396
|
+
let existingLicense = licenseTypeData.find(item => item.name === licenseName);
|
|
397
|
+
if (existingLicense) {
|
|
398
|
+
// Update existing license data
|
|
399
|
+
existingLicense.grossTotal += sale.total;
|
|
400
|
+
existingLicense.taxTotal += saleTax;
|
|
401
|
+
existingLicense.discountTotal += saleDiscounts;
|
|
402
|
+
existingLicense.refundTotal += saleRefunds;
|
|
403
|
+
existingLicense.total += sale.total;
|
|
404
|
+
existingLicense.orders += 1;
|
|
405
|
+
} else {
|
|
406
|
+
// Create new license entry with detailed financials
|
|
407
|
+
licenseTypeData.push({
|
|
408
|
+
name: licenseName,
|
|
409
|
+
total: sale.total,
|
|
410
|
+
grossTotal: sale.total, // Total before any adjustments
|
|
411
|
+
taxTotal: saleTax,
|
|
412
|
+
discountTotal: saleDiscounts,
|
|
413
|
+
refundTotal: saleRefunds,
|
|
414
|
+
orders: 1
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Apply discounts and refunds to the adjusted total
|
|
419
|
+
existingLicense = licenseTypeData.find(item => item.name === licenseName);
|
|
420
|
+
sale?.discountAmounts?.forEach(discountAmount => {
|
|
421
|
+
existingLicense.total -= discountAmount.amount;
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
sale?.refunds?.forEach(refund => {
|
|
425
|
+
existingLicense.total -= refund.adjustedTotal;
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Calculate derived totals
|
|
430
|
+
licenseTypeData.forEach(license => {
|
|
431
|
+
// Total with discounts applied, but before tax and refunds
|
|
432
|
+
license.totalWithDiscounts = license.grossTotal - license.discountTotal;
|
|
433
|
+
|
|
434
|
+
// Total before tax, but after discounts and refunds
|
|
435
|
+
license.pretaxTotal = license.totalWithDiscounts - license.refundTotal;
|
|
436
|
+
|
|
437
|
+
// Net total after all adjustments (same as license.total)
|
|
438
|
+
license.netTotal = license.total;
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Sort by name
|
|
442
|
+
return licenseTypeData.sort((a, b) => a.name.localeCompare(b.name));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function processDesignersData(typefaceData) {
|
|
446
|
+
let designersData = [];
|
|
447
|
+
|
|
448
|
+
typefaceData?.forEach(typeface => {
|
|
449
|
+
let designer = designersData.find(author => author._id === typeface.author._id);
|
|
450
|
+
if (designer) {
|
|
451
|
+
designer.total += typeface.total;
|
|
452
|
+
designer.orders += typeface.orders;
|
|
453
|
+
} else {
|
|
454
|
+
designersData.push({
|
|
455
|
+
_id: typeface.author._id,
|
|
456
|
+
firstName: typeface.author.firstName,
|
|
457
|
+
lastName: typeface.author.lastName,
|
|
458
|
+
total: typeface.total,
|
|
459
|
+
orders: typeface.orders,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return designersData;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Generate chart series data
|
|
469
|
+
* @param {Object} chartState - Current chart state
|
|
470
|
+
* @param {boolean} displayLosses - Whether to display loss data
|
|
471
|
+
* @returns {Array} Chart series data
|
|
472
|
+
*/
|
|
473
|
+
export function generateSeriesData(chartState, displayLosses) {
|
|
474
|
+
const {
|
|
475
|
+
discountData,
|
|
476
|
+
discountFirstOrderData,
|
|
477
|
+
firstOrderData,
|
|
478
|
+
regularOrderData,
|
|
479
|
+
shippedOrdersData,
|
|
480
|
+
refundData,
|
|
481
|
+
shippingData,
|
|
482
|
+
taxData,
|
|
483
|
+
totalSalesData,
|
|
484
|
+
salesData,
|
|
485
|
+
discountLostData,
|
|
486
|
+
changeMax,
|
|
487
|
+
salesMax,
|
|
488
|
+
xAxis
|
|
489
|
+
} = chartState;
|
|
490
|
+
|
|
491
|
+
// Calculate start date as first day of the selected month
|
|
492
|
+
const startDate = new Date(chartState.xAxis[0]);
|
|
493
|
+
|
|
494
|
+
return [
|
|
495
|
+
!!(discountData?.length > 0 && discountData.some(value => value !== 0)) && {
|
|
496
|
+
type: 'bar',
|
|
497
|
+
data: (discountData && changeMax && salesMax) ? discountData.map((v) => (v / changeMax) * salesMax) : [0],
|
|
498
|
+
valueFormatter: (v) => v != null ? `${Math.round((v * changeMax) / salesMax)}` : '0',
|
|
499
|
+
color: 'url(#discountHatch) rgba(var(--blackRGB, 0,0,0), 0.1)',
|
|
500
|
+
label: 'Discounted Orders',
|
|
501
|
+
id: 'discountsId',
|
|
502
|
+
stack: 'total',
|
|
503
|
+
startDate: startDate
|
|
504
|
+
},
|
|
505
|
+
!!(discountFirstOrderData?.length > 0 && discountFirstOrderData.some(value => value !== 0)) && {
|
|
506
|
+
type: 'bar',
|
|
507
|
+
data: (discountFirstOrderData && changeMax && salesMax) ? discountFirstOrderData.map((v) => (v / changeMax) * salesMax) : [0],
|
|
508
|
+
valueFormatter: (v) => v != null ? `${Math.round((v * changeMax) / salesMax)}` : '0',
|
|
509
|
+
color: 'url(#discountFirstOrderHatch) rgba(var(--blueRGB, 0,0,255), .25)',
|
|
510
|
+
label: 'Discounted First Orders',
|
|
511
|
+
id: 'discountFirstOrderId',
|
|
512
|
+
stack: 'total',
|
|
513
|
+
startDate: startDate
|
|
514
|
+
},
|
|
515
|
+
!!(firstOrderData?.length > 0 && firstOrderData.some(value => value !== 0)) && {
|
|
516
|
+
type: 'bar',
|
|
517
|
+
data: (firstOrderData && changeMax && salesMax) ? firstOrderData.map((v) => (v / changeMax) * salesMax) : [0],
|
|
518
|
+
valueFormatter: (v) => v != null ? `${Math.round((v * changeMax) / salesMax)}` : '0',
|
|
519
|
+
color: 'rgba(var(--blueRGB, 0,0,255), .25)',
|
|
520
|
+
label: 'First Orders',
|
|
521
|
+
id: 'firstOrderId',
|
|
522
|
+
stack: 'total',
|
|
523
|
+
startDate: startDate
|
|
524
|
+
},
|
|
525
|
+
!!(regularOrderData?.length > 0 && regularOrderData.some(value => value !== 0)) && {
|
|
526
|
+
type: 'bar',
|
|
527
|
+
data: (regularOrderData && changeMax && salesMax) ? regularOrderData.map((v) => (v / changeMax) * salesMax) : [0],
|
|
528
|
+
valueFormatter: (v) => v != null ? `${Math.round((v * changeMax) / salesMax)}` : '0',
|
|
529
|
+
color: 'rgba(var(--blackRGB, 0,0,0), 0.25)',
|
|
530
|
+
label: 'Orders',
|
|
531
|
+
id: 'ordersId',
|
|
532
|
+
stack: 'total',
|
|
533
|
+
startDate: startDate
|
|
534
|
+
},
|
|
535
|
+
!!(shippedOrdersData?.length > 0 && shippedOrdersData.some(value => value !== 0)) && {
|
|
536
|
+
type: 'bar',
|
|
537
|
+
data: (shippedOrdersData && changeMax && salesMax) ? shippedOrdersData.map((v) => (v / changeMax) * salesMax) : [0],
|
|
538
|
+
valueFormatter: (v) => v != null ? `${Math.round((v * changeMax) / salesMax)}` : '0',
|
|
539
|
+
color: 'rgba(var(--orangeRGB, 255,165,0), .75)',
|
|
540
|
+
label: 'Shipped Orders',
|
|
541
|
+
id: 'shippedOrdersId',
|
|
542
|
+
stack: 'total',
|
|
543
|
+
startDate: startDate
|
|
544
|
+
},
|
|
545
|
+
!!(refundData?.length > 0 && refundData.some(value => value !== 0)) && {
|
|
546
|
+
type: 'bar',
|
|
547
|
+
data: (refundData && changeMax && salesMax) ? refundData.map((v) => -((v / changeMax) * salesMax)) : [0],
|
|
548
|
+
valueFormatter: (v) => v != null ? `${-1 * Math.round((v * changeMax) / salesMax)}` : '0',
|
|
549
|
+
color: 'rgba(var(--redRGB, 255,0,0), .75)',
|
|
550
|
+
label: 'Refunds & Disputes',
|
|
551
|
+
id: 'refundsId',
|
|
552
|
+
stack: 'total',
|
|
553
|
+
startDate: startDate
|
|
554
|
+
},
|
|
555
|
+
!!(shippingData?.length > 0 && shippingData.some(value => value !== 0)) && {
|
|
556
|
+
type: 'line',
|
|
557
|
+
data: shippingData,
|
|
558
|
+
valueFormatter: (v) => v != null ? `$${v.toLocaleString('en-US', { minimumFractionDigits: 2 })}` : '$0.00',
|
|
559
|
+
color: 'var(--orange, orange)',
|
|
560
|
+
label: 'Accrued Shipping',
|
|
561
|
+
startDate: startDate
|
|
562
|
+
},
|
|
563
|
+
!!(taxData?.length > 0 && taxData.some(value => value !== 0)) && {
|
|
564
|
+
type: 'line',
|
|
565
|
+
data: taxData,
|
|
566
|
+
valueFormatter: (v) => v != null ? `$${v.toLocaleString('en-US', { minimumFractionDigits: 2 })}` : '$0.00',
|
|
567
|
+
color: 'var(--red, red)',
|
|
568
|
+
label: 'Accrued Tax',
|
|
569
|
+
startDate: startDate
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
type: 'line',
|
|
573
|
+
data: totalSalesData,
|
|
574
|
+
valueFormatter: (v) => v != null ? `$${v.toLocaleString('en-US', { minimumFractionDigits: 2 })}` : '$0.00',
|
|
575
|
+
color: 'var(--green, green)',
|
|
576
|
+
label: 'Gross Accrued Sales',
|
|
577
|
+
startDate: startDate
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
type: 'line',
|
|
581
|
+
data: salesData,
|
|
582
|
+
valueFormatter: (v) => v != null ? `$${v.toLocaleString('en-US', { minimumFractionDigits: 2 })}` : '$0.00',
|
|
583
|
+
color: 'var(--blue, blue)',
|
|
584
|
+
label: "Day's Net Sales",
|
|
585
|
+
startDate: startDate
|
|
586
|
+
},
|
|
587
|
+
!!(displayLosses) && {
|
|
588
|
+
type: 'line',
|
|
589
|
+
data: discountLostData,
|
|
590
|
+
valueFormatter: (v) => v != null ? `$${v.toLocaleString('en-US', { minimumFractionDigits: 2 })}` : '$0.00',
|
|
591
|
+
color: 'var(--black, black)',
|
|
592
|
+
label: 'Missed Discounted Earnings',
|
|
593
|
+
startDate: startDate
|
|
594
|
+
},
|
|
595
|
+
].filter(Boolean);
|
|
596
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Hook for managing sales date filtering via URL query parameters
|
|
2
|
+
import { useRouter } from 'next/router';
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Custom hook that manages month/year selection via URL query parameters
|
|
7
|
+
* Provides synchronized state for date selection and URL updates
|
|
8
|
+
* @returns {Object} Date state and update function
|
|
9
|
+
*/
|
|
10
|
+
export function useSalesDateQuery() {
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
|
|
13
|
+
// State for date management
|
|
14
|
+
const [month, setMonth] = useState(-1);
|
|
15
|
+
const [year, setYear] = useState(-1);
|
|
16
|
+
const [date, setDate] = useState(null);
|
|
17
|
+
|
|
18
|
+
// Initialize from URL query parameters when router is ready
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!router.isReady) return;
|
|
21
|
+
|
|
22
|
+
const queryMonth = router.query.month !== undefined ? parseInt(router.query.month) : -1;
|
|
23
|
+
const queryYear = router.query.year !== undefined ? parseInt(router.query.year) : -1;
|
|
24
|
+
|
|
25
|
+
// If we have valid query params, use them
|
|
26
|
+
if (queryMonth >= 0 && queryMonth <= 11 && queryYear > 2000) {
|
|
27
|
+
setMonth(queryMonth);
|
|
28
|
+
setYear(queryYear);
|
|
29
|
+
} else if (month === -1 || year === -1) {
|
|
30
|
+
// Otherwise use current date (only if not already set)
|
|
31
|
+
const currentDate = new Date();
|
|
32
|
+
setMonth(currentDate.getUTCMonth());
|
|
33
|
+
setYear(currentDate.getUTCFullYear());
|
|
34
|
+
}
|
|
35
|
+
}, [router.isReady, router.query]);
|
|
36
|
+
|
|
37
|
+
// Update date object when month/year change
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (month >= 0 && year > 0) {
|
|
40
|
+
const newDate = new Date(Date.UTC(year, month, 1));
|
|
41
|
+
setDate(newDate);
|
|
42
|
+
}
|
|
43
|
+
}, [month, year]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Updates the date state and URL query parameters
|
|
47
|
+
* @param {Date} newDate - The new date to set
|
|
48
|
+
*/
|
|
49
|
+
const updateDate = (newDate) => {
|
|
50
|
+
const newMonth = new Date(newDate).getUTCMonth();
|
|
51
|
+
const newYear = new Date(newDate).getUTCFullYear();
|
|
52
|
+
|
|
53
|
+
// Update state
|
|
54
|
+
setMonth(newMonth);
|
|
55
|
+
setYear(newYear);
|
|
56
|
+
|
|
57
|
+
// Update URL query parameters without page reload
|
|
58
|
+
router.push(
|
|
59
|
+
{
|
|
60
|
+
pathname: router.pathname,
|
|
61
|
+
query: { ...router.query, month: newMonth, year: newYear }
|
|
62
|
+
},
|
|
63
|
+
undefined,
|
|
64
|
+
{ shallow: true }
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return { month, year, date, updateDate };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default useSalesDateQuery;
|