|
|
@@ -0,0 +1,252 @@
|
|
|
+<template>
|
|
|
+ <q-dialog ref="dialogRef" @hide="onDialogHide">
|
|
|
+ <q-card class="rating-dialog-card bg-surface shadow-card" :flat="false">
|
|
|
+
|
|
|
+ <div class="row justify-end q-pt-sm q-pr-sm">
|
|
|
+ <q-btn flat round dense icon="close" color="grey-6" size="sm" @click="onDialogCancel" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Avatar -->
|
|
|
+ <div class="column items-center q-pb-sm">
|
|
|
+ <q-avatar size="64px" class="q-mb-sm">
|
|
|
+ <span
|
|
|
+ :style="avatarStyle"
|
|
|
+ class="text-weight-bold full-width full-height flex flex-center"
|
|
|
+ style="font-size: 20px; border-radius: 50%;"
|
|
|
+ >
|
|
|
+ {{ initials }}
|
|
|
+ </span>
|
|
|
+ </q-avatar>
|
|
|
+ <div class="text-body1 text-text text-weight-bold text-center q-px-lg" style="line-height:1.3">
|
|
|
+ {{ $t('provider.dashboard.schedule_rating.title') }}
|
|
|
+ <span class="text-primary"> {{ schedule.client_name + '?' }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Estrelas -->
|
|
|
+ <div class="column items-center q-pb-xs">
|
|
|
+ <q-rating
|
|
|
+ v-model="stars"
|
|
|
+ :max="5"
|
|
|
+ size="lg"
|
|
|
+ color="amber"
|
|
|
+ icon="mdi-star-outline"
|
|
|
+ icon-selected="mdi-star"
|
|
|
+ @update:model-value="onStarsChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Tags de melhoria/qualidade -->
|
|
|
+ <q-card-section v-if="stars > 0" class="q-pt-xs q-pb-xs">
|
|
|
+ <div class="text-caption text-grey-7 text-center q-mb-sm">
|
|
|
+ {{ isNegative ? $t('provider.dashboard.schedule_rating.negative_label') : $t('provider.dashboard.schedule_rating.positive_label') }}
|
|
|
+ </div>
|
|
|
+ <div v-if="loadingTags" class="row justify-center q-py-sm">
|
|
|
+ <q-spinner-dots color="primary" size="24px" />
|
|
|
+ </div>
|
|
|
+ <div v-else class="row justify-center q-gutter-xs">
|
|
|
+ <div
|
|
|
+ v-for="tag in tags"
|
|
|
+ :key="tag.id"
|
|
|
+ class="tag-pill"
|
|
|
+ :class="{ 'tag-pill--selected': selectedTagIds.includes(tag.id) }"
|
|
|
+ @click="toggleTag(tag.id)"
|
|
|
+ >
|
|
|
+ {{ tag.description }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </q-card-section>
|
|
|
+
|
|
|
+ <!-- Comentário -->
|
|
|
+ <q-card-section class="q-pt-xs q-pb-xs q-px-lg">
|
|
|
+ <div class="text-caption text-grey-7 q-mb-xs">
|
|
|
+ {{ $t('provider.dashboard.schedule_rating.comment_placeholder') }}
|
|
|
+ </div>
|
|
|
+ <q-input
|
|
|
+ v-model="comment"
|
|
|
+ type="textarea"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ rows="3"
|
|
|
+ color="primary"
|
|
|
+ input-class="text-black"
|
|
|
+ hide-bottom-space
|
|
|
+ />
|
|
|
+ </q-card-section>
|
|
|
+
|
|
|
+ <!-- Checkbox — apenas para avaliação negativa -->
|
|
|
+ <q-card-section v-if="isNegative" class="q-pt-xs q-pb-xs q-px-lg">
|
|
|
+ <q-checkbox
|
|
|
+ v-model="blockClient"
|
|
|
+ :label="$t('provider.dashboard.schedule_rating.block_label')"
|
|
|
+ color="primary"
|
|
|
+ keep-color
|
|
|
+ class="text-text"
|
|
|
+ checked-icon="mdi-check-circle"
|
|
|
+ unchecked-icon="mdi-checkbox-blank-circle-outline"
|
|
|
+ />
|
|
|
+ </q-card-section>
|
|
|
+
|
|
|
+ <!-- Botão enviar -->
|
|
|
+ <q-card-section class="q-pt-sm q-pb-xs q-px-lg row">
|
|
|
+ <q-btn
|
|
|
+ unelevated
|
|
|
+ rounded
|
|
|
+ no-caps
|
|
|
+ full-width
|
|
|
+ color="primary"
|
|
|
+ class="submit-btn col-12"
|
|
|
+ :label="$t('provider.dashboard.schedule_rating.submit_btn')"
|
|
|
+ :loading="loading"
|
|
|
+ :disable="stars === 0"
|
|
|
+ @click="submit"
|
|
|
+ />
|
|
|
+ </q-card-section>
|
|
|
+
|
|
|
+ <!-- Ajuda -->
|
|
|
+ <q-card-section class="q-pt-xs q-pb-lg text-center">
|
|
|
+ <span class="text-caption text-grey-6 cursor-pointer" @click="openHelp">
|
|
|
+ {{ $t('provider.dashboard.schedule_rating.help_link') }}
|
|
|
+ </span>
|
|
|
+ </q-card-section>
|
|
|
+
|
|
|
+ </q-card>
|
|
|
+ </q-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed, onMounted } from 'vue'
|
|
|
+import { useDialogPluginComponent, useQuasar } from 'quasar'
|
|
|
+import { useI18n } from 'vue-i18n'
|
|
|
+import { createReview, getImprovementTypes } from 'src/api/review'
|
|
|
+import { userStore } from 'src/stores/user'
|
|
|
+import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ schedule: {
|
|
|
+ type: Object,
|
|
|
+ required: true
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+defineEmits([...useDialogPluginComponent.emits])
|
|
|
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
|
|
|
+const { t } = useI18n()
|
|
|
+const $q = useQuasar()
|
|
|
+const store = userStore()
|
|
|
+
|
|
|
+const stars = ref(0)
|
|
|
+const selectedTagIds = ref([])
|
|
|
+const comment = ref(null)
|
|
|
+const blockClient = ref(false)
|
|
|
+const tags = ref([])
|
|
|
+const loadingTags = ref(false)
|
|
|
+const loading = ref(false)
|
|
|
+
|
|
|
+const isNegative = computed(() => stars.value > 0 && stars.value <= 2)
|
|
|
+const isPositive = computed(() => stars.value >= 3)
|
|
|
+
|
|
|
+const avatarColors = [
|
|
|
+ { background: '#ffd5df', color: '#932e57' },
|
|
|
+ { background: '#d7e8ff', color: '#2158a8' },
|
|
|
+ { background: '#dfd', color: '#2a7a3b' },
|
|
|
+ { background: '#ffe5cc', color: '#8a4500' },
|
|
|
+]
|
|
|
+
|
|
|
+const avatarStyle = computed(() => {
|
|
|
+ const c = avatarColors[props.schedule.client_id % avatarColors.length]
|
|
|
+ return { background: c.background, color: c.color }
|
|
|
+})
|
|
|
+
|
|
|
+const initials = computed(() =>
|
|
|
+ props.schedule.client_name?.slice(0, 2).toUpperCase() ?? '??'
|
|
|
+)
|
|
|
+
|
|
|
+const onStarsChange = () => {
|
|
|
+ selectedTagIds.value = []
|
|
|
+ blockClient.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const toggleTag = (id) => {
|
|
|
+ const idx = selectedTagIds.value.indexOf(id)
|
|
|
+ if (idx === -1) selectedTagIds.value.push(id)
|
|
|
+ else selectedTagIds.value.splice(idx, 1)
|
|
|
+}
|
|
|
+
|
|
|
+const openHelp = () => {
|
|
|
+ $q.dialog({ component: ProfileHelpDialog })
|
|
|
+}
|
|
|
+
|
|
|
+const submit = async () => {
|
|
|
+ if (stars.value === 0) return
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ await createReview({
|
|
|
+ schedule_id: props.schedule.id,
|
|
|
+ origin: 'provider',
|
|
|
+ origin_id: store.user.provider.id,
|
|
|
+ stars: stars.value,
|
|
|
+ comment: comment.value || null,
|
|
|
+ improvements_ids: selectedTagIds.value,
|
|
|
+ block_provider: false,
|
|
|
+ block_client: isNegative.value && blockClient.value,
|
|
|
+ favorite_provider: false,
|
|
|
+ })
|
|
|
+
|
|
|
+ onDialogOK(true)
|
|
|
+ } catch (error) {
|
|
|
+ const status = error?.response?.status
|
|
|
+ if (status === 422) {
|
|
|
+ $q.notify({ message: t('provider.dashboard.schedule_rating.already_reviewed'), color: 'negative', icon: 'mdi-alert-circle-outline' })
|
|
|
+ } else {
|
|
|
+ $q.notify({ message: t('http.errors.failed'), color: 'negative' })
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ loadingTags.value = true
|
|
|
+ try {
|
|
|
+ const result = await getImprovementTypes('provider')
|
|
|
+ tags.value = result ?? []
|
|
|
+ } catch {
|
|
|
+ tags.value = []
|
|
|
+ } finally {
|
|
|
+ loadingTags.value = false
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.rating-dialog-card {
|
|
|
+ width: 320px;
|
|
|
+ max-width: 96vw;
|
|
|
+ border-radius: 20px !important;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-pill {
|
|
|
+ border: 1.5px solid #d1d5db;
|
|
|
+ border-radius: 20px;
|
|
|
+ padding: 5px 14px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6b7280;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: border-color 0.15s, color 0.15s;
|
|
|
+ user-select: none;
|
|
|
+
|
|
|
+ &--selected {
|
|
|
+ border-color: #8B5CF6;
|
|
|
+ color: #8B5CF6;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.submit-btn {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 700;
|
|
|
+ padding: 10px 0;
|
|
|
+}
|
|
|
+</style>
|