<template>
<q-card class="q-pt-none">
<q-card-actions align="right" class="q-py-none">
<q-btn v-if="$store.formChanged" flat color="positive" :label="$t('Save')" @click="save" />
<q-btn flat color="negative" :label="$store.formChanged ? $t('Cancel') : $t('Close')" @click="cancel" />
</q-card-actions>
<q-card-section class="q-py-none">
<q-input dense outlined v-model="o.name" :label="$t('Location name')" style="width:300px"/>
<div class = "row">
<q-btn v-if="hasFeatureStatsButton" icon="bar_chart" flat dense @click="parent.showFeatureStats(false)" :label="$t('Statistics')" no-caps>
<q-tooltip>{{ $t("Show statistics for this feature") }}</q-tooltip>
</q-btn>
<q-btn v-if="hasFeatureStatsButton" icon="compare" flat dense @click="parent.showFeatureStats(true)" :label="$t('Compare')" no-caps>
<q-tooltip>{{ $t("Compare statistics for this feature with containing geometries") }}</q-tooltip>
</q-btn>
<q-btn v-if="hasHistoryButton" icon="show_chart" flat dense @click="parent.showHistory()" :label="$t('History')" no-caps>
<q-tooltip>{{ $t("Show history for this feature") }}</q-tooltip>
</q-btn>
</div>
<hr class="hr2">
<div class="text-weight-bold">{{ $t("Observations") }}</div>
<q-tabs dense align="left" no-caps outside-arrows v-model="activeTab" indicator-color="positive">
<q-tab dense v-for="so of o.simple_observations" :key="so.id" :name="tabName(so.id)" :disable="$store.formChanged">
<div class="row items-center no-wrap q-gutter-sm">
<span v-html="(so.indicator ?? '') + '<br/>' + (so.id ? so.date : 'New')" style="max-width:90px; white-space: normal; word-wrap: break-word;"/>
</div>
</q-tab>
</q-tabs>
<q-tab-panels v-model="activeTab">
<q-tab-panel v-for="so of o.simple_observations" :key="so.id" :name="tabName(so.id)">
<q-form :ref="'qform' + tabName(so.id)">
<div class="row justify-between">
<q-input :rules="$store.rules.required" class="q-pb-xs" type="date" dense outlined v-model="so.date" :label="$t('Date')" />
<q-btn :disable="$store.formChanged" dense flat v-if="so.id" icon="delete" color="negative" @click="deleteObservation(so.id)" :label="$t('Delete this observation')" no-caps/>
</div>
<autocomplete :rules="$store.rules.required" class="q-pb-xs" v-model="so.indicator_id" :label="$t('Type')" map-options emit-value :lookup="simple_observation_lookup" :clearable="false" @update:model-value="selectedTypeChanged" hide-bottom-space/>
<!-- <q-input :rules="$store.rules.required" class="q-pb-xs" dense outlined v-model="so.name" :label="$t('Name')" /> -->
<div class="row" v-if="selectedType.numerical" >
<q-input type="number" class="q-pb-xs" dense outlined v-model="so.value" :label="$t('Value')" style="width:150px" :mask="mask" reverse-fill-mask :rules="[...rule, ...$store.rules.required]" hide-bottom-space/>
<span class="q-ml-sm">{{ selectedType.unit }}</span>
</div>
<q-input class="q-pb-xs" type="textarea" dense outlined v-model="so.comment" :label="$t('Comment')" rows="2"/>
</q-form>
<hr class="hr2">
<div class="text-weight-bold">
{{ $t("Images") }}
</div>
<div>
<span class="q-ml-sm">{{ $t("Use ") }}</span>
<q-radio v-model="coordType" val="gps" :label="$t('GPS')"><q-tooltip>{{ $t("Set image position to GPS coordinates") }}</q-tooltip></q-radio>
<q-radio v-model="coordType" val="loc" :label="$t('Location')"><q-tooltip>{{ $t("Set image position to location coordinates") }}</q-tooltip></q-radio>
<q-btn class="q-ml-lg" flat color="positive" icon="photo_camera" @click="addImage" :label="$t('Add image')" no-caps>
<q-tooltip>{{ $t("Select or take a new image") }}</q-tooltip>
</q-btn>
</div>
<div v-if="so.images.length == 0">{{ $t("No images") }}</div>
<q-markup-table class="q-pa-none q-ma-none" dense flat v-if="so.images.length > 0">
<tbody>
<template v-for="(f, index) of so.images" :key="f.id">
<tr>
<td class="text-left" width="60px">
<q-btn :disable="$store.formChanged" class="q-py-none q-pr-sm" dense flat icon="edit" @click="editImage(f)">
<q-tooltip>{{ $t("Edit properties") }}</q-tooltip>
</q-btn>
<q-btn :disable="$store.formChanged" class="q-py-none q-pr-sm" dense flat icon="visibility" @click="showImage(index)">
<q-tooltip>{{ $t("Show image") }}</q-tooltip>
</q-btn>
<q-btn :disable="$store.formChanged" class="q-py-none q-pr-sm" dense flat color="negative" @click="deleteImage(f)"
icon="delete">
<q-tooltip>{{ $t("Delete image") }}</q-tooltip>
</q-btn></td>
<td @click="showImage(index)" class="q-py-none text-left"
style="max-width:300px; overflow: hidden; cursor: pointer;">
{{ f.name }}</td>
</tr>
</template>
</tbody>
</q-markup-table>
</q-tab-panel>
</q-tab-panels>
</q-card-section>
<q-uploader v-show="false" ref="uploader" :url="uploadURL" accept="image/*,text/plain" :form-fields="formFields" :upload-factory="customUploadFactory"
@uploaded="filesUploaded" @failed="filesFailed" @added="filesAdded" :headers="[{ name: 'Authorization', value: 'Bearer ' + this.$keycloak.token }]"
/>
<q-dialog v-model="editingImage" persistent style="z-index: 9999;">
<q-card>
<q-card-section>
<q-input v-model="editedImage.name" dense :label="$t('Name')" style="width:300px;" />
<div class="row">
<q-input v-model="editedImage.compass" dense :label="$t('Compass')" style="width:100px;" />
<q-input v-model="editedImage.lon" dense :label="$t('Longitude')" style="width:120px;" />
<q-input v-model="editedImage.lat" dense :label="$t('Latitude')" style="width:120px;" />
</div>
<div class="row">
<q-btn no-caps flat :icon="$icons.my_location" @click="setGPSPosition"
:label="$t('Set GPS position')"></q-btn>
<q-btn no-caps flat icon="location_on" @click="setLocationPosition"
:label="$t('Set location position')"></q-btn>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn v-if="imageChanged" flat color="positive" @click="saveImage" :label="$t('Save')"></q-btn>
<q-btn flat color="negative" @click="cancelImage" :label="imageChanged ? $t('Cancel') : $t('Close')"></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
</q-card>
</template>
<script>
/**
* SimpleObservation component
* @component components/simple-observation
* @description SimpleObservation component
* @example
* <SimpleObservation />
*/
import { loadComponent } from '@/common/component-loader';
import { SimpleObservationMixin } from '../mixins/simple-observation';
//import eventBus from '@/common/event-bus';
export default {
name: "SimpleObservation",
mixins: [SimpleObservationMixin],
components: {
autocomplete: loadComponent('autocomplete'),
},
props: ["parentPopup"],
data() {
return {
simple_observation_lookup: {
refTable: 'catalog_simple_observation'
},
o: {},
oSav: {},
activeSO: {},
activeTab: null,
editIndex: false,
editedImage: null,
imageSav: {},
editingImage: false,
coordType: "gps",
selectedType: {},
mask: null,
rule: null,
parent: null
};
},
computed: {
uploadURL: function () {
// return this.axios.API.defaults.baseURL + 'User/UploadImage/' + this.activeSO.id;
return 'User/UploadImage/' + this.activeSO.id;
},
imageChanged: function () {
return !this.equalObjects(this.editedImage, this.imageSav);
},
/**
* Checks should we display feature stats button
*
* @returns {boolean} True if the feature has a history button, false otherwise.
*/
hasFeatureStatsButton() {
return this.parent && this.parent.selectedTitle && this.parent.selectedFeature.get("key") && this.parent.selectedFeature.getGeometry().getType() != "Point";
},
/**
* Checks should we display feature history button
*
* @returns {boolean} True if the feature has a history button, false otherwise.
*/
hasHistoryButton() {
return this.parent && this.parent.selectedTitle && this.parent.selectedFeature.get("key") && this.parent.selectedFeature.getGeometry().getType() == "Point";
},
},
watch: {
o: {
handler: function (val) {
this.$store.formChanged = !this.deepEqualObjects(this.o, this.oSav);
},
deep: true,
},
activeTab: {
handler: function (val) {
let search = val;
if (val == "new") {
search = null;
} else {
search = val;
}
this.activeSO = this.o.simple_observations.filter (so => so.id == search)[0];
}
},
},
async mounted() {
this.initializeComponent(this.parentPopup);
await this.loadLookup(this.simple_observation_lookup, true);
if (this.id != null) {
this.o = await this.get("User/GetSingleGeometry", { id: this.id }, true);
} else {
this.o = { id: null, name: "New " + this.formatDate(new Date()) };
}
if (!this.o.simple_observations) this.o.simple_observations = [];
this.o.simple_observations.push ({id : null, date: this.defaultDate(), name: null, indicator_id: null, comment: null, images: []});
for (let so of this.o.simple_observations) {
if (!so.images) so.images = [];
}
this.activeTab = this.tabName(this.o.simple_observations[0].id);
await this.$nextTick();
this.selectedTypeChanged();
window.addEventListener('deviceorientationabsolute', this.getCompass);
//eventBus.on('popupClosed', this.popupClosed);
this.oSav = this.deepClone(this.o);
this.$store.formChanged = false;
},
unmounted() {
//eventBus.off('popupClosed');
window.removeEventListener('deviceorientationabsolute', this.getCompass);
},
methods: {
selectedTypeChanged() {
this.selectedType = this.simple_observation_lookup.options.filter(x => x.id == this.activeSO.indicator_id)[0] ?? {};
this.activeSO.indicator = this.selectedType.name;
if (!this.selectedType.numerical) {
this.activeSO.value = null;
this.mask = null;
} else {
this.mask = this.createMask(this.selectedType.decimals_for_display);
}
this.rule = this.createBetweenRule(this.selectedType.value_from, this.selectedType.value_to);
},
// async close() {
// if (this.$store.formChanged) {
// if (await this.confirmDialog(this.$t("All changes will be lost. Do you want to continue?"))) {
// this.$store.formChanged = false;
// this.cancel();
// }
// } else {
// this.closePopup();
// }
// console.log("parent", this.parent);
// if (this.parent.reload) {
// await this.parent.reload();
// }
// },
async customUploadFactory(files) {
// Polja iz `form-fields` koja želite dodati uz upload
const extraFields = this.formFields.reduce((acc, field) => {
acc[field.name] = field.value;
return acc;
}, {});
const headers = {
'Content-Type': 'multipart/form-data',
Authorization: 'Bearer ' + this.$keycloak.token, // Dodavanje autentifikacijskog tokena
};
// Kreiramo niz Promise-ova za svaki upload
const uploadPromises = files.map((file) => {
// Kreiramo formData koji uključuje datoteku i dodatna polja
let formData = new FormData();
formData.append('file', file);
// Dodajemo sva dodatna polja u formData
Object.keys(extraFields).forEach((key) => {
formData.append(key, extraFields[key]);
});
// Kreiramo i vraćamo axios post zahtjev
return this.post(this.uploadURL, formData, true
// , {
// headers: headers,
// onUploadProgress: (progressEvent) => {
// // Praćenje napretka uploada (ako želite)
// const percentCompleted = Math.round(
// (progressEvent.loaded * 100) / progressEvent.total
// );
// console.log(`Upload progress for ${file.name}: ${percentCompleted}%`);
// },
// }
);
});
try {
// Čekamo da se svi uploadi završe
const responses = await Promise.all(uploadPromises);
// Ako želite da Q-Uploader vidi da su svi prošli, možete vratiti uspjeh
return { success: true };
} catch (error) {
console.error("Error uploading files:", error);
// Ako želite da Q-Uploader vidi grešku, možete je ovako vratiti
throw new Error("Failed to upload files");
}
},
},
};
</script>