2403
|
1 <template>
|
|
2 <div :class="detail">
|
|
3 <div class="d-flex flex-row">
|
|
4 <div class="mt-auto d-flex flex-row mb-auto small name text-left">
|
|
5 <a
|
|
6 v-if="isSoundingResult(data.kind.toUpperCase())"
|
|
7 class="text-left"
|
|
8 @click="zoomTo()"
|
|
9 href="#"
|
|
10 >{{ data.summary.bottleneck }}</a
|
|
11 >
|
|
12 <span v-if="isBottleneck(data.kind.toUpperCase())" class="text-left"
|
|
13 ><translate>Bottlenecks</translate> ({{
|
|
14 data.summary.bottlenecks.length
|
|
15 }})</span
|
|
16 >
|
|
17 <a
|
|
18 v-if="isApprovedGaugeMeasurement(data.kind.toUpperCase())"
|
|
19 class="text-left"
|
|
20 ><translate>Approved Gauge Measurements</translate> ({{
|
|
21 data.summary.length
|
|
22 }})</a
|
|
23 >
|
|
24 <span
|
|
25 class="text-left"
|
|
26 v-if="isFairwayDimension(data.kind.toUpperCase())"
|
|
27 >{{ data.summary["source-organization"] }} (LOS:
|
|
28 {{ data.summary.los }})</span
|
|
29 >
|
|
30 <a
|
|
31 href="#"
|
|
32 class="text-left"
|
|
33 @click="zoomToStretch(data.summary.stretch)"
|
|
34 v-if="isStretch(data.kind.toUpperCase())"
|
|
35 >{{ data.summary.stretch }}</a
|
|
36 >
|
|
37 </div>
|
|
38 <div class="mt-auto mb-auto small text-left type">
|
|
39 {{ data.kind.toUpperCase() }}
|
|
40 </div>
|
|
41 <div v-if="data.summary" class="mt-auto mb-auto small text-left date">
|
|
42 {{ formatSurveyDate(data.summary.date) }}
|
|
43 </div>
|
|
44 <div v-else class="mt-auto mb-auto small text-left date">-</div>
|
|
45 <div class="mt-auto mb-auto small text-left imported">
|
|
46 {{ formatSurveyDate(data.enqueued.split("T")[0]) }}
|
|
47 </div>
|
|
48 <div class="mt-auto mb-auto small text-left username">
|
|
49 {{ data.user }}
|
|
50 </div>
|
|
51 <div class="controls d-flex flex-row justify-content-end">
|
|
52 <div>
|
|
53 <button
|
|
54 :class="{
|
|
55 'ml-3': true,
|
|
56 'mr-3': true,
|
|
57 btn: true,
|
|
58 'btn-sm': true,
|
|
59 'btn-outline-success': needsApproval(data) || isRejected(data),
|
|
60 'btn-success': isApproved(data)
|
|
61 }"
|
|
62 @click="toggleApproval(data.id, $options.STATES.APPROVED)"
|
|
63 >
|
|
64 <font-awesome-icon icon="check"></font-awesome-icon>
|
|
65 </button>
|
|
66 </div>
|
|
67 <div>
|
|
68 <button
|
|
69 :class="{
|
|
70 'mr-3': true,
|
|
71 btn: true,
|
|
72 'btn-sm': true,
|
|
73 'btn-outline-danger': needsApproval(data) || isApproved(data),
|
|
74 'btn-danger': isRejected(data)
|
|
75 }"
|
|
76 @click="toggleApproval(data.id, $options.STATES.REJECTED)"
|
|
77 >
|
|
78 <font-awesome-icon icon="times" class="pointer"></font-awesome-icon>
|
|
79 </button>
|
|
80 </div>
|
|
81 <div
|
|
82 v-if="
|
|
83 !isBottleneck(data.kind.toUpperCase()) ||
|
|
84 isApprovedGaugeMeasurement(data.kind.toUpperCase())
|
|
85 "
|
|
86 class="expander"
|
|
87 ></div>
|
|
88 <div v-if="isBottleneck(data.kind.toUpperCase())">
|
|
89 <div class="mt-auto mb-auto text-info text-left">
|
|
90 <font-awesome-icon
|
|
91 class="pointer"
|
|
92 @click="showDetails()"
|
|
93 v-if="show"
|
|
94 icon="angle-up"
|
|
95 fixed-width
|
|
96 ></font-awesome-icon>
|
|
97 <font-awesome-icon
|
|
98 class="pointer"
|
|
99 @click="showDetails()"
|
|
100 v-if="loading"
|
|
101 icon="spinner"
|
|
102 fixed-width
|
|
103 ></font-awesome-icon>
|
|
104 <font-awesome-icon
|
|
105 @click="showDetails()"
|
|
106 class="pointer"
|
|
107 v-if="!show && !loading"
|
|
108 icon="angle-down"
|
|
109 fixed-width
|
|
110 ></font-awesome-icon>
|
|
111 </div>
|
|
112 </div>
|
|
113 <div v-if="isApprovedGaugeMeasurement(data.kind.toUpperCase())">
|
|
114 <div
|
|
115 @click="showAGMDetails = !showAGMDetails"
|
|
116 class="mt-auto mb-auto text-info text-left"
|
|
117 >
|
|
118 <font-awesome-icon
|
|
119 class="pointer"
|
|
120 v-if="showAGMDetails"
|
|
121 icon="angle-up"
|
|
122 fixed-width
|
|
123 ></font-awesome-icon>
|
|
124 <font-awesome-icon
|
|
125 class="pointer"
|
|
126 v-if="!showAGMDetails"
|
|
127 icon="angle-down"
|
|
128 fixed-width
|
|
129 ></font-awesome-icon>
|
|
130 </div>
|
|
131 </div>
|
|
132 <div v-else class="empty"></div>
|
|
133 </div>
|
|
134 </div>
|
|
135 <div v-if="show && bottlenecks.length > 0" class="bottlenecksdetails">
|
|
136 <div
|
|
137 v-for="(bottleneck, index) in bottlenecks"
|
|
138 :key="index"
|
|
139 class="d-flex flex-row"
|
|
140 >
|
|
141 <div class="d-flex flex-column">
|
|
142 <div class="d-flex flex-row">
|
|
143 <a @click="moveToBottleneck(index)" class="small" href="#">{{
|
|
144 bottleneck.properties.objnam
|
|
145 }}</a>
|
|
146 <div
|
|
147 @click="showBottleneckDetails(index)"
|
|
148 class="small mt-auto mb-auto text-info text-left"
|
|
149 >
|
|
150 <font-awesome-icon
|
|
151 class="pointer"
|
|
152 v-if="showBottleneckDetail === index"
|
|
153 icon="angle-up"
|
|
154 fixed-width
|
|
155 ></font-awesome-icon>
|
|
156 <font-awesome-icon
|
|
157 class="pointer"
|
|
158 v-if="!(showBottleneckDetail === index)"
|
|
159 icon="angle-down"
|
|
160 fixed-width
|
|
161 ></font-awesome-icon>
|
|
162 </div>
|
|
163 </div>
|
|
164
|
|
165 <div class="d-flex flex-row" v-if="showBottleneckDetail === index">
|
|
166 <table>
|
|
167 <tr
|
|
168 v-for="(info, index) in Object.keys(bottleneck.properties)"
|
|
169 :key="index"
|
|
170 class="mr-1 small text-muted"
|
|
171 >
|
|
172 <td class="condensed text-left">{{ info }}</td>
|
|
173 <td class="condensed pl-3 text-left">
|
|
174 {{ bottleneck.properties[info] }}
|
|
175 </td>
|
|
176 </tr>
|
|
177 </table>
|
|
178 </div>
|
|
179 </div>
|
|
180 </div>
|
|
181 </div>
|
|
182 <div v-if="showAGMDetails">
|
|
183 <div class="pl-3 d-flex flex-row">
|
|
184 <span class="condensed agmcode text-left"
|
|
185 ><small><translate>ISRS Code</translate></small></span
|
|
186 >
|
|
187 <span class="condensed agmdetail text-left"
|
|
188 ><small><translate>Date of measurement</translate></small></span
|
|
189 >
|
|
190 </div>
|
|
191 <div class="diffs">
|
|
192 <div v-for="(result, index) in data.summary" :key="index">
|
|
193 <div class="pl-3 d-flex flex-row">
|
|
194 <span
|
|
195 v-if="result.versions.length == 1"
|
|
196 class="condensed agmcode text-left"
|
|
197 ><small
|
|
198 >{{ result["fk-gauge-id"] }}
|
|
199 <translate>( New )</translate></small
|
|
200 ></span
|
|
201 >
|
|
202 <span
|
|
203 v-if="result.versions.length == 2"
|
|
204 class="condensed agmcode text-left"
|
|
205 ><small>{{ result["fk-gauge-id"] }}</small></span
|
|
206 >
|
|
207 <span class="condensed agmdetail text-left"
|
|
208 ><small>{{ formatDateTime(result["measure-date"]) }}</small></span
|
|
209 >
|
|
210 <div
|
|
211 @click="toggleDiff(index)"
|
|
212 class="small ml-auto mt-auto mb-auto text-info text-left"
|
|
213 >
|
|
214 <font-awesome-icon
|
|
215 class="pointer"
|
|
216 v-if="showDiff == index"
|
|
217 icon="angle-up"
|
|
218 fixed-width
|
|
219 ></font-awesome-icon>
|
|
220 <font-awesome-icon
|
|
221 class="pointer"
|
|
222 v-if="showDiff != index"
|
|
223 icon="angle-down"
|
|
224 fixed-width
|
|
225 ></font-awesome-icon>
|
|
226 </div>
|
|
227 </div>
|
|
228 <div v-if="showDiff == index" class="pl-3 d-flex flex-row">
|
|
229 <div>
|
|
230 <div class="d-flex flex-row condensed pl-3 text-left">
|
|
231 <div class="header border-bottom agmdetailskeys">
|
|
232 <small><translate>Value</translate></small>
|
|
233 </div>
|
|
234 <div
|
|
235 v-if="result.versions.length == 2"
|
|
236 class="header border-bottom agmdetailsvalues"
|
|
237 >
|
|
238 <small><translate>Old</translate></small>
|
|
239 </div>
|
|
240 <div class="header border-bottom agmdetailsvalues">
|
|
241 <small><translate>New</translate></small>
|
|
242 </div>
|
|
243 </div>
|
|
244 <div
|
|
245 class="d-flex flex-row condensed pl-3 text-left"
|
|
246 v-for="(entry, index) in Object.keys(result.versions[0])"
|
|
247 :key="index"
|
|
248 >
|
|
249 <div
|
|
250 v-if="
|
|
251 result.versions.length == 1 ||
|
|
252 result.versions[0][entry] != result.versions[1][entry]
|
|
253 "
|
|
254 class="agmdetailskeys"
|
|
255 >
|
|
256 <small>{{ entry }}</small>
|
|
257 </div>
|
|
258 <div
|
|
259 v-if="
|
|
260 result.versions.length == 1 ||
|
|
261 result.versions[0][entry] != result.versions[1][entry]
|
|
262 "
|
|
263 class="agmdetailsvalues"
|
|
264 >
|
|
265 <small>{{ result.versions[0][entry] }}</small>
|
|
266 </div>
|
|
267 <div
|
|
268 v-if="
|
|
269 result.versions.length == 2 &&
|
|
270 result.versions[0][entry] != result.versions[1][entry]
|
|
271 "
|
|
272 class="agmdetailsvalues"
|
|
273 >
|
|
274 <small>{{ result.versions[1][entry] }}</small>
|
|
275 </div>
|
|
276 </div>
|
|
277 </div>
|
|
278 </div>
|
|
279 </div>
|
|
280 </div>
|
|
281 </div>
|
|
282 </div>
|
|
283 </template>
|
|
284
|
|
285 <script>
|
|
286 /* This is Free Software under GNU Affero General Public License v >= 3.0
|
|
287 * without warranty, see README.md and license for details.
|
|
288 *
|
|
289 * SPDX-License-Identifier: AGPL-3.0-or-later
|
|
290 * License-Filename: LICENSES/AGPL-3.0.txt
|
|
291 *
|
|
292 * Copyright (C) 2018 by via donau
|
|
293 * – Österreichische Wasserstraßen-Gesellschaft mbH
|
|
294 * Software engineering by Intevation GmbH
|
|
295 *
|
|
296 * Author(s):
|
|
297 * Thomas Junk <thomas.junk@intevation.de>
|
|
298 */
|
|
299
|
|
300 import { formatSurveyDate, formatDateTime } from "@/lib/date.js";
|
|
301 import { STATES } from "@/store/imports.js";
|
|
302 import { HTTP } from "@/lib/http";
|
|
303 import { WFS } from "ol/format.js";
|
|
304 import { or as orFilter, equalTo as equalToFilter } from "ol/format/filter.js";
|
|
305 import { displayError } from "@/lib/errors.js";
|
|
306 import { mapState } from "vuex";
|
|
307 import { LAYERS } from "@/store/map.js";
|
|
308
|
|
309 const NO_DIFF = -1;
|
|
310 const NO_BOTTLENECK = -1;
|
|
311
|
|
312 export default {
|
|
313 name: "stagingdetail",
|
|
314 props: ["data"],
|
|
315 data() {
|
|
316 return {
|
|
317 showDiff: NO_DIFF,
|
|
318 showAGMDetails: false,
|
|
319 showBottleneckDetail: NO_BOTTLENECK,
|
|
320 show: false,
|
|
321 loading: false,
|
|
322 bottlenecks: []
|
|
323 };
|
|
324 },
|
|
325 mounted() {
|
|
326 this.bottlenecks = [];
|
|
327 const { id } = this.$route.params;
|
|
328 this.$store.commit("imports/setImportToReview", id);
|
|
329 if (this.open) this.showDetails();
|
|
330 },
|
|
331 computed: {
|
|
332 ...mapState("imports", ["importToReview"]),
|
|
333 open() {
|
|
334 return this.importToReview == this.data.id;
|
|
335 },
|
|
336 detail() {
|
|
337 return [
|
|
338 "pb-2",
|
|
339 "pt-2",
|
|
340 "d-flex",
|
|
341 "flex-column",
|
|
342 "w-100",
|
|
343 {
|
|
344 highlight: this.open && this.needsApproval(this.data)
|
|
345 }
|
|
346 ];
|
|
347 }
|
|
348 },
|
|
349 watch: {
|
|
350 showAGMDetails() {
|
|
351 if (!this.showAGMDetails) this.showDiff = NO_DIFF;
|
|
352 },
|
|
353 open() {
|
|
354 this.show = this.open;
|
|
355 },
|
|
356 $route() {
|
|
357 const { id } = this.$route.params;
|
|
358 this.$store.commit("imports/setImportToReview", id);
|
|
359 if (this.open) this.showDetails();
|
|
360 }
|
|
361 },
|
|
362 methods: {
|
|
363 showBottleneckDetails(index) {
|
|
364 if (index == this.showBottleneckDetail) {
|
|
365 this.showBottleneckDetail = NO_BOTTLENECK;
|
|
366 return;
|
|
367 }
|
|
368 this.showBottleneckDetail = index;
|
|
369 },
|
|
370 toggleDiff(number) {
|
|
371 if (this.showDiff !== number || this.showDiff == -1) {
|
|
372 this.showDiff = number;
|
|
373 } else {
|
|
374 this.showDiff = -1;
|
|
375 }
|
|
376 },
|
|
377 zoomToStretch(name) {
|
|
378 this.$store.commit("map/setLayerVisible", LAYERS.STRETCHES);
|
|
379 this.$store
|
|
380 .dispatch("imports/loadStretch", name)
|
|
381 .then(response => {
|
|
382 if (response.data.features.length < 1)
|
|
383 throw new Error("no feaures found for: " + name);
|
|
384 this.moveToExtent(response.data.features[0]);
|
|
385 })
|
|
386 .catch(error => {
|
|
387 console.log(error);
|
|
388 const { status, data } = error.response;
|
|
389 displayError({
|
|
390 title: this.$gettext("Backend Error"),
|
|
391 message: `${status}: ${data.message || data}`
|
|
392 });
|
|
393 });
|
|
394 },
|
|
395 showDetails() {
|
|
396 if (!this.isBottleneck(this.data.kind.toUpperCase())) return;
|
|
397 if (this.show) {
|
|
398 this.show = false;
|
|
399 return;
|
|
400 }
|
|
401 if (this.bottlenecks.length > 0) {
|
|
402 this.show = true;
|
|
403 return;
|
|
404 }
|
|
405 this.loading = true;
|
|
406 const generateFilter = () => {
|
|
407 const { bottlenecks } = this.data.summary;
|
|
408 if (bottlenecks.length === 1)
|
|
409 return equalToFilter("bottleneck_id", bottlenecks[0]);
|
|
410 const orExpressions = bottlenecks.map(x => {
|
|
411 return equalToFilter("bottleneck_id", x);
|
|
412 });
|
|
413 return orFilter(...orExpressions);
|
|
414 };
|
|
415 const filterExpression = generateFilter();
|
|
416 const bottleneckFeatureCollectionRequest = new WFS().writeGetFeature({
|
|
417 srsName: "EPSG:4326",
|
|
418 featureNS: "gemma",
|
|
419 featurePrefix: "gemma",
|
|
420 featureTypes: ["bottlenecks_geoserver"],
|
|
421 outputFormat: "application/json",
|
|
422 filter: filterExpression
|
|
423 });
|
|
424 HTTP.post(
|
|
425 "/internal/wfs",
|
|
426 new XMLSerializer().serializeToString(
|
|
427 bottleneckFeatureCollectionRequest
|
|
428 ),
|
|
429 {
|
|
430 headers: {
|
|
431 "X-Gemma-Auth": localStorage.getItem("token"),
|
|
432 "Content-type": "text/xml; charset=UTF-8"
|
|
433 }
|
|
434 }
|
|
435 )
|
|
436 .then(response => {
|
|
437 this.bottlenecks = response.data.features;
|
|
438 this.show = true;
|
|
439 this.loading = false;
|
|
440 })
|
|
441 .catch(error => {
|
|
442 const { status, data } = error.response;
|
|
443 displayError({
|
|
444 title: this.$gettext("Backend Error"),
|
|
445 message: `${status}: ${data.message || data}`
|
|
446 });
|
|
447 });
|
|
448 },
|
|
449 isFairwayDimension(kind) {
|
|
450 return kind === "FD";
|
|
451 },
|
|
452 isApprovedGaugeMeasurement(kind) {
|
|
453 return kind === "AGM";
|
|
454 },
|
|
455 isBottleneck(kind) {
|
|
456 return kind === "BN" || kind === "UBN";
|
|
457 },
|
|
458 isStretch(kind) {
|
|
459 return kind === "ST";
|
|
460 },
|
|
461 isSoundingResult(kind) {
|
|
462 return kind === "SR";
|
|
463 },
|
|
464 formatSurveyDate(date) {
|
|
465 return formatSurveyDate(date);
|
|
466 },
|
|
467 formatDateTime(date) {
|
|
468 return formatDateTime(date);
|
|
469 },
|
|
470 needsApproval(item) {
|
|
471 return item.status === STATES.NEEDSAPPROVAL;
|
|
472 },
|
|
473 isRejected(item) {
|
|
474 return item.status === STATES.REJECTED;
|
|
475 },
|
|
476 isApproved(item) {
|
|
477 return item.status === STATES.APPROVED;
|
|
478 },
|
|
479 moveToBottleneck(index) {
|
|
480 this.$store.commit("map/setLayerVisible", LAYERS.BOTTLENECKS);
|
|
481 this.moveToExtent(this.bottlenecks[index]);
|
|
482 },
|
|
483 moveToExtent(feature) {
|
|
484 this.$store.commit("map/moveToExtent", {
|
|
485 feature: feature,
|
|
486 zoom: 17,
|
|
487 preventZoomOut: true
|
|
488 });
|
|
489 },
|
|
490 moveMap(coordinates) {
|
|
491 this.$store.commit("map/moveMap", {
|
|
492 coordinates: coordinates,
|
|
493 zoom: 17,
|
|
494 preventZoomOut: true
|
|
495 });
|
|
496 },
|
|
497 zoomTo() {
|
|
498 const { lat, lon, bottleneck, date } = this.data.summary;
|
|
499 const coordinates = [lat, lon];
|
|
500 this.moveMap(coordinates);
|
|
501 this.$store
|
|
502 .dispatch("bottlenecks/setSelectedBottleneck", bottleneck)
|
|
503 .then(() => {
|
|
504 this.$store.commit("bottlenecks/setSelectedSurveyByDate", date);
|
|
505 });
|
|
506 },
|
|
507 toggleApproval(id, newStatus) {
|
|
508 this.$store.commit("imports/toggleApproval", {
|
|
509 id: id,
|
|
510 newStatus: newStatus
|
|
511 });
|
|
512 }
|
|
513 },
|
|
514 STATES: STATES
|
|
515 };
|
|
516 </script>
|
|
517
|
|
518 <style lang="scss" scoped></style>
|