2026-05-18 20:18:28 +03:30
|
|
|
|
<template>
|
|
|
|
|
|
<WebAppLayout>
|
2026-05-25 17:49:48 +03:30
|
|
|
|
<h1 class="mb-1 text-xl font-bold text-text">
|
|
|
|
|
|
{{ auth.isBusinessOwner ? 'رزروهای کسبوکار' : 'رزروهای من' }}
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
<p class="mb-4 text-sm text-muted">
|
|
|
|
|
|
{{
|
|
|
|
|
|
auth.isBusinessOwner
|
|
|
|
|
|
? 'درخواستهای مشتریان برای خدمات شما'
|
|
|
|
|
|
: 'وضعیت رزروهای ثبتشده'
|
|
|
|
|
|
}}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
|
|
<p
|
|
|
|
|
|
v-if="error"
|
|
|
|
|
|
class="mb-4 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>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="loading" class="py-8 text-center text-sm text-muted">در حال بارگذاری…</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else-if="!bookings.length"
|
|
|
|
|
|
class="rounded-2xl border border-dashed border-border bg-surface-2 p-6 text-center text-sm text-muted"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{
|
|
|
|
|
|
auth.isBusinessOwner
|
|
|
|
|
|
? 'هنوز رزروی برای کسبوکار شما ثبت نشده است.'
|
|
|
|
|
|
: 'هنوز رزروی ندارید. از صفحه کسبوکارها یک خدمت را رزرو کنید.'
|
|
|
|
|
|
}}
|
|
|
|
|
|
<RouterLink
|
|
|
|
|
|
v-if="auth.isCustomer"
|
|
|
|
|
|
to="/app"
|
|
|
|
|
|
class="mt-3 inline-block text-brand-600 dark:text-brand-500"
|
|
|
|
|
|
>
|
|
|
|
|
|
مشاهده کسبوکارها
|
|
|
|
|
|
</RouterLink>
|
2026-05-18 20:18:28 +03:30
|
|
|
|
</div>
|
2026-05-25 17:49:48 +03:30
|
|
|
|
|
|
|
|
|
|
<ul v-else class="space-y-3">
|
|
|
|
|
|
<li
|
|
|
|
|
|
v-for="b in bookings"
|
|
|
|
|
|
:key="b.id"
|
|
|
|
|
|
class="rounded-2xl border border-border bg-surface-2 p-4"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-start justify-between gap-2">
|
|
|
|
|
|
<div class="min-w-0 flex-1">
|
|
|
|
|
|
<p class="font-semibold text-text">
|
|
|
|
|
|
{{ b.business?.title ?? 'کسبوکار' }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p class="mt-0.5 text-sm text-muted">
|
|
|
|
|
|
{{ serviceTitle(b) }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</ul>
|
2026-05-18 20:18:28 +03:30
|
|
|
|
</WebAppLayout>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-05-25 17:49:48 +03:30
|
|
|
|
import { onMounted, ref } from 'vue';
|
|
|
|
|
|
import { cancelBooking, confirmBooking, fetchBookings } from '@/api/bookings';
|
|
|
|
|
|
import { ApiError } from '@/api/client';
|
|
|
|
|
|
import type { Booking, BookingStatus } from '@/api/types';
|
|
|
|
|
|
import AppButton from '@/components/ui/AppButton.vue';
|
2026-05-18 20:18:28 +03:30
|
|
|
|
import WebAppLayout from '@/layouts/WebAppLayout.vue';
|
2026-05-25 17:49:48 +03:30
|
|
|
|
import { useAuthStore } from '@/stores/auth';
|
|
|
|
|
|
import { bookingStatusLabel, formatDate } from '@/utils/format';
|
|
|
|
|
|
|
|
|
|
|
|
const auth = useAuthStore();
|
|
|
|
|
|
const bookings = ref<Booking[]>([]);
|
|
|
|
|
|
const loading = ref(true);
|
|
|
|
|
|
const error = ref('');
|
|
|
|
|
|
const actingId = ref<string | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
function serviceTitle(b: Booking): string {
|
|
|
|
|
|
return b.businessService?.serviceTemplate?.title ?? 'خدمت';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function statusClass(status: BookingStatus): string {
|
|
|
|
|
|
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';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function canCancel(b: Booking): boolean {
|
|
|
|
|
|
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() {
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
error.value = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
bookings.value = await fetchBookings();
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
error.value = e instanceof ApiError ? e.message : 'بارگذاری رزروها ناموفق بود.';
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function onConfirm(b: Booking) {
|
|
|
|
|
|
actingId.value = b.id;
|
|
|
|
|
|
error.value = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
await confirmBooking(b.id);
|
|
|
|
|
|
await load();
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
error.value = e instanceof ApiError ? e.message : 'تأیید رزرو ناموفق بود.';
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actingId.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function onCancel(b: Booking) {
|
|
|
|
|
|
if (!confirm('این رزرو لغو شود؟')) return;
|
|
|
|
|
|
actingId.value = b.id;
|
|
|
|
|
|
error.value = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
await cancelBooking(b.id);
|
|
|
|
|
|
await load();
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
error.value = e instanceof ApiError ? e.message : 'لغو رزرو ناموفق بود.';
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actingId.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(load);
|
2026-05-18 20:18:28 +03:30
|
|
|
|
</script>
|