edit business owner
This commit is contained in:
@@ -289,8 +289,10 @@ const classStepRequired = computed(
|
||||
() => Boolean(props.business.serviceClassesEnabled) && serviceClasses.value.length > 0,
|
||||
);
|
||||
|
||||
const classesEnabled = computed(() => Boolean(props.business.serviceClassesEnabled));
|
||||
|
||||
const perClass = computed(
|
||||
() => Boolean(props.business.serviceClassesEnabled) && Boolean(selectedClassId.value),
|
||||
() => classesEnabled.value && Boolean(selectedClassId.value),
|
||||
);
|
||||
|
||||
const selectedServices = computed(() =>
|
||||
@@ -347,7 +349,7 @@ const summaryItems = computed(() =>
|
||||
),
|
||||
durationLabel: serviceDurationLabel(
|
||||
service,
|
||||
perClass.value,
|
||||
classesEnabled.value,
|
||||
serviceClasses.value,
|
||||
selectedClassId.value || undefined,
|
||||
),
|
||||
@@ -440,7 +442,7 @@ const canProceed = computed(() => {
|
||||
function durationFor(service: BusinessService) {
|
||||
return serviceDurationLabel(
|
||||
service,
|
||||
perClass.value,
|
||||
classesEnabled.value,
|
||||
serviceClasses.value,
|
||||
selectedClassId.value || undefined,
|
||||
);
|
||||
@@ -449,7 +451,7 @@ function durationFor(service: BusinessService) {
|
||||
function priceFor(service: BusinessService) {
|
||||
return servicePriceLabel(
|
||||
service,
|
||||
perClass.value,
|
||||
classesEnabled.value,
|
||||
serviceClasses.value,
|
||||
selectedClassId.value || undefined,
|
||||
);
|
||||
|
||||
@@ -33,6 +33,8 @@ import { ApiError } from '@/api/client';
|
||||
import type { Business, BusinessCategory } from '@/api/types';
|
||||
import HomeBusinessList from '@/components/home/HomeBusinessList.vue';
|
||||
import HomeSearchAndFilter from '@/components/home/HomeSearchAndFilter.vue';
|
||||
import { useOwnedBusiness } from '@/composables/useOwnedBusiness';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -41,6 +43,9 @@ const props = withDefaults(
|
||||
{ listVariant: 'compact' },
|
||||
);
|
||||
|
||||
const auth = useAuthStore();
|
||||
const { ensureLoaded, isOwnBusiness } = useOwnedBusiness();
|
||||
|
||||
const categories = ref<BusinessCategory[]>([]);
|
||||
const businesses = ref<Business[]>([]);
|
||||
const selectedCategoryId = ref<string | null>(null);
|
||||
@@ -58,10 +63,16 @@ const showSearch = computed(() => businesses.value.length > 1);
|
||||
|
||||
const filteredBusinesses = computed(() => {
|
||||
const q = searchQuery.value.trim().toLowerCase();
|
||||
if (!q) {
|
||||
return businesses.value;
|
||||
let items = businesses.value;
|
||||
|
||||
if (auth.isBusinessOwner) {
|
||||
items = items.filter((business) => !isOwnBusiness(business.id));
|
||||
}
|
||||
return businesses.value.filter((b) => {
|
||||
|
||||
if (!q) {
|
||||
return items;
|
||||
}
|
||||
return items.filter((b) => {
|
||||
const haystack = [b.title, b.description, b.address]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
@@ -107,6 +118,10 @@ watch(selectedCategoryId, () => {
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (auth.isLoggedIn) {
|
||||
await auth.loadUser();
|
||||
await ensureLoaded();
|
||||
}
|
||||
await loadCategories();
|
||||
await loadBusinesses();
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
v-if="props.showBookButton"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-brand-600 px-6 py-3 text-[15px] font-semibold text-white shadow-sm transition hover:bg-brand-700"
|
||||
@click="emit('book')"
|
||||
@@ -99,6 +100,13 @@ import { useTenantStore } from '@/stores/tenant';
|
||||
import type { AppIconName } from '@/types/icons';
|
||||
import { businessCoverUrl } from '@/utils/media';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
showBookButton?: boolean;
|
||||
}>(),
|
||||
{ showBookButton: true },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
book: [];
|
||||
browse: [];
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<template>
|
||||
<RouterLink
|
||||
:to="to"
|
||||
class="flex min-w-0 flex-1 flex-col rounded-xl border bg-white px-4 py-5 text-center shadow-[0_2px_12px_rgba(15,23,42,0.06)] transition hover:-translate-y-0.5 hover:shadow-md dark:bg-surface"
|
||||
:class="
|
||||
featured
|
||||
? 'border-brand-600 ring-1 ring-brand-600/25'
|
||||
: 'border-slate-100 dark:border-border'
|
||||
"
|
||||
<component
|
||||
:is="linkEnabled ? RouterLink : 'div'"
|
||||
:to="linkEnabled ? to : undefined"
|
||||
class="flex min-w-0 flex-1 flex-col rounded-xl border border-slate-100 bg-white px-4 py-5 text-center shadow-[0_2px_12px_rgba(15,23,42,0.06)] transition dark:border-border dark:bg-surface hover:-translate-y-0.5 hover:border-brand-600 hover:shadow-md hover:ring-1 hover:ring-brand-600/25 focus-visible:border-brand-600 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-brand-600/25 active:border-brand-600 active:ring-1 active:ring-brand-600/25"
|
||||
>
|
||||
<div class="mb-3 flex justify-center text-slate-400 dark:text-muted">
|
||||
<AppIcon :name="icon" size="lg" />
|
||||
@@ -26,22 +22,26 @@
|
||||
<p v-if="durationLabel" class="mt-3 text-xs text-landing-muted dark:text-muted">
|
||||
مدت زمان: {{ durationLabel }}
|
||||
</p>
|
||||
</RouterLink>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router';
|
||||
import AppIcon from '@/components/ui/AppIcon.vue';
|
||||
import type { AppIconName } from '@/types/icons';
|
||||
|
||||
defineProps<{
|
||||
to:
|
||||
| string
|
||||
| { name: string; params?: Record<string, string>; hash?: string }
|
||||
| { path: string; hash?: string };
|
||||
title: string;
|
||||
description?: string | null;
|
||||
durationLabel?: string | null;
|
||||
icon: AppIconName;
|
||||
featured?: boolean;
|
||||
}>();
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
to:
|
||||
| string
|
||||
| { name: string; params?: Record<string, string>; hash?: string }
|
||||
| { path: string; hash?: string };
|
||||
title: string;
|
||||
description?: string | null;
|
||||
durationLabel?: string | null;
|
||||
icon: AppIconName;
|
||||
linkEnabled?: boolean;
|
||||
}>(),
|
||||
{ linkEnabled: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -29,14 +29,14 @@
|
||||
:class="gridClass"
|
||||
>
|
||||
<HomeServiceCard
|
||||
v-for="(item, index) in items"
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:to="serviceLink(item)"
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
:duration-label="item.durationLabel"
|
||||
:icon="item.icon"
|
||||
:featured="index === featuredIndex"
|
||||
:link-enabled="props.bookingEnabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,11 +50,15 @@ import HomeServiceCard from '@/components/home/HomeServiceCard.vue';
|
||||
import ServiceGridSkeleton from '@/components/ui/ServiceGridSkeleton.vue';
|
||||
import type { LandingServiceItem } from '@/types/landing';
|
||||
|
||||
const props = defineProps<{
|
||||
items: LandingServiceItem[];
|
||||
loading: boolean;
|
||||
error: string;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
items: LandingServiceItem[];
|
||||
loading: boolean;
|
||||
error: string;
|
||||
bookingEnabled?: boolean;
|
||||
}>(),
|
||||
{ bookingEnabled: true },
|
||||
);
|
||||
|
||||
const gridClass = computed(() => {
|
||||
const count = props.items.length;
|
||||
@@ -64,13 +68,8 @@ const gridClass = computed(() => {
|
||||
return 'lg:grid-cols-2';
|
||||
});
|
||||
|
||||
const featuredIndex = computed(() => {
|
||||
const len = props.items.length;
|
||||
if (!len) return 0;
|
||||
return Math.min(2, len - 1);
|
||||
});
|
||||
|
||||
function serviceLink(item: LandingServiceItem) {
|
||||
if (!props.bookingEnabled) return '#services';
|
||||
return {
|
||||
name: 'app-business-detail',
|
||||
params: { id: item.businessId },
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<template>
|
||||
<DesktopSiteLayout content-width="full">
|
||||
<HomeHeroSection @book="goToBooking" @browse="scrollToServices" />
|
||||
<HomeHeroSection
|
||||
:show-book-button="props.bookingEnabled"
|
||||
@book="goToBooking"
|
||||
@browse="scrollToServices"
|
||||
/>
|
||||
|
||||
<HomeServicesSection
|
||||
:items="services"
|
||||
:loading="loadingServices"
|
||||
:error="servicesError"
|
||||
:booking-enabled="props.bookingEnabled"
|
||||
/>
|
||||
|
||||
<HomeFeatureBar />
|
||||
@@ -22,11 +27,15 @@ import DesktopSiteLayout from '@/layouts/DesktopSiteLayout.vue';
|
||||
import { useTenantStore } from '@/stores/tenant';
|
||||
import type { LandingServiceItem } from '@/types/landing';
|
||||
|
||||
defineProps<{
|
||||
services: LandingServiceItem[];
|
||||
loadingServices: boolean;
|
||||
servicesError: string;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
services: LandingServiceItem[];
|
||||
loadingServices: boolean;
|
||||
servicesError: string;
|
||||
bookingEnabled?: boolean;
|
||||
}>(),
|
||||
{ bookingEnabled: true },
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
const tenant = useTenantStore();
|
||||
@@ -38,6 +47,11 @@ function scrollToServices() {
|
||||
}
|
||||
|
||||
function goToBooking() {
|
||||
if (!props.bookingEnabled) {
|
||||
void router.push({ name: 'app-owner' });
|
||||
return;
|
||||
}
|
||||
|
||||
const id = singleBusinessId.value;
|
||||
if (!id) {
|
||||
scrollToServices();
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
<section id="contact" class="scroll-mt-28">
|
||||
<h3 class="mb-4 text-sm font-bold text-landing-ink dark:text-text">تماس با ما</h3>
|
||||
<ul class="space-y-3 text-sm text-landing-muted dark:text-muted">
|
||||
<li v-if="address" class="flex items-start gap-2">
|
||||
<AppIcon name="map-pin" size="sm" class="mt-0.5 shrink-0 text-brand-600" />
|
||||
{{ address }}
|
||||
</li>
|
||||
<li v-if="contactPhone" class="flex items-center gap-2" dir="ltr">
|
||||
<AppIcon name="phone" size="sm" class="shrink-0 text-brand-600" />
|
||||
{{ contactPhone }}
|
||||
@@ -42,10 +46,6 @@
|
||||
<AppIcon name="phone" size="sm" class="shrink-0 text-brand-600" />
|
||||
{{ contactMobile }}
|
||||
</li>
|
||||
<li v-if="address" class="flex items-start gap-2">
|
||||
<AppIcon name="map-pin" size="sm" class="mt-0.5 shrink-0 text-brand-600" />
|
||||
{{ address }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user