diff --git a/backend/src/index.ts b/backend/src/index.ts index ad88225..cef4cf8 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -191,6 +191,10 @@ async function runMigrations() { IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'products' AND column_name = 'ai_extraction_disabled') THEN ALTER TABLE products ADD COLUMN ai_extraction_disabled BOOLEAN DEFAULT false; END IF; + -- Per-product checking pause flag + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'products' AND column_name = 'checking_paused') THEN + ALTER TABLE products ADD COLUMN checking_paused BOOLEAN DEFAULT false; + END IF; END $$; `); diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 96b4185..e51a7a6 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -344,6 +344,7 @@ export interface Product { notify_back_in_stock: boolean; ai_verification_disabled: boolean; ai_extraction_disabled: boolean; + checking_paused: boolean; created_at: Date; } @@ -587,8 +588,8 @@ export const productQueries = { findDueForRefresh: async (): Promise => { const result = await pool.query( `SELECT * FROM products - WHERE next_check_at IS NULL - OR next_check_at < CURRENT_TIMESTAMP` + WHERE (next_check_at IS NULL OR next_check_at < CURRENT_TIMESTAMP) + AND (checking_paused IS NULL OR checking_paused = false)` ); return result.rows; }, @@ -638,6 +639,15 @@ export const productQueries = { ); return result.rows[0]?.ai_extraction_disabled === true; }, + + bulkSetCheckingPaused: async (ids: number[], userId: number, paused: boolean): Promise => { + if (ids.length === 0) return 0; + const result = await pool.query( + `UPDATE products SET checking_paused = $1 WHERE id = ANY($2) AND user_id = $3`, + [paused, ids, userId] + ); + return result.rowCount || 0; + }, }; // Price History types and queries diff --git a/backend/src/routes/products.ts b/backend/src/routes/products.ts index f2d4d32..aadca67 100644 --- a/backend/src/routes/products.ts +++ b/backend/src/routes/products.ts @@ -268,4 +268,31 @@ router.delete('/:id', async (req: AuthRequest, res: Response) => { } }); +// Bulk pause/resume checking +router.post('/bulk/pause', async (req: AuthRequest, res: Response) => { + try { + const userId = req.userId!; + const { ids, paused } = req.body; + + if (!Array.isArray(ids) || ids.length === 0) { + res.status(400).json({ error: 'Product IDs array is required' }); + return; + } + + if (typeof paused !== 'boolean') { + res.status(400).json({ error: 'Paused status (boolean) is required' }); + return; + } + + const updated = await productQueries.bulkSetCheckingPaused(ids, userId, paused); + res.json({ + message: `${updated} product(s) ${paused ? 'paused' : 'resumed'}`, + updated + }); + } catch (error) { + console.error('Error bulk updating pause status:', error); + res.status(500).json({ error: 'Failed to update pause status' }); + } +}); + export default router; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 3278e1d..0eea30c 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -67,6 +67,7 @@ export interface Product { notify_back_in_stock: boolean; ai_verification_disabled: boolean; ai_extraction_disabled: boolean; + checking_paused: boolean; created_at: string; current_price: number | null; currency: string | null; @@ -138,6 +139,9 @@ export const productsApi = { }) => api.put(`/products/${id}`, data), delete: (id: number) => api.delete(`/products/${id}`), + + bulkPause: (ids: number[], paused: boolean) => + api.post<{ message: string; updated: number }>('/products/bulk/pause', { ids, paused }), }; // Prices API diff --git a/frontend/src/components/ProductCard.tsx b/frontend/src/components/ProductCard.tsx index 43c8b28..b532526 100644 --- a/frontend/src/components/ProductCard.tsx +++ b/frontend/src/components/ProductCard.tsx @@ -117,8 +117,10 @@ export default function ProductCard({ product, onDelete, onRefresh, isSelected, const isNearHistoricalLow = !isHistoricalLow && product.current_price && product.min_price && product.current_price <= product.min_price * 1.05; // Within 5% of low + const isPaused = product.checking_paused; + return ( -
+