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;
|
||||
@@ -79,6 +79,18 @@ interface TableStatus {
|
||||
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() {
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
@@ -90,6 +102,7 @@ export function DataManagement() {
|
||||
const [tableStatus, setTableStatus] = useState<TableStatus[]>([]);
|
||||
const [scriptOutput, setScriptOutput] = useState<string[]>([]);
|
||||
const [eventSource, setEventSource] = useState<EventSource | null>(null);
|
||||
const [tableCounts, setTableCounts] = useState<GroupedTableCounts | null>(null);
|
||||
|
||||
// Add useRef for scroll handling
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
@@ -187,6 +200,7 @@ export function DataManagement() {
|
||||
const handleFullUpdate = async () => {
|
||||
setIsUpdating(true);
|
||||
setScriptOutput([]);
|
||||
fetchHistory(); // Refresh at start
|
||||
|
||||
try {
|
||||
const source = new EventSource(`${config.apiUrl}/csv/update/progress`, {
|
||||
@@ -207,10 +221,10 @@ export function DataManagement() {
|
||||
source.close();
|
||||
setEventSource(null);
|
||||
setIsUpdating(false);
|
||||
fetchHistory(); // Refresh at end
|
||||
|
||||
if (data.status === 'complete') {
|
||||
toast.success("Update completed successfully");
|
||||
fetchHistory();
|
||||
} else if (data.status === 'error') {
|
||||
toast.error(`Update failed: ${data.error || 'Unknown error'}`);
|
||||
} else {
|
||||
@@ -257,6 +271,7 @@ export function DataManagement() {
|
||||
const handleFullReset = async () => {
|
||||
setIsResetting(true);
|
||||
setScriptOutput([]);
|
||||
fetchHistory(); // Refresh at start
|
||||
|
||||
try {
|
||||
const source = new EventSource(`${config.apiUrl}/csv/reset/progress`, {
|
||||
@@ -277,10 +292,10 @@ export function DataManagement() {
|
||||
source.close();
|
||||
setEventSource(null);
|
||||
setIsResetting(false);
|
||||
fetchHistory(); // Refresh at end
|
||||
|
||||
if (data.status === 'complete') {
|
||||
toast.success("Reset completed successfully");
|
||||
fetchHistory();
|
||||
} else if (data.status === 'error') {
|
||||
toast.error(`Reset failed: ${data.error || 'Unknown error'}`);
|
||||
} else {
|
||||
@@ -359,26 +374,29 @@ export function DataManagement() {
|
||||
};
|
||||
|
||||
const fetchHistory = async () => {
|
||||
let shouldSetLoading = !importHistory.length || !calculateHistory.length;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
if (shouldSetLoading) setIsLoading(true);
|
||||
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/calculate`, { credentials: 'include' }),
|
||||
fetch(`${config.apiUrl}/csv/status/modules`, { 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');
|
||||
}
|
||||
|
||||
const [importData, calcData, moduleData, tableData] = await Promise.all([
|
||||
const [importData, calcData, moduleData, tableData, tableCountsData] = await Promise.all([
|
||||
importRes.json(),
|
||||
calcRes.json(),
|
||||
moduleRes.json(),
|
||||
tableRes.json(),
|
||||
tableCountsRes.json(),
|
||||
]);
|
||||
|
||||
// Process import history to add duration_minutes if it doesn't exist
|
||||
@@ -405,17 +423,19 @@ export function DataManagement() {
|
||||
setCalculateHistory(processedCalcData);
|
||||
setModuleStatus(moduleData || []);
|
||||
setTableStatus(tableData || []);
|
||||
setTableCounts(tableCountsData);
|
||||
setHasError(false);
|
||||
} catch (error) {
|
||||
console.error("Error fetching history:", error);
|
||||
console.error("Error fetching data:", error);
|
||||
setHasError(true);
|
||||
toast.error("Failed to load data. Please try again.");
|
||||
setImportHistory([]);
|
||||
setCalculateHistory([]);
|
||||
setModuleStatus([]);
|
||||
setTableStatus([]);
|
||||
setTableCounts(null);
|
||||
} 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 (
|
||||
<div className="space-y-8 max-w-4xl">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
@@ -632,7 +703,7 @@ export function DataManagement() {
|
||||
{(isUpdating || isResetting) && renderTerminal()}
|
||||
|
||||
{/* History Section */}
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-2xl font-bold">History & Status</h2>
|
||||
<Button
|
||||
@@ -652,76 +723,81 @@ export function DataManagement() {
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{/* Table Status */}
|
||||
<Card className="">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle>Last Import Times</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="">
|
||||
{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}
|
||||
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 className="space-y-4 flex flex-col h-[670px]">
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle>Last Import Times</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[calc(50%)]">
|
||||
<div className="">
|
||||
{isLoading && !tableStatus.length ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground py-4 text-center">
|
||||
{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">
|
||||
<CardTitle>Last Calculation Times</CardTitle>
|
||||
</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>
|
||||
) : 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 className="text-sm text-muted-foreground py-4 text-center">
|
||||
{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 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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Module Status */}
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle>Last Calculation Times</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[calc(50%)]">
|
||||
<div className="">
|
||||
{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>
|
||||
|
||||
{/* Recent Import History */}
|
||||
@@ -732,7 +808,7 @@ export function DataManagement() {
|
||||
<CardContent className="px-4 mb-4 max-h-[300px] overflow-y-auto">
|
||||
<Table>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
{isLoading && !importHistory.length ? (
|
||||
<TableRow>
|
||||
<TableCell className="text-center py-8">
|
||||
<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">
|
||||
<Table>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
{isLoading && !calculateHistory.length ? (
|
||||
<TableRow>
|
||||
<TableCell className="text-center py-8">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
|
||||
Reference in New Issue
Block a user