add business_owner
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="{ name: 'app-business-detail', params: { id: business.id } }"
|
:to="{ name: 'app-business-detail', params: { id: business.id } }"
|
||||||
class="group flex h-full flex-col rounded-2xl border border-slate-200 bg-white p-6 text-center shadow-sm transition hover:-translate-y-0.5 hover:border-brand-500 hover:shadow-md dark:border-border dark:bg-surface"
|
class="group flex h-full flex-col rounded-xl border border-slate-200 bg-white p-4 text-center shadow-sm transition hover:-translate-y-0.5 hover:border-brand-500 hover:shadow-md dark:border-border dark:bg-surface"
|
||||||
:class="featured ? 'border-brand-500 ring-1 ring-brand-500/20' : ''"
|
:class="featured ? 'border-brand-500 ring-1 ring-brand-500/20' : ''"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx-auto mb-4 flex h-16 w-16 items-center justify-center overflow-hidden rounded-full bg-brand-500/10 text-brand-600 transition group-hover:bg-brand-500/15 dark:text-brand-500"
|
class="mx-auto mb-3 flex h-12 w-12 items-center justify-center overflow-hidden rounded-full bg-brand-500/10 text-brand-600 transition group-hover:bg-brand-500/15 dark:text-brand-500"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="cover"
|
v-if="cover"
|
||||||
@@ -14,42 +14,35 @@
|
|||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<AppIcon v-else name="store" size="lg" />
|
<AppIcon v-else name="store" size="md" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-base font-bold text-slate-800 dark:text-text">{{ business.title }}</h3>
|
<h3 class="text-sm font-bold text-slate-800 dark:text-text">{{ business.title }}</h3>
|
||||||
|
|
||||||
<BusinessRatingBadge
|
<BusinessRatingBadge
|
||||||
class="mx-auto mt-2"
|
class="mx-auto mt-1.5"
|
||||||
:average-rating="business.averageRating"
|
:average-rating="business.averageRating"
|
||||||
:review-count="business.reviewCount"
|
:review-count="business.reviewCount"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="business.description"
|
v-if="business.description"
|
||||||
class="mt-2 line-clamp-3 min-h-[4.5rem] text-sm leading-7 text-slate-600 dark:text-muted"
|
class="mt-1.5 line-clamp-2 text-xs leading-6 text-slate-600 dark:text-muted"
|
||||||
>
|
>
|
||||||
{{ business.description }}
|
{{ business.description }}
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="mt-2 min-h-[4.5rem] text-sm text-slate-400 dark:text-muted">—</p>
|
|
||||||
|
|
||||||
<p v-if="business.category" class="mt-3 text-xs font-medium text-brand-600 dark:text-brand-500">
|
<p v-if="business.category" class="mt-2 text-[11px] font-medium text-brand-600 dark:text-brand-500">
|
||||||
{{ business.category.title }}
|
{{ business.category.title }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="business.address"
|
v-if="business.address"
|
||||||
class="mt-2 flex items-center justify-center gap-1 text-xs text-slate-500 dark:text-muted"
|
class="mt-1.5 flex items-center justify-center gap-1 text-[11px] text-slate-500 dark:text-muted"
|
||||||
>
|
>
|
||||||
<AppIcon name="map-pin" size="sm" class="shrink-0" />
|
<AppIcon name="map-pin" size="sm" class="shrink-0" />
|
||||||
<span class="line-clamp-1">{{ business.address }}</span>
|
<span class="line-clamp-1">{{ business.address }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<span
|
|
||||||
class="mt-auto pt-5 text-sm font-semibold text-brand-600 transition group-hover:text-brand-700 dark:text-brand-500"
|
|
||||||
>
|
|
||||||
مشاهده و رزرو
|
|
||||||
</span>
|
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
v-else
|
v-else
|
||||||
:class="
|
:class="
|
||||||
variant === 'featured'
|
variant === 'featured'
|
||||||
? 'grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3'
|
? 'grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4'
|
||||||
: 'grid grid-cols-1 gap-2'
|
: 'grid grid-cols-1 gap-2'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
|
|
||||||
<div class="relative shadow-[0_20px_50px_-12px_rgba(0,0,0,0.18)]">
|
<div class="relative shadow-[0_20px_50px_-12px_rgba(0,0,0,0.18)]">
|
||||||
<BusinessImageGallery
|
<BusinessImageGallery
|
||||||
:images="singleBusiness?.images"
|
:images="landingBusiness?.images"
|
||||||
:title="heroTitle"
|
:title="heroTitle"
|
||||||
variant="hero"
|
variant="hero"
|
||||||
frame-extra-class="landing-hero-blob bg-slate-200"
|
frame-extra-class="landing-hero-blob bg-slate-200"
|
||||||
@@ -103,13 +103,13 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const tenant = useTenantStore();
|
const tenant = useTenantStore();
|
||||||
const { singleBusiness } = storeToRefs(tenant);
|
const { landingBusiness } = storeToRefs(tenant);
|
||||||
|
|
||||||
const heroTitle = computed(() => singleBusiness.value?.title ?? '');
|
const heroTitle = computed(() => landingBusiness.value?.title ?? '');
|
||||||
|
|
||||||
const heroSubtitle = computed(() => singleBusiness.value?.subtitle?.trim() ?? '');
|
const heroSubtitle = computed(() => landingBusiness.value?.subtitle?.trim() ?? '');
|
||||||
|
|
||||||
const heroDescription = computed(() => singleBusiness.value?.description?.trim() ?? '');
|
const heroDescription = computed(() => landingBusiness.value?.description?.trim() ?? '');
|
||||||
|
|
||||||
const highlights: { label: string; icon: AppIconName }[] = [
|
const highlights: { label: string; icon: AppIconName }[] = [
|
||||||
{ label: 'رضایت مشتریان', icon: 'shield' },
|
{ label: 'رضایت مشتریان', icon: 'shield' },
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const gridClass = computed(() => {
|
|||||||
function serviceLink(item: LandingServiceItem) {
|
function serviceLink(item: LandingServiceItem) {
|
||||||
if (!props.bookingEnabled) return '#services';
|
if (!props.bookingEnabled) return '#services';
|
||||||
return {
|
return {
|
||||||
name: 'app-business-detail',
|
name: 'app-business-book',
|
||||||
params: { id: item.businessId },
|
params: { id: item.businessId },
|
||||||
hash: 'services',
|
hash: 'services',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const props = withDefaults(
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const tenant = useTenantStore();
|
const tenant = useTenantStore();
|
||||||
const { singleBusinessId } = storeToRefs(tenant);
|
const { landingBusinessId } = storeToRefs(tenant);
|
||||||
|
|
||||||
function scrollToServices() {
|
function scrollToServices() {
|
||||||
const el = document.getElementById('services');
|
const el = document.getElementById('services');
|
||||||
@@ -55,7 +55,7 @@ function goToBooking() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = singleBusinessId.value;
|
const id = landingBusinessId.value;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
scrollToServices();
|
scrollToServices();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
class="overflow-hidden rounded-2xl bg-slate-100 shadow-[0_8px_30px_rgba(15,23,42,0.12)] dark:bg-surface-2 dark:shadow-black/30"
|
class="overflow-hidden rounded-2xl bg-slate-100 shadow-[0_8px_30px_rgba(15,23,42,0.12)] dark:bg-surface-2 dark:shadow-black/30"
|
||||||
>
|
>
|
||||||
<BusinessImageGallery
|
<BusinessImageGallery
|
||||||
:images="singleBusiness?.images"
|
:images="landingBusiness?.images"
|
||||||
:title="businessTitle"
|
:title="businessTitle"
|
||||||
variant="card"
|
variant="card"
|
||||||
/>
|
/>
|
||||||
@@ -132,11 +132,11 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const tenant = useTenantStore();
|
const tenant = useTenantStore();
|
||||||
const { singleBusiness } = storeToRefs(tenant);
|
const { landingBusiness } = storeToRefs(tenant);
|
||||||
|
|
||||||
const businessTitle = computed(() => singleBusiness.value?.title ?? '');
|
const businessTitle = computed(() => landingBusiness.value?.title ?? '');
|
||||||
const businessSubtitle = computed(() => singleBusiness.value?.subtitle?.trim() ?? '');
|
const businessSubtitle = computed(() => landingBusiness.value?.subtitle?.trim() ?? '');
|
||||||
const aboutText = computed(() => singleBusiness.value?.description?.trim() ?? '');
|
const aboutText = computed(() => landingBusiness.value?.description?.trim() ?? '');
|
||||||
|
|
||||||
const quickActions: {
|
const quickActions: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -80,23 +80,23 @@ import AppIcon from '@/components/ui/AppIcon.vue';
|
|||||||
import { useTenantStore } from '@/stores/tenant';
|
import { useTenantStore } from '@/stores/tenant';
|
||||||
|
|
||||||
const tenant = useTenantStore();
|
const tenant = useTenantStore();
|
||||||
const { brand, singleBusiness, isSingleBusiness } = storeToRefs(tenant);
|
const { brand, landingBusiness } = storeToRefs(tenant);
|
||||||
|
|
||||||
const contactPhone = computed(() => {
|
const contactPhone = computed(() => {
|
||||||
if (isSingleBusiness.value && singleBusiness.value?.contactPhone) {
|
if (landingBusiness.value?.contactPhone) {
|
||||||
return singleBusiness.value.contactPhone;
|
return landingBusiness.value.contactPhone;
|
||||||
}
|
}
|
||||||
return '021-12345678';
|
return '021-12345678';
|
||||||
});
|
});
|
||||||
|
|
||||||
const contactMobile = computed(() => {
|
const contactMobile = computed(() => {
|
||||||
if (isSingleBusiness.value) return null;
|
if (landingBusiness.value) return null;
|
||||||
return '0912-123-4567';
|
return '0912-123-4567';
|
||||||
});
|
});
|
||||||
|
|
||||||
const address = computed(() => {
|
const address = computed(() => {
|
||||||
if (isSingleBusiness.value && singleBusiness.value?.address) {
|
if (landingBusiness.value?.address) {
|
||||||
return singleBusiness.value.address;
|
return landingBusiness.value.address;
|
||||||
}
|
}
|
||||||
return 'تهران، خیابان ولیعصر، پلاک ۱۲۳';
|
return 'تهران، خیابان ولیعصر، پلاک ۱۲۳';
|
||||||
});
|
});
|
||||||
|
|||||||
+9
-1
@@ -30,6 +30,11 @@ const router = createRouter({
|
|||||||
name: 'app-business-detail',
|
name: 'app-business-detail',
|
||||||
component: () => import('@/views/app/AppBusinessDetailView.vue'),
|
component: () => import('@/views/app/AppBusinessDetailView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/app/businesses/:id/book',
|
||||||
|
name: 'app-business-book',
|
||||||
|
component: () => import('@/views/app/AppBusinessBookingView.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/app/bookings',
|
path: '/app/bookings',
|
||||||
name: 'app-bookings',
|
name: 'app-bookings',
|
||||||
@@ -86,7 +91,10 @@ router.beforeEach(async (to) => {
|
|||||||
return auth.isBusinessOwner ? { name: 'app-owner' } : { name: 'app-home' };
|
return auth.isBusinessOwner ? { name: 'app-owner' } : { name: 'app-home' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (to.name === 'app-business-detail' && auth.isBusinessOwner) {
|
if (
|
||||||
|
(to.name === 'app-business-detail' || to.name === 'app-business-book') &&
|
||||||
|
auth.isBusinessOwner
|
||||||
|
) {
|
||||||
const { ensureLoaded, isOwnBusiness } = useOwnedBusiness();
|
const { ensureLoaded, isOwnBusiness } = useOwnedBusiness();
|
||||||
await ensureLoaded();
|
await ensureLoaded();
|
||||||
if (isOwnBusiness(String(to.params.id))) {
|
if (isOwnBusiness(String(to.params.id))) {
|
||||||
|
|||||||
+24
-10
@@ -10,22 +10,28 @@ export const useTenantStore = defineStore('tenant', () => {
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const isSingleBusiness = ref(false);
|
const isSingleBusiness = ref(false);
|
||||||
const singleBusiness = ref<Business | null>(null);
|
const singleBusiness = ref<Business | null>(null);
|
||||||
|
/** Business landing opened from multi-tenant discovery (not platform-wide single-business mode). */
|
||||||
|
const focusedBusiness = ref<Business | null>(null);
|
||||||
|
|
||||||
|
const landingBusiness = computed(() =>
|
||||||
|
isSingleBusiness.value ? singleBusiness.value : focusedBusiness.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
const landingBusinessId = computed(() => landingBusiness.value?.id ?? null);
|
||||||
|
|
||||||
const brand = computed<SiteBrand>(() => {
|
const brand = computed<SiteBrand>(() => {
|
||||||
const base = isSingleBusiness.value ? NIVASH_BRAND : MONTIME_BRAND;
|
const biz = landingBusiness.value;
|
||||||
|
if (biz) {
|
||||||
if (isSingleBusiness.value && singleBusiness.value) {
|
|
||||||
return {
|
return {
|
||||||
...base,
|
...NIVASH_BRAND,
|
||||||
name: singleBusiness.value.title,
|
name: biz.title,
|
||||||
shortName: singleBusiness.value.title,
|
shortName: biz.title,
|
||||||
copyrightName: singleBusiness.value.title,
|
copyrightName: biz.title,
|
||||||
aboutText:
|
aboutText: biz.description?.trim() || NIVASH_BRAND.aboutText,
|
||||||
singleBusiness.value.description?.trim() || base.aboutText,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return base;
|
return MONTIME_BRAND;
|
||||||
});
|
});
|
||||||
|
|
||||||
const singleBusinessId = computed(() => singleBusiness.value?.id ?? null);
|
const singleBusinessId = computed(() => singleBusiness.value?.id ?? null);
|
||||||
@@ -35,6 +41,10 @@ export const useTenantStore = defineStore('tenant', () => {
|
|||||||
singleBusiness.value = business;
|
singleBusiness.value = business;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setFocusedBusiness(business: Business | null) {
|
||||||
|
focusedBusiness.value = business;
|
||||||
|
}
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
if (loaded.value || loading.value) return;
|
if (loaded.value || loading.value) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -68,8 +78,12 @@ export const useTenantStore = defineStore('tenant', () => {
|
|||||||
isSingleBusiness,
|
isSingleBusiness,
|
||||||
singleBusiness,
|
singleBusiness,
|
||||||
singleBusinessId,
|
singleBusinessId,
|
||||||
|
focusedBusiness,
|
||||||
|
landingBusiness,
|
||||||
|
landingBusinessId,
|
||||||
brand,
|
brand,
|
||||||
bootstrap,
|
bootstrap,
|
||||||
syncSingleBusiness,
|
syncSingleBusiness,
|
||||||
|
setFocusedBusiness,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function loginRouteWithRedirect(redirectPath: string) {
|
|||||||
|
|
||||||
export function businessBookingPath(businessId: string, hash?: string) {
|
export function businessBookingPath(businessId: string, hash?: string) {
|
||||||
const normalizedHash = hash?.replace(/^#/, '');
|
const normalizedHash = hash?.replace(/^#/, '');
|
||||||
const base = `/app/businesses/${businessId}`;
|
const base = `/app/businesses/${businessId}/book`;
|
||||||
return normalizedHash ? `${base}#${normalizedHash}` : base;
|
return normalizedHash ? `${base}#${normalizedHash}` : base;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export function bookingDestination(
|
|||||||
|
|
||||||
const normalizedHash = hash?.replace(/^#/, '');
|
const normalizedHash = hash?.replace(/^#/, '');
|
||||||
return {
|
return {
|
||||||
name: 'app-business-detail' as const,
|
name: 'app-business-book' as const,
|
||||||
params: { id: businessId },
|
params: { id: businessId },
|
||||||
...(normalizedHash ? { hash: normalizedHash } : {}),
|
...(normalizedHash ? { hash: normalizedHash } : {}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<ResponsiveAppLayout>
|
||||||
|
<DetailPageSkeleton v-if="loading" />
|
||||||
|
<p
|
||||||
|
v-else-if="error"
|
||||||
|
class="rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
|
<section v-else-if="business" class="min-w-0 max-w-full space-y-6">
|
||||||
|
<div
|
||||||
|
v-if="!business.isActive"
|
||||||
|
class="rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-700 dark:text-amber-400"
|
||||||
|
>
|
||||||
|
این کسبوکار فعلاً غیرفعال است.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="isOwnBusinessView"
|
||||||
|
class="rounded-2xl border border-border bg-surface-2 p-6 text-center"
|
||||||
|
>
|
||||||
|
<p class="text-sm text-muted">
|
||||||
|
شما صاحب این کسبوکار هستید و نمیتوانید برای خودتان نوبت رزرو کنید.
|
||||||
|
</p>
|
||||||
|
<RouterLink
|
||||||
|
:to="{ name: 'app-owner' }"
|
||||||
|
class="mt-4 inline-flex items-center justify-center rounded-xl bg-brand-600 px-5 py-3 text-sm font-semibold text-white"
|
||||||
|
>
|
||||||
|
ورود به پنل مدیریت
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DesktopBookingFlow
|
||||||
|
v-else
|
||||||
|
:business="business"
|
||||||
|
:services="services"
|
||||||
|
:loading-services="loadingServices"
|
||||||
|
:services-error="servicesError"
|
||||||
|
:home-to="homeRoute"
|
||||||
|
@login="goToLoginForBooking"
|
||||||
|
@view-bookings="router.push({ name: 'app-bookings' })"
|
||||||
|
@go-home="router.push(homeRoute)"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</ResponsiveAppLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||||
|
import { fetchBusinessServices } from '@/api/business-services';
|
||||||
|
import { fetchBusiness } from '@/api/businesses';
|
||||||
|
import { ApiError } from '@/api/client';
|
||||||
|
import type { Business, BusinessService } from '@/api/types';
|
||||||
|
import DesktopBookingFlow from '@/components/booking/DesktopBookingFlow.vue';
|
||||||
|
import DetailPageSkeleton from '@/components/ui/DetailPageSkeleton.vue';
|
||||||
|
import ResponsiveAppLayout from '@/layouts/ResponsiveAppLayout.vue';
|
||||||
|
import { useOwnedBusiness } from '@/composables/useOwnedBusiness';
|
||||||
|
import { loginRouteWithRedirect } from '@/utils/auth-redirect';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const { ensureLoaded, isOwnBusiness } = useOwnedBusiness();
|
||||||
|
|
||||||
|
const business = ref<Business | null>(null);
|
||||||
|
const services = ref<BusinessService[]>([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
const loadingServices = ref(false);
|
||||||
|
const error = ref('');
|
||||||
|
const servicesError = ref('');
|
||||||
|
|
||||||
|
const homeRoute = computed(() => ({
|
||||||
|
name: 'app-business-detail' as const,
|
||||||
|
params: { id: String(route.params.id) },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const isOwnBusinessView = computed(() =>
|
||||||
|
business.value ? isOwnBusiness(business.value.id) : false,
|
||||||
|
);
|
||||||
|
|
||||||
|
async function loadBusiness() {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
business.value = await fetchBusiness(String(route.params.id));
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e instanceof ApiError ? e.message : 'بارگذاری کسبوکار ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadServices() {
|
||||||
|
if (!business.value) return;
|
||||||
|
loadingServices.value = true;
|
||||||
|
servicesError.value = '';
|
||||||
|
try {
|
||||||
|
services.value = await fetchBusinessServices(business.value.id, { activeOnly: true });
|
||||||
|
} catch (e) {
|
||||||
|
servicesError.value = e instanceof ApiError ? e.message : 'بارگذاری خدمات ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
loadingServices.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToLoginForBooking() {
|
||||||
|
void router.push(loginRouteWithRedirect(route.fullPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await ensureLoaded();
|
||||||
|
await loadBusiness();
|
||||||
|
if (!isOwnBusinessView.value) {
|
||||||
|
await loadServices();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,114 +1,141 @@
|
|||||||
<template>
|
<template>
|
||||||
<ResponsiveAppLayout>
|
<div
|
||||||
<DetailPageSkeleton v-if="loading" />
|
v-if="loading"
|
||||||
|
class="flex min-h-dvh items-center justify-center bg-white dark:bg-[#0d1117]"
|
||||||
|
>
|
||||||
|
<BusinessCardSkeleton class="w-full max-w-5xl px-8" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="error" class="mx-auto max-w-lg px-6 py-16">
|
||||||
<p
|
<p
|
||||||
v-else-if="error"
|
|
||||||
class="rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"
|
class="rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"
|
||||||
>
|
>
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</p>
|
</p>
|
||||||
<section v-else-if="business" class="min-w-0 max-w-full space-y-6">
|
</div>
|
||||||
<div
|
|
||||||
v-if="!business.isActive"
|
|
||||||
class="rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-700 dark:text-amber-400"
|
|
||||||
>
|
|
||||||
این کسبوکار فعلاً غیرفعال است.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<DesktopLandingPage
|
||||||
v-else-if="isOwnBusinessView"
|
v-else-if="business && isDesktop"
|
||||||
class="rounded-2xl border border-border bg-surface-2 p-6 text-center"
|
:services="landingServices"
|
||||||
>
|
:loading-services="loadingServices"
|
||||||
<p class="text-sm text-muted">
|
:services-error="servicesError"
|
||||||
شما صاحب این کسبوکار هستید و نمیتوانید برای خودتان نوبت رزرو کنید.
|
:booking-enabled="bookingEnabled"
|
||||||
</p>
|
/>
|
||||||
<RouterLink
|
|
||||||
:to="{ name: 'app-owner' }"
|
|
||||||
class="mt-4 inline-flex items-center justify-center rounded-xl bg-brand-600 px-5 py-3 text-sm font-semibold text-white"
|
|
||||||
>
|
|
||||||
ورود به پنل مدیریت
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DesktopBookingFlow
|
<WebAppLayout v-else-if="business">
|
||||||
v-else
|
<div
|
||||||
:business="business"
|
v-if="!business.isActive"
|
||||||
:services="services"
|
class="mb-4 rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-700 dark:text-amber-400"
|
||||||
:loading-services="loadingServices"
|
>
|
||||||
:services-error="servicesError"
|
این کسبوکار فعلاً غیرفعال است.
|
||||||
:home-to="homeRoute"
|
</div>
|
||||||
@login="goToLoginForBooking"
|
<MobileBusinessLanding
|
||||||
@view-bookings="router.push({ name: 'app-bookings' })"
|
:services="landingServices"
|
||||||
@go-home="router.push(homeRoute)"
|
:loading-services="loadingServices"
|
||||||
/>
|
:services-error="servicesError"
|
||||||
</section>
|
:booking-enabled="bookingEnabled"
|
||||||
</ResponsiveAppLayout>
|
@book="goToBooking"
|
||||||
|
/>
|
||||||
|
</WebAppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { useMediaQuery } from '@vueuse/core';
|
||||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { fetchBusinessServices } from '@/api/business-services';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { fetchBusiness } from '@/api/businesses';
|
import { fetchBusiness } from '@/api/businesses';
|
||||||
import { ApiError } from '@/api/client';
|
import { ApiError } from '@/api/client';
|
||||||
import type { Business, BusinessService } from '@/api/types';
|
import type { Business } from '@/api/types';
|
||||||
import DesktopBookingFlow from '@/components/booking/DesktopBookingFlow.vue';
|
import MobileBusinessLanding from '@/components/landing/MobileBusinessLanding.vue';
|
||||||
import DetailPageSkeleton from '@/components/ui/DetailPageSkeleton.vue';
|
import DesktopLandingPage from '@/components/landing/DesktopLandingPage.vue';
|
||||||
import ResponsiveAppLayout from '@/layouts/ResponsiveAppLayout.vue';
|
import BusinessCardSkeleton from '@/components/ui/BusinessCardSkeleton.vue';
|
||||||
|
import WebAppLayout from '@/layouts/WebAppLayout.vue';
|
||||||
import { useOwnedBusiness } from '@/composables/useOwnedBusiness';
|
import { useOwnedBusiness } from '@/composables/useOwnedBusiness';
|
||||||
import { loginRouteWithRedirect } from '@/utils/auth-redirect';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import { useTenantStore } from '@/stores/tenant';
|
||||||
|
import type { LandingServiceItem } from '@/types/landing';
|
||||||
|
import { fetchLandingServices } from '@/utils/landing-services';
|
||||||
|
import { bookingDestination } from '@/utils/auth-redirect';
|
||||||
|
|
||||||
|
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const tenant = useTenantStore();
|
||||||
const { ensureLoaded, isOwnBusiness } = useOwnedBusiness();
|
const { ensureLoaded, isOwnBusiness } = useOwnedBusiness();
|
||||||
|
|
||||||
const business = ref<Business | null>(null);
|
const business = ref<Business | null>(null);
|
||||||
const services = ref<BusinessService[]>([]);
|
const landingServices = ref<LandingServiceItem[]>([]);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const loadingServices = ref(false);
|
const loadingServices = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const servicesError = ref('');
|
const servicesError = ref('');
|
||||||
|
|
||||||
const homeRoute = computed(() => ({ name: 'app-home' }));
|
const bookingEnabled = computed(() => {
|
||||||
|
if (!business.value) return false;
|
||||||
const isOwnBusinessView = computed(() =>
|
if (!business.value.isActive) return false;
|
||||||
business.value ? isOwnBusiness(business.value.id) : false,
|
return !isOwnBusiness(business.value.id);
|
||||||
);
|
});
|
||||||
|
|
||||||
async function loadBusiness() {
|
async function loadBusiness() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
business.value = await fetchBusiness(String(route.params.id));
|
const id = String(route.params.id);
|
||||||
|
business.value = await fetchBusiness(id);
|
||||||
|
tenant.setFocusedBusiness(business.value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
business.value = null;
|
||||||
|
tenant.setFocusedBusiness(null);
|
||||||
error.value = e instanceof ApiError ? e.message : 'بارگذاری کسبوکار ناموفق بود.';
|
error.value = e instanceof ApiError ? e.message : 'بارگذاری کسبوکار ناموفق بود.';
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadServices() {
|
async function loadLandingServices() {
|
||||||
if (!business.value) return;
|
if (!business.value) return;
|
||||||
loadingServices.value = true;
|
loadingServices.value = true;
|
||||||
servicesError.value = '';
|
servicesError.value = '';
|
||||||
try {
|
try {
|
||||||
services.value = await fetchBusinessServices(business.value.id, { activeOnly: true });
|
landingServices.value = await fetchLandingServices(business.value.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
servicesError.value = e instanceof ApiError ? e.message : 'بارگذاری خدمات ناموفق بود.';
|
landingServices.value = [];
|
||||||
|
servicesError.value =
|
||||||
|
e instanceof ApiError ? e.message : 'بارگذاری خدمات ناموفق بود.';
|
||||||
} finally {
|
} finally {
|
||||||
loadingServices.value = false;
|
loadingServices.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToLoginForBooking() {
|
function goToBooking() {
|
||||||
void router.push(loginRouteWithRedirect(route.fullPath));
|
if (!business.value || !bookingEnabled.value) return;
|
||||||
|
void router.push(bookingDestination(business.value.id, auth.isLoggedIn));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.params.id,
|
||||||
|
async () => {
|
||||||
|
await loadBusiness();
|
||||||
|
if (business.value) {
|
||||||
|
await loadLandingServices();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await ensureLoaded();
|
if (auth.isLoggedIn) {
|
||||||
|
await auth.loadUser();
|
||||||
|
await ensureLoaded();
|
||||||
|
}
|
||||||
|
await tenant.bootstrap();
|
||||||
await loadBusiness();
|
await loadBusiness();
|
||||||
if (!isOwnBusinessView.value) {
|
if (business.value) {
|
||||||
await loadServices();
|
await loadLandingServices();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
tenant.setFocusedBusiness(null);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user