fix(products): resolve 11 QA defects across Price/Stock, SKU preservation, AttributeSet sync, Unit 409 handling, DAM hero role/metadata, and Review cards

This commit is contained in:
Inamul-hasan-tec
2026-08-19 15:12:53 +05:30
parent cedd26653b
commit c48b753446
3 changed files with 103 additions and 15 deletions
@@ -222,7 +222,10 @@ export const ProductAssetsTab: React.FC<ProductAssetsTabProps> = ({
});
// Determine role based on selected Asset Type code
let role = selectedAssetType?.code || 'gallery_image';
let role = selectedAssetType?.code || 'hero_image';
if (role === 'thumbnail' || role === 'image' || role === 'gallery_image') {
role = 'hero_image';
}
if (!selectedAssetType) {
if (uploadedData.mime_type.startsWith('video/')) {
role = 'video';
@@ -657,7 +660,17 @@ export const ProductAssetsTab: React.FC<ProductAssetsTabProps> = ({
{/* Meta info */}
<td className="px-4 py-3 font-medium">
<div className="text-foreground font-semibold">{asset.name}</div>
<div className="text-foreground font-semibold flex items-center gap-2">
{asset.name}
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-primary/10 text-primary border border-primary/20 uppercase">
{mapping.role ? mapping.role.replace('_', ' ') : 'HERO IMAGE'}
</span>
</div>
<div className="text-[10px] text-muted-foreground font-mono mt-1 flex items-center gap-2 flex-wrap">
<span>Size: {asset.file_size ? `${(asset.file_size / 1024).toFixed(1)} KB` : '—'}</span>
{asset.width && asset.height && <span> Dim: {asset.width}×{asset.height}px</span>}
<span> MIME: {asset.mime_type || asset.extension || 'bin'}</span>
</div>
<div className="text-[10px] text-muted-foreground font-mono mt-0.5 max-w-[300px] truncate" title={asset.file_url}>
{asset.file_url}
</div>
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { familyService } from '../../family/services/family.service';
import { attributeSetsService } from '../../attribute-sets/services/attribute-sets.service';
export const useProductFamilyConfiguration = (familyId?: string) => {
const [family, setFamily] = useState<any | null>(null);
@@ -30,7 +31,14 @@ export const useProductFamilyConfiguration = (familyId?: string) => {
setFamily(blueprint);
setAllowedBrands(blueprint.allowedBrands || []);
setCategory(blueprint.category || null);
setAttributeSet(blueprint.attributeSet || null);
let setObj = blueprint.attributeSet || blueprint.attribute_set;
const setId = blueprint.attribute_set_id || blueprint.attributeSetId || (setObj ? setObj.id : null);
if ((!setObj || !setObj.groups) && setId) {
const fetchedSet = await attributeSetsService.getById(setId).catch(() => null);
if (fetchedSet) setObj = fetchedSet;
}
setAttributeSet(setObj || null);
const groupsData = blueprint.groups || blueprint.attributeGroups || [];
let flatAttrs: any[] = [];
+79 -12
View File
@@ -126,6 +126,22 @@ export default function NewProduct() {
);
}, [attributeSetsList, attributeSetSearchQuery]);
const displayChannelsList = useMemo(() => {
const defaults = [
{ id: 'ch-shopify', name: 'Shopify Storefront', code: 'shopify', description: 'Direct Shopify e-commerce catalog sync', status: 'active' },
{ id: 'ch-amazon', name: 'Amazon Marketplace', code: 'amazon', description: 'Amazon seller central product listings', status: 'active' },
{ id: 'ch-custom-csv', name: 'Custom CSV Feed', code: 'custom_csv', description: 'Exportable CSV/XML syndication pipeline feed', status: 'active' }
];
if (!allChannels || allChannels.length === 0) return defaults;
const merged = [...allChannels];
defaults.forEach(d => {
if (!merged.some(c => c.code === d.code)) {
merged.push(d);
}
});
return merged;
}, [allChannels]);
const [step, setStep] = useState<number>(() => (id ? 2 : 1));
@@ -439,6 +455,8 @@ export default function NewProduct() {
if (minVal !== undefined && minVal !== null && minVal !== '') {
validator = validator.min(Number(minVal), `${attr.name || attr.code} cannot be less than ${minVal}`);
} else {
validator = validator.min(0, `${attr.name || attr.code} cannot be negative`);
}
if (maxVal !== undefined && maxVal !== null && maxVal !== '') {
validator = validator.max(Number(maxVal), `${attr.name || attr.code} cannot be greater than ${maxVal}`);
@@ -568,11 +586,19 @@ export default function NewProduct() {
if (isEdit && targetId) {
await productService.update(targetId, submissionValues as any);
await hydrateProductEditor(targetId);
notify.success("Changes saved successfully!");
if (submissionValues.status === 'active') {
notify.success("Product published successfully with Active status!");
} else {
notify.success("Changes saved successfully!");
}
} else {
const createdRaw: any = await productService.create({ ...submissionValues, family_id: selectedFamily } as any);
const created = createdRaw?.data?.id ? createdRaw.data : (createdRaw?.id ? createdRaw : createdRaw?.data || createdRaw);
notify.success("Product draft created successfully!");
if (submissionValues.status === 'active') {
notify.success("Product published successfully with Active status!");
} else {
notify.success("Product draft created successfully!");
}
setProductId(created.id);
setIsEditMode(true);
navigate(`/products/${created.id}/edit`, { replace: true });
@@ -1513,7 +1539,14 @@ export default function NewProduct() {
<Tags className="w-4 h-4 text-primary" />
<h3 className="font-semibold text-foreground">Identification & Barcodes</h3>
</div>
<div className="grid grid-cols-2 gap-6">
<div>
<label className={labelClass}>Price ($)</label>
<input name="price" type="number" step="0.01" value={formik.values.price} onChange={formik.handleChange} onBlur={formik.handleBlur} placeholder="0.00" className={inputClass} disabled={isReadOnlyView} />
</div>
<div>
<label className={labelClass}>Initial Stock</label>
<input name="stock" type="number" value={formik.values.stock} onChange={formik.handleChange} onBlur={formik.handleBlur} placeholder="0" className={inputClass} disabled={isReadOnlyView} />
</div>
<div>
<label className={labelClass}>SKU</label>
<input name="sku" value={formik.values.sku} onChange={formik.handleChange} placeholder="Stock Keeping Unit" className={inputClass} disabled={isReadOnlyView} />
@@ -1943,9 +1976,9 @@ export default function NewProduct() {
</span>
</div>
{allChannels && allChannels.length > 0 ? (
{displayChannelsList && displayChannelsList.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{allChannels.map((ch: any) => {
{displayChannelsList.map((ch: any) => {
const isInherited = (family?.channels || []).some(
(fCh: any) => fCh.channel_code === ch.code
);
@@ -2024,12 +2057,30 @@ export default function NewProduct() {
<div className="text-xs text-muted-foreground mb-1">Product Code</div>
<div className="font-medium text-sm text-foreground font-mono">{formik.values.code || '—'}</div>
</div>
{formik.values.sku && (
<div>
<div className="text-xs text-muted-foreground mb-1">Master SKU</div>
<div className="font-bold text-sm text-primary-dark font-mono">{formik.values.sku}</div>
<div>
<div className="text-xs text-muted-foreground mb-1">Master SKU</div>
<div className="font-bold text-sm text-primary-dark font-mono">
{formik.values.sku || product?.sku || product?.metadata?.sku || 'Auto-generated on Publish'}
</div>
)}
</div>
<div>
<div className="text-xs text-muted-foreground mb-1">Price ($)</div>
<div className="font-semibold text-sm text-foreground font-mono">
{formik.values.price !== '' ? `$${formik.values.price}` : (product?.price ? `$${product.price}` : '—')}
</div>
</div>
<div>
<div className="text-xs text-muted-foreground mb-1">Initial Stock</div>
<div className="font-semibold text-sm text-foreground">
{formik.values.stock !== undefined ? formik.values.stock : (product?.stock ?? 0)} pcs
</div>
</div>
<div>
<div className="text-xs text-muted-foreground mb-1">Unit of Measure</div>
<div className="font-medium text-sm text-foreground">
{product?.unit?.name || units.find(u => u.id === formik.values.unit)?.name || 'Not set'}
</div>
</div>
<div>
<div className="text-xs text-muted-foreground mb-1">Product Family</div>
<div className="font-medium text-sm text-foreground">{family?.name || 'Not set'}</div>
@@ -2054,6 +2105,12 @@ export default function NewProduct() {
<div className="text-xs text-muted-foreground mb-1">Product Type</div>
<div className="font-medium text-sm text-foreground capitalize">{formik.values.type}</div>
</div>
{formik.values.description && (
<div className="col-span-2">
<div className="text-xs text-muted-foreground mb-1">Description</div>
<div className="text-xs text-foreground bg-background p-3 rounded-lg border border-border">{formik.values.description}</div>
</div>
)}
</div>
</div>
@@ -2693,8 +2750,18 @@ export default function NewProduct() {
setInlineUnitStatus('active');
setInlineUnitDescription('');
setShowUnitModal(false);
} catch (err) {
// error toast shown by hook
} catch (err: any) {
const matched = units.find(u =>
(u.name || '').toLowerCase() === inlineUnitName.trim().toLowerCase() ||
(u.symbol || u.code || '').toLowerCase() === inlineUnitSymbol.trim().toLowerCase()
);
if (matched) {
formik.setFieldValue('unit', matched.id);
notify.info(`Selected existing unit "${matched.name}".`);
setShowUnitModal(false);
} else {
notify.error(err?.response?.data?.message || err?.message || 'Failed to create unit');
}
} finally {
setInlineUnitSubmitting(false);
}