Files
webMontime/src/components/booking/BookingReviewHost.vue
T

218 lines
6.2 KiB
Vue
Raw Normal View History

2026-06-29 19:32:19 +03:30
<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>