Fix/enhance data management page

This commit is contained in:
2025-03-27 17:09:06 -04:00
parent 957c7b5eb1
commit 087ec710f6
2 changed files with 209 additions and 79 deletions

View File

@@ -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;

View File

@@ -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,13 +723,14 @@ 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]">
<Card className="flex-1">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle>Last Import Times</CardTitle> <CardTitle>Last Import Times</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="h-[calc(50%)]">
<div className=""> <div className="">
{isLoading ? ( {isLoading && !tableStatus.length ? (
<div className="flex justify-center py-4"> <div className="flex justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-gray-400" /> <Loader2 className="h-6 w-6 animate-spin text-gray-400" />
</div> </div>
@@ -688,13 +760,13 @@ export function DataManagement() {
</Card> </Card>
{/* Module Status */} {/* Module Status */}
<Card> <Card className="flex-1">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle>Last Calculation Times</CardTitle> <CardTitle>Last Calculation Times</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="h-[calc(50%)]">
<div className=""> <div className="">
{isLoading ? ( {isLoading && !moduleStatus.length ? (
<div className="flex justify-center py-4"> <div className="flex justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-gray-400" /> <Loader2 className="h-6 w-6 animate-spin text-gray-400" />
</div> </div>
@@ -724,6 +796,10 @@ export function DataManagement() {
</Card> </Card>
</div> </div>
{/* Add Table Counts section here */}
{renderTableCountsSection()}
</div>
{/* Recent Import History */} {/* Recent Import History */}
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
@@ -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">