From efb0051ab9adcf08ce3089d522dc8a3135bb0cd7 Mon Sep 17 00:00:00 2001 From: "s.esmaeili" Date: Thu, 10 Sep 2026 20:11:27 +0330 Subject: [PATCH] chore: dockerize monorepo with multi-stage builds and update frontend --- .dockerignore | 15 +- Dockerfile | 12 - apps/admin/Dockerfile | 31 + apps/admin/src/App.vue | 74 +- apps/admin/src/layouts/AdminLayout.vue | 835 +++++++++-- apps/admin/src/services/api/client.ts | 9 +- apps/admin/src/stores/auth.store.ts | 13 + apps/admin/src/views/audit/AuditLogView.vue | 273 ++-- apps/admin/src/views/auth/LoginView.vue | 102 +- .../src/views/campaigns/CampaignFormView.vue | 9 +- .../src/views/campaigns/CampaignListView.vue | 425 ++++-- .../src/views/dashboard/DashboardView.vue | 1304 +++++++++++++++-- apps/admin/src/views/prizes/PrizeListView.vue | 201 ++- apps/admin/src/views/reports/ReportView.vue | 255 +++- apps/admin/src/views/sms/SmsLogListView.vue | 249 +++- apps/admin/src/views/spins/SpinListView.vue | 251 +++- apps/admin/src/views/users/UserDetailView.vue | 142 +- apps/admin/src/views/users/UserListView.vue | 421 ++++-- .../src/views/winners/WinnerListView.vue | 405 +++-- apps/api/Dockerfile | 22 + .../admin-auth/admin-auth.controller.ts | 12 +- .../modules/admin-auth/admin-auth.service.ts | 36 + .../dto/update-admin-profile.dto.ts | 27 + apps/api/src/modules/report/report.service.ts | 33 +- apps/client/Dockerfile | 31 + docker-compose.yml | 45 + packages/shared-types/src/farsi-number.ts | 14 +- packages/shared-types/src/index.ts | 2 + 28 files changed, 4179 insertions(+), 1069 deletions(-) delete mode 100644 Dockerfile create mode 100644 apps/admin/Dockerfile create mode 100644 apps/api/Dockerfile create mode 100644 apps/api/src/modules/admin-auth/dto/update-admin-profile.dto.ts create mode 100644 apps/client/Dockerfile diff --git a/.dockerignore b/.dockerignore index bf77ef1..e381c02 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,17 @@ node_modules +**/node_modules dist +**/dist .git -.env \ No newline at end of file +.idea +.vscode +.env +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.npm +.pnpm-store +Dockerfile* +docker-compose*.yml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 43c9779..0000000 --- a/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM node:20-alpine - -WORKDIR /app - -COPY package*.json ./ -RUN npm install - -COPY . . - -EXPOSE 3000 - -CMD ["npm", "start"] \ No newline at end of file diff --git a/apps/admin/Dockerfile b/apps/admin/Dockerfile new file mode 100644 index 0000000..fbf74a5 --- /dev/null +++ b/apps/admin/Dockerfile @@ -0,0 +1,31 @@ +FROM node:20-alpine AS builder + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable pnpm + +WORKDIR /app + +COPY . . + +RUN pnpm install --frozen-lockfile +RUN pnpm --filter @lucky-wheel/admin build + +FROM nginx:alpine + +COPY --from=builder /app/apps/admin/dist /usr/share/nginx/html + +# Configure Nginx for Single Page Application +RUN echo 'server { \ + listen 80; \ + server_name localhost; \ + location / { \ + root /usr/share/nginx/html; \ + index index.html index.htm; \ + try_files $uri $uri/ /index.html; \ + } \ +}' > /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/admin/src/App.vue b/apps/admin/src/App.vue index a5bbd84..8cc75f9 100644 --- a/apps/admin/src/App.vue +++ b/apps/admin/src/App.vue @@ -7,9 +7,15 @@ dayjs.locale('fa'); const theme = { token: { - colorPrimary: '#1677ff', - fontFamily: 'IRANSans, Tahoma, sans-serif', - borderRadius: 6, + colorPrimary: '#4f46e5', + fontFamily: 'IRANSans, "IRANSansWeb(FaNum)", Tahoma, sans-serif', + borderRadius: 8, + colorBgContainer: '#ffffff', + colorBgLayout: '#f4f6fa', + colorSuccess: '#10b981', + colorWarning: '#f59e0b', + colorError: '#ef4444', + controlHeight: 36, }, }; @@ -25,7 +31,65 @@ body { margin: 0; padding: 0; direction: rtl; - font-family: 'IRANSans', Tahoma, sans-serif; - background-color: #f0f2f5; + font-family: 'IRANSans', 'IRANSansWeb(FaNum)', Tahoma, sans-serif; + background-color: #f4f6fa; + color: #1e293b; + -webkit-font-smoothing: antialiased; +} + +/* Modern Ant Table Global Enhancements */ +.ant-table-wrapper .ant-table { + border-radius: 10px; +} + +.ant-table-wrapper .ant-table-thead > tr > th { + background: #f8fafc !important; + color: #475569 !important; + font-weight: 600 !important; + font-size: 13px !important; + border-bottom: 1px solid #e2e8f0 !important; + padding: 13px 16px !important; +} + +.ant-table-wrapper .ant-table-tbody > tr > td { + padding: 12px 16px !important; + border-bottom: 1px solid #f1f5f9 !important; + transition: background-color 0.2s ease; +} + +.ant-table-wrapper .ant-table-tbody > tr:hover > td { + background-color: #f8faff !important; +} + +.ant-table-wrapper .ant-pagination { + margin: 16px 0 8px 0 !important; +} + +/* Action Dropdown Menu Modern Style */ +.action-dropdown-menu.ant-dropdown-menu { + border-radius: 10px; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.05); + border: 1px solid #f1f5f9; + padding: 6px; + min-width: 170px; +} + +.action-dropdown-menu .ant-dropdown-menu-item { + border-radius: 6px; + padding: 8px 12px; + font-size: 13px; + transition: all 0.15s ease; +} + +.action-dropdown-menu .ant-dropdown-menu-item:hover { + background-color: #f1f5f9; +} + +/* Ant Modal Global Viewport Safeguards */ +.ant-modal { + max-width: calc(100vw - 32px); +} +.ant-modal .ant-modal-content { + border-radius: 12px; } diff --git a/apps/admin/src/layouts/AdminLayout.vue b/apps/admin/src/layouts/AdminLayout.vue index 8817a3a..681985d 100644 --- a/apps/admin/src/layouts/AdminLayout.vue +++ b/apps/admin/src/layouts/AdminLayout.vue @@ -1,17 +1,25 @@ @@ -42,125 +155,677 @@ async function handleLogout() { +
-
🎡
-

