@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.
package/editor.html CHANGED
@@ -10,18 +10,20 @@
10
10
 
11
11
  <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
12
12
  <script src="https://cdn.jsdelivr.net/npm/jquery-ui@1.14.1/dist/jquery-ui.min.js"></script>
13
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-ui@1.14.1/themes/base/jquery-ui.css">
13
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-ui@1.14.1/themes/base/all.min.css">
14
14
 
15
15
  <script src="https://cdn.jsdelivr.net/npm/underscore@1.13.7/underscore-umd-min.js"></script>
16
16
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
17
17
  integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
18
18
 
19
+ <script src="./js/intervention.js"></script>
20
+ <script src="./js/step-model.js"></script>
19
21
  <script src="./js/chart-render.js"></script>
20
- <script src="./js/editor-attributes.js"></script>
21
22
  <script src="./js/editor-interventions.js"></script>
22
- <script src="./js/editor-crops.js"></script>
23
- <script src="./js/editor-export.js"></script>
24
- <script src="./js/editor-wiki-editor.js"></script>
23
+ <script src="./js/editor-loader-default.js"></script>
24
+ <script src="./js/editor-loader-wiki.js"></script>
25
+ <script src="./js/editor-loader-itinera.js"></script>
26
+ <script src="./js/editor-main.js"></script>
25
27
 
26
28
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
27
29
  integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
@@ -34,41 +36,8 @@
34
36
  <body>
35
37
  <div class="container-fluid">
36
38
  <div class="row">
37
- <div class="col-auto text-left main-header file-icons mb-2">
38
- <div id="WikiButtons" class="d-none">
39
- <div class="btn-group me-2" role="group">
40
- <button type="button" class="btn btn-outline-primary primary-button" onclick="loadFromWiki()"><i class="fa fa-upload" aria-hidden="true"></i> Charger une rotation</a></button>
41
- <button type="button" class="btn btn-outline-primary primary-button dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
42
- <span class="visually-hidden">Autres options de chargement</span>
43
- </button>
44
- <ul class="dropdown-menu">
45
- <li><a class="dropdown-item" href="#" onclick="importFromTestJson()"><i class="fa fa-lightbulb-o" aria-hidden="true"></i> Charger un exemple</a></li>
46
- <li><a class="dropdown-item" href="#" onclick="importFromJsonFile()"><i class="fa fa-upload" aria-hidden="true"></i> Importer (JSON)</a></li>
47
- </ul>
48
- </div><div class="btn-group me-2" role="group">
49
- <button type="button" onclick="saveToWiki()" class="btn btn-outline-primary primary-button"><i class="fa fa-download" aria-hidden="true"></i> Enregistrer dans le wiki</button>
50
- <button type="button" class="btn btn-outline-primary primary-button dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
51
- <span class="visually-hidden">Autres options de chargement</span>
52
- </button>
53
- <ul class="dropdown-menu">
54
- <li><a class="dropdown-item" href="#" onclick="showSaveAsModal()"><i class="fa fa-save" aria-hidden="true"></i> Enregistrer sous</a></li>
55
- <li><a class="dropdown-item" href="#" onclick="doExportToJsonFile()"><i class="fa fa-download" aria-hidden="true"></i> Exporter</a></li>
56
- </ul>
57
- </div>
58
- </div>
59
- <div id="NonWikiButtons" class="">
60
- <div class="btn-group me-2" role="group">
61
- <button type="button" class="btn btn-outline-primary primary-button" onclick="importFromJsonFile()"><i class="fa fa-upload" aria-hidden="true"></i> Charger une rotation</a></button>
62
- <button type="button" class="btn btn-outline-primary primary-button dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
63
- <span class="visually-hidden">Autres options de chargement</span>
64
- </button>
65
- <ul class="dropdown-menu">
66
- <li><a class="dropdown-item" href="#" onclick="importFromTestJson()"><i class="fa fa-lightbulb-o" aria-hidden="true"></i> Charger un exemple</a></li>
67
- </ul>
68
- </div><button type="button" onclick="doExportToJsonFile()" class="btn btn-outline-primary primary-button me-2"><i class="fa fa-download" aria-hidden="true"></i> Exporter (JSON)</button>
69
- </div>
70
-
71
- <button type="button" onclick="wipe(crops)" class="btn btn-outline-primary primary-button"><i class="fa fa-trash" aria-hidden="true"></i> Tout effacer</button>
39
+ <div class="col-auto text-left main-header file-icons mb-2" id="toolbar-buttons-container">
40
+ <!-- Wiki/Itinera/NonWiki buttons and wipe button will be inserted here dynamically -->
72
41
  </div>
