@osfarm/itineraire-technique 1.0.0
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/workflows/publish.yml +32 -0
- package/LICENSE +21 -0
- package/README.md +27 -0
- package/css/styles-editor.css +1 -0
- package/css/styles-editor.css.map +1 -0
- package/css/styles-rendering.css +1 -0
- package/css/styles-rendering.css.map +1 -0
- package/editor.html +725 -0
- package/images/rendu_frise.png +0 -0
- package/images/rendu_rotation.png +0 -0
- package/js/chart-render.js +853 -0
- package/js/editor-attributes.js +96 -0
- package/js/editor-crops.js +78 -0
- package/js/editor-export.js +118 -0
- package/js/editor-interventions.js +117 -0
- package/package.json +16 -0
- package/rendu_statique_1.html +664 -0
- package/scss/styles-editor.scss +106 -0
- package/scss/styles-rendering.scss +101 -0
- package/test/itk-templates/export-itk-1.json +153 -0
- package/test/test.json +324 -0
package/editor.html
ADDED
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<title>Itinéraire technique TIKA</title>
|
|
8
|
+
|
|
9
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.js"></script>
|
|
10
|
+
|
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
|
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">
|
|
14
|
+
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.7/underscore-umd-min.js"></script>
|
|
16
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
|
17
|
+
|
|
18
|
+
<script src="./js/chart-render.js"></script>
|
|
19
|
+
<script src="./js/editor-attributes.js"></script>
|
|
20
|
+
<script src="./js/editor-interventions.js"></script>
|
|
21
|
+
<script src="./js/editor-crops.js"></script>
|
|
22
|
+
<script src="./js/editor-export.js"></script>
|
|
23
|
+
|
|
24
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
|
25
|
+
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
|
26
|
+
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
|
27
|
+
<link href="./css/styles-editor.css" rel="stylesheet">
|
|
28
|
+
<link href="./css/styles-rendering.css" rel="stylesheet">
|
|
29
|
+
</head>
|
|
30
|
+
|
|
31
|
+
<body>
|
|
32
|
+
<div class="container-fluid">
|
|
33
|
+
<div class="row">
|
|
34
|
+
<div class="col-auto text-left main-header file-icons mb-2">
|
|
35
|
+
<button type="button" onclick="importFromJsonFile()" class="btn btn-outline-primary primary-button"
|
|
36
|
+
id="importFromJsonButton"><i class="fa fa-upload" aria-hidden="true"></i> Charger (JSON)</button>
|
|
37
|
+
<button type="button" onclick="importFromTestJson()" class="btn btn-outline-primary primary-button"
|
|
38
|
+
id="importFromJsonButton"><i class="fa fa-upload" aria-hidden="true"></i> Charger un
|
|
39
|
+
exemple</button>
|
|
40
|
+
<button type="button" onclick="exportToJsonFile(crops)"
|
|
41
|
+
class="btn btn-outline-primary primary-button" id="exportToJsonButton"><i
|
|
42
|
+
class="fa fa-download" aria-hidden="true"></i> Exporter</button>
|
|
43
|
+
<button type="button" onclick="wipe(crops)" class="btn btn-outline-primary primary-button"
|
|
44
|
+
id="exportToJsonButton"><i class="fa fa-trash" aria-hidden="true"></i> Tout effacer</button>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="col text-center main-header mb-2">
|
|
47
|
+
<h1 id="title" contenteditable="true" style="width: 100%">Itinéraire technique</h1>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="col-auto text-right main-header file-icons mb-2">
|
|
50
|
+
<button type="button" class="btn btn-outline-primary primary-button"
|
|
51
|
+
id="exportToJsonButton" data-bs-toggle="modal" data-bs-target="#modalParams"><i class="fa fa-cog" aria-hidden="true"></i> Réglages</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="row">
|
|
55
|
+
<div class="col col-12 col-lg-3 editor-view"> <!-- Editor zone-->
|
|
56
|
+
|
|
57
|
+
<div class="welcome-view" id="welcomeView">
|
|
58
|
+
|
|
59
|
+
<div class="row">
|
|
60
|
+
<div class="col-12">
|
|
61
|
+
<p>Bienvenue dans l'éditeur</p>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="row">
|
|
66
|
+
<div class="col-12">
|
|
67
|
+
<button type="button" onclick="addNewStepClickEvent()"
|
|
68
|
+
class="w-100 btn btn-outline-primary primary-button" id="addNewCropButton"><i
|
|
69
|
+
class="fa fa-plus-square" aria-hidden="true"></i>
|
|
70
|
+
Ajouter une étape de rotation</button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="d-none welcome-view container px-4" id="cropListView">
|
|
77
|
+
<div class="row">
|
|
78
|
+
<div class="col-12">
|
|
79
|
+
<p>Liste des étapes</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="row">
|
|
83
|
+
<div id="cropsContainer" class="container"></div>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="row">
|
|
86
|
+
<button type="button" onclick="addNewStepClickEvent()"
|
|
87
|
+
class="w-100 btn btn-outline-primary primary-button" id="addNewCropButton"><i
|
|
88
|
+
class="fa fa-plus-square" aria-hidden="true"></i>
|
|
89
|
+
Ajouter une étape de rotation</button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="d-none" id="cropDetailView">
|
|
94
|
+
<div class="row card-holder mb-2">
|
|
95
|
+
<div class="col-12">
|
|
96
|
+
<button class="float-end close-step-button close-step-times" ><i class="fa fa-times" aria-hidden="true"></i></button>
|
|
97
|
+
<h4>Étape</h4>
|
|
98
|
+
<form id="cropForm">
|
|
99
|
+
<div class="row mb-2">
|
|
100
|
+
<div class="col">
|
|
101
|
+
<input type="text" id="cropName" class="form-control" placeholder="Nom de la culture">
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="row mb-2">
|
|
105
|
+
<div class="col">
|
|
106
|
+
<textarea type="text" id="cropDescription" class="form-control"
|
|
107
|
+
placeholder="Description" rows="4"></textarea>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="row mb-2">
|
|
111
|
+
<div class="col-5 pe-0">
|
|
112
|
+
<label for="cropStartDate" class="form-label">Début</label>
|
|
113
|
+
<input type="date" id="cropStartDate" class="form-control">
|
|
114
|
+
</div>
|
|
115
|
+
<div class="col-5 pe-0">
|
|
116
|
+
<label for="cropEndDate" class="form-label">Fin</label>
|
|
117
|
+
<input type="date" id="cropEndDate" class="form-control">
|
|
118
|
+
</div>
|
|
119
|
+
<div class="col-2 d-flex align-items-end">
|
|
120
|
+
<label for="cropColor" class="form-label"></label>
|
|
121
|
+
<input type="color" id="cropColor" class="form-control form-control-color w-100">
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="row"><div id="itk-api-comment" class="col text-center text-muted small"></div></div>
|
|
125
|
+
</form>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="row card-holder attributes-view mb-2">
|
|
129
|
+
<div class="col-12">
|
|
130
|
+
<h5>Attributs</h5>
|
|
131
|
+
<div class="row">
|
|
132
|
+
<div id="attributesContainer" class="container"></div>
|
|
133
|
+
</div>
|
|
134
|
+
<button id="newAttributeButton" type="button" onclick="createAttributForm()"
|
|
135
|
+
class="btn btn-primary primary-button w-100"><i class="fa fa-plus-square"
|
|
136
|
+
aria-hidden="true"></i> Ajouter</button>
|
|
137
|
+
<div id="newAttributeContainer"></div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="row card-holder">
|
|
141
|
+
<div class="col-12">
|
|
142
|
+
<h5>Interventions</h5>
|
|
143
|
+
<div class="row">
|
|
144
|
+
<p id="interventionsTopName" class="h6"></p>
|
|
145
|
+
<div id="interventionsTopContainer" class="container"></div>
|
|
146
|
+
</div>
|
|
147
|
+
<hr class="my-2">
|
|
148
|
+
<div class="row">
|
|
149
|
+
<p id="interventionsBottomName" class="h6"></p>
|
|
150
|
+
<div id="interventionsBottomContainer" class="container"></div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<button id="newInterventionButton" type="button" onclick="createInterventionForm()"
|
|
154
|
+
class="btn btn-primary primary-button w-100"><i class="fa fa-plus-square"
|
|
155
|
+
aria-hidden="true"></i> Ajouter</button>
|
|
156
|
+
<div id="newInterventionContainer"></div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="row">
|
|
160
|
+
<div class="col-12">
|
|
161
|
+
<button type="button"
|
|
162
|
+
class="w-100 btn btn-outline-primary primary-button mt-2 close-step-button">Retour à la liste des
|
|
163
|
+
étapes</button>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div class="col col-12 col-lg-3">
|
|
170
|
+
<div id="itk_text">
|
|
171
|
+
<div id="itk_text_content"></div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="col col-12 col-lg-6">
|
|
176
|
+
<div id="itk_chart" class="charts"></div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div class="modal fade" id="jsonErrorModal" tabindex="-1" aria-labelledby="jsonErrorModalLabel" aria-hidden="true">
|
|
182
|
+
<div class="modal-dialog">
|
|
183
|
+
<div class="modal-content">
|
|
184
|
+
<div class="modal-header">
|
|
185
|
+
<h5 class="modal-title" id="jsonErrorModalLabel">Erreur de chargement JSON</h5>
|
|
186
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
187
|
+
</div>
|
|
188
|
+
<div class="modal-body">
|
|
189
|
+
<p id="jsonErrorMessage">Une erreur s'est produite lors du chargement du fichier JSON.</p>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="modal-footer">
|
|
192
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div class="modal fade" id="confirmationModal" tabindex="-1" aria-labelledby="confirmationModalLabel"
|
|
199
|
+
aria-hidden="true">
|
|
200
|
+
<div class="modal-dialog">
|
|
201
|
+
<div class="modal-content">
|
|
202
|
+
<div class="modal-header">
|
|
203
|
+
<h5 class="modal-title" id="confirmationModalLabel">Confirmation</h5>
|
|
204
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="modal-body">
|
|
207
|
+
Attention : des étapes existent déjà. Importer un nouvel itinéraire écrasera les données existantes.
|
|
208
|
+
Voulez-vous continuer ?
|
|
209
|
+
</div>
|
|
210
|
+
<div class="modal-footer">
|
|
211
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
|
|
212
|
+
id="cancelImport">Annuler</button>
|
|
213
|
+
<button type="button" class="btn btn-primary" id="confirmImport">Continuer</button>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<!-- Modal -->
|
|
220
|
+
<div class="modal fade" id="modalParams" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
|
221
|
+
<div class="modal-dialog">
|
|
222
|
+
<div class="modal-content">
|
|
223
|
+
<div class="modal-header">
|
|
224
|
+
<h5 class="modal-title" id="exampleModalLabel">Paramètres de l'itinéraire technique</h5>
|
|
225
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="modal-body">
|
|
228
|
+
<form>
|
|
229
|
+
<div class="mb-3">
|
|
230
|
+
<label for="regionInput" class="form-label">Région</label>
|
|
231
|
+
<input type="text" class="form-control" id="regionInput" value="France">
|
|
232
|
+
</div>
|
|
233
|
+
<div class="mb-3">
|
|
234
|
+
<label for="viewSelect" class="form-label">Affichage par défaut</label>
|
|
235
|
+
<select class="form-select" id="viewSelect">
|
|
236
|
+
<option value="horizontal">En frise</option>
|
|
237
|
+
<option value="vertical">En donut</option>
|
|
238
|
+
</select>
|
|
239
|
+
</div>
|
|
240
|
+
<div class="mb-3 form-check">
|
|
241
|
+
<input type="checkbox" class="form-check-input" id="showTranscriptCheckbox" checked>
|
|
242
|
+
<label class="form-check-label" for="showTranscriptCheckbox">Afficher la transcription textuelle</label>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="mb-3">
|
|
245
|
+
<label for="topInterventionsTitle" class="form-label">Titre des interventions du haut</label>
|
|
246
|
+
<input type="text" class="form-control" id="topInterventionsTitle" value="Contrôle adventices">
|
|
247
|
+
</div>
|
|
248
|
+
<div class="mb-3">
|
|
249
|
+
<label for="bottomInterventionsTitle" class="form-label">Titre des interventions du bas</label>
|
|
250
|
+
<input type="text" class="form-control" id="bottomInterventionsTitle" value="Autres interventions">
|
|
251
|
+
</div>
|
|
252
|
+
<div class="mb-3">
|
|
253
|
+
<label for="stepsTitle" class="form-label">Titre des étapes de la rotation</label>
|
|
254
|
+
<input type="text" class="form-control" id="stepsTitle" value="Étapes de la rotation dans la parcelle">
|
|
255
|
+
</div>
|
|
256
|
+
</form>
|
|
257
|
+
</div>
|
|
258
|
+
<div class="modal-footer">
|
|
259
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
|
260
|
+
<button type="button" class="btn btn-primary" id="paramsModalSaveButton">Enregistrer les modifications</button>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</body>
|
|
266
|
+
|
|
267
|
+
</html>
|
|
268
|
+
|
|
269
|
+
<script>
|
|
270
|
+
|
|
271
|
+
const DEFAULT_TITLE = "Nouvel itinéraire technique";
|
|
272
|
+
|
|
273
|
+
let crops = {
|
|
274
|
+
"title": DEFAULT_TITLE,
|
|
275
|
+
"options": {
|
|
276
|
+
"view": "horizontal",
|
|
277
|
+
"show_transcript": true,
|
|
278
|
+
"title_top_interventions": "Contrôle adventices",
|
|
279
|
+
"title_bottom_interventions": "Autres interventions",
|
|
280
|
+
"title_steps": "Étapes de la rotation dans la parcelle",
|
|
281
|
+
"region": "France"
|
|
282
|
+
},
|
|
283
|
+
"steps": []
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
let selectedStep; // an instance of Crop
|
|
287
|
+
|
|
288
|
+
initializeOptions();
|
|
289
|
+
|
|
290
|
+
function initializeOptions() {
|
|
291
|
+
document.getElementById("title").textContent = crops.title;
|
|
292
|
+
document.getElementById("interventionsTopName").textContent = crops.options.title_top_interventions;
|
|
293
|
+
document.getElementById("interventionsBottomName").textContent = crops.options.title_bottom_interventions;
|
|
294
|
+
|
|
295
|
+
attachCropInputListeners();
|
|
296
|
+
enableTitleEditing();
|
|
297
|
+
|
|
298
|
+
$('.close-step-button').click(function() {
|
|
299
|
+
displayCropListView();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
$(function() {
|
|
303
|
+
$('#cropForm').on('keydown', function(e) {
|
|
304
|
+
// Prevent form submit on Enter, but allow Enter in textarea
|
|
305
|
+
if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') {
|
|
306
|
+
e.preventDefault();
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Make cropsContainer sortable
|
|
313
|
+
$("#cropsContainer").sortable({
|
|
314
|
+
handle: '.drag-handle',
|
|
315
|
+
axis: 'y',
|
|
316
|
+
update: function (event, ui) {
|
|
317
|
+
// Set new order
|
|
318
|
+
let movedStepId = ui.item.data('id');
|
|
319
|
+
let lastStepEnd = null;
|
|
320
|
+
|
|
321
|
+
let newOrder = [];
|
|
322
|
+
$(event.target).children('.step-row').each(function () {
|
|
323
|
+
let id = $(this).data('id');
|
|
324
|
+
|
|
325
|
+
let step = crops.steps.find(function (crop) { return crop.id == id });
|
|
326
|
+
|
|
327
|
+
if (movedStepId == id) {
|
|
328
|
+
let movedStep = new StepModel(step);
|
|
329
|
+
let duration = movedStep.getDurationInDays();
|
|
330
|
+
|
|
331
|
+
if (lastStepEnd != null) {
|
|
332
|
+
lastStepEnd.setDate(lastStepEnd.getDate() + 1);
|
|
333
|
+
|
|
334
|
+
movedStep.setStartDate(lastStepEnd);
|
|
335
|
+
movedStep.setDurationInDays(duration);
|
|
336
|
+
} else {
|
|
337
|
+
// This is the first step in the list, set its start date before the previously first step start date
|
|
338
|
+
let nextStep = crops.steps[0];
|
|
339
|
+
let newStartDate = new Date(nextStep.startDate.valueOf());
|
|
340
|
+
newStartDate.setDate(newStartDate.getDate() - duration);
|
|
341
|
+
movedStep.setStartDate(newStartDate);
|
|
342
|
+
movedStep.setDurationInDays(duration);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
step = movedStep.getStep();
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
lastStepEnd = new Date(step.endDate.valueOf());
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
newOrder.push(step);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
crops.steps = newOrder;
|
|
355
|
+
|
|
356
|
+
refreshAllTables();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
$('#paramsModal').on('show.bs.modal', function (event) {
|
|
362
|
+
|
|
363
|
+
// Set the modal form inputs values from crops.options
|
|
364
|
+
$("#viewSelect").val(crops.options.view);
|
|
365
|
+
$("#showTranscriptCheckbox").prop("checked", crops.options.show_transcript);
|
|
366
|
+
$("#topInterventionsTitle").val(crops.options.title_top_interventions);
|
|
367
|
+
$("#bottomInterventionsTitle").val(crops.options.title_bottom_interventions);
|
|
368
|
+
$("#stepsTitle").val(crops.options.title_steps);
|
|
369
|
+
$("#regionInput").val(crops.options.region);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
$("#paramsModalSaveButton").click(function () {
|
|
373
|
+
crops.options.view = $("#viewSelect").val();
|
|
374
|
+
crops.options.show_transcript = $("#showTranscriptCheckbox").prop("checked");
|
|
375
|
+
crops.options.title_top_interventions = $("#topInterventionsTitle").val();
|
|
376
|
+
crops.options.title_bottom_interventions = $("#bottomInterventionsTitle").val();
|
|
377
|
+
crops.options.title_steps = $("#stepsTitle").val();
|
|
378
|
+
crops.options.region = $("#regionInput").val();
|
|
379
|
+
|
|
380
|
+
// Close the modal:
|
|
381
|
+
let modal = bootstrap.Modal.getInstance(document.getElementById('modalParams'));
|
|
382
|
+
modal.hide();
|
|
383
|
+
|
|
384
|
+
refreshAllTables();
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
class StepModel {
|
|
389
|
+
constructor(step) {
|
|
390
|
+
this.step = step; // keep a reference to the original object
|
|
391
|
+
|
|
392
|
+
this.step.id = step.id ?? crypto.randomUUID();
|
|
393
|
+
this.step.name = step.name ?? "Nouvelle culture";
|
|
394
|
+
this.step.color = step.color ?? "#0db3bf";
|
|
395
|
+
this.step.startDate = step.startDate ? new Date(step.startDate) : new Date();
|
|
396
|
+
this.step.endDate = step.endDate ? new Date(step.endDate) : new Date();
|
|
397
|
+
this.step.description = step.description ?? "";
|
|
398
|
+
this.step.interventions = step.interventions?.map(i => new Intervention(i.day, i.name, i.type, i.description)) || [];
|
|
399
|
+
this.step.attributes = step.attributes?.map(a => new Attribute(a.name, a.value)) || [];
|
|
400
|
+
|
|
401
|
+
this.step.useDefaultColor = step.useDefaultColor ?? true;
|
|
402
|
+
this.step.useDefaultStartDate = step.useDefaultStartDate ?? true;
|
|
403
|
+
this.step.useDefaultEndDate = step.useDefaultEndDate ?? true;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
getStep() {
|
|
407
|
+
return this.step;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
setStartDate(date) {
|
|
411
|
+
this.step.startDate = new Date(date);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
setDurationInMonths(durationInMonths) {
|
|
415
|
+
this.step.endDate = new Date(this.step.startDate);
|
|
416
|
+
this.step.endDate.setMonth(this.step.startDate.getMonth() + durationInMonths);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
setDurationInDays(durationInDays) {
|
|
420
|
+
this.step.endDate = new Date(this.step.startDate);
|
|
421
|
+
this.step.endDate.setDate(this.step.startDate.getDate() + durationInDays);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
getDurationInDays() {
|
|
425
|
+
const diffTime = Math.abs(this.step.endDate - this.step.startDate);
|
|
426
|
+
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
addIntervention(day, name, type, description) {
|
|
430
|
+
let intervention = new Intervention(day, name, type, description);
|
|
431
|
+
this.step.interventions.push(intervention);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
addAttribute(key, value) {
|
|
435
|
+
let attribute = new Attribute(key, value);
|
|
436
|
+
this.step.attributes.push(attribute);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
updateAttribute(attributeId, key, value) {
|
|
440
|
+
this.step.attributes = this.step.attributes.map(function (attribute) {
|
|
441
|
+
if (attribute.id == attributeId) {
|
|
442
|
+
attribute.name = key;
|
|
443
|
+
attribute.value = value;
|
|
444
|
+
}
|
|
445
|
+
return attribute;
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
removeAttribute(attributeId) {
|
|
450
|
+
this.step.attributes = this.step.attributes.filter(function (attribute) { return attribute.id != attributeId });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
updateIntervention(interventionId, day, name, type, description) {
|
|
454
|
+
let intervention = this.step.interventions.find((it) => it.id == interventionId);
|
|
455
|
+
|
|
456
|
+
if (intervention != undefined) {
|
|
457
|
+
intervention.day = day;
|
|
458
|
+
intervention.name = name;
|
|
459
|
+
intervention.type = type;
|
|
460
|
+
intervention.description = description;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
removeIntervention(interventionId) {
|
|
465
|
+
this.step.interventions = this.step.interventions.filter(function (intervention) { return intervention.id != interventionId });
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
updateFromForm() {
|
|
469
|
+
this.step.name = getInputValue("cropName");
|
|
470
|
+
this.step.description = getInputValue("cropDescription");
|
|
471
|
+
this.step.startDate = new Date(getInputValue("cropStartDate"));
|
|
472
|
+
this.step.endDate = new Date(getInputValue("cropEndDate"));
|
|
473
|
+
this.step.color = getInputValue("cropColor");
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
class Intervention {
|
|
478
|
+
constructor(day, name, type, description) {
|
|
479
|
+
this.id = crypto.randomUUID();
|
|
480
|
+
this.day = day;
|
|
481
|
+
this.name = name || "";
|
|
482
|
+
this.type = type;
|
|
483
|
+
this.description = description || "";
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
class Attribute {
|
|
488
|
+
constructor(name, value) {
|
|
489
|
+
this.id = crypto.randomUUID();
|
|
490
|
+
this.name = name || "";
|
|
491
|
+
this.value = value || "";
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function updateSelectedStepColor() {
|
|
496
|
+
selectedStep.step.useDefaultColor = false;
|
|
497
|
+
|
|
498
|
+
selectedStep.updateFromForm();
|
|
499
|
+
|
|
500
|
+
refreshAllTables();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function updateSelectedStepStartDate() {
|
|
504
|
+
selectedStep.step.useDefaultStartDate = false;
|
|
505
|
+
|
|
506
|
+
selectedStep.updateFromForm();
|
|
507
|
+
|
|
508
|
+
refreshAllTables();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
function updateSelectedStepEndDate() {
|
|
513
|
+
selectedStep.step.useDefaultEndDate = false;
|
|
514
|
+
|
|
515
|
+
selectedStep.updateFromForm();
|
|
516
|
+
|
|
517
|
+
refreshAllTables();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function updateSelectedStep() {
|
|
521
|
+
selectedStep.updateFromForm();
|
|
522
|
+
|
|
523
|
+
refreshAllTables();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function getInputValue(elementId) {
|
|
527
|
+
return document.getElementById(elementId).value;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function setInputValue(elementId, value) {
|
|
531
|
+
document.getElementById(elementId).value = value;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function refreshAllTables() {
|
|
535
|
+
refreshStepsButtonList();
|
|
536
|
+
refreshAttributesTable();
|
|
537
|
+
refreshInterventionsTable();
|
|
538
|
+
renderChart();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function displayView(viewsToShow, viewsToHide) {
|
|
542
|
+
viewsToShow.forEach(view => document.querySelector(view).classList.remove("d-none"));
|
|
543
|
+
viewsToHide.forEach(view => document.querySelector(view).classList.add("d-none"));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function displayCropDetailView() {
|
|
547
|
+
displayView(["#cropDetailView"], ["#welcomeView", "#cropListView"]);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function displayCropListView() {
|
|
551
|
+
displayView(["#cropListView"], ["#welcomeView", "#cropDetailView"]);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function renderChart() {
|
|
555
|
+
let renderer = new RotationRenderer('itk_chart', 'itk_text', crops);
|
|
556
|
+
renderer.render();
|
|
557
|
+
|
|
558
|
+
$('.step-edit').click(function (event) {
|
|
559
|
+
event.stopPropagation();
|
|
560
|
+
let stepId = event.target.closest('.rotation_item').id;
|
|
561
|
+
let index = stepId.split('_')[1];
|
|
562
|
+
selectedStep = new StepModel(crops.steps[index]);
|
|
563
|
+
// populateCropDetailView(selectedStep);
|
|
564
|
+
// displayCropDetailView();
|
|
565
|
+
SelectStep(selectedStep);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function getRotationEndDate() {
|
|
570
|
+
let latestEndDate = null;
|
|
571
|
+
crops.steps.forEach(function (crop) {
|
|
572
|
+
if (latestEndDate == null || crop.endDate > latestEndDate) {
|
|
573
|
+
latestEndDate = crop.endDate;
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// Clone the date to avoid reference issues
|
|
578
|
+
if (latestEndDate == null)
|
|
579
|
+
latestEndDate = new Date();
|
|
580
|
+
else {
|
|
581
|
+
latestEndDate = new Date(latestEndDate.valueOf());
|
|
582
|
+
|
|
583
|
+
// Move to the next day
|
|
584
|
+
latestEndDate.setDate(latestEndDate.getDate() + 1);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return latestEndDate;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function addEditAndRemoveButtons(rowDiv, deleteId, editFunction, deleteFunction) {
|
|
591
|
+
rowDiv = $(rowDiv);
|
|
592
|
+
|
|
593
|
+
let actionContainer = $('<div class="col-auto edit-buttons"></div>');
|
|
594
|
+
|
|
595
|
+
actionContainer.append($('<button class="btn btn-outline-danger float-end"><i class="fa fa-trash"></i></button>').click(function (event) {
|
|
596
|
+
event.stopPropagation(); // Prevent other onclick events from triggering
|
|
597
|
+
deleteFunction(deleteId);
|
|
598
|
+
}));
|
|
599
|
+
|
|
600
|
+
actionContainer.append($('<button class="edit-button btn btn-outline-primary float-end me-1"><i class="fa fa-pencil"></i></button>').click(function (event) {
|
|
601
|
+
event.stopPropagation(); // Prevent other onclick events from triggering
|
|
602
|
+
editFunction();
|
|
603
|
+
}));
|
|
604
|
+
|
|
605
|
+
rowDiv.append(actionContainer);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function getAndCleanElement(elementId) {
|
|
609
|
+
let element = document.getElementById(elementId);
|
|
610
|
+
element.innerHTML = "";
|
|
611
|
+
return element;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function reloadCropsFromJson(cropsFromJson) {
|
|
615
|
+
console.log("Données JSON importées :", cropsFromJson);
|
|
616
|
+
|
|
617
|
+
crops = cropsFromJson;
|
|
618
|
+
|
|
619
|
+
initializeOptions();
|
|
620
|
+
refreshAllTables();
|
|
621
|
+
displayCropListView();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function getChatGPTInfoFromCropName() {
|
|
625
|
+
|
|
626
|
+
if (selectedStep.step.useDefaultColor == false &&
|
|
627
|
+
selectedStep.step.useDefaultStartDate == false &&
|
|
628
|
+
selectedStep.step.useDefaultEndDate == false) {
|
|
629
|
+
// If the user has modified all fields, do not call the API again
|
|
630
|
+
$('#itk-api-comment').text('');
|
|
631
|
+
console.log("All fields modified by user, skipping API call");
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
let newCropName = $("#cropName").val();
|
|
636
|
+
|
|
637
|
+
$('#itk-api-comment').html('<i class="fa fa-spinner fa-spin"></i> ...');
|
|
638
|
+
|
|
639
|
+
$.ajax({
|
|
640
|
+
url: "https://itk-info.tripleperformance.fr/api/culture",
|
|
641
|
+
method: "POST",
|
|
642
|
+
contentType: "application/json",
|
|
643
|
+
data: JSON.stringify({
|
|
644
|
+
culture: newCropName,
|
|
645
|
+
region: crops.options.region ?? "France"
|
|
646
|
+
}),
|
|
647
|
+
success: function(data) {
|
|
648
|
+
console.log("Réponse:", data);
|
|
649
|
+
|
|
650
|
+
if (data.color_hex && selectedStep.step.useDefaultColor) {
|
|
651
|
+
setInputValue("cropColor", data.color_hex);
|
|
652
|
+
updateSelectedStep();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
let startDate = new Date();
|
|
656
|
+
if (data.average_sowing_date && selectedStep.step.useDefaultStartDate) {
|
|
657
|
+
// data.average_sowing_date is in MM-DD format, we need to convert it to YYYY-MM-DD
|
|
658
|
+
// for the current year
|
|
659
|
+
let currentYear = new Date().getFullYear();
|
|
660
|
+
startDate = new Date(`${currentYear}-${data.average_sowing_date}`);
|
|
661
|
+
// If the start date is before today, it means the crop starts next year
|
|
662
|
+
if (startDate < new Date()) {
|
|
663
|
+
startDate.setFullYear(startDate.getFullYear() + 1);
|
|
664
|
+
}
|
|
665
|
+
setInputValue("cropStartDate", startDate.toISOString().split('T')[0]);
|
|
666
|
+
updateSelectedStep();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (data.end_of_season && selectedStep.step.useDefaultEndDate) {
|
|
670
|
+
// data.end_of_season is in MM-DD format, we need to convert it to YYYY-MM-DD
|
|
671
|
+
// for the current year
|
|
672
|
+
let currentYear = new Date().getFullYear();
|
|
673
|
+
let endDate = new Date(`${currentYear}-${data.end_of_season}`);
|
|
674
|
+
// If the end date is before the start date, it means the crop ends the next year
|
|
675
|
+
if (endDate < startDate) {
|
|
676
|
+
endDate.setFullYear(startDate.getFullYear() + 1);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
setInputValue("cropEndDate", endDate.toISOString().split('T')[0]);
|
|
680
|
+
updateSelectedStep();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (data.source_explanation) {
|
|
684
|
+
$('#itk-api-comment').text(data.source_explanation);
|
|
685
|
+
} else {
|
|
686
|
+
$('#itk-api-comment').text('');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// {
|
|
690
|
+
// "culture": "Colza",
|
|
691
|
+
// "region": "France",
|
|
692
|
+
// "average_sowing_date": "08-15",
|
|
693
|
+
// "end_of_season": "07-15",
|
|
694
|
+
// "color_hex": "#FFD700",
|
|
695
|
+
// "confidence": "high",
|
|
696
|
+
// "source_explanation": "Colza is typically sown in late summer and harvested in mid-summer in France."
|
|
697
|
+
// }
|
|
698
|
+
|
|
699
|
+
},
|
|
700
|
+
error: function(err) {
|
|
701
|
+
console.error("Erreur:", err);
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function attachCropInputListeners() {
|
|
707
|
+
$("#cropName").on("input", _.debounce(getChatGPTInfoFromCropName, 1000));
|
|
708
|
+
|
|
709
|
+
$("#cropName").on("input", _.debounce(updateSelectedStep, 500));
|
|
710
|
+
$("#cropDescription").on("input", _.debounce(updateSelectedStep, 500));
|
|
711
|
+
$("#cropStartDate").on("change", _.debounce(updateSelectedStepStartDate, 500));
|
|
712
|
+
$("#cropEndDate").on("change", _.debounce(updateSelectedStepEndDate, 500));
|
|
713
|
+
$("#cropColor").on("input", _.debounce(updateSelectedStepColor, 500));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function enableTitleEditing() {
|
|
717
|
+
document.getElementById("title").addEventListener("blur", function () {
|
|
718
|
+
if (this.textContent.trim() === "") {
|
|
719
|
+
this.textContent = DEFAULT_TITLE;
|
|
720
|
+
}
|
|
721
|
+
crops.title = this.textContent;
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
</script>
|