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 === '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(() => {
-
-