From a2de3451124f982d866a9d84e017b1f07ca5669f Mon Sep 17 00:00:00 2001 From: Mahdi Date: Sat, 13 Jun 2026 15:37:58 +0330 Subject: [PATCH] add&edit cancellationReason --- index.html | 18 ++ src/api/types.ts | 198 ++++++++++++- src/layouts/AdminLayout.vue | 86 +++++- src/main.ts | 12 +- src/router/index.ts | 98 ++++++- src/styles/main.css | 37 ++- src/views/BusinessesView.vue | 550 ++++++++++++++++++++++++++++++++++- src/views/DashboardView.vue | 168 ++++++++++- tailwind.config.js | 38 ++- 9 files changed, 1193 insertions(+), 12 deletions(-) diff --git a/index.html b/index.html index e69de29..0caae71 100644 --- a/index.html +++ b/index.html @@ -0,0 +1,18 @@ + + + + + + + منتایم — پنل مدیریت + + + + + + + + +
+ + diff --git a/src/api/types.ts b/src/api/types.ts index af37416..2cd7b0e 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,4 +1,200 @@ +export type UserRole = 'customer' | 'business_owner' | 'admin'; +export type User = { + id: string; + mobile: string; + fullName: string | null; + role: UserRole; + isActive: boolean; + hasPassword?: boolean; + createdAt?: string; + updatedAt?: string; +}; + +export type UpdateUserPayload = { + fullName?: string | null; + role?: UserRole; + isActive?: boolean; +}; + +export type AdminSetUserPasswordPayload = { + password: string; + confirmPassword: string; +}; + +export type BookingStatus = 'pending' | 'confirmed' | 'cancelled' | 'completed'; + +export type BusinessCategory = { + id: string; + slug: string; + title: string; + isActive: boolean; +}; + +export type CreateCategoryPayload = { + title: string; + slug?: string; + isActive?: boolean; +}; + +export type UpdateCategoryPayload = { + title?: string; + slug?: string; + isActive?: boolean; +}; + +export type BusinessImage = { + id: string; + url: string; + originalName?: string | null; + sortOrder: number; + isCover: boolean; +}; + +export type Business = { + id: string; + title: string; + slug: string; subtitle: string | null; + description: string | null; + contactPhone: string | null; + address: string | null; + latitude: number | null; + longitude: number | null; + isActive: boolean; + categoryId: string; + ownerId?: string | null; + category?: BusinessCategory; + images?: BusinessImage[]; + concurrentCapacity?: number; + slotStepMinutes?: number; + serviceClassesEnabled?: boolean; + createdAt?: string; + updatedAt?: string; +}; + +export type CreateBusinessPayload = { + title: string; + slug?: string; + categoryId: string; subtitle?: string; - subtitle?: string | null; \ No newline at end of file + description?: string; + contactPhone?: string; + address?: string; + latitude?: number; + longitude?: number; + isActive?: boolean; +}; + +export type UpdateBusinessPayload = { + title?: string; + slug?: string; + categoryId?: string; + subtitle?: string | null; + description?: string | null; + contactPhone?: string | null; + address?: string | null; + latitude?: number | null; + longitude?: number | null; + isActive?: boolean; +}; + +export type AssignOwnerPayload = { + mobile: string; + fullName?: string | null; +}; + +export type ServiceTemplate = { + id: string; + title: string; + slug: string; + description?: string | null; + defaultDurationMinutes?: number | null; + isActive: boolean; + categoryId: string; +}; + +export type CreateServiceTemplatePayload = { + title: string; + slug?: string; + description?: string; + defaultDurationMinutes?: number; + isActive?: boolean; + categoryId: string; +}; + +export type UpdateServiceTemplatePayload = { + title?: string; + slug?: string; + description?: string | null; + defaultDurationMinutes?: number | null; + isActive?: boolean; +}; + +export type BusinessService = { + id: string; + businessId: string; + serviceTemplateId: string; + price: string; + durationMinutes?: number | null; + isActive: boolean; + serviceTemplate?: ServiceTemplate; +}; + +export type Booking = { + id: string; + customerId: string; + businessId: string; + businessServiceId: string; + serviceClassId?: string | null; + bookingGroupId?: string | null; + scheduledAt: string; + status: BookingStatus; + note?: string | null; + cancellationReason?: string | null; + business?: Business; + businessService?: BusinessService; + customer?: { + id: string; + mobile: string; + fullName?: string | null; + }; +}; + +export type BusinessRegistrationStatus = 'pending' | 'approved' | 'rejected'; + +export type BusinessRegistrationRequest = { + id: string; + status: BusinessRegistrationStatus; + title: string; + slug: string; + categoryId: string; + subtitle?: string | null; + description?: string | null; + contactPhone?: string | null; + address?: string | null; + adminNote?: string | null; + applicantId: string; + applicant?: { + id: string; + mobile: string; + fullName?: string | null; + }; + category?: BusinessCategory; + createdAt?: string; +}; + +export type BusinessServiceRequestStatus = 'pending' | 'approved' | 'rejected'; + +export type BusinessServiceRequest = { + id: string; + businessId: string; + status: BusinessServiceRequestStatus; + title: string; + description?: string | null; + price: string; + durationMinutes?: number | null; + adminNote?: string | null; + business?: Business; + createdAt?: string; +}; diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index c40100c..9774d15 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -1,8 +1,90 @@ - + + + diff --git a/src/main.ts b/src/main.ts index 0bb9662..0b59d1f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,2 +1,12 @@ +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import App from './App.vue'; +import router from './router'; -import './styles/fonts.css'; \ No newline at end of file +import './styles/fonts.css'; +import './styles/main.css'; + +const app = createApp(App); +app.use(createPinia()); +app.use(router); +app.mount('#app'); diff --git a/src/router/index.ts b/src/router/index.ts index bc77409..e946890 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,7 +1,103 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import { useAuthStore } from '@/stores/auth'; +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/login', + name: 'login', + component: () => import('@/views/LoginView.vue'), + meta: { guest: true }, + }, + { + path: '/verify', + name: 'verify', + component: () => import('@/views/VerifyOtpView.vue'), + meta: { guest: true }, + }, + { + path: '/', + name: 'dashboard', + component: () => import('@/views/DashboardView.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + }, + { + path: '/categories', + name: 'categories', + component: () => import('@/views/CategoriesView.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + }, + { + path: '/service-templates', + name: 'service-templates', + component: () => import('@/views/ServiceTemplatesView.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + }, + { + path: '/businesses', + name: 'businesses', + component: () => import('@/views/BusinessesView.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + }, + { + path: '/users', + name: 'users', + component: () => import('@/views/UsersView.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + }, + { + path: '/business-requests', + name: 'business-requests', + 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', + component: () => import('@/views/BookingsView.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + }, + { + path: '/profile', + name: 'profile', + component: () => import('@/views/ProfileView.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + }, { path: '/settings', name: 'settings', component: () => import('@/views/PlatformSettingsView.vue'), meta: { requiresAuth: true, requiresAdmin: true }, - }, \ No newline at end of file + }, + ], +}); + +router.beforeEach(async (to) => { + const auth = useAuthStore(); + + if (auth.token && !auth.user) { + await auth.loadUser(); + } + + if (to.meta.requiresAuth && !auth.isLoggedIn) { + return { name: 'login', query: { redirect: to.fullPath } }; + } + + if (to.meta.requiresAdmin && !auth.isAdmin) { + auth.logout(); + return { name: 'login' }; + } + + if (to.meta.guest && auth.isLoggedIn && auth.isAdmin) { + return { name: 'dashboard' }; + } +}); + +export default router; diff --git a/src/styles/main.css b/src/styles/main.css index b8b4f0f..4120355 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -1,4 +1,29 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +@layer base { + :root { + --color-surface: #ffffff; + --color-surface-2: #f8fafc; + --color-surface-3: #f1f5f9; + --color-border: #e2e8f0; + --color-text: #0f172a; + --color-muted: #64748b; + --color-placeholder: #e2e8f0; + } + + .dark { + --color-surface: #0f1419; + --color-surface-2: #1a2332; + --color-surface-3: #243044; + --color-border: #334155; + --color-text: #f1f5f9; + --color-muted: #94a3b8; + --color-placeholder: #3d4f63; + } + + html { @apply antialiased font-sans; font-family: IRANYekanX, ui-sans-serif, system-ui, sans-serif; } @@ -8,4 +33,14 @@ input, textarea, select { - font-family: inherit; \ No newline at end of file + font-family: inherit; + } + + body { + @apply bg-surface-2 text-text; + } + + #app { + min-height: 100dvh; + } +} diff --git a/src/views/BusinessesView.vue b/src/views/BusinessesView.vue index 2916d11..84100c8 100644 --- a/src/views/BusinessesView.vue +++ b/src/views/BusinessesView.vue @@ -1,12 +1,560 @@ + + + diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 0e753a1..4215652 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -1,2 +1,168 @@ + + + diff --git a/tailwind.config.js b/tailwind.config.js index 5d69f51..440ef1b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,4 +1,34 @@ - - fontFamily: { - sans: ['IRANYekanX', 'ui-sans-serif', 'system-ui', 'sans-serif'], - }, \ No newline at end of file +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], + darkMode: 'class', + theme: { + extend: { + fontFamily: { + sans: ['IRANYekanX', 'ui-sans-serif', 'system-ui', 'sans-serif'], + }, + maxWidth: { + app: '768px', + }, + colors: { + border: 'var(--color-border)', + text: 'var(--color-text)', + muted: 'var(--color-muted)', + surface: { + DEFAULT: 'var(--color-surface)', + 2: 'var(--color-surface-2)', + 3: 'var(--color-surface-3)', + }, + brand: { + 50: '#e6f7ee', + 100: '#ccefdc', + 400: '#22c55e', + 500: '#00a651', + 600: '#009640', + 700: '#007a35', + }, + }, + }, + }, + plugins: [], +};