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>
|