fix: AppButton import in owner panel and service-pricing types
This commit is contained in:
@@ -27,9 +27,19 @@
|
||||
مشتری فقط در بازههای تعریفشده میتواند رزرو کند. ابتدا حالت کلی را انتخاب کنید.
|
||||
</p>
|
||||
|
||||
<p v-if="error" class="rounded-xl bg-red-500/10 px-3 py-2 text-sm text-red-600">
|
||||
{{ error }}
|
||||
</p>
|
||||
<div
|
||||
v-if="error"
|
||||
class="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-600"
|
||||
>
|
||||
<p class="min-w-0 flex-1">{{ error }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-lg border border-red-500/30 px-3 py-1 text-xs font-medium text-red-700 transition hover:bg-red-500/10"
|
||||
@click="load"
|
||||
>
|
||||
تلاش مجدد
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
v-if="message"
|
||||
class="rounded-xl bg-brand-500/10 px-3 py-2 text-sm text-brand-700 dark:text-brand-500"
|
||||
@@ -124,26 +134,33 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-border bg-surface-2 p-4">
|
||||
<label class="flex items-center justify-between gap-3">
|
||||
<span class="text-sm text-text">پذیرش آنلاین این روز</span>
|
||||
<div class="overflow-hidden rounded-2xl border border-border bg-surface-2">
|
||||
<label
|
||||
class="flex cursor-pointer items-center justify-between gap-3 border-b border-border px-4 py-3 transition hover:bg-surface-3/60"
|
||||
>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-text">پذیرش آنلاین این روز</span>
|
||||
<p class="mt-0.5 text-xs text-muted">
|
||||
{{ dayActive[activeDay] ? 'مشتری میتواند در این روز رزرو کند' : 'این روز از رزرو آنلاین خارج است' }}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-5 w-5 rounded border-border text-brand-600 focus:ring-brand-500/30"
|
||||
class="h-5 w-5 shrink-0 rounded border-border text-brand-600 focus:ring-brand-500/30"
|
||||
:checked="dayActive[activeDay]"
|
||||
@change="toggleDayActive(activeDay)"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div v-if="dayActive[activeDay]" class="mt-4">
|
||||
<div v-if="dayActive[activeDay]" class="p-4">
|
||||
<TimeRangeList
|
||||
:ranges="slotsByDay[activeDay] ?? []"
|
||||
@add="(start, end) => addCustomRange(activeDay, start, end)"
|
||||
@remove="(i) => removeSlot(activeDay, i)"
|
||||
/>
|
||||
</div>
|
||||
<p v-else class="mt-3 text-center text-sm text-muted">
|
||||
این روز غیرفعال است — رزرو آنلاین نمایش داده نمیشود.
|
||||
<p v-else class="px-4 py-8 text-center text-sm text-muted">
|
||||
برای فعالسازی، گزینه بالا را روشن کنید.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,92 +1,74 @@
|
||||
<template>
|
||||
<div ref="rootRef" class="relative min-w-0 flex-1">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-center rounded-xl border border-border bg-surface-2 px-2 py-2.5 transition hover:border-brand-500/40"
|
||||
:aria-expanded="open"
|
||||
@click.stop="open = !open"
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="sr-only">ساعت</label>
|
||||
<select
|
||||
v-model="hour"
|
||||
class="min-w-0 flex-1 rounded-xl border border-border bg-surface-2 px-2 py-3 text-center text-lg font-semibold tabular-nums text-text outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20"
|
||||
dir="ltr"
|
||||
>
|
||||
<span class="text-lg font-semibold tabular-nums text-text" dir="ltr">
|
||||
{{ hour }}:{{ minute }}
|
||||
</span>
|
||||
</button>
|
||||
<option v-for="h in hours" :key="h" :value="h">{{ h }}</option>
|
||||
</select>
|
||||
|
||||
<div
|
||||
v-if="open"
|
||||
class="absolute inset-x-0 top-full z-50 mt-1 overflow-hidden rounded-xl border border-border bg-surface shadow-lg"
|
||||
@click.stop
|
||||
<span class="shrink-0 text-xl font-semibold text-muted" aria-hidden="true">:</span>
|
||||
|
||||
<label class="sr-only">دقیقه</label>
|
||||
<select
|
||||
v-model="minute"
|
||||
class="min-w-0 flex-1 rounded-xl border border-border bg-surface-2 px-2 py-3 text-center text-lg font-semibold tabular-nums text-text outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20"
|
||||
dir="ltr"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="min-w-0 flex-1 border-e border-border">
|
||||
<p class="border-b border-border py-1 text-center text-xs text-muted">ساعت</p>
|
||||
<ul class="max-h-36 overflow-y-auto overscroll-contain">
|
||||
<li v-for="h in hours" :key="h">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-2 py-2 text-center text-sm tabular-nums transition"
|
||||
:class="
|
||||
h === hour
|
||||
? 'bg-brand-500/15 font-semibold text-brand-700 dark:text-brand-500'
|
||||
: 'text-text hover:bg-surface-3'
|
||||
"
|
||||
@click="pickHour(h)"
|
||||
>
|
||||
{{ h }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="border-b border-border py-1 text-center text-xs text-muted">دقیقه</p>
|
||||
<ul class="max-h-36 overflow-y-auto overscroll-contain">
|
||||
<li v-for="m in minutes" :key="m">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-2 py-2 text-center text-sm tabular-nums transition"
|
||||
:class="
|
||||
m === minute
|
||||
? 'bg-brand-500/15 font-semibold text-brand-700 dark:text-brand-500'
|
||||
: 'text-text hover:bg-surface-3'
|
||||
"
|
||||
@click="pickMinute(m)"
|
||||
>
|
||||
{{ m }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<option v-for="m in minuteOptions" :key="m" :value="m">{{ m }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
const hours = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
|
||||
const minutes = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0'));
|
||||
export default defineComponent({
|
||||
name: 'HmTimePickers',
|
||||
});
|
||||
</script>
|
||||
|
||||
const hour = defineModel<string>('hour', { required: true });
|
||||
const minute = defineModel<string>('minute', { required: true });
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const open = ref(false);
|
||||
const rootRef = ref<HTMLElement | null>(null);
|
||||
const HOUR_OPTIONS = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
|
||||
const MINUTE_STEPS = Array.from({ length: 12 }, (_, i) => String(i * 5).padStart(2, '0'));
|
||||
|
||||
function pickHour(v: string) {
|
||||
hour.value = v;
|
||||
const model = defineModel<string>({ required: true });
|
||||
|
||||
const hours = HOUR_OPTIONS;
|
||||
|
||||
function parseHm(value: string): { hour: string; minute: string } {
|
||||
const [hour = '09', minute = '00'] = value.split(':');
|
||||
return {
|
||||
hour: hour.padStart(2, '0'),
|
||||
minute: minute.padStart(2, '0'),
|
||||
};
|
||||
}
|
||||
|
||||
const hour = computed({
|
||||
get: () => parseHm(model.value).hour,
|
||||
set: (value: string) => {
|
||||
const { minute: currentMinute } = parseHm(model.value);
|
||||
model.value = `${value.padStart(2, '0')}:${currentMinute}`;
|
||||
},
|
||||
});
|
||||
|
||||
function pickMinute(v: string) {
|
||||
minute.value = v;
|
||||
}
|
||||
const minute = computed({
|
||||
get: () => parseHm(model.value).minute,
|
||||
set: (value: string) => {
|
||||
const { hour: currentHour } = parseHm(model.value);
|
||||
model.value = `${currentHour}:${value.padStart(2, '0')}`;
|
||||
},
|
||||
});
|
||||
|
||||
function onDocumentClick(e: MouseEvent) {
|
||||
if (!open.value || !rootRef.value) return;
|
||||
if (!rootRef.value.contains(e.target as Node)) {
|
||||
open.value = false;
|
||||
const minuteOptions = computed(() => {
|
||||
const current = minute.value;
|
||||
if (MINUTE_STEPS.includes(current)) {
|
||||
return MINUTE_STEPS;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('click', onDocumentClick));
|
||||
onUnmounted(() => document.removeEventListener('click', onDocumentClick));
|
||||
return [...MINUTE_STEPS, current].sort((a, b) => Number(a) - Number(b));
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -30,32 +30,103 @@
|
||||
هنوز بازهای ثبت نشده
|
||||
</p>
|
||||
|
||||
<div class="rounded-2xl border border-dashed border-brand-500/40 bg-brand-500/5 p-3">
|
||||
<p class="mb-2 text-xs font-medium text-muted">بازه جدید</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="shrink-0 text-xs font-medium text-muted">از</span>
|
||||
<HmTimePickers v-model:hour="startH" v-model:minute="startM" />
|
||||
<span class="shrink-0 text-xs font-medium text-muted">تا</span>
|
||||
<HmTimePickers v-model:hour="endH" v-model:minute="endM" />
|
||||
<div class="overflow-hidden rounded-2xl border border-border bg-surface-3">
|
||||
<div class="border-b border-border bg-brand-500/5 px-4 py-3">
|
||||
<p class="text-sm font-semibold text-text">افزودن بازه جدید</p>
|
||||
<p class="mt-0.5 text-xs text-muted">ساعت شروع و پایان را جداگانه انتخاب کنید</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 p-4">
|
||||
<div
|
||||
class="flex items-center justify-center gap-4 rounded-2xl border border-brand-500/20 bg-surface-2 px-4 py-5"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="text-center">
|
||||
<p class="mb-1 text-xs font-medium text-muted">از</p>
|
||||
<p class="text-3xl font-bold tabular-nums tracking-tight text-text" dir="ltr">
|
||||
{{ startTime }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-brand-500/10 text-brand-700 dark:text-brand-500"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<AppIcon name="arrow-right" size="sm" class="rotate-180" />
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mb-1 text-xs font-medium text-muted">تا</p>
|
||||
<p class="text-3xl font-bold tabular-nums tracking-tight text-text" dir="ltr">
|
||||
{{ endTime }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-xs font-medium text-muted">انتخاب سریع</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="preset in presets"
|
||||
:key="preset.label"
|
||||
type="button"
|
||||
class="rounded-full border px-3 py-1.5 text-xs font-medium transition"
|
||||
:class="
|
||||
isPresetActive(preset)
|
||||
? 'border-brand-500 bg-brand-500/15 text-brand-700 dark:text-brand-500'
|
||||
: 'border-border bg-surface-2 text-text hover:border-brand-500/40'
|
||||
"
|
||||
@click="applyPreset(preset)"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="rounded-xl border border-border bg-surface-2 p-3">
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<span class="flex h-6 w-6 items-center justify-center rounded-full bg-brand-500/15 text-xs font-bold text-brand-700 dark:text-brand-500">
|
||||
۱
|
||||
</span>
|
||||
<p class="text-sm font-medium text-text">ساعت شروع</p>
|
||||
</div>
|
||||
<HmTimePickers v-model="startTime" />
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-border bg-surface-2 p-3">
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<span class="flex h-6 w-6 items-center justify-center rounded-full bg-brand-500/15 text-xs font-bold text-brand-700 dark:text-brand-500">
|
||||
۲
|
||||
</span>
|
||||
<p class="text-sm font-medium text-text">ساعت پایان</p>
|
||||
</div>
|
||||
<HmTimePickers v-model="endTime" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="localError" class="rounded-xl bg-red-500/10 px-3 py-2 text-xs text-red-600">
|
||||
{{ localError }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-center gap-2 rounded-xl bg-brand-600 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-brand-700 active:scale-[0.99]"
|
||||
@click="onAdd"
|
||||
>
|
||||
<AppIcon name="plus" size="sm" />
|
||||
افزودن این بازه
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="localError" class="mt-2 text-xs text-red-600">{{ localError }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 flex w-full items-center justify-center gap-2 rounded-xl border border-brand-500/30 bg-surface-2 py-2.5 text-sm font-medium text-brand-700 transition hover:bg-brand-500/10 dark:text-brand-500"
|
||||
@click="onAdd"
|
||||
>
|
||||
<AppIcon name="plus" size="sm" />
|
||||
افزودن بازه
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import AppIcon from '@/components/ui/AppIcon.vue';
|
||||
import HmTimePickers from '@/components/owner/HmTimePickers.vue';
|
||||
import { isHmRangeValid, normalizeHmTime } from '@/utils/format';
|
||||
import AppIcon from '../ui/app-icon';
|
||||
import HmTimePickers from './hm-time-pickers';
|
||||
import { isHmRangeValid, normalizeHmTime } from '../../utils/format';
|
||||
|
||||
defineProps<{
|
||||
ranges: { startTime: string; endTime: string }[];
|
||||
@@ -66,20 +137,31 @@ const emit = defineEmits<{
|
||||
remove: [index: number];
|
||||
}>();
|
||||
|
||||
const startH = ref('09');
|
||||
const startM = ref('00');
|
||||
const endH = ref('13');
|
||||
const endM = ref('00');
|
||||
const presets = [
|
||||
{ label: 'صبح · ۰۹ تا ۱۳', start: '09:00', end: '13:00' },
|
||||
{ label: 'ظهر · ۱۳ تا ۱۷', start: '13:00', end: '17:00' },
|
||||
{ label: 'عصر · ۱۷ تا ۲۱', start: '17:00', end: '21:00' },
|
||||
{ label: 'تمامروز · ۰۹ تا ۲۱', start: '09:00', end: '21:00' },
|
||||
] as const;
|
||||
|
||||
const startTime = ref('09:00');
|
||||
const endTime = ref('13:00');
|
||||
const localError = ref('');
|
||||
|
||||
function toHm(h: string, m: string): string {
|
||||
return `${h.padStart(2, '0')}:${m.padStart(2, '0')}`;
|
||||
function isPresetActive(preset: (typeof presets)[number]): boolean {
|
||||
return startTime.value === preset.start && endTime.value === preset.end;
|
||||
}
|
||||
|
||||
function applyPreset(preset: (typeof presets)[number]) {
|
||||
startTime.value = preset.start;
|
||||
endTime.value = preset.end;
|
||||
localError.value = '';
|
||||
}
|
||||
|
||||
function onAdd() {
|
||||
localError.value = '';
|
||||
const start = normalizeHmTime(toHm(startH.value, startM.value));
|
||||
const end = normalizeHmTime(toHm(endH.value, endM.value));
|
||||
const start = normalizeHmTime(startTime.value);
|
||||
const end = normalizeHmTime(endTime.value);
|
||||
if (!start || !end) {
|
||||
localError.value = 'فرمت ساعت نامعتبر است.';
|
||||
return;
|
||||
@@ -89,5 +171,7 @@ function onAdd() {
|
||||
return;
|
||||
}
|
||||
emit('add', start, end);
|
||||
startTime.value = '09:00';
|
||||
endTime.value = '13:00';
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { DefineComponent } from 'vue';
|
||||
import HmTimePickers from './HmTimePickers.vue';
|
||||
|
||||
export default HmTimePickers as DefineComponent<{
|
||||
modelValue: string;
|
||||
}>;
|
||||
@@ -7,6 +7,14 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AppIcon',
|
||||
});
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { DefineComponent } from 'vue';
|
||||
import AppIcon from './AppIcon.vue';
|
||||
import type { AppIconName } from '../../types/icons';
|
||||
|
||||
export default AppIcon as DefineComponent<{
|
||||
name: AppIconName;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
strokeWidth?: number;
|
||||
}>;
|
||||
Reference in New Issue
Block a user