| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- <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
- color="grey-6"
- dense
- flat
- icon="close"
- round
- size="sm"
- @click="onDialogCancel"
- />
- </div>
- <div class="column items-center q-pb-sm">
- <q-avatar
- class="q-mb-sm"
- size="64px"
- >
- <img
- v-if="photoUrl"
- style="object-fit: cover"
- :src="photoUrl"
- />
- <span
- v-else
- class="full-width full-height flex flex-center"
- style="font-size: 20px; border-radius: 50%;"
- :style="avatarStyle"
- >
- {{ initials }}
- </span>
- </q-avatar>
-
- <div
- class="text-text text-center font16 fontbold q-px-lg"
- style="line-height:1.3"
- >
- {{ $t('provider.dashboard.schedule_rating.title') }}
- <span class="text-primary">
- {{ (getFirstName(schedule.client_name) || '—') + '?' }}
- </span>
- </div>
- </div>
- <div class="column items-center q-pb-xs">
- <q-rating
- v-model="stars"
- color="amber"
- icon="mdi-star-outline"
- icon-selected="mdi-star"
- size="lg"
- :max="5"
- @update:model-value="onStarsChange"
- />
- </div>
- <q-card-section
- v-if="stars > 0"
- class="q-pt-xs q-pb-xs"
- >
- <div class="text-grey-7 text-center font12 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--selected': selectedTagIds.includes(tag.id), 'fontsemibold': selectedTagIds.includes(tag.id) }"
- class="tag-pill font12"
- @click="toggleTag(tag.id)"
- >
- {{ tag.description }}
- </div>
- </div>
- </q-card-section>
- <q-card-section class="q-pt-xs q-pb-xs q-px-lg">
- <div class="text-grey-7 font12 q-mb-xs">
- {{ $t('provider.dashboard.schedule_rating.comment_placeholder') }}
- </div>
- <q-input
- v-model="comment"
- color="primary"
- dense
- hide-bottom-space
- input-class="text-black"
- outlined
- rows="3"
- type="textarea"
- />
- </q-card-section>
- <q-card-section
- v-if="isNegative"
- class="q-pt-xs q-pb-xs q-px-lg"
- >
- <input
- ref="photoInputRef"
- accept="image/jpeg,image/png,image/webp"
- class="hidden"
- multiple
- type="file"
- @change="onPhotosSelected"
- />
- <div class="row q-gutter-xs">
- <div
- v-for="(preview, idx)
- in photoPreviews"
- :key="idx"
- class="photo-thumb"
- >
- <img :src="preview" />
- <q-btn
- class="photo-thumb__remove"
- dense
- flat
- icon="close"
- round
- size="xs"
- @click="removePhoto(idx)"
- />
- </div>
- <div
- v-if="photos.length < MAX_PHOTOS"
- class="photo-add"
- @click="photoInputRef.click()"
- >
- <q-icon
- color="grey-5"
- name="mdi-camera-plus-outline"
- size="24px"
- />
- <div class="photo-add__label font9">
- {{ $t('provider.dashboard.schedule_rating.add_photo') }}
- </div>
- </div>
- </div>
- </q-card-section>
- <q-card-section
- v-if="isNegative"
- class="q-pt-xs q-pb-xs q-px-lg"
- >
- <q-checkbox
- v-model="blockClient"
- checked-icon="mdi-check-circle"
- class="text-text"
- color="primary"
- keep-color
- unchecked-icon="mdi-checkbox-blank-circle-outline"
- :label="$t('provider.dashboard.schedule_rating.block_label')"
- />
- </q-card-section>
- <q-card-section class="q-pt-sm q-pb-xs q-px-lg row">
- <q-btn
- class="submit-btn font16 fontbold col-12"
- color="primary"
- full-width
- no-caps
- rounded
- unelevated
- :disable="stars === 0"
- :label="$t('provider.dashboard.schedule_rating.submit_btn')"
- :loading="loading"
- @click="submit"
- />
- </q-card-section>
- <q-card-section class="q-pt-xs q-pb-lg text-center">
- <span
- class="text-grey-6 font12 cursor-pointer"
- @click="openHelp"
- >
- {{ $t('provider.dashboard.schedule_rating.help_link') }}
- </span>
- </q-card-section>
- </q-card>
- </q-dialog>
- </template>
- <script setup>
- import { avatarColors, getFirstName } from 'src/helpers/utils'
- import { createReview, getImprovementTypes } from 'src/api/review'
- import { computed, onMounted, ref } from 'vue'
- import { useDialogPluginComponent, useQuasar } from 'quasar'
- import { useI18n } from 'vue-i18n'
- 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 MAX_PHOTOS = 5
- 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 photos = ref([])
- const photoPreviews = ref([])
- const photoInputRef = ref(null)
- const avatarStyle = computed(() => {
- const c = avatarColors[props.schedule.client_id % avatarColors.length]
- return { background: c.background, color: c.color }
- })
- const isNegative = computed(() => stars.value > 0 && stars.value <= 2)
- const photoUrl = computed(() =>
- props.schedule.customer_photo
- ?? props.schedule.client_photo
- ?? props.schedule.client?.profile_media?.url
- ?? null
- )
- const initials = computed(() =>
- getFirstName(props.schedule.client_name)?.slice(0, 2).toUpperCase() ?? '??'
- )
- const onPhotosSelected = (event) => {
- const files = Array.from(event.target.files)
- const remaining = MAX_PHOTOS - photos.value.length
- files.slice(0, remaining).forEach(file => {
- photos.value.push(file)
- photoPreviews.value.push(URL.createObjectURL(file))
- })
- event.target.value = ''
- }
- const onStarsChange = () => {
- selectedTagIds.value = []
- blockClient.value = false
- if (!isNegative.value) clearPhotos()
- }
- const clearPhotos = () => {
- photoPreviews.value.forEach(preview => URL.revokeObjectURL(preview))
- photos.value = []
- photoPreviews.value = []
- }
- const removePhoto = (idx) => {
- URL.revokeObjectURL(photoPreviews.value[idx])
- photos.value.splice(idx, 1)
- photoPreviews.value.splice(idx, 1)
- }
- const toggleTag = (id) => {
- const idx = selectedTagIds.value.indexOf(id)
- if (idx === -1) selectedTagIds.value.push(id)
- else selectedTagIds.value.splice(idx, 1)
- }
- //
- 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,
- photos: isNegative.value ? photos.value : [],
- })
- 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
- }
- }
- //
- const openHelp = () => {
- $q.dialog({ component: ProfileHelpDialog })
- }
- //
- 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;
- color: #6b7280;
- cursor: pointer;
- transition: border-color 0.15s, color 0.15s;
- user-select: none;
- &--selected {
- border-color: #8B5CF6;
- color: #8B5CF6;
- }
- }
- .submit-btn {
- padding: 10px 0;
- }
- .photo-thumb {
- position: relative;
- width: 64px;
- height: 64px;
- border-radius: 8px;
- overflow: hidden;
- flex-shrink: 0;
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- &__remove {
- position: absolute !important;
- top: 2px;
- right: 2px;
- background: rgba(0, 0, 0, 0.5) !important;
- color: white !important;
- min-height: unset !important;
- }
- }
- .photo-add {
- width: 64px;
- height: 64px;
- border-radius: 8px;
- border: 1.5px dashed #d1d5db;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- flex-shrink: 0;
- &__label {
- color: #9ca3af;
- text-align: center;
- margin-top: 2px;
- line-height: 1.2;
- }
- &:active { background: rgba(0, 0, 0, 0.03); }
- }
- .hidden { display: none; }
- </style>
|