archive products
This commit is contained in:
@@ -78,7 +78,7 @@ router.get('/', async (req, res, next) => {
|
|||||||
FROM products p
|
FROM products p
|
||||||
JOIN product_categories pc ON p.category_id = pc.id
|
JOIN product_categories pc ON p.category_id = pc.id
|
||||||
LEFT JOIN product_rates pr ON p.id = pr.product_id
|
LEFT JOIN product_rates pr ON p.id = pr.product_id
|
||||||
WHERE 1=1 ${whereClause}
|
WHERE 1=1 ${whereClause} AND COALESCE(p.archived,false) = false
|
||||||
GROUP BY p.id, pc.name
|
GROUP BY p.id, pc.name
|
||||||
ORDER BY p.name
|
ORDER BY p.name
|
||||||
`;
|
`;
|
||||||
@@ -93,7 +93,7 @@ router.get('/', async (req, res, next) => {
|
|||||||
LEFT JOIN products p ON up.product_id = p.id
|
LEFT JOIN products p ON up.product_id = p.id
|
||||||
LEFT JOIN product_categories pc ON p.category_id = pc.id
|
LEFT JOIN product_categories pc ON p.category_id = pc.id
|
||||||
LEFT JOIN product_categories upc ON up.category_id = upc.id
|
LEFT JOIN product_categories upc ON up.category_id = upc.id
|
||||||
WHERE up.user_id = $1
|
WHERE up.user_id = $1 AND COALESCE(up.archived,false) = false
|
||||||
ORDER BY COALESCE(up.custom_name, p.name)
|
ORDER BY COALESCE(up.custom_name, p.name)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -662,7 +662,9 @@ router.delete('/user/:id', validateParams(idParamSchema), async (req, res, next)
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (parseInt(usageCheck.rows[0].count) > 0) {
|
if (parseInt(usageCheck.rows[0].count) > 0) {
|
||||||
throw new AppError('Cannot delete product that has been used in applications', 400);
|
// Soft-archive instead of delete when used
|
||||||
|
await pool.query('UPDATE user_products SET archived = true, updated_at = CURRENT_TIMESTAMP WHERE id = $1', [userProductId]);
|
||||||
|
return res.json({ success: true, message: 'Custom product archived (in use by applications)' });
|
||||||
}
|
}
|
||||||
|
|
||||||
await pool.query('DELETE FROM user_products WHERE id = $1', [userProductId]);
|
await pool.query('DELETE FROM user_products WHERE id = $1', [userProductId]);
|
||||||
@@ -676,6 +678,30 @@ router.delete('/user/:id', validateParams(idParamSchema), async (req, res, next)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @route PUT /api/products/user/:id/archive
|
||||||
|
// @desc Archive user's custom product
|
||||||
|
// @access Private
|
||||||
|
router.put('/user/:id/archive', validateParams(idParamSchema), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userProductId = req.params.id;
|
||||||
|
const upd = await pool.query('UPDATE user_products SET archived = true, updated_at = CURRENT_TIMESTAMP WHERE id = $1 AND user_id = $2', [userProductId, req.user.id]);
|
||||||
|
if (upd.rowCount === 0) throw new AppError('User product not found', 404);
|
||||||
|
res.json({ success: true, message: 'Custom product archived' });
|
||||||
|
} catch (error) { next(error); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// @route PUT /api/products/user/:id/unarchive
|
||||||
|
// @desc Unarchive user's custom product
|
||||||
|
// @access Private
|
||||||
|
router.put('/user/:id/unarchive', validateParams(idParamSchema), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userProductId = req.params.id;
|
||||||
|
const upd = await pool.query('UPDATE user_products SET archived = false, updated_at = CURRENT_TIMESTAMP WHERE id = $1 AND user_id = $2', [userProductId, req.user.id]);
|
||||||
|
if (upd.rowCount === 0) throw new AppError('User product not found', 404);
|
||||||
|
res.json({ success: true, message: 'Custom product unarchived' });
|
||||||
|
} catch (error) { next(error); }
|
||||||
|
});
|
||||||
|
|
||||||
// @route GET /api/products/search
|
// @route GET /api/products/search
|
||||||
// @desc Search products by name or ingredients
|
// @desc Search products by name or ingredients
|
||||||
// @access Private
|
// @access Private
|
||||||
@@ -721,4 +747,4 @@ router.get('/search', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
12
database/migrations/V7__archive_products.sql
Normal file
12
database/migrations/V7__archive_products.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
-- Add archived flag to products and user_products
|
||||||
|
ALTER TABLE IF EXISTS products
|
||||||
|
ADD COLUMN IF NOT EXISTS archived BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS user_products
|
||||||
|
ADD COLUMN IF NOT EXISTS archived BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- Indexes for filtering
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_products_archived ON products (archived);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_products_archived ON user_products (archived);
|
||||||
|
|
||||||
@@ -113,7 +113,18 @@ const AdminProducts = () => {
|
|||||||
if (selectedProduct.isShared) {
|
if (selectedProduct.isShared) {
|
||||||
await adminAPI.deleteProduct(selectedProduct.id);
|
await adminAPI.deleteProduct(selectedProduct.id);
|
||||||
} else {
|
} else {
|
||||||
await productsAPI.deleteUserProduct(selectedProduct.id);
|
try {
|
||||||
|
await productsAPI.deleteUserProduct(selectedProduct.id);
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e?.response?.data?.message || '';
|
||||||
|
if (msg.includes('used in applications')) {
|
||||||
|
// Fallback to archive instead of delete
|
||||||
|
await productsAPI.archiveUserProduct(selectedProduct.id);
|
||||||
|
toast('Product archived instead of deleted (in use)');
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
toast.success('Product deleted successfully');
|
toast.success('Product deleted successfully');
|
||||||
setShowDeleteModal(false);
|
setShowDeleteModal(false);
|
||||||
@@ -121,7 +132,7 @@ const AdminProducts = () => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete product:', error);
|
console.error('Failed to delete product:', error);
|
||||||
toast.error('Failed to delete product');
|
toast.error(error?.response?.data?.message || 'Failed to delete product');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ export const productsAPI = {
|
|||||||
getUserProduct: (id) => apiClient.get(`/products/user/${id}`),
|
getUserProduct: (id) => apiClient.get(`/products/user/${id}`),
|
||||||
updateUserProduct: (id, productData) => apiClient.put(`/products/user/${id}`, productData),
|
updateUserProduct: (id, productData) => apiClient.put(`/products/user/${id}`, productData),
|
||||||
deleteUserProduct: (id) => apiClient.delete(`/products/user/${id}`),
|
deleteUserProduct: (id) => apiClient.delete(`/products/user/${id}`),
|
||||||
|
archiveUserProduct: (id) => apiClient.put(`/products/user/${id}/archive`),
|
||||||
|
unarchiveUserProduct: (id) => apiClient.put(`/products/user/${id}/unarchive`),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Applications API endpoints
|
// Applications API endpoints
|
||||||
|
|||||||
Reference in New Issue
Block a user