Compare commits
3 Commits
3991341376
...
2ff325a132
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ff325a132 | |||
| 5d46a2a7e5 | |||
| 512b351429 |
@@ -12,6 +12,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"luxon": "^3.5.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"mysql2": "^3.6.5",
|
"mysql2": "^3.6.5",
|
||||||
"ssh2": "^1.14.0"
|
"ssh2": "^1.14.0"
|
||||||
@@ -826,6 +827,15 @@
|
|||||||
"url": "https://github.com/sponsors/wellwelwel"
|
"url": "https://github.com/sponsors/wellwelwel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/luxon": {
|
||||||
|
"version": "3.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
|
||||||
|
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"ssh2": "^1.14.0",
|
"ssh2": "^1.14.0",
|
||||||
"mysql2": "^3.6.5",
|
"mysql2": "^3.6.5",
|
||||||
"compression": "^1.7.4"
|
"compression": "^1.7.4",
|
||||||
|
"luxon": "^3.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1"
|
"nodemon": "^3.0.1"
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { DateTime } = require('luxon');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { getDbConnection, getPoolStatus } = require('../db/connection');
|
const { getDbConnection, getPoolStatus } = require('../db/connection');
|
||||||
const { getTimeRangeConditions, formatBusinessDate, getBusinessDayBounds } = require('../utils/timeUtils');
|
const {
|
||||||
|
getTimeRangeConditions,
|
||||||
|
formatBusinessDate,
|
||||||
|
getBusinessDayBounds,
|
||||||
|
_internal: timeHelpers
|
||||||
|
} = require('../utils/timeUtils');
|
||||||
|
|
||||||
|
const TIMEZONE = 'America/New_York';
|
||||||
|
const BUSINESS_DAY_START_HOUR = timeHelpers?.BUSINESS_DAY_START_HOUR ?? 1;
|
||||||
|
|
||||||
// Image URL generation utility
|
// Image URL generation utility
|
||||||
const getImageUrls = (pid, iid = 1) => {
|
const getImageUrls = (pid, iid = 1) => {
|
||||||
@@ -421,6 +431,24 @@ router.get('/financials', async (req, res) => {
|
|||||||
const { whereClause, params, dateRange } = getTimeRangeConditions(timeRange, startDate, endDate);
|
const { whereClause, params, dateRange } = getTimeRangeConditions(timeRange, startDate, endDate);
|
||||||
const financialWhere = whereClause.replace(/date_placed/g, 'date_change');
|
const financialWhere = whereClause.replace(/date_placed/g, 'date_change');
|
||||||
|
|
||||||
|
const formatDebugBound = (value) => {
|
||||||
|
if (!value) return 'n/a';
|
||||||
|
const parsed = DateTime.fromSQL(value, { zone: 'UTC-05:00' });
|
||||||
|
if (!parsed.isValid) {
|
||||||
|
return `invalid(${value})`;
|
||||||
|
}
|
||||||
|
return parsed.setZone(TIMEZONE).toISO();
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[FINANCIALS] request params', {
|
||||||
|
timeRange: timeRange || 'default',
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
whereClause: financialWhere,
|
||||||
|
params,
|
||||||
|
boundsEastern: Array.isArray(params) ? params.map(formatDebugBound) : [],
|
||||||
|
});
|
||||||
|
|
||||||
const [totalsRows] = await connection.execute(
|
const [totalsRows] = await connection.execute(
|
||||||
buildFinancialTotalsQuery(financialWhere),
|
buildFinancialTotalsQuery(financialWhere),
|
||||||
params
|
params
|
||||||
@@ -428,6 +456,11 @@ router.get('/financials', async (req, res) => {
|
|||||||
|
|
||||||
const totals = normalizeFinancialTotals(totalsRows[0]);
|
const totals = normalizeFinancialTotals(totalsRows[0]);
|
||||||
|
|
||||||
|
console.log('[FINANCIALS] totals query result', {
|
||||||
|
rows: totalsRows.length,
|
||||||
|
totals,
|
||||||
|
});
|
||||||
|
|
||||||
const [trendRows] = await connection.execute(
|
const [trendRows] = await connection.execute(
|
||||||
buildFinancialTrendQuery(financialWhere),
|
buildFinancialTrendQuery(financialWhere),
|
||||||
params
|
params
|
||||||
@@ -435,11 +468,25 @@ router.get('/financials', async (req, res) => {
|
|||||||
|
|
||||||
const trend = trendRows.map(normalizeFinancialTrendRow);
|
const trend = trendRows.map(normalizeFinancialTrendRow);
|
||||||
|
|
||||||
|
console.log('[FINANCIALS] trend query result', {
|
||||||
|
rows: trendRows.length,
|
||||||
|
first: trend[0] || null,
|
||||||
|
last: trend[trend.length - 1] || null,
|
||||||
|
});
|
||||||
|
|
||||||
let previousTotals = null;
|
let previousTotals = null;
|
||||||
let comparison = null;
|
let comparison = null;
|
||||||
|
|
||||||
const previousRange = getPreviousPeriodRange(timeRange, startDate, endDate);
|
const previousRange = getPreviousPeriodRange(timeRange, startDate, endDate);
|
||||||
if (previousRange) {
|
if (previousRange) {
|
||||||
|
console.log('[FINANCIALS] previous range params', {
|
||||||
|
timeRange: timeRange || 'default',
|
||||||
|
prevWhere: previousRange.whereClause.replace(/date_placed/g, 'date_change'),
|
||||||
|
params: previousRange.params,
|
||||||
|
boundsEastern: Array.isArray(previousRange.params)
|
||||||
|
? previousRange.params.map(formatDebugBound)
|
||||||
|
: [],
|
||||||
|
});
|
||||||
const prevWhere = previousRange.whereClause.replace(/date_placed/g, 'date_change');
|
const prevWhere = previousRange.whereClause.replace(/date_placed/g, 'date_change');
|
||||||
const [previousRows] = await connection.execute(
|
const [previousRows] = await connection.execute(
|
||||||
buildFinancialTotalsQuery(prevWhere),
|
buildFinancialTotalsQuery(prevWhere),
|
||||||
@@ -450,19 +497,45 @@ router.get('/financials', async (req, res) => {
|
|||||||
grossSales: calculateComparison(totals.grossSales, previousTotals.grossSales),
|
grossSales: calculateComparison(totals.grossSales, previousTotals.grossSales),
|
||||||
refunds: calculateComparison(totals.refunds, previousTotals.refunds),
|
refunds: calculateComparison(totals.refunds, previousTotals.refunds),
|
||||||
taxCollected: calculateComparison(totals.taxCollected, previousTotals.taxCollected),
|
taxCollected: calculateComparison(totals.taxCollected, previousTotals.taxCollected),
|
||||||
|
discounts: calculateComparison(totals.discounts, previousTotals.discounts),
|
||||||
cogs: calculateComparison(totals.cogs, previousTotals.cogs),
|
cogs: calculateComparison(totals.cogs, previousTotals.cogs),
|
||||||
netRevenue: calculateComparison(totals.netRevenue, previousTotals.netRevenue),
|
income: calculateComparison(totals.income, previousTotals.income),
|
||||||
profit: calculateComparison(totals.profit, previousTotals.profit),
|
profit: calculateComparison(totals.profit, previousTotals.profit),
|
||||||
margin: calculateComparison(totals.margin, previousTotals.margin),
|
margin: calculateComparison(totals.margin, previousTotals.margin),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trendDebugSample = trend.slice(-3).map((item) => ({
|
||||||
|
date: item.date,
|
||||||
|
timestamp: item.timestamp,
|
||||||
|
income: item.income,
|
||||||
|
grossSales: item.grossSales,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const debugInfo = {
|
||||||
|
serverTimeUtc: new Date().toISOString(),
|
||||||
|
timeRange: timeRange || 'default',
|
||||||
|
params,
|
||||||
|
boundsEastern: Array.isArray(params) ? params.map(formatDebugBound) : [],
|
||||||
|
trendCount: trend.length,
|
||||||
|
trendSample: trendDebugSample,
|
||||||
|
previousRange: previousRange
|
||||||
|
? {
|
||||||
|
params: previousRange.params,
|
||||||
|
boundsEastern: Array.isArray(previousRange.params)
|
||||||
|
? previousRange.params.map(formatDebugBound)
|
||||||
|
: [],
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
dateRange,
|
dateRange,
|
||||||
totals,
|
totals,
|
||||||
previousTotals,
|
previousTotals,
|
||||||
comparison,
|
comparison,
|
||||||
trend,
|
trend,
|
||||||
|
debug: debugInfo,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in /financials:', error);
|
console.error('Error in /financials:', error);
|
||||||
@@ -661,44 +734,35 @@ function processShippingData(shippingResult, totalShipped) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function calculatePeriodProgress(timeRange) {
|
function calculatePeriodProgress(timeRange) {
|
||||||
const now = new Date();
|
if (!['today', 'thisWeek', 'thisMonth'].includes(timeRange)) {
|
||||||
const easternTime = new Date(now.getTime() - (5 * 60 * 60 * 1000)); // UTC-5
|
|
||||||
|
|
||||||
switch (timeRange) {
|
|
||||||
case 'today': {
|
|
||||||
const { start } = getBusinessDayBounds('today');
|
|
||||||
const businessStart = new Date(start);
|
|
||||||
const businessEnd = new Date(businessStart);
|
|
||||||
businessEnd.setDate(businessEnd.getDate() + 1);
|
|
||||||
businessEnd.setHours(0, 59, 59, 999); // 12:59 AM next day
|
|
||||||
|
|
||||||
const elapsed = easternTime.getTime() - businessStart.getTime();
|
|
||||||
const total = businessEnd.getTime() - businessStart.getTime();
|
|
||||||
return Math.min(100, Math.max(0, (elapsed / total) * 100));
|
|
||||||
}
|
|
||||||
case 'thisWeek': {
|
|
||||||
const startOfWeek = new Date(easternTime);
|
|
||||||
startOfWeek.setDate(easternTime.getDate() - easternTime.getDay()); // Sunday
|
|
||||||
startOfWeek.setHours(1, 0, 0, 0); // 1 AM business day start
|
|
||||||
|
|
||||||
const endOfWeek = new Date(startOfWeek);
|
|
||||||
endOfWeek.setDate(endOfWeek.getDate() + 7);
|
|
||||||
|
|
||||||
const elapsed = easternTime.getTime() - startOfWeek.getTime();
|
|
||||||
const total = endOfWeek.getTime() - startOfWeek.getTime();
|
|
||||||
return Math.min(100, Math.max(0, (elapsed / total) * 100));
|
|
||||||
}
|
|
||||||
case 'thisMonth': {
|
|
||||||
const startOfMonth = new Date(easternTime.getFullYear(), easternTime.getMonth(), 1, 1, 0, 0, 0);
|
|
||||||
const endOfMonth = new Date(easternTime.getFullYear(), easternTime.getMonth() + 1, 1, 0, 59, 59, 999);
|
|
||||||
|
|
||||||
const elapsed = easternTime.getTime() - startOfMonth.getTime();
|
|
||||||
const total = endOfMonth.getTime() - startOfMonth.getTime();
|
|
||||||
return Math.min(100, Math.max(0, (elapsed / total) * 100));
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return 100;
|
return 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = DateTime.now().setZone(TIMEZONE);
|
||||||
|
|
||||||
|
let range;
|
||||||
|
try {
|
||||||
|
range = timeHelpers.getRangeForTimeRange(timeRange, now);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[STATS] Failed to derive range for ${timeRange}:`, error);
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!range?.start || !range?.end) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = range.end.toMillis() - range.start.toMillis();
|
||||||
|
if (total <= 0) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = Math.min(
|
||||||
|
Math.max(now.toMillis() - range.start.toMillis(), 0),
|
||||||
|
total
|
||||||
|
);
|
||||||
|
|
||||||
|
return Math.min(100, Math.max(0, (elapsed / total) * 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFinancialTotalsQuery(whereClause) {
|
function buildFinancialTotalsQuery(whereClause) {
|
||||||
@@ -706,45 +770,58 @@ function buildFinancialTotalsQuery(whereClause) {
|
|||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(sale_amount), 0) as grossSales,
|
COALESCE(SUM(sale_amount), 0) as grossSales,
|
||||||
COALESCE(SUM(refund_amount), 0) as refunds,
|
COALESCE(SUM(refund_amount), 0) as refunds,
|
||||||
|
COALESCE(SUM(shipping_collected_amount + small_order_fee_amount + rush_fee_amount), 0) as shippingFees,
|
||||||
COALESCE(SUM(tax_collected_amount), 0) as taxCollected,
|
COALESCE(SUM(tax_collected_amount), 0) as taxCollected,
|
||||||
|
COALESCE(SUM(discount_total_amount), 0) as discounts,
|
||||||
COALESCE(SUM(cogs_amount), 0) as cogs
|
COALESCE(SUM(cogs_amount), 0) as cogs
|
||||||
FROM report_sales_data
|
FROM report_sales_data
|
||||||
WHERE ${whereClause}
|
WHERE ${whereClause}
|
||||||
|
AND action IN (1, 2, 3)
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFinancialTrendQuery(whereClause) {
|
function buildFinancialTrendQuery(whereClause) {
|
||||||
|
const businessDayOffset = BUSINESS_DAY_START_HOUR;
|
||||||
return `
|
return `
|
||||||
SELECT
|
SELECT
|
||||||
DATE(date_change) as date,
|
DATE_FORMAT(
|
||||||
|
DATE_SUB(date_change, INTERVAL ${businessDayOffset} HOUR),
|
||||||
|
'%Y-%m-%d'
|
||||||
|
) as businessDate,
|
||||||
SUM(sale_amount) as grossSales,
|
SUM(sale_amount) as grossSales,
|
||||||
SUM(refund_amount) as refunds,
|
SUM(refund_amount) as refunds,
|
||||||
|
SUM(shipping_collected_amount + small_order_fee_amount + rush_fee_amount) as shippingFees,
|
||||||
SUM(tax_collected_amount) as taxCollected,
|
SUM(tax_collected_amount) as taxCollected,
|
||||||
|
SUM(discount_total_amount) as discounts,
|
||||||
SUM(cogs_amount) as cogs
|
SUM(cogs_amount) as cogs
|
||||||
FROM report_sales_data
|
FROM report_sales_data
|
||||||
WHERE ${whereClause}
|
WHERE ${whereClause}
|
||||||
GROUP BY DATE(date_change)
|
AND action IN (1, 2, 3)
|
||||||
ORDER BY date ASC
|
GROUP BY businessDate
|
||||||
|
ORDER BY businessDate ASC
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeFinancialTotals(row = {}) {
|
function normalizeFinancialTotals(row = {}) {
|
||||||
const grossSales = parseFloat(row.grossSales || 0);
|
const grossSales = parseFloat(row.grossSales || 0);
|
||||||
const refunds = parseFloat(row.refunds || 0);
|
const refunds = parseFloat(row.refunds || 0);
|
||||||
|
const shippingFees = parseFloat(row.shippingFees || 0);
|
||||||
const taxCollected = parseFloat(row.taxCollected || 0);
|
const taxCollected = parseFloat(row.taxCollected || 0);
|
||||||
|
const discounts = parseFloat(row.discounts || 0);
|
||||||
const cogs = parseFloat(row.cogs || 0);
|
const cogs = parseFloat(row.cogs || 0);
|
||||||
const netSales = grossSales - refunds;
|
const productNet = grossSales - refunds - discounts;
|
||||||
const netRevenue = netSales - taxCollected;
|
const income = productNet + shippingFees;
|
||||||
const profit = netRevenue - cogs;
|
const profit = income - cogs;
|
||||||
const margin = netRevenue !== 0 ? (profit / netRevenue) * 100 : 0;
|
const margin = income !== 0 ? (profit / income) * 100 : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
grossSales,
|
grossSales,
|
||||||
refunds,
|
refunds,
|
||||||
|
shippingFees,
|
||||||
taxCollected,
|
taxCollected,
|
||||||
|
discounts,
|
||||||
cogs,
|
cogs,
|
||||||
netSales,
|
income,
|
||||||
netRevenue,
|
|
||||||
profit,
|
profit,
|
||||||
margin,
|
margin,
|
||||||
};
|
};
|
||||||
@@ -753,28 +830,64 @@ function normalizeFinancialTotals(row = {}) {
|
|||||||
function normalizeFinancialTrendRow(row = {}) {
|
function normalizeFinancialTrendRow(row = {}) {
|
||||||
const grossSales = parseFloat(row.grossSales || 0);
|
const grossSales = parseFloat(row.grossSales || 0);
|
||||||
const refunds = parseFloat(row.refunds || 0);
|
const refunds = parseFloat(row.refunds || 0);
|
||||||
|
const shippingFees = parseFloat(row.shippingFees || 0);
|
||||||
const taxCollected = parseFloat(row.taxCollected || 0);
|
const taxCollected = parseFloat(row.taxCollected || 0);
|
||||||
|
const discounts = parseFloat(row.discounts || 0);
|
||||||
const cogs = parseFloat(row.cogs || 0);
|
const cogs = parseFloat(row.cogs || 0);
|
||||||
const netSales = grossSales - refunds;
|
const productNet = grossSales - refunds - discounts;
|
||||||
const netRevenue = netSales - taxCollected;
|
const income = productNet + shippingFees;
|
||||||
const profit = netRevenue - cogs;
|
const profit = income - cogs;
|
||||||
const margin = netRevenue !== 0 ? (profit / netRevenue) * 100 : 0;
|
const margin = income !== 0 ? (profit / income) * 100 : 0;
|
||||||
let timestamp = null;
|
let timestamp = null;
|
||||||
|
let dateValue = row.businessDate || row.date || null;
|
||||||
|
|
||||||
if (row.date instanceof Date) {
|
const resolveBusinessDayStart = (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dt;
|
||||||
|
if (value instanceof Date) {
|
||||||
|
dt = DateTime.fromJSDate(value, { zone: TIMEZONE });
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
dt = DateTime.fromISO(value, { zone: TIMEZONE });
|
||||||
|
if (!dt.isValid) {
|
||||||
|
dt = DateTime.fromSQL(value, { zone: TIMEZONE });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dt || !dt.isValid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hour = BUSINESS_DAY_START_HOUR;
|
||||||
|
return dt.set({
|
||||||
|
hour,
|
||||||
|
minute: 0,
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const businessDayStart = resolveBusinessDayStart(dateValue);
|
||||||
|
if (businessDayStart) {
|
||||||
|
timestamp = businessDayStart.toUTC().toISO();
|
||||||
|
dateValue = businessDayStart.toISO();
|
||||||
|
} else if (row.date instanceof Date) {
|
||||||
timestamp = new Date(row.date.getTime()).toISOString();
|
timestamp = new Date(row.date.getTime()).toISOString();
|
||||||
} else if (typeof row.date === 'string') {
|
} else if (typeof row.date === 'string') {
|
||||||
timestamp = new Date(`${row.date}T00:00:00Z`).toISOString();
|
timestamp = new Date(`${row.date}T00:00:00Z`).toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date: row.date,
|
date: dateValue,
|
||||||
grossSales,
|
grossSales,
|
||||||
refunds,
|
refunds,
|
||||||
|
shippingFees,
|
||||||
taxCollected,
|
taxCollected,
|
||||||
|
discounts,
|
||||||
cogs,
|
cogs,
|
||||||
netSales,
|
income,
|
||||||
netRevenue,
|
|
||||||
profit,
|
profit,
|
||||||
margin,
|
margin,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
|||||||
@@ -1,217 +1,219 @@
|
|||||||
// Time utilities for handling business day logic and time ranges
|
const { DateTime } = require('luxon');
|
||||||
// Business day is 1am-12:59am Eastern time (UTC-5)
|
|
||||||
|
|
||||||
const getBusinessDayBounds = (timeRange) => {
|
const TIMEZONE = 'America/New_York';
|
||||||
const now = new Date();
|
const DB_TIMEZONE = 'UTC-05:00';
|
||||||
const easternTime = new Date(now.getTime() - (5 * 60 * 60 * 1000)); // UTC-5
|
const BUSINESS_DAY_START_HOUR = 1; // 1 AM Eastern
|
||||||
|
const WEEK_START_DAY = 7; // Sunday (Luxon uses 1 = Monday, 7 = Sunday)
|
||||||
|
const DB_DATETIME_FORMAT = 'yyyy-LL-dd HH:mm:ss';
|
||||||
|
|
||||||
|
const isDateTime = (value) => DateTime.isDateTime(value);
|
||||||
|
|
||||||
|
const ensureDateTime = (value, { zone = TIMEZONE } = {}) => {
|
||||||
|
if (!value) return null;
|
||||||
|
|
||||||
|
if (isDateTime(value)) {
|
||||||
|
return value.setZone(zone);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return DateTime.fromJSDate(value, { zone });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return DateTime.fromMillis(value, { zone });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
let dt = DateTime.fromISO(value, { zone, setZone: true });
|
||||||
|
if (!dt.isValid) {
|
||||||
|
dt = DateTime.fromSQL(value, { zone });
|
||||||
|
}
|
||||||
|
return dt.isValid ? dt : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNow = () => DateTime.now().setZone(TIMEZONE);
|
||||||
|
|
||||||
|
const getDayStart = (input = getNow()) => {
|
||||||
|
const dt = ensureDateTime(input);
|
||||||
|
if (!dt || !dt.isValid) {
|
||||||
|
const fallback = getNow();
|
||||||
|
return fallback.set({
|
||||||
|
hour: BUSINESS_DAY_START_HOUR,
|
||||||
|
minute: 0,
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sameDayStart = dt.set({
|
||||||
|
hour: BUSINESS_DAY_START_HOUR,
|
||||||
|
minute: 0,
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
return dt.hour < BUSINESS_DAY_START_HOUR
|
||||||
|
? sameDayStart.minus({ days: 1 })
|
||||||
|
: sameDayStart;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDayEnd = (input = getNow()) => {
|
||||||
|
return getDayStart(input).plus({ days: 1 }).minus({ milliseconds: 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWeekStart = (input = getNow()) => {
|
||||||
|
const dt = ensureDateTime(input);
|
||||||
|
if (!dt || !dt.isValid) {
|
||||||
|
return getDayStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
const startOfWeek = dt.set({ weekday: WEEK_START_DAY }).startOf('day');
|
||||||
|
const normalized = startOfWeek > dt ? startOfWeek.minus({ weeks: 1 }) : startOfWeek;
|
||||||
|
return normalized.set({
|
||||||
|
hour: BUSINESS_DAY_START_HOUR,
|
||||||
|
minute: 0,
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRangeForTimeRange = (timeRange = 'today', now = getNow()) => {
|
||||||
|
const current = ensureDateTime(now);
|
||||||
|
if (!current || !current.isValid) {
|
||||||
|
throw new Error('Invalid reference time for range calculation');
|
||||||
|
}
|
||||||
|
|
||||||
switch (timeRange) {
|
switch (timeRange) {
|
||||||
case 'today': {
|
case 'today': {
|
||||||
const start = new Date(easternTime);
|
return {
|
||||||
start.setHours(1, 0, 0, 0); // 1 AM start of business day
|
start: getDayStart(current),
|
||||||
|
end: getDayEnd(current)
|
||||||
const end = new Date(start);
|
};
|
||||||
end.setDate(end.getDate() + 1);
|
|
||||||
end.setHours(0, 59, 59, 999); // 12:59 AM next day
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'yesterday': {
|
case 'yesterday': {
|
||||||
const start = new Date(easternTime);
|
const target = current.minus({ days: 1 });
|
||||||
start.setDate(start.getDate() - 1);
|
return {
|
||||||
start.setHours(1, 0, 0, 0);
|
start: getDayStart(target),
|
||||||
|
end: getDayEnd(target)
|
||||||
const end = new Date(start);
|
};
|
||||||
end.setDate(end.getDate() + 1);
|
|
||||||
end.setHours(0, 59, 59, 999);
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'thisWeek': {
|
|
||||||
const start = new Date(easternTime);
|
|
||||||
start.setDate(easternTime.getDate() - easternTime.getDay()); // Sunday
|
|
||||||
start.setHours(1, 0, 0, 0);
|
|
||||||
|
|
||||||
const end = new Date(easternTime);
|
|
||||||
end.setDate(end.getDate() + 1);
|
|
||||||
end.setHours(0, 59, 59, 999);
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'lastWeek': {
|
|
||||||
const start = new Date(easternTime);
|
|
||||||
start.setDate(easternTime.getDate() - easternTime.getDay() - 7); // Previous Sunday
|
|
||||||
start.setHours(1, 0, 0, 0);
|
|
||||||
|
|
||||||
const end = new Date(start);
|
|
||||||
end.setDate(end.getDate() + 7);
|
|
||||||
end.setHours(0, 59, 59, 999);
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'thisMonth': {
|
|
||||||
const start = new Date(easternTime.getFullYear(), easternTime.getMonth(), 1, 1, 0, 0, 0);
|
|
||||||
const end = new Date(easternTime);
|
|
||||||
end.setDate(end.getDate() + 1);
|
|
||||||
end.setHours(0, 59, 59, 999);
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'lastMonth': {
|
|
||||||
const start = new Date(easternTime.getFullYear(), easternTime.getMonth() - 1, 1, 1, 0, 0, 0);
|
|
||||||
const end = new Date(easternTime.getFullYear(), easternTime.getMonth(), 1, 0, 59, 59, 999);
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'last7days': {
|
|
||||||
const end = new Date(easternTime);
|
|
||||||
end.setHours(0, 59, 59, 999);
|
|
||||||
|
|
||||||
const start = new Date(end);
|
|
||||||
start.setDate(start.getDate() - 7);
|
|
||||||
start.setHours(1, 0, 0, 0);
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'last30days': {
|
|
||||||
const end = new Date(easternTime);
|
|
||||||
end.setHours(0, 59, 59, 999);
|
|
||||||
|
|
||||||
const start = new Date(end);
|
|
||||||
start.setDate(start.getDate() - 30);
|
|
||||||
start.setHours(1, 0, 0, 0);
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'last90days': {
|
|
||||||
const end = new Date(easternTime);
|
|
||||||
end.setHours(0, 59, 59, 999);
|
|
||||||
|
|
||||||
const start = new Date(end);
|
|
||||||
start.setDate(start.getDate() - 90);
|
|
||||||
start.setHours(1, 0, 0, 0);
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'previous7days': {
|
|
||||||
const end = new Date(easternTime);
|
|
||||||
end.setDate(end.getDate() - 1);
|
|
||||||
end.setHours(0, 59, 59, 999);
|
|
||||||
|
|
||||||
const start = new Date(end);
|
|
||||||
start.setDate(start.getDate() - 6);
|
|
||||||
start.setHours(1, 0, 0, 0);
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'previous30days': {
|
|
||||||
const end = new Date(easternTime);
|
|
||||||
end.setDate(end.getDate() - 1);
|
|
||||||
end.setHours(0, 59, 59, 999);
|
|
||||||
|
|
||||||
const start = new Date(end);
|
|
||||||
start.setDate(start.getDate() - 29);
|
|
||||||
start.setHours(1, 0, 0, 0);
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'previous90days': {
|
|
||||||
const end = new Date(easternTime);
|
|
||||||
end.setDate(end.getDate() - 1);
|
|
||||||
end.setHours(0, 59, 59, 999);
|
|
||||||
|
|
||||||
const start = new Date(end);
|
|
||||||
start.setDate(start.getDate() - 89);
|
|
||||||
start.setHours(1, 0, 0, 0);
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'twoDaysAgo': {
|
case 'twoDaysAgo': {
|
||||||
const start = new Date(easternTime);
|
const target = current.minus({ days: 2 });
|
||||||
start.setDate(start.getDate() - 2);
|
return {
|
||||||
start.setHours(1, 0, 0, 0);
|
start: getDayStart(target),
|
||||||
|
end: getDayEnd(target)
|
||||||
const end = new Date(start);
|
};
|
||||||
end.setDate(end.getDate() + 1);
|
}
|
||||||
end.setHours(0, 59, 59, 999);
|
case 'thisWeek': {
|
||||||
|
return {
|
||||||
return { start, end };
|
start: getWeekStart(current),
|
||||||
|
end: getDayEnd(current)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'lastWeek': {
|
||||||
|
const lastWeek = current.minus({ weeks: 1 });
|
||||||
|
const weekStart = getWeekStart(lastWeek);
|
||||||
|
const weekEnd = weekStart.plus({ days: 6 });
|
||||||
|
return {
|
||||||
|
start: weekStart,
|
||||||
|
end: getDayEnd(weekEnd)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'thisMonth': {
|
||||||
|
const dayStart = getDayStart(current);
|
||||||
|
const monthStart = dayStart.startOf('month').set({ hour: BUSINESS_DAY_START_HOUR });
|
||||||
|
return {
|
||||||
|
start: monthStart,
|
||||||
|
end: getDayEnd(current)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'lastMonth': {
|
||||||
|
const lastMonth = current.minus({ months: 1 });
|
||||||
|
const monthStart = lastMonth
|
||||||
|
.startOf('month')
|
||||||
|
.set({ hour: BUSINESS_DAY_START_HOUR, minute: 0, second: 0, millisecond: 0 });
|
||||||
|
const monthEnd = monthStart.plus({ months: 1 }).minus({ days: 1 });
|
||||||
|
return {
|
||||||
|
start: monthStart,
|
||||||
|
end: getDayEnd(monthEnd)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'last7days': {
|
||||||
|
const dayStart = getDayStart(current);
|
||||||
|
return {
|
||||||
|
start: dayStart.minus({ days: 6 }),
|
||||||
|
end: getDayEnd(current)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'last30days': {
|
||||||
|
const dayStart = getDayStart(current);
|
||||||
|
return {
|
||||||
|
start: dayStart.minus({ days: 29 }),
|
||||||
|
end: getDayEnd(current)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'last90days': {
|
||||||
|
const dayStart = getDayStart(current);
|
||||||
|
return {
|
||||||
|
start: dayStart.minus({ days: 89 }),
|
||||||
|
end: getDayEnd(current)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'previous7days': {
|
||||||
|
const currentPeriodStart = getDayStart(current).minus({ days: 6 });
|
||||||
|
const previousEndDay = currentPeriodStart.minus({ days: 1 });
|
||||||
|
const previousStartDay = previousEndDay.minus({ days: 6 });
|
||||||
|
return {
|
||||||
|
start: getDayStart(previousStartDay),
|
||||||
|
end: getDayEnd(previousEndDay)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'previous30days': {
|
||||||
|
const currentPeriodStart = getDayStart(current).minus({ days: 29 });
|
||||||
|
const previousEndDay = currentPeriodStart.minus({ days: 1 });
|
||||||
|
const previousStartDay = previousEndDay.minus({ days: 29 });
|
||||||
|
return {
|
||||||
|
start: getDayStart(previousStartDay),
|
||||||
|
end: getDayEnd(previousEndDay)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'previous90days': {
|
||||||
|
const currentPeriodStart = getDayStart(current).minus({ days: 89 });
|
||||||
|
const previousEndDay = currentPeriodStart.minus({ days: 1 });
|
||||||
|
const previousStartDay = previousEndDay.minus({ days: 89 });
|
||||||
|
return {
|
||||||
|
start: getDayStart(previousStartDay),
|
||||||
|
end: getDayEnd(previousEndDay)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown time range: ${timeRange}`);
|
throw new Error(`Unknown time range: ${timeRange}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTimeRangeConditions = (timeRange, startDate, endDate) => {
|
const toDatabaseSqlString = (dt) => {
|
||||||
if (timeRange === 'custom' && startDate && endDate) {
|
const normalized = ensureDateTime(dt);
|
||||||
// Custom date range
|
if (!normalized || !normalized.isValid) {
|
||||||
const start = new Date(startDate);
|
throw new Error('Invalid datetime provided for SQL conversion');
|
||||||
const end = new Date(endDate);
|
|
||||||
|
|
||||||
// Convert to UTC-5 (Eastern time)
|
|
||||||
const startUTC5 = new Date(start.getTime() - (5 * 60 * 60 * 1000));
|
|
||||||
const endUTC5 = new Date(end.getTime() - (5 * 60 * 60 * 1000));
|
|
||||||
|
|
||||||
return {
|
|
||||||
whereClause: 'date_placed >= ? AND date_placed <= ?',
|
|
||||||
params: [
|
|
||||||
startUTC5.toISOString().slice(0, 19).replace('T', ' '),
|
|
||||||
endUTC5.toISOString().slice(0, 19).replace('T', ' ')
|
|
||||||
],
|
|
||||||
dateRange: {
|
|
||||||
start: startDate,
|
|
||||||
end: endDate,
|
|
||||||
label: `${formatBusinessDate(start)} - ${formatBusinessDate(end)}`
|
|
||||||
}
|
}
|
||||||
};
|
const dbTime = normalized.setZone(DB_TIMEZONE, { keepLocalTime: true });
|
||||||
}
|
return dbTime.toFormat(DB_DATETIME_FORMAT);
|
||||||
|
|
||||||
if (!timeRange) {
|
|
||||||
timeRange = 'today';
|
|
||||||
}
|
|
||||||
|
|
||||||
const { start, end } = getBusinessDayBounds(timeRange);
|
|
||||||
|
|
||||||
// Convert to MySQL datetime format (UTC-5)
|
|
||||||
const startStr = start.toISOString().slice(0, 19).replace('T', ' ');
|
|
||||||
const endStr = end.toISOString().slice(0, 19).replace('T', ' ');
|
|
||||||
|
|
||||||
return {
|
|
||||||
whereClause: 'date_placed >= ? AND date_placed <= ?',
|
|
||||||
params: [startStr, endStr],
|
|
||||||
dateRange: {
|
|
||||||
start: start.toISOString(),
|
|
||||||
end: end.toISOString(),
|
|
||||||
label: getTimeRangeLabel(timeRange)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatBusinessDate = (date) => {
|
const formatBusinessDate = (input) => {
|
||||||
return date.toLocaleDateString('en-US', {
|
const dt = ensureDateTime(input);
|
||||||
month: 'short',
|
if (!dt || !dt.isValid) return '';
|
||||||
day: 'numeric',
|
return dt.setZone(TIMEZONE).toFormat('LLL d, yyyy');
|
||||||
year: 'numeric'
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTimeRangeLabel = (timeRange) => {
|
const getTimeRangeLabel = (timeRange) => {
|
||||||
const labels = {
|
const labels = {
|
||||||
today: 'Today',
|
today: 'Today',
|
||||||
yesterday: 'Yesterday',
|
yesterday: 'Yesterday',
|
||||||
|
twoDaysAgo: 'Two Days Ago',
|
||||||
thisWeek: 'This Week',
|
thisWeek: 'This Week',
|
||||||
lastWeek: 'Last Week',
|
lastWeek: 'Last Week',
|
||||||
thisMonth: 'This Month',
|
thisMonth: 'This Month',
|
||||||
@@ -221,32 +223,75 @@ const getTimeRangeLabel = (timeRange) => {
|
|||||||
last90days: 'Last 90 Days',
|
last90days: 'Last 90 Days',
|
||||||
previous7days: 'Previous 7 Days',
|
previous7days: 'Previous 7 Days',
|
||||||
previous30days: 'Previous 30 Days',
|
previous30days: 'Previous 30 Days',
|
||||||
previous90days: 'Previous 90 Days',
|
previous90days: 'Previous 90 Days'
|
||||||
twoDaysAgo: 'Two Days Ago'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return labels[timeRange] || timeRange;
|
return labels[timeRange] || timeRange;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to convert MySQL datetime to JavaScript Date
|
const getTimeRangeConditions = (timeRange, startDate, endDate) => {
|
||||||
|
if (timeRange === 'custom' && startDate && endDate) {
|
||||||
|
const start = ensureDateTime(startDate);
|
||||||
|
const end = ensureDateTime(endDate);
|
||||||
|
|
||||||
|
if (!start || !start.isValid || !end || !end.isValid) {
|
||||||
|
throw new Error('Invalid custom date range provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
whereClause: 'date_placed >= ? AND date_placed <= ?',
|
||||||
|
params: [toDatabaseSqlString(start), toDatabaseSqlString(end)],
|
||||||
|
dateRange: {
|
||||||
|
start: start.toUTC().toISO(),
|
||||||
|
end: end.toUTC().toISO(),
|
||||||
|
label: `${formatBusinessDate(start)} - ${formatBusinessDate(end)}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedRange = timeRange || 'today';
|
||||||
|
const range = getRangeForTimeRange(normalizedRange);
|
||||||
|
|
||||||
|
return {
|
||||||
|
whereClause: 'date_placed >= ? AND date_placed <= ?',
|
||||||
|
params: [toDatabaseSqlString(range.start), toDatabaseSqlString(range.end)],
|
||||||
|
dateRange: {
|
||||||
|
start: range.start.toUTC().toISO(),
|
||||||
|
end: range.end.toUTC().toISO(),
|
||||||
|
label: getTimeRangeLabel(normalizedRange)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBusinessDayBounds = (timeRange) => {
|
||||||
|
const range = getRangeForTimeRange(timeRange);
|
||||||
|
return {
|
||||||
|
start: range.start.toJSDate(),
|
||||||
|
end: range.end.toJSDate()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const parseBusinessDate = (mysqlDatetime) => {
|
const parseBusinessDate = (mysqlDatetime) => {
|
||||||
if (!mysqlDatetime || mysqlDatetime === '0000-00-00 00:00:00') {
|
if (!mysqlDatetime || mysqlDatetime === '0000-00-00 00:00:00') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MySQL datetime is stored in UTC-5, so we need to add 5 hours to get UTC
|
const dt = DateTime.fromSQL(mysqlDatetime, { zone: DB_TIMEZONE });
|
||||||
const date = new Date(mysqlDatetime + ' UTC');
|
if (!dt.isValid) {
|
||||||
date.setHours(date.getHours() + 5);
|
console.error('[timeUtils] Failed to parse MySQL datetime:', mysqlDatetime, dt.invalidExplanation);
|
||||||
return date;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dt.toUTC().toJSDate();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to format date for MySQL queries
|
const formatMySQLDate = (input) => {
|
||||||
const formatMySQLDate = (date) => {
|
if (!input) return null;
|
||||||
if (!date) return null;
|
|
||||||
|
|
||||||
// Convert to UTC-5 for storage
|
const dt = ensureDateTime(input, { zone: 'utc' });
|
||||||
const utc5Date = new Date(date.getTime() - (5 * 60 * 60 * 1000));
|
if (!dt || !dt.isValid) return null;
|
||||||
return utc5Date.toISOString().slice(0, 19).replace('T', ' ');
|
|
||||||
|
return dt.setZone(DB_TIMEZONE).toFormat(DB_DATETIME_FORMAT);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -255,5 +300,13 @@ module.exports = {
|
|||||||
formatBusinessDate,
|
formatBusinessDate,
|
||||||
getTimeRangeLabel,
|
getTimeRangeLabel,
|
||||||
parseBusinessDate,
|
parseBusinessDate,
|
||||||
formatMySQLDate
|
formatMySQLDate,
|
||||||
|
// Expose helpers for tests or advanced consumers
|
||||||
|
_internal: {
|
||||||
|
getDayStart,
|
||||||
|
getDayEnd,
|
||||||
|
getWeekStart,
|
||||||
|
getRangeForTimeRange,
|
||||||
|
BUSINESS_DAY_START_HOUR
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/dashboard/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Loader2, AlertCircle, CheckCircle, RefreshCw } from "lucide-react";
|
import { Loader2, AlertCircle, CheckCircle, RefreshCw } from "lucide-react";
|
||||||
|
|
||||||
const AcotTest = () => {
|
const AcotTest = () => {
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
} from "@/components/dashboard/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/dashboard/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Alert, AlertDescription } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/dashboard/ui/table";
|
} from "@/components/ui/table";
|
||||||
import {
|
import {
|
||||||
PhoneCall,
|
PhoneCall,
|
||||||
PhoneMissed,
|
PhoneMissed,
|
||||||
@@ -37,15 +37,15 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
Search,
|
Search,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/dashboard/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Progress } from "@/components/dashboard/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/dashboard/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import {
|
import {
|
||||||
LineChart,
|
LineChart,
|
||||||
Line,
|
Line,
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/dashboard/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/dashboard/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/dashboard/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import {
|
import {
|
||||||
LineChart,
|
LineChart,
|
||||||
Line,
|
Line,
|
||||||
@@ -21,10 +21,10 @@ import {
|
|||||||
ReferenceLine,
|
ReferenceLine,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { Loader2, TrendingUp, AlertCircle } from "lucide-react";
|
import { Loader2, TrendingUp, AlertCircle } from "lucide-react";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/dashboard/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/dashboard/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
|
|
||||||
// Add helper function for currency formatting
|
// Add helper function for currency formatting
|
||||||
const formatCurrency = (value, useFractionDigits = true) => {
|
const formatCurrency = (value, useFractionDigits = true) => {
|
||||||
@@ -186,7 +186,6 @@ export const AnalyticsDashboard = () => {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!result?.data?.rows) {
|
if (!result?.data?.rows) {
|
||||||
console.log("No result data received");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Card, CardContent } from '@/components/dashboard/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Calendar as CalendarComponent } from '@/components/dashboard/ui/calendaredit';
|
import { Calendar as CalendarComponent } from '@/components/ui/calendaredit';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/dashboard/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { Alert, AlertDescription } from '@/components/dashboard/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import {
|
import {
|
||||||
Sun,
|
Sun,
|
||||||
Cloud,
|
Cloud,
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/dashboard/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/dashboard/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { ScrollArea } from "@/components/dashboard/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import {
|
import {
|
||||||
Package,
|
Package,
|
||||||
Truck,
|
Truck,
|
||||||
@@ -31,24 +31,24 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/dashboard/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/dashboard/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/dashboard/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/dashboard/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
|
||||||
const METRIC_IDS = {
|
const METRIC_IDS = {
|
||||||
PLACED_ORDER: "Y8cqcF",
|
PLACED_ORDER: "Y8cqcF",
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/dashboard/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/dashboard/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -22,8 +22,8 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/dashboard/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Separator } from "@/components/dashboard/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/dashboard/ui/table";
|
} from "@/components/ui/table";
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
@@ -44,8 +44,8 @@ import {
|
|||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import type { TooltipProps } from "recharts";
|
import type { TooltipProps } from "recharts";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { ArrowUpRight, ArrowDownRight, Minus, TrendingUp, AlertCircle } from "lucide-react";
|
import { ArrowUpRight, ArrowDownRight, Minus, TrendingUp, AlertCircle } from "lucide-react";
|
||||||
|
|
||||||
type TrendDirection = "up" | "down" | "flat";
|
type TrendDirection = "up" | "down" | "flat";
|
||||||
@@ -61,26 +61,28 @@ type ComparisonValue = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type FinancialTotals = {
|
type FinancialTotals = {
|
||||||
grossSales: number;
|
grossSales?: number;
|
||||||
refunds: number;
|
refunds?: number;
|
||||||
taxCollected: number;
|
taxCollected?: number;
|
||||||
|
shippingFees?: number;
|
||||||
|
discounts?: number;
|
||||||
cogs: number;
|
cogs: number;
|
||||||
netSales: number;
|
income?: number;
|
||||||
netRevenue: number;
|
|
||||||
profit: number;
|
profit: number;
|
||||||
margin: number;
|
margin: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FinancialTrendPoint = {
|
type FinancialTrendPoint = {
|
||||||
date: string | Date | null;
|
date: string | Date | null;
|
||||||
grossSales: number;
|
income: number;
|
||||||
refunds: number;
|
|
||||||
taxCollected: number;
|
|
||||||
cogs: number;
|
cogs: number;
|
||||||
netSales: number;
|
|
||||||
netRevenue: number;
|
|
||||||
profit: number;
|
profit: number;
|
||||||
margin: number;
|
margin: number;
|
||||||
|
grossSales?: number;
|
||||||
|
refunds?: number;
|
||||||
|
shippingFees?: number;
|
||||||
|
taxCollected?: number;
|
||||||
|
discounts?: number;
|
||||||
timestamp: string | null;
|
timestamp: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,8 +90,9 @@ type FinancialComparison = {
|
|||||||
grossSales?: ComparisonValue;
|
grossSales?: ComparisonValue;
|
||||||
refunds?: ComparisonValue;
|
refunds?: ComparisonValue;
|
||||||
taxCollected?: ComparisonValue;
|
taxCollected?: ComparisonValue;
|
||||||
|
discounts?: ComparisonValue;
|
||||||
cogs?: ComparisonValue;
|
cogs?: ComparisonValue;
|
||||||
netRevenue?: ComparisonValue;
|
income?: ComparisonValue;
|
||||||
profit?: ComparisonValue;
|
profit?: ComparisonValue;
|
||||||
margin?: ComparisonValue;
|
margin?: ComparisonValue;
|
||||||
[key: string]: ComparisonValue | undefined;
|
[key: string]: ComparisonValue | undefined;
|
||||||
@@ -129,15 +132,14 @@ type YearPeriod = {
|
|||||||
|
|
||||||
type CustomPeriod = MonthPeriod | QuarterPeriod | YearPeriod;
|
type CustomPeriod = MonthPeriod | QuarterPeriod | YearPeriod;
|
||||||
|
|
||||||
type ChartSeriesKey = "grossSales" | "netRevenue" | "cogs" | "profit" | "margin";
|
type ChartSeriesKey = "income" | "cogs" | "profit" | "margin";
|
||||||
|
|
||||||
type GroupByOption = "day" | "month" | "quarter" | "year";
|
type GroupByOption = "day" | "month" | "quarter" | "year";
|
||||||
|
|
||||||
type ChartPoint = {
|
type ChartPoint = {
|
||||||
label: string;
|
label: string;
|
||||||
timestamp: string | null;
|
timestamp: string | null;
|
||||||
grossSales: number | null;
|
income: number | null;
|
||||||
netRevenue: number | null;
|
|
||||||
cogs: number | null;
|
cogs: number | null;
|
||||||
profit: number | null;
|
profit: number | null;
|
||||||
margin: number | null;
|
margin: number | null;
|
||||||
@@ -146,18 +148,16 @@ type ChartPoint = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const chartColors: Record<ChartSeriesKey, string> = {
|
const chartColors: Record<ChartSeriesKey, string> = {
|
||||||
grossSales: "#7c3aed",
|
income: "#2563eb",
|
||||||
netRevenue: "#6366f1",
|
|
||||||
cogs: "#f97316",
|
cogs: "#f97316",
|
||||||
profit: "#10b981",
|
profit: "#10b981",
|
||||||
margin: "#0ea5e9",
|
margin: "#0ea5e9",
|
||||||
};
|
};
|
||||||
|
|
||||||
const SERIES_LABELS: Record<ChartSeriesKey, string> = {
|
const SERIES_LABELS: Record<ChartSeriesKey, string> = {
|
||||||
grossSales: "Gross Sales",
|
income: "Total Income",
|
||||||
netRevenue: "Net Revenue",
|
|
||||||
cogs: "COGS",
|
cogs: "COGS",
|
||||||
profit: "Profit",
|
profit: "Gross Profit",
|
||||||
margin: "Profit Margin",
|
margin: "Profit Margin",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -166,8 +166,7 @@ const SERIES_DEFINITIONS: Array<{
|
|||||||
label: string;
|
label: string;
|
||||||
type: "currency" | "percentage";
|
type: "currency" | "percentage";
|
||||||
}> = [
|
}> = [
|
||||||
{ key: "grossSales", label: SERIES_LABELS.grossSales, type: "currency" },
|
{ key: "income", label: SERIES_LABELS.income, type: "currency" },
|
||||||
{ key: "netRevenue", label: SERIES_LABELS.netRevenue, type: "currency" },
|
|
||||||
{ key: "cogs", label: SERIES_LABELS.cogs, type: "currency" },
|
{ key: "cogs", label: SERIES_LABELS.cogs, type: "currency" },
|
||||||
{ key: "profit", label: SERIES_LABELS.profit, type: "currency" },
|
{ key: "profit", label: SERIES_LABELS.profit, type: "currency" },
|
||||||
{ key: "margin", label: SERIES_LABELS.margin, type: "percentage" },
|
{ key: "margin", label: SERIES_LABELS.margin, type: "percentage" },
|
||||||
@@ -277,10 +276,12 @@ const formatPercentage = (value: number, digits = 1, suffix = "%") => {
|
|||||||
|
|
||||||
type RawTrendPoint = {
|
type RawTrendPoint = {
|
||||||
date: Date;
|
date: Date;
|
||||||
grossSales: number;
|
income: number;
|
||||||
netRevenue: number;
|
|
||||||
cogs: number;
|
cogs: number;
|
||||||
profit: number;
|
shippingFees: number;
|
||||||
|
taxCollected: number;
|
||||||
|
grossSales: number;
|
||||||
|
discounts: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AggregatedTrendPoint = {
|
type AggregatedTrendPoint = {
|
||||||
@@ -289,8 +290,7 @@ type AggregatedTrendPoint = {
|
|||||||
tooltipLabel: string;
|
tooltipLabel: string;
|
||||||
detailLabel: string;
|
detailLabel: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
grossSales: number;
|
income: number;
|
||||||
netRevenue: number;
|
|
||||||
cogs: number;
|
cogs: number;
|
||||||
profit: number;
|
profit: number;
|
||||||
margin: number;
|
margin: number;
|
||||||
@@ -367,10 +367,8 @@ const aggregateTrendPoints = (points: RawTrendPoint[], groupBy: GroupByOption):
|
|||||||
key: string;
|
key: string;
|
||||||
start: Date;
|
start: Date;
|
||||||
end: Date;
|
end: Date;
|
||||||
grossSales: number;
|
income: number;
|
||||||
netRevenue: number;
|
|
||||||
cogs: number;
|
cogs: number;
|
||||||
profit: number;
|
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
@@ -382,10 +380,8 @@ const aggregateTrendPoints = (points: RawTrendPoint[], groupBy: GroupByOption):
|
|||||||
key,
|
key,
|
||||||
start: point.date,
|
start: point.date,
|
||||||
end: point.date,
|
end: point.date,
|
||||||
grossSales: point.grossSales,
|
income: point.income,
|
||||||
netRevenue: point.netRevenue,
|
|
||||||
cogs: point.cogs,
|
cogs: point.cogs,
|
||||||
profit: point.profit,
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -397,18 +393,17 @@ const aggregateTrendPoints = (points: RawTrendPoint[], groupBy: GroupByOption):
|
|||||||
bucket.end = point.date;
|
bucket.end = point.date;
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket.grossSales += point.grossSales;
|
bucket.income += point.income;
|
||||||
bucket.netRevenue += point.netRevenue;
|
|
||||||
bucket.cogs += point.cogs;
|
bucket.cogs += point.cogs;
|
||||||
bucket.profit += point.profit;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(bucketMap.values())
|
return Array.from(bucketMap.values())
|
||||||
.sort((a, b) => a.start.getTime() - b.start.getTime())
|
.sort((a, b) => a.start.getTime() - b.start.getTime())
|
||||||
.map((bucket) => {
|
.map((bucket) => {
|
||||||
const { axisLabel, tooltipLabel, detailLabel } = buildGroupLabels(bucket.start, bucket.end, groupBy);
|
const { axisLabel, tooltipLabel, detailLabel } = buildGroupLabels(bucket.start, bucket.end, groupBy);
|
||||||
const { netRevenue, profit } = bucket;
|
const income = bucket.income;
|
||||||
const margin = netRevenue !== 0 ? (profit / netRevenue) * 100 : 0;
|
const profit = income - bucket.cogs;
|
||||||
|
const margin = income !== 0 ? (profit / income) * 100 : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: bucket.key,
|
id: bucket.key,
|
||||||
@@ -416,8 +411,7 @@ const aggregateTrendPoints = (points: RawTrendPoint[], groupBy: GroupByOption):
|
|||||||
tooltipLabel,
|
tooltipLabel,
|
||||||
detailLabel,
|
detailLabel,
|
||||||
timestamp: bucket.start.toISOString(),
|
timestamp: bucket.start.toISOString(),
|
||||||
grossSales: bucket.grossSales,
|
income,
|
||||||
netRevenue,
|
|
||||||
cogs: bucket.cogs,
|
cogs: bucket.cogs,
|
||||||
profit,
|
profit,
|
||||||
margin,
|
margin,
|
||||||
@@ -516,8 +510,7 @@ const extendAggregatedTrendPoints = (
|
|||||||
tooltipLabel,
|
tooltipLabel,
|
||||||
detailLabel,
|
detailLabel,
|
||||||
timestamp: bucketStart.toISOString(),
|
timestamp: bucketStart.toISOString(),
|
||||||
grossSales: 0,
|
income: 0,
|
||||||
netRevenue: 0,
|
|
||||||
cogs: 0,
|
cogs: 0,
|
||||||
profit: 0,
|
profit: 0,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
@@ -536,14 +529,21 @@ const monthsBetween = (start: Date, end: Date) => {
|
|||||||
|
|
||||||
const buildTrendLabel = (
|
const buildTrendLabel = (
|
||||||
comparison?: ComparisonValue | null,
|
comparison?: ComparisonValue | null,
|
||||||
options?: { isPercentage?: boolean }
|
options?: { isPercentage?: boolean; invertDirection?: boolean }
|
||||||
): TrendSummary | null => {
|
): TrendSummary | null => {
|
||||||
if (!comparison || comparison.absolute === null) {
|
if (!comparison || comparison.absolute === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { absolute, percentage } = comparison;
|
const { absolute, percentage } = comparison;
|
||||||
const direction: TrendDirection = absolute > 0 ? "up" : absolute < 0 ? "down" : "flat";
|
const rawDirection: TrendDirection = absolute > 0 ? "up" : absolute < 0 ? "down" : "flat";
|
||||||
|
const direction: TrendDirection = options?.invertDirection
|
||||||
|
? rawDirection === "up"
|
||||||
|
? "down"
|
||||||
|
: rawDirection === "down"
|
||||||
|
? "up"
|
||||||
|
: "flat"
|
||||||
|
: rawDirection;
|
||||||
const absoluteValue = Math.abs(absolute);
|
const absoluteValue = Math.abs(absolute);
|
||||||
|
|
||||||
if (options?.isPercentage) {
|
if (options?.isPercentage) {
|
||||||
@@ -566,6 +566,39 @@ const buildTrendLabel = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const safeNumeric = (value: number | null | undefined) =>
|
||||||
|
typeof value === "number" && Number.isFinite(value) ? value : 0;
|
||||||
|
|
||||||
|
const computeTotalIncome = (totals?: FinancialTotals | null) => {
|
||||||
|
if (!totals || typeof totals.income !== "number" || !Number.isFinite(totals.income)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totals.income;
|
||||||
|
};
|
||||||
|
|
||||||
|
const computeProfitFrom = (income: number, cogs?: number | null | undefined) => income - safeNumeric(cogs);
|
||||||
|
|
||||||
|
const computeMarginFrom = (profit: number, income: number) => (income !== 0 ? (profit / income) * 100 : 0);
|
||||||
|
|
||||||
|
const buildComparisonFromValues = (current?: number | null, previous?: number | null): ComparisonValue | null => {
|
||||||
|
if (current == null || previous == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isFinite(current) || !Number.isFinite(previous)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolute = current - previous;
|
||||||
|
const percentage = previous !== 0 ? (absolute / Math.abs(previous)) * 100 : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
absolute,
|
||||||
|
percentage,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const generateYearOptions = (span: number) => {
|
const generateYearOptions = (span: number) => {
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
return Array.from({ length: span }, (_, index) => currentYear - index);
|
return Array.from({ length: span }, (_, index) => currentYear - index);
|
||||||
@@ -638,9 +671,8 @@ const FinancialOverview = () => {
|
|||||||
count: 1,
|
count: 1,
|
||||||
});
|
});
|
||||||
const [metrics, setMetrics] = useState<Record<ChartSeriesKey, boolean>>({
|
const [metrics, setMetrics] = useState<Record<ChartSeriesKey, boolean>>({
|
||||||
grossSales: true,
|
income: true,
|
||||||
netRevenue: true,
|
cogs: true,
|
||||||
cogs: false,
|
|
||||||
profit: true,
|
profit: true,
|
||||||
margin: true,
|
margin: true,
|
||||||
});
|
});
|
||||||
@@ -724,12 +756,24 @@ const FinancialOverview = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof point.income !== "number" || !Number.isFinite(point.income)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const grossSalesValue = toNumber((point as { grossSales?: number }).grossSales);
|
||||||
|
const shippingFeesValue = toNumber((point as { shippingFees?: number }).shippingFees);
|
||||||
|
const taxCollectedValue = toNumber((point as { taxCollected?: number }).taxCollected);
|
||||||
|
const discountsValue = toNumber((point as { discounts?: number }).discounts);
|
||||||
|
const incomeValue = point.income;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date,
|
date,
|
||||||
grossSales: toNumber(point.grossSales),
|
income: incomeValue,
|
||||||
netRevenue: toNumber(point.netRevenue),
|
shippingFees: shippingFeesValue,
|
||||||
|
taxCollected: taxCollectedValue,
|
||||||
|
grossSales: grossSalesValue,
|
||||||
|
discounts: discountsValue,
|
||||||
cogs: toNumber(point.cogs),
|
cogs: toNumber(point.cogs),
|
||||||
profit: toNumber(point.profit),
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((value): value is RawTrendPoint => Boolean(value))
|
.filter((value): value is RawTrendPoint => Boolean(value))
|
||||||
@@ -848,45 +892,74 @@ const FinancialOverview = () => {
|
|||||||
|
|
||||||
const totals = data.totals;
|
const totals = data.totals;
|
||||||
const previous = data.previousTotals ?? undefined;
|
const previous = data.previousTotals ?? undefined;
|
||||||
const comparison = data.comparison ?? {};
|
|
||||||
|
|
||||||
const safeCurrency = (value: number | undefined | null, digits = 0) =>
|
const safeCurrency = (value: number | undefined | null, digits = 0) =>
|
||||||
typeof value === "number" && Number.isFinite(value) ? formatCurrency(value, digits) : "—";
|
typeof value === "number" && Number.isFinite(value) ? formatCurrency(value, digits) : "—";
|
||||||
|
|
||||||
const safePercentage = (value: number | undefined | null, digits = 1) =>
|
const safePercentage = (value: number | undefined | null, digits = 1) =>
|
||||||
typeof value === "number" && Number.isFinite(value) ? formatPercentage(value, digits) : "—";
|
typeof value === "number" && Number.isFinite(value) ? formatPercentage(value, digits) : "—";
|
||||||
|
|
||||||
|
const comparison = data.comparison ?? {};
|
||||||
|
|
||||||
|
const totalIncome = computeTotalIncome(totals);
|
||||||
|
const previousIncome = previous ? computeTotalIncome(previous) : null;
|
||||||
|
|
||||||
|
const cogsValue = safeNumeric(totals.cogs);
|
||||||
|
const previousCogs = previous?.cogs != null ? safeNumeric(previous.cogs) : null;
|
||||||
|
|
||||||
|
const profitValue = Number.isFinite(totals.profit)
|
||||||
|
? safeNumeric(totals.profit)
|
||||||
|
: computeProfitFrom(totalIncome, totals.cogs);
|
||||||
|
const previousProfitValue = previousIncome != null
|
||||||
|
? Number.isFinite(previous?.profit)
|
||||||
|
? safeNumeric(previous?.profit)
|
||||||
|
: computeProfitFrom(previousIncome, previous?.cogs)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const marginValue = Number.isFinite(totals.margin)
|
||||||
|
? safeNumeric(totals.margin)
|
||||||
|
: computeMarginFrom(profitValue, totalIncome);
|
||||||
|
const previousMarginValue = previousIncome != null
|
||||||
|
? Number.isFinite(previous?.margin)
|
||||||
|
? safeNumeric(previous?.margin)
|
||||||
|
: computeMarginFrom(previousProfitValue ?? 0, previousIncome)
|
||||||
|
: null;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: "grossSales",
|
key: "income",
|
||||||
title: "Gross Sales",
|
title: "Total Income",
|
||||||
value: safeCurrency(totals.grossSales, 0),
|
value: safeCurrency(totalIncome, 0),
|
||||||
description: previous?.grossSales != null ? `Previous: ${safeCurrency(previous.grossSales, 0)}` : undefined,
|
description: previousIncome != null ? `Previous: ${safeCurrency(previousIncome, 0)}` : undefined,
|
||||||
trend: buildTrendLabel(comparison.grossSales),
|
trend: buildTrendLabel(comparison?.income ?? buildComparisonFromValues(totalIncome, previousIncome ?? null)),
|
||||||
accentClass: "text-emerald-600 dark:text-emerald-400",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "netRevenue",
|
|
||||||
title: "Net Revenue",
|
|
||||||
value: safeCurrency(totals.netRevenue, 0),
|
|
||||||
description: previous?.netRevenue != null ? `Previous: ${safeCurrency(previous.netRevenue, 0)}` : undefined,
|
|
||||||
trend: buildTrendLabel(comparison.netRevenue),
|
|
||||||
accentClass: "text-indigo-600 dark:text-indigo-400",
|
accentClass: "text-indigo-600 dark:text-indigo-400",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "cogs",
|
||||||
|
title: "COGS",
|
||||||
|
value: safeCurrency(cogsValue, 0),
|
||||||
|
description: previousCogs != null ? `Previous: ${safeCurrency(previousCogs, 0)}` : undefined,
|
||||||
|
trend: buildTrendLabel(comparison?.cogs ?? buildComparisonFromValues(cogsValue, previousCogs ?? null), {
|
||||||
|
invertDirection: true,
|
||||||
|
}),
|
||||||
|
accentClass: "text-amber-600 dark:text-amber-400",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "profit",
|
key: "profit",
|
||||||
title: "Profit",
|
title: "Gross Profit",
|
||||||
value: safeCurrency(totals.profit, 0),
|
value: safeCurrency(profitValue, 0),
|
||||||
description: previous?.profit != null ? `Previous: ${safeCurrency(previous.profit, 0)}` : undefined,
|
description: previousProfitValue != null ? `Previous: ${safeCurrency(previousProfitValue, 0)}` : undefined,
|
||||||
trend: buildTrendLabel(comparison.profit),
|
trend: buildTrendLabel(comparison?.profit ?? buildComparisonFromValues(profitValue, previousProfitValue ?? null)),
|
||||||
accentClass: "text-emerald-600 dark:text-emerald-400",
|
accentClass: "text-emerald-600 dark:text-emerald-400",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "margin",
|
key: "margin",
|
||||||
title: "Profit Margin",
|
title: "Profit Margin",
|
||||||
value: safePercentage(totals.margin, 1),
|
value: safePercentage(marginValue, 1),
|
||||||
description: previous?.margin != null ? `Previous: ${safePercentage(previous.margin, 1)}` : undefined,
|
description: previousMarginValue != null ? `Previous: ${safePercentage(previousMarginValue, 1)}` : undefined,
|
||||||
trend: buildTrendLabel(comparison.margin, { isPercentage: true }),
|
trend: buildTrendLabel(
|
||||||
|
comparison?.margin ?? buildComparisonFromValues(marginValue, previousMarginValue ?? null),
|
||||||
|
{ isPercentage: true }
|
||||||
|
),
|
||||||
accentClass: "text-sky-600 dark:text-sky-400",
|
accentClass: "text-sky-600 dark:text-sky-400",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -907,8 +980,7 @@ const FinancialOverview = () => {
|
|||||||
return aggregatedPoints.map((point) => ({
|
return aggregatedPoints.map((point) => ({
|
||||||
label: point.label,
|
label: point.label,
|
||||||
timestamp: point.timestamp,
|
timestamp: point.timestamp,
|
||||||
grossSales: point.isFuture ? null : point.grossSales,
|
income: point.isFuture ? null : point.income,
|
||||||
netRevenue: point.isFuture ? null : point.netRevenue,
|
|
||||||
cogs: point.isFuture ? null : point.cogs,
|
cogs: point.isFuture ? null : point.cogs,
|
||||||
profit: point.isFuture ? null : point.profit,
|
profit: point.isFuture ? null : point.profit,
|
||||||
margin: point.isFuture ? null : point.margin,
|
margin: point.isFuture ? null : point.margin,
|
||||||
@@ -977,8 +1049,7 @@ const FinancialOverview = () => {
|
|||||||
id: point.id,
|
id: point.id,
|
||||||
label: point.detailLabel,
|
label: point.detailLabel,
|
||||||
timestamp: point.timestamp,
|
timestamp: point.timestamp,
|
||||||
grossSales: point.grossSales,
|
income: point.income,
|
||||||
netRevenue: point.netRevenue,
|
|
||||||
cogs: point.cogs,
|
cogs: point.cogs,
|
||||||
profit: point.profit,
|
profit: point.profit,
|
||||||
margin: point.margin,
|
margin: point.margin,
|
||||||
@@ -1174,18 +1245,15 @@ const FinancialOverview = () => {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Date</TableHead>
|
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Date</TableHead>
|
||||||
{metrics.grossSales && (
|
{metrics.income && (
|
||||||
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Gross Sales</TableHead>
|
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Total Income</TableHead>
|
||||||
)}
|
|
||||||
{metrics.netRevenue && (
|
|
||||||
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Net Revenue</TableHead>
|
|
||||||
)}
|
|
||||||
{metrics.profit && (
|
|
||||||
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Profit</TableHead>
|
|
||||||
)}
|
)}
|
||||||
{metrics.cogs && (
|
{metrics.cogs && (
|
||||||
<TableHead className="px-6 py-3 whitespace-nowrap text-center">COGS</TableHead>
|
<TableHead className="px-6 py-3 whitespace-nowrap text-center">COGS</TableHead>
|
||||||
)}
|
)}
|
||||||
|
{metrics.profit && (
|
||||||
|
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Gross Profit</TableHead>
|
||||||
|
)}
|
||||||
{metrics.margin && (
|
{metrics.margin && (
|
||||||
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Margin</TableHead>
|
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Margin</TableHead>
|
||||||
)}
|
)}
|
||||||
@@ -1197,19 +1265,9 @@ const FinancialOverview = () => {
|
|||||||
<TableCell className="px-6 py-3 whitespace-nowrap text-center text-sm text-muted-foreground">
|
<TableCell className="px-6 py-3 whitespace-nowrap text-center text-sm text-muted-foreground">
|
||||||
{row.label || "—"}
|
{row.label || "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{metrics.grossSales && (
|
{metrics.income && (
|
||||||
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
|
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
|
||||||
{row.isFuture ? "—" : formatCurrency(row.grossSales, 0)}
|
{row.isFuture ? "—" : formatCurrency(row.income, 0)}
|
||||||
</TableCell>
|
|
||||||
)}
|
|
||||||
{metrics.netRevenue && (
|
|
||||||
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
|
|
||||||
{row.isFuture ? "—" : formatCurrency(row.netRevenue, 0)}
|
|
||||||
</TableCell>
|
|
||||||
)}
|
|
||||||
{metrics.profit && (
|
|
||||||
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
|
|
||||||
{row.isFuture ? "—" : formatCurrency(row.profit, 0)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{metrics.cogs && (
|
{metrics.cogs && (
|
||||||
@@ -1217,6 +1275,11 @@ const FinancialOverview = () => {
|
|||||||
{row.isFuture ? "—" : formatCurrency(row.cogs, 0)}
|
{row.isFuture ? "—" : formatCurrency(row.cogs, 0)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
|
{metrics.profit && (
|
||||||
|
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
|
||||||
|
{row.isFuture ? "—" : formatCurrency(row.profit, 0)}
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
{metrics.margin && (
|
{metrics.margin && (
|
||||||
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
|
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
|
||||||
{row.isFuture ? "—" : formatPercentage(row.margin, 1)}
|
{row.isFuture ? "—" : formatPercentage(row.margin, 1)}
|
||||||
@@ -1387,13 +1450,9 @@ const FinancialOverview = () => {
|
|||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<ComposedChart data={chartData} margin={{ top: 10, right: 30, left: -10, bottom: 0 }}>
|
<ComposedChart data={chartData} margin={{ top: 10, right: 30, left: -10, bottom: 0 }}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="financialGrossSales" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="financialIncome" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="5%" stopColor={chartColors.grossSales} stopOpacity={0.35} />
|
<stop offset="5%" stopColor={chartColors.income} stopOpacity={0.3} />
|
||||||
<stop offset="95%" stopColor={chartColors.grossSales} stopOpacity={0.05} />
|
<stop offset="95%" stopColor={chartColors.income} stopOpacity={0.05} />
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="financialNetRevenue" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="5%" stopColor={chartColors.netRevenue} stopOpacity={0.3} />
|
|
||||||
<stop offset="95%" stopColor={chartColors.netRevenue} stopOpacity={0.05} />
|
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="financialCogs" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="financialCogs" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="5%" stopColor={chartColors.cogs} stopOpacity={0.25} />
|
<stop offset="5%" stopColor={chartColors.cogs} stopOpacity={0.25} />
|
||||||
@@ -1424,25 +1483,14 @@ const FinancialOverview = () => {
|
|||||||
)}
|
)}
|
||||||
<Tooltip content={<FinancialTooltip />} />
|
<Tooltip content={<FinancialTooltip />} />
|
||||||
<Legend formatter={(value: string) => SERIES_LABELS[value as ChartSeriesKey] ?? value} />
|
<Legend formatter={(value: string) => SERIES_LABELS[value as ChartSeriesKey] ?? value} />
|
||||||
{metrics.grossSales ? (
|
{metrics.income ? (
|
||||||
<Area
|
<Area
|
||||||
yAxisId="left"
|
yAxisId="left"
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="grossSales"
|
dataKey="income"
|
||||||
name={SERIES_LABELS.grossSales}
|
name={SERIES_LABELS.income}
|
||||||
stroke={chartColors.grossSales}
|
stroke={chartColors.income}
|
||||||
fill="url(#financialGrossSales)"
|
fill="url(#financialIncome)"
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{metrics.netRevenue ? (
|
|
||||||
<Area
|
|
||||||
yAxisId="left"
|
|
||||||
type="monotone"
|
|
||||||
dataKey="netRevenue"
|
|
||||||
name={SERIES_LABELS.netRevenue}
|
|
||||||
stroke={chartColors.netRevenue}
|
|
||||||
fill="url(#financialNetRevenue)"
|
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -1510,7 +1558,7 @@ function FinancialStatGrid({
|
|||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{cards.map((card) => (
|
{cards.map((card) => (
|
||||||
<FinancialStatCard key={card.key} {...card} />
|
<FinancialStatCard key={card.key} title={card.title} value={card.value} description={card.description} trend={card.trend} accentClass={card.accentClass} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { Card, CardContent, CardHeader } from "@/components/dashboard/ui/card";
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/dashboard/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -14,8 +14,8 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/dashboard/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
Clock,
|
Clock,
|
||||||
Star,
|
Star,
|
||||||
@@ -225,13 +225,6 @@ const GorgiasOverview = () => {
|
|||||||
.then(res => res.data?.data?.data?.data || []),
|
.then(res => res.data?.data?.data?.data || []),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log('Raw API responses:', {
|
|
||||||
overview,
|
|
||||||
channelStats,
|
|
||||||
agentStats,
|
|
||||||
satisfaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
setData({
|
setData({
|
||||||
overview,
|
overview,
|
||||||
channels: channelStats,
|
channels: channelStats,
|
||||||
@@ -270,8 +263,6 @@ const GorgiasOverview = () => {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
console.log('Processed stats:', stats);
|
|
||||||
|
|
||||||
// Process satisfaction data
|
// Process satisfaction data
|
||||||
const satisfactionStats = (data.satisfaction || []).reduce((acc, item) => {
|
const satisfactionStats = (data.satisfaction || []).reduce((acc, item) => {
|
||||||
if (item.name !== 'response_distribution') {
|
if (item.name !== 'response_distribution') {
|
||||||
@@ -285,8 +276,6 @@ const GorgiasOverview = () => {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
console.log('Processed satisfaction stats:', satisfactionStats);
|
|
||||||
|
|
||||||
// Process channel data
|
// Process channel data
|
||||||
const channels = data.channels?.map(line => ({
|
const channels = data.channels?.map(line => ({
|
||||||
name: line[0]?.value || '',
|
name: line[0]?.value || '',
|
||||||
@@ -295,8 +284,6 @@ const GorgiasOverview = () => {
|
|||||||
delta: line[3]?.value || 0
|
delta: line[3]?.value || 0
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
console.log('Processed channels:', channels);
|
|
||||||
|
|
||||||
// Process agent data
|
// Process agent data
|
||||||
const agents = data.agents?.map(line => ({
|
const agents = data.agents?.map(line => ({
|
||||||
name: line[0]?.value || '',
|
name: line[0]?.value || '',
|
||||||
@@ -306,8 +293,6 @@ const GorgiasOverview = () => {
|
|||||||
delta: line[4]?.value || 0
|
delta: line[4]?.value || 0
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
console.log('Processed agents:', agents);
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Card className="h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
<Card className="h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Card, CardContent } from "@/components/dashboard/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
Clock,
|
Clock,
|
||||||
@@ -33,13 +33,13 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/dashboard/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/dashboard/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { Alert, AlertDescription } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
|
||||||
const CraftsIcon = () => (
|
const CraftsIcon = () => (
|
||||||
<svg viewBox="0 0 2687 3338" className="w-6 h-6" aria-hidden="true">
|
<svg viewBox="0 0 2687 3338" className="w-6 h-6" aria-hidden="true">
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/dashboard/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/dashboard/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -13,11 +13,11 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/dashboard/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { TIME_RANGES } from "@/lib/dashboard/constants";
|
import { TIME_RANGES } from "@/lib/dashboard/constants";
|
||||||
import { Mail, MessageSquare, ArrowUpDown, BookOpen } from "lucide-react";
|
import { Mail, MessageSquare, ArrowUpDown, BookOpen } from "lucide-react";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
// Helper functions for formatting
|
// Helper functions for formatting
|
||||||
const formatRate = (value, isSMS = false, hideForSMS = false) => {
|
const formatRate = (value, isSMS = false, hideForSMS = false) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Lock } from "lucide-react";
|
import { Lock } from "lucide-react";
|
||||||
|
|
||||||
const LockButton = () => {
|
const LockButton = () => {
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/dashboard/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/dashboard/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/dashboard/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
import {
|
||||||
Instagram,
|
Instagram,
|
||||||
Loader2,
|
Loader2,
|
||||||
@@ -27,8 +27,8 @@ import {
|
|||||||
MessageCircle,
|
MessageCircle,
|
||||||
Hash,
|
Hash,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
// Helper functions for formatting
|
// Helper functions for formatting
|
||||||
const formatCurrency = (value, decimalPlaces = 2) =>
|
const formatCurrency = (value, decimalPlaces = 2) =>
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/dashboard/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/dashboard/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { ScrollArea } from "@/components/dashboard/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import {
|
import {
|
||||||
Package,
|
Package,
|
||||||
Truck,
|
Truck,
|
||||||
@@ -22,10 +22,10 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { EventDialog } from "./EventFeed.jsx";
|
import { EventDialog } from "./EventFeed.jsx";
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
const METRIC_IDS = {
|
const METRIC_IDS = {
|
||||||
PLACED_ORDER: "Y8cqcF",
|
PLACED_ORDER: "Y8cqcF",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/dashboard/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
BarChart,
|
BarChart,
|
||||||
Bar,
|
Bar,
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { AlertTriangle, Users, Activity } from "lucide-react";
|
import { AlertTriangle, Users, Activity } from "lucide-react";
|
||||||
import { Alert, AlertDescription } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import {
|
import {
|
||||||
summaryCard,
|
summaryCard,
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
SkeletonBarChart,
|
SkeletonBarChart,
|
||||||
processBasicData,
|
processBasicData,
|
||||||
} from "./RealtimeAnalytics";
|
} from "./RealtimeAnalytics";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
const SkeletonCard = ({ colorScheme = "sky" }) => (
|
const SkeletonCard = ({ colorScheme = "sky" }) => (
|
||||||
<Card className={`w-full h-[150px] bg-gradient-to-br from-sky-900 to-sky-800 backdrop-blur-sm`}>
|
<Card className={`w-full h-[150px] bg-gradient-to-br from-sky-900 to-sky-800 backdrop-blur-sm`}>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/dashboard/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
LineChart,
|
LineChart,
|
||||||
Line,
|
Line,
|
||||||
@@ -18,8 +18,8 @@ import {
|
|||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { AlertCircle, TrendingUp, DollarSign, ShoppingCart, Truck, PiggyBank, ArrowUp,ArrowDown, Banknote, Package } from "lucide-react";
|
import { AlertCircle, TrendingUp, DollarSign, ShoppingCart, Truck, PiggyBank, ArrowUp,ArrowDown, Banknote, Package } from "lucide-react";
|
||||||
import { formatCurrency, CustomTooltip, processData, StatCard } from "./SalesChart.jsx";
|
import { formatCurrency, CustomTooltip, processData, StatCard } from "./SalesChart.jsx";
|
||||||
|
|
||||||
|
|||||||
@@ -7,23 +7,23 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/dashboard/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/dashboard/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/dashboard/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { TIME_RANGES } from "@/lib/dashboard/constants";
|
import { TIME_RANGES } from "@/lib/dashboard/constants";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import {
|
import {
|
||||||
DollarSign,
|
DollarSign,
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
@@ -32,13 +32,13 @@ import {
|
|||||||
CircleDollarSign,
|
CircleDollarSign,
|
||||||
Loader2,
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
} from "@/components/dashboard/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -46,7 +46,7 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/dashboard/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
|
||||||
// Import the detail view components and utilities from StatCards
|
// Import the detail view components and utilities from StatCards
|
||||||
import {
|
import {
|
||||||
@@ -256,16 +256,6 @@ const MiniStatCards = ({
|
|||||||
: stats.revenue;
|
: stats.revenue;
|
||||||
const prevRevenue = stats.prevPeriodRevenue; // Previous period's total revenue
|
const prevRevenue = stats.prevPeriodRevenue; // Previous period's total revenue
|
||||||
|
|
||||||
console.log('[MiniStatCards RevenueTrend Debug]', {
|
|
||||||
periodProgress: stats.periodProgress,
|
|
||||||
currentRevenue,
|
|
||||||
smartProjection: projection?.projectedRevenue,
|
|
||||||
simpleProjection: stats.projectedRevenue,
|
|
||||||
actualRevenue: stats.revenue,
|
|
||||||
prevRevenue,
|
|
||||||
isProjected: stats.periodProgress < 100
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!currentRevenue || !prevRevenue) return null;
|
if (!currentRevenue || !prevRevenue) return null;
|
||||||
|
|
||||||
// Calculate absolute difference percentage
|
// Calculate absolute difference percentage
|
||||||
@@ -273,12 +263,6 @@ const MiniStatCards = ({
|
|||||||
const diff = Math.abs(currentRevenue - prevRevenue);
|
const diff = Math.abs(currentRevenue - prevRevenue);
|
||||||
const percentage = (diff / prevRevenue) * 100;
|
const percentage = (diff / prevRevenue) * 100;
|
||||||
|
|
||||||
console.log('[MiniStatCards RevenueTrend Result]', {
|
|
||||||
trend,
|
|
||||||
percentage,
|
|
||||||
calculation: `(|${currentRevenue} - ${prevRevenue}| / ${prevRevenue}) * 100 = ${percentage}%`
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
trend,
|
trend,
|
||||||
value: percentage,
|
value: percentage,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/dashboard/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useScroll } from "@/contexts/DashboardScrollContext";
|
import { useScroll } from "@/contexts/DashboardScrollContext";
|
||||||
import { ArrowUpToLine } from "lucide-react";
|
import { ArrowUpToLine } from "lucide-react";
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import {
|
|||||||
InputOTP,
|
InputOTP,
|
||||||
InputOTPGroup,
|
InputOTPGroup,
|
||||||
InputOTPSlot,
|
InputOTPSlot,
|
||||||
} from "@/components/dashboard/ui/input-otp"
|
} from "@/components/ui/input-otp"
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/dashboard/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Lock, Delete } from "lucide-react";
|
import { Lock, Delete } from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { acotService } from "@/services/dashboard/acotService";
|
import { acotService } from "@/services/dashboard/acotService";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/dashboard/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { ScrollArea } from "@/components/dashboard/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Loader2, ArrowUpDown, AlertCircle, Package, Settings2, Search, X } from "lucide-react";
|
import { Loader2, ArrowUpDown, AlertCircle, Package, Settings2, Search, X } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -11,26 +11,26 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/dashboard/ui/table";
|
} from "@/components/ui/table";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/dashboard/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Input } from "@/components/dashboard/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { TIME_RANGES } from "@/lib/dashboard/constants";
|
import { TIME_RANGES } from "@/lib/dashboard/constants";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/dashboard/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
|
||||||
const ProductGrid = ({
|
const ProductGrid = ({
|
||||||
timeRange = "today",
|
timeRange = "today",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/dashboard/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
BarChart,
|
BarChart,
|
||||||
Bar,
|
Bar,
|
||||||
@@ -17,10 +17,10 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
} from "@/components/dashboard/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Alert, AlertDescription } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/dashboard/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
@@ -28,9 +28,9 @@ import {
|
|||||||
TableBody,
|
TableBody,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableCell,
|
TableCell,
|
||||||
} from "@/components/dashboard/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
export const METRIC_COLORS = {
|
export const METRIC_COLORS = {
|
||||||
activeUsers: {
|
activeUsers: {
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/dashboard/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/dashboard/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Input } from "@/components/dashboard/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/dashboard/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Loader2,
|
Loader2,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
@@ -44,7 +44,7 @@ import {
|
|||||||
formatDateForInput,
|
formatDateForInput,
|
||||||
parseDateFromInput,
|
parseDateFromInput,
|
||||||
} from "@/lib/dashboard/constants";
|
} from "@/lib/dashboard/constants";
|
||||||
import { Checkbox } from "@/components/dashboard/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
@@ -52,7 +52,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
} from "@/components/dashboard/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -61,16 +61,16 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/dashboard/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Separator } from "@/components/dashboard/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/dashboard/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/dashboard/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
|
||||||
const METRIC_IDS = {
|
const METRIC_IDS = {
|
||||||
PLACED_ORDER: "Y8cqcF",
|
PLACED_ORDER: "Y8cqcF",
|
||||||
|
|||||||
@@ -7,20 +7,20 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/dashboard/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/dashboard/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/dashboard/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
LineChart,
|
LineChart,
|
||||||
Line,
|
Line,
|
||||||
@@ -67,12 +67,12 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/dashboard/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/dashboard/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Progress } from "@/components/dashboard/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/dashboard/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
const formatCurrency = (value, minimumFractionDigits = 0) => {
|
const formatCurrency = (value, minimumFractionDigits = 0) => {
|
||||||
if (!value || isNaN(value)) return "$0";
|
if (!value || isNaN(value)) return "$0";
|
||||||
@@ -1397,12 +1397,10 @@ const StatCards = ({
|
|||||||
const cachedData = getCacheData(detailTimeRange, metric);
|
const cachedData = getCacheData(detailTimeRange, metric);
|
||||||
|
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
console.log(`Using cached data for ${metric}`);
|
|
||||||
setDetailData((prev) => ({ ...prev, [metric]: cachedData }));
|
setDetailData((prev) => ({ ...prev, [metric]: cachedData }));
|
||||||
return cachedData;
|
return cachedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Fetching detail data for ${metric}`);
|
|
||||||
setDetailDataLoading((prev) => ({ ...prev, [metric]: true }));
|
setDetailDataLoading((prev) => ({ ...prev, [metric]: true }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1481,7 +1479,6 @@ const StatCards = ({
|
|||||||
eventType: "PLACED_ORDER",
|
eventType: "PLACED_ORDER",
|
||||||
});
|
});
|
||||||
const data = response.stats;
|
const data = response.stats;
|
||||||
console.log("Fetched order range data:", data);
|
|
||||||
setCacheData(detailTimeRange, metric, data);
|
setCacheData(detailTimeRange, metric, data);
|
||||||
setDetailData((prev) => ({ ...prev, [metric]: data }));
|
setDetailData((prev) => ({ ...prev, [metric]: data }));
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -1570,16 +1567,6 @@ const StatCards = ({
|
|||||||
: stats.revenue;
|
: stats.revenue;
|
||||||
const prevRevenue = stats.prevPeriodRevenue; // Previous period's total revenue
|
const prevRevenue = stats.prevPeriodRevenue; // Previous period's total revenue
|
||||||
|
|
||||||
console.log('[RevenueTrend Debug]', {
|
|
||||||
periodProgress: stats.periodProgress,
|
|
||||||
currentRevenue,
|
|
||||||
smartProjection: projection?.projectedRevenue,
|
|
||||||
simpleProjection: stats.projectedRevenue,
|
|
||||||
actualRevenue: stats.revenue,
|
|
||||||
prevRevenue,
|
|
||||||
isProjected: stats.periodProgress < 100
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!currentRevenue || !prevRevenue) return null;
|
if (!currentRevenue || !prevRevenue) return null;
|
||||||
|
|
||||||
// Calculate absolute difference percentage
|
// Calculate absolute difference percentage
|
||||||
@@ -1587,12 +1574,6 @@ const StatCards = ({
|
|||||||
const diff = Math.abs(currentRevenue - prevRevenue);
|
const diff = Math.abs(currentRevenue - prevRevenue);
|
||||||
const percentage = (diff / prevRevenue) * 100;
|
const percentage = (diff / prevRevenue) * 100;
|
||||||
|
|
||||||
console.log('[RevenueTrend Result]', {
|
|
||||||
trend,
|
|
||||||
percentage,
|
|
||||||
calculation: `(|${currentRevenue} - ${prevRevenue}| / ${prevRevenue}) * 100 = ${percentage}%`
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
trend,
|
trend,
|
||||||
value: percentage,
|
value: percentage,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/dashboard/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -14,11 +14,11 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/dashboard/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Badge } from "@/components/dashboard/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { ScrollArea } from "@/components/dashboard/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/dashboard/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { AlertCircle } from "lucide-react";
|
import { AlertCircle } from "lucide-react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/dashboard/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/dashboard/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/dashboard/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/dashboard/ui/table";
|
} from "@/components/ui/table";
|
||||||
import {
|
import {
|
||||||
PieChart,
|
PieChart,
|
||||||
Pie,
|
Pie,
|
||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
Legend,
|
Legend,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { Skeleton } from "@/components/dashboard/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
// Add skeleton components
|
// Add skeleton components
|
||||||
const SkeletonTable = ({ rows = 12 }) => (
|
const SkeletonTable = ({ rows = 12 }) => (
|
||||||
@@ -86,7 +86,6 @@ export const UserBehaviorDashboard = () => {
|
|||||||
|
|
||||||
const processPageData = (data) => {
|
const processPageData = (data) => {
|
||||||
if (!data?.rows) {
|
if (!data?.rows) {
|
||||||
console.log("No rows in page data");
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +100,6 @@ export const UserBehaviorDashboard = () => {
|
|||||||
|
|
||||||
const processDeviceData = (data) => {
|
const processDeviceData = (data) => {
|
||||||
if (!data?.rows) {
|
if (!data?.rows) {
|
||||||
console.log("No rows in device data");
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +121,6 @@ export const UserBehaviorDashboard = () => {
|
|||||||
|
|
||||||
const processSourceData = (data) => {
|
const processSourceData = (data) => {
|
||||||
if (!data?.rows) {
|
if (!data?.rows) {
|
||||||
console.log("No rows in source data");
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +147,6 @@ export const UserBehaviorDashboard = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
console.log("Raw user behavior response:", result);
|
|
||||||
|
|
||||||
if (!result?.success) {
|
if (!result?.success) {
|
||||||
throw new Error("Invalid response structure");
|
throw new Error("Invalid response structure");
|
||||||
@@ -164,12 +160,6 @@ export const UserBehaviorDashboard = () => {
|
|||||||
const deviceResponse = rawData?.deviceResponse || rawData?.reports?.[1];
|
const deviceResponse = rawData?.deviceResponse || rawData?.reports?.[1];
|
||||||
const sourceResponse = rawData?.sourceResponse || rawData?.reports?.[2];
|
const sourceResponse = rawData?.sourceResponse || rawData?.reports?.[2];
|
||||||
|
|
||||||
console.log("Extracted responses:", {
|
|
||||||
pageResponse,
|
|
||||||
deviceResponse,
|
|
||||||
sourceResponse,
|
|
||||||
});
|
|
||||||
|
|
||||||
const processed = {
|
const processed = {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
@@ -181,7 +171,6 @@ export const UserBehaviorDashboard = () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Final processed data:", processed);
|
|
||||||
setData(processed);
|
setData(processed);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch behavior data:", error);
|
console.error("Failed to fetch behavior data:", error);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Moon, Sun } from "lucide-react"
|
import { Moon, Sun } from "lucide-react"
|
||||||
import { useTheme } from "@/components/dashboard/theme/ThemeProvider"
|
import { useTheme } from "@/components/dashboard/theme/ThemeProvider"
|
||||||
import { Button } from "@/components/dashboard/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { cva } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const alertVariants = cva(
|
|
||||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-background text-foreground",
|
|
||||||
destructive:
|
|
||||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const Alert = React.forwardRef(({ className, variant, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
role="alert"
|
|
||||||
className={cn(alertVariants({ variant }), className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
Alert.displayName = "Alert"
|
|
||||||
|
|
||||||
const AlertTitle = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<h5
|
|
||||||
ref={ref}
|
|
||||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
AlertTitle.displayName = "AlertTitle"
|
|
||||||
|
|
||||||
const AlertDescription = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
AlertDescription.displayName = "AlertDescription"
|
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription }
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { cva } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const badgeVariants = cva(
|
|
||||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
|
||||||
secondary:
|
|
||||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
||||||
destructive:
|
|
||||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
|
||||||
outline: "text-foreground",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Badge({
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (<div className={cn(badgeVariants({ variant }), className)} {...props} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const buttonVariants = cva(
|
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
||||||
destructive:
|
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
||||||
outline:
|
|
||||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
||||||
secondary:
|
|
||||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-9 px-4 py-2",
|
|
||||||
sm: "h-8 rounded-md px-3 text-xs",
|
|
||||||
lg: "h-10 rounded-md px-8",
|
|
||||||
icon: "h-9 w-9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
return (
|
|
||||||
(<Comp
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
ref={ref}
|
|
||||||
{...props} />)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
Button.displayName = "Button"
|
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
||||||
import { DayPicker } from "react-day-picker"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { buttonVariants } from "@/components/dashboard/ui/button"
|
|
||||||
|
|
||||||
function Calendar({
|
|
||||||
className,
|
|
||||||
classNames,
|
|
||||||
showOutsideDays = true,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
(<DayPicker
|
|
||||||
showOutsideDays={showOutsideDays}
|
|
||||||
className={cn("p-3", className)}
|
|
||||||
classNames={{
|
|
||||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
|
||||||
month: "space-y-4",
|
|
||||||
caption: "flex justify-center pt-1 relative items-center",
|
|
||||||
caption_label: "text-sm font-medium",
|
|
||||||
nav: "space-x-1 flex items-center",
|
|
||||||
nav_button: cn(
|
|
||||||
buttonVariants({ variant: "outline" }),
|
|
||||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
|
||||||
),
|
|
||||||
nav_button_previous: "absolute left-1",
|
|
||||||
nav_button_next: "absolute right-1",
|
|
||||||
table: "w-full border-collapse space-y-1",
|
|
||||||
head_row: "flex",
|
|
||||||
head_cell:
|
|
||||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
|
||||||
row: "flex w-full mt-2",
|
|
||||||
cell: cn(
|
|
||||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
|
||||||
props.mode === "range"
|
|
||||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
|
||||||
: "[&:has([aria-selected])]:rounded-md"
|
|
||||||
),
|
|
||||||
day: cn(
|
|
||||||
buttonVariants({ variant: "ghost" }),
|
|
||||||
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
|
|
||||||
),
|
|
||||||
day_range_start: "day-range-start",
|
|
||||||
day_range_end: "day-range-end",
|
|
||||||
day_selected:
|
|
||||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
|
||||||
day_today: "bg-accent text-accent-foreground",
|
|
||||||
day_outside:
|
|
||||||
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
|
||||||
day_disabled: "text-muted-foreground opacity-50",
|
|
||||||
day_range_middle:
|
|
||||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
|
||||||
day_hidden: "invisible",
|
|
||||||
...classNames,
|
|
||||||
}}
|
|
||||||
components={{
|
|
||||||
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
|
|
||||||
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
|
|
||||||
}}
|
|
||||||
{...props} />)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Calendar.displayName = "Calendar"
|
|
||||||
|
|
||||||
export { Calendar }
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
||||||
import { DayPicker } from "react-day-picker"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { buttonVariants } from "@/components/dashboard/ui/button"
|
|
||||||
|
|
||||||
function Calendar({
|
|
||||||
className,
|
|
||||||
classNames,
|
|
||||||
showOutsideDays = true,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DayPicker
|
|
||||||
showOutsideDays={showOutsideDays}
|
|
||||||
className={cn("p-2", className)}
|
|
||||||
classNames={{
|
|
||||||
months: "flex flex-col sm:flex-row space-y-2 sm:space-x-2 sm:space-y-0",
|
|
||||||
month: "w-full",
|
|
||||||
caption: "flex justify-center relative items-center",
|
|
||||||
caption_label: "text-lg font-medium", // Reduced from text-4xl
|
|
||||||
nav: "space-x-1 flex items-center",
|
|
||||||
nav_button: cn(
|
|
||||||
buttonVariants({ variant: "ghost", size: "sm"}), // Changed from lg to sm
|
|
||||||
"h-6 w-6" // Reduced from h-12 w-18
|
|
||||||
),
|
|
||||||
nav_button_previous: "absolute left-1",
|
|
||||||
nav_button_next: "absolute right-1",
|
|
||||||
table: "w-full border-collapse space-y-1",
|
|
||||||
head_row: "flex",
|
|
||||||
head_cell: "text-muted-foreground rounded-md w-6 font-normal text-[0.7rem] w-full", // Reduced sizes
|
|
||||||
row: "flex w-full mt-1", // Reduced margin
|
|
||||||
cell: cn(
|
|
||||||
"w-full relative p-0 text-center text-xs focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
|
||||||
props.mode === "range"
|
|
||||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
|
||||||
: "[&:has([aria-selected])]:rounded-md"
|
|
||||||
),
|
|
||||||
day: cn(
|
|
||||||
buttonVariants({ variant: "ghost" }),
|
|
||||||
"h-6 w-6 p-0 font-normal text-xs aria-selected:opacity-100" // Reduced from h-12 w-12 and text-lg
|
|
||||||
),
|
|
||||||
day_range_start: "day-range-start",
|
|
||||||
day_range_end: "day-range-end",
|
|
||||||
day_selected:
|
|
||||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
|
||||||
day_today: "bg-accent text-accent-foreground",
|
|
||||||
day_outside:
|
|
||||||
"day-outside text-muted-foreground/50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
|
||||||
day_disabled: "text-muted-foreground opacity-50",
|
|
||||||
day_range_middle:
|
|
||||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
|
||||||
day_hidden: "invisible",
|
|
||||||
...classNames,
|
|
||||||
}}
|
|
||||||
components={{
|
|
||||||
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
|
|
||||||
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Calendar.displayName = "Calendar"
|
|
||||||
export { Calendar }
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Card = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
Card.displayName = "Card"
|
|
||||||
|
|
||||||
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
CardHeader.displayName = "CardHeader"
|
|
||||||
|
|
||||||
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
CardTitle.displayName = "CardTitle"
|
|
||||||
|
|
||||||
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
CardDescription.displayName = "CardDescription"
|
|
||||||
|
|
||||||
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
||||||
))
|
|
||||||
CardContent.displayName = "CardContent"
|
|
||||||
|
|
||||||
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("flex items-center p-6 pt-0", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
CardFooter.displayName = "CardFooter"
|
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as RechartsPrimitive from "recharts"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
|
||||||
const THEMES = {
|
|
||||||
light: "",
|
|
||||||
dark: ".dark"
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartContext = React.createContext(null)
|
|
||||||
|
|
||||||
function useChart() {
|
|
||||||
const context = React.useContext(ChartContext)
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useChart must be used within a <ChartContainer />")
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartContainer = React.forwardRef(({ id, className, children, config, ...props }, ref) => {
|
|
||||||
const uniqueId = React.useId()
|
|
||||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
|
||||||
|
|
||||||
return (
|
|
||||||
(<ChartContext.Provider value={{ config }}>
|
|
||||||
<div
|
|
||||||
data-chart={chartId}
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}>
|
|
||||||
<ChartStyle id={chartId} config={config} />
|
|
||||||
<RechartsPrimitive.ResponsiveContainer>
|
|
||||||
{children}
|
|
||||||
</RechartsPrimitive.ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</ChartContext.Provider>)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
ChartContainer.displayName = "Chart"
|
|
||||||
|
|
||||||
const ChartStyle = ({
|
|
||||||
id,
|
|
||||||
config
|
|
||||||
}) => {
|
|
||||||
const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color)
|
|
||||||
|
|
||||||
if (!colorConfig.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
(<style
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: Object.entries(THEMES)
|
|
||||||
.map(([theme, prefix]) => `
|
|
||||||
${prefix} [data-chart=${id}] {
|
|
||||||
${colorConfig
|
|
||||||
.map(([key, itemConfig]) => {
|
|
||||||
const color =
|
|
||||||
itemConfig.theme?.[theme] ||
|
|
||||||
itemConfig.color
|
|
||||||
return color ? ` --color-${key}: ${color};` : null
|
|
||||||
})
|
|
||||||
.join("\n")}
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
.join("\n"),
|
|
||||||
}} />)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
|
||||||
|
|
||||||
const ChartTooltipContent = React.forwardRef((
|
|
||||||
{
|
|
||||||
active,
|
|
||||||
payload,
|
|
||||||
className,
|
|
||||||
indicator = "dot",
|
|
||||||
hideLabel = false,
|
|
||||||
hideIndicator = false,
|
|
||||||
label,
|
|
||||||
labelFormatter,
|
|
||||||
labelClassName,
|
|
||||||
formatter,
|
|
||||||
color,
|
|
||||||
nameKey,
|
|
||||||
labelKey,
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const { config } = useChart()
|
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
|
||||||
if (hideLabel || !payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const [item] = payload
|
|
||||||
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
const value =
|
|
||||||
!labelKey && typeof label === "string"
|
|
||||||
? config[label]?.label || label
|
|
||||||
: itemConfig?.label
|
|
||||||
|
|
||||||
if (labelFormatter) {
|
|
||||||
return (
|
|
||||||
(<div className={cn("font-medium", labelClassName)}>
|
|
||||||
{labelFormatter(value, payload)}
|
|
||||||
</div>)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
|
||||||
}, [
|
|
||||||
label,
|
|
||||||
labelFormatter,
|
|
||||||
payload,
|
|
||||||
hideLabel,
|
|
||||||
labelClassName,
|
|
||||||
config,
|
|
||||||
labelKey,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!active || !payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
|
||||||
|
|
||||||
return (
|
|
||||||
(<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
|
||||||
className
|
|
||||||
)}>
|
|
||||||
{!nestLabel ? tooltipLabel : null}
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
{payload.map((item, index) => {
|
|
||||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
const indicatorColor = color || item.payload.fill || item.color
|
|
||||||
|
|
||||||
return (
|
|
||||||
(<div
|
|
||||||
key={item.dataKey}
|
|
||||||
className={cn(
|
|
||||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
|
||||||
indicator === "dot" && "items-center"
|
|
||||||
)}>
|
|
||||||
{formatter && item?.value !== undefined && item.name ? (
|
|
||||||
formatter(item.value, item.name, item, index, item.payload)
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{itemConfig?.icon ? (
|
|
||||||
<itemConfig.icon />
|
|
||||||
) : (
|
|
||||||
!hideIndicator && (
|
|
||||||
<div
|
|
||||||
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
|
|
||||||
"h-2.5 w-2.5": indicator === "dot",
|
|
||||||
"w-1": indicator === "line",
|
|
||||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
|
||||||
indicator === "dashed",
|
|
||||||
"my-0.5": nestLabel && indicator === "dashed",
|
|
||||||
})}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--color-bg": indicatorColor,
|
|
||||||
"--color-border": indicatorColor
|
|
||||||
}
|
|
||||||
} />
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 justify-between leading-none",
|
|
||||||
nestLabel ? "items-end" : "items-center"
|
|
||||||
)}>
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
{nestLabel ? tooltipLabel : null}
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{itemConfig?.label || item.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{item.value && (
|
|
||||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
|
||||||
{item.value.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>)
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
ChartTooltipContent.displayName = "ChartTooltip"
|
|
||||||
|
|
||||||
const ChartLegend = RechartsPrimitive.Legend
|
|
||||||
|
|
||||||
const ChartLegendContent = React.forwardRef((
|
|
||||||
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const { config } = useChart()
|
|
||||||
|
|
||||||
if (!payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
(<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-center gap-4",
|
|
||||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
|
||||||
className
|
|
||||||
)}>
|
|
||||||
{payload.map((item) => {
|
|
||||||
const key = `${nameKey || item.dataKey || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
|
|
||||||
return (
|
|
||||||
(<div
|
|
||||||
key={item.value}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
|
||||||
)}>
|
|
||||||
{itemConfig?.icon && !hideIcon ? (
|
|
||||||
<itemConfig.icon />
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
|
||||||
style={{
|
|
||||||
backgroundColor: item.color,
|
|
||||||
}} />
|
|
||||||
)}
|
|
||||||
{itemConfig?.label}
|
|
||||||
</div>)
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
ChartLegendContent.displayName = "ChartLegend"
|
|
||||||
|
|
||||||
// Helper to extract item config from a payload.
|
|
||||||
function getPayloadConfigFromPayload(
|
|
||||||
config,
|
|
||||||
payload,
|
|
||||||
key
|
|
||||||
) {
|
|
||||||
if (typeof payload !== "object" || payload === null) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const payloadPayload =
|
|
||||||
"payload" in payload &&
|
|
||||||
typeof payload.payload === "object" &&
|
|
||||||
payload.payload !== null
|
|
||||||
? payload.payload
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
let configLabelKey = key
|
|
||||||
|
|
||||||
if (
|
|
||||||
key in payload &&
|
|
||||||
typeof payload[key] === "string"
|
|
||||||
) {
|
|
||||||
configLabelKey = payload[key]
|
|
||||||
} else if (
|
|
||||||
payloadPayload &&
|
|
||||||
key in payloadPayload &&
|
|
||||||
typeof payloadPayload[key] === "string"
|
|
||||||
) {
|
|
||||||
configLabelKey = payloadPayload[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
return configLabelKey in config
|
|
||||||
? config[configLabelKey]
|
|
||||||
: config[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
ChartContainer,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartStyle,
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
||||||
import { Check } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Checkbox = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<CheckboxPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}>
|
|
||||||
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</CheckboxPrimitive.Indicator>
|
|
||||||
</CheckboxPrimitive.Root>
|
|
||||||
))
|
|
||||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { Checkbox }
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
||||||
import { X } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root
|
|
||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger
|
|
||||||
|
|
||||||
const DialogPortal = DialogPrimitive.Portal
|
|
||||||
|
|
||||||
const DialogClose = DialogPrimitive.Close
|
|
||||||
|
|
||||||
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Overlay
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
|
||||||
<DialogPortal>
|
|
||||||
<DialogOverlay />
|
|
||||||
<DialogPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}>
|
|
||||||
{children}
|
|
||||||
<DialogPrimitive.Close
|
|
||||||
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
</DialogPrimitive.Content>
|
|
||||||
</DialogPortal>
|
|
||||||
))
|
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const DialogHeader = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) => (
|
|
||||||
<div
|
|
||||||
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
|
|
||||||
{...props} />
|
|
||||||
)
|
|
||||||
DialogHeader.displayName = "DialogHeader"
|
|
||||||
|
|
||||||
const DialogFooter = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) => (
|
|
||||||
<div
|
|
||||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
|
||||||
{...props} />
|
|
||||||
)
|
|
||||||
DialogFooter.displayName = "DialogFooter"
|
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
|
||||||
|
|
||||||
export {
|
|
||||||
Dialog,
|
|
||||||
DialogPortal,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogTrigger,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
||||||
|
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
||||||
|
|
||||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
||||||
|
|
||||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}>
|
|
||||||
{children}
|
|
||||||
<ChevronRight className="ml-auto" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
))
|
|
||||||
DropdownMenuSubTrigger.displayName =
|
|
||||||
DropdownMenuPrimitive.SubTrigger.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
DropdownMenuSubContent.displayName =
|
|
||||||
DropdownMenuPrimitive.SubContent.displayName
|
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
))
|
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
))
|
|
||||||
DropdownMenuCheckboxItem.displayName =
|
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
))
|
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
(<span
|
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
|
||||||
{...props} />)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
||||||
|
|
||||||
export {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { OTPInput, OTPInputContext } from "input-otp"
|
|
||||||
import { Minus } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const InputOTP = React.forwardRef(({ className, containerClassName, ...props }, ref) => (
|
|
||||||
<OTPInput
|
|
||||||
ref={ref}
|
|
||||||
containerClassName={cn("flex items-center gap-2 has-[:disabled]:opacity-50", containerClassName)}
|
|
||||||
className={cn("disabled:cursor-not-allowed", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
InputOTP.displayName = "InputOTP"
|
|
||||||
|
|
||||||
const InputOTPGroup = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
|
||||||
))
|
|
||||||
InputOTPGroup.displayName = "InputOTPGroup"
|
|
||||||
|
|
||||||
const InputOTPSlot = React.forwardRef(({ index, className, ...props }, ref) => {
|
|
||||||
const inputOTPContext = React.useContext(OTPInputContext)
|
|
||||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
|
|
||||||
|
|
||||||
return (
|
|
||||||
(<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
|
||||||
isActive && "z-10 ring-1 ring-ring",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}>
|
|
||||||
{char}
|
|
||||||
{hasFakeCaret && (
|
|
||||||
<div
|
|
||||||
className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
||||||
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
InputOTPSlot.displayName = "InputOTPSlot"
|
|
||||||
|
|
||||||
const InputOTPSeparator = React.forwardRef(({ ...props }, ref) => (
|
|
||||||
<div ref={ref} role="separator" {...props}>
|
|
||||||
<Minus />
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
InputOTPSeparator.displayName = "InputOTPSeparator"
|
|
||||||
|
|
||||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
(<input
|
|
||||||
type={type}
|
|
||||||
className={cn(
|
|
||||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props} />)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
Input.displayName = "Input"
|
|
||||||
|
|
||||||
export { Input }
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
||||||
import { cva } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const labelVariants = cva(
|
|
||||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
)
|
|
||||||
|
|
||||||
const Label = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
|
||||||
))
|
|
||||||
Label.displayName = LabelPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { Label }
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Popover = PopoverPrimitive.Root
|
|
||||||
|
|
||||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
|
||||||
|
|
||||||
const PopoverAnchor = PopoverPrimitive.Anchor
|
|
||||||
|
|
||||||
const PopoverContent = React.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
|
||||||
<PopoverPrimitive.Portal>
|
|
||||||
<PopoverPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
align={align}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
</PopoverPrimitive.Portal>
|
|
||||||
))
|
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Progress = React.forwardRef(({ className, value, ...props }, ref) => (
|
|
||||||
<ProgressPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}>
|
|
||||||
<ProgressPrimitive.Indicator
|
|
||||||
className="h-full w-full flex-1 bg-primary transition-all"
|
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} />
|
|
||||||
</ProgressPrimitive.Root>
|
|
||||||
))
|
|
||||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { Progress }
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const ScrollArea = React.forwardRef(({ className, children, ...props }, ref) => (
|
|
||||||
<ScrollAreaPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn("relative overflow-hidden", className)}
|
|
||||||
{...props}>
|
|
||||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
|
||||||
{children}
|
|
||||||
</ScrollAreaPrimitive.Viewport>
|
|
||||||
<ScrollBar />
|
|
||||||
<ScrollAreaPrimitive.Corner />
|
|
||||||
</ScrollAreaPrimitive.Root>
|
|
||||||
))
|
|
||||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const ScrollBar = React.forwardRef(({ className, orientation = "vertical", ...props }, ref) => (
|
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
||||||
ref={ref}
|
|
||||||
orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
"flex touch-none select-none transition-colors",
|
|
||||||
orientation === "vertical" &&
|
|
||||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
|
||||||
orientation === "horizontal" &&
|
|
||||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}>
|
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
||||||
))
|
|
||||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar }
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
||||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root
|
|
||||||
|
|
||||||
const SelectGroup = SelectPrimitive.Group
|
|
||||||
|
|
||||||
const SelectValue = SelectPrimitive.Value
|
|
||||||
|
|
||||||
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}>
|
|
||||||
{children}
|
|
||||||
<SelectPrimitive.Icon asChild>
|
|
||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
||||||
</SelectPrimitive.Icon>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
))
|
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollUpButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
|
||||||
{...props}>
|
|
||||||
<ChevronUp className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
|
||||||
))
|
|
||||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
|
||||||
|
|
||||||
const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
|
||||||
{...props}>
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
|
||||||
))
|
|
||||||
SelectScrollDownButton.displayName =
|
|
||||||
SelectPrimitive.ScrollDownButton.displayName
|
|
||||||
|
|
||||||
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
position === "popper" &&
|
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
position={position}
|
|
||||||
{...props}>
|
|
||||||
<SelectScrollUpButton />
|
|
||||||
<SelectPrimitive.Viewport
|
|
||||||
className={cn("p-1", position === "popper" &&
|
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")}>
|
|
||||||
{children}
|
|
||||||
</SelectPrimitive.Viewport>
|
|
||||||
<SelectScrollDownButton />
|
|
||||||
</SelectPrimitive.Content>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
))
|
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}>
|
|
||||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<SelectPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
))
|
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
export {
|
|
||||||
Select,
|
|
||||||
SelectGroup,
|
|
||||||
SelectValue,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
|
||||||
SelectLabel,
|
|
||||||
SelectItem,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectScrollDownButton,
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Separator = React.forwardRef((
|
|
||||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
|
||||||
ref
|
|
||||||
) => (
|
|
||||||
<SeparatorPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
decorative={decorative}
|
|
||||||
orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
"shrink-0 bg-border",
|
|
||||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { Separator }
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
|
||||||
import { cva } from "class-variance-authority";
|
|
||||||
import { X } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Sheet = SheetPrimitive.Root
|
|
||||||
|
|
||||||
const SheetTrigger = SheetPrimitive.Trigger
|
|
||||||
|
|
||||||
const SheetClose = SheetPrimitive.Close
|
|
||||||
|
|
||||||
const SheetPortal = SheetPrimitive.Portal
|
|
||||||
|
|
||||||
const SheetOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<SheetPrimitive.Overlay
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
ref={ref} />
|
|
||||||
))
|
|
||||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const sheetVariants = cva(
|
|
||||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
side: {
|
|
||||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
|
||||||
bottom:
|
|
||||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
|
||||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
|
||||||
right:
|
|
||||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
side: "right",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const SheetContent = React.forwardRef(({ side = "right", className, children, ...props }, ref) => (
|
|
||||||
<SheetPortal>
|
|
||||||
<SheetOverlay />
|
|
||||||
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
|
||||||
<SheetPrimitive.Close
|
|
||||||
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</SheetPrimitive.Close>
|
|
||||||
{children}
|
|
||||||
</SheetPrimitive.Content>
|
|
||||||
</SheetPortal>
|
|
||||||
))
|
|
||||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const SheetHeader = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) => (
|
|
||||||
<div
|
|
||||||
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
|
|
||||||
{...props} />
|
|
||||||
)
|
|
||||||
SheetHeader.displayName = "SheetHeader"
|
|
||||||
|
|
||||||
const SheetFooter = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) => (
|
|
||||||
<div
|
|
||||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
|
||||||
{...props} />
|
|
||||||
)
|
|
||||||
SheetFooter.displayName = "SheetFooter"
|
|
||||||
|
|
||||||
const SheetTitle = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<SheetPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-lg font-semibold text-foreground", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
|
||||||
|
|
||||||
const SheetDescription = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<SheetPrimitive.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
|
||||||
|
|
||||||
export {
|
|
||||||
Sheet,
|
|
||||||
SheetPortal,
|
|
||||||
SheetOverlay,
|
|
||||||
SheetTrigger,
|
|
||||||
SheetClose,
|
|
||||||
SheetContent,
|
|
||||||
SheetHeader,
|
|
||||||
SheetFooter,
|
|
||||||
SheetTitle,
|
|
||||||
SheetDescription,
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Skeleton({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
(<div
|
|
||||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
|
||||||
{...props} />)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Skeleton }
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Table = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<div className="relative w-full overflow-auto">
|
|
||||||
<table
|
|
||||||
ref={ref}
|
|
||||||
className={cn("w-full caption-bottom text-sm", className)}
|
|
||||||
{...props} />
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
Table.displayName = "Table"
|
|
||||||
|
|
||||||
const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
|
||||||
))
|
|
||||||
TableHeader.displayName = "TableHeader"
|
|
||||||
|
|
||||||
const TableBody = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<tbody
|
|
||||||
ref={ref}
|
|
||||||
className={cn("[&_tr:last-child]:border-0", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
TableBody.displayName = "TableBody"
|
|
||||||
|
|
||||||
const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<tfoot
|
|
||||||
ref={ref}
|
|
||||||
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
TableFooter.displayName = "TableFooter"
|
|
||||||
|
|
||||||
const TableRow = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<tr
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
TableRow.displayName = "TableRow"
|
|
||||||
|
|
||||||
const TableHead = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<th
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
TableHead.displayName = "TableHead"
|
|
||||||
|
|
||||||
const TableCell = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<td
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
TableCell.displayName = "TableCell"
|
|
||||||
|
|
||||||
const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<caption
|
|
||||||
ref={ref}
|
|
||||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
TableCaption.displayName = "TableCaption"
|
|
||||||
|
|
||||||
export {
|
|
||||||
Table,
|
|
||||||
TableHeader,
|
|
||||||
TableBody,
|
|
||||||
TableFooter,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TableCell,
|
|
||||||
TableCaption,
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Tabs = TabsPrimitive.Root
|
|
||||||
|
|
||||||
const TabsList = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
TabsList.displayName = TabsPrimitive.List.displayName
|
|
||||||
|
|
||||||
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import * as React from "react"
|
|
||||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
|
||||||
import { cva } from "class-variance-authority";
|
|
||||||
import { X } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const ToastProvider = ToastPrimitives.Provider
|
|
||||||
|
|
||||||
const ToastViewport = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Viewport
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
|
||||||
|
|
||||||
const toastVariants = cva(
|
|
||||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "border bg-background text-foreground",
|
|
||||||
destructive:
|
|
||||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const Toast = React.forwardRef(({ className, variant, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
(<ToastPrimitives.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(toastVariants({ variant }), className)}
|
|
||||||
{...props} />)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
Toast.displayName = ToastPrimitives.Root.displayName
|
|
||||||
|
|
||||||
const ToastAction = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Action
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
|
||||||
|
|
||||||
const ToastClose = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Close
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
toast-close=""
|
|
||||||
{...props}>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</ToastPrimitives.Close>
|
|
||||||
))
|
|
||||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
|
||||||
|
|
||||||
const ToastTitle = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
|
||||||
|
|
||||||
const ToastDescription = React.forwardRef(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
|
|
||||||
))
|
|
||||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
|
||||||
|
|
||||||
export { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction };
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useToast } from "@/hooks/use-toast"
|
|
||||||
import {
|
|
||||||
Toast,
|
|
||||||
ToastClose,
|
|
||||||
ToastDescription,
|
|
||||||
ToastProvider,
|
|
||||||
ToastTitle,
|
|
||||||
ToastViewport,
|
|
||||||
} from "@/components/dashboard/ui/toast"
|
|
||||||
|
|
||||||
export function Toaster() {
|
|
||||||
const { toasts } = useToast()
|
|
||||||
|
|
||||||
return (
|
|
||||||
(<ToastProvider>
|
|
||||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
|
||||||
return (
|
|
||||||
(<Toast key={id} {...props}>
|
|
||||||
<div className="grid gap-1">
|
|
||||||
{title && <ToastTitle>{title}</ToastTitle>}
|
|
||||||
{description && (
|
|
||||||
<ToastDescription>{description}</ToastDescription>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{action}
|
|
||||||
<ToastClose />
|
|
||||||
</Toast>)
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<ToastViewport />
|
|
||||||
</ToastProvider>)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { toggleVariants } from "@/components/dashboard/ui/toggle"
|
|
||||||
|
|
||||||
const ToggleGroupContext = React.createContext({
|
|
||||||
size: "default",
|
|
||||||
variant: "default",
|
|
||||||
})
|
|
||||||
|
|
||||||
const ToggleGroup = React.forwardRef(({ className, variant, size, children, ...props }, ref) => (
|
|
||||||
<ToggleGroupPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn("flex items-center justify-center gap-1", className)}
|
|
||||||
{...props}>
|
|
||||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
|
||||||
{children}
|
|
||||||
</ToggleGroupContext.Provider>
|
|
||||||
</ToggleGroupPrimitive.Root>
|
|
||||||
))
|
|
||||||
|
|
||||||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const ToggleGroupItem = React.forwardRef(({ className, children, variant, size, ...props }, ref) => {
|
|
||||||
const context = React.useContext(ToggleGroupContext)
|
|
||||||
|
|
||||||
return (
|
|
||||||
(<ToggleGroupPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(toggleVariants({
|
|
||||||
variant: context.variant || variant,
|
|
||||||
size: context.size || size,
|
|
||||||
}), className)}
|
|
||||||
{...props}>
|
|
||||||
{children}
|
|
||||||
</ToggleGroupPrimitive.Item>)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
|
|
||||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
|
|
||||||
|
|
||||||
export { ToggleGroup, ToggleGroupItem }
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
|
||||||
import { cva } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const toggleVariants = cva(
|
|
||||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-transparent",
|
|
||||||
outline:
|
|
||||||
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-9 px-2 min-w-9",
|
|
||||||
sm: "h-8 px-1.5 min-w-8",
|
|
||||||
lg: "h-10 px-2.5 min-w-10",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const Toggle = React.forwardRef(({ className, variant, size, ...props }, ref) => (
|
|
||||||
<TogglePrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(toggleVariants({ variant, size, className }))}
|
|
||||||
{...props} />
|
|
||||||
))
|
|
||||||
|
|
||||||
Toggle.displayName = TogglePrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { Toggle, toggleVariants }
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const TooltipProvider = TooltipPrimitive.Provider
|
|
||||||
|
|
||||||
const Tooltip = TooltipPrimitive.Root
|
|
||||||
|
|
||||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
|
||||||
|
|
||||||
const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
))
|
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
||||||
@@ -40,14 +40,12 @@ export function ForecastMetrics() {
|
|||||||
startDate: dateRange.from?.toISOString() || "",
|
startDate: dateRange.from?.toISOString() || "",
|
||||||
endDate: dateRange.to?.toISOString() || "",
|
endDate: dateRange.to?.toISOString() || "",
|
||||||
});
|
});
|
||||||
console.log('Fetching forecast metrics with params:', params.toString());
|
|
||||||
const response = await fetch(`${config.apiUrl}/dashboard/forecast/metrics?${params}`)
|
const response = await fetch(`${config.apiUrl}/dashboard/forecast/metrics?${params}`)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
throw new Error(`Failed to fetch forecast metrics: ${text}`);
|
throw new Error(`Failed to fetch forecast metrics: ${text}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Forecast metrics response:', data);
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ export function PurchaseMetrics() {
|
|||||||
const { data, error, isLoading } = useQuery<PurchaseMetricsData>({
|
const { data, error, isLoading } = useQuery<PurchaseMetricsData>({
|
||||||
queryKey: ["purchase-metrics"],
|
queryKey: ["purchase-metrics"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
console.log('Fetching from:', `${config.apiUrl}/dashboard/purchase/metrics`);
|
|
||||||
const response = await fetch(`${config.apiUrl}/dashboard/purchase/metrics`)
|
const response = await fetch(`${config.apiUrl}/dashboard/purchase/metrics`)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
@@ -111,7 +110,6 @@ export function PurchaseMetrics() {
|
|||||||
throw new Error(`Failed to fetch purchase metrics: ${response.status} ${response.statusText}`);
|
throw new Error(`Failed to fetch purchase metrics: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('API Response:', data);
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export function ReplenishmentMetrics() {
|
|||||||
const { data, error, isLoading } = useQuery<ReplenishmentMetricsData>({
|
const { data, error, isLoading } = useQuery<ReplenishmentMetricsData>({
|
||||||
queryKey: ["replenishment-metrics"],
|
queryKey: ["replenishment-metrics"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
console.log('Fetching from:', `${config.apiUrl}/dashboard/replenishment/metrics`);
|
|
||||||
const response = await fetch(`${config.apiUrl}/dashboard/replenishment/metrics`)
|
const response = await fetch(`${config.apiUrl}/dashboard/replenishment/metrics`)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
@@ -33,7 +32,6 @@ export function ReplenishmentMetrics() {
|
|||||||
throw new Error(`Failed to fetch replenishment metrics: ${response.status} ${response.statusText} - ${text}`)
|
throw new Error(`Failed to fetch replenishment metrics: ${response.status} ${response.statusText} - ${text}`)
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('API Response:', data);
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ export function StockMetrics() {
|
|||||||
const { data, error, isLoading } = useQuery<StockMetricsData>({
|
const { data, error, isLoading } = useQuery<StockMetricsData>({
|
||||||
queryKey: ["stock-metrics"],
|
queryKey: ["stock-metrics"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
console.log('Fetching from:', `${config.apiUrl}/dashboard/stock/metrics`);
|
|
||||||
const response = await fetch(`${config.apiUrl}/dashboard/stock/metrics`);
|
const response = await fetch(`${config.apiUrl}/dashboard/stock/metrics`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
@@ -111,7 +110,6 @@ export function StockMetrics() {
|
|||||||
throw new Error(`Failed to fetch stock metrics: ${response.status} ${response.statusText}`);
|
throw new Error(`Failed to fetch stock metrics: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('API Response:', data);
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1029,7 +1029,6 @@ export const MatchColumnsStep = React.memo(<T extends string>({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (matchingOption) {
|
if (matchingOption) {
|
||||||
console.log(`Auto-matched "${entryValue}" to "${matchingOption.label}" (${matchingOption.value})`);
|
|
||||||
return {
|
return {
|
||||||
...option,
|
...option,
|
||||||
value: matchingOption.value
|
value: matchingOption.value
|
||||||
@@ -1094,7 +1093,6 @@ export const MatchColumnsStep = React.memo(<T extends string>({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (matchingOption) {
|
if (matchingOption) {
|
||||||
console.log(`Auto-matched "${entryValue}" to "${matchingOption.label}" (${matchingOption.value})`);
|
|
||||||
return {
|
return {
|
||||||
...option,
|
...option,
|
||||||
value: matchingOption.value
|
value: matchingOption.value
|
||||||
@@ -1159,18 +1157,6 @@ export const MatchColumnsStep = React.memo(<T extends string>({
|
|||||||
// Convert the fields to the expected type
|
// Convert the fields to the expected type
|
||||||
const fieldsArray = Array.isArray(fields) ? fields : [fields];
|
const fieldsArray = Array.isArray(fields) ? fields : [fields];
|
||||||
|
|
||||||
// Log the fields for debugging
|
|
||||||
console.log("All fields:", fieldsArray);
|
|
||||||
|
|
||||||
// Log validation rules for each field
|
|
||||||
fieldsArray.forEach(field => {
|
|
||||||
if (field.validations && Array.isArray(field.validations)) {
|
|
||||||
console.log(`Field ${field.key} validations:`, field.validations.map((v: any) => v.rule));
|
|
||||||
} else {
|
|
||||||
console.log(`Field ${field.key} has no validations`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for required fields based on validations
|
// Check for required fields based on validations
|
||||||
const required = fieldsArray.filter(field => {
|
const required = fieldsArray.filter(field => {
|
||||||
// Check if the field has validations
|
// Check if the field has validations
|
||||||
@@ -1183,12 +1169,10 @@ export const MatchColumnsStep = React.memo(<T extends string>({
|
|||||||
(v: any) => v.rule === 'required' || v.required === true
|
(v: any) => v.rule === 'required' || v.required === true
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`Field ${field.key} required:`, isRequired, field.validations);
|
|
||||||
|
|
||||||
return isRequired;
|
return isRequired;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Required fields:", required);
|
|
||||||
return required;
|
return required;
|
||||||
}, [fields]);
|
}, [fields]);
|
||||||
|
|
||||||
|
|||||||
@@ -62,14 +62,8 @@ export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps
|
|||||||
const [selectedRowIndex] = selectedRows;
|
const [selectedRowIndex] = selectedRows;
|
||||||
const selectedHeaderRow = localData[selectedRowIndex];
|
const selectedHeaderRow = localData[selectedRowIndex];
|
||||||
|
|
||||||
// Debug: Log the selected header row
|
|
||||||
console.log("Selected header row:", selectedHeaderRow);
|
|
||||||
|
|
||||||
const normalizedHeaderRow = normalizeRowForComparison(selectedHeaderRow);
|
const normalizedHeaderRow = normalizeRowForComparison(selectedHeaderRow);
|
||||||
|
|
||||||
// Debug: Log the normalized header row
|
|
||||||
console.log("Normalized header row:", normalizedHeaderRow);
|
|
||||||
|
|
||||||
const selectedHeaderStr = JSON.stringify(Object.entries(normalizedHeaderRow).sort());
|
const selectedHeaderStr = JSON.stringify(Object.entries(normalizedHeaderRow).sort());
|
||||||
|
|
||||||
// Filter out empty rows, rows with single values (if we have multi-value rows),
|
// Filter out empty rows, rows with single values (if we have multi-value rows),
|
||||||
@@ -97,11 +91,6 @@ export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps
|
|||||||
// Check if it's a duplicate (case-insensitive)
|
// Check if it's a duplicate (case-insensitive)
|
||||||
const normalizedRow = normalizeRowForComparison(row);
|
const normalizedRow = normalizeRowForComparison(row);
|
||||||
|
|
||||||
// Debug: If this row might be a duplicate of the header, log it
|
|
||||||
if (index < 5 || index === selectedRowIndex + 1 || index === selectedRowIndex - 1) {
|
|
||||||
console.log(`Row ${index} normalized:`, normalizedRow);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowStr = JSON.stringify(Object.entries(normalizedRow).sort());
|
const rowStr = JSON.stringify(Object.entries(normalizedRow).sort());
|
||||||
|
|
||||||
if (seen.has(rowStr)) {
|
if (seen.has(rowStr)) {
|
||||||
@@ -117,9 +106,6 @@ export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Debug: Log removed rows
|
|
||||||
console.log("Removed rows:", removedRows);
|
|
||||||
|
|
||||||
// Only update if we actually removed any rows
|
// Only update if we actually removed any rows
|
||||||
if (filteredRows.length < localData.length) {
|
if (filteredRows.length < localData.length) {
|
||||||
// Adjust the selected row index if needed
|
// Adjust the selected row index if needed
|
||||||
@@ -127,9 +113,6 @@ export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps
|
|||||||
JSON.stringify(Object.entries(normalizeRowForComparison(row)).sort()) === selectedHeaderStr
|
JSON.stringify(Object.entries(normalizeRowForComparison(row)).sort()) === selectedHeaderStr
|
||||||
);
|
);
|
||||||
|
|
||||||
// Debug: Log the new selected index
|
|
||||||
console.log("New selected index:", newSelectedIndex);
|
|
||||||
|
|
||||||
setLocalData(filteredRows);
|
setLocalData(filteredRows);
|
||||||
setSelectedRows(new Set([newSelectedIndex]));
|
setSelectedRows(new Set([newSelectedIndex]));
|
||||||
|
|
||||||
|
|||||||
@@ -184,11 +184,9 @@ const ValidationContainer = <T extends string>({
|
|||||||
const fetchFieldOptions = useCallback(async () => {
|
const fetchFieldOptions = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/import/field-options');
|
const response = await axios.get('/api/import/field-options');
|
||||||
console.log('Field options from API:', response.data);
|
|
||||||
|
|
||||||
// Check if suppliers are included in the response
|
// Check if suppliers are included in the response
|
||||||
if (response.data && response.data.suppliers) {
|
if (response.data && response.data.suppliers) {
|
||||||
console.log('Suppliers available:', response.data.suppliers.length);
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('No suppliers found in field options response');
|
console.warn('No suppliers found in field options response');
|
||||||
}
|
}
|
||||||
@@ -282,24 +280,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
try {
|
try {
|
||||||
const options = await fetchFieldOptions();
|
const options = await fetchFieldOptions();
|
||||||
if (options && options.suppliers) {
|
if (options && options.suppliers) {
|
||||||
console.log(`Loaded ${options.suppliers.length} suppliers for template form`);
|
|
||||||
|
|
||||||
// Log if we can find a match for our supplier
|
|
||||||
if (templateData.supplier !== undefined) {
|
|
||||||
// Need to compare numeric values since supplier options have numeric values
|
|
||||||
const supplierMatch = options.suppliers.find((s: { value: string | number }) =>
|
|
||||||
s.value === templateData.supplier ||
|
|
||||||
Number(s.value) === Number(templateData.supplier)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Found supplier match:', supplierMatch ? 'Yes' : 'No',
|
|
||||||
'For supplier value:', templateData.supplier,
|
|
||||||
'Type:', typeof templateData.supplier);
|
|
||||||
|
|
||||||
if (supplierMatch) {
|
|
||||||
console.log('Matched supplier:', supplierMatch.label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsTemplateFormOpen(true);
|
setIsTemplateFormOpen(true);
|
||||||
} else {
|
} else {
|
||||||
@@ -344,7 +324,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
.filter(([_, selected]) => selected === true)
|
.filter(([_, selected]) => selected === true)
|
||||||
.map(([key, _]) => key);
|
.map(([key, _]) => key);
|
||||||
|
|
||||||
console.log('Selected row keys for deletion:', selectedKeys);
|
|
||||||
|
|
||||||
if (selectedKeys.length === 0) {
|
if (selectedKeys.length === 0) {
|
||||||
toast.error("No rows selected");
|
toast.error("No rows selected");
|
||||||
@@ -361,7 +340,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
return index;
|
return index;
|
||||||
}).filter(index => index !== -1); // Filter out any not found
|
}).filter(index => index !== -1); // Filter out any not found
|
||||||
|
|
||||||
console.log('Mapped row indices for deletion:', selectedIndices);
|
|
||||||
|
|
||||||
if (selectedIndices.length === 0) {
|
if (selectedIndices.length === 0) {
|
||||||
toast.error('Could not find selected rows');
|
toast.error('Could not find selected rows');
|
||||||
@@ -567,7 +545,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
const upcValue = (data[rowIndex] as any)?.upc || (data[rowIndex] as any)?.barcode;
|
const upcValue = (data[rowIndex] as any)?.upc || (data[rowIndex] as any)?.barcode;
|
||||||
|
|
||||||
if (upcValue) {
|
if (upcValue) {
|
||||||
console.log(`Validating UPC: rowIndex=${rowIndex}, supplier=${value}, upc=${upcValue}`);
|
|
||||||
|
|
||||||
// Mark the item_number cell as being validated
|
// Mark the item_number cell as being validated
|
||||||
const cellKey = `${rowIndex}-item_number`;
|
const cellKey = `${rowIndex}-item_number`;
|
||||||
@@ -581,7 +558,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
upcValidation.validateUpc(rowIndex, value.toString(), upcValue.toString())
|
upcValidation.validateUpc(rowIndex, value.toString(), upcValue.toString())
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(`UPC validation successful for row ${rowIndex}`);
|
|
||||||
upcValidation.applyItemNumbersToData();
|
upcValidation.applyItemNumbersToData();
|
||||||
|
|
||||||
// Mark for revalidation after item numbers are updated
|
// Mark for revalidation after item numbers are updated
|
||||||
@@ -606,14 +582,12 @@ const ValidationContainer = <T extends string>({
|
|||||||
|
|
||||||
// Handle line change - clear subline and fetch sublines
|
// Handle line change - clear subline and fetch sublines
|
||||||
if (key === 'line' && value) {
|
if (key === 'line' && value) {
|
||||||
console.log(`Line changed to ${value} for row ${rowIndex}, updating sublines`);
|
|
||||||
|
|
||||||
// Clear any existing subline value
|
// Clear any existing subline value
|
||||||
setData(prevData => {
|
setData(prevData => {
|
||||||
const newData = [...prevData];
|
const newData = [...prevData];
|
||||||
const idx = newData.findIndex(item => item.__index === rowId);
|
const idx = newData.findIndex(item => item.__index === rowId);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
console.log(`Clearing subline values for row with ID ${rowId}`);
|
|
||||||
newData[idx] = {
|
newData[idx] = {
|
||||||
...newData[idx],
|
...newData[idx],
|
||||||
subline: undefined
|
subline: undefined
|
||||||
@@ -628,9 +602,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
if (rowId && value !== undefined) {
|
if (rowId && value !== undefined) {
|
||||||
const lineId = value.toString();
|
const lineId = value.toString();
|
||||||
|
|
||||||
// Force immediate fetch for better UX
|
|
||||||
console.log(`Immediately fetching sublines for line ${lineId} for row ${rowId}`);
|
|
||||||
|
|
||||||
// Set loading state first
|
// Set loading state first
|
||||||
setValidatingCells(prev => {
|
setValidatingCells(prev => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
@@ -639,8 +610,7 @@ const ValidationContainer = <T extends string>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
fetchSublines(rowId, lineId)
|
fetchSublines(rowId, lineId)
|
||||||
.then(sublines => {
|
.then(() => {
|
||||||
console.log(`Successfully loaded ${sublines.length} sublines for line ${lineId}`);
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(`Error fetching sublines for line ${lineId}:`, err);
|
console.error(`Error fetching sublines for line ${lineId}:`, err);
|
||||||
@@ -664,7 +634,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
const supplier = (data[rowIndex] as any)?.supplier;
|
const supplier = (data[rowIndex] as any)?.supplier;
|
||||||
|
|
||||||
if (supplier) {
|
if (supplier) {
|
||||||
console.log(`Validating UPC from UPC change: rowIndex=${rowIndex}, supplier=${supplier}, upc=${value}`);
|
|
||||||
|
|
||||||
// Mark the item_number cell as being validated
|
// Mark the item_number cell as being validated
|
||||||
const cellKey = `${rowIndex}-item_number`;
|
const cellKey = `${rowIndex}-item_number`;
|
||||||
@@ -678,7 +647,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
upcValidation.validateUpc(rowIndex, supplier.toString(), value.toString())
|
upcValidation.validateUpc(rowIndex, supplier.toString(), value.toString())
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(`UPC validation successful for row ${rowIndex}`);
|
|
||||||
upcValidation.applyItemNumbersToData();
|
upcValidation.applyItemNumbersToData();
|
||||||
|
|
||||||
// Mark for revalidation after item numbers are updated
|
// Mark for revalidation after item numbers are updated
|
||||||
@@ -818,7 +786,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
|
|
||||||
// Run validations in parallel but limit the batch size
|
// Run validations in parallel but limit the batch size
|
||||||
if (validationsToRun.length > 0) {
|
if (validationsToRun.length > 0) {
|
||||||
console.log(`Running ${validationsToRun.length} UPC validations for copyDown`);
|
|
||||||
|
|
||||||
// Mark all cells as validating
|
// Mark all cells as validating
|
||||||
validationsToRun.forEach(({ rowIndex }) => {
|
validationsToRun.forEach(({ rowIndex }) => {
|
||||||
@@ -877,7 +844,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
// Add a small delay between batches to prevent UI freezing
|
// Add a small delay between batches to prevent UI freezing
|
||||||
setTimeout(() => processBatch(endIdx), 100);
|
setTimeout(() => processBatch(endIdx), 100);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Completed all ${validationsToRun.length} UPC validations`);
|
|
||||||
// Final application of all item numbers if not done by individual batches
|
// Final application of all item numbers if not done by individual batches
|
||||||
upcValidation.applyItemNumbersToData(updatedRowIds => {
|
upcValidation.applyItemNumbersToData(updatedRowIds => {
|
||||||
// Mark these rows for revalidation after a delay
|
// Mark these rows for revalidation after a delay
|
||||||
@@ -1112,8 +1078,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 shadow-xs"
|
className="h-8 shadow-xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log('Delete/Discard button clicked');
|
|
||||||
console.log('Row selection state:', rowSelection);
|
|
||||||
deleteSelectedRows();
|
deleteSelectedRows();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
40
inventory/src/types/dashboard-shims.d.ts
vendored
40
inventory/src/types/dashboard-shims.d.ts
vendored
@@ -30,42 +30,4 @@ declare module "@/lib/dashboard/*" {
|
|||||||
export default value;
|
export default value;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "@/components/dashboard/ui/card" {
|
// UI component declarations removed - now using proper TypeScript components from @/components/ui/*
|
||||||
export const Card: React.ComponentType<any>;
|
|
||||||
export const CardContent: React.ComponentType<any>;
|
|
||||||
export const CardDescription: React.ComponentType<any>;
|
|
||||||
export const CardHeader: React.ComponentType<any>;
|
|
||||||
export const CardTitle: React.ComponentType<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@/components/dashboard/ui/button" {
|
|
||||||
export const Button: React.ComponentType<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@/components/dashboard/ui/select" {
|
|
||||||
export const Select: React.ComponentType<any>;
|
|
||||||
export const SelectContent: React.ComponentType<any>;
|
|
||||||
export const SelectItem: React.ComponentType<any>;
|
|
||||||
export const SelectTrigger: React.ComponentType<any>;
|
|
||||||
export const SelectValue: React.ComponentType<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@/components/dashboard/ui/alert" {
|
|
||||||
export const Alert: React.ComponentType<any>;
|
|
||||||
export const AlertDescription: React.ComponentType<any>;
|
|
||||||
export const AlertTitle: React.ComponentType<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@/components/dashboard/ui/skeleton" {
|
|
||||||
export const Skeleton: React.ComponentType<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@/components/dashboard/ui/*" {
|
|
||||||
const components: any;
|
|
||||||
export default components;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@/components/dashboard/ui/toggle-group" {
|
|
||||||
export const ToggleGroup: React.ComponentType<any>;
|
|
||||||
export const ToggleGroupItem: React.ComponentType<any>;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export default defineConfig(({ mode }) => {
|
|||||||
await fs.ensureDir(path.dirname(targetPath));
|
await fs.ensureDir(path.dirname(targetPath));
|
||||||
await fs.remove(targetPath);
|
await fs.remove(targetPath);
|
||||||
await fs.copy(sourcePath, targetPath);
|
await fs.copy(sourcePath, targetPath);
|
||||||
console.log('Build files copied successfully to server directory!');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error copying build files:', error);
|
console.error('Error copying build files:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -105,7 +104,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
rewrite: (path) => path.replace(/^\/api/, "/api"),
|
rewrite: (path) => path.replace(/^\/api/, "/api"),
|
||||||
configure: (proxy, _options) => {
|
configure: (proxy, _options) => {
|
||||||
proxy.on("error", (err, req, res) => {
|
proxy.on("error", (err, req, res) => {
|
||||||
console.log("API proxy error:", err)
|
console.error("API proxy error:", err)
|
||||||
res.writeHead(500, {
|
res.writeHead(500, {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
})
|
})
|
||||||
@@ -113,20 +112,6 @@ export default defineConfig(({ mode }) => {
|
|||||||
JSON.stringify({ error: "Proxy Error", message: err.message })
|
JSON.stringify({ error: "Proxy Error", message: err.message })
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
proxy.on("proxyReq", (proxyReq, req, _res) => {
|
|
||||||
console.log("Outgoing request to API:", {
|
|
||||||
method: req.method,
|
|
||||||
url: req.url,
|
|
||||||
headers: proxyReq.getHeaders(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
proxy.on("proxyRes", (proxyRes, req, _res) => {
|
|
||||||
console.log("API Proxy response:", {
|
|
||||||
statusCode: proxyRes.statusCode,
|
|
||||||
url: req.url,
|
|
||||||
headers: proxyRes.headers,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"/dashboard-auth": {
|
"/dashboard-auth": {
|
||||||
@@ -153,7 +138,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
rewrite: (path) => path.replace(/^\/auth-inv/, "/auth-inv"),
|
rewrite: (path) => path.replace(/^\/auth-inv/, "/auth-inv"),
|
||||||
configure: (proxy, _options) => {
|
configure: (proxy, _options) => {
|
||||||
proxy.on("error", (err, req, res) => {
|
proxy.on("error", (err, req, res) => {
|
||||||
console.log("Auth proxy error:", err)
|
console.error("Auth proxy error:", err)
|
||||||
res.writeHead(500, {
|
res.writeHead(500, {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
})
|
})
|
||||||
@@ -161,20 +146,6 @@ export default defineConfig(({ mode }) => {
|
|||||||
JSON.stringify({ error: "Proxy Error", message: err.message })
|
JSON.stringify({ error: "Proxy Error", message: err.message })
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
proxy.on("proxyReq", (proxyReq, req, _res) => {
|
|
||||||
console.log("Outgoing request to Auth:", {
|
|
||||||
method: req.method,
|
|
||||||
url: req.url,
|
|
||||||
headers: proxyReq.getHeaders(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
proxy.on("proxyRes", (proxyRes, req, _res) => {
|
|
||||||
console.log("Auth Proxy response:", {
|
|
||||||
statusCode: proxyRes.statusCode,
|
|
||||||
url: req.url,
|
|
||||||
headers: proxyRes.headers,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"/chat-api": {
|
"/chat-api": {
|
||||||
@@ -188,7 +159,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
rewrite: (path) => path,
|
rewrite: (path) => path,
|
||||||
configure: (proxy, _options) => {
|
configure: (proxy, _options) => {
|
||||||
proxy.on("error", (err, req, res) => {
|
proxy.on("error", (err, req, res) => {
|
||||||
console.log("Chat API proxy error:", err)
|
console.error("Chat API proxy error:", err)
|
||||||
res.writeHead(500, {
|
res.writeHead(500, {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
})
|
})
|
||||||
@@ -196,20 +167,6 @@ export default defineConfig(({ mode }) => {
|
|||||||
JSON.stringify({ error: "Proxy Error", message: err.message })
|
JSON.stringify({ error: "Proxy Error", message: err.message })
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
proxy.on("proxyReq", (proxyReq, req, _res) => {
|
|
||||||
console.log("Outgoing request to Chat API:", {
|
|
||||||
method: req.method,
|
|
||||||
url: req.url,
|
|
||||||
headers: proxyReq.getHeaders(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
proxy.on("proxyRes", (proxyRes, req, _res) => {
|
|
||||||
console.log("Chat API Proxy response:", {
|
|
||||||
statusCode: proxyRes.statusCode,
|
|
||||||
url: req.url,
|
|
||||||
headers: proxyRes.headers,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"/uploads": {
|
"/uploads": {
|
||||||
|
|||||||
Reference in New Issue
Block a user