73
42
  <div class="col text-center main-header mb-2">
74
43
  <h1 id="title" contenteditable="true" style="width: 100%">Itinéraire technique</h1>
@@ -100,7 +69,7 @@
100
69
 
101
70
  </div>
102
71
 
103
- <div class="d-none welcome-view container px-4" id="cropListView">
72
+ <div class="welcome-view container px-4" id="cropListView" style="display:none;">
104
73
  <div class="row">
105
74
  <div class="col-12">
106
75
  <p>Liste des étapes</p>
@@ -117,8 +86,8 @@
117
86
  </div>
118
87
  </div>
119
88
 
120
- <div class="d-none" id="cropDetailView">
121
- <div class="row card-holder mb-2">
89
+ <div id="cropEditorView" style="display:none">
90
+ <div id="cropDetailView" class="row card-holder mb-2">
122
91
  <div class="col-12">
123
92
  <button class="float-end close-step-button close-step-times" ><i class="fa fa-times" aria-hidden="true"></i></button>
124
93
  <h4>Étape</h4>
@@ -162,37 +131,9 @@
162
131
  </form>
163
132
  </div>
164
133
  </div>
165
- <div class="row card-holder attributes-view mb-2">
166
- <div class="col-12">
167
- <h5>Attributs</h5>
168
- <div class="row">
169
- <div id="attributesContainer" class="container"></div>
170
- </div>
171
- <button id="newAttributeButton" type="button" onclick="createAttributForm()"
172
- class="btn btn-primary primary-button w-100"><i class="fa fa-plus-square"
173
- aria-hidden="true"></i> Ajouter</button>
174
- <div id="newAttributeContainer"></div>
175
- </div>
176
- </div>
177
- <div class="row card-holder">
178
- <div class="col-12">
179
- <h5>Interventions</h5>
180
- <div class="row">
181
- <p id="interventionsTopName" class="h6"></p>
182
- <div id="interventionsTopContainer" class="container"></div>
183
- </div>
184
- <hr class="my-2">
185
- <div class="row">
186
- <p id="interventionsBottomName" class="h6"></p>
187
- <div id="interventionsBottomContainer" class="container"></div>
188
- </div>
189
134
 
190
- <button id="newInterventionButton" type="button" onclick="createInterventionForm()"
191
- class="btn btn-primary primary-button w-100"><i class="fa fa-plus-square"
192
- aria-hidden="true"></i> Ajouter</button>
193
- <div id="newInterventionContainer"></div>
194
- </div>
195
- </div>
135
+ <!-- Interventions table will be inserted here dynamically by InterventionTable.setupDiv() -->
136
+
196
137
  <div class="row">
197
138
  <div class="col-12">
198
139
  <button type="button"
@@ -248,6 +189,17 @@
248
189
  </div>
249
190
  </div>
250
191
  </div>
192
+
193
+ <div class="toast-container position-fixed bottom-0 end-0 p-3">
194
+ <div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
195
+ <div class="toast-header">
196
+ <strong class="me-auto">Itinéra</strong>
197
+ <small>11 mins ago</small>
198
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
199
+ </div>
200
+ <div class="toast-body"></div>
201
+ </div>
202
+ </div>
251
203
 
252
204
  <!-- Wiki Files Modal -->
253
205
  <div class="modal fade" id="wikiFilesModal" tabindex="-1" aria-labelledby="wikiFilesModalLabel" aria-hidden="true">
@@ -360,7 +312,7 @@
360
312
  <label for="stepsTitle" class="form-label">Titre des étapes de la rotation</label>
361
313
  <input type="text" class="form-control" id="stepsTitle" value="Étapes de la rotation dans la parcelle">
362
314
  </div>
363
- <div class="mb-3 d-none" id="codeSnippetDiv">
315
+ <div class="mb-3" id="codeSnippetDiv" style="display:none;">
364
316
  <label for="code-snippet" class="form-label">Code à insérer dans la page</label>
365
317
  <textarea readonly class="form-control-plaintext" id="code-snippet" rows="5"></textarea>
366
318
  </div>
@@ -418,686 +370,24 @@
418
370
  </div>
419
371
  </div>
420
372
  </div>
