update rezerv
This commit is contained in:
+249
-46
@@ -69,6 +69,14 @@
|
||||
:business-id="business.id"
|
||||
/>
|
||||
|
||||
<ServiceClassManager
|
||||
v-if="business"
|
||||
:business-id="business.id"
|
||||
:enabled="business.serviceClassesEnabled === true"
|
||||
@updated="onServiceClassesUpdated"
|
||||
@update:enabled="onServiceClassesEnabledChange"
|
||||
/>
|
||||
|
||||
<section class="space-y-4 border-t border-border pt-6">
|
||||
<h2 class="text-base font-semibold text-text">خدمات و قیمتها</h2>
|
||||
<p class="text-xs text-muted">
|
||||
@@ -106,19 +114,45 @@
|
||||
</div>
|
||||
|
||||
<div v-if="editingId === svc.id" class="mt-3 space-y-2">
|
||||
<PriceInput
|
||||
id="editPrice"
|
||||
v-model="editForm.price"
|
||||
label="قیمت (تومان)"
|
||||
/>
|
||||
<AppInput
|
||||
id="editDuration"
|
||||
v-model="editForm.durationMinutes"
|
||||
label="مدت (دقیقه)"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
ltr
|
||||
/>
|
||||
<template v-if="hasServiceClasses">
|
||||
<p class="text-sm font-medium text-text">قیمت بر اساس کلاس</p>
|
||||
<div
|
||||
v-for="cls in activeClasses"
|
||||
:key="cls.id"
|
||||
class="space-y-2 rounded-xl border border-border bg-surface px-3 py-3"
|
||||
>
|
||||
<p class="text-sm font-medium text-text">{{ cls.title }}</p>
|
||||
<PriceInput
|
||||
:id="`edit-class-price-${svc.id}-${cls.id}`"
|
||||
v-model="editClassPrices[cls.id].price"
|
||||
label="قیمت (تومان)"
|
||||
required
|
||||
/>
|
||||
<AppInput
|
||||
:id="`edit-class-duration-${svc.id}-${cls.id}`"
|
||||
v-model="editClassPrices[cls.id].durationMinutes"
|
||||
label="مدت (دقیقه — اختیاری)"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
ltr
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<PriceInput
|
||||
id="editPrice"
|
||||
v-model="editForm.price"
|
||||
label="قیمت (تومان)"
|
||||
/>
|
||||
<AppInput
|
||||
id="editDuration"
|
||||
v-model="editForm.durationMinutes"
|
||||
label="مدت (دقیقه)"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
ltr
|
||||
/>
|
||||
</template>
|
||||
<div class="flex gap-2">
|
||||
<AppButton class="!w-auto flex-1" :loading="savingService" @click="onSaveEdit(svc)">
|
||||
ذخیره
|
||||
@@ -129,21 +163,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="mt-2 flex flex-wrap items-center gap-2 text-sm text-muted">
|
||||
<span>{{ formatPriceToman(svc.price) }} تومان</span>
|
||||
<span>·</span>
|
||||
<span>{{
|
||||
formatDuration(
|
||||
svc.durationMinutes,
|
||||
svc.serviceTemplate?.defaultDurationMinutes,
|
||||
)
|
||||
}}</span>
|
||||
<div v-else class="mt-2 space-y-2 text-sm text-muted">
|
||||
<template v-if="hasServiceClasses">
|
||||
<div
|
||||
v-for="cls in activeClasses"
|
||||
:key="cls.id"
|
||||
class="flex flex-wrap items-center gap-2"
|
||||
>
|
||||
<span class="font-medium text-text">{{ cls.title }}:</span>
|
||||
<span>
|
||||
{{
|
||||
getClassPriceForService(svc, cls.id)
|
||||
? `${formatPriceToman(getClassPriceForService(svc, cls.id)!.price)} تومان`
|
||||
: '—'
|
||||
}}
|
||||
</span>
|
||||
<span v-if="getClassPriceForService(svc, cls.id)">·</span>
|
||||
<span v-if="getClassPriceForService(svc, cls.id)">
|
||||
{{
|
||||
formatDuration(
|
||||
getClassPriceForService(svc, cls.id)!.durationMinutes,
|
||||
svc.durationMinutes ?? svc.serviceTemplate?.defaultDurationMinutes,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="flex flex-wrap items-center gap-2">
|
||||
<span>{{ formatPriceToman(svc.price) }} تومان</span>
|
||||
<span>·</span>
|
||||
<span>{{
|
||||
formatDuration(
|
||||
svc.durationMinutes,
|
||||
svc.serviceTemplate?.defaultDurationMinutes,
|
||||
)
|
||||
}}</span>
|
||||
</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>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -181,12 +244,14 @@
|
||||
</div>
|
||||
|
||||
<PriceInput
|
||||
v-if="!hasServiceClasses"
|
||||
id="svcPrice"
|
||||
v-model="serviceForm.price"
|
||||
label="قیمت (تومان)"
|
||||
required
|
||||
/>
|
||||
<AppInput
|
||||
v-if="!hasServiceClasses"
|
||||
id="svcDuration"
|
||||
v-model="serviceForm.durationMinutes"
|
||||
label="مدت (دقیقه — اختیاری)"
|
||||
@@ -195,6 +260,31 @@
|
||||
ltr
|
||||
/>
|
||||
|
||||
<div v-if="hasServiceClasses" class="space-y-2">
|
||||
<p class="text-sm font-medium text-text">قیمت بر اساس کلاس</p>
|
||||
<div
|
||||
v-for="cls in activeClasses"
|
||||
:key="cls.id"
|
||||
class="space-y-2 rounded-xl border border-border bg-surface px-3 py-3"
|
||||
>
|
||||
<p class="text-sm font-medium text-text">{{ cls.title }}</p>
|
||||
<PriceInput
|
||||
:id="`add-class-price-${cls.id}`"
|
||||
v-model="classPriceForm[cls.id].price"
|
||||
label="قیمت (تومان)"
|
||||
required
|
||||
/>
|
||||
<AppInput
|
||||
:id="`add-class-duration-${cls.id}`"
|
||||
v-model="classPriceForm[cls.id].durationMinutes"
|
||||
label="مدت (دقیقه — اختیاری)"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
ltr
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AppButton type="submit" :loading="addingService">افزودن</AppButton>
|
||||
</form>
|
||||
|
||||
@@ -272,17 +362,21 @@ import {
|
||||
createBusinessService,
|
||||
deleteBusinessService,
|
||||
fetchBusinessServices,
|
||||
setBusinessServiceClassPrices,
|
||||
updateBusinessService,
|
||||
type ClassPriceItem,
|
||||
} from '@/api/business-services';
|
||||
import { fetchServiceTemplates } from '@/api/service-templates';
|
||||
import type {
|
||||
Business,
|
||||
BusinessService,
|
||||
BusinessServiceClass,
|
||||
BusinessServiceRequest,
|
||||
BusinessServiceRequestStatus,
|
||||
} from '@/api/types';
|
||||
import { ApiError } from '@/api/client';
|
||||
import BusinessAvailabilityEditor from '@/components/owner/BusinessAvailabilityEditor.vue';
|
||||
import ServiceClassManager from '@/components/owner/ServiceClassManager.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppInput from '@/components/ui/AppInput.vue';
|
||||
import PriceInput from '@/components/ui/PriceInput.vue';
|
||||
@@ -294,9 +388,14 @@ import {
|
||||
parseOptionalMinutes,
|
||||
parsePriceInput,
|
||||
} from '@/utils/format';
|
||||
import {
|
||||
activeServiceClasses,
|
||||
getClassPriceForService,
|
||||
} from '@/utils/service-pricing';
|
||||
|
||||
const business = ref<Business | null>(null);
|
||||
const services = ref<BusinessService[]>([]);
|
||||
const serviceClasses = ref<BusinessServiceClass[]>([]);
|
||||
const templates = ref<Awaited<ReturnType<typeof fetchServiceTemplates>>>([]);
|
||||
const loading = ref(true);
|
||||
const loadingServices = ref(false);
|
||||
@@ -327,6 +426,9 @@ const editForm = ref({
|
||||
durationMinutes: '',
|
||||
});
|
||||
|
||||
const classPriceForm = ref<Record<string, { price: string; durationMinutes: string }>>({});
|
||||
const editClassPrices = ref<Record<string, { price: string; durationMinutes: string }>>({});
|
||||
|
||||
const customForm = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
@@ -339,6 +441,67 @@ const availableTemplates = computed(() => {
|
||||
return templates.value.filter((t) => !used.has(t.id));
|
||||
});
|
||||
|
||||
const activeClasses = computed(() => activeServiceClasses(serviceClasses.value));
|
||||
const hasServiceClasses = computed(
|
||||
() => business.value?.serviceClassesEnabled === true && activeClasses.value.length > 0,
|
||||
);
|
||||
|
||||
function emptyClassPriceForm() {
|
||||
const result: Record<string, { price: string; durationMinutes: string }> = {};
|
||||
for (const cls of activeClasses.value) {
|
||||
result[cls.id] = { price: '', durationMinutes: '' };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildClassPricesPayload(
|
||||
form: Record<string, { price: string; durationMinutes: string }>,
|
||||
): ClassPriceItem[] | null {
|
||||
const prices: ClassPriceItem[] = [];
|
||||
for (const cls of activeClasses.value) {
|
||||
const row = form[cls.id];
|
||||
const price = parsePriceInput(row?.price ?? '');
|
||||
if (price === null) {
|
||||
return null;
|
||||
}
|
||||
const duration = parseOptionalMinutes(row?.durationMinutes ?? '');
|
||||
if (duration === null) {
|
||||
return null;
|
||||
}
|
||||
prices.push({
|
||||
classId: cls.id,
|
||||
price,
|
||||
durationMinutes: duration && duration > 0 ? duration : null,
|
||||
});
|
||||
}
|
||||
return prices;
|
||||
}
|
||||
|
||||
function onServiceClassesUpdated(classes: BusinessServiceClass[]) {
|
||||
serviceClasses.value = classes;
|
||||
classPriceForm.value = emptyClassPriceForm();
|
||||
}
|
||||
|
||||
async function onServiceClassesEnabledChange(value: boolean) {
|
||||
if (!business.value) return;
|
||||
error.value = '';
|
||||
message.value = '';
|
||||
try {
|
||||
business.value = await updateBusiness(business.value.id, {
|
||||
serviceClassesEnabled: value,
|
||||
});
|
||||
if (!value) {
|
||||
serviceClasses.value = [];
|
||||
classPriceForm.value = {};
|
||||
}
|
||||
message.value = value
|
||||
? 'کلاسبندی فعال شد. کلاسهای خود را تعریف کنید.'
|
||||
: 'کلاسبندی غیرفعال شد. قیمتگذاری به حالت عادی برمیگردد.';
|
||||
} catch (e) {
|
||||
error.value = e instanceof ApiError ? e.message : 'تغییر وضعیت کلاسبندی ناموفق بود';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadServicesAndTemplates() {
|
||||
if (!business.value) return;
|
||||
loadingServices.value = true;
|
||||
@@ -405,27 +568,47 @@ async function onSave() {
|
||||
|
||||
async function onAddService() {
|
||||
if (!business.value || !serviceForm.value.serviceTemplateId) return;
|
||||
const price = parsePriceInput(serviceForm.value.price);
|
||||
if (price === null) {
|
||||
error.value = 'قیمت معتبر نیست.';
|
||||
return;
|
||||
}
|
||||
const duration = parseOptionalMinutes(serviceForm.value.durationMinutes);
|
||||
if (duration === null) {
|
||||
error.value = 'مدت زمان معتبر نیست.';
|
||||
return;
|
||||
|
||||
let price = 0;
|
||||
let duration: number | null | undefined;
|
||||
let classPrices: ClassPriceItem[] | null = null;
|
||||
|
||||
if (hasServiceClasses.value) {
|
||||
classPrices = buildClassPricesPayload(classPriceForm.value);
|
||||
if (!classPrices?.length) {
|
||||
error.value = 'قیمت همه کلاسها را وارد کنید.';
|
||||
return;
|
||||
}
|
||||
price = Math.min(...classPrices.map((item) => item.price));
|
||||
} else {
|
||||
const parsedPrice = parsePriceInput(serviceForm.value.price);
|
||||
if (parsedPrice === null) {
|
||||
error.value = 'قیمت معتبر نیست.';
|
||||
return;
|
||||
}
|
||||
price = parsedPrice;
|
||||
duration = parseOptionalMinutes(serviceForm.value.durationMinutes);
|
||||
if (duration === null) {
|
||||
error.value = 'مدت زمان معتبر نیست.';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
addingService.value = true;
|
||||
error.value = '';
|
||||
message.value = '';
|
||||
try {
|
||||
await createBusinessService({
|
||||
const created = await createBusinessService({
|
||||
businessId: business.value.id,
|
||||
serviceTemplateId: serviceForm.value.serviceTemplateId,
|
||||
price,
|
||||
durationMinutes: duration && duration > 0 ? duration : undefined,
|
||||
});
|
||||
if (classPrices?.length) {
|
||||
await setBusinessServiceClassPrices(created.id, classPrices);
|
||||
}
|
||||
serviceForm.value = { serviceTemplateId: '', price: '', durationMinutes: '' };
|
||||
classPriceForm.value = emptyClassPriceForm();
|
||||
await loadServicesAndTemplates();
|
||||
if (availableTemplates.value[0]) {
|
||||
serviceForm.value.serviceTemplateId = availableTemplates.value[0].id;
|
||||
@@ -491,6 +674,17 @@ function startEdit(svc: BusinessService) {
|
||||
price: formatPriceInputDisplay(Math.round(Number.parseFloat(svc.price))),
|
||||
durationMinutes: svc.durationMinutes != null ? String(svc.durationMinutes) : '',
|
||||
};
|
||||
const form = emptyClassPriceForm();
|
||||
for (const classPrice of svc.classPrices ?? []) {
|
||||
if (form[classPrice.classId]) {
|
||||
form[classPrice.classId] = {
|
||||
price: formatPriceInputDisplay(Math.round(Number.parseFloat(classPrice.price))),
|
||||
durationMinutes:
|
||||
classPrice.durationMinutes != null ? String(classPrice.durationMinutes) : '',
|
||||
};
|
||||
}
|
||||
}
|
||||
editClassPrices.value = form;
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
@@ -498,23 +692,32 @@ function cancelEdit() {
|
||||
}
|
||||
|
||||
async function onSaveEdit(svc: BusinessService) {
|
||||
const price = parsePriceInput(editForm.value.price);
|
||||
if (price === null) {
|
||||
error.value = 'قیمت معتبر نیست.';
|
||||
return;
|
||||
}
|
||||
const duration = parseOptionalMinutes(editForm.value.durationMinutes);
|
||||
if (duration === null) {
|
||||
error.value = 'مدت زمان معتبر نیست.';
|
||||
return;
|
||||
}
|
||||
savingService.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
await updateBusinessService(svc.id, {
|
||||
price,
|
||||
durationMinutes: duration ?? null,
|
||||
});
|
||||
if (hasServiceClasses.value) {
|
||||
const classPrices = buildClassPricesPayload(editClassPrices.value);
|
||||
if (!classPrices?.length) {
|
||||
error.value = 'قیمت همه کلاسها را وارد کنید.';
|
||||
return;
|
||||
}
|
||||
await setBusinessServiceClassPrices(svc.id, classPrices);
|
||||
} else {
|
||||
const price = parsePriceInput(editForm.value.price);
|
||||
if (price === null) {
|
||||
error.value = 'قیمت معتبر نیست.';
|
||||
return;
|
||||
}
|
||||
const duration = parseOptionalMinutes(editForm.value.durationMinutes);
|
||||
if (duration === null) {
|
||||
error.value = 'مدت زمان معتبر نیست.';
|
||||
return;
|
||||
}
|
||||
await updateBusinessService(svc.id, {
|
||||
price,
|
||||
durationMinutes: duration ?? null,
|
||||
});
|
||||
}
|
||||
editingId.value = null;
|
||||
await loadServicesAndTemplates();
|
||||
message.value = 'خدمت بهروزرسانی شد.';
|
||||
|
||||
Reference in New Issue
Block a user