218 lines
6.2 KiB
Vue
218 lines
6.2 KiB
Vue
<template>
|
||||
|
|
<AppModal
|
|||
|
|
:open="reviewOpen"
|
|||
|
|
:title="`امتیاز به ${activeReview?.businessTitle ?? 'کسبوکار'}`"
|
|||
|
|
subtitle="از ۱ تا ۵ ستاره — تجربه خود را با ما به اشتراک بگذارید"
|
|||
|
|
@close="dismissReview"
|
|||
|
|
>
|
|||
|
|
<div class="space-y-6">
|
|||
|
|
<p class="text-center text-sm text-muted">
|
|||
|
|
خدمت: {{ activeReview?.serviceTitle ?? '—' }}
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
<StarRating v-model="rating" />
|
|||
|
|
|
|||
|
|
<template v-if="activeReview?.tipsEnabled">
|
|||
|
|
<div class="rounded-2xl border border-amber-500/25 bg-amber-50/80 p-4 dark:bg-amber-500/10">
|
|||
|
|
<h3 class="text-sm font-bold text-text">حسن انجام کار</h3>
|
|||
|
|
<p class="mt-2 text-sm leading-7 text-muted">
|
|||
|
|
در صورتی که از خدمات ارائهشده توسط
|
|||
|
|
<span class="font-semibold text-text">{{ activeReview?.businessTitle }}</span>
|
|||
|
|
رضایت دارید، میتوانید مبلغی را بهعنوان قدردانی اختصاص دهید.
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
|
|||
|
|
|
|||
|
|
<div class="flex flex-col gap-2 sm:flex-row">
|
|||
|
|
<AppButton
|
|||
|
|
class="flex-1"
|
|||
|
|
variant="outline"
|
|||
|
|
:disabled="!rating || submitting"
|
|||
|
|
@click="submitWithoutTip"
|
|||
|
|
>
|
|||
|
|
تمایل ندارم
|
|||
|
|
</AppButton>
|
|||
|
|
<AppButton
|
|||
|
|
class="flex-1"
|
|||
|
|
:disabled="!rating || submitting"
|
|||
|
|
@click="openTipModal"
|
|||
|
|
>
|
|||
|
|
تمایل دارم
|
|||
|
|
</AppButton>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<template v-else>
|
|||
|
|
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
|
|||
|
|
<AppButton
|
|||
|
|
class="w-full"
|
|||
|
|
:disabled="!rating || submitting"
|
|||
|
|
:loading="submitting"
|
|||
|
|
@click="submitWithoutTip"
|
|||
|
|
>
|
|||
|
|
ثبت امتیاز
|
|||
|
|
</AppButton>
|
|||
|
|
</template>
|
|||
|
|
</div>
|
|||
|
|
</AppModal>
|
|||
|
|
|
|||
|
|
<AppModal
|
|||
|
|
:open="tipOpen"
|
|||
|
|
title="حسن انجام کار"
|
|||
|
|
:subtitle="`مبلغ قدردانی برای ${activeReview?.businessTitle ?? 'کسبوکار'}`"
|
|||
|
|
@close="closeTipModal"
|
|||
|
|
>
|
|||
|
|
<form class="space-y-4" @submit.prevent="submitTip">
|
|||
|
|
<AppInput
|
|||
|
|
id="tipAmount"
|
|||
|
|
v-model="tipAmountInput"
|
|||
|
|
label="مبلغ (تومان)"
|
|||
|
|
inputmode="numeric"
|
|||
|
|
ltr
|
|||
|
|
placeholder="مثلاً ۵۰٬۰۰۰"
|
|||
|
|
required
|
|||
|
|
/>
|
|||
|
|
<p v-if="tipError" class="text-sm text-red-600">{{ tipError }}</p>
|
|||
|
|
<AppButton type="submit" class="w-full" :loading="tipSubmitting" :disabled="!tipAmountToman">
|
|||
|
|
پرداخت
|
|||
|
|
</AppButton>
|
|||
|
|
</form>
|
|||
|
|
</AppModal>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { computed, onMounted, ref, watch } from 'vue';
|
|||
|
|
import {
|
|||
|
|
fetchPendingReviews,
|
|||
|
|
initiateBookingTip,
|
|||
|
|
submitBookingReview,
|
|||
|
|
type PendingBookingReview,
|
|||
|
|
} from '@/api/bookings';
|
|||
|
|
import { ApiError } from '@/api/client';
|
|||
|
|
import StarRating from '@/components/booking/StarRating.vue';
|
|||
|
|
import AppButton from '@/components/ui/AppButton.vue';
|
|||
|
|
import AppInput from '@/components/ui/AppInput.vue';
|
|||
|
|
import AppModal from '@/components/ui/AppModal.vue';
|
|||
|
|
import { useAuthStore } from '@/stores/auth';
|
|||
|
|
import { parsePriceInput } from '@/utils/format';
|
|||
|
|
|
|||
|
|
const auth = useAuthStore();
|
|||
|
|
|
|||
|
|
const pending = ref<PendingBookingReview[]>([]);
|
|||
|
|
const activeReview = ref<PendingBookingReview | null>(null);
|
|||
|
|
const reviewOpen = ref(false);
|
|||
|
|
const tipOpen = ref(false);
|
|||
|
|
const rating = ref(0);
|
|||
|
|
const error = ref('');
|
|||
|
|
const submitting = ref(false);
|
|||
|
|
const tipAmountInput = ref('');
|
|||
|
|
const tipError = ref('');
|
|||
|
|
const tipSubmitting = ref(false);
|
|||
|
|
|
|||
|
|
const tipAmountToman = computed(() => parsePriceInput(tipAmountInput.value));
|
|||
|
|
|
|||
|
|
async function loadPending() {
|
|||
|
|
if (!auth.isLoggedIn || auth.isBusinessOwner) return;
|
|||
|
|
try {
|
|||
|
|
pending.value = await fetchPendingReviews();
|
|||
|
|
if (!activeReview.value && pending.value.length) {
|
|||
|
|
activeReview.value = pending.value[0] ?? null;
|
|||
|
|
reviewOpen.value = true;
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
/* ignore */
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resetForm() {
|
|||
|
|
rating.value = 0;
|
|||
|
|
error.value = '';
|
|||
|
|
tipAmountInput.value = '';
|
|||
|
|
tipError.value = '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function dismissReview() {
|
|||
|
|
reviewOpen.value = false;
|
|||
|
|
resetForm();
|
|||
|
|
if (pending.value.length > 1) {
|
|||
|
|
pending.value = pending.value.slice(1);
|
|||
|
|
activeReview.value = pending.value[0] ?? null;
|
|||
|
|
if (activeReview.value) {
|
|||
|
|
reviewOpen.value = true;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
activeReview.value = null;
|
|||
|
|
pending.value = [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openTipModal() {
|
|||
|
|
if (!rating.value) return;
|
|||
|
|
tipOpen.value = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeTipModal() {
|
|||
|
|
tipOpen.value = false;
|
|||
|
|
tipError.value = '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function submitWithoutTip() {
|
|||
|
|
if (!activeReview.value || !rating.value) return;
|
|||
|
|
submitting.value = true;
|
|||
|
|
error.value = '';
|
|||
|
|
try {
|
|||
|
|
await submitBookingReview(activeReview.value.bookingId, rating.value);
|
|||
|
|
reviewOpen.value = false;
|
|||
|
|
resetForm();
|
|||
|
|
pending.value = pending.value.filter(
|
|||
|
|
(item) => item.bookingId !== activeReview.value?.bookingId,
|
|||
|
|
);
|
|||
|
|
activeReview.value = pending.value[0] ?? null;
|
|||
|
|
if (activeReview.value) {
|
|||
|
|
reviewOpen.value = true;
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
error.value = e instanceof ApiError ? e.message : 'ثبت امتیاز ناموفق بود.';
|
|||
|
|
} finally {
|
|||
|
|
submitting.value = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function submitTip() {
|
|||
|
|
if (!activeReview.value || !rating.value || !tipAmountToman.value) return;
|
|||
|
|
const bookingId = activeReview.value.bookingId;
|
|||
|
|
tipSubmitting.value = true;
|
|||
|
|
tipError.value = '';
|
|||
|
|
try {
|
|||
|
|
await submitBookingReview(bookingId, rating.value);
|
|||
|
|
const result = await initiateBookingTip(bookingId, tipAmountToman.value);
|
|||
|
|
tipOpen.value = false;
|
|||
|
|
reviewOpen.value = false;
|
|||
|
|
resetForm();
|
|||
|
|
pending.value = pending.value.filter((item) => item.bookingId !== bookingId);
|
|||
|
|
activeReview.value = pending.value[0] ?? null;
|
|||
|
|
if (result.mock) {
|
|||
|
|
if (activeReview.value) reviewOpen.value = true;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
window.location.href = result.startUrl;
|
|||
|
|
} catch (e) {
|
|||
|
|
tipError.value = e instanceof ApiError ? e.message : 'پرداخت ناموفق بود.';
|
|||
|
|
} finally {
|
|||
|
|
tipSubmitting.value = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
watch(
|
|||
|
|
() => auth.isLoggedIn,
|
|||
|
|
(loggedIn) => {
|
|||
|
|
if (loggedIn) void loadPending();
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
void loadPending();
|
|||
|
|
});
|
|||
|
|
</script>
|