tipsEnabled
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<RouterView />
|
||||
<ProfileModal />
|
||||
<BookingReviewHost />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BookingReviewHost from '@/components/booking/BookingReviewHost.vue';
|
||||
import ProfileModal from '@/components/profile/ProfileModal.vue';
|
||||
</script>
|
||||
|
||||
@@ -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<PendingBookingReview[]>('/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<PaymentInitResult>(`/bookings/${bookingId}/tip/init`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ amountToman }),
|
||||
});
|
||||
}
|
||||
|
||||
export function completeBooking(id: string) {
|
||||
return api<Booking>(`/bookings/${id}/complete`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function fetchAvailableSlots(
|
||||
businessId: string,
|
||||
dateYmd: string,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="canConfirm || canCancel || canPay"
|
||||
v-if="canConfirm || canCancel || canPay || canComplete"
|
||||
class="mt-auto flex flex-wrap gap-2 border-t border-border pt-3"
|
||||
>
|
||||
<AppButton
|
||||
@@ -76,6 +76,15 @@
|
||||
>
|
||||
پرداخت
|
||||
</AppButton>
|
||||
<AppButton
|
||||
v-if="canComplete"
|
||||
variant="outline"
|
||||
class="!w-auto flex-1 px-3"
|
||||
:loading="acting"
|
||||
@click="emit('complete', bookings)"
|
||||
>
|
||||
انجام شد
|
||||
</AppButton>
|
||||
<AppButton
|
||||
v-if="canConfirm"
|
||||
class="!w-auto flex-1 px-3"
|
||||
@@ -115,12 +124,14 @@ const props = defineProps<{
|
||||
canConfirm: boolean;
|
||||
canCancel: boolean;
|
||||
canPay: boolean;
|
||||
canComplete: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [bookings: Booking[]];
|
||||
cancel: [bookings: Booking[]];
|
||||
pay: [bookings: Booking[]];
|
||||
complete: [bookings: Booking[]];
|
||||
}>();
|
||||
|
||||
const primary = computed(() => props.bookings[0]!);
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
<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>
|
||||
@@ -2,6 +2,10 @@
|
||||
<div class="min-w-0 max-w-full space-y-6 overflow-x-hidden pb-24 lg:pb-0">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<h1 class="text-xl font-bold text-text lg:text-2xl">رزرو خدمات</h1>
|
||||
<BusinessRatingBadge
|
||||
:average-rating="business.averageRating"
|
||||
:review-count="business.reviewCount"
|
||||
/>
|
||||
<RouterLink
|
||||
:to="homeTo"
|
||||
class="inline-flex items-center gap-2 text-sm text-muted transition hover:text-brand-600"
|
||||
@@ -269,6 +273,7 @@ import { fetchBusinessServiceClasses } from '@/api/business-service-classes';
|
||||
import { ApiError } from '@/api/client';
|
||||
import type { Booking, Business, BusinessService, BusinessServiceClass } from '@/api/types';
|
||||
import BookingOrderSummary from '@/components/booking/BookingOrderSummary.vue';
|
||||
import BusinessRatingBadge from '@/components/business/BusinessRatingBadge.vue';
|
||||
import BookingServiceClassStep from '@/components/booking/BookingServiceClassStep.vue';
|
||||
import BookingStepper from '@/components/booking/BookingStepper.vue';
|
||||
import BookingSuccessView from '@/components/booking/BookingSuccessView.vue';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center gap-2" role="radiogroup" :aria-label="ariaLabel">
|
||||
<button
|
||||
v-for="star in 5"
|
||||
:key="star"
|
||||
type="button"
|
||||
class="rounded-lg p-1 transition hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/60"
|
||||
:aria-label="`${star} از ۵`"
|
||||
:aria-checked="star <= modelValue"
|
||||
role="radio"
|
||||
@click="emit('update:modelValue', star)"
|
||||
@mouseenter="hovered = star"
|
||||
@mouseleave="hovered = 0"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
class="h-10 w-10 drop-shadow-sm transition sm:h-12 sm:w-12"
|
||||
:class="star <= displayValue ? 'text-amber-400' : 'text-slate-300 dark:text-slate-600'"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 2.5l2.86 5.8 6.4.93-4.63 4.52 1.09 6.37L12 17.77l-5.72 3.01 1.09-6.37L2.74 9.23l6.4-.93L12 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: number;
|
||||
ariaLabel?: string;
|
||||
}>(),
|
||||
{ ariaLabel: 'امتیاز از ۱ تا ۵' },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: number];
|
||||
}>();
|
||||
|
||||
const hovered = ref(0);
|
||||
|
||||
const displayValue = computed(() => hovered.value || props.modelValue);
|
||||
</script>
|
||||
@@ -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"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<h2 class="truncate text-sm font-semibold text-text">{{ business.title }}</h2>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<h2 class="truncate text-sm font-semibold text-text">{{ business.title }}</h2>
|
||||
<BusinessRatingBadge
|
||||
class="mt-1"
|
||||
:average-rating="business.averageRating"
|
||||
:review-count="business.reviewCount"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
v-if="business.category"
|
||||
class="shrink-0 rounded-lg bg-brand-500/10 px-1.5 py-0.5 text-[10px] text-brand-700 dark:text-brand-500"
|
||||
@@ -49,6 +56,7 @@
|
||||
import { computed } from 'vue';
|
||||
import type { Business } from '@/api/types';
|
||||
import AppIcon from '@/components/ui/AppIcon.vue';
|
||||
import BusinessRatingBadge from '@/components/business/BusinessRatingBadge.vue';
|
||||
import { businessCoverUrl } from '@/utils/media';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="reviewCount > 0 && averageRating != null"
|
||||
class="inline-flex items-center gap-1 rounded-lg bg-amber-500/10 px-2 py-0.5 text-xs font-medium text-amber-700 dark:text-amber-400"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" class="h-3.5 w-3.5 text-amber-400" fill="currentColor" aria-hidden="true">
|
||||
<path
|
||||
d="M10 1.5l2.38 4.82 5.32.77-3.85 3.76.91 5.3L10 13.77l-4.76 2.5.91-5.3-3.85-3.76 5.32-.77L10 1.5z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ formattedRating }}</span>
|
||||
<span class="text-amber-600/70 dark:text-amber-500/70">({{ reviewCount }})</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
averageRating?: number | null;
|
||||
reviewCount?: number;
|
||||
}>();
|
||||
|
||||
const reviewCount = computed(() => props.reviewCount ?? 0);
|
||||
|
||||
const formattedRating = computed(() => {
|
||||
if (props.averageRating == null) return '—';
|
||||
return props.averageRating.toLocaleString('fa-IR', {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -19,6 +19,12 @@
|
||||
|
||||
<h3 class="text-base font-bold text-slate-800 dark:text-text">{{ business.title }}</h3>
|
||||
|
||||
<BusinessRatingBadge
|
||||
class="mx-auto mt-2"
|
||||
:average-rating="business.averageRating"
|
||||
:review-count="business.reviewCount"
|
||||
/>
|
||||
|
||||
<p
|
||||
v-if="business.description"
|
||||
class="mt-2 line-clamp-3 min-h-[4.5rem] text-sm leading-7 text-slate-600 dark:text-muted"
|
||||
@@ -51,6 +57,7 @@
|
||||
import { computed } from 'vue';
|
||||
import type { Business } from '@/api/types';
|
||||
import AppIcon from '@/components/ui/AppIcon.vue';
|
||||
import BusinessRatingBadge from '@/components/business/BusinessRatingBadge.vue';
|
||||
import { businessCoverUrl } from '@/utils/media';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -78,9 +78,11 @@
|
||||
:can-confirm="canConfirmEntry(entry.bookings)"
|
||||
:can-cancel="canCancelEntry(entry.bookings)"
|
||||
:can-pay="canPayEntry(entry.bookings)"
|
||||
:can-complete="canCompleteEntry(entry.bookings)"
|
||||
@confirm="onConfirm"
|
||||
@cancel="onCancelRequest"
|
||||
@pay="onPay"
|
||||
@complete="onComplete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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;
|
||||
|
||||
@@ -163,6 +163,21 @@
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="flex cursor-pointer items-start gap-3 rounded-xl border border-border bg-surface px-4 py-3">
|
||||
<input
|
||||
v-model="bookingForm.tipsEnabled"
|
||||
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>
|
||||
@@ -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 = 'تنظیمات نوبتدهی ذخیره شد.';
|
||||
|
||||
Reference in New Issue
Block a user