diff --git a/src/App.vue b/src/App.vue index 8d10130..d474239 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,8 +1,10 @@ diff --git a/src/api/bookings.ts b/src/api/bookings.ts index 97ce2c5..82074a4 100644 --- a/src/api/bookings.ts +++ b/src/api/bookings.ts @@ -86,6 +86,37 @@ export function initiateBookingPaymentByToken(token: string) { ); } +export type PendingBookingReview = { + bookingId: string; + businessId: string; + businessTitle: string; + serviceTitle: string; + scheduledAt: string; + tipsEnabled: boolean; +}; + +export function fetchPendingReviews() { + return api('/bookings/pending-reviews'); +} + +export function submitBookingReview(bookingId: string, rating: number) { + return api<{ id: string; rating: number }>(`/bookings/${bookingId}/review`, { + method: 'POST', + body: JSON.stringify({ rating }), + }); +} + +export function initiateBookingTip(bookingId: string, amountToman: number) { + return api(`/bookings/${bookingId}/tip/init`, { + method: 'POST', + body: JSON.stringify({ amountToman }), + }); +} + +export function completeBooking(id: string) { + return api(`/bookings/${id}/complete`, { method: 'POST' }); +} + export function fetchAvailableSlots( businessId: string, dateYmd: string, diff --git a/src/api/types.ts b/src/api/types.ts index 83a7c73..650adc9 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -37,6 +37,9 @@ export type Business = { slotStepMinutes?: number; serviceClassesEnabled?: boolean; autoApproveBookings?: boolean; + tipsEnabled?: boolean; + averageRating?: number | null; + reviewCount?: number; createdAt?: string; updatedAt?: string; }; @@ -53,6 +56,7 @@ export type UpdateBusinessPayload = { slotStepMinutes?: number; serviceClassesEnabled?: boolean; autoApproveBookings?: boolean; + tipsEnabled?: boolean; }; export type ServiceTemplate = { diff --git a/src/components/booking/BookingItem.vue b/src/components/booking/BookingItem.vue index f5253bd..3643faf 100644 --- a/src/components/booking/BookingItem.vue +++ b/src/components/booking/BookingItem.vue @@ -65,7 +65,7 @@
پرداخت + + انجام شد + (); const emit = defineEmits<{ confirm: [bookings: Booking[]]; cancel: [bookings: Booking[]]; pay: [bookings: Booking[]]; + complete: [bookings: Booking[]]; }>(); const primary = computed(() => props.bookings[0]!); diff --git a/src/components/booking/BookingReviewHost.vue b/src/components/booking/BookingReviewHost.vue new file mode 100644 index 0000000..67d38de --- /dev/null +++ b/src/components/booking/BookingReviewHost.vue @@ -0,0 +1,217 @@ + + + diff --git a/src/components/booking/DesktopBookingFlow.vue b/src/components/booking/DesktopBookingFlow.vue index c3c1e68..e80db71 100644 --- a/src/components/booking/DesktopBookingFlow.vue +++ b/src/components/booking/DesktopBookingFlow.vue @@ -2,6 +2,10 @@

رزرو خدمات

+ +
+ +
+ + + diff --git a/src/components/business/BusinessCard.vue b/src/components/business/BusinessCard.vue index 6b46cca..e3cc1a4 100644 --- a/src/components/business/BusinessCard.vue +++ b/src/components/business/BusinessCard.vue @@ -3,9 +3,16 @@ :to="{ name: 'app-business-detail', params: { id: business.id } }" class="flex items-center gap-3 rounded-xl border border-border bg-surface-2 p-2.5 transition hover:border-brand-500/40 hover:shadow-sm" > -
-
-

{{ business.title }}

+
+
+
+

{{ business.title }}

+ +
+
+ + {{ formattedRating }} + ({{ reviewCount }}) +
+ + + diff --git a/src/components/home/HomeBusinessCard.vue b/src/components/home/HomeBusinessCard.vue index 8b3da26..93c25a4 100644 --- a/src/components/home/HomeBusinessCard.vue +++ b/src/components/home/HomeBusinessCard.vue @@ -19,6 +19,12 @@

{{ business.title }}

+ +

@@ -138,6 +140,7 @@ import { useRouter } from 'vue-router'; import { cancelBooking, confirmBooking, + completeBooking, fetchBookings, initiateBookingPayment, } from '@/api/bookings'; @@ -277,6 +280,31 @@ async function onConfirm(entry: Booking[]) { } } +function canComplete(booking: Booking) { + return auth.isBusinessOwner && booking.status === 'confirmed'; +} + +function canCompleteEntry(entry: Booking[]) { + return entry.some((booking) => canComplete(booking)); +} + +async function onComplete(entry: Booking[]) { + const completable = entry.filter((booking) => canComplete(booking)); + if (!completable.length) return; + + actingKey.value = entry[0]?.bookingGroupId ?? entry[0]?.id ?? null; + try { + const updated = await Promise.all( + completable.map((booking) => completeBooking(booking.id)), + ); + applyUpdatedBookings(updated); + } catch (e) { + error.value = e instanceof ApiError ? e.message : 'ثبت انجام خدمت ناموفق بود.'; + } finally { + actingKey.value = null; + } +} + async function onPay(entry: Booking[]) { const payable = entry.find((booking) => canPay(booking)); if (!payable) return; diff --git a/src/views/app/AppOwnerView.vue b/src/views/app/AppOwnerView.vue index e60f4cb..5ad6a0d 100644 --- a/src/views/app/AppOwnerView.vue +++ b/src/views/app/AppOwnerView.vue @@ -163,6 +163,21 @@ + + ذخیره تنظیمات نوبت‌دهی @@ -543,6 +558,7 @@ const bookingForm = ref({ concurrentCapacity: '1', slotStepMinutes: '30', autoApproveBookings: false, + tipsEnabled: false, }); const bookingExpanded = ref(false); @@ -590,7 +606,8 @@ const servicesSummary = computed(() => { const bookingSummary = computed(() => { const auto = bookingForm.value.autoApproveBookings ? 'تأیید خودکار' : 'تأیید دستی'; - return `${bookingForm.value.concurrentCapacity} همزمان · گام ${bookingForm.value.slotStepMinutes} دقیقه · ${auto}`; + const tips = bookingForm.value.tipsEnabled ? 'حسن انجام کار فعال' : 'بدون حسن انجام کار'; + return `${bookingForm.value.concurrentCapacity} همزمان · گام ${bookingForm.value.slotStepMinutes} دقیقه · ${auto} · ${tips}`; }); function emptyClassPriceForm() { @@ -690,6 +707,7 @@ onMounted(async () => { concurrentCapacity: String(business.value.concurrentCapacity ?? 1), slotStepMinutes: String(business.value.slotStepMinutes ?? 30), autoApproveBookings: business.value.autoApproveBookings === true, + tipsEnabled: business.value.tipsEnabled === true, }; await loadServicesAndTemplates(); } @@ -813,6 +831,7 @@ async function onSaveBookingSettings() { concurrentCapacity, slotStepMinutes, autoApproveBookings: bookingForm.value.autoApproveBookings, + tipsEnabled: bookingForm.value.tipsEnabled, }); tenant.syncSingleBusiness(business.value); bookingMessage.value = 'تنظیمات نوبت‌دهی ذخیره شد.';