diff --git a/src/api/business-service-requests.ts b/src/api/business-service-requests.ts new file mode 100644 index 0000000..f75fe25 --- /dev/null +++ b/src/api/business-service-requests.ts @@ -0,0 +1,20 @@ +import { api } from './client'; +import type { BusinessServiceRequest, BusinessServiceRequestStatus } from './types'; + +export function fetchServiceRequests(status?: BusinessServiceRequestStatus) { + const query = status ? `?status=${status}` : ''; + return api(`/business-service-requests${query}`); +} + +export function approveServiceRequest(id: string) { + return api(`/business-service-requests/${id}/approve`, { + method: 'POST', + }); +} + +export function rejectServiceRequest(id: string, adminNote?: string) { + return api(`/business-service-requests/${id}/reject`, { + method: 'POST', + body: JSON.stringify({ adminNote: adminNote || undefined }), + }); +} diff --git a/src/api/types.ts b/src/api/types.ts index e532299..e5c6c72 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -144,6 +144,25 @@ export type BusinessRegistrationRequest = { applicant?: User; }; +export type BusinessServiceRequestStatus = 'pending' | 'approved' | 'rejected'; + +export type BusinessServiceRequest = { + id: string; + businessId: string; + requestedById: string; + status: BusinessServiceRequestStatus; + title: string; + description: string | null; + price: string; + durationMinutes: number | null; + adminNote: string | null; + serviceTemplateId: string | null; + businessServiceId: string | null; + createdAt: string; + business?: Business; + requestedBy?: User; +}; + export type Booking = { id: string; customerId: string; diff --git a/src/components/admin/BusinessServicesSection.vue b/src/components/admin/BusinessServicesSection.vue index da01ec9..2bd8a40 100644 --- a/src/components/admin/BusinessServicesSection.vue +++ b/src/components/admin/BusinessServicesSection.vue @@ -20,7 +20,7 @@

{{ svc.serviceTemplate?.title ?? 'خدمت' }}

- {{ formatPrice(svc.price) }} تومان + {{ formatPriceToman(svc.price) }} تومان · {{ svc.durationMinutes }} دقیقه

@@ -45,13 +45,10 @@ label="نوع خدمت" :options="templateOptions" /> - @@ -79,8 +76,9 @@ import { fetchServiceTemplates } from '@/api/service-templates'; import type { ServiceTemplate } from '@/api/types'; import { ApiError } from '@/api/client'; import AppButton from '@/components/ui/AppButton.vue'; -import AppInput from '@/components/ui/AppInput.vue'; import AppSelect from '@/components/ui/AppSelect.vue'; +import PriceInput from '@/components/ui/PriceInput.vue'; +import { formatPriceToman, parsePriceInput } from '@/utils/price'; const props = defineProps<{ businessId: string; @@ -104,12 +102,6 @@ const templateOptions = computed(() => availableTemplates.value.map((t) => ({ value: t.id, label: t.title })), ); -function formatPrice(price: string): string { - const n = Number.parseFloat(price); - if (!Number.isFinite(n)) return price; - return new Intl.NumberFormat('fa-IR').format(Math.round(n)); -} - async function load() { loading.value = true; error.value = ''; @@ -130,17 +122,8 @@ async function load() { } } -function parsePrice(value: string | number): number | null { - if (value === '' || value == null) return null; - if (typeof value === 'number') { - return Number.isFinite(value) && value >= 0 ? value : null; - } - const n = Number.parseFloat(String(value).replace(/,/g, '').trim()); - return Number.isFinite(n) && n >= 0 ? n : null; -} - async function onAdd() { - const price = parsePrice(form.value.price); + const price = parsePriceInput(form.value.price); if (!form.value.serviceTemplateId || price === null) { error.value = 'نوع خدمت و قیمت معتبر را وارد کنید.'; return; diff --git a/src/components/ui/PriceInput.vue b/src/components/ui/PriceInput.vue new file mode 100644 index 0000000..8e39a03 --- /dev/null +++ b/src/components/ui/PriceInput.vue @@ -0,0 +1,61 @@ + + + diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index 01becca..f3ff8e1 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -69,7 +69,8 @@ const items: AdminNavItem[] = [ { to: '/', name: 'dashboard', label: 'داشبورد', icon: 'layout-dashboard' }, { to: '/categories', name: 'categories', label: 'دسته', icon: 'folder' }, { to: '/businesses', name: 'businesses', label: 'کسب‌وکار', icon: 'store' }, - { to: '/business-requests', name: 'business-requests', label: 'درخواست', icon: 'clipboard-list' }, + { to: '/business-requests', name: 'business-requests', label: 'ثبت کسب', icon: 'clipboard-list' }, + { to: '/service-requests', name: 'service-requests', label: 'خدمت', icon: 'plus' }, { to: '/bookings', name: 'bookings', label: 'رزرو', icon: 'calendar' }, { to: '/users', name: 'users', label: 'کاربران', icon: 'users' }, ]; diff --git a/src/router/index.ts b/src/router/index.ts index ca04c79..b3d4c4a 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -52,6 +52,12 @@ const router = createRouter({ component: () => import('@/views/BusinessRequestsView.vue'), meta: { requiresAuth: true, requiresAdmin: true }, }, + { + path: '/service-requests', + name: 'service-requests', + component: () => import('@/views/ServiceRequestsView.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + }, { path: '/bookings', name: 'bookings', diff --git a/src/utils/price.ts b/src/utils/price.ts new file mode 100644 index 0000000..1ad432d --- /dev/null +++ b/src/utils/price.ts @@ -0,0 +1,46 @@ +const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹'; +const ARABIC_DIGITS = '٠١٢٣٤٥٦٧٨٩'; + +function toAsciiDigits(value: string): string { + return value.replace(/./g, (ch) => { + const pi = PERSIAN_DIGITS.indexOf(ch); + if (pi >= 0) return String(pi); + const ai = ARABIC_DIGITS.indexOf(ch); + if (ai >= 0) return String(ai); + return ch; + }); +} + +export function digitsOnlyPrice(value: string | number | null | undefined): string { + if (value == null || value === '') return ''; + if (typeof value === 'number') { + return Number.isFinite(value) && value >= 0 ? String(Math.trunc(value)) : ''; + } + return toAsciiDigits(String(value)).replace(/[^\d]/g, ''); +} + +export function formatPriceInputDisplay( + value: string | number | null | undefined, +): string { + const digits = digitsOnlyPrice(value); + if (!digits) return ''; + const n = Number.parseInt(digits, 10); + if (!Number.isFinite(n)) return ''; + return new Intl.NumberFormat('fa-IR').format(n); +} + +export function parsePriceInput( + value: string | number | null | undefined, +): number | null { + const digits = digitsOnlyPrice(value); + if (!digits) return null; + const n = Number.parseInt(digits, 10); + if (!Number.isFinite(n) || n < 0) return null; + return n; +} + +export function formatPriceToman(price: string): string { + const value = Number.parseFloat(price); + if (!Number.isFinite(value)) return price; + return new Intl.NumberFormat('fa-IR').format(Math.round(value)); +} diff --git a/src/views/ServiceRequestsView.vue b/src/views/ServiceRequestsView.vue new file mode 100644 index 0000000..032360d --- /dev/null +++ b/src/views/ServiceRequestsView.vue @@ -0,0 +1,192 @@ + + +