Add business services management in admin edit form
This commit is contained in:
@@ -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>
|
||||
@@ -102,6 +102,12 @@
|
||||
@remove-existing="onRemoveImage"
|
||||
/>
|
||||
|
||||
<BusinessServicesSection
|
||||
v-if="editingId && form.categoryId"
|
||||
:business-id="editingId"
|
||||
:category-id="form.categoryId"
|
||||
/>
|
||||
|
||||
<AppButton type="submit" :loading="saving">ذخیره</AppButton>
|
||||
</form>
|
||||
</AppModal>
|
||||
@@ -189,6 +195,7 @@ import { fetchUsers, fetchUser } from '@/api/users';
|
||||
import type { Business, BusinessCategory, User } from '@/api/types';
|
||||
import { ApiError } from '@/api/client';
|
||||
import BusinessImageUpload from '@/components/admin/BusinessImageUpload.vue';
|
||||
import BusinessServicesSection from '@/components/admin/BusinessServicesSection.vue';
|
||||
import BusinessListCard from '@/components/admin/BusinessListCard.vue';
|
||||
import AppBadge from '@/components/ui/AppBadge.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
|
||||
Reference in New Issue
Block a user