@bit.rhplus/ag-grid 0.0.122 → 0.0.124

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.
Files changed (35) hide show
  1. package/Aggregations.js +1 -1
  2. package/BulkEdit/useBulkCellEdit.js +7 -4
  3. package/Editors/ModuleLookupEditor.jsx +123 -34
  4. package/Editors/RouteCellEditor.jsx +124 -0
  5. package/Editors/index.jsx +1 -0
  6. package/OnCellValueChanged.js +2 -2
  7. package/Renderers/RouteCellRenderer.jsx +76 -0
  8. package/Renderers/SelectCellRenderer.jsx +33 -7
  9. package/Renderers/index.jsx +1 -0
  10. package/dist/Aggregations.js +1 -1
  11. package/dist/Aggregations.js.map +1 -1
  12. package/dist/BulkEdit/useBulkCellEdit.js +7 -5
  13. package/dist/BulkEdit/useBulkCellEdit.js.map +1 -1
  14. package/dist/Editors/ModuleLookupEditor.js +103 -32
  15. package/dist/Editors/ModuleLookupEditor.js.map +1 -1
  16. package/dist/Editors/RouteCellEditor.d.ts +21 -0
  17. package/dist/Editors/RouteCellEditor.js +79 -0
  18. package/dist/Editors/RouteCellEditor.js.map +1 -0
  19. package/dist/Editors/index.js +1 -0
  20. package/dist/Editors/index.js.map +1 -1
  21. package/dist/OnCellValueChanged.js +2 -1
  22. package/dist/OnCellValueChanged.js.map +1 -1
  23. package/dist/Renderers/RouteCellRenderer.d.ts +9 -0
  24. package/dist/Renderers/RouteCellRenderer.js +48 -0
  25. package/dist/Renderers/RouteCellRenderer.js.map +1 -0
  26. package/dist/Renderers/SelectCellRenderer.d.ts +1 -5
  27. package/dist/Renderers/SelectCellRenderer.js +23 -5
  28. package/dist/Renderers/SelectCellRenderer.js.map +1 -1
  29. package/dist/Renderers/index.js +1 -0
  30. package/dist/Renderers/index.js.map +1 -1
  31. package/dist/index.js +4 -0
  32. package/dist/index.js.map +1 -1
  33. package/index.jsx +8 -0
  34. package/package.json +6 -5
  35. /package/dist/{preview-1775220690959.js → preview-1775722983848.js} +0 -0
package/Aggregations.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable */
2
2
  // ✅ OPTIMALIZACE: Enumerable odstraněn - nahrazen native JS metodami
3
3
  import moment from 'moment';
4
- import { currencySymbols } from '@bit.rhplus/ui.grid/';
4
+ import { currencySymbols } from '@bit.rhplus/ui.grid';
5
5
 
6
6
  const getColumnOrderDirection = (range) => (range.columns[0].colId === range.startColumn.colId) ? 1 : -1;
7
7
 