421
- </body>
422
-
423
- </html>
424
-
425
373
  <script>
426
-
427
- const DEFAULT_TITLE = "Nouvel itinéraire technique";
428
-
429
- let crops = {
430
- "title": DEFAULT_TITLE,
431
- "options": {
432
- "view": "horizontal",
433
- "show_transcript": true,
434
- "title_top_interventions": "Contrôle adventices",
435
- "title_bottom_interventions": "Autres interventions",
436
- "title_steps": "Étapes de la rotation dans la parcelle",
437
- "region": "France"
438
- },
439
- "steps": []
440
- };
441
-
442
- let selectedStep; // an instance of Crop
443
- let we; // WikiEditor instance
444
-
445
- initializeOptions();
446
-
447
- function initializeOptions() {
448
- document.getElementById("title").textContent = crops.title;
449
- document.getElementById("interventionsTopName").textContent = crops.options.title_top_interventions;
450
- document.getElementById("interventionsBottomName").textContent = crops.options.title_bottom_interventions;
451
-
452
- attachCropInputListeners();
453
- enableTitleEditing();
454
-
455
- $('.close-step-button').click(function() {
456
- // Set the current step to be fully edited now:
457
- selectedStep.setAsEdited();
458
-
459
- displayCropListView();
460
- });
461
-
462
- $(function() {
463
- $('#cropForm').on('keydown', function(e) {
464
- // Prevent form submit on Enter, but allow Enter in textarea
465
- if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') {
466
- e.preventDefault();
467
- return false;
468
- }
469
- });
470
- });
471
-
472
- // Make cropsContainer sortable
473
- $("#cropsContainer").sortable({
474
- handle: '.drag-handle',
475
- axis: 'y',
476
- update: function (event, ui) {
477
- // Set new order
478
- let movedStepId = ui.item.data('id');
479
- let lastStepEnd = null;
480
-
481
- let newOrder = [];
482
- $(event.target).children('.step-row').each(function () {
483
- let id = $(this).data('id');
484
-
485
- let step = crops.steps.find(function (crop) { return crop.id == id });
486
-
487
- if (movedStepId == id) {
488
- let movedStep = new StepModel(step);
489
- let duration = movedStep.getDurationInDays();
490
-
491
- if (lastStepEnd != null) {
492
- lastStepEnd.setDate(lastStepEnd.getDate() + 1);
493
-
494
- movedStep.setStartDate(lastStepEnd);
495
- movedStep.setDurationInDays(duration);
496
- } else {
497
- // This is the first step in the list, set its start date before the previously first step start date
498
- let nextStep = crops.steps[0];
499
- let newStartDate = new Date(nextStep.startDate.valueOf());
500
- newStartDate.setDate(newStartDate.getDate() - duration);
501
- movedStep.setStartDate(newStartDate);
502
- movedStep.setDurationInDays(duration);
503
- }
504
-
505
- step = movedStep.getStep();
506
- }
507
- else {
508
- lastStepEnd = new Date(step.endDate.valueOf());
509
- }
510
-
511
- newOrder.push(step);
512
- });
513
-
514
- crops.steps = newOrder;
515
-
516
- refreshAllTables();
517
- }
518
- });
519
-
520
-
521
- $('#modalParams').on('show.bs.modal', function (event) {
522
-
523
- // Set the modal form inputs values from crops.options
524
- $("#viewSelect").val(crops.options.view);
525
- $("#showTranscriptCheckbox").prop("checked", crops.options.show_transcript);
526
- $("#topInterventionsTitle").val(crops.options.title_top_interventions);
527
- $("#bottomInterventionsTitle").val(crops.options.title_bottom_interventions);
528
- $("#stepsTitle").val(crops.options.title_steps);
529
- $("#regionInput").val(crops.options.region ?? "France");
530
- $("#addressInput").val(crops.options.address ?? "");
531
- $("#latitudeInput").val(crops.options.latitude ?? "");
532
- $("#longitudeInput").val(crops.options.longitude ?? "");
533
-
534
- // Load ombrothermic data from climate_data if present
535
- let hasClimateData = crops.options.climate_data &&
536
- crops.options.climate_data.temperatures &&
537
- crops.options.climate_data.precipitations;
538
-
539
- // Check the checkbox if show_climate_diagram is explicitly true OR if climate_data exists
540
- let showDiagram = crops.options.show_climate_diagram === true;
541
- $("#ombroCheck").prop("checked", showDiagram);
542
-
543
- if (hasClimateData) {
544
- let tempLine = crops.options.climate_data.temperatures.join(' ');
545
- let precipLine = crops.options.climate_data.precipitations.join(' ');
546
- $("#ombroData").val(tempLine + '\n' + precipLine);
547
- } else {
548
- $("#ombroData").val("");
549
- }
550
-
551
- // Enable/disable textarea based on checkbox state
552
- $("#ombroData").prop("disabled", !showDiagram);
553
- });
554
-
555
- // Add event listener to toggle textarea when checkbox changes
556
- $("#ombroCheck").on("change", function() {
557
- $("#ombroData").prop("disabled", !this.checked);
558
- });
559
-
560
- // Update Google Maps link when coordinates change
561
- function updateGoogleMapsLink() {
562
- const lat = $("#latitudeInput").val().trim();
563
- const lon = $("#longitudeInput").val().trim();
564
-
565
- if (lat && lon && !isNaN(parseFloat(lat)) && !isNaN(parseFloat(lon))) {
566
- const mapsUrl = `https://www.google.com/maps?q=${lat},${lon}`;
567
- $("#googleMapsLink a").attr("href", mapsUrl);
568
- $("#googleMapsLink").show();
569
- } else {
570
- $("#googleMapsLink").hide();
571
- }
572
- }
573
-
574
- // Attach listeners to lat/long inputs
575
- $("#latitudeInput, #longitudeInput").on("input change", updateGoogleMapsLink);
576
-
577
- // Update link when modal opens
578
- $('#modalParams').on('shown.bs.modal', function() {
579
- updateGoogleMapsLink();
580
- });
581
-
582
- // Location search button handler
583
- $("#searchLocationBtn").on("click", function() {
584
- const address = $("#addressInput").val().trim();
585
-
586
- if (!address) {
587
- $("#locationSearchStatus").html('<span class="text-warning">Veuillez entrer une adresse</span>');
588
- return;
589
- }
590
-
591
- // Show loading state
592
- $("#searchLocationBtn").prop("disabled", true);
593
- $("#locationSearchStatus").html('<i class="fa fa-spinner fa-spin"></i> Recherche en cours...');
594
-
595
- $.ajax({
596
- url: "https://itk-info.tripleperformance.fr/api/location",
597
- method: "POST",
598
- contentType: "application/json",
599
- data: JSON.stringify({ address: address }),
600
- success: function(data) {
601
- console.log("Location data received:", data);
602
-
603
- // Populate latitude and longitude
604
- if (data.latitude && data.longitude) {
605
- $("#latitudeInput").val(data.latitude);
606
- $("#longitudeInput").val(data.longitude);
607
- updateGoogleMapsLink();
608
- }
609
-
610
- // Populate climate data if available
611
- if (data.monthly_temperatures && data.monthly_rainfall) {
612
- let tempLine = data.monthly_temperatures.join(' ');
613
- let precipLine = data.monthly_rainfall.join(' ');
614
- $("#ombroData").val(tempLine + '\n' + precipLine);
615
- $("#ombroCheck").prop("checked", true);
616
- $("#ombroData").prop("disabled", false);
617
- }
618
-
619
- // Show success message
620
- let message = '<span class="text-success">✓ Coordonnées trouvées';
621
- if (data.source_explanation) {
622
- message += ' — ' + data.source_explanation;
623
- }
624
- message += '</span>';
625
- $("#locationSearchStatus").html(message);
626
- },
627
- error: function(err) {
628
- console.error("Location search error:", err);
629
- $("#locationSearchStatus").html('<span class="text-danger">Erreur lors de la recherche</span>');
630
- },
631
- complete: function() {
632
- $("#searchLocationBtn").prop("disabled", false);
633
- }
634
- });
635
- });
636
-
637
- $("#paramsModalSaveButton").click(function () {
638
- crops.options.view = $("#viewSelect").val();
639
- crops.options.show_transcript = $("#showTranscriptCheckbox").prop("checked");
640
- crops.options.title_top_interventions = $("#topInterventionsTitle").val();
641
- crops.options.title_bottom_interventions = $("#bottomInterventionsTitle").val();
642
- crops.options.title_steps = $("#stepsTitle").val();
643
- crops.options.region = $("#regionInput").val();
644
- crops.options.address = $("#addressInput").val().trim();
645
- crops.options.latitude = $("#latitudeInput").val().trim();
646
- crops.options.longitude = $("#longitudeInput").val().trim();
647
-
648
- // Convert ombrothermic data to climate_data object
649
- let ombroEnabled = $("#ombroCheck").prop("checked");
650
-
651
- if (ombroEnabled) {
652
- crops.options.show_climate_diagram = true;
653
-
654
- let ombroText = $("#ombroData").val().trim();
655
- let lines = ombroText.split('\n');
656
-
657
- if (lines.length >= 2) {
658
- let temperatures = lines[0].trim().split(/\s+/).map(v => parseFloat(v)).filter(v => !isNaN(v));
659
- let precipitations = lines[1].trim().split(/\s+/).map(v => parseFloat(v)).filter(v => !isNaN(v));
660
-
661
- if (temperatures.length > 0 && precipitations.length > 0) {
662
- crops.options.climate_data = {
663
- temperatures: temperatures,
664
- precipitations: precipitations
665
- };
666
- } else {
667
- delete crops.options.climate_data;
668
- }
669
- } else {
670
- delete crops.options.climate_data;
671
- }
672
- } else {
673
- crops.options.show_climate_diagram = false;
674
- }
675
-
676
- // Close the modal:
677
- let modal = bootstrap.Modal.getInstance(document.getElementById('modalParams'));
678
- modal.hide();
679
-
680
- refreshAllTables();
681
- });
682
- }
683
-
684
- // On page load, create WikiEditor instance
374
+ // Initialize the TikaEditor
375
+ let tikaEditor;
376
+
377
+ // Initialize editor when DOM is ready
685
378
  $(document).ready(function() {
686
-
687
- // If we are in a wiki (the domain contains "tripleperformance.ag or tripleperformance.fr" then show the Wiki buttons
688
- if (window.location.hostname.includes("tripleperformance.ag") || window.location.hostname.includes("tripleperformance.fr")) {
689
- document.getElementById("WikiButtons").classList.remove("d-none");
690
- document.getElementById("NonWikiButtons").classList.add("d-none");
691
-
692
- we = new WikiEditor();
693
- we.loadPageFromURL();
694
-
695
- // Set up Save As modal event listeners
696
- $('#saveAsUseExistingPage').on('change', function() {
697
- $('#saveAsPageSelect').prop('disabled', !this.checked);
698
- });
699
-
700
- $('#saveAsConfirmBtn').on('click', function() {
701
- if (we) {
702
- we.saveAs();
703
- }
704
- });
705
- } else {
706
- document.getElementById("WikiButtons").classList.add("d-none");
707
- document.getElementById("NonWikiButtons").classList.remove("d-none");
708
- }
379
+ tikaEditor = new TikaEditor();
380
+ tikaEditor.initialize();
709
381
  });
