Add import session save/restore
This commit is contained in:
29
inventory-server/migrations/001_create_import_sessions.sql
Normal file
29
inventory-server/migrations/001_create_import_sessions.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
-- Migration: Create import_sessions table
|
||||
-- Run this against your PostgreSQL database
|
||||
|
||||
CREATE TABLE IF NOT EXISTS import_sessions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
name VARCHAR(255), -- NULL for unnamed/autosave sessions
|
||||
current_step VARCHAR(50) NOT NULL, -- 'validation' | 'imageUpload'
|
||||
data JSONB NOT NULL, -- Product rows
|
||||
product_images JSONB, -- Image assignments
|
||||
global_selections JSONB, -- Supplier, company, line, subline
|
||||
validation_state JSONB, -- Errors, UPC status, generated item numbers
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Ensure only one unnamed session per user (autosave slot)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_unnamed_session_per_user
|
||||
ON import_sessions (user_id)
|
||||
WHERE name IS NULL;
|
||||
|
||||
-- Index for fast user lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_import_sessions_user_id
|
||||
ON import_sessions (user_id);
|
||||
|
||||
-- Add comment for documentation
|
||||
COMMENT ON TABLE import_sessions IS 'Stores in-progress product import sessions for users';
|
||||
COMMENT ON COLUMN import_sessions.name IS 'Session name - NULL indicates the single unnamed/autosave session per user';
|
||||
COMMENT ON COLUMN import_sessions.current_step IS 'Which step the user was on: validation or imageUpload';
|
||||
325
inventory-server/src/routes/import-sessions.js
Normal file
325
inventory-server/src/routes/import-sessions.js
Normal file
@@ -0,0 +1,325 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// Get all import sessions for a user (named + unnamed)
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { user_id } = req.query;
|
||||
|
||||
if (!user_id) {
|
||||
return res.status(400).json({ error: 'user_id query parameter is required' });
|
||||
}
|
||||
|
||||
const pool = req.app.locals.pool;
|
||||
if (!pool) {
|
||||
throw new Error('Database pool not initialized');
|
||||
}
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
name,
|
||||
current_step,
|
||||
jsonb_array_length(data) as row_count,
|
||||
global_selections,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM import_sessions
|
||||
WHERE user_id = $1
|
||||
ORDER BY
|
||||
CASE WHEN name IS NULL THEN 0 ELSE 1 END,
|
||||
updated_at DESC
|
||||
`, [user_id]);
|
||||
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching import sessions:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch import sessions',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get session by ID
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const pool = req.app.locals.pool;
|
||||
if (!pool) {
|
||||
throw new Error('Database pool not initialized');
|
||||
}
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT * FROM import_sessions
|
||||
WHERE id = $1
|
||||
`, [id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Import session not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Error fetching import session:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch import session',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Create new named session
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
user_id,
|
||||
name,
|
||||
current_step,
|
||||
data,
|
||||
product_images,
|
||||
global_selections,
|
||||
validation_state
|
||||
} = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!user_id) {
|
||||
return res.status(400).json({ error: 'user_id is required' });
|
||||
}
|
||||
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'name is required for creating a named session' });
|
||||
}
|
||||
if (!current_step) {
|
||||
return res.status(400).json({ error: 'current_step is required' });
|
||||
}
|
||||
if (!data || !Array.isArray(data)) {
|
||||
return res.status(400).json({ error: 'data must be an array' });
|
||||
}
|
||||
|
||||
const pool = req.app.locals.pool;
|
||||
if (!pool) {
|
||||
throw new Error('Database pool not initialized');
|
||||
}
|
||||
|
||||
const result = await pool.query(`
|
||||
INSERT INTO import_sessions (
|
||||
user_id,
|
||||
name,
|
||||
current_step,
|
||||
data,
|
||||
product_images,
|
||||
global_selections,
|
||||
validation_state
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *
|
||||
`, [
|
||||
user_id,
|
||||
name.trim(),
|
||||
current_step,
|
||||
JSON.stringify(data),
|
||||
product_images ? JSON.stringify(product_images) : null,
|
||||
global_selections ? JSON.stringify(global_selections) : null,
|
||||
validation_state ? JSON.stringify(validation_state) : null
|
||||
]);
|
||||
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Error creating import session:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to create import session',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update named session by ID
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
current_step,
|
||||
data,
|
||||
product_images,
|
||||
global_selections,
|
||||
validation_state
|
||||
} = req.body;
|
||||
|
||||
if (!current_step) {
|
||||
return res.status(400).json({ error: 'current_step is required' });
|
||||
}
|
||||
if (!data || !Array.isArray(data)) {
|
||||
return res.status(400).json({ error: 'data must be an array' });
|
||||
}
|
||||
|
||||
const pool = req.app.locals.pool;
|
||||
if (!pool) {
|
||||
throw new Error('Database pool not initialized');
|
||||
}
|
||||
|
||||
// Check if session exists
|
||||
const checkResult = await pool.query('SELECT * FROM import_sessions WHERE id = $1', [id]);
|
||||
if (checkResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Import session not found' });
|
||||
}
|
||||
|
||||
const result = await pool.query(`
|
||||
UPDATE import_sessions
|
||||
SET
|
||||
current_step = $1,
|
||||
data = $2,
|
||||
product_images = $3,
|
||||
global_selections = $4,
|
||||
validation_state = $5,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $6
|
||||
RETURNING *
|
||||
`, [
|
||||
current_step,
|
||||
JSON.stringify(data),
|
||||
product_images ? JSON.stringify(product_images) : null,
|
||||
global_selections ? JSON.stringify(global_selections) : null,
|
||||
validation_state ? JSON.stringify(validation_state) : null,
|
||||
id
|
||||
]);
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Error updating import session:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to update import session',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Autosave - upsert unnamed session for user
|
||||
router.put('/autosave', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
user_id,
|
||||
current_step,
|
||||
data,
|
||||
product_images,
|
||||
global_selections,
|
||||
validation_state
|
||||
} = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!user_id) {
|
||||
return res.status(400).json({ error: 'user_id is required' });
|
||||
}
|
||||
if (!current_step) {
|
||||
return res.status(400).json({ error: 'current_step is required' });
|
||||
}
|
||||
if (!data || !Array.isArray(data)) {
|
||||
return res.status(400).json({ error: 'data must be an array' });
|
||||
}
|
||||
|
||||
const pool = req.app.locals.pool;
|
||||
if (!pool) {
|
||||
throw new Error('Database pool not initialized');
|
||||
}
|
||||
|
||||
// Upsert: insert or update the unnamed session for this user
|
||||
const result = await pool.query(`
|
||||
INSERT INTO import_sessions (
|
||||
user_id,
|
||||
name,
|
||||
current_step,
|
||||
data,
|
||||
product_images,
|
||||
global_selections,
|
||||
validation_state
|
||||
) VALUES ($1, NULL, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (user_id) WHERE name IS NULL
|
||||
DO UPDATE SET
|
||||
current_step = EXCLUDED.current_step,
|
||||
data = EXCLUDED.data,
|
||||
product_images = EXCLUDED.product_images,
|
||||
global_selections = EXCLUDED.global_selections,
|
||||
validation_state = EXCLUDED.validation_state,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING *
|
||||
`, [
|
||||
user_id,
|
||||
current_step,
|
||||
JSON.stringify(data),
|
||||
product_images ? JSON.stringify(product_images) : null,
|
||||
global_selections ? JSON.stringify(global_selections) : null,
|
||||
validation_state ? JSON.stringify(validation_state) : null
|
||||
]);
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Error autosaving import session:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to autosave import session',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Delete session by ID
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const pool = req.app.locals.pool;
|
||||
if (!pool) {
|
||||
throw new Error('Database pool not initialized');
|
||||
}
|
||||
|
||||
const result = await pool.query('DELETE FROM import_sessions WHERE id = $1 RETURNING *', [id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Import session not found' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Import session deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting import session:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to delete import session',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Delete unnamed session for user (clear autosave)
|
||||
router.delete('/autosave/:user_id', async (req, res) => {
|
||||
try {
|
||||
const { user_id } = req.params;
|
||||
const pool = req.app.locals.pool;
|
||||
if (!pool) {
|
||||
throw new Error('Database pool not initialized');
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
'DELETE FROM import_sessions WHERE user_id = $1 AND name IS NULL RETURNING *',
|
||||
[user_id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'No autosave session found for user' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Autosave session deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting autosave session:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to delete autosave session',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
router.use((err, req, res, next) => {
|
||||
console.error('Import sessions route error:', err);
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
details: err.message
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -23,6 +23,7 @@ const categoriesAggregateRouter = require('./routes/categoriesAggregate');
|
||||
const vendorsAggregateRouter = require('./routes/vendorsAggregate');
|
||||
const brandsAggregateRouter = require('./routes/brandsAggregate');
|
||||
const htsLookupRouter = require('./routes/hts-lookup');
|
||||
const importSessionsRouter = require('./routes/import-sessions');
|
||||
|
||||
// Get the absolute path to the .env file
|
||||
const envPath = '/var/www/html/inventory/.env';
|
||||
@@ -130,6 +131,7 @@ async function startServer() {
|
||||
app.use('/api/ai-prompts', aiPromptsRouter);
|
||||
app.use('/api/reusable-images', reusableImagesRouter);
|
||||
app.use('/api/hts-lookup', htsLookupRouter);
|
||||
app.use('/api/import-sessions', importSessionsRouter);
|
||||
|
||||
// Basic health check route
|
||||
app.get('/health', (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user