edit one business
This commit is contained in:
@@ -12,10 +12,12 @@ export type CreateBookingPayload = {
|
|||||||
export function fetchBookings(params?: {
|
export function fetchBookings(params?: {
|
||||||
businessId?: string;
|
businessId?: string;
|
||||||
status?: BookingStatus;
|
status?: BookingStatus;
|
||||||
|
scope?: 'mine';
|
||||||
}) {
|
}) {
|
||||||
const search = new URLSearchParams();
|
const search = new URLSearchParams();
|
||||||
if (params?.businessId) search.set('businessId', params.businessId);
|
if (params?.businessId) search.set('businessId', params.businessId);
|
||||||
if (params?.status) search.set('status', params.status);
|
if (params?.status) search.set('status', params.status);
|
||||||
|
if (params?.scope) search.set('scope', params.scope);
|
||||||
const query = search.toString();
|
const query = search.toString();
|
||||||
return api<Booking[]>(`/bookings${query ? `?${query}` : ''}`);
|
return api<Booking[]>(`/bookings${query ? `?${query}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-start justify-between gap-2">
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<p class="font-semibold text-text">
|
||||||
|
{{ booking.business?.title ?? 'کسبوکار' }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-0.5 text-sm text-muted">
|
||||||
|
{{ serviceTitle }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-xs text-muted">
|
||||||
|
{{ formatDate(booking.scheduledAt) }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
v-if="showCustomer && booking.customer?.mobile"
|
||||||
|
class="mt-1 text-xs text-muted"
|
||||||
|
dir="ltr"
|
||||||
|
>
|
||||||
|
{{ booking.customer.fullName ? `${booking.customer.fullName} · ` : '' }}{{ booking.customer.mobile }}
|
||||||
|
</p>
|
||||||
|
<p v-if="booking.note" class="mt-2 text-xs text-muted">{{ booking.note }}</p>
|
||||||
|
<span
|
||||||
|
class="mt-2 inline-block rounded-lg px-2 py-0.5 text-xs"
|
||||||
|
:class="statusClass"
|
||||||
|
>
|
||||||
|
{{ bookingStatusLabel(booking.status) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="canConfirm || canCancel"
|
||||||
|
class="mt-3 flex flex-wrap gap-2 border-t border-border pt-3"
|
||||||
|
>
|
||||||
|
<AppButton
|
||||||
|
v-if="canConfirm"
|
||||||
|
class="!w-auto flex-1 px-3"
|
||||||
|
:loading="actingId === booking.id"
|
||||||
|
@click="emit('confirm', booking)"
|
||||||
|
>
|
||||||
|
تأیید
|
||||||
|
</AppButton>
|
||||||
|
<AppButton
|
||||||
|
v-if="canCancel"
|
||||||
|
variant="danger"
|
||||||
|
class="!w-auto flex-1 px-3"
|
||||||
|
:loading="actingId === booking.id"
|
||||||
|
@click="emit('cancel', booking)"
|
||||||
|
>
|
||||||
|
لغو
|
||||||
|
</AppButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { Booking } from '@/api/types';
|
||||||
|
import AppButton from '@/components/ui/AppButton.vue';
|
||||||
|
import { bookingStatusLabel, formatDate } from '@/utils/format';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
booking: Booking;
|
||||||
|
actingId: string | null;
|
||||||
|
showCustomer: boolean;
|
||||||
|
canConfirm: boolean;
|
||||||
|
canCancel: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
confirm: [booking: Booking];
|
||||||
|
cancel: [booking: Booking];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const serviceTitle = computed(
|
||||||
|
() => props.booking.businessService?.serviceTemplate?.title ?? 'خدمت',
|
||||||
|
);
|
||||||
|
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
const status = props.booking.status;
|
||||||
|
if (status === 'confirmed' || status === 'completed') {
|
||||||
|
return 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400';
|
||||||
|
}
|
||||||
|
if (status === 'pending') {
|
||||||
|
return 'bg-amber-500/15 text-amber-700 dark:text-amber-400';
|
||||||
|
}
|
||||||
|
if (status === 'cancelled') {
|
||||||
|
return 'bg-red-500/15 text-red-600 dark:text-red-400';
|
||||||
|
}
|
||||||
|
return 'bg-surface-3 text-muted';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -13,6 +13,12 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
const isAdmin = computed(() => user.value?.role === 'admin');
|
const isAdmin = computed(() => user.value?.role === 'admin');
|
||||||
const isBusinessOwner = computed(() => user.value?.role === 'business_owner');
|
const isBusinessOwner = computed(() => user.value?.role === 'business_owner');
|
||||||
const isCustomer = computed(() => user.value?.role === 'customer');
|
const isCustomer = computed(() => user.value?.role === 'customer');
|
||||||
|
const canBookAsCustomer = computed(
|
||||||
|
() =>
|
||||||
|
user.value?.role === 'customer' ||
|
||||||
|
user.value?.role === 'business_owner' ||
|
||||||
|
user.value?.role === 'admin',
|
||||||
|
);
|
||||||
|
|
||||||
function setSession(accessToken: string, profile: User) {
|
function setSession(accessToken: string, profile: User) {
|
||||||
token.value = accessToken;
|
token.value = accessToken;
|
||||||
@@ -51,6 +57,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
isAdmin,
|
isAdmin,
|
||||||
isBusinessOwner,
|
isBusinessOwner,
|
||||||
isCustomer,
|
isCustomer,
|
||||||
|
canBookAsCustomer,
|
||||||
setSession,
|
setSession,
|
||||||
logout,
|
logout,
|
||||||
loadUser,
|
loadUser,
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<WebAppLayout>
|
<WebAppLayout>
|
||||||
<h1 class="mb-1 text-xl font-bold text-text">
|
<h1 class="mb-1 text-xl font-bold text-text">رزروها</h1>
|
||||||
{{ auth.isBusinessOwner ? 'رزروهای کسبوکار' : 'رزروهای من' }}
|
|
||||||
</h1>
|
|
||||||
<p class="mb-4 text-sm text-muted">
|
<p class="mb-4 text-sm text-muted">
|
||||||
{{
|
{{
|
||||||
auth.isBusinessOwner
|
auth.isBusinessOwner
|
||||||
? 'درخواستهای مشتریان برای خدمات شما'
|
? 'رزروهای شما و درخواستهای مشتریان برای کسبوکار شما'
|
||||||
: 'وضعیت رزروهای ثبتشده'
|
: 'وضعیت رزروهای ثبتشده'
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
@@ -24,13 +22,8 @@
|
|||||||
v-else-if="!bookings.length"
|
v-else-if="!bookings.length"
|
||||||
class="rounded-2xl border border-dashed border-border bg-surface-2 p-6 text-center text-sm text-muted"
|
class="rounded-2xl border border-dashed border-border bg-surface-2 p-6 text-center text-sm text-muted"
|
||||||
>
|
>
|
||||||
{{
|
هنوز رزروی ندارید. از صفحه کسبوکارها یک خدمت را رزرو کنید.
|
||||||
auth.isBusinessOwner
|
|
||||||
? 'هنوز رزروی برای کسبوکار شما ثبت نشده است.'
|
|
||||||
: 'هنوز رزروی ندارید. از صفحه کسبوکارها یک خدمت را رزرو کنید.'
|
|
||||||
}}
|
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-if="auth.isCustomer"
|
|
||||||
to="/app"
|
to="/app"
|
||||||
class="mt-3 inline-block text-brand-600 dark:text-brand-500"
|
class="mt-3 inline-block text-brand-600 dark:text-brand-500"
|
||||||
>
|
>
|
||||||
@@ -38,76 +31,60 @@
|
|||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-else class="space-y-3">
|
<div v-else class="space-y-6">
|
||||||
|
<section v-if="myBookings.length">
|
||||||
|
<h2 class="mb-3 text-sm font-semibold text-text">رزروهای من</h2>
|
||||||
|
<ul class="space-y-3">
|
||||||
<li
|
<li
|
||||||
v-for="b in bookings"
|
v-for="b in myBookings"
|
||||||
:key="b.id"
|
:key="`mine-${b.id}`"
|
||||||
class="rounded-2xl border border-border bg-surface-2 p-4"
|
class="rounded-2xl border border-border bg-surface-2 p-4"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-2">
|
<BookingItem
|
||||||
<div class="min-w-0 flex-1">
|
:booking="b"
|
||||||
<p class="font-semibold text-text">
|
:acting-id="actingId"
|
||||||
{{ b.business?.title ?? 'کسبوکار' }}
|
:show-customer="false"
|
||||||
</p>
|
:can-confirm="false"
|
||||||
<p class="mt-0.5 text-sm text-muted">
|
:can-cancel="canCancel(b)"
|
||||||
{{ serviceTitle(b) }}
|
@confirm="onConfirm"
|
||||||
</p>
|
@cancel="onCancel"
|
||||||
<p class="mt-1 text-xs text-muted">
|
/>
|
||||||
{{ formatDate(b.scheduledAt) }}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
v-if="auth.isBusinessOwner && b.customer?.mobile"
|
|
||||||
class="mt-1 text-xs text-muted"
|
|
||||||
dir="ltr"
|
|
||||||
>
|
|
||||||
{{ b.customer.fullName ? `${b.customer.fullName} · ` : '' }}{{ b.customer.mobile }}
|
|
||||||
</p>
|
|
||||||
<p v-if="b.note" class="mt-2 text-xs text-muted">{{ b.note }}</p>
|
|
||||||
<span
|
|
||||||
class="mt-2 inline-block rounded-lg px-2 py-0.5 text-xs"
|
|
||||||
:class="statusClass(b.status)"
|
|
||||||
>
|
|
||||||
{{ bookingStatusLabel(b.status) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="canActOn(b)"
|
|
||||||
class="mt-3 flex flex-wrap gap-2 border-t border-border pt-3"
|
|
||||||
>
|
|
||||||
<AppButton
|
|
||||||
v-if="auth.isBusinessOwner && b.status === 'pending'"
|
|
||||||
class="!w-auto flex-1 px-3"
|
|
||||||
:loading="actingId === b.id"
|
|
||||||
@click="onConfirm(b)"
|
|
||||||
>
|
|
||||||
تأیید
|
|
||||||
</AppButton>
|
|
||||||
<AppButton
|
|
||||||
v-if="canCancel(b)"
|
|
||||||
variant="danger"
|
|
||||||
class="!w-auto flex-1 px-3"
|
|
||||||
:loading="actingId === b.id"
|
|
||||||
@click="onCancel(b)"
|
|
||||||
>
|
|
||||||
لغو
|
|
||||||
</AppButton>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="incomingBookings.length">
|
||||||
|
<h2 class="mb-3 text-sm font-semibold text-text">رزروهای کسبوکار من</h2>
|
||||||
|
<ul class="space-y-3">
|
||||||
|
<li
|
||||||
|
v-for="b in incomingBookings"
|
||||||
|
:key="`incoming-${b.id}`"
|
||||||
|
class="rounded-2xl border border-border bg-surface-2 p-4"
|
||||||
|
>
|
||||||
|
<BookingItem
|
||||||
|
:booking="b"
|
||||||
|
:acting-id="actingId"
|
||||||
|
:show-customer="true"
|
||||||
|
:can-confirm="b.status === 'pending'"
|
||||||
|
:can-cancel="canCancel(b)"
|
||||||
|
@confirm="onConfirm"
|
||||||
|
@cancel="onCancel"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</WebAppLayout>
|
</WebAppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { cancelBooking, confirmBooking, fetchBookings } from '@/api/bookings';
|
import { cancelBooking, confirmBooking, fetchBookings } from '@/api/bookings';
|
||||||
import { ApiError } from '@/api/client';
|
import { ApiError } from '@/api/client';
|
||||||
import type { Booking, BookingStatus } from '@/api/types';
|
import type { Booking } from '@/api/types';
|
||||||
import AppButton from '@/components/ui/AppButton.vue';
|
import BookingItem from '@/components/booking/BookingItem.vue';
|
||||||
import WebAppLayout from '@/layouts/WebAppLayout.vue';
|
import WebAppLayout from '@/layouts/WebAppLayout.vue';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { bookingStatusLabel, formatDate } from '@/utils/format';
|
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const bookings = ref<Booking[]>([]);
|
const bookings = ref<Booking[]>([]);
|
||||||
@@ -115,38 +92,26 @@ const loading = ref(true);
|
|||||||
const error = ref('');
|
const error = ref('');
|
||||||
const actingId = ref<string | null>(null);
|
const actingId = ref<string | null>(null);
|
||||||
|
|
||||||
function serviceTitle(b: Booking): string {
|
const myBookings = computed(() =>
|
||||||
return b.businessService?.serviceTemplate?.title ?? 'خدمت';
|
bookings.value.filter((b) => b.customerId === auth.user?.id),
|
||||||
}
|
);
|
||||||
|
|
||||||
function statusClass(status: BookingStatus): string {
|
const incomingBookings = computed(() =>
|
||||||
if (status === 'confirmed' || status === 'completed') {
|
bookings.value.filter(
|
||||||
return 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400';
|
(b) =>
|
||||||
}
|
b.business?.ownerId === auth.user?.id && b.customerId !== auth.user?.id,
|
||||||
if (status === 'pending') {
|
),
|
||||||
return 'bg-amber-500/15 text-amber-700 dark:text-amber-400';
|
);
|
||||||
}
|
|
||||||
if (status === 'cancelled') {
|
|
||||||
return 'bg-red-500/15 text-red-600 dark:text-red-400';
|
|
||||||
}
|
|
||||||
return 'bg-surface-3 text-muted';
|
|
||||||
}
|
|
||||||
|
|
||||||
function canCancel(b: Booking): boolean {
|
function canCancel(b: Booking): boolean {
|
||||||
return b.status === 'pending' || b.status === 'confirmed';
|
return b.status === 'pending' || b.status === 'confirmed';
|
||||||
}
|
}
|
||||||
|
|
||||||
function canActOn(b: Booking): boolean {
|
|
||||||
if (b.status === 'cancelled' || b.status === 'completed') return false;
|
|
||||||
if (auth.isBusinessOwner && b.status === 'pending') return true;
|
|
||||||
return canCancel(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
bookings.value = await fetchBookings();
|
bookings.value = await fetchBookings({ scope: 'mine' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof ApiError ? e.message : 'بارگذاری رزروها ناموفق بود.';
|
error.value = e instanceof ApiError ? e.message : 'بارگذاری رزروها ناموفق بود.';
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<section v-else-if="business" class="space-y-6">
|
<section v-else-if="business" class="space-y-6">
|
||||||
<button
|
<button
|
||||||
|
v-if="showBackLink"
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center gap-1 text-sm text-brand-600 dark:text-brand-500"
|
class="inline-flex items-center gap-1 text-sm text-brand-600 dark:text-brand-500"
|
||||||
@click="router.back()"
|
@click="router.back()"
|
||||||
@@ -159,7 +160,7 @@ import { computed, onMounted, ref } from 'vue';
|
|||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { createBooking } from '@/api/bookings';
|
import { createBooking } from '@/api/bookings';
|
||||||
import { fetchBusinessServices } from '@/api/business-services';
|
import { fetchBusinessServices } from '@/api/business-services';
|
||||||
import { fetchBusiness } from '@/api/businesses';
|
import { fetchBusiness, fetchBusinesses } from '@/api/businesses';
|
||||||
import { ApiError } from '@/api/client';
|
import { ApiError } from '@/api/client';
|
||||||
import type { Business, BusinessService } from '@/api/types';
|
import type { Business, BusinessService } from '@/api/types';
|
||||||
import AppButton from '@/components/ui/AppButton.vue';
|
import AppButton from '@/components/ui/AppButton.vue';
|
||||||
@@ -190,12 +191,17 @@ const bookingForm = ref({
|
|||||||
note: '',
|
note: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const canBook = computed(
|
const showBackLink = ref(false);
|
||||||
() =>
|
|
||||||
auth.isCustomer &&
|
const canBook = computed(() => {
|
||||||
business.value?.isActive === true &&
|
if (!auth.user || business.value?.isActive !== true || !services.value.length) {
|
||||||
services.value.length > 0,
|
return false;
|
||||||
);
|
}
|
||||||
|
if (business.value.ownerId && business.value.ownerId === auth.user.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return auth.canBookAsCustomer;
|
||||||
|
});
|
||||||
|
|
||||||
function serviceLabel(svc: BusinessService): string {
|
function serviceLabel(svc: BusinessService): string {
|
||||||
return svc.serviceTemplate?.title ?? 'خدمت';
|
return svc.serviceTemplate?.title ?? 'خدمت';
|
||||||
@@ -220,7 +226,12 @@ async function load() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
business.value = await fetchBusiness(id);
|
const [businessResult, activeBusinesses] = await Promise.all([
|
||||||
|
fetchBusiness(id),
|
||||||
|
fetchBusinesses({ isActive: true }),
|
||||||
|
]);
|
||||||
|
business.value = businessResult;
|
||||||
|
showBackLink.value = activeBusinesses.length > 1;
|
||||||
await loadServices(id);
|
await loadServices(id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value =
|
error.value =
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<WebAppLayout>
|
<WebAppLayout>
|
||||||
<section class="space-y-5">
|
<div v-if="bootstrapping" class="py-12 text-center text-sm text-muted">
|
||||||
|
در حال بارگذاری…
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section v-else class="space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-xl font-bold text-text">کشف خدمات</h1>
|
<h1 class="text-xl font-bold text-text">کشف خدمات</h1>
|
||||||
<p class="mt-1 text-sm text-muted">کسبوکارهای محلی نزدیک شما</p>
|
<p class="mt-1 text-sm text-muted">کسبوکارهای محلی نزدیک شما</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative">
|
<div v-if="showSearch" class="relative">
|
||||||
<label class="sr-only" for="search">جستجو</label>
|
<label class="sr-only" for="search">جستجو</label>
|
||||||
<AppIcon
|
<AppIcon
|
||||||
name="search"
|
name="search"
|
||||||
@@ -22,11 +26,8 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="loadingCategories" class="text-center text-sm text-muted">
|
|
||||||
در حال بارگذاری دستهها…
|
|
||||||
</div>
|
|
||||||
<CategoryFilter
|
<CategoryFilter
|
||||||
v-else-if="categories.length"
|
v-if="showCategoryFilter"
|
||||||
:categories="activeCategories"
|
:categories="activeCategories"
|
||||||
:selected-id="selectedCategoryId"
|
:selected-id="selectedCategoryId"
|
||||||
@select="onCategorySelect"
|
@select="onCategorySelect"
|
||||||
@@ -58,6 +59,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import { fetchBusinessCategories } from '@/api/business-categories';
|
import { fetchBusinessCategories } from '@/api/business-categories';
|
||||||
import { fetchBusinesses } from '@/api/businesses';
|
import { fetchBusinesses } from '@/api/businesses';
|
||||||
import { ApiError } from '@/api/client';
|
import { ApiError } from '@/api/client';
|
||||||
@@ -67,18 +69,24 @@ import AppIcon from '@/components/ui/AppIcon.vue';
|
|||||||
import CategoryFilter from '@/components/business/CategoryFilter.vue';
|
import CategoryFilter from '@/components/business/CategoryFilter.vue';
|
||||||
import WebAppLayout from '@/layouts/WebAppLayout.vue';
|
import WebAppLayout from '@/layouts/WebAppLayout.vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const categories = ref<BusinessCategory[]>([]);
|
const categories = ref<BusinessCategory[]>([]);
|
||||||
const businesses = ref<Business[]>([]);
|
const businesses = ref<Business[]>([]);
|
||||||
const selectedCategoryId = ref<string | null>(null);
|
const selectedCategoryId = ref<string | null>(null);
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const loadingCategories = ref(true);
|
const bootstrapping = ref(true);
|
||||||
const loadingBusinesses = ref(true);
|
const loadingBusinesses = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
|
|
||||||
const activeCategories = computed(() =>
|
const activeCategories = computed(() =>
|
||||||
categories.value.filter((c) => c.isActive),
|
categories.value.filter((c) => c.isActive),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const showCategoryFilter = computed(() => activeCategories.value.length > 1);
|
||||||
|
|
||||||
|
const showSearch = computed(() => businesses.value.length > 1);
|
||||||
|
|
||||||
const filteredBusinesses = computed(() => {
|
const filteredBusinesses = computed(() => {
|
||||||
const q = searchQuery.value.trim().toLowerCase();
|
const q = searchQuery.value.trim().toLowerCase();
|
||||||
if (!q) {
|
if (!q) {
|
||||||
@@ -94,15 +102,14 @@ const filteredBusinesses = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function loadCategories() {
|
async function loadCategories() {
|
||||||
loadingCategories.value = true;
|
|
||||||
try {
|
try {
|
||||||
const all = await fetchBusinessCategories();
|
categories.value = await fetchBusinessCategories();
|
||||||
categories.value = all;
|
if (activeCategories.value.length === 1) {
|
||||||
|
selectedCategoryId.value = activeCategories.value[0].id;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value =
|
error.value =
|
||||||
e instanceof ApiError ? e.message : 'بارگذاری دستهها ناموفق بود.';
|
e instanceof ApiError ? e.message : 'بارگذاری دستهها ناموفق بود.';
|
||||||
} finally {
|
|
||||||
loadingCategories.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,10 +134,23 @@ function onCategorySelect(id: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(selectedCategoryId, () => {
|
watch(selectedCategoryId, () => {
|
||||||
loadBusinesses();
|
if (!bootstrapping.value) {
|
||||||
|
void loadBusinesses();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
void Promise.all([loadCategories(), loadBusinesses()]);
|
await loadCategories();
|
||||||
|
await loadBusinesses();
|
||||||
|
|
||||||
|
if (businesses.value.length === 1) {
|
||||||
|
await router.replace({
|
||||||
|
name: 'app-business-detail',
|
||||||
|
params: { id: businesses.value[0].id },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrapping.value = false;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user