@@ -4,15 +4,12 @@ import { useFamily } from '../hook/useFamily';
import { useCategory } from '../../categories/hook/useCategory' ;
import { useAttribute } from '../../attributes/hook/useAttribute' ;
import { useChannel } from '../../channels/hook/useChannel' ;
import { useAssetType } from '../../asset-types/hook/useAssetType' ;
import { useWorkflow } from '../../workflow/hook/useWorkflow' ;
import { StageTimeline } from '../../workflow/components/StageTimeline' ;
import { Search , X , Layers , Activity , Activity } from 'lucide-react' ;
import { Button } from "../../../components/customs/Button" ;
import { Search , X , Layers , Activity } from 'lucide-react' ;
import { useAttributeSet } from '../../attribute-sets/hook/useAttributeSet' ;
import { useAssetFamily } from '../../asset-families/hook/useAssetFamily' ;
import { useBrand } from '../../brands/hook/useBrand' ;
import { useUnit } from '../../units/hook/useUnit' ;
import {
FileText , LayoutGrid , Tags , Globe , Eye , Settings2 , Save ,
CheckCircle2 , AlertCircle , CheckSquare , Image as ImageIcon , Check ,
@@ -34,7 +31,7 @@ const TABS = [
{ id : 'attributes' , label : 'Attribute Groups' , icon : LayoutGrid , step : 2 } ,
{ id : 'variants' , label : 'Variant Strategy' , icon : Tags , step : 3 } ,
{ id : 'channels' , label : 'Allowed Channels' , icon : Globe , step : 4 } ,
{ id : 'assets' , label : 'Default Asset Typ es' , icon : ImageIcon , step : 5 } ,
{ id : 'assets' , label : 'Asset Famili es' , icon : ImageIcon , step : 5 } ,
{ id : 'workflow' , label : 'Workflow Assignment' , icon : Settings2 , step : 6 } ,
{ id : 'rules' , label : 'Completeness Rules' , icon : CheckSquare , step : 7 } ,
{ id : 'summary' , label : 'Inheritance Summary' , icon : Eye , step : 8 } ,
@@ -233,14 +230,12 @@ export default function NewFamily() {
const { categories , fetchCategories , loading : categoriesLoading } = useCategory ( ) ;
const { attributes , fetchAttributes , loading : attributesLoading } = useAttribute ( ) ;
const { items : channelsList , fetchItems : fetchChannels , loading : channelsLoading } = useChannel ( ) ;
const { items : assetTypesList , fetchItems : fetchAssetTypes , loading : assetTypesLoading } = useAssetType ( ) ;
const { fetchItems : fetchAssetFamilies , loading : assetFamiliesLoading } = useAssetFamily ( ) ;
const { items : assetFamiliesList , fetchItems : fetchAssetFamilies , loading : assetFamiliesLoading } = useAssetFamily ( ) ;
const { items : workflowsList , fetchItems : fetchWorkflows , loading : workflowsLoading } = useWorkflow ( ) ;
const { items : attributeSetsList , fetchItems : fetchAttributeSets , loading : setsLoading } = useAttributeSet ( ) ;
const { brands , fetchBrands , loading : brandsLoading } = useBrand ( ) ;
const { fetchUnits , loading : unitsLoading } = useUnit ( ) ;
const [ familyLoading , setFamilyLoading ] = useState ( false ) ;
const [ activeTab , setActiveTab ] = useState ( 'basic' ) ;
@@ -294,7 +289,6 @@ export default function NewFamily() {
attributes : [ ] as string [ ] ,
variantAxes : [ ] as string [ ] ,
allowedBrands : [ ] as string [ ] ,
allowedUnits : [ ] as string [ ] ,
channels : [ ] as string [ ] ,
assetRequirements : [ ] as string [ ] ,
completenessRules : {
@@ -333,13 +327,11 @@ export default function NewFamily() {
fetchCategories ( ) ;
fetchAttributes ( ) ;
fetchChannels ( ) ;
fetchAssetTypes ( ) ;
fetchAssetFamilies ( ) ;
fetchWorkflows ( ) ;
fetchAttributeSets ( ) ;
fetchBrands ( ) ;
fetchUnits ( ) ;
} , [ fetchCategories , fetchAttributes , fetchChannels , fetchAssetTypes , fetchAssetFamilies , fetchWorkflows , fetchAttributeSets , fetchBrands , fetchUnits ] ) ;
} , [ fetchCategories , fetchAttributes , fetchChannels , fetchAssetFamilies , fetchWorkflows , fetchAttributeSets , fetchBrands ] ) ;
// Load family details in Edit mode
useEffect ( ( ) = > {
@@ -362,7 +354,6 @@ export default function NewFamily() {
channels : Array.isArray ( data . channels ) ? data . channels . map ( ( c : any ) = > c . channelCode || c . channel_code || c ) : [ ] ,
assetRequirements : Array.isArray ( data . assetRequirements ) ? data . assetRequirements . map ( ( r : any ) = > r . id || r ) : [ ] ,
allowedBrands : Array.isArray ( data . allowedBrands ) ? data . allowedBrands . map ( ( b : any ) = > b . id || b ) : [ ] ,
allowedUnits : Array.isArray ( data . allowedUnits ) ? data . allowedUnits . map ( ( u : any ) = > u . id || u ) : [ ] ,
completenessRules : data.completenessRules || {
required_attributes : 40 ,
at_least_one_image : 20 ,
@@ -395,7 +386,7 @@ export default function NewFamily() {
} , [ formik . values . attributeSetId , loadBlueprintPreview ] ) ;
const activeIndex = TABS . findIndex ( t = > t . id === activeTab ) ;
const isLoading = familyLoading || categoriesLoading || attributesLoading || channelsLoading || assetTypesLoading || assetFamiliesLoading || workflowsLoading || setsLoading || brandsLoading || unitsLoading ;
const isLoading = familyLoading || categoriesLoading || attributesLoading || channelsLoading || assetFamiliesLoading || workflowsLoading || setsLoading || brandsLoading ;
// Selected attributes list for Step 3 Axis Filtering
const selectedAttributesList = attributes . filter ( attr = >
@@ -687,22 +678,24 @@ export default function NewFamily() {
/ >
< / div >
{ /* Allowed Units */ }
< div className = "col-span-2" >
< label className = { labelClass } > Allowed Units < span className = "text-red-400" > * < / span > < / label >
< SearchableMultiSelect
title = "Allowed Units"
items = { units }
selectedIds = { formik . values . allowedUnits }
onChange = { ( ids ) = > formik . setFieldValue ( 'allowedUnits' , ids ) }
searchPlaceholder = "Search units by name, code or symbol..."
getDisplayInfo = { ( u ) = > ( { id : u.id , name : u.name , code : u.code , badgeText : u.symbol , status : u.status } ) }
error = { formik . touched . allowedUnits && formik . errors . allowedUnits ? String ( formik . errors . allowedUnits ) : undefined }
/ >
< / div >
{ /* Row: Attribute Set Select */ }
{ /* Row 3 — Description last */ }
< div className = "col-span-2" >
< label className = { labelClass } > Description < / label >
< textarea name = "description" value = { formik . values . description } onChange = { formik . handleChange } placeholder = "Describe this product family..." rows = { 4 } className = { ` ${ inputClass } resize-y ` } style = { { minHeight : '120px' } } / >
< / div >
< / div >
< / Card >
) }
{ /* ── Attribute Assignment ── */ }
{ activeTab === 'attributes' && (
< Card >
< CardHeader title = "Attribute Set Blueprint Groups" subtitle = "Attributes are defined by the Attribute Set. Expand groups to enable/disable optional items." / >
< div className = "p-6 space-y-6" >
{ /* Row: Attribute Set Select */ }
< div className = "border-b border-gray-100 pb-5" >
< label className = { labelClass } > Attribute Set < span className = "text-red-400" > * < / span > < / label >
< Select
name = "attributeSetId"
@@ -735,21 +728,6 @@ export default function NewFamily() {
< div className = "text-xs text-red-500 mt-1 font-medium" > { formik . errors . attributeSetId } < / div >
) }
< / div >
{ /* Row 3 — Description last */ }
< div className = "col-span-2" >
< label className = { labelClass } > Description < / label >
< textarea name = "description" value = { formik . values . description } onChange = { formik . handleChange } placeholder = "Describe this product family..." rows = { 4 } className = { ` ${ inputClass } resize-y ` } style = { { minHeight : '120px' } } / >
< / div >
< / div >
< / Card >
) }
{ /* ── Attribute Assignment ── */ }
{ activeTab === 'attributes' && (
< Card >
< CardHeader title = "Attribute Set Blueprint Groups" subtitle = "Attributes are defined by the Attribute Set. Expand groups to enable/disable optional items." / >
< div className = "p-6" >
{ formik . values . attributeSetId ? (
< div className = "space-y-4 max-h-[420px] overflow-y-auto pr-2" >
{ ( ( ) = > {
@@ -913,37 +891,41 @@ export default function NewFamily() {
< / Card >
) }
{ /* ── Default Asset Typ es ── */ }
{ /* ── Asset Famili es ── */ }
{ activeTab === 'assets' && (
< Card >
< CardHeader title = "Default Asset Typ es" subtitle = "Define which asset typ es are expected for products in this family" / >
< CardHeader title = "Asset Famili es" subtitle = "Select which asset famili es are assigned to this product family" / >
< div className = "p-6 space-y-2" >
{ assetTypesList . map ( ( asset ) = > {
const isChecked = formik . values . assetRequirements . includes ( asset . id ) ;
{ assetFamiliesList . map ( ( assetFamily ) = > {
const isChecked = formik . values . assetRequirements . includes ( assetFamily . id ) ;
const handleToggle = ( ) = > {
const current = [ . . . formik . values . assetRequirements ] ;
if ( isChecked ) {
formik . setFieldValue ( 'assetRequirements' , current . filter ( id = > id !== asset . id ) ) ;
formik . setFieldValue ( 'assetRequirements' , current . filter ( id = > id !== assetFamily . id ) ) ;
} else {
formik . setFieldValue ( 'assetRequirements' , [ . . . current , asset . id ] ) ;
formik . setFieldValue ( 'assetRequirements' , [ . . . current , assetFamily . id ] ) ;
}
} ;
return (
< label key = { asset . id } onClick = { handleToggle } className = { ` flex items-center justify-between px-4 py-3.5 rounded-lg border cursor-pointer transition-all ${ isChecked ? 'border-primary/20 bg-primary/5/40 shadow-sm' : 'border-gray-100 hover:border-primary/10 hover:bg-primary/5/20'
< label key = { assetFamily . id } onClick = { handleToggle } className = { ` flex items-center justify-between px-4 py-3.5 rounded-lg border cursor-pointer transition-all ${ isChecked ? 'border-primary/20 bg-primary/5/40 shadow-sm' : 'border-gray-100 hover:border-primary/10 hover:bg-primary/5/20'
} ` } >
< div className = "flex items-center gap-4" >
< input type = "checkbox" checked = { isChecked } readOnly className = "w-4 h-4 text-primary rounded border-gray-300 focus:ring-primary-light" / >
< div >
< div className = "font-medium text-sm text-gray-900" > { asset . name } < / div >
{ asset . isRequired && < div className = "text-xs text-red-400 mt-0.5" > Required Asset < / div > }
< div className = "font-medium text-sm text-gray-900" > { assetFamily . name } < / div >
{ assetFamily . assetTypes && assetFamily . assetTypes . length > 0 && (
< div className = "text-xs text-gray-400 mt-0.5" >
Expected Types : { assetFamily . assetTypes . map ( ( t : any ) = > t . name ) . join ( ', ' ) }
< / div >
) }
< / div >
< / div >
{ isChecked && < CheckCircle2 className = "w-4 h-4 text-primary" / > }
< / label >
) ;
} ) }
{ assetTypesList . length === 0 && (
< div className = "text-center py-8 text-gray-400 text-sm" > No asset types available . < / div >
{ assetFamiliesList . length === 0 && (
< div className = "text-center py-8 text-gray-400 text-sm" > No asset families available . < / div >
) }
< / div >
< / Card >
@@ -1163,7 +1145,7 @@ export default function NewFamily() {
< / div >
< span className = "text-gray-400 font-bold" > ➔ < / span >
< div className = "px-3 py-1.5 bg-rose-50 border border-rose-200 rounded-lg shadow-xs" >
🖼 ️ { formik . values . assetRequirements . length } Asset Types
🖼 ️ { formik . values . assetRequirements . length } Asset Families
< / div >
< span className = "text-gray-400 font-bold" > ➔ < / span >
< div className = "px-3 py-1.5 bg-teal-50 border border-teal-200 rounded-lg shadow-xs" >
@@ -1187,7 +1169,7 @@ export default function NewFamily() {
{ icon : FileText , title : 'Total Attributes' , value : ` ${ formik . values . attributes . length } Fields ` , desc : 'Inherited EAV schema fields' } ,
{ icon : Tags , title : 'Variant Strategy' , value : ` ${ selectedAttributesList . length } Axes ` , desc : selectedAttributesList.map ( a = > a . name ) . join ( ', ' ) || 'No variants' } ,
{ icon : Globe , title : 'Allowed Channels' , value : ` ${ formik . values . channels . length } Mappings ` , desc : formik.values.channels.join ( ', ' ) || 'No channels selected' } ,
{ icon : ImageIcon , title : 'Media Requirements' , value : ` ${ formik . values . assetRequirements . length } Asset Typ es ` , desc : 'Enforced for completeness' } ,
{ icon : ImageIcon , title : 'Media Requirements' , value : ` ${ formik . values . assetRequirements . length } Asset Famili es ` , desc : 'Enforced for completeness' } ,
{ icon : Settings2 , title : 'Lifecycle Workflow' , value : workflowsList.find ( w = > w . code === selectedWorkflow ) ? . name || selectedWorkflow , desc : 'Products lifecycle flow' } ,
{ icon : Info , title : 'Completeness Rules' , value : ` ${ Object . keys ( formik . values . completenessRules ) . length } Rules ` , desc : 'Auto-calculated criteria' }
] . map ( ( { icon : Icon , title , value , desc } , i ) = > (