Add reset database script and frontend
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -49,5 +49,9 @@ dashboard-server/meta-server/._package-lock.json
|
|||||||
dashboard-server/meta-server/._services
|
dashboard-server/meta-server/._services
|
||||||
|
|
||||||
# CSV data files
|
# CSV data files
|
||||||
|
*.csv
|
||||||
csv/*
|
csv/*
|
||||||
|
csv/**/*
|
||||||
|
**/csv/*
|
||||||
|
**/csv/**/*
|
||||||
!csv/.gitkeep
|
!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
125
inventory-server/scripts/reset-db.js
Normal file
125
inventory-server/scripts/reset-db.js
Normal 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();
|
||||||
@@ -16,6 +16,7 @@ let importProgress = null;
|
|||||||
// SSE clients for progress updates
|
// SSE clients for progress updates
|
||||||
const updateClients = new Set();
|
const updateClients = new Set();
|
||||||
const importClients = new Set();
|
const importClients = new Set();
|
||||||
|
const resetClients = new Set();
|
||||||
|
|
||||||
// Helper to send progress to specific clients
|
// Helper to send progress to specific clients
|
||||||
function sendProgressToClients(clients, progress) {
|
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
|
// Debug endpoint to verify route registration
|
||||||
router.get('/test', (req, res) => {
|
router.get('/test', (req, res) => {
|
||||||
console.log('CSV test endpoint hit');
|
console.log('CSV test endpoint hit');
|
||||||
@@ -296,13 +318,26 @@ router.post('/cancel', (req, res) => {
|
|||||||
activeImport = null;
|
activeImport = null;
|
||||||
importProgress = 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 = {
|
const cancelMessage = {
|
||||||
status: 'complete',
|
status: 'complete',
|
||||||
operation: 'Operation cancelled'
|
operation: 'Operation cancelled'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
switch (operation) {
|
||||||
|
case 'update':
|
||||||
sendProgressToClients(updateClients, cancelMessage);
|
sendProgressToClients(updateClients, cancelMessage);
|
||||||
|
break;
|
||||||
|
case 'import':
|
||||||
sendProgressToClients(importClients, cancelMessage);
|
sendProgressToClients(importClients, cancelMessage);
|
||||||
|
break;
|
||||||
|
case 'reset':
|
||||||
|
sendProgressToClients(resetClients, cancelMessage);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} 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;
|
module.exports = router;
|
||||||
51
inventory/package-lock.json
generated
51
inventory/package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "inventory",
|
"name": "inventory",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||||
"@radix-ui/react-avatar": "^1.1.2",
|
"@radix-ui/react-avatar": "^1.1.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.2",
|
"@radix-ui/react-collapsible": "^1.1.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.4",
|
"@radix-ui/react-dialog": "^1.1.4",
|
||||||
@@ -29,11 +30,13 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
|
"next-themes": "^0.4.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^7.1.1",
|
"react-router-dom": "^7.1.1",
|
||||||
"recharts": "^2.15.0",
|
"recharts": "^2.15.0",
|
||||||
|
"sonner": "^1.7.1",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tanstack": "^1.0.0"
|
"tanstack": "^1.0.0"
|
||||||
@@ -1170,6 +1173,34 @@
|
|||||||
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
|
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
|
||||||
@@ -4761,6 +4792,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/node-domexception": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||||
@@ -5784,6 +5825,16 @@
|
|||||||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||||
"@radix-ui/react-avatar": "^1.1.2",
|
"@radix-ui/react-avatar": "^1.1.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.2",
|
"@radix-ui/react-collapsible": "^1.1.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.4",
|
"@radix-ui/react-dialog": "^1.1.4",
|
||||||
@@ -31,11 +32,13 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
|
"next-themes": "^0.4.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^7.1.1",
|
"react-router-dom": "^7.1.1",
|
||||||
"recharts": "^2.15.0",
|
"recharts": "^2.15.0",
|
||||||
|
"sonner": "^1.7.1",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tanstack": "^1.0.0"
|
"tanstack": "^1.0.0"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Import } from './pages/Import';
|
|||||||
import { Dashboard } from './pages/Dashboard';
|
import { Dashboard } from './pages/Dashboard';
|
||||||
import { Orders } from './pages/Orders';
|
import { Orders } from './pages/Orders';
|
||||||
import { Settings } from './pages/Settings';
|
import { Settings } from './pages/Settings';
|
||||||
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ function App() {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Router>
|
<Router>
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
|
<Toaster />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Dashboard />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
<Route path="/products" element={<Products />} />
|
<Route path="/products" element={<Products />} />
|
||||||
|
|||||||
139
inventory/src/components/ui/alert-dialog.tsx
Normal file
139
inventory/src/components/ui/alert-dialog.tsx
Normal 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,
|
||||||
|
}
|
||||||
29
inventory/src/components/ui/sonner.tsx
Normal file
29
inventory/src/components/ui/sonner.tsx
Normal 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 }
|
||||||
@@ -5,6 +5,17 @@ import { Progress } from "@/components/ui/progress";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
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 { Loader2, RefreshCw, Upload, X } from "lucide-react";
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
|
||||||
@@ -36,7 +47,10 @@ interface ImportLimits {
|
|||||||
export function Settings() {
|
export function Settings() {
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [isImporting, setIsImporting] = 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 [eventSource, setEventSource] = useState<EventSource | null>(null);
|
||||||
const [limits, setLimits] = useState<ImportLimits>({
|
const [limits, setLimits] = useState<ImportLimits>({
|
||||||
products: 0,
|
products: 0,
|
||||||
@@ -54,10 +68,14 @@ export function Settings() {
|
|||||||
}
|
}
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
setIsImporting(false);
|
setIsImporting(false);
|
||||||
setProgress(null);
|
setIsResetting(false);
|
||||||
|
setUpdateProgress(null);
|
||||||
|
setImportProgress(null);
|
||||||
|
setResetProgress(null);
|
||||||
|
|
||||||
// Fire and forget the cancel request
|
// Fire and forget the cancel request with the operation type
|
||||||
fetch(`${config.apiUrl}/csv/cancel`, {
|
const operation = isImporting ? 'import' : isUpdating ? 'update' : 'reset';
|
||||||
|
fetch(`${config.apiUrl}/csv/cancel?operation=${operation}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
@@ -65,7 +83,7 @@ export function Settings() {
|
|||||||
|
|
||||||
const handleUpdateCSV = async () => {
|
const handleUpdateCSV = async () => {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
setProgress({ status: 'running', operation: 'Starting CSV update' });
|
setUpdateProgress({ status: 'running', operation: 'Starting CSV update' });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Set up SSE connection for progress updates first
|
// Set up SSE connection for progress updates first
|
||||||
@@ -89,8 +107,8 @@ export function Settings() {
|
|||||||
setEventSource(null);
|
setEventSource(null);
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
// Only show connection error if we're not in a cancelled state
|
// Only show connection error if we're not in a cancelled state
|
||||||
if (!progress?.operation?.includes('cancelled')) {
|
if (!updateProgress?.operation?.includes('cancelled')) {
|
||||||
setProgress(prev => ({
|
setUpdateProgress(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
error: 'Connection to server lost'
|
error: 'Connection to server lost'
|
||||||
@@ -106,7 +124,7 @@ export function Settings() {
|
|||||||
(typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress)
|
(typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress)
|
||||||
: data;
|
: data;
|
||||||
|
|
||||||
setProgress(prev => {
|
setUpdateProgress(prev => {
|
||||||
// If we're getting a new operation, clear out old messages
|
// If we're getting a new operation, clear out old messages
|
||||||
if (progressData.operation && progressData.operation !== prev?.operation) {
|
if (progressData.operation && progressData.operation !== prev?.operation) {
|
||||||
return {
|
return {
|
||||||
@@ -146,7 +164,7 @@ export function Settings() {
|
|||||||
setIsImporting(false);
|
setIsImporting(false);
|
||||||
if (!progressData.operation?.includes('cancelled')) {
|
if (!progressData.operation?.includes('cancelled')) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setProgress(null);
|
setUpdateProgress(null);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
} else if (progressData.status === 'error' && !progressData.operation?.includes('cancelled')) {
|
} else if (progressData.status === 'error' && !progressData.operation?.includes('cancelled')) {
|
||||||
@@ -176,15 +194,15 @@ export function Settings() {
|
|||||||
}
|
}
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
// Don't show any errors if we're cleaning up
|
// Don't show any errors if we're cleaning up
|
||||||
if (progress?.status === 'running') {
|
if (updateProgress?.status === 'running') {
|
||||||
setProgress(null);
|
setUpdateProgress(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImportCSV = async () => {
|
const handleImportCSV = async () => {
|
||||||
setIsImporting(true);
|
setIsImporting(true);
|
||||||
setProgress({ status: 'running', operation: 'Starting import process' });
|
setImportProgress({ status: 'running', operation: 'Starting import process' });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Set up SSE connection for progress updates first
|
// Set up SSE connection for progress updates first
|
||||||
@@ -208,8 +226,8 @@ export function Settings() {
|
|||||||
setEventSource(null);
|
setEventSource(null);
|
||||||
setIsImporting(false);
|
setIsImporting(false);
|
||||||
// Only show connection error if we're not in a cancelled state
|
// Only show connection error if we're not in a cancelled state
|
||||||
if (!progress?.operation?.includes('cancelled') && progress?.status !== 'complete') {
|
if (!importProgress?.operation?.includes('cancelled') && importProgress?.status !== 'complete') {
|
||||||
setProgress(prev => ({
|
setImportProgress(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
error: 'Connection to server lost'
|
error: 'Connection to server lost'
|
||||||
@@ -225,7 +243,7 @@ export function Settings() {
|
|||||||
(typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress)
|
(typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress)
|
||||||
: data;
|
: data;
|
||||||
|
|
||||||
setProgress(prev => {
|
setImportProgress(prev => {
|
||||||
// If we're getting a new operation, clear out old messages
|
// If we're getting a new operation, clear out old messages
|
||||||
if (progressData.operation && progressData.operation !== prev?.operation) {
|
if (progressData.operation && progressData.operation !== prev?.operation) {
|
||||||
return {
|
return {
|
||||||
@@ -265,7 +283,7 @@ export function Settings() {
|
|||||||
setIsImporting(false);
|
setIsImporting(false);
|
||||||
if (!progressData.operation?.includes('cancelled')) {
|
if (!progressData.operation?.includes('cancelled')) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setProgress(null);
|
setImportProgress(null);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
} else if (progressData.status === 'error' && !progressData.operation?.includes('cancelled')) {
|
} else if (progressData.status === 'error' && !progressData.operation?.includes('cancelled')) {
|
||||||
@@ -299,8 +317,125 @@ export function Settings() {
|
|||||||
}
|
}
|
||||||
setIsImporting(false);
|
setIsImporting(false);
|
||||||
// Don't show any errors if we're cleaning up
|
// Don't show any errors if we're cleaning up
|
||||||
if (progress?.status === 'running') {
|
if (importProgress?.status === 'running') {
|
||||||
setProgress(null);
|
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]);
|
}, [eventSource]);
|
||||||
|
|
||||||
const renderProgress = () => {
|
const renderProgress = (progress: ImportProgress | null) => {
|
||||||
if (!progress) return null;
|
if (!progress) return null;
|
||||||
|
|
||||||
let percentage = progress.percentage ? parseFloat(progress.percentage) :
|
let percentage = progress.percentage ? parseFloat(progress.percentage) :
|
||||||
@@ -404,7 +539,7 @@ export function Settings() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isUpdating && renderProgress()}
|
{isUpdating && renderProgress(updateProgress)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -486,17 +621,56 @@ export function Settings() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isImporting && renderProgress()}
|
{isImporting && renderProgress(importProgress)}
|
||||||
|
|
||||||
|
|
||||||
{/* Show progress outside cards if neither operation is running but we have progress state */}
|
|
||||||
{!isUpdating && !isImporting && progress && (
|
|
||||||
<>
|
|
||||||
{renderProgress()}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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 && !isResetting && (updateProgress || importProgress || resetProgress) && (
|
||||||
|
<>
|
||||||
|
{renderProgress(updateProgress || importProgress || resetProgress)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -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"}
|
||||||
@@ -95,6 +95,10 @@ export default defineConfig(function (_a) {
|
|||||||
target: "https://inventory.kent.pw",
|
target: "https://inventory.kent.pw",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
|
ws: true,
|
||||||
|
xfwd: true,
|
||||||
|
cookieDomainRewrite: "",
|
||||||
|
withCredentials: true,
|
||||||
rewrite: function (path) { return path.replace(/^\/api/, "/api"); },
|
rewrite: function (path) { return path.replace(/^\/api/, "/api"); },
|
||||||
configure: function (proxy, _options) {
|
configure: function (proxy, _options) {
|
||||||
proxy.on("error", function (err, req, res) {
|
proxy.on("error", function (err, req, res) {
|
||||||
|
|||||||
Reference in New Issue
Block a user