From 2f485914b4f18060d96ed454e43d404858780fa4 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Mon, 25 May 2026 21:38:17 +0330 Subject: [PATCH] add business category --- src/api/business-service-requests.ts | 16 ++++ src/api/types.ts | 25 +++++ src/components/ui/PriceInput.vue | 61 ++++++++++++ src/utils/format.ts | 62 +++++++----- src/views/app/AppOwnerView.vue | 137 ++++++++++++++++++++++++--- 5 files changed, 266 insertions(+), 35 deletions(-) create mode 100644 src/api/business-service-requests.ts create mode 100644 src/components/ui/PriceInput.vue diff --git a/src/api/business-service-requests.ts b/src/api/business-service-requests.ts new file mode 100644 index 0000000..d7f14c7 --- /dev/null +++ b/src/api/business-service-requests.ts @@ -0,0 +1,16 @@ +import { api } from './client'; +import type { + BusinessServiceRequest, + CreateBusinessServiceRequestPayload, +} from './types'; + +export function fetchMyServiceRequests() { + return api('/business-service-requests/mine'); +} + +export function createServiceRequest(payload: CreateBusinessServiceRequestPayload) { + return api('/business-service-requests', { + method: 'POST', + body: JSON.stringify(payload), + }); +} diff --git a/src/api/types.ts b/src/api/types.ts index 7bcb7df..e0a66f0 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -83,6 +83,31 @@ export type BusinessService = { serviceTemplate?: ServiceTemplate; }; +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; +}; + +export type CreateBusinessServiceRequestPayload = { + title: string; + description?: string; + price: number; + durationMinutes?: number; +}; + export type BookingStatus = | 'pending' | 'confirmed' diff --git a/src/components/ui/PriceInput.vue b/src/components/ui/PriceInput.vue new file mode 100644 index 0000000..b4f50f7 --- /dev/null +++ b/src/components/ui/PriceInput.vue @@ -0,0 +1,61 @@ + + + diff --git a/src/utils/format.ts b/src/utils/format.ts index 3008f92..4b78c24 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -1,17 +1,44 @@ -/** Parses price from form input (string or number from type="number"). */ +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; + }); +} + +/** Strips grouping separators; keeps digits only for price inputs. */ +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, ''); +} + +/** Formats price for text inputs with fa-IR thousand separators. */ +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); +} + +/** Parses price from form input (formatted or plain digits). */ export function parsePriceInput( value: string | number | null | undefined, ): number | null { - if (value == null || value === '') { - return null; - } - if (typeof value === 'number') { - return Number.isFinite(value) && value >= 0 ? value : null; - } - const n = Number.parseFloat(String(value).replace(/,/g, '').trim()); - if (!Number.isFinite(n) || n < 0) { - return 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; } @@ -69,19 +96,6 @@ export function bookingStatusLabel(status: string): string { return map[status] ?? status; } -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; - }); -} - /** Normalizes browser/API time to HH:mm (24h) for availability API. */ export function normalizeHmTime(value: string): string | null { const raw = toAsciiDigits(String(value ?? '').trim()).normalize('NFKC'); diff --git a/src/views/app/AppOwnerView.vue b/src/views/app/AppOwnerView.vue index 34e7bff..ceacf57 100644 --- a/src/views/app/AppOwnerView.vue +++ b/src/views/app/AppOwnerView.vue @@ -106,13 +106,10 @@
-
- همهٔ انواع خدمت این دسته را اضافه کرده‌اید.

+ +
+

خدمت سفارشی (خارج از لیست ادمین)

+

+ اگر خدمتی در لیست بالا نیست، اینجا پیشنهاد دهید. پس از تأیید مدیر، به خدمات کسب‌وکار شما + اضافه می‌شود. +

+ +
    +
  • +

    {{ req.title }}

    +

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

    + + {{ requestStatusLabel(req.status) }} + +

    {{ req.adminNote }}

    +
  • +
+ +
+ +
+ +