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..6c1d416229 --- /dev/null +++ b/packages/ketchup/src/assets/planner-example-7.js @@ -0,0 +1,370 @@ +// 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); + +const props = { + data: { + columns: [ + { name: 'CODCOM' }, + { name: 'CODSEQ' }, + { name: 'CODFAS' }, + { name: 'DESFAS' }, + { name: 'COLFAS' }, + { name: 'INISIM' }, + { name: 'FINSIM' }, + { name: 'INICON' }, + { name: 'FINCON' }, + { name: 'OPEDIP' }, + ], + rows: [ + { + id: 'cm1', + cells: { + CODCOM: { value: 'CM1' }, + CODSEQ: { value: '001' }, + DESFAS: { value: 'COMMESSA 1' }, + INICON: { + value: '2025-03-01', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + FINCON: { + value: '2025-07-04', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + INISIM: { + value: '2025-09-01', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + FINSIM: { + value: '2025-12-05', + obj: { k: '', p: '*YYMD', t: 'D8' }, + }, + OPEDIP: { value: '' }, + }, + cssClass: 'clickable', + readOnly: true, + }, + ], + }, + taskIdCol: 'CODCOM', + taskNameCol: 'CODCOM', + taskDates: ['INICON', 'FINCON'], + taskPrevDates: ['INISIM', 'FINSIM'], + phaseIdCol: 'CODFAS', + phaseNameCol: 'DESFAS', + phaseDates: ['INICON', 'FINCON'], + phasePrevDates: ['INISIM', 'FINSIM'], + phaseColorCol: 'COLFAS', + dependencyCol: 'OPEDIP', + dependencies: [], +}; + +const phases = { + columns: [ + { name: 'CODCOM' }, + { name: 'CODSEQ' }, + { name: 'CODFAS' }, + { name: 'DESFAS' }, + { name: 'COLFAS' }, + { name: 'INISIM' }, + { name: 'FINSIM' }, + { name: 'INICON' }, + { name: 'FINCON' }, + { name: 'OPEDIP' }, + ], + rows: [ + { + 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: '' }, + }, + readOnly: true, + }, + { + 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' }, + }, + readOnly: true, + }, + { + 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' }, + }, + readOnly: true, + }, + { + id: '4', + cells: { + 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' }, + }, + readOnly: true, + }, + { + id: '5', + cells: { + 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' }, + }, + readOnly: true, + }, + ], +}; + +// 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); +} + +// 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 (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. +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 +} + +// 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 858bf453e6..c9230c63a9 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,15 @@ export namespace Components { * @default null */ "data": KupDataDataset; + /** + * 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 @@ -8544,6 +8555,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 +8737,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 +9240,15 @@ declare namespace LocalJSX { * @default null */ "data"?: KupDataDataset; + /** + * 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-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..1af17ab06d 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, @@ -326,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 @@ -389,6 +401,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 +713,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 +742,112 @@ 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 */ + } + + // 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; @@ -999,6 +1124,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 +1261,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 +1286,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 +1312,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/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-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..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 @@ -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,214 @@ 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 (source__target) + 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); + } + + // 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) + 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; + // 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 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; + 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 + ); + + // 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( + + + + + ); + }); + } + return rendered; + } + drownPathAndTriangle( taskFrom: KupPlannerBarTask, taskTo: KupPlannerBarTask, @@ -1081,11 +1293,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..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 @@ -19,6 +19,7 @@ | `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` | 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 (
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 new file mode 100644 index 0000000000..5576629dea --- /dev/null +++ b/packages/ketchup/src/planner-example-7.html @@ -0,0 +1,83 @@ + + + + + + + + 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' },