From 1dbc23576fb0abec12d3361b73cdfcde58fc9d89 Mon Sep 17 00:00:00 2001 From: Luca Foscili <45429703+lucafoscili@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:06:16 +0200 Subject: [PATCH 1/3] Add structured dependencies support to kup-planner Introduces the KupPlannerDependency type and adds a new 'dependencies' prop to kup-planner and related components, enabling structured rendering of task and phase dependencies as arrows. Updates example 7 to demonstrate multiple and stacked dependencies, and propagates the dependencies prop through the planner, gantt, and grid renderer layers. Also updates type declarations and documentation accordingly. --- packages/ketchup/src/assets/index.js | 4 + .../ketchup/src/assets/planner-example-7.js | 1096 +++++++++++++++++ packages/ketchup/src/components.d.ts | 16 +- .../kup-planner/kup-planner-declarations.ts | 29 +- .../components/kup-planner/kup-planner.tsx | 43 +- .../kup-planner/utils/kup-gantt/kup-gantt.tsx | 60 +- .../kup-planner/utils/kup-gantt/readme.md | 1 + .../kup-grid-renderer/kup-grid-renderer.tsx | 172 ++- .../utils/kup-grid-renderer/readme.md | 71 +- .../utils/kup-planner-renderer.tsx | 4 + .../utils/kup-task-gantt/kup-task-gantt.tsx | 7 +- packages/ketchup/src/planner-example-7.html | 46 + packages/ketchup/stencil.config.ts | 1 + 13 files changed, 1479 insertions(+), 71 deletions(-) create mode 100644 packages/ketchup/src/assets/planner-example-7.js create mode 100644 packages/ketchup/src/planner-example-7.html diff --git a/packages/ketchup/src/assets/index.js b/packages/ketchup/src/assets/index.js index a241422a51..570270c10a 100644 --- a/packages/ketchup/src/assets/index.js +++ b/packages/ketchup/src/assets/index.js @@ -214,6 +214,10 @@ components.data = [ value: 'Planner example 6', id: 'planner-example-6.html', }, + { + value: 'Planner example 7', + id: 'planner-example-7.html', + }, { value: 'Progress Bar', id: 'progress-bar.html', diff --git a/packages/ketchup/src/assets/planner-example-7.js b/packages/ketchup/src/assets/planner-example-7.js new file mode 100644 index 0000000000..b99371c341 --- /dev/null +++ b/packages/ketchup/src/assets/planner-example-7.js @@ -0,0 +1,1096 @@ +const comp = document.getElementById('planner'); + +comp.addEventListener('kup-planner-click', onclick); +comp.addEventListener('kup-planner-didunload', (e) => { + console.log('Planner removed', e); +}); +document.addEventListener('kup-button-click', () => { + console.log('Removing planner'); + comp.remove(); +}); + +document.addEventListener('kup-planner-datechange', (e) => { + console.log(e); +}); + +document.addEventListener('kup-planner-phasedrop', (e) => { + console.log(e); +}); + +// Base props copied from example-6, trimmed for brevity. The important part is the tasks ids +// which we will reference in the dependencies below. +const props = { + data: { + columns: [ + { + isEditable: false, + isKey: false, + name: 'R§COMM', + obj: { + k: '', + p: '', + t: 'CM', + }, + title: 'Commessa', + tooltip: false, + }, + { + isEditable: false, + isKey: false, + name: 'DATPRE', + obj: { + k: '', + p: '*YYMD', + t: 'D8', + }, + title: 'Data Cons.\nAttualizz.', + tooltip: false, + }, + { + isEditable: false, + isKey: false, + name: 'DATORD', + obj: { + k: '', + p: '*YYMD', + t: 'D8', + }, + title: 'Data Cons.\nP.Ordine', + tooltip: false, + }, + { + isEditable: false, + isKey: false, + name: 'R§CDCL', + objs: [ + { + k: '', + p: 'CLP', + t: 'CN', + }, + ], + title: 'Ente', + tooltip: false, + }, + { + isEditable: false, + isKey: false, + name: 'DATINZ', + obj: { + k: '', + p: '*YYMD', + t: 'D8', + }, + title: 'Data inizio\nAttualizz.', + tooltip: false, + }, + { + isEditable: false, + isKey: false, + name: 'INZORD', + obj: { + k: '', + p: '*YYMD', + t: 'D8', + }, + title: 'Data inizio\nP.Ordine', + tooltip: false, + }, + { + isEditable: false, + isKey: false, + name: 'INPRHHMMSS', + obj: { + k: '', + p: '2', + t: 'I1', + }, + title: 'Init preview hour (HH:mm:ss)', + tooltip: true, + }, + { + isEditable: false, + isKey: false, + name: 'INITHHMMSS', + obj: { + k: '', + p: '2', + t: 'I1', + }, + title: 'Init hour (HH:mm:ss)', + tooltip: true, + }, + { + isEditable: false, + isKey: false, + name: 'ENPRHHMMSS', + obj: { + k: '', + p: '2', + t: 'I1', + }, + title: 'End preview hour (HH:mm:ss)', + tooltip: true, + }, + { + isEditable: false, + isKey: false, + name: 'ENDHHMMSS', + obj: { + k: '', + p: '2', + t: 'I1', + }, + title: 'End hour (HH:mm:ss)', + tooltip: true, + }, + { + isEditable: false, + isKey: false, + name: 'INPRHHMM', + obj: { + k: '', + p: '3', + t: 'I1', + }, + title: 'Init previewed hour (HH:mm)', + tooltip: true, + }, + { + isEditable: false, + isKey: false, + name: 'INITHHMM', + obj: { + k: '', + p: '3', + t: 'I1', + }, + title: 'Init hour (HH:mm)', + tooltip: true, + }, + { + isEditable: false, + isKey: false, + name: 'ENPRHHMM', + obj: { + k: '', + p: '3', + t: 'I1', + }, + title: 'End preview hour (HH:mm)', + tooltip: true, + }, + { + isEditable: false, + isKey: false, + name: 'ENDHHMM', + obj: { + k: '', + p: '3', + t: 'I1', + }, + title: 'End hour (HH:mm)', + tooltip: true, + }, + ], + rows: [ + { + cells: { + 'R§CDCL': { + data: { + size: 15, + helperEnabled: false, + hiddenCounter: true, + maxLength: 15, + }, + isEditable: false, + obj: { + k: 'BENARM', + p: 'CLP', + t: 'CN', + }, + value: 'BENARM', + displayedValue: 'BENARM', + }, + DATPRE: { + data: { + size: 8, + helperEnabled: false, + hiddenCounter: true, + maxLength: 8, + }, + isEditable: false, + obj: { + k: '20240228', + p: '*YYMD', + t: 'D8', + }, + value: '2024-02-28', + }, + INZORD: { + data: { + size: 8, + helperEnabled: false, + hiddenCounter: true, + maxLength: 8, + }, + isEditable: false, + obj: { + k: '20230512', + p: '*YYMD', + t: 'D8', + }, + value: '2023-05-12', + }, + DATORD: { + data: { + size: 8, + helperEnabled: false, + hiddenCounter: true, + maxLength: 8, + }, + isEditable: false, + obj: { + k: '20240228', + p: '*YYMD', + t: 'D8', + }, + value: '2024-02-28', + }, + DATINZ: { + data: { + size: 8, + helperEnabled: false, + hiddenCounter: true, + maxLength: 8, + }, + isEditable: false, + obj: { + k: '20230512', + p: '*YYMD', + t: 'D8', + }, + value: '2023-05-12', + }, + 'R§COMM': { + data: { + size: 10, + helperEnabled: false, + hiddenCounter: true, + maxLength: 10, + }, + isEditable: false, + obj: { + k: 'G418', + p: '', + t: 'CM', + }, + value: 'G418', + displayedValue: 'G418', + }, + INPRHHMMSS: { + isEditable: false, + obj: { + k: '070030', + p: '2', + t: 'I1', + }, + value: '07:00:30', + }, + INITHHMMSS: { + isEditable: false, + obj: { + k: '080000', + p: '2', + t: 'I1', + }, + value: '08:00:00', + }, + ENPRHHMMSS: { + isEditable: false, + obj: { + k: '163050', + p: '2', + t: 'I1', + }, + value: '16:30:50', + }, + ENDHHMMSS: { + isEditable: false, + obj: { + k: '180000', + p: '2', + t: 'I1', + }, + value: '18:00:00', + }, + INPRHHMM: { + isEditable: false, + obj: { + k: '0700', + p: '3', + t: 'I1', + }, + value: '07:00', + }, + INITHHMM: { + isEditable: false, + obj: { + k: '0800', + p: '3', + t: 'I1', + }, + value: '08:00', + }, + ENPRHHMM: { + isEditable: false, + obj: { + k: '1630', + p: '3', + t: 'I1', + }, + value: '16:30', + }, + ENDHHMM: { + isEditable: false, + obj: { + k: '1800', + p: '3', + t: 'I1', + }, + value: '18:00', + }, + }, + cssClass: 'clickable', + id: '1', + object: '', + readOnly: true, + }, + ], + }, + taskHeight: 400, + taskIdCol: 'R§COMM', + taskNameCol: 'R§COMM', + taskDates: ['DATINZ', 'DATPRE'], + taskPrevDates: ['INZORD', 'DATORD'], + taskColumns: ['R§COMM', 'R§CDCL', 'DATINZ', 'DATPRE'], + scrollableTaskList: true, + // Ensure phase-related props are defined so addPhases can parse the provided phases dataset + phaseIdCol: 'CODFAS', + phaseNameCol: 'DESFAS', + phaseDates: ['DATINI', 'DATFIN'], + phasePrevDates: ['DATINZ', 'DATFPO'], + phaseColumns: ['CODFAS', 'DESFAS', 'DATINI', 'DATFIN'], + phaseHours: ['INITHHMMSS', 'ENDHHMMSS'], + phasePrevHours: ['INPRHHMMSS', 'ENPRHHMMSS'], + phaseColorCol: 'COLFAS', + // Add structured dependencies. To target phases, use the planner's phase id + // format: _ (the component creates phase ids as taskId + '_' + CODFAS) + // Use runtime planner task ids here (taskId and taskId_phaseId) so + // they match the ids created by addPhases (e.g. G418_P410). + dependencies: [ + // Two stacked arrows from task 'G418' to its phase 'P410' + { id: 'd1', sourceId: 'G418', targetId: 'G418_P410', type: 'FS' }, + { id: 'd2', sourceId: 'G418', targetId: 'G418_P410', type: 'FS' }, + // Arrow from task 'G418' to its phase 'P750' + { id: 'd3', sourceId: 'G418', targetId: 'G418_P750', type: 'FS' }, + ], +}; + +if (props) { + for (const key in props) { + comp[key] = props[key]; + } +} + +function onclick(event) { + console.log('planner.js onclick', event.detail); + const taskAction = event.detail && event.detail.taskAction; + // Be resilient to slightly different action naming and use the clicked task id + if ( + taskAction === 'onTaskOpening' || + (typeof taskAction === 'string' && + taskAction.toLowerCase().includes('opening')) + ) { + const clickedId = + event.detail && event.detail.value && event.detail.value.id; + if (clickedId) { + // Use the actual clicked task id so example works regardless of task id value + comp.addPhases(clickedId, phases); + } else { + console.warn( + 'Could not determine clicked task id, skipping addPhases' + ); + } + } +} + +const phases = { + columns: [ + { + isEditable: false, + isKey: false, + name: 'CODFAS', + obj: { + k: '', + p: '', + t: 'OP', + }, + title: 'Fase', + tooltip: false, + }, + { + isEditable: false, + isKey: false, + name: 'DESFAS', + title: 'Des\nFase', + tooltip: false, + }, + { + isEditable: false, + isKey: false, + name: 'DATINI', + obj: { + k: '', + p: '*YYMD', + t: 'D8', + }, + title: 'Data\nInizio', + tooltip: false, + }, + { + isEditable: false, + isKey: false, + name: 'DATINZ', + obj: { + k: '', + p: '*YYMD', + t: 'D8', + }, + title: 'Data\nInizio P.O', + tooltip: false, + }, + { + isEditable: false, + isKey: false, + name: 'DATFIN', + obj: { + k: '', + p: '*YYMD', + t: 'D8', + }, + title: 'Data\nFine', + tooltip: false, + }, + { + isEditable: false, + isKey: false, + name: 'DATFPO', + obj: { + k: '', + p: '*YYMD', + t: 'D8', + }, + title: 'Data\nFine\nPrev.Ordine', + tooltip: false, + }, + { + isEditable: false, + isKey: false, + name: 'COLFAS', + title: 'Sty\nColore', + tooltip: false, + visible: false, + }, + { + isEditable: false, + isKey: false, + name: 'INPRHHMMSS', + obj: { + k: '', + p: '2', + t: 'I1', + }, + title: 'Init prev hour (HH:mm:ss)', + tooltip: true, + }, + { + isEditable: false, + isKey: false, + name: 'INITHHMMSS', + obj: { + k: '', + p: '2', + t: 'I1', + }, + title: 'Init hour (HH:mm:ss)', + tooltip: true, + }, + { + isEditable: false, + isKey: false, + name: 'ENPRHHMMSS', + obj: { + k: '', + p: '2', + t: 'I1', + }, + title: 'End pre hour (HH:mm:ss)', + tooltip: true, + }, + { + isEditable: false, + isKey: false, + name: 'ENDHHMMSS', + obj: { + k: '', + p: '2', + t: 'I1', + }, + title: 'End hour (HH:mm:ss)', + tooltip: true, + }, + { + isEditable: false, + isKey: false, + name: 'INPRHHMM', + obj: { + k: '', + p: '3', + t: 'I1', + }, + title: 'Init pre hour (HH:mm)', + tooltip: true, + }, + { + isEditable: false, + isKey: false, + name: 'INITHHMM', + obj: { + k: '', + p: '3', + t: 'I1', + }, + title: 'Init hour (HH:mm)', + tooltip: true, + }, + { + isEditable: false, + isKey: false, + name: 'ENPRHHMM', + obj: { + k: '', + p: '3', + t: 'I1', + }, + title: 'End pre hour (HH:mm)', + tooltip: true, + }, + { + isEditable: false, + isKey: false, + name: 'ENDHHMM', + obj: { + k: '', + p: '3', + t: 'I1', + }, + title: 'End hour (HH:mm)', + tooltip: true, + }, + ], + rows: [ + { + cells: { + COLFAS: { + data: { + size: 10, + helperEnabled: false, + hiddenCounter: true, + maxLength: 10, + }, + isEditable: false, + obj: { + k: '#000000', + p: '', + t: '', + }, + value: '#000000', + }, + + DATFIN: { + data: { + size: 8, + helperEnabled: false, + hiddenCounter: true, + maxLength: 8, + }, + isEditable: false, + obj: { + k: '20230905', + p: '*YYMD', + t: 'D8', + }, + value: '2023-09-05', + }, + + DATINI: { + data: { + size: 8, + helperEnabled: false, + hiddenCounter: true, + maxLength: 8, + }, + isEditable: false, + obj: { + k: '20230522', + p: '*YYMD', + t: 'D8', + }, + value: '2023-05-22', + }, + DATFPO: { + data: { + size: 8, + helperEnabled: false, + hiddenCounter: true, + maxLength: 8, + }, + isEditable: false, + obj: { + k: '20230904', + p: '*YYMD', + t: 'D8', + }, + value: '2023-09-04', + }, + + CODFAS: { + cssClass: 'strong-text', + data: { + size: 15, + helperEnabled: false, + hiddenCounter: true, + maxLength: 15, + }, + isEditable: false, + obj: { + k: 'P410 ', + p: '', + t: 'OP', + }, + value: 'P410 ', + }, + + DESFAS: { + data: { + size: 35, + helperEnabled: false, + hiddenCounter: true, + maxLength: 35, + }, + isEditable: false, + obj: { + k: 'MONTAGGIO MECCANICO', + p: '', + t: '', + }, + value: 'MONTAGGIO MECCANICO', + displayedValue: 'MONTAGGIO MECCANICO', + }, + DATINZ: { + data: { + size: 8, + helperEnabled: false, + hiddenCounter: true, + maxLength: 8, + }, + isEditable: false, + obj: { + k: '20230522', + p: '*YYMD', + t: 'D8', + }, + value: '2023-05-22', + }, + INPRHHMMSS: { + isEditable: false, + obj: { + k: '103015', + p: '2', + t: 'I1', + }, + value: '10:30:15', + }, + INITHHMMSS: { + isEditable: false, + obj: { + k: '113000', + p: '2', + t: 'I1', + }, + value: '11:30:00', + }, + ENPRHHMMSS: { + isEditable: false, + obj: { + k: '124515', + p: '2', + t: 'I1', + }, + value: '12:45:15', + }, + ENDHHMMSS: { + isEditable: false, + obj: { + k: '164500', + p: '2', + t: 'I1', + }, + value: '16:45:00', + }, + + INPRHHMM: { + isEditable: false, + obj: { + k: '1030', + p: '3', + t: 'I1', + }, + value: '10:30', + }, + INITHHMM: { + isEditable: false, + obj: { + k: '1130', + p: '3', + t: 'I1', + }, + value: '11:30', + }, + + ENPRHHMM: { + isEditable: false, + obj: { + k: '1545', + p: '3', + t: 'I1', + }, + value: '15:45', + }, + + ENDHHMM: { + isEditable: false, + obj: { + k: '1645', + p: '3', + t: 'I1', + }, + value: '16:45', + }, + }, + id: '1', + object: '', + readOnly: true, + }, + { + cells: { + COLFAS: { + data: { + size: 10, + helperEnabled: false, + hiddenCounter: true, + maxLength: 10, + }, + isEditable: false, + obj: { + k: '#7030A0', + p: '', + t: '', + }, + value: '#7030A0', + }, + + DATFIN: { + data: { + size: 8, + helperEnabled: false, + hiddenCounter: true, + maxLength: 8, + }, + isEditable: false, + obj: { + k: '20240228', + p: '*YYMD', + t: 'D8', + }, + value: '2024-02-28', + }, + + DATINI: { + data: { + size: 8, + helperEnabled: false, + hiddenCounter: true, + maxLength: 8, + }, + isEditable: false, + obj: { + k: '20240131', + p: '*YYMD', + t: 'D8', + }, + value: '2024-01-31', + }, + DATFPO: { + data: { + size: 8, + helperEnabled: false, + hiddenCounter: true, + maxLength: 8, + }, + isEditable: false, + obj: { + k: '20240228', + p: '*YYMD', + t: 'D8', + }, + value: '2024-02-28', + }, + + CODFAS: { + cssClass: 'strong-text', + data: { + size: 15, + helperEnabled: false, + hiddenCounter: true, + maxLength: 15, + }, + isEditable: false, + obj: { + k: 'P750 ', + p: '', + t: 'OP', + }, + value: 'P750 ', + }, + + DESFAS: { + data: { + size: 35, + helperEnabled: false, + hiddenCounter: true, + maxLength: 35, + }, + isEditable: false, + obj: { + k: 'INSTALLAZIONE', + p: '', + t: '', + }, + value: 'INSTALLAZIONE', + displayedValue: 'INSTALLAZIONE', + }, + DATINZ: { + data: { + size: 8, + helperEnabled: false, + hiddenCounter: true, + maxLength: 8, + }, + isEditable: false, + obj: { + k: '20240131', + p: '*YYMD', + t: 'D8', + }, + value: '2024-01-31', + }, + INPRHHMMSS: { + isEditable: false, + obj: { + k: '103005', + p: '2', + t: 'I1', + }, + value: '10:30:05', + }, + INITHHMMSS: { + isEditable: false, + obj: { + k: '073000', + p: '2', + t: 'I1', + }, + value: '07:30:00', + }, + + ENPRHHMMSS: { + isEditable: false, + obj: { + k: '081500', + p: '2', + t: 'I1', + }, + value: '08:15:00', + }, + ENDHHMMSS: { + isEditable: false, + obj: { + k: '094500', + p: '2', + t: 'I1', + }, + value: '09:45:00', + }, + + INPRHHMM: { + isEditable: false, + obj: { + k: '1030', + p: '3', + t: 'I1', + }, + value: '10:30', + }, + + INITHHMM: { + isEditable: false, + obj: { + k: '0730', + p: '3', + t: 'I1', + }, + value: '07:30', + }, + ENPRHHMM: { + isEditable: false, + obj: { + k: '0815', + p: '3', + t: 'I1', + }, + value: '08:15', + }, + ENDHHMM: { + isEditable: false, + obj: { + k: '0945', + p: '3', + t: 'I1', + }, + value: '09:45', + }, + }, + id: '9', + object: '', + readOnly: true, + }, + ], +}; + +// Minimal phases/tasks data are attached by the component in the example 6 files. +// This demo re-uses the default sample rows that are already present in example-6. + +// --- Extra mock tasks & dependencies for stress-testing stacked arrows --- +// To ensure injected rows have the exact shape the component expects we clone +// an existing sample row (props.data.rows[0]) and only change the id and +// the displayed task code (`R§COMM`). This avoids malformed rows that fail +// the component's validation/filtering. +const extraTasks = []; +try { + if ( + props && + props.data && + Array.isArray(props.data.rows) && + props.data.rows[0] + ) { + const sampleRow = props.data.rows[0]; + ['G419', 'G420', 'G421'].forEach((gid, idx) => { + try { + const clone = JSON.parse(JSON.stringify(sampleRow)); + clone.id = (idx + 2).toString(); + if (clone.cells && clone.cells['R§COMM']) { + clone.cells['R§COMM'].value = gid; + clone.cells['R§COMM'].displayedValue = gid; + } + extraTasks.push(clone); + } catch (e) { + // fallback: don't push malformed clones + } + }); + + // Insert the clones after the first sample row so they are visible + if (extraTasks.length) props.data.rows.splice(1, 0, ...extraTasks); + } +} catch (e) { + // ignore in case the props structure differs +} + +// Add more structured dependencies to stress stacked rendering. +// We'll include multiple dependencies targeting the same phase and cross-task links. +const extraDeps = [ + // multiple dependencies from G419 to its (imaginary) phase P100 and P101 + { id: 'gd1', sourceId: 'G419', targetId: 'G419_P100', type: 'FS' }, + { id: 'gd2', sourceId: 'G419', targetId: 'G419_P100', type: 'FS' }, + { id: 'gd3', sourceId: 'G419', targetId: 'G419_P100', type: 'FS' }, + // cross-task dependencies + { id: 'gd4', sourceId: 'G418', targetId: 'G419', type: 'FS' }, + { id: 'gd5', sourceId: 'G420', targetId: 'G421', type: 'FS' }, + // two dependencies from G420 to G418_P750 (mixed targets) + { id: 'gd6', sourceId: 'G420', targetId: 'G418_P750', type: 'FS' }, + { id: 'gd7', sourceId: 'G420', targetId: 'G418_P750', type: 'FS' }, +]; + +try { + if (!props.dependencies) props.dependencies = []; + props.dependencies.push(...extraDeps); +} catch (e) { + // ignore +} + +// Re-assign props to the component so changes apply at runtime when example loads. +// Use deep clones for `data` and `dependencies` so the kup-planner prop watchers +// detect new object references and update internal state. +try { + if (comp && props) { + for (const key in props) { + if (key === 'data' && props.data) { + // deep clone to ensure a new reference + try { + comp.data = JSON.parse(JSON.stringify(props.data)); + continue; + } catch (e) { + // fallback to direct assign if cloning fails + comp.data = props.data; + continue; + } + } + + if (key === 'dependencies' && props.dependencies) { + try { + comp.dependencies = JSON.parse( + JSON.stringify(props.dependencies) + ); + continue; + } catch (e) { + comp.dependencies = props.dependencies; + continue; + } + } + + comp[key] = props[key]; + } + // Trigger a refresh so the component rebuilds its internal items from the + // newly assigned `data` and `dependencies` (some implementations rely on + // prop watchers which fire on reference changes; calling refresh ensures + // the UI syncs immediately). + try { + if (typeof comp.refresh === 'function') comp.refresh(); + } catch (e) { + // ignore + } + } +} catch (e) { + // ignore assignment failures +} diff --git a/packages/ketchup/src/components.d.ts b/packages/ketchup/src/components.d.ts index 858bf453e6..c9f5bcc4ee 100644 --- a/packages/ketchup/src/components.d.ts +++ b/packages/ketchup/src/components.d.ts @@ -30,7 +30,7 @@ import { KupChipChangeEventPayload, KupChipEventPayload, KupChipNode } from "./c import { FChipSize, FChipStyling, FChipType } from "./f-components/f-chip/f-chip-declarations"; import { KupColorPickerEventPayload } from "./components/kup-color-picker/kup-color-picker-declarations"; import { KupComboboxEventPayload, KupComboboxIconClickEventPayload } from "./components/kup-combobox/kup-combobox-declarations"; -import { KupGanttPlannerProps, KupPlannerBarDisplayProps, KupPlannerBarTask, KupPlannerCalendarProps, KupPlannerClickEventPayload, KupPlannerEventOption, KupPlannerEventPayload, KupPlannerGanttEvent, KupPlannerGanttProps, KupPlannerGanttRow, KupPlannerGanttTask, KupPlannerGanttTaskN, KupPlannerItemDetail, KupPlannerPhase, KupPlannerSwitcherProps, KupPlannerTask, KupPlannerTaskGanttContentProps, KupPlannerTaskGanttProps, KupPlannerTaskItemProps, KupPlannerTaskListProps, KupPlannerTaskType, KupPlannerUnloadEventPayload, KupPlannerViewMode, PlannerProps } from "./components/kup-planner/kup-planner-declarations"; +import { KupGanttPlannerProps, KupPlannerBarDisplayProps, KupPlannerBarTask, KupPlannerCalendarProps, KupPlannerClickEventPayload, KupPlannerDependency, KupPlannerEventOption, KupPlannerEventPayload, KupPlannerGanttEvent, KupPlannerGanttProps, KupPlannerGanttRow, KupPlannerGanttTask, KupPlannerGanttTaskN, KupPlannerItemDetail, KupPlannerPhase, KupPlannerSwitcherProps, KupPlannerTask, KupPlannerTaskGanttContentProps, KupPlannerTaskGanttProps, KupPlannerTaskItemProps, KupPlannerTaskListProps, KupPlannerTaskType, KupPlannerUnloadEventPayload, KupPlannerViewMode, PlannerProps } from "./components/kup-planner/kup-planner-declarations"; import { KupDashboardEventPayload, KupDataDashboard } from "./components/kup-dashboard/kup-dashboard-declarations"; import { GenericFilter, KupGlobalFilterMode } from "./utils/filters/filters-declarations"; import { KupDropEventPayload } from "./managers/kup-interact/kup-interact-declarations"; @@ -92,7 +92,7 @@ export { KupChipChangeEventPayload, KupChipEventPayload, KupChipNode } from "./c export { FChipSize, FChipStyling, FChipType } from "./f-components/f-chip/f-chip-declarations"; export { KupColorPickerEventPayload } from "./components/kup-color-picker/kup-color-picker-declarations"; export { KupComboboxEventPayload, KupComboboxIconClickEventPayload } from "./components/kup-combobox/kup-combobox-declarations"; -export { KupGanttPlannerProps, KupPlannerBarDisplayProps, KupPlannerBarTask, KupPlannerCalendarProps, KupPlannerClickEventPayload, KupPlannerEventOption, KupPlannerEventPayload, KupPlannerGanttEvent, KupPlannerGanttProps, KupPlannerGanttRow, KupPlannerGanttTask, KupPlannerGanttTaskN, KupPlannerItemDetail, KupPlannerPhase, KupPlannerSwitcherProps, KupPlannerTask, KupPlannerTaskGanttContentProps, KupPlannerTaskGanttProps, KupPlannerTaskItemProps, KupPlannerTaskListProps, KupPlannerTaskType, KupPlannerUnloadEventPayload, KupPlannerViewMode, PlannerProps } from "./components/kup-planner/kup-planner-declarations"; +export { KupGanttPlannerProps, KupPlannerBarDisplayProps, KupPlannerBarTask, KupPlannerCalendarProps, KupPlannerClickEventPayload, KupPlannerDependency, KupPlannerEventOption, KupPlannerEventPayload, KupPlannerGanttEvent, KupPlannerGanttProps, KupPlannerGanttRow, KupPlannerGanttTask, KupPlannerGanttTaskN, KupPlannerItemDetail, KupPlannerPhase, KupPlannerSwitcherProps, KupPlannerTask, KupPlannerTaskGanttContentProps, KupPlannerTaskGanttProps, KupPlannerTaskItemProps, KupPlannerTaskListProps, KupPlannerTaskType, KupPlannerUnloadEventPayload, KupPlannerViewMode, PlannerProps } from "./components/kup-planner/kup-planner-declarations"; export { KupDashboardEventPayload, KupDataDashboard } from "./components/kup-dashboard/kup-dashboard-declarations"; export { GenericFilter, KupGlobalFilterMode } from "./utils/filters/filters-declarations"; export { KupDropEventPayload } from "./managers/kup-interact/kup-interact-declarations"; @@ -2649,6 +2649,7 @@ export namespace Components { "dateChange": KupPlannerGanttProps['dateChange']; "dateTimeFormatters": KupPlannerGanttProps['dateTimeFormatters']; "delete": KupPlannerGanttProps['delete']; + "dependencies": KupPlannerDependency[]; "displayedEndDate": KupPlannerGanttProps['displayedEndDate']; "displayedStartDate": KupPlannerGanttProps['displayedStartDate']; "doubleClick": KupPlannerGanttProps['doubleClick']; @@ -2864,6 +2865,7 @@ export namespace Components { "dateChange": KupPlannerEventOption['dateChange']; "dates": KupPlannerTaskGanttContentProps['dates']; "delete": KupPlannerEventOption['delete']; + "dependencies": KupPlannerDependency[]; "doubleClick": KupPlannerEventOption['doubleClick']; "eMouseDown": KupPlannerBarDisplayProps['onMouseDown']; "eventStart": KupPlannerTaskItemProps['onEventStart']; @@ -3557,6 +3559,10 @@ export namespace Components { * @default null */ "data": KupDataDataset; + /** + * Structured dependencies to render as arrows + */ + "dependencies": KupPlannerDependency[]; /** * Column containing the detail color, in hex format * @default null @@ -8544,6 +8550,7 @@ declare namespace LocalJSX { "dateChange"?: KupPlannerGanttProps['dateChange']; "dateTimeFormatters"?: KupPlannerGanttProps['dateTimeFormatters']; "delete"?: KupPlannerGanttProps['delete']; + "dependencies"?: KupPlannerDependency[]; "displayedEndDate"?: KupPlannerGanttProps['displayedEndDate']; "displayedStartDate"?: KupPlannerGanttProps['displayedStartDate']; "doubleClick"?: KupPlannerGanttProps['doubleClick']; @@ -8725,6 +8732,7 @@ declare namespace LocalJSX { "dateChange"?: KupPlannerEventOption['dateChange']; "dates"?: KupPlannerTaskGanttContentProps['dates']; "delete"?: KupPlannerEventOption['delete']; + "dependencies"?: KupPlannerDependency[]; "doubleClick"?: KupPlannerEventOption['doubleClick']; "eMouseDown"?: KupPlannerBarDisplayProps['onMouseDown']; "eventStart"?: KupPlannerTaskItemProps['onEventStart']; @@ -9227,6 +9235,10 @@ declare namespace LocalJSX { * @default null */ "data"?: KupDataDataset; + /** + * Structured dependencies to render as arrows + */ + "dependencies"?: KupPlannerDependency[]; /** * Column containing the detail color, in hex format * @default null diff --git a/packages/ketchup/src/components/kup-planner/kup-planner-declarations.ts b/packages/ketchup/src/components/kup-planner/kup-planner-declarations.ts index 7b94a25e11..a11e9fd379 100644 --- a/packages/ketchup/src/components/kup-planner/kup-planner-declarations.ts +++ b/packages/ketchup/src/components/kup-planner/kup-planner-declarations.ts @@ -143,7 +143,7 @@ export const defaultStylingOptions = { barProgressSelectedColor: '#A2A415', barBackgroundColor: '#A2A415', barBackgroundSelectedColor: '#A2A415', - barDropZoneColor: '#4d9f0240' + barDropZoneColor: '#4d9f0240', }; export interface KupPlannerDatesSanitized { @@ -225,6 +225,20 @@ export interface KupPlannerScheduleItem { endHour?: string; } +/** Dependency between tasks */ +export type KupPlannerDependencyType = 'FS' | 'SS' | 'FF' | 'SF'; + +export interface KupPlannerDependency { + /** optional unique id for the dependency */ + id?: string; + /** source task id */ + sourceId: string; + /** target task id */ + targetId: string; + /** dependency type: Finish-Start, Start-Start, Finish-Finish, Start-Finish */ + type?: KupPlannerDependencyType; +} + export interface KupPlannerTask { id: string; type: KupPlannerTaskType; @@ -336,6 +350,8 @@ export interface KupPlannerGanttProps KupPlannerCustomOptions { id: string; tasks: KupPlannerTask[]; + /** Structured dependencies to render as arrows */ + dependencies?: KupPlannerDependency[]; projection?: KupPlannerGanttPhaseProjection; filter: HTMLElement; initialScrollX?: number; @@ -408,7 +424,7 @@ export interface KupPlannerEventOption { originalPhaseData: KupPlannerTask, originalTaskData: KupPlannerTask, finalPhaseData: KupPlannerTask, - destinationData: KupPlannerTask, + destinationData: KupPlannerTask ) => void | boolean | Promise | Promise; } @@ -503,12 +519,13 @@ export interface KupPlannerBarTask extends KupPlannerTask { progressColor: string; progressSelectedColor: string; }; - ySecondary?: number + ySecondary?: number; } export type KupPlannerTaskTypeInternal = KupPlannerTaskType | 'smalltask'; export type KupPlannerTaskGanttContentProps = { + dependencies: KupPlannerDependency[]; tasks: KupPlannerBarTask[]; dates: Date[]; ganttEvent: KupPlannerGanttEvent; @@ -664,7 +681,7 @@ export type KupPlannerBarDisplayProps = { xSecondary?: number; widthSecondary?: number; showSecondaryDates: boolean; - ySecondary?: number + ySecondary?: number; }; export type KupPlannerBarDateHandleProps = { @@ -709,6 +726,8 @@ export interface KupGanttPlannerProps { showSecondaryDates?: boolean; ganttHeight?: number; hideDependencies?: boolean; + /** Structured dependencies to render as arrows */ + dependencies?: KupPlannerDependency[]; title: string; filter: HTMLElement; initialScrollX?: number; @@ -732,6 +751,8 @@ export interface KupGanttPlannerDetailsProps { hideLabel?: boolean; ganttHeight?: number; hideDependencies?: boolean; + /** Structured dependencies to render as arrows */ + dependencies?: KupPlannerDependency[]; title: string; filter: HTMLElement; initialScrollX?: number; diff --git a/packages/ketchup/src/components/kup-planner/kup-planner.tsx b/packages/ketchup/src/components/kup-planner/kup-planner.tsx index 712febbae1..04a2ba6d3b 100644 --- a/packages/ketchup/src/components/kup-planner/kup-planner.tsx +++ b/packages/ketchup/src/components/kup-planner/kup-planner.tsx @@ -31,6 +31,7 @@ import { KupPlannerStoredSettings, KupPlannerUnloadEventPayload, KupPlannerGanttRow, + KupPlannerDependency, PlannerProps, KupPlannerTaskType, defaultStylingOptions, @@ -389,6 +390,10 @@ export class KupPlanner { @Prop() phasePrevDates: string[]; + /** Structured dependencies to render as arrows */ + @Prop() + dependencies: KupPlannerDependency[] = []; + /** * When true, the two gantts are not interactable. * @default false @@ -697,10 +702,13 @@ export class KupPlanner { let iconUrl = this.#getIconUrl(row, this.phaseIconCol); let iconColor = this.#getIconColor(row, this.phaseIconCol); + const _phaseIdRaw = + (row.cells[this.phaseIdCol]?.value ?? '') + ''; let phase: KupPlannerPhase = { taskRow: task.taskRow, phaseRow: row, - id: task.id + '_' + row.cells[this.phaseIdCol].value, + // trim the phase id value to avoid padded/trailing spaces + id: `${task.id}_${_phaseIdRaw.trim()}`, phaseRowId: row.id, taskRowId: task.taskRowId, name: row.cells[this.phaseNameCol].value, @@ -723,6 +731,17 @@ export class KupPlanner { }; return phase; }); + // Diagnostic: log created phase ids for the task + try { + // eslint-disable-next-line no-console + console.log( + 'kup-planner: addPhases created phases for', + taskId, + task.phases ? task.phases.map((p) => p.id) : [] + ); + } catch (e) { + /* ignore */ + } } this.plannerProps.mainGantt.initialScrollX = this.#storedSettings.taskInitialScrollX; @@ -999,6 +1018,15 @@ export class KupPlanner { ), }, }; + // Diagnostic: inspect whether planner-level `dependencies` prop is set + try { + // eslint-disable-next-line no-console + console.log( + 'kup-planner: componentDidLoad - this.dependencies', + this.dependencies + ); + } catch (e) {} + this.plannerProps = { ...this.plannerProps, ...newGantt, @@ -1127,6 +1155,8 @@ export class KupPlanner { onPhaseDrop: ( nativeEvent: KupPlannerGanttTask | KupPlannerPhase ) => this.handleOnPhaseDrop(nativeEvent), + // forward structured dependencies provided at planner level + dependencies: this.dependencies, }, secondaryGantt: details ? { @@ -1150,6 +1180,8 @@ export class KupPlanner { initialScrollX: this.detailInitialScrollX, initialScrollY: this.detailInitialScrollY, readOnly: this.readOnly, + // forward structured dependencies to secondary gantt as well + dependencies: this.dependencies, onScrollY: (y: number) => { window.clearTimeout(detailScrollYTimeout); detailScrollYTimeout = window.setTimeout( @@ -1174,6 +1206,15 @@ export class KupPlanner { }, }; + // Diagnostic: log what was forwarded into plannerProps.mainGantt.dependencies + try { + // eslint-disable-next-line no-console + console.log( + 'kup-planner: componentDidLoad - plannerProps.mainGantt.dependencies', + this.plannerProps?.mainGantt?.dependencies + ); + } catch (e) {} + this.kupReady.emit({ comp: this, id: this.rootElement.id, diff --git a/packages/ketchup/src/components/kup-planner/utils/kup-gantt/kup-gantt.tsx b/packages/ketchup/src/components/kup-planner/utils/kup-gantt/kup-gantt.tsx index 58468d77fa..db2b2169b9 100644 --- a/packages/ketchup/src/components/kup-planner/utils/kup-gantt/kup-gantt.tsx +++ b/packages/ketchup/src/components/kup-planner/utils/kup-gantt/kup-gantt.tsx @@ -1,41 +1,42 @@ import { Component, Element, + forceUpdate, h, + Listen, + Method, Prop, State, Watch, - Listen, - Method, - forceUpdate, } from '@stencil/core'; import { - KupPlannerCurrentDateIndicator, - KupPlannerGanttProps, - KupPlannerTask, - KupPlannerCalendarProps, + GanttSyncScrollEvent, + KupGanttPlannerProps, KupPlannerBarTask, - KupPlannerTaskListProps, - KupPlannerGridProps, + KupPlannerCalendarProps, + KupPlannerCurrentDateIndicator, KupPlannerDateSetup, + KupPlannerDependency, KupPlannerGanttEvent, - GanttSyncScrollEvent, - KupPlannerTaskGanttContentProps, + KupPlannerGanttProps, + KupPlannerGanttRow, KupPlannerGanttTaskN, + KupPlannerGridProps, KupPlannerItemDetail, - KupPlannerGanttRow, - KupGanttPlannerProps, + KupPlannerTask, + KupPlannerTaskGanttContentProps, + KupPlannerTaskListProps, } from '../../kup-planner-declarations'; -import { - ganttDateRangeFromTask, - seedDates, -} from '../kup-planner-renderer-helper'; -import { removeHiddenTasks, sortTasks } from '../helpers/other.helpers'; import { calculateCurrentDateCalculator, calculateProjection, convertToBarTasks, } from '../helpers/bar.helpers'; +import { removeHiddenTasks, sortTasks } from '../helpers/other.helpers'; +import { + ganttDateRangeFromTask, + seedDates, +} from '../kup-planner-renderer-helper'; @Component({ tag: 'kup-gantt', @@ -180,6 +181,9 @@ export class KupGantt { @Prop() hideDependencies: KupPlannerGanttProps['hideDependencies'] = false; + @Prop() + dependencies: KupPlannerDependency[] = []; + @Prop() projection: KupPlannerGanttProps['projection']; @@ -344,8 +348,7 @@ export class KupGantt { color: string; } | undefined; - - + @State() taskListScrollWidth: number; @@ -525,7 +528,7 @@ export class KupGantt { this.showSecondaryDates ); } - + @Watch('viewDate') @Watch('columnWidth') @Watch('dateSetup') @@ -898,11 +901,11 @@ export class KupGantt { handleTaskListScrollX(event: UIEvent) { const currentTarget = event.currentTarget as HTMLDivElement; - this.taskListScrollX = currentTarget.scrollLeft + this.taskListScrollX = currentTarget.scrollLeft; } handlePhaseDragScroll(scrollY: number) { - this.scrollY = scrollY + this.scrollY = scrollY; } setFailedTask(task: KupPlannerBarTask | null) { @@ -959,6 +962,7 @@ export class KupGantt { currentDateIndicator: this.currentDateIndicatorContent, projection: this.projectionContent, readOnly: this.readOnly, + dependencies: this.dependencies, setGanttEvent: this.setGanttEvent.bind(this), setFailedTask: this.setFailedTask.bind(this), setSelectedTask: this.handleSelectedTask.bind(this), @@ -969,7 +973,7 @@ export class KupGantt { barDblClick: this.barDblClick, barContextMenu: this.barContextMenu, delete: this.delete, - phaseDrop: this.phaseDrop + phaseDrop: this.phaseDrop, }; const tableProps: KupPlannerTaskListProps = { @@ -989,7 +993,7 @@ export class KupGantt { setSelectedTask: this.handleSelectedTask.bind(this), expanderClick: this.handleExpanderClick.bind(this), TaskListHeader: this.TaskListHeader, - TaskListTable: this.TaskListTable + TaskListTable: this.TaskListTable, }; return ( @@ -1017,7 +1021,7 @@ export class KupGantt { scrollableTaskList={this.scrollableTaskList} updateTaskListScrollX={this.ignoreScrollEvent} ontaskListScrollWidth={(width) => { - this.taskListScrollWidth = width + this.taskListScrollWidth = width; }} taskListScrollX={this.taskListScrollX} ref={(el) => (this.taskListTrueRef = el)} @@ -1069,7 +1073,9 @@ export class KupGantt { scrollNumber={this.scrollX} rtl={this.rtl} horizontalScroll={this.handleScrollX.bind(this)} - horizontalTaskListScroll={this.handleTaskListScrollX.bind(this)} + horizontalTaskListScroll={this.handleTaskListScrollX.bind( + this + )} listCellWidth={this.listCellWidth} scrollableTaskList={this.scrollableTaskList} taskListScrollWidth={this.taskListScrollWidth} diff --git a/packages/ketchup/src/components/kup-planner/utils/kup-gantt/readme.md b/packages/ketchup/src/components/kup-planner/utils/kup-gantt/readme.md index d14b0aff3d..f2ea6bfc00 100644 --- a/packages/ketchup/src/components/kup-planner/utils/kup-gantt/readme.md +++ b/packages/ketchup/src/components/kup-planner/utils/kup-gantt/readme.md @@ -28,6 +28,7 @@ | `dateChange` | -- | | `(task: KupPlannerTask, children: KupPlannerTask[]) => boolean \| void \| Promise \| Promise` | `undefined` | | `dateTimeFormatters` | -- | | `{ year?: KupPlannerDateTimeFormatter; month?: KupPlannerDateTimeFormatter; monthAndYear?: KupPlannerDateTimeFormatter; week?: KupPlannerDateTimeFormatter; day?: KupPlannerDateTimeFormatter; hour?: KupPlannerDateTimeFormatter; dayAndMonth?: KupPlannerDateTimeFormatter; }` | `undefined` | | `delete` | -- | | `(task: KupPlannerTask) => boolean \| void \| Promise \| Promise` | `undefined` | +| `dependencies` | -- | | `KupPlannerDependency[]` | `[]` | | `displayedEndDate` | -- | | `Date` | `undefined` | | `displayedStartDate` | -- | | `Date` | `undefined` | | `doubleClick` | -- | | `(task: KupPlannerTask) => void` | `undefined` | diff --git a/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/kup-grid-renderer.tsx b/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/kup-grid-renderer.tsx index d727ceb937..3ea51fd8a1 100644 --- a/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/kup-grid-renderer.tsx +++ b/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/kup-grid-renderer.tsx @@ -13,6 +13,7 @@ import { KupPlannerTaskIconProps, defaultStylingOptions, } from '../../kup-planner-declarations'; +import { KupPlannerDependency } from '../../kup-planner-declarations'; import { addToDate } from '../kup-planner-renderer-helper'; import { handleTaskBySVGMouseEvent, @@ -85,6 +86,9 @@ export class KupGridRenderer { @Prop() readOnly: KupPlannerTaskGanttContentProps['readOnly'] = false; + @Prop() + dependencies: KupPlannerDependency[] = []; + @Prop() gridProps: KupPlannerTaskGanttProps['gridProps']; @@ -889,6 +893,169 @@ export class KupGridRenderer { ); } + /** + * Render dependencies passed as structured data. Supports multiple dependencies + * between the same pair by offsetting paths. + */ + renderDependencies() { + if (!this.dependencies || this.dependencies.length === 0) return null; + + // Build a map of task id -> KupPlannerBarTask for quick lookup + const taskById = new Map(); + for (const t of this.tasks) taskById.set(t.id, t); + + // Group dependencies by pair key + const groups = new Map(); + for (const dep of this.dependencies) { + const key = `${dep.sourceId}__${dep.targetId}`; + const arr = groups.get(key) ?? []; + arr.push(dep); + groups.set(key, arr); + } + + const rendered: any[] = []; + const OFFSET_STEP = 8; // px + + for (const [key, deps] of groups.entries()) { + const [sourceId, targetId] = key.split('__'); + // tolerate different id formats: exact, trimmed, and taskId_phaseId (with padded phase ids) + let sourceTask = + taskById.get(sourceId) || + this.tasks.find( + (t) => t.id && t.id.trim() === (sourceId + '').trim() + ); + + // try exact match first + let targetTask = + taskById.get(targetId) || + this.tasks.find( + (t) => t.id && t.id.trim() === (targetId + '').trim() + ); + + // if not found, try combined formats like _ (with possible padding) + if (!targetTask && sourceTask) { + const candidate1 = `${sourceTask.id}_${targetId}`; + const candidate2 = `${sourceTask.id}_${(targetId + '').trim()}`; + targetTask = + taskById.get(candidate1) || + taskById.get(candidate2) || + this.tasks.find( + (t) => + t.id && + (t.id === candidate1 || + t.id === candidate2 || + t.id.trim() === candidate2.trim()) + ); + } + + // as a last resort try matching by trimming both sides against all tasks + if (!sourceTask || !targetTask) { + const trimmedSource = (sourceId + '').trim(); + const trimmedTarget = (targetId + '').trim(); + if (!sourceTask) { + sourceTask = this.tasks.find( + (t) => t.id && t.id.trim() === trimmedSource + ); + } + if (!targetTask) { + targetTask = this.tasks.find( + (t) => t.id && t.id.trim() === trimmedTarget + ); + } + + // Extra fallback: some dependency definitions use the original row id + // (taskRowId) or row-based ids like '1_P410'. Try to resolve those to + // the runtime task objects using taskRowId / phaseRowId mappings. + try { + // If source is still not found, try to match by taskRowId or taskRow.id + if (!sourceTask) { + sourceTask = this.tasks.find( + (t) => + (t as any).taskRowId == sourceId || + (t as any).taskRow?.id == sourceId || + (t as any).phaseRowId == sourceId + ); + } + + // If target is not found, handle cases like '1_P410' where the left + // part is the taskRowId and the right part is the phase code. We'll + // try to find a phase whose taskRowId matches the left part and + // whose id ends with the phase suffix. + if (!targetTask) { + const parts = trimmedTarget.split('_'); + if (parts.length > 1) { + const left = parts[0]; + const right = parts.slice(1).join('_'); + targetTask = this.tasks.find( + (t) => + ((t as any).taskRowId == left && + t.id && + t.id.endsWith('_' + right)) || + ((t as any).taskRow?.id == left && + t.id && + t.id.endsWith('_' + right)) + ); + } + + // also try matching target by row id directly + if (!targetTask) { + targetTask = this.tasks.find( + (t) => + (t as any).taskRowId == targetId || + (t as any).taskRow?.id == targetId || + (t as any).phaseRowId == targetId + ); + } + } + } catch (e) { + // ignore matching errors + } + } + + if (!sourceTask || !targetTask) { + continue; + } + + const total = deps.length; + deps.forEach((dep, idx) => { + // compute offset: center the stack around 0 + const offset = (idx - (total - 1) / 2) * OFFSET_STEP; + + // we will re-use drownPathAndTriangle but need temporary synthetic tasks shifted by offset + const shiftedFrom = { ...sourceTask } as KupPlannerBarTask; + const shiftedTo = { ...targetTask } as KupPlannerBarTask; + + // shift vertically + shiftedFrom.y = sourceTask.y + offset; + shiftedTo.y = targetTask.y + offset; + + const [path, trianglePoints] = this.rtl + ? this.drownPathAndTriangleRTL( + shiftedFrom, + shiftedTo, + this.rowHeight, + this.taskHeight, + this.arrowIndent + ) + : this.drownPathAndTriangle( + shiftedFrom, + shiftedTo, + this.rowHeight, + this.taskHeight, + this.arrowIndent + ); + + rendered.push( + + + + + ); + }); + } + return rendered; + } + drownPathAndTriangle( taskFrom: KupPlannerBarTask, taskTo: KupPlannerBarTask, @@ -1081,11 +1248,14 @@ export class KupGridRenderer { fill={this.arrowColor} stroke={this.arrowColor} > + {this.renderDependencies()} + {/* Legacy per-task children arrows (keep for backwards compatibility) */} {this.tasks.map((task) => { return task.barChildren.map((child) => { if (task.type !== 'timeline') { - this.renderKupArrow(task, child); + return this.renderKupArrow(task, child); } + return null; }); })} diff --git a/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/readme.md b/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/readme.md index b15b36c7fb..b70a9adca3 100644 --- a/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/readme.md +++ b/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/readme.md @@ -7,41 +7,42 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ---------------------- | ---------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | -| `arrowColor` | `arrow-color` | | `string` | `''` | -| `arrowIndent` | `arrow-indent` | | `number` | `0` | -| `barClick` | -- | | `(task: KupPlannerTask) => void` | `undefined` | -| `barContextMenu` | -- | | `(event: UIEvent, task: KupPlannerTask) => void` | `undefined` | -| `barDblClick` | -- | | `(task: KupPlannerTask) => void` | `undefined` | -| `columnWidth` | `column-width` | | `number` | `0` | -| `currentDateIndicator` | -- | | `KupPlannerCurrentDateIndicator` | `undefined` | -| `dateChange` | -- | | `(task: KupPlannerTask, children: KupPlannerTask[]) => boolean \| void \| Promise \| Promise` | `undefined` | -| `dates` | -- | | `Date[]` | `undefined` | -| `delete` | -- | | `(task: KupPlannerTask) => boolean \| void \| Promise \| Promise` | `undefined` | -| `doubleClick` | -- | | `(task: KupPlannerTask) => void` | `undefined` | -| `eMouseDown` | -- | | `(event: MouseEvent) => void` | `undefined` | -| `eventStart` | -- | | `(action: KupPlannerGanttContentMoveAction, selectedTask: KupPlannerBarTask, event?: KeyboardEvent \| MouseEvent) => any` | `undefined` | -| `fontFamily` | `font-family` | | `string` | `''` | -| `fontSize` | `font-size` | | `string` | `''` | -| `ganttEvent` | -- | | `{ changedTask?: KupPlannerBarTask; originalSelectedTask?: KupPlannerBarTask; action: KupPlannerGanttContentMoveAction; }` | `undefined` | -| `gridProps` | -- | | `{ tasks: KupPlannerTask[]; dates: Date[]; svgWidth: number; rowHeight: number; columnWidth: number; todayColor: string; rtl: boolean; }` | `undefined` | -| `hideLabel` | `hide-label` | | `boolean` | `false` | -| `phaseDragScroll` | -- | | `(scrollY: number) => void` | `undefined` | -| `phaseDrop` | -- | | `(originalPhaseData: KupPlannerTask, originalTaskData: KupPlannerTask, finalPhaseData: KupPlannerTask, destinationData: KupPlannerTask) => boolean \| void \| Promise \| Promise` | `undefined` | -| `progressChange` | -- | | `(task: KupPlannerTask, children: KupPlannerTask[]) => boolean \| void \| Promise \| Promise` | `undefined` | -| `projection` | -- | | `{ x0: number; xf: number; color: string; }` | `undefined` | -| `readOnly` | `read-only` | | `boolean` | `false` | -| `rowHeight` | `row-height` | | `number` | `0` | -| `rtl` | `rtl` | | `boolean` | `false` | -| `selectedTask` | -- | | `KupPlannerBarTask` | `undefined` | -| `setFailedTask` | -- | | `(value: KupPlannerBarTask) => void` | `undefined` | -| `setGanttEvent` | -- | | `(gantt: KupPlannerGanttEvent) => void` | `undefined` | -| `setSelectedTask` | -- | | `(taskId: string) => void` | `undefined` | -| `showSecondaryDates` | `show-secondary-dates` | | `boolean` | `false` | -| `taskHeight` | `task-height` | | `number` | `0` | -| `tasks` | -- | | `KupPlannerBarTask[]` | `undefined` | -| `timeStep` | `time-step` | | `number` | `0` | +| Property | Attribute | Description | Type | Default | +| ---------------------- | ---------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | +| `arrowColor` | `arrow-color` | | `string` | `''` | +| `arrowIndent` | `arrow-indent` | | `number` | `0` | +| `barClick` | -- | | `(task: KupPlannerTask) => void` | `undefined` | +| `barContextMenu` | -- | | `(event: UIEvent, task: KupPlannerTask) => void` | `undefined` | +| `barDblClick` | -- | | `(task: KupPlannerTask) => void` | `undefined` | +| `columnWidth` | `column-width` | | `number` | `0` | +| `currentDateIndicator` | -- | | `KupPlannerCurrentDateIndicator` | `undefined` | +| `dateChange` | -- | | `(task: KupPlannerTask, children: KupPlannerTask[]) => boolean \| void \| Promise \| Promise` | `undefined` | +| `dates` | -- | | `Date[]` | `undefined` | +| `delete` | -- | | `(task: KupPlannerTask) => boolean \| void \| Promise \| Promise` | `undefined` | +| `dependencies` | -- | Optional structured dependencies to draw as arrows | `KupPlannerDependency[]` | `[]` | +| `doubleClick` | -- | | `(task: KupPlannerTask) => void` | `undefined` | +| `eMouseDown` | -- | | `(event: MouseEvent) => void` | `undefined` | +| `eventStart` | -- | | `(action: KupPlannerGanttContentMoveAction, selectedTask: KupPlannerBarTask, event?: KeyboardEvent \| MouseEvent) => any` | `undefined` | +| `fontFamily` | `font-family` | | `string` | `''` | +| `fontSize` | `font-size` | | `string` | `''` | +| `ganttEvent` | -- | | `{ changedTask?: KupPlannerBarTask; originalSelectedTask?: KupPlannerBarTask; action: KupPlannerGanttContentMoveAction; }` | `undefined` | +| `gridProps` | -- | | `{ tasks: KupPlannerTask[]; dates: Date[]; svgWidth: number; rowHeight: number; columnWidth: number; todayColor: string; rtl: boolean; }` | `undefined` | +| `hideLabel` | `hide-label` | | `boolean` | `false` | +| `phaseDragScroll` | -- | | `(scrollY: number) => void` | `undefined` | +| `phaseDrop` | -- | | `(originalPhaseData: KupPlannerTask, originalTaskData: KupPlannerTask, finalPhaseData: KupPlannerTask, destinationData: KupPlannerTask) => boolean \| void \| Promise \| Promise` | `undefined` | +| `progressChange` | -- | | `(task: KupPlannerTask, children: KupPlannerTask[]) => boolean \| void \| Promise \| Promise` | `undefined` | +| `projection` | -- | | `{ x0: number; xf: number; color: string; }` | `undefined` | +| `readOnly` | `read-only` | | `boolean` | `false` | +| `rowHeight` | `row-height` | | `number` | `0` | +| `rtl` | `rtl` | | `boolean` | `false` | +| `selectedTask` | -- | | `KupPlannerBarTask` | `undefined` | +| `setFailedTask` | -- | | `(value: KupPlannerBarTask) => void` | `undefined` | +| `setGanttEvent` | -- | | `(gantt: KupPlannerGanttEvent) => void` | `undefined` | +| `setSelectedTask` | -- | | `(taskId: string) => void` | `undefined` | +| `showSecondaryDates` | `show-secondary-dates` | | `boolean` | `false` | +| `taskHeight` | `task-height` | | `number` | `0` | +| `tasks` | -- | | `KupPlannerBarTask[]` | `undefined` | +| `timeStep` | `time-step` | | `number` | `0` | ## Dependencies diff --git a/packages/ketchup/src/components/kup-planner/utils/kup-planner-renderer.tsx b/packages/ketchup/src/components/kup-planner/utils/kup-planner-renderer.tsx index ea448fd1c9..71284e0ea2 100644 --- a/packages/ketchup/src/components/kup-planner/utils/kup-planner-renderer.tsx +++ b/packages/ketchup/src/components/kup-planner/utils/kup-planner-renderer.tsx @@ -424,6 +424,7 @@ export class KupPlannerRenderer { tasks={this.tasks} columnWidth={columnWidthForTimeUnit(this.timeUnit)} viewMode={this.timeUnit} + dependencies={this.props.mainGantt.dependencies} {...this.props.mainGantt.stylingOptions} TaskListHeader={ this.props.mainGantt.taskListHeaderProject @@ -576,6 +577,9 @@ export class KupPlannerRenderer { this.timeUnit )} viewMode={this.timeUnit} + dependencies={ + this.props.secondaryGantt.dependencies + } {...this.props.secondaryGantt.stylingOptions} TaskListHeader={ this.props.secondaryGantt diff --git a/packages/ketchup/src/components/kup-planner/utils/kup-task-gantt/kup-task-gantt.tsx b/packages/ketchup/src/components/kup-planner/utils/kup-task-gantt/kup-task-gantt.tsx index 304104d767..3bc34a3971 100644 --- a/packages/ketchup/src/components/kup-planner/utils/kup-task-gantt/kup-task-gantt.tsx +++ b/packages/ketchup/src/components/kup-planner/utils/kup-task-gantt/kup-task-gantt.tsx @@ -77,7 +77,12 @@ export class TaskGantt { } render() { - const newBarProps = { ...this.barProps, gridProps: this.gridProps, phaseDragScroll: this.phaseDragScroll }; + const newBarProps = { + ...this.barProps, + gridProps: this.gridProps, + phaseDragScroll: this.phaseDragScroll, + dependencies: this.barProps.dependencies || [], + }; return (
+ + + + + + + Ketchup planner (example 7) + + + + + + + + + + + + + diff --git a/packages/ketchup/stencil.config.ts b/packages/ketchup/stencil.config.ts index 8fd57fe024..cc5c727562 100644 --- a/packages/ketchup/stencil.config.ts +++ b/packages/ketchup/stencil.config.ts @@ -102,6 +102,7 @@ export const config: Config = { { src: 'planner-example-4.html' }, { src: 'planner-example-5.html' }, { src: 'planner-example-6.html' }, + { src: 'planner-example-7.html' }, { src: 'probe.html' }, { src: 'progress-bar.html' }, { src: 'radio.html' }, From c27ea1512ec3fffa826da36bf390b9bdbd37d092 Mon Sep 17 00:00:00 2001 From: Luca Foscili <45429703+lucafoscili@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:02:51 +0200 Subject: [PATCH 2/3] feat: Add dependencyCol prop for phase dependencies in planner Introduces the `dependencyCol` property to kup-planner, allowing automatic creation of structured dependencies between phases based on a column in the phases dataset (e.g., OPEDIP). Updates example 7 and documentation to demonstrate and describe the new feature, and extends types to support the new prop. --- .../ketchup/src/assets/planner-example-7.js | 1260 ++++------------- packages/ketchup/src/components.d.ts | 10 + .../components/kup-planner/kup-planner.tsx | 106 ++ .../src/components/kup-planner/readme.md | 104 +- .../kup-grid-renderer/kup-grid-renderer.tsx | 34 +- .../utils/kup-grid-renderer/readme.md | 72 +- .../utils/kup-task-gantt/readme.md | 20 +- packages/ketchup/src/planner-example-7.html | 37 + 8 files changed, 550 insertions(+), 1093 deletions(-) diff --git a/packages/ketchup/src/assets/planner-example-7.js b/packages/ketchup/src/assets/planner-example-7.js index b99371c341..6c1d416229 100644 --- a/packages/ketchup/src/assets/planner-example-7.js +++ b/packages/ketchup/src/assets/planner-example-7.js @@ -1,1054 +1,290 @@ +// Example 7 - Planner demo for stacked/grouped dependencies +// Minimal demo that maps client column names, includes phases with OPEDIP, +// injects extra tasks and structured dependencies to stress the renderer. + const comp = document.getElementById('planner'); +if (!comp) throw new Error('No #planner element found on page'); comp.addEventListener('kup-planner-click', onclick); -comp.addEventListener('kup-planner-didunload', (e) => { - console.log('Planner removed', e); -}); -document.addEventListener('kup-button-click', () => { - console.log('Removing planner'); - comp.remove(); -}); - -document.addEventListener('kup-planner-datechange', (e) => { - console.log(e); -}); -document.addEventListener('kup-planner-phasedrop', (e) => { - console.log(e); -}); - -// Base props copied from example-6, trimmed for brevity. The important part is the tasks ids -// which we will reference in the dependencies below. const props = { data: { columns: [ - { - isEditable: false, - isKey: false, - name: 'R§COMM', - obj: { - k: '', - p: '', - t: 'CM', - }, - title: 'Commessa', - tooltip: false, - }, - { - isEditable: false, - isKey: false, - name: 'DATPRE', - obj: { - k: '', - p: '*YYMD', - t: 'D8', - }, - title: 'Data Cons.\nAttualizz.', - tooltip: false, - }, - { - isEditable: false, - isKey: false, - name: 'DATORD', - obj: { - k: '', - p: '*YYMD', - t: 'D8', - }, - title: 'Data Cons.\nP.Ordine', - tooltip: false, - }, - { - isEditable: false, - isKey: false, - name: 'R§CDCL', - objs: [ - { - k: '', - p: 'CLP', - t: 'CN', - }, - ], - title: 'Ente', - tooltip: false, - }, - { - isEditable: false, - isKey: false, - name: 'DATINZ', - obj: { - k: '', - p: '*YYMD', - t: 'D8', - }, - title: 'Data inizio\nAttualizz.', - tooltip: false, - }, - { - isEditable: false, - isKey: false, - name: 'INZORD', - obj: { - k: '', - p: '*YYMD', - t: 'D8', - }, - title: 'Data inizio\nP.Ordine', - tooltip: false, - }, - { - isEditable: false, - isKey: false, - name: 'INPRHHMMSS', - obj: { - k: '', - p: '2', - t: 'I1', - }, - title: 'Init preview hour (HH:mm:ss)', - tooltip: true, - }, - { - isEditable: false, - isKey: false, - name: 'INITHHMMSS', - obj: { - k: '', - p: '2', - t: 'I1', - }, - title: 'Init hour (HH:mm:ss)', - tooltip: true, - }, - { - isEditable: false, - isKey: false, - name: 'ENPRHHMMSS', - obj: { - k: '', - p: '2', - t: 'I1', - }, - title: 'End preview hour (HH:mm:ss)', - tooltip: true, - }, - { - isEditable: false, - isKey: false, - name: 'ENDHHMMSS', - obj: { - k: '', - p: '2', - t: 'I1', - }, - title: 'End hour (HH:mm:ss)', - tooltip: true, - }, - { - isEditable: false, - isKey: false, - name: 'INPRHHMM', - obj: { - k: '', - p: '3', - t: 'I1', - }, - title: 'Init previewed hour (HH:mm)', - tooltip: true, - }, - { - isEditable: false, - isKey: false, - name: 'INITHHMM', - obj: { - k: '', - p: '3', - t: 'I1', - }, - title: 'Init hour (HH:mm)', - tooltip: true, - }, - { - isEditable: false, - isKey: false, - name: 'ENPRHHMM', - obj: { - k: '', - p: '3', - t: 'I1', - }, - title: 'End preview hour (HH:mm)', - tooltip: true, - }, - { - isEditable: false, - isKey: false, - name: 'ENDHHMM', - obj: { - k: '', - p: '3', - t: 'I1', - }, - title: 'End hour (HH:mm)', - tooltip: true, - }, + { name: 'CODCOM' }, + { name: 'CODSEQ' }, + { name: 'CODFAS' }, + { name: 'DESFAS' }, + { name: 'COLFAS' }, + { name: 'INISIM' }, + { name: 'FINSIM' }, + { name: 'INICON' }, + { name: 'FINCON' }, + { name: 'OPEDIP' }, ], rows: [ { + id: 'cm1', cells: { - 'R§CDCL': { - data: { - size: 15, - helperEnabled: false, - hiddenCounter: true, - maxLength: 15, - }, - isEditable: false, - obj: { - k: 'BENARM', - p: 'CLP', - t: 'CN', - }, - value: 'BENARM', - displayedValue: 'BENARM', - }, - DATPRE: { - data: { - size: 8, - helperEnabled: false, - hiddenCounter: true, - maxLength: 8, - }, - isEditable: false, - obj: { - k: '20240228', - p: '*YYMD', - t: 'D8', - }, - value: '2024-02-28', - }, - INZORD: { - data: { - size: 8, - helperEnabled: false, - hiddenCounter: true, - maxLength: 8, - }, - isEditable: false, - obj: { - k: '20230512', - p: '*YYMD', - t: 'D8', - }, - value: '2023-05-12', - }, - DATORD: { - data: { - size: 8, - helperEnabled: false, - hiddenCounter: true, - maxLength: 8, - }, - isEditable: false, - obj: { - k: '20240228', - p: '*YYMD', - t: 'D8', - }, - value: '2024-02-28', - }, - DATINZ: { - data: { - size: 8, - helperEnabled: false, - hiddenCounter: true, - maxLength: 8, - }, - isEditable: false, - obj: { - k: '20230512', - p: '*YYMD', - t: 'D8', - }, - value: '2023-05-12', + CODCOM: { value: 'CM1' }, + CODSEQ: { value: '001' }, + DESFAS: { value: 'COMMESSA 1' }, + INICON: { + value: '2025-03-01', + obj: { k: '', p: '*YYMD', t: 'D8' }, }, - 'R§COMM': { - data: { - size: 10, - helperEnabled: false, - hiddenCounter: true, - maxLength: 10, - }, - isEditable: false, - obj: { - k: 'G418', - p: '', - t: 'CM', - }, - value: 'G418', - displayedValue: 'G418', + FINCON: { + value: '2025-07-04', + obj: { k: '', p: '*YYMD', t: 'D8' }, }, - INPRHHMMSS: { - isEditable: false, - obj: { - k: '070030', - p: '2', - t: 'I1', - }, - value: '07:00:30', + INISIM: { + value: '2025-09-01', + obj: { k: '', p: '*YYMD', t: 'D8' }, }, - INITHHMMSS: { - isEditable: false, - obj: { - k: '080000', - p: '2', - t: 'I1', - }, - value: '08:00:00', - }, - ENPRHHMMSS: { - isEditable: false, - obj: { - k: '163050', - p: '2', - t: 'I1', - }, - value: '16:30:50', - }, - ENDHHMMSS: { - isEditable: false, - obj: { - k: '180000', - p: '2', - t: 'I1', - }, - value: '18:00:00', - }, - INPRHHMM: { - isEditable: false, - obj: { - k: '0700', - p: '3', - t: 'I1', - }, - value: '07:00', - }, - INITHHMM: { - isEditable: false, - obj: { - k: '0800', - p: '3', - t: 'I1', - }, - value: '08:00', - }, - ENPRHHMM: { - isEditable: false, - obj: { - k: '1630', - p: '3', - t: 'I1', - }, - value: '16:30', - }, - ENDHHMM: { - isEditable: false, - obj: { - k: '1800', - p: '3', - t: 'I1', - }, - value: '18:00', + FINSIM: { + value: '2025-12-05', + obj: { k: '', p: '*YYMD', t: 'D8' }, }, + OPEDIP: { value: '' }, }, cssClass: 'clickable', - id: '1', - object: '', readOnly: true, }, ], }, - taskHeight: 400, - taskIdCol: 'R§COMM', - taskNameCol: 'R§COMM', - taskDates: ['DATINZ', 'DATPRE'], - taskPrevDates: ['INZORD', 'DATORD'], - taskColumns: ['R§COMM', 'R§CDCL', 'DATINZ', 'DATPRE'], - scrollableTaskList: true, - // Ensure phase-related props are defined so addPhases can parse the provided phases dataset + taskIdCol: 'CODCOM', + taskNameCol: 'CODCOM', + taskDates: ['INICON', 'FINCON'], + taskPrevDates: ['INISIM', 'FINSIM'], phaseIdCol: 'CODFAS', phaseNameCol: 'DESFAS', - phaseDates: ['DATINI', 'DATFIN'], - phasePrevDates: ['DATINZ', 'DATFPO'], - phaseColumns: ['CODFAS', 'DESFAS', 'DATINI', 'DATFIN'], - phaseHours: ['INITHHMMSS', 'ENDHHMMSS'], - phasePrevHours: ['INPRHHMMSS', 'ENPRHHMMSS'], + phaseDates: ['INICON', 'FINCON'], + phasePrevDates: ['INISIM', 'FINSIM'], phaseColorCol: 'COLFAS', - // Add structured dependencies. To target phases, use the planner's phase id - // format: _ (the component creates phase ids as taskId + '_' + CODFAS) - // Use runtime planner task ids here (taskId and taskId_phaseId) so - // they match the ids created by addPhases (e.g. G418_P410). - dependencies: [ - // Two stacked arrows from task 'G418' to its phase 'P410' - { id: 'd1', sourceId: 'G418', targetId: 'G418_P410', type: 'FS' }, - { id: 'd2', sourceId: 'G418', targetId: 'G418_P410', type: 'FS' }, - // Arrow from task 'G418' to its phase 'P750' - { id: 'd3', sourceId: 'G418', targetId: 'G418_P750', type: 'FS' }, - ], + dependencyCol: 'OPEDIP', + dependencies: [], }; -if (props) { - for (const key in props) { - comp[key] = props[key]; - } -} - -function onclick(event) { - console.log('planner.js onclick', event.detail); - const taskAction = event.detail && event.detail.taskAction; - // Be resilient to slightly different action naming and use the clicked task id - if ( - taskAction === 'onTaskOpening' || - (typeof taskAction === 'string' && - taskAction.toLowerCase().includes('opening')) - ) { - const clickedId = - event.detail && event.detail.value && event.detail.value.id; - if (clickedId) { - // Use the actual clicked task id so example works regardless of task id value - comp.addPhases(clickedId, phases); - } else { - console.warn( - 'Could not determine clicked task id, skipping addPhases' - ); - } - } -} - const phases = { columns: [ + { name: 'CODCOM' }, + { name: 'CODSEQ' }, + { name: 'CODFAS' }, + { name: 'DESFAS' }, + { name: 'COLFAS' }, + { name: 'INISIM' }, + { name: 'FINSIM' }, + { name: 'INICON' }, + { name: 'FINCON' }, + { name: 'OPEDIP' }, + ], + rows: [ { - isEditable: false, - isKey: false, - name: 'CODFAS', - obj: { - k: '', - p: '', - t: 'OP', - }, - title: 'Fase', - tooltip: false, - }, - { - isEditable: false, - isKey: false, - name: 'DESFAS', - title: 'Des\nFase', - tooltip: false, - }, - { - isEditable: false, - isKey: false, - name: 'DATINI', - obj: { - k: '', - p: '*YYMD', - t: 'D8', - }, - title: 'Data\nInizio', - tooltip: false, - }, - { - isEditable: false, - isKey: false, - name: 'DATINZ', - obj: { - k: '', - p: '*YYMD', - t: 'D8', - }, - title: 'Data\nInizio P.O', - tooltip: false, - }, - { - isEditable: false, - isKey: false, - name: 'DATFIN', - obj: { - k: '', - p: '*YYMD', - t: 'D8', - }, - title: 'Data\nFine', - tooltip: false, - }, - { - isEditable: false, - isKey: false, - name: 'DATFPO', - obj: { - k: '', - p: '*YYMD', - t: 'D8', - }, - title: 'Data\nFine\nPrev.Ordine', - tooltip: false, - }, - { - isEditable: false, - isKey: false, - name: 'COLFAS', - title: 'Sty\nColore', - tooltip: false, - visible: false, - }, - { - isEditable: false, - isKey: false, - name: 'INPRHHMMSS', - obj: { - k: '', - p: '2', - t: 'I1', - }, - title: 'Init prev hour (HH:mm:ss)', - tooltip: true, - }, - { - isEditable: false, - isKey: false, - name: 'INITHHMMSS', - obj: { - k: '', - p: '2', - t: 'I1', - }, - title: 'Init hour (HH:mm:ss)', - tooltip: true, - }, - { - isEditable: false, - isKey: false, - name: 'ENPRHHMMSS', - obj: { - k: '', - p: '2', - t: 'I1', - }, - title: 'End pre hour (HH:mm:ss)', - tooltip: true, - }, - { - isEditable: false, - isKey: false, - name: 'ENDHHMMSS', - obj: { - k: '', - p: '2', - t: 'I1', - }, - title: 'End hour (HH:mm:ss)', - tooltip: true, - }, - { - isEditable: false, - isKey: false, - name: 'INPRHHMM', - obj: { - k: '', - p: '3', - t: 'I1', - }, - title: 'Init pre hour (HH:mm)', - tooltip: true, - }, - { - isEditable: false, - isKey: false, - name: 'INITHHMM', - obj: { - k: '', - p: '3', - t: 'I1', + id: '1', + cells: { + CODCOM: { value: 'CM1' }, + CODSEQ: { value: '110' }, + CODFAS: { value: '010' }, + DESFAS: { value: 'MONTAGGIO' }, + COLFAS: { value: '#000000' }, + INISIM: { + value: '2025-09-01', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + FINSIM: { + value: '2025-12-05', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + INICON: { + value: '2025-03-01', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + FINCON: { + value: '2025-07-04', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + OPEDIP: { value: '' }, }, - title: 'Init hour (HH:mm)', - tooltip: true, + readOnly: true, }, { - isEditable: false, - isKey: false, - name: 'ENPRHHMM', - obj: { - k: '', - p: '3', - t: 'I1', + id: '2', + cells: { + CODCOM: { value: 'CM1' }, + CODSEQ: { value: '120' }, + CODFAS: { value: '020' }, + DESFAS: { value: 'COLLAUDO' }, + COLFAS: { value: '#05E808' }, + INISIM: { + value: '2025-12-08', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + FINSIM: { + value: '2026-04-08', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + INICON: { + value: '2025-07-07', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + FINCON: { + value: '2025-09-29', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + OPEDIP: { value: '010' }, }, - title: 'End pre hour (HH:mm)', - tooltip: true, + readOnly: true, }, { - isEditable: false, - isKey: false, - name: 'ENDHHMM', - obj: { - k: '', - p: '3', - t: 'I1', + id: '3', + cells: { + CODCOM: { value: 'CM1' }, + CODSEQ: { value: '130' }, + CODFAS: { value: '030' }, + DESFAS: { value: 'SPEDIZIONE' }, + COLFAS: { value: '#05CAE8' }, + INISIM: { + value: '2026-05-13', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + FINSIM: { + value: '2026-07-08', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + INICON: { + value: '2025-09-30', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + FINCON: { + value: '2025-12-09', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + OPEDIP: { value: '010,020' }, }, - title: 'End hour (HH:mm)', - tooltip: true, + readOnly: true, }, - ], - rows: [ { + id: '4', cells: { - COLFAS: { - data: { - size: 10, - helperEnabled: false, - hiddenCounter: true, - maxLength: 10, - }, - isEditable: false, - obj: { - k: '#000000', - p: '', - t: '', - }, - value: '#000000', - }, - - DATFIN: { - data: { - size: 8, - helperEnabled: false, - hiddenCounter: true, - maxLength: 8, - }, - isEditable: false, - obj: { - k: '20230905', - p: '*YYMD', - t: 'D8', - }, - value: '2023-09-05', - }, - - DATINI: { - data: { - size: 8, - helperEnabled: false, - hiddenCounter: true, - maxLength: 8, - }, - isEditable: false, - obj: { - k: '20230522', - p: '*YYMD', - t: 'D8', - }, - value: '2023-05-22', - }, - DATFPO: { - data: { - size: 8, - helperEnabled: false, - hiddenCounter: true, - maxLength: 8, - }, - isEditable: false, - obj: { - k: '20230904', - p: '*YYMD', - t: 'D8', - }, - value: '2023-09-04', - }, - - CODFAS: { - cssClass: 'strong-text', - data: { - size: 15, - helperEnabled: false, - hiddenCounter: true, - maxLength: 15, - }, - isEditable: false, - obj: { - k: 'P410 ', - p: '', - t: 'OP', - }, - value: 'P410 ', - }, - - DESFAS: { - data: { - size: 35, - helperEnabled: false, - hiddenCounter: true, - maxLength: 35, - }, - isEditable: false, - obj: { - k: 'MONTAGGIO MECCANICO', - p: '', - t: '', - }, - value: 'MONTAGGIO MECCANICO', - displayedValue: 'MONTAGGIO MECCANICO', - }, - DATINZ: { - data: { - size: 8, - helperEnabled: false, - hiddenCounter: true, - maxLength: 8, - }, - isEditable: false, - obj: { - k: '20230522', - p: '*YYMD', - t: 'D8', - }, - value: '2023-05-22', - }, - INPRHHMMSS: { - isEditable: false, - obj: { - k: '103015', - p: '2', - t: 'I1', - }, - value: '10:30:15', - }, - INITHHMMSS: { - isEditable: false, - obj: { - k: '113000', - p: '2', - t: 'I1', - }, - value: '11:30:00', - }, - ENPRHHMMSS: { - isEditable: false, - obj: { - k: '124515', - p: '2', - t: 'I1', - }, - value: '12:45:15', - }, - ENDHHMMSS: { - isEditable: false, - obj: { - k: '164500', - p: '2', - t: 'I1', - }, - value: '16:45:00', - }, - - INPRHHMM: { - isEditable: false, - obj: { - k: '1030', - p: '3', - t: 'I1', - }, - value: '10:30', - }, - INITHHMM: { - isEditable: false, - obj: { - k: '1130', - p: '3', - t: 'I1', - }, - value: '11:30', - }, - - ENPRHHMM: { - isEditable: false, - obj: { - k: '1545', - p: '3', - t: 'I1', - }, - value: '15:45', - }, - - ENDHHMM: { - isEditable: false, - obj: { - k: '1645', - p: '3', - t: 'I1', - }, - value: '16:45', - }, + CODCOM: { value: 'CM1' }, + CODSEQ: { value: '140' }, + CODFAS: { value: '040' }, + DESFAS: { value: 'INSTALLAZIONE' }, + COLFAS: { value: '#3605E8' }, + INISIM: { + value: '2026-07-09', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + FINSIM: { + value: '2026-11-05', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + INICON: { + value: '2025-12-10', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + FINCON: { + value: '2026-03-04', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + OPEDIP: { value: '030' }, }, - id: '1', - object: '', readOnly: true, }, { + id: '5', cells: { - COLFAS: { - data: { - size: 10, - helperEnabled: false, - hiddenCounter: true, - maxLength: 10, - }, - isEditable: false, - obj: { - k: '#7030A0', - p: '', - t: '', - }, - value: '#7030A0', - }, - - DATFIN: { - data: { - size: 8, - helperEnabled: false, - hiddenCounter: true, - maxLength: 8, - }, - isEditable: false, - obj: { - k: '20240228', - p: '*YYMD', - t: 'D8', - }, - value: '2024-02-28', - }, - - DATINI: { - data: { - size: 8, - helperEnabled: false, - hiddenCounter: true, - maxLength: 8, - }, - isEditable: false, - obj: { - k: '20240131', - p: '*YYMD', - t: 'D8', - }, - value: '2024-01-31', - }, - DATFPO: { - data: { - size: 8, - helperEnabled: false, - hiddenCounter: true, - maxLength: 8, - }, - isEditable: false, - obj: { - k: '20240228', - p: '*YYMD', - t: 'D8', - }, - value: '2024-02-28', - }, - - CODFAS: { - cssClass: 'strong-text', - data: { - size: 15, - helperEnabled: false, - hiddenCounter: true, - maxLength: 15, - }, - isEditable: false, - obj: { - k: 'P750 ', - p: '', - t: 'OP', - }, - value: 'P750 ', - }, - - DESFAS: { - data: { - size: 35, - helperEnabled: false, - hiddenCounter: true, - maxLength: 35, - }, - isEditable: false, - obj: { - k: 'INSTALLAZIONE', - p: '', - t: '', - }, - value: 'INSTALLAZIONE', - displayedValue: 'INSTALLAZIONE', - }, - DATINZ: { - data: { - size: 8, - helperEnabled: false, - hiddenCounter: true, - maxLength: 8, - }, - isEditable: false, - obj: { - k: '20240131', - p: '*YYMD', - t: 'D8', - }, - value: '2024-01-31', - }, - INPRHHMMSS: { - isEditable: false, - obj: { - k: '103005', - p: '2', - t: 'I1', - }, - value: '10:30:05', - }, - INITHHMMSS: { - isEditable: false, - obj: { - k: '073000', - p: '2', - t: 'I1', - }, - value: '07:30:00', - }, - - ENPRHHMMSS: { - isEditable: false, - obj: { - k: '081500', - p: '2', - t: 'I1', - }, - value: '08:15:00', - }, - ENDHHMMSS: { - isEditable: false, - obj: { - k: '094500', - p: '2', - t: 'I1', - }, - value: '09:45:00', - }, - - INPRHHMM: { - isEditable: false, - obj: { - k: '1030', - p: '3', - t: 'I1', - }, - value: '10:30', - }, - - INITHHMM: { - isEditable: false, - obj: { - k: '0730', - p: '3', - t: 'I1', - }, - value: '07:30', - }, - ENPRHHMM: { - isEditable: false, - obj: { - k: '0815', - p: '3', - t: 'I1', - }, - value: '08:15', - }, - ENDHHMM: { - isEditable: false, - obj: { - k: '0945', - p: '3', - t: 'I1', - }, - value: '09:45', - }, + CODCOM: { value: 'CM1' }, + CODSEQ: { value: '150' }, + CODFAS: { value: '050' }, + DESFAS: { value: 'FORMAZIONE' }, + COLFAS: { value: '#BB05E8' }, + INISIM: { + value: '2026-11-19', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + FINSIM: { + value: '2028-02-02', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + INICON: { + value: '2026-03-05', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + FINCON: { + value: '2026-07-23', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + OPEDIP: { value: '040' }, }, - id: '9', - object: '', readOnly: true, }, ], }; -// Minimal phases/tasks data are attached by the component in the example 6 files. -// This demo re-uses the default sample rows that are already present in example-6. - -// --- Extra mock tasks & dependencies for stress-testing stacked arrows --- -// To ensure injected rows have the exact shape the component expects we clone -// an existing sample row (props.data.rows[0]) and only change the id and -// the displayed task code (`R§COMM`). This avoids malformed rows that fail -// the component's validation/filtering. -const extraTasks = []; -try { - if ( - props && - props.data && - Array.isArray(props.data.rows) && - props.data.rows[0] - ) { - const sampleRow = props.data.rows[0]; - ['G419', 'G420', 'G421'].forEach((gid, idx) => { - try { - const clone = JSON.parse(JSON.stringify(sampleRow)); - clone.id = (idx + 2).toString(); - if (clone.cells && clone.cells['R§COMM']) { - clone.cells['R§COMM'].value = gid; - clone.cells['R§COMM'].displayedValue = gid; - } - extraTasks.push(clone); - } catch (e) { - // fallback: don't push malformed clones - } - }); - - // Insert the clones after the first sample row so they are visible - if (extraTasks.length) props.data.rows.splice(1, 0, ...extraTasks); - } -} catch (e) { - // ignore in case the props structure differs +// Inject extra mock tasks and structured dependencies for stress testing +const extras = { tasks: [], deps: [] }; +if ( + props && + props.data && + Array.isArray(props.data.rows) && + props.data.rows[0] +) { + const sampleRow = props.data.rows[0]; + ['G419', 'G420', 'G421'].forEach((gid, idx) => { + const clone = JSON.parse(JSON.stringify(sampleRow)); + clone.id = 'x-' + gid; + if (!clone.cells) clone.cells = {}; + clone.cells[props.taskIdCol] = { value: gid }; + // ensure visible dates (ISO format with date metadata so planner accepts them) + clone.cells[props.taskDates[0]] = { + value: '2025-01-01', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }; + clone.cells[props.taskDates[1]] = { + value: '2025-02-01', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }; + extras.tasks.push(clone); + }); + if (extras.tasks.length) props.data.rows.splice(1, 0, ...extras.tasks); + /* + extras.deps.push( + { id: 'gd1', sourceId: 'G419', targetId: 'G419_P100', type: 'FS' }, + { id: 'gd2', sourceId: 'G419', targetId: 'G419_P100', type: 'FS' }, + { id: 'gd3', sourceId: 'G419', targetId: 'G419_P100', type: 'FS' }, + { id: 'gd4', sourceId: 'G418', targetId: 'G419', type: 'FS' }, + { id: 'gd5', sourceId: 'G420', targetId: 'G421', type: 'FS' }, + { id: 'gd6', sourceId: 'G420', targetId: 'G418_P750', type: 'FS' }, + { id: 'gd7', sourceId: 'G420', targetId: 'G418_P750', type: 'FS' } + ); +*/ + props.dependencies.push(...extras.deps); } -// Add more structured dependencies to stress stacked rendering. -// We'll include multiple dependencies targeting the same phase and cross-task links. -const extraDeps = [ - // multiple dependencies from G419 to its (imaginary) phase P100 and P101 - { id: 'gd1', sourceId: 'G419', targetId: 'G419_P100', type: 'FS' }, - { id: 'gd2', sourceId: 'G419', targetId: 'G419_P100', type: 'FS' }, - { id: 'gd3', sourceId: 'G419', targetId: 'G419_P100', type: 'FS' }, - // cross-task dependencies - { id: 'gd4', sourceId: 'G418', targetId: 'G419', type: 'FS' }, - { id: 'gd5', sourceId: 'G420', targetId: 'G421', type: 'FS' }, - // two dependencies from G420 to G418_P750 (mixed targets) - { id: 'gd6', sourceId: 'G420', targetId: 'G418_P750', type: 'FS' }, - { id: 'gd7', sourceId: 'G420', targetId: 'G418_P750', type: 'FS' }, -]; +// assign props to the component (deep-clone data to ensure watchers trigger) +for (const key in props) { + if (key === 'data') comp.data = JSON.parse(JSON.stringify(props.data)); + else comp[key] = props[key]; +} try { - if (!props.dependencies) props.dependencies = []; - props.dependencies.push(...extraDeps); + if (typeof comp.refresh === 'function') comp.refresh(); } catch (e) { // ignore } +function onclick(event) { + const clickedId = + event.detail && event.detail.value && event.detail.value.id; + console.log('planner-example-7 onclick', { + clickedId, + event: event.detail, + }); + if (clickedId) comp.addPhases(clickedId, phases); +} + +// (Extra dependencies already injected above in `extras.deps` and props.dependencies) + // Re-assign props to the component so changes apply at runtime when example loads. // Use deep clones for `data` and `dependencies` so the kup-planner prop watchers // detect new object references and update internal state. @@ -1094,3 +330,41 @@ try { } catch (e) { // ignore assignment failures } + +// Expose phases for manual inspection and auto-invoke addPhases for a quick smoke test. +try { + // store phases globally so it's easy to call from the browser console + window.__planner_example_7_phases = phases; + // try to auto-invoke addPhases when the component is ready + const tryInvoke = () => { + const c = document.getElementById('planner'); + if (c && typeof c.addPhases === 'function') { + try { + console.log( + 'planner-example-7: auto-invoking addPhases for CM1' + ); + c.addPhases('CM1', phases); + // also print plannerProps deps if available after a short delay + setTimeout(() => { + try { + // eslint-disable-next-line no-console + console.log( + 'planner-example-7: plannerProps deps', + c.plannerProps && c.plannerProps.mainGantt + ? c.plannerProps.mainGantt.dependencies + : undefined + ); + } catch (e) {} + }, 300); + } catch (e) { + // ignore invocation errors + } + } else { + // retry a few times + setTimeout(tryInvoke, 200); + } + }; + tryInvoke(); +} catch (e) { + // ignore global attach errors +} diff --git a/packages/ketchup/src/components.d.ts b/packages/ketchup/src/components.d.ts index c9f5bcc4ee..c9230c63a9 100644 --- a/packages/ketchup/src/components.d.ts +++ b/packages/ketchup/src/components.d.ts @@ -3563,6 +3563,11 @@ export namespace Components { * Structured dependencies to render as arrows */ "dependencies": KupPlannerDependency[]; + /** + * Optional column name inside the phases dataset containing a reference to a dependent phase (for example: 'OPEDIP'). When set, `addPhases` will read that column and create structured dependencies (FS) from the referenced phase to the current phase (source -> target). Multiple references can be separated by commas in the cell. + * @default undefined + */ + "dependencyCol": string; /** * Column containing the detail color, in hex format * @default null @@ -9239,6 +9244,11 @@ declare namespace LocalJSX { * Structured dependencies to render as arrows */ "dependencies"?: KupPlannerDependency[]; + /** + * Optional column name inside the phases dataset containing a reference to a dependent phase (for example: 'OPEDIP'). When set, `addPhases` will read that column and create structured dependencies (FS) from the referenced phase to the current phase (source -> target). Multiple references can be separated by commas in the cell. + * @default undefined + */ + "dependencyCol"?: string; /** * Column containing the detail color, in hex format * @default null diff --git a/packages/ketchup/src/components/kup-planner/kup-planner.tsx b/packages/ketchup/src/components/kup-planner/kup-planner.tsx index 04a2ba6d3b..1af17ab06d 100644 --- a/packages/ketchup/src/components/kup-planner/kup-planner.tsx +++ b/packages/ketchup/src/components/kup-planner/kup-planner.tsx @@ -327,6 +327,17 @@ export class KupPlanner { @Prop() phaseColorCol: string; + /** + * Optional column name inside the phases dataset containing a reference + * to a dependent phase (for example: 'OPEDIP'). When set, `addPhases` + * will read that column and create structured dependencies (FS) from the + * referenced phase to the current phase (source -> target). + * Multiple references can be separated by commas in the cell. + * @default undefined + */ + @Prop() + dependencyCol: string; + /** * Columns containing informations displayed in the left box ,near the gantt of phases * @default null @@ -742,6 +753,101 @@ export class KupPlanner { } catch (e) { /* ignore */ } + + // If the phases dataset includes a dependency column, parse it and + // create structured dependencies. The column may contain a single + // dependency id or multiple comma-separated ids. The values can be + // either phase codes (e.g. 'P410') or full runtime ids + // ('G418_P410'). We'll normalize to runtime ids using the current + // task id when necessary. + try { + if ( + this.dependencyCol && + data.columns.find((c) => c.name == this.dependencyCol) + ) { + const parsedDeps: any[] = []; + for (const row of data.rows || []) { + const raw = + (row.cells?.[this.dependencyCol]?.value ?? '') + ''; + if (!raw) continue; + const parts = raw + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + + // compute current phase runtime id (target of dependencies) + const phaseCode = + (row.cells?.[this.phaseIdCol]?.value ?? '') + ''; + const currentPhaseId = `${task.id}_${phaseCode.trim()}`; + + for (const part of parts) { + // normalize referenced (source) phase id + let sourcePhaseId = part; + if (!sourcePhaseId.includes('_')) { + // assume phase code in the same task -> make runtime id + sourcePhaseId = `${task.id}_${sourcePhaseId}`; + } + + // dependency goes from the referenced phase (source) to the current phase (target) + const depId = `${sourcePhaseId}__${currentPhaseId}`; + parsedDeps.push({ + id: depId, + sourceId: sourcePhaseId, + targetId: currentPhaseId, + type: 'FS', + }); + } + } + + if (parsedDeps.length) { + // ensure plannerProps.mainGantt.dependencies exists + try { + if (!this.plannerProps.mainGantt.dependencies) { + this.plannerProps.mainGantt.dependencies = []; + } + } catch (e) { + // ensure plannerProps in general exists + if (!this.plannerProps) + this.plannerProps = {} as any; + if (!this.plannerProps.mainGantt) + this.plannerProps.mainGantt = {} as any; + this.plannerProps.mainGantt.dependencies = []; + } + + // merge while avoiding duplicates by id + const existing = new Map( + ( + this.plannerProps.mainGantt.dependencies || [] + ).map((d: any) => [d.id, d]) + ); + for (const pd of parsedDeps) { + if (!existing.has(pd.id)) { + existing.set(pd.id, pd); + } + } + this.plannerProps.mainGantt.dependencies = Array.from( + existing.values() + ); + + // also forward to secondary if present + if (this.plannerProps.secondaryGantt) { + this.plannerProps.secondaryGantt.dependencies = + this.plannerProps.mainGantt.dependencies; + } + + // Diagnostic + try { + // eslint-disable-next-line no-console + console.log( + 'kup-planner: addPhases - injected dependencies', + parsedDeps + ); + } catch (e) {} + } + } + } catch (e) { + // ignore parsing errors + } } this.plannerProps.mainGantt.initialScrollX = this.#storedSettings.taskInitialScrollX; diff --git a/packages/ketchup/src/components/kup-planner/readme.md b/packages/ketchup/src/components/kup-planner/readme.md index 6b21e48f35..bb5de64baa 100644 --- a/packages/ketchup/src/components/kup-planner/readme.md +++ b/packages/ketchup/src/components/kup-planner/readme.md @@ -5,57 +5,59 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ---------------------- | ------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------ | ----------- | -| `customStyle` | `custom-style` | Custom style of the component. | `string` | `''` | -| `data` | -- | Dataset containg the tasks list | `KupDataDataset` | `undefined` | -| `detailColorCol` | `detail-color-col` | Column containing the detail color, in hex format | `string` | `undefined` | -| `detailColumns` | -- | Columns containing informations displayed in the left box, near the gantt of details | `string[]` | `undefined` | -| `detailData` | -- | Dataset containg the details list | `KupDataDataset` | `undefined` | -| `detailDates` | -- | Columns containing detail duration, from (firstDate) to (secondDate) | `string[]` | `undefined` | -| `detailFilter` | `detail-filter` | Sets the detail's filter. | `string` | `undefined` | -| `detailHeight` | `detail-height` | Height for detail gantt | `number` | `undefined` | -| `detailHours` | -- | Columns containing detail hour duration, from (firstDate) to (secondDate) | `string[]` | `[]` | -| `detailIconCol` | `detail-icon-col` | Column containing icon name to show, for detail | `string` | `undefined` | -| `detailIdCol` | `detail-id-col` | Column containing unique detail identifier | `string` | `undefined` | -| `detailInitialScrollX` | `detail-initial-scroll-x` | Sets the initial scroll X for the detail. | `number` | `undefined` | -| `detailInitialScrollY` | `detail-initial-scroll-y` | Sets the initial scroll Y for the detail. | `number` | `undefined` | -| `detailNameCol` | `detail-name-col` | Column containing detail name displayed | `string` | `undefined` | -| `detailPrevDates` | -- | Columns containing forecast detail duration, from (firstDate) to (secondDate) | `string[]` | `undefined` | -| `detailPrevHours` | -- | Columns containing forecast detail duration, from (firstHour) to (secondHour) | `string[]` | `[]` | -| `listCellWidth` | `list-cell-width` | Total size of the cells inside to the left box, near the gantt | `string` | `'300px'` | -| `mainFilter` | -- | Sets the filter for main gantt. | `HTMLElement` | `undefined` | -| `maxWidth` | `max-width` | Max width for component | `string` | `'90vw'` | -| `phaseColParDep` | `phase-col-par-dep` | Column containing the name of the parent phases | `string` | `undefined` | -| `phaseColorCol` | `phase-color-col` | Column containing the phase color in hex format | `string` | `undefined` | -| `phaseColumns` | -- | Columns containing informations displayed in the left box ,near the gantt of phases | `string[]` | `undefined` | -| `phaseDates` | -- | Columns containing phase duration, from (firstDate) to (secondDate) | `string[]` | `undefined` | -| `phaseHours` | -- | Columns containing phase hour duration, from (firstDate) to (secondDate) | `string[]` | `[]` | -| `phaseIconCol` | `phase-icon-col` | Column containing icon name to show, for phase | `string` | `undefined` | -| `phaseIdCol` | `phase-id-col` | Column containing unique phase identifier | `string` | `undefined` | -| `phaseNameCol` | `phase-name-col` | Column containing phase name displayed | `string` | `undefined` | -| `phasePrevDates` | -- | Columns containing forecast phase duration, from (firstDate) to (secondDate) | `string[]` | `undefined` | -| `phasePrevHours` | -- | Columns containing forecast phase duration, from (firstHour) to (secondHour) | `string[]` | `[]` | -| `readOnly` | `read-only` | When true, the two gantts are not interactable. | `boolean` | `false` | -| `scrollableTaskList` | `scrollable-task-list` | Sets the scroll bar for task list. | `boolean` | `false` | -| `secondaryFilter` | -- | Sets the filter for secondary gantt. | `HTMLElement` | `undefined` | -| `showSecondaryDates` | `show-secondary-dates` | Enable/disable display of secondary dates | `boolean` | `false` | -| `stateId` | `state-id` | | `string` | `''` | -| `store` | -- | | `KupStore` | `undefined` | -| `taskColumns` | -- | Columns containing informations displayed in the left box, near the gantt | `string[]` | `undefined` | -| `taskDates` | -- | Columns containing task duration, from (firstDate) to (secondDate) | `string[]` | `undefined` | -| `taskFilter` | `task-filter` | Sets the task's filter. | `string` | `undefined` | -| `taskHeight` | `task-height` | Height for main gantt | `number` | `undefined` | -| `taskHours` | -- | Columns containing task hours duration, from (firstDate) to (secondDate) | `string[]` | `[]` | -| `taskIconCol` | `task-icon-col` | Column containing icon name to show, for task | `string` | `undefined` | -| `taskIdCol` | `task-id-col` | Column containing unique task identifier | `string` | `undefined` | -| `taskInitialScrollX` | `task-initial-scroll-x` | Sets the initial scroll X for the task. | `number` | `undefined` | -| `taskInitialScrollY` | `task-initial-scroll-y` | Sets the initial scroll Y for the task. | `number` | `undefined` | -| `taskNameCol` | `task-name-col` | Column containing task name displayed | `string` | `undefined` | -| `taskPrevDates` | -- | Columns containing forecast task duration, from (firstDate) to (secondDate) | `string[]` | `undefined` | -| `taskPrevHours` | -- | Columns containing forecast task duration, from (firstHour) to (secondHour) | `string[]` | `[]` | -| `titleMess` | `title-mess` | Message displayed on top | `string` | `undefined` | -| `viewMode` | `view-mode` | Sets the view mode. | `"day" \| "hour" \| "month" \| "week" \| "year"` | `'month'` | +| Property | Attribute | Description | Type | Default | +| ---------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------- | +| `customStyle` | `custom-style` | Custom style of the component. | `string` | `''` | +| `data` | -- | Dataset containg the tasks list | `KupDataDataset` | `undefined` | +| `dependencies` | -- | Structured dependencies to render as arrows | `KupPlannerDependency[]` | `[]` | +| `dependencyCol` | `dependency-col` | Optional column name inside the phases dataset containing a reference to a dependent phase (for example: 'OPEDIP'). When set, `addPhases` will read that column and create structured dependencies (FS) from the referenced phase to the current phase (source -> target). Multiple references can be separated by commas in the cell. | `string` | `undefined` | +| `detailColorCol` | `detail-color-col` | Column containing the detail color, in hex format | `string` | `undefined` | +| `detailColumns` | -- | Columns containing informations displayed in the left box, near the gantt of details | `string[]` | `undefined` | +| `detailData` | -- | Dataset containg the details list | `KupDataDataset` | `undefined` | +| `detailDates` | -- | Columns containing detail duration, from (firstDate) to (secondDate) | `string[]` | `undefined` | +| `detailFilter` | `detail-filter` | Sets the detail's filter. | `string` | `undefined` | +| `detailHeight` | `detail-height` | Height for detail gantt | `number` | `undefined` | +| `detailHours` | -- | Columns containing detail hour duration, from (firstDate) to (secondDate) | `string[]` | `[]` | +| `detailIconCol` | `detail-icon-col` | Column containing icon name to show, for detail | `string` | `undefined` | +| `detailIdCol` | `detail-id-col` | Column containing unique detail identifier | `string` | `undefined` | +| `detailInitialScrollX` | `detail-initial-scroll-x` | Sets the initial scroll X for the detail. | `number` | `undefined` | +| `detailInitialScrollY` | `detail-initial-scroll-y` | Sets the initial scroll Y for the detail. | `number` | `undefined` | +| `detailNameCol` | `detail-name-col` | Column containing detail name displayed | `string` | `undefined` | +| `detailPrevDates` | -- | Columns containing forecast detail duration, from (firstDate) to (secondDate) | `string[]` | `undefined` | +| `detailPrevHours` | -- | Columns containing forecast detail duration, from (firstHour) to (secondHour) | `string[]` | `[]` | +| `listCellWidth` | `list-cell-width` | Total size of the cells inside to the left box, near the gantt | `string` | `'300px'` | +| `mainFilter` | -- | Sets the filter for main gantt. | `HTMLElement` | `undefined` | +| `maxWidth` | `max-width` | Max width for component | `string` | `'90vw'` | +| `phaseColParDep` | `phase-col-par-dep` | Column containing the name of the parent phases | `string` | `undefined` | +| `phaseColorCol` | `phase-color-col` | Column containing the phase color in hex format | `string` | `undefined` | +| `phaseColumns` | -- | Columns containing informations displayed in the left box ,near the gantt of phases | `string[]` | `undefined` | +| `phaseDates` | -- | Columns containing phase duration, from (firstDate) to (secondDate) | `string[]` | `undefined` | +| `phaseHours` | -- | Columns containing phase hour duration, from (firstDate) to (secondDate) | `string[]` | `[]` | +| `phaseIconCol` | `phase-icon-col` | Column containing icon name to show, for phase | `string` | `undefined` | +| `phaseIdCol` | `phase-id-col` | Column containing unique phase identifier | `string` | `undefined` | +| `phaseNameCol` | `phase-name-col` | Column containing phase name displayed | `string` | `undefined` | +| `phasePrevDates` | -- | Columns containing forecast phase duration, from (firstDate) to (secondDate) | `string[]` | `undefined` | +| `phasePrevHours` | -- | Columns containing forecast phase duration, from (firstHour) to (secondHour) | `string[]` | `[]` | +| `readOnly` | `read-only` | When true, the two gantts are not interactable. | `boolean` | `false` | +| `scrollableTaskList` | `scrollable-task-list` | Sets the scroll bar for task list. | `boolean` | `false` | +| `secondaryFilter` | -- | Sets the filter for secondary gantt. | `HTMLElement` | `undefined` | +| `showSecondaryDates` | `show-secondary-dates` | Enable/disable display of secondary dates | `boolean` | `false` | +| `stateId` | `state-id` | | `string` | `''` | +| `store` | -- | | `KupStore` | `undefined` | +| `taskColumns` | -- | Columns containing informations displayed in the left box, near the gantt | `string[]` | `undefined` | +| `taskDates` | -- | Columns containing task duration, from (firstDate) to (secondDate) | `string[]` | `undefined` | +| `taskFilter` | `task-filter` | Sets the task's filter. | `string` | `undefined` | +| `taskHeight` | `task-height` | Height for main gantt | `number` | `undefined` | +| `taskHours` | -- | Columns containing task hours duration, from (firstDate) to (secondDate) | `string[]` | `[]` | +| `taskIconCol` | `task-icon-col` | Column containing icon name to show, for task | `string` | `undefined` | +| `taskIdCol` | `task-id-col` | Column containing unique task identifier | `string` | `undefined` | +| `taskInitialScrollX` | `task-initial-scroll-x` | Sets the initial scroll X for the task. | `number` | `undefined` | +| `taskInitialScrollY` | `task-initial-scroll-y` | Sets the initial scroll Y for the task. | `number` | `undefined` | +| `taskNameCol` | `task-name-col` | Column containing task name displayed | `string` | `undefined` | +| `taskPrevDates` | -- | Columns containing forecast task duration, from (firstDate) to (secondDate) | `string[]` | `undefined` | +| `taskPrevHours` | -- | Columns containing forecast task duration, from (firstHour) to (secondHour) | `string[]` | `[]` | +| `titleMess` | `title-mess` | Message displayed on top | `string` | `undefined` | +| `viewMode` | `view-mode` | Sets the view mode. | `"day" \| "hour" \| "month" \| "week" \| "year"` | `'month'` | ## Events diff --git a/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/kup-grid-renderer.tsx b/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/kup-grid-renderer.tsx index 3ea51fd8a1..7ab33ad3a5 100644 --- a/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/kup-grid-renderer.tsx +++ b/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/kup-grid-renderer.tsx @@ -904,7 +904,7 @@ export class KupGridRenderer { const taskById = new Map(); for (const t of this.tasks) taskById.set(t.id, t); - // Group dependencies by pair key + // Group dependencies by pair key (source__target) const groups = new Map(); for (const dep of this.dependencies) { const key = `${dep.sourceId}__${dep.targetId}`; @@ -913,9 +913,34 @@ export class KupGridRenderer { groups.set(key, arr); } + // Also group by target to handle multiple different sources pointing to the same target. + const byTarget = new Map(); // targetId -> array of pair keys + for (const key of groups.keys()) { + const [, targetId] = key.split('__'); + const arr = byTarget.get(targetId) ?? []; + arr.push(key); + byTarget.set(targetId, arr); + } + const rendered: any[] = []; const OFFSET_STEP = 8; // px + // For each target that has multiple source groups, compute a per-group vertical offset + // so the groups themselves are arranged and then individual deps inside each group are + // offset relative to their group's offset. + const groupOffsets = new Map(); // pairKey -> base offset + for (const [targetId, pairKeys] of byTarget.entries()) { + if (pairKeys.length === 1) continue; + // center the groups around 0 + const totalGroups = pairKeys.length; + for (let i = 0; i < pairKeys.length; i++) { + const pk = pairKeys[i]; + const baseOffset = + (i - (totalGroups - 1) / 2) * (OFFSET_STEP * 3); + groupOffsets.set(pk, baseOffset); + } + } + for (const [key, deps] of groups.entries()) { const [sourceId, targetId] = key.split('__'); // tolerate different id formats: exact, trimmed, and taskId_phaseId (with padded phase ids) @@ -1017,9 +1042,12 @@ export class KupGridRenderer { } const total = deps.length; + // base offset for this pair (if groups were arranged around the target) + const base = groupOffsets.get(key) ?? 0; deps.forEach((dep, idx) => { - // compute offset: center the stack around 0 - const offset = (idx - (total - 1) / 2) * OFFSET_STEP; + // compute offset: center the stack around 0 and add group base offset + const intraOffset = (idx - (total - 1) / 2) * OFFSET_STEP; + const offset = base + intraOffset; // we will re-use drownPathAndTriangle but need temporary synthetic tasks shifted by offset const shiftedFrom = { ...sourceTask } as KupPlannerBarTask; diff --git a/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/readme.md b/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/readme.md index b70a9adca3..07b7016ec7 100644 --- a/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/readme.md +++ b/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/readme.md @@ -7,42 +7,42 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ---------------------- | ---------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | -| `arrowColor` | `arrow-color` | | `string` | `''` | -| `arrowIndent` | `arrow-indent` | | `number` | `0` | -| `barClick` | -- | | `(task: KupPlannerTask) => void` | `undefined` | -| `barContextMenu` | -- | | `(event: UIEvent, task: KupPlannerTask) => void` | `undefined` | -| `barDblClick` | -- | | `(task: KupPlannerTask) => void` | `undefined` | -| `columnWidth` | `column-width` | | `number` | `0` | -| `currentDateIndicator` | -- | | `KupPlannerCurrentDateIndicator` | `undefined` | -| `dateChange` | -- | | `(task: KupPlannerTask, children: KupPlannerTask[]) => boolean \| void \| Promise \| Promise` | `undefined` | -| `dates` | -- | | `Date[]` | `undefined` | -| `delete` | -- | | `(task: KupPlannerTask) => boolean \| void \| Promise \| Promise` | `undefined` | -| `dependencies` | -- | Optional structured dependencies to draw as arrows | `KupPlannerDependency[]` | `[]` | -| `doubleClick` | -- | | `(task: KupPlannerTask) => void` | `undefined` | -| `eMouseDown` | -- | | `(event: MouseEvent) => void` | `undefined` | -| `eventStart` | -- | | `(action: KupPlannerGanttContentMoveAction, selectedTask: KupPlannerBarTask, event?: KeyboardEvent \| MouseEvent) => any` | `undefined` | -| `fontFamily` | `font-family` | | `string` | `''` | -| `fontSize` | `font-size` | | `string` | `''` | -| `ganttEvent` | -- | | `{ changedTask?: KupPlannerBarTask; originalSelectedTask?: KupPlannerBarTask; action: KupPlannerGanttContentMoveAction; }` | `undefined` | -| `gridProps` | -- | | `{ tasks: KupPlannerTask[]; dates: Date[]; svgWidth: number; rowHeight: number; columnWidth: number; todayColor: string; rtl: boolean; }` | `undefined` | -| `hideLabel` | `hide-label` | | `boolean` | `false` | -| `phaseDragScroll` | -- | | `(scrollY: number) => void` | `undefined` | -| `phaseDrop` | -- | | `(originalPhaseData: KupPlannerTask, originalTaskData: KupPlannerTask, finalPhaseData: KupPlannerTask, destinationData: KupPlannerTask) => boolean \| void \| Promise \| Promise` | `undefined` | -| `progressChange` | -- | | `(task: KupPlannerTask, children: KupPlannerTask[]) => boolean \| void \| Promise \| Promise` | `undefined` | -| `projection` | -- | | `{ x0: number; xf: number; color: string; }` | `undefined` | -| `readOnly` | `read-only` | | `boolean` | `false` | -| `rowHeight` | `row-height` | | `number` | `0` | -| `rtl` | `rtl` | | `boolean` | `false` | -| `selectedTask` | -- | | `KupPlannerBarTask` | `undefined` | -| `setFailedTask` | -- | | `(value: KupPlannerBarTask) => void` | `undefined` | -| `setGanttEvent` | -- | | `(gantt: KupPlannerGanttEvent) => void` | `undefined` | -| `setSelectedTask` | -- | | `(taskId: string) => void` | `undefined` | -| `showSecondaryDates` | `show-secondary-dates` | | `boolean` | `false` | -| `taskHeight` | `task-height` | | `number` | `0` | -| `tasks` | -- | | `KupPlannerBarTask[]` | `undefined` | -| `timeStep` | `time-step` | | `number` | `0` | +| Property | Attribute | Description | Type | Default | +| ---------------------- | ---------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | +| `arrowColor` | `arrow-color` | | `string` | `''` | +| `arrowIndent` | `arrow-indent` | | `number` | `0` | +| `barClick` | -- | | `(task: KupPlannerTask) => void` | `undefined` | +| `barContextMenu` | -- | | `(event: UIEvent, task: KupPlannerTask) => void` | `undefined` | +| `barDblClick` | -- | | `(task: KupPlannerTask) => void` | `undefined` | +| `columnWidth` | `column-width` | | `number` | `0` | +| `currentDateIndicator` | -- | | `KupPlannerCurrentDateIndicator` | `undefined` | +| `dateChange` | -- | | `(task: KupPlannerTask, children: KupPlannerTask[]) => boolean \| void \| Promise \| Promise` | `undefined` | +| `dates` | -- | | `Date[]` | `undefined` | +| `delete` | -- | | `(task: KupPlannerTask) => boolean \| void \| Promise \| Promise` | `undefined` | +| `dependencies` | -- | | `KupPlannerDependency[]` | `[]` | +| `doubleClick` | -- | | `(task: KupPlannerTask) => void` | `undefined` | +| `eMouseDown` | -- | | `(event: MouseEvent) => void` | `undefined` | +| `eventStart` | -- | | `(action: KupPlannerGanttContentMoveAction, selectedTask: KupPlannerBarTask, event?: KeyboardEvent \| MouseEvent) => any` | `undefined` | +| `fontFamily` | `font-family` | | `string` | `''` | +| `fontSize` | `font-size` | | `string` | `''` | +| `ganttEvent` | -- | | `{ changedTask?: KupPlannerBarTask; originalSelectedTask?: KupPlannerBarTask; action: KupPlannerGanttContentMoveAction; }` | `undefined` | +| `gridProps` | -- | | `{ tasks: KupPlannerTask[]; dates: Date[]; svgWidth: number; rowHeight: number; columnWidth: number; todayColor: string; rtl: boolean; }` | `undefined` | +| `hideLabel` | `hide-label` | | `boolean` | `false` | +| `phaseDragScroll` | -- | | `(scrollY: number) => void` | `undefined` | +| `phaseDrop` | -- | | `(originalPhaseData: KupPlannerTask, originalTaskData: KupPlannerTask, finalPhaseData: KupPlannerTask, destinationData: KupPlannerTask) => boolean \| void \| Promise \| Promise` | `undefined` | +| `progressChange` | -- | | `(task: KupPlannerTask, children: KupPlannerTask[]) => boolean \| void \| Promise \| Promise` | `undefined` | +| `projection` | -- | | `{ x0: number; xf: number; color: string; }` | `undefined` | +| `readOnly` | `read-only` | | `boolean` | `false` | +| `rowHeight` | `row-height` | | `number` | `0` | +| `rtl` | `rtl` | | `boolean` | `false` | +| `selectedTask` | -- | | `KupPlannerBarTask` | `undefined` | +| `setFailedTask` | -- | | `(value: KupPlannerBarTask) => void` | `undefined` | +| `setGanttEvent` | -- | | `(gantt: KupPlannerGanttEvent) => void` | `undefined` | +| `setSelectedTask` | -- | | `(taskId: string) => void` | `undefined` | +| `showSecondaryDates` | `show-secondary-dates` | | `boolean` | `false` | +| `taskHeight` | `task-height` | | `number` | `0` | +| `tasks` | -- | | `KupPlannerBarTask[]` | `undefined` | +| `timeStep` | `time-step` | | `number` | `0` | ## Dependencies diff --git a/packages/ketchup/src/components/kup-planner/utils/kup-task-gantt/readme.md b/packages/ketchup/src/components/kup-planner/utils/kup-task-gantt/readme.md index 0f326e0dd1..1c6bf908c6 100644 --- a/packages/ketchup/src/components/kup-planner/utils/kup-task-gantt/readme.md +++ b/packages/ketchup/src/components/kup-planner/utils/kup-task-gantt/readme.md @@ -7,16 +7,16 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ----------------- | -------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -| `barProps` | -- | | `{ tasks: KupPlannerBarTask[]; dates: Date[]; ganttEvent: KupPlannerGanttEvent; selectedTask: KupPlannerBarTask; rowHeight: number; columnWidth: number; timeStep: number; svg?: SVGSVGElement; svgWidth: number; taskHeight: number; arrowColor: string; arrowIndent: number; fontSize: string; fontFamily: string; rtl: boolean; ganttHeight: number; hideLabel?: boolean; showSecondaryDates?: boolean; currentDateIndicator?: KupPlannerCurrentDateIndicator; projection?: { x0: number; xf: number; color: string; }; readOnly: boolean; setGanttEvent: (value: KupPlannerGanttEvent) => void; setFailedTask: (value: KupPlannerBarTask) => void; setSelectedTask: (taskId: string) => void; } & KupPlannerEventOption` | `undefined` | -| `calendarProps` | -- | | `{ dateSetup: KupPlannerDateSetup; locale: string; viewMode: KupPlannerViewMode; rtl: boolean; headerHeight: number; columnWidth: number; fontFamily: string; fontSize: string; dateTimeFormatters?: KupPlannerDateTimeFormatters; singleLineHeader: boolean; currentDateIndicator?: KupPlannerCurrentDateIndicator; }` | `undefined` | -| `ganttHeight` | `gantt-height` | | `number` | `undefined` | -| `gridProps` | -- | | `{ tasks: KupPlannerTask[]; dates: Date[]; svgWidth: number; rowHeight: number; columnWidth: number; todayColor: string; rtl: boolean; }` | `undefined` | -| `phaseDragScroll` | -- | | `(scrollY: number) => void` | `undefined` | -| `scrollX` | `scroll-x` | | `number` | `undefined` | -| `scrollY` | `scroll-y` | | `number` | `undefined` | -| `taskGanttRef` | -- | | `HTMLDivElement` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ----------------- | -------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | +| `barProps` | -- | | `{ dependencies: KupPlannerDependency[]; tasks: KupPlannerBarTask[]; dates: Date[]; ganttEvent: KupPlannerGanttEvent; selectedTask: KupPlannerBarTask; rowHeight: number; columnWidth: number; timeStep: number; svg?: SVGSVGElement; svgWidth: number; taskHeight: number; arrowColor: string; arrowIndent: number; fontSize: string; fontFamily: string; rtl: boolean; ganttHeight: number; hideLabel?: boolean; showSecondaryDates?: boolean; currentDateIndicator?: KupPlannerCurrentDateIndicator; projection?: { x0: number; xf: number; color: string; }; readOnly: boolean; setGanttEvent: (value: KupPlannerGanttEvent) => void; setFailedTask: (value: KupPlannerBarTask) => void; setSelectedTask: (taskId: string) => void; } & KupPlannerEventOption` | `undefined` | +| `calendarProps` | -- | | `{ dateSetup: KupPlannerDateSetup; locale: string; viewMode: KupPlannerViewMode; rtl: boolean; headerHeight: number; columnWidth: number; fontFamily: string; fontSize: string; dateTimeFormatters?: KupPlannerDateTimeFormatters; singleLineHeader: boolean; currentDateIndicator?: KupPlannerCurrentDateIndicator; }` | `undefined` | +| `ganttHeight` | `gantt-height` | | `number` | `undefined` | +| `gridProps` | -- | | `{ tasks: KupPlannerTask[]; dates: Date[]; svgWidth: number; rowHeight: number; columnWidth: number; todayColor: string; rtl: boolean; }` | `undefined` | +| `phaseDragScroll` | -- | | `(scrollY: number) => void` | `undefined` | +| `scrollX` | `scroll-x` | | `number` | `undefined` | +| `scrollY` | `scroll-y` | | `number` | `undefined` | +| `taskGanttRef` | -- | | `HTMLDivElement` | `undefined` | ## Dependencies diff --git a/packages/ketchup/src/planner-example-7.html b/packages/ketchup/src/planner-example-7.html index 3097ed72df..5576629dea 100644 --- a/packages/ketchup/src/planner-example-7.html +++ b/packages/ketchup/src/planner-example-7.html @@ -43,4 +43,41 @@ > + From ada03183664800db57326c105cd89ad7faf8a1ae Mon Sep 17 00:00:00 2001 From: Luca Foscili <45429703+lucafoscili@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:25:00 +0200 Subject: [PATCH 3/3] feat: Use source task color for dependency arrows Connector arrows in the grid renderer now use the source task's background color if available, falling back to the arrowColor prop or a default grey. This improves visual association between tasks and their dependencies. --- .../kup-grid-renderer/kup-grid-renderer.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/kup-grid-renderer.tsx b/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/kup-grid-renderer.tsx index 7ab33ad3a5..c8cb46055b 100644 --- a/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/kup-grid-renderer.tsx +++ b/packages/ketchup/src/components/kup-planner/utils/kup-grid-renderer/kup-grid-renderer.tsx @@ -1073,10 +1073,27 @@ export class KupGridRenderer { this.arrowIndent ); + // pick a color for the connector: prefer the source task's color, + // then component arrowColor prop, and finally a neutral grey + const sourceColor = + (sourceTask && + sourceTask.styles && + sourceTask.styles.backgroundColor) || + this.arrowColor || + '#9e9e9e'; rendered.push( - - + + ); });