Fix/enhance data management page
This commit is contained in:
@@ -836,4 +836,58 @@ router.get('/status/tables', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /status/table-counts - Get record counts for all tables
|
||||||
|
router.get('/status/table-counts', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = req.app.locals.pool;
|
||||||
|
const tables = [
|
||||||
|
// Core tables
|
||||||
|
'products', 'categories', 'product_categories', 'orders', 'purchase_orders',
|
||||||
|
// Metrics tables
|
||||||
|
'product_metrics', 'product_time_aggregates', 'vendor_metrics', 'category_metrics',
|
||||||
|
'vendor_time_metrics', 'category_time_metrics', 'category_sales_metrics',
|
||||||
|
'brand_metrics', 'brand_time_metrics', 'sales_forecasts', 'category_forecasts',
|
||||||
|
// Config tables
|
||||||
|
'stock_thresholds', 'lead_time_thresholds', 'sales_velocity_config',
|
||||||
|
'abc_classification_config', 'safety_stock_config', 'turnover_config',
|
||||||
|
'sales_seasonality', 'financial_calc_config'
|
||||||
|
];
|
||||||
|
|
||||||
|
const counts = await Promise.all(
|
||||||
|
tables.map(table =>
|
||||||
|
pool.query(`SELECT COUNT(*) as count FROM ${table}`)
|
||||||
|
.then(result => ({
|
||||||
|
table_name: table,
|
||||||
|
count: parseInt(result.rows[0].count)
|
||||||
|
}))
|
||||||
|
.catch(err => ({
|
||||||
|
table_name: table,
|
||||||
|
count: null,
|
||||||
|
error: err.message
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Group tables by type
|
||||||
|
const groupedCounts = {
|
||||||
|
core: counts.filter(c => ['products', 'categories', 'product_categories', 'orders', 'purchase_orders'].includes(c.table_name)),
|
||||||
|
metrics: counts.filter(c => [
|
||||||
|
'product_metrics', 'product_time_aggregates', 'vendor_metrics', 'category_metrics',
|
||||||
|
'vendor_time_metrics', 'category_time_metrics', 'category_sales_metrics',
|
||||||
|
'brand_metrics', 'brand_time_metrics', 'sales_forecasts', 'category_forecasts'
|
||||||
|
].includes(c.table_name)),
|
||||||
|
config: counts.filter(c => [
|
||||||
|
'stock_thresholds', 'lead_time_thresholds', 'sales_velocity_config',
|
||||||
|
'abc_classification_config', 'safety_stock_config', 'turnover_config',
|
||||||
|
'sales_seasonality', 'financial_calc_config'
|
||||||
|
].includes(c.table_name))
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(groupedCounts);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching table counts:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -79,6 +79,18 @@ interface TableStatus {
|
|||||||
last_sync_timestamp: string;
|
last_sync_timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TableCount {
|
||||||
|
table_name: string;
|
||||||
|
count: number | null;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupedTableCounts {
|
||||||
|
core: TableCount[];
|
||||||
|
metrics: TableCount[];
|
||||||
|
config: TableCount[];
|
||||||
|
}
|
||||||
|
|
||||||
export function DataManagement() {
|
export function DataManagement() {
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [isResetting, setIsResetting] = useState(false);
|
const [isResetting, setIsResetting] = useState(false);
|
||||||
@@ -90,6 +102,7 @@ export function DataManagement() {
|
|||||||
const [tableStatus, setTableStatus] = useState<TableStatus[]>([]);
|
const [tableStatus, setTableStatus] = useState<TableStatus[]>([]);
|
||||||
const [scriptOutput, setScriptOutput] = useState<string[]>([]);
|
const [scriptOutput, setScriptOutput] = useState<string[]>([]);
|
||||||
const [eventSource, setEventSource] = useState<EventSource | null>(null);
|
const [eventSource, setEventSource] = useState<EventSource | null>(null);
|
||||||
|
const [tableCounts, setTableCounts] = useState<GroupedTableCounts | null>(null);
|
||||||
|
|
||||||
// Add useRef for scroll handling
|
// Add useRef for scroll handling
|
||||||
const terminalRef = useRef<HTMLDivElement>(null);
|
const terminalRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -187,6 +200,7 @@ export function DataManagement() {
|
|||||||
const handleFullUpdate = async () => {
|
const handleFullUpdate = async () => {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
setScriptOutput([]);
|
setScriptOutput([]);
|
||||||
|
fetchHistory(); // Refresh at start
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const source = new EventSource(`${config.apiUrl}/csv/update/progress`, {
|
const source = new EventSource(`${config.apiUrl}/csv/update/progress`, {
|
||||||
@@ -207,10 +221,10 @@ export function DataManagement() {
|
|||||||
source.close();
|
source.close();
|
||||||
setEventSource(null);
|
setEventSource(null);
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
|
fetchHistory(); // Refresh at end
|
||||||
|
|
||||||
if (data.status === 'complete') {
|
if (data.status === 'complete') {
|
||||||
toast.success("Update completed successfully");
|
toast.success("Update completed successfully");
|
||||||
fetchHistory();
|
|
||||||
} else if (data.status === 'error') {
|
} else if (data.status === 'error') {
|
||||||
toast.error(`Update failed: ${data.error || 'Unknown error'}`);
|
toast.error(`Update failed: ${data.error || 'Unknown error'}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -257,6 +271,7 @@ export function DataManagement() {
|
|||||||
const handleFullReset = async () => {
|
const handleFullReset = async () => {
|
||||||
setIsResetting(true);
|
setIsResetting(true);
|
||||||
setScriptOutput([]);
|
setScriptOutput([]);
|
||||||
|
fetchHistory(); // Refresh at start
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const source = new EventSource(`${config.apiUrl}/csv/reset/progress`, {
|
const source = new EventSource(`${config.apiUrl}/csv/reset/progress`, {
|
||||||
@@ -277,10 +292,10 @@ export function DataManagement() {
|
|||||||
source.close();
|
source.close();
|
||||||
setEventSource(null);
|
setEventSource(null);
|
||||||
setIsResetting(false);
|
setIsResetting(false);
|
||||||
|
fetchHistory(); // Refresh at end
|
||||||
|
|
||||||
if (data.status === 'complete') {
|
if (data.status === 'complete') {
|
||||||
toast.success("Reset completed successfully");
|
toast.success("Reset completed successfully");
|
||||||
fetchHistory();
|
|
||||||
} else if (data.status === 'error') {
|
} else if (data.status === 'error') {
|
||||||
toast.error(`Reset failed: ${data.error || 'Unknown error'}`);
|
toast.error(`Reset failed: ${data.error || 'Unknown error'}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -359,26 +374,29 @@ export function DataManagement() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchHistory = async () => {
|
const fetchHistory = async () => {
|
||||||
|
let shouldSetLoading = !importHistory.length || !calculateHistory.length;
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
if (shouldSetLoading) setIsLoading(true);
|
||||||
setHasError(false);
|
setHasError(false);
|
||||||
|
|
||||||
const [importRes, calcRes, moduleRes, tableRes] = await Promise.all([
|
const [importRes, calcRes, moduleRes, tableRes, tableCountsRes] = await Promise.all([
|
||||||
fetch(`${config.apiUrl}/csv/history/import`, { credentials: 'include' }),
|
fetch(`${config.apiUrl}/csv/history/import`, { credentials: 'include' }),
|
||||||
fetch(`${config.apiUrl}/csv/history/calculate`, { credentials: 'include' }),
|
fetch(`${config.apiUrl}/csv/history/calculate`, { credentials: 'include' }),
|
||||||
fetch(`${config.apiUrl}/csv/status/modules`, { credentials: 'include' }),
|
fetch(`${config.apiUrl}/csv/status/modules`, { credentials: 'include' }),
|
||||||
fetch(`${config.apiUrl}/csv/status/tables`, { credentials: 'include' }),
|
fetch(`${config.apiUrl}/csv/status/tables`, { credentials: 'include' }),
|
||||||
|
fetch(`${config.apiUrl}/csv/status/table-counts`, { credentials: 'include' }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!importRes.ok || !calcRes.ok || !moduleRes.ok || !tableRes.ok) {
|
if (!importRes.ok || !calcRes.ok || !moduleRes.ok || !tableRes.ok || !tableCountsRes.ok) {
|
||||||
throw new Error('One or more requests failed');
|
throw new Error('One or more requests failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const [importData, calcData, moduleData, tableData] = await Promise.all([
|
const [importData, calcData, moduleData, tableData, tableCountsData] = await Promise.all([
|
||||||
importRes.json(),
|
importRes.json(),
|
||||||
calcRes.json(),
|
calcRes.json(),
|
||||||
moduleRes.json(),
|
moduleRes.json(),
|
||||||
tableRes.json(),
|
tableRes.json(),
|
||||||
|
tableCountsRes.json(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Process import history to add duration_minutes if it doesn't exist
|
// Process import history to add duration_minutes if it doesn't exist
|
||||||
@@ -405,17 +423,19 @@ export function DataManagement() {
|
|||||||
setCalculateHistory(processedCalcData);
|
setCalculateHistory(processedCalcData);
|
||||||
setModuleStatus(moduleData || []);
|
setModuleStatus(moduleData || []);
|
||||||
setTableStatus(tableData || []);
|
setTableStatus(tableData || []);
|
||||||
|
setTableCounts(tableCountsData);
|
||||||
setHasError(false);
|
setHasError(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching history:", error);
|
console.error("Error fetching data:", error);
|
||||||
setHasError(true);
|
setHasError(true);
|
||||||
toast.error("Failed to load data. Please try again.");
|
toast.error("Failed to load data. Please try again.");
|
||||||
setImportHistory([]);
|
setImportHistory([]);
|
||||||
setCalculateHistory([]);
|
setCalculateHistory([]);
|
||||||
setModuleStatus([]);
|
setModuleStatus([]);
|
||||||
setTableStatus([]);
|
setTableStatus([]);
|
||||||
|
setTableCounts(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
if (shouldSetLoading) setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -528,6 +548,57 @@ export function DataManagement() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add formatNumber helper
|
||||||
|
const formatNumber = (num: number | null) => {
|
||||||
|
if (num === null) return 'N/A';
|
||||||
|
return new Intl.NumberFormat().format(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update renderTableCountsSection to match other cards' styling
|
||||||
|
const renderTableCountsSection = () => {
|
||||||
|
if (!tableCounts) return null;
|
||||||
|
|
||||||
|
const renderTableGroup = (title: string, tables: TableCount[]) => (
|
||||||
|
<div className="mt-0 border-t first:border-t-0 first:mt-0">
|
||||||
|
<div>
|
||||||
|
{tables.map((table, index) => (
|
||||||
|
<div
|
||||||
|
key={table.table_name}
|
||||||
|
className="flex justify-between text-sm items-center py-2 border-b last:border-0"
|
||||||
|
>
|
||||||
|
<span className="font-medium">{table.table_name}</span>
|
||||||
|
{table.error ? (
|
||||||
|
<span className="text-red-600 text-sm">{table.error}</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-gray-600">{formatNumber(table.count)}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="md:col-start-2 md:row-span-2 h-[670px]">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle>Table Record Counts</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoading && !tableCounts.core.length ? (
|
||||||
|
<div className="flex justify-center py-4">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div className="bg-sky-50/50 rounded-t-md px-2">{renderTableGroup('Core Tables', tableCounts.core)}</div>
|
||||||
|
<div className="bg-green-50/50 rounded-b-md px-2">{renderTableGroup('Metrics Tables', tableCounts.metrics)}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8 max-w-4xl">
|
<div className="space-y-8 max-w-4xl">
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
@@ -632,7 +703,7 @@ export function DataManagement() {
|
|||||||
{(isUpdating || isResetting) && renderTerminal()}
|
{(isUpdating || isResetting) && renderTerminal()}
|
||||||
|
|
||||||
{/* History Section */}
|
{/* History Section */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h2 className="text-2xl font-bold">History & Status</h2>
|
<h2 className="text-2xl font-bold">History & Status</h2>
|
||||||
<Button
|
<Button
|
||||||
@@ -652,76 +723,81 @@ export function DataManagement() {
|
|||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{/* Table Status */}
|
{/* Table Status */}
|
||||||
<Card className="">
|
<div className="space-y-4 flex flex-col h-[670px]">
|
||||||
<CardHeader className="pb-3">
|
<Card className="flex-1">
|
||||||
<CardTitle>Last Import Times</CardTitle>
|
<CardHeader className="pb-3">
|
||||||
</CardHeader>
|
<CardTitle>Last Import Times</CardTitle>
|
||||||
<CardContent>
|
</CardHeader>
|
||||||
<div className="">
|
<CardContent className="h-[calc(50%)]">
|
||||||
{isLoading ? (
|
<div className="">
|
||||||
<div className="flex justify-center py-4">
|
{isLoading && !tableStatus.length ? (
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
|
<div className="flex justify-center py-4">
|
||||||
</div>
|
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
|
||||||
) : tableStatus.length > 0 ? (
|
|
||||||
tableStatus.map((table) => (
|
|
||||||
<div
|
|
||||||
key={table.table_name}
|
|
||||||
className="flex justify-between text-sm items-center py-2 border-b last:border-0"
|
|
||||||
>
|
|
||||||
<span className="font-medium">{table.table_name}</span>
|
|
||||||
<span className="text-sm text-gray-600">
|
|
||||||
{formatStatusTime(table.last_sync_timestamp)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
) : tableStatus.length > 0 ? (
|
||||||
) : (
|
tableStatus.map((table) => (
|
||||||
<div className="text-sm text-muted-foreground py-4 text-center">
|
<div
|
||||||
{hasError ? (
|
key={table.table_name}
|
||||||
"Failed to load data. Please try refreshing."
|
className="flex justify-between text-sm items-center py-2 border-b last:border-0"
|
||||||
) : (
|
>
|
||||||
<>No imports have been performed yet.<br/>Run a full update or reset to import data.</>
|
<span className="font-medium">{table.table_name}</span>
|
||||||
)}
|
<span className="text-sm text-gray-600">
|
||||||
</div>
|
{formatStatusTime(table.last_sync_timestamp)}
|
||||||
)}
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
))
|
||||||
</Card>
|
) : (
|
||||||
|
<div className="text-sm text-muted-foreground py-4 text-center">
|
||||||
{/* Module Status */}
|
{hasError ? (
|
||||||
<Card>
|
"Failed to load data. Please try refreshing."
|
||||||
<CardHeader className="pb-3">
|
) : (
|
||||||
<CardTitle>Last Calculation Times</CardTitle>
|
<>No imports have been performed yet.<br/>Run a full update or reset to import data.</>
|
||||||
</CardHeader>
|
)}
|
||||||
<CardContent>
|
|
||||||
<div className="">
|
|
||||||
{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}
|
|
||||||
className="flex justify-between text-sm items-center py-2 border-b last:border-0"
|
|
||||||
>
|
|
||||||
<span className="font-medium">{module.module_name}</span>
|
|
||||||
<span className="text-sm text-gray-600">
|
|
||||||
{formatStatusTime(module.last_calculation_timestamp)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
)}
|
||||||
) : (
|
</div>
|
||||||
<div className="text-sm text-muted-foreground py-4 text-center">
|
</CardContent>
|
||||||
{hasError ? (
|
</Card>
|
||||||
"Failed to load data. Please try refreshing."
|
|
||||||
) : (
|
{/* Module Status */}
|
||||||
<>No metrics have been calculated yet.<br/>Run a full update or reset to calculate metrics.</>
|
<Card className="flex-1">
|
||||||
)}
|
<CardHeader className="pb-3">
|
||||||
</div>
|
<CardTitle>Last Calculation Times</CardTitle>
|
||||||
)}
|
</CardHeader>
|
||||||
</div>
|
<CardContent className="h-[calc(50%)]">
|
||||||
</CardContent>
|
<div className="">
|
||||||
</Card>
|
{isLoading && !moduleStatus.length ? (
|
||||||
|
<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}
|
||||||
|
className="flex justify-between text-sm items-center py-2 border-b last:border-0"
|
||||||
|
>
|
||||||
|
<span className="font-medium">{module.module_name}</span>
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
{formatStatusTime(module.last_calculation_timestamp)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-muted-foreground py-4 text-center">
|
||||||
|
{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>
|
||||||
|
|
||||||
|
{/* Add Table Counts section here */}
|
||||||
|
{renderTableCountsSection()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recent Import History */}
|
{/* Recent Import History */}
|
||||||
@@ -732,7 +808,7 @@ export function DataManagement() {
|
|||||||
<CardContent className="px-4 mb-4 max-h-[300px] overflow-y-auto">
|
<CardContent className="px-4 mb-4 max-h-[300px] overflow-y-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{isLoading ? (
|
{isLoading && !importHistory.length ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="text-center py-8">
|
<TableCell className="text-center py-8">
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
@@ -843,7 +919,7 @@ export function DataManagement() {
|
|||||||
<CardContent className="px-4 mb-4 max-h-[300px] overflow-y-auto">
|
<CardContent className="px-4 mb-4 max-h-[300px] overflow-y-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{isLoading ? (
|
{isLoading && !calculateHistory.length ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="text-center py-8">
|
<TableCell className="text-center py-8">
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
|||||||
Reference in New Issue
Block a user