Add new filter options and metrics to product filters and pages; enhance SQL schema for financial calculations
This commit is contained in:
@@ -51,7 +51,9 @@ const FILTER_OPTIONS: FilterOption[] = [
|
||||
// Basic Info Group
|
||||
{ id: "search", label: "Search", type: "text", group: "Basic Info" },
|
||||
{ id: "sku", label: "SKU", type: "text", group: "Basic Info" },
|
||||
{ id: "barcode", label: "UPC/Barcode", type: "text", group: "Basic Info" },
|
||||
{ id: "vendor", label: "Vendor", type: "select", group: "Basic Info" },
|
||||
{ id: "vendor_reference", label: "Supplier #", type: "text", group: "Basic Info" },
|
||||
{ id: "brand", label: "Brand", type: "select", group: "Basic Info" },
|
||||
{ id: "category", label: "Category", type: "select", group: "Basic Info" },
|
||||
|
||||
@@ -84,6 +86,27 @@ const FILTER_OPTIONS: FilterOption[] = [
|
||||
group: "Inventory",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
{
|
||||
id: "weeksOfStock",
|
||||
label: "Weeks of Stock",
|
||||
type: "number",
|
||||
group: "Inventory",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
{
|
||||
id: "reorderPoint",
|
||||
label: "Reorder Point",
|
||||
type: "number",
|
||||
group: "Inventory",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
{
|
||||
id: "safetyStock",
|
||||
label: "Safety Stock",
|
||||
type: "number",
|
||||
group: "Inventory",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
{
|
||||
id: "replenishable",
|
||||
label: "Replenishable",
|
||||
@@ -94,6 +117,17 @@ const FILTER_OPTIONS: FilterOption[] = [
|
||||
],
|
||||
group: "Inventory",
|
||||
},
|
||||
{
|
||||
id: "abcClass",
|
||||
label: "ABC Class",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "A", value: "A" },
|
||||
{ label: "B", value: "B" },
|
||||
{ label: "C", value: "C" },
|
||||
],
|
||||
group: "Inventory",
|
||||
},
|
||||
|
||||
// Pricing Group
|
||||
{
|
||||
@@ -140,6 +174,32 @@ const FILTER_OPTIONS: FilterOption[] = [
|
||||
group: "Sales Metrics",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
{
|
||||
id: "avgQuantityPerOrder",
|
||||
label: "Avg Qty/Order",
|
||||
type: "number",
|
||||
group: "Sales Metrics",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
{
|
||||
id: "numberOfOrders",
|
||||
label: "Order Count",
|
||||
type: "number",
|
||||
group: "Sales Metrics",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
{
|
||||
id: "firstSaleDate",
|
||||
label: "First Sale Date",
|
||||
type: "text",
|
||||
group: "Sales Metrics",
|
||||
},
|
||||
{
|
||||
id: "lastSaleDate",
|
||||
label: "Last Sale Date",
|
||||
type: "text",
|
||||
group: "Sales Metrics",
|
||||
},
|
||||
|
||||
// Financial Metrics Group
|
||||
{
|
||||
@@ -156,6 +216,34 @@ const FILTER_OPTIONS: FilterOption[] = [
|
||||
group: "Financial Metrics",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
{
|
||||
id: "inventoryValue",
|
||||
label: "Inventory Value",
|
||||
type: "number",
|
||||
group: "Financial Metrics",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
{
|
||||
id: "costOfGoodsSold",
|
||||
label: "COGS",
|
||||
type: "number",
|
||||
group: "Financial Metrics",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
{
|
||||
id: "grossProfit",
|
||||
label: "Gross Profit",
|
||||
type: "number",
|
||||
group: "Financial Metrics",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
{
|
||||
id: "turnoverRate",
|
||||
label: "Turnover Rate",
|
||||
type: "number",
|
||||
group: "Financial Metrics",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
|
||||
// Lead Time & Stock Coverage Group
|
||||
{
|
||||
@@ -165,6 +253,20 @@ const FILTER_OPTIONS: FilterOption[] = [
|
||||
group: "Lead Time & Coverage",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
{
|
||||
id: "currentLeadTime",
|
||||
label: "Current Lead Time",
|
||||
type: "number",
|
||||
group: "Lead Time & Coverage",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
{
|
||||
id: "targetLeadTime",
|
||||
label: "Target Lead Time",
|
||||
type: "number",
|
||||
group: "Lead Time & Coverage",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
{
|
||||
id: "leadTimeStatus",
|
||||
label: "Lead Time Status",
|
||||
@@ -183,19 +285,26 @@ const FILTER_OPTIONS: FilterOption[] = [
|
||||
group: "Lead Time & Coverage",
|
||||
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||
},
|
||||
|
||||
// Classification Group
|
||||
{
|
||||
id: "abcClass",
|
||||
label: "ABC Class",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "A", value: "A" },
|
||||
{ label: "B", value: "B" },
|
||||
{ label: "C", value: "C" },
|
||||
],
|
||||
group: "Classification",
|
||||
id: "lastPurchaseDate",
|
||||
label: "Last Purchase Date",
|
||||
type: "text",
|
||||
group: "Lead Time & Coverage",
|
||||
},
|
||||
{
|
||||
id: "firstReceivedDate",
|
||||
label: "First Received Date",
|
||||
type: "text",
|
||||
group: "Lead Time & Coverage",
|
||||
},
|
||||
{
|
||||
id: "lastReceivedDate",
|
||||
label: "Last Received Date",
|
||||
type: "text",
|
||||
group: "Lead Time & Coverage",
|
||||
},
|
||||
|
||||
// Classification Group
|
||||
{
|
||||
id: "managingStock",
|
||||
label: "Managing Stock",
|
||||
|
||||
@@ -47,17 +47,16 @@ interface HistoryRecord {
|
||||
id: number;
|
||||
start_time: string;
|
||||
end_time: string | null;
|
||||
duration_minutes: number;
|
||||
duration_minutes?: number;
|
||||
status: "running" | "completed" | "failed" | "cancelled";
|
||||
error_message: string | null;
|
||||
additional_info?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface ImportHistoryRecord extends HistoryRecord {
|
||||
table_name: string;
|
||||
records_added: number;
|
||||
records_updated: number;
|
||||
is_incremental: boolean;
|
||||
is_incremental?: boolean;
|
||||
}
|
||||
|
||||
interface CalculateHistoryRecord extends HistoryRecord {
|
||||
@@ -67,6 +66,7 @@ interface CalculateHistoryRecord extends HistoryRecord {
|
||||
processed_products: number;
|
||||
processed_orders: number;
|
||||
processed_purchase_orders: number;
|
||||
duration_minutes?: number;
|
||||
}
|
||||
|
||||
interface ModuleStatus {
|
||||
@@ -82,13 +82,14 @@ interface TableStatus {
|
||||
export function DataManagement() {
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
const [] = useState<ImportProgress | null>(null);
|
||||
const [eventSource, setEventSource] = useState<EventSource | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [importHistory, setImportHistory] = useState<ImportHistoryRecord[]>([]);
|
||||
const [calculateHistory, setCalculateHistory] = useState<CalculateHistoryRecord[]>([]);
|
||||
const [moduleStatus, setModuleStatus] = useState<ModuleStatus[]>([]);
|
||||
const [tableStatus, setTableStatus] = useState<TableStatus[]>([]);
|
||||
const [scriptOutput, setScriptOutput] = useState<string[]>([]);
|
||||
const [eventSource, setEventSource] = useState<EventSource | null>(null);
|
||||
|
||||
// Add useRef for scroll handling
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
@@ -359,11 +360,14 @@ export function DataManagement() {
|
||||
|
||||
const fetchHistory = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setHasError(false);
|
||||
|
||||
const [importRes, calcRes, moduleRes, tableRes] = await Promise.all([
|
||||
fetch(`${config.apiUrl}/csv/history/import`),
|
||||
fetch(`${config.apiUrl}/csv/history/calculate`),
|
||||
fetch(`${config.apiUrl}/csv/status/modules`),
|
||||
fetch(`${config.apiUrl}/csv/status/tables`),
|
||||
fetch(`${config.apiUrl}/csv/history/import`, { credentials: 'include' }),
|
||||
fetch(`${config.apiUrl}/csv/history/calculate`, { credentials: 'include' }),
|
||||
fetch(`${config.apiUrl}/csv/status/modules`, { credentials: 'include' }),
|
||||
fetch(`${config.apiUrl}/csv/status/tables`, { credentials: 'include' }),
|
||||
]);
|
||||
|
||||
if (!importRes.ok || !calcRes.ok || !moduleRes.ok || !tableRes.ok) {
|
||||
@@ -377,18 +381,41 @@ export function DataManagement() {
|
||||
tableRes.json(),
|
||||
]);
|
||||
|
||||
// Ensure we're setting arrays even if the response is empty or invalid
|
||||
setImportHistory(Array.isArray(importData) ? importData : []);
|
||||
setCalculateHistory(Array.isArray(calcData) ? calcData : []);
|
||||
setModuleStatus(Array.isArray(moduleData) ? moduleData : []);
|
||||
setTableStatus(Array.isArray(tableData) ? tableData : []);
|
||||
// Process import history to add duration_minutes if it doesn't exist
|
||||
const processedImportData = (importData || []).map((record: ImportHistoryRecord) => {
|
||||
if (!record.duration_minutes && record.start_time && record.end_time) {
|
||||
const start = new Date(record.start_time).getTime();
|
||||
const end = new Date(record.end_time).getTime();
|
||||
record.duration_minutes = (end - start) / (1000 * 60);
|
||||
}
|
||||
return record;
|
||||
});
|
||||
|
||||
// Process calculate history to add duration_minutes if it doesn't exist
|
||||
const processedCalcData = (calcData || []).map((record: CalculateHistoryRecord) => {
|
||||
if (!record.duration_minutes && record.start_time && record.end_time) {
|
||||
const start = new Date(record.start_time).getTime();
|
||||
const end = new Date(record.end_time).getTime();
|
||||
record.duration_minutes = (end - start) / (1000 * 60);
|
||||
}
|
||||
return record;
|
||||
});
|
||||
|
||||
setImportHistory(processedImportData);
|
||||
setCalculateHistory(processedCalcData);
|
||||
setModuleStatus(moduleData || []);
|
||||
setTableStatus(tableData || []);
|
||||
setHasError(false);
|
||||
} catch (error) {
|
||||
console.error("Error fetching history:", error);
|
||||
// Set empty arrays as fallback
|
||||
setHasError(true);
|
||||
toast.error("Failed to load data. Please try again.");
|
||||
setImportHistory([]);
|
||||
setCalculateHistory([]);
|
||||
setModuleStatus([]);
|
||||
setTableStatus([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -398,6 +425,7 @@ export function DataManagement() {
|
||||
if (!response.ok) throw new Error('Failed to fetch table status');
|
||||
const data = await response.json();
|
||||
setTableStatus(Array.isArray(data) ? data : []);
|
||||
toast.success("Table status refreshed");
|
||||
} catch (error) {
|
||||
toast.error("Failed to refresh table status");
|
||||
setTableStatus([]);
|
||||
@@ -441,21 +469,26 @@ export function DataManagement() {
|
||||
};
|
||||
|
||||
const refreshAllData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await Promise.all([
|
||||
refreshTableStatus(),
|
||||
refreshModuleStatus(),
|
||||
refreshImportHistory(),
|
||||
refreshCalculateHistory()
|
||||
]);
|
||||
await fetchHistory();
|
||||
toast.success("All data refreshed");
|
||||
} catch (error) {
|
||||
toast.error("Failed to refresh some data");
|
||||
toast.error("Failed to refresh data");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch data immediately on component mount
|
||||
fetchHistory();
|
||||
|
||||
// Set up periodic refresh every minute
|
||||
const refreshInterval = setInterval(fetchHistory, 60000);
|
||||
|
||||
// Clean up interval on component unmount
|
||||
return () => clearInterval(refreshInterval);
|
||||
}, []);
|
||||
|
||||
// Add useEffect to handle auto-scrolling
|
||||
@@ -607,8 +640,13 @@ export function DataManagement() {
|
||||
size="icon"
|
||||
onClick={refreshAllData}
|
||||
className="h-8 w-8"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -620,7 +658,11 @@ export function DataManagement() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="">
|
||||
{tableStatus.length > 0 ? (
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
|
||||
</div>
|
||||
) : tableStatus.length > 0 ? (
|
||||
tableStatus.map((table) => (
|
||||
<div
|
||||
key={table.table_name}
|
||||
@@ -634,12 +676,17 @@ export function DataManagement() {
|
||||
))
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground py-4 text-center">
|
||||
No imports have been performed yet.<br/>Run a full update or reset to import data.
|
||||
{hasError ? (
|
||||
"Failed to load data. Please try refreshing."
|
||||
) : (
|
||||
<>No imports have been performed yet.<br/>Run a full update or reset to import data.</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Module Status */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
@@ -647,7 +694,11 @@ export function DataManagement() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="">
|
||||
{moduleStatus.length > 0 ? (
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
|
||||
</div>
|
||||
) : moduleStatus.length > 0 ? (
|
||||
moduleStatus.map((module) => (
|
||||
<div
|
||||
key={module.module_name}
|
||||
@@ -661,13 +712,18 @@ export function DataManagement() {
|
||||
))
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground py-4 text-center">
|
||||
No metrics have been calculated yet.<br/>Run a full update or reset to calculate metrics.
|
||||
{hasError ? (
|
||||
"Failed to load data. Please try refreshing."
|
||||
) : (
|
||||
<>No metrics have been calculated yet.<br/>Run a full update or reset to calculate metrics.</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Recent Import History */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
@@ -676,7 +732,16 @@ export function DataManagement() {
|
||||
<CardContent className="px-4 mb-4 max-h-[300px] overflow-y-auto">
|
||||
<Table>
|
||||
<TableBody>
|
||||
{importHistory.length > 0 ? (
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell className="text-center py-8">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-gray-400 mb-2" />
|
||||
<span className="text-sm text-muted-foreground">Loading import history...</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : importHistory.length > 0 ? (
|
||||
importHistory.slice(0, 20).map((record) => (
|
||||
<TableRow key={record.id} className="hover:bg-transparent">
|
||||
<TableCell className="w-full p-0">
|
||||
@@ -686,33 +751,41 @@ export function DataManagement() {
|
||||
className="border-0"
|
||||
>
|
||||
<AccordionTrigger className="px-4 py-2">
|
||||
<div className="flex justify-between items-start w-full pr-4">
|
||||
<span className="font-medium min-w-[60px]">
|
||||
#{record.id}
|
||||
</span>
|
||||
<span className="text-sm text-gray-600 min-w-[120px]">
|
||||
{formatDate(record.start_time)}
|
||||
</span>
|
||||
<span className="text-sm min-w-[100px]">
|
||||
{formatDurationWithSeconds(
|
||||
record.duration_minutes,
|
||||
record.status === "running",
|
||||
record.start_time
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`min-w-[80px] ${
|
||||
record.status === "completed"
|
||||
? "text-green-600"
|
||||
: record.status === "failed"
|
||||
? "text-red-600"
|
||||
: record.status === "cancelled"
|
||||
? "text-yellow-600"
|
||||
: "text-blue-600"
|
||||
}`}
|
||||
>
|
||||
{record.status}
|
||||
</span>
|
||||
<div className="flex justify-between items-center w-full pr-4">
|
||||
<div className="w-[50px]">
|
||||
<span className="font-medium">
|
||||
#{record.id}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-[170px]">
|
||||
<span className="text-sm text-gray-600">
|
||||
{formatDate(record.start_time)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-[140px]">
|
||||
<span className="text-sm">
|
||||
{formatDurationWithSeconds(
|
||||
record.duration_minutes || 0,
|
||||
record.status === "running",
|
||||
record.start_time
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-[80px]">
|
||||
<span
|
||||
className={`${
|
||||
record.status === "completed"
|
||||
? "text-green-600"
|
||||
: record.status === "failed"
|
||||
? "text-red-600"
|
||||
: record.status === "cancelled"
|
||||
? "text-yellow-600"
|
||||
: "text-blue-600"
|
||||
}`}
|
||||
>
|
||||
{record.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-2">
|
||||
@@ -749,7 +822,11 @@ export function DataManagement() {
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="text-center text-sm text-muted-foreground py-4">
|
||||
No import history available
|
||||
{hasError ? (
|
||||
"Failed to load import history. Please try refreshing."
|
||||
) : (
|
||||
"No import history available"
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
@@ -766,7 +843,16 @@ export function DataManagement() {
|
||||
<CardContent className="px-4 mb-4 max-h-[300px] overflow-y-auto">
|
||||
<Table>
|
||||
<TableBody>
|
||||
{calculateHistory.length > 0 ? (
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell className="text-center py-8">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-gray-400 mb-2" />
|
||||
<span className="text-sm text-muted-foreground">Loading calculation history...</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : calculateHistory.length > 0 ? (
|
||||
calculateHistory.slice(0, 20).map((record) => (
|
||||
<TableRow key={record.id} className="hover:bg-transparent">
|
||||
<TableCell className="w-full p-0">
|
||||
@@ -776,34 +862,41 @@ export function DataManagement() {
|
||||
className="border-0"
|
||||
>
|
||||
<AccordionTrigger className="px-4 py-2">
|
||||
<div className="flex justify-between items-start w-full pr-4">
|
||||
<span className="font-medium min-w-[60px]">
|
||||
#{record.id}
|
||||
</span>
|
||||
<span className="text-sm text-gray-600 min-w-[120px]">
|
||||
{formatDate(record.start_time)}
|
||||
</span>
|
||||
<span className="text-sm min-w-[100px]">
|
||||
{formatDurationWithSeconds(
|
||||
record.duration_minutes,
|
||||
record.status === "running",
|
||||
record.start_time
|
||||
)}
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={`min-w-[80px] ${
|
||||
record.status === "completed"
|
||||
? "text-green-600"
|
||||
: record.status === "failed"
|
||||
? "text-red-600"
|
||||
: record.status === "cancelled"
|
||||
? "text-yellow-600"
|
||||
: "text-blue-600"
|
||||
}`}
|
||||
>
|
||||
{record.status}
|
||||
</span>
|
||||
<div className="flex justify-between items-center w-full pr-4">
|
||||
<div className="w-[50px]">
|
||||
<span className="font-medium">
|
||||
#{record.id}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-[170px]">
|
||||
<span className="text-sm text-gray-600">
|
||||
{formatDate(record.start_time)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-[140px]">
|
||||
<span className="text-sm">
|
||||
{formatDurationWithSeconds(
|
||||
record.duration_minutes || 0,
|
||||
record.status === "running",
|
||||
record.start_time
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-[80px]">
|
||||
<span
|
||||
className={`${
|
||||
record.status === "completed"
|
||||
? "text-green-600"
|
||||
: record.status === "failed"
|
||||
? "text-red-600"
|
||||
: record.status === "cancelled"
|
||||
? "text-yellow-600"
|
||||
: "text-blue-600"
|
||||
}`}
|
||||
>
|
||||
{record.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-2">
|
||||
@@ -817,28 +910,22 @@ export function DataManagement() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">
|
||||
Processed Products:
|
||||
</span>
|
||||
<span>{record.processed_products}</span>
|
||||
<span className="text-gray-600">Products:</span>
|
||||
<span>{record.processed_products} of {record.total_products}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">
|
||||
Processed Orders:
|
||||
</span>
|
||||
<span>{record.processed_orders}</span>
|
||||
<span className="text-gray-600">Orders:</span>
|
||||
<span>{record.processed_orders} of {record.total_orders}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">
|
||||
Processed Purchase Orders:
|
||||
</span>
|
||||
<span>{record.processed_purchase_orders}</span>
|
||||
<span className="text-gray-600">Purchase Orders:</span>
|
||||
<span>{record.processed_purchase_orders} of {record.total_purchase_orders}</span>
|
||||
</div>
|
||||
{record.error_message && (
|
||||
<div className="text-sm text-red-600 mt-2">
|
||||
{record.error_message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{record.additional_info &&
|
||||
formatJsonData(record.additional_info)}
|
||||
</div>
|
||||
@@ -851,14 +938,18 @@ export function DataManagement() {
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="text-center text-sm text-muted-foreground py-4">
|
||||
No calculation history available
|
||||
{hasError ? (
|
||||
"Failed to load calculation history. Please try refreshing."
|
||||
) : (
|
||||
"No calculation history available"
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -55,10 +55,13 @@ const AVAILABLE_COLUMNS: ColumnDef[] = [
|
||||
{ key: 'stock_quantity', label: 'Shelf Count', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||
{ key: 'stock_status', label: 'Stock Status', group: 'Stock' },
|
||||
{ key: 'days_of_inventory', label: 'Days of Stock', group: 'Stock', format: (v) => v?.toFixed(1) ?? '-' },
|
||||
{ key: 'weeks_of_inventory', label: 'Weeks of Stock', group: 'Stock', format: (v) => v?.toFixed(1) ?? '-' },
|
||||
{ key: 'abc_class', label: 'ABC Class', group: 'Stock' },
|
||||
{ key: 'replenishable', label: 'Replenishable', group: 'Stock' },
|
||||
{ key: 'moq', label: 'MOQ', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||
{ key: 'reorder_qty', label: 'Reorder Qty', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||
{ key: 'reorder_point', label: 'Reorder Point', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||
{ key: 'safety_stock', label: 'Safety Stock', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||
{ key: 'overstocked_amt', label: 'Overstock Amt', group: 'Stock', format: (v) => v?.toString() ?? '-' },
|
||||
{ key: 'price', label: 'Price', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' },
|
||||
{ key: 'regular_price', label: 'Default Price', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' },
|
||||
@@ -67,15 +70,22 @@ const AVAILABLE_COLUMNS: ColumnDef[] = [
|
||||
{ key: 'daily_sales_avg', label: 'Daily Sales', group: 'Sales', format: (v) => v?.toFixed(1) ?? '-' },
|
||||
{ key: 'weekly_sales_avg', label: 'Weekly Sales', group: 'Sales', format: (v) => v?.toFixed(1) ?? '-' },
|
||||
{ key: 'monthly_sales_avg', label: 'Monthly Sales', group: 'Sales', format: (v) => v?.toFixed(1) ?? '-' },
|
||||
{ key: 'avg_quantity_per_order', label: 'Avg Qty/Order', group: 'Sales', format: (v) => v?.toFixed(1) ?? '-' },
|
||||
{ key: 'number_of_orders', label: 'Order Count', group: 'Sales', format: (v) => v?.toString() ?? '-' },
|
||||
{ key: 'first_sale_date', label: 'First Sale', group: 'Sales' },
|
||||
{ key: 'last_sale_date', label: 'Last Sale', group: 'Sales' },
|
||||
{ key: 'gmroi', label: 'GMROI', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
||||
{ key: 'turnover_rate', label: 'Turnover Rate', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
||||
{ key: 'avg_margin_percent', label: 'Margin %', group: 'Financial', format: (v) => v ? `${v.toFixed(1)}%` : '-' },
|
||||
{ key: 'inventory_value', label: 'Inventory Value', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
||||
{ key: 'cost_of_goods_sold', label: 'COGS', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
||||
{ key: 'gross_profit', label: 'Gross Profit', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' },
|
||||
{ key: 'current_lead_time', label: 'Current Lead Time', group: 'Lead Time', format: (v) => v?.toFixed(1) ?? '-' },
|
||||
{ key: 'target_lead_time', label: 'Target Lead Time', group: 'Lead Time', format: (v) => v?.toFixed(1) ?? '-' },
|
||||
{ key: 'lead_time_status', label: 'Lead Time Status', group: 'Lead Time' },
|
||||
{ key: 'last_purchase_date', label: 'Last Purchase', group: 'Lead Time' },
|
||||
{ key: 'first_received_date', label: 'First Received', group: 'Lead Time' },
|
||||
{ key: 'last_received_date', label: 'Last Received', group: 'Lead Time' },
|
||||
];
|
||||
|
||||
// Define default columns for each view
|
||||
@@ -93,14 +103,17 @@ const VIEW_COLUMNS: Record<string, ColumnKey[]> = {
|
||||
'daily_sales_avg',
|
||||
'weekly_sales_avg',
|
||||
'monthly_sales_avg',
|
||||
'inventory_value',
|
||||
],
|
||||
critical: [
|
||||
'image',
|
||||
'title',
|
||||
'stock_quantity',
|
||||
'safety_stock',
|
||||
'daily_sales_avg',
|
||||
'weekly_sales_avg',
|
||||
'reorder_qty',
|
||||
'reorder_point',
|
||||
'vendor',
|
||||
'last_purchase_date',
|
||||
'current_lead_time',
|
||||
@@ -109,11 +122,13 @@ const VIEW_COLUMNS: Record<string, ColumnKey[]> = {
|
||||
'image',
|
||||
'title',
|
||||
'stock_quantity',
|
||||
'reorder_point',
|
||||
'daily_sales_avg',
|
||||
'weekly_sales_avg',
|
||||
'reorder_qty',
|
||||
'vendor',
|
||||
'last_purchase_date',
|
||||
'avg_lead_time_days',
|
||||
],
|
||||
overstocked: [
|
||||
'image',
|
||||
@@ -123,15 +138,19 @@ const VIEW_COLUMNS: Record<string, ColumnKey[]> = {
|
||||
'weekly_sales_avg',
|
||||
'overstocked_amt',
|
||||
'days_of_inventory',
|
||||
'inventory_value',
|
||||
'turnover_rate',
|
||||
],
|
||||
'at-risk': [
|
||||
'image',
|
||||
'title',
|
||||
'stock_quantity',
|
||||
'safety_stock',
|
||||
'daily_sales_avg',
|
||||
'weekly_sales_avg',
|
||||
'days_of_inventory',
|
||||
'last_sale_date',
|
||||
'current_lead_time',
|
||||
],
|
||||
new: [
|
||||
'image',
|
||||
@@ -141,6 +160,7 @@ const VIEW_COLUMNS: Record<string, ColumnKey[]> = {
|
||||
'brand',
|
||||
'price',
|
||||
'regular_price',
|
||||
'first_received_date',
|
||||
],
|
||||
healthy: [
|
||||
'image',
|
||||
@@ -150,6 +170,8 @@ const VIEW_COLUMNS: Record<string, ColumnKey[]> = {
|
||||
'weekly_sales_avg',
|
||||
'monthly_sales_avg',
|
||||
'days_of_inventory',
|
||||
'gross_profit',
|
||||
'gmroi',
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user