edit business

This commit is contained in:
2026-06-01 21:01:45 +03:30
parent 02da1834e0
commit 031f0a44ab
9 changed files with 387 additions and 145 deletions
+2
View File
@@ -24,6 +24,7 @@ export type Business = {
isActive: boolean; isActive: boolean;
serviceClassesEnabled?: boolean; serviceClassesEnabled?: boolean;
concurrentCapacity?: number; concurrentCapacity?: number;
slotStepMinutes?: number;
categoryId: string; categoryId: string;
ownerId?: string | null; ownerId?: string | null;
category?: BusinessCategory; category?: BusinessCategory;
@@ -67,6 +68,7 @@ export type UpdateBusinessPayload = {
longitude?: number | null; longitude?: number | null;
serviceClassesEnabled?: boolean; serviceClassesEnabled?: boolean;
concurrentCapacity?: number; concurrentCapacity?: number;
slotStepMinutes?: number;
}; };
export type BusinessServiceClass = { export type BusinessServiceClass = {
+2 -2
View File
@@ -7,12 +7,12 @@
/> />
<template v-else> <template v-else>
<img <img
src="/brand/logo-dark.png" src="/brand/logo-lite.png"
alt="منتایم" alt="منتایم"
:class="['dark:hidden', imgClass]" :class="['dark:hidden', imgClass]"
/> />
<img <img
src="/brand/logo-lite.png" src="/brand/logo-dark.png"
alt="منتایم" alt="منتایم"
:class="['hidden dark:block', imgClass]" :class="['hidden dark:block', imgClass]"
/> />
+48 -20
View File
@@ -1,28 +1,47 @@
<template> <template>
<section class="space-y-3 rounded-2xl border border-border bg-surface-2 p-4"> <section class="border-t border-border pt-4">
<div class="flex items-start justify-between gap-3"> <button
<div> type="button"
class="flex w-full items-center justify-between gap-3 rounded-2xl border border-border bg-surface-2 px-4 py-3 text-start transition hover:border-brand-500/30"
:aria-expanded="expanded"
@click="expanded = !expanded"
>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<AppIcon name="folder" size="sm" class="text-brand-600 dark:text-brand-500" />
<h2 class="text-base font-semibold text-text">کلاس‌بندی خدمات</h2> <h2 class="text-base font-semibold text-text">کلاس‌بندی خدمات</h2>
<p class="mt-1 text-xs text-muted"> </div>
اختیاری — برای مثال کارواش می‌تواند بر اساس اندازه خودرو قیمت‌گذاری کند. <p v-if="!expanded" class="mt-1 truncate text-xs text-muted">
{{ collapsedSummary }}
</p> </p>
</div> </div>
<label class="flex shrink-0 items-center gap-2 text-sm text-text"> <div class="flex shrink-0 items-center gap-2">
<input <label class="flex items-center gap-2 text-sm text-text" @click.stop>
type="checkbox" <input type="checkbox" :checked="enabled" @change="onToggleEnabled" />
:checked="enabled"
@change="onToggleEnabled"
/>
فعال فعال
</label> </label>
<AppIcon
:name="expanded ? 'chevron-up' : 'chevron-down'"
size="sm"
class="text-muted"
/>
</div> </div>
</button>
<div v-show="expanded" class="mt-3 space-y-3">
<p class="text-xs leading-relaxed text-muted">
اختیاری — برای مثال کارواش می‌تواند بر اساس اندازه خودرو قیمت‌گذاری کند.
</p>
<p v-if="!enabled" class="text-sm text-muted"> <p v-if="!enabled" class="text-sm text-muted">
در حالت غیرفعال، هر خدمت یک قیمت ثابت دارد. با فعال‌کردن، می‌توانید کلاس‌ها را تعریف کنید. در حالت غیرفعال، هر خدمت یک قیمت ثابت دارد. با فعال‌کردن، می‌توانید کلاس‌ها را تعریف کنید.
</p> </p>
<template v-else> <template v-else>
<p v-if="error" class="rounded-xl border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-600"> <p
v-if="error"
class="rounded-xl border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-600"
>
{{ error }} {{ error }}
</p> </p>
@@ -67,13 +86,9 @@
غیرفعال غیرفعال
</span> </span>
</div> </div>
<div class="flex shrink-0 gap-1"> <div class="flex shrink-0 items-center gap-1">
<AppButton variant="ghost" class="!w-auto px-2 text-xs" @click="startEdit(item)"> <AppIconButton icon="pencil" label="ویرایش کلاس" @click="startEdit(item)" />
ویرایش <AppIconButton icon="trash" label="حذف کلاس" variant="danger" @click="onDelete(item)" />
</AppButton>
<AppButton variant="danger" class="!w-auto px-2 text-xs" @click="onDelete(item)">
حذف
</AppButton>
</div> </div>
</div> </div>
</li> </li>
@@ -96,11 +111,12 @@
<AppButton type="submit" :loading="adding">افزودن کلاس</AppButton> <AppButton type="submit" :loading="adding">افزودن کلاس</AppButton>
</form> </form>
</template> </template>
</div>
</section> </section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { import {
createBusinessServiceClass, createBusinessServiceClass,
deleteBusinessServiceClass, deleteBusinessServiceClass,
@@ -110,6 +126,8 @@ import {
import { ApiError } from '@/api/client'; import { ApiError } from '@/api/client';
import type { BusinessServiceClass } from '@/api/types'; import type { BusinessServiceClass } from '@/api/types';
import AppButton from '@/components/ui/AppButton.vue'; import AppButton from '@/components/ui/AppButton.vue';
import AppIcon from '@/components/ui/AppIcon.vue';
import AppIconButton from '@/components/ui/AppIconButton.vue';
import AppInput from '@/components/ui/AppInput.vue'; import AppInput from '@/components/ui/AppInput.vue';
const props = defineProps<{ const props = defineProps<{
@@ -122,6 +140,7 @@ const emit = defineEmits<{
'update:enabled': [value: boolean]; 'update:enabled': [value: boolean];
}>(); }>();
const expanded = ref(false);
const classes = ref<BusinessServiceClass[]>([]); const classes = ref<BusinessServiceClass[]>([]);
const loading = ref(false); const loading = ref(false);
const adding = ref(false); const adding = ref(false);
@@ -132,6 +151,15 @@ const editingId = ref<string | null>(null);
const form = ref({ title: '', description: '' }); const form = ref({ title: '', description: '' });
const editForm = ref({ title: '', description: '', isActive: true }); const editForm = ref({ title: '', description: '', isActive: true });
const collapsedSummary = computed(() => {
if (!props.enabled) return 'غیرفعال — هر خدمت یک قیمت ثابت';
if (loading.value) return 'فعال — در حال بارگذاری…';
const count = classes.value.length;
if (count === 0) return 'فعال — هنوز کلاسی تعریف نشده';
const activeCount = classes.value.filter((c) => c.isActive).length;
return `فعال · ${count} کلاس (${activeCount} فعال)`;
});
async function load() { async function load() {
if (!props.enabled) { if (!props.enabled) {
classes.value = []; classes.value = [];
+10
View File
@@ -19,16 +19,21 @@ import {
CircleCheck, CircleCheck,
Clock, Clock,
ClipboardList, ClipboardList,
Eye,
EyeOff,
FolderOpen, FolderOpen,
Home, Home,
LayoutDashboard, LayoutDashboard,
MapPin, MapPin,
Moon, Moon,
Plus, Plus,
Pencil,
Search, Search,
Store, Store,
Sun, Sun,
Trash2,
User, User,
UserCog,
Users, Users,
X, X,
} from '@lucide/vue'; } from '@lucide/vue';
@@ -55,6 +60,11 @@ const iconMap: Record<AppIconName, Component> = {
'chevron-down': ChevronDown, 'chevron-down': ChevronDown,
'chevron-up': ChevronUp, 'chevron-up': ChevronUp,
clock: Clock, clock: Clock,
pencil: Pencil,
trash: Trash2,
'user-cog': UserCog,
eye: Eye,
'eye-off': EyeOff,
}; };
const props = withDefaults( const props = withDefaults(
+39
View File
@@ -0,0 +1,39 @@
<template>
<button
type="button"
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/50 disabled:pointer-events-none disabled:opacity-50"
:class="variantClasses"
:aria-label="label"
:disabled="disabled"
@click="emit('click', $event)"
>
<AppIcon :name="icon" size="sm" />
</button>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import AppIcon from '@/components/ui/AppIcon.vue';
import type { AppIconName } from '@/types/icons';
const props = withDefaults(
defineProps<{
icon: AppIconName;
label: string;
variant?: 'default' | 'danger';
disabled?: boolean;
}>(),
{
variant: 'default',
disabled: false,
},
);
const emit = defineEmits<{ click: [event: MouseEvent] }>();
const variantClasses = computed(() =>
props.variant === 'danger'
? 'text-muted hover:bg-red-500/10 hover:text-red-600'
: 'text-muted hover:bg-surface-3 hover:text-text',
);
</script>
+43 -4
View File
@@ -3,22 +3,40 @@
<label v-if="label" :for="id" class="block text-sm font-medium text-muted"> <label v-if="label" :for="id" class="block text-sm font-medium text-muted">
{{ label }} {{ label }}
</label> </label>
<div class="relative">
<input <input
:id="id" :id="id"
v-model="model" v-model="model"
class="w-full rounded-xl border border-border bg-surface-2 px-4 py-3 text-text outline-none transition placeholder:text-[var(--color-placeholder)] focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30" class="w-full rounded-xl border border-border bg-surface-2 py-3 text-text outline-none transition placeholder:text-[var(--color-placeholder)] focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30"
:class="ltr ? 'text-left' : ''" :class="inputPaddingClass"
v-bind="$attrs" :type="resolvedType"
v-bind="inputAttrs"
/> />
<button
v-if="isPassword"
type="button"
class="absolute top-1/2 flex h-9 w-9 -translate-y-1/2 items-center justify-center rounded-lg text-muted transition hover:text-text"
:class="toggleButtonClass"
:aria-label="showPassword ? 'مخفی کردن رمز' : 'نمایش رمز'"
@click="showPassword = !showPassword"
>
<AppIcon :name="showPassword ? 'eye-off' : 'eye'" size="sm" />
</button>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, useAttrs } from 'vue';
import AppIcon from '@/components/ui/AppIcon.vue';
defineOptions({ inheritAttrs: false }); defineOptions({ inheritAttrs: false });
const model = defineModel<string>({ default: '' }); const model = defineModel<string>({ default: '' });
const attrs = useAttrs();
const showPassword = ref(false);
withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
label?: string; label?: string;
id?: string; id?: string;
@@ -26,4 +44,25 @@ withDefaults(
}>(), }>(),
{ ltr: false }, { ltr: false },
); );
const isPassword = computed(() => attrs.type === 'password');
const resolvedType = computed(() => {
if (!isPassword.value) return attrs.type as string | undefined;
return showPassword.value ? 'text' : 'password';
});
const inputAttrs = computed(() => {
const { type: _type, ...rest } = attrs;
return rest;
});
const inputPaddingClass = computed(() => {
if (isPassword.value) {
return props.ltr ? 'pl-4 pr-11 text-left' : 'pl-11 pr-4';
}
return props.ltr ? 'px-4 text-left' : 'px-4';
});
const toggleButtonClass = computed(() => (props.ltr ? 'right-1' : 'left-1'));
</script> </script>
+1 -1
View File
@@ -4,7 +4,7 @@
class="sticky top-0 z-10 flex items-center justify-between border-b border-border bg-surface/95 px-4 py-3 backdrop-blur" class="sticky top-0 z-10 flex items-center justify-between border-b border-border bg-surface/95 px-4 py-3 backdrop-blur"
> >
<div class="flex min-w-0 items-center gap-3"> <div class="flex min-w-0 items-center gap-3">
<AppLogo variant="horizontal" class="h-8 shrink-0" /> <AppLogo variant="horizontal" class="h-10 shrink-0" />
<p v-if="auth.user" class="text-xs text-muted" dir="ltr">{{ auth.user.mobile }}</p> <p v-if="auth.user" class="text-xs text-muted" dir="ltr">{{ auth.user.mobile }}</p>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
+6 -1
View File
@@ -18,7 +18,12 @@ export type AppIconName =
| 'search' | 'search'
| 'chevron-down' | 'chevron-down'
| 'chevron-up' | 'chevron-up'
| 'clock'; | 'clock'
| 'pencil'
| 'trash'
| 'user-cog'
| 'eye'
| 'eye-off';
export type NavItem = { export type NavItem = {
to: string; to: string;
+143 -24
View File
@@ -28,16 +28,23 @@
<form class="space-y-4" @submit.prevent="onSave"> <form class="space-y-4" @submit.prevent="onSave">
<h2 class="text-base font-semibold text-text">اطلاعات کسب‌وکار</h2> <h2 class="text-base font-semibold text-text">اطلاعات کسب‌وکار</h2>
<div class="rounded-2xl border border-border bg-surface-2 p-4"> <dl class="space-y-3 rounded-2xl border border-border bg-surface-2 p-4">
<p class="text-xs text-muted">دسته</p> <div class="flex items-center justify-between gap-4">
<p class="mt-0.5 font-medium text-text">{{ business.category?.title ?? '—' }}</p> <dt class="shrink-0 text-xs text-muted">دسته</dt>
<dd class="font-medium text-text">{{ business.category?.title ?? '—' }}</dd>
</div>
<div class="flex items-center justify-between gap-4">
<dt class="shrink-0 text-xs text-muted">وضعیت</dt>
<dd>
<span <span
class="mt-2 inline-block rounded-lg px-2 py-0.5 text-xs" class="inline-block rounded-lg px-2 py-0.5 text-xs"
:class="business.isActive ? 'bg-green-500/15 text-green-700' : 'bg-amber-500/15 text-amber-700'" :class="business.isActive ? 'bg-green-500/15 text-green-700' : 'bg-amber-500/15 text-amber-700'"
> >
{{ business.isActive ? 'فعال' : 'غیرفعال' }} {{ business.isActive ? 'فعال' : 'غیرفعال' }}
</span> </span>
</dd>
</div> </div>
</dl>
<AppInput id="ownerTitle" v-model="form.title" label="نام کسب‌وکار" required /> <AppInput id="ownerTitle" v-model="form.title" label="نام کسب‌وکار" required />
@@ -61,9 +68,36 @@
/> />
<AppInput id="ownerAddress" v-model="form.address" label="آدرس" /> <AppInput id="ownerAddress" v-model="form.address" label="آدرس" />
<AppButton type="submit" :loading="saving">ذخیره تغییرات</AppButton>
</form>
<section class="border-t border-border pt-4">
<button
type="button"
class="flex w-full items-center justify-between gap-3 rounded-2xl border border-border bg-surface-2 px-4 py-3 text-start transition hover:border-brand-500/30"
:aria-expanded="bookingExpanded"
@click="bookingExpanded = !bookingExpanded"
>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<AppIcon name="calendar" size="sm" class="text-brand-600 dark:text-brand-500" />
<h2 class="text-base font-semibold text-text">تنظیمات نوبت‌دهی</h2>
</div>
<p v-if="!bookingExpanded" class="mt-1 truncate text-xs text-muted">
{{ bookingSummary }}
</p>
</div>
<AppIcon
:name="bookingExpanded ? 'chevron-up' : 'chevron-down'"
size="sm"
class="shrink-0 text-muted"
/>
</button>
<div v-show="bookingExpanded" class="mt-3 space-y-4">
<AppInput <AppInput
id="ownerConcurrentCapacity" id="ownerConcurrentCapacity"
v-model="form.concurrentCapacity" v-model="bookingForm.concurrentCapacity"
label="پذیرش همزمان" label="پذیرش همزمان"
type="number" type="number"
inputmode="numeric" inputmode="numeric"
@@ -74,8 +108,24 @@
حداکثر تعداد مشتری که همزمان می‌توانید پذیرش کنید. مثلاً ۳ یعنی هر ساعت تا ۳ رزرو هم‌پوشان. حداکثر تعداد مشتری که همزمان می‌توانید پذیرش کنید. مثلاً ۳ یعنی هر ساعت تا ۳ رزرو هم‌پوشان.
</p> </p>
<AppButton type="submit" :loading="saving">ذخیره تغییرات</AppButton> <AppInput
</form> id="ownerSlotStep"
v-model="bookingForm.slotStepMinutes"
label="گام نوبت‌دهی (دقیقه)"
type="number"
inputmode="numeric"
ltr
required
/>
<p class="-mt-2 text-xs text-muted">
فاصله بین ساعت‌های قابل رزرو. مثلاً ۳۰ یعنی ۱۰:۰۰، ۱۰:۳۰، ۱۱:۰۰ و …
</p>
<AppButton :loading="savingBooking" @click="onSaveBookingSettings">
ذخیره تنظیمات نوبت‌دهی
</AppButton>
</div>
</section>
<BusinessAvailabilityEditor <BusinessAvailabilityEditor
v-if="business" v-if="business"
@@ -90,8 +140,30 @@
@update:enabled="onServiceClassesEnabledChange" @update:enabled="onServiceClassesEnabledChange"
/> />
<section class="space-y-4 border-t border-border pt-6"> <section class="border-t border-border pt-4">
<button
type="button"
class="flex w-full items-center justify-between gap-3 rounded-2xl border border-border bg-surface-2 px-4 py-3 text-start transition hover:border-brand-500/30"
:aria-expanded="servicesExpanded"
@click="servicesExpanded = !servicesExpanded"
>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<AppIcon name="clipboard-list" size="sm" class="text-brand-600 dark:text-brand-500" />
<h2 class="text-base font-semibold text-text">خدمات و قیمت‌ها</h2> <h2 class="text-base font-semibold text-text">خدمات و قیمت‌ها</h2>
</div>
<p v-if="!servicesExpanded" class="mt-1 truncate text-xs text-muted">
{{ servicesSummary }}
</p>
</div>
<AppIcon
:name="servicesExpanded ? 'chevron-up' : 'chevron-down'"
size="sm"
class="shrink-0 text-muted"
/>
</button>
<div v-show="servicesExpanded" class="mt-3 space-y-4">
<p class="text-xs text-muted"> <p class="text-xs text-muted">
خدماتی که اینجا ثبت می‌کنید در صفحه کسب‌وکار برای مشتریان و مقایسه نمایش داده می‌شود. خدماتی که اینجا ثبت می‌کنید در صفحه کسب‌وکار برای مشتریان و مقایسه نمایش داده می‌شود.
</p> </p>
@@ -116,7 +188,8 @@
{{ svc.serviceTemplate.description }} {{ svc.serviceTemplate.description }}
</p> </p>
</div> </div>
<label class="flex shrink-0 items-center gap-1 text-xs text-muted"> <div class="flex shrink-0 items-center gap-1">
<label class="flex items-center gap-1 text-xs text-muted">
<input <input
type="checkbox" type="checkbox"
:checked="svc.isActive" :checked="svc.isActive"
@@ -124,6 +197,11 @@
/> />
فعال فعال
</label> </label>
<template v-if="editingId !== svc.id">
<AppIconButton icon="pencil" label="ویرایش خدمت" @click="startEdit(svc)" />
<AppIconButton icon="trash" label="حذف خدمت" variant="danger" @click="onDelete(svc)" />
</template>
</div>
</div> </div>
<div v-if="editingId === svc.id" class="mt-3 space-y-2"> <div v-if="editingId === svc.id" class="mt-3 space-y-2">
@@ -212,14 +290,6 @@
) )
}}</span> }}</span>
</div> </div>
<div class="flex flex-wrap items-center gap-2">
<AppButton variant="ghost" class="!w-auto px-2 text-xs" @click="startEdit(svc)">
ویرایش
</AppButton>
<AppButton variant="danger" class="!w-auto px-2 text-xs" @click="onDelete(svc)">
حذف
</AppButton>
</div>
</div> </div>
</li> </li>
</ul> </ul>
@@ -359,6 +429,7 @@
<AppButton type="submit" :loading="addingCustom">ارسال برای تأیید</AppButton> <AppButton type="submit" :loading="addingCustom">ارسال برای تأیید</AppButton>
</form> </form>
</section> </section>
</div>
</section> </section>
</div> </div>
</WebAppLayout> </WebAppLayout>
@@ -391,6 +462,8 @@ import { ApiError } from '@/api/client';
import BusinessAvailabilityEditor from '@/components/owner/BusinessAvailabilityEditor.vue'; import BusinessAvailabilityEditor from '@/components/owner/BusinessAvailabilityEditor.vue';
import ServiceClassManager from '@/components/owner/ServiceClassManager.vue'; import ServiceClassManager from '@/components/owner/ServiceClassManager.vue';
import AppButton from '@/components/ui/AppButton.vue'; import AppButton from '@/components/ui/AppButton.vue';
import AppIcon from '@/components/ui/AppIcon.vue';
import AppIconButton from '@/components/ui/AppIconButton.vue';
import AppInput from '@/components/ui/AppInput.vue'; import AppInput from '@/components/ui/AppInput.vue';
import PriceInput from '@/components/ui/PriceInput.vue'; import PriceInput from '@/components/ui/PriceInput.vue';
import WebAppLayout from '@/layouts/WebAppLayout.vue'; import WebAppLayout from '@/layouts/WebAppLayout.vue';
@@ -413,6 +486,7 @@ const templates = ref<Awaited<ReturnType<typeof fetchServiceTemplates>>>([]);
const loading = ref(true); const loading = ref(true);
const loadingServices = ref(false); const loadingServices = ref(false);
const saving = ref(false); const saving = ref(false);
const savingBooking = ref(false);
const savingService = ref(false); const savingService = ref(false);
const addingService = ref(false); const addingService = ref(false);
const addingCustom = ref(false); const addingCustom = ref(false);
@@ -420,15 +494,22 @@ const serviceRequests = ref<BusinessServiceRequest[]>([]);
const error = ref(''); const error = ref('');
const message = ref(''); const message = ref('');
const editingId = ref<string | null>(null); const editingId = ref<string | null>(null);
const servicesExpanded = ref(false);
const form = ref({ const form = ref({
title: '', title: '',
description: '', description: '',
contactPhone: '', contactPhone: '',
address: '', address: '',
concurrentCapacity: '1',
}); });
const bookingForm = ref({
concurrentCapacity: '1',
slotStepMinutes: '30',
});
const bookingExpanded = ref(false);
const serviceForm = ref({ const serviceForm = ref({
serviceTemplateId: '', serviceTemplateId: '',
price: '', price: '',
@@ -460,6 +541,19 @@ const hasServiceClasses = computed(
() => business.value?.serviceClassesEnabled === true && activeClasses.value.length > 0, () => business.value?.serviceClassesEnabled === true && activeClasses.value.length > 0,
); );
const servicesSummary = computed(() => {
if (loadingServices.value) return 'در حال بارگذاری…';
const count = services.value.length;
if (count === 0) return 'هنوز خدمتی ثبت نشده';
const activeCount = services.value.filter((s) => s.isActive).length;
return `${count} خدمت · ${activeCount} فعال`;
});
const bookingSummary = computed(
() =>
`${bookingForm.value.concurrentCapacity} همزمان · گام ${bookingForm.value.slotStepMinutes} دقیقه`,
);
function emptyClassPriceForm() { function emptyClassPriceForm() {
const result: Record<string, { price: string; durationMinutes: string }> = {}; const result: Record<string, { price: string; durationMinutes: string }> = {};
for (const cls of activeClasses.value) { for (const cls of activeClasses.value) {
@@ -550,7 +644,10 @@ onMounted(async () => {
description: business.value.description ?? '', description: business.value.description ?? '',
contactPhone: business.value.contactPhone ?? '', contactPhone: business.value.contactPhone ?? '',
address: business.value.address ?? '', address: business.value.address ?? '',
};
bookingForm.value = {
concurrentCapacity: String(business.value.concurrentCapacity ?? 1), concurrentCapacity: String(business.value.concurrentCapacity ?? 1),
slotStepMinutes: String(business.value.slotStepMinutes ?? 30),
}; };
await loadServicesAndTemplates(); await loadServicesAndTemplates();
} }
@@ -563,11 +660,6 @@ onMounted(async () => {
async function onSave() { async function onSave() {
if (!business.value) return; if (!business.value) return;
const concurrentCapacity = Number.parseInt(form.value.concurrentCapacity, 10);
if (!Number.isFinite(concurrentCapacity) || concurrentCapacity < 1 || concurrentCapacity > 20) {
error.value = 'پذیرش همزمان باید عددی بین ۱ تا ۲۰ باشد.';
return;
}
saving.value = true; saving.value = true;
error.value = ''; error.value = '';
message.value = ''; message.value = '';
@@ -577,7 +669,6 @@ async function onSave() {
description: form.value.description.trim() || null, description: form.value.description.trim() || null,
contactPhone: form.value.contactPhone.trim() || null, contactPhone: form.value.contactPhone.trim() || null,
address: form.value.address.trim() || null, address: form.value.address.trim() || null,
concurrentCapacity,
}); });
message.value = 'اطلاعات ذخیره شد.'; message.value = 'اطلاعات ذخیره شد.';
} catch (e) { } catch (e) {
@@ -587,6 +678,34 @@ async function onSave() {
} }
} }
async function onSaveBookingSettings() {
if (!business.value) return;
const concurrentCapacity = Number.parseInt(bookingForm.value.concurrentCapacity, 10);
if (!Number.isFinite(concurrentCapacity) || concurrentCapacity < 1 || concurrentCapacity > 20) {
error.value = 'پذیرش همزمان باید عددی بین ۱ تا ۲۰ باشد.';
return;
}
const slotStepMinutes = Number.parseInt(bookingForm.value.slotStepMinutes, 10);
if (!Number.isFinite(slotStepMinutes) || slotStepMinutes < 5 || slotStepMinutes > 120) {
error.value = 'گام نوبت‌دهی باید عددی بین ۵ تا ۱۲۰ دقیقه باشد.';
return;
}
savingBooking.value = true;
error.value = '';
message.value = '';
try {
business.value = await updateBusiness(business.value.id, {
concurrentCapacity,
slotStepMinutes,
});
message.value = 'تنظیمات نوبت‌دهی ذخیره شد.';
} catch (e) {
error.value = e instanceof ApiError ? e.message : 'خطا در ذخیره';
} finally {
savingBooking.value = false;
}
}
async function onAddService() { async function onAddService() {
if (!business.value || !serviceForm.value.serviceTemplateId) return; if (!business.value || !serviceForm.value.serviceTemplateId) return;