@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/.github/copilot-instructions.md +56 -0
- package/.github/workflows/publish.yml +34 -0
- package/README.md +2 -34
- package/css/styles-editor.css +1 -1
- package/css/styles-editor.css.map +1 -1
- package/editor.html +38 -748
- package/js/chart-render.js +16 -11
- package/js/editor-interventions.js +315 -189
- package/js/editor-loader-default.js +238 -0
- package/js/editor-loader-itinera.js +135 -0
- package/js/{editor-wiki-editor.js → editor-loader-wiki.js} +99 -11
- package/js/editor-main.js +752 -0
- package/js/intervention.js +12 -0
- package/js/step-model.js +69 -0
- package/package.json +6 -59
- package/scss/styles-editor.scss +145 -0
- package/scss/styles-rendering.scss +184 -0
- package/examples/README.md +0 -137
- package/examples/nextjs-_document.tsx +0 -66
- package/examples/nextjs-api-route.ts +0 -122
- package/examples/nextjs-app-router-editor.tsx +0 -304
- package/examples/nextjs-app-router-viewer.tsx +0 -90
- package/js/editor-attributes.js +0 -99
- package/js/editor-crops.js +0 -136
- package/js/editor-export.js +0 -118
- package/react/QUICKSTART.md +0 -172
- package/react/README.md +0 -305
- package/react/TikaEditor.jsx +0 -212
- package/react/TikaRenderer.jsx +0 -116
- package/react/hooks.ts +0 -217
- package/react/index.ts +0 -19
- package/react/types.ts +0 -152
|
@@ -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
|
+
}
|