sku auto collapse
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { permissions } from "@mrp/shared";
|
import { permissions } from "@mrp/shared";
|
||||||
import type { InventorySkuCatalogTreeDto, InventorySkuFamilyInput, InventorySkuNodeDto, InventorySkuNodeInput } from "@mrp/shared/dist/inventory/types.js";
|
import type { InventorySkuCatalogTreeDto, InventorySkuFamilyInput, InventorySkuNodeInput } from "@mrp/shared/dist/inventory/types.js";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -31,6 +31,7 @@ export function InventorySkuMasterPage() {
|
|||||||
const [familyForm, setFamilyForm] = useState<InventorySkuFamilyInput>(emptyFamilyForm);
|
const [familyForm, setFamilyForm] = useState<InventorySkuFamilyInput>(emptyFamilyForm);
|
||||||
const [nodeForm, setNodeForm] = useState<InventorySkuNodeInput>(emptyNodeForm);
|
const [nodeForm, setNodeForm] = useState<InventorySkuNodeInput>(emptyNodeForm);
|
||||||
const [selectedFamilyId, setSelectedFamilyId] = useState("");
|
const [selectedFamilyId, setSelectedFamilyId] = useState("");
|
||||||
|
const [expandedNodeIds, setExpandedNodeIds] = useState<string[]>([]);
|
||||||
const [status, setStatus] = useState("Loading SKU master...");
|
const [status, setStatus] = useState("Loading SKU master...");
|
||||||
|
|
||||||
const canManage = user?.permissions.includes(permissions.inventoryWrite) ?? false;
|
const canManage = user?.permissions.includes(permissions.inventoryWrite) ?? false;
|
||||||
@@ -46,6 +47,7 @@ export function InventorySkuMasterPage() {
|
|||||||
setCatalog(nextCatalog);
|
setCatalog(nextCatalog);
|
||||||
const firstFamilyId = nextCatalog.families[0]?.id ?? "";
|
const firstFamilyId = nextCatalog.families[0]?.id ?? "";
|
||||||
setSelectedFamilyId((current) => current || firstFamilyId);
|
setSelectedFamilyId((current) => current || firstFamilyId);
|
||||||
|
setExpandedNodeIds([]);
|
||||||
setNodeForm((current) => ({
|
setNodeForm((current) => ({
|
||||||
...current,
|
...current,
|
||||||
familyId: current.familyId || firstFamilyId,
|
familyId: current.familyId || firstFamilyId,
|
||||||
@@ -70,39 +72,66 @@ export function InventorySkuMasterPage() {
|
|||||||
[familyNodes]
|
[familyNodes]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function toggleNode(nodeId: string) {
|
||||||
|
setExpandedNodeIds((current) => (current.includes(nodeId) ? current.filter((id) => id !== nodeId) : [...current, nodeId]));
|
||||||
|
}
|
||||||
|
|
||||||
function renderNodes(parentNodeId: string | null, depth = 0): ReactNode {
|
function renderNodes(parentNodeId: string | null, depth = 0): ReactNode {
|
||||||
const branchNodes = familyNodes.filter((node) => node.parentNodeId === parentNodeId);
|
const branchNodes = familyNodes.filter((node) => node.parentNodeId === parentNodeId);
|
||||||
if (!branchNodes.length) {
|
if (!branchNodes.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return branchNodes.map((node) => (
|
return branchNodes.map((node) => {
|
||||||
<div key={node.id} className="space-y-2">
|
const isExpanded = expandedNodeIds.includes(node.id);
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3" style={{ marginLeft: `${depth * 16}px` }}>
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
return (
|
||||||
<div>
|
<div key={node.id} className="space-y-2">
|
||||||
<div className="text-sm font-semibold text-text">{node.code} <span className="text-muted">- {node.label}</span></div>
|
<div className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3" style={{ marginLeft: `${depth * 16}px` }}>
|
||||||
<div className="mt-1 text-xs text-muted">Level {node.level} • {node.childCount} child branch(es)</div>
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{node.childCount > 0 ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleNode(node.id)}
|
||||||
|
className="inline-flex h-7 w-7 items-center justify-center rounded-xl border border-line/70 text-sm font-semibold text-text transition hover:bg-page/80"
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
aria-label={`${isExpanded ? "Collapse" : "Expand"} ${node.code}`}
|
||||||
|
>
|
||||||
|
{isExpanded ? "-" : "+"}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex h-7 w-7 items-center justify-center rounded-xl border border-dashed border-line/50 text-xs text-muted">
|
||||||
|
o
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-sm font-semibold text-text">{node.code} <span className="text-muted">- {node.label}</span></div>
|
||||||
|
<div className="mt-1 text-xs text-muted">Level {node.level} • {node.childCount} child branch(es)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setNodeForm((current) => ({
|
||||||
|
...current,
|
||||||
|
familyId: selectedFamilyId,
|
||||||
|
parentNodeId: node.id,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="rounded-2xl border border-line/70 px-2 py-2 text-xs font-semibold text-text"
|
||||||
|
>
|
||||||
|
Add child
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
{node.description ? <div className="mt-2 text-xs text-muted">{node.description}</div> : null}
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
setNodeForm((current) => ({
|
|
||||||
...current,
|
|
||||||
familyId: selectedFamilyId,
|
|
||||||
parentNodeId: node.id,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="rounded-2xl border border-line/70 px-2 py-2 text-xs font-semibold text-text"
|
|
||||||
>
|
|
||||||
Add child
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{node.description ? <div className="mt-2 text-xs text-muted">{node.description}</div> : null}
|
{isExpanded ? renderNodes(node.id, depth + 1) : null}
|
||||||
</div>
|
</div>
|
||||||
{renderNodes(node.id, depth + 1)}
|
);
|
||||||
</div>
|
});
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reloadCatalog() {
|
async function reloadCatalog() {
|
||||||
@@ -127,6 +156,7 @@ export function InventorySkuMasterPage() {
|
|||||||
const created = await api.createInventorySkuFamily(token, familyForm);
|
const created = await api.createInventorySkuFamily(token, familyForm);
|
||||||
setFamilyForm(emptyFamilyForm);
|
setFamilyForm(emptyFamilyForm);
|
||||||
setSelectedFamilyId(created.id);
|
setSelectedFamilyId(created.id);
|
||||||
|
setExpandedNodeIds([]);
|
||||||
setNodeForm((current) => ({ ...current, familyId: created.id, parentNodeId: null }));
|
setNodeForm((current) => ({ ...current, familyId: created.id, parentNodeId: null }));
|
||||||
await reloadCatalog();
|
await reloadCatalog();
|
||||||
setStatus(`Created SKU family ${created.code}.`);
|
setStatus(`Created SKU family ${created.code}.`);
|
||||||
@@ -143,6 +173,13 @@ export function InventorySkuMasterPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const created = await api.createInventorySkuNode(token, nodeForm);
|
const created = await api.createInventorySkuNode(token, nodeForm);
|
||||||
|
setExpandedNodeIds((current) => {
|
||||||
|
if (!created.parentNodeId || current.includes(created.parentNodeId)) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...current, created.parentNodeId];
|
||||||
|
});
|
||||||
setNodeForm((current) => ({
|
setNodeForm((current) => ({
|
||||||
...emptyNodeForm,
|
...emptyNodeForm,
|
||||||
familyId: current.familyId,
|
familyId: current.familyId,
|
||||||
@@ -184,6 +221,7 @@ export function InventorySkuMasterPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedFamilyId(family.id);
|
setSelectedFamilyId(family.id);
|
||||||
|
setExpandedNodeIds([]);
|
||||||
setNodeForm((current) => ({ ...current, familyId: family.id, parentNodeId: null }));
|
setNodeForm((current) => ({ ...current, familyId: family.id, parentNodeId: null }));
|
||||||
}}
|
}}
|
||||||
className={`block w-full rounded-[18px] border px-3 py-3 text-left transition ${
|
className={`block w-full rounded-[18px] border px-3 py-3 text-left transition ${
|
||||||
|
|||||||
Reference in New Issue
Block a user