From 4bc6c003f30bf87b801c1be22ae290be1b095d9d Mon Sep 17 00:00:00 2001 From: Mahdi Date: Thu, 21 May 2026 14:20:52 +0330 Subject: [PATCH] Complete super admin panel with CRUD, bookings, profile, and dev API proxy. Co-authored-by: Cursor --- .env.example | 4 +- .env.production | 1 + index.html | 1 + public/favicon.svg | 4 + src/api/auth.ts | 14 +- src/api/bookings.ts | 17 ++ src/api/business-categories.ts | 34 +++ src/api/businesses.ts | 47 ++++ src/api/types.ts | 99 ++++++++ src/api/users.ts | 17 ++ src/components/ui/AppBadge.vue | 30 +++ src/components/ui/AppInput.vue | 3 + src/components/ui/AppModal.vue | 40 ++++ src/components/ui/AppSelect.vue | 36 +++ src/components/ui/AppTextarea.vue | 30 +++ src/components/ui/EmptyState.vue | 7 + src/components/ui/PageHeader.vue | 13 ++ src/layouts/AdminLayout.vue | 30 ++- src/router/index.ts | 18 ++ src/stores/auth.ts | 5 + src/utils/format.ts | 24 ++ src/utils/mobile.ts | 37 +++ src/utils/slug.ts | 16 ++ src/views/BookingsView.vue | 151 ++++++++++++ src/views/BusinessesView.vue | 373 +++++++++++++++++++++++++++++- src/views/CategoriesView.vue | 180 ++++++++++++++ src/views/DashboardView.vue | 84 ++++++- src/views/LoginView.vue | 10 +- src/views/ProfileView.vue | 78 +++++++ src/views/UsersView.vue | 138 ++++++++++- vite.config.ts | 10 + 31 files changed, 1508 insertions(+), 43 deletions(-) create mode 100644 .env.production create mode 100644 public/favicon.svg create mode 100644 src/api/bookings.ts create mode 100644 src/api/business-categories.ts create mode 100644 src/api/businesses.ts create mode 100644 src/api/types.ts create mode 100644 src/api/users.ts create mode 100644 src/components/ui/AppBadge.vue create mode 100644 src/components/ui/AppModal.vue create mode 100644 src/components/ui/AppSelect.vue create mode 100644 src/components/ui/AppTextarea.vue create mode 100644 src/components/ui/EmptyState.vue create mode 100644 src/components/ui/PageHeader.vue create mode 100644 src/utils/format.ts create mode 100644 src/utils/mobile.ts create mode 100644 src/utils/slug.ts create mode 100644 src/views/BookingsView.vue create mode 100644 src/views/CategoriesView.vue create mode 100644 src/views/ProfileView.vue diff --git a/.env.example b/.env.example index d693075..00c96a5 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ -VITE_API_URL=https://api.montime.ir +# Dev (npm run dev): /api — proxied to api.montime.ir by Vite +# Build (npm run build): use .env.production → https://api.montime.ir +VITE_API_URL=/api diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..d693075 --- /dev/null +++ b/.env.production @@ -0,0 +1 @@ +VITE_API_URL=https://api.montime.ir diff --git a/index.html b/index.html index b9f443b..1d055e6 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ مون‌تایم — پنل مدیریت + + + A + diff --git a/src/api/auth.ts b/src/api/auth.ts index 3df4d09..1483ef6 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,12 +1,8 @@ +import { normalizeIranMobile } from '@/utils/mobile'; import { api } from './client'; +import type { User } from './types'; -export type User = { - id: string; - mobile: string; - fullName: string | null; - role: string; - isActive: boolean; -}; +export type { User }; export type RequestOtpResponse = { expiresInSeconds: number; @@ -21,14 +17,14 @@ export type VerifyOtpResponse = { export function requestOtp(mobile: string) { return api('/auth/request-otp', { method: 'POST', - body: JSON.stringify({ mobile }), + body: JSON.stringify({ mobile: normalizeIranMobile(mobile) }), }); } export function verifyOtp(mobile: string, code: string) { return api('/auth/verify-otp', { method: 'POST', - body: JSON.stringify({ mobile, code }), + body: JSON.stringify({ mobile: normalizeIranMobile(mobile), code }), }); } diff --git a/src/api/bookings.ts b/src/api/bookings.ts new file mode 100644 index 0000000..74e344d --- /dev/null +++ b/src/api/bookings.ts @@ -0,0 +1,17 @@ +import { api } from './client'; +import type { Booking, BookingStatus } from './types'; + +export function fetchBookings(params?: { + businessId?: string; + status?: BookingStatus; +}) { + const search = new URLSearchParams(); + if (params?.businessId) search.set('businessId', params.businessId); + if (params?.status) search.set('status', params.status); + const query = search.toString(); + return api(`/bookings${query ? `?${query}` : ''}`); +} + +export function deleteBooking(id: string) { + return api<{ success: true }>(`/bookings/${id}`, { method: 'DELETE' }); +} diff --git a/src/api/business-categories.ts b/src/api/business-categories.ts new file mode 100644 index 0000000..75f1109 --- /dev/null +++ b/src/api/business-categories.ts @@ -0,0 +1,34 @@ +import { api } from './client'; +import type { + BusinessCategory, + CreateCategoryPayload, + UpdateCategoryPayload, +} from './types'; + +export function fetchCategories() { + return api('/business-categories'); +} + +export function fetchCategory(id: string) { + return api(`/business-categories/${id}`); +} + +export function createCategory(payload: CreateCategoryPayload) { + return api('/business-categories', { + method: 'POST', + body: JSON.stringify(payload), + }); +} + +export function updateCategory(id: string, payload: UpdateCategoryPayload) { + return api(`/business-categories/${id}`, { + method: 'PATCH', + body: JSON.stringify(payload), + }); +} + +export function deleteCategory(id: string) { + return api<{ success: true }>(`/business-categories/${id}`, { + method: 'DELETE', + }); +} diff --git a/src/api/businesses.ts b/src/api/businesses.ts new file mode 100644 index 0000000..8fd953a --- /dev/null +++ b/src/api/businesses.ts @@ -0,0 +1,47 @@ +import { api } from './client'; +import type { + AssignOwnerPayload, + Business, + CreateBusinessPayload, + UpdateBusinessPayload, +} from './types'; + +export function fetchBusinesses(params?: { + categoryId?: string; + isActive?: boolean; +}) { + const search = new URLSearchParams(); + if (params?.categoryId) search.set('categoryId', params.categoryId); + if (params?.isActive !== undefined) search.set('isActive', String(params.isActive)); + const query = search.toString(); + return api(`/businesses${query ? `?${query}` : ''}`); +} + +export function fetchBusiness(id: string) { + return api(`/businesses/${id}`); +} + +export function createBusiness(payload: CreateBusinessPayload) { + return api('/businesses', { + method: 'POST', + body: JSON.stringify(payload), + }); +} + +export function updateBusiness(id: string, payload: UpdateBusinessPayload) { + return api(`/businesses/${id}`, { + method: 'PATCH', + body: JSON.stringify(payload), + }); +} + +export function deleteBusiness(id: string) { + return api<{ success: true }>(`/businesses/${id}`, { method: 'DELETE' }); +} + +export function assignBusinessOwner(id: string, payload: AssignOwnerPayload) { + return api(`/businesses/${id}/assign-owner`, { + method: 'POST', + body: JSON.stringify(payload), + }); +} diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..92f9f15 --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,99 @@ +export type UserRole = 'customer' | 'business_owner' | 'admin'; + +export type User = { + id: string; + mobile: string; + fullName: string | null; + role: UserRole; + isActive: boolean; + createdAt?: string; + updatedAt?: string; +}; + +export type BusinessCategory = { + id: string; + slug: string; + title: string; + isActive: boolean; + createdAt?: string; + updatedAt?: string; +}; + +export type Business = { + id: string; + title: string; + slug: string; + description: string | null; + contactPhone: string | null; + address: string | null; + latitude: number | null; + longitude: number | null; + isActive: boolean; + categoryId: string; + ownerId: string | null; + category?: BusinessCategory; + owner?: User | null; + createdAt?: string; + updatedAt?: string; +}; + +export type CreateBusinessPayload = { + title: string; + slug: string; + categoryId: string; + description?: string; + contactPhone?: string; + address?: string; + latitude?: number; + longitude?: number; + isActive?: boolean; +}; + +export type UpdateBusinessPayload = Partial & { + description?: string | null; + contactPhone?: string | null; + address?: string | null; + latitude?: number | null; + longitude?: number | null; + ownerId?: string | null; +}; + +export type CreateCategoryPayload = { + title: string; + slug: string; + isActive?: boolean; +}; + +export type UpdateCategoryPayload = Partial; + +export type UpdateUserPayload = { + fullName?: string | null; + role?: UserRole; + isActive?: boolean; +}; + +export type AssignOwnerPayload = { + mobile: string; + fullName?: string | null; +}; + +export type BookingStatus = + | 'pending' + | 'confirmed' + | 'cancelled' + | 'completed'; + +export type Booking = { + id: string; + customerId: string; + businessId: string; + businessServiceId: string; + scheduledAt: string; + status: BookingStatus; + note: string | null; + customer?: User; + business?: Business; + businessService?: { + serviceTemplate?: { title: string }; + }; +}; diff --git a/src/api/users.ts b/src/api/users.ts new file mode 100644 index 0000000..88298b9 --- /dev/null +++ b/src/api/users.ts @@ -0,0 +1,17 @@ +import { api } from './client'; +import type { UpdateUserPayload, User } from './types'; + +export function fetchUsers() { + return api('/users'); +} + +export function fetchUser(id: string) { + return api(`/users/${id}`); +} + +export function updateUser(id: string, payload: UpdateUserPayload) { + return api(`/users/${id}`, { + method: 'PATCH', + body: JSON.stringify(payload), + }); +} diff --git a/src/components/ui/AppBadge.vue b/src/components/ui/AppBadge.vue new file mode 100644 index 0000000..bf09bc2 --- /dev/null +++ b/src/components/ui/AppBadge.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/components/ui/AppInput.vue b/src/components/ui/AppInput.vue index 2e0074b..d2c71f6 100644 --- a/src/components/ui/AppInput.vue +++ b/src/components/ui/AppInput.vue @@ -5,6 +5,7 @@ defineOptions({ inheritAttrs: false }); +const model = defineModel({ default: '' }); + withDefaults( defineProps<{ label?: string; diff --git a/src/components/ui/AppModal.vue b/src/components/ui/AppModal.vue new file mode 100644 index 0000000..2247bbb --- /dev/null +++ b/src/components/ui/AppModal.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/components/ui/AppSelect.vue b/src/components/ui/AppSelect.vue new file mode 100644 index 0000000..968e088 --- /dev/null +++ b/src/components/ui/AppSelect.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/components/ui/AppTextarea.vue b/src/components/ui/AppTextarea.vue new file mode 100644 index 0000000..d6f694b --- /dev/null +++ b/src/components/ui/AppTextarea.vue @@ -0,0 +1,30 @@ +