Newsletter recommendations tweaks, add enter to blur on validation table
This commit is contained in:
@@ -31,13 +31,14 @@ const CATEGORY_FILTERS = {
|
|||||||
back_in_stock: "AND is_back_in_stock = true",
|
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",
|
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",
|
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 } = {}) {
|
function buildScoredCTE({ forCount = false } = {}) {
|
||||||
// forCount=true returns minimal columns for COUNT(*)
|
// forCount=true returns minimal columns for COUNT(*)
|
||||||
const selectColumns = forCount ? `
|
const selectColumns = forCount ? `
|
||||||
p.pid,
|
p.pid,
|
||||||
p.created_at,
|
p.created_at as product_created_at,
|
||||||
p.date_online,
|
p.date_online,
|
||||||
p.shop_score,
|
p.shop_score,
|
||||||
p.preorder_count,
|
p.preorder_count,
|
||||||
|
|||||||
6
inventory/package-lock.json
generated
6
inventory/package-lock.json
generated
@@ -4313,9 +4313,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001739",
|
"version": "1.0.30001766",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
|
||||||
"integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==",
|
"integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -382,10 +382,12 @@ export function ProductEditForm({
|
|||||||
originalImagesRef.current = [...productImages];
|
originalImagesRef.current = [...productImages];
|
||||||
reset(data);
|
reset(data);
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message ?? "Failed to update product");
|
const errorDetail = Array.isArray(result.error)
|
||||||
if (result.error) {
|
? result.error.filter((e) => e !== "Errors").join("; ")
|
||||||
console.error("Edit error details:", result.error);
|
: typeof result.error === "string"
|
||||||
}
|
? result.error
|
||||||
|
: null;
|
||||||
|
toast.error(errorDetail || result.message || "Failed to update product");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(
|
toast.error(
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ interface Props {
|
|||||||
data: Product[];
|
data: Product[];
|
||||||
file: File;
|
file: File;
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
onSubmit: (data: Product[], file: File, options: SubmitOptions) => void | Promise<any>;
|
onSubmit: (data: Product[], file: File, options: SubmitOptions) => void | Promise<boolean | void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImageUploadStep = ({
|
export const ImageUploadStep = ({
|
||||||
@@ -228,15 +228,17 @@ export const ImageUploadStep = ({
|
|||||||
showNewProduct,
|
showNewProduct,
|
||||||
};
|
};
|
||||||
|
|
||||||
await onSubmit(updatedData, file, submitOptions);
|
const success = await onSubmit(updatedData, file, submitOptions);
|
||||||
|
|
||||||
// Delete the import session on successful submit
|
// Only delete the import session after a successful submit response
|
||||||
|
if (success) {
|
||||||
try {
|
try {
|
||||||
await deleteImportSession();
|
await deleteImportSession();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Non-critical - log but don't fail the submission
|
// Non-critical - log but don't fail the submission
|
||||||
console.warn('Failed to delete import session:', err);
|
console.warn('Failed to delete import session:', err);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Submit error:', error);
|
console.error('Submit error:', error);
|
||||||
toast.error(`Failed to submit: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
toast.error(`Failed to submit: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
|
|||||||
invalidData: [] as Data<string>[],
|
invalidData: [] as Data<string>[],
|
||||||
all: data as Data<string>[]
|
all: data as Data<string>[]
|
||||||
};
|
};
|
||||||
onSubmit(result, file, options);
|
return onSubmit(result, file, options);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -113,6 +113,14 @@ const InputCellComponent = ({
|
|||||||
setIsFocused(true);
|
setIsFocused(true);
|
||||||
}, [cellPopoverClosedAt]);
|
}, [cellPopoverClosedAt]);
|
||||||
|
|
||||||
|
// Handle Enter key to blur the field (useful for barcode scanners that send Enter after scan)
|
||||||
|
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
inputRef.current?.blur();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Update store only on blur - this is when validation runs too
|
// Update store only on blur - this is when validation runs too
|
||||||
// IMPORTANT: We store FULL precision for price fields to allow accurate calculations
|
// IMPORTANT: We store FULL precision for price fields to allow accurate calculations
|
||||||
// The display formatting happens separately via displayValue
|
// The display formatting happens separately via displayValue
|
||||||
@@ -151,6 +159,7 @@ const InputCellComponent = ({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
disabled={isValidating}
|
disabled={isValidating}
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-8 text-sm',
|
'h-8 text-sm',
|
||||||
|
|||||||
@@ -331,12 +331,16 @@ const MultilineInputComponent = ({
|
|||||||
onBlur(editedSuggestion);
|
onBlur(editedSuggestion);
|
||||||
onDismissAiSuggestion?.(); // Clear the suggestion after accepting
|
onDismissAiSuggestion?.(); // Clear the suggestion after accepting
|
||||||
setAiSuggestionExpanded(false);
|
setAiSuggestionExpanded(false);
|
||||||
|
intentionalCloseRef.current = true;
|
||||||
|
setPopoverOpen(false);
|
||||||
}, [editedSuggestion, onBlur, onDismissAiSuggestion]);
|
}, [editedSuggestion, onBlur, onDismissAiSuggestion]);
|
||||||
|
|
||||||
// Handle dismissing the AI suggestion
|
// Handle dismissing the AI suggestion
|
||||||
const handleDismissSuggestion = useCallback(() => {
|
const handleDismissSuggestion = useCallback(() => {
|
||||||
onDismissAiSuggestion?.();
|
onDismissAiSuggestion?.();
|
||||||
setAiSuggestionExpanded(false);
|
setAiSuggestionExpanded(false);
|
||||||
|
intentionalCloseRef.current = true;
|
||||||
|
setPopoverOpen(false);
|
||||||
}, [onDismissAiSuggestion]);
|
}, [onDismissAiSuggestion]);
|
||||||
|
|
||||||
// Calculate display value
|
// Calculate display value
|
||||||
@@ -446,7 +450,7 @@ const MultilineInputComponent = ({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Main textarea */}
|
{/* Main textarea */}
|
||||||
<div data-col="left" className="flex flex-col min-h-0 w-full lg:w-1/2">
|
<div data-col="left" className={cn("flex flex-col min-h-0 w-full", hasAiSuggestion && "lg:w-1/2")}>
|
||||||
<div className={cn(hasAiSuggestion ? 'px-3 py-2 bg-accent' : '', 'flex flex-col flex-1 min-h-0')}>
|
<div className={cn(hasAiSuggestion ? 'px-3 py-2 bg-accent' : '', 'flex flex-col flex-1 min-h-0')}>
|
||||||
{/* Product name - shown inline on mobile, in measured spacer on desktop */}
|
{/* Product name - shown inline on mobile, in measured spacer on desktop */}
|
||||||
{hasAiSuggestion && productName && (
|
{hasAiSuggestion && productName && (
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ export function Import() {
|
|||||||
return stringValue;
|
return stringValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleData = async (data: ImportResult, _file: File, submitOptions: SubmitOptions) => {
|
const handleData = async (data: ImportResult, _file: File, submitOptions: SubmitOptions): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const rows = ((data.all?.length ? data.all : data.validData) ?? []) as Data<string>[];
|
const rows = ((data.all?.length ? data.all : data.validData) ?? []) as Data<string>[];
|
||||||
const formattedRows: NormalizedProduct[] = rows.map((row) => {
|
const formattedRows: NormalizedProduct[] = rows.map((row) => {
|
||||||
@@ -579,7 +579,7 @@ export function Import() {
|
|||||||
setStartFromScratch(false);
|
setStartFromScratch(false);
|
||||||
|
|
||||||
toast.success(`[DEBUG] Skipped API submission for ${formattedRows.length} product(s)`);
|
toast.success(`[DEBUG] Skipped API submission for ${formattedRows.length} product(s)`);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await submitNewProducts({
|
const response = await submitNewProducts({
|
||||||
@@ -638,11 +638,14 @@ export function Import() {
|
|||||||
} else {
|
} else {
|
||||||
toast.error(resolvedFailureMessage ?? defaultFailureMessage);
|
toast.error(resolvedFailureMessage ?? defaultFailureMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return isSuccess;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Import error:", error);
|
console.error("Import error:", error);
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : "Failed to import data. Please try again.";
|
error instanceof Error ? error.message : "Failed to import data. Please try again.";
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -924,7 +927,7 @@ export function Import() {
|
|||||||
<Alert className="border-success bg-success/10">
|
<Alert className="border-success bg-success/10">
|
||||||
<CheckCircle className="h-4 w-4" style={{ color: 'hsl(var(--success))' }} />
|
<CheckCircle className="h-4 w-4" style={{ color: 'hsl(var(--success))' }} />
|
||||||
<AlertTitle className="text-success-foreground">Success</AlertTitle>
|
<AlertTitle className="text-success-foreground">Success</AlertTitle>
|
||||||
<AlertDescription className="text-success-foreground">All products created successfully.</AlertDescription>
|
<AlertDescription className="text-success-foreground">All products created successfully. Please note that images may take a few minutes to finish uploading before they'll be visible in backend.</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
) : createdProducts.length > 0 && erroredProducts.length > 0 ? (
|
) : createdProducts.length > 0 && erroredProducts.length > 0 ? (
|
||||||
<Alert className="border-warning bg-warning/10">
|
<Alert className="border-warning bg-warning/10">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const CATEGORIES = [
|
|||||||
{ value: "clearance", label: "Clearance" },
|
{ value: "clearance", label: "Clearance" },
|
||||||
{ value: "daily_deals", label: "Daily Deals" },
|
{ value: "daily_deals", label: "Daily Deals" },
|
||||||
{ value: "never_featured", label: "Never Featured" },
|
{ value: "never_featured", label: "Never Featured" },
|
||||||
|
{ value: "no_interest", label: "No Interest" },
|
||||||
]
|
]
|
||||||
|
|
||||||
export function Newsletter() {
|
export function Newsletter() {
|
||||||
|
|||||||
Reference in New Issue
Block a user