@igea/oac_frontend 1.0.97 → 1.0.98

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igea/oac_frontend",
3
- "version": "1.0.97",
3
+ "version": "1.0.98",
4
4
  "description": "Frontend service for the OAC project",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -29,10 +29,13 @@ module.exports = function(serviceName) {
29
29
  router.get('/view/:uuid', (req, res) => {
30
30
  const { uuid } = req.params;
31
31
 
32
+ //const formId = OntologyForm.getIdFromUuid(uuid);
33
+
32
34
  let data = new DataModel(req, {
33
35
  root: serviceName,
34
36
  title: 'Indagine viewer',
35
37
  uuid,
38
+ //form_id: formId,
36
39
 
37
40
  // 👇 NASCONDE MENU E SIDEBAR
38
41
  activeMenu: null,
@@ -158,6 +158,7 @@ document.addEventListener('DOMContentLoaded', () => {
158
158
  form_keep_lock_timer: null,
159
159
  form_keep_locking: false,
160
160
  inEditing: false,
161
+ inEditingViewer: false,
161
162
  isVisible: false,
162
163
  enabled: el.dataset.editing == "true",
163
164
  serializedForm: "",
@@ -190,6 +191,7 @@ document.addEventListener('DOMContentLoaded', () => {
190
191
  if (showForm && this.uuid) {
191
192
  this.isVisible = true;
192
193
  this.inEditing = true;
194
+ this.inEditingViewer = false;
193
195
  this.resetShaclForm(this.uuid, true); // ❌ uuid non definito
194
196
  }
195
197
 
@@ -242,6 +244,8 @@ document.addEventListener('DOMContentLoaded', () => {
242
244
  _this.form_id = null;
243
245
  //_this.resetShaclForm(null, true);
244
246
  clearInterval(_this.form_keep_lock_timer);
247
+ window.location.reload();
248
+
245
249
  });
246
250
 
247
251
  window.addEventListener('beforeunload', () => {
@@ -771,7 +775,58 @@ document.addEventListener('DOMContentLoaded', () => {
771
775
  },
772
776
  reset() {
773
777
  window.location.reload();
774
- }
778
+ },
779
+ stopEdit() {
780
+ console.log('STOP EDIT (viewer)');
781
+ window.dispatchEvent(new CustomEvent('edit-stop'));
782
+ },
783
+ async startEdit() {
784
+ try {
785
+ // entra subito in editing → UI aggiornata
786
+ this.inEditingViewer = true;
787
+ this.enabled = true;
788
+
789
+ // cerco l'indagine tramite uuid
790
+ const res = await fetch('/backend/ontology/form/search', {
791
+ method: 'POST',
792
+ headers: { 'Content-Type': 'application/json' },
793
+ body: JSON.stringify({ query: this.uuid, limit: 1 })
794
+ });
795
+
796
+ const response = await res.json();
797
+ if (!response.success || !response.data?.length) {
798
+ alert("Indagine non trovata");
799
+ this.inEditing = false;
800
+ this.enabled = false;
801
+ return;
802
+ }
803
+
804
+ const item = response.data[0];
805
+
806
+ // lock
807
+ const lockResp = await fetch(
808
+ `/backend/ontology/form/lock/${item.id}/${CLIENT_UUID}`
809
+ ).then(r => r.json());
810
+
811
+ if (!lockResp.success) {
812
+ alert("Indagine in modifica da un altro utente");
813
+ this.inEditing = false;
814
+ this.enabled = false;
815
+ return;
816
+ }
817
+
818
+ // entra ufficialmente in editing
819
+ window.dispatchEvent(new CustomEvent('edit-item', {
820
+ detail: { id: item.id, uuid: item.uuid }
821
+ }));
822
+
823
+ } catch (e) {
824
+ console.error(e);
825
+ alert("Errore durante l'attivazione della modifica");
826
+ this.inEditing = false;
827
+ this.enabled = false;
828
+ }
829
+ }
775
830
  }
776
831
  });
777
832
 
@@ -1,3 +1,4 @@
1
+
1
2
  //
2
3
  // Place any custom JS here
3
4
  //
@@ -11,14 +12,24 @@ const sparnatural = document.querySelector("spar-natural");
11
12
 
12
13
  let viewMode = "direct";
13
14
 
15
+ // cache risultati
16
+ let directResults = null; // risposta SPARQL originale
17
+ let indaginiResults = null; // risposta SPARQL costruita
18
+
19
+ // per evitare race condition
20
+ let currentRunId = 0;
21
+
22
+
14
23
  document.querySelectorAll('input[name="viewMode"]').forEach(radio => {
15
24
  radio.addEventListener("change", e => {
16
25
  viewMode = e.target.value;
17
- sparnatural.enablePlayBtn();
26
+ updateView();
27
+ sparnatural.enablePlayBtn();
18
28
  console.log("View mode:", viewMode);
19
29
  });
20
30
  });
21
31
 
32
+
22
33
  /* =========================
23
34
  INDAGINE RESOLVERS MAP
24
35
  ========================= */
@@ -38,6 +49,10 @@ const INDAGINE_RESOLVERS = {
38
49
  },
39
50
  "j.0:S13_Sample": {
40
51
  property: "crm:P16_used_specific_object"
52
+ },
53
+ // 🆕 ATTIVITÀ DIAGNOSTICA
54
+ "crm:E7_Activity": {
55
+ property: "crm:P134_continued"
41
56
  }
42
57
  };
43
58
 
@@ -89,64 +104,179 @@ yasr.plugins["TableX"].config.uriHrefAdapter = function (uri) {
89
104
  QUERY RESPONSE HANDLER
90
105
  ========================= */
91
106
 
92
- yasqe.on("queryResponse", function (_yasqe, response, duration) {
93
- yasr.setResponse(response, duration);
107
+ yasqe.on("queryResponse", async function (_yasqe, response, duration) {
108
+ const runId = ++currentRunId;
94
109
 
95
- if (viewMode === "indagine") {
96
- resolvedIndagini.clear();
97
- const sparqlResponse = normalizeSparqlResponse(response);
98
- if (sparqlResponse) {
99
- resolveIndagini(sparqlResponse);
100
- }
101
- }
110
+ const normalized = normalizeSparqlResponse(response);
111
+ sparnatural.enablePlayBtn();
112
+
113
+ if (!normalized) {
114
+ directResults = null;
115
+ indaginiResults = null;
116
+ yasr.setResponse(emptyResponse("Risposta SPARQL non valida"), duration);
117
+ return;
118
+ }
119
+
120
+ // salva risultati diretti
121
+ directResults = normalized;
122
+
123
+ // mostra subito i diretti
124
+ yasr.setResponse(directResults, duration);
125
+
126
+ // calcola DERIVATE (in background)
127
+ // CASO SPECIALE: ricerca INDAGINE
128
+ if (await isIndagineSearch(normalized)) {
129
+
130
+ // Tabella B = stessa risposta della A
131
+ indaginiResults = normalized;
132
+
133
+ if (viewMode === "indagine") {
134
+ yasr.setResponse(indaginiResults);
135
+ }
136
+
137
+ } else {
138
+ resolveIndagini(normalized, runId);
139
+ }
102
140
 
103
141
 
104
- sparnatural.enablePlayBtn();
105
142
  });
106
143
 
144
+
107
145
  /* =========================
108
146
  RESOLVER PIPELINE
109
147
  ========================= */
110
148
 
111
- function resolveIndagini(response) {
149
+ async function isIndagineSearch(response) {
112
150
  const uris = extractUrisFromResponse(response);
151
+ if (!uris.length) return false;
152
+
153
+ // sicurezza: tutte sotto namespace indagine
154
+ if (!uris.every(u => u.startsWith("http://indagine/"))) {
155
+ return false;
156
+ }
157
+
158
+ // controllo RDF type
159
+ return await areAllIndaginiRoot(uris);
160
+ }
161
+
162
+ function updateView() {
163
+ if (viewMode === "direct") {
164
+ yasr.setResponse(directResults ?? emptyResponse("Nessun risultato"));
165
+ } else if (viewMode === "indagine") {
166
+ yasr.setResponse(indaginiResults ?? emptyResponse("Calcolo in corso..."));
167
+ }
168
+ }
169
+
170
+ async function areAllIndaginiRoot(uris) {
171
+ const values = uris.map(u => `(<${u}>)`).join(" ");
172
+
173
+ const query = `
174
+ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
175
+ PREFIX crm: <http://www.cidoc-crm.org/cidoc-crm/>
176
+
177
+ SELECT ?x ?type WHERE {
178
+ VALUES (?x) { ${values} }
113
179
 
114
- console.log("URIs trovate:", uris);
180
+ ?x rdf:type crm:E7_Activity .
181
+
182
+ # root = non deve essere continuazione di un'altra activity
183
+ FILTER NOT EXISTS {
184
+ ?parent crm:P134_continued ?x .
185
+ }
186
+ }
187
+ `;
188
+
189
+ const data = await fetchSparql(query);
190
+ const bindings = data?.results?.bindings || [];
191
+
192
+ // devono tornare TUTTE
193
+ return bindings.length === uris.length;
194
+ }
195
+
196
+
197
+
198
+ function resolveIndagini(response, runId) {
199
+ const uris = extractUrisFromResponse(response);
200
+
201
+ if (!uris.length) {
202
+ indaginiResults = emptyResponse("Nessuna indagine collegata");
203
+ return;
204
+ }
205
+
206
+ resolvedIndagini.clear();
207
+
208
+ let pending = 0;
115
209
 
116
210
  uris.forEach(uri => {
117
- resolveIndagineFromUri(uri);
211
+ pending++;
212
+ resolveIndagineFromUri(uri, runId, () => {
213
+ pending--;
214
+ if (pending === 0 && runId === currentRunId) {
215
+ buildIndaginiResponse();
216
+ }
217
+ });
118
218
  });
119
219
  }
120
220
 
121
- function resolveIndagineFromUri(uri) {
221
+
222
+ function buildIndaginiResponse() {
223
+ const bindings = Array.from(resolvedIndagini).map(uri => ({
224
+ indagine: { type: "uri", value: uri }
225
+ }));
226
+
227
+ indaginiResults = bindings.length
228
+ ? {
229
+ head: { vars: ["indagine"] },
230
+ results: { bindings }
231
+ }
232
+ : emptyResponse("Nessuna indagine collegata");
233
+
234
+ // se l’utente è già in vista “indagine”, aggiorna subito
235
+ if (viewMode === "indagine") {
236
+ yasr.setResponse(indaginiResults);
237
+ }
238
+ }
239
+
240
+
241
+ function resolveIndagineFromUri(uri, runId, done) {
122
242
  const typeQuery = `
123
243
  PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
124
-
125
- SELECT DISTINCT ?type
126
- WHERE {
244
+ SELECT DISTINCT ?type WHERE {
127
245
  <${uri}> rdf:type ?type .
128
246
  }
129
- LIMIT 10
130
247
  `;
131
248
 
132
249
  fetchSparql(typeQuery).then(data => {
133
250
  const types = data.results.bindings
134
- .map(b => normalizeRdfType(b.type.value))
135
- .filter(Boolean);
136
-
137
- console.log("Tipi normalizzati:", types);
138
-
139
- types.forEach(t => {
140
- const resolver = INDAGINE_RESOLVERS[t];
141
- if (!resolver) return;
142
-
143
- const q = buildResolverQuery(uri, resolver.property);
144
- fetchIndagini(q);
145
- });
251
+ .map(b => normalizeRdfType(b.type.value))
252
+ .filter(Boolean);
253
+
254
+ let innerPending = 0;
255
+
256
+ types.forEach(t => {
257
+ const resolver = INDAGINE_RESOLVERS[t];
258
+ if (!resolver) return;
259
+
260
+ innerPending++;
261
+ const q = buildResolverQuery(uri, resolver.property);
262
+
263
+ fetchSparql(q).then(r => {
264
+ r.results?.bindings?.forEach(b => {
265
+ if (b.indagine?.type === "uri") {
266
+ resolvedIndagini.add(b.indagine.value);
267
+ }
268
+ });
269
+ }).finally(() => {
270
+ innerPending--;
271
+ if (innerPending === 0) done();
272
+ });
273
+ });
146
274
 
275
+ if (types.length === 0) done();
147
276
  });
148
277
  }
149
278
 
279
+
150
280
  let resolvedIndagini = new Set();
151
281
 
152
282
  function fetchIndagini(query) {
@@ -46,9 +46,7 @@
46
46
 
47
47
  <div class="viewer-container">
48
48
 
49
- <div class="viewer-header">
50
- <h2>Indagine</h2>
51
- </div>
49
+
52
50
 
53
51
  {# --------------------------------------------------
54
52
  Vue root (SENZA search)
@@ -58,12 +56,39 @@
58
56
  id="investigation-app"
59
57
  data-cur_role="{{ user.role }}"
60
58
  data-uuid="{{ uuid }}"
61
- data-editing="{{ user.role != 3 }}"
59
+ data-form-id="{{ form_id }}"
60
+ data-editing="false"
62
61
  data-show-form="true"
63
62
  data-label_save_ok="{{ t('investigation.save_ok') }}"
64
63
  data-label_save_err="{{ t('investigation.save_err') }}"
65
64
  >
66
65
 
66
+ <div class="viewer-header">
67
+ {% if user.role != 3 %}
68
+ <!-- MODIFICA -->
69
+ <button v-if="!inEditingViewer"
70
+ title="{{ t('investigation.edit_item') }}"
71
+ @click="startEdit"
72
+ type="button"
73
+ class="btn btn-info">
74
+ <i class="fa-solid fa-pen-to-square"></i>
75
+ </button>
76
+
77
+ <!-- STOP EDITING -->
78
+ <button v-if="inEditingViewer"
79
+ title="{{ t('investigation.stop_edit') }}"
80
+ @click="stopEdit"
81
+ type="button"
82
+ class="btn btn-success">
83
+ <i class="fa-solid fa-circle-stop" style="color:red;"></i>
84
+ </button>
85
+
86
+ {% endif %}
87
+
88
+
89
+
90
+ </div>
91
+
67
92
 
68
93
  <template v-if="isVisible">
69
94
 
@@ -81,7 +106,7 @@
81
106
  TURTLE
82
107
  </button>
83
108
 
84
- <button v-if="inEditing && validForm"
109
+ <button v-if="inEditingViewer && validForm"
85
110
  :disabled="saving"
86
111
  class="btn btn-primary mx-2"
87
112
  @click="save(false)">
@@ -89,6 +114,8 @@
89
114
  <i v-else class="fa-solid fa-save"></i>
90
115
  </button>
91
116
 
117
+
118
+
92
119
  </div>
93
120
  {% endif %}
94
121
 
@@ -110,7 +137,7 @@
110
137
 
111
138
  {# ---------------- DEBUG OUTPUT (admin) ---------------- #}
112
139
  {% if user.role in [0,1] %}
113
- <fieldset v-if="inEditing"
140
+ <fieldset v-if="inEditingViewer"
114
141
  style="margin-top:15px;
115
142
  border: solid 1px gray;
116
143
  border-radius: 6px;
@@ -150,6 +177,72 @@ function tryCloseViewer() {
150
177
  if (hint) hint.style.display = 'block';
151
178
  }, 200);
152
179
  }
180
+
181
+
182
+ async function startEditFromViewer(btn) {
183
+ if (btn) {
184
+ btn.disabled = true;
185
+ btn.classList.add('disabled');
186
+ }
187
+
188
+ const appEl = document.getElementById('investigation-app');
189
+ if (!appEl) return;
190
+
191
+ const uuid = appEl.dataset.uuid;
192
+ if (!uuid) {
193
+ alert("UUID non disponibile");
194
+ if (btn) btn.disabled = false;
195
+ return;
196
+ }
197
+
198
+ try {
199
+ // 🔍 riuso search
200
+ const res = await fetch('/backend/ontology/form/search', {
201
+ method: 'POST',
202
+ headers: { 'Content-Type': 'application/json' },
203
+ body: JSON.stringify({ query: uuid, limit: 1 })
204
+ });
205
+
206
+ const response = await res.json();
207
+
208
+ if (!response.success || !response.data?.length) {
209
+ alert("Indagine non trovata");
210
+ if (btn) btn.disabled = false;
211
+ return;
212
+ }
213
+
214
+ const item = response.data[0];
215
+
216
+ // 🔐 lock
217
+ const lockResp = await fetch(
218
+ `/backend/ontology/form/lock/${item.id}/${window.CLIENT_UUID}`
219
+ ).then(r => r.json());
220
+
221
+ if (!lockResp.success) {
222
+ alert("Indagine in modifica da un altro UTENTES");
223
+ if (btn) btn.disabled = false;
224
+ return;
225
+ }
226
+
227
+ // 🔥 entra in editing
228
+ window.dispatchEvent(
229
+ new CustomEvent('edit-item', {
230
+ detail: {
231
+ id: item.id,
232
+ uuid: item.uuid
233
+ }
234
+ })
235
+ );
236
+
237
+ } catch (e) {
238
+ console.error(e);
239
+ alert("Errore durante l'attivazione della modifica");
240
+ if (btn) btn.disabled = false;
241
+ }
242
+ }
243
+
244
+
245
+
153
246
  </script>
154
247
 
155
- {% endblock %}
248
+ {% endblock %}
@@ -5,6 +5,15 @@
5
5
  <div class="container my-4">
6
6
 
7
7
  <h2>Indagine</h2>
8
+
9
+ <h2 class="d-flex justify-content-between align-items-center">
10
+ <span>Indagine</span>
11
+
12
+ <button id="btn-edit"
13
+ class="btn btn-primary btn-sm">
14
+ <i class="fa-solid fa-pen"></i> Modifica
15
+ </button>
16
+ </h2>
8
17
 
9
18
  <div id="shacl-container"
10
19
  style="margin-top:10px;
@@ -25,4 +34,19 @@
25
34
 
26
35
  </div>
27
36
 
37
+ <script>
38
+ document.getElementById('btn-edit')?.addEventListener('click', function () {
39
+ const form = document.getElementById('shacl-form');
40
+ if (!form) return;
41
+
42
+ // rimuove readonly → entra in editing
43
+ form.removeAttribute('data-readonly');
44
+
45
+ // feedback UI
46
+ this.disabled = true;
47
+ this.innerHTML = '<i class="fa-solid fa-lock-open"></i> Modifica attiva';
48
+ });
49
+ </script>
50
+
51
+
28
52
  {% endblock %}