| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489 |
- <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="border-radius: 50%"
- :style="avatarStyle"
- >
- {{ initials }}
- </span>
- </q-avatar>
- <div
- class="font16 fontbold text-text text-center q-px-lg"
- style="line-height: 1.3"
- >
- {{ $t("dashboard_client.schedule_rating.title") }}
- <span class="text-primary">
- {{ (getFirstName(schedule.provider_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 font14 fontbold text-center q-mb-sm">
- {{
- isNegative
- ? $t("dashboard_client.schedule_rating.negative_label")
- : $t("dashboard_client.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>
- <q-card-section class="q-pt-xs q-pb-xs q-px-lg">
- <div class="font14 fontbold text-text q-mb-xs text-center">
- {{ $t("dashboard_client.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">
- {{ $t("dashboard_client.schedule_rating.add_photo") }}
- </div>
- </div>
- </div>
- </q-card-section>
- <q-card-section
- v-if="stars > 0"
- class="q-pt-xs q-pb-xs q-px-lg"
- >
- <q-checkbox
- v-model="checkboxValue"
- checked-icon="mdi-check-circle"
- class="text-text"
- color="primary"
- keep-color
- unchecked-icon="mdi-checkbox-blank-circle-outline"
- :label="isNegative ? $t('dashboard_client.schedule_rating.block_label') : $t('dashboard_client.schedule_rating.favorite_label')"
- />
- </q-card-section>
- <q-card-section class="q-pt-sm q-pb-xs q-px-lg row">
- <q-btn
- class="submit-btn col-12"
- color="primary"
- full-width
- no-caps
- padding="8px 12px"
- rounded
- unelevated
- :disable="stars === 0"
- :label="$t('dashboard_client.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-text font12 fontbold cursor-pointer"
- @click="openHelp"
- >
- {{ $t("dashboard_client.schedule_rating.help_link") }}
- </span>
- </q-card-section>
- </q-card>
- </q-dialog>
- </template>
- <script setup>
- import { avatarColors } from "src/helpers/avatarColors";
- import { computed, onMounted, ref } from "vue";
- import { createReview, getImprovementTypes } from "src/api/review";
- import {
- deleteClientFavoriteProvider,
- getClientFavoriteProviders,
- } from "src/api/clientFavoriteProvider";
- import { getFirstName } from "src/helpers/utils";
- import { getProfileMediaUrl } from "src/helpers/profileMedia";
- import { useDialogPluginComponent, useQuasar } from "quasar";
- import { useI18n } from "vue-i18n";
- import { userStore } from "src/stores/user";
- import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
- defineEmits([...useDialogPluginComponent.emits]);
- const props = defineProps({
- schedule: {
- type: Object,
- required: true,
- },
- });
- const $q = useQuasar();
- const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
- useDialogPluginComponent();
- const store = userStore();
- const { t } = useI18n();
- const MAX_PHOTOS = 5;
- const checkboxValue = ref(false);
- const comment = ref(null);
- const favoriteId = ref(null);
- const isFavoriteProvider = ref(false);
- const loading = ref(false);
- const loadingTags = ref(false);
- const photoInputRef = ref(null);
- const photoPreviews = ref([]);
- const photos = ref([]);
- const selectedTagIds = ref([]);
- const stars = ref(0);
- const tags = ref([]);
- const avatarStyle = computed(() => {
- const c = avatarColors[props.schedule.provider_id % avatarColors.length];
- return { background: c.background, color: c.color };
- });
- const photoUrl = computed(() => getProfileMediaUrl(props.schedule));
- const initials = computed(() => {
- const firstName = getFirstName(props.schedule.provider_name);
- return firstName?.slice(0, 2).toUpperCase() ?? "??";
- });
- const isNegative = computed(() => stars.value > 0 && stars.value <= 2);
- const isPositive = computed(() => stars.value >= 3);
- 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 = [];
- checkboxValue.value = isPositive.value && isFavoriteProvider.value;
- if (!isNegative.value) clearPhotos();
- };
- const openHelp = () => {
- $q.dialog({ component: ProfileHelpDialog });
- };
- const clearPhotos = () => {
- photoPreviews.value.forEach((preview) => URL.revokeObjectURL(preview));
- photos.value = [];
- photoPreviews.value = [];
- };
- const loadFavoriteState = async () => {
- const clientId = store.user?.client?.id;
- if (!clientId) return;
- try {
- const favorites = await getClientFavoriteProviders(clientId);
- const match = (favorites ?? []).find(
- (f) => Number(f.provider_id) === Number(props.schedule.provider_id),
- );
- isFavoriteProvider.value = !!match;
- favoriteId.value = match?.id ?? null;
- if (isPositive.value && isFavoriteProvider.value) checkboxValue.value = true;
- } catch {
- isFavoriteProvider.value = false;
- favoriteId.value = null;
- }
- };
- const removePhoto = (idx) => {
- URL.revokeObjectURL(photoPreviews.value[idx]);
- photos.value.splice(idx, 1);
- photoPreviews.value.splice(idx, 1);
- };
- const submit = async () => {
- if (stars.value === 0) return;
- loading.value = true;
- try {
- await createReview({
- schedule_id: props.schedule.id,
- origin: "client",
- origin_id: store.user.client.id,
- stars: stars.value,
- comment: comment.value || null,
- improvements_ids: selectedTagIds.value,
- block_provider: isNegative.value && checkboxValue.value,
- block_client: false,
- favorite_provider: isPositive.value && checkboxValue.value,
- photos: isNegative.value ? photos.value : [],
- });
- if (
- isPositive.value &&
- !checkboxValue.value &&
- isFavoriteProvider.value &&
- favoriteId.value
- ) {
- try {
- await deleteClientFavoriteProvider(favoriteId.value);
- } catch {
- // manter a avaliação enviada mesmo se a remoção do favorito falhar
- }
- }
- onDialogOK(true);
- } catch (error) {
- const status = error?.response?.status;
- if (status === 422) {
- $q.notify({
- message: t("dashboard_client.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 toggleTag = (id) => {
- const idx = selectedTagIds.value.indexOf(id);
- if (idx === -1) selectedTagIds.value.push(id);
- else selectedTagIds.value.splice(idx, 1);
- };
- onMounted(async () => {
- loadFavoriteState();
- loadingTags.value = true;
- try {
- const result = await getImprovementTypes("client");
- 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>
|