gantt-lib 0.82.0 → 0.83.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/index.js CHANGED
@@ -7137,6 +7137,7 @@ var TaskList = ({
7137
7137
  [resolvedColumns]
7138
7138
  );
7139
7139
  const effectiveTaskListWidth = Math.max(taskListWidth, MIN_TASK_LIST_WIDTH, resolvedColumnWidthTotal);
7140
+ const tableHeaderHeight = headerHeight + 1;
7140
7141
  return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
7141
7142
  "div",
7142
7143
  {
@@ -7144,7 +7145,7 @@ var TaskList = ({
7144
7145
  className: `gantt-tl-overlay${show ? "" : " gantt-tl-hidden"}${hasRightShadow ? " gantt-tl-overlay-shadowed" : ""}`,
7145
7146
  style: { "--tasklist-width": `${effectiveTaskListWidth}px` },
7146
7147
  children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "gantt-tl-table", children: [
7147
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "gantt-tl-header", style: { height: `${headerHeight + 0.5}px` }, children: resolvedColumns.map((col) => {
7148
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "gantt-tl-header", style: { height: `${tableHeaderHeight}px` }, children: resolvedColumns.map((col) => {
7148
7149
  if (col.id === "dependencies") {
7149
7150
  return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
7150
7151
  "div",
@@ -7328,7 +7329,7 @@ var TaskList = ({
7328
7329
  };
7329
7330
 
7330
7331
  // src/components/ResourceTimelineChart/ResourceTimelineChart.tsx
7331
- var import_react14 = require("react");
7332
+ var import_react14 = __toESM(require("react"));
7332
7333
 
7333
7334
  // src/utils/resourceTimelineLayout.ts
7334
7335
  var isInvalidDate = (date) => Number.isNaN(date.getTime());
@@ -7748,6 +7749,7 @@ var DEFAULT_HEADER_HEIGHT = 40;
7748
7749
  var DEFAULT_LANE_HEIGHT = 40;
7749
7750
  var DEFAULT_ROW_HEADER_WIDTH = 420;
7750
7751
  var DEFAULT_RESOURCE_ROW_GAP = 8;
7752
+ var DEFAULT_RESOURCE_GROUP_HEIGHT = 28;
7751
7753
  var ITEM_OUTER_VERTICAL_INSET = 2;
7752
7754
  var ITEM_INNER_VERTICAL_INSET = 1;
7753
7755
  var ITEM_START_HORIZONTAL_INSET = 2;
@@ -7846,6 +7848,30 @@ var RESOURCE_TYPE_CLASS_NAMES = {
7846
7848
  \u041C\u0430\u0442\u0435\u0440\u0438\u0430\u043B\u044B: "Materials",
7847
7849
  \u0414\u0440\u0443\u0433\u043E\u0435: "Other"
7848
7850
  };
7851
+ var RESOURCE_TYPE_ORDER = {
7852
+ \u041B\u044E\u0434\u0438: 0,
7853
+ \u041E\u0431\u043E\u0440\u0443\u0434\u043E\u0432\u0430\u043D\u0438\u0435: 1,
7854
+ \u041C\u0430\u0442\u0435\u0440\u0438\u0430\u043B\u044B: 2,
7855
+ \u0414\u0440\u0443\u0433\u043E\u0435: 3
7856
+ };
7857
+ var RESOURCE_SCOPE_ORDER = {
7858
+ Shared: 0,
7859
+ Project: 1
7860
+ };
7861
+ var getResourceType = (resource) => resource.type ?? "\u0414\u0440\u0443\u0433\u043E\u0435";
7862
+ var getResourceScopeOrder = (resource) => RESOURCE_SCOPE_ORDER[resource.scope ?? "Project"] ?? 99;
7863
+ var orderResourcesByType = (resources) => {
7864
+ return resources.map((resource, index) => ({ resource, index })).sort((left, right) => {
7865
+ const leftType = getResourceType(left.resource);
7866
+ const rightType = getResourceType(right.resource);
7867
+ const typeDiff = (RESOURCE_TYPE_ORDER[leftType] ?? 99) - (RESOURCE_TYPE_ORDER[rightType] ?? 99);
7868
+ if (typeDiff !== 0) {
7869
+ return typeDiff;
7870
+ }
7871
+ const scopeDiff = getResourceScopeOrder(left.resource) - getResourceScopeOrder(right.resource);
7872
+ return scopeDiff !== 0 ? scopeDiff : left.index - right.index;
7873
+ }).map(({ resource }) => resource);
7874
+ };
7849
7875
  var ResourceTypeIcon = ({ type }) => {
7850
7876
  if (type === "\u041B\u044E\u0434\u0438") {
7851
7877
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("svg", { className: "gantt-resourceTimeline-resourceTypeIcon gantt-resourceTimeline-resourceTypeIconPeople", width: "16", height: "16", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
@@ -7886,12 +7912,16 @@ var ResourceHeader = ({
7886
7912
  paddingBottom,
7887
7913
  menuCommands,
7888
7914
  onResourceChange,
7915
+ onResourceNameClick,
7889
7916
  onConflictBadgeClick
7890
7917
  }) => {
7891
7918
  const [menuOpen, setMenuOpen] = (0, import_react14.useState)(false);
7892
7919
  const [typeMenuOpen, setTypeMenuOpen] = (0, import_react14.useState)(false);
7893
7920
  const [scopeMenuOpen, setScopeMenuOpen] = (0, import_react14.useState)(false);
7894
- const [draftName, setDraftName] = (0, import_react14.useState)(resource.name);
7921
+ const [editingName, setEditingName] = (0, import_react14.useState)(false);
7922
+ const [nameValue, setNameValue] = (0, import_react14.useState)(resource.name);
7923
+ const nameInputRef = (0, import_react14.useRef)(null);
7924
+ const nameConfirmedRef = (0, import_react14.useRef)(false);
7895
7925
  const visibleCommands = (0, import_react14.useMemo)(
7896
7926
  () => menuCommands.filter((command) => command.isVisible?.(resource) ?? true),
7897
7927
  [menuCommands, resource]
@@ -7900,31 +7930,20 @@ var ResourceHeader = ({
7900
7930
  const type = resource.type ?? "\u0414\u0440\u0443\u0433\u043E\u0435";
7901
7931
  const scope = resource.scope ?? "Project";
7902
7932
  const scopeLabel = RESOURCE_SCOPE_LABELS[scope] ?? scope;
7903
- (0, import_react14.useEffect)(() => {
7904
- setDraftName(resource.name);
7905
- }, [resource.name]);
7906
7933
  const applyResourcePatch = (0, import_react14.useCallback)((patch) => {
7907
7934
  onResourceChange?.({ ...resource, ...patch });
7908
7935
  }, [onResourceChange, resource]);
7909
- const handleNameCommit = (0, import_react14.useCallback)(() => {
7910
- const nextName = draftName.trim();
7911
- if (!nextName) {
7912
- setDraftName(resource.name);
7913
- return;
7914
- }
7915
- if (nextName !== resource.name) {
7916
- applyResourcePatch({ name: nextName });
7936
+ (0, import_react14.useEffect)(() => {
7937
+ if (!editingName) {
7938
+ setNameValue(resource.name);
7917
7939
  }
7918
- }, [applyResourcePatch, draftName, resource.name]);
7919
- const handleNameKeyDown = (0, import_react14.useCallback)((event) => {
7920
- if (event.key === "Enter") {
7921
- event.preventDefault();
7922
- event.currentTarget.blur();
7923
- } else if (event.key === "Escape") {
7924
- setDraftName(resource.name);
7925
- event.currentTarget.blur();
7940
+ }, [editingName, resource.name]);
7941
+ (0, import_react14.useEffect)(() => {
7942
+ if (editingName && nameInputRef.current) {
7943
+ nameInputRef.current.focus();
7944
+ nameInputRef.current.select();
7926
7945
  }
7927
- }, [resource.name]);
7946
+ }, [editingName]);
7928
7947
  const handleCommandClick = (command, event) => {
7929
7948
  event.stopPropagation();
7930
7949
  if (command.closeOnSelect !== false) {
@@ -7932,6 +7951,42 @@ var ResourceHeader = ({
7932
7951
  }
7933
7952
  command.onSelect(resource);
7934
7953
  };
7954
+ const handleNameDoubleClick = (0, import_react14.useCallback)((event) => {
7955
+ if (!onResourceChange) {
7956
+ return;
7957
+ }
7958
+ event.stopPropagation();
7959
+ nameConfirmedRef.current = false;
7960
+ setNameValue(resource.name);
7961
+ setEditingName(true);
7962
+ }, [onResourceChange, resource.name]);
7963
+ const handleNameSave = (0, import_react14.useCallback)(() => {
7964
+ if (nameConfirmedRef.current) {
7965
+ nameConfirmedRef.current = false;
7966
+ return;
7967
+ }
7968
+ const nextName = nameValue.trim();
7969
+ if (nextName && nextName !== resource.name) {
7970
+ applyResourcePatch({ name: nextName });
7971
+ }
7972
+ setEditingName(false);
7973
+ }, [applyResourcePatch, nameValue, resource.name]);
7974
+ const handleNameCancel = (0, import_react14.useCallback)(() => {
7975
+ setNameValue(resource.name);
7976
+ setEditingName(false);
7977
+ }, [resource.name]);
7978
+ const handleNameKeyDown = (0, import_react14.useCallback)((event) => {
7979
+ if (event.key === "Enter") {
7980
+ nameConfirmedRef.current = true;
7981
+ const nextName = nameValue.trim();
7982
+ if (nextName && nextName !== resource.name) {
7983
+ applyResourcePatch({ name: nextName });
7984
+ }
7985
+ setEditingName(false);
7986
+ } else if (event.key === "Escape") {
7987
+ handleNameCancel();
7988
+ }
7989
+ }, [applyResourcePatch, handleNameCancel, nameValue, resource.name]);
7935
7990
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7936
7991
  "div",
7937
7992
  {
@@ -7978,18 +8033,31 @@ var ResourceHeader = ({
7978
8033
  option
7979
8034
  )) })
7980
8035
  ] }),
7981
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
7982
- "textarea",
8036
+ editingName ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
8037
+ Input,
7983
8038
  {
7984
- className: "gantt-resourceTimeline-resourceNameInput",
7985
- value: draftName,
7986
- disabled: !onResourceChange,
7987
- "aria-label": `\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u0430 ${resource.name}`,
7988
- rows: 2,
7989
- onChange: (event) => setDraftName(event.target.value),
7990
- onBlur: handleNameCommit,
8039
+ ref: nameInputRef,
8040
+ value: nameValue,
8041
+ onChange: (event) => setNameValue(event.target.value),
8042
+ onBlur: handleNameSave,
7991
8043
  onKeyDown: handleNameKeyDown,
7992
- onClick: (event) => event.stopPropagation()
8044
+ onClick: (event) => event.stopPropagation(),
8045
+ "aria-label": `\u041D\u043E\u0432\u043E\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u0430 ${resource.name}`,
8046
+ className: "gantt-tl-name-input gantt-resourceTimeline-resourceNameInput"
8047
+ }
8048
+ ) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
8049
+ "button",
8050
+ {
8051
+ type: "button",
8052
+ className: "gantt-resourceTimeline-resourceNameButton",
8053
+ "aria-label": `\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u0430 ${resource.name}`,
8054
+ title: resource.name,
8055
+ onClick: (event) => {
8056
+ event.stopPropagation();
8057
+ onResourceNameClick?.(resourceId);
8058
+ },
8059
+ onDoubleClick: handleNameDoubleClick,
8060
+ children: resource.name
7993
8061
  }
7994
8062
  )
7995
8063
  ] }),
@@ -8033,15 +8101,15 @@ var ResourceHeader = ({
8033
8101
  "aria-label": `\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F \u0440\u0435\u0441\u0443\u0440\u0441\u0430 ${resource.name}: ${assignmentCount}, ${workedDays} \u0434\u043D.`,
8034
8102
  children: [
8035
8103
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "gantt-resourceTimeline-resourceWorkedDays", children: [
8036
- workedDays,
8037
- " \u0434\u043D."
8104
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "gantt-resourceTimeline-resourceMetricValue", children: workedDays > 0 ? workedDays : "-" }),
8105
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "gantt-resourceTimeline-resourceMetricLabel", children: workedDays > 0 ? "\u0434\u043D." : "" })
8038
8106
  ] }),
8039
8107
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "gantt-resourceTimeline-resourceAssignmentCount", children: [
8040
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("svg", { className: "gantt-resourceTimeline-resourceAssignmentIcon", width: "14", height: "14", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
8108
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "gantt-resourceTimeline-resourceMetricValue", children: assignmentCount > 0 ? assignmentCount : "" }),
8109
+ assignmentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("svg", { className: "gantt-resourceTimeline-resourceAssignmentIcon", width: "14", height: "14", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
8041
8110
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("rect", { width: "15", height: "5", x: "4", y: "5", rx: "2" }),
8042
8111
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("rect", { width: "10", height: "5", x: "4", y: "14", rx: "2" })
8043
- ] }),
8044
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { children: assignmentCount })
8112
+ ] })
8045
8113
  ] })
8046
8114
  ]
8047
8115
  }
@@ -8166,6 +8234,7 @@ function ResourceTimelineChart({
8166
8234
  customDays,
8167
8235
  isWeekend: isWeekend3,
8168
8236
  businessDays = true,
8237
+ resourceGrouping = false,
8169
8238
  readonly,
8170
8239
  disableResourceReassignment,
8171
8240
  renderItem,
@@ -8183,9 +8252,12 @@ function ResourceTimelineChart({
8183
8252
  const gridRef = (0, import_react14.useRef)(null);
8184
8253
  const panStateRef = (0, import_react14.useRef)(null);
8185
8254
  const conflictNavigationIndexRef = (0, import_react14.useRef)(/* @__PURE__ */ new Map());
8255
+ const resourceNavigationIndexRef = (0, import_react14.useRef)(/* @__PURE__ */ new Map());
8186
8256
  const conflictHighlightTimeoutRef = (0, import_react14.useRef)(null);
8187
8257
  const [isCreatingResource, setIsCreatingResource] = (0, import_react14.useState)(false);
8258
+ const [creatingResourceGroupType, setCreatingResourceGroupType] = (0, import_react14.useState)(null);
8188
8259
  const [activeConflictItemId, setActiveConflictItemId] = (0, import_react14.useState)(null);
8260
+ const [resourceColumnHasRightShadow, setResourceColumnHasRightShadow] = (0, import_react14.useState)(false);
8189
8261
  const validItems = (0, import_react14.useMemo)(() => collectValidItems(resources), [resources]);
8190
8262
  const dateRange = (0, import_react14.useMemo)(() => getMultiMonthDays(validItems), [validItems]);
8191
8263
  const monthStart = (0, import_react14.useMemo)(() => {
@@ -8198,6 +8270,10 @@ function ResourceTimelineChart({
8198
8270
  );
8199
8271
  const gridWidth = (0, import_react14.useMemo)(() => Math.round(dateRange.length * dayWidth), [dateRange.length, dayWidth]);
8200
8272
  const effectiveRowHeaderWidth = Math.max(rowHeaderWidth, DEFAULT_ROW_HEADER_WIDTH);
8273
+ const orderedResources = (0, import_react14.useMemo)(
8274
+ () => resourceGrouping === "type" ? orderResourcesByType(resources) : resources,
8275
+ [resourceGrouping, resources]
8276
+ );
8201
8277
  const workedDaysByResourceId = (0, import_react14.useMemo)(() => {
8202
8278
  const map = /* @__PURE__ */ new Map();
8203
8279
  for (const resource of resources) {
@@ -8218,14 +8294,87 @@ function ResourceTimelineChart({
8218
8294
  return map;
8219
8295
  }, [businessDays, resources, weekendPredicate]);
8220
8296
  const layout = (0, import_react14.useMemo)(
8221
- () => layoutResourceTimelineItems(resources, {
8297
+ () => layoutResourceTimelineItems(orderedResources, {
8222
8298
  monthStart,
8223
8299
  dayWidth,
8224
8300
  laneHeight,
8225
8301
  rowGap: DEFAULT_RESOURCE_ROW_GAP
8226
8302
  }),
8227
- [resources, monthStart, dayWidth, laneHeight]
8303
+ [orderedResources, monthStart, dayWidth, laneHeight]
8228
8304
  );
8305
+ const canAddResource = enableAddResource && Boolean(onAddResource);
8306
+ const resourceAddRowHeight = laneHeight + DEFAULT_RESOURCE_ROW_GAP;
8307
+ const displayLayout = (0, import_react14.useMemo)(() => {
8308
+ if (resourceGrouping !== "type") {
8309
+ return {
8310
+ rows: layout.rows,
8311
+ items: layout.items,
8312
+ groupHeaders: [],
8313
+ groupAddRows: [],
8314
+ totalHeight: layout.totalHeight
8315
+ };
8316
+ }
8317
+ const countsByType = /* @__PURE__ */ new Map();
8318
+ for (const row of layout.rows) {
8319
+ const type = getResourceType(row.resource);
8320
+ countsByType.set(type, (countsByType.get(type) ?? 0) + 1);
8321
+ }
8322
+ let offset = 0;
8323
+ let previousType = null;
8324
+ const rowTopOffsets = /* @__PURE__ */ new Map();
8325
+ const groupHeaders = [];
8326
+ const groupAddRows = [];
8327
+ const rows = layout.rows.map((row) => {
8328
+ const type = getResourceType(row.resource);
8329
+ if (type !== previousType) {
8330
+ if (previousType !== null && creatingResourceGroupType === previousType) {
8331
+ groupAddRows.push({
8332
+ key: previousType,
8333
+ top: row.resourceRowTop + offset,
8334
+ height: resourceAddRowHeight
8335
+ });
8336
+ offset += resourceAddRowHeight;
8337
+ }
8338
+ groupHeaders.push({
8339
+ key: type,
8340
+ label: type,
8341
+ count: countsByType.get(type) ?? 0,
8342
+ top: row.resourceRowTop + offset,
8343
+ height: DEFAULT_RESOURCE_GROUP_HEIGHT
8344
+ });
8345
+ offset += DEFAULT_RESOURCE_GROUP_HEIGHT;
8346
+ previousType = type;
8347
+ }
8348
+ rowTopOffsets.set(row.resourceId, offset);
8349
+ return {
8350
+ ...row,
8351
+ resourceRowTop: row.resourceRowTop + offset
8352
+ };
8353
+ });
8354
+ if (previousType !== null && creatingResourceGroupType === previousType) {
8355
+ groupAddRows.push({
8356
+ key: previousType,
8357
+ top: layout.totalHeight + offset,
8358
+ height: resourceAddRowHeight
8359
+ });
8360
+ offset += resourceAddRowHeight;
8361
+ }
8362
+ const items = layout.items.map((item) => {
8363
+ const itemOffset = rowTopOffsets.get(item.resourceId) ?? 0;
8364
+ return {
8365
+ ...item,
8366
+ resourceRowTop: item.resourceRowTop + itemOffset,
8367
+ top: item.top + itemOffset
8368
+ };
8369
+ });
8370
+ return {
8371
+ rows,
8372
+ items,
8373
+ groupHeaders,
8374
+ groupAddRows,
8375
+ totalHeight: layout.totalHeight + offset
8376
+ };
8377
+ }, [creatingResourceGroupType, layout, resourceAddRowHeight, resourceGrouping]);
8229
8378
  const todayInRange = (0, import_react14.useMemo)(() => {
8230
8379
  const now = /* @__PURE__ */ new Date();
8231
8380
  const today = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
@@ -8233,16 +8382,16 @@ function ResourceTimelineChart({
8233
8382
  }, [dateRange]);
8234
8383
  const itemsByResourceId = (0, import_react14.useMemo)(() => {
8235
8384
  const map = /* @__PURE__ */ new Map();
8236
- for (const item of layout.items) {
8385
+ for (const item of displayLayout.items) {
8237
8386
  const next = map.get(item.resourceId) ?? [];
8238
8387
  next.push(item);
8239
8388
  map.set(item.resourceId, next);
8240
8389
  }
8241
8390
  return map;
8242
- }, [layout.items]);
8391
+ }, [displayLayout.items]);
8243
8392
  const conflictItemsByResourceId = (0, import_react14.useMemo)(() => {
8244
8393
  const map = /* @__PURE__ */ new Map();
8245
- for (const item of layout.items) {
8394
+ for (const item of displayLayout.items) {
8246
8395
  if (item.conflictRanges.length === 0) {
8247
8396
  continue;
8248
8397
  }
@@ -8256,21 +8405,28 @@ function ResourceTimelineChart({
8256
8405
  );
8257
8406
  }
8258
8407
  return map;
8259
- }, [layout.items]);
8260
- const canAddResource = enableAddResource && Boolean(onAddResource);
8261
- const resourceAddRowHeight = laneHeight + DEFAULT_RESOURCE_ROW_GAP;
8262
- const displayTotalHeight = layout.totalHeight + (canAddResource ? resourceAddRowHeight : 0);
8408
+ }, [displayLayout.items]);
8409
+ const displayTotalHeight = displayLayout.totalHeight + (canAddResource ? resourceAddRowHeight : 0);
8410
+ const timelineHeaderHeight = headerHeight + 1;
8263
8411
  const handleConfirmNewResource = (0, import_react14.useCallback)((name) => {
8264
8412
  onAddResource?.({
8265
8413
  id: crypto.randomUUID(),
8266
8414
  name,
8267
- type: "\u0414\u0440\u0443\u0433\u043E\u0435",
8415
+ type: creatingResourceGroupType ?? "\u0414\u0440\u0443\u0433\u043E\u0435",
8268
8416
  scope: "Project",
8269
8417
  items: []
8270
8418
  });
8271
8419
  setIsCreatingResource(false);
8272
- }, [onAddResource]);
8273
- const handleCancelNewResource = (0, import_react14.useCallback)(() => setIsCreatingResource(false), []);
8420
+ setCreatingResourceGroupType(null);
8421
+ }, [creatingResourceGroupType, onAddResource]);
8422
+ const handleStartAddResourceToGroup = (0, import_react14.useCallback)((type) => {
8423
+ setIsCreatingResource(false);
8424
+ setCreatingResourceGroupType(type);
8425
+ }, []);
8426
+ const handleCancelNewResource = (0, import_react14.useCallback)(() => {
8427
+ setIsCreatingResource(false);
8428
+ setCreatingResourceGroupType(null);
8429
+ }, []);
8274
8430
  const handleConflictBadgeClick = (0, import_react14.useCallback)((resourceId) => {
8275
8431
  const conflictItems = conflictItemsByResourceId.get(resourceId) ?? [];
8276
8432
  if (conflictItems.length === 0) {
@@ -8296,10 +8452,31 @@ function ResourceTimelineChart({
8296
8452
  conflictHighlightTimeoutRef.current = null;
8297
8453
  }, 1600);
8298
8454
  }, [conflictItemsByResourceId, dayWidth, laneHeight]);
8455
+ const handleResourceNameClick = (0, import_react14.useCallback)((resourceId) => {
8456
+ const resourceItems = itemsByResourceId.get(resourceId) ?? [];
8457
+ if (resourceItems.length === 0) {
8458
+ return;
8459
+ }
8460
+ const orderedItems = [...resourceItems].sort(
8461
+ (left, right) => left.left - right.left || left.top - right.top || left.itemId.localeCompare(right.itemId)
8462
+ );
8463
+ const currentIndex = resourceNavigationIndexRef.current.get(resourceId) ?? 0;
8464
+ const targetItem = orderedItems[currentIndex % orderedItems.length];
8465
+ resourceNavigationIndexRef.current.set(resourceId, (currentIndex + 1) % orderedItems.length);
8466
+ const container = scrollContainerRef.current;
8467
+ if (!container || !targetItem) {
8468
+ return;
8469
+ }
8470
+ container.scrollTo({
8471
+ left: Math.max(0, Math.round(targetItem.left - dayWidth * 2)),
8472
+ top: Math.max(0, Math.round(targetItem.top - laneHeight)),
8473
+ behavior: "smooth"
8474
+ });
8475
+ }, [dayWidth, itemsByResourceId, laneHeight]);
8299
8476
  const { preview, startDrag } = useResourceItemDrag({
8300
8477
  dayWidth,
8301
8478
  monthStart,
8302
- rows: layout.rows,
8479
+ rows: displayLayout.rows,
8303
8480
  gridElementRef: gridRef,
8304
8481
  readonly,
8305
8482
  disableResourceReassignment,
@@ -8307,6 +8484,18 @@ function ResourceTimelineChart({
8307
8484
  weekendPredicate,
8308
8485
  onResourceItemMove
8309
8486
  });
8487
+ (0, import_react14.useEffect)(() => {
8488
+ const container = scrollContainerRef.current;
8489
+ if (!container) return;
8490
+ const updateShadow = () => {
8491
+ setResourceColumnHasRightShadow(container.scrollLeft > 0);
8492
+ };
8493
+ updateShadow();
8494
+ container.addEventListener("scroll", updateShadow, { passive: true });
8495
+ return () => {
8496
+ container.removeEventListener("scroll", updateShadow);
8497
+ };
8498
+ }, []);
8310
8499
  const handlePanStart = (0, import_react14.useCallback)((event) => {
8311
8500
  if (event.button !== 0) {
8312
8501
  return;
@@ -8364,6 +8553,16 @@ function ResourceTimelineChart({
8364
8553
  window.removeEventListener("mouseup", handlePanEnd);
8365
8554
  };
8366
8555
  }, [allowVerticalPan]);
8556
+ (0, import_react14.useEffect)(() => {
8557
+ const nextNavigation = /* @__PURE__ */ new Map();
8558
+ for (const [resourceId, resourceItems] of itemsByResourceId.entries()) {
8559
+ if (resourceItems.length === 0) {
8560
+ continue;
8561
+ }
8562
+ nextNavigation.set(resourceId, (resourceNavigationIndexRef.current.get(resourceId) ?? 0) % resourceItems.length);
8563
+ }
8564
+ resourceNavigationIndexRef.current = nextNavigation;
8565
+ }, [itemsByResourceId]);
8367
8566
  (0, import_react14.useEffect)(() => {
8368
8567
  return () => {
8369
8568
  if (conflictHighlightTimeoutRef.current) {
@@ -8387,14 +8586,14 @@ function ResourceTimelineChart({
8387
8586
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
8388
8587
  "div",
8389
8588
  {
8390
- className: "gantt-resourceTimeline-resourceColumn",
8589
+ className: `gantt-resourceTimeline-resourceColumn${resourceColumnHasRightShadow ? " gantt-resourceTimeline-resourceColumnShadowed" : ""}`,
8391
8590
  style: { width: `${effectiveRowHeaderWidth}px`, minWidth: `${effectiveRowHeaderWidth}px` },
8392
8591
  children: [
8393
8592
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
8394
8593
  "div",
8395
8594
  {
8396
8595
  className: "gantt-resourceTimeline-corner",
8397
- style: { height: `${headerHeight + 0.5}px` },
8596
+ style: { height: `${timelineHeaderHeight}px` },
8398
8597
  children: [
8399
8598
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "gantt-resourceTimeline-resourceHeaderCell gantt-resourceTimeline-resourceHeaderNumber", children: "#" }),
8400
8599
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "gantt-resourceTimeline-resourceHeaderCell gantt-resourceTimeline-resourceHeaderName", children: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435" }),
@@ -8404,7 +8603,62 @@ function ResourceTimelineChart({
8404
8603
  ]
8405
8604
  }
8406
8605
  ),
8407
- layout.rows.map((row, rowIndex) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
8606
+ displayLayout.groupHeaders.length > 0 ? displayLayout.groupHeaders.map((group) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_react14.default.Fragment, { children: [
8607
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
8608
+ "div",
8609
+ {
8610
+ className: "gantt-resourceTimeline-resourceGroupHeader",
8611
+ style: { height: `${group.height}px` },
8612
+ children: [
8613
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "gantt-resourceTimeline-resourceGroupTitle", children: group.label }),
8614
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "gantt-resourceTimeline-resourceGroupCount", children: group.count }),
8615
+ canAddResource && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
8616
+ "button",
8617
+ {
8618
+ type: "button",
8619
+ className: "gantt-resourceTimeline-resourceGroupAddButton",
8620
+ "aria-label": `\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0440\u0435\u0441\u0443\u0440\u0441 \u0432 \u0433\u0440\u0443\u043F\u043F\u0443 ${group.label}`,
8621
+ title: `\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0440\u0435\u0441\u0443\u0440\u0441 \u0432 \u0433\u0440\u0443\u043F\u043F\u0443 ${group.label}`,
8622
+ onClick: (event) => {
8623
+ event.stopPropagation();
8624
+ handleStartAddResourceToGroup(group.key);
8625
+ },
8626
+ children: "+"
8627
+ }
8628
+ )
8629
+ ]
8630
+ }
8631
+ ),
8632
+ displayLayout.rows.filter((row) => getResourceType(row.resource) === group.key).map((row) => {
8633
+ const rowIndex = displayLayout.rows.findIndex((candidate) => candidate.resourceId === row.resourceId);
8634
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
8635
+ ResourceHeader,
8636
+ {
8637
+ resource: row.resource,
8638
+ resourceId: row.resourceId,
8639
+ rowIndex,
8640
+ conflictCount: row.conflictCount,
8641
+ workedDays: workedDaysByResourceId.get(row.resourceId) ?? 0,
8642
+ assignmentCount: row.resource.items.length,
8643
+ height: row.resourceRowHeight + DEFAULT_RESOURCE_ROW_GAP,
8644
+ paddingBottom: DEFAULT_RESOURCE_ROW_GAP,
8645
+ menuCommands: resourceMenuCommands,
8646
+ onResourceChange,
8647
+ onResourceNameClick: handleResourceNameClick,
8648
+ onConflictBadgeClick: handleConflictBadgeClick
8649
+ },
8650
+ row.resourceId
8651
+ );
8652
+ }),
8653
+ displayLayout.groupAddRows.some((row) => row.key === group.key) && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
8654
+ NewResourceRow,
8655
+ {
8656
+ height: resourceAddRowHeight,
8657
+ onConfirm: handleConfirmNewResource,
8658
+ onCancel: handleCancelNewResource
8659
+ }
8660
+ )
8661
+ ] }, group.key)) : displayLayout.rows.map((row, rowIndex) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
8408
8662
  ResourceHeader,
8409
8663
  {
8410
8664
  resource: row.resource,
@@ -8417,6 +8671,7 @@ function ResourceTimelineChart({
8417
8671
  paddingBottom: DEFAULT_RESOURCE_ROW_GAP,
8418
8672
  menuCommands: resourceMenuCommands,
8419
8673
  onResourceChange,
8674
+ onResourceNameClick: handleResourceNameClick,
8420
8675
  onConflictBadgeClick: handleConflictBadgeClick
8421
8676
  },
8422
8677
  row.resourceId
@@ -8434,7 +8689,10 @@ function ResourceTimelineChart({
8434
8689
  className: "gantt-resourceTimeline-addResourceButton",
8435
8690
  type: "button",
8436
8691
  style: { height: `${resourceAddRowHeight}px` },
8437
- onClick: () => setIsCreatingResource(true),
8692
+ onClick: () => {
8693
+ setCreatingResourceGroupType(null);
8694
+ setIsCreatingResource(true);
8695
+ },
8438
8696
  children: "+ \u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0440\u0435\u0441\u0443\u0440\u0441"
8439
8697
  }
8440
8698
  ))
@@ -8447,16 +8705,23 @@ function ResourceTimelineChart({
8447
8705
  className: "gantt-resourceTimeline-chartSurface",
8448
8706
  style: { minWidth: `${gridWidth}px` },
8449
8707
  children: [
8450
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "gantt-resourceTimeline-stickyHeader", style: { width: `${gridWidth}px` }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
8451
- TimeScaleHeader_default,
8708
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
8709
+ "div",
8452
8710
  {
8453
- days: dateRange,
8454
- dayWidth,
8455
- headerHeight,
8456
- viewMode,
8457
- isCustomWeekend: weekendPredicate
8711
+ className: "gantt-resourceTimeline-stickyHeader",
8712
+ style: { width: `${gridWidth}px`, height: `${timelineHeaderHeight}px` },
8713
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
8714
+ TimeScaleHeader_default,
8715
+ {
8716
+ days: dateRange,
8717
+ dayWidth,
8718
+ headerHeight,
8719
+ viewMode,
8720
+ isCustomWeekend: weekendPredicate
8721
+ }
8722
+ )
8458
8723
  }
8459
- ) }),
8724
+ ),
8460
8725
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
8461
8726
  "div",
8462
8727
  {
@@ -8475,7 +8740,19 @@ function ResourceTimelineChart({
8475
8740
  }
8476
8741
  ),
8477
8742
  todayInRange && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(TodayIndicator_default, { monthStart, dayWidth }),
8478
- layout.rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
8743
+ displayLayout.groupHeaders.map((group) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
8744
+ "div",
8745
+ {
8746
+ className: "gantt-resourceTimeline-row gantt-resourceTimeline-groupRow",
8747
+ "data-resource-group": group.key,
8748
+ style: {
8749
+ top: `${group.top}px`,
8750
+ height: `${group.height}px`
8751
+ }
8752
+ },
8753
+ `group-row-${group.key}`
8754
+ )),
8755
+ displayLayout.rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
8479
8756
  "div",
8480
8757
  {
8481
8758
  className: "gantt-resourceTimeline-row",
@@ -8493,11 +8770,23 @@ function ResourceTimelineChart({
8493
8770
  className: "gantt-resourceTimeline-row gantt-resourceTimeline-rowNew",
8494
8771
  "data-resource-add-row": "true",
8495
8772
  style: {
8496
- top: `${layout.totalHeight}px`,
8773
+ top: `${displayLayout.totalHeight}px`,
8497
8774
  height: `${resourceAddRowHeight}px`
8498
8775
  }
8499
8776
  }
8500
8777
  ),
8778
+ displayLayout.groupAddRows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
8779
+ "div",
8780
+ {
8781
+ className: "gantt-resourceTimeline-row gantt-resourceTimeline-rowNew",
8782
+ "data-resource-group-add-row": row.key,
8783
+ style: {
8784
+ top: `${row.top}px`,
8785
+ height: `${row.height}px`
8786
+ }
8787
+ },
8788
+ `group-add-row-${row.key}`
8789
+ )),
8501
8790
  Array.from(itemsByResourceId.values()).flatMap(
8502
8791
  (resourceItems) => resourceItems.map((layoutItem) => {
8503
8792
  const customClassName = getItemClassName?.(layoutItem.item);
@@ -9104,6 +9393,7 @@ function TaskGanttChartInner(props, ref) {
9104
9393
  () => visibleTasks.length * rowHeight,
9105
9394
  [visibleTasks.length, rowHeight]
9106
9395
  );
9396
+ const timelineHeaderHeight = headerHeight + 1;
9107
9397
  const monthStart = (0, import_react15.useMemo)(() => {
9108
9398
  if (dateRange.length === 0) {
9109
9399
  return new Date(Date.UTC((/* @__PURE__ */ new Date()).getUTCFullYear(), (/* @__PURE__ */ new Date()).getUTCMonth(), 1));
@@ -9586,16 +9876,23 @@ function TaskGanttChartInner(props, ref) {
9586
9876
  className: showChart ? "gantt-chartSurface" : "gantt-chartSurface gantt-chart-hidden",
9587
9877
  style: { minWidth: `${gridWidth}px`, flex: 1, display: showChart ? void 0 : "none" },
9588
9878
  children: [
9589
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "gantt-stickyHeader", style: { width: `${gridWidth}px` }, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
9590
- TimeScaleHeader_default,
9879
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
9880
+ "div",
9591
9881
  {
9592
- days: dateRange,
9593
- dayWidth,
9594
- headerHeight,
9595
- viewMode,
9596
- isCustomWeekend
9882
+ className: "gantt-stickyHeader",
9883
+ style: { width: `${gridWidth}px`, height: `${timelineHeaderHeight}px` },
9884
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
9885
+ TimeScaleHeader_default,
9886
+ {
9887
+ days: dateRange,
9888
+ dayWidth,
9889
+ headerHeight,
9890
+ viewMode,
9891
+ isCustomWeekend
9892
+ }
9893
+ )
9597
9894
  }
9598
- ) }),
9895
+ ),
9599
9896
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
9600
9897
  "div",
9601
9898
  {