AddFirstBookingDiscount
This commit is contained in:
@@ -38,6 +38,11 @@ export type Business = {
|
|||||||
serviceClassesEnabled?: boolean;
|
serviceClassesEnabled?: boolean;
|
||||||
autoApproveBookings?: boolean;
|
autoApproveBookings?: boolean;
|
||||||
tipsEnabled?: boolean;
|
tipsEnabled?: boolean;
|
||||||
|
promoDiscountEnabled?: boolean;
|
||||||
|
promoDiscountTitle?: string | null;
|
||||||
|
promoDiscountPercent?: number | null;
|
||||||
|
promoDiscountCode?: string | null;
|
||||||
|
promoDiscountMode?: 'first_booking' | 'always';
|
||||||
averageRating?: number | null;
|
averageRating?: number | null;
|
||||||
reviewCount?: number;
|
reviewCount?: number;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
@@ -57,6 +62,11 @@ export type UpdateBusinessPayload = {
|
|||||||
serviceClassesEnabled?: boolean;
|
serviceClassesEnabled?: boolean;
|
||||||
autoApproveBookings?: boolean;
|
autoApproveBookings?: boolean;
|
||||||
tipsEnabled?: boolean;
|
tipsEnabled?: boolean;
|
||||||
|
promoDiscountEnabled?: boolean;
|
||||||
|
promoDiscountTitle?: string | null;
|
||||||
|
promoDiscountPercent?: number | null;
|
||||||
|
promoDiscountCode?: string | null;
|
||||||
|
promoDiscountMode?: 'first_booking' | 'always';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServiceTemplate = {
|
export type ServiceTemplate = {
|
||||||
@@ -122,6 +132,9 @@ export type Booking = {
|
|||||||
paymentToken?: string | null;
|
paymentToken?: string | null;
|
||||||
paymentAmount?: string | null;
|
paymentAmount?: string | null;
|
||||||
loyaltyRedeemed?: boolean;
|
loyaltyRedeemed?: boolean;
|
||||||
|
promoDiscountApplied?: boolean;
|
||||||
|
promoDiscountPercent?: number | null;
|
||||||
|
promoDiscountTitle?: string | null;
|
||||||
business?: Business;
|
business?: Business;
|
||||||
businessService?: BusinessService;
|
businessService?: BusinessService;
|
||||||
serviceClass?: BusinessServiceClass;
|
serviceClass?: BusinessServiceClass;
|
||||||
|
|||||||
@@ -6,6 +6,15 @@
|
|||||||
@browse="scrollToServices"
|
@browse="scrollToServices"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FirstBookingDiscountBanner
|
||||||
|
:enabled="discountEnabled"
|
||||||
|
:title="discountTitle"
|
||||||
|
:percent="discountPercent"
|
||||||
|
:code="discountCode"
|
||||||
|
:mode="discountMode"
|
||||||
|
:compact="false"
|
||||||
|
/>
|
||||||
|
|
||||||
<HomeServicesSection
|
<HomeServicesSection
|
||||||
:items="services"
|
:items="services"
|
||||||
:loading="loadingServices"
|
:loading="loadingServices"
|
||||||
@@ -18,11 +27,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import HomeFeatureBar from '@/components/home/HomeFeatureBar.vue';
|
import HomeFeatureBar from '@/components/home/HomeFeatureBar.vue';
|
||||||
import HomeHeroSection from '@/components/home/HomeHeroSection.vue';
|
import HomeHeroSection from '@/components/home/HomeHeroSection.vue';
|
||||||
import HomeServicesSection from '@/components/home/HomeServicesSection.vue';
|
import HomeServicesSection from '@/components/home/HomeServicesSection.vue';
|
||||||
|
import FirstBookingDiscountBanner from '@/components/landing/FirstBookingDiscountBanner.vue';
|
||||||
import DesktopSiteLayout from '@/layouts/DesktopSiteLayout.vue';
|
import DesktopSiteLayout from '@/layouts/DesktopSiteLayout.vue';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { useTenantStore } from '@/stores/tenant';
|
import { useTenantStore } from '@/stores/tenant';
|
||||||
@@ -42,7 +53,25 @@ const props = withDefaults(
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const tenant = useTenantStore();
|
const tenant = useTenantStore();
|
||||||
const { landingBusinessId } = storeToRefs(tenant);
|
const { landingBusinessId, landingBusiness } = storeToRefs(tenant);
|
||||||
|
|
||||||
|
const discountEnabled = computed(
|
||||||
|
() =>
|
||||||
|
landingBusiness.value?.promoDiscountEnabled === true &&
|
||||||
|
!!landingBusiness.value?.promoDiscountPercent,
|
||||||
|
);
|
||||||
|
const discountTitle = computed(
|
||||||
|
() => landingBusiness.value?.promoDiscountTitle?.trim() || 'تخفیف ویژه',
|
||||||
|
);
|
||||||
|
const discountPercent = computed(
|
||||||
|
() => landingBusiness.value?.promoDiscountPercent ?? null,
|
||||||
|
);
|
||||||
|
const discountCode = computed(
|
||||||
|
() => landingBusiness.value?.promoDiscountCode?.trim() ?? null,
|
||||||
|
);
|
||||||
|
const discountMode = computed(
|
||||||
|
() => landingBusiness.value?.promoDiscountMode ?? 'first_booking',
|
||||||
|
);
|
||||||
|
|
||||||
function scrollToServices() {
|
function scrollToServices() {
|
||||||
const el = document.getElementById('services');
|
const el = document.getElementById('services');
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<section
|
||||||
|
v-if="enabled"
|
||||||
|
id="club"
|
||||||
|
class="scroll-mt-24"
|
||||||
|
:class="compact ? 'px-5 pb-6' : 'mx-auto max-w-[1200px] px-6 pb-10 lg:px-8'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between gap-3 rounded-2xl bg-brand-50 px-4 py-4 dark:bg-brand-500/10"
|
||||||
|
>
|
||||||
|
<div class="min-w-0 flex-1 space-y-1">
|
||||||
|
<p class="text-xs text-landing-muted dark:text-muted">{{ title }}</p>
|
||||||
|
<p class="text-2xl font-black text-brand-600 dark:text-brand-500">
|
||||||
|
{{ percentLabel }}٪ تخفیف
|
||||||
|
</p>
|
||||||
|
<p v-if="code" class="text-xs font-medium text-landing-ink dark:text-text">
|
||||||
|
کد تخفیف:
|
||||||
|
<span class="font-bold" dir="ltr">{{ code }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-[11px] leading-5 text-landing-muted dark:text-muted">
|
||||||
|
{{ helperText }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex h-16 w-16 shrink-0 items-center justify-center rounded-2xl bg-white shadow-sm dark:bg-surface dark:shadow-black/20"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<AppIcon name="gift" size="lg" class="text-brand-600 dark:text-brand-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import AppIcon from '@/components/ui/AppIcon.vue';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
enabled?: boolean;
|
||||||
|
title?: string | null;
|
||||||
|
percent?: number | null;
|
||||||
|
code?: string | null;
|
||||||
|
mode?: 'first_booking' | 'always' | null;
|
||||||
|
compact?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
title: 'تخفیف ویژه',
|
||||||
|
percent: null,
|
||||||
|
code: null,
|
||||||
|
mode: 'first_booking',
|
||||||
|
compact: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const percentLabel = computed(() => {
|
||||||
|
const n = props.percent ?? 0;
|
||||||
|
return n.toLocaleString('fa-IR');
|
||||||
|
});
|
||||||
|
|
||||||
|
const helperText = computed(() =>
|
||||||
|
props.mode === 'always'
|
||||||
|
? 'این تخفیف هنگام رزرو بهصورت خودکار روی مبلغ اعمال میشود.'
|
||||||
|
: 'برای اولین رزرو شما در این کسبوکار بهصورت خودکار اعمال میشود.',
|
||||||
|
);
|
||||||
|
</script>
|
||||||
@@ -70,26 +70,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="club" class="scroll-mt-24 px-5 pb-6">
|
<FirstBookingDiscountBanner
|
||||||
<div
|
:enabled="discountEnabled"
|
||||||
class="flex items-center justify-between gap-3 rounded-2xl bg-brand-50 px-4 py-4 dark:bg-brand-500/10"
|
:title="discountTitle"
|
||||||
>
|
:percent="discountPercent"
|
||||||
<div class="min-w-0 flex-1 space-y-1">
|
:code="discountCode"
|
||||||
<p class="text-xs text-landing-muted dark:text-muted">اولین رزرو شما</p>
|
:mode="discountMode"
|
||||||
<p class="text-2xl font-black text-brand-600 dark:text-brand-500">۱۰٪ تخفیف</p>
|
compact
|
||||||
<p class="text-xs font-medium text-landing-ink dark:text-text">
|
/>
|
||||||
کد تخفیف:
|
|
||||||
<span class="font-bold" dir="ltr">NEW10</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex h-16 w-16 shrink-0 items-center justify-center rounded-2xl bg-white shadow-sm dark:bg-surface dark:shadow-black/20"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<AppIcon name="gift" size="lg" class="text-brand-600 dark:text-brand-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<HomeServicesSection
|
<HomeServicesSection
|
||||||
:items="services"
|
:items="services"
|
||||||
@@ -115,6 +103,7 @@ import { computed } from 'vue';
|
|||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import BusinessImageGallery from '@/components/business/BusinessImageGallery.vue';
|
import BusinessImageGallery from '@/components/business/BusinessImageGallery.vue';
|
||||||
import HomeServicesSection from '@/components/home/HomeServicesSection.vue';
|
import HomeServicesSection from '@/components/home/HomeServicesSection.vue';
|
||||||
|
import FirstBookingDiscountBanner from '@/components/landing/FirstBookingDiscountBanner.vue';
|
||||||
import AppIcon from '@/components/ui/AppIcon.vue';
|
import AppIcon from '@/components/ui/AppIcon.vue';
|
||||||
import { useTenantStore } from '@/stores/tenant';
|
import { useTenantStore } from '@/stores/tenant';
|
||||||
import type { AppIconName } from '@/types/icons';
|
import type { AppIconName } from '@/types/icons';
|
||||||
@@ -138,17 +127,40 @@ const businessTitle = computed(() => landingBusiness.value?.title ?? '');
|
|||||||
const businessSubtitle = computed(() => landingBusiness.value?.subtitle?.trim() ?? '');
|
const businessSubtitle = computed(() => landingBusiness.value?.subtitle?.trim() ?? '');
|
||||||
const aboutText = computed(() => landingBusiness.value?.description?.trim() ?? '');
|
const aboutText = computed(() => landingBusiness.value?.description?.trim() ?? '');
|
||||||
|
|
||||||
const quickActions: {
|
const discountEnabled = computed(
|
||||||
id: string;
|
() =>
|
||||||
label: string;
|
landingBusiness.value?.promoDiscountEnabled === true &&
|
||||||
icon: AppIconName;
|
!!landingBusiness.value?.promoDiscountPercent,
|
||||||
targetId: string;
|
);
|
||||||
}[] = [
|
const discountTitle = computed(
|
||||||
{ id: 'services', label: 'خدمات کارواش', icon: 'car', targetId: 'services' },
|
() => landingBusiness.value?.promoDiscountTitle?.trim() || 'تخفیف ویژه',
|
||||||
{ id: 'pricing', label: 'تعرفهها', icon: 'badge-percent', targetId: 'services' },
|
);
|
||||||
{ id: 'club', label: 'باشگاه مشتریان', icon: 'sparkles', targetId: 'club' },
|
const discountPercent = computed(
|
||||||
{ id: 'about', label: 'درباره ما', icon: 'info', targetId: 'about' },
|
() => landingBusiness.value?.promoDiscountPercent ?? null,
|
||||||
];
|
);
|
||||||
|
const discountCode = computed(
|
||||||
|
() => landingBusiness.value?.promoDiscountCode?.trim() ?? null,
|
||||||
|
);
|
||||||
|
const discountMode = computed(
|
||||||
|
() => landingBusiness.value?.promoDiscountMode ?? 'first_booking',
|
||||||
|
);
|
||||||
|
|
||||||
|
const quickActions = computed(() => {
|
||||||
|
const items: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon: AppIconName;
|
||||||
|
targetId: string;
|
||||||
|
}[] = [
|
||||||
|
{ id: 'services', label: 'خدمات', icon: 'car', targetId: 'services' },
|
||||||
|
{ id: 'pricing', label: 'تعرفهها', icon: 'badge-percent', targetId: 'services' },
|
||||||
|
];
|
||||||
|
if (discountEnabled.value) {
|
||||||
|
items.push({ id: 'club', label: 'تخفیف ویژه', icon: 'sparkles', targetId: 'club' });
|
||||||
|
}
|
||||||
|
items.push({ id: 'about', label: 'درباره ما', icon: 'info', targetId: 'about' });
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
function scrollTo(targetId: string) {
|
function scrollTo(targetId: string) {
|
||||||
const el = document.getElementById(targetId);
|
const el = document.getElementById(targetId);
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<footer class="border-t border-landing-line bg-landing-surface dark:border-border dark:bg-surface-2">
|
<footer class="border-t border-landing-line bg-landing-surface dark:border-border dark:bg-surface-2">
|
||||||
<div id="club" class="sr-only" aria-hidden="true" />
|
|
||||||
|
|
||||||
<div class="mx-auto max-w-[1200px] px-6 py-12 lg:px-8">
|
<div class="mx-auto max-w-[1200px] px-6 py-12 lg:px-8">
|
||||||
<div class="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-4">
|
<div class="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@@ -178,6 +178,56 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</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.promoDiscountEnabled"
|
||||||
|
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>
|
||||||
|
<template v-if="bookingForm.promoDiscountEnabled">
|
||||||
|
<AppInput
|
||||||
|
id="ownerPromoDiscountTitle"
|
||||||
|
v-model="bookingForm.promoDiscountTitle"
|
||||||
|
label="عنوان تخفیف"
|
||||||
|
placeholder="مثلاً: اولین رزرو شما / تخفیف ویژه تابستان"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<AppInput
|
||||||
|
id="ownerPromoDiscountPercent"
|
||||||
|
v-model="bookingForm.promoDiscountPercent"
|
||||||
|
label="درصد تخفیف"
|
||||||
|
type="number"
|
||||||
|
inputmode="numeric"
|
||||||
|
ltr
|
||||||
|
placeholder="مثلاً ۱۰"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<AppInput
|
||||||
|
id="ownerPromoDiscountCode"
|
||||||
|
v-model="bookingForm.promoDiscountCode"
|
||||||
|
label="کد تخفیف (نمایشی)"
|
||||||
|
ltr
|
||||||
|
placeholder="مثلاً NEW10"
|
||||||
|
/>
|
||||||
|
<AppSelect
|
||||||
|
id="ownerPromoDiscountMode"
|
||||||
|
v-model="bookingForm.promoDiscountMode"
|
||||||
|
label="محدوده اعمال"
|
||||||
|
:options="promoDiscountModeOptions"
|
||||||
|
/>
|
||||||
|
<p class="-mt-2 text-xs text-muted">
|
||||||
|
اگر کد خالی باشد، بهصورت خودکار با فرمت NEW + درصد ساخته میشود (مثلاً NEW10).
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
<AppButton :loading="savingBooking" @click="onSaveBookingSettings">
|
<AppButton :loading="savingBooking" @click="onSaveBookingSettings">
|
||||||
ذخیره تنظیمات نوبتدهی
|
ذخیره تنظیمات نوبتدهی
|
||||||
</AppButton>
|
</AppButton>
|
||||||
@@ -587,8 +637,18 @@ const bookingForm = ref({
|
|||||||
slotStepMinutes: '30',
|
slotStepMinutes: '30',
|
||||||
autoApproveBookings: false,
|
autoApproveBookings: false,
|
||||||
tipsEnabled: false,
|
tipsEnabled: false,
|
||||||
|
promoDiscountEnabled: false,
|
||||||
|
promoDiscountTitle: 'اولین رزرو شما',
|
||||||
|
promoDiscountPercent: '10',
|
||||||
|
promoDiscountCode: '',
|
||||||
|
promoDiscountMode: 'first_booking',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const promoDiscountModeOptions = [
|
||||||
|
{ value: 'first_booking', label: 'فقط اولین رزرو مشتری' },
|
||||||
|
{ value: 'always', label: 'همه رزروها (تا وقتی فعال است)' },
|
||||||
|
];
|
||||||
|
|
||||||
const bookingExpanded = ref(false);
|
const bookingExpanded = ref(false);
|
||||||
const bookingMessage = ref('');
|
const bookingMessage = ref('');
|
||||||
const bookingError = ref('');
|
const bookingError = ref('');
|
||||||
@@ -637,7 +697,10 @@ const servicesSummary = computed(() => {
|
|||||||
const bookingSummary = computed(() => {
|
const bookingSummary = computed(() => {
|
||||||
const auto = bookingForm.value.autoApproveBookings ? 'تأیید خودکار' : 'تأیید دستی';
|
const auto = bookingForm.value.autoApproveBookings ? 'تأیید خودکار' : 'تأیید دستی';
|
||||||
const tips = bookingForm.value.tipsEnabled ? 'حسن انجام کار فعال' : 'بدون حسن انجام کار';
|
const tips = bookingForm.value.tipsEnabled ? 'حسن انجام کار فعال' : 'بدون حسن انجام کار';
|
||||||
return `${bookingForm.value.concurrentCapacity} همزمان · گام ${bookingForm.value.slotStepMinutes} دقیقه · ${auto} · ${tips}`;
|
const discount = bookingForm.value.promoDiscountEnabled
|
||||||
|
? `${bookingForm.value.promoDiscountTitle || 'تخفیف'} ${bookingForm.value.promoDiscountPercent}٪`
|
||||||
|
: 'بدون تخفیف';
|
||||||
|
return `${bookingForm.value.concurrentCapacity} همزمان · گام ${bookingForm.value.slotStepMinutes} دقیقه · ${auto} · ${tips} · ${discount}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
function emptyClassPriceForm() {
|
function emptyClassPriceForm() {
|
||||||
@@ -738,6 +801,14 @@ onMounted(async () => {
|
|||||||
slotStepMinutes: String(business.value.slotStepMinutes ?? 30),
|
slotStepMinutes: String(business.value.slotStepMinutes ?? 30),
|
||||||
autoApproveBookings: business.value.autoApproveBookings === true,
|
autoApproveBookings: business.value.autoApproveBookings === true,
|
||||||
tipsEnabled: business.value.tipsEnabled === true,
|
tipsEnabled: business.value.tipsEnabled === true,
|
||||||
|
promoDiscountEnabled: business.value.promoDiscountEnabled === true,
|
||||||
|
promoDiscountTitle: business.value.promoDiscountTitle?.trim() || 'اولین رزرو شما',
|
||||||
|
promoDiscountPercent:
|
||||||
|
business.value.promoDiscountPercent != null
|
||||||
|
? String(business.value.promoDiscountPercent)
|
||||||
|
: '10',
|
||||||
|
promoDiscountCode: business.value.promoDiscountCode ?? '',
|
||||||
|
promoDiscountMode: business.value.promoDiscountMode === 'always' ? 'always' : 'first_booking',
|
||||||
};
|
};
|
||||||
await loadServicesAndTemplates();
|
await loadServicesAndTemplates();
|
||||||
}
|
}
|
||||||
@@ -851,6 +922,33 @@ async function onSaveBookingSettings() {
|
|||||||
bookingMessage.value = '';
|
bookingMessage.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let promoDiscountPercent: number | null = null;
|
||||||
|
let promoDiscountCode: string | null = null;
|
||||||
|
let promoDiscountTitle: string | null = null;
|
||||||
|
let promoDiscountMode: 'first_booking' | 'always' = 'first_booking';
|
||||||
|
if (bookingForm.value.promoDiscountEnabled) {
|
||||||
|
promoDiscountTitle = bookingForm.value.promoDiscountTitle.trim();
|
||||||
|
if (!promoDiscountTitle) {
|
||||||
|
bookingError.value = 'عنوان تخفیف را وارد کنید.';
|
||||||
|
bookingMessage.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
promoDiscountPercent = Number.parseInt(bookingForm.value.promoDiscountPercent, 10);
|
||||||
|
if (
|
||||||
|
!Number.isFinite(promoDiscountPercent) ||
|
||||||
|
promoDiscountPercent < 1 ||
|
||||||
|
promoDiscountPercent > 100
|
||||||
|
) {
|
||||||
|
bookingError.value = 'درصد تخفیف باید عددی بین ۱ تا ۱۰۰ باشد.';
|
||||||
|
bookingMessage.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
promoDiscountCode = bookingForm.value.promoDiscountCode.trim().toUpperCase() || null;
|
||||||
|
promoDiscountMode =
|
||||||
|
bookingForm.value.promoDiscountMode === 'always' ? 'always' : 'first_booking';
|
||||||
|
}
|
||||||
|
|
||||||
savingBooking.value = true;
|
savingBooking.value = true;
|
||||||
bookingError.value = '';
|
bookingError.value = '';
|
||||||
bookingMessage.value = '';
|
bookingMessage.value = '';
|
||||||
@@ -862,6 +960,11 @@ async function onSaveBookingSettings() {
|
|||||||
slotStepMinutes,
|
slotStepMinutes,
|
||||||
autoApproveBookings: bookingForm.value.autoApproveBookings,
|
autoApproveBookings: bookingForm.value.autoApproveBookings,
|
||||||
tipsEnabled: bookingForm.value.tipsEnabled,
|
tipsEnabled: bookingForm.value.tipsEnabled,
|
||||||
|
promoDiscountEnabled: bookingForm.value.promoDiscountEnabled,
|
||||||
|
promoDiscountTitle,
|
||||||
|
promoDiscountPercent,
|
||||||
|
promoDiscountCode,
|
||||||
|
promoDiscountMode,
|
||||||
});
|
});
|
||||||
tenant.syncSingleBusiness(business.value);
|
tenant.syncSingleBusiness(business.value);
|
||||||
bookingMessage.value = 'تنظیمات نوبتدهی ذخیره شد.';
|
bookingMessage.value = 'تنظیمات نوبتدهی ذخیره شد.';
|
||||||
|
|||||||
@@ -58,12 +58,25 @@
|
|||||||
</template>
|
</template>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="promoDiscountLabel"
|
||||||
|
class="text-xs text-brand-700 dark:text-brand-400"
|
||||||
|
>
|
||||||
|
{{ promoDiscountLabel }}
|
||||||
|
</p>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<ul class="space-y-1 text-sm text-muted">
|
<ul class="space-y-1 text-sm text-muted">
|
||||||
<li v-for="booking in details.bookings" :key="booking.id">
|
<li v-for="booking in details.bookings" :key="booking.id">
|
||||||
{{ booking.businessService?.serviceTemplate?.title ?? 'خدمت' }}
|
{{ booking.businessService?.serviceTemplate?.title ?? 'خدمت' }}
|
||||||
<span v-if="booking.loyaltyRedeemed" class="text-brand-600"> (رایگان)</span>
|
<span v-if="booking.loyaltyRedeemed" class="text-brand-600"> (رایگان)</span>
|
||||||
|
<span
|
||||||
|
v-else-if="booking.promoDiscountApplied && booking.promoDiscountPercent"
|
||||||
|
class="text-brand-600"
|
||||||
|
>
|
||||||
|
({{ booking.promoDiscountPercent.toLocaleString('fa-IR') }}٪
|
||||||
|
{{ booking.promoDiscountTitle || 'تخفیف' }})
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -127,6 +140,13 @@ const canPay = computed(() => {
|
|||||||
return details.value.bookings.some((booking) => booking.status === 'awaiting_payment');
|
return details.value.bookings.some((booking) => booking.status === 'awaiting_payment');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const promoDiscountLabel = computed(() => {
|
||||||
|
const hit = details.value?.bookings.find((booking) => booking.promoDiscountApplied);
|
||||||
|
if (!hit?.promoDiscountPercent) return '';
|
||||||
|
const title = hit.promoDiscountTitle?.trim() || 'تخفیف';
|
||||||
|
return `${title} — ${hit.promoDiscountPercent.toLocaleString('fa-IR')}٪ اعمال شد.`;
|
||||||
|
});
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const token = String(route.query.token ?? '').trim();
|
const token = String(route.query.token ?? '').trim();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user