@pablo2410/shared-ui 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/dist/chunk-JT3XLKKD.js +11 -0
- package/dist/chunk-JT3XLKKD.js.map +1 -0
- package/dist/components/index.d.ts +152 -0
- package/dist/components/index.js +575 -0
- package/dist/components/index.js.map +1 -0
- package/dist/hierarchy/index.d.ts +179 -0
- package/dist/hierarchy/index.js +475 -0
- package/dist/hierarchy/index.js.map +1 -0
- package/dist/hooks/index.d.ts +32 -0
- package/dist/hooks/index.js +41 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/layout/index.d.ts +336 -0
- package/dist/layout/index.js +788 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/primitives/index.d.ts +570 -0
- package/dist/primitives/index.js +5648 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/rbac/index.d.ts +64 -0
- package/dist/rbac/index.js +87 -0
- package/dist/rbac/index.js.map +1 -0
- package/dist/reporting/index.d.ts +116 -0
- package/dist/reporting/index.js +258 -0
- package/dist/reporting/index.js.map +1 -0
- package/dist/theme/index.d.ts +94 -0
- package/dist/theme/index.js +82 -0
- package/dist/theme/index.js.map +1 -0
- package/package.json +119 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @oplytics/shared-ui/hierarchy — Org Hierarchy Types & Utilities
|
|
6
|
+
*
|
|
7
|
+
* Hierarchy: Enterprise > Business Unit > Site > Area > Asset
|
|
8
|
+
*
|
|
9
|
+
* Single source of truth for org-hierarchy types used by:
|
|
10
|
+
* - Portal (direct DB via tRPC)
|
|
11
|
+
* - All subdomains (via Portal Service API)
|
|
12
|
+
*
|
|
13
|
+
* The canonical data shape matches the Portal Service API response:
|
|
14
|
+
* GET /api/service/hierarchy
|
|
15
|
+
*/
|
|
16
|
+
interface OrgHierarchyNode {
|
|
17
|
+
id: number;
|
|
18
|
+
name: string;
|
|
19
|
+
code?: string;
|
|
20
|
+
}
|
|
21
|
+
interface OrgHierarchyEnterprise extends OrgHierarchyNode {
|
|
22
|
+
}
|
|
23
|
+
interface OrgHierarchyBusinessUnit extends OrgHierarchyNode {
|
|
24
|
+
enterpriseId: number;
|
|
25
|
+
}
|
|
26
|
+
interface OrgHierarchySite extends OrgHierarchyNode {
|
|
27
|
+
enterpriseId: number;
|
|
28
|
+
businessUnitId: number;
|
|
29
|
+
location?: string | null;
|
|
30
|
+
}
|
|
31
|
+
interface OrgHierarchyArea extends OrgHierarchyNode {
|
|
32
|
+
siteId: number;
|
|
33
|
+
}
|
|
34
|
+
interface OrgHierarchyAsset extends OrgHierarchyNode {
|
|
35
|
+
siteId: number;
|
|
36
|
+
areaId: number;
|
|
37
|
+
assetType?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Canonical flat hierarchy payload.
|
|
41
|
+
* Every consumer (Portal tRPC, Service API REST) must return this shape.
|
|
42
|
+
*/
|
|
43
|
+
interface OrgHierarchyData {
|
|
44
|
+
enterprises: OrgHierarchyEnterprise[];
|
|
45
|
+
businessUnits: OrgHierarchyBusinessUnit[];
|
|
46
|
+
sites: OrgHierarchySite[];
|
|
47
|
+
areas: OrgHierarchyArea[];
|
|
48
|
+
assets: OrgHierarchyAsset[];
|
|
49
|
+
}
|
|
50
|
+
interface OrgHierarchySelection {
|
|
51
|
+
enterprise: OrgHierarchyNode | null;
|
|
52
|
+
businessUnit: OrgHierarchyNode | null;
|
|
53
|
+
site: OrgHierarchyNode | null;
|
|
54
|
+
area: OrgHierarchyNode | null;
|
|
55
|
+
asset: OrgHierarchyNode | null;
|
|
56
|
+
}
|
|
57
|
+
declare const ORG_HIERARCHY_EMPTY: OrgHierarchySelection;
|
|
58
|
+
interface OrgHierarchyContextValue {
|
|
59
|
+
/** Current selection at each level */
|
|
60
|
+
selection: OrgHierarchySelection;
|
|
61
|
+
/** Flat lists filtered by current selection */
|
|
62
|
+
enterprises: OrgHierarchyNode[];
|
|
63
|
+
businessUnits: OrgHierarchyNode[];
|
|
64
|
+
sites: OrgHierarchyNode[];
|
|
65
|
+
areas: OrgHierarchyNode[];
|
|
66
|
+
assets: OrgHierarchyNode[];
|
|
67
|
+
/** Setters — selecting a level clears all levels below it */
|
|
68
|
+
selectEnterprise: (node: OrgHierarchyNode | null) => void;
|
|
69
|
+
selectBusinessUnit: (node: OrgHierarchyNode | null) => void;
|
|
70
|
+
selectSite: (node: OrgHierarchyNode | null) => void;
|
|
71
|
+
selectArea: (node: OrgHierarchyNode | null) => void;
|
|
72
|
+
selectAsset: (node: OrgHierarchyNode | null) => void;
|
|
73
|
+
/** Whether the user can switch enterprises (platform_admin only) */
|
|
74
|
+
canSwitchEnterprise: boolean;
|
|
75
|
+
/** Loading state */
|
|
76
|
+
loading: boolean;
|
|
77
|
+
/** Breadcrumb trail as an array of { level, node } */
|
|
78
|
+
breadcrumbs: {
|
|
79
|
+
level: string;
|
|
80
|
+
node: OrgHierarchyNode;
|
|
81
|
+
}[];
|
|
82
|
+
}
|
|
83
|
+
declare const ORG_HIERARCHY_LEVELS: readonly ["enterprise", "businessUnit", "site", "area", "asset"];
|
|
84
|
+
type OrgHierarchyLevel = (typeof ORG_HIERARCHY_LEVELS)[number];
|
|
85
|
+
/** Human-readable labels for each level */
|
|
86
|
+
declare const ORG_HIERARCHY_LABELS: Record<OrgHierarchyLevel, string>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Enterprise Override — shared helper for platform_admin enterprise switching.
|
|
90
|
+
*
|
|
91
|
+
* When a platform_admin switches enterprise in the Portal UI, a cookie
|
|
92
|
+
* `oplytics-active-enterprise` is set with the selected enterprise ID.
|
|
93
|
+
* This cookie is shared across all *.oplytics.digital subdomains.
|
|
94
|
+
*
|
|
95
|
+
* Each subdomain's createContext should call applyEnterpriseOverride()
|
|
96
|
+
* to override the user's DB-stored enterpriseId with the cookie value.
|
|
97
|
+
*
|
|
98
|
+
* Used by: Portal, SQDCP, OEE Manager, Action Manager, Business Hub, Connect
|
|
99
|
+
*/
|
|
100
|
+
/**
|
|
101
|
+
* Cookie name for the platform_admin active enterprise override.
|
|
102
|
+
* Shared across all *.oplytics.digital subdomains via the .oplytics.digital domain.
|
|
103
|
+
*/
|
|
104
|
+
declare const ACTIVE_ENTERPRISE_COOKIE = "oplytics-active-enterprise";
|
|
105
|
+
/**
|
|
106
|
+
* Apply the enterprise override for platform_admin users.
|
|
107
|
+
*
|
|
108
|
+
* If the user is a platform_admin and has the active enterprise cookie set,
|
|
109
|
+
* returns a shallow copy of the user with the overridden enterpriseId.
|
|
110
|
+
* Otherwise returns the user unchanged.
|
|
111
|
+
*
|
|
112
|
+
* @param user - The authenticated user (or null)
|
|
113
|
+
* @param cookieHeader - The raw Cookie header string from the request
|
|
114
|
+
* @returns The user with potentially overridden enterpriseId
|
|
115
|
+
*/
|
|
116
|
+
declare function applyEnterpriseOverride<T extends {
|
|
117
|
+
role: string;
|
|
118
|
+
enterpriseId?: number | null;
|
|
119
|
+
}>(user: T | null, cookieHeader: string): T | null;
|
|
120
|
+
|
|
121
|
+
interface CreateOrgHierarchyContextOptions {
|
|
122
|
+
/**
|
|
123
|
+
* Unique sessionStorage key for this subdomain's selection persistence.
|
|
124
|
+
* e.g. "oplytics-portal-org-hierarchy", "oplytics-sqdcp-org-hierarchy"
|
|
125
|
+
*/
|
|
126
|
+
storageKey: string;
|
|
127
|
+
/**
|
|
128
|
+
* React hook that fetches the flat hierarchy data.
|
|
129
|
+
* Must return { data, isLoading } where data is OrgHierarchyData | null.
|
|
130
|
+
*/
|
|
131
|
+
useHierarchyData: () => {
|
|
132
|
+
data: OrgHierarchyData | null;
|
|
133
|
+
isLoading: boolean;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
declare function createOrgHierarchyContext(options: CreateOrgHierarchyContextOptions): {
|
|
137
|
+
OrgHierarchyProvider: ({ children, canSwitchEnterprise, onEnterpriseSwitched, }: {
|
|
138
|
+
children: ReactNode;
|
|
139
|
+
canSwitchEnterprise?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Callback fired when a platform_admin switches enterprise.
|
|
142
|
+
* Used to persist the switch to the backend (e.g. via auth.switchEnterprise mutation).
|
|
143
|
+
* Only called when canSwitchEnterprise is true and the user explicitly changes enterprise.
|
|
144
|
+
*/
|
|
145
|
+
onEnterpriseSwitched?: (enterpriseId: number | null) => void;
|
|
146
|
+
}) => react_jsx_runtime.JSX.Element;
|
|
147
|
+
useOrgHierarchy: () => OrgHierarchyContextValue;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
interface DropdownRenderProps {
|
|
151
|
+
trigger: ReactNode;
|
|
152
|
+
items: Array<{
|
|
153
|
+
key: number;
|
|
154
|
+
label: string;
|
|
155
|
+
code?: string;
|
|
156
|
+
icon: React.ElementType;
|
|
157
|
+
isActive: boolean;
|
|
158
|
+
onClick: () => void;
|
|
159
|
+
}>;
|
|
160
|
+
align?: "start" | "end";
|
|
161
|
+
}
|
|
162
|
+
interface HierarchyNavigatorProps {
|
|
163
|
+
/** The hierarchy context value — pass from useOrgHierarchy() */
|
|
164
|
+
hierarchy: OrgHierarchyContextValue;
|
|
165
|
+
/** Show in compact mode (smaller text, tighter spacing) */
|
|
166
|
+
compact?: boolean;
|
|
167
|
+
/** Maximum depth to show (default: all 5 levels) */
|
|
168
|
+
maxDepth?: number;
|
|
169
|
+
/** CSS class for the container */
|
|
170
|
+
className?: string;
|
|
171
|
+
/**
|
|
172
|
+
* Render prop for dropdown menus. If not provided, dropdowns won't open.
|
|
173
|
+
* Consuming apps should pass a function that renders their shadcn/ui DropdownMenu.
|
|
174
|
+
*/
|
|
175
|
+
renderDropdown?: (props: DropdownRenderProps) => ReactNode;
|
|
176
|
+
}
|
|
177
|
+
declare function HierarchyNavigator({ hierarchy: ctx, compact, maxDepth, className, renderDropdown, }: HierarchyNavigatorProps): react_jsx_runtime.JSX.Element;
|
|
178
|
+
|
|
179
|
+
export { ACTIVE_ENTERPRISE_COOKIE, type CreateOrgHierarchyContextOptions, type DropdownRenderProps, HierarchyNavigator, type HierarchyNavigatorProps, ORG_HIERARCHY_EMPTY, ORG_HIERARCHY_LABELS, ORG_HIERARCHY_LEVELS, type OrgHierarchyArea, type OrgHierarchyAsset, type OrgHierarchyBusinessUnit, type OrgHierarchyContextValue, type OrgHierarchyData, type OrgHierarchyEnterprise, type OrgHierarchyLevel, type OrgHierarchyNode, type OrgHierarchySelection, type OrgHierarchySite, applyEnterpriseOverride, createOrgHierarchyContext };
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cn
|
|
3
|
+
} from "../chunk-JT3XLKKD.js";
|
|
4
|
+
|
|
5
|
+
// src/hierarchy/orgHierarchy.ts
|
|
6
|
+
var ORG_HIERARCHY_EMPTY = {
|
|
7
|
+
enterprise: null,
|
|
8
|
+
businessUnit: null,
|
|
9
|
+
site: null,
|
|
10
|
+
area: null,
|
|
11
|
+
asset: null
|
|
12
|
+
};
|
|
13
|
+
var ORG_HIERARCHY_LEVELS = [
|
|
14
|
+
"enterprise",
|
|
15
|
+
"businessUnit",
|
|
16
|
+
"site",
|
|
17
|
+
"area",
|
|
18
|
+
"asset"
|
|
19
|
+
];
|
|
20
|
+
var ORG_HIERARCHY_LABELS = {
|
|
21
|
+
enterprise: "Enterprise",
|
|
22
|
+
businessUnit: "Business Unit",
|
|
23
|
+
site: "Site",
|
|
24
|
+
area: "Area",
|
|
25
|
+
asset: "Asset"
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// src/hierarchy/enterpriseOverride.ts
|
|
29
|
+
var ACTIVE_ENTERPRISE_COOKIE = "oplytics-active-enterprise";
|
|
30
|
+
function parseCookieValue(cookieHeader, name) {
|
|
31
|
+
const match = cookieHeader.split(";").map((c) => c.trim()).find((c) => c.startsWith(`${name}=`));
|
|
32
|
+
if (!match) return null;
|
|
33
|
+
return decodeURIComponent(match.slice(name.length + 1));
|
|
34
|
+
}
|
|
35
|
+
function applyEnterpriseOverride(user, cookieHeader) {
|
|
36
|
+
if (!user || user.role !== "platform_admin") return user;
|
|
37
|
+
const activeEnterprise = parseCookieValue(
|
|
38
|
+
cookieHeader,
|
|
39
|
+
ACTIVE_ENTERPRISE_COOKIE
|
|
40
|
+
);
|
|
41
|
+
if (!activeEnterprise) return user;
|
|
42
|
+
const parsedId = parseInt(activeEnterprise, 10);
|
|
43
|
+
if (isNaN(parsedId) || parsedId <= 0) return user;
|
|
44
|
+
return { ...user, enterpriseId: parsedId };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/hierarchy/OrgHierarchyContext.tsx
|
|
48
|
+
import {
|
|
49
|
+
createContext,
|
|
50
|
+
useContext,
|
|
51
|
+
useCallback,
|
|
52
|
+
useMemo,
|
|
53
|
+
useState,
|
|
54
|
+
useEffect
|
|
55
|
+
} from "react";
|
|
56
|
+
import { jsx } from "react/jsx-runtime";
|
|
57
|
+
function loadPersistedSelection(key) {
|
|
58
|
+
try {
|
|
59
|
+
const raw = sessionStorage.getItem(key);
|
|
60
|
+
if (!raw) return null;
|
|
61
|
+
return JSON.parse(raw);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function persistSelection(key, sel) {
|
|
67
|
+
try {
|
|
68
|
+
sessionStorage.setItem(key, JSON.stringify(sel));
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function createOrgHierarchyContext(options) {
|
|
73
|
+
const { storageKey, useHierarchyData } = options;
|
|
74
|
+
const OrgHierarchyCtx = createContext({
|
|
75
|
+
selection: ORG_HIERARCHY_EMPTY,
|
|
76
|
+
enterprises: [],
|
|
77
|
+
businessUnits: [],
|
|
78
|
+
sites: [],
|
|
79
|
+
areas: [],
|
|
80
|
+
assets: [],
|
|
81
|
+
selectEnterprise: () => {
|
|
82
|
+
},
|
|
83
|
+
selectBusinessUnit: () => {
|
|
84
|
+
},
|
|
85
|
+
selectSite: () => {
|
|
86
|
+
},
|
|
87
|
+
selectArea: () => {
|
|
88
|
+
},
|
|
89
|
+
selectAsset: () => {
|
|
90
|
+
},
|
|
91
|
+
canSwitchEnterprise: false,
|
|
92
|
+
loading: true,
|
|
93
|
+
breadcrumbs: []
|
|
94
|
+
});
|
|
95
|
+
function OrgHierarchyProvider({
|
|
96
|
+
children,
|
|
97
|
+
canSwitchEnterprise = false,
|
|
98
|
+
onEnterpriseSwitched
|
|
99
|
+
}) {
|
|
100
|
+
const [selection, setSelection] = useState(
|
|
101
|
+
() => loadPersistedSelection(storageKey) || ORG_HIERARCHY_EMPTY
|
|
102
|
+
);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
persistSelection(storageKey, selection);
|
|
105
|
+
}, [selection]);
|
|
106
|
+
const { data: hierarchy, isLoading } = useHierarchyData();
|
|
107
|
+
const enterprises = useMemo(() => {
|
|
108
|
+
if (!hierarchy?.enterprises) return [];
|
|
109
|
+
return hierarchy.enterprises.map((e) => ({
|
|
110
|
+
id: e.id,
|
|
111
|
+
name: e.name,
|
|
112
|
+
code: e.code
|
|
113
|
+
}));
|
|
114
|
+
}, [hierarchy]);
|
|
115
|
+
const businessUnits = useMemo(() => {
|
|
116
|
+
if (!hierarchy?.businessUnits || !selection.enterprise) return [];
|
|
117
|
+
return hierarchy.businessUnits.filter((bu) => bu.enterpriseId === selection.enterprise.id).map((bu) => ({ id: bu.id, name: bu.name, code: bu.code }));
|
|
118
|
+
}, [hierarchy, selection.enterprise]);
|
|
119
|
+
const sites = useMemo(() => {
|
|
120
|
+
if (!hierarchy?.sites || !selection.enterprise) return [];
|
|
121
|
+
let filtered = hierarchy.sites.filter(
|
|
122
|
+
(s) => s.enterpriseId === selection.enterprise.id
|
|
123
|
+
);
|
|
124
|
+
if (selection.businessUnit) {
|
|
125
|
+
filtered = filtered.filter(
|
|
126
|
+
(s) => s.businessUnitId === selection.businessUnit.id
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return filtered.map((s) => ({ id: s.id, name: s.name, code: s.code }));
|
|
130
|
+
}, [hierarchy, selection.enterprise, selection.businessUnit]);
|
|
131
|
+
const areas = useMemo(() => {
|
|
132
|
+
if (!hierarchy?.areas || !selection.site) return [];
|
|
133
|
+
return hierarchy.areas.filter((a) => a.siteId === selection.site.id).map((a) => ({ id: a.id, name: a.name, code: a.code }));
|
|
134
|
+
}, [hierarchy, selection.site]);
|
|
135
|
+
const assets = useMemo(() => {
|
|
136
|
+
if (!hierarchy?.assets || !selection.area) return [];
|
|
137
|
+
return hierarchy.assets.filter((a) => a.areaId === selection.area.id).map((a) => ({ id: a.id, name: a.name, code: a.code }));
|
|
138
|
+
}, [hierarchy, selection.area]);
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (isLoading || enterprises.length === 0) return;
|
|
141
|
+
if (!selection.enterprise) {
|
|
142
|
+
if (!canSwitchEnterprise || enterprises.length === 1) {
|
|
143
|
+
setSelection((prev) => ({ ...prev, enterprise: enterprises[0] }));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}, [isLoading, enterprises, selection.enterprise, canSwitchEnterprise]);
|
|
147
|
+
const selectEnterprise = useCallback((node) => {
|
|
148
|
+
setSelection({
|
|
149
|
+
enterprise: node,
|
|
150
|
+
businessUnit: null,
|
|
151
|
+
site: null,
|
|
152
|
+
area: null,
|
|
153
|
+
asset: null
|
|
154
|
+
});
|
|
155
|
+
if (canSwitchEnterprise && onEnterpriseSwitched) {
|
|
156
|
+
onEnterpriseSwitched(node?.id ?? null);
|
|
157
|
+
}
|
|
158
|
+
}, [canSwitchEnterprise, onEnterpriseSwitched]);
|
|
159
|
+
const selectBusinessUnit = useCallback((node) => {
|
|
160
|
+
setSelection((prev) => ({
|
|
161
|
+
...prev,
|
|
162
|
+
businessUnit: node,
|
|
163
|
+
site: null,
|
|
164
|
+
area: null,
|
|
165
|
+
asset: null
|
|
166
|
+
}));
|
|
167
|
+
}, []);
|
|
168
|
+
const selectSite = useCallback((node) => {
|
|
169
|
+
setSelection((prev) => ({
|
|
170
|
+
...prev,
|
|
171
|
+
site: node,
|
|
172
|
+
area: null,
|
|
173
|
+
asset: null
|
|
174
|
+
}));
|
|
175
|
+
}, []);
|
|
176
|
+
const selectArea = useCallback((node) => {
|
|
177
|
+
setSelection((prev) => ({
|
|
178
|
+
...prev,
|
|
179
|
+
area: node,
|
|
180
|
+
asset: null
|
|
181
|
+
}));
|
|
182
|
+
}, []);
|
|
183
|
+
const selectAsset = useCallback((node) => {
|
|
184
|
+
setSelection((prev) => ({
|
|
185
|
+
...prev,
|
|
186
|
+
asset: node
|
|
187
|
+
}));
|
|
188
|
+
}, []);
|
|
189
|
+
const breadcrumbs = useMemo(() => {
|
|
190
|
+
const crumbs = [];
|
|
191
|
+
if (selection.enterprise)
|
|
192
|
+
crumbs.push({ level: "Enterprise", node: selection.enterprise });
|
|
193
|
+
if (selection.businessUnit)
|
|
194
|
+
crumbs.push({ level: "Business Unit", node: selection.businessUnit });
|
|
195
|
+
if (selection.site)
|
|
196
|
+
crumbs.push({ level: "Site", node: selection.site });
|
|
197
|
+
if (selection.area)
|
|
198
|
+
crumbs.push({ level: "Area", node: selection.area });
|
|
199
|
+
if (selection.asset)
|
|
200
|
+
crumbs.push({ level: "Asset", node: selection.asset });
|
|
201
|
+
return crumbs;
|
|
202
|
+
}, [selection]);
|
|
203
|
+
const value = useMemo(
|
|
204
|
+
() => ({
|
|
205
|
+
selection,
|
|
206
|
+
enterprises,
|
|
207
|
+
businessUnits,
|
|
208
|
+
sites,
|
|
209
|
+
areas,
|
|
210
|
+
assets,
|
|
211
|
+
selectEnterprise,
|
|
212
|
+
selectBusinessUnit,
|
|
213
|
+
selectSite,
|
|
214
|
+
selectArea,
|
|
215
|
+
selectAsset,
|
|
216
|
+
canSwitchEnterprise,
|
|
217
|
+
loading: isLoading,
|
|
218
|
+
breadcrumbs
|
|
219
|
+
}),
|
|
220
|
+
[
|
|
221
|
+
selection,
|
|
222
|
+
enterprises,
|
|
223
|
+
businessUnits,
|
|
224
|
+
sites,
|
|
225
|
+
areas,
|
|
226
|
+
assets,
|
|
227
|
+
selectEnterprise,
|
|
228
|
+
selectBusinessUnit,
|
|
229
|
+
selectSite,
|
|
230
|
+
selectArea,
|
|
231
|
+
selectAsset,
|
|
232
|
+
canSwitchEnterprise,
|
|
233
|
+
isLoading,
|
|
234
|
+
breadcrumbs
|
|
235
|
+
]
|
|
236
|
+
);
|
|
237
|
+
return /* @__PURE__ */ jsx(OrgHierarchyCtx.Provider, { value, children });
|
|
238
|
+
}
|
|
239
|
+
function useOrgHierarchy() {
|
|
240
|
+
return useContext(OrgHierarchyCtx);
|
|
241
|
+
}
|
|
242
|
+
return { OrgHierarchyProvider, useOrgHierarchy };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/hierarchy/HierarchyNavigator.tsx
|
|
246
|
+
import { ChevronRight, ChevronDown, Building2, Network, MapPin, LayoutGrid, Box } from "lucide-react";
|
|
247
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
248
|
+
function BreadcrumbLevel({
|
|
249
|
+
config,
|
|
250
|
+
isLast,
|
|
251
|
+
compact,
|
|
252
|
+
renderDropdown
|
|
253
|
+
}) {
|
|
254
|
+
const nodes = config.getNodes();
|
|
255
|
+
const selected = config.getSelected();
|
|
256
|
+
const Icon = config.icon;
|
|
257
|
+
const canSwitch = config.canSwitch !== false;
|
|
258
|
+
const hasMultiple = nodes.length > 1;
|
|
259
|
+
if (!selected) return null;
|
|
260
|
+
if (!hasMultiple || !canSwitch) {
|
|
261
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
262
|
+
/* @__PURE__ */ jsxs(
|
|
263
|
+
"div",
|
|
264
|
+
{
|
|
265
|
+
className: cn(
|
|
266
|
+
"flex items-center gap-1.5 px-2 py-1 rounded-md text-sm",
|
|
267
|
+
"text-[#E2E8F0]",
|
|
268
|
+
compact && "px-1.5 py-0.5 text-xs"
|
|
269
|
+
),
|
|
270
|
+
children: [
|
|
271
|
+
/* @__PURE__ */ jsx2(Icon, { className: cn("shrink-0 text-[#8890A0]", compact ? "h-3 w-3" : "h-3.5 w-3.5") }),
|
|
272
|
+
/* @__PURE__ */ jsx2("span", { className: "truncate max-w-[120px]", children: selected.name })
|
|
273
|
+
]
|
|
274
|
+
}
|
|
275
|
+
),
|
|
276
|
+
!isLast && /* @__PURE__ */ jsx2(ChevronRight, { className: cn("shrink-0 text-[#596475]", compact ? "h-3 w-3" : "h-3.5 w-3.5") })
|
|
277
|
+
] });
|
|
278
|
+
}
|
|
279
|
+
const trigger = /* @__PURE__ */ jsxs(
|
|
280
|
+
"button",
|
|
281
|
+
{
|
|
282
|
+
className: cn(
|
|
283
|
+
"flex items-center gap-1.5 px-2 py-1 rounded-md text-sm",
|
|
284
|
+
"text-[#E2E8F0] hover:bg-[#1E2738] hover:text-white",
|
|
285
|
+
"transition-colors cursor-pointer outline-none",
|
|
286
|
+
"focus-visible:ring-1 focus-visible:ring-[#8C34E9]",
|
|
287
|
+
compact && "px-1.5 py-0.5 text-xs"
|
|
288
|
+
),
|
|
289
|
+
children: [
|
|
290
|
+
/* @__PURE__ */ jsx2(Icon, { className: cn("shrink-0 text-[#8890A0]", compact ? "h-3 w-3" : "h-3.5 w-3.5") }),
|
|
291
|
+
/* @__PURE__ */ jsx2("span", { className: "truncate max-w-[120px]", children: selected.name }),
|
|
292
|
+
/* @__PURE__ */ jsx2(ChevronDown, { className: cn("shrink-0 text-[#596475]", compact ? "h-2.5 w-2.5" : "h-3 w-3") })
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
const items = nodes.map((node) => ({
|
|
297
|
+
key: node.id,
|
|
298
|
+
label: node.name,
|
|
299
|
+
code: node.code,
|
|
300
|
+
icon: Icon,
|
|
301
|
+
isActive: selected.id === node.id,
|
|
302
|
+
onClick: () => config.onSelect(node)
|
|
303
|
+
}));
|
|
304
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
305
|
+
renderDropdown ? renderDropdown({ trigger, items, align: "start" }) : (
|
|
306
|
+
// Fallback: simple select-style rendering
|
|
307
|
+
/* @__PURE__ */ jsx2("div", { className: "relative group", children: trigger })
|
|
308
|
+
),
|
|
309
|
+
!isLast && /* @__PURE__ */ jsx2(ChevronRight, { className: cn("shrink-0 text-[#596475]", compact ? "h-3 w-3" : "h-3.5 w-3.5") })
|
|
310
|
+
] });
|
|
311
|
+
}
|
|
312
|
+
function DrillDownPrompt({
|
|
313
|
+
label,
|
|
314
|
+
icon: Icon,
|
|
315
|
+
nodes,
|
|
316
|
+
onSelect,
|
|
317
|
+
compact,
|
|
318
|
+
renderDropdown
|
|
319
|
+
}) {
|
|
320
|
+
if (nodes.length === 0) return null;
|
|
321
|
+
if (nodes.length === 1) {
|
|
322
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
323
|
+
/* @__PURE__ */ jsx2(ChevronRight, { className: cn("shrink-0 text-[#596475]", compact ? "h-3 w-3" : "h-3.5 w-3.5") }),
|
|
324
|
+
/* @__PURE__ */ jsxs(
|
|
325
|
+
"button",
|
|
326
|
+
{
|
|
327
|
+
onClick: () => onSelect(nodes[0]),
|
|
328
|
+
className: cn(
|
|
329
|
+
"flex items-center gap-1.5 px-2 py-1 rounded-md text-sm",
|
|
330
|
+
"text-[#596475] hover:bg-[#1E2738] hover:text-[#8890A0]",
|
|
331
|
+
"transition-colors cursor-pointer",
|
|
332
|
+
compact && "px-1.5 py-0.5 text-xs"
|
|
333
|
+
),
|
|
334
|
+
children: [
|
|
335
|
+
/* @__PURE__ */ jsx2(Icon, { className: cn("shrink-0", compact ? "h-3 w-3" : "h-3.5 w-3.5") }),
|
|
336
|
+
/* @__PURE__ */ jsx2("span", { className: "truncate", children: nodes[0].name })
|
|
337
|
+
]
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
] });
|
|
341
|
+
}
|
|
342
|
+
const trigger = /* @__PURE__ */ jsxs(
|
|
343
|
+
"button",
|
|
344
|
+
{
|
|
345
|
+
className: cn(
|
|
346
|
+
"flex items-center gap-1.5 px-2 py-1 rounded-md text-sm",
|
|
347
|
+
"text-[#596475] hover:bg-[#1E2738] hover:text-[#8890A0]",
|
|
348
|
+
"transition-colors cursor-pointer outline-none",
|
|
349
|
+
"border border-dashed border-[#2A2A3E] hover:border-[#596475]",
|
|
350
|
+
compact && "px-1.5 py-0.5 text-xs"
|
|
351
|
+
),
|
|
352
|
+
children: [
|
|
353
|
+
/* @__PURE__ */ jsx2(Icon, { className: cn("shrink-0", compact ? "h-3 w-3" : "h-3.5 w-3.5") }),
|
|
354
|
+
/* @__PURE__ */ jsxs("span", { className: "truncate", children: [
|
|
355
|
+
"Select ",
|
|
356
|
+
label
|
|
357
|
+
] }),
|
|
358
|
+
/* @__PURE__ */ jsx2(ChevronDown, { className: cn("shrink-0", compact ? "h-2.5 w-2.5" : "h-3 w-3") })
|
|
359
|
+
]
|
|
360
|
+
}
|
|
361
|
+
);
|
|
362
|
+
const items = nodes.map((node) => ({
|
|
363
|
+
key: node.id,
|
|
364
|
+
label: node.name,
|
|
365
|
+
code: node.code,
|
|
366
|
+
icon: Icon,
|
|
367
|
+
isActive: false,
|
|
368
|
+
onClick: () => onSelect(node)
|
|
369
|
+
}));
|
|
370
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
371
|
+
/* @__PURE__ */ jsx2(ChevronRight, { className: cn("shrink-0 text-[#596475]", compact ? "h-3 w-3" : "h-3.5 w-3.5") }),
|
|
372
|
+
renderDropdown ? renderDropdown({ trigger, items, align: "start" }) : /* @__PURE__ */ jsx2("div", { className: "relative group", children: trigger })
|
|
373
|
+
] });
|
|
374
|
+
}
|
|
375
|
+
function HierarchyNavigator({
|
|
376
|
+
hierarchy: ctx,
|
|
377
|
+
compact = false,
|
|
378
|
+
maxDepth = 5,
|
|
379
|
+
className,
|
|
380
|
+
renderDropdown
|
|
381
|
+
}) {
|
|
382
|
+
if (ctx.loading) {
|
|
383
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2 animate-pulse", className), children: [
|
|
384
|
+
/* @__PURE__ */ jsx2("div", { className: "h-6 w-24 rounded bg-[#1E2738]" }),
|
|
385
|
+
/* @__PURE__ */ jsx2(ChevronRight, { className: "h-3.5 w-3.5 text-[#596475]" }),
|
|
386
|
+
/* @__PURE__ */ jsx2("div", { className: "h-6 w-20 rounded bg-[#1E2738]" })
|
|
387
|
+
] });
|
|
388
|
+
}
|
|
389
|
+
const levels = [
|
|
390
|
+
{
|
|
391
|
+
key: "enterprise",
|
|
392
|
+
label: "Enterprise",
|
|
393
|
+
icon: Building2,
|
|
394
|
+
getNodes: () => ctx.enterprises,
|
|
395
|
+
getSelected: () => ctx.selection.enterprise,
|
|
396
|
+
onSelect: ctx.selectEnterprise,
|
|
397
|
+
canSwitch: ctx.canSwitchEnterprise
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
key: "businessUnit",
|
|
401
|
+
label: "Business Unit",
|
|
402
|
+
icon: Network,
|
|
403
|
+
getNodes: () => ctx.businessUnits,
|
|
404
|
+
getSelected: () => ctx.selection.businessUnit,
|
|
405
|
+
onSelect: ctx.selectBusinessUnit
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
key: "site",
|
|
409
|
+
label: "Site",
|
|
410
|
+
icon: MapPin,
|
|
411
|
+
getNodes: () => ctx.sites,
|
|
412
|
+
getSelected: () => ctx.selection.site,
|
|
413
|
+
onSelect: ctx.selectSite
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
key: "area",
|
|
417
|
+
label: "Area",
|
|
418
|
+
icon: LayoutGrid,
|
|
419
|
+
getNodes: () => ctx.areas,
|
|
420
|
+
getSelected: () => ctx.selection.area,
|
|
421
|
+
onSelect: ctx.selectArea
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
key: "asset",
|
|
425
|
+
label: "Asset",
|
|
426
|
+
icon: Box,
|
|
427
|
+
getNodes: () => ctx.assets,
|
|
428
|
+
getSelected: () => ctx.selection.asset,
|
|
429
|
+
onSelect: ctx.selectAsset
|
|
430
|
+
}
|
|
431
|
+
].slice(0, maxDepth);
|
|
432
|
+
const selectedLevels = levels.filter((l) => l.getSelected() !== null);
|
|
433
|
+
const nextLevel = levels.find((l) => l.getSelected() === null && l.getNodes().length > 0);
|
|
434
|
+
return /* @__PURE__ */ jsxs(
|
|
435
|
+
"nav",
|
|
436
|
+
{
|
|
437
|
+
className: cn("flex items-center flex-wrap gap-0.5", className),
|
|
438
|
+
"aria-label": "Hierarchy navigation",
|
|
439
|
+
children: [
|
|
440
|
+
selectedLevels.map((level, idx) => /* @__PURE__ */ jsx2(
|
|
441
|
+
BreadcrumbLevel,
|
|
442
|
+
{
|
|
443
|
+
config: level,
|
|
444
|
+
isLast: idx === selectedLevels.length - 1 && !nextLevel,
|
|
445
|
+
compact,
|
|
446
|
+
renderDropdown
|
|
447
|
+
},
|
|
448
|
+
level.key
|
|
449
|
+
)),
|
|
450
|
+
nextLevel && /* @__PURE__ */ jsx2(
|
|
451
|
+
DrillDownPrompt,
|
|
452
|
+
{
|
|
453
|
+
label: nextLevel.label,
|
|
454
|
+
icon: nextLevel.icon,
|
|
455
|
+
nodes: nextLevel.getNodes(),
|
|
456
|
+
onSelect: nextLevel.onSelect,
|
|
457
|
+
compact,
|
|
458
|
+
renderDropdown
|
|
459
|
+
}
|
|
460
|
+
),
|
|
461
|
+
selectedLevels.length === 0 && !nextLevel && /* @__PURE__ */ jsx2("span", { className: "text-sm text-[#596475] italic", children: "No hierarchy data available" })
|
|
462
|
+
]
|
|
463
|
+
}
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
export {
|
|
467
|
+
ACTIVE_ENTERPRISE_COOKIE,
|
|
468
|
+
HierarchyNavigator,
|
|
469
|
+
ORG_HIERARCHY_EMPTY,
|
|
470
|
+
ORG_HIERARCHY_LABELS,
|
|
471
|
+
ORG_HIERARCHY_LEVELS,
|
|
472
|
+
applyEnterpriseOverride,
|
|
473
|
+
createOrgHierarchyContext
|
|
474
|
+
};
|
|
475
|
+
//# sourceMappingURL=index.js.map
|