Implement product attribute enhancements
This commit is contained in:
Generated
+58
-42
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user