update rezervasion
This commit is contained in:
@@ -33,6 +33,13 @@ export function serviceHasActiveClassPrices(
|
||||
);
|
||||
}
|
||||
|
||||
export function isServiceBookableForClass(
|
||||
service: BusinessService,
|
||||
classId: string,
|
||||
): boolean {
|
||||
return service.isActive && !!getClassPriceForService(service, classId);
|
||||
}
|
||||
|
||||
export function isServiceBookable(
|
||||
service: BusinessService,
|
||||
classesEnabled: boolean,
|
||||
@@ -47,7 +54,13 @@ export function servicePriceLabel(
|
||||
service: BusinessService,
|
||||
classesEnabled: boolean,
|
||||
classes: BusinessServiceClass[],
|
||||
classId?: string | null,
|
||||
): string {
|
||||
if (classId) {
|
||||
const price = resolvedServicePrice(service, classId);
|
||||
return price ? `${formatPriceToman(price)} تومان` : 'قیمت تعیین نشده';
|
||||
}
|
||||
|
||||
if (!usesServiceClasses(classesEnabled, classes)) {
|
||||
return `${formatPriceToman(service.price)} تومان`;
|
||||
}
|
||||
|
||||
@@ -60,88 +60,140 @@
|
||||
>
|
||||
{{ servicesError }}
|
||||
</p>
|
||||
<div
|
||||
v-else-if="!bookableServices.length"
|
||||
class="rounded-xl border border-dashed border-border p-4 text-center text-sm text-muted"
|
||||
>
|
||||
{{
|
||||
services.length
|
||||
? 'خدمات این کسبوکار هنوز برای رزرو آماده نیست.'
|
||||
: 'هنوز خدمتی برای این کسبوکار ثبت نشده است.'
|
||||
}}
|
||||
<span v-if="auth.isBusinessOwner" class="mt-2 block">
|
||||
از بخش «کسبوکار» در منو، خدمات و قیمت را اضافه کنید.
|
||||
</span>
|
||||
</div>
|
||||
<p v-else-if="canBook" class="mb-3 text-xs text-muted">
|
||||
خدمات مورد نظر را انتخاب کنید، سپس زمان مراجعه را تعیین کنید.
|
||||
</p>
|
||||
<ul v-if="bookableServices.length" class="grid grid-cols-2 gap-3">
|
||||
<li
|
||||
v-for="svc in bookableServices"
|
||||
:key="svc.id"
|
||||
class="flex flex-col items-center rounded-2xl border p-3 text-center transition"
|
||||
:class="
|
||||
canBook && isServiceSelected(svc.id)
|
||||
? 'border-brand-500 bg-brand-500/10'
|
||||
: 'border-border bg-surface-2'
|
||||
"
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="requiresServiceClass && canBook"
|
||||
class="mb-4 space-y-2 rounded-2xl border border-border bg-surface-2 p-4"
|
||||
>
|
||||
<label
|
||||
v-if="canBook"
|
||||
class="flex w-full cursor-pointer flex-col items-center"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="sr-only"
|
||||
:checked="isServiceSelected(svc.id)"
|
||||
@change="toggleService(svc.id)"
|
||||
/>
|
||||
<span
|
||||
class="mb-2 flex h-5 w-5 items-center justify-center rounded-md border text-xs"
|
||||
<p class="text-sm font-medium text-text">۱. کلاس خودرو را انتخاب کنید</p>
|
||||
<p class="text-xs text-muted">
|
||||
قیمت و مدت هر خدمت بر اساس کلاس انتخابشده نمایش داده میشود.
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
v-for="cls in activeClasses"
|
||||
:key="cls.id"
|
||||
type="button"
|
||||
class="rounded-xl border px-3 py-3 text-center text-sm transition"
|
||||
:class="
|
||||
isServiceSelected(svc.id)
|
||||
? 'border-brand-500 bg-brand-500 text-white'
|
||||
: 'border-border bg-surface'
|
||||
selectedClassId === cls.id
|
||||
? 'border-brand-500 bg-brand-500/15 text-brand-700 dark:text-brand-500'
|
||||
: 'border-border bg-surface text-text hover:border-brand-500/40'
|
||||
"
|
||||
@click="selectClass(cls.id)"
|
||||
>
|
||||
<span class="font-medium">{{ cls.title }}</span>
|
||||
<span v-if="cls.description" class="mt-1 block text-xs text-muted">
|
||||
{{ cls.description }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!bookableServices.length"
|
||||
class="rounded-xl border border-dashed border-border p-4 text-center text-sm text-muted"
|
||||
>
|
||||
{{
|
||||
services.length
|
||||
? 'خدمات این کسبوکار هنوز برای رزرو آماده نیست.'
|
||||
: 'هنوز خدمتی برای این کسبوکار ثبت نشده است.'
|
||||
}}
|
||||
<span v-if="auth.isBusinessOwner" class="mt-2 block">
|
||||
از بخش «کسبوکار» در منو، خدمات و قیمت را اضافه کنید.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-else-if="requiresServiceClass && canBook && !selectedClassId"
|
||||
class="rounded-xl border border-dashed border-border p-4 text-center text-sm text-muted"
|
||||
>
|
||||
برای دیدن خدمات و قیمتها، ابتدا کلاس خودرو را انتخاب کنید.
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-else-if="requiresServiceClass && selectedClassId && !displayedServices.length"
|
||||
class="rounded-xl border border-dashed border-border p-4 text-center text-sm text-muted"
|
||||
>
|
||||
برای این کلاس هنوز خدمتی با قیمت ثبت نشده است.
|
||||
</div>
|
||||
|
||||
<template v-else-if="displayedServices.length">
|
||||
<p v-if="canBook" class="mb-3 text-xs text-muted">
|
||||
{{
|
||||
requiresServiceClass
|
||||
? '۲. خدمات مورد نظر را انتخاب کنید، سپس زمان مراجعه را تعیین کنید.'
|
||||
: 'خدمات مورد نظر را انتخاب کنید، سپس زمان مراجعه را تعیین کنید.'
|
||||
}}
|
||||
</p>
|
||||
<ul class="grid grid-cols-2 gap-3">
|
||||
<li
|
||||
v-for="svc in displayedServices"
|
||||
:key="svc.id"
|
||||
class="flex flex-col items-center rounded-2xl border p-3 text-center transition"
|
||||
:class="
|
||||
canBook && isServiceSelected(svc.id)
|
||||
? 'border-brand-500 bg-brand-500/10'
|
||||
: 'border-border bg-surface-2'
|
||||
"
|
||||
>
|
||||
{{ isServiceSelected(svc.id) ? '✓' : '' }}
|
||||
</span>
|
||||
<p class="font-medium text-text">
|
||||
{{ svc.serviceTemplate?.title ?? 'خدمت' }}
|
||||
</p>
|
||||
<p
|
||||
v-if="svc.serviceTemplate?.description"
|
||||
class="mt-1 line-clamp-2 text-xs text-muted"
|
||||
>
|
||||
{{ svc.serviceTemplate.description }}
|
||||
</p>
|
||||
<div class="mt-auto space-y-0.5 pt-2 text-xs text-muted">
|
||||
<p>{{ servicePriceLabel(svc, classesEnabled, serviceClasses) }}</p>
|
||||
<p>{{ serviceDurationLabel(svc, classesEnabled, serviceClasses) }}</p>
|
||||
</div>
|
||||
</label>
|
||||
<template v-else>
|
||||
<p class="font-medium text-text">
|
||||
{{ svc.serviceTemplate?.title ?? 'خدمت' }}
|
||||
</p>
|
||||
<p
|
||||
v-if="svc.serviceTemplate?.description"
|
||||
class="mt-1 line-clamp-2 text-xs text-muted"
|
||||
>
|
||||
{{ svc.serviceTemplate.description }}
|
||||
</p>
|
||||
<div class="mt-auto space-y-0.5 pt-2 text-xs text-muted">
|
||||
<p>{{ servicePriceLabel(svc, classesEnabled, serviceClasses) }}</p>
|
||||
<p>{{ serviceDurationLabel(svc, classesEnabled, serviceClasses) }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
<label
|
||||
v-if="canBook"
|
||||
class="flex w-full cursor-pointer flex-col items-center"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="sr-only"
|
||||
:checked="isServiceSelected(svc.id)"
|
||||
@change="toggleService(svc.id)"
|
||||
/>
|
||||
<span
|
||||
class="mb-2 flex h-5 w-5 items-center justify-center rounded-md border text-xs"
|
||||
:class="
|
||||
isServiceSelected(svc.id)
|
||||
? 'border-brand-500 bg-brand-500 text-white'
|
||||
: 'border-border bg-surface'
|
||||
"
|
||||
>
|
||||
{{ isServiceSelected(svc.id) ? '✓' : '' }}
|
||||
</span>
|
||||
<p class="font-medium text-text">
|
||||
{{ svc.serviceTemplate?.title ?? 'خدمت' }}
|
||||
</p>
|
||||
<p
|
||||
v-if="svc.serviceTemplate?.description"
|
||||
class="mt-1 line-clamp-2 text-xs text-muted"
|
||||
>
|
||||
{{ svc.serviceTemplate.description }}
|
||||
</p>
|
||||
<div class="mt-auto space-y-0.5 pt-2 text-xs text-muted">
|
||||
<p>{{ servicePriceLabel(svc, classesEnabled, serviceClasses, selectedClassId || null) }}</p>
|
||||
<p>{{ serviceDurationLabel(svc, classesEnabled, serviceClasses, selectedClassId || null) }}</p>
|
||||
</div>
|
||||
</label>
|
||||
<template v-else>
|
||||
<p class="font-medium text-text">
|
||||
{{ svc.serviceTemplate?.title ?? 'خدمت' }}
|
||||
</p>
|
||||
<p
|
||||
v-if="svc.serviceTemplate?.description"
|
||||
class="mt-1 line-clamp-2 text-xs text-muted"
|
||||
>
|
||||
{{ svc.serviceTemplate.description }}
|
||||
</p>
|
||||
<div class="mt-auto space-y-0.5 pt-2 text-xs text-muted">
|
||||
<p>{{ servicePriceLabel(svc, classesEnabled, serviceClasses, selectedClassId || null) }}</p>
|
||||
<p>{{ serviceDurationLabel(svc, classesEnabled, serviceClasses, selectedClassId || null) }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="canBook && selectedServices.length"
|
||||
v-if="canBook && selectedServices.length && canProceedToBooking"
|
||||
class="sticky bottom-2 z-10 rounded-2xl border border-brand-500/40 bg-surface/95 p-4 shadow-lg backdrop-blur"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
@@ -177,6 +229,8 @@
|
||||
<div class="rounded-xl border border-border bg-surface-2 p-3 text-sm">
|
||||
<p class="text-muted">کسبوکار</p>
|
||||
<p class="font-medium text-text">{{ business.title }}</p>
|
||||
<p v-if="selectedClass" class="mt-2 text-muted">کلاس</p>
|
||||
<p v-if="selectedClass" class="font-medium text-text">{{ selectedClass.title }}</p>
|
||||
<ul class="mt-3 space-y-2 border-t border-border pt-3">
|
||||
<li
|
||||
v-for="svc in selectedServices"
|
||||
@@ -186,8 +240,8 @@
|
||||
<span class="text-text">{{ svc.serviceTemplate?.title ?? 'خدمت' }}</span>
|
||||
<span class="shrink-0 text-muted">
|
||||
{{
|
||||
requiresServiceClass && bookingForm.serviceClassId
|
||||
? `${formatPriceToman(resolvedServicePrice(svc, bookingForm.serviceClassId) ?? svc.price)} تومان`
|
||||
requiresServiceClass && selectedClassId
|
||||
? `${formatPriceToman(resolvedServicePrice(svc, selectedClassId) ?? svc.price)} تومان`
|
||||
: `${formatPriceToman(svc.price)} تومان`
|
||||
}}
|
||||
</span>
|
||||
@@ -199,43 +253,13 @@
|
||||
<p class="font-medium text-text">{{ totalDurationLabel }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="requiresServiceClass" class="space-y-1.5">
|
||||
<label for="bookingClass" class="block text-sm font-medium text-muted">
|
||||
کلاس خودرو / دسته
|
||||
</label>
|
||||
<select
|
||||
id="bookingClass"
|
||||
v-model="bookingForm.serviceClassId"
|
||||
required
|
||||
class="w-full rounded-xl border border-border bg-surface-2 px-4 py-3 text-sm text-text outline-none focus:border-brand-500"
|
||||
@change="bookingForm.scheduledAt = ''"
|
||||
>
|
||||
<option value="" disabled>انتخاب کنید</option>
|
||||
<option
|
||||
v-for="cls in activeClasses"
|
||||
:key="cls.id"
|
||||
:value="cls.id"
|
||||
:disabled="!selectedServices.every((svc) => getClassPriceForService(svc, cls.id))"
|
||||
>
|
||||
{{ cls.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<BookingSlotPicker
|
||||
v-if="!requiresServiceClass || bookingForm.serviceClassId"
|
||||
v-model="bookingForm.scheduledAt"
|
||||
label="تاریخ و ساعت مراجعه (شمسی)"
|
||||
:business-id="business.id"
|
||||
:business-service-ids="selectedServiceIds"
|
||||
:service-class-id="bookingForm.serviceClassId || null"
|
||||
:service-class-id="selectedClassId || null"
|
||||
/>
|
||||
<p
|
||||
v-else
|
||||
class="rounded-xl border border-dashed border-border px-4 py-3 text-center text-sm text-muted"
|
||||
>
|
||||
ابتدا کلاس خودرو را انتخاب کنید.
|
||||
</p>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label for="bookingNote" class="block text-sm font-medium text-muted">
|
||||
@@ -276,8 +300,8 @@ import { useAuthStore } from '@/stores/auth';
|
||||
import { formatPriceToman } from '@/utils/format';
|
||||
import {
|
||||
activeServiceClasses,
|
||||
getClassPriceForService,
|
||||
isServiceBookable,
|
||||
isServiceBookableForClass,
|
||||
resolvedServicePrice,
|
||||
serviceDurationLabel,
|
||||
servicePriceLabel,
|
||||
@@ -299,11 +323,11 @@ const error = ref('');
|
||||
const servicesError = ref('');
|
||||
|
||||
const bookingOpen = ref(false);
|
||||
const selectedClassId = ref('');
|
||||
const selectedServiceIds = ref<string[]>([]);
|
||||
const bookingSaving = ref(false);
|
||||
const bookingError = ref('');
|
||||
const bookingForm = ref({
|
||||
serviceClassId: '',
|
||||
scheduledAt: '',
|
||||
note: '',
|
||||
});
|
||||
@@ -321,8 +345,28 @@ const bookableServices = computed(() =>
|
||||
),
|
||||
);
|
||||
|
||||
const displayedServices = computed(() => {
|
||||
if (!requiresServiceClass.value) {
|
||||
return bookableServices.value;
|
||||
}
|
||||
if (!selectedClassId.value) {
|
||||
return [];
|
||||
}
|
||||
return bookableServices.value.filter((svc) =>
|
||||
isServiceBookableForClass(svc, selectedClassId.value),
|
||||
);
|
||||
});
|
||||
|
||||
const selectedClass = computed(() =>
|
||||
activeClasses.value.find((cls) => cls.id === selectedClassId.value) ?? null,
|
||||
);
|
||||
|
||||
const canProceedToBooking = computed(
|
||||
() => !requiresServiceClass.value || !!selectedClassId.value,
|
||||
);
|
||||
|
||||
const selectedServices = computed(() =>
|
||||
bookableServices.value.filter((svc) => selectedServiceIds.value.includes(svc.id)),
|
||||
displayedServices.value.filter((svc) => selectedServiceIds.value.includes(svc.id)),
|
||||
);
|
||||
|
||||
const totalDurationLabel = computed(() => {
|
||||
@@ -330,21 +374,18 @@ const totalDurationLabel = computed(() => {
|
||||
selectedServices.value,
|
||||
classesEnabled.value,
|
||||
serviceClasses.value,
|
||||
bookingForm.value.serviceClassId || null,
|
||||
selectedClassId.value || null,
|
||||
);
|
||||
return `${minutes} دقیقه`;
|
||||
});
|
||||
|
||||
const bookingPriceLabel = computed(() => {
|
||||
if (!selectedServices.value.length) return '—';
|
||||
if (requiresServiceClass.value && !bookingForm.value.serviceClassId) {
|
||||
return 'کلاس را انتخاب کنید';
|
||||
}
|
||||
const total = totalPriceForServices(
|
||||
selectedServices.value,
|
||||
classesEnabled.value,
|
||||
serviceClasses.value,
|
||||
bookingForm.value.serviceClassId || null,
|
||||
selectedClassId.value || null,
|
||||
);
|
||||
return total != null ? `${formatPriceToman(String(total))} تومان` : '—';
|
||||
});
|
||||
@@ -363,7 +404,18 @@ function isServiceSelected(id: string): boolean {
|
||||
return selectedServiceIds.value.includes(id);
|
||||
}
|
||||
|
||||
function selectClass(classId: string) {
|
||||
if (selectedClassId.value === classId) {
|
||||
return;
|
||||
}
|
||||
selectedClassId.value = classId;
|
||||
selectedServiceIds.value = [];
|
||||
}
|
||||
|
||||
function toggleService(id: string) {
|
||||
if (requiresServiceClass.value && !selectedClassId.value) {
|
||||
return;
|
||||
}
|
||||
if (isServiceSelected(id)) {
|
||||
selectedServiceIds.value = selectedServiceIds.value.filter((item) => item !== id);
|
||||
} else {
|
||||
@@ -372,9 +424,8 @@ function toggleService(id: string) {
|
||||
}
|
||||
|
||||
function openBooking() {
|
||||
if (!auth.user || !selectedServices.value.length) return;
|
||||
if (!auth.user || !selectedServices.value.length || !canProceedToBooking.value) return;
|
||||
bookingForm.value = {
|
||||
serviceClassId: '',
|
||||
scheduledAt: '',
|
||||
note: '',
|
||||
};
|
||||
@@ -442,7 +493,7 @@ async function onSubmitBooking() {
|
||||
bookingError.value = 'لطفاً یک ساعت از لیست انتخاب کنید.';
|
||||
return;
|
||||
}
|
||||
if (requiresServiceClass.value && !bookingForm.value.serviceClassId) {
|
||||
if (requiresServiceClass.value && !selectedClassId.value) {
|
||||
bookingError.value = 'لطفاً کلاس خودرو را انتخاب کنید.';
|
||||
return;
|
||||
}
|
||||
@@ -456,11 +507,12 @@ async function onSubmitBooking() {
|
||||
items: selectedServices.value.map((svc) => ({
|
||||
businessServiceId: svc.id,
|
||||
})),
|
||||
serviceClassId: bookingForm.value.serviceClassId || null,
|
||||
serviceClassId: selectedClassId.value || null,
|
||||
scheduledAt: scheduled.toISOString(),
|
||||
note: bookingForm.value.note.trim() || null,
|
||||
});
|
||||
selectedServiceIds.value = [];
|
||||
selectedClassId.value = '';
|
||||
closeBooking();
|
||||
router.push({ name: 'app-bookings' });
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user