710
382
 
711
- // Global wrapper function for loadFromWiki
712
- function loadFromWiki() {
713
- if (typeof we !== 'undefined') {
714
- showConfirmationModal(() => {
715
- we.loadFromWiki();
716
- });
717
- } else {
718
- console.error('WikiEditor instance not available');
719
- }
720
- }
721
-
722
- function saveToWiki() {
723
- if (typeof we !== 'undefined') {
724
- we.saveToWiki();
725
- } else {
726
- console.error('WikiEditor instance not available');
727
- }
728
- }
729
-
730
- function showSaveAsModal() {
731
- if (typeof we !== 'undefined') {
732
- we.showSaveAsModal();
733
- } else {
734
- console.error('WikiEditor instance not available');
383
+ function addNewStepClickEvent() {
384
+ if (tikaEditor) {
385
+ tikaEditor.addNewStepClickEvent();
735
386
  }
736
387
  }
737
388
 
738
- class StepModel {
739
- constructor(step) {
740
- this.step = step; // keep a reference to the original object
741
-
742
- this.step.id = step.id ?? crypto.randomUUID();
743
- this.step.name = step.name ?? "";
744
- this.step.color = step.color ?? "#0db3bf";
745
- this.step.startDate = step.startDate ? new Date(step.startDate) : new Date();
746
- this.step.endDate = step.endDate ? new Date(step.endDate) : new Date();
747
- this.step.description = step.description ?? "";
748
- this.step.interventions = step.interventions?.map(i => new Intervention(i.day, i.name, i.type, i.description)) || [];
749
- this.step.attributes = step.attributes?.map(a => new Attribute(a.name, a.value)) || [];
750
- this.step.secondary_crop = step.secondary_crop ?? false;
751
-
752
- this.step.useDefaultColor = step.useDefaultColor ?? true;
753
- this.step.useDefaultStartDate = step.useDefaultStartDate ?? true;
754
- this.step.useDefaultEndDate = step.useDefaultEndDate ?? true;
755
- }
756
-
757
- setAsEdited() {
758
- this.step.useDefaultColor = false;
759
- this.step.useDefaultStartDate = false;
760
- this.step.useDefaultEndDate = false;
761
- }
762
-
763
- getStep() {
764
- return this.step;
765
- }
766
-
767
- setStartDate(date) {
768
- this.step.startDate = new Date(date);
769
- }
770
-
771
- setDurationInMonths(durationInMonths) {
772
- this.step.endDate = new Date(this.step.startDate);
773
- this.step.endDate.setMonth(this.step.startDate.getMonth() + durationInMonths);
774
- }
775
-
776
- setDurationInDays(durationInDays) {
777
- this.step.endDate = new Date(this.step.startDate);
778
- this.step.endDate.setDate(this.step.startDate.getDate() + durationInDays);
779
- }
780
-
781
- getDurationInDays() {
782
- const diffTime = Math.abs(this.step.endDate - this.step.startDate);
783
- return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
784
- }
785
-
786
- addIntervention(day, name, type, description) {
787
- let intervention = new Intervention(day, name, type, description);
788
- this.step.interventions.push(intervention);
789
- }
790
-
791
- addAttribute(key, value) {
792
- let attribute = new Attribute(key, value);
793
- this.step.attributes.push(attribute);
794
- }
795
-
796
- updateAttribute(attributeId, key, value) {
797
- this.step.attributes = this.step.attributes.map(function (attribute) {
798
- if (attribute.id == attributeId) {
799
- attribute.name = key;
800
- attribute.value = value;
801
- }
802
- return attribute;
803
- });
804
- }
389
+ </script>
805
390
 
806
- removeAttribute(attributeId) {
807
- this.step.attributes = this.step.attributes.filter(function (attribute) { return attribute.id != attributeId });
808
- }
809
-
810
- updateIntervention(interventionId, day, name, type, description) {
811
- let intervention = this.step.interventions.find((it) => it.id == interventionId);
812
-
813
- if (intervention != undefined) {
814
- intervention.day = day;
815
- intervention.name = name;
816
- intervention.type = type;
817
- intervention.description = description;
818
- }
819
- }
820
-
821
- removeIntervention(interventionId) {
822
- this.step.interventions = this.step.interventions.filter(function (intervention) { return intervention.id != interventionId });
823
- }
824
-
825
- updateFromForm() {
826
- this.step.name = getInputValue("cropName");
827
- this.step.description = getInputValue("cropDescription");
828
- this.step.startDate = new Date(getInputValue("cropStartDate"));
829
- this.step.endDate = new Date(getInputValue("cropEndDate"));
830
- this.step.color = getInputValue("cropColor");
831
- this.step.secondary_crop = document.getElementById("cropSecondary").checked;
832
- }
833
- }
834
-
835
- class Intervention {
836
- constructor(day, name, type, description) {
837
- this.id = crypto.randomUUID();
838
- this.day = day;
839
- this.name = name || "";
840
- this.type = type;
841
- this.description = description || "";
842
- }
843
- }
844
-
845
- class Attribute {
846
- constructor(name, value) {
847
- this.id = crypto.randomUUID();
848
- this.name = name || "";
849
- this.value = value || "";
850
- }
851
- }
852
-
853
- function updateSelectedStepColor() {
854
- selectedStep.step.useDefaultColor = false;
855
-
856
- selectedStep.updateFromForm();
857
-
858
- refreshAllTables();
859
- }
860
-
861
- function updateSelectedStepStartDate() {
862
- selectedStep.step.useDefaultStartDate = false;
863
-
864
- selectedStep.updateFromForm();
865
-
866
- refreshAllTables();
867
- }
868
-
869
-
870
- function updateSelectedStepEndDate() {
871
- selectedStep.step.useDefaultEndDate = false;
872
-
873
- selectedStep.updateFromForm();
874
-
875
- refreshAllTables();
876
- }
877
-
878
- function updateSelectedStep() {
879
- selectedStep.updateFromForm();
880
-
881
- refreshAllTables();
882
- }
883
-
884
- function getInputValue(elementId) {
885
- return document.getElementById(elementId).value;
886
- }
887
-
888
- function setInputValue(elementId, value) {
889
- document.getElementById(elementId).value = value;
890
- }
891
-
892
- function refreshAllTables() {
893
- refreshStepsButtonList();
894
- refreshAttributesTable();
895
- refreshInterventionsTable();
896
- renderChart();
897
- }
898
-
899
- function displayView(viewsToShow, viewsToHide) {
900
- viewsToShow.forEach(view => document.querySelector(view).classList.remove("d-none"));
901
- viewsToHide.forEach(view => document.querySelector(view).classList.add("d-none"));
902
- }
903
-
904
- function displayCropDetailView() {
905
- displayView(["#cropDetailView"], ["#welcomeView", "#cropListView"]);
906
- }
907
-
908
- function displayCropListView() {
909
- displayView(["#cropListView"], ["#welcomeView", "#cropDetailView"]);
910
- }
911
-
912
- function renderChart() {
913
- let renderer = new RotationRenderer('itk', crops);
914
- renderer.render();
915
-
916
- $('.step-edit').click(function (event) {
917
- event.stopPropagation();
918
- let stepId = renderer.getElementID(event.target.closest('.rotation_item'));;
919
-
920
- let index = stepId.split('_')[1];
921
- selectedStep = new StepModel(crops.steps[index]);
922
- // populateCropDetailView(selectedStep);
923
- // displayCropDetailView();
924
- SelectStep(selectedStep);
925
- });
926
- }
927
-
928
- function getRotationEndDate() {
929
- let latestEndDate = null;
930
- crops.steps.forEach(function (crop) {
931
- if (latestEndDate == null || crop.endDate > latestEndDate) {
932
- latestEndDate = crop.endDate;
933
- }
934
- });
935
-
936
- // Clone the date to avoid reference issues
937
- if (latestEndDate == null)
938
- latestEndDate = new Date();
939
- else {
940
- latestEndDate = new Date(latestEndDate.valueOf());
941
-
942
- // Move to the next day
943
- latestEndDate.setDate(latestEndDate.getDate() + 1);
944
- }
945
-
946
- return latestEndDate;
947
- }
948
-
949
- function addEditAndRemoveButtons(rowDiv, deleteId, editFunction, deleteFunction, duplicateFunction, style="btn-group") {
950
- rowDiv = $(rowDiv);
951
-
952
- let actionContainer = $(`<div class="col-auto edit-buttons m-1 ${style}" role="group"></div>`);
953
-
954
- rowDiv.append(actionContainer);
955
-
956
- actionContainer.append($('<button class="edit-button btn btn-outline-primary p-2"><i class="fa fa-pencil"></i></button>').click(function(event) {
957
- event.stopPropagation(); // Prevent other onclick events from triggering
958
- editFunction();
959
- }));
960
-
961
- rowDiv.find('.col').click(function(event) {
962
- event.stopPropagation(); // Prevent other onclick events from triggering
963
- editFunction();
964
- });
965
-
966
- if (duplicateFunction != null) {
967
- actionContainer.append($('<button class="btn btn-outline-secondary p-2"><i class="fa fa-copy"></i></button>').click(function (event) {
968
- event.stopPropagation(); // Prevent other onclick events from triggering
969
- duplicateFunction(deleteId);
970
- }));
971
- }
972
-
973
- actionContainer.append($('<button class="btn btn-outline-danger p-2"><i class="fa fa-trash"></i></button>').click(function (event) {
974
- event.stopPropagation(); // Prevent other onclick events from triggering
975
- deleteFunction(deleteId);
976
- }));
977
- }
978
-
979
- function getAndCleanElement(elementId) {
980
- let element = document.getElementById(elementId);
981
- element.innerHTML = "";
982
- return element;
983
- }
984
-
985
- function reloadCropsFromJson(cropsFromJson) {
986
- console.log("Données JSON importées :", cropsFromJson);
987
-
988
- crops = cropsFromJson;
989
-
990
- initializeOptions();
991
- refreshAllTables();
992
- displayCropListView();
993
- }
994
-
995
- function getChatGPTInfoFromCropName() {
996
-
997
- if (selectedStep.step.useDefaultColor == false &&
998
- selectedStep.step.useDefaultStartDate == false &&
999
- selectedStep.step.useDefaultEndDate == false) {
1000
- // If the user has modified all fields, do not call the API again
1001
- $('#itk-api-comment').text('');
1002
- console.log("All fields modified by user, skipping API call");
1003
- return;
1004
- }
1005
-
1006
- let newCropName = $("#cropName").val();
1007
-
1008
- $('#itk-api-comment').html('<i class="fa fa-spinner fa-spin"></i> ...');
1009
-
1010
- $.ajax({
1011
- url: "https://itk-info.tripleperformance.fr/api/culture",
1012
- method: "POST",
1013
- contentType: "application/json",
1014
- data: JSON.stringify({
1015
- culture: newCropName,
1016
- region: crops.options.region ?? "France"
1017
- }),
1018
- success: function(data) {
1019
- console.log("Réponse:", data);
1020
-
1021
- if (data.color_hex && selectedStep.step.useDefaultColor) {
1022
- setInputValue("cropColor", data.color_hex);
1023
- updateSelectedStep();
1024
- }
1025
-
1026
- let startDate = getRotationEndDate();
1027
- if (data.average_sowing_date && selectedStep.step.useDefaultStartDate) {
1028
- // data.average_sowing_date is in MM-DD format, we need to convert it to YYYY-MM-DD
1029
- // for the current year
1030
- let currentYear = startDate.getFullYear();
1031
- startDate = new Date(`${currentYear}-${data.average_sowing_date}`);
1032
- // If the start date is before today, it means the crop starts next year
1033
- if (startDate < new Date()) {
1034
- startDate.setFullYear(startDate.getFullYear() + 1);
1035
- }
1036
- setInputValue("cropStartDate", startDate.toISOString().split('T')[0]);
1037
- updateSelectedStep();
1038
- }
1039
-
1040
- if (data.end_of_season && selectedStep.step.useDefaultEndDate) {
1041
- // data.end_of_season is in MM-DD format, we need to convert it to YYYY-MM-DD
1042
- // for the current year
1043
- let currentYear = startDate.getFullYear();
1044
- let endDate = new Date(`${currentYear}-${data.end_of_season}`);
1045
- // If the end date is before the start date, it means the crop ends the next year
1046
- if (endDate < startDate) {
1047
- endDate.setFullYear(startDate.getFullYear() + 1);
1048
- }
1049
-
1050
- setInputValue("cropEndDate", endDate.toISOString().split('T')[0]);
1051
- updateSelectedStep();
1052
- }
1053
-
1054
- if (data.source_explanation) {
1055
- $('#itk-api-comment').text(data.source_explanation);
1056
- } else {
1057
- $('#itk-api-comment').text('');
1058
- }
1059
-
1060
- // {
1061
- // "culture": "Colza",
1062
- // "region": "France",
1063
- // "average_sowing_date": "08-15",
1064
- // "end_of_season": "07-15",
1065
- // "color_hex": "#FFD700",
1066
- // "confidence": "high",
1067
- // "source_explanation": "Colza is typically sown in late summer and harvested in mid-summer in France."
1068
- // }
1069
-
1070
- },
1071
- error: function(err) {
1072
- console.error("Erreur:", err);
1073
- }
1074
- });
1075
- }
1076
-
1077
- function attachCropInputListeners() {
1078
- $("#cropName").on("input", _.debounce(getChatGPTInfoFromCropName, 1000));
1079
-
1080
- $("#cropName").on("input", _.debounce(updateSelectedStep, 500));
1081
- $("#cropDescription").on("input", _.debounce(updateSelectedStep, 500));
1082
- $("#cropStartDate").on("change", _.debounce(updateSelectedStepStartDate, 500));
1083
- $("#cropEndDate").on("change", _.debounce(updateSelectedStepEndDate, 500));
1084
- $("#cropColor").on("input", _.debounce(updateSelectedStepColor, 500));
1085
- $("#cropSecondary").on("change", _.debounce(updateSelectedStep, 500));
1086
- }
1087
-
1088
- function enableTitleEditing() {
1089
- document.getElementById("title").addEventListener("blur", function () {
1090
- if (this.textContent.trim() === "") {
1091
- this.textContent = DEFAULT_TITLE;
1092
- }
1093
-
1094
- crops.title = this.textContent;
1095
- crops.defaultTitle = false;
1096
- });
1097
- }
391
+ </body>
1098
392
 
1099
- function doExportToJsonFile() {
1100
- let jsonName = crops.title.replace(/\s+/g, '-').toLowerCase() + ".json";
1101
- exportToJsonFile(crops, jsonName);
1102
- }
1103
- </script>
393
+ </html>