|
|
@@ -0,0 +1,298 @@
|
|
|
+<template>
|
|
|
+ <q-dialog ref="dialogRef" @hide="onDialogHide">
|
|
|
+ <q-card class="q-dialog-plugin" style="width: 800px; max-width: 90vw">
|
|
|
+ <DefaultDialogHeader :title="title" @close="onDialogCancel" />
|
|
|
+ <q-form ref="formRef" @submit="onOKClick">
|
|
|
+ <q-card-section class="row q-col-gutter-sm">
|
|
|
+
|
|
|
+ <q-select
|
|
|
+ v-if="!review"
|
|
|
+ v-model="selectedSchedule"
|
|
|
+ :label="$t('reviews.schedule')"
|
|
|
+ :options="scheduleOptions"
|
|
|
+ :rules="[inputRules.required]"
|
|
|
+ :error="!!serverErrors?.schedule_id"
|
|
|
+ :error-message="serverErrors?.schedule_id"
|
|
|
+ :loading="loadingSchedules"
|
|
|
+ option-value="value"
|
|
|
+ option-label="label"
|
|
|
+ emit-value
|
|
|
+ map-options
|
|
|
+ class="col-12"
|
|
|
+ @update:model-value="onScheduleChange"
|
|
|
+ />
|
|
|
+
|
|
|
+ <q-input
|
|
|
+ v-else
|
|
|
+ :model-value="review.schedule_label"
|
|
|
+ :label="$t('reviews.schedule')"
|
|
|
+ readonly
|
|
|
+ class="col-12"
|
|
|
+ />
|
|
|
+
|
|
|
+ <q-select
|
|
|
+ v-if="!review"
|
|
|
+ v-model="form.origin"
|
|
|
+ :label="$t('reviews.origin')"
|
|
|
+ :options="originOptions"
|
|
|
+ :rules="[inputRules.required]"
|
|
|
+ :error="!!serverErrors?.origin"
|
|
|
+ :error-message="serverErrors?.origin"
|
|
|
+ emit-value
|
|
|
+ map-options
|
|
|
+ class="col-12 col-md-6"
|
|
|
+ @update:model-value="onOriginChange"
|
|
|
+ />
|
|
|
+
|
|
|
+ <q-input
|
|
|
+ v-else
|
|
|
+ :model-value="$t(`reviews.origins.${review.origin}`)"
|
|
|
+ :label="$t('reviews.origin')"
|
|
|
+ readonly
|
|
|
+ class="col-12 col-md-6"
|
|
|
+ />
|
|
|
+
|
|
|
+ <template v-if="!review">
|
|
|
+ <ProviderSelect
|
|
|
+ v-if="form.origin === 'providers'"
|
|
|
+ :key="'provider-select'"
|
|
|
+ v-model="selectedOriginModel"
|
|
|
+ :label="$t('reviews.origin_id')"
|
|
|
+ :rules="[inputRules.required]"
|
|
|
+ :error="!!serverErrors?.origin_id"
|
|
|
+ :error-message="serverErrors?.origin_id"
|
|
|
+ class="col-12 col-md-6"
|
|
|
+ @update:model-value="onOriginIdChange"
|
|
|
+ />
|
|
|
+ <ClientSelect
|
|
|
+ v-else-if="form.origin === 'clients'"
|
|
|
+ :key="'client-select'"
|
|
|
+ v-model="selectedOriginModel"
|
|
|
+ :label="$t('reviews.origin_id')"
|
|
|
+ :rules="[inputRules.required]"
|
|
|
+ :error="!!serverErrors?.origin_id"
|
|
|
+ :error-message="serverErrors?.origin_id"
|
|
|
+ class="col-12 col-md-6"
|
|
|
+ @update:model-value="onOriginIdChange"
|
|
|
+ />
|
|
|
+ <q-input
|
|
|
+ v-else
|
|
|
+ :label="$t('reviews.origin_id')"
|
|
|
+ readonly
|
|
|
+ :placeholder="$t('reviews.origin')"
|
|
|
+ class="col-12 col-md-6"
|
|
|
+ disable
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div class="col-12">
|
|
|
+ <div class="text-caption q-mb-xs">{{ $t('reviews.stars') }}</div>
|
|
|
+ <q-rating
|
|
|
+ v-model="form.stars"
|
|
|
+ :max="5"
|
|
|
+ size="md"
|
|
|
+ color="amber"
|
|
|
+ icon="star_border"
|
|
|
+ icon-selected="star"
|
|
|
+ class="q-mb-xs"
|
|
|
+ />
|
|
|
+ <div
|
|
|
+ v-if="serverErrors?.stars"
|
|
|
+ class="text-negative text-caption"
|
|
|
+ >
|
|
|
+ {{ serverErrors.stars }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <q-input
|
|
|
+ v-model="form.comment"
|
|
|
+ :label="$t('reviews.comment')"
|
|
|
+ :error="!!serverErrors?.comment"
|
|
|
+ :error-message="serverErrors?.comment"
|
|
|
+ type="textarea"
|
|
|
+ autogrow
|
|
|
+ class="col-12"
|
|
|
+ @update:model-value="serverErrors.comment = null"
|
|
|
+ />
|
|
|
+
|
|
|
+ <q-select
|
|
|
+ v-if="!review"
|
|
|
+ v-model="selectedImprovements"
|
|
|
+ :label="$t('reviews.improvements')"
|
|
|
+ :options="improvementOptions"
|
|
|
+ :loading="loadingImprovements"
|
|
|
+ option-value="value"
|
|
|
+ option-label="label"
|
|
|
+ emit-value
|
|
|
+ map-options
|
|
|
+ multiple
|
|
|
+ use-chips
|
|
|
+ class="col-12"
|
|
|
+ @update:model-value="($event) => attImprovementsOnForm($event)"
|
|
|
+ />
|
|
|
+ <div v-else>
|
|
|
+ <div>{{reviewsImprovements?.length > 0 ? t('reviews.improvements_suggested') : t('reviews.no_improvements_suggested')}}</div>
|
|
|
+ <q-chip
|
|
|
+ v-for="improvement in reviewsImprovements"
|
|
|
+ :key="improvement.id"
|
|
|
+ class="q-mb-xs"
|
|
|
+ >
|
|
|
+ {{ improvement.description }}
|
|
|
+ </q-chip>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ </q-card-section>
|
|
|
+
|
|
|
+ <q-card-actions align="right" class="q-px-md q-pb-md">
|
|
|
+ <q-btn
|
|
|
+ flat
|
|
|
+ :label="$t('common.actions.cancel')"
|
|
|
+ color="negative"
|
|
|
+ @click="onDialogCancel"
|
|
|
+ />
|
|
|
+ <q-btn
|
|
|
+ type="submit"
|
|
|
+ :label="$t('common.actions.save')"
|
|
|
+ :loading="loading"
|
|
|
+ :disable="!hasUpdatedFields"
|
|
|
+ color="primary"
|
|
|
+ unelevated
|
|
|
+ />
|
|
|
+ </q-card-actions>
|
|
|
+ </q-form>
|
|
|
+ </q-card>
|
|
|
+ </q-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed, onMounted } from 'vue'
|
|
|
+import { useDialogPluginComponent } from 'quasar'
|
|
|
+import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker'
|
|
|
+import { useSubmitHandler } from 'src/composables/useSubmitHandler'
|
|
|
+import { createReview, updateReview, getFinishedSchedules } from 'src/api/review'
|
|
|
+import { createReviewImprovement } from 'src/api/reviewImprovement'
|
|
|
+import { getImprovementTypes } from 'src/api/improvementType'
|
|
|
+import DefaultDialogHeader from 'src/components/defaults/DefaultDialogHeader.vue'
|
|
|
+import ProviderSelect from 'src/components/provider/ProviderSelect.vue'
|
|
|
+import ClientSelect from 'src/components/client/ClientSelect.vue'
|
|
|
+import { useInputRules } from 'src/composables/useInputRules'
|
|
|
+import { useI18n } from 'vue-i18n'
|
|
|
+import { format, parseISO } from 'date-fns'
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ review: {
|
|
|
+ type: Object,
|
|
|
+ default: null
|
|
|
+ },
|
|
|
+ title: {
|
|
|
+ type: Function,
|
|
|
+ default: () => ''
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+defineEmits([...useDialogPluginComponent.emits])
|
|
|
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
|
|
|
+const { inputRules } = useInputRules()
|
|
|
+const { t } = useI18n()
|
|
|
+const formRef = ref(null)
|
|
|
+
|
|
|
+const selectedSchedule = ref(props.review ? props.review.schedule_id : null)
|
|
|
+const selectedOriginModel = ref(null)
|
|
|
+const selectedImprovements = ref([])
|
|
|
+
|
|
|
+const scheduleOptions = ref([])
|
|
|
+const improvementOptions = ref([])
|
|
|
+const loadingSchedules = ref(false)
|
|
|
+const loadingImprovements = ref(false)
|
|
|
+const reviewsImprovements = ref([])
|
|
|
+
|
|
|
+const originOptions = computed(() => [
|
|
|
+ { label: t('reviews.origins.providers'), value: 'providers' },
|
|
|
+ { label: t('reviews.origins.clients'), value: 'clients' }
|
|
|
+])
|
|
|
+
|
|
|
+const { form, hasUpdatedFields } = useFormUpdateTracker({
|
|
|
+ schedule_id: props.review ? props.review.schedule_id : null,
|
|
|
+ origin: props.review ? props.review.origin : null,
|
|
|
+ origin_id: props.review ? props.review.origin_id : null,
|
|
|
+ stars: props.review ? Number(props.review.stars) : 0,
|
|
|
+ comment: props.review ? props.review.comment : null,
|
|
|
+ improvements_ids: props.review ? props.review.improvements?.map(i => i.id) : [],
|
|
|
+})
|
|
|
+
|
|
|
+const {
|
|
|
+ loading,
|
|
|
+ serverErrors,
|
|
|
+ execute: submitForm,
|
|
|
+} = useSubmitHandler({
|
|
|
+ onSuccess: () => onDialogOK(true),
|
|
|
+ formRef: formRef,
|
|
|
+})
|
|
|
+
|
|
|
+const onScheduleChange = (val) => {
|
|
|
+ form.schedule_id = val
|
|
|
+ serverErrors.schedule_id = null
|
|
|
+}
|
|
|
+
|
|
|
+const onOriginChange = () => {
|
|
|
+ selectedOriginModel.value = null
|
|
|
+ form.origin_id = null
|
|
|
+ serverErrors.origin = null
|
|
|
+ serverErrors.origin_id = null
|
|
|
+}
|
|
|
+
|
|
|
+const onOriginIdChange = (selected) => {
|
|
|
+ form.origin_id = selected?.value ?? null
|
|
|
+ serverErrors.origin_id = null
|
|
|
+}
|
|
|
+
|
|
|
+const onOKClick = async () => {
|
|
|
+ await submitForm(async () => {
|
|
|
+ if (props.review) {
|
|
|
+ return updateReview(props.review.id, {
|
|
|
+ stars: form.stars,
|
|
|
+ comment: form.comment,
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ const created = await createReview({ ...form })
|
|
|
+ for (const improvementTypeId of selectedImprovements.value) {
|
|
|
+ await createReviewImprovement({
|
|
|
+ review_id: created.id,
|
|
|
+ improvement_type_id: improvementTypeId,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return created
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const attImprovementsOnForm = (event) => {
|
|
|
+ form.improvements_ids = event;
|
|
|
+ serverErrors.improvements_ids = null;
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ loadingSchedules.value = true
|
|
|
+ loadingImprovements.value = true
|
|
|
+ try {
|
|
|
+ const [schedules, improvements] = await Promise.all([
|
|
|
+ getFinishedSchedules(),
|
|
|
+ getImprovementTypes(),
|
|
|
+ ])
|
|
|
+ scheduleOptions.value = schedules.map((s) => ({
|
|
|
+ value: s.id,
|
|
|
+ label: `${s.id} - ${s.client_name ?? '?'} - ${s.provider_name ?? '?'} - ${s.date ? format(parseISO(s.date), 'dd/MM/yyyy') : '?'}`,
|
|
|
+ }))
|
|
|
+ improvementOptions.value = improvements.map((i) => ({
|
|
|
+ value: i.id,
|
|
|
+ label: i.description,
|
|
|
+ }))
|
|
|
+ reviewsImprovements.value = props.review?.reviews_improvements;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error loading data:', error)
|
|
|
+ } finally {
|
|
|
+ loadingSchedules.value = false
|
|
|
+ loadingImprovements.value = false
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|