diff --git a/inventory-server/src/routes/newsletter.js b/inventory-server/src/routes/newsletter.js index 6d526c4..faea0b9 100644 --- a/inventory-server/src/routes/newsletter.js +++ b/inventory-server/src/routes/newsletter.js @@ -31,13 +31,14 @@ const CATEGORY_FILTERS = { back_in_stock: "AND is_back_in_stock = true", bestsellers: "AND shop_score > 20 AND COALESCE(current_stock, 0) > 0 AND COALESCE(sales_30d, 0) > 0", never_featured: "AND times_featured IS NULL AND line_last_featured_at IS NULL", + no_interest: "AND COALESCE(total_sold, 0) = 0 AND COALESCE(current_stock, 0) > 0 AND COALESCE(date_online, product_created_at) <= CURRENT_DATE - INTERVAL '30 days'", }; function buildScoredCTE({ forCount = false } = {}) { // forCount=true returns minimal columns for COUNT(*) const selectColumns = forCount ? ` p.pid, - p.created_at, + p.created_at as product_created_at, p.date_online, p.shop_score, p.preorder_count, diff --git a/inventory/package-lock.json b/inventory/package-lock.json index 86b39c6..d574bf0 100644 --- a/inventory/package-lock.json +++ b/inventory/package-lock.json @@ -4313,9 +4313,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001739", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", - "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", "dev": true, "funding": [ { diff --git a/inventory/src/components/product-editor/ProductEditForm.tsx b/inventory/src/components/product-editor/ProductEditForm.tsx index 73a4f96..4936c3c 100644 --- a/inventory/src/components/product-editor/ProductEditForm.tsx +++ b/inventory/src/components/product-editor/ProductEditForm.tsx @@ -382,10 +382,12 @@ export function ProductEditForm({ originalImagesRef.current = [...productImages]; reset(data); } else { - toast.error(result.message ?? "Failed to update product"); - if (result.error) { - console.error("Edit error details:", result.error); - } + const errorDetail = Array.isArray(result.error) + ? result.error.filter((e) => e !== "Errors").join("; ") + : typeof result.error === "string" + ? result.error + : null; + toast.error(errorDetail || result.message || "Failed to update product"); } } catch (err) { toast.error( diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx b/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx index 0fefe33..9e43396 100644 --- a/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx +++ b/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx @@ -45,7 +45,7 @@ interface Props { data: Product[]; file: File; onBack?: () => void; - onSubmit: (data: Product[], file: File, options: SubmitOptions) => void | Promise; + onSubmit: (data: Product[], file: File, options: SubmitOptions) => void | Promise; } export const ImageUploadStep = ({ @@ -228,14 +228,16 @@ export const ImageUploadStep = ({ showNewProduct, }; - await onSubmit(updatedData, file, submitOptions); + const success = await onSubmit(updatedData, file, submitOptions); - // Delete the import session on successful submit - try { - await deleteImportSession(); - } catch (err) { - // Non-critical - log but don't fail the submission - console.warn('Failed to delete import session:', err); + // Only delete the import session after a successful submit response + if (success) { + try { + await deleteImportSession(); + } catch (err) { + // Non-critical - log but don't fail the submission + console.warn('Failed to delete import session:', err); + } } } catch (error) { console.error('Submit error:', error); diff --git a/inventory/src/components/product-import/steps/UploadFlow.tsx b/inventory/src/components/product-import/steps/UploadFlow.tsx index 21d0316..f0b2251 100644 --- a/inventory/src/components/product-import/steps/UploadFlow.tsx +++ b/inventory/src/components/product-import/steps/UploadFlow.tsx @@ -337,7 +337,7 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => { invalidData: [] as Data[], all: data as Data[] }; - onSubmit(result, file, options); + return onSubmit(result, file, options); }} /> ) diff --git a/inventory/src/components/product-import/steps/ValidationStep/components/cells/InputCell.tsx b/inventory/src/components/product-import/steps/ValidationStep/components/cells/InputCell.tsx index f8ede1b..88346b0 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/components/cells/InputCell.tsx +++ b/inventory/src/components/product-import/steps/ValidationStep/components/cells/InputCell.tsx @@ -113,6 +113,14 @@ const InputCellComponent = ({ setIsFocused(true); }, [cellPopoverClosedAt]); + // Handle Enter key to blur the field (useful for barcode scanners that send Enter after scan) + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + inputRef.current?.blur(); + } + }, []); + // Update store only on blur - this is when validation runs too // IMPORTANT: We store FULL precision for price fields to allow accurate calculations // The display formatting happens separately via displayValue @@ -151,6 +159,7 @@ const InputCellComponent = ({ onChange={handleChange} onFocus={handleFocus} onBlur={handleBlur} + onKeyDown={handleKeyDown} disabled={isValidating} className={cn( 'h-8 text-sm', diff --git a/inventory/src/components/product-import/steps/ValidationStep/components/cells/MultilineInput.tsx b/inventory/src/components/product-import/steps/ValidationStep/components/cells/MultilineInput.tsx index dee542f..784df05 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/components/cells/MultilineInput.tsx +++ b/inventory/src/components/product-import/steps/ValidationStep/components/cells/MultilineInput.tsx @@ -331,12 +331,16 @@ const MultilineInputComponent = ({ onBlur(editedSuggestion); onDismissAiSuggestion?.(); // Clear the suggestion after accepting setAiSuggestionExpanded(false); + intentionalCloseRef.current = true; + setPopoverOpen(false); }, [editedSuggestion, onBlur, onDismissAiSuggestion]); // Handle dismissing the AI suggestion const handleDismissSuggestion = useCallback(() => { onDismissAiSuggestion?.(); setAiSuggestionExpanded(false); + intentionalCloseRef.current = true; + setPopoverOpen(false); }, [onDismissAiSuggestion]); // Calculate display value @@ -446,7 +450,7 @@ const MultilineInputComponent = ({ {/* Main textarea */} -
+
{/* Product name - shown inline on mobile, in measured spacer on desktop */} {hasAiSuggestion && productName && ( diff --git a/inventory/src/pages/Import.tsx b/inventory/src/pages/Import.tsx index c897055..504a934 100644 --- a/inventory/src/pages/Import.tsx +++ b/inventory/src/pages/Import.tsx @@ -520,7 +520,7 @@ export function Import() { return stringValue; }; - const handleData = async (data: ImportResult, _file: File, submitOptions: SubmitOptions) => { + const handleData = async (data: ImportResult, _file: File, submitOptions: SubmitOptions): Promise => { try { const rows = ((data.all?.length ? data.all : data.validData) ?? []) as Data[]; const formattedRows: NormalizedProduct[] = rows.map((row) => { @@ -579,7 +579,7 @@ export function Import() { setStartFromScratch(false); toast.success(`[DEBUG] Skipped API submission for ${formattedRows.length} product(s)`); - return; + return true; } const response = await submitNewProducts({ @@ -638,11 +638,14 @@ export function Import() { } else { toast.error(resolvedFailureMessage ?? defaultFailureMessage); } + + return isSuccess; } catch (error) { console.error("Import error:", error); const errorMessage = error instanceof Error ? error.message : "Failed to import data. Please try again."; toast.error(errorMessage); + return false; } }; @@ -924,7 +927,7 @@ export function Import() { Success - All products created successfully. + All products created successfully. Please note that images may take a few minutes to finish uploading before they'll be visible in backend. ) : createdProducts.length > 0 && erroredProducts.length > 0 ? ( diff --git a/inventory/src/pages/Newsletter.tsx b/inventory/src/pages/Newsletter.tsx index b2c10f6..8e46f7f 100644 --- a/inventory/src/pages/Newsletter.tsx +++ b/inventory/src/pages/Newsletter.tsx @@ -14,6 +14,7 @@ const CATEGORIES = [ { value: "clearance", label: "Clearance" }, { value: "daily_deals", label: "Daily Deals" }, { value: "never_featured", label: "Never Featured" }, + { value: "no_interest", label: "No Interest" }, ] export function Newsletter() {