گردونه شانس

+
+
+ + + + + + + + + + + + +
+

گردونه شانس

+
+ + +
- - - - داشبورد آماری - + +
+ +
- - - مدیریت کمپین‌ها - + +
- سامانه جامع قرعه‌کشی و گردونه شانس + سامانه مدیریت گردونه شانس
- - {{ adminRole }} - - {{ adminName }} - - - - خروج - - + + + + +
- - + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ adminRole === 'SUPER_ADMIN' ? 'مدیر ارشد سامانه (SUPER_ADMIN)' : 'اپراتور سامانه (OPERATOR)' }} + + + +
diff --git a/apps/admin/src/services/api/client.ts b/apps/admin/src/services/api/client.ts index a651841..4ad046b 100644 --- a/apps/admin/src/services/api/client.ts +++ b/apps/admin/src/services/api/client.ts @@ -49,9 +49,14 @@ const processQueue = (error: any, token: string | null = null) => { apiClient.interceptors.response.use( (response) => { - // If backend wrapped response inside { success: true, data: ... } or paginated { data: [...], meta: ... } + // If response is a Blob (e.g. file export), return response as-is + if (response.data instanceof Blob) { + return response; + } + + // If backend wrapped response inside { success: true, data: ..., meta: ... } if (response.data && typeof response.data === 'object') { - if (response.data.success !== undefined && response.data.data !== undefined) { + if ('success' in response.data && 'data' in response.data) { return { ...response, data: response.data.data, meta: response.data.meta }; } if ('data' in response.data && 'meta' in response.data) { diff --git a/apps/admin/src/stores/auth.store.ts b/apps/admin/src/stores/auth.store.ts index ecd0fb7..f07aedd 100644 --- a/apps/admin/src/stores/auth.store.ts +++ b/apps/admin/src/stores/auth.store.ts @@ -55,6 +55,18 @@ export const useAuthStore = defineStore('adminAuth', () => { } } + async function updateProfile(payload: { name?: string; email?: string; mobile?: string; password?: string }) { + loading.value = true; + try { + const response = await apiClient.patch('/admin/auth/profile', payload); + admin.value = response.data; + localStorage.setItem('admin_info', JSON.stringify(response.data)); + return response.data; + } finally { + loading.value = false; + } + } + return { token, admin, @@ -62,5 +74,6 @@ export const useAuthStore = defineStore('adminAuth', () => { isAuthenticated, login, logout, + updateProfile, }; }); diff --git a/apps/admin/src/views/audit/AuditLogView.vue b/apps/admin/src/views/audit/AuditLogView.vue index ad58ed4..f2b898a 100644 --- a/apps/admin/src/views/audit/AuditLogView.vue +++ b/apps/admin/src/views/audit/AuditLogView.vue @@ -47,6 +47,14 @@ function showDetails(record: any) { isDetailsModalVisible.value = true; } +const entityTypeLabels: Record = { + Campaign: 'کمپین', + Prize: 'جایزه', + User: 'کاربر', + Chance: 'شانس', + UserPrize: 'وضعیت برنده', +}; + const columns = [ { title: 'مدیر انجام‌دهنده', dataIndex: ['admin', 'name'], key: 'adminName', width: 150 }, { title: 'ایمیل مدیر', dataIndex: ['admin', 'email'], key: 'adminEmail', width: 180 }, @@ -55,7 +63,7 @@ const columns = [ { title: 'شناسه موجودیت', dataIndex: 'entityId', key: 'entityId', ellipsis: true }, { title: 'آی‌پی', dataIndex: 'ipAddress', key: 'ipAddress', width: 130 }, { title: 'زمان ثبت', dataIndex: 'createdAt', key: 'createdAt', width: 160 }, - { title: 'جزئیات تغییرات', key: 'details', width: 120 }, + { title: 'عملیات', key: 'details', width: 70, align: 'center' as const }, ]; function getActionColor(action: string) { @@ -71,95 +79,152 @@ onMounted(() => { diff --git a/apps/admin/src/views/auth/LoginView.vue b/apps/admin/src/views/auth/LoginView.vue index a322367..fa60ef9 100644 --- a/apps/admin/src/views/auth/LoginView.vue +++ b/apps/admin/src/views/auth/LoginView.vue @@ -32,9 +32,16 @@ async function onFinish() { + + diff --git a/apps/admin/src/views/campaigns/CampaignListView.vue b/apps/admin/src/views/campaigns/CampaignListView.vue index f6c5b79..d08d28f 100644 --- a/apps/admin/src/views/campaigns/CampaignListView.vue +++ b/apps/admin/src/views/campaigns/CampaignListView.vue @@ -5,11 +5,12 @@ import { EditOutlined, EyeOutlined, GiftOutlined, + MoreOutlined, PauseCircleOutlined, PlusOutlined, SearchOutlined, } from '@ant-design/icons-vue'; -import { message } from 'ant-design-vue'; +import { Modal, message } from 'ant-design-vue'; import { formatFarsiNumber, toFarsiDigits } from '@lucky-wheel/shared-types'; import { onMounted, reactive, ref } from 'vue'; import { useRouter } from 'vue-router'; @@ -57,6 +58,16 @@ async function duplicateCampaign(id: string) { } } +function confirmDuplicate(record: any) { + Modal.confirm({ + title: 'کپی‌برداری از کمپین', + content: `آیا از کپی کردن کمپین «${record.title}» و تمام جوایز آن اطمینان دارید؟`, + okText: 'بله، کپی کن', + cancelText: 'انصراف', + onOk: () => duplicateCampaign(record.id), + }); +} + async function quickSetStatus(id: string, newStatus: string) { try { await apiClient.patch(`/admin/campaigns/${id}`, { status: newStatus }); @@ -68,6 +79,19 @@ async function quickSetStatus(id: string, newStatus: string) { } } +const wheelDesignLabels: Record = { + CLASSIC: 'کلاسیک', + CLASSIC_CASINO: 'کلاسیک کازینو', + NEON: 'نئون دیجیتال', + NEON_CYBER: 'نئون سایبر', + MINIMAL: 'مینیمال مدرن', + MINIMAL_FLAT: 'مینیمال فلت', + LUXURY: 'لاکچری طلایی', + LUXURY_GOLD: 'لاکچری طلایی', + FESTIVAL: 'جشنواره‌ای شاد', + PLAYFUL_CARTOON: 'کارتونی شاد', +}; + const columns = [ { title: 'عنوان کمپین', dataIndex: 'title', key: 'title' }, { title: 'اسلاگ (URL)', dataIndex: 'slug', key: 'slug' }, @@ -75,23 +99,23 @@ const columns = [ { title: 'وضعیت', dataIndex: 'status', key: 'status' }, { title: 'شروع', dataIndex: 'startAt', key: 'startAt' }, { title: 'پایان', dataIndex: 'endAt', key: 'endAt' }, - { title: 'تعداد جوایز', dataIndex: ['_count', 'prizes'], key: 'prizesCount' }, - { title: 'تعداد اسپین‌ها', dataIndex: ['_count', 'spins'], key: 'spinsCount' }, - { title: 'عملیات', key: 'actions', width: 250 }, + { title: 'تعداد جوایز', key: 'prizesCount', align: 'center' as const }, + { title: 'تعداد اسپین‌ها', key: 'spinsCount', align: 'center' as const }, + { title: 'عملیات', key: 'actions', width: 70, align: 'center' as const }, ]; function getStatusInfo(status: string) { switch (status) { case 'ACTIVE': - return { color: 'green', text: 'فعال' }; + return { color: 'green', dotColor: '#10b981', text: 'فعال' }; case 'PAUSED': - return { color: 'orange', text: 'متوقف' }; + return { color: 'orange', dotColor: '#f59e0b', text: 'متوقف' }; case 'FINISHED': - return { color: 'red', text: 'پایان‌یافته' }; + return { color: 'red', dotColor: '#ef4444', text: 'پایان‌یافته' }; case 'DRAFT': - return { color: 'gold', text: 'پیش‌نویس' }; + return { color: 'gold', dotColor: '#d97706', text: 'پیش‌نویس' }; default: - return { color: 'default', text: status }; + return { color: 'default', dotColor: '#94a3b8', text: status }; } } @@ -101,141 +125,206 @@ onMounted(() => {