@@ -17,6 +17,7 @@ export const useBulkCellEdit = (gridRef, options = {}) => {
17
17
  const {
18
18
  enabled = false,
19
19
  minCells = 2,
20
+ showOnSingleCell = false, // Pokud true, ikona se zobrazí již při výběru 1 buňky (přepíše minCells na 1)
20
21
  allowMultiColumn = false,
21
22
  buttonPosition = 'bottom-right',
22
23
  buttonOffset = { x: 5, y: 5 },
@@ -25,6 +26,8 @@ export const useBulkCellEdit = (gridRef, options = {}) => {
25
26
  onBulkEditComplete,
26
27
  } = options;
27
28
 
29
+ const effectiveMinCells = showOnSingleCell ? 1 : minCells;
30
+
28
31
  const { fetchDataUIAsync } = useData();
29
32
 
30
33
  const [floatingButton, setFloatingButton] = useState({
@@ -114,7 +117,7 @@ export const useBulkCellEdit = (gridRef, options = {}) => {
114
117
  if (allowMultiColumn || range.columns.length === 1) {
115
118
  const cellCount = Math.abs(range.endRow.rowIndex - range.startRow.rowIndex) + 1;
116
119
 
117
- if (cellCount >= minCells) {
120
+ if (cellCount >= effectiveMinCells) {
118
121
  // Kontrola zda má sloupec bulkEditApi (rychlá kontrola)
119
122
  const column = range.columns[0];
120
123
  const colDef = column?.getColDef();
@@ -241,7 +244,7 @@ export const useBulkCellEdit = (gridRef, options = {}) => {
241
244
  }
242
245
  }, 50);
243
246
  },
244
- [enabled, allowMultiColumn, minCells, calculateButtonPosition]
247
+ [enabled, allowMultiColumn, effectiveMinCells, calculateButtonPosition]
245
248
  );
246
249
 
247
250
  /**
@@ -277,7 +280,7 @@ export const useBulkCellEdit = (gridRef, options = {}) => {
277
280
  Math.abs(range.endRow.rowIndex - range.startRow.rowIndex) + 1;
278
281
 
279
282
  // Kontrola min počtu buněk
280
- if (cellCount < minCells) {
283
+ if (cellCount < effectiveMinCells) {
281
284
  pendingFloatingButtonRef.current = null;
282
285
  setFloatingButton({ visible: false });
283
286
  return;
@@ -294,7 +297,7 @@ export const useBulkCellEdit = (gridRef, options = {}) => {
294
297
  // Jediný setFloatingButton call → eliminuje rerender #2
295
298
  setFloatingButton(pendingFloatingButtonRef.current);
296
299
  },
297
- [minCells, allowMultiColumn, buttonPosition, buttonOffset, calculateButtonPosition, gridRef]
300
+ [effectiveMinCells, allowMultiColumn, buttonPosition, buttonOffset, calculateButtonPosition, gridRef]
298
301
  );
299
302
 
300
303
  /**
@@ -61,6 +61,8 @@ const ModuleLookupEditor = React.forwardRef(({
61
61
  stopEditing,
62
62
  moduleDefinition,
63
63
  allowFreeText = false,
64
+ rowHeight = 42,
65
+ autoOpen = true,
64
66
  }, _ref) => {
65
67
 
66
68
  // ─── Hooks (vždy voláno) ──────────────────────────────────────────────────────
@@ -95,6 +97,8 @@ const ModuleLookupEditor = React.forwardRef(({
95
97
 
96
98
  // ─── Catalog-only stav ───────────────────────────────────────────────────────
97
99
  const [catalogSelected, setCatalogSelected] = useState(allowFreeText ? null : value);
100
+ // Ref pro synchronní přístup v isCancelAfterEnd (state je stale v closure při stopEditing)
101
+ const catalogSelectedRef = useRef(allowFreeText ? null : value);
98
102
 
99
103
  // ─── Free-text stav ──────────────────────────────────────────────────────────
100
104
  const initialText = React.useMemo(() => {
@@ -106,6 +110,12 @@ const ModuleLookupEditor = React.forwardRef(({
106
110
 
107
111
  const [inputText, setInputText] = useState(initialText);
108
112
  const [dropdownOpen, setDropdownOpen] = useState(false);
113
+
114
+ useEffect(() => {
115
+ if (autoOpen) {
116
+ setDropdownOpen(true);
117
+ }
118
+ }, [autoOpen]);
109
119
  const [multiMatchData, setMultiMatchData] = useState([]);
110
120
  const [multiMatchModalOpen, setMultiMatchModalOpen] = useState(false);
111
121
  const catalogModalOpenRef = useRef(false);
@@ -114,7 +124,7 @@ const ModuleLookupEditor = React.forwardRef(({
114
124
  multiMatchModalOpenRef.current = multiMatchModalOpen;
115
125
  const currentTextRef = useRef(initialText);
116
126
  const inputTextRef = useRef(initialText); // vždy aktuální inputText pro native handler
117
- const freeTextWrapperRef = useRef(null); // ref na wrapper div → querySelector('input')
127
+ const editorWrapperRef = useRef(null); // ref na wrapper div → querySelector('input')
118
128
  const isStoppedRef = useRef(false);
119
129
  const justSelectedRef = useRef(false);
120
130
  // Nastaven synchronně před každým stopEditing() voláním.
@@ -130,6 +140,7 @@ const ModuleLookupEditor = React.forwardRef(({
130
140
  // Ref na suggestions a handleAutoCompleteSelect — přístupné v capture handleru bez stale closure
131
141
  const suggestionsRef = useRef([]);
132
142
  const handleAutoCompleteSelectRef = useRef(null);
143
+ const handleCatalogSelectChangeRef = useRef(null);
133
144
 
134
145
  // ─── Lokální filtrování návrhů (bez API volání) ───────────────────────────────
135
146
  const suggestions = React.useMemo(() => {
@@ -140,27 +151,64 @@ const ModuleLookupEditor = React.forwardRef(({
140
151
  const label = moduleDefinition?.getDisplayValue?.(item) || item.code || item.name || '';
141
152
  return label.toLowerCase().includes(inputText.toLowerCase());
142
153
  });
143
- return source.slice(0, 15).map(item => ({
144
- value: String(item[vf]),
145
- label: moduleDefinition?.getDisplayValue?.(item) || item.code || item.name || '',
146
- item,
147
- }));
148
- }, [allItems, inputText, moduleDefinition]);
154
+ const result = source.map(item => {
155
+ const displayText = moduleDefinition?.getDisplayValue?.(item) || item.code || item.name || String(item[vf] || '');
156
+ const originalLabel = moduleDefinition?.renderOption ? moduleDefinition.renderOption(item, displayText) : displayText;
157
+ return {
158
+ value: String(item[vf]),
159
+ label: (
160
+ <div data-mle-value={String(item[vf])} style={{ display: 'contents' }}>
161
+ {originalLabel}
162
+ </div>
163
+ ),
164
+ displayText,
165
+ item,
166
+ };
167
+ });
168
+ return allowFreeText ? result.slice(0, 50) : result;
169
+ }, [allItems, inputText, moduleDefinition, allowFreeText]);
149
170
  suggestionsRef.current = suggestions;
150
171
 
151
- // ─── AG Grid isCancelAfterEnd ─────────────────────────────────────────────────
152
- // Poznámka: AG Grid 35 používá CellEditorCompProxy.getValue() (vrací compProxy.value aktualizované
153
- // přes onValueChange prop), nikoli useImperativeHandle ref. useImperativeHandle zde není potřeba.
172
+ // ─── AG Grid useGridCellEditor ───────────────────────────────────────────────
173
+ // AG Grid 35 vyžaduje getValue() pro získání výsledné hodnoty při stopEditing().
174
+ // Bez getValue() může grid ignorovat změnu, i když byl volán onValueChange.
175
+ const initialValueRef = useRef(value);
176
+
154
177
  useGridCellEditor({
178
+ getValue: () => {
179
+ // Pokud máme pendingValue (zrovna vybráno), použijeme ji
180
+ if (pendingValueRef.current !== null) return pendingValueRef.current;
181
+
182
+ // Catalog-only mód: catalogSelectedRef je vždy aktuální
183
+ if (!allowFreeText) return catalogSelectedRef.current;
184
+
185
+ // Fallback na aktuální stav textu pro free-text
186
+ return { id: null, text: currentTextRef.current || '' };
187
+ },
155
188
  isCancelAfterEnd: () => {
189
+ // Pokud existuje pendingValue, uživatel explicitně něco vybral/potvrdil -> NErušit.
190
+ if (pendingValueRef.current !== null) {
191
+ console.log('[MLE] isCancelAfterEnd: false (has pending value)');
192
+ return false;
193
+ }
194
+
156
195
  if (!allowFreeText) {
157
- return !catalogSelected || catalogSelected === value;
196
+ // Porovnání pomocí ID proti INITIAL hodnotě (prop value se může v čase měnit)
197
+ const vf = moduleDefinition?.valueField || 'id';
198
+ const isSame = catalogSelectedRef.current?.[vf] === initialValueRef.current?.[vf];
199
+ console.log('[MLE] isCancelAfterEnd (catalog):', isSame, {
200
+ new: catalogSelectedRef.current?.[vf],
201
+ old: initialValueRef.current?.[vf]
202
+ });
203
+ return isSame;
158
204
  }
205
+
206
+ // Free-text porovnání proti INITIAL hodnotě
159
207
  const currentText = currentTextRef.current ?? '';
160
- const originalText = typeof value === 'object' ? (value?.text || '') : (value || '');
161
- const result = pendingValueRef.current !== null ? false : currentText === originalText;
162
- console.log('[MLE] isCancelAfterEnd', { pendingValue: pendingValueRef.current, currentText, originalText, result });
163
- return result;
208
+ const originalText = typeof initialValueRef.current === 'object' ? (initialValueRef.current?.text || '') : (initialValueRef.current || '');
209
+ const isSame = currentText === originalText;
210
+ console.log('[MLE] isCancelAfterEnd (free-text):', isSame);
211
+ return isSame;
164
212
  },
165
213
  });
166
214
 
@@ -179,10 +227,13 @@ const ModuleLookupEditor = React.forwardRef(({
179
227
  const vf = moduleDefinition?.valueField || 'id';
180
228
  const item = allItems.find(i => String(i[vf]) === String(selectedId));
181
229
  if (!item) return;
230
+ catalogSelectedRef.current = item;
231
+ pendingValueRef.current = item; // Synchronní příznak pro isCancelAfterEnd
182
232
  setCatalogSelected(item);
183
233
  if (onValueChange) onValueChange(item);
184
234
  setTimeout(() => stopEditing(), 0);
185
235
  }, [allItems, moduleDefinition, onValueChange, stopEditing]);
236
+ handleCatalogSelectChangeRef.current = handleCatalogSelectChange;
186
237
 
187
238
  const catalogCurrentId = React.useMemo(() => {
188
239
  if (allowFreeText || !value) return null;
@@ -204,6 +255,8 @@ const ModuleLookupEditor = React.forwardRef(({
204
255
  pendingValueRef.current = selected;
205
256
  if (onValueChange) onValueChange(selected);
206
257
  } else {
258
+ catalogSelectedRef.current = item;
259
+ pendingValueRef.current = item; // Synchronní příznak pro isCancelAfterEnd
207
260
  setCatalogSelected(item);
208
261
  if (onValueChange) onValueChange(item);
209
262
  }
@@ -324,8 +377,7 @@ const ModuleLookupEditor = React.forwardRef(({
324
377
  // rc-select volá stopPropagation() na Enter v bubble fázi → náš React onKeyDown
325
378
  // na wrapperu ho nikdy nedostane. Capture-phase native listener se spustí jako první.
326
379
  useEffect(() => {
327
- if (!allowFreeText) return;
328
- const el = freeTextWrapperRef.current;
380
+ const el = editorWrapperRef.current;
329
381
  if (!el) return;
330
382
  const input = el.querySelector('input');
331
383
  if (!input) return;
@@ -349,17 +401,41 @@ const ModuleLookupEditor = React.forwardRef(({
349
401
  if (activeOptionEl) {
350
402
  e.stopImmediatePropagation();
351
403
  e.preventDefault();
352
- const labelText = activeOptionEl.querySelector('.ant-select-item-option-content')?.textContent?.trim();
353
- if (labelText) {
354
- const matched = suggestionsRef.current.find((s) => s.label === labelText);
355
- if (matched) {
356
- handleAutoCompleteSelectRef.current?.(matched.value, matched);
357
- return;
404
+ const contentEl = activeOptionEl.querySelector('.ant-select-item-option-content');
405
+ if (contentEl) {
406
+ const mleValue = contentEl.querySelector('[data-mle-value]')?.getAttribute('data-mle-value');
407
+ if (mleValue) {
408
+ const matched = suggestionsRef.current.find((s) => s.value === mleValue);
409
+ if (matched) {
410
+ if (allowFreeText) {
411
+ handleAutoCompleteSelectRef.current?.(matched.value, matched);
412
+ } else {
413
+ handleCatalogSelectChangeRef.current?.(matched.value);
414
+ }
415
+ return;
416
+ }
417
+ }
418
+
419
+ const spanText = contentEl.querySelector('span')?.textContent?.trim();
420
+ const fullText = contentEl.textContent?.trim();
421
+ const labelText = spanText || fullText;
422
+ if (labelText) {
423
+ const matched = suggestionsRef.current.find((s) => s.displayText === labelText || String(s.label) === labelText);
424
+ if (matched) {
425
+ if (allowFreeText) {
426
+ handleAutoCompleteSelectRef.current?.(matched.value, matched);
427
+ } else {
428
+ handleCatalogSelectChangeRef.current?.(matched.value);
429
+ }
430
+ return;
431
+ }
358
432
  }
359
433
  }
360
434
  return;
361
435
  }
362
436
 
437
+ if (!allowFreeText) return;
438
+
363
439
  const text = inputTextRef.current || '';
364
440
  // Prázdný input bez předchozí hodnoty → nechat AG Grid zpracovat (cancel)
365
441
  if (!text.trim() && pendingValueRef.current === null && !initialText) return;
@@ -408,30 +484,42 @@ const ModuleLookupEditor = React.forwardRef(({
408
484
  // ─── Render: catalog-only mód ─────────────────────────────────────────────────
409
485
  if (!allowFreeText) {
410
486
  const vf = moduleDefinition?.valueField || 'id';
411
- const selectOptions = allItems.map(item => ({
412
- value: item[vf],
413
- label: moduleDefinition?.getDisplayValue?.(item) || item.code || item.name || String(item[vf]),
414
- }));
487
+ const selectOptions = allItems.map(item => {
488
+ const displayText = moduleDefinition?.getDisplayValue?.(item) || item.code || item.name || String(item[vf] || '');
489
+ const originalLabel = moduleDefinition?.renderOption ? moduleDefinition.renderOption(item, displayText) : displayText;
490
+ return {
491
+ value: item[vf],
492
+ label: (
493
+ <div data-mle-value={String(item[vf])} style={{ display: 'contents' }}>
494
+ {originalLabel}
495
+ </div>
496
+ ),
497
+ displayText,
498
+ };
499
+ });
415
500
 
416
501
  return (
417
- <div style={{ width: '100%', minWidth: 250, padding: 4, borderRadius: 0, background: '#fff', boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }}>
418
- <div style={{ display: 'flex', gap: 4 }}>
502
+ <div style={{ width: '100%', minWidth: 250, height: rowHeight, display: 'flex', alignItems: 'stretch' }} ref={editorWrapperRef}>
503
+ <div style={{ display: 'flex', flex: 1, height: '100%' }}>
419
504
  <Select
420
- style={{ flex: 1 }}
505
+ style={{ flex: 1, height: '100%' }}
421
506
  value={catalogCurrentId}
422
507
  options={selectOptions}
508
+ optionLabelProp="displayText"
423
509
  loading={allItemsLoading}
424
510
  showSearch
425
511
  filterOption={(input, option) =>
426
- String(option?.label || '').toLowerCase().includes(input.toLowerCase())
512
+ String(option?.displayText || option?.label || '').toLowerCase().includes(input.toLowerCase())
427
513
  }
428
514
  onChange={handleCatalogSelectChange}
515
+ onSearch={handleInputChange}
429
516
  getPopupContainer={() => document.body}
430
517
  dropdownStyle={{ zIndex: 9999 }}
431
518
  placeholder={moduleDefinition?.placeholder || 'Vyberte...'}
519
+ defaultOpen={autoOpen}
432
520
  autoFocus
433
521
  />
434
- <Button icon={<SearchOutlined />} onMouseDown={e => e.preventDefault()} onClick={() => setCatalogModalOpen(true)} type="primary" />
522
+ <Button icon={<SearchOutlined />} onMouseDown={e => e.preventDefault()} onClick={() => setCatalogModalOpen(true)} type="primary" style={{ height: '100%', borderRadius: 0 }} />
435
523
  </div>
436
524
  {catalogModalContent}
437
525
  </div>
@@ -440,8 +528,8 @@ const ModuleLookupEditor = React.forwardRef(({
440
528
 
441
529
  // ─── Render: free-text mód ────────────────────────────────────────────────────
442
530
  return (
443
- <div style={{ width: '100%', minWidth: 250, padding: 4, borderRadius: 0, background: '#fff', boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }}>
444
- <div ref={freeTextWrapperRef} style={{ display: 'flex', gap: 4 }}>
531
+ <div style={{ width: '100%', minWidth: 250, height: rowHeight, display: 'flex', alignItems: 'stretch' }}>
532
+ <div ref={editorWrapperRef} style={{ display: 'flex', flex: 1, height: '100%' }}>
445
533
  <AutoComplete
446
534
  style={{ flex: 1 }}
447
535
  value={inputText}
@@ -469,6 +557,7 @@ const ModuleLookupEditor = React.forwardRef(({
469
557
  setCatalogModalOpen(true);
470
558
  }}
471
559
  type="primary"
560
+ style={{ height: '100%', borderRadius: 0 }}
472
561
  />
473
562
  </div>
474
563
 
@@ -0,0 +1,124 @@
1
+ /* eslint-disable */
2
+ import React, { useState, useRef, useCallback, useEffect } from 'react';
3
+ import { useGridCellEditor } from 'ag-grid-react';
4
+ import DraggableModal from '@bit.rhplus/draggable-modal';
5
+ import { useSharedGridItems } from '@bit.rhplus/shared-grid-form';
6
+
7
+ /**
8
+ * Vnitřní komponenta editoru — přístup k Jotai SharedGridForm kontextu.
9
+ */
10
+ const RouteEditorInner = ({
11
+ instanceId,
12
+ initialData,
13
+ gridComponent: GridComponent,
14
+ onCancel,
15
+ onSave,
16
+ }) => {
17
+ const { effectiveItems, setItems } = useSharedGridItems(instanceId);
18
+ const distanceKmRef = useRef(null);
19
+
20
+ // Inicializace dat při mountu
21
+ useEffect(() => {
22
+ if (Array.isArray(initialData)) {
23
+ setItems(initialData);
24
+ }
25
+ }, []); // Pouze při mountu
26
+
27
+ const handleRouteCalculated = useCallback((info) => {
28
+ if (info?.distance != null && info.distance > 0) {
29
+ distanceKmRef.current = Math.round(info.distance / 1000);
30
+ }
31
+ }, []);
32
+
33
+ const handleOk = useCallback(() => {
34
+ onSave(effectiveItems, distanceKmRef.current);
35
+ }, [effectiveItems, onSave]);
36
+
37
+ return (
38
+ <DraggableModal
39
+ title="Editace trasy"
40
+ open={true}
41
+ onOk={handleOk}
42
+ onCancel={onCancel}
43
+ width={1100}
44
+ height="80vh"
45
+ resizable={true}
46
+ getContainer={() => document.body}
47
+ destroyOnClose
48
+ styles={{ body: { height: '100%', overflow: 'hidden', padding: 0 } }}
49
+ centered
50
+ maskClosable={false}
51
+ >
52
+ <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
53
+ <GridComponent instanceId={instanceId} showAddButton={true} onRouteCalculated={handleRouteCalculated} />
54
+ </div>
55
+ </DraggableModal>
56
+ );
57
+ };
58
+
59
+ /**
60
+ * RouteCellEditor — AG Grid 35 Cell Editor pro správu tras.
61
+ *
62
+ * Používá useGridCellEditor hook (povinné v AG Grid 31+).
63
+ * Props jsou předány přímo z AG Grid (value, onValueChange, stopEditing + cellEditorParams).
64
+ */
65
+ /**
66
+ * RouteCellEditor — AG Grid 35 Cell Editor pro správu tras.
67
+ *
68
+ * Používá useGridCellEditor hook (povinné v AG Grid 31+).
69
+ * Props jsou předány přímo z AG Grid (value, onValueChange, stopEditing + cellEditorParams).
70
+ *
71
+ * @param {Function} transformValue - volitelná funkce z cellEditorParams pro transformaci
72
+ * effectiveItems před uložením (např. pro převod temp ID na null a type string na transportTypeId).
73
+ * Pokud není zadána, data se uloží bez transformace.
74
+ */
75
+ const RouteCellEditor = ({ value, onValueChange, stopEditing, data, gridComponent: GridComponent, transformValue }) => {
76
+ const [visible, setVisible] = useState(true);
77
+
78
+ // Ref pro synchronní přístup z getValue() — AG Grid 35 volá getValue() při stopEditing()
79
+ const valueRef = useRef(value);
80
+
81
+ // AG Grid 35: useGridCellEditor je povinné pro správné getValue() při stopEditing()
82
+ useGridCellEditor({
83
+ getValue: () => valueRef.current,
84
+ });
85
+
86
+ // Stabilní instanceId pro celý editační proces
87
+ const [instanceId] = useState(() => `route-editor-${data?.id || Date.now()}`);
88
+
89
+ const handleSave = useCallback((items, distanceKm) => {
90
+ // Transformace dat před uložením — připraví formát kompatibilní s backendem
91
+ const prepared = transformValue ? transformValue(items) : items;
92
+ const payload = distanceKm != null ? { routes: prepared, destination: distanceKm } : prepared;
93
+ valueRef.current = payload;
94
+ if (onValueChange) onValueChange(payload);
95
+ setVisible(false);
96
+ stopEditing();
97
+ }, [transformValue, onValueChange, stopEditing]);
98
+
99
+ const handleCancel = useCallback(() => {
100
+ setVisible(false);
101
+ setTimeout(() => stopEditing(true), 0); // true = cancel, necommitovat
102
+ }, [stopEditing]);
103
+
104
+ if (!visible) return null;
105
+
106
+ if (!GridComponent) {
107
+ console.error('RouteCellEditor: gridComponent missing in cellEditorParams');
108
+ return null;
109
+ }
110
+
111
+ return (
112
+ <RouteEditorInner
113
+ instanceId={instanceId}
114
+ initialData={value}
115
+ gridComponent={GridComponent}
116
+ onSave={handleSave}
117
+ onCancel={handleCancel}
118
+ />
119
+ );
120
+ };
121
+
122
+ RouteCellEditor.displayName = 'RouteCellEditor';
123
+
124
+ export default RouteCellEditor;
package/Editors/index.jsx CHANGED
@@ -1,3 +1,4 @@
1
1
  export {default as DatePickerEditor} from './DatePickerEditor';
2
2
  export {default as ModuleLookupEditor} from './ModuleLookupEditor';
3
3
  export {default as AddressLookupEditor} from './AddressLookupEditor';
4
+ export {default as RouteCellEditor} from './RouteCellEditor';
@@ -1,7 +1,7 @@
1
-
2
- /* eslint-disable no-param-reassign */
1
+ /* eslint-disable */
3
2
  export const RhPlusOnCellValueChanged = (event, options) =>
4
3
  {
4
+ console.log("onCellValueChanged");
5
5
  event.data._rh_plus_ag_grid_new_item = false;
6
6
  event.data._rh_plus_ag_grid_row_changed = true;
7
7
 
@@ -0,0 +1,76 @@
1
+ /* eslint-disable */
2
+ import React from 'react';
3
+ import { MapPin, Flag, ChevronRight } from 'lucide-react';
4
+
5
+ /**
6
+ * RouteCellRenderer — zobrazuje trasu se šipkami a ikonami pro nakládku/vykládku.
7
+ * Bucovice => slavkov u Brna => brno => praha
8
+ */
9
+ const RouteCellRenderer = (params) => {
10
+ const routes = params.value || params.data?.routes || [];
11
+ if (!Array.isArray(routes) || routes.length === 0) {
12
+ return <span style={{ color: '#bfbfbf', fontStyle: 'italic' }}>Nezadána</span>;
13
+ }
14
+
15
+
16
+ return (
17
+ <div style={{
18
+ display: 'flex',
19
+ alignItems: 'center',
20
+ gap: '4px',
21
+ overflow: 'hidden',
22
+ height: '100%',
23
+ padding: '0 4px'
24
+ }}>
25
+ {routes.map((route, index) => {
26
+ const isLast = index === routes.length - 1;
27
+ const isFirst = index === 0;
28
+
29
+ // Typ: 1 = loading (nakládka), 2 = unloading (vykládka)
30
+ const transportTypeId = route.transportTypeId || (route.type === 'loading' ? 1 : 2);
31
+
32
+ const isLoading = transportTypeId === 1;
33
+ const color = isLoading ? '#1890ff' : '#52c41a';
34
+ const Icon = isLoading ? MapPin : Flag;
35
+
36
+ const name = route.project?.name || route.name || route.address || '???';
37
+
38
+ return (
39
+ <React.Fragment key={route.id || index}>
40
+ <div style={{
41
+ display: 'flex',
42
+ alignItems: 'center',
43
+ gap: '2px',
44
+ flexShrink: 0,
45
+ maxWidth: '120px'
46
+ }}>
47
+ <Icon
48
+ size={14}
49
+ color={color}
50
+ fill={isFirst || isLast ? `${color}33` : 'none'}
51
+ style={{ flexShrink: 0 }}
52
+ />
53
+ <span style={{
54
+ fontSize: '12px',
55
+ fontWeight: isFirst || isLast ? '500' : '400',
56
+ overflow: 'hidden',
57
+ textOverflow: 'ellipsis',
58
+ whiteSpace: 'nowrap',
59
+ color: '#262626'
60
+ }}>
61
+ {name}
62
+ </span>
63
+ </div>
64
+ {!isLast && (
65
+ <ChevronRight size={12} color="#bfbfbf" style={{ flexShrink: 0 }} />
66
+ )}
67
+ </React.Fragment>
68
+ );
69
+ })}
70
+ </div>
71
+ );
72
+ };
73
+
74
+ RouteCellRenderer.displayName = 'RouteCellRenderer';
75
+
76
+ export default RouteCellRenderer;
@@ -2,16 +2,37 @@
2
2
  import React from 'react';
3
3
  import * as LucideIcons from 'lucide-react';
4
4
 
5
- export default ({value, displayField = 'name', imageField = null}) => {
5
+ export default (params) => {
6
+ const { value, data, displayField = 'name', imageField = null, icon, iconColor } = params;
6
7
  if (value == null) return null;
8
+
7
9
  const displayText = typeof value === 'object'
8
10
  ? (value[displayField] ?? value.name ?? '')
9
11
  : String(value);
10
- const icon = value?.icon;
12
+
11
13
  const imageUrl = imageField && typeof value === 'object' ? value[imageField] : null;
12
14
 
13
- // Přímý přístup ke komponentě ikony
14
- const Icon = icon && LucideIcons[icon] ? LucideIcons[icon] : null;
15
+ // Lucide ikona z hodnoty (hodnota má pole icon s názvem Lucide komponenty)
16
+ const lucideIconName = value?.icon;
17
+ const LucideIcon = !imageUrl && !icon && lucideIconName && LucideIcons[lucideIconName]
18
+ ? LucideIcons[lucideIconName]
19
+ : null;
20
+
21
+ // Custom ikona předaná přes cellRendererParams.icon — může být element, funkce nebo komponenta
22
+ let iconElement = null;
23
+ if (!imageUrl && icon) {
24
+ const resolvedColor = typeof iconColor === 'function' ? iconColor({ data, value }) : iconColor;
25
+ const colorStyle = resolvedColor ? { color: resolvedColor } : {};
26
+
27
+ if (React.isValidElement(icon)) {
28
+ iconElement = React.cloneElement(icon, { style: { ...colorStyle, ...icon.props.style } });
29
+ } else if (typeof icon === 'function') {
30
+ const rendered = icon({ data, value });
31
+ iconElement = resolvedColor && React.isValidElement(rendered)
32
+ ? React.cloneElement(rendered, { style: { ...colorStyle, ...rendered.props?.style } })
33
+ : rendered;
34
+ }
35
+ }
15
36
 
16
37
  return (
17
38
  <div style={{
@@ -29,14 +50,19 @@ export default ({value, displayField = 'name', imageField = null}) => {
29
50
  onError={(e) => { e.currentTarget.style.display = 'none'; }}
30
51
  />
31
52
  )}
32
- {!imageUrl && Icon && (
53
+ {iconElement && (
54
+ <span style={{ flexShrink: 0, display: 'flex', alignItems: 'center' }}>
55
+ {iconElement}
56
+ </span>
57
+ )}
58
+ {!iconElement && LucideIcon && (
33
59
  <span style={{ flexShrink: 0, display: 'flex', alignItems: 'center' }}>
34
- <Icon size={16} />
60
+ <LucideIcon size={16} />
35
61
  </span>
36
62
  )}
37
63
  <span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
38
64
  {displayText}
39
65
  </span>
40
66
  </div>
41
- )
67
+ );
42
68
  }
@@ -2,3 +2,4 @@ export {default as SelectCellRenderer} from './SelectCellRenderer';
2
2
  export {default as ModuleLookupRenderer} from './ModuleLookupRenderer';
3
3
  export {default as AddressLookupRenderer} from './AddressLookupRenderer';
4
4
  export {default as NumberedListRenderer} from './NumberedListRenderer';
5
+ export {default as RouteCellRenderer} from './RouteCellRenderer';
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable */
2
2
  // ✅ OPTIMALIZACE: Enumerable odstraněn - nahrazen native JS metodami
3
3
  import moment from 'moment';
4
- import { currencySymbols } from '@bit.rhplus/ui.grid/';
4
+ import { currencySymbols } from '@bit.rhplus/ui.grid';
5
5
  const getColumnOrderDirection = (range) => (range.columns[0].colId === range.startColumn.colId) ? 1 : -1;
6
6
  const getColumnOrderIndex = (range, index) => {
7
7
  if (getColumnOrderDirection(range) === 1) {