fix: AppButton import in owner panel and service-pricing types
This commit is contained in:
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"],
|
||||
"unwantedRecommendations": ["octref.vetur"]
|
||||
}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"vetur.validation.script": false,
|
||||
"vetur.validation.template": false,
|
||||
"vetur.validation.style": false,
|
||||
"vetur.useWorkspaceDependencies": true,
|
||||
"vetur.ignoreProjectWarning": true,
|
||||
"vetur.experimental.templateInterpolationService": false,
|
||||
"vue.server.hybridMode": false
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"target": "ES2022",
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["shims-vue.d.ts", "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"exclude": ["dist", "dist-admin", "node_modules"]
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<object, object, unknown>;
|
||||
export default component;
|
||||
}
|
||||
+43
-17
@@ -20,6 +20,36 @@ export function getApiToken(): string | null {
|
||||
return localStorage.getItem('montime_access_token');
|
||||
}
|
||||
|
||||
function resolveApiErrorMessage(
|
||||
status: number,
|
||||
statusText: string,
|
||||
body: { message?: string | string[] } | null,
|
||||
): string {
|
||||
if (Array.isArray(body?.message)) {
|
||||
const lines = body.message.map(String);
|
||||
const timeErrors = lines.filter(
|
||||
(l) => /startTime|endTime/.test(l) && /regular expression|HH:mm/i.test(l),
|
||||
);
|
||||
if (timeErrors.length > 0) {
|
||||
return 'فرمت ساعت نامعتبر است. لطفاً دوباره بازه را اضافه و ذخیره کنید.';
|
||||
}
|
||||
return lines.join(' · ');
|
||||
}
|
||||
|
||||
const raw = typeof body?.message === 'string' ? body.message : statusText;
|
||||
const genericServer =
|
||||
status >= 500 &&
|
||||
(!raw ||
|
||||
raw === 'Internal Server Error' ||
|
||||
/^5\d\d/.test(String(raw)));
|
||||
|
||||
if (genericServer) {
|
||||
return 'ارتباط با سرور برقرار نشد. اتصال اینترنت را بررسی کنید یا چند لحظه بعد دوباره تلاش کنید.';
|
||||
}
|
||||
|
||||
return raw || 'خطا در ارتباط با سرور';
|
||||
}
|
||||
|
||||
export async function api<T>(path: string, options: ApiOptions = {}): Promise<T> {
|
||||
const token = options.token ?? getApiToken() ?? undefined;
|
||||
const headers: Record<string, string> = {
|
||||
@@ -33,10 +63,18 @@ export async function api<T>(path: string, options: ApiOptions = {}): Promise<T>
|
||||
|
||||
const { token: _token, ...fetchOptions } = options;
|
||||
|
||||
const res = await fetch(`${baseUrl}${path}`, {
|
||||
...fetchOptions,
|
||||
headers,
|
||||
});
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`${baseUrl}${path}`, {
|
||||
...fetchOptions,
|
||||
headers,
|
||||
});
|
||||
} catch {
|
||||
throw new ApiError(
|
||||
0,
|
||||
'ارتباط با سرور برقرار نشد. اتصال اینترنت را بررسی کنید یا چند لحظه بعد دوباره تلاش کنید.',
|
||||
);
|
||||
}
|
||||
|
||||
logApiServerTiming(path, res);
|
||||
|
||||
@@ -44,19 +82,7 @@ export async function api<T>(path: string, options: ApiOptions = {}): Promise<T>
|
||||
const body = (await res.json().catch(() => null)) as
|
||||
| { message?: string | string[] }
|
||||
| null;
|
||||
let msg: string;
|
||||
if (Array.isArray(body?.message)) {
|
||||
const lines = body.message.map(String);
|
||||
const timeErrors = lines.filter((l) =>
|
||||
/startTime|endTime/.test(l) && /regular expression|HH:mm/i.test(l),
|
||||
);
|
||||
msg =
|
||||
timeErrors.length > 0
|
||||
? 'فرمت ساعت نامعتبر است. لطفاً دوباره بازه را اضافه و ذخیره کنید.'
|
||||
: lines.join(' · ');
|
||||
} else {
|
||||
msg = body?.message ?? res.statusText;
|
||||
}
|
||||
const msg = resolveApiErrorMessage(res.status, res.statusText, body);
|
||||
throw new ApiError(res.status, msg);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}>;
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<object, object, unknown>;
|
||||
export default component;
|
||||
}
|
||||
@@ -90,14 +90,14 @@ export function serviceDurationLabel(
|
||||
const classPrice = getClassPriceForService(service, classId);
|
||||
return formatDuration(
|
||||
classPrice?.durationMinutes ?? service.durationMinutes,
|
||||
service.serviceTemplate?.defaultDurationMinutes,
|
||||
service.serviceTemplate?.defaultDurationMinutes ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
if (!usesServiceClasses(classesEnabled, classes)) {
|
||||
return formatDuration(
|
||||
service.durationMinutes,
|
||||
service.serviceTemplate?.defaultDurationMinutes,
|
||||
service.serviceTemplate?.defaultDurationMinutes ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export function serviceDurationLabel(
|
||||
if (!durations.length) {
|
||||
return formatDuration(
|
||||
service.durationMinutes,
|
||||
service.serviceTemplate?.defaultDurationMinutes,
|
||||
service.serviceTemplate?.defaultDurationMinutes ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -262,33 +262,17 @@
|
||||
class="flex flex-wrap items-center gap-2"
|
||||
>
|
||||
<span class="font-medium text-text">{{ cls.title }}:</span>
|
||||
<span>
|
||||
{{
|
||||
getClassPriceForService(svc, cls.id)
|
||||
? `${formatPriceToman(getClassPriceForService(svc, cls.id)!.price)} تومان`
|
||||
: '—'
|
||||
}}
|
||||
</span>
|
||||
<span v-if="getClassPriceForService(svc, cls.id)">·</span>
|
||||
<span v-if="getClassPriceForService(svc, cls.id)">
|
||||
{{
|
||||
formatDuration(
|
||||
getClassPriceForService(svc, cls.id)!.durationMinutes,
|
||||
svc.durationMinutes ?? svc.serviceTemplate?.defaultDurationMinutes,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span>{{ servicePriceLabel(svc, true, serviceClasses, cls.id) }}</span>
|
||||
<template v-if="getClassPriceForService(svc, cls.id)">
|
||||
<span>·</span>
|
||||
<span>{{ serviceDurationLabel(svc, true, serviceClasses, cls.id) }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="flex flex-wrap items-center gap-2">
|
||||
<span>{{ formatPriceToman(svc.price) }} تومان</span>
|
||||
<span>·</span>
|
||||
<span>{{
|
||||
formatDuration(
|
||||
svc.durationMinutes,
|
||||
svc.serviceTemplate?.defaultDurationMinutes,
|
||||
)
|
||||
}}</span>
|
||||
<span>{{ serviceDurationLabel(svc, false, serviceClasses) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -463,13 +447,13 @@ import BusinessAvailabilityEditor from '@/components/owner/BusinessAvailabilityE
|
||||
import ServiceClassManager from '@/components/owner/ServiceClassManager.vue';
|
||||
import FormPageSkeleton from '@/components/ui/FormPageSkeleton.vue';
|
||||
import ListCardSkeleton from '@/components/ui/ListCardSkeleton.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppIcon from '@/components/ui/AppIcon.vue';
|
||||
import AppIconButton from '@/components/ui/AppIconButton.vue';
|
||||
import AppInput from '@/components/ui/AppInput.vue';
|
||||
import PriceInput from '@/components/ui/PriceInput.vue';
|
||||
import WebAppLayout from '@/layouts/WebAppLayout.vue';
|
||||
import {
|
||||
formatDuration,
|
||||
formatPriceInputDisplay,
|
||||
formatPriceToman,
|
||||
parseOptionalMinutes,
|
||||
@@ -478,6 +462,8 @@ import {
|
||||
import {
|
||||
activeServiceClasses,
|
||||
getClassPriceForService,
|
||||
serviceDurationLabel,
|
||||
servicePriceLabel,
|
||||
} from '@/utils/service-pricing';
|
||||
|
||||
const business = ref<Business | null>(null);
|
||||
|
||||
+2
-1
@@ -15,10 +15,11 @@
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"include": ["shims-vue.d.ts", "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"exclude": ["dist", "dist-admin", "node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/** @type {import('vls').VeturConfiguration} */
|
||||
module.exports = {
|
||||
projects: [
|
||||
{
|
||||
root: '.',
|
||||
jsconfig: './jsconfig.json',
|
||||
},
|
||||
],
|
||||
};
|
||||
+4
-2
@@ -17,13 +17,15 @@ export default defineConfig({
|
||||
'/api': {
|
||||
target: API_TARGET,
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
secure: false,
|
||||
timeout: 30_000,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
'/uploads': {
|
||||
target: API_TARGET,
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
secure: false,
|
||||
timeout: 30_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user