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 @@
-
همهٔ انواع خدمت این دسته را اضافه کردهاید.
+
+
+ خدمت سفارشی (خارج از لیست ادمین)
+
+ اگر خدمتی در لیست بالا نیست، اینجا پیشنهاد دهید. پس از تأیید مدیر، به خدمات کسبوکار شما
+ اضافه میشود.
+
+
+
+
+
+
@@ -218,6 +264,10 @@