2 Commits

9 changed files with 434 additions and 158 deletions

View File

@@ -184,7 +184,7 @@ async function resetDatabase() {
SELECT string_agg(tablename, ', ') as tables SELECT string_agg(tablename, ', ') as tables
FROM pg_tables FROM pg_tables
WHERE schemaname = 'public' WHERE schemaname = 'public'
AND tablename NOT IN ('users', 'permissions', 'user_permissions', 'calculate_history', 'import_history', 'ai_prompts', 'ai_validation_performance', 'templates'); AND tablename NOT IN ('users', 'permissions', 'user_permissions', 'calculate_history', 'import_history', 'ai_prompts', 'ai_validation_performance', 'templates', 'reusable_images');
`); `);
if (!tablesResult.rows[0].tables) { if (!tablesResult.rows[0].tables) {
@@ -204,7 +204,7 @@ async function resetDatabase() {
// Drop all tables except users // Drop all tables except users
const tables = tablesResult.rows[0].tables.split(', '); const tables = tablesResult.rows[0].tables.split(', ');
for (const table of tables) { for (const table of tables) {
if (!['users'].includes(table)) { if (!['users', 'reusable_images'].includes(table)) {
await client.query(`DROP TABLE IF EXISTS "${table}" CASCADE`); await client.query(`DROP TABLE IF EXISTS "${table}" CASCADE`);
} }
} }

View File

@@ -39,6 +39,19 @@ const METRICS_TABLES = [
'vendor_details' 'vendor_details'
]; ];
// Tables to always protect from being dropped
const PROTECTED_TABLES = [
'users',
'permissions',
'user_permissions',
'calculate_history',
'import_history',
'ai_prompts',
'ai_validation_performance',
'templates',
'reusable_images'
];
// Split SQL into individual statements // Split SQL into individual statements
function splitSQLStatements(sql) { function splitSQLStatements(sql) {
sql = sql.replace(/\r\n/g, '\n'); sql = sql.replace(/\r\n/g, '\n');
@@ -109,7 +122,8 @@ async function resetMetrics() {
FROM pg_tables FROM pg_tables
WHERE schemaname = 'public' WHERE schemaname = 'public'
AND tablename = ANY($1) AND tablename = ANY($1)
`, [METRICS_TABLES]); AND tablename NOT IN (SELECT unnest($2::text[]))
`, [METRICS_TABLES, PROTECTED_TABLES]);
outputProgress({ outputProgress({
operation: 'Initial state', operation: 'Initial state',
@@ -126,6 +140,15 @@ async function resetMetrics() {
}); });
for (const table of [...METRICS_TABLES].reverse()) { for (const table of [...METRICS_TABLES].reverse()) {
// Skip protected tables
if (PROTECTED_TABLES.includes(table)) {
outputProgress({
operation: 'Protected table',
message: `Skipping protected table: ${table}`
});
continue;
}
try { try {
// Use NOWAIT to avoid hanging if there's a lock // Use NOWAIT to avoid hanging if there's a lock
await client.query(`DROP TABLE IF EXISTS "${table}" CASCADE`); await client.query(`DROP TABLE IF EXISTS "${table}" CASCADE`);

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

@@ -8,7 +8,9 @@ const fs = require('fs');
// Create uploads directory if it doesn't exist // Create uploads directory if it doesn't exist
const uploadsDir = path.join('/var/www/html/inventory/uploads/products'); const uploadsDir = path.join('/var/www/html/inventory/uploads/products');
const reusableUploadsDir = path.join('/var/www/html/inventory/uploads/reusable');
fs.mkdirSync(uploadsDir, { recursive: true }); fs.mkdirSync(uploadsDir, { recursive: true });
fs.mkdirSync(reusableUploadsDir, { recursive: true });
// Create a Map to track image upload times and their scheduled deletion // Create a Map to track image upload times and their scheduled deletion
const imageUploadMap = new Map(); const imageUploadMap = new Map();
@@ -35,6 +37,12 @@ const connectionCache = {
// Function to schedule image deletion after 24 hours // Function to schedule image deletion after 24 hours
const scheduleImageDeletion = (filename, filePath) => { const scheduleImageDeletion = (filename, filePath) => {
// Only schedule deletion for images in the products folder
if (!filePath.includes('/uploads/products/')) {
console.log(`Skipping deletion for non-product image: ${filename}`);
return;
}
// Delete any existing timeout for this file // Delete any existing timeout for this file
if (imageUploadMap.has(filename)) { if (imageUploadMap.has(filename)) {
clearTimeout(imageUploadMap.get(filename).timeoutId); clearTimeout(imageUploadMap.get(filename).timeoutId);
@@ -407,6 +415,14 @@ router.delete('/delete-image', (req, res) => {
return res.status(404).json({ error: 'File not found' }); return res.status(404).json({ error: 'File not found' });
} }
// Only allow deletion of images in the products folder
if (!filePath.includes('/uploads/products/')) {
return res.status(403).json({
error: 'Cannot delete images outside the products folder',
message: 'This image is in a protected folder and cannot be deleted through this endpoint'
});
}
// Delete the file // Delete the file
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
@@ -641,11 +657,19 @@ router.get('/check-file/:filename', (req, res) => {
return res.status(400).json({ error: 'Invalid filename' }); return res.status(400).json({ error: 'Invalid filename' });
} }
const filePath = path.join(uploadsDir, filename); // First check in products directory
let filePath = path.join(uploadsDir, filename);
let exists = fs.existsSync(filePath);
// If not found in products, check in reusable directory
if (!exists) {
filePath = path.join(reusableUploadsDir, filename);
exists = fs.existsSync(filePath);
}
try { try {
// Check if file exists // Check if file exists
if (!fs.existsSync(filePath)) { if (!exists) {
return res.status(404).json({ return res.status(404).json({
error: 'File not found', error: 'File not found',
path: filePath, path: filePath,
@@ -685,13 +709,23 @@ router.get('/check-file/:filename', (req, res) => {
// List all files in uploads directory // List all files in uploads directory
router.get('/list-uploads', (req, res) => { router.get('/list-uploads', (req, res) => {
try { try {
if (!fs.existsSync(uploadsDir)) { const { directory = 'products' } = req.query;
return res.status(404).json({ error: 'Uploads directory not found', path: uploadsDir });
// Determine which directory to list
let targetDir;
if (directory === 'reusable') {
targetDir = reusableUploadsDir;
} else {
targetDir = uploadsDir; // default to products
} }
const files = fs.readdirSync(uploadsDir); if (!fs.existsSync(targetDir)) {
return res.status(404).json({ error: 'Uploads directory not found', path: targetDir });
}
const files = fs.readdirSync(targetDir);
const fileDetails = files.map(file => { const fileDetails = files.map(file => {
const filePath = path.join(uploadsDir, file); const filePath = path.join(targetDir, file);
try { try {
const stats = fs.statSync(filePath); const stats = fs.statSync(filePath);
return { return {
@@ -709,12 +743,13 @@ router.get('/list-uploads', (req, res) => {
}); });
return res.json({ return res.json({
directory: uploadsDir, directory: targetDir,
type: directory,
count: files.length, count: files.length,
files: fileDetails files: fileDetails
}); });
} catch (error) { } catch (error) {
return res.status(500).json({ error: error.message, path: uploadsDir }); return res.status(500).json({ error: error.message });
} }
}); });

View File

@@ -0,0 +1,66 @@
import { useQuery } from '@tanstack/react-query';
import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import config from '../../config';
interface SalesData {
date: string;
total: number;
}
export function Overview() {
const { data, isLoading, error } = useQuery<SalesData[]>({
queryKey: ['sales-overview'],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/sales-overview`);
if (!response.ok) {
throw new Error('Failed to fetch sales overview');
}
const rawData = await response.json();
return rawData.map((item: SalesData) => ({
...item,
total: parseFloat(item.total.toString()),
date: new Date(item.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
}));
},
});
if (isLoading) {
return <div>Loading chart...</div>;
}
if (error) {
return <div className="text-red-500">Error loading sales overview</div>;
}
return (
<ResponsiveContainer width="100%" height={350}>
<LineChart data={data}>
<XAxis
dataKey="date"
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `$${value.toLocaleString()}`}
/>
<Tooltip
formatter={(value: number) => [`$${value.toLocaleString()}`, 'Sales']}
labelFormatter={(label) => `Date: ${label}`}
/>
<Line
type="monotone"
dataKey="total"
stroke="hsl(var(--primary))"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
);
}

View File

@@ -0,0 +1,79 @@
import { useQuery } from "@tanstack/react-query"
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Progress } from "@/components/ui/progress"
import config from "@/config"
interface VendorMetrics {
vendor: string
avg_lead_time: number
on_time_delivery_rate: number
avg_fill_rate: number
total_orders: number
active_orders: number
overdue_orders: number
}
export function VendorPerformance() {
const { data: vendors } = useQuery<VendorMetrics[]>({
queryKey: ["vendor-metrics"],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/vendor/performance`)
if (!response.ok) {
throw new Error("Failed to fetch vendor metrics")
}
return response.json()
},
})
// Sort vendors by on-time delivery rate
const sortedVendors = vendors
?.sort((a, b) => b.on_time_delivery_rate - a.on_time_delivery_rate)
return (
<>
<CardHeader>
<CardTitle className="text-lg font-medium">Top Vendor Performance</CardTitle>
</CardHeader>
<CardContent className="max-h-[400px] overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Vendor</TableHead>
<TableHead>On-Time</TableHead>
<TableHead className="text-right">Fill Rate</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedVendors?.map((vendor) => (
<TableRow key={vendor.vendor}>
<TableCell className="font-medium">{vendor.vendor}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Progress
value={vendor.on_time_delivery_rate}
className="h-2"
/>
<span className="w-10 text-sm">
{vendor.on_time_delivery_rate.toFixed(0)}%
</span>
</div>
</TableCell>
<TableCell className="text-right">
{vendor.avg_fill_rate.toFixed(0)}%
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</>
)
}

View File

@@ -29,19 +29,6 @@ import config from "../../config";
import { toast } from "sonner"; import { toast } from "sonner";
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
interface ImportProgress {
status: "running" | "error" | "complete" | "cancelled";
operation?: string;
current?: number;
total?: number;
rate?: number;
elapsed?: string;
remaining?: string;
progress?: string;
error?: string;
percentage?: string;
message?: string;
}
interface HistoryRecord { interface HistoryRecord {
id: number; id: number;
@@ -79,6 +66,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 +89,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 +187,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 +208,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 +258,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 +279,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 +361,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,68 +410,25 @@ 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);
} }
}; };
const refreshTableStatus = async () => {
try {
const response = await fetch(`${config.apiUrl}/csv/status/tables`);
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([]);
}
};
const refreshModuleStatus = async () => {
try {
const response = await fetch(`${config.apiUrl}/csv/status/modules`);
if (!response.ok) throw new Error('Failed to fetch module status');
const data = await response.json();
setModuleStatus(Array.isArray(data) ? data : []);
} catch (error) {
toast.error("Failed to refresh module status");
setModuleStatus([]);
}
};
const refreshImportHistory = async () => {
try {
const response = await fetch(`${config.apiUrl}/csv/history/import`);
if (!response.ok) throw new Error('Failed to fetch import history');
const data = await response.json();
setImportHistory(Array.isArray(data) ? data : []);
} catch (error) {
toast.error("Failed to refresh import history");
setImportHistory([]);
}
};
const refreshCalculateHistory = async () => {
try {
const response = await fetch(`${config.apiUrl}/csv/history/calculate`);
if (!response.ok) throw new Error('Failed to fetch calculate history');
const data = await response.json();
setCalculateHistory(Array.isArray(data) ? data : []);
} catch (error) {
toast.error("Failed to refresh calculate history");
setCalculateHistory([]);
}
};
const refreshAllData = async () => { const refreshAllData = async () => {
setIsLoading(true); setIsLoading(true);
@@ -528,12 +490,63 @@ 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) => (
<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">
{/* Full Update Card */} {/* Full Update Card */}
<Card> <Card className="relative">
<CardHeader> <CardHeader className="pb-12">
<CardTitle>Full Update</CardTitle> <CardTitle>Full Update</CardTitle>
<CardDescription> <CardDescription>
Import latest data and recalculate all metrics Import latest data and recalculate all metrics
@@ -542,7 +555,7 @@ export function DataManagement() {
<CardContent> <CardContent>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
className="flex-1" className="absolute bottom-4 right-[50%] translate-x-[50%] w-3/4"
onClick={handleFullUpdate} onClick={handleFullUpdate}
disabled={isUpdating || isResetting} disabled={isUpdating || isResetting}
> >
@@ -560,7 +573,7 @@ export function DataManagement() {
</Button> </Button>
{isUpdating && ( {isUpdating && (
<Button variant="destructive" onClick={handleCancel}> <Button variant="destructive" onClick={handleCancel} className="absolute top-4 right-4">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
)} )}
@@ -569,8 +582,8 @@ export function DataManagement() {
</Card> </Card>
{/* Full Reset Card */} {/* Full Reset Card */}
<Card> <Card className="relative">
<CardHeader> <CardHeader className="pb-12">
<CardTitle>Full Reset</CardTitle> <CardTitle>Full Reset</CardTitle>
<CardDescription> <CardDescription>
Reset database, reimport all data, and recalculate metrics Reset database, reimport all data, and recalculate metrics
@@ -582,7 +595,7 @@ export function DataManagement() {
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button <Button
variant="destructive" variant="destructive"
className="flex-1" className="absolute bottom-4 right-[50%] translate-x-[50%] w-3/4"
disabled={isUpdating || isResetting} disabled={isUpdating || isResetting}
> >
{isResetting ? ( {isResetting ? (
@@ -619,7 +632,7 @@ export function DataManagement() {
</AlertDialog> </AlertDialog>
{isResetting && ( {isResetting && (
<Button variant="destructive" onClick={handleCancel}> <Button variant="destructive" onClick={handleCancel} className="absolute top-4 right-4">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
)} )}
@@ -632,7 +645,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 +665,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 +702,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,15 +738,19 @@ export function DataManagement() {
</Card> </Card>
</div> </div>
{/* Add Table Counts section here */}
{renderTableCountsSection()}
</div>
{/* Recent Import History */} {/* Recent Import History */}
<Card> <Card className="!mt-0">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle>Recent Imports</CardTitle> <CardTitle>Recent Imports</CardTitle>
</CardHeader> </CardHeader>
<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 +861,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">

View File

@@ -43,6 +43,7 @@ export interface Product {
gross_profit?: string; // numeric(15,3) gross_profit?: string; // numeric(15,3)
gmroi?: string; // numeric(15,3) gmroi?: string; // numeric(15,3)
avg_lead_time_days?: string; // numeric(15,3) avg_lead_time_days?: string; // numeric(15,3)
first_received_date?: string;
last_received_date?: string; last_received_date?: string;
abc_class?: string; abc_class?: string;
stock_status?: string; stock_status?: string;

File diff suppressed because one or more lines are too long