@osfarm/itineraire-technique 1.2.0 → 1.2.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,752 @@
1
+ /**
2
+ * TikaEditor - Main class for managing the technical itinerary editor
3
+ * Handles initialization, state management, and orchestration of the editor components
4
+ */
5
+ class TikaEditor {
6
+ constructor() {
7
+ this.DEFAULT_TITLE = "Nouvel itinéraire technique";
8
+
9
+ // Initialize the crops data structure
10
+ this.system = {
11
+ "title": this.DEFAULT_TITLE,
12
+ "options": {
13
+ "view": "horizontal",
14
+ "show_transcript": true,
15
+ "title_top_interventions": "Contrôle adventices",
16
+ "title_bottom_interventions": "Autres interventions",
17
+ "title_steps": "Étapes de la rotation dans la parcelle",
18
+ "region": "France"
19
+ },
20
+ "steps": []
21
+ };
22
+
23
+ this.selectedStep = null; // Current step being edited
24
+ this.editorLoader = null; // WikiLoader or ItineraLoader instance
25
+ this.InterventionTableManager = new InterventionTable(
26
+ this.system.options.title_top_interventions,
27
+ this.system.options.title_bottom_interventions,
28
+ this
29
+ );
30
+ }
31
+
32
+ /**
33
+ * Initialize the editor on page load
34
+ */
35
+ initialize() {
36
+ this.initializeOptions();
37
+ this.setupEventListeners();
38
+ this.setupDomReady();
39
+ }
40
+
41
+ /**
42
+ * Initialize editor options and UI components
43
+ */
44
+ initializeOptions() {
45
+ document.getElementById("title").textContent = this.system.title;
46
+
47
+ this.attachCropInputListeners();
48
+ this.enableTitleEditing();
49
+ this.setupCloseStepButtons();
50
+ this.setupCropFormKeydown();
51
+ this.setupCropsSortable();
52
+ this.setupParamsModal();
53
+ }
54
+
55
+ /**
56
+ * Setup close step buttons event handlers
57
+ */
58
+ setupCloseStepButtons() {
59
+ const self = this;
60
+ $('.close-step-button').click(function() {
61
+ // Set the current step to be fully edited now:
62
+ self.selectedStep.setAsEdited();
63
+ self.hideStepEditor();
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Setup form keydown prevention
69
+ */
70
+ setupCropFormKeydown() {
71
+ $(function() {
72
+ $('#cropForm').on('keydown', function(e) {
73
+ // Prevent form submit on Enter, but allow Enter in textarea
74
+ if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') {
75
+ e.preventDefault();
76
+ return false;
77
+ }
78
+ });
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Make crops container sortable with drag and drop
84
+ */
85
+ setupCropsSortable() {
86
+ const self = this;
87
+ $("#cropsContainer").sortable({
88
+ handle: '.drag-handle',
89
+ axis: 'y',
90
+ update: function (event, ui) {
91
+ // Set new order
92
+ let movedStepId = ui.item.data('id');
93
+ let lastStepEnd = null;
94
+
95
+ let newOrder = [];
96
+ $(event.target).children('.step-row').each(function () {
97
+ let id = $(this).data('id');
98
+ let step = self.system.steps.find(function (crop) { return crop.id == id });
99
+
100
+ if (movedStepId == id) {
101
+ let movedStep = new StepModel(step);
102
+ let duration = movedStep.getDurationInDays();
103
+
104
+ if (lastStepEnd != null) {
105
+ lastStepEnd.setDate(lastStepEnd.getDate() + 1);
106
+
107
+ movedStep.setStartDate(lastStepEnd);
108
+ movedStep.setDurationInDays(duration);
109
+ } else {
110
+ // This is the first step in the list, set its start date before the previously first step start date
111
+ let nextStep = self.system.steps[0];
112
+ let newStartDate = new Date(nextStep.startDate.valueOf());
113
+ newStartDate.setDate(newStartDate.getDate() - duration);
114
+ movedStep.setStartDate(newStartDate);
115
+ movedStep.setDurationInDays(duration);
116
+ }
117
+
118
+ step = movedStep.getStep();
119
+ }
120
+ else {
121
+ lastStepEnd = new Date(step.endDate.valueOf());
122
+ }
123
+
124
+ newOrder.push(step);
125
+ });
126
+
127
+ self.system.steps = newOrder;
128
+ self.refreshAllTables();
129
+ }
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Setup parameters modal event handlers
135
+ */
136
+ setupParamsModal() {
137
+ const self = this;
138
+
139
+ $('#modalParams').on('show.bs.modal', function (event) {
140
+ // Set the modal form inputs values from crops.options
141
+ $("#viewSelect").val(self.system.options.view);
142
+ $("#showTranscriptCheckbox").prop("checked", self.system.options.show_transcript);
143
+ $("#topInterventionsTitle").val(self.system.options.title_top_interventions);
144
+ $("#bottomInterventionsTitle").val(self.system.options.title_bottom_interventions);
145
+ $("#stepsTitle").val(self.system.options.title_steps);
146
+ $("#regionInput").val(self.system.options.region ?? "France");
147
+ $("#addressInput").val(self.system.options.address ?? "");
148
+ $("#latitudeInput").val(self.system.options.latitude ?? "");
149
+ $("#longitudeInput").val(self.system.options.longitude ?? "");
150
+
151
+ // Load ombrothermic data from climate_data if present
152
+ let hasClimateData = self.system.options.climate_data &&
153
+ self.system.options.climate_data.temperatures &&
154
+ self.system.options.climate_data.precipitations;
155
+
156
+ // Check the checkbox if show_climate_diagram is explicitly true OR if climate_data exists
157
+ let showDiagram = self.system.options.show_climate_diagram === true;
158
+ $("#ombroCheck").prop("checked", showDiagram);
159
+
160
+ if (hasClimateData) {
161
+ let tempLine = self.system.options.climate_data.temperatures.join(' ');
162
+ let precipLine = self.system.options.climate_data.precipitations.join(' ');
163
+ $("#ombroData").val(tempLine + '\n' + precipLine);
164
+ } else {
165
+ $("#ombroData").val("");
166
+ }
167
+
168
+ // Enable/disable textarea based on checkbox state
169
+ $("#ombroData").prop("disabled", !showDiagram);
170
+ });
171
+
172
+ // Add event listener to toggle textarea when checkbox changes
173
+ $("#ombroCheck").on("change", function() {
174
+ $("#ombroData").prop("disabled", !this.checked);
175
+ });
176
+
177
+ // Update Google Maps link when coordinates change
178
+ function updateGoogleMapsLink() {
179
+ const lat = $("#latitudeInput").val().trim();
180
+ const lon = $("#longitudeInput").val().trim();
181
+
182
+ if (lat && lon && !isNaN(parseFloat(lat)) && !isNaN(parseFloat(lon))) {
183
+ const mapsUrl = `https://www.google.com/maps?q=${lat},${lon}`;
184
+ $("#googleMapsLink a").attr("href", mapsUrl);
185
+ $("#googleMapsLink").show();
186
+ } else {
187
+ $("#googleMapsLink").hide();
188
+ }
189
+ }
190
+
191
+ // Attach listeners to lat/long inputs
192
+ $("#latitudeInput, #longitudeInput").on("input change", updateGoogleMapsLink);
193
+
194
+ // Update link when modal opens
195
+ $('#modalParams').on('shown.bs.modal', function() {
196
+ updateGoogleMapsLink();
197
+ });
198
+
199
+ // Location search button handler
200
+ $("#searchLocationBtn").on("click", function() {
201
+ const address = $("#addressInput").val().trim();
202
+
203
+ if (!address) {
204
+ $("#locationSearchStatus").html('<span class="text-warning">Veuillez entrer une adresse</span>');
205
+ return;
206
+ }
207
+
208
+ // Show loading state
209
+ $("#searchLocationBtn").prop("disabled", true);
210
+ $("#locationSearchStatus").html('<i class="fa fa-spinner fa-spin"></i> Recherche en cours...');
211
+
212
+ $.ajax({
213
+ url: "https://itk-info.tripleperformance.fr/api/location",
214
+ method: "POST",
215
+ contentType: "application/json",
216
+ data: JSON.stringify({ address: address }),
217
+ success: function(data) {
218
+ console.log("Location data received:", data);
219
+
220
+ // Populate latitude and longitude
221
+ if (data.latitude && data.longitude) {
222
+ $("#latitudeInput").val(data.latitude);
223
+ $("#longitudeInput").val(data.longitude);
224
+ updateGoogleMapsLink();
225
+ }
226
+
227
+ // Populate climate data if available
228
+ if (data.monthly_temperatures && data.monthly_rainfall) {
229
+ let tempLine = data.monthly_temperatures.join(' ');
230
+ let precipLine = data.monthly_rainfall.join(' ');
231
+ $("#ombroData").val(tempLine + '\n' + precipLine);
232
+ $("#ombroCheck").prop("checked", true);
233
+ $("#ombroData").prop("disabled", false);
234
+ }
235
+
236
+ // Show success message
237
+ let message = '<span class="text-success">✓ Coordonnées trouvées';
238
+ if (data.source_explanation) {
239
+ message += ' — ' + data.source_explanation;
240
+ }
241
+ message += '</span>';
242
+ $("#locationSearchStatus").html(message);
243
+ },
244
+ error: function(err) {
245
+ console.error("Location search error:", err);
246
+ $("#locationSearchStatus").html('<span class="text-danger">Erreur lors de la recherche</span>');
247
+ },
248
+ complete: function() {
249
+ $("#searchLocationBtn").prop("disabled", false);
250
+ }
251
+ });
252
+ });
253
+
254
+ $("#paramsModalSaveButton").click(function () {
255
+ self.system.options.view = $("#viewSelect").val();
256
+ self.system.options.show_transcript = $("#showTranscriptCheckbox").prop("checked");
257
+ self.system.options.title_top_interventions = $("#topInterventionsTitle").val();
258
+ self.system.options.title_bottom_interventions = $("#bottomInterventionsTitle").val();
259
+ self.system.options.title_steps = $("#stepsTitle").val();
260
+ self.system.options.region = $("#regionInput").val();
261
+ self.system.options.address = $("#addressInput").val().trim();
262
+ self.system.options.latitude = $("#latitudeInput").val().trim();
263
+ self.system.options.longitude = $("#longitudeInput").val().trim();
264
+
265
+ // Convert ombrothermic data to climate_data object
266
+ let ombroEnabled = $("#ombroCheck").prop("checked");
267
+
268
+ if (ombroEnabled) {
269
+ self.system.options.show_climate_diagram = true;
270
+
271
+ let ombroText = $("#ombroData").val().trim();
272
+ let lines = ombroText.split('\n');
273
+
274
+ if (lines.length >= 2) {
275
+ let temperatures = lines[0].trim().split(/\s+/).map(v => parseFloat(v)).filter(v => !isNaN(v));
276
+ let precipitations = lines[1].trim().split(/\s+/).map(v => parseFloat(v)).filter(v => !isNaN(v));
277
+
278
+ if (temperatures.length > 0 && precipitations.length > 0) {
279
+ self.system.options.climate_data = {
280
+ temperatures: temperatures,
281
+ precipitations: precipitations
282
+ };
283
+ } else {
284
+ delete self.system.options.climate_data;
285
+ }
286
+ } else {
287
+ delete self.system.options.climate_data;
288
+ }
289
+ } else {
290
+ self.system.options.show_climate_diagram = false;
291
+ }
292
+
293
+ // Close the modal:
294
+ let modal = bootstrap.Modal.getInstance(document.getElementById('modalParams'));
295
+ modal.hide();
296
+
297
+ self.refreshAllTables();
298
+ });
299
+ }
300
+
301
+ /**
302
+ * Setup event listeners (to be called after DOM is ready)
303
+ */
304
+ setupEventListeners() {
305
+ // Will be populated when moving more code
306
+ }
307
+
308
+ /**
309
+ * Setup actions when DOM is ready
310
+ */
311
+ setupDomReady() {
312
+ const self = this;
313
+ $(document).ready(function() {
314
+ // If we are in a wiki (the domain contains "tripleperformance.ag or tripleperformance.fr" then show the Wiki buttons
315
+ if (window.location.hostname.includes("tripleperformance.ag") || window.location.hostname.includes("tripleperformance.fr")) {
316
+
317
+ // Hide NonWikiButtons
318
+ self.editorLoader = new WikiLoader(self);
319
+ self.editorLoader.setupButtons();
320
+ self.editorLoader.loadPageFromURL();
321
+
322
+ } else if (window.location.hostname.includes("itinera.ag") ||
323
+ (window.location.hostname.includes("localhost") && window.location.search.includes("itinera")) ) {
324
+
325
+ // If we are in Itinera - the domain is *.itinera.ag or localhost with a itinera param
326
+ self.editorLoader = new ItineraLoader(self);
327
+ self.editorLoader.setupButtons();
328
+ self.editorLoader.loadPageFromURL();
329
+
330
+ } else {
331
+
332
+ self.editorLoader = new DefaultLoader(self); // Use DefaultLoader for NonWikiButtons mode
333
+ self.editorLoader.setupButtons();
334
+ }
335
+ });
336
+ }
337
+
338
+ /**
339
+ * Update color of the selected step
340
+ */
341
+ updateSelectedStepColor() {
342
+ this.selectedStep.step.useDefaultColor = false;
343
+ this.selectedStep.updateFromForm();
344
+ this.refreshAllTables();
345
+ }
346
+
347
+ /**
348
+ * Update start date of the selected step
349
+ */
350
+ updateSelectedStepStartDate() {
351
+ this.selectedStep.step.useDefaultStartDate = false;
352
+ this.selectedStep.updateFromForm();
353
+ this.refreshAllTables();
354
+ }
355
+
356
+ /**
357
+ * Update end date of the selected step
358
+ */
359
+ updateSelectedStepEndDate() {
360
+ this.selectedStep.step.useDefaultEndDate = false;
361
+ this.selectedStep.updateFromForm();
362
+ this.refreshAllTables();
363
+ }
364
+
365
+ /**
366
+ * Update the selected step from form values
367
+ */
368
+ updateSelectedStep() {
369
+ this.selectedStep.updateFromForm();
370
+ this.refreshAllTables();
371
+ }
372
+
373
+ /**
374
+ * Set value to an input element
375
+ */
376
+ setInputValue(elementId, value) {
377
+ document.getElementById(elementId).value = value;
378
+ }
379
+
380
+ /**
381
+ * Refresh all tables and views
382
+ */
383
+ refreshAllTables() {
384
+ this.refreshStepsButtonList();
385
+ this.renderChart();
386
+ }
387
+
388
+ /**
389
+ * Display crop detail view
390
+ */
391
+ showStepEditor() {
392
+ $('#cropEditorView').show();
393
+ $('#welcomeView').hide();
394
+ $('#cropListView').hide();
395
+ }
396
+
397
+ /**
398
+ * Display crop list view
399
+ */
400
+ hideStepEditor() {
401
+ $('#cropListView').show();
402
+ $('#welcomeView').hide();
403
+ $('#cropEditorView').hide();
404
+ }
405
+
406
+ /**
407
+ * Render the chart
408
+ */
409
+ renderChart() {
410
+ const self = this;
411
+ let renderer = new RotationRenderer('itk', this.system);
412
+ renderer.render();
413
+
414
+ $('.step-edit').click(function (event) {
415
+ event.stopPropagation();
416
+ let stepId = renderer.getElementID(event.target.closest('.rotation_item'));
417
+
418
+ let index = stepId.split('_')[1];
419
+ self.selectedStep = new StepModel(self.system.steps[index]);
420
+ self.selectStep(self.selectedStep);
421
+ });
422
+ }
423
+
424
+ /**
425
+ * Get the end date of the rotation (last step end date + 1 day)
426
+ */
427
+ getRotationEndDate() {
428
+ let latestEndDate = null;
429
+ this.system.steps.forEach(function (crop) {
430
+ if (latestEndDate == null || crop.endDate > latestEndDate) {
431
+ latestEndDate = crop.endDate;
432
+ }
433
+ });
434
+
435
+ // Clone the date to avoid reference issues
436
+ if (latestEndDate == null)
437
+ latestEndDate = new Date();
438
+ else {
439
+ latestEndDate = new Date(latestEndDate.valueOf());
440
+
441
+ // Move to the next day
442
+ latestEndDate.setDate(latestEndDate.getDate() + 1);
443
+ }
444
+
445
+ return latestEndDate;
446
+ }
447
+
448
+ /**
449
+ * Reload crops data from JSON
450
+ */
451
+ reloadCropsFromJson(cropsFromJson) {
452
+ console.log("Données JSON importées :", cropsFromJson);
453
+
454
+ this.system = cropsFromJson;
455
+
456
+ this.initializeOptions();
457
+ this.refreshAllTables();
458
+ this.hideStepEditor();
459
+ }
460
+
461
+ /**
462
+ * Get crop information from API based on crop name
463
+ */
464
+ getChatGPTInfoFromCropName() {
465
+ const self = this;
466
+
467
+ if (this.selectedStep.step.useDefaultColor == false &&
468
+ this.selectedStep.step.useDefaultStartDate == false &&
469
+ this.selectedStep.step.useDefaultEndDate == false) {
470
+ // If the user has modified all fields, do not call the API again
471
+ $('#itk-api-comment').text('');
472
+ console.log("All fields modified by user, skipping API call");
473
+ return;
474
+ }
475
+
476
+ let newCropName = $("#cropName").val();
477
+
478
+ $('#itk-api-comment').html('<i class="fa fa-spinner fa-spin"></i> ...');
479
+
480
+ $.ajax({
481
+ url: "https://itk-info.tripleperformance.fr/api/culture",
482
+ method: "POST",
483
+ contentType: "application/json",
484
+ data: JSON.stringify({
485
+ culture: newCropName,
486
+ region: self.system.options.region ?? "France"
487
+ }),
488
+ success: function(data) {
489
+ console.log("Réponse:", data);
490
+
491
+ if (data.color_hex && self.selectedStep.step.useDefaultColor) {
492
+ self.setInputValue("cropColor", data.color_hex);
493
+ self.updateSelectedStep();
494
+ }
495
+
496
+ let startDate = self.getRotationEndDate();
497
+ if (data.average_sowing_date && self.selectedStep.step.useDefaultStartDate) {
498
+ // data.average_sowing_date is in MM-DD format, we need to convert it to YYYY-MM-DD
499
+ // for the current year
500
+ let currentYear = startDate.getFullYear();
501
+ startDate = new Date(`${currentYear}-${data.average_sowing_date}`);
502
+ // If the start date is before today, it means the crop starts next year
503
+ if (startDate < new Date()) {
504
+ startDate.setFullYear(startDate.getFullYear() + 1);
505
+ }
506
+ self.setInputValue("cropStartDate", startDate.toISOString().split('T')[0]);
507
+ self.updateSelectedStep();
508
+ }
509
+
510
+ if (data.end_of_season && self.selectedStep.step.useDefaultEndDate) {
511
+ // data.end_of_season is in MM-DD format, we need to convert it to YYYY-MM-DD
512
+ // for the current year
513
+ let currentYear = startDate.getFullYear();
514
+ let endDate = new Date(`${currentYear}-${data.end_of_season}`);
515
+ // If the end date is before the start date, it means the crop ends the next year
516
+ if (endDate < startDate) {
517
+ endDate.setFullYear(startDate.getFullYear() + 1);
518
+ }
519
+
520
+ self.setInputValue("cropEndDate", endDate.toISOString().split('T')[0]);
521
+ self.updateSelectedStep();
522
+ }
523
+
524
+ if (data.source_explanation) {
525
+ $('#itk-api-comment').text(data.source_explanation);
526
+ } else {
527
+ $('#itk-api-comment').text('');
528
+ }
529
+ },
530
+ error: function(err) {
531
+ console.error("Erreur:", err);
532
+ }
533
+ });
534
+ }
535
+
536
+ /**
537
+ * Attach input listeners to crop form fields
538
+ */
539
+ attachCropInputListeners() {
540
+ const self = this;
541
+ $("#cropName").on("input", _.debounce(function() { self.getChatGPTInfoFromCropName(); }, 1000));
542
+ $("#cropName").on("input", _.debounce(function() { self.updateSelectedStep(); }, 500));
543
+ $("#cropDescription").on("input", _.debounce(function() { self.updateSelectedStep(); }, 500));
544
+ $("#cropStartDate").on("change", _.debounce(function() { self.updateSelectedStepStartDate(); }, 500));
545
+ $("#cropEndDate").on("change", _.debounce(function() { self.updateSelectedStepEndDate(); }, 500));
546
+ $("#cropColor").on("input", _.debounce(function() { self.updateSelectedStepColor(); }, 500));
547
+ $("#cropSecondary").on("change", _.debounce(function() { self.updateSelectedStep(); }, 500));
548
+ }
549
+
550
+ /**
551
+ * Enable title editing functionality
552
+ */
553
+ enableTitleEditing() {
554
+ const self = this;
555
+ document.getElementById("title").addEventListener("blur", function () {
556
+ if (this.textContent.trim() === "") {
557
+ this.textContent = self.DEFAULT_TITLE;
558
+ }
559
+
560
+ self.system.title = this.textContent;
561
+ self.system.defaultTitle = false;
562
+ });
563
+ }
564
+
565
+ /**
566
+ * Add new step click event handler
567
+ */
568
+ addNewStepClickEvent() {
569
+ this.createAndSelectEmptyCrop();
570
+ this.loadSelectedStepToEditor(this.selectedStep);
571
+ this.showStepEditor();
572
+
573
+ this.InterventionTableManager.setupDiv();
574
+ this.InterventionTableManager.refreshInterventionsTable(this.selectedStep);
575
+
576
+ this.refreshAllTables();
577
+ }
578
+
579
+ /**
580
+ * Create and select an empty crop
581
+ */
582
+ createAndSelectEmptyCrop() {
583
+ let crop = new StepModel({
584
+ startDate: this.getRotationEndDate(),
585
+ });
586
+
587
+ crop.setDurationInMonths(2);
588
+ this.system.steps.push(crop.getStep());
589
+
590
+ // Select last created crop to be editable
591
+ this.selectedStep = crop;
592
+ }
593
+
594
+ /**
595
+ * Load selected step to editor form
596
+ */
597
+ loadSelectedStepToEditor(aStep) {
598
+ this.setInputValue("cropName", aStep.getStep().name);
599
+ this.setInputValue("cropColor", aStep.getStep().color);
600
+ this.setInputValue("cropStartDate", aStep.getStep().startDate.toISOString().split('T')[0]);
601
+ this.setInputValue("cropEndDate", aStep.getStep().endDate.toISOString().split('T')[0]);
602
+ this.setInputValue("cropDescription", aStep.getStep().description);
603
+ document.getElementById("cropSecondary").checked = aStep.getStep().secondary_crop || false;
604
+ }
605
+
606
+ /**
607
+ * Refresh the steps button list
608
+ */
609
+ refreshStepsButtonList() {
610
+ let cropsContainer = $("#cropsContainer");
611
+ cropsContainer.html('');
612
+
613
+ this.system.steps.forEach((crop) => {
614
+ const rowDiv = this.createCropRow(crop);
615
+ cropsContainer.append(rowDiv);
616
+ });
617
+
618
+ cropsContainer.sortable("refresh");
619
+ }
620
+
621
+ addEditAndRemoveButtons(rowDiv, deleteId, editFunction, deleteFunction, duplicateFunction, style="btn-group") {
622
+ rowDiv = $(rowDiv);
623
+
624
+ let actionContainer = $(`<div class="col-auto edit-buttons m-1 ${style}" role="group"></div>`);
625
+
626
+ rowDiv.append(actionContainer);
627
+
628
+ actionContainer.append($('<button class="edit-button btn btn-outline-primary p-2"><i class="fa fa-pencil"></i></button>').click(function(event) {
629
+ event.stopPropagation();
630
+ editFunction();
631
+ }));
632
+
633
+ rowDiv.find('.col').click(function(event) {
634
+ event.stopPropagation();
635
+ editFunction();
636
+ });
637
+
638
+ if (duplicateFunction != null) {
639
+ actionContainer.append($('<button class="btn btn-outline-secondary p-2"><i class="fa fa-copy"></i></button>').click(function (event) {
640
+ event.stopPropagation();
641
+ duplicateFunction(deleteId);
642
+ }));
643
+ }
644
+
645
+ actionContainer.append($('<button class="btn btn-outline-danger p-2"><i class="fa fa-trash"></i></button>').click(function (event) {
646
+ event.stopPropagation();
647
+ deleteFunction(deleteId);
648
+ }));
649
+ }
650
+
651
+ /**
652
+ * Create a crop row element
653
+ */
654
+ createCropRow(crop) {
655
+ const self = this;
656
+ let step = new StepModel(crop); // in case crop is a plain object, convert to StepModel instance
657
+
658
+ let rowDiv = $('<div class="row mb-2 step-row editable-row position-relative" data-id="'+step.getStep().id +'"></div>');
659
+
660
+ rowDiv.append($('<div class="col"></div>')
661
+ .append($('<i class="fa fa-bars drag-handle" aria-hidden="true"></i>'))
662
+ .append($('<strong>' + step.getStep().name + '</strong>')));
663
+
664
+ this.addEditAndRemoveButtons(rowDiv,
665
+ step.getStep().id,
666
+ function () {
667
+ console.log("Selected step:", step.getStep().name);
668
+ self.selectStep(step);
669
+ },
670
+ function(id) {
671
+ self.system.steps = self.system.steps.filter(function (crop) { return crop.id != id });
672
+ self.refreshAllTables();
673
+ self.hideStepEditor();
674
+ },
675
+ function(id) {
676
+ self.duplicateStep(id);
677
+ });
678
+
679
+ rowDiv.click();
680
+
681
+ return rowDiv;
682
+ }
683
+
684
+ /**
685
+ * Duplicate a step
686
+ */
687
+ duplicateStep(stepId) {
688
+ // Find the step to duplicate
689
+ let originalStep = this.system.steps.find(crop => crop.id === stepId);
690
+ if (!originalStep) return;
691
+
692
+ // Get the latest end date in the rotation
693
+ let latestEndDate = this.getRotationEndDate();
694
+
695
+ // Calculate the duration of the original step
696
+ let originalStart = new Date(originalStep.startDate);
697
+ let originalEnd = new Date(originalStep.endDate);
698
+
699
+ // Calculate how many years to add to position after the latest step
700
+ let yearsToAdd = 0;
701
+ let newStartDate = new Date(originalStart);
702
+ let newEndDate = new Date(originalEnd);
703
+
704
+ // Keep adding years until the new start date is after the latest end date
705
+ while (newStartDate < latestEndDate) {
706
+ yearsToAdd++;
707
+ newStartDate = new Date(originalStart);
708
+ newStartDate.setFullYear(originalStart.getFullYear() + yearsToAdd);
709
+ newEndDate.setFullYear(originalEnd.getFullYear() + yearsToAdd);
710
+ }
711
+
712
+ // Create the new step with all properties cloned
713
+ let newStep = {
714
+ name: originalStep.name,
715
+ color: originalStep.color,
716
+ startDate: newStartDate,
717
+ endDate: newEndDate,
718
+ description: originalStep.description,
719
+ secondary_crop: originalStep.secondary_crop || false,
720
+ useDefaultColor: originalStep.useDefaultColor,
721
+ useDefaultStartDate: originalStep.useDefaultStartDate,
722
+ useDefaultEndDate: originalStep.useDefaultEndDate,
723
+ interventions: originalStep.interventions ? originalStep.interventions.map(i => ({
724
+ day: i.day,
725
+ name: i.name,
726
+ type: i.type,
727
+ description: i.description
728
+ })) : []
729
+ };
730
+
731
+ // Create a StepModel instance to ensure proper initialization
732
+ let stepModel = new StepModel(newStep);
733
+
734
+ // Add the duplicated step to the rotation
735
+ this.system.steps.push(stepModel.getStep());
736
+
737
+ // Refresh the UI
738
+ this.refreshAllTables();
739
+ }
740
+
741
+ /**
742
+ * Select a step for editing
743
+ */
744
+ selectStep(step) {
745
+ this.selectedStep = step;
746
+ this.loadSelectedStepToEditor(this.selectedStep);
747
+ this.showStepEditor();
748
+
749
+ this.InterventionTableManager.setupDiv();
750
+ this.InterventionTableManager.refreshInterventionsTable(this.selectedStep);
751
+ }
752
+ }