awaiting_payment
This commit is contained in:
@@ -60,6 +60,32 @@ export function confirmBooking(id: string) {
|
||||
return api<Booking>(`/bookings/${id}/confirm`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function fetchPaymentByToken(token: string) {
|
||||
return api<{
|
||||
bookings: Booking[];
|
||||
totalAmountToman: number;
|
||||
}>(`/bookings/payment/by-token/${encodeURIComponent(token)}`);
|
||||
}
|
||||
|
||||
export type PaymentInitResult = {
|
||||
startUrl: string;
|
||||
amountToman: number;
|
||||
mock?: boolean;
|
||||
};
|
||||
|
||||
export function initiateBookingPayment(id: string) {
|
||||
return api<PaymentInitResult>(`/bookings/${id}/payment/init`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export function initiateBookingPaymentByToken(token: string) {
|
||||
return api<PaymentInitResult>(
|
||||
`/bookings/payment/by-token/${encodeURIComponent(token)}/init`,
|
||||
{ method: 'POST' },
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchAvailableSlots(
|
||||
businessId: string,
|
||||
dateYmd: string,
|
||||
|
||||
+10
-1
@@ -1,4 +1,9 @@
|
||||
export type BookingStatus = 'pending' | 'confirmed' | 'cancelled' | 'completed';
|
||||
export type BookingStatus =
|
||||
| 'pending'
|
||||
| 'awaiting_payment'
|
||||
| 'confirmed'
|
||||
| 'cancelled'
|
||||
| 'completed';
|
||||
|
||||
export type BusinessCategory = {
|
||||
id: string;
|
||||
@@ -31,6 +36,7 @@ export type Business = {
|
||||
concurrentCapacity?: number;
|
||||
slotStepMinutes?: number;
|
||||
serviceClassesEnabled?: boolean;
|
||||
autoApproveBookings?: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
@@ -46,6 +52,7 @@ export type UpdateBusinessPayload = {
|
||||
concurrentCapacity?: number;
|
||||
slotStepMinutes?: number;
|
||||
serviceClassesEnabled?: boolean;
|
||||
autoApproveBookings?: boolean;
|
||||
};
|
||||
|
||||
export type ServiceTemplate = {
|
||||
@@ -98,6 +105,8 @@ export type Booking = {
|
||||
status: BookingStatus;
|
||||
note?: string | null;
|
||||
cancellationReason?: string | null;
|
||||
paymentToken?: string | null;
|
||||
paymentAmount?: string | null;
|
||||
business?: Business;
|
||||
businessService?: BusinessService;
|
||||
serviceClass?: BusinessServiceClass;
|
||||
|
||||
@@ -65,9 +65,17 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="canConfirm || canCancel"
|
||||
v-if="canConfirm || canCancel || canPay"
|
||||
class="mt-auto flex flex-wrap gap-2 border-t border-border pt-3"
|
||||
>
|
||||
<AppButton
|
||||
v-if="canPay"
|
||||
class="!w-auto flex-1 px-3"
|
||||
:loading="acting"
|
||||
@click="emit('pay', bookings)"
|
||||
>
|
||||
پرداخت
|
||||
</AppButton>
|
||||
<AppButton
|
||||
v-if="canConfirm"
|
||||
class="!w-auto flex-1 px-3"
|
||||
@@ -106,11 +114,13 @@ const props = defineProps<{
|
||||
showCustomer: boolean;
|
||||
canConfirm: boolean;
|
||||
canCancel: boolean;
|
||||
canPay: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [bookings: Booking[]];
|
||||
cancel: [bookings: Booking[]];
|
||||
pay: [bookings: Booking[]];
|
||||
}>();
|
||||
|
||||
const primary = computed(() => props.bookings[0]!);
|
||||
@@ -139,6 +149,9 @@ function statusClassFor(status: BookingStatus | 'mixed') {
|
||||
if (status === 'confirmed' || status === 'completed') {
|
||||
return 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400';
|
||||
}
|
||||
if (status === 'awaiting_payment') {
|
||||
return 'bg-sky-500/15 text-sky-700 dark:text-sky-400';
|
||||
}
|
||||
if (status === 'pending') {
|
||||
return 'bg-amber-500/15 text-amber-700 dark:text-amber-400';
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-text">رزرو شما با موفقیت ثبت شد!</h2>
|
||||
<h2 class="text-2xl font-bold text-text">{{ title }}</h2>
|
||||
<p class="mt-2 text-sm text-muted">
|
||||
جزئیات رزرو برای شما پیامک و در بخش رزروها ذخیره شد.
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,10 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-stretch gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:justify-center sm:gap-3">
|
||||
<AppButton @click="emit('view-bookings')">
|
||||
<AppButton v-if="showPayButton" @click="emit('pay')">
|
||||
پرداخت آنلاین
|
||||
</AppButton>
|
||||
<AppButton :variant="showPayButton ? 'outline' : 'primary'" @click="emit('view-bookings')">
|
||||
مشاهده رزروها
|
||||
</AppButton>
|
||||
<AppButton variant="outline" @click="emit('add-to-calendar')">
|
||||
@@ -81,22 +84,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppIcon from '@/components/ui/AppIcon.vue';
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
serviceTitle: string;
|
||||
dateLabel: string;
|
||||
timeLabel: string;
|
||||
durationLabel: string;
|
||||
address?: string | null;
|
||||
bookingRef: string;
|
||||
successMode?: 'pending_approval' | 'awaiting_payment';
|
||||
showPayButton?: boolean;
|
||||
}>();
|
||||
|
||||
const title = computed(() =>
|
||||
props.successMode === 'awaiting_payment'
|
||||
? 'رزرو تأیید شد — آماده پرداخت'
|
||||
: 'درخواست رزرو ثبت شد',
|
||||
);
|
||||
|
||||
const subtitle = computed(() =>
|
||||
props.successMode === 'awaiting_payment'
|
||||
? 'برای نهایی شدن رزرو، پرداخت آنلاین را انجام دهید.'
|
||||
: 'درخواست شما برای صاحب کسبوکار ارسال شد. پس از تأیید، لینک پرداخت در اپ و پیامک برای شما ارسال میشود.',
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'view-bookings': [];
|
||||
'add-to-calendar': [];
|
||||
'go-home': [];
|
||||
pay: [];
|
||||
}>();
|
||||
|
||||
const confettiDots = [
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
@view-bookings="emit('view-bookings')"
|
||||
@add-to-calendar="downloadCalendar"
|
||||
@go-home="emit('go-home')"
|
||||
@pay="goToPayment"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
@@ -123,7 +124,7 @@
|
||||
placeholder="در صورت نیاز توضیحی بنویسید..."
|
||||
/>
|
||||
<p class="mt-4 text-sm text-muted">
|
||||
پرداخت در محل انجام میشود. نیازی به پرداخت آنلاین نیست.
|
||||
{{ paymentInfoText }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -223,6 +224,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { createBooking, createBookingBatch, fetchAvailableSlots } from '@/api/bookings';
|
||||
import { fetchBusinessServiceClasses } from '@/api/business-service-classes';
|
||||
import { ApiError } from '@/api/client';
|
||||
@@ -275,8 +277,13 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const auth = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
const currentStep = ref(1);
|
||||
const paymentInfoText = computed(() =>
|
||||
props.business.autoApproveBookings
|
||||
? 'پس از ثبت نهایی، مستقیماً به صفحه پرداخت آنلاین هدایت میشوید.'
|
||||
: 'پس از ثبت، درخواست شما بررسی میشود. در صورت تأیید صاحب کسبوکار، لینک پرداخت آنلاین برای شما در اپ و پیامک ارسال میشود.',
|
||||
);
|
||||
const phase = ref<'booking' | 'success'>('booking');
|
||||
const selectedClassId = ref('');
|
||||
const selectedServiceIds = ref<string[]>([]);
|
||||
@@ -405,6 +412,11 @@ const successDetails = computed(() => {
|
||||
durationLabel: selectedDurationLabel.value ?? '—',
|
||||
address: props.business.address,
|
||||
bookingRef: createdBooking.value.id.slice(0, 8).toUpperCase(),
|
||||
successMode:
|
||||
createdBooking.value.status === 'awaiting_payment'
|
||||
? 'awaiting_payment'
|
||||
: 'pending_approval',
|
||||
showPayButton: createdBooking.value.status === 'awaiting_payment',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -581,6 +593,17 @@ async function submitBooking() {
|
||||
});
|
||||
createdBooking.value = bookings[0] ?? null;
|
||||
}
|
||||
if (
|
||||
createdBooking.value?.status === 'awaiting_payment' &&
|
||||
createdBooking.value.paymentToken &&
|
||||
props.business.autoApproveBookings
|
||||
) {
|
||||
await router.push({
|
||||
path: '/app/bookings/pay',
|
||||
query: { token: createdBooking.value.paymentToken },
|
||||
});
|
||||
return;
|
||||
}
|
||||
phase.value = 'success';
|
||||
} catch (e) {
|
||||
bookingError.value = e instanceof ApiError ? e.message : 'ثبت رزرو ناموفق بود.';
|
||||
@@ -589,6 +612,15 @@ async function submitBooking() {
|
||||
}
|
||||
}
|
||||
|
||||
function goToPayment() {
|
||||
const token = createdBooking.value?.paymentToken;
|
||||
if (!token) return;
|
||||
void router.push({
|
||||
path: '/app/bookings/pay',
|
||||
query: { token },
|
||||
});
|
||||
}
|
||||
|
||||
function downloadCalendar() {
|
||||
if (!createdBooking.value) return;
|
||||
const start = new Date(createdBooking.value.scheduledAt);
|
||||
|
||||
@@ -36,6 +36,12 @@ const router = createRouter({
|
||||
component: () => import('@/views/app/AppBookingsView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/app/bookings/pay',
|
||||
name: 'app-booking-pay',
|
||||
component: () => import('@/views/app/BookingPaymentView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/app/profile',
|
||||
name: 'app-profile',
|
||||
|
||||
@@ -57,7 +57,15 @@ export function entryMatchesOwnerTab(
|
||||
}
|
||||
|
||||
if (tab === 'confirmed') {
|
||||
return status === 'confirmed' || status === 'completed';
|
||||
return (
|
||||
status === 'confirmed' ||
|
||||
status === 'completed' ||
|
||||
status === 'awaiting_payment' ||
|
||||
(status === 'mixed' &&
|
||||
bookings.some((booking) =>
|
||||
['confirmed', 'completed', 'awaiting_payment'].includes(booking.status),
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
return status === 'cancelled';
|
||||
|
||||
@@ -89,6 +89,7 @@ export function formatDate(value?: string | Date): string {
|
||||
export function bookingStatusLabel(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'در انتظار تأیید',
|
||||
awaiting_payment: 'منتظر پرداخت',
|
||||
confirmed: 'تأیید شده',
|
||||
completed: 'انجام شده',
|
||||
cancelled: 'لغو شده',
|
||||
|
||||
@@ -77,8 +77,10 @@
|
||||
:show-customer="auth.isBusinessOwner"
|
||||
:can-confirm="canConfirmEntry(entry.bookings)"
|
||||
:can-cancel="canCancelEntry(entry.bookings)"
|
||||
:can-pay="canPayEntry(entry.bookings)"
|
||||
@confirm="onConfirm"
|
||||
@cancel="onCancelRequest"
|
||||
@pay="onPay"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,7 +134,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { cancelBooking, confirmBooking, fetchBookings } from '@/api/bookings';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {
|
||||
cancelBooking,
|
||||
confirmBooking,
|
||||
fetchBookings,
|
||||
initiateBookingPayment,
|
||||
} from '@/api/bookings';
|
||||
import { ApiError } from '@/api/client';
|
||||
import type { Booking } from '@/api/types';
|
||||
import BookingItem from '@/components/booking/BookingItem.vue';
|
||||
@@ -150,6 +158,7 @@ import {
|
||||
} from '@/utils/booking-groups';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const router = useRouter();
|
||||
const bookings = ref<Booking[]>([]);
|
||||
const loading = ref(true);
|
||||
const error = ref('');
|
||||
@@ -203,7 +212,15 @@ function canConfirm(booking: Booking) {
|
||||
}
|
||||
|
||||
function canCancel(booking: Booking) {
|
||||
return booking.status === 'pending' || booking.status === 'confirmed';
|
||||
return (
|
||||
booking.status === 'pending' ||
|
||||
booking.status === 'awaiting_payment' ||
|
||||
booking.status === 'confirmed'
|
||||
);
|
||||
}
|
||||
|
||||
function canPay(booking: Booking) {
|
||||
return !auth.isBusinessOwner && booking.status === 'awaiting_payment';
|
||||
}
|
||||
|
||||
function canConfirmEntry(entry: Booking[]) {
|
||||
@@ -214,6 +231,10 @@ function canCancelEntry(entry: Booking[]) {
|
||||
return entry.some((booking) => canCancel(booking));
|
||||
}
|
||||
|
||||
function canPayEntry(entry: Booking[]) {
|
||||
return entry.some((booking) => canPay(booking));
|
||||
}
|
||||
|
||||
function entryKey(entry: BookingListEntry) {
|
||||
return entry.key;
|
||||
}
|
||||
@@ -256,6 +277,36 @@ async function onConfirm(entry: Booking[]) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onPay(entry: Booking[]) {
|
||||
const payable = entry.find((booking) => canPay(booking));
|
||||
if (!payable) return;
|
||||
|
||||
if (payable.paymentToken) {
|
||||
await router.push({
|
||||
path: '/app/bookings/pay',
|
||||
query: { token: payable.paymentToken },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
actingKey.value = entry[0]?.bookingGroupId ?? entry[0]?.id ?? null;
|
||||
try {
|
||||
const result = await initiateBookingPayment(payable.id);
|
||||
if (result.mock && payable.paymentToken) {
|
||||
await router.push({
|
||||
path: '/app/bookings/pay',
|
||||
query: { token: payable.paymentToken, payment: 'success' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
window.location.href = result.startUrl;
|
||||
} catch (e) {
|
||||
error.value = e instanceof ApiError ? e.message : 'شروع پرداخت ناموفق بود.';
|
||||
} finally {
|
||||
actingKey.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onCancelRequest(entry: Booking[]) {
|
||||
if (auth.isBusinessOwner) {
|
||||
pendingCancelEntry.value = entry;
|
||||
|
||||
@@ -135,6 +135,21 @@
|
||||
فاصله بین ساعتهای قابل رزرو. مثلاً ۳۰ یعنی ۱۰:۰۰، ۱۰:۳۰، ۱۱:۰۰ و …
|
||||
</p>
|
||||
|
||||
<label class="flex cursor-pointer items-start gap-3 rounded-xl border border-border bg-surface px-4 py-3">
|
||||
<input
|
||||
v-model="bookingForm.autoApproveBookings"
|
||||
type="checkbox"
|
||||
class="mt-1"
|
||||
/>
|
||||
<span class="min-w-0">
|
||||
<span class="block text-sm font-medium text-text">تأیید خودکار درخواستها</span>
|
||||
<span class="mt-1 block text-xs text-muted">
|
||||
اگر فعال باشد، رزرو بلافاصله برای پرداخت آنلاین ارسال میشود. در غیر این صورت ابتدا
|
||||
شما درخواست را تأیید میکنید و سپس لینک پرداخت برای مشتری ارسال میشود.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<AppButton :loading="savingBooking" @click="onSaveBookingSettings">
|
||||
ذخیره تنظیمات نوبتدهی
|
||||
</AppButton>
|
||||
@@ -514,6 +529,7 @@ const form = ref({
|
||||
const bookingForm = ref({
|
||||
concurrentCapacity: '1',
|
||||
slotStepMinutes: '30',
|
||||
autoApproveBookings: false,
|
||||
});
|
||||
|
||||
const bookingExpanded = ref(false);
|
||||
@@ -557,10 +573,10 @@ const servicesSummary = computed(() => {
|
||||
return `${count} خدمت · ${activeCount} فعال`;
|
||||
});
|
||||
|
||||
const bookingSummary = computed(
|
||||
() =>
|
||||
`${bookingForm.value.concurrentCapacity} همزمان · گام ${bookingForm.value.slotStepMinutes} دقیقه`,
|
||||
);
|
||||
const bookingSummary = computed(() => {
|
||||
const auto = bookingForm.value.autoApproveBookings ? 'تأیید خودکار' : 'تأیید دستی';
|
||||
return `${bookingForm.value.concurrentCapacity} همزمان · گام ${bookingForm.value.slotStepMinutes} دقیقه · ${auto}`;
|
||||
});
|
||||
|
||||
function emptyClassPriceForm() {
|
||||
const result: Record<string, { price: string; durationMinutes: string }> = {};
|
||||
@@ -658,6 +674,7 @@ onMounted(async () => {
|
||||
bookingForm.value = {
|
||||
concurrentCapacity: String(business.value.concurrentCapacity ?? 1),
|
||||
slotStepMinutes: String(business.value.slotStepMinutes ?? 30),
|
||||
autoApproveBookings: business.value.autoApproveBookings === true,
|
||||
};
|
||||
await loadServicesAndTemplates();
|
||||
}
|
||||
@@ -776,6 +793,7 @@ async function onSaveBookingSettings() {
|
||||
business.value = await updateBusiness(business.value.id, {
|
||||
concurrentCapacity,
|
||||
slotStepMinutes,
|
||||
autoApproveBookings: bookingForm.value.autoApproveBookings,
|
||||
});
|
||||
tenant.syncSingleBusiness(business.value);
|
||||
message.value = 'تنظیمات نوبتدهی ذخیره شد.';
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<ResponsiveAppLayout>
|
||||
<h1 class="mb-1 text-xl font-bold text-text">پرداخت رزرو</h1>
|
||||
<p class="mb-4 text-sm text-muted">پرداخت آنلاین رزرو تأییدشده</p>
|
||||
|
||||
<p
|
||||
v-if="mockMode && canPay"
|
||||
class="mb-4 rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-800 dark:text-amber-300"
|
||||
>
|
||||
حالت تست: پرداخت واقعی انجام نمیشود و فقط برای آزمایش جریان رزرو است.
|
||||
</p>
|
||||
|
||||
<p
|
||||
v-if="paymentStatus === 'success'"
|
||||
class="mb-4 rounded-xl border border-emerald-500/30 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-700 dark:text-emerald-400"
|
||||
>
|
||||
پرداخت با موفقیت انجام شد. رزرو شما نهایی شد.
|
||||
</p>
|
||||
<p
|
||||
v-else-if="paymentStatus === 'failed'"
|
||||
class="mb-4 rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-600"
|
||||
>
|
||||
پرداخت ناموفق بود. میتوانید دوباره تلاش کنید.
|
||||
</p>
|
||||
|
||||
<p
|
||||
v-if="error"
|
||||
class="mb-4 rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-600"
|
||||
>
|
||||
{{ error }}
|
||||
</p>
|
||||
|
||||
<ListCardSkeleton v-if="loading" :count="1" />
|
||||
|
||||
<div
|
||||
v-else-if="details"
|
||||
class="mx-auto max-w-lg space-y-4 rounded-2xl border border-border bg-surface-2 p-5"
|
||||
>
|
||||
<dl class="space-y-3 text-sm">
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-muted">کسبوکار</dt>
|
||||
<dd class="font-medium text-text">
|
||||
{{ details.bookings[0]?.business?.title ?? '—' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-muted">تاریخ</dt>
|
||||
<dd class="font-medium text-text">
|
||||
{{ formatDate(details.bookings[0]?.scheduledAt ?? '') }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4 border-t border-border pt-3">
|
||||
<dt class="text-muted">مبلغ قابل پرداخت</dt>
|
||||
<dd class="text-lg font-bold text-brand-600">
|
||||
{{ formatPriceToman(String(details.totalAmountToman)) }} تومان
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<ul class="space-y-1 text-sm text-muted">
|
||||
<li v-for="booking in details.bookings" :key="booking.id">
|
||||
{{ booking.businessService?.serviceTemplate?.title ?? 'خدمت' }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<AppButton
|
||||
v-if="canPay"
|
||||
class="w-full"
|
||||
:loading="paying"
|
||||
@click="startPayment"
|
||||
>
|
||||
{{ mockMode ? 'پرداخت آزمایشی' : 'پرداخت آنلاین' }}
|
||||
</AppButton>
|
||||
|
||||
<RouterLink
|
||||
to="/app/bookings"
|
||||
class="block text-center text-sm text-brand-600 dark:text-brand-500"
|
||||
>
|
||||
بازگشت به رزروها
|
||||
</RouterLink>
|
||||
</div>
|
||||
</ResponsiveAppLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import {
|
||||
fetchPaymentByToken,
|
||||
initiateBookingPaymentByToken,
|
||||
} from '@/api/bookings';
|
||||
import { ApiError } from '@/api/client';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import ListCardSkeleton from '@/components/ui/ListCardSkeleton.vue';
|
||||
import ResponsiveAppLayout from '@/layouts/ResponsiveAppLayout.vue';
|
||||
import { formatDate, formatPriceToman } from '@/utils/format';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const loading = ref(true);
|
||||
const paying = ref(false);
|
||||
const mockMode = ref(true);
|
||||
const error = ref('');
|
||||
const details = ref<{
|
||||
bookings: Awaited<ReturnType<typeof fetchPaymentByToken>>['bookings'];
|
||||
totalAmountToman: number;
|
||||
} | null>(null);
|
||||
|
||||
const paymentStatus = computed(() => {
|
||||
const raw = route.query.payment;
|
||||
return raw === 'success' || raw === 'failed' ? raw : null;
|
||||
});
|
||||
|
||||
const canPay = computed(() => {
|
||||
if (!details.value?.bookings.length) return false;
|
||||
if (paymentStatus.value === 'success') return false;
|
||||
return details.value.bookings.some((booking) => booking.status === 'awaiting_payment');
|
||||
});
|
||||
|
||||
async function load() {
|
||||
const token = String(route.query.token ?? '').trim();
|
||||
if (!token) {
|
||||
error.value = 'لینک پرداخت معتبر نیست.';
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
details.value = await fetchPaymentByToken(token);
|
||||
} catch (e) {
|
||||
error.value = e instanceof ApiError ? e.message : 'بارگذاری اطلاعات پرداخت ناموفق بود.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function startPayment() {
|
||||
const token = String(route.query.token ?? '').trim();
|
||||
if (!token) return;
|
||||
|
||||
paying.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const result = await initiateBookingPaymentByToken(token);
|
||||
if (result.mock) {
|
||||
await router.replace({
|
||||
path: '/app/bookings/pay',
|
||||
query: { token, payment: 'success' },
|
||||
});
|
||||
await load();
|
||||
return;
|
||||
}
|
||||
window.location.href = result.startUrl;
|
||||
} catch (e) {
|
||||
error.value = e instanceof ApiError ? e.message : 'شروع پرداخت ناموفق بود.';
|
||||
paying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void load();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user