update rezerv

This commit is contained in:
2026-05-31 21:38:09 +03:30
parent f36a4af379
commit 5eb5bd233c
11 changed files with 1056 additions and 128 deletions
+249 -46
View File
@@ -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 = 'خدمت به‌روزرسانی شد.';