Add reset database script and frontend

This commit is contained in:
2025-01-10 15:32:32 -05:00
parent f093446f83
commit 4ae012d9dd
15 changed files with 684 additions and 1346716 deletions

4
.gitignore vendored
View File

@@ -49,5 +49,9 @@ dashboard-server/meta-server/._package-lock.json
dashboard-server/meta-server/._services
# CSV data files
*.csv
csv/*
csv/**/*
**/csv/*
**/csv/**/*
!csv/.gitkeep

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
const mysql = require('mysql2/promise');
const path = require('path');
const dotenv = require('dotenv');
const { spawn } = require('child_process');
dotenv.config({ path: path.join(__dirname, '../.env') });
const dbConfig = {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
multipleStatements: true
};
// Helper function to output progress in JSON format
function outputProgress(data) {
if (!data.status) {
data = {
status: 'running',
...data
};
}
console.log(JSON.stringify(data));
}
async function resetDatabase() {
outputProgress({
operation: 'Starting database reset',
message: 'Connecting to database...'
});
const connection = await mysql.createConnection(dbConfig);
try {
// Get list of all tables
outputProgress({
operation: 'Getting table list',
message: 'Retrieving all table names...'
});
const [tables] = await connection.query(
'SELECT table_name FROM information_schema.tables WHERE table_schema = ?',
[dbConfig.database]
);
if (tables.length === 0) {
outputProgress({
operation: 'No tables found',
message: 'Database is already empty'
});
} else {
// Disable foreign key checks to allow dropping tables with dependencies
await connection.query('SET FOREIGN_KEY_CHECKS = 0');
// Drop each table
for (let i = 0; i < tables.length; i++) {
const tableName = tables[i].TABLE_NAME;
outputProgress({
operation: 'Dropping tables',
message: `Dropping table: ${tableName}`,
current: i + 1,
total: tables.length,
percentage: (((i + 1) / tables.length) * 100).toFixed(1)
});
await connection.query(`DROP TABLE IF EXISTS \`${tableName}\``);
}
// Re-enable foreign key checks
await connection.query('SET FOREIGN_KEY_CHECKS = 1');
}
// Run setup-db.js
outputProgress({
operation: 'Running database setup',
message: 'Creating new tables...'
});
const setupScript = path.join(__dirname, 'setup-db.js');
const setupProcess = spawn('node', [setupScript]);
setupProcess.stdout.on('data', (data) => {
const output = data.toString().trim();
outputProgress({
operation: 'Database setup',
message: output
});
});
setupProcess.stderr.on('data', (data) => {
const error = data.toString().trim();
outputProgress({
status: 'error',
error
});
});
await new Promise((resolve, reject) => {
setupProcess.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Setup process exited with code ${code}`));
}
});
});
outputProgress({
status: 'complete',
operation: 'Database reset complete',
message: 'Database has been reset and tables recreated'
});
} catch (error) {
outputProgress({
status: 'error',
error: error.message
});
process.exit(1);
} finally {
await connection.end();
}
}
// Run the reset
resetDatabase();

View File

@@ -16,6 +16,7 @@ let importProgress = null;
// SSE clients for progress updates
const updateClients = new Set();
const importClients = new Set();
const resetClients = new Set();
// Helper to send progress to specific clients
function sendProgressToClients(clients, progress) {
@@ -85,6 +86,27 @@ router.get('/import/progress', (req, res) => {
});
});
router.get('/reset/progress', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': req.headers.origin || '*',
'Access-Control-Allow-Credentials': 'true'
});
// Send an initial message to test the connection
res.write('data: {"status":"running","operation":"Initializing connection..."}\n\n');
// Add this client to the reset set
resetClients.add(res);
// Remove client when connection closes
req.on('close', () => {
resetClients.delete(res);
});
});
// Debug endpoint to verify route registration
router.get('/test', (req, res) => {
console.log('CSV test endpoint hit');
@@ -296,13 +318,26 @@ router.post('/cancel', (req, res) => {
activeImport = null;
importProgress = null;
// Notify all clients
// Get the operation type from the request
const { operation } = req.query;
// Send cancel message only to the appropriate client set
const cancelMessage = {
status: 'complete',
operation: 'Operation cancelled'
};
sendProgressToClients(updateClients, cancelMessage);
sendProgressToClients(importClients, cancelMessage);
switch (operation) {
case 'update':
sendProgressToClients(updateClients, cancelMessage);
break;
case 'import':
sendProgressToClients(importClients, cancelMessage);
break;
case 'reset':
sendProgressToClients(resetClients, cancelMessage);
break;
}
res.json({ success: true });
} catch (error) {
@@ -313,4 +348,90 @@ router.post('/cancel', (req, res) => {
}
});
// Route to reset database
router.post('/reset', async (req, res) => {
if (activeImport) {
return res.status(409).json({ error: 'Import already in progress' });
}
try {
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'reset-db.js');
if (!require('fs').existsSync(scriptPath)) {
return res.status(500).json({ error: 'Reset script not found' });
}
activeImport = spawn('node', [scriptPath]);
activeImport.stdout.on('data', (data) => {
const output = data.toString().trim();
try {
// Try to parse as JSON
const jsonData = JSON.parse(output);
sendProgressToClients(resetClients, {
status: 'running',
...jsonData
});
} catch (e) {
// If not JSON, send as plain progress
sendProgressToClients(resetClients, {
status: 'running',
progress: output
});
}
});
activeImport.stderr.on('data', (data) => {
const error = data.toString().trim();
try {
// Try to parse as JSON
const jsonData = JSON.parse(error);
sendProgressToClients(resetClients, {
status: 'error',
...jsonData
});
} catch {
sendProgressToClients(resetClients, {
status: 'error',
error
});
}
});
await new Promise((resolve, reject) => {
activeImport.on('close', (code) => {
// Don't treat cancellation (code 143/SIGTERM) as an error
if (code === 0 || code === 143) {
sendProgressToClients(resetClients, {
status: 'complete',
operation: code === 143 ? 'Operation cancelled' : 'Reset complete'
});
resolve();
} else {
const errorMsg = `Reset process exited with code ${code}`;
sendProgressToClients(resetClients, {
status: 'error',
error: errorMsg
});
reject(new Error(errorMsg));
}
activeImport = null;
importProgress = null;
});
});
res.json({ success: true });
} catch (error) {
console.error('Error resetting database:', error);
activeImport = null;
importProgress = null;
sendProgressToClients(resetClients, {
status: 'error',
error: error.message
});
res.status(500).json({ error: 'Failed to reset database', details: error.message });
}
});
module.exports = router;

View File

@@ -8,6 +8,7 @@
"name": "inventory",
"version": "0.0.0",
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4",
@@ -29,11 +30,13 @@
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"lucide-react": "^0.469.0",
"next-themes": "^0.4.4",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.1.1",
"recharts": "^2.15.0",
"sonner": "^1.7.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"tanstack": "^1.0.0"
@@ -1170,6 +1173,34 @@
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.4.tgz",
"integrity": "sha512-A6Kh23qZDLy3PSU4bh2UJZznOrUdHImIXqF8YtUa6CN73f8EOO9XlXSCd9IHyPvIquTaa/kwaSWzZTtUvgXVGw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dialog": "1.1.4",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
@@ -4761,6 +4792,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/next-themes": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz",
"integrity": "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -5784,6 +5825,16 @@
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"license": "MIT"
},
"node_modules/sonner": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.1.tgz",
"integrity": "sha512-b6LHBfH32SoVasRFECrdY8p8s7hXPDn3OHUFbZZbiB1ctLS9Gdh6rpX2dVrpQA0kiL5jcRzDDldwwLkSKk3+QQ==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",

View File

@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4",
@@ -31,11 +32,13 @@
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"lucide-react": "^0.469.0",
"next-themes": "^0.4.4",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.1.1",
"recharts": "^2.15.0",
"sonner": "^1.7.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"tanstack": "^1.0.0"

View File

@@ -6,6 +6,7 @@ import { Import } from './pages/Import';
import { Dashboard } from './pages/Dashboard';
import { Orders } from './pages/Orders';
import { Settings } from './pages/Settings';
import { Toaster } from '@/components/ui/sonner';
const queryClient = new QueryClient();
@@ -14,6 +15,7 @@ function App() {
<QueryClientProvider client={queryClient}>
<Router>
<MainLayout>
<Toaster />
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/products" element={<Products />} />

View File

@@ -0,0 +1,139 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@@ -0,0 +1,29 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

View File

@@ -5,6 +5,17 @@ import { Progress } from "@/components/ui/progress";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Loader2, RefreshCw, Upload, X } from "lucide-react";
import config from '../config';
@@ -36,7 +47,10 @@ interface ImportLimits {
export function Settings() {
const [isUpdating, setIsUpdating] = useState(false);
const [isImporting, setIsImporting] = useState(false);
const [progress, setProgress] = useState<ImportProgress | null>(null);
const [isResetting, setIsResetting] = useState(false);
const [updateProgress, setUpdateProgress] = useState<ImportProgress | null>(null);
const [importProgress, setImportProgress] = useState<ImportProgress | null>(null);
const [resetProgress, setResetProgress] = useState<ImportProgress | null>(null);
const [eventSource, setEventSource] = useState<EventSource | null>(null);
const [limits, setLimits] = useState<ImportLimits>({
products: 0,
@@ -54,10 +68,14 @@ export function Settings() {
}
setIsUpdating(false);
setIsImporting(false);
setProgress(null);
setIsResetting(false);
setUpdateProgress(null);
setImportProgress(null);
setResetProgress(null);
// Fire and forget the cancel request
fetch(`${config.apiUrl}/csv/cancel`, {
// Fire and forget the cancel request with the operation type
const operation = isImporting ? 'import' : isUpdating ? 'update' : 'reset';
fetch(`${config.apiUrl}/csv/cancel?operation=${operation}`, {
method: 'POST',
credentials: 'include'
}).catch(() => {});
@@ -65,7 +83,7 @@ export function Settings() {
const handleUpdateCSV = async () => {
setIsUpdating(true);
setProgress({ status: 'running', operation: 'Starting CSV update' });
setUpdateProgress({ status: 'running', operation: 'Starting CSV update' });
try {
// Set up SSE connection for progress updates first
@@ -89,8 +107,8 @@ export function Settings() {
setEventSource(null);
setIsUpdating(false);
// Only show connection error if we're not in a cancelled state
if (!progress?.operation?.includes('cancelled')) {
setProgress(prev => ({
if (!updateProgress?.operation?.includes('cancelled')) {
setUpdateProgress(prev => ({
...prev,
status: 'error',
error: 'Connection to server lost'
@@ -106,7 +124,7 @@ export function Settings() {
(typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress)
: data;
setProgress(prev => {
setUpdateProgress(prev => {
// If we're getting a new operation, clear out old messages
if (progressData.operation && progressData.operation !== prev?.operation) {
return {
@@ -146,7 +164,7 @@ export function Settings() {
setIsImporting(false);
if (!progressData.operation?.includes('cancelled')) {
setTimeout(() => {
setProgress(null);
setUpdateProgress(null);
}, 1000);
}
} else if (progressData.status === 'error' && !progressData.operation?.includes('cancelled')) {
@@ -176,15 +194,15 @@ export function Settings() {
}
setIsUpdating(false);
// Don't show any errors if we're cleaning up
if (progress?.status === 'running') {
setProgress(null);
if (updateProgress?.status === 'running') {
setUpdateProgress(null);
}
}
};
const handleImportCSV = async () => {
setIsImporting(true);
setProgress({ status: 'running', operation: 'Starting import process' });
setImportProgress({ status: 'running', operation: 'Starting import process' });
try {
// Set up SSE connection for progress updates first
@@ -208,8 +226,8 @@ export function Settings() {
setEventSource(null);
setIsImporting(false);
// Only show connection error if we're not in a cancelled state
if (!progress?.operation?.includes('cancelled') && progress?.status !== 'complete') {
setProgress(prev => ({
if (!importProgress?.operation?.includes('cancelled') && importProgress?.status !== 'complete') {
setImportProgress(prev => ({
...prev,
status: 'error',
error: 'Connection to server lost'
@@ -225,7 +243,7 @@ export function Settings() {
(typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress)
: data;
setProgress(prev => {
setImportProgress(prev => {
// If we're getting a new operation, clear out old messages
if (progressData.operation && progressData.operation !== prev?.operation) {
return {
@@ -265,7 +283,7 @@ export function Settings() {
setIsImporting(false);
if (!progressData.operation?.includes('cancelled')) {
setTimeout(() => {
setProgress(null);
setImportProgress(null);
}, 1000);
}
} else if (progressData.status === 'error' && !progressData.operation?.includes('cancelled')) {
@@ -299,8 +317,125 @@ export function Settings() {
}
setIsImporting(false);
// Don't show any errors if we're cleaning up
if (progress?.status === 'running') {
setProgress(null);
if (importProgress?.status === 'running') {
setImportProgress(null);
}
}
};
const handleResetDB = async () => {
setIsResetting(true);
setResetProgress({ status: 'running', operation: 'Starting database reset' });
try {
// Set up SSE connection for progress updates first
if (eventSource) {
eventSource.close();
setEventSource(null);
}
// Set up SSE connection for progress updates
const source = new EventSource(`${config.apiUrl}/csv/reset/progress`, {
withCredentials: true
});
setEventSource(source);
// Add event listeners for all SSE events
source.onopen = () => {};
source.onerror = () => {
if (source.readyState === EventSource.CLOSED) {
source.close();
setEventSource(null);
setIsResetting(false);
// Only show connection error if we're not in a cancelled state
if (!resetProgress?.operation?.includes('cancelled')) {
setResetProgress(prev => ({
...prev,
status: 'error',
error: 'Connection to server lost'
}));
}
}
};
source.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
let progressData = data.progress ?
(typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress)
: data;
setResetProgress(prev => {
// If we're getting a new operation, clear out old messages
if (progressData.operation && progressData.operation !== prev?.operation) {
return {
status: progressData.status || 'running',
operation: progressData.operation,
current: progressData.current !== undefined ? Number(progressData.current) : undefined,
total: progressData.total !== undefined ? Number(progressData.total) : undefined,
rate: progressData.rate !== undefined ? Number(progressData.rate) : undefined,
percentage: progressData.percentage,
elapsed: progressData.elapsed,
remaining: progressData.remaining,
message: progressData.message,
error: progressData.error
};
}
// Otherwise update existing state
return {
...prev,
status: progressData.status || prev?.status || 'running',
operation: progressData.operation || prev?.operation,
current: progressData.current !== undefined ? Number(progressData.current) : prev?.current,
total: progressData.total !== undefined ? Number(progressData.total) : prev?.total,
rate: progressData.rate !== undefined ? Number(progressData.rate) : prev?.rate,
percentage: progressData.percentage !== undefined ? progressData.percentage : prev?.percentage,
elapsed: progressData.elapsed || prev?.elapsed,
remaining: progressData.remaining || prev?.remaining,
error: progressData.error || prev?.error,
message: progressData.message || prev?.message
};
});
if (progressData.status === 'complete') {
source.close();
setEventSource(null);
setIsResetting(false);
if (!progressData.operation?.includes('cancelled')) {
setTimeout(() => {
setResetProgress(null);
}, 1000);
}
} else if (progressData.status === 'error' && !progressData.operation?.includes('cancelled')) {
source.close();
setEventSource(null);
setIsResetting(false);
}
} catch (error) {
// Silently handle parsing errors
}
};
// Now make the reset request
const response = await fetch(`${config.apiUrl}/csv/reset`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to reset database');
}
} catch (error) {
if (eventSource) {
eventSource.close();
setEventSource(null);
}
setIsResetting(false);
// Don't show any errors if we're cleaning up
if (resetProgress?.status === 'running') {
setResetProgress(null);
}
}
};
@@ -314,7 +449,7 @@ export function Settings() {
};
}, [eventSource]);
const renderProgress = () => {
const renderProgress = (progress: ImportProgress | null) => {
if (!progress) return null;
let percentage = progress.percentage ? parseFloat(progress.percentage) :
@@ -404,7 +539,7 @@ export function Settings() {
)}
</div>
{isUpdating && renderProgress()}
{isUpdating && renderProgress(updateProgress)}
</CardContent>
</Card>
@@ -486,17 +621,56 @@ export function Settings() {
)}
</div>
{isImporting && renderProgress()}
{isImporting && renderProgress(importProgress)}
</CardContent>
</Card>
{/* Reset Database Card */}
<Card>
<CardHeader>
<CardTitle>Reset Database</CardTitle>
<CardDescription>Drop all tables and recreate the database schema. This will delete ALL data.</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-2">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="destructive"
className="flex-1"
disabled={isUpdating || isImporting}
>
Reset Database
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete all data
from the database and reset it to its initial state.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleResetDB}>
Reset Database
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
{isResetting && renderProgress(resetProgress)}
</CardContent>
</Card>
{/* Show progress outside cards if neither operation is running but we have progress state */}
{!isUpdating && !isImporting && progress && (
{!isUpdating && !isImporting && !isResetting && (updateProgress || importProgress || resetProgress) && (
<>
{renderProgress()}
{renderProgress(updateProgress || importProgress || resetProgress)}
</>
)}
</CardContent>
</Card>
</div>
</div>
);

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/dashboard/inventorystats.tsx","./src/components/dashboard/overview.tsx","./src/components/dashboard/recentsales.tsx","./src/components/dashboard/salesbycategory.tsx","./src/components/dashboard/trendingproducts.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/products/producteditdialog.tsx","./src/components/products/productfilters.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/hooks/use-mobile.tsx","./src/lib/utils.ts","./src/pages/dashboard.tsx","./src/pages/import.tsx","./src/pages/orders.tsx","./src/pages/products.tsx","./src/pages/settings.tsx"],"version":"5.6.3"}
{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/dashboard/inventorystats.tsx","./src/components/dashboard/overview.tsx","./src/components/dashboard/recentsales.tsx","./src/components/dashboard/salesbycategory.tsx","./src/components/dashboard/trendingproducts.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/products/producteditdialog.tsx","./src/components/products/productfilters.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/hooks/use-mobile.tsx","./src/lib/utils.ts","./src/pages/dashboard.tsx","./src/pages/import.tsx","./src/pages/orders.tsx","./src/pages/products.tsx","./src/pages/settings.tsx"],"version":"5.6.3"}

View File

@@ -95,6 +95,10 @@ export default defineConfig(function (_a) {
target: "https://inventory.kent.pw",
changeOrigin: true,
secure: false,
ws: true,
xfwd: true,
cookieDomainRewrite: "",
withCredentials: true,
rewrite: function (path) { return path.replace(/^\/api/, "/api"); },
configure: function (proxy, _options) {
proxy.on("error", function (err, req, res) {