Implement product attribute enhancements

This commit is contained in:
Mahir-Mohamed
2026-08-10 12:03:00 +05:30
parent 07df2ccdd3
commit d04610f1ac
11 changed files with 1738 additions and 393 deletions
+58 -42
View File
@@ -86,6 +86,7 @@
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -301,31 +302,10 @@
"integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==",
"license": "MIT"
},
"node_modules/@emnapi/core": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz",
"integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
"integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
"integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -645,7 +625,6 @@
"integrity": "sha512-h5LUOzGArYemnW1NMz/DuuQhBi96J6JL2Bk8zE4kvqxB5Sg3jxmCiH4uyOWHDkiKSt5vWlG4FIwCR/DbstcNRQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/Boshen"
}
@@ -1382,7 +1361,6 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1400,7 +1378,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1418,7 +1395,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1436,7 +1412,6 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1454,7 +1429,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1472,7 +1446,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1490,7 +1463,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1508,7 +1480,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1526,7 +1497,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1544,7 +1514,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1562,7 +1531,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1580,7 +1548,6 @@
"os": [
"openharmony"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1595,7 +1562,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/core": "1.11.2",
"@emnapi/runtime": "1.11.2",
@@ -1605,6 +1571,40 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz",
"integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
"integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.0.tgz",
@@ -1618,7 +1618,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1636,7 +1635,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1647,6 +1645,7 @@
"integrity": "sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"picomatch": "^4.0.4"
},
@@ -2122,6 +2121,7 @@
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.18.0"
}
@@ -2131,6 +2131,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -2141,6 +2142,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -2196,6 +2198,7 @@
"integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.61.1",
"@typescript-eslint/types": "8.61.1",
@@ -2426,6 +2429,7 @@
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2508,6 +2512,7 @@
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/types": "^7.26.0"
}
@@ -2568,6 +2573,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -3019,6 +3025,7 @@
"integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==",
"dev": true,
"license": "MIT",
"peer": true,
"workspaces": [
"packages/*"
],
@@ -4282,6 +4289,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4317,6 +4325,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -4335,6 +4344,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.81.0.tgz",
"integrity": "sha512-ocbmr2p5KBMoAfj4WCUvped33lVi1Kd5DuDUvQDnB6VEAacOjPI/jMbtDdbhco4y9ct4xUuCmMY0b/C9L0QHjw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -4350,13 +4360,15 @@
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz",
"integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/react-redux": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
"integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@@ -4545,7 +4557,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@@ -4809,6 +4822,7 @@
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -4968,6 +4982,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
@@ -5448,6 +5463,7 @@
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+2 -10
View File
@@ -1,6 +1,6 @@
import React, { useState, useMemo, useEffect, useRef } from 'react';
import {
ChevronDown, ChevronRight, Folder, FolderOpen, Search, Plus, X,
ChevronDown, ChevronRight, Folder, FolderOpen, Search, X,
Check, Loader2, FolderPlus
} from 'lucide-react';
import { categoryService } from '../../features/categories/services/category.service';
@@ -254,7 +254,7 @@ export const CategoryTreeSelect: React.FC<CategoryTreeSelectProps> = ({
{isOpen && (
<div className="absolute z-50 mt-1.5 w-full bg-white border border-gray-200 rounded-xl shadow-xl overflow-hidden flex flex-col max-h-80 animate-in fade-in zoom-in-95 duration-100">
{/* Search Header */}
<div className="p-2 border-b border-gray-100 flex items-center gap-2 bg-gray-50/50">
<div className="p-2 border-b border-gray-100 flex items-center bg-gray-50/50">
<div className="relative flex-1">
<Search className="w-3.5 h-3.5 text-gray-400 absolute left-2.5 top-2.5" />
<input
@@ -265,14 +265,6 @@ export const CategoryTreeSelect: React.FC<CategoryTreeSelectProps> = ({
className="w-full pl-8 pr-3 py-1.5 text-xs border border-gray-200 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary bg-white"
/>
</div>
<button
type="button"
onClick={() => setIsDrawerOpen(true)}
className="px-2.5 py-1.5 bg-primary hover:bg-primary-hover text-white text-xs font-semibold rounded-lg flex items-center gap-1 shrink-0 transition-colors shadow-2xs"
>
<Plus className="w-3.5 h-3.5" />
Quick Create
</button>
</div>
{/* Category Tree Body */}
@@ -25,6 +25,7 @@ interface DynamicAttributeRendererProps {
onChange: (value: any) => void;
error?: string;
touched?: boolean;
readOnly?: boolean;
}
export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> = ({
@@ -33,6 +34,7 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
onChange,
error,
touched,
readOnly,
}) => {
const isRequired = attribute.is_required || attribute.isRequired;
const inputClass = "w-full border border-border focus:ring-primary rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:border-transparent bg-surface text-foreground";
@@ -48,6 +50,7 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
placeholder={`Enter ${attribute.name}`}
rows={3}
className={`${inputClass} resize-none`}
disabled={readOnly}
/>
);
case 'number':
@@ -63,6 +66,7 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
}}
placeholder={`Enter ${attribute.name}`}
className={inputClass}
disabled={readOnly}
/>
);
case 'date':
@@ -72,6 +76,7 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
value={value || ''}
onChange={(e) => onChange(e.target.value)}
className={inputClass}
disabled={readOnly}
/>
);
case 'boolean':
@@ -82,6 +87,7 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
const val = e.target.value;
onChange(val === 'true' ? true : val === 'false' ? false : undefined);
}}
disabled={readOnly}
>
<option value="">Select option</option>
<option value="true">True</option>
@@ -95,6 +101,7 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
<Select
value={value || ''}
onChange={(e) => onChange(e.target.value)}
disabled={readOnly}
>
<option value="">Select option</option>
{attribute.optionsList?.map((opt) => (
@@ -118,12 +125,13 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
{attribute.optionsList?.map((opt) => {
const isChecked = selectedValues.includes(opt.code);
return (
<label key={opt.id} className="flex items-center gap-2 cursor-pointer text-xs font-medium text-foreground">
<label key={opt.id} className={`flex items-center gap-2 cursor-pointer text-xs font-medium text-foreground ${readOnly ? 'cursor-not-allowed opacity-75' : ''}`}>
<input
type="checkbox"
checked={isChecked}
onChange={(e) => handleCheckboxChange(opt.code, e.target.checked)}
className="w-4 h-4 text-primary rounded border-border focus:ring-primary"
disabled={readOnly}
/>
{opt.label}
</label>
@@ -143,6 +151,7 @@ export const DynamicAttributeRenderer: React.FC<DynamicAttributeRendererProps> =
onChange={(e) => onChange(e.target.value)}
placeholder={`Enter ${attribute.name}`}
className={inputClass}
disabled={readOnly}
/>
);
}
@@ -32,6 +32,7 @@ interface DynamicAttributesSectionProps {
errors?: Record<string, any>;
touched?: Record<string, any>;
onAttributeChange: (code: string, value: any) => void;
readOnly?: boolean;
}
export const DynamicAttributesSection: React.FC<DynamicAttributesSectionProps> = ({
@@ -41,6 +42,7 @@ export const DynamicAttributesSection: React.FC<DynamicAttributesSectionProps> =
errors = {},
touched = {},
onAttributeChange,
readOnly,
}) => {
if (!hasAttributeSet) {
return (
@@ -87,6 +89,7 @@ export const DynamicAttributesSection: React.FC<DynamicAttributesSectionProps> =
errors={errors}
touched={touched}
onAttributeChange={onAttributeChange}
readOnly={readOnly}
/>
))}
</div>
@@ -12,11 +12,13 @@ import { getAssetUrl, isImageFile } from '../../../lib/utils';
interface ProductAssetsTabProps {
productId?: string;
family: any;
readOnly?: boolean;
}
export const ProductAssetsTab: React.FC<ProductAssetsTabProps> = ({
productId,
family
family,
readOnly
}) => {
const [assignedAssets, setAssignedAssets] = useState<AssetMapping[]>([]);
const [loading, setLoading] = useState(false);
@@ -271,55 +273,57 @@ export const ProductAssetsTab: React.FC<ProductAssetsTabProps> = ({
)}
{/* Upload Zone & Picker Trigger */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={`md:col-span-2 border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all flex flex-col items-center justify-center ${dragOver ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/20 hover:bg-background/50 bg-surface'
}`}
>
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleFileChange}
accept="image/*,video/*,application/pdf"
/>
{uploading ? (
<div className="space-y-2">
<Loader2 className="w-8 h-8 text-primary animate-spin mx-auto" />
<p className="font-semibold text-xs text-foreground">Uploading new media file...</p>
</div>
) : (
<div className="space-y-3">
<Upload className="w-8 h-8 text-muted-foreground mx-auto" />
<div>
<h4 className="text-foreground font-semibold text-xs">Drag & Drop Files Here</h4>
<p className="text-[11px] text-muted-foreground mt-1">or click to browse media files</p>
</div>
</div>
)}
</div>
<div className="bg-surface border border-border rounded-xl p-6 flex flex-col justify-between">
<div>
<h4 className="font-semibold text-foreground text-xs">Asset Library</h4>
<p className="text-[11px] text-muted-foreground mt-1">
Select existing images, catalogs or spec documents from the central Asset library.
</p>
</div>
<button
type="button"
onClick={() => setShowPicker(true)}
className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-2 bg-surface border border-border hover:bg-background text-foreground rounded-lg text-xs font-semibold shadow-2xs"
{!readOnly && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={`md:col-span-2 border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all flex flex-col items-center justify-center ${dragOver ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/20 hover:bg-background/50 bg-surface'
}`}
>
<Search className="w-3.5 h-3.5" />
Browse Asset Library
</button>
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleFileChange}
accept="image/*,video/*,application/pdf"
/>
{uploading ? (
<div className="space-y-2">
<Loader2 className="w-8 h-8 text-primary animate-spin mx-auto" />
<p className="font-semibold text-xs text-foreground">Uploading new media file...</p>
</div>
) : (
<div className="space-y-3">
<Upload className="w-8 h-8 text-muted-foreground mx-auto" />
<div>
<h4 className="text-foreground font-semibold text-xs">Drag & Drop Files Here</h4>
<p className="text-[11px] text-muted-foreground mt-1">or click to browse media files</p>
</div>
</div>
)}
</div>
<div className="bg-surface border border-border rounded-xl p-6 flex flex-col justify-between">
<div>
<h4 className="font-semibold text-foreground text-xs">Asset Library</h4>
<p className="text-[11px] text-muted-foreground mt-1">
Select existing images, catalogs or spec documents from the central Asset library.
</p>
</div>
<button
type="button"
onClick={() => setShowPicker(true)}
className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-2 bg-surface border border-border hover:bg-background text-foreground rounded-lg text-xs font-semibold shadow-2xs"
>
<Search className="w-3.5 h-3.5" />
Browse Asset Library
</button>
</div>
</div>
</div>
)}
{/* Grid of assigned assets */}
{loading && assignedAssets.length === 0 ? (
@@ -331,12 +335,12 @@ export const ProductAssetsTab: React.FC<ProductAssetsTabProps> = ({
<table className="w-full border-collapse text-left text-xs text-foreground">
<thead>
<tr className="bg-background border-b border-border text-[10px] font-bold text-muted-foreground uppercase tracking-wider">
<th className="px-4 py-3 w-16 text-center">Order</th>
{!readOnly && <th className="px-4 py-3 w-16 text-center">Order</th>}
<th className="px-4 py-3 w-20">Preview</th>
<th className="px-4 py-3">Asset Details</th>
<th className="px-4 py-3 w-40">Role Classification</th>
<th className="px-4 py-3 w-32 text-center">Primary</th>
<th className="px-4 py-3 w-20 text-right">Actions</th>
{!readOnly && <th className="px-4 py-3 w-20 text-right">Actions</th>}
</tr>
</thead>
<tbody className="divide-y divide-border">
@@ -351,26 +355,28 @@ export const ProductAssetsTab: React.FC<ProductAssetsTabProps> = ({
return (
<tr key={mapping.id} className="hover:bg-background/50 transition-colors">
{/* Display Order sorting */}
<td className="px-4 py-3 text-center">
<div className="flex items-center justify-center gap-1">
<button
type="button"
disabled={idx === 0}
onClick={() => handleMoveOrder(idx, 'up')}
className="p-1 hover:bg-surface-muted rounded text-muted-foreground disabled:opacity-30 disabled:hover:bg-transparent"
>
<ArrowUp className="w-3.5 h-3.5" />
</button>
<button
type="button"
disabled={idx === assignedAssets.length - 1}
onClick={() => handleMoveOrder(idx, 'down')}
className="p-1 hover:bg-surface-muted rounded text-muted-foreground disabled:opacity-30 disabled:hover:bg-transparent"
>
<ArrowDown className="w-3.5 h-3.5" />
</button>
</div>
</td>
{!readOnly && (
<td className="px-4 py-3 text-center">
<div className="flex items-center justify-center gap-1">
<button
type="button"
disabled={idx === 0}
onClick={() => handleMoveOrder(idx, 'up')}
className="p-1 hover:bg-surface-muted rounded text-muted-foreground disabled:opacity-30 disabled:hover:bg-transparent"
>
<ArrowUp className="w-3.5 h-3.5" />
</button>
<button
type="button"
disabled={idx === assignedAssets.length - 1}
onClick={() => handleMoveOrder(idx, 'down')}
className="p-1 hover:bg-surface-muted rounded text-muted-foreground disabled:opacity-30 disabled:hover:bg-transparent"
>
<ArrowDown className="w-3.5 h-3.5" />
</button>
</div>
</td>
)}
{/* Preview Thumbnail */}
<td className="px-4 py-3">
@@ -395,19 +401,29 @@ export const ProductAssetsTab: React.FC<ProductAssetsTabProps> = ({
</div>
</td>
{/* Role dropdown selection */}
{/* Role dropdown selection or read-only label */}
<td className="px-4 py-3">
<select
value={mapping.role}
onChange={(e) => handleRoleChange(asset.id, e.target.value)}
className="text-xs border border-border focus:ring-primary rounded-lg px-2.5 py-1 focus:outline-none bg-surface font-medium text-foreground"
>
<option value="primary_image">Primary Image</option>
<option value="gallery_image">Gallery Image</option>
<option value="thumbnail">Thumbnail</option>
<option value="video">Video</option>
<option value="document">Documentation PDF</option>
</select>
{readOnly ? (
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-surface-muted border border-border text-muted-foreground">
{mapping.role === 'primary_image' ? 'Primary Image' :
mapping.role === 'gallery_image' ? 'Gallery Image' :
mapping.role === 'thumbnail' ? 'Thumbnail' :
mapping.role === 'video' ? 'Video' :
mapping.role === 'document' ? 'Documentation PDF' : mapping.role}
</span>
) : (
<select
value={mapping.role}
onChange={(e) => handleRoleChange(asset.id, e.target.value)}
className="text-xs border border-border focus:ring-primary rounded-lg px-2.5 py-1 focus:outline-none bg-surface font-medium text-foreground"
>
<option value="primary_image">Primary Image</option>
<option value="gallery_image">Gallery Image</option>
<option value="thumbnail">Thumbnail</option>
<option value="video">Video</option>
<option value="document">Documentation PDF</option>
</select>
)}
</td>
{/* Primary Badge toggle button */}
@@ -417,6 +433,8 @@ export const ProductAssetsTab: React.FC<ProductAssetsTabProps> = ({
<Shield className="w-3 h-3" />
Primary
</span>
) : readOnly ? (
<span className="text-xs text-muted-foreground"></span>
) : (
<button
type="button"
@@ -429,15 +447,17 @@ export const ProductAssetsTab: React.FC<ProductAssetsTabProps> = ({
</td>
{/* Unassign */}
<td className="px-4 py-3 text-right">
<button
type="button"
onClick={() => handleUnassign(asset.id)}
className="p-1.5 hover:bg-red-50 text-muted-foreground hover:text-red-600 rounded-lg transition-colors"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</td>
{!readOnly && (
<td className="px-4 py-3 text-right">
<button
type="button"
onClick={() => handleUnassign(asset.id)}
className="p-1.5 hover:bg-red-50 text-muted-foreground hover:text-red-600 rounded-lg transition-colors"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</td>
)}
</tr>
);
})}
@@ -33,6 +33,7 @@ interface ProductAttributeGroupProps {
errors?: Record<string, any>;
touched?: Record<string, any>;
onAttributeChange: (code: string, value: any) => void;
readOnly?: boolean;
}
export const ProductAttributeGroup: React.FC<ProductAttributeGroupProps> = ({
@@ -41,6 +42,7 @@ export const ProductAttributeGroup: React.FC<ProductAttributeGroupProps> = ({
errors = {},
touched = {},
onAttributeChange,
readOnly,
}) => {
const [isExpanded, setIsExpanded] = useState(true);
const attributes = group.attributes || [];
@@ -75,6 +77,7 @@ export const ProductAttributeGroup: React.FC<ProductAttributeGroupProps> = ({
onChange={(val) => onAttributeChange(attr.code, val)}
error={errors[attr.code]}
touched={touched[attr.code]}
readOnly={readOnly}
/>
))}
</div>
@@ -10,6 +10,7 @@ interface VariantEditorRowProps {
onUpdate: (id: string, updates: Partial<Variant>) => Promise<any>;
onDelete: (id: string) => void;
onArchive: (id: string) => void;
readOnly?: boolean;
}
export const VariantEditorRow: React.FC<VariantEditorRowProps> = ({
@@ -19,7 +20,8 @@ export const VariantEditorRow: React.FC<VariantEditorRowProps> = ({
onSelect,
onUpdate,
onDelete,
onArchive
onArchive,
readOnly
}) => {
const [sku, setSku] = useState(variant.sku);
const [price, setPrice] = useState(String(variant.price));
@@ -29,7 +31,6 @@ export const VariantEditorRow: React.FC<VariantEditorRowProps> = ({
const [savingStatus, setSavingStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
// Synchronize state if variant updates externally
useEffect(() => {
setSku(variant.sku);
setPrice(String(variant.price));
@@ -39,7 +40,6 @@ export const VariantEditorRow: React.FC<VariantEditorRowProps> = ({
}, [variant]);
const handleFieldSave = async () => {
// Basic validation
const pNum = parseFloat(price);
const cpNum = parseFloat(costPrice);
const sNum = parseInt(stock, 10);
@@ -77,6 +77,45 @@ export const VariantEditorRow: React.FC<VariantEditorRowProps> = ({
}
};
// ── Read-only row ──────────────────────────────────────────────────────────
if (readOnly) {
return (
<tr className="hover:bg-background/50 transition-colors border-b border-border">
<td className="px-4 py-3">
<div className="flex flex-col">
<span className="font-semibold text-foreground text-xs truncate max-w-[200px]" title={variant.name}>
{variant.name.split(' - ')[1] || variant.name}
</span>
<div className="flex flex-wrap gap-1 mt-1">
{axesKeys.map(key => {
const val = variant.attributes[key];
if (!val) return null;
return (
<span key={key} className="inline-block bg-surface-muted text-[10px] px-1.5 py-0.5 rounded text-muted-foreground font-mono">
{key}: {val}
</span>
);
})}
</div>
</div>
</td>
<td className="px-4 py-3 font-mono text-xs text-foreground">{sku}</td>
<td className="px-4 py-3 text-right text-xs text-foreground">${price}</td>
<td className="px-4 py-3 text-right text-xs text-foreground">${costPrice}</td>
<td className="px-4 py-3 text-center text-xs text-foreground">{stock}</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider ${
status === 'active' ? 'bg-success/10 text-success' :
status === 'draft' ? 'bg-warning/10 text-warning' : 'bg-surface-muted text-muted-foreground'
}`}>
{status}
</span>
</td>
</tr>
);
}
// ── Editable row ───────────────────────────────────────────────────────────
return (
<tr className={`hover:bg-background/50 transition-colors border-b border-border ${isSelected ? 'bg-primary/5/10' : ''}`}>
{/* Checkbox */}
@@ -89,7 +128,7 @@ export const VariantEditorRow: React.FC<VariantEditorRowProps> = ({
/>
</td>
{/* Cartesian Combination specifications */}
{/* Variant Specification */}
<td className="px-4 py-3">
<div className="flex flex-col">
<span className="font-semibold text-foreground text-xs truncate max-w-[200px]" title={variant.name}>
@@ -100,10 +139,7 @@ export const VariantEditorRow: React.FC<VariantEditorRowProps> = ({
const val = variant.attributes[key];
if (!val) return null;
return (
<span
key={key}
className="inline-block bg-surface-muted text-[10px] px-1.5 py-0.5 rounded text-muted-foreground font-mono"
>
<span key={key} className="inline-block bg-surface-muted text-[10px] px-1.5 py-0.5 rounded text-muted-foreground font-mono">
{key}: {val}
</span>
);
@@ -183,7 +219,7 @@ export const VariantEditorRow: React.FC<VariantEditorRowProps> = ({
</select>
</td>
{/* Save Status Spinner/Tick */}
{/* Save Status */}
<td className="px-4 py-3 text-center">
{savingStatus === 'saving' && <Loader className="w-3.5 h-3.5 text-primary animate-spin mx-auto" />}
{savingStatus === 'saved' && <Check className="w-3.5 h-3.5 text-emerald-500 mx-auto" />}
@@ -11,6 +11,7 @@ interface VariantListViewProps {
onUpdate: (id: string, updates: Partial<Variant>) => Promise<any>;
onDelete: (id: string) => void;
onArchive: (id: string) => void;
readOnly?: boolean;
}
export const VariantListView: React.FC<VariantListViewProps> = ({
@@ -21,7 +22,8 @@ export const VariantListView: React.FC<VariantListViewProps> = ({
onSelectAllChange,
onUpdate,
onDelete,
onArchive
onArchive,
readOnly
}) => {
const allSelected = variants.length > 0 && variants.every(v => selectedIds.has(v.id));
const someSelected = variants.length > 0 && variants.some(v => selectedIds.has(v.id)) && !allSelected;
@@ -31,25 +33,27 @@ export const VariantListView: React.FC<VariantListViewProps> = ({
<table className="w-full border-collapse text-left min-w-[800px]">
<thead>
<tr className="bg-background/70 border-b border-border text-[10px] font-bold text-muted-foreground uppercase tracking-wider">
<th className="px-4 py-3 text-center w-12">
<input
type="checkbox"
checked={allSelected}
ref={(el) => {
if (el) el.indeterminate = someSelected;
}}
onChange={(e) => onSelectAllChange(e.target.checked)}
className="w-3.5 h-3.5 text-primary rounded border-border focus:ring-primary/20 cursor-pointer"
/>
</th>
{!readOnly && (
<th className="px-4 py-3 text-center w-12">
<input
type="checkbox"
checked={allSelected}
ref={(el) => {
if (el) el.indeterminate = someSelected;
}}
onChange={(e) => onSelectAllChange(e.target.checked)}
className="w-3.5 h-3.5 text-primary rounded border-border focus:ring-primary/20 cursor-pointer"
/>
</th>
)}
<th className="px-4 py-3">Variant Specification</th>
<th className="px-4 py-3 w-48">SKU Code</th>
<th className="px-4 py-3 w-28 text-right">Sale Price</th>
<th className="px-4 py-3 w-28 text-right">Cost Price</th>
<th className="px-4 py-3 w-24 text-center">Stock</th>
<th className="px-4 py-3 w-32">Status</th>
<th className="px-4 py-3 w-16 text-center">Save</th>
<th className="px-4 py-3 w-24 text-right">Actions</th>
{!readOnly && <th className="px-4 py-3 w-16 text-center">Save</th>}
{!readOnly && <th className="px-4 py-3 w-24 text-right">Actions</th>}
</tr>
</thead>
<tbody className="divide-y divide-border">
@@ -63,11 +67,12 @@ export const VariantListView: React.FC<VariantListViewProps> = ({
onUpdate={onUpdate}
onDelete={onDelete}
onArchive={onArchive}
readOnly={readOnly}
/>
))}
{variants.length === 0 && (
<tr>
<td colSpan={9} className="px-6 py-10 text-center text-xs text-muted-foreground font-medium">
<td colSpan={readOnly ? 6 : 9} className="px-6 py-10 text-center text-xs text-muted-foreground font-medium">
No variants found matching criteria.
</td>
</tr>
@@ -11,6 +11,7 @@ interface VariantMatrixViewProps {
onUpdate: (id: string, updates: Partial<Variant>) => Promise<any>;
onDelete: (id: string) => void;
onArchive: (id: string) => void;
readOnly?: boolean;
}
export const VariantMatrixView: React.FC<VariantMatrixViewProps> = ({
@@ -22,7 +23,8 @@ export const VariantMatrixView: React.FC<VariantMatrixViewProps> = ({
onSelectAllChange,
onUpdate,
onDelete,
onArchive
onArchive,
readOnly
}) => {
const allSelected = variants.length > 0 && variants.every(v => selectedIds.has(v.id));
const someSelected = variants.length > 0 && variants.some(v => selectedIds.has(v.id)) && !allSelected;
@@ -32,17 +34,19 @@ export const VariantMatrixView: React.FC<VariantMatrixViewProps> = ({
<table className="w-full border-collapse text-left min-w-[900px]">
<thead>
<tr className="bg-primary/5/30 border-b border-border text-[10px] font-bold text-muted-foreground uppercase tracking-wider">
<th className="px-4 py-3.5 text-center w-12">
<input
type="checkbox"
checked={allSelected}
ref={(el) => {
if (el) el.indeterminate = someSelected;
}}
onChange={(e) => onSelectAllChange(e.target.checked)}
className="w-3.5 h-3.5 text-primary rounded border-border focus:ring-primary/20 cursor-pointer"
/>
</th>
{!readOnly && (
<th className="px-4 py-3.5 text-center w-12">
<input
type="checkbox"
checked={allSelected}
ref={(el) => {
if (el) el.indeterminate = someSelected;
}}
onChange={(e) => onSelectAllChange(e.target.checked)}
className="w-3.5 h-3.5 text-primary rounded border-border focus:ring-primary/20 cursor-pointer"
/>
</th>
)}
{/* Dynamic columns for each variant axis */}
{axesKeys.map(key => (
@@ -56,8 +60,8 @@ export const VariantMatrixView: React.FC<VariantMatrixViewProps> = ({
<th className="px-4 py-3.5 w-28 text-right">Cost Price</th>
<th className="px-4 py-3.5 w-24 text-center">Stock</th>
<th className="px-4 py-3.5 w-32">Status</th>
<th className="px-4 py-3.5 w-16 text-center">Save</th>
<th className="px-4 py-3.5 w-24 text-right">Actions</th>
{!readOnly && <th className="px-4 py-3.5 w-16 text-center">Save</th>}
{!readOnly && <th className="px-4 py-3.5 w-24 text-right">Actions</th>}
</tr>
</thead>
<tbody className="divide-y divide-border">
@@ -69,14 +73,16 @@ export const VariantMatrixView: React.FC<VariantMatrixViewProps> = ({
}`}
>
{/* Checkbox */}
<td className="px-4 py-3 text-center">
<input
type="checkbox"
checked={selectedIds.has(variant.id)}
onChange={(e) => onSelectChange(variant.id, e.target.checked)}
className="w-3.5 h-3.5 text-primary rounded border-border focus:ring-primary/20 cursor-pointer"
/>
</td>
{!readOnly && (
<td className="px-4 py-3 text-center">
<input
type="checkbox"
checked={selectedIds.has(variant.id)}
onChange={(e) => onSelectChange(variant.id, e.target.checked)}
className="w-3.5 h-3.5 text-primary rounded border-border focus:ring-primary/20 cursor-pointer"
/>
</td>
)}
{/* Dynamic cells for each variant axis */}
{axesKeys.map(key => {
@@ -100,12 +106,13 @@ export const VariantMatrixView: React.FC<VariantMatrixViewProps> = ({
onUpdate={onUpdate}
onDelete={onDelete}
onArchive={onArchive}
readOnly={readOnly}
/>
</tr>
))}
{variants.length === 0 && (
<tr>
<td colSpan={axesKeys.length + 8} className="px-6 py-10 text-center text-xs text-muted-foreground font-medium">
<td colSpan={axesKeys.length + (readOnly ? 5 : 8)} className="px-6 py-10 text-center text-xs text-muted-foreground font-medium">
No variants found matching criteria.
</td>
</tr>
@@ -122,9 +129,10 @@ interface InlineCellsProps {
onUpdate: (id: string, updates: Partial<Variant>) => Promise<any>;
onDelete: (id: string) => void;
onArchive: (id: string) => void;
readOnly?: boolean;
}
const InlineEditorCells: React.FC<InlineCellsProps> = ({ variant, onUpdate, onDelete, onArchive }) => {
const InlineEditorCells: React.FC<InlineCellsProps> = ({ variant, onUpdate, onDelete, onArchive, readOnly }) => {
const [sku, setSku] = useState(variant.sku);
const [price, setPrice] = useState(String(variant.price));
const [costPrice, setCostPrice] = useState(String(variant.costPrice));
@@ -179,6 +187,25 @@ const InlineEditorCells: React.FC<InlineCellsProps> = ({ variant, onUpdate, onDe
}
};
if (readOnly) {
return (
<>
<td className="px-4 py-3 font-mono text-xs text-foreground">{sku}</td>
<td className="px-4 py-3 text-right text-xs text-foreground">${price}</td>
<td className="px-4 py-3 text-right text-xs text-foreground">${costPrice}</td>
<td className="px-4 py-3 text-center text-xs text-foreground">{stock}</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider ${
status === 'active' ? 'bg-success/10 text-success' :
status === 'draft' ? 'bg-warning/10 text-warning' : 'bg-surface-muted text-muted-foreground'
}`}>
{status}
</span>
</td>
</>
);
}
return (
<>
<td className="px-4 py-3">
@@ -13,13 +13,15 @@ interface VariantsTabProps {
productType: string;
parentSku: string;
family: any; // Product Family details
readOnly?: boolean;
}
export const VariantsTab: React.FC<VariantsTabProps> = ({
productId,
productType,
parentSku,
family
family,
readOnly
}) => {
const {
variants,
@@ -213,14 +215,16 @@ export const VariantsTab: React.FC<VariantsTabProps> = ({
{/* View settings panel */}
<div className="flex items-center justify-between bg-surface rounded-xl border border-border p-4 shadow-xs">
<div className="flex items-center gap-4">
<button
type="button"
onClick={() => setShowGenerator(true)}
className="inline-flex items-center gap-1 px-3 py-1.5 bg-primary hover:bg-primary-hover text-white rounded-lg text-xs font-semibold transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Add / Generate Variants
</button>
{!readOnly && (
<button
type="button"
onClick={() => setShowGenerator(true)}
className="inline-flex items-center gap-1 px-3 py-1.5 bg-primary hover:bg-primary-hover text-white rounded-lg text-xs font-semibold transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Add / Generate Variants
</button>
)}
<button
type="button"
onClick={() => fetchByProduct(productId)}
@@ -280,6 +284,7 @@ export const VariantsTab: React.FC<VariantsTabProps> = ({
onUpdate={handleSingleUpdate}
onDelete={deleteVariant}
onArchive={archiveVariant}
readOnly={readOnly}
/>
) : (
<VariantListView
@@ -291,6 +296,7 @@ export const VariantsTab: React.FC<VariantsTabProps> = ({
onUpdate={handleSingleUpdate}
onDelete={deleteVariant}
onArchive={archiveVariant}
readOnly={readOnly}
/>
)}
</div>
File diff suppressed because it is too large Load Diff