diff --git a/src/features/family/pages/NewFamily.tsx b/src/features/family/pages/NewFamily.tsx
index c81422a..ffede8b 100644
--- a/src/features/family/pages/NewFamily.tsx
+++ b/src/features/family/pages/NewFamily.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback, useMemo } from 'react';
+import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import apiClient from '../../../api/axiosInstance';
import { useFamily } from '../hook/useFamily';
@@ -6,14 +6,16 @@ import { useAttribute } from '../../attributes/hook/useAttribute';
import { useChannel } from '../../channels/hook/useChannel';
import { useWorkflow } from '../../workflow/hook/useWorkflow';
import { StageTimeline } from '../../workflow/components/StageTimeline';
-import { Search, X, Layers, Activity } from 'lucide-react';
+import { Search, X, Layers, Activity, Plus, Loader2, ChevronDown } from 'lucide-react';
import { useUnit } from '../../units/hook/useUnit';
import { useAttributeSet } from '../../attribute-sets/hook/useAttributeSet';
+import { useAttributeGroup } from '../../attribute-groups/hook/useAttributeGroup';
+import { attributeGroupsService } from '../../attribute-groups/services/attribute-groups.service';
import { useAssetFamily } from '../../asset-families/hook/useAssetFamily';
import { useBrand } from '../../brands/hook/useBrand';
import {
FileText, LayoutGrid, Tags, Globe, Eye, Settings2, Save,
- CheckCircle2, AlertCircle, CheckSquare, Image as ImageIcon, Check,
+ CheckCircle2, AlertCircle, Image as ImageIcon, Check,
Box, Info
} from 'lucide-react';
import { Breadcrumb } from "../../../components/layouts/Breadcrumb";
@@ -35,8 +37,7 @@ const TABS = [
{ id: 'channels', label: 'Allowed Channels', icon: Globe, step: 4 },
{ id: 'assets', label: 'Asset Families', 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 },
+ { id: 'summary', label: 'Inheritance Summary', icon: Eye, step: 7 },
];
const inputClass = "w-full border border-primary/10 focus:ring-primary-light rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:border-transparent bg-surface placeholder-muted-foreground";
@@ -75,7 +76,8 @@ export default function NewFamily() {
const isEdit = Boolean(id) && !isView;
const { createFamily, updateFamily } = useFamily();
- const { attributes, fetchAttributes, loading: attributesLoading } = useAttribute();
+ const { attributes, fetchAttributes, createAttribute, loading: attributesLoading } = useAttribute();
+ const { items: attributeGroupsList, fetchItems: fetchAttributeGroups } = useAttributeGroup();
const { items: channelsList, fetchItems: fetchChannels, loading: channelsLoading } = useChannel();
const { items: assetFamiliesList, fetchItems: fetchAssetFamilies, loading: assetFamiliesLoading } = useAssetFamily();
const { items: workflowsList, fetchItems: fetchWorkflows, loading: workflowsLoading } = useWorkflow();
@@ -95,6 +97,54 @@ export default function NewFamily() {
const [expandedGroups, setExpandedGroups] = useState
>({});
const [submitError, setSubmitError] = useState(null);
+ // Step 2 Filtering & Inline Quick Create Attribute state
+ const [selectedGroupFilter, setSelectedGroupFilter] = useState('');
+ const [attributeSearchQuery, setAttributeSearchQuery] = useState('');
+
+ // Searchable Attribute Set Dropdown state & ref
+ const [isAttributeSetDropdownOpen, setIsAttributeSetDropdownOpen] = useState(false);
+ const [attributeSetSearchQuery, setAttributeSetSearchQuery] = useState('');
+ const attributeSetDropdownRef = useRef(null);
+
+ // Step 5 Asset Families Search & Dropdown state & ref
+ const [assetFamilySearchQuery, setAssetFamilySearchQuery] = useState('');
+ const [isAssetFamilyDropdownOpen, setIsAssetFamilyDropdownOpen] = useState(false);
+ const assetFamilyDropdownRef = useRef(null);
+
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (attributeSetDropdownRef.current && !attributeSetDropdownRef.current.contains(event.target as Node)) {
+ setIsAttributeSetDropdownOpen(false);
+ }
+ if (assetFamilyDropdownRef.current && !assetFamilyDropdownRef.current.contains(event.target as Node)) {
+ setIsAssetFamilyDropdownOpen(false);
+ }
+ }
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ const filteredAttributeSets = useMemo(() => {
+ if (!attributeSetSearchQuery.trim()) return attributeSetsList;
+ const q = attributeSetSearchQuery.toLowerCase().trim();
+ return attributeSetsList.filter((s: any) =>
+ (s.name && s.name.toLowerCase().includes(q)) ||
+ (s.code && s.code.toLowerCase().includes(q))
+ );
+ }, [attributeSetsList, attributeSetSearchQuery]);
+
+
+
+ const [showAttributeModal, setShowAttributeModal] = useState(false);
+ const [inlineAttributeName, setInlineAttributeName] = useState('');
+ const [inlineAttributeType, setInlineAttributeType] = useState('text');
+ const [inlineAttributeRequired, setInlineAttributeRequired] = useState(false);
+ const [inlineAttributeOptions, setInlineAttributeOptions] = useState('');
+ const [inlineAttributeGroupId, setInlineAttributeGroupId] = useState('');
+ const [inlineAttributeSubmitting, setInlineAttributeSubmitting] = useState(false);
+
// Maps form fields to which tab they live on for auto-navigation
const FIELD_TAB_MAP: Record = {
name: 'basic', code: 'basic', status: 'basic',
@@ -103,7 +153,6 @@ export default function NewFamily() {
variantAxes: 'variants',
channels: 'channels',
assetRequirements: 'assets',
- completenessRules: 'rules',
};
@@ -157,6 +206,16 @@ export default function NewFamily() {
onSubmit: async (values, { setSubmitting }) => {
setSubmitError(null);
+ // Validate completeness rules sum to 100%
+ const rulesSum = Object.values(values.completenessRules || {}).reduce((sum, v) => sum + (Number(v) || 0), 0);
+ if (rulesSum !== 100) {
+ const msg = `Completeness rules weights must equal 100%. Current sum: ${rulesSum}%`;
+ notify.error(msg);
+ setSubmitError(msg);
+ setSubmitting(false);
+ return;
+ }
+
// Validate the full form first and navigate to first tab with an error
const errors = await formik.validateForm();
const errorFields = Object.keys(errors);
@@ -215,13 +274,14 @@ export default function NewFamily() {
// Fetch dynamic lookup lists on mount
useEffect(() => {
fetchAttributes();
+ fetchAttributeGroups();
fetchChannels();
fetchAssetFamilies();
fetchWorkflows();
fetchAttributeSets();
fetchBrands();
fetchUnits();
- }, [fetchAttributes, fetchChannels, fetchAssetFamilies, fetchWorkflows, fetchAttributeSets, fetchBrands, fetchUnits]);
+ }, [fetchAttributes, fetchAttributeGroups, fetchChannels, fetchAssetFamilies, fetchWorkflows, fetchAttributeSets, fetchBrands, fetchUnits]);
// Load family details in Edit mode
useEffect(() => {
@@ -298,6 +358,34 @@ export default function NewFamily() {
const activeIndex = TABS.findIndex(t => t.id === activeTab);
const isLoading = familyLoading || attributesLoading || channelsLoading || assetFamiliesLoading || workflowsLoading || setsLoading || brandsLoading || unitsLoading;
+ const familyCompleteness = useMemo(() => {
+ let score = 0;
+ if (formik.values.name && formik.values.code) score += 20;
+ if (formik.values.category) score += 20;
+ if (formik.values.attributeSetId) score += 20;
+ if (formik.values.channels && formik.values.channels.length > 0) score += 20;
+ if (formik.values.assetRequirements && formik.values.assetRequirements.length > 0) score += 20;
+ return score;
+ }, [formik.values.name, formik.values.code, formik.values.category, formik.values.attributeSetId, formik.values.channels, formik.values.assetRequirements]);
+
+ const assignedAssetFamiliesList = useMemo(() => {
+ const assignedIds = new Set(formik.values.assetRequirements || []);
+ return assetFamiliesList.filter((af: any) => assignedIds.has(af.id));
+ }, [assetFamiliesList, formik.values.assetRequirements]);
+
+ const filteredUnassignedAssetFamilies = useMemo(() => {
+ const assignedIds = new Set(formik.values.assetRequirements || []);
+ let unassigned = assetFamiliesList.filter((af: any) => !assignedIds.has(af.id));
+ if (assetFamilySearchQuery.trim()) {
+ const q = assetFamilySearchQuery.toLowerCase().trim();
+ unassigned = unassigned.filter((af: any) =>
+ (af.name && af.name.toLowerCase().includes(q)) ||
+ (af.code && af.code.toLowerCase().includes(q))
+ );
+ }
+ return unassigned;
+ }, [assetFamiliesList, formik.values.assetRequirements, assetFamilySearchQuery]);
+
// Selected attributes list for Step 3 Axis Filtering
const selectedAttributesList = useMemo(() => {
const selectedIds = new Set(formik.values.attributes || []);
@@ -402,6 +490,16 @@ export default function NewFamily() {
const handleSaveFamily = async () => {
setSubmitError(null);
+
+ // Validate completeness rules sum to 100%
+ const rulesSum = Object.values(formik.values.completenessRules || {}).reduce((sum, v) => sum + (Number(v) || 0), 0);
+ if (rulesSum !== 100) {
+ const msg = `Completeness rules weights must equal 100%. Current sum: ${rulesSum}%`;
+ notify.error(msg);
+ setSubmitError(msg);
+ return;
+ }
+
const errors = await formik.validateForm();
const errorFields = Object.keys(errors);
@@ -561,6 +659,47 @@ export default function NewFamily() {
);
})}
+
+ {/* Completeness Score */}
+
+
Completeness
+
+
+
+
+
+ {familyCompleteness}%
+
+
+
+
+
+ {[
+ { label: 'Variants', value: String(formik.values.variantAxes?.length || 0) },
+ { label: 'Assets', value: String(formik.values.assetRequirements?.length || 0) },
+ { label: 'Category', value: formik.values.category ? '1' : '0' },
+ { label: 'Channels', value: String(formik.values.channels?.length || 0) },
+ ].map(({ label, value }) => (
+
+ {label}
+ {value}
+
+ ))}
+
+
{/* Content */}
@@ -686,55 +825,215 @@ export default function NewFamily() {
{/* ── Attribute Assignment ── */}
{activeTab === 'attributes' && (
-
+
- {/* Row: Attribute Set Select */}
-
-
-
);
diff --git a/src/features/product/pages/NewProduct.tsx b/src/features/product/pages/NewProduct.tsx
index 005b355..e81f896 100644
--- a/src/features/product/pages/NewProduct.tsx
+++ b/src/features/product/pages/NewProduct.tsx
@@ -1378,10 +1378,12 @@ export default function NewProduct() {
{[
{ label: 'Variants', value: String(product?.variants?.length || 0) },
- { label: 'Assets', value: String(
- (product?.productAssets?.length || 0) +
- (product?.variants || []).reduce((s: number, v: any) => s + (v.images?.length ?? 0), 0)
- )},
+ {
+ label: 'Assets', value: String(
+ (product?.productAssets?.length || 0) +
+ (product?.variants || []).reduce((s: number, v: any) => s + (v.images?.length ?? 0), 0)
+ )
+ },
{ label: 'Category', value: formik.values.category ? '1' : '0' },
{ label: 'Channels', value: String((formik.values.metadata?.channels?.length || 0) + (family?.channels?.length || 0)) },
].map(({ label, value }) => (