@farm-investimentos/front-mfe-components 15.12.1 → 15.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,561 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import GanttChart from '../GanttChart.vue';
3
+
4
+ describe('GanttChart component', () => {
5
+ let wrapper;
6
+ let component;
7
+
8
+ const defaultProps = {
9
+ data: {
10
+ groups: [
11
+ {
12
+ title: 'Test Group',
13
+ bars: [
14
+ {
15
+ id: 1,
16
+ label: 'Test Bar',
17
+ start: new Date(2025, 0, 1),
18
+ end: new Date(2025, 1, 1),
19
+ color: '#7BC4F7',
20
+ },
21
+ ],
22
+ },
23
+ ],
24
+ },
25
+ };
26
+
27
+ beforeEach(() => {
28
+ wrapper = shallowMount(GanttChart, {
29
+ propsData: defaultProps,
30
+ });
31
+ component = wrapper.vm;
32
+ });
33
+
34
+ test('Created hook', () => {
35
+ expect(wrapper).toBeDefined();
36
+ });
37
+
38
+ describe('mount component', () => {
39
+ it('renders correctly', () => {
40
+ expect(wrapper.element).toMatchSnapshot();
41
+ });
42
+
43
+ it('renders groups correctly', () => {
44
+ expect(wrapper.findAll('.farm-gantt-chart__group')).toHaveLength(1);
45
+ });
46
+
47
+ it('renders month headers', () => {
48
+ const monthHeaders = wrapper.findAll('.farm-gantt-chart__month-header');
49
+ expect(monthHeaders.length).toBeGreaterThan(0);
50
+ });
51
+
52
+ it('displays group titles correctly', () => {
53
+ const groupLabel = wrapper.find('.farm-gantt-chart__group-label');
54
+ expect(groupLabel.text()).toContain('Test Group');
55
+ });
56
+ });
57
+
58
+ describe('Props', () => {
59
+ it('accepts data prop with new structure', () => {
60
+ expect(component.data).toEqual(defaultProps.data);
61
+ });
62
+
63
+ it('validates required data prop structure', () => {
64
+ expect(component.data.groups).toBeDefined();
65
+ expect(Array.isArray(component.data.groups)).toBe(true);
66
+ });
67
+
68
+ it('validates group structure', () => {
69
+ const group = component.data.groups[0];
70
+ expect(group.title).toBeDefined();
71
+ expect(group.bars).toBeDefined();
72
+ expect(Array.isArray(group.bars)).toBe(true);
73
+ });
74
+
75
+ it('validates bar structure', () => {
76
+ const bar = component.data.groups[0].bars[0];
77
+ expect(bar.id).toBeDefined();
78
+ expect(bar.label).toBeDefined();
79
+ expect(bar.start).toBeDefined();
80
+ expect(bar.end).toBeDefined();
81
+ expect(bar.color).toBeDefined();
82
+ });
83
+ });
84
+
85
+ describe('Automatic Calculations', () => {
86
+ describe('Date Range Calculation', () => {
87
+ it('should calculate date range automatically from bars', () => {
88
+ const testData = {
89
+ groups: [
90
+ {
91
+ title: 'Group 1',
92
+ bars: [
93
+ {
94
+ id: 1,
95
+ label: 'Early Bar',
96
+ start: new Date(2025, 0, 15), // Jan 15, 2025
97
+ end: new Date(2025, 2, 10), // Mar 10, 2025
98
+ color: '#7BC4F7',
99
+ },
100
+ {
101
+ id: 2,
102
+ label: 'Late Bar',
103
+ start: new Date(2025, 4, 5), // May 5, 2025
104
+ end: new Date(2025, 6, 20), // Jul 20, 2025
105
+ color: '#8BB455',
106
+ },
107
+ ],
108
+ },
109
+ ],
110
+ };
111
+
112
+ const testWrapper = shallowMount(GanttChart, {
113
+ propsData: { data: testData },
114
+ });
115
+
116
+ // Should calculate from January 1st to July 31st (full months)
117
+ const monthColumns = testWrapper.vm.monthColumns;
118
+ expect(monthColumns.length).toBe(7); // Jan to Jul = 7 months
119
+ expect(monthColumns[0].label).toContain('Jan');
120
+ expect(monthColumns[monthColumns.length - 1].label).toContain('Jul');
121
+ });
122
+
123
+ it('should handle single month range', () => {
124
+ const testData = {
125
+ groups: [
126
+ {
127
+ title: 'Single Month Group',
128
+ bars: [
129
+ {
130
+ id: 1,
131
+ label: 'Single Month Bar',
132
+ start: new Date(2025, 3, 5), // Apr 5, 2025
133
+ end: new Date(2025, 3, 25), // Apr 25, 2025
134
+ color: '#7BC4F7',
135
+ },
136
+ ],
137
+ },
138
+ ],
139
+ };
140
+
141
+ const testWrapper = shallowMount(GanttChart, {
142
+ propsData: { data: testData },
143
+ });
144
+
145
+ const monthColumns = testWrapper.vm.monthColumns;
146
+ expect(monthColumns.length).toBe(1);
147
+ expect(monthColumns[0].label).toContain('Abr');
148
+ });
149
+ });
150
+
151
+ describe('Legend Generation', () => {
152
+ it('should generate legend automatically from unique colors and labels', () => {
153
+ const testData = {
154
+ groups: [
155
+ {
156
+ title: 'Group 1',
157
+ bars: [
158
+ {
159
+ id: 1,
160
+ label: 'Design',
161
+ start: new Date(2025, 0, 1),
162
+ end: new Date(2025, 1, 1),
163
+ color: '#8E44AD',
164
+ },
165
+ {
166
+ id: 2,
167
+ label: 'Development',
168
+ start: new Date(2025, 1, 1),
169
+ end: new Date(2025, 2, 1),
170
+ color: '#16A085',
171
+ },
172
+ {
173
+ id: 3,
174
+ label: 'Design', // Same label, same color - should not duplicate
175
+ start: new Date(2025, 2, 1),
176
+ end: new Date(2025, 3, 1),
177
+ color: '#8E44AD',
178
+ },
179
+ ],
180
+ },
181
+ ],
182
+ };
183
+
184
+ const testWrapper = shallowMount(GanttChart, {
185
+ propsData: { data: testData },
186
+ });
187
+
188
+ const legend = testWrapper.vm.autoGeneratedLegend;
189
+ expect(legend).toHaveLength(2); // Only unique combinations
190
+ expect(legend.some(item => item.label === 'Design' && item.color === '#8E44AD')).toBe(true);
191
+ expect(legend.some(item => item.label === 'Development' && item.color === '#16A085')).toBe(true);
192
+ });
193
+
194
+ it('should display generated legend in template', () => {
195
+ const legendItems = wrapper.findAll('.farm-gantt-chart__legend-item');
196
+ expect(legendItems.length).toBeGreaterThan(0);
197
+ });
198
+ });
199
+ });
200
+
201
+ describe('Methods', () => {
202
+ describe('getPositionedBars', () => {
203
+ it('should position bars correctly with each bar in its own row', () => {
204
+ const bars = [
205
+ {
206
+ id: 1,
207
+ start: new Date(2025, 0, 1),
208
+ end: new Date(2025, 1, 1),
209
+ label: 'Bar 1',
210
+ color: '#7BC4F7',
211
+ },
212
+ {
213
+ id: 2,
214
+ start: new Date(2025, 0, 15),
215
+ end: new Date(2025, 1, 15),
216
+ label: 'Bar 2',
217
+ color: '#8BB455',
218
+ },
219
+ ];
220
+ const positionedBars = component.getPositionedBars(bars);
221
+ expect(positionedBars).toHaveLength(2);
222
+ // Each bar should have its own unique row position
223
+ expect(positionedBars[0].rowPosition).toBe(0);
224
+ expect(positionedBars[1].rowPosition).toBe(1);
225
+ });
226
+
227
+ it('should maintain original order and position bars in separate rows', () => {
228
+ const bars = [
229
+ {
230
+ id: 3,
231
+ start: new Date(2025, 2, 1), // Latest date but first in array
232
+ end: new Date(2025, 2, 15),
233
+ label: 'Latest Bar',
234
+ color: '#FFB84D',
235
+ },
236
+ {
237
+ id: 1,
238
+ start: new Date(2025, 0, 1), // Earliest date but second in array
239
+ end: new Date(2025, 0, 15),
240
+ label: 'Early Bar',
241
+ color: '#7BC4F7',
242
+ },
243
+ {
244
+ id: 2,
245
+ start: new Date(2025, 1, 1), // Middle date but third in array
246
+ end: new Date(2025, 1, 15),
247
+ label: 'Middle Bar',
248
+ color: '#8BB455',
249
+ },
250
+ ];
251
+ const positionedBars = component.getPositionedBars(bars);
252
+ expect(positionedBars).toHaveLength(3);
253
+ // Bars should maintain original order, not be sorted by date
254
+ expect(positionedBars[0].id).toBe(3); // Latest date stays first
255
+ expect(positionedBars[1].id).toBe(1); // Earliest date stays second
256
+ expect(positionedBars[2].id).toBe(2); // Middle date stays third
257
+ expect(positionedBars[0].rowPosition).toBe(0);
258
+ expect(positionedBars[1].rowPosition).toBe(1);
259
+ expect(positionedBars[2].rowPosition).toBe(2);
260
+ });
261
+
262
+ it('should handle empty bars array', () => {
263
+ const positionedBars = component.getPositionedBars([]);
264
+ expect(positionedBars).toEqual([]);
265
+ });
266
+
267
+ it('should handle invalid dates gracefully', () => {
268
+ const bars = [
269
+ {
270
+ id: 1,
271
+ start: 'invalid-date',
272
+ end: 'invalid-date',
273
+ label: 'Invalid Bar',
274
+ color: '#7BC4F7',
275
+ },
276
+ ];
277
+ const positionedBars = component.getPositionedBars(bars);
278
+ expect(positionedBars).toHaveLength(1);
279
+ expect(positionedBars[0].rowPosition).toBeDefined();
280
+ });
281
+ });
282
+
283
+ describe('getBarGridStyle', () => {
284
+ it('should return correct grid style for bars', () => {
285
+ const bar = {
286
+ id: 1,
287
+ start: new Date(2025, 0, 1),
288
+ end: new Date(2025, 1, 1),
289
+ label: 'Test Bar',
290
+ color: '#7BC4F7',
291
+ rowPosition: 0,
292
+ };
293
+ const style = component.getBarGridStyle(bar);
294
+ expect(style['background-color']).toBe('#7BC4F7');
295
+ expect(style['grid-row']).toBe('1');
296
+ expect(style['grid-column-start']).toBeDefined();
297
+ expect(style['grid-column-end']).toBeDefined();
298
+ });
299
+
300
+ });
301
+ });
302
+
303
+ describe('Color Fallback', () => {
304
+ it('should use fallback color when bar.color is not provided', () => {
305
+ const testData = {
306
+ groups: [{
307
+ title: 'Test Group',
308
+ bars: [{
309
+ id: 1,
310
+ label: 'Bar Without Color',
311
+ start: new Date(2025, 0, 1),
312
+ end: new Date(2025, 1, 1),
313
+ // color não definido - deve usar fallback
314
+ }]
315
+ }]
316
+ };
317
+
318
+ const testWrapper = shallowMount(GanttChart, {
319
+ propsData: { data: testData },
320
+ });
321
+
322
+ const bar = {
323
+ id: 1,
324
+ label: 'Bar Without Color',
325
+ start: new Date(2025, 0, 1),
326
+ end: new Date(2025, 1, 1),
327
+ rowPosition: 0
328
+ };
329
+
330
+ const style = testWrapper.vm.getBarGridStyle(bar);
331
+ expect(style['background-color']).toBe('var(--farm-primary-base)');
332
+ });
333
+
334
+ it('should use provided color when bar.color is defined', () => {
335
+ const bar = {
336
+ id: 1,
337
+ label: 'Bar With Color',
338
+ start: new Date(2025, 0, 1),
339
+ end: new Date(2025, 1, 1),
340
+ color: '#FF5733',
341
+ rowPosition: 0
342
+ };
343
+
344
+ const style = component.getBarGridStyle(bar);
345
+ expect(style['background-color']).toBe('#FF5733');
346
+ });
347
+ });
348
+
349
+ describe('Data Validation', () => {
350
+ it('should validate invalid data prop structure', () => {
351
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
352
+
353
+ // Teste com data null
354
+ const validator = wrapper.vm.$options.props.data.validator;
355
+ expect(validator(null)).toBe(false);
356
+ expect(consoleSpy).toHaveBeenCalledWith('GanttChart: prop "data" deve ser um objeto.');
357
+
358
+ // Teste com data sem groups
359
+ expect(validator({})).toBe(false);
360
+ expect(consoleSpy).toHaveBeenCalledWith('GanttChart: prop "data.groups" deve ser um array.');
361
+
362
+ // Teste com groups inválido
363
+ expect(validator({ groups: 'invalid' })).toBe(false);
364
+ expect(consoleSpy).toHaveBeenCalledWith('GanttChart: prop "data.groups" deve ser um array.');
365
+
366
+ consoleSpy.mockRestore();
367
+ });
368
+
369
+ it('should validate group structure', () => {
370
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
371
+ const validator = wrapper.vm.$options.props.data.validator;
372
+
373
+ // Teste com grupo sem title
374
+ const invalidData = {
375
+ groups: [{
376
+ bars: []
377
+ }]
378
+ };
379
+ expect(validator(invalidData)).toBe(false);
380
+ expect(consoleSpy).toHaveBeenCalledWith('GanttChart: cada grupo deve ter título (string) e barras (array).');
381
+
382
+ // Teste com grupo sem bars
383
+ const invalidData2 = {
384
+ groups: [{
385
+ title: 'Valid Title'
386
+ }]
387
+ };
388
+ expect(validator(invalidData2)).toBe(false);
389
+ expect(consoleSpy).toHaveBeenCalledWith('GanttChart: cada grupo deve ter título (string) e barras (array).');
390
+
391
+ consoleSpy.mockRestore();
392
+ });
393
+
394
+ it('should validate correct data structure', () => {
395
+ const validator = wrapper.vm.$options.props.data.validator;
396
+ const validData = {
397
+ groups: [{
398
+ title: 'Valid Group',
399
+ bars: []
400
+ }]
401
+ };
402
+ expect(validator(validData)).toBe(true);
403
+ });
404
+ });
405
+
406
+ describe('Date Handling', () => {
407
+ it('should correct dates when end is before start', () => {
408
+ const bar = {
409
+ id: 1,
410
+ label: 'Inverted Date Bar',
411
+ // Datas invertidas (end antes de start)
412
+ start: new Date(2025, 3, 15), // 15 de abril
413
+ end: new Date(2025, 2, 15), // 15 de março (antes do início)
414
+ color: '#7BC4F7',
415
+ };
416
+
417
+ // Normalizar barra via função de posicionamento
418
+ const dates = component.normalizeBarDates(bar);
419
+
420
+ // Verificar se as datas foram invertidas corretamente
421
+ expect(dates.startDate.getTime()).toBe(new Date(2025, 2, 15).getTime()); // Agora é 15 de março
422
+ expect(dates.endDate.getTime()).toBe(new Date(2025, 3, 15).getTime()); // Agora é 15 de abril
423
+ });
424
+
425
+ it('should handle invalid dates', () => {
426
+ const bar = {
427
+ id: 1,
428
+ label: 'Invalid Date Bar',
429
+ start: 'invalid-date',
430
+ end: 'invalid-date',
431
+ color: '#7BC4F7',
432
+ };
433
+
434
+ const dates = component.normalizeBarDates(bar);
435
+ expect(dates).toBeNull();
436
+ });
437
+ });
438
+
439
+ describe('Tooltip System', () => {
440
+ it('should show tooltip on bar mouseenter', async () => {
441
+ // Encontrar uma barra
442
+ const bar = wrapper.find('.farm-gantt-chart__bar');
443
+
444
+ if (bar.exists()) {
445
+ // Disparar evento mouseenter
446
+ await bar.trigger('mouseenter', {
447
+ clientX: 100,
448
+ clientY: 100,
449
+ });
450
+
451
+ // Verificar se tooltipState foi atualizado
452
+ expect(component.tooltipState.visible).toBe(true);
453
+ expect(component.tooltipState.title).toBe('Test Bar');
454
+ expect(component.tooltipState.barData).not.toBeNull();
455
+ }
456
+ });
457
+
458
+ it('should hide tooltip on bar mouseleave', async () => {
459
+ // Preparar estado (mostrar tooltip)
460
+ component.tooltipState.visible = true;
461
+ component.tooltipState.title = 'Teste';
462
+ component.tooltipState.barData = { label: 'Teste' };
463
+
464
+ // Encontrar e disparar mouseleave
465
+ const bar = wrapper.find('.farm-gantt-chart__bar');
466
+ if (bar.exists()) {
467
+ await bar.trigger('mouseleave');
468
+
469
+ // Verificar se tooltipState foi atualizado
470
+ expect(component.tooltipState.visible).toBe(false);
471
+ expect(component.tooltipState.barData).toBeNull();
472
+ }
473
+ });
474
+
475
+ it('should show structured tooltip when tooltipData is available', () => {
476
+ // Criar wrapper com dados para tooltip
477
+ const testData = {
478
+ groups: [{
479
+ title: 'Group with Tooltips',
480
+ bars: [{
481
+ id: 1,
482
+ label: 'Bar with Tooltip',
483
+ start: new Date(2025, 0, 1),
484
+ end: new Date(2025, 1, 1),
485
+ color: '#7BC4F7',
486
+ tooltipData: {
487
+ 'Taxa': '1,75%',
488
+ 'Vigência': '01/01/2025 a 31/01/2025'
489
+ }
490
+ }]
491
+ }]
492
+ };
493
+
494
+ const tooltipWrapper = shallowMount(GanttChart, {
495
+ propsData: { data: testData },
496
+ });
497
+
498
+ // Simular mouseenter
499
+ const barWithTooltip = tooltipWrapper.find('.farm-gantt-chart__bar');
500
+ if (barWithTooltip.exists()) {
501
+ barWithTooltip.trigger('mouseenter', {
502
+ clientX: 100,
503
+ clientY: 100
504
+ });
505
+
506
+ // Verificar que tooltip com dados estruturados será exibido
507
+ // quando tooltipState.barData.tooltipData existir
508
+ expect(tooltipWrapper.vm.tooltipState.barData.tooltipData).toBeDefined();
509
+ expect(tooltipWrapper.vm.tooltipState.barData.tooltipData.Taxa).toBe('1,75%');
510
+ }
511
+ });
512
+ });
513
+
514
+ describe('Empty Data Handling', () => {
515
+ it('should handle empty groups array gracefully', () => {
516
+ const emptyData = { groups: [] };
517
+ const emptyWrapper = shallowMount(GanttChart, {
518
+ propsData: { data: emptyData },
519
+ });
520
+
521
+ // Componente deve renderizar sem erros mesmo com dados vazios
522
+ expect(emptyWrapper.exists()).toBe(true);
523
+ expect(emptyWrapper.find('.farm-gantt-chart').exists()).toBe(true);
524
+
525
+ // Não deve haver grupos renderizados
526
+ const groups = emptyWrapper.findAll('.farm-gantt-chart__group');
527
+ expect(groups.length).toBe(0);
528
+ });
529
+
530
+ it('should render timeline headers even with empty data', () => {
531
+ const emptyData = { groups: [] };
532
+ const emptyWrapper = shallowMount(GanttChart, {
533
+ propsData: { data: emptyData },
534
+ });
535
+
536
+ // Timeline headers devem estar presentes (baseado em data range padrão ou calculado)
537
+ const header = emptyWrapper.find('.farm-gantt-chart__header');
538
+ expect(header.exists()).toBe(true);
539
+ });
540
+ });
541
+
542
+ describe('Computed Properties', () => {
543
+ it('should generate month columns correctly', () => {
544
+ expect(component.monthColumns).toBeDefined();
545
+ expect(Array.isArray(component.monthColumns)).toBe(true);
546
+ expect(component.monthColumns.length).toBeGreaterThan(0);
547
+ });
548
+
549
+ it('should generate timeline grid style', () => {
550
+ expect(component.timelineGridStyle).toBeDefined();
551
+ expect(component.timelineGridStyle.gridTemplateColumns).toBeDefined();
552
+ });
553
+
554
+ it('should calculate component style with height', () => {
555
+ expect(component.componentStyle).toBeDefined();
556
+ expect(component.componentStyle['--gantt-content-height']).toBeDefined();
557
+ });
558
+ });
559
+
560
+
561
+ });
@@ -0,0 +1,145 @@
1
+ import { isValid } from 'date-fns';
2
+ import type { GanttBar } from '../types';
3
+ import { getColumnForDate, getDaysInMonth } from '../utils/dateUtils';
4
+
5
+ export default function buildBarPositioning(dateRange, monthColumns) {
6
+ const normalizeBarDates = (bar: GanttBar) => {
7
+ let startDate = bar.start instanceof Date ? bar.start : new Date(bar.start);
8
+ let endDate = bar.end instanceof Date ? bar.end : new Date(bar.end);
9
+
10
+ if (!isValid(startDate) || !isValid(endDate)) {
11
+ return null;
12
+ }
13
+
14
+ if (endDate < startDate) {
15
+ const temp = startDate;
16
+ startDate = endDate;
17
+ endDate = temp;
18
+ }
19
+
20
+ return { startDate, endDate };
21
+ };
22
+
23
+ const calculateSingleMonthPositioning = (startDate: Date, endDate: Date) => {
24
+ const startDay = startDate.getDate();
25
+ const startMonth = startDate.getMonth();
26
+ const startYear = startDate.getFullYear();
27
+ const endDay = endDate.getDate();
28
+ const endMonth = endDate.getMonth();
29
+ const endYear = endDate.getFullYear();
30
+
31
+ const daysInMonth = getDaysInMonth(startYear, startMonth);
32
+ const startFraction = (startDay - 1) / daysInMonth;
33
+ const effectiveEndDay =
34
+ startYear === endYear && startMonth === endMonth ? endDay : daysInMonth;
35
+ const endFraction = effectiveEndDay / daysInMonth;
36
+
37
+ return {
38
+ marginLeft: `calc(${startFraction * 100}%)`,
39
+ width: `calc(${(endFraction - startFraction) * 100}%)`,
40
+ };
41
+ };
42
+
43
+ const calculateMultiMonthPositioning = (
44
+ startDate: Date,
45
+ endDate: Date,
46
+ visualMonthsSpanned: number,
47
+ gridColumnsSpanned: number
48
+ ) => {
49
+ const startDay = startDate.getDate();
50
+ const endDay = endDate.getDate();
51
+ const startMonth = startDate.getMonth();
52
+ const startYear = startDate.getFullYear();
53
+ const endMonth = endDate.getMonth();
54
+ const endYear = endDate.getFullYear();
55
+
56
+ const daysInStartMonth = getDaysInMonth(startYear, startMonth);
57
+ const daysInEndMonth = getDaysInMonth(endYear, endMonth);
58
+
59
+ const fractionBeforeBar = (startDay - 1) / daysInStartMonth;
60
+ const fractionInStartMonth = (daysInStartMonth - (startDay - 1)) / daysInStartMonth;
61
+ const fractionInEndMonth = endDay / daysInEndMonth;
62
+ const fullIntermediateMonths = Math.max(0, visualMonthsSpanned - 2);
63
+
64
+ if (gridColumnsSpanned <= 0) {
65
+ return { marginLeft: '0%', width: '100%' };
66
+ }
67
+
68
+ const totalBarCoverage = fractionInStartMonth + fullIntermediateMonths + fractionInEndMonth;
69
+
70
+ return {
71
+ marginLeft: `calc((${fractionBeforeBar} / ${gridColumnsSpanned}) * 100%)`,
72
+ width: `calc((${totalBarCoverage} / ${gridColumnsSpanned}) * 100%)`,
73
+ };
74
+ };
75
+
76
+ const getBarGridStyle = (bar: GanttBar) => {
77
+ const { start: chartStartDate } = dateRange.value;
78
+
79
+ const dates = normalizeBarDates(bar);
80
+ if (!dates) {
81
+ const backgroundColor = bar.color || 'var(--farm-primary-base)';
82
+ return {
83
+ gridColumn: '1 / 2',
84
+ backgroundColor: backgroundColor,
85
+ gridRow: `${(bar.rowPosition || 0) + 1}`,
86
+ };
87
+ }
88
+
89
+ const { startDate, endDate } = dates;
90
+
91
+ const startColumnIndex = getColumnForDate(startDate, chartStartDate);
92
+ const endColumnIndex = getColumnForDate(endDate, chartStartDate);
93
+
94
+ const gridColumnStart = Math.max(1, startColumnIndex + 1);
95
+ const gridColumnEnd = Math.min(monthColumns.value.length + 1, endColumnIndex + 2);
96
+ const gridColumnsSpanned = gridColumnEnd - gridColumnStart;
97
+
98
+ const visualStartCol = getColumnForDate(startDate, chartStartDate);
99
+ const visualEndCol = getColumnForDate(endDate, chartStartDate);
100
+ const visualMonthsSpanned = visualEndCol - visualStartCol + 1;
101
+
102
+ let positioning;
103
+ if (visualMonthsSpanned === 1) {
104
+ positioning = calculateSingleMonthPositioning(startDate, endDate);
105
+ } else {
106
+ positioning = calculateMultiMonthPositioning(
107
+ startDate,
108
+ endDate,
109
+ visualMonthsSpanned,
110
+ gridColumnsSpanned
111
+ );
112
+ }
113
+
114
+ const backgroundColor = bar.color || 'var(--farm-primary-base)';
115
+
116
+ return {
117
+ 'grid-column-start': gridColumnStart,
118
+ 'grid-column-end': gridColumnEnd,
119
+ 'background-color': backgroundColor,
120
+ 'grid-row': `${(bar.rowPosition || 0) + 1}`,
121
+ 'margin-left': positioning.marginLeft,
122
+ width: positioning.width,
123
+ };
124
+ };
125
+
126
+ const getPositionedBars = (bars: GanttBar[]) => {
127
+ if (!bars || bars.length === 0) return [];
128
+
129
+ const positionedBars = JSON.parse(JSON.stringify(bars));
130
+
131
+ positionedBars.forEach((bar: GanttBar, index: number) => {
132
+ bar.rowPosition = index;
133
+ });
134
+
135
+ return positionedBars;
136
+ };
137
+
138
+ return {
139
+ normalizeBarDates,
140
+ calculateSingleMonthPositioning,
141
+ calculateMultiMonthPositioning,
142
+ getBarGridStyle,
143
+ getPositionedBars,
144
+ };
145
+ }