@wipco/sdui 0.27.0 → 0.28.0
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/dist/chunk-KBLFOC37.js +1242 -0
- package/dist/meta.d.ts +3 -1
- package/dist/meta.js +72 -1
- package/dist/widgets/OrgChartBoard.d.ts +72 -0
- package/dist/widgets/OrgChartBoard.js +6 -0
- package/dist/widgets/index.d.ts +4 -1
- package/dist/widgets/index.js +5 -2
- package/package.json +5 -1
|
@@ -0,0 +1,1242 @@
|
|
|
1
|
+
import { ImageLightbox } from './chunk-XPBZMECW.js';
|
|
2
|
+
import { useSduiAdapter, hostFetchOf, defaultAdapter } from './chunk-DVOPZFQS.js';
|
|
3
|
+
import { Badge, Button, IconButton, Icon } from './chunk-WIDDHR7L.js';
|
|
4
|
+
import { boxed } from './chunk-SAN4IX6N.js';
|
|
5
|
+
import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
6
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
var PAGE_SIZE = 500;
|
|
9
|
+
var MAX_PAGES = 20;
|
|
10
|
+
var UNASSIGNED_GROUP_ID = "kc_unassigned_department";
|
|
11
|
+
var BOARD_COLORS = [
|
|
12
|
+
"var(--wp-series-1)",
|
|
13
|
+
"var(--wp-series-2)",
|
|
14
|
+
"var(--wp-series-4)",
|
|
15
|
+
"var(--wp-series-5)",
|
|
16
|
+
"var(--wp-series-3)",
|
|
17
|
+
"var(--wp-series-4)",
|
|
18
|
+
"var(--wp-series-1)",
|
|
19
|
+
"var(--wp-series-3)"
|
|
20
|
+
];
|
|
21
|
+
var CANVAS_PADDING = 80;
|
|
22
|
+
var GROUP_WIDTH = 230;
|
|
23
|
+
var GROUP_HEIGHT = 36;
|
|
24
|
+
var EMPLOYEE_WIDTH = 118;
|
|
25
|
+
var EMPLOYEE_HEIGHT = 46;
|
|
26
|
+
var EMPLOYEE_GAP = 8;
|
|
27
|
+
var ROOT_GAP = 48;
|
|
28
|
+
var CHILD_GAP = 22;
|
|
29
|
+
var TEAM_BOX_WIDTH = 44;
|
|
30
|
+
var TEAM_BOX_PAD = 54;
|
|
31
|
+
var TEAM_BOX_CHAR = 19;
|
|
32
|
+
var TEAM_ROW_INDENT = 28;
|
|
33
|
+
var TEAM_TRUNK_X = 12;
|
|
34
|
+
var TEAM_MEMBER_GAP = 12;
|
|
35
|
+
var TEAM_ROW_GAP = 18;
|
|
36
|
+
var MEMBER_COLUMNS = 2;
|
|
37
|
+
var LEADER_WIDTH = 230;
|
|
38
|
+
var LEADER_HEIGHT = 62;
|
|
39
|
+
var TOP_GAP = 58;
|
|
40
|
+
var ROOT_ROW_GAP = 70;
|
|
41
|
+
var MIN_ZOOM = 0.35;
|
|
42
|
+
var MAX_ZOOM = 1.8;
|
|
43
|
+
var DEFAULT_VIEW = { scale: 0.82, x: 48, y: 48 };
|
|
44
|
+
var DUTY_ORDER = [
|
|
45
|
+
"\uD68C\uC7A5",
|
|
46
|
+
"\uBD80\uD68C\uC7A5",
|
|
47
|
+
"\uB300\uD45C\uC774\uC0AC",
|
|
48
|
+
"\uB300\uD45C",
|
|
49
|
+
"\uC0AC\uC7A5",
|
|
50
|
+
"\uBD80\uC0AC\uC7A5",
|
|
51
|
+
"\uC804\uBB34",
|
|
52
|
+
"\uC0C1\uBB34",
|
|
53
|
+
"\uBCF8\uBD80\uC7A5",
|
|
54
|
+
"\uC2E4\uC7A5",
|
|
55
|
+
"\uD300\uC7A5",
|
|
56
|
+
"\uD30C\uD2B8\uC7A5"
|
|
57
|
+
];
|
|
58
|
+
var GRADE_ORDER = [
|
|
59
|
+
"\uD68C\uC7A5",
|
|
60
|
+
"\uBD80\uD68C\uC7A5",
|
|
61
|
+
"\uB300\uD45C\uC774\uC0AC",
|
|
62
|
+
"\uB300\uD45C",
|
|
63
|
+
"\uC0AC\uC7A5",
|
|
64
|
+
"\uBD80\uC0AC\uC7A5",
|
|
65
|
+
"\uC804\uBB34",
|
|
66
|
+
"\uC0C1\uBB34",
|
|
67
|
+
"\uC774\uC0AC",
|
|
68
|
+
"\uC218\uC11D",
|
|
69
|
+
"\uBD80\uC7A5",
|
|
70
|
+
"\uCC28\uC7A5",
|
|
71
|
+
"\uACFC\uC7A5",
|
|
72
|
+
"\uB300\uB9AC",
|
|
73
|
+
"\uC8FC\uC784",
|
|
74
|
+
"\uC0AC\uC6D0",
|
|
75
|
+
"\uC778\uD134"
|
|
76
|
+
];
|
|
77
|
+
var OCB_CSS = `
|
|
78
|
+
.erp-ocb-member { transition: border-color .15s ease, background-color .15s ease; }
|
|
79
|
+
.erp-ocb-member:hover { border-color: var(--wp-border-brand); background: var(--wp-surface-hover); }
|
|
80
|
+
.erp-ocb-group { transition: opacity .15s ease; }
|
|
81
|
+
.erp-ocb-group:hover { opacity: .9; }
|
|
82
|
+
`;
|
|
83
|
+
async function fetchAllPages(api, endpoint, pick) {
|
|
84
|
+
const all = [];
|
|
85
|
+
const sep = endpoint.includes("?") ? "&" : "?";
|
|
86
|
+
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
87
|
+
const body = await api(`${endpoint}${sep}limit=${PAGE_SIZE}&offset=${page * PAGE_SIZE}`, {
|
|
88
|
+
method: "GET"
|
|
89
|
+
});
|
|
90
|
+
const rows = pick(body ?? {});
|
|
91
|
+
all.push(...rows);
|
|
92
|
+
const total = typeof body?.total === "number" ? body.total : null;
|
|
93
|
+
if (rows.length === 0) break;
|
|
94
|
+
if (total !== null ? all.length >= total : rows.length < PAGE_SIZE) break;
|
|
95
|
+
}
|
|
96
|
+
return all;
|
|
97
|
+
}
|
|
98
|
+
function rowsOf(body, keys) {
|
|
99
|
+
for (const key of keys) {
|
|
100
|
+
const value = body[key];
|
|
101
|
+
if (Array.isArray(value)) return value;
|
|
102
|
+
}
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
function pickEmployees(body) {
|
|
106
|
+
return rowsOf(body, ["data", "employees"]).flatMap((row) => {
|
|
107
|
+
if (typeof row !== "object" || row === null) return [];
|
|
108
|
+
const record = row;
|
|
109
|
+
if (typeof record.id !== "string" || typeof record.name !== "string") return [];
|
|
110
|
+
return [record];
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function pickGroups(body) {
|
|
114
|
+
return rowsOf(body, ["data", "groups", "groupsWithPaths"]).flatMap((row) => {
|
|
115
|
+
if (typeof row !== "object" || row === null) return [];
|
|
116
|
+
const record = row;
|
|
117
|
+
if (typeof record.id !== "string" || typeof record.name !== "string") return [];
|
|
118
|
+
return [
|
|
119
|
+
{
|
|
120
|
+
id: record.id,
|
|
121
|
+
name: record.name,
|
|
122
|
+
parent_id: typeof record.parent_id === "string" ? record.parent_id : null,
|
|
123
|
+
path: typeof record.path === "string" ? record.path : null
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function pickPositions(body) {
|
|
129
|
+
return rowsOf(body, ["data"]).flatMap((row) => {
|
|
130
|
+
if (typeof row !== "object" || row === null) return [];
|
|
131
|
+
const record = row;
|
|
132
|
+
if (typeof record.id !== "string" || typeof record.name !== "string") return [];
|
|
133
|
+
return [
|
|
134
|
+
{
|
|
135
|
+
id: record.id,
|
|
136
|
+
name: record.name,
|
|
137
|
+
rank_order: typeof record.rank_order === "number" ? record.rank_order : 0,
|
|
138
|
+
is_active: typeof record.is_active === "boolean" ? record.is_active : true
|
|
139
|
+
}
|
|
140
|
+
];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function jobPositionRankMap(positions) {
|
|
144
|
+
const map = /* @__PURE__ */ new Map();
|
|
145
|
+
for (const position of positions) {
|
|
146
|
+
const name = position.name.trim();
|
|
147
|
+
if (name === "" || map.has(name)) continue;
|
|
148
|
+
map.set(name, position.rank_order ?? 0);
|
|
149
|
+
}
|
|
150
|
+
return map;
|
|
151
|
+
}
|
|
152
|
+
function jobPositionRankOf(map, value) {
|
|
153
|
+
const text = (value ?? "").trim();
|
|
154
|
+
if (text === "") return null;
|
|
155
|
+
const exact = map.get(text);
|
|
156
|
+
if (exact !== void 0) return exact;
|
|
157
|
+
let best = null;
|
|
158
|
+
let bestLength = 0;
|
|
159
|
+
for (const [name, rank] of map) {
|
|
160
|
+
if (text.includes(name) && name.length > bestLength) {
|
|
161
|
+
best = rank;
|
|
162
|
+
bestLength = name.length;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return best;
|
|
166
|
+
}
|
|
167
|
+
function rankIn(order, value) {
|
|
168
|
+
const text = (value ?? "").trim();
|
|
169
|
+
if (!text) return order.length + 1;
|
|
170
|
+
const exact = order.indexOf(text);
|
|
171
|
+
if (exact >= 0) return exact;
|
|
172
|
+
let best = order.length;
|
|
173
|
+
let bestLength = 0;
|
|
174
|
+
order.forEach((entry, index) => {
|
|
175
|
+
if (text.includes(entry) && entry.length > bestLength) {
|
|
176
|
+
best = index;
|
|
177
|
+
bestLength = entry.length;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
return best;
|
|
181
|
+
}
|
|
182
|
+
function compareGrades(a, b, positionRanks) {
|
|
183
|
+
const rankA = jobPositionRankOf(positionRanks, a);
|
|
184
|
+
const rankB = jobPositionRankOf(positionRanks, b);
|
|
185
|
+
if (rankA !== null || rankB !== null) {
|
|
186
|
+
if (rankA === null) return 1;
|
|
187
|
+
if (rankB === null) return -1;
|
|
188
|
+
return rankA - rankB;
|
|
189
|
+
}
|
|
190
|
+
return rankIn(GRADE_ORDER, a) - rankIn(GRADE_ORDER, b);
|
|
191
|
+
}
|
|
192
|
+
function compareMembers(a, b, positionRanks) {
|
|
193
|
+
const dutyDiff = rankIn(DUTY_ORDER, a.job_title) - rankIn(DUTY_ORDER, b.job_title);
|
|
194
|
+
if (dutyDiff !== 0) return dutyDiff;
|
|
195
|
+
const gradeDiff = compareGrades(a.grade, b.grade, positionRanks);
|
|
196
|
+
if (gradeDiff !== 0) return gradeDiff;
|
|
197
|
+
const levelDiff = (b.level ?? 0) - (a.level ?? 0);
|
|
198
|
+
if (levelDiff !== 0) return levelDiff;
|
|
199
|
+
return a.name.localeCompare(b.name, "ko");
|
|
200
|
+
}
|
|
201
|
+
function positionText(employee) {
|
|
202
|
+
return (employee.grade ?? "").trim() || (employee.job_title ?? "").trim() || "-";
|
|
203
|
+
}
|
|
204
|
+
function isUnassignedGroup(group) {
|
|
205
|
+
return group.id === UNASSIGNED_GROUP_ID || (group.name ?? "").trim() === "\uBBF8\uC9C0\uC815" || group.id.trim() === "\uBBF8\uC9C0\uC815";
|
|
206
|
+
}
|
|
207
|
+
function employeeGroupId(employee) {
|
|
208
|
+
return employee.department_id ?? employee.department?.id ?? "";
|
|
209
|
+
}
|
|
210
|
+
function groupsFromEmployees(employees) {
|
|
211
|
+
const groupsById = /* @__PURE__ */ new Map();
|
|
212
|
+
for (const employee of employees) {
|
|
213
|
+
const ref = employee.department;
|
|
214
|
+
const id = employee.department_id ?? ref?.id ?? "";
|
|
215
|
+
if (!id || groupsById.has(id)) continue;
|
|
216
|
+
const name = (ref?.name ?? "").trim() || "\uBBF8\uC9C0\uC815";
|
|
217
|
+
if (isUnassignedGroup({ id, name })) continue;
|
|
218
|
+
groupsById.set(id, { id, name, parent_id: ref?.parent_id ?? null });
|
|
219
|
+
}
|
|
220
|
+
return Array.from(groupsById.values());
|
|
221
|
+
}
|
|
222
|
+
function mergeEmployeeGroups(groups, employees) {
|
|
223
|
+
const groupsById = new Map(
|
|
224
|
+
groups.filter((group) => !isUnassignedGroup(group)).map((group) => [group.id, group])
|
|
225
|
+
);
|
|
226
|
+
for (const employee of employees) {
|
|
227
|
+
const ref = employee.department;
|
|
228
|
+
const id = employee.department_id ?? ref?.id ?? "";
|
|
229
|
+
if (!id || groupsById.has(id)) continue;
|
|
230
|
+
const name = (ref?.name ?? "").trim() || "\uBBF8\uC9C0\uC815";
|
|
231
|
+
if (isUnassignedGroup({ id, name })) continue;
|
|
232
|
+
groupsById.set(id, { id, name, parent_id: ref?.parent_id ?? null });
|
|
233
|
+
}
|
|
234
|
+
return Array.from(groupsById.values());
|
|
235
|
+
}
|
|
236
|
+
function buildModel(groups, employees, positionRanks) {
|
|
237
|
+
const visibleGroups = groups.filter((group) => !isUnassignedGroup(group));
|
|
238
|
+
const groupsById = new Map(visibleGroups.map((group) => [group.id, group]));
|
|
239
|
+
const employeesByGroupId = /* @__PURE__ */ new Map();
|
|
240
|
+
for (const employee of employees) {
|
|
241
|
+
const groupId = employeeGroupId(employee);
|
|
242
|
+
if (!groupId || !groupsById.has(groupId)) continue;
|
|
243
|
+
const list = employeesByGroupId.get(groupId) ?? [];
|
|
244
|
+
list.push(employee);
|
|
245
|
+
employeesByGroupId.set(groupId, list);
|
|
246
|
+
}
|
|
247
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
248
|
+
for (const group of visibleGroups) {
|
|
249
|
+
const parentId = group.parent_id && group.parent_id !== group.id && groupsById.has(group.parent_id) ? group.parent_id : null;
|
|
250
|
+
const list = childrenByParent.get(parentId) ?? [];
|
|
251
|
+
list.push(group);
|
|
252
|
+
childrenByParent.set(parentId, list);
|
|
253
|
+
}
|
|
254
|
+
childrenByParent.forEach(
|
|
255
|
+
(items) => items.sort((a, b) => a.name.localeCompare(b.name, "ko"))
|
|
256
|
+
);
|
|
257
|
+
employeesByGroupId.forEach(
|
|
258
|
+
(items) => items.sort((a, b) => compareMembers(a, b, positionRanks))
|
|
259
|
+
);
|
|
260
|
+
return {
|
|
261
|
+
childrenByParent,
|
|
262
|
+
employeesByGroupId,
|
|
263
|
+
visibleRoots: childrenByParent.get(null) ?? []
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function employeesInGroup(groupId, model) {
|
|
267
|
+
const result = [...model.employeesByGroupId.get(groupId) ?? []];
|
|
268
|
+
for (const child of model.childrenByParent.get(groupId) ?? []) {
|
|
269
|
+
result.push(...employeesInGroup(child.id, model));
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
function groupEmployees(groupId, model, excludedIds) {
|
|
274
|
+
return (model.employeesByGroupId.get(groupId) ?? []).filter(
|
|
275
|
+
(employee) => !excludedIds.has(employee.id)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
function leadershipEmployees(employees) {
|
|
279
|
+
return employees.filter((employee) => {
|
|
280
|
+
const title = `${employee.grade ?? ""} ${employee.job_title ?? ""}`;
|
|
281
|
+
return /회장|대표|CEO|사장/.test(title);
|
|
282
|
+
}).sort((a, b) => leadershipRank(a) - leadershipRank(b)).slice(0, 3);
|
|
283
|
+
}
|
|
284
|
+
function leadershipRank(employee) {
|
|
285
|
+
const title = `${employee.grade ?? ""} ${employee.job_title ?? ""}`;
|
|
286
|
+
if (title.includes("\uD68C\uC7A5")) return 0;
|
|
287
|
+
if (title.includes("\uB300\uD45C\uC774\uC0AC")) return 1;
|
|
288
|
+
if (title.includes("\uB300\uD45C")) return 2;
|
|
289
|
+
if (title.includes("\uC0AC\uC7A5") || title.includes("CEO")) return 3;
|
|
290
|
+
return 9;
|
|
291
|
+
}
|
|
292
|
+
function buildCanvasLayout(model, employees) {
|
|
293
|
+
const leadership = leadershipEmployees(employees);
|
|
294
|
+
const leadershipIds = new Set(leadership.map((employee) => employee.id));
|
|
295
|
+
const topGroup = model.visibleRoots.length === 1 ? model.visibleRoots[0] : null;
|
|
296
|
+
const topChildren = topGroup ? model.childrenByParent.get(topGroup.id) ?? [] : [];
|
|
297
|
+
const hasCompanyRoot = !!topGroup && topChildren.length > 1;
|
|
298
|
+
const boardRoots = hasCompanyRoot ? topChildren : model.visibleRoots;
|
|
299
|
+
const rootBlocks = boardRoots.map(
|
|
300
|
+
(group, index) => buildRootBlock(group, model, leadershipIds, index)
|
|
301
|
+
);
|
|
302
|
+
const rootsWidth = Math.max(
|
|
303
|
+
GROUP_WIDTH,
|
|
304
|
+
rootBlocks.reduce(
|
|
305
|
+
(total, block, index) => total + block.width + (index > 0 ? ROOT_GAP : 0),
|
|
306
|
+
0
|
|
307
|
+
)
|
|
308
|
+
);
|
|
309
|
+
const leaderStackHeight = leadership.length * LEADER_HEIGHT + Math.max(0, leadership.length - 1) * EMPLOYEE_GAP;
|
|
310
|
+
const hasTopArea = leadership.length > 0 || hasCompanyRoot;
|
|
311
|
+
const topAreaHeight = (leadership.length > 0 ? leaderStackHeight : 0) + (leadership.length > 0 && hasCompanyRoot ? TOP_GAP : 0) + (hasCompanyRoot ? GROUP_HEIGHT : 0);
|
|
312
|
+
const rootY = CANVAS_PADDING + (hasTopArea ? topAreaHeight + ROOT_ROW_GAP : 0);
|
|
313
|
+
const width = rootsWidth + CANVAS_PADDING * 2;
|
|
314
|
+
const centerX = CANVAS_PADDING + rootsWidth / 2;
|
|
315
|
+
const placedGroups = [];
|
|
316
|
+
const placedEmployees = [];
|
|
317
|
+
const connectors = [];
|
|
318
|
+
let topAnchor = null;
|
|
319
|
+
if (leadership.length > 0) {
|
|
320
|
+
const leaderX = centerX - LEADER_WIDTH / 2;
|
|
321
|
+
leadership.forEach((employee, index) => {
|
|
322
|
+
placedEmployees.push({
|
|
323
|
+
employee,
|
|
324
|
+
x: leaderX,
|
|
325
|
+
y: CANVAS_PADDING + index * (LEADER_HEIGHT + EMPLOYEE_GAP),
|
|
326
|
+
featured: true
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
topAnchor = { x: centerX, y: CANVAS_PADDING + leaderStackHeight };
|
|
330
|
+
}
|
|
331
|
+
if (hasCompanyRoot && topGroup) {
|
|
332
|
+
const companyY = CANVAS_PADDING + (leadership.length > 0 ? leaderStackHeight + TOP_GAP : 0);
|
|
333
|
+
placedGroups.push({
|
|
334
|
+
group: topGroup,
|
|
335
|
+
x: centerX - GROUP_WIDTH / 2,
|
|
336
|
+
y: companyY,
|
|
337
|
+
width: GROUP_WIDTH,
|
|
338
|
+
color: BOARD_COLORS[0],
|
|
339
|
+
depth: 0,
|
|
340
|
+
totalMembers: employeesInGroup(topGroup.id, model).length
|
|
341
|
+
});
|
|
342
|
+
if (topAnchor) {
|
|
343
|
+
connectors.push({ fromX: topAnchor.x, fromY: topAnchor.y, toX: centerX, toY: companyY });
|
|
344
|
+
}
|
|
345
|
+
topAnchor = { x: centerX, y: companyY + GROUP_HEIGHT };
|
|
346
|
+
}
|
|
347
|
+
let cursorX = CANVAS_PADDING;
|
|
348
|
+
let maxBottom = rootY;
|
|
349
|
+
for (const block of rootBlocks) {
|
|
350
|
+
placedGroups.push(
|
|
351
|
+
...block.groups.map((item) => ({ ...item, x: item.x + cursorX, y: item.y + rootY }))
|
|
352
|
+
);
|
|
353
|
+
placedEmployees.push(
|
|
354
|
+
...block.employees.map((item) => ({ ...item, x: item.x + cursorX, y: item.y + rootY }))
|
|
355
|
+
);
|
|
356
|
+
connectors.push(
|
|
357
|
+
...block.connectors.map((item) => ({
|
|
358
|
+
fromX: item.fromX + cursorX,
|
|
359
|
+
fromY: item.fromY + rootY,
|
|
360
|
+
toX: item.toX + cursorX,
|
|
361
|
+
toY: item.toY + rootY
|
|
362
|
+
}))
|
|
363
|
+
);
|
|
364
|
+
if (topAnchor) {
|
|
365
|
+
connectors.push({
|
|
366
|
+
fromX: topAnchor.x,
|
|
367
|
+
fromY: topAnchor.y,
|
|
368
|
+
toX: block.topAnchor.x + cursorX,
|
|
369
|
+
toY: block.topAnchor.y + rootY
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
maxBottom = Math.max(maxBottom, rootY + block.height);
|
|
373
|
+
cursorX += block.width + ROOT_GAP;
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
width,
|
|
377
|
+
height: maxBottom + CANVAS_PADDING,
|
|
378
|
+
groups: placedGroups,
|
|
379
|
+
employees: placedEmployees,
|
|
380
|
+
connectors
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function buildRootBlock(group, model, leadershipIds, siblingIndex) {
|
|
384
|
+
const teams = model.childrenByParent.get(group.id) ?? [];
|
|
385
|
+
const directEmployees = groupEmployees(group.id, model, leadershipIds);
|
|
386
|
+
const teamRows = teams.map((team, index) => buildTeamRow(team, model, leadershipIds, index));
|
|
387
|
+
const employeeColumns = Math.min(2, Math.max(1, directEmployees.length));
|
|
388
|
+
const employeeRows = directEmployees.length > 0 ? Math.ceil(directEmployees.length / employeeColumns) : 0;
|
|
389
|
+
const employeeGridWidth = directEmployees.length > 0 ? employeeColumns * EMPLOYEE_WIDTH + Math.max(0, employeeColumns - 1) * EMPLOYEE_GAP : 0;
|
|
390
|
+
const employeeGridHeight = directEmployees.length > 0 ? employeeRows * EMPLOYEE_HEIGHT + Math.max(0, employeeRows - 1) * EMPLOYEE_GAP : 0;
|
|
391
|
+
const teamsWidth = teamRows.reduce((max, row) => Math.max(max, TEAM_ROW_INDENT + row.width), 0);
|
|
392
|
+
const width = Math.max(GROUP_WIDTH, employeeGridWidth, teamsWidth);
|
|
393
|
+
const color = BOARD_COLORS[siblingIndex % BOARD_COLORS.length];
|
|
394
|
+
const groups = [
|
|
395
|
+
{
|
|
396
|
+
group,
|
|
397
|
+
x: 0,
|
|
398
|
+
y: 0,
|
|
399
|
+
width,
|
|
400
|
+
color,
|
|
401
|
+
depth: 0,
|
|
402
|
+
totalMembers: employeesInGroup(group.id, model).length
|
|
403
|
+
}
|
|
404
|
+
];
|
|
405
|
+
const employees = [];
|
|
406
|
+
const connectors = [];
|
|
407
|
+
let cursorY = GROUP_HEIGHT;
|
|
408
|
+
if (directEmployees.length > 0) {
|
|
409
|
+
cursorY += 10;
|
|
410
|
+
const startX = (width - employeeGridWidth) / 2;
|
|
411
|
+
directEmployees.forEach((employee, index) => {
|
|
412
|
+
const column = index % employeeColumns;
|
|
413
|
+
const row = Math.floor(index / employeeColumns);
|
|
414
|
+
employees.push({
|
|
415
|
+
employee,
|
|
416
|
+
x: startX + column * (EMPLOYEE_WIDTH + EMPLOYEE_GAP),
|
|
417
|
+
y: cursorY + row * (EMPLOYEE_HEIGHT + EMPLOYEE_GAP)
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
cursorY += employeeGridHeight;
|
|
421
|
+
}
|
|
422
|
+
if (teamRows.length > 0) {
|
|
423
|
+
const trunkTopY = cursorY;
|
|
424
|
+
const dropY = trunkTopY + CHILD_GAP / 2;
|
|
425
|
+
cursorY += CHILD_GAP;
|
|
426
|
+
let lastElbowY = dropY;
|
|
427
|
+
for (const row of teamRows) {
|
|
428
|
+
const rowY = cursorY;
|
|
429
|
+
groups.push(
|
|
430
|
+
...row.groups.map((item) => ({ ...item, x: item.x + TEAM_ROW_INDENT, y: item.y + rowY }))
|
|
431
|
+
);
|
|
432
|
+
employees.push(
|
|
433
|
+
...row.employees.map((item) => ({
|
|
434
|
+
...item,
|
|
435
|
+
x: item.x + TEAM_ROW_INDENT,
|
|
436
|
+
y: item.y + rowY
|
|
437
|
+
}))
|
|
438
|
+
);
|
|
439
|
+
connectors.push(
|
|
440
|
+
...row.connectors.map((item) => ({
|
|
441
|
+
fromX: item.fromX + TEAM_ROW_INDENT,
|
|
442
|
+
fromY: item.fromY + rowY,
|
|
443
|
+
toX: item.toX + TEAM_ROW_INDENT,
|
|
444
|
+
toY: item.toY + rowY
|
|
445
|
+
}))
|
|
446
|
+
);
|
|
447
|
+
const elbowY = rowY + row.boxCenterY;
|
|
448
|
+
connectors.push({ fromX: TEAM_TRUNK_X, fromY: elbowY, toX: TEAM_ROW_INDENT, toY: elbowY });
|
|
449
|
+
lastElbowY = elbowY;
|
|
450
|
+
cursorY += row.height + TEAM_ROW_GAP;
|
|
451
|
+
}
|
|
452
|
+
cursorY -= TEAM_ROW_GAP;
|
|
453
|
+
connectors.push({ fromX: width / 2, fromY: trunkTopY, toX: width / 2, toY: dropY });
|
|
454
|
+
connectors.push({ fromX: width / 2, fromY: dropY, toX: TEAM_TRUNK_X, toY: dropY });
|
|
455
|
+
connectors.push({ fromX: TEAM_TRUNK_X, fromY: dropY, toX: TEAM_TRUNK_X, toY: lastElbowY });
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
width,
|
|
459
|
+
height: cursorY,
|
|
460
|
+
groups,
|
|
461
|
+
employees,
|
|
462
|
+
connectors,
|
|
463
|
+
topAnchor: { x: width / 2, y: 0 }
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function buildTeamRow(group, model, leadershipIds, siblingIndex, depth = 1) {
|
|
467
|
+
const children = model.childrenByParent.get(group.id) ?? [];
|
|
468
|
+
const members = groupEmployees(group.id, model, leadershipIds);
|
|
469
|
+
const childBlocks = children.map(
|
|
470
|
+
(child, index) => buildGroupBlock(child, model, leadershipIds, depth + 1, index)
|
|
471
|
+
);
|
|
472
|
+
const labelMinHeight = TEAM_BOX_PAD + group.name.length * TEAM_BOX_CHAR;
|
|
473
|
+
const memberColumns = Math.min(MEMBER_COLUMNS, Math.max(1, members.length));
|
|
474
|
+
const memberRowCount = members.length > 0 ? Math.ceil(members.length / memberColumns) : 0;
|
|
475
|
+
const membersWidth = members.length > 0 ? memberColumns * EMPLOYEE_WIDTH + Math.max(0, memberColumns - 1) * EMPLOYEE_GAP : 0;
|
|
476
|
+
const membersHeight = memberRowCount > 0 ? memberRowCount * EMPLOYEE_HEIGHT + Math.max(0, memberRowCount - 1) * EMPLOYEE_GAP : 0;
|
|
477
|
+
const childAreaWidth = childBlocks.reduce((max, block) => Math.max(max, block.width), 0);
|
|
478
|
+
const contentX = TEAM_BOX_WIDTH + TEAM_MEMBER_GAP;
|
|
479
|
+
const contentWidth = Math.max(membersWidth, childAreaWidth);
|
|
480
|
+
const width = contentWidth > 0 ? contentX + contentWidth : TEAM_BOX_WIDTH;
|
|
481
|
+
const totalMembers = employeesInGroup(group.id, model).length;
|
|
482
|
+
const color = BOARD_COLORS[(depth + siblingIndex) % BOARD_COLORS.length];
|
|
483
|
+
const groups = [];
|
|
484
|
+
const employees = [];
|
|
485
|
+
const connectors = [];
|
|
486
|
+
members.forEach((employee, index) => {
|
|
487
|
+
const column = index % memberColumns;
|
|
488
|
+
const row = Math.floor(index / memberColumns);
|
|
489
|
+
employees.push({
|
|
490
|
+
employee,
|
|
491
|
+
x: contentX + column * (EMPLOYEE_WIDTH + EMPLOYEE_GAP),
|
|
492
|
+
y: row * (EMPLOYEE_HEIGHT + EMPLOYEE_GAP)
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
let height = Math.max(GROUP_HEIGHT, membersHeight);
|
|
496
|
+
if (childBlocks.length > 0) {
|
|
497
|
+
let childY = height + CHILD_GAP;
|
|
498
|
+
for (const block of childBlocks) {
|
|
499
|
+
groups.push(
|
|
500
|
+
...block.groups.map((item) => ({ ...item, x: item.x + contentX, y: item.y + childY }))
|
|
501
|
+
);
|
|
502
|
+
employees.push(
|
|
503
|
+
...block.employees.map((item) => ({ ...item, x: item.x + contentX, y: item.y + childY }))
|
|
504
|
+
);
|
|
505
|
+
connectors.push(
|
|
506
|
+
...block.connectors.map((item) => ({
|
|
507
|
+
fromX: item.fromX + contentX,
|
|
508
|
+
fromY: item.fromY + childY,
|
|
509
|
+
toX: item.toX + contentX,
|
|
510
|
+
toY: item.toY + childY
|
|
511
|
+
}))
|
|
512
|
+
);
|
|
513
|
+
connectors.push({
|
|
514
|
+
fromX: TEAM_BOX_WIDTH,
|
|
515
|
+
fromY: childY + GROUP_HEIGHT / 2,
|
|
516
|
+
toX: contentX,
|
|
517
|
+
toY: childY + GROUP_HEIGHT / 2
|
|
518
|
+
});
|
|
519
|
+
childY += block.height + TEAM_ROW_GAP;
|
|
520
|
+
}
|
|
521
|
+
height = childY - TEAM_ROW_GAP;
|
|
522
|
+
}
|
|
523
|
+
height = Math.max(height, labelMinHeight);
|
|
524
|
+
if (childBlocks.length === 0 && members.length > 0) {
|
|
525
|
+
const offsetY = Math.max(0, (height - membersHeight) / 2);
|
|
526
|
+
for (const item of employees) item.y += offsetY;
|
|
527
|
+
}
|
|
528
|
+
groups.unshift({
|
|
529
|
+
group,
|
|
530
|
+
x: 0,
|
|
531
|
+
y: 0,
|
|
532
|
+
width: TEAM_BOX_WIDTH,
|
|
533
|
+
height,
|
|
534
|
+
vertical: true,
|
|
535
|
+
color,
|
|
536
|
+
depth,
|
|
537
|
+
totalMembers
|
|
538
|
+
});
|
|
539
|
+
return { width, height, groups, employees, connectors, boxCenterY: height / 2 };
|
|
540
|
+
}
|
|
541
|
+
function buildGroupBlock(group, model, leadershipIds, depth, siblingIndex) {
|
|
542
|
+
const children = model.childrenByParent.get(group.id) ?? [];
|
|
543
|
+
const directEmployees = groupEmployees(group.id, model, leadershipIds);
|
|
544
|
+
const childRows = children.map(
|
|
545
|
+
(child, index) => buildTeamRow(child, model, leadershipIds, index, depth + 1)
|
|
546
|
+
);
|
|
547
|
+
const employeeColumns = Math.min(2, Math.max(1, directEmployees.length));
|
|
548
|
+
const employeeRows = directEmployees.length > 0 ? Math.ceil(directEmployees.length / employeeColumns) : 0;
|
|
549
|
+
const employeeGridWidth = directEmployees.length > 0 ? employeeColumns * EMPLOYEE_WIDTH + Math.max(0, employeeColumns - 1) * EMPLOYEE_GAP : 0;
|
|
550
|
+
const employeeGridHeight = directEmployees.length > 0 ? employeeRows * EMPLOYEE_HEIGHT + Math.max(0, employeeRows - 1) * EMPLOYEE_GAP : 0;
|
|
551
|
+
const rowsWidth = childRows.reduce((max, row) => Math.max(max, TEAM_ROW_INDENT + row.width), 0);
|
|
552
|
+
const width = Math.max(GROUP_WIDTH, employeeGridWidth, rowsWidth);
|
|
553
|
+
const color = BOARD_COLORS[(depth + siblingIndex) % BOARD_COLORS.length];
|
|
554
|
+
const groups = [
|
|
555
|
+
{
|
|
556
|
+
group,
|
|
557
|
+
x: 0,
|
|
558
|
+
y: 0,
|
|
559
|
+
width,
|
|
560
|
+
color,
|
|
561
|
+
depth,
|
|
562
|
+
totalMembers: employeesInGroup(group.id, model).length
|
|
563
|
+
}
|
|
564
|
+
];
|
|
565
|
+
const employees = [];
|
|
566
|
+
const connectors = [];
|
|
567
|
+
let cursorY = GROUP_HEIGHT;
|
|
568
|
+
if (directEmployees.length > 0) {
|
|
569
|
+
cursorY += 10;
|
|
570
|
+
const startX = (width - employeeGridWidth) / 2;
|
|
571
|
+
directEmployees.forEach((employee, index) => {
|
|
572
|
+
const column = index % employeeColumns;
|
|
573
|
+
const row = Math.floor(index / employeeColumns);
|
|
574
|
+
employees.push({
|
|
575
|
+
employee,
|
|
576
|
+
x: startX + column * (EMPLOYEE_WIDTH + EMPLOYEE_GAP),
|
|
577
|
+
y: cursorY + row * (EMPLOYEE_HEIGHT + EMPLOYEE_GAP)
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
cursorY += employeeGridHeight;
|
|
581
|
+
}
|
|
582
|
+
if (childRows.length > 0) {
|
|
583
|
+
const trunkTopY = cursorY;
|
|
584
|
+
const dropY = trunkTopY + CHILD_GAP / 2;
|
|
585
|
+
cursorY += CHILD_GAP;
|
|
586
|
+
let lastElbowY = dropY;
|
|
587
|
+
for (const row of childRows) {
|
|
588
|
+
const rowY = cursorY;
|
|
589
|
+
groups.push(
|
|
590
|
+
...row.groups.map((item) => ({ ...item, x: item.x + TEAM_ROW_INDENT, y: item.y + rowY }))
|
|
591
|
+
);
|
|
592
|
+
employees.push(
|
|
593
|
+
...row.employees.map((item) => ({
|
|
594
|
+
...item,
|
|
595
|
+
x: item.x + TEAM_ROW_INDENT,
|
|
596
|
+
y: item.y + rowY
|
|
597
|
+
}))
|
|
598
|
+
);
|
|
599
|
+
connectors.push(
|
|
600
|
+
...row.connectors.map((item) => ({
|
|
601
|
+
fromX: item.fromX + TEAM_ROW_INDENT,
|
|
602
|
+
fromY: item.fromY + rowY,
|
|
603
|
+
toX: item.toX + TEAM_ROW_INDENT,
|
|
604
|
+
toY: item.toY + rowY
|
|
605
|
+
}))
|
|
606
|
+
);
|
|
607
|
+
const elbowY = rowY + row.boxCenterY;
|
|
608
|
+
connectors.push({ fromX: TEAM_TRUNK_X, fromY: elbowY, toX: TEAM_ROW_INDENT, toY: elbowY });
|
|
609
|
+
lastElbowY = elbowY;
|
|
610
|
+
cursorY += row.height + TEAM_ROW_GAP;
|
|
611
|
+
}
|
|
612
|
+
cursorY -= TEAM_ROW_GAP;
|
|
613
|
+
connectors.push(
|
|
614
|
+
{ fromX: width / 2, fromY: trunkTopY, toX: width / 2, toY: dropY },
|
|
615
|
+
{ fromX: width / 2, fromY: dropY, toX: TEAM_TRUNK_X, toY: dropY },
|
|
616
|
+
{ fromX: TEAM_TRUNK_X, fromY: dropY, toX: TEAM_TRUNK_X, toY: lastElbowY }
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
width,
|
|
621
|
+
height: cursorY,
|
|
622
|
+
groups,
|
|
623
|
+
employees,
|
|
624
|
+
connectors,
|
|
625
|
+
topAnchor: { x: width / 2, y: 0 }
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function connectorPath(connector) {
|
|
629
|
+
const dx = Math.abs(connector.toX - connector.fromX);
|
|
630
|
+
const dy = Math.abs(connector.toY - connector.fromY);
|
|
631
|
+
if (dx > dy) {
|
|
632
|
+
const midX = connector.fromX + (connector.toX - connector.fromX) / 2;
|
|
633
|
+
return [
|
|
634
|
+
`M ${connector.fromX} ${connector.fromY}`,
|
|
635
|
+
`H ${midX}`,
|
|
636
|
+
`V ${connector.toY}`,
|
|
637
|
+
`H ${connector.toX}`
|
|
638
|
+
].join(" ");
|
|
639
|
+
}
|
|
640
|
+
const midY = connector.fromY + (connector.toY - connector.fromY) / 2;
|
|
641
|
+
return [
|
|
642
|
+
`M ${connector.fromX} ${connector.fromY}`,
|
|
643
|
+
`V ${midY}`,
|
|
644
|
+
`H ${connector.toX}`,
|
|
645
|
+
`V ${connector.toY}`
|
|
646
|
+
].join(" ");
|
|
647
|
+
}
|
|
648
|
+
function clamp(value, min, max) {
|
|
649
|
+
return Math.min(max, Math.max(min, value));
|
|
650
|
+
}
|
|
651
|
+
function applyZoom(current, nextScale, focal) {
|
|
652
|
+
const scale = clamp(nextScale, MIN_ZOOM, MAX_ZOOM);
|
|
653
|
+
return {
|
|
654
|
+
scale,
|
|
655
|
+
x: focal.x - (focal.x - current.x) / current.scale * scale,
|
|
656
|
+
y: focal.y - (focal.y - current.y) / current.scale * scale
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function EmployeeAvatar({ employee, featured }) {
|
|
660
|
+
const size = featured ? 40 : 32;
|
|
661
|
+
if (employee.image) {
|
|
662
|
+
return /* @__PURE__ */ jsx(
|
|
663
|
+
"span",
|
|
664
|
+
{
|
|
665
|
+
style: { flexShrink: 0, display: "inline-flex" },
|
|
666
|
+
onDragStart: (event) => event.preventDefault(),
|
|
667
|
+
children: /* @__PURE__ */ jsx(
|
|
668
|
+
ImageLightbox,
|
|
669
|
+
{
|
|
670
|
+
src: employee.image,
|
|
671
|
+
alt: employee.name,
|
|
672
|
+
thumbStyle: {
|
|
673
|
+
width: size,
|
|
674
|
+
height: size,
|
|
675
|
+
borderRadius: "var(--wp-radius-xs)",
|
|
676
|
+
objectFit: "cover"
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
)
|
|
680
|
+
}
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
return /* @__PURE__ */ jsx(
|
|
684
|
+
"span",
|
|
685
|
+
{
|
|
686
|
+
style: {
|
|
687
|
+
width: size,
|
|
688
|
+
height: size,
|
|
689
|
+
flexShrink: 0,
|
|
690
|
+
display: "inline-flex",
|
|
691
|
+
alignItems: "center",
|
|
692
|
+
justifyContent: "center",
|
|
693
|
+
borderRadius: "var(--wp-radius-xs)",
|
|
694
|
+
background: "var(--wp-surface-sunken)",
|
|
695
|
+
color: "var(--wp-text-muted)"
|
|
696
|
+
},
|
|
697
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "user", size: featured ? 20 : 16 })
|
|
698
|
+
}
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
var SELECTED_RING = "0 0 0 2px var(--wp-surface-card), 0 0 0 4px var(--wp-accent)";
|
|
702
|
+
function CanvasGroupNode({
|
|
703
|
+
item,
|
|
704
|
+
selected,
|
|
705
|
+
onSelect
|
|
706
|
+
}) {
|
|
707
|
+
const height = item.height ?? GROUP_HEIGHT;
|
|
708
|
+
if (item.vertical) {
|
|
709
|
+
return /* @__PURE__ */ jsxs(
|
|
710
|
+
"button",
|
|
711
|
+
{
|
|
712
|
+
"data-org-card": true,
|
|
713
|
+
type: "button",
|
|
714
|
+
className: "erp-ocb-group",
|
|
715
|
+
style: {
|
|
716
|
+
position: "absolute",
|
|
717
|
+
left: item.x,
|
|
718
|
+
top: item.y,
|
|
719
|
+
width: item.width,
|
|
720
|
+
height,
|
|
721
|
+
display: "flex",
|
|
722
|
+
flexDirection: "column",
|
|
723
|
+
alignItems: "center",
|
|
724
|
+
gap: 6,
|
|
725
|
+
overflow: "hidden",
|
|
726
|
+
borderRadius: "var(--wp-radius-xs)",
|
|
727
|
+
border: `1px solid ${item.color}`,
|
|
728
|
+
padding: "8px 4px",
|
|
729
|
+
color: "#fff",
|
|
730
|
+
backgroundColor: item.color,
|
|
731
|
+
boxShadow: selected ? SELECTED_RING : "var(--wp-shadow-xs)",
|
|
732
|
+
cursor: "pointer"
|
|
733
|
+
},
|
|
734
|
+
onClick: () => onSelect(item.group.id),
|
|
735
|
+
children: [
|
|
736
|
+
/* @__PURE__ */ jsx(Icon, { name: "building-2", size: 16, style: { flexShrink: 0 } }),
|
|
737
|
+
/* @__PURE__ */ jsx(
|
|
738
|
+
"span",
|
|
739
|
+
{
|
|
740
|
+
style: {
|
|
741
|
+
minHeight: 0,
|
|
742
|
+
flex: 1,
|
|
743
|
+
overflow: "hidden",
|
|
744
|
+
whiteSpace: "nowrap",
|
|
745
|
+
fontSize: 14,
|
|
746
|
+
fontWeight: 600,
|
|
747
|
+
writingMode: "vertical-rl",
|
|
748
|
+
textOrientation: "upright"
|
|
749
|
+
},
|
|
750
|
+
children: item.group.name
|
|
751
|
+
}
|
|
752
|
+
),
|
|
753
|
+
/* @__PURE__ */ jsx("span", { style: { flexShrink: 0, fontSize: 10, fontWeight: 500, lineHeight: 1, opacity: 0.9 }, children: item.totalMembers })
|
|
754
|
+
]
|
|
755
|
+
}
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
return /* @__PURE__ */ jsxs(
|
|
759
|
+
"button",
|
|
760
|
+
{
|
|
761
|
+
"data-org-card": true,
|
|
762
|
+
type: "button",
|
|
763
|
+
className: "erp-ocb-group",
|
|
764
|
+
style: {
|
|
765
|
+
position: "absolute",
|
|
766
|
+
left: item.x,
|
|
767
|
+
top: item.y,
|
|
768
|
+
width: item.width,
|
|
769
|
+
height,
|
|
770
|
+
display: "flex",
|
|
771
|
+
alignItems: "center",
|
|
772
|
+
gap: 8,
|
|
773
|
+
borderRadius: "var(--wp-radius-xs)",
|
|
774
|
+
border: `1px solid ${item.color}`,
|
|
775
|
+
padding: "0 12px",
|
|
776
|
+
textAlign: "left",
|
|
777
|
+
color: "#fff",
|
|
778
|
+
backgroundColor: item.color,
|
|
779
|
+
boxShadow: selected ? SELECTED_RING : "var(--wp-shadow-xs)",
|
|
780
|
+
cursor: "pointer"
|
|
781
|
+
},
|
|
782
|
+
onClick: () => onSelect(item.group.id),
|
|
783
|
+
children: [
|
|
784
|
+
/* @__PURE__ */ jsx(Icon, { name: "building-2", size: 16, style: { flexShrink: 0 } }),
|
|
785
|
+
/* @__PURE__ */ jsxs(
|
|
786
|
+
"span",
|
|
787
|
+
{
|
|
788
|
+
style: {
|
|
789
|
+
minWidth: 0,
|
|
790
|
+
flex: 1,
|
|
791
|
+
overflow: "hidden",
|
|
792
|
+
textOverflow: "ellipsis",
|
|
793
|
+
whiteSpace: "nowrap",
|
|
794
|
+
fontSize: 14,
|
|
795
|
+
fontWeight: 600
|
|
796
|
+
},
|
|
797
|
+
children: [
|
|
798
|
+
item.group.name,
|
|
799
|
+
"(",
|
|
800
|
+
item.totalMembers,
|
|
801
|
+
")"
|
|
802
|
+
]
|
|
803
|
+
}
|
|
804
|
+
)
|
|
805
|
+
]
|
|
806
|
+
}
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
function CanvasEmployeeNode({
|
|
810
|
+
item,
|
|
811
|
+
onOpenCard
|
|
812
|
+
}) {
|
|
813
|
+
const width = item.featured ? LEADER_WIDTH : EMPLOYEE_WIDTH;
|
|
814
|
+
const height = item.featured ? LEADER_HEIGHT : EMPLOYEE_HEIGHT;
|
|
815
|
+
return /* @__PURE__ */ jsxs(
|
|
816
|
+
"button",
|
|
817
|
+
{
|
|
818
|
+
"data-org-card": true,
|
|
819
|
+
type: "button",
|
|
820
|
+
className: "erp-ocb-member",
|
|
821
|
+
style: {
|
|
822
|
+
position: "absolute",
|
|
823
|
+
left: item.x,
|
|
824
|
+
top: item.y,
|
|
825
|
+
width,
|
|
826
|
+
height,
|
|
827
|
+
minWidth: 0,
|
|
828
|
+
display: "flex",
|
|
829
|
+
alignItems: "center",
|
|
830
|
+
gap: 8,
|
|
831
|
+
borderRadius: "var(--wp-radius-xs)",
|
|
832
|
+
border: "var(--wp-border-w) solid var(--wp-border-default)",
|
|
833
|
+
background: "var(--wp-surface-card)",
|
|
834
|
+
padding: "0 8px",
|
|
835
|
+
textAlign: "left",
|
|
836
|
+
boxShadow: "var(--wp-shadow-xs)",
|
|
837
|
+
cursor: "pointer"
|
|
838
|
+
},
|
|
839
|
+
onClick: () => onOpenCard(item.employee),
|
|
840
|
+
children: [
|
|
841
|
+
/* @__PURE__ */ jsx(EmployeeAvatar, { employee: item.employee, featured: !!item.featured }),
|
|
842
|
+
/* @__PURE__ */ jsxs("span", { style: { minWidth: 0, flex: 1 }, children: [
|
|
843
|
+
/* @__PURE__ */ jsx(
|
|
844
|
+
"span",
|
|
845
|
+
{
|
|
846
|
+
style: {
|
|
847
|
+
display: "block",
|
|
848
|
+
overflow: "hidden",
|
|
849
|
+
textOverflow: "ellipsis",
|
|
850
|
+
whiteSpace: "nowrap",
|
|
851
|
+
fontWeight: 600,
|
|
852
|
+
fontSize: item.featured ? 14 : 11,
|
|
853
|
+
color: "var(--wp-text-default)"
|
|
854
|
+
},
|
|
855
|
+
children: item.employee.name
|
|
856
|
+
}
|
|
857
|
+
),
|
|
858
|
+
/* @__PURE__ */ jsx(
|
|
859
|
+
"span",
|
|
860
|
+
{
|
|
861
|
+
style: {
|
|
862
|
+
display: "block",
|
|
863
|
+
overflow: "hidden",
|
|
864
|
+
textOverflow: "ellipsis",
|
|
865
|
+
whiteSpace: "nowrap",
|
|
866
|
+
fontSize: item.featured ? 12 : 10,
|
|
867
|
+
color: "var(--wp-text-muted)"
|
|
868
|
+
},
|
|
869
|
+
children: positionText(item.employee)
|
|
870
|
+
}
|
|
871
|
+
)
|
|
872
|
+
] })
|
|
873
|
+
]
|
|
874
|
+
}
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
function OrganizationCanvas({
|
|
878
|
+
model,
|
|
879
|
+
employees,
|
|
880
|
+
selectedGroupId,
|
|
881
|
+
onSelect,
|
|
882
|
+
onOpenCard,
|
|
883
|
+
height
|
|
884
|
+
}) {
|
|
885
|
+
const viewportRef = useRef(null);
|
|
886
|
+
const dragRef = useRef(null);
|
|
887
|
+
const [view, setView] = useState(DEFAULT_VIEW);
|
|
888
|
+
const [dragging, setDragging] = useState(false);
|
|
889
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
890
|
+
const layout = useMemo(() => buildCanvasLayout(model, employees), [model, employees]);
|
|
891
|
+
useEffect(() => {
|
|
892
|
+
const handleFullscreenChange = () => {
|
|
893
|
+
setIsFullscreen(document.fullscreenElement === viewportRef.current);
|
|
894
|
+
};
|
|
895
|
+
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
896
|
+
return () => document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
|
897
|
+
}, []);
|
|
898
|
+
useEffect(() => {
|
|
899
|
+
setView(DEFAULT_VIEW);
|
|
900
|
+
}, [layout.width, layout.height]);
|
|
901
|
+
useEffect(() => {
|
|
902
|
+
const viewport = viewportRef.current;
|
|
903
|
+
if (!viewport) return;
|
|
904
|
+
const onWheel = (event) => {
|
|
905
|
+
event.preventDefault();
|
|
906
|
+
const rect = viewport.getBoundingClientRect();
|
|
907
|
+
const focal = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
908
|
+
setView(
|
|
909
|
+
(current) => applyZoom(current, current.scale * (event.deltaY > 0 ? 0.9 : 1.1), focal)
|
|
910
|
+
);
|
|
911
|
+
};
|
|
912
|
+
viewport.addEventListener("wheel", onWheel, { passive: false });
|
|
913
|
+
return () => viewport.removeEventListener("wheel", onWheel);
|
|
914
|
+
}, []);
|
|
915
|
+
const viewportCenter = useCallback(() => {
|
|
916
|
+
const rect = viewportRef.current?.getBoundingClientRect();
|
|
917
|
+
if (!rect) return { x: 0, y: 0 };
|
|
918
|
+
return { x: rect.width / 2, y: rect.height / 2 };
|
|
919
|
+
}, []);
|
|
920
|
+
const zoomStep = (delta) => {
|
|
921
|
+
const focal = viewportCenter();
|
|
922
|
+
setView((current) => applyZoom(current, current.scale + delta, focal));
|
|
923
|
+
};
|
|
924
|
+
const handlePointerDown = (event) => {
|
|
925
|
+
const target = event.target;
|
|
926
|
+
if (target.closest("[data-org-card]")) return;
|
|
927
|
+
dragRef.current = {
|
|
928
|
+
pointerId: event.pointerId,
|
|
929
|
+
startX: event.clientX,
|
|
930
|
+
startY: event.clientY,
|
|
931
|
+
originX: view.x,
|
|
932
|
+
originY: view.y
|
|
933
|
+
};
|
|
934
|
+
setDragging(true);
|
|
935
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
936
|
+
};
|
|
937
|
+
const handlePointerMove = (event) => {
|
|
938
|
+
const drag = dragRef.current;
|
|
939
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
940
|
+
setView((current) => ({
|
|
941
|
+
...current,
|
|
942
|
+
x: drag.originX + event.clientX - drag.startX,
|
|
943
|
+
y: drag.originY + event.clientY - drag.startY
|
|
944
|
+
}));
|
|
945
|
+
};
|
|
946
|
+
const handlePointerUp = (event) => {
|
|
947
|
+
if (dragRef.current?.pointerId !== event.pointerId) return;
|
|
948
|
+
dragRef.current = null;
|
|
949
|
+
setDragging(false);
|
|
950
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
951
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
const toggleFullscreen = async () => {
|
|
955
|
+
const viewport = viewportRef.current;
|
|
956
|
+
if (!viewport) return;
|
|
957
|
+
try {
|
|
958
|
+
if (document.fullscreenElement === viewport) {
|
|
959
|
+
await document.exitFullscreen();
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
await viewport.requestFullscreen();
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
return /* @__PURE__ */ jsxs(
|
|
967
|
+
"div",
|
|
968
|
+
{
|
|
969
|
+
ref: viewportRef,
|
|
970
|
+
style: {
|
|
971
|
+
position: "relative",
|
|
972
|
+
height,
|
|
973
|
+
minHeight: 620,
|
|
974
|
+
overflow: "hidden",
|
|
975
|
+
borderTop: "var(--wp-border-w) solid var(--wp-border-default)",
|
|
976
|
+
background: "var(--wp-surface-page)",
|
|
977
|
+
userSelect: "none",
|
|
978
|
+
cursor: dragging ? "grabbing" : "grab",
|
|
979
|
+
touchAction: "none"
|
|
980
|
+
},
|
|
981
|
+
onPointerDown: handlePointerDown,
|
|
982
|
+
onPointerMove: handlePointerMove,
|
|
983
|
+
onPointerUp: handlePointerUp,
|
|
984
|
+
onPointerCancel: handlePointerUp,
|
|
985
|
+
children: [
|
|
986
|
+
/* @__PURE__ */ jsxs(
|
|
987
|
+
"div",
|
|
988
|
+
{
|
|
989
|
+
"data-org-card": true,
|
|
990
|
+
style: {
|
|
991
|
+
position: "absolute",
|
|
992
|
+
right: 12,
|
|
993
|
+
top: 12,
|
|
994
|
+
zIndex: 30,
|
|
995
|
+
display: "flex",
|
|
996
|
+
alignItems: "center",
|
|
997
|
+
gap: 4,
|
|
998
|
+
borderRadius: "var(--wp-radius-md)",
|
|
999
|
+
border: "var(--wp-border-w) solid var(--wp-border-default)",
|
|
1000
|
+
background: "var(--wp-surface-card)",
|
|
1001
|
+
padding: 4,
|
|
1002
|
+
boxShadow: "var(--wp-shadow-sm)"
|
|
1003
|
+
},
|
|
1004
|
+
children: [
|
|
1005
|
+
/* @__PURE__ */ jsx(IconButton, { icon: "minus", label: "\uCD95\uC18C", size: "sm", onClick: () => zoomStep(-0.1) }),
|
|
1006
|
+
/* @__PURE__ */ jsxs("div", { style: { width: 48, textAlign: "center", fontSize: 12, fontWeight: 500 }, children: [
|
|
1007
|
+
Math.round(view.scale * 100),
|
|
1008
|
+
"%"
|
|
1009
|
+
] }),
|
|
1010
|
+
/* @__PURE__ */ jsx(IconButton, { icon: "plus", label: "\uD655\uB300", size: "sm", onClick: () => zoomStep(0.1) }),
|
|
1011
|
+
/* @__PURE__ */ jsx(
|
|
1012
|
+
IconButton,
|
|
1013
|
+
{
|
|
1014
|
+
icon: isFullscreen ? "minimize" : "maximize",
|
|
1015
|
+
label: isFullscreen ? "\uC804\uCCB4\uD654\uBA74 \uD574\uC81C" : "\uC804\uCCB4\uD654\uBA74",
|
|
1016
|
+
size: "sm",
|
|
1017
|
+
onClick: () => void toggleFullscreen()
|
|
1018
|
+
}
|
|
1019
|
+
)
|
|
1020
|
+
]
|
|
1021
|
+
}
|
|
1022
|
+
),
|
|
1023
|
+
/* @__PURE__ */ jsxs(
|
|
1024
|
+
"div",
|
|
1025
|
+
{
|
|
1026
|
+
style: {
|
|
1027
|
+
position: "absolute",
|
|
1028
|
+
left: 0,
|
|
1029
|
+
top: 0,
|
|
1030
|
+
width: layout.width,
|
|
1031
|
+
height: layout.height,
|
|
1032
|
+
transform: `translate(${view.x}px, ${view.y}px) scale(${view.scale})`,
|
|
1033
|
+
transformOrigin: "0 0"
|
|
1034
|
+
},
|
|
1035
|
+
children: [
|
|
1036
|
+
/* @__PURE__ */ jsx(
|
|
1037
|
+
"svg",
|
|
1038
|
+
{
|
|
1039
|
+
style: { position: "absolute", left: 0, top: 0, pointerEvents: "none" },
|
|
1040
|
+
width: layout.width,
|
|
1041
|
+
height: layout.height,
|
|
1042
|
+
children: layout.connectors.map((connector, index) => /* @__PURE__ */ jsx(
|
|
1043
|
+
"path",
|
|
1044
|
+
{
|
|
1045
|
+
d: connectorPath(connector),
|
|
1046
|
+
fill: "none",
|
|
1047
|
+
stroke: "var(--wp-border-default)",
|
|
1048
|
+
strokeWidth: 2
|
|
1049
|
+
},
|
|
1050
|
+
index
|
|
1051
|
+
))
|
|
1052
|
+
}
|
|
1053
|
+
),
|
|
1054
|
+
layout.groups.map((item) => /* @__PURE__ */ jsx(
|
|
1055
|
+
CanvasGroupNode,
|
|
1056
|
+
{
|
|
1057
|
+
item,
|
|
1058
|
+
selected: selectedGroupId === item.group.id,
|
|
1059
|
+
onSelect
|
|
1060
|
+
},
|
|
1061
|
+
item.group.id
|
|
1062
|
+
)),
|
|
1063
|
+
layout.employees.map((item) => /* @__PURE__ */ jsx(
|
|
1064
|
+
CanvasEmployeeNode,
|
|
1065
|
+
{
|
|
1066
|
+
item,
|
|
1067
|
+
onOpenCard
|
|
1068
|
+
},
|
|
1069
|
+
`${item.featured ? "leader" : "employee"}-${item.employee.id}`
|
|
1070
|
+
))
|
|
1071
|
+
]
|
|
1072
|
+
}
|
|
1073
|
+
)
|
|
1074
|
+
]
|
|
1075
|
+
}
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
function OrgChartBoardImpl({
|
|
1079
|
+
onMemberClick,
|
|
1080
|
+
height = "72vh",
|
|
1081
|
+
employeesEndpoint = "/employees",
|
|
1082
|
+
departmentsEndpoint = "/departments?include=paths",
|
|
1083
|
+
positionsEndpoint = "/job-positions"
|
|
1084
|
+
}) {
|
|
1085
|
+
const adapter = useSduiAdapter();
|
|
1086
|
+
const api = hostFetchOf(adapter) ?? defaultAdapter.fetch;
|
|
1087
|
+
const [groups, setGroups] = useState([]);
|
|
1088
|
+
const [employees, setEmployees] = useState([]);
|
|
1089
|
+
const [positions, setPositions] = useState([]);
|
|
1090
|
+
const [state, setState] = useState("idle");
|
|
1091
|
+
const [offline, setOffline] = useState(false);
|
|
1092
|
+
const [selectedGroupId, setSelectedGroupId] = useState("");
|
|
1093
|
+
const load = useCallback(async () => {
|
|
1094
|
+
setState("loading");
|
|
1095
|
+
setOffline(false);
|
|
1096
|
+
try {
|
|
1097
|
+
const [nextEmployees, fetchedGroups, nextPositions] = await Promise.all([
|
|
1098
|
+
fetchAllPages(api, employeesEndpoint, pickEmployees),
|
|
1099
|
+
// 부서 조회가 죽어도 조직도는 떠야 한다 — 사원 데이터에서 합성한다.
|
|
1100
|
+
fetchAllPages(api, departmentsEndpoint, pickGroups).catch(
|
|
1101
|
+
() => []
|
|
1102
|
+
),
|
|
1103
|
+
// 직위 마스터는 정렬 보조라 못 불러와도 조직도는 떠야 한다.
|
|
1104
|
+
fetchAllPages(api, positionsEndpoint, pickPositions).catch(
|
|
1105
|
+
() => []
|
|
1106
|
+
)
|
|
1107
|
+
]);
|
|
1108
|
+
let nextGroups = fetchedGroups.length > 0 ? fetchedGroups : groupsFromEmployees(nextEmployees);
|
|
1109
|
+
nextGroups = mergeEmployeeGroups(nextGroups, nextEmployees);
|
|
1110
|
+
const model2 = buildModel(nextGroups, nextEmployees, jobPositionRankMap(nextPositions));
|
|
1111
|
+
const firstGroupId = model2.visibleRoots[0]?.id ?? (nextEmployees[0] ? employeeGroupId(nextEmployees[0]) : "");
|
|
1112
|
+
setEmployees(nextEmployees);
|
|
1113
|
+
setGroups(nextGroups);
|
|
1114
|
+
setPositions(nextPositions);
|
|
1115
|
+
setSelectedGroupId(firstGroupId);
|
|
1116
|
+
setState("ready");
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
setOffline(error?.status === 503);
|
|
1119
|
+
setState("error");
|
|
1120
|
+
}
|
|
1121
|
+
}, []);
|
|
1122
|
+
useEffect(() => {
|
|
1123
|
+
void load();
|
|
1124
|
+
}, [load]);
|
|
1125
|
+
const model = useMemo(
|
|
1126
|
+
() => buildModel(groups, employees, jobPositionRankMap(positions)),
|
|
1127
|
+
[groups, employees, positions]
|
|
1128
|
+
);
|
|
1129
|
+
const openCard = useCallback(
|
|
1130
|
+
(employee) => {
|
|
1131
|
+
onMemberClick?.(employee);
|
|
1132
|
+
},
|
|
1133
|
+
[onMemberClick]
|
|
1134
|
+
);
|
|
1135
|
+
let content;
|
|
1136
|
+
if (state === "idle" || state === "loading") {
|
|
1137
|
+
content = /* @__PURE__ */ jsx(
|
|
1138
|
+
"div",
|
|
1139
|
+
{
|
|
1140
|
+
className: "wp-body-sm",
|
|
1141
|
+
style: { padding: "48px 0", textAlign: "center", color: "var(--wp-text-muted)" },
|
|
1142
|
+
children: "\uC870\uC9C1\uB3C4 \uC815\uBCF4\uB97C \uBD88\uB7EC\uC624\uB294 \uC911\uC785\uB2C8\uB2E4..."
|
|
1143
|
+
}
|
|
1144
|
+
);
|
|
1145
|
+
} else if (state === "error") {
|
|
1146
|
+
content = /* @__PURE__ */ jsxs(
|
|
1147
|
+
"div",
|
|
1148
|
+
{
|
|
1149
|
+
className: "wp-col",
|
|
1150
|
+
style: { alignItems: "center", gap: "var(--wp-space-3)", padding: "48px 0" },
|
|
1151
|
+
children: [
|
|
1152
|
+
/* @__PURE__ */ jsx("span", { className: "wp-body-sm", style: { color: "var(--wp-text-muted)" }, children: offline ? "\uBC31\uC5D4\uB4DC\uAC00 \uC5F0\uACB0\uB418\uC9C0 \uC54A\uC544 \uC870\uC9C1\uB3C4\uB97C \uD45C\uC2DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4." : "\uC870\uC9C1\uB3C4 \uC815\uBCF4\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4." }),
|
|
1153
|
+
offline ? null : /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: () => void load(), children: "\uB2E4\uC2DC \uC2DC\uB3C4" })
|
|
1154
|
+
]
|
|
1155
|
+
}
|
|
1156
|
+
);
|
|
1157
|
+
} else if (model.visibleRoots.length === 0) {
|
|
1158
|
+
content = /* @__PURE__ */ jsx(
|
|
1159
|
+
"div",
|
|
1160
|
+
{
|
|
1161
|
+
className: "wp-body-sm",
|
|
1162
|
+
style: {
|
|
1163
|
+
minHeight: 288,
|
|
1164
|
+
display: "flex",
|
|
1165
|
+
alignItems: "center",
|
|
1166
|
+
justifyContent: "center",
|
|
1167
|
+
color: "var(--wp-text-muted)"
|
|
1168
|
+
},
|
|
1169
|
+
children: "\uD45C\uC2DC\uD560 \uC870\uC9C1\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."
|
|
1170
|
+
}
|
|
1171
|
+
);
|
|
1172
|
+
} else {
|
|
1173
|
+
content = /* @__PURE__ */ jsx(
|
|
1174
|
+
OrganizationCanvas,
|
|
1175
|
+
{
|
|
1176
|
+
model,
|
|
1177
|
+
employees,
|
|
1178
|
+
selectedGroupId,
|
|
1179
|
+
onSelect: setSelectedGroupId,
|
|
1180
|
+
onOpenCard: openCard,
|
|
1181
|
+
height
|
|
1182
|
+
}
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
return /* @__PURE__ */ jsxs(
|
|
1186
|
+
"div",
|
|
1187
|
+
{
|
|
1188
|
+
style: {
|
|
1189
|
+
border: "var(--wp-border-w) solid var(--wp-border-default)",
|
|
1190
|
+
borderRadius: "var(--wp-radius-card)",
|
|
1191
|
+
background: "var(--wp-surface-card)",
|
|
1192
|
+
overflow: "hidden"
|
|
1193
|
+
},
|
|
1194
|
+
children: [
|
|
1195
|
+
/* @__PURE__ */ jsx("style", { children: OCB_CSS }),
|
|
1196
|
+
/* @__PURE__ */ jsxs(
|
|
1197
|
+
"div",
|
|
1198
|
+
{
|
|
1199
|
+
className: "wp-row",
|
|
1200
|
+
style: {
|
|
1201
|
+
alignItems: "center",
|
|
1202
|
+
justifyContent: "space-between",
|
|
1203
|
+
flexWrap: "wrap",
|
|
1204
|
+
gap: "var(--wp-space-2)",
|
|
1205
|
+
padding: "var(--wp-space-3) var(--wp-space-4)"
|
|
1206
|
+
},
|
|
1207
|
+
children: [
|
|
1208
|
+
/* @__PURE__ */ jsx("span", { className: "wp-label", children: "\uC870\uC9C1\uB3C4" }),
|
|
1209
|
+
/* @__PURE__ */ jsxs("span", { className: "wp-row", style: { alignItems: "center", gap: "var(--wp-space-2)" }, children: [
|
|
1210
|
+
state === "ready" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1211
|
+
/* @__PURE__ */ jsxs(Badge, { children: [
|
|
1212
|
+
model.visibleRoots.length,
|
|
1213
|
+
"\uAC1C \uB8E8\uD2B8"
|
|
1214
|
+
] }),
|
|
1215
|
+
/* @__PURE__ */ jsxs(Badge, { children: [
|
|
1216
|
+
employees.length,
|
|
1217
|
+
"\uBA85"
|
|
1218
|
+
] })
|
|
1219
|
+
] }) : null,
|
|
1220
|
+
/* @__PURE__ */ jsx(
|
|
1221
|
+
Button,
|
|
1222
|
+
{
|
|
1223
|
+
size: "sm",
|
|
1224
|
+
variant: "secondary",
|
|
1225
|
+
icon: "refresh-cw",
|
|
1226
|
+
disabled: state === "loading",
|
|
1227
|
+
onClick: () => void load(),
|
|
1228
|
+
children: "\uC0C8\uB85C\uACE0\uCE68"
|
|
1229
|
+
}
|
|
1230
|
+
)
|
|
1231
|
+
] })
|
|
1232
|
+
]
|
|
1233
|
+
}
|
|
1234
|
+
),
|
|
1235
|
+
content
|
|
1236
|
+
]
|
|
1237
|
+
}
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
var OrgChartBoard = boxed(OrgChartBoardImpl, "OrgChartBoard");
|
|
1241
|
+
|
|
1242
|
+
export { OrgChartBoard };
|