edit business owner

This commit is contained in:
2026-06-13 10:19:31 +03:30
parent 5eec309b54
commit d1a471199b
17 changed files with 1268 additions and 220 deletions
@@ -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,
);
+18 -3
View File
@@ -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();
});
+8
View File
@@ -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: [];
+20 -20
View File
@@ -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>
+12 -13
View File
@@ -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 },
+20 -6
View File
@@ -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();
+4 -4
View File
@@ -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>