Add business services management in admin edit form

This commit is contained in:
2026-05-25 17:50:33 +03:30
parent 750c306b32
commit 43f0524ae2
3 changed files with 229 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
import { api } from './client';
export type BusinessService = {
id: string;
businessId: string;
serviceTemplateId: string;
price: string;
durationMinutes: number | null;
isActive: boolean;
serviceTemplate?: {
id: string;
title: string;
description: string | null;
defaultDurationMinutes: number;
};
};
export function fetchBusinessServices(businessId: string) {
const search = new URLSearchParams({ businessId });
return api<BusinessService[]>(`/business-services?${search}`);
}
export function createBusinessService(payload: {
businessId: string;
serviceTemplateId: string;
price: number;
durationMinutes?: number | null;
isActive?: boolean;
}) {
return api<BusinessService>('/business-services', {
method: 'POST',
body: JSON.stringify(payload),
});
}
export function updateBusinessService(
id: string,
payload: { price?: number; durationMinutes?: number | null; isActive?: boolean },
) {
return api<BusinessService>(`/business-services/${id}`, {
method: 'PATCH',
body: JSON.stringify(payload),
});
}
export function deleteBusinessService(id: string) {
return api<{ success: true }>(`/business-services/${id}`, { method: 'DELETE' });
}
@@ -0,0 +1,174 @@
<template>
<section class="space-y-3 border-t border-border pt-4">
<div>
<h3 class="text-sm font-semibold text-text">خدمات (برای نمایش در اپ)</h3>
<p class="mt-0.5 text-xs text-muted">
بدون ثبت خدمت، مشتری در اپ چیزی برای رزرو نمی‌بیند.
</p>
</div>
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
<div v-if="loading" class="text-sm text-muted">در حال بارگذاری خدمات…</div>
<ul v-else-if="services.length" class="space-y-2">
<li
v-for="svc in services"
:key="svc.id"
class="flex items-center justify-between gap-2 rounded-xl border border-border bg-surface-3 px-3 py-2 text-sm"
>
<div class="min-w-0">
<p class="font-medium text-text">{{ svc.serviceTemplate?.title ?? 'خدمت' }}</p>
<p class="text-xs text-muted">
{{ formatPrice(svc.price) }} تومان
<span v-if="svc.durationMinutes"> · {{ svc.durationMinutes }} دقیقه</span>
</p>
</div>
<button
type="button"
class="shrink-0 text-xs text-red-600"
@click="onDelete(svc)"
>
حذف
</button>
</li>
</ul>
<form
v-if="availableTemplates.length"
class="grid gap-2 sm:grid-cols-2"
@submit.prevent="onAdd"
>
<AppSelect
id="svcTpl"
v-model="form.serviceTemplateId"
label="نوع خدمت"
:options="templateOptions"
/>
<AppInput
id="svcPrice"
v-model="form.price"
label="قیمت (تومان)"
type="number"
inputmode="numeric"
ltr
required
/>
<AppButton type="submit" class="sm:col-span-2" :loading="adding">
افزودن خدمت
</AppButton>
</form>
<p v-else-if="!loading && templates.length" class="text-xs text-muted">
همه قالب‌های این دسته اضافه شده‌اند.
</p>
<p v-else-if="!loading" class="text-xs text-muted">
ابتدا برای این دسته «قالب خدمت» بسازید.
</p>
</section>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import {
createBusinessService,
deleteBusinessService,
fetchBusinessServices,
type BusinessService,
} from '@/api/business-services';
import { fetchServiceTemplates } from '@/api/service-templates';
import type { ServiceTemplate } from '@/api/types';
import { ApiError } from '@/api/client';
import AppButton from '@/components/ui/AppButton.vue';
import AppInput from '@/components/ui/AppInput.vue';
import AppSelect from '@/components/ui/AppSelect.vue';
const props = defineProps<{
businessId: string;
categoryId: string;
}>();
const services = ref<BusinessService[]>([]);
const templates = ref<ServiceTemplate[]>([]);
const loading = ref(true);
const adding = ref(false);
const error = ref('');
const form = ref({ serviceTemplateId: '', price: '' });
const availableTemplates = computed(() => {
const used = new Set(services.value.map((s) => s.serviceTemplateId));
return templates.value.filter((t) => t.isActive && !used.has(t.id));
});
const templateOptions = computed(() =>
availableTemplates.value.map((t) => ({ value: t.id, label: t.title })),
);
function formatPrice(price: string): string {
const n = Number.parseFloat(price);
if (!Number.isFinite(n)) return price;
return new Intl.NumberFormat('fa-IR').format(Math.round(n));
}
async function load() {
loading.value = true;
error.value = '';
try {
const [svc, tpl] = await Promise.all([
fetchBusinessServices(props.businessId),
fetchServiceTemplates(props.categoryId),
]);
services.value = svc;
templates.value = tpl.filter((t) => t.isActive);
if (availableTemplates.value[0] && !form.value.serviceTemplateId) {
form.value.serviceTemplateId = availableTemplates.value[0].id;
}
} catch (e) {
error.value = e instanceof ApiError ? e.message : 'خطا در بارگذاری خدمات';
} finally {
loading.value = false;
}
}
async function onAdd() {
const price = Number.parseFloat(form.value.price.replace(/,/g, ''));
if (!form.value.serviceTemplateId || !Number.isFinite(price) || price < 0) {
error.value = 'نوع خدمت و قیمت معتبر را وارد کنید.';
return;
}
adding.value = true;
error.value = '';
try {
await createBusinessService({
businessId: props.businessId,
serviceTemplateId: form.value.serviceTemplateId,
price,
});
form.value.price = '';
await load();
} catch (e) {
error.value = e instanceof ApiError ? e.message : 'افزودن خدمت ناموفق بود';
} finally {
adding.value = false;
}
}
async function onDelete(svc: BusinessService) {
const title = svc.serviceTemplate?.title ?? 'خدمت';
if (!confirm(`«${title}» حذف شود؟`)) return;
error.value = '';
try {
await deleteBusinessService(svc.id);
await load();
} catch (e) {
error.value = e instanceof ApiError ? e.message : 'حذف ناموفق بود';
}
}
watch(
() => [props.businessId, props.categoryId],
() => load(),
);
onMounted(load);
</script>
+7
View File
@@ -102,6 +102,12 @@
@remove-existing="onRemoveImage" @remove-existing="onRemoveImage"
/> />
<BusinessServicesSection
v-if="editingId && form.categoryId"
:business-id="editingId"
:category-id="form.categoryId"
/>
<AppButton type="submit" :loading="saving">ذخیره</AppButton> <AppButton type="submit" :loading="saving">ذخیره</AppButton>
</form> </form>
</AppModal> </AppModal>
@@ -189,6 +195,7 @@ import { fetchUsers, fetchUser } from '@/api/users';
import type { Business, BusinessCategory, User } from '@/api/types'; import type { Business, BusinessCategory, User } from '@/api/types';
import { ApiError } from '@/api/client'; import { ApiError } from '@/api/client';
import BusinessImageUpload from '@/components/admin/BusinessImageUpload.vue'; import BusinessImageUpload from '@/components/admin/BusinessImageUpload.vue';
import BusinessServicesSection from '@/components/admin/BusinessServicesSection.vue';
import BusinessListCard from '@/components/admin/BusinessListCard.vue'; import BusinessListCard from '@/components/admin/BusinessListCard.vue';
import AppBadge from '@/components/ui/AppBadge.vue'; import AppBadge from '@/components/ui/AppBadge.vue';
import AppButton from '@/components/ui/AppButton.vue'; import AppButton from '@/components/ui/AppButton.vue';