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 @@
+
+
+
+
+ جدید
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+ {{
+ searchQuery.trim()
+ ? 'نتیجهای برای جستجوی شما یافت نشد.'
+ : 'کسبوکاری یافت نشد.'
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
خوش آمدید
+
+ {{ auth.user?.fullName ?? 'سوپر ادمین' }}
+
+
{{ auth.user?.mobile }}
+
+
- { label: 'تنظیمات', value: '—', icon: 'camera' as const, to: '/settings', tone: 'sky' as const },
\ No newline at end of file
+
+ {{ error }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
آخرین کسبوکارها
+
+ همه
+
+
+
+
+ هنوز کسبوکاری ثبت نشده.
+
+ افزودن کسبوکار
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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: [],
+};