From 5eb5bd233c5f936ec26d8163ac328fc11e884126 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Sun, 31 May 2026 21:38:09 +0330 Subject: [PATCH] update rezerv --- src/api/bookings.ts | 34 +- src/api/business-service-classes.ts | 51 +++ src/api/business-services.ts | 17 + src/api/types.ts | 25 ++ src/components/booking/BookingItem.vue | 15 +- src/components/booking/BookingSlotPicker.vue | 24 +- src/components/owner/ServiceClassManager.vue | 230 ++++++++++++++ src/layouts/WebAppLayout.vue | 6 +- src/utils/service-pricing.ts | 179 +++++++++++ src/views/app/AppBusinessDetailView.vue | 308 +++++++++++++++---- src/views/app/AppOwnerView.vue | 295 +++++++++++++++--- 11 files changed, 1056 insertions(+), 128 deletions(-) create mode 100644 src/api/business-service-classes.ts create mode 100644 src/components/owner/ServiceClassManager.vue create mode 100644 src/utils/service-pricing.ts diff --git a/src/api/bookings.ts b/src/api/bookings.ts index 156e424..b125239 100644 --- a/src/api/bookings.ts +++ b/src/api/bookings.ts @@ -5,6 +5,16 @@ export type CreateBookingPayload = { customerId: string; businessId: string; businessServiceId: string; + serviceClassId?: string | null; + scheduledAt: string; + note?: string | null; +}; + +export type CreateBookingBatchPayload = { + customerId: string; + businessId: string; + items: { businessServiceId: string }[]; + serviceClassId?: string | null; scheduledAt: string; note?: string | null; }; @@ -29,6 +39,13 @@ export function createBooking(payload: CreateBookingPayload) { }); } +export function createBookingBatch(payload: CreateBookingBatchPayload) { + return api('/bookings/batch', { + method: 'POST', + body: JSON.stringify(payload), + }); +} + export function cancelBooking(id: string) { return api(`/bookings/${id}/cancel`, { method: 'POST' }); } @@ -39,15 +56,26 @@ export function confirmBooking(id: string) { export function fetchAvailableSlots( businessId: string, - businessServiceId: string, dateYmd: string, + options: { + businessServiceId?: string; + businessServiceIds?: string[]; + serviceClassId?: string; + }, ) { const search = new URLSearchParams({ businessId, - businessServiceId, date: dateYmd, }); - return api<{ date: string; slots: string[] }>( + if (options.businessServiceIds?.length) { + search.set('businessServiceIds', options.businessServiceIds.join(',')); + } else if (options.businessServiceId) { + search.set('businessServiceId', options.businessServiceId); + } + if (options.serviceClassId) { + search.set('serviceClassId', options.serviceClassId); + } + return api<{ date: string; slots: string[]; totalDurationMinutes?: number }>( `/bookings/available-slots?${search}`, ); } diff --git a/src/api/business-service-classes.ts b/src/api/business-service-classes.ts new file mode 100644 index 0000000..ff72c38 --- /dev/null +++ b/src/api/business-service-classes.ts @@ -0,0 +1,51 @@ +import { api } from './client'; +import type { BusinessServiceClass } from './types'; + +export type CreateBusinessServiceClassPayload = { + businessId: string; + title: string; + description?: string | null; + sortOrder?: number; + isActive?: boolean; +}; + +export type UpdateBusinessServiceClassPayload = { + title?: string; + description?: string | null; + sortOrder?: number; + isActive?: boolean; +}; + +export function fetchBusinessServiceClasses( + businessId: string, + options?: { activeOnly?: boolean }, +) { + const search = new URLSearchParams({ businessId }); + if (options?.activeOnly) { + search.set('isActive', 'true'); + } + return api(`/business-service-classes?${search}`); +} + +export function createBusinessServiceClass(payload: CreateBusinessServiceClassPayload) { + return api('/business-service-classes', { + method: 'POST', + body: JSON.stringify(payload), + }); +} + +export function updateBusinessServiceClass( + id: string, + payload: UpdateBusinessServiceClassPayload, +) { + return api(`/business-service-classes/${id}`, { + method: 'PATCH', + body: JSON.stringify(payload), + }); +} + +export function deleteBusinessServiceClass(id: string) { + return api<{ success: true }>(`/business-service-classes/${id}`, { + method: 'DELETE', + }); +} diff --git a/src/api/business-services.ts b/src/api/business-services.ts index 2ebf23f..c51d0f3 100644 --- a/src/api/business-services.ts +++ b/src/api/business-services.ts @@ -45,3 +45,20 @@ export function deleteBusinessService(id: string) { method: 'DELETE', }); } + +export type ClassPriceItem = { + classId: string; + price: number; + durationMinutes?: number | null; + isActive?: boolean; +}; + +export function setBusinessServiceClassPrices( + id: string, + prices: ClassPriceItem[], +) { + return api(`/business-services/${id}/class-prices`, { + method: 'PUT', + body: JSON.stringify({ prices }), + }); +} diff --git a/src/api/types.ts b/src/api/types.ts index ebb0b61..23b3362 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -22,6 +22,7 @@ export type Business = { latitude: number | null; longitude: number | null; isActive: boolean; + serviceClassesEnabled?: boolean; categoryId: string; ownerId?: string | null; category?: BusinessCategory; @@ -63,6 +64,26 @@ export type UpdateBusinessPayload = { address?: string | null; latitude?: number | null; longitude?: number | null; + serviceClassesEnabled?: boolean; +}; + +export type BusinessServiceClass = { + id: string; + businessId: string; + title: string; + description: string | null; + sortOrder: number; + isActive: boolean; +}; + +export type BusinessServiceClassPrice = { + id: string; + businessServiceId: string; + classId: string; + price: string; + durationMinutes: number | null; + isActive: boolean; + serviceClass?: BusinessServiceClass; }; export type ServiceTemplate = { @@ -81,6 +102,7 @@ export type BusinessService = { durationMinutes: number | null; isActive: boolean; serviceTemplate?: ServiceTemplate; + classPrices?: BusinessServiceClassPrice[]; }; export type BusinessServiceRequestStatus = 'pending' | 'approved' | 'rejected'; @@ -119,6 +141,8 @@ export type Booking = { customerId: string; businessId: string; businessServiceId: string; + serviceClassId?: string | null; + bookingGroupId?: string | null; scheduledAt: string; status: BookingStatus; note: string | null; @@ -131,4 +155,5 @@ export type Booking = { }; business?: Business; businessService?: BusinessService; + serviceClass?: BusinessServiceClass; }; diff --git a/src/components/booking/BookingItem.vue b/src/components/booking/BookingItem.vue index c17186a..bdfd3c1 100644 --- a/src/components/booking/BookingItem.vue +++ b/src/components/booking/BookingItem.vue @@ -8,6 +8,9 @@

{{ serviceTitle }}

+

+ {{ groupHint }} +

{{ formatDate(booking.scheduledAt) }}

@@ -72,8 +75,16 @@ const emit = defineEmits<{ cancel: [booking: Booking]; }>(); -const serviceTitle = computed( - () => props.booking.businessService?.serviceTemplate?.title ?? 'خدمت', +const serviceTitle = computed(() => { + const title = props.booking.businessService?.serviceTemplate?.title ?? 'خدمت'; + if (props.booking.serviceClass?.title) { + return `${title} · ${props.booking.serviceClass.title}`; + } + return title; +}); + +const groupHint = computed(() => + props.booking.bookingGroupId ? 'بخشی از رزرو چندخدمتی' : '', ); const statusClass = computed(() => { diff --git a/src/components/booking/BookingSlotPicker.vue b/src/components/booking/BookingSlotPicker.vue index ab38d7d..3cbb709 100644 --- a/src/components/booking/BookingSlotPicker.vue +++ b/src/components/booking/BookingSlotPicker.vue @@ -93,7 +93,8 @@ const model = defineModel({ default: '' }); const props = defineProps<{ label?: string; businessId: string; - businessServiceId: string; + businessServiceIds: string[]; + serviceClassId?: string | null; }>(); const dateParts = ref>({ @@ -145,11 +146,10 @@ async function loadSlots() { try { ensureDateNotPast(); const ymd = jalaliToGregorianYmd(dateParts.value); - const res = await fetchAvailableSlots( - props.businessId, - props.businessServiceId, - ymd, - ); + const res = await fetchAvailableSlots(props.businessId, ymd, { + businessServiceIds: props.businessServiceIds, + serviceClassId: props.serviceClassId ?? undefined, + }); slots.value = res.slots; if (model.value && !slots.value.includes(model.value)) { model.value = slots.value[0] ?? ''; @@ -182,12 +182,18 @@ function selectSlot(iso: string) { } watch( - () => [props.businessId, props.businessServiceId], + () => [props.businessId, props.businessServiceIds.join(','), props.serviceClassId], () => { model.value = ''; - loadSlots(); + if (props.businessServiceIds.length) { + loadSlots(); + } }, ); -onMounted(loadSlots); +onMounted(() => { + if (props.businessServiceIds.length) { + loadSlots(); + } +}); diff --git a/src/components/owner/ServiceClassManager.vue b/src/components/owner/ServiceClassManager.vue new file mode 100644 index 0000000..e7ac37e --- /dev/null +++ b/src/components/owner/ServiceClassManager.vue @@ -0,0 +1,230 @@ +