Compare commits

...
3 Commits
Author SHA1 Message Date
s.esmaeili d406d286ec build: update admin and client production assets for dokploy deployment 2026-10-02 16:40:12 +03:30
s.esmaeili 127a1f91e8 Merge branch 'main' of https://get.feep.ir/s.esmaeili/Lucky_Wheel 2026-10-02 16:38:53 +03:30
s.esmaeili c5a841c35f feat(rbac): implement full admin management, dynamic roles, permissions matrix, and workspace access control
- Add Workspace, Role, Permission, RolePermission, AdminRoleAssignment, and AdminWorkspaceAssignment models to Prisma schema\n- Create database migration and RBAC seed script\n- Implement PermissionsGuard, @RequirePermission decorator, and real-time permission resolution in JwtAdminStrategy\n- Implement AdminManagementModule with endpoints for managers CRUD, roles matrix, permissions catalog, and workspaces\n- Secure all sensitive admin controllers with granular permission requirements\n- Add useAuthStore helpers (can, canAny, fetchMe) and v-permission directive\n- Implement Desktop views: AdminListView, AdminFormDrawer (RTL right-side), RoleMatrixView, WorkspaceListView, and ForbiddenView\n- Implement Mobile views: MAdminListView, MAdminFormView (stepped mobile form), MRoleMatrixView, MWorkspaceListView, and MForbiddenView\n- Fix sticky header behavior across desktop and mobile shells\n- Update route guards and device detection logic for mobile prefixing
2026-10-02 16:29:23 +03:30
137 changed files with 6231 additions and 48 deletions
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.super-admin-banner[data-v-3204cd81]{background:#f5f3ff;border:1px solid #ddd6fe;border-radius:8px;padding:12px 16px}.banner-sub[data-v-3204cd81]{margin:6px 0 0 24px;font-size:12px;color:#6b7280;line-height:1.6}.role-item-card[data-v-3204cd81]{padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;transition:all .2s ease;background:#fff}.role-item-card.is-selected[data-v-3204cd81]{border-color:#3b82f6;background:#eff6ff}.role-name[data-v-3204cd81]{font-weight:600;font-size:14px;color:#1e293b}.role-desc[data-v-3204cd81]{margin-right:24px;font-size:12px;color:#64748b;margin-top:2px}[data-v-3204cd81] .ant-drawer-content,[data-v-3204cd81] .ant-drawer-header{direction:rtl;text-align:right}[data-v-3204cd81] .ant-drawer-title{text-align:right}[data-v-3204cd81] .ant-drawer-close{margin-right:0;margin-left:var(--ant-margin-xs, 8px)}.admin-list-page[data-v-0a86c8bb]{padding-bottom:24px}.page-header-title[data-v-0a86c8bb]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.title[data-v-0a86c8bb]{font-size:22px;font-weight:700;color:#1e293b;margin:0}.subtitle[data-v-0a86c8bb]{font-size:13px;color:#64748b;margin-top:4px;margin-bottom:0}.main-card[data-v-0a86c8bb]{border-radius:12px;box-shadow:0 1px 3px #0000000d}.table-header[data-v-0a86c8bb]{margin-bottom:16px}.filters[data-v-0a86c8bb]{display:flex;gap:12px;flex-wrap:wrap}.admin-profile-cell[data-v-0a86c8bb]{display:flex;align-items:center;gap:12px}.admin-details[data-v-0a86c8bb]{display:flex;flex-direction:column}.admin-name[data-v-0a86c8bb]{font-weight:600;font-size:14px;color:#1e293b}.admin-email[data-v-0a86c8bb]{font-size:12px;color:#64748b;direction:ltr;text-align:right}.mobile-cell[data-v-0a86c8bb]{font-weight:500;color:#334155;direction:ltr;display:inline-block}.roles-tag-group[data-v-0a86c8bb],.workspaces-tag-group[data-v-0a86c8bb]{display:flex;flex-wrap:wrap;gap:4px}.role-pill[data-v-0a86c8bb],.scope-pill[data-v-0a86c8bb]{border-radius:6px;font-size:12px;padding:2px 8px}.status-tag[data-v-0a86c8bb]{display:inline-flex;align-items:center;gap:6px;padding:2px 10px;border-radius:12px;font-size:12px}.status-dot[data-v-0a86c8bb]{width:6px;height:6px;border-radius:50%;display:inline-block}.date-cell[data-v-0a86c8bb]{font-size:13px;color:#64748b}.action-more-btn[data-v-0a86c8bb]{color:#64748b}.action-more-btn[data-v-0a86c8bb]:hover{background-color:#f1f5f9;color:#1e293b}
+1
View File
@@ -0,0 +1 @@
.page-header-title[data-v-1ee35815]{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:12px}.page-header-title .title[data-v-1ee35815]{font-size:20px;font-weight:700;color:#0f172a;margin:0;letter-spacing:-.3px}.page-header-title .subtitle[data-v-1ee35815]{font-size:13px;color:#64748b;margin:4px 0 0}.main-card[data-v-1ee35815]{border-radius:12px;box-shadow:0 1px 3px #0000000a;border:1px solid #f1f5f9}.table-header[data-v-1ee35815]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.filters[data-v-1ee35815]{display:flex;gap:12px}.admin-name[data-v-1ee35815]{font-weight:500;color:#1e293b}.code-badge[data-v-1ee35815]{background:#f8fafc;border:1px solid #e2e8f0;padding:2px 6px;border-radius:4px;font-family:monospace;font-size:11px;color:#64748b}.date-cell[data-v-1ee35815]{color:#334155;font-size:13px}.text-muted[data-v-1ee35815]{color:#94a3b8;font-size:12px}.action-more-btn[data-v-1ee35815]{color:#64748b;width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;transition:all .2s ease}.action-more-btn[data-v-1ee35815]:hover{background-color:#eef2ff!important}.json-box[data-v-1ee35815]{background:#0f172a;color:#38bdf8;padding:14px;border-radius:8px;font-family:monospace;font-size:12px;max-height:240px;overflow-y:auto;direction:ltr;text-align:left;border:1px solid #1e293b}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.wheel-picker-grid[data-v-3f417136]{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px;margin-top:10px}.design-card[data-v-3f417136]{border:2px solid #e2e8f0;border-radius:12px;overflow:hidden;cursor:pointer;transition:all .25s ease;background:#fff;display:flex;flex-direction:column}.design-card[data-v-3f417136]:hover{border-color:#1677ff;transform:translateY(-3px);box-shadow:0 8px 20px #0000001a}.design-card.is-selected[data-v-3f417136]{border-color:#1677ff;box-shadow:0 0 0 2px #1677ff40}.preview-stage[data-v-3f417136]{height:140px;position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden;border-bottom:1px solid rgba(0,0,0,.08)}.theme-tag[data-v-3f417136]{position:absolute;top:10px;right:10px;z-index:10;margin:0;font-weight:600}.stage-tools[data-v-3f417136]{position:absolute;bottom:8px;left:8px;display:flex;gap:6px;z-index:10}.tool-btn[data-v-3f417136]{background:#ffffffd9;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);border:none;box-shadow:0 2px 6px #00000026}.tool-btn[data-v-3f417136]:hover{background:#fff;color:#1677ff}.wheel-wrapper[data-v-3f417136]{position:relative;width:96px;height:96px;display:flex;align-items:center;justify-content:center}.pointer-wrapper[data-v-3f417136]{position:absolute;top:-8px;left:50%;transform:translate(-50%);z-index:20;filter:drop-shadow(0 2px 4px rgba(0,0,0,.4))}.mini-wheel-svg[data-v-3f417136]{width:100%;height:100%;border-radius:50%;filter:drop-shadow(0 4px 10px rgba(0,0,0,.3))}.card-body[data-v-3f417136]{padding:14px 16px;flex:1;display:flex;flex-direction:column}.card-title-row[data-v-3f417136]{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.title-text[data-v-3f417136]{font-weight:700;font-size:13.5px;color:#1e293b}.selected-icon[data-v-3f417136]{color:#1677ff;font-size:18px}.card-desc[data-v-3f417136]{font-size:11.5px;color:#64748b;line-height:1.6;margin:0}.modal-preview-container[data-v-3f417136]{padding:30px 20px;border-radius:12px;display:flex;flex-direction:column;align-items:center}.modal-wheel-box[data-v-3f417136]{position:relative;width:240px;height:240px}.large-pointer[data-v-3f417136]{position:absolute;top:-12px;left:50%;transform:translate(-50%);z-index:20;filter:drop-shadow(0 3px 6px rgba(0,0,0,.5))}.large-wheel-svg[data-v-3f417136]{width:100%;height:100%;border-radius:50%;filter:drop-shadow(0 8px 24px rgba(0,0,0,.4))}.submit-btn[data-v-654e8a71]{font-weight:600;border-radius:8px}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.page-header-title[data-v-bd3d8160]{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:12px}.page-header-title .title[data-v-bd3d8160]{font-size:20px;font-weight:700;color:#0f172a;margin:0;letter-spacing:-.3px}.page-header-title .subtitle[data-v-bd3d8160]{font-size:13px;color:#64748b;margin:4px 0 0}.create-btn[data-v-bd3d8160]{font-weight:600;border-radius:8px;box-shadow:0 2px 4px #4f46e533}.main-card[data-v-bd3d8160]{border-radius:12px;box-shadow:0 1px 3px #0000000a;border:1px solid #f1f5f9}.table-header[data-v-bd3d8160]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;flex-wrap:wrap;gap:12px}.filters[data-v-bd3d8160]{display:flex;gap:12px}.campaign-title[data-v-bd3d8160]{font-weight:600;color:#1e293b;cursor:pointer;transition:color .15s ease}.campaign-title[data-v-bd3d8160]:hover{color:#4f46e5}.slug-badge[data-v-bd3d8160]{background:#f8fafc;border:1px solid #e2e8f0;padding:2px 8px;border-radius:6px;color:#475569;font-size:12px;font-family:monospace}.date-cell[data-v-bd3d8160],.num-cell[data-v-bd3d8160]{color:#334155;font-size:13px}.status-tag[data-v-bd3d8160]{display:inline-flex;align-items:center;gap:6px;padding:2px 10px;border-radius:20px;font-weight:500}.status-dot[data-v-bd3d8160]{width:6px;height:6px;border-radius:50%;display:inline-block}.action-more-btn[data-v-bd3d8160]{color:#64748b;width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;transition:all .2s ease}.action-more-btn[data-v-bd3d8160]:hover{color:#4f46e5!important;background-color:#eef2ff!important}.dropdown-action-item[data-v-bd3d8160]{display:flex;align-items:center;gap:8px;color:#334155;text-decoration:none;font-size:13px}.dropdown-item-icon[data-v-bd3d8160]{font-size:14px}.text-success[data-v-bd3d8160]{color:#10b981}.text-warning[data-v-bd3d8160]{color:#f59e0b}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{b as l,I as i}from"./index-BIHaUtqF.js";var u={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M899.6 276.5L705 396.4 518.4 147.5a8.06 8.06 0 00-12.9 0L319 396.4 124.3 276.5c-5.7-3.5-13.1 1.2-12.2 7.9L188.5 865c1.1 7.9 7.9 14 16 14h615.1c8 0 14.9-6 15.9-14l76.4-580.6c.8-6.7-6.5-11.4-12.3-7.9zm-126 534.1H250.3l-53.8-409.4 139.8 86.1L512 252.9l175.7 234.4 139.8-86.1-53.9 409.4zM512 509c-62.1 0-112.6 50.5-112.6 112.6S449.9 734.2 512 734.2s112.6-50.5 112.6-112.6S574.1 509 512 509zm0 160.9c-26.6 0-48.2-21.6-48.2-48.3 0-26.6 21.6-48.3 48.2-48.3s48.2 21.6 48.2 48.3c0 26.6-21.6 48.3-48.2 48.3z"}}]},name:"crown",theme:"outlined"};function c(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){f(r,a,e[a])})}return r}function f(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var n=c({},t,e.attrs);return l(i,c({},n,{icon:u}),null)};o.displayName="CrownOutlined";o.inheritAttrs=!1;export{o as C};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{d as _,c as i,b as e,w as t,r as o,o as p,f as u,i as l,_ as f}from"./index-BIHaUtqF.js";const m={class:"forbidden-page"},b=_({__name:"ForbiddenView",setup(x){const s=l();function n(){s.push("/dashboard")}return(w,a)=>{const r=o("a-button"),c=o("a-result"),d=o("a-card");return p(),i("div",m,[e(d,{bordered:!1,class:"forbidden-card"},{default:t(()=>[e(c,{status:"403",title:"۴۰۳ — دسترسی غیرمجاز","sub-title":"شما مجوز لازم برای مشاهده این بخش یا انجام این عملیات را در سامانه ندارید."},{extra:t(()=>[e(r,{type:"primary",onClick:n},{default:t(()=>[...a[0]||(a[0]=[u(" بازگشت به داشبورد ",-1)])]),_:1})]),_:1})]),_:1})])}}}),V=f(b,[["__scopeId","data-v-88a9cfca"]]);export{V as default};
+1
View File
@@ -0,0 +1 @@
.forbidden-page[data-v-88a9cfca]{padding:40px 24px;display:flex;justify-content:center;align-items:center;min-height:70vh}.forbidden-card[data-v-88a9cfca]{max-width:600px;width:100%;border-radius:12px;box-shadow:0 4px 16px #0000000a}
+1
View File
@@ -0,0 +1 @@
import{b as f,I as u}from"./index-BIHaUtqF.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M880 310H732.4c13.6-21.4 21.6-46.8 21.6-74 0-76.1-61.9-138-138-138-41.4 0-78.7 18.4-104 47.4-25.3-29-62.6-47.4-104-47.4-76.1 0-138 61.9-138 138 0 27.2 7.9 52.6 21.6 74H144c-17.7 0-32 14.3-32 32v200c0 4.4 3.6 8 8 8h40v344c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V550h40c4.4 0 8-3.6 8-8V342c0-17.7-14.3-32-32-32zm-334-74c0-38.6 31.4-70 70-70s70 31.4 70 70-31.4 70-70 70h-70v-70zm-138-70c38.6 0 70 31.4 70 70v70h-70c-38.6 0-70-31.4-70-70s31.4-70 70-70zM180 482V378h298v104H180zm48 68h250v308H228V550zm568 308H546V550h250v308zm48-376H546V378h298v104z"}}]},name:"gift",theme:"outlined"};function c(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){o(r,a,e[a])})}return r}function o(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var i=function(t,e){var n=c({},t,e.attrs);return f(u,c({},n,{icon:l}),null)};i.displayName="GiftOutlined";i.inheritAttrs=!1;export{i as G};
+1
View File
@@ -0,0 +1 @@
import{b as i,I as l}from"./index-BIHaUtqF.js";var u={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M832 464h-68V240c0-70.7-57.3-128-128-128H388c-70.7 0-128 57.3-128 128v224h-68c-17.7 0-32 14.3-32 32v384c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32zM332 240c0-30.9 25.1-56 56-56h248c30.9 0 56 25.1 56 56v224H332V240zm460 600H232V536h560v304zM484 701v53c0 4.4 3.6 8 8 8h40c4.4 0 8-3.6 8-8v-53a48.01 48.01 0 10-56 0z"}}]},name:"lock",theme:"outlined"};function a(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(c){return Object.getOwnPropertyDescriptor(e,c).enumerable}))),n.forEach(function(c){f(r,c,e[c])})}return r}function f(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var n=a({},t,e.attrs);return i(l,a({},n,{icon:u}),null)};o.displayName="LockOutlined";o.inheritAttrs=!1;export{o as L};
+1
View File
@@ -0,0 +1 @@
.login-container[data-v-e149ae7f]{min-height:100vh;display:flex;align-items:center;justify-content:center;background:radial-gradient(circle at 50% 20%,#1e1b4b,#0f172a 70%,#020617);padding:24px;position:relative;overflow:hidden}.login-bg-decorations[data-v-e149ae7f]{position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none;overflow:hidden}.decor-circle[data-v-e149ae7f]{position:absolute;border-radius:50%;filter:blur(80px);opacity:.15}.circle-1[data-v-e149ae7f]{width:400px;height:400px;background:#6366f1;top:10%;left:20%}.circle-2[data-v-e149ae7f]{width:350px;height:350px;background:#a855f7;bottom:10%;right:20%}.login-card[data-v-e149ae7f]{width:100%;max-width:420px;background:#fff;border-radius:16px;padding:40px 36px;box-shadow:0 25px 50px -12px #00000073,0 0 0 1px #ffffff1a;position:relative;z-index:1}.header[data-v-e149ae7f]{text-align:center;margin-bottom:32px}.logo-wrapper[data-v-e149ae7f]{width:68px;height:68px;margin:0 auto 16px;background:#eef2ff;border-radius:16px;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px #4f46e51f}.logo[data-v-e149ae7f]{font-size:34px;line-height:1}h1[data-v-e149ae7f]{font-size:20px;font-weight:700;margin:0;color:#0f172a;letter-spacing:-.3px}p[data-v-e149ae7f]{font-size:13px;color:#64748b;margin-top:6px;margin-bottom:0}.login-submit-btn[data-v-e149ae7f]{height:44px;font-size:15px;font-weight:600;border-radius:8px;background:#4f46e5;box-shadow:0 4px 14px #4f46e559;transition:all .2s ease}.login-submit-btn[data-v-e149ae7f]:hover{background:#4338ca!important;box-shadow:0 6px 18px #4f46e573}
+1
View File
@@ -0,0 +1 @@
import{d as w,u as b,c as y,a,b as o,w as t,r as l,o as h,e as d,f as x,g as k,h as V,i as L,j as S,_ as z}from"./index-BIHaUtqF.js";import{M as B}from"./MailOutlined-KzUsXxyh.js";import{L as C}from"./LockOutlined-Dc6AcXiY.js";const M={class:"login-container"},N={class:"login-card"},q=w({__name:"LoginView",setup(A){const c=L(),u=b(),s=k({email:"admin@luckywheel.local",password:"Admin@SecurePass2026"}),r=V(!1);async function m(){r.value=!0;try{await u.login({email:s.email,password:s.password}),c.push(S()?"/m/dashboard":"/dashboard")}catch{}finally{r.value=!1}}return(p,e)=>{const _=l("a-input"),n=l("a-form-item"),f=l("a-input-password"),g=l("a-button"),v=l("a-form");return h(),y("div",M,[e[4]||(e[4]=a("div",{class:"login-bg-decorations"},[a("div",{class:"decor-circle circle-1"}),a("div",{class:"decor-circle circle-2"})],-1)),a("div",N,[e[3]||(e[3]=a("div",{class:"header"},[a("div",{class:"logo-wrapper"},[a("span",{class:"logo"},"🎡")]),a("h1",null,"ورود به پنل مدیریت"),a("p",null,"سامانه گردونه شانس و قرعه‌کشی")],-1)),o(v,{model:s,layout:"vertical",onFinish:m},{default:t(()=>[o(n,{label:"ایمیل مدیریت",name:"email",rules:[{required:!0,message:"لطفاً ایمیل را وارد کنید"},{type:"email",message:"فرمت ایمیل نامعتبر است"}]},{default:t(()=>[o(_,{value:s.email,"onUpdate:value":e[0]||(e[0]=i=>s.email=i),size:"large",placeholder:"admin@example.com"},{prefix:t(()=>[o(d(B),{style:{color:"#6366f1"}})]),_:1},8,["value"])]),_:1}),o(n,{label:"کلمه عبور",name:"password",rules:[{required:!0,message:"لطفاً کلمه عبور را وارد کنید"}]},{default:t(()=>[o(f,{value:s.password,"onUpdate:value":e[1]||(e[1]=i=>s.password=i),size:"large",placeholder:"••••••••"},{prefix:t(()=>[o(d(C),{style:{color:"#6366f1"}})]),_:1},8,["value"])]),_:1}),o(n,{style:{"margin-top":"28px"}},{default:t(()=>[o(g,{type:"primary","html-type":"submit",size:"large",block:"",loading:r.value,class:"login-submit-btn"},{default:t(()=>[...e[2]||(e[2]=[x(" ورود به پنل ",-1)])]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"])])])}}}),j=z(q,[["__scopeId","data-v-e149ae7f"]]);export{j as default};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.m-form-page[data-v-ccc89cd2]{padding:14px 14px 28px;direction:rtl}.m-steps[data-v-ccc89cd2]{display:flex;gap:6px;margin-bottom:14px}.m-step[data-v-ccc89cd2]{flex:1;text-align:center;font-size:11px;font-weight:700;padding:10px 4px;border-radius:10px;background:#e2e8f0;color:#64748b;display:flex;align-items:center;justify-content:center;gap:4px;transition:all .2s ease}.m-step.active[data-v-ccc89cd2]{background:#4f46e5;color:#fff}.m-step.done[data-v-ccc89cd2]{background:#c7d2fe;color:#3730a3}.m-card[data-v-ccc89cd2]{background:#fff;border-radius:16px;padding:18px 16px;border:1px solid #f1f5f9;margin-bottom:16px;box-shadow:0 1px 3px #00000008}.m-radio-full[data-v-ccc89cd2]{display:flex;width:100%}.m-radio-full[data-v-ccc89cd2] .ant-radio-button-wrapper{flex:1;text-align:center}.m-super-admin-card[data-v-ccc89cd2]{background:#fdf4ff;border:1px solid #f0abfc;border-radius:12px;padding:14px;margin-bottom:16px;transition:all .2s ease}.m-super-admin-card.is-selected[data-v-ccc89cd2]{background:#fae8ff;border-color:#d946ef;box-shadow:0 0 0 2px #d946ef26}.m-super-title[data-v-ccc89cd2]{font-weight:700;font-size:14px;color:#86198f}.m-super-desc[data-v-ccc89cd2]{font-size:11.5px;color:#701a75;margin:6px 24px 0 0;line-height:1.5}.m-section-label[data-v-ccc89cd2]{font-size:13px;font-weight:700;color:#334155;margin-bottom:10px}.m-roles-list[data-v-ccc89cd2]{display:flex;flex-direction:column;gap:8px}.m-role-pill[data-v-ccc89cd2]{padding:12px;border:1px solid #e2e8f0;border-radius:12px;background:#f8fafc;cursor:pointer;transition:all .2s ease}.m-role-pill.is-active[data-v-ccc89cd2]{border-color:#4f46e5;background:#eef2ff}.m-role-pill-top[data-v-ccc89cd2]{display:flex;align-items:center;gap:8px}.m-role-name[data-v-ccc89cd2]{font-weight:700;font-size:13.5px;color:#1e293b}.m-role-desc[data-v-ccc89cd2]{margin:4px 28px 0 0;font-size:11.5px;color:#64748b;line-height:1.5}.m-form-actions[data-v-ccc89cd2]{display:flex;flex-direction:column;gap:10px;margin-top:10px}.m-form-actions[data-v-ccc89cd2] .ant-btn{height:48px;border-radius:14px;font-weight:700;font-size:14px}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.m-admin-page[data-v-2848d12c]{padding:12px}.m-search-box[data-v-2848d12c]{margin-bottom:10px}.m-add-bar[data-v-2848d12c]{margin-bottom:12px}.m-cards-list[data-v-2848d12c]{display:flex;flex-direction:column;gap:12px}.m-admin-card[data-v-2848d12c]{background:#fff;border-radius:10px;border:1px solid #e2e8f0;padding:12px;box-shadow:0 1px 2px #0000000a}.m-card-header[data-v-2848d12c]{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #f1f5f9;padding-bottom:8px;margin-bottom:8px}.m-user-info[data-v-2848d12c]{display:flex;align-items:center;gap:8px}.m-name-box[data-v-2848d12c]{display:flex;flex-direction:column}.m-name[data-v-2848d12c]{font-weight:600;font-size:13px;color:#1e293b}.m-email[data-v-2848d12c]{font-size:11px;color:#94a3b8;direction:ltr}.m-status-tag[data-v-2848d12c]{border-radius:10px;font-size:11px}.m-card-body[data-v-2848d12c]{display:flex;flex-direction:column;gap:6px;font-size:12px}.m-info-row[data-v-2848d12c]{display:flex;align-items:center;justify-content:space-between}.m-info-label[data-v-2848d12c]{color:#64748b}.m-info-val[data-v-2848d12c]{font-weight:500;color:#334155;direction:ltr}.m-tags-wrap[data-v-2848d12c]{display:flex;flex-wrap:wrap;gap:2px}.m-card-actions[data-v-2848d12c]{display:flex;align-items:center;gap:6px;margin-top:10px;padding-top:8px;border-top:1px dashed #e2e8f0}.m-pagination[data-v-2848d12c]{display:flex;justify-content:center;margin:16px 0}.m-empty[data-v-2848d12c]{text-align:center;padding:40px;color:#94a3b8}
+1
View File
@@ -0,0 +1 @@
.m-page[data-v-5a9f7e17]{padding:12px 14px 24px}.m-filters[data-v-5a9f7e17]{display:flex;gap:8px;margin-bottom:12px}.m-filters[data-v-5a9f7e17] .ant-select-selector{border-radius:12px!important;min-height:42px!important}.m-icon-btn[data-v-5a9f7e17]{width:42px;height:42px;border:none;border-radius:12px;background:#fff;color:#4f46e5}.m-empty[data-v-5a9f7e17]{text-align:center;color:#94a3b8;padding:40px}.m-card[data-v-5a9f7e17]{background:#fff;border-radius:14px;padding:12px;margin-bottom:8px;border:1px solid #f1f5f9}.m-card__top[data-v-5a9f7e17]{display:flex;justify-content:space-between;gap:8px;margin-bottom:8px}.m-card__admin[data-v-5a9f7e17]{font-weight:800;font-size:13px;color:#0f172a}.m-card__entity[data-v-5a9f7e17]{font-size:11px;color:#94a3b8;margin-top:2px;direction:ltr;text-align:right}.m-card__meta[data-v-5a9f7e17]{display:flex;justify-content:space-between;font-size:11px;color:#94a3b8}.m-detail[data-v-5a9f7e17]{margin-top:10px;width:100%;border:1px solid #e2e8f0;background:#f8fafc;border-radius:10px;padding:10px;font-weight:700;font-size:12px;color:#334155;min-height:42px}.m-pager[data-v-5a9f7e17]{display:flex;justify-content:center;padding:12px 0}.detail__row[data-v-5a9f7e17]{display:flex;justify-content:space-between;margin-bottom:8px;font-size:13px;color:#64748b}.detail__row strong[data-v-5a9f7e17]{color:#0f172a}.detail h4[data-v-5a9f7e17]{margin:16px 0 8px;font-size:13px}.detail pre[data-v-5a9f7e17]{background:#0f172a;color:#e2e8f0;border-radius:12px;padding:12px;font-size:11px;overflow:auto;direction:ltr;text-align:left;max-height:180px}
+1
View File
@@ -0,0 +1 @@
import{t as U}from"./farsi-number-stB7JMN0.js";import{d as x,l as L,c as _,a as t,b as a,w as o,e as S,R as I,h as f,g as R,n as J,r as c,o as m,f as p,x as A,F as M,z as P,t as s,J as B,_ as F}from"./index-BIHaUtqF.js";const H={class:"m-page"},G={class:"m-filters"},$={key:0,class:"m-empty"},j={class:"m-card__top"},q={class:"m-card__admin"},K={class:"m-card__entity"},Q={class:"m-card__meta"},W={dir:"ltr"},X=["onClick"],Y={key:1,class:"m-pager"},Z={key:0,class:"detail"},h={class:"detail__row"},ee={class:"detail__row"},te={class:"detail__row"},ne=x({__name:"MAuditLogView",setup(ae){const v=f(!1),T=f([]),w=f(0),z=f(!1),u=f(null),l=R({page:1,pageSize:10,entityType:void 0});async function g(){var i,e,d,y;v.value=!0;try{const r=await J.get("/admin/audit-logs",{params:{page:l.page,pageSize:l.pageSize,entityType:l.entityType||void 0}}),C=Array.isArray(r.data)?r.data:((i=r.data)==null?void 0:i.data)||[];T.value=C,w.value=((e=r.meta)==null?void 0:e.total)||((y=(d=r.data)==null?void 0:d.meta)==null?void 0:y.total)||C.length}finally{v.value=!1}}function k(i){u.value=i,z.value=!0}function V(i){const e=(i||"").toUpperCase();return e.includes("CREATE")||e.includes("ADD")?"green":e.includes("UPDATE")||e.includes("EDIT")||e.includes("PATCH")?"blue":e.includes("DELETE")||e.includes("REMOVE")?"red":e.includes("LOGIN")||e.includes("AUTH")?"purple":"default"}function b(i){return i?U(new Date(i).toLocaleDateString("fa-IR",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"})):"—"}return L(g),(i,e)=>{const d=c("a-select-option"),y=c("a-select"),r=c("a-tag"),C=c("a-pagination"),N=c("a-spin"),O=c("a-drawer");return m(),_("div",H,[t("div",G,[a(y,{value:l.entityType,"onUpdate:value":e[0]||(e[0]=n=>l.entityType=n),"allow-clear":"",placeholder:"نوع موجودیت",style:{flex:"1"},onChange:e[1]||(e[1]=()=>{l.page=1,g()})},{default:o(()=>[a(d,{value:"Campaign"},{default:o(()=>[...e[4]||(e[4]=[p("کمپین",-1)])]),_:1}),a(d,{value:"Prize"},{default:o(()=>[...e[5]||(e[5]=[p("جایزه",-1)])]),_:1}),a(d,{value:"User"},{default:o(()=>[...e[6]||(e[6]=[p("کاربر",-1)])]),_:1}),a(d,{value:"Chance"},{default:o(()=>[...e[7]||(e[7]=[p("شانس",-1)])]),_:1}),a(d,{value:"UserPrize"},{default:o(()=>[...e[8]||(e[8]=[p("جایزه کاربر",-1)])]),_:1})]),_:1},8,["value"]),t("button",{type:"button",class:"m-icon-btn",onClick:g},[a(S(I),{spin:v.value},null,8,["spin"])])]),a(N,{spinning:v.value},{default:o(()=>[!T.value.length&&!v.value?(m(),_("div",$,"لاگی یافت نشد")):A("",!0),(m(!0),_(M,null,P(T.value,n=>{var D,E;return m(),_("div",{key:n.id,class:"m-card"},[t("div",j,[t("div",null,[t("div",q,s(((D=n.admin)==null?void 0:D.name)||((E=n.admin)==null?void 0:E.email)||"سیستم"),1),t("div",K,s(n.entityType)+" · "+s(n.entityId||"—"),1)]),a(r,{color:V(n.action)},{default:o(()=>[p(s(n.action),1)]),_:2},1032,["color"])]),t("div",Q,[t("span",W,s(n.ipAddress||"—"),1),t("span",null,s(b(n.createdAt)),1)]),t("button",{type:"button",class:"m-detail",onClick:se=>k(n)},[a(S(B)),e[9]||(e[9]=p(" جزئیات JSON ",-1))],8,X)])}),128)),w.value>l.pageSize?(m(),_("div",Y,[a(C,{current:l.page,"onUpdate:current":e[2]||(e[2]=n=>l.page=n),total:w.value,"page-size":l.pageSize,size:"small",onChange:g},null,8,["current","total","page-size"])])):A("",!0)]),_:1},8,["spinning"]),a(O,{open:z.value,"onUpdate:open":e[3]||(e[3]=n=>z.value=n),placement:"bottom",height:"70%",title:"جزئیات لاگ"},{default:o(()=>{var n;return[u.value?(m(),_("div",Z,[t("div",h,[e[10]||(e[10]=t("span",null,"ادمین",-1)),t("strong",null,s(((n=u.value.admin)==null?void 0:n.name)||"—"),1)]),t("div",ee,[e[11]||(e[11]=t("span",null,"اکشن",-1)),t("strong",null,s(u.value.action),1)]),t("div",te,[e[12]||(e[12]=t("span",null,"موجودیت",-1)),t("strong",null,s(u.value.entityType),1)]),e[13]||(e[13]=t("h4",null,"قبل",-1)),t("pre",null,s(JSON.stringify(u.value.oldValues||{},null,2)),1),e[14]||(e[14]=t("h4",null,"بعد",-1)),t("pre",null,s(JSON.stringify(u.value.newValues||{},null,2)),1)])):A("",!0)]}),_:1},8,["open"])])}}}),oe=F(ne,[["__scopeId","data-v-5a9f7e17"]]);export{oe as default};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.m-form-page[data-v-e36aa9f0]{padding:12px 14px 24px}.m-steps[data-v-e36aa9f0]{display:flex;gap:6px;margin-bottom:14px}.m-step[data-v-e36aa9f0]{flex:1;text-align:center;font-size:11px;font-weight:700;padding:8px 4px;border-radius:10px;background:#e2e8f0;color:#64748b}.m-step.active[data-v-e36aa9f0]{background:#4f46e5;color:#fff}.m-step.done[data-v-e36aa9f0]{background:#c7d2fe;color:#3730a3}.m-card[data-v-e36aa9f0]{background:#fff;border-radius:16px;padding:16px;border:1px solid #f1f5f9;margin-bottom:14px}.m-wheel-grid[data-v-e36aa9f0]{display:grid;grid-template-columns:1fr 1fr;gap:8px}.m-wheel-opt[data-v-e36aa9f0]{border:1px solid #e2e8f0;background:#f8fafc;border-radius:12px;padding:12px 8px;font-size:12px;font-weight:700;color:#334155;min-height:48px}.m-wheel-opt.active[data-v-e36aa9f0]{border-color:#4f46e5;background:#eef2ff;color:#4f46e5}.m-form-actions[data-v-e36aa9f0]{display:flex;flex-direction:column;gap:8px}.m-form-actions[data-v-e36aa9f0] .ant-btn{height:48px;border-radius:14px;font-weight:700}
+1
View File
@@ -0,0 +1 @@
.m-page[data-v-bad48e3c]{padding:12px 14px 88px;position:relative}.m-search-bar[data-v-bad48e3c]{display:flex;gap:8px;margin-bottom:12px}.m-search-bar[data-v-bad48e3c] .ant-input-affix-wrapper,.m-search-bar[data-v-bad48e3c] .ant-select-selector{border-radius:12px!important;min-height:42px!important}.m-empty[data-v-bad48e3c]{text-align:center;color:#94a3b8;padding:48px 16px;font-size:13px}.m-camp-card[data-v-bad48e3c]{background:#fff;border-radius:16px;padding:14px;margin-bottom:10px;border:1px solid #f1f5f9;box-shadow:0 2px 10px #0f172a0a}.m-camp-card__top[data-v-bad48e3c]{display:flex;justify-content:space-between;gap:10px;margin-bottom:8px}.m-camp-card__title[data-v-bad48e3c]{font-size:15px;font-weight:800;color:#0f172a}.m-camp-card__slug[data-v-bad48e3c]{font-size:11px;color:#94a3b8;direction:ltr;text-align:right;margin-top:2px}.m-status[data-v-bad48e3c]{font-size:11px;font-weight:700;padding:4px 10px;border-radius:999px;white-space:nowrap;height:fit-content}.m-camp-card__meta[data-v-bad48e3c]{display:flex;flex-direction:column;gap:4px;font-size:11px;color:#64748b;margin-bottom:12px}.m-camp-card__actions[data-v-bad48e3c]{display:flex;gap:6px}.m-action[data-v-bad48e3c]{flex:1;border:1px solid #e2e8f0;background:#f8fafc;border-radius:10px;padding:10px 6px;font-size:11px;font-weight:700;color:#334155;display:inline-flex;align-items:center;justify-content:center;gap:4px;min-height:42px}.m-pager[data-v-bad48e3c]{display:flex;justify-content:center;padding:12px 0}.m-fab[data-v-bad48e3c]{position:fixed;left:18px;bottom:calc(80px + env(safe-area-inset-bottom,0px));width:56px;height:56px;border:none;border-radius:18px;background:linear-gradient(135deg,#4f46e5,#7c3aed);color:#fff;font-size:22px;box-shadow:0 10px 24px #4f46e566;z-index:90}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.m-dash[data-v-d7576723]{padding:12px 14px 24px}.m-dash__toolbar[data-v-d7576723]{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.m-dash__live[data-v-d7576723]{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;color:#059669;background:#ecfdf5;border-radius:999px;padding:4px 10px}.pulse[data-v-d7576723]{width:7px;height:7px;border-radius:50%;background:#10b981;box-shadow:0 0 #10b98180;animation:pulse-d7576723 1.6s infinite}@keyframes pulse-d7576723{70%{box-shadow:0 0 0 8px #10b98100}to{box-shadow:0 0 #10b98100}}.m-dash__sub[data-v-d7576723]{margin:8px 2px 0;color:#64748b;font-size:12px}.m-icon-btn[data-v-d7576723]{width:42px;height:42px;border:none;border-radius:12px;background:#fff;color:#4f46e5;box-shadow:0 2px 10px #0f172a0f;font-size:16px}.m-kpi-grid[data-v-d7576723]{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px}.m-kpi[data-v-d7576723]{background:#fff;border-radius:16px;padding:14px;box-shadow:0 2px 12px #0f172a0a;border:1px solid #f1f5f9}.m-kpi__head[data-v-d7576723]{display:flex;justify-content:space-between;align-items:center;color:#64748b;font-size:12px;font-weight:600;margin-bottom:8px}.m-kpi__val[data-v-d7576723]{font-size:22px;font-weight:800;color:#0f172a;margin-bottom:4px}.m-kpi__sub[data-v-d7576723]{font-size:11px;color:#94a3b8;display:flex;align-items:center;gap:4px}.m-kpi.indigo .m-kpi__head[data-v-d7576723]{color:#4f46e5}.m-kpi.amber .m-kpi__head[data-v-d7576723]{color:#d97706}.m-kpi.emerald .m-kpi__head[data-v-d7576723]{color:#059669}.m-kpi.violet .m-kpi__head[data-v-d7576723]{color:#7c3aed}.m-quick[data-v-d7576723]{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px}.m-quick__btn[data-v-d7576723]{border:1px solid #e2e8f0;background:#fff;border-radius:14px;padding:12px 10px;font-size:12px;font-weight:700;color:#334155;display:flex;align-items:center;justify-content:center;gap:6px;min-height:48px}.m-quick__btn.primary[data-v-d7576723]{background:linear-gradient(135deg,#4f46e5,#7c3aed);border-color:transparent;color:#fff}.m-card[data-v-d7576723]{background:#fff;border-radius:16px;padding:14px;margin-bottom:12px;border:1px solid #f1f5f9;box-shadow:0 2px 12px #0f172a0a}.m-card__title[data-v-d7576723]{font-size:14px;font-weight:800;color:#0f172a;margin-bottom:10px}.m-chart[data-v-d7576723]{height:200px;width:100%}.m-winner-row[data-v-d7576723]{display:flex;justify-content:space-between;gap:10px;padding:10px 0;border-bottom:1px solid #f1f5f9}.m-winner-row[data-v-d7576723]:last-child{border-bottom:none;padding-bottom:0}.m-winner-row__name[data-v-d7576723]{font-size:13px;font-weight:700;color:#0f172a}.m-winner-row__meta[data-v-d7576723]{font-size:11px;color:#94a3b8;margin-top:2px}.m-winner-row__side[data-v-d7576723]{display:flex;flex-direction:column;align-items:flex-end;gap:4px}.m-tag[data-v-d7576723]{margin:0!important;font-size:10px!important;line-height:18px!important}.m-code[data-v-d7576723]{border:none;background:#f8fafc;color:#4f46e5;font-size:10px;font-family:monospace;border-radius:8px;padding:2px 6px;direction:ltr}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.m-forbidden-page[data-v-ef2d9064]{padding:32px 16px;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:60vh}
+1
View File
@@ -0,0 +1 @@
import{d as c,c as d,b as t,w as o,r as s,o as i,f as p,i as u,_ as l}from"./index-BIHaUtqF.js";const m={class:"m-forbidden-page"},f=c({__name:"MForbiddenView",setup(b){const a=u();function n(){a.push("/m/dashboard")}return(x,e)=>{const r=s("a-button"),_=s("a-result");return i(),d("div",m,[t(_,{status:"403",title:"۴۰۳ — دسترسی غیرمجاز","sub-title":"شما مجوز دسترسی به این بخش را ندارید."},{extra:o(()=>[t(r,{type:"primary",block:"",onClick:n},{default:o(()=>[...e[0]||(e[0]=[p(" بازگشت به داشبورد ",-1)])]),_:1})]),_:1})])}}}),w=l(f,[["__scopeId","data-v-ef2d9064"]]);export{w as default};
+1
View File
@@ -0,0 +1 @@
import{d as g,u as b,c as w,k as y,a as i,b as e,w as s,r as t,o as h,e as r,f as x,g as k,h as V,i as S,_ as L}from"./index-BIHaUtqF.js";import{M}from"./MailOutlined-KzUsXxyh.js";import{L as N}from"./LockOutlined-Dc6AcXiY.js";const z={class:"m-login"},B={class:"m-login__sheet"},C=g({__name:"MLoginView",setup(q){const m=S(),u=b(),o=k({email:"admin@luckywheel.local",password:"Admin@SecurePass2026"}),l=V(!1);async function p(){l.value=!0;try{await u.login({email:o.email,password:o.password}),m.push("/m/dashboard")}catch{}finally{l.value=!1}}return(A,a)=>{const _=t("a-input"),n=t("a-form-item"),c=t("a-input-password"),f=t("a-button"),v=t("a-form");return h(),w("div",z,[a[5]||(a[5]=y('<div class="m-login__hero" data-v-8fde6dca><div class="m-login__blob blob-1" data-v-8fde6dca></div><div class="m-login__blob blob-2" data-v-8fde6dca></div><div class="m-login__brand" data-v-8fde6dca><div class="m-login__logo" data-v-8fde6dca>🎡</div><h1 data-v-8fde6dca>گردونه شانس</h1><p data-v-8fde6dca>پنل مدیریت موبایل</p></div></div>',1)),i("div",B,[a[3]||(a[3]=i("h2",null,"ورود به حساب",-1)),a[4]||(a[4]=i("p",{class:"m-login__hint"},"با حساب ادمین وارد شوید",-1)),e(v,{model:o,layout:"vertical",onFinish:p},{default:s(()=>[e(n,{label:"ایمیل",name:"email",rules:[{required:!0,message:"ایمیل را وارد کنید"},{type:"email",message:"فرمت ایمیل نامعتبر است"}]},{default:s(()=>[e(_,{value:o.email,"onUpdate:value":a[0]||(a[0]=d=>o.email=d),size:"large",placeholder:"admin@example.com"},{prefix:s(()=>[e(r(M),{style:{color:"#6366f1"}})]),_:1},8,["value"])]),_:1}),e(n,{label:"کلمه عبور",name:"password",rules:[{required:!0,message:"کلمه عبور را وارد کنید"}]},{default:s(()=>[e(c,{value:o.password,"onUpdate:value":a[1]||(a[1]=d=>o.password=d),size:"large",placeholder:"••••••••"},{prefix:s(()=>[e(r(N),{style:{color:"#6366f1"}})]),_:1},8,["value"])]),_:1}),e(f,{type:"primary","html-type":"submit",size:"large",block:"",loading:l.value,class:"m-login__btn"},{default:s(()=>[...a[2]||(a[2]=[x(" ورود ",-1)])]),_:1},8,["loading"])]),_:1},8,["model"])])])}}}),F=L(C,[["__scopeId","data-v-8fde6dca"]]);export{F as default};
+1
View File
@@ -0,0 +1 @@
.m-login[data-v-8fde6dca]{min-height:100vh;min-height:100dvh;display:flex;flex-direction:column;background:#0f172a}.m-login__hero[data-v-8fde6dca]{position:relative;flex:0 0 auto;padding:calc(env(safe-area-inset-top,0px) + 48px) 24px 56px;overflow:hidden;background:linear-gradient(160deg,#312e81,#4f46e5 45%,#7c3aed)}.m-login__blob[data-v-8fde6dca]{position:absolute;border-radius:50%;filter:blur(40px);opacity:.35}.blob-1[data-v-8fde6dca]{width:180px;height:180px;background:#a78bfa;top:-40px;left:-30px}.blob-2[data-v-8fde6dca]{width:140px;height:140px;background:#38bdf8;bottom:20px;right:-20px}.m-login__brand[data-v-8fde6dca]{position:relative;z-index:1;text-align:center;color:#fff}.m-login__logo[data-v-8fde6dca]{width:72px;height:72px;margin:0 auto 14px;border-radius:20px;background:#ffffff2e;display:flex;align-items:center;justify-content:center;font-size:36px;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.m-login__brand h1[data-v-8fde6dca]{margin:0 0 6px;font-size:24px;font-weight:800}.m-login__brand p[data-v-8fde6dca]{margin:0;opacity:.85;font-size:13px}.m-login__sheet[data-v-8fde6dca]{flex:1;margin-top:-28px;background:#fff;border-radius:28px 28px 0 0;padding:28px 20px calc(24px + env(safe-area-inset-bottom,0px));position:relative;z-index:2}.m-login__sheet h2[data-v-8fde6dca]{margin:0 0 4px;font-size:20px;font-weight:800;color:#0f172a}.m-login__hint[data-v-8fde6dca]{margin:0 0 22px;color:#64748b;font-size:13px}.m-login__btn[data-v-8fde6dca]{height:48px!important;border-radius:14px!important;font-weight:700!important;margin-top:8px;background:linear-gradient(135deg,#4f46e5,#7c3aed)!important;border:none!important}[data-v-8fde6dca] .ant-input-affix-wrapper,[data-v-8fde6dca] .ant-input{border-radius:12px!important}
+1
View File
@@ -0,0 +1 @@
.m-page[data-v-e6ec264a]{padding:12px 14px 88px}.m-banner[data-v-e6ec264a]{display:flex;justify-content:space-between;align-items:center;background:#fff;border-radius:16px;padding:14px;margin-bottom:12px;border:1px solid #f1f5f9}.m-banner__title[data-v-e6ec264a]{font-weight:800;font-size:15px;color:#0f172a}.m-banner__sub[data-v-e6ec264a]{font-size:11px;color:#94a3b8;margin-top:2px}.m-icon-btn[data-v-e6ec264a]{width:40px;height:40px;border:none;border-radius:12px;background:#eef2ff;color:#4f46e5;font-size:15px}.m-empty[data-v-e6ec264a]{text-align:center;color:#94a3b8;padding:40px 12px}.m-prize-card[data-v-e6ec264a]{display:flex;gap:0;background:#fff;border-radius:16px;overflow:hidden;margin-bottom:10px;border:1px solid #f1f5f9}.m-prize-card__color[data-v-e6ec264a]{width:8px;flex-shrink:0}.m-prize-card__body[data-v-e6ec264a]{flex:1;padding:12px}.m-prize-card__top[data-v-e6ec264a]{display:flex;justify-content:space-between;gap:8px;margin-bottom:6px}.m-prize-card__title[data-v-e6ec264a]{font-weight:800;font-size:14px;color:#0f172a}.m-prize-card__meta[data-v-e6ec264a]{font-size:11px;color:#64748b;margin-bottom:4px}.m-prize-card__actions[data-v-e6ec264a]{display:flex;gap:6px;margin-top:10px}.m-action[data-v-e6ec264a]{flex:1;border:1px solid #e2e8f0;background:#f8fafc;border-radius:10px;padding:10px;font-size:11px;font-weight:700;color:#334155;min-height:42px}.m-action.danger[data-v-e6ec264a]{color:#ef4444;flex:0 0 48px}.m-fab[data-v-e6ec264a]{position:fixed;left:18px;bottom:calc(80px + env(safe-area-inset-bottom,0px));width:56px;height:56px;border:none;border-radius:18px;background:linear-gradient(135deg,#4f46e5,#7c3aed);color:#fff;font-size:22px;box-shadow:0 10px 24px #4f46e566;z-index:90}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{f as n}from"./farsi-number-stB7JMN0.js";import{d as B,l as N,c as w,a as s,b as e,w as v,e as t,R as S,h as u,g as z,n as U,r as m,o as _,F as A,z as F,U as x,t as o,q as M,T as G,y as q,f as D,_ as E}from"./index-BIHaUtqF.js";import{T as I}from"./ThunderboltOutlined-x4094hHm.js";import{G as L}from"./GiftOutlined-CRB-YJuc.js";const j={class:"m-page"},H={class:"m-filters"},J={class:"m-kpi-grid"},K={class:"m-kpi"},P={class:"val"},Q={class:"m-kpi"},W={class:"val"},X={class:"m-kpi"},Y={class:"val"},Z={class:"m-kpi"},$={class:"val"},ss={class:"m-kpi"},as={class:"val"},ts={class:"m-kpi"},es={class:"val"},is={class:"m-kpi wide"},ls={class:"val"},os=B({__name:"MReportView",setup(ns){const c=u(!1),i=u(null),f=u([]),r=z({campaignId:void 0});async function O(){var a;const d=await U.get("/admin/campaigns",{params:{pageSize:100}});f.value=Array.isArray(d.data)?d.data:((a=d.data)==null?void 0:a.data)||[]}async function p(){c.value=!0;try{const d=await U.get("/admin/reports/participation",{params:{campaignId:r.campaignId||void 0}});i.value=d.data}finally{c.value=!1}}return N(()=>{O(),p()}),(d,a)=>{const T=m("a-select-option"),R=m("a-select"),V=m("a-spin");return _(),w("div",j,[s("div",H,[e(R,{value:r.campaignId,"onUpdate:value":a[0]||(a[0]=l=>r.campaignId=l),"allow-clear":"",placeholder:"همه کمپین‌ها",style:{flex:"1"},onChange:p},{default:v(()=>[(_(!0),w(A,null,F(f.value,l=>(_(),q(T,{key:l.id,value:l.id},{default:v(()=>[D(o(l.title),1)]),_:2},1032,["value"]))),128))]),_:1},8,["value"]),s("button",{type:"button",class:"m-icon-btn",onClick:p},[e(t(S),{spin:c.value},null,8,["spin"])])]),e(V,{spinning:c.value},{default:v(()=>{var l,g,b,k,y,h,C;return[s("div",J,[s("div",K,[e(t(x),{class:"ico indigo"}),s("div",P,o(t(n)(((l=i.value)==null?void 0:l.totalUsers)||0)),1),a[1]||(a[1]=s("div",{class:"lbl"},"کل کاربران",-1))]),s("div",Q,[e(t(I),{class:"ico amber"}),s("div",W,o(t(n)(((g=i.value)==null?void 0:g.totalSpins)||0)),1),a[2]||(a[2]=s("div",{class:"lbl"},"کل اسپین",-1))]),s("div",X,[e(t(M),{class:"ico emerald"}),s("div",Y,o(t(n)(((b=i.value)==null?void 0:b.winnersCount)||0)),1),a[3]||(a[3]=s("div",{class:"lbl"},"برندگان",-1))]),s("div",Z,[e(t(L),{class:"ico violet"}),s("div",$,o(t(n)(((k=i.value)==null?void 0:k.totalChancesAllocated)||0)),1),a[4]||(a[4]=s("div",{class:"lbl"},"شانس تخصیص",-1))]),s("div",ss,[e(t(G),{class:"ico blue"}),s("div",as,o(t(n)(((y=i.value)==null?void 0:y.totalChancesUsed)||0)),1),a[5]||(a[5]=s("div",{class:"lbl"},"شانس مصرف",-1))]),s("div",ts,[e(t(x),{class:"ico slate"}),s("div",es,o(t(n)(((h=i.value)==null?void 0:h.zeroSpinUsers)||0)),1),a[6]||(a[6]=s("div",{class:"lbl"},"بدون اسپین",-1))]),s("div",is,[e(t(I),{class:"ico indigo"}),s("div",ls,o(t(n)(((C=i.value)==null?void 0:C.remainingChanceUsers)||0)),1),a[7]||(a[7]=s("div",{class:"lbl"},"کاربران با شانس باقی‌مانده",-1))])])]}),_:1},8,["spinning"])])}}}),vs=E(os,[["__scopeId","data-v-4777863b"]]);export{vs as default};
+1
View File
@@ -0,0 +1 @@
.m-page[data-v-4777863b]{padding:12px 14px 24px}.m-filters[data-v-4777863b]{display:flex;gap:8px;margin-bottom:12px}.m-filters[data-v-4777863b] .ant-select-selector{border-radius:12px!important;min-height:42px!important}.m-icon-btn[data-v-4777863b]{width:42px;height:42px;border:none;border-radius:12px;background:#fff;color:#4f46e5}.m-kpi-grid[data-v-4777863b]{display:grid;grid-template-columns:1fr 1fr;gap:10px}.m-kpi[data-v-4777863b]{background:#fff;border-radius:16px;padding:16px 12px;text-align:center;border:1px solid #f1f5f9;box-shadow:0 2px 10px #0f172a0a}.m-kpi.wide[data-v-4777863b]{grid-column:1 / -1}.ico[data-v-4777863b]{font-size:18px;margin-bottom:8px}.ico.indigo[data-v-4777863b]{color:#4f46e5}.ico.amber[data-v-4777863b]{color:#d97706}.ico.emerald[data-v-4777863b]{color:#059669}.ico.violet[data-v-4777863b]{color:#7c3aed}.ico.blue[data-v-4777863b]{color:#2563eb}.ico.slate[data-v-4777863b]{color:#64748b}.val[data-v-4777863b]{font-size:22px;font-weight:800;color:#0f172a}.lbl[data-v-4777863b]{font-size:11px;color:#64748b;margin-top:4px;font-weight:600}
+1
View File
@@ -0,0 +1 @@
.m-roles-page[data-v-0735d43a]{padding:12px}.m-role-selector-bar[data-v-0735d43a]{display:flex;gap:8px;margin-bottom:12px}.m-save-bar[data-v-0735d43a]{margin-bottom:12px}.m-notice[data-v-0735d43a]{background:#f5f3ff;border:1px solid #ddd6fe;border-radius:8px;padding:10px 12px;display:flex;align-items:center;gap:8px;font-size:12px;color:#6b7280;margin-bottom:12px}.m-modules-list[data-v-0735d43a]{display:flex;flex-direction:column;gap:10px}.m-mod-card[data-v-0735d43a]{background:#fff;border:1px solid #e2e8f0;border-radius:8px;overflow:hidden}.m-mod-title[data-v-0735d43a]{background:#f8fafc;padding:8px 12px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #e2e8f0;font-size:13px}.m-mod-count[data-v-0735d43a]{font-size:11px;color:#64748b}.m-mod-perms[data-v-0735d43a]{padding:6px 12px}.m-perm-row[data-v-0735d43a]{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid #f8fafc}.m-perm-row[data-v-0735d43a]:last-child{border-bottom:none}.m-p-name[data-v-0735d43a]{font-size:13px;color:#1e293b}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{t as k}from"./farsi-number-stB7JMN0.js";import{b as o,I as P,d as V,l as B,c as p,a as u,w as f,e as E,R as j,h as O,g as R,n as I,r as v,o as m,f as y,x as w,F as T,z as $,t as g,B as Q,_ as q}from"./index-BIHaUtqF.js";var G={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M931.4 498.9L94.9 79.5c-3.4-1.7-7.3-2.1-11-1.2a15.99 15.99 0 00-11.7 19.3l86.2 352.2c1.3 5.3 5.2 9.6 10.4 11.3l147.7 50.7-147.6 50.7c-5.2 1.8-9.1 6-10.3 11.3L72.2 926.5c-.9 3.7-.5 7.6 1.2 10.9 3.9 7.9 13.5 11.1 21.5 7.2l836.5-417c3.1-1.5 5.6-4.1 7.2-7.1 3.9-8 .7-17.6-7.2-21.6zM170.8 826.3l50.3-205.6 295.2-101.3c2.3-.8 4.2-2.6 5-5 1.4-4.2-.8-8.7-5-10.2L221.1 403 171 198.2l628 314.9-628.2 313.2z"}}]},name:"send",theme:"outlined"};function M(l){for(var a=1;a<arguments.length;a++){var s=arguments[a]!=null?Object(arguments[a]):{},r=Object.keys(s);typeof Object.getOwnPropertySymbols=="function"&&(r=r.concat(Object.getOwnPropertySymbols(s).filter(function(c){return Object.getOwnPropertyDescriptor(s,c).enumerable}))),r.forEach(function(c){H(l,c,s[c])})}return l}function H(l,a,s){return a in l?Object.defineProperty(l,a,{value:s,enumerable:!0,configurable:!0,writable:!0}):l[a]=s,l}var z=function(a,s){var r=M({},a,s.attrs);return o(P,M({},r,{icon:G}),null)};z.displayName="SendOutlined";z.inheritAttrs=!1;const J={class:"m-page"},W={class:"m-filters"},X={class:"m-filters__row"},Y={key:0,class:"m-empty"},Z={class:"m-card__top"},K={class:"mobile",dir:"ltr"},ee={class:"m-card__msg"},te={class:"m-card__meta"},ae={key:0,class:"m-card__err"},se=["disabled","onClick"],ne={key:1,class:"m-pager"},oe=V({__name:"MSmsLogView",setup(l){const a=O(!1),s=O([]),r=O(0),c=O(null),n=R({page:1,pageSize:10,mobile:"",status:void 0}),D={SENT:{text:"ارسال شد",color:"success"},QUEUED:{text:"صف",color:"processing"},FAILED:{text:"ناموفق",color:"error"}};async function b(){var i,t,S,_;a.value=!0;try{const d=await I.get("/admin/sms-logs",{params:{page:n.page,pageSize:n.pageSize,mobile:n.mobile||void 0,status:n.status||void 0}}),h=Array.isArray(d.data)?d.data:((i=d.data)==null?void 0:i.data)||[];s.value=h,r.value=((t=d.meta)==null?void 0:t.total)||((_=(S=d.data)==null?void 0:S.meta)==null?void 0:_.total)||h.length}finally{a.value=!1}}async function A(i){c.value=i;try{await I.post(`/admin/sms-logs/${i}/resend`),Q.success("ارسال مجدد انجام شد"),b()}finally{c.value=null}}function L(){n.page=1,b()}function N(i){return i?k(new Date(i).toLocaleDateString("fa-IR",{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"})):"—"}return B(b),(i,t)=>{const S=v("a-input"),_=v("a-select-option"),d=v("a-select"),h=v("a-tag"),U=v("a-pagination"),F=v("a-spin");return m(),p("div",J,[u("div",W,[o(S,{value:n.mobile,"onUpdate:value":t[0]||(t[0]=e=>n.mobile=e),"allow-clear":"",placeholder:"شماره موبایل...",onPressEnter:L},null,8,["value"]),u("div",X,[o(d,{value:n.status,"onUpdate:value":t[1]||(t[1]=e=>n.status=e),"allow-clear":"",placeholder:"وضعیت",style:{flex:"1"},onChange:L},{default:f(()=>[o(_,{value:"SENT"},{default:f(()=>[...t[3]||(t[3]=[y("ارسال شد",-1)])]),_:1}),o(_,{value:"QUEUED"},{default:f(()=>[...t[4]||(t[4]=[y("صف",-1)])]),_:1}),o(_,{value:"FAILED"},{default:f(()=>[...t[5]||(t[5]=[y("ناموفق",-1)])]),_:1})]),_:1},8,["value"]),u("button",{type:"button",class:"m-icon-btn",onClick:b},[o(E(j),{spin:a.value},null,8,["spin"])])])]),o(F,{spinning:a.value},{default:f(()=>[!s.value.length&&!a.value?(m(),p("div",Y,"لاگی یافت نشد")):w("",!0),(m(!0),p(T,null,$(s.value,e=>{var C;return m(),p("div",{key:e.id,class:"m-card"},[u("div",Z,[u("span",K,g(E(k)(e.mobile)),1),o(h,{color:((C=D[e.status])==null?void 0:C.color)||"default"},{default:f(()=>{var x;return[y(g(((x=D[e.status])==null?void 0:x.text)||e.status),1)]}),_:2},1032,["color"])]),u("div",ee,g(e.message),1),u("div",te,[u("span",null,g(e.provider||"—"),1),u("span",null,g(N(e.createdAt)),1)]),e.errorMessage?(m(),p("div",ae,g(e.errorMessage),1)):w("",!0),e.status==="FAILED"?(m(),p("button",{key:1,type:"button",class:"m-resend",disabled:c.value===e.id,onClick:x=>A(e.id)},[o(E(z)),t[6]||(t[6]=y(" ارسال مجدد ",-1))],8,se)):w("",!0)])}),128)),r.value>n.pageSize?(m(),p("div",ne,[o(U,{current:n.page,"onUpdate:current":t[2]||(t[2]=e=>n.page=e),total:r.value,"page-size":n.pageSize,size:"small",onChange:b},null,8,["current","total","page-size"])])):w("",!0)]),_:1},8,["spinning"])])}}}),ie=q(oe,[["__scopeId","data-v-9317eb8a"]]);export{ie as default};
+1
View File
@@ -0,0 +1 @@
.m-page[data-v-9317eb8a]{padding:12px 14px 24px}.m-filters[data-v-9317eb8a]{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}.m-filters__row[data-v-9317eb8a]{display:flex;gap:8px}.m-filters[data-v-9317eb8a] .ant-input,.m-filters[data-v-9317eb8a] .ant-select-selector{border-radius:12px!important;min-height:42px!important}.m-icon-btn[data-v-9317eb8a]{width:42px;height:42px;border:none;border-radius:12px;background:#fff;color:#4f46e5}.m-empty[data-v-9317eb8a]{text-align:center;color:#94a3b8;padding:40px}.m-card[data-v-9317eb8a]{background:#fff;border-radius:14px;padding:12px;margin-bottom:8px;border:1px solid #f1f5f9}.m-card__top[data-v-9317eb8a]{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.mobile[data-v-9317eb8a]{font-weight:800;color:#0f172a;font-size:13px}.m-card__msg[data-v-9317eb8a]{font-size:12px;color:#334155;line-height:1.6;margin-bottom:8px}.m-card__meta[data-v-9317eb8a]{display:flex;justify-content:space-between;font-size:11px;color:#94a3b8}.m-card__err[data-v-9317eb8a]{margin-top:8px;font-size:11px;color:#ef4444;background:#fef2f2;padding:8px;border-radius:8px}.m-resend[data-v-9317eb8a]{margin-top:10px;width:100%;border:none;background:#eef2ff;color:#4f46e5;border-radius:10px;padding:10px;font-weight:700;font-size:12px;min-height:42px}.m-pager[data-v-9317eb8a]{display:flex;justify-content:center;padding:12px 0}
+1
View File
@@ -0,0 +1 @@
import{t as A}from"./farsi-number-stB7JMN0.js";import{d as O,l as U,c as r,a as s,b as c,w as v,e as f,R as E,h,g as W,n as V,r as p,o,S as $,F as D,z as B,x as L,i as P,y as T,f as j,t as l,O as q,_ as G}from"./index-BIHaUtqF.js";const H={class:"m-page"},J={class:"m-filters"},K={class:"m-filters__row"},Q={key:0,class:"m-empty"},X={class:"m-card__top"},Y=["onClick"],Z={class:"m-card__line"},aa={class:"m-card__line"},ea={class:"m-card__meta"},ta={dir:"ltr"},sa={key:1,class:"m-pager"},na=O({__name:"MSpinListView",setup(ia){const N=P(),d=h(!1),y=h([]),S=h(0),z=h([]),e=W({page:1,pageSize:10,campaignId:void 0,search:""});async function R(){var t;const n=await V.get("/admin/campaigns",{params:{pageSize:100}});z.value=Array.isArray(n.data)?n.data:((t=n.data)==null?void 0:t.data)||[]}async function u(){var n,t,_,m;d.value=!0;try{const i=await V.get("/admin/spins",{params:{page:e.page,pageSize:e.pageSize,campaignId:e.campaignId||void 0,search:e.search||void 0}}),g=Array.isArray(i.data)?i.data:((n=i.data)==null?void 0:n.data)||[];y.value=g,S.value=((t=i.meta)==null?void 0:t.total)||((m=(_=i.data)==null?void 0:_.meta)==null?void 0:m.total)||g.length}finally{d.value=!1}}function C(){e.page=1,u()}function F(n){return n?A(new Date(n).toLocaleDateString("fa-IR",{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"})):"—"}return U(()=>{R(),u()}),(n,t)=>{const _=p("a-input"),m=p("a-select-option"),i=p("a-select"),g=p("a-pagination"),M=p("a-spin");return o(),r("div",H,[s("div",J,[c(_,{value:e.search,"onUpdate:value":t[0]||(t[0]=a=>e.search=a),"allow-clear":"",placeholder:"موبایل کاربر...",onPressEnter:C},{prefix:v(()=>[c(f($),{style:{color:"#94a3b8"}})]),_:1},8,["value"]),s("div",K,[c(i,{value:e.campaignId,"onUpdate:value":t[1]||(t[1]=a=>e.campaignId=a),"allow-clear":"",placeholder:"کمپین",style:{flex:"1"},onChange:C},{default:v(()=>[(o(!0),r(D,null,B(z.value,a=>(o(),T(m,{key:a.id,value:a.id},{default:v(()=>[j(l(a.title),1)]),_:2},1032,["value"]))),128))]),_:1},8,["value"]),s("button",{type:"button",class:"m-icon-btn",onClick:u},[c(f(E),{spin:d.value},null,8,["spin"])])])]),c(M,{spinning:d.value},{default:v(()=>[!y.value.length&&!d.value?(o(),r("div",Q,"اسپینی یافت نشد")):L("",!0),(o(!0),r(D,null,B(y.value,a=>{var w,k,b,x;return o(),r("div",{key:a.id,class:"m-card"},[s("div",X,[s("button",{type:"button",class:"m-card__user",onClick:oa=>{var I;return((I=a.user)==null?void 0:I.id)&&f(N).push(`/m/users/${a.user.id}`)}},l(((w=a.user)==null?void 0:w.name)||f(A)(((k=a.user)==null?void 0:k.mobile)||"—")),9,Y),s("span",{class:q(["m-pill",a.isWin?"win":"lose"])},l(a.isWin?"برد":"باخت"),3)]),s("div",Z,l(((b=a.campaign)==null?void 0:b.title)||"—"),1),s("div",aa,"جایزه: "+l(((x=a.prize)==null?void 0:x.title)||"—"),1),s("div",ea,[s("span",ta,l(a.ipAddress||"—"),1),s("span",null,l(F(a.createdAt)),1)])])}),128)),S.value>e.pageSize?(o(),r("div",sa,[c(g,{current:e.page,"onUpdate:current":t[2]||(t[2]=a=>e.page=a),total:S.value,"page-size":e.pageSize,size:"small",onChange:u},null,8,["current","total","page-size"])])):L("",!0)]),_:1},8,["spinning"])])}}}),ca=G(na,[["__scopeId","data-v-56443bf6"]]);export{ca as default};
+1
View File
@@ -0,0 +1 @@
.m-page[data-v-56443bf6]{padding:12px 14px 24px}.m-filters[data-v-56443bf6]{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}.m-filters__row[data-v-56443bf6]{display:flex;gap:8px}.m-filters[data-v-56443bf6] .ant-input-affix-wrapper,.m-filters[data-v-56443bf6] .ant-select-selector{border-radius:12px!important;min-height:42px!important}.m-icon-btn[data-v-56443bf6]{width:42px;height:42px;border:none;border-radius:12px;background:#fff;color:#4f46e5;box-shadow:0 2px 8px #0f172a0f}.m-empty[data-v-56443bf6]{text-align:center;color:#94a3b8;padding:40px}.m-card[data-v-56443bf6]{background:#fff;border-radius:14px;padding:12px;margin-bottom:8px;border:1px solid #f1f5f9}.m-card__top[data-v-56443bf6]{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}.m-card__user[data-v-56443bf6]{border:none;background:transparent;padding:0;font-weight:800;color:#4f46e5;font-size:13px}.m-pill[data-v-56443bf6]{font-size:10px;font-weight:700;padding:3px 8px;border-radius:999px}.m-pill.win[data-v-56443bf6]{background:#ecfdf5;color:#059669}.m-pill.lose[data-v-56443bf6]{background:#f1f5f9;color:#64748b}.m-card__line[data-v-56443bf6]{font-size:12px;color:#334155;margin-bottom:2px}.m-card__meta[data-v-56443bf6]{display:flex;justify-content:space-between;margin-top:8px;font-size:11px;color:#94a3b8}.m-pager[data-v-56443bf6]{display:flex;justify-content:center;padding:12px 0}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.m-page[data-v-a256a055]{padding:12px 14px 24px}.m-profile[data-v-a256a055]{background:#fff;border-radius:18px;padding:20px 16px;text-align:center;border:1px solid #f1f5f9;margin-bottom:12px}.m-profile__avatar[data-v-a256a055]{width:64px;height:64px;margin:0 auto 10px;border-radius:20px;background:linear-gradient(135deg,#4f46e5,#7c3aed);color:#fff;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:800}.m-profile__name[data-v-a256a055]{font-size:17px;font-weight:800;color:#0f172a}.m-profile__mobile[data-v-a256a055]{font-size:13px;color:#64748b;direction:ltr;margin-top:4px}.m-profile__meta[data-v-a256a055]{display:flex;justify-content:center;gap:10px;margin-top:10px;font-size:11px;color:#94a3b8}.m-profile__meta .ok[data-v-a256a055]{color:#059669;font-weight:700}.m-profile__meta .bad[data-v-a256a055]{color:#ef4444;font-weight:700}.m-profile__actions[data-v-a256a055]{display:flex;gap:8px;margin-top:14px}.m-btn[data-v-a256a055]{flex:1;border:none;border-radius:12px;min-height:44px;font-weight:700;font-size:12px;display:inline-flex;align-items:center;justify-content:center;gap:6px}.m-btn.ghost[data-v-a256a055]{background:#f1f5f9;color:#334155}.m-btn.primary[data-v-a256a055]{background:linear-gradient(135deg,#4f46e5,#7c3aed);color:#fff}.m-tabs[data-v-a256a055]{display:flex;background:#fff;border-radius:14px;padding:4px;margin-bottom:12px;gap:4px}.m-tabs button[data-v-a256a055]{flex:1;border:none;background:transparent;border-radius:10px;padding:10px;font-size:12px;font-weight:700;color:#64748b;min-height:40px}.m-tabs button.active[data-v-a256a055]{background:#eef2ff;color:#4f46e5}.m-card[data-v-a256a055]{background:#fff;border-radius:14px;padding:14px;margin-bottom:8px;border:1px solid #f1f5f9}.m-card__title[data-v-a256a055]{font-weight:800;font-size:14px;margin-bottom:8px;color:#0f172a}.m-card__row[data-v-a256a055]{display:flex;justify-content:space-between;font-size:12px;color:#64748b;margin-bottom:4px}.m-card__row strong[data-v-a256a055]{color:#0f172a}.m-link[data-v-a256a055]{margin-top:8px;border:none;background:#eef2ff;color:#4f46e5;border-radius:10px;width:100%;padding:10px;font-weight:700;font-size:12px}.m-empty[data-v-a256a055]{text-align:center;color:#94a3b8;padding:28px;font-size:13px}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.m-page[data-v-ee50e1ce]{padding:12px 14px 24px}.m-search-bar[data-v-ee50e1ce]{display:flex;gap:8px;margin-bottom:12px}.m-search-bar[data-v-ee50e1ce] .ant-input-affix-wrapper,.m-search-bar[data-v-ee50e1ce] .ant-select-selector{border-radius:12px!important;min-height:42px!important}.m-empty[data-v-ee50e1ce]{text-align:center;color:#94a3b8;padding:48px 12px}.m-user-card[data-v-ee50e1ce]{display:flex;gap:12px;background:#fff;border-radius:16px;padding:14px;margin-bottom:10px;border:1px solid #f1f5f9}.m-user-card__avatar[data-v-ee50e1ce]{width:44px;height:44px;border-radius:14px;flex-shrink:0;background:linear-gradient(135deg,#4f46e5,#7c3aed);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800}.m-user-card__body[data-v-ee50e1ce]{flex:1;min-width:0}.m-user-card__top[data-v-ee50e1ce]{display:flex;justify-content:space-between;gap:8px}.m-user-card__name[data-v-ee50e1ce]{font-weight:800;font-size:14px;color:#0f172a}.m-user-card__mobile[data-v-ee50e1ce]{font-size:12px;color:#64748b;direction:ltr;text-align:right}.m-badge[data-v-ee50e1ce]{font-size:10px;font-weight:700;padding:3px 8px;border-radius:999px;height:fit-content}.m-badge.active[data-v-ee50e1ce]{background:#ecfdf5;color:#059669}.m-badge.blocked[data-v-ee50e1ce]{background:#fef2f2;color:#ef4444}.m-user-card__meta[data-v-ee50e1ce]{font-size:11px;color:#94a3b8;margin:8px 0}.m-user-card__actions[data-v-ee50e1ce]{display:flex;gap:6px}.m-action[data-v-ee50e1ce]{flex:1;border:1px solid #e2e8f0;background:#f8fafc;border-radius:10px;padding:10px 4px;font-size:11px;font-weight:700;color:#334155;display:inline-flex;align-items:center;justify-content:center;gap:4px;min-height:42px}.m-pager[data-v-ee50e1ce]{display:flex;justify-content:center;padding:12px 0}.m-drawer-hint[data-v-ee50e1ce]{color:#64748b;margin-bottom:12px}
+1
View File
@@ -0,0 +1 @@
.m-page[data-v-1f6f7c15]{padding:12px 14px 24px}.m-filters[data-v-1f6f7c15]{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}.m-filters__row[data-v-1f6f7c15]{display:flex;gap:6px}.m-filters[data-v-1f6f7c15] .ant-input-affix-wrapper,.m-filters[data-v-1f6f7c15] .ant-select-selector{border-radius:12px!important;min-height:42px!important}.m-icon-btn[data-v-1f6f7c15]{width:42px;height:42px;border:none;border-radius:12px;background:#fff;color:#4f46e5}.m-empty[data-v-1f6f7c15]{text-align:center;color:#94a3b8;padding:40px}.m-card[data-v-1f6f7c15]{background:#fff;border-radius:14px;padding:12px;margin-bottom:8px;border:1px solid #f1f5f9}.m-card__top[data-v-1f6f7c15]{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}.m-card__user[data-v-1f6f7c15]{border:none;background:transparent;padding:0;font-weight:800;color:#4f46e5;font-size:13px}.m-card__line[data-v-1f6f7c15]{font-size:12px;color:#334155}.m-card__meta[data-v-1f6f7c15]{display:flex;justify-content:space-between;align-items:center;margin-top:8px;font-size:11px;color:#94a3b8}.m-code[data-v-1f6f7c15]{border:none;background:#f8fafc;color:#4f46e5;border-radius:8px;padding:4px 8px;font-size:11px;direction:ltr}.m-edit[data-v-1f6f7c15]{margin-top:10px;width:100%;border:1px solid #e2e8f0;background:#f8fafc;border-radius:10px;padding:10px;font-weight:700;font-size:12px;color:#334155;min-height:42px}.m-pager[data-v-1f6f7c15]{display:flex;justify-content:center;padding:12px 0}.hint[data-v-1f6f7c15]{color:#64748b;margin-bottom:12px;font-size:12px}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.m-workspaces-page[data-v-99bdf33a]{padding:12px}.m-add-bar[data-v-99bdf33a]{margin-bottom:12px}.m-ws-list[data-v-99bdf33a]{display:flex;flex-direction:column;gap:10px}.m-ws-card[data-v-99bdf33a]{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:12px}.m-ws-header[data-v-99bdf33a]{display:flex;justify-content:space-between;align-items:center}.m-ws-slug[data-v-99bdf33a]{font-size:11px;color:#94a3b8;direction:ltr;text-align:right;margin:4px 0}.m-ws-desc[data-v-99bdf33a]{font-size:12px;color:#64748b;margin-bottom:8px}.m-ws-footer[data-v-99bdf33a]{display:flex;gap:6px;margin-top:8px;padding-top:6px;border-top:1px dashed #f1f5f9}
+1
View File
@@ -0,0 +1 @@
import{t as A}from"./farsi-number-stB7JMN0.js";import{d as q,u as F,l as P,c as _,e as l,b as t,w as o,x as k,h as u,n as b,r as i,o as d,f as x,G as U,a as m,F as D,z as I,g as $,a3 as j,t as f,y as G,E as H,T as J,B as h,_ as K}from"./index-BIHaUtqF.js";import{P as Q}from"./ProjectOutlined-D-83WHG-.js";const R={class:"m-workspaces-page"},X={key:0,class:"m-add-bar"},Y={class:"m-ws-list"},Z={class:"m-ws-header"},ee={style:{display:"flex","align-items":"center",gap:"8px"}},ae={class:"m-ws-slug"},te={key:0,class:"m-ws-desc"},se={class:"m-ws-footer"},ne=q({__name:"MWorkspaceListView",setup(oe){const w=F(),g=u(!1),C=u([]),p=u(!1),r=u(!1),v=u(null),y=u(!1),e=$({name:"",slug:"",description:""});async function O(){var n;g.value=!0;try{const a=await b.get("/admin/workspaces");C.value=Array.isArray(a.data)?a.data:((n=a.data)==null?void 0:n.data)||[]}finally{g.value=!1}}function B(){r.value=!1,v.value=null,e.name="",e.slug="",e.description="",p.value=!0}function E(n){r.value=!0,v.value=n.id,e.name=n.name,e.slug=n.slug,e.description=n.description||"",p.value=!0}async function T(){var n,a;if(!e.name.trim()){h.error("نام فضای کاری الزامی است.");return}y.value=!0;try{r.value&&v.value?(await b.patch(`/admin/workspaces/${v.value}`,{name:e.name.trim(),description:((n=e.description)==null?void 0:n.trim())||void 0}),h.success("به‌روزرسانی شد.")):(await b.post("/admin/workspaces",{name:e.name.trim(),slug:e.slug.trim().toLowerCase(),description:((a=e.description)==null?void 0:a.trim())||void 0}),h.success("فضای کاری ایجاد شد.")),p.value=!1,O()}finally{y.value=!1}}return P(()=>{O()}),(n,a)=>{const S=i("a-button"),z=i("a-tag"),M=i("a-spin"),L=i("a-input"),V=i("a-form-item"),N=i("a-form"),W=i("a-modal");return d(),_("div",R,[l(w).can("workspaces:manage")?(d(),_("div",X,[t(S,{type:"primary",block:"",size:"large",onClick:B},{icon:o(()=>[t(l(U))]),default:o(()=>[a[3]||(a[3]=x(" تعریف فضای کاری جدید ",-1))]),_:1})])):k("",!0),t(M,{spinning:g.value},{default:o(()=>[m("div",Y,[(d(!0),_(D,null,I(C.value,s=>(d(),_("div",{key:s.id,class:"m-ws-card"},[m("div",Z,[m("div",ee,[t(l(j),{style:{color:"#2563eb","font-size":"16px"}}),m("strong",null,f(s.name),1)]),l(w).can("workspaces:manage")?(d(),G(S,{key:0,type:"text",size:"small",onClick:c=>E(s)},{default:o(()=>[t(l(H),{style:{color:"#2563eb"}})]),_:1},8,["onClick"])):k("",!0)]),m("div",ae,f(s.slug),1),s.description?(d(),_("div",te,f(s.description),1)):k("",!0),m("div",se,[t(z,{color:"purple",size:"small"},{default:o(()=>{var c;return[t(l(Q),{style:{"margin-left":"4px"}}),x(" "+f(l(A)(((c=s._count)==null?void 0:c.campaigns)??0))+" کمپین ",1)]}),_:2},1024),t(z,{color:"cyan",size:"small"},{default:o(()=>{var c;return[t(l(J),{style:{"margin-left":"4px"}}),x(" "+f(l(A)(((c=s._count)==null?void 0:c.adminAssignments)??0))+" مدیر ",1)]}),_:2},1024)])]))),128))])]),_:1},8,["spinning"]),t(W,{open:p.value,"onUpdate:open":a[2]||(a[2]=s=>p.value=s),title:r.value?"ویرایش فضای کاری":"فضای کاری جدید",okText:"ذخیره",cancelText:"انصراف",confirmLoading:y.value,onOk:T},{default:o(()=>[t(N,{layout:"vertical",style:{"margin-top":"12px"}},{default:o(()=>[t(V,{label:"نام فضای کاری",required:""},{default:o(()=>[t(L,{value:e.name,"onUpdate:value":a[0]||(a[0]=s=>e.name=s),placeholder:"مثال: شعبه مشهد"},null,8,["value"])]),_:1}),t(V,{label:"شناسه انگلیسی (Slug)",required:!r.value},{default:o(()=>[t(L,{value:e.slug,"onUpdate:value":a[1]||(a[1]=s=>e.slug=s),placeholder:"mashhad-branch",dir:"ltr",disabled:r.value},null,8,["value","disabled"])]),_:1},8,["required"])]),_:1})]),_:1},8,["open","title","confirmLoading"])])}}}),ce=K(ne,[["__scopeId","data-v-99bdf33a"]]);export{ce as default};
+1
View File
@@ -0,0 +1 @@
import{b as c,I as u}from"./index-BIHaUtqF.js";var o={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6a55.99 55.99 0 0068.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z"}}]},name:"mail",theme:"outlined"};function l(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){f(r,a,e[a])})}return r}function f(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var i=function(t,e){var n=l({},t,e.attrs);return c(u,l({},n,{icon:o}),null)};i.displayName="MailOutlined";i.inheritAttrs=!1;export{i as M};
+1
View File
@@ -0,0 +1 @@
import{b as i,I as u}from"./index-BIHaUtqF.js";var s={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M696 480H544V328c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v152H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h152v152c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V544h152c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"}},{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}}]},name:"plus-circle",theme:"outlined"};function a(r){for(var e=1;e<arguments.length;e++){var t=arguments[e]!=null?Object(arguments[e]):{},n=Object.keys(t);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(t).filter(function(c){return Object.getOwnPropertyDescriptor(t,c).enumerable}))),n.forEach(function(c){o(r,c,t[c])})}return r}function o(r,e,t){return e in r?Object.defineProperty(r,e,{value:t,enumerable:!0,configurable:!0,writable:!0}):r[e]=t,r}var l=function(e,t){var n=a({},e,t.attrs);return i(u,a({},n,{icon:s}),null)};l.displayName="PlusCircleOutlined";l.inheritAttrs=!1;export{l as P};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.action-more-btn[data-v-bf123cb2]{color:#64748b;width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;transition:all .2s ease}.action-more-btn[data-v-bf123cb2]:hover{color:#4f46e5!important;background-color:#eef2ff!important}.dropdown-action-item[data-v-bf123cb2]{display:flex;align-items:center;gap:8px;color:#334155;text-decoration:none;font-size:13px}.dropdown-item-icon[data-v-bf123cb2]{font-size:14px}.text-danger[data-v-bf123cb2]{color:#ef4444}.prize-modal-form[data-v-bf123cb2] .ant-form-item{margin-bottom:12px}
+1
View File
@@ -0,0 +1 @@
import{b as i,I as u}from"./index-BIHaUtqF.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M280 752h80c4.4 0 8-3.6 8-8V280c0-4.4-3.6-8-8-8h-80c-4.4 0-8 3.6-8 8v464c0 4.4 3.6 8 8 8zm192-280h80c4.4 0 8-3.6 8-8V280c0-4.4-3.6-8-8-8h-80c-4.4 0-8 3.6-8 8v184c0 4.4 3.6 8 8 8zm192 72h80c4.4 0 8-3.6 8-8V280c0-4.4-3.6-8-8-8h-80c-4.4 0-8 3.6-8 8v256c0 4.4 3.6 8 8 8zm216-432H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 728H184V184h656v656z"}}]},name:"project",theme:"outlined"};function a(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},c=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(c=c.concat(Object.getOwnPropertySymbols(e).filter(function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),c.forEach(function(n){f(r,n,e[n])})}return r}function f(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var c=a({},t,e.attrs);return i(u,a({},c,{icon:l}),null)};o.displayName="ProjectOutlined";o.inheritAttrs=!1;export{o as P};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.page-header-title[data-v-a8d97f14]{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:12px}.page-header-title .title[data-v-a8d97f14]{font-size:20px;font-weight:700;color:#0f172a;margin:0;letter-spacing:-.3px}.page-header-title .subtitle[data-v-a8d97f14]{font-size:13px;color:#64748b;margin:4px 0 0}.main-card[data-v-a8d97f14]{border-radius:12px;box-shadow:0 1px 3px #0000000a;border:1px solid #f1f5f9}.kpi-card[data-v-a8d97f14]{border-radius:12px;box-shadow:0 1px 3px #0000000a;border:1px solid #f1f5f9;transition:all .25s ease}.kpi-card[data-v-a8d97f14]:hover{transform:translateY(-2px);box-shadow:0 6px 16px #00000012}.kpi-flex[data-v-a8d97f14]{display:flex;align-items:center;gap:16px}.kpi-icon-box[data-v-a8d97f14]{width:52px;height:52px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0}.icon-indigo[data-v-a8d97f14]{background:#eef2ff;color:#4f46e5}.icon-sky[data-v-a8d97f14]{background:#f0f9ff;color:#0284c7}.icon-emerald[data-v-a8d97f14]{background:#ecfdf5;color:#059669}.icon-purple[data-v-a8d97f14]{background:#faf5ff;color:#9333ea}.icon-amber[data-v-a8d97f14]{background:#fffbeb;color:#d97706}.icon-rose[data-v-a8d97f14]{background:#fff1f2;color:#e11d48}.icon-teal[data-v-a8d97f14]{background:#f0fdfa;color:#0d9488}.kpi-details[data-v-a8d97f14]{display:flex;flex-direction:column}.kpi-label[data-v-a8d97f14]{font-size:13px;color:#64748b;font-weight:500}.kpi-value[data-v-a8d97f14]{font-size:22px;font-weight:700;color:#0f172a;line-height:1.2;margin:2px 0 0}.filters-header[data-v-a8d97f14]{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px}.filters[data-v-a8d97f14]{display:flex;gap:12px}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.roles-matrix-page[data-v-e87b0776]{padding-bottom:24px}.page-header-title[data-v-e87b0776]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.title[data-v-e87b0776]{font-size:22px;font-weight:700;color:#1e293b;margin:0}.subtitle[data-v-e87b0776]{font-size:13px;color:#64748b;margin-top:4px;margin-bottom:0}.roles-sidebar-card[data-v-e87b0776],.matrix-card[data-v-e87b0776]{border-radius:12px;box-shadow:0 1px 3px #0000000d}.roles-list[data-v-e87b0776]{display:flex;flex-direction:column;gap:10px}.role-item[data-v-e87b0776]{padding:12px;border:1px solid #e2e8f0;border-radius:8px;cursor:pointer;transition:all .2s ease;background:#fff}.role-item[data-v-e87b0776]:hover{border-color:#93c5fd;background:#f8fafc}.role-item.is-active[data-v-e87b0776]{border-color:#2563eb;background:#eff6ff}.role-item-header[data-v-e87b0776]{display:flex;justify-content:space-between;align-items:center}.role-code[data-v-e87b0776]{font-size:11px;color:#94a3b8;direction:ltr;text-align:right;margin:2px 0 6px}.role-desc[data-v-e87b0776]{font-size:12px;color:#64748b;line-height:1.5}.role-footer[data-v-e87b0776]{margin-top:10px;padding-top:8px;border-top:1px dashed #e2e8f0;display:flex;justify-content:space-between;align-items:center;font-size:12px;color:#64748b}.matrix-header[data-v-e87b0776]{display:flex;justify-content:space-between;align-items:center}.super-admin-notice[data-v-e87b0776]{display:flex;gap:12px;align-items:center;background:#f5f3ff;border:1px solid #ddd6fe;border-radius:8px;padding:12px 16px;margin-bottom:20px}.modules-grid[data-v-e87b0776]{display:flex;flex-direction:column;gap:16px}.module-perm-block[data-v-e87b0776]{border:1px solid #e2e8f0;border-radius:8px;overflow:hidden}.module-header[data-v-e87b0776]{background:#f8fafc;padding:10px 14px;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center}.module-title h4[data-v-e87b0776]{margin:0;font-size:14px;font-weight:600;display:inline-block;color:#1e293b}.count-label[data-v-e87b0776]{font-size:12px;color:#64748b;margin-right:6px}.perms-list[data-v-e87b0776]{padding:12px;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px}.perm-checkbox-item[data-v-e87b0776]{padding:8px 10px;border:1px solid #f1f5f9;border-radius:6px;transition:all .15s ease;cursor:pointer;background:#fff}.perm-checkbox-item[data-v-e87b0776]:hover{border-color:#cbd5e1}.perm-checkbox-item.is-checked[data-v-e87b0776]{background:#eff6ff;border-color:#bfdbfe}.perm-name[data-v-e87b0776]{font-size:13px;color:#1e293b}.perm-code[data-v-e87b0776]{font-size:11px;color:#94a3b8;direction:ltr;text-align:right;margin:2px 24px 0 0}.perm-desc[data-v-e87b0776]{font-size:11px;color:#64748b;margin:2px 24px 0 0}
+1
View File
@@ -0,0 +1 @@
import{b as l,I as u}from"./index-BIHaUtqF.js";var o={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M893.3 293.3L730.7 130.7c-7.5-7.5-16.7-13-26.7-16V112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V338.5c0-17-6.7-33.2-18.7-45.2zM384 184h256v104H384V184zm456 656H184V184h136v136c0 17.7 14.3 32 32 32h320c17.7 0 32-14.3 32-32V205.8l136 136V840zM512 442c-79.5 0-144 64.5-144 144s64.5 144 144 144 144-64.5 144-144-64.5-144-144-144zm0 224c-44.2 0-80-35.8-80-80s35.8-80 80-80 80 35.8 80 80-35.8 80-80 80z"}}]},name:"save",theme:"outlined"};function c(r){for(var e=1;e<arguments.length;e++){var t=arguments[e]!=null?Object(arguments[e]):{},n=Object.keys(t);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(t).filter(function(a){return Object.getOwnPropertyDescriptor(t,a).enumerable}))),n.forEach(function(a){f(r,a,t[a])})}return r}function f(r,e,t){return e in r?Object.defineProperty(r,e,{value:t,enumerable:!0,configurable:!0,writable:!0}):r[e]=t,r}var i=function(e,t){var n=c({},e,t.attrs);return l(u,c({},n,{icon:o}),null)};i.displayName="SaveOutlined";i.inheritAttrs=!1;export{i as S};
+1
View File
@@ -0,0 +1 @@
.page-header-title[data-v-3ed5f551]{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:12px}.page-header-title .title[data-v-3ed5f551]{font-size:20px;font-weight:700;color:#0f172a;margin:0;letter-spacing:-.3px}.page-header-title .subtitle[data-v-3ed5f551]{font-size:13px;color:#64748b;margin:4px 0 0}.main-card[data-v-3ed5f551]{border-radius:12px;box-shadow:0 1px 3px #0000000a;border:1px solid #f1f5f9}.table-header[data-v-3ed5f551]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.filters[data-v-3ed5f551]{display:flex;gap:12px}.code-badge[data-v-3ed5f551]{background:#f8fafc;border:1px solid #e2e8f0;padding:2px 6px;border-radius:4px;font-family:monospace;font-size:11px;color:#64748b}.num-cell[data-v-3ed5f551],.date-cell[data-v-3ed5f551]{color:#334155;font-size:13px}.text-danger[data-v-3ed5f551]{color:#ef4444;font-size:12px}.text-muted[data-v-3ed5f551]{color:#94a3b8}.status-tag[data-v-3ed5f551]{display:inline-flex;align-items:center;gap:6px;padding:2px 10px;border-radius:20px;font-weight:500}.status-dot[data-v-3ed5f551]{width:6px;height:6px;border-radius:50%;display:inline-block}.action-more-btn[data-v-3ed5f551]{color:#64748b;width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;transition:all .2s ease}.action-more-btn[data-v-3ed5f551]:hover{background-color:#eef2ff!important}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.page-header-title[data-v-98454002]{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:12px}.page-header-title .title[data-v-98454002]{font-size:20px;font-weight:700;color:#0f172a;margin:0;letter-spacing:-.3px}.page-header-title .subtitle[data-v-98454002]{font-size:13px;color:#64748b;margin:4px 0 0}.main-card[data-v-98454002]{border-radius:12px;box-shadow:0 1px 3px #0000000a;border:1px solid #f1f5f9}.table-header[data-v-98454002]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;flex-wrap:wrap;gap:12px}.filters[data-v-98454002]{display:flex;gap:12px}.mobile-cell[data-v-98454002]{font-weight:600;color:#1e293b}.user-mobile-link[data-v-98454002]{cursor:pointer;transition:color .15s ease}.user-mobile-link[data-v-98454002]:hover{color:#4f46e5}.user-name[data-v-98454002],.prize-text[data-v-98454002]{font-weight:500;color:#1e293b}.code-badge[data-v-98454002]{background:#f8fafc;border:1px solid #e2e8f0;padding:2px 6px;border-radius:4px;font-family:monospace;font-size:11px;color:#64748b}.num-cell[data-v-98454002],.date-cell[data-v-98454002]{color:#334155;font-size:13px}.text-muted[data-v-98454002]{color:#94a3b8}.status-tag[data-v-98454002]{display:inline-flex;align-items:center;gap:6px;padding:2px 10px;border-radius:20px;font-weight:500}.status-dot[data-v-98454002]{width:6px;height:6px;border-radius:50%;display:inline-block}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{b as u,I as c}from"./index-BIHaUtqF.js";var s={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M608 112c-167.9 0-304 136.1-304 304 0 70.3 23.9 135 63.9 186.5L255.8 713.6l-62.3-62.3a8.19 8.19 0 00-11.4 0l-39.8 39.8a8.19 8.19 0 000 11.4l62.3 62.3-44.9 44.9-62.3-62.3a8.19 8.19 0 00-11.4 0l-39.8 39.8a8.19 8.19 0 000 11.4l110.3 111.2c3.1 3.1 8.2 3.1 11.3 0l253.6-253.6A304.1 304.1 0 00608 720c167.9 0 304-136.1 304-304S775.9 112 608 112m161.2 465.2C726.2 620.3 668.9 644 608 644s-118.2-23.7-161.2-66.8C403.7 534.2 380 476.9 380 416s23.7-118.2 66.8-161.2c43-43.1 100.3-66.8 161.2-66.8s118.2 23.7 161.2 66.8c43.1 43 66.8 100.3 66.8 161.2s-23.7 118.2-66.8 161.2"}}]},name:"key",theme:"outlined"};function l(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){p(r,a,e[a])})}return r}function p(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var n=l({},t,e.attrs);return u(c,l({},n,{icon:s}),null)};o.displayName="KeyOutlined";o.inheritAttrs=!1;var O={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372 0-89 31.3-170.8 83.5-234.8l523.3 523.3C682.8 852.7 601 884 512 884zm288.5-137.2L277.2 223.5C341.2 171.3 423 140 512 140c205.4 0 372 166.6 372 372 0 89-31.3 170.8-83.5 234.8z"}}]},name:"stop",theme:"outlined"};function i(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){m(r,a,e[a])})}return r}function m(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var f=function(t,e){var n=i({},t,e.attrs);return u(c,i({},n,{icon:O}),null)};f.displayName="StopOutlined";f.inheritAttrs=!1;export{o as K,f as S};
@@ -0,0 +1 @@
import{b as o,I as c}from"./index-BIHaUtqF.js";var i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M848 359.3H627.7L825.8 109c4.1-5.3.4-13-6.3-13H436c-2.8 0-5.5 1.5-6.9 4L170 547.5c-3.1 5.3.7 12 6.9 12h174.4l-89.4 357.6c-1.9 7.8 7.5 13.3 13.3 7.7L853.5 373c5.2-4.9 1.7-13.7-5.5-13.7zM378.2 732.5l60.3-241H281.1l189.6-327.4h224.6L487 427.4h211L378.2 732.5z"}}]},name:"thunderbolt",theme:"outlined"};function l(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){f(r,a,e[a])})}return r}function f(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var u=function(t,e){var n=l({},t,e.attrs);return o(c,l({},n,{icon:i}),null)};u.displayName="ThunderboltOutlined";u.inheritAttrs=!1;export{u as T};
+1
View File
@@ -0,0 +1 @@
import{b as o,I as i}from"./index-BIHaUtqF.js";var u={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M832 464H332V240c0-30.9 25.1-56 56-56h248c30.9 0 56 25.1 56 56v68c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-68c0-70.7-57.3-128-128-128H388c-70.7 0-128 57.3-128 128v224h-68c-17.7 0-32 14.3-32 32v384c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32zm-40 376H232V536h560v304zM484 701v53c0 4.4 3.6 8 8 8h40c4.4 0 8-3.6 8-8v-53a48.01 48.01 0 10-56 0z"}}]},name:"unlock",theme:"outlined"};function a(n){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},r=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(r=r.concat(Object.getOwnPropertySymbols(e).filter(function(c){return Object.getOwnPropertyDescriptor(e,c).enumerable}))),r.forEach(function(c){f(n,c,e[c])})}return n}function f(n,t,e){return t in n?Object.defineProperty(n,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):n[t]=e,n}var l=function(t,e){var r=a({},t,e.attrs);return o(i,a({},r,{icon:u}),null)};l.displayName="UnlockOutlined";l.inheritAttrs=!1;export{l as U};
+1
View File
@@ -0,0 +1 @@
import{b as o,I as i}from"./index-BIHaUtqF.js";var u={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M400 317.7h73.9V656c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V317.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 163a8 8 0 00-12.6 0l-112 141.7c-4.1 5.3-.4 13 6.3 13zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"}}]},name:"upload",theme:"outlined"};function c(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){f(r,a,e[a])})}return r}function f(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var l=function(t,e){var n=c({},t,e.attrs);return o(i,c({},n,{icon:u}),null)};l.displayName="UploadOutlined";l.inheritAttrs=!1;export{l as U};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.detail-header[data-v-2b05c7cf]{background:#fff;margin-bottom:20px;border-radius:12px;border:1px solid #f1f5f9;box-shadow:0 1px 3px #0000000a}.section-card[data-v-2b05c7cf]{border-radius:12px;border:1px solid #f1f5f9;box-shadow:0 1px 3px #0000000a}.detail-tabs[data-v-2b05c7cf]{background:#fff;padding:16px;border-radius:12px;border:1px solid #f1f5f9;box-shadow:0 1px 3px #0000000a}.code-badge[data-v-2b05c7cf]{background:#f1f5f9;padding:2px 6px;border-radius:4px;font-family:monospace;font-size:12px;color:#334155}.num-cell[data-v-2b05c7cf],.date-cell[data-v-2b05c7cf]{color:#334155;font-size:13px}.action-text-btn[data-v-2b05c7cf]{color:#4f46e5!important;font-size:12px;font-weight:500;border-radius:6px;transition:all .2s ease}.action-text-btn[data-v-2b05c7cf]:hover{background-color:#eef2ff!important}.text-muted[data-v-2b05c7cf]{color:#cbd5e1}
+1
View File
@@ -0,0 +1 @@
.page-header-title[data-v-665b0d54]{margin-bottom:20px}.page-header-title .title[data-v-665b0d54]{font-size:20px;font-weight:700;color:#0f172a;margin:0;letter-spacing:-.3px}.page-header-title .subtitle[data-v-665b0d54]{font-size:13px;color:#64748b;margin:4px 0 0}.main-card[data-v-665b0d54]{border-radius:12px;box-shadow:0 1px 3px #0000000a;border:1px solid #f1f5f9}.table-header[data-v-665b0d54]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.filters[data-v-665b0d54]{display:flex;gap:12px}.user-mobile-link[data-v-665b0d54]{font-weight:600;color:#1e293b;cursor:pointer;transition:color .15s ease}.user-mobile-link[data-v-665b0d54]:hover{color:#4f46e5}.user-name[data-v-665b0d54]{font-weight:500;color:#334155}.num-cell[data-v-665b0d54],.date-cell[data-v-665b0d54]{color:#334155;font-size:13px}.status-tag[data-v-665b0d54]{display:inline-flex;align-items:center;gap:6px;padding:2px 10px;border-radius:20px;font-weight:500}.status-dot[data-v-665b0d54]{width:6px;height:6px;border-radius:50%;display:inline-block}.action-more-btn[data-v-665b0d54]{color:#64748b;width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;transition:all .2s ease}.action-more-btn[data-v-665b0d54]:hover{color:#4f46e5!important;background-color:#eef2ff!important}.dropdown-action-item[data-v-665b0d54]{display:flex;align-items:center;gap:8px;color:#334155;text-decoration:none;font-size:13px}.dropdown-item-icon[data-v-665b0d54]{font-size:14px}.text-success[data-v-665b0d54]{color:#10b981}.text-danger[data-v-665b0d54]{color:#ef4444}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.page-header-title[data-v-c0982f6e]{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:12px}.page-header-title .title[data-v-c0982f6e]{font-size:20px;font-weight:700;color:#0f172a;margin:0;letter-spacing:-.3px}.page-header-title .subtitle[data-v-c0982f6e]{font-size:13px;color:#64748b;margin:4px 0 0}.main-card[data-v-c0982f6e]{border-radius:12px;box-shadow:0 1px 3px #0000000a;border:1px solid #f1f5f9}.table-header[data-v-c0982f6e]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;flex-wrap:wrap;gap:12px}.filters[data-v-c0982f6e]{display:flex;gap:12px}.mobile-cell[data-v-c0982f6e]{font-weight:600;color:#1e293b}.user-link[data-v-c0982f6e]{cursor:pointer;transition:color .15s ease}.user-link[data-v-c0982f6e]:hover{color:#4f46e5}.user-name[data-v-c0982f6e]{font-weight:500;color:#334155}.campaign-cell[data-v-c0982f6e]{color:#475569}.prize-text[data-v-c0982f6e]{font-weight:600;color:#1e293b}.code-badge[data-v-c0982f6e]{background:#f8fafc;border:1px solid #e2e8f0;padding:2px 6px;border-radius:4px;font-family:monospace;font-size:12px;color:#475569}.clickable-code[data-v-c0982f6e]{cursor:pointer;transition:all .15s ease}.clickable-code[data-v-c0982f6e]:hover{background:#eef2ff;border-color:#c7d2fe;color:#4f46e5}.date-cell[data-v-c0982f6e]{color:#334155;font-size:13px}.text-muted[data-v-c0982f6e]{color:#94a3b8}.status-tag[data-v-c0982f6e]{display:inline-flex;align-items:center;gap:6px;padding:2px 10px;border-radius:20px;font-weight:500}.status-dot[data-v-c0982f6e]{width:6px;height:6px;border-radius:50%;display:inline-block}.action-more-btn[data-v-c0982f6e]{color:#64748b;width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;transition:all .2s ease}.action-more-btn[data-v-c0982f6e]:hover{color:#4f46e5!important;background-color:#eef2ff!important}.dropdown-action-item[data-v-c0982f6e]{display:flex;align-items:center;gap:8px;color:#334155;text-decoration:none;font-size:13px}.dropdown-item-icon[data-v-c0982f6e]{font-size:14px}
+1
View File
@@ -0,0 +1 @@
.workspaces-page[data-v-03dd7926]{padding-bottom:24px}.page-header-title[data-v-03dd7926]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.title[data-v-03dd7926]{font-size:22px;font-weight:700;color:#1e293b;margin:0}.subtitle[data-v-03dd7926]{font-size:13px;color:#64748b;margin-top:4px;margin-bottom:0}.main-card[data-v-03dd7926]{border-radius:12px;box-shadow:0 1px 3px #0000000d}.date-cell[data-v-03dd7926]{font-size:13px;color:#64748b}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{b as c,I as s}from"./index-BIHaUtqF.js";var u={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm-88-532h-48c-4.4 0-8 3.6-8 8v304c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V360c0-4.4-3.6-8-8-8zm224 0h-48c-4.4 0-8 3.6-8 8v304c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V360c0-4.4-3.6-8-8-8z"}}]},name:"pause-circle",theme:"outlined"};function i(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]!=null?Object(arguments[t]):{},r=Object.keys(n);typeof Object.getOwnPropertySymbols=="function"&&(r=r.concat(Object.getOwnPropertySymbols(n).filter(function(o){return Object.getOwnPropertyDescriptor(n,o).enumerable}))),r.forEach(function(o){f(e,o,n[o])})}return e}function f(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var a=function(t,n){var r=i({},t,n.attrs);return c(s,i({},r,{icon:u}),null)};a.displayName="PauseCircleOutlined";a.inheritAttrs=!1;const l="http://localhost:5174";function p(){if(typeof window>"u")return!1;const e=String(window.location.href||""),t=String(window.location.hostname||""),n=String(window.location.port||"");return!!(e.includes("localhost")||e.includes("127.0.0.1")||e.includes("[::1]")||t==="localhost"||t==="127.0.0.1"||t==="[::1]"||n==="5173"||n==="5174"||n==="8080")}function d(e){const t=String(e||"").trim().replace(/^\/+/,"");if(!t)return"";if(/^https?:\/\//i.test(t)||t.includes("wheel.montest.ir"))try{const r=(/^https?:\/\//i.test(t)?new URL(t):new URL(`https://${t}`)).pathname.split("/").filter(Boolean);return r[r.length-1]||""}catch{const n=t.split("/").filter(Boolean);return n[n.length-1]||t}return t}function h(e){const t=d(e);return p()?t?`${l}/${t}`:`${l}/`:t?`/${t}`:"/"}function m(e){if(typeof window>"u")return;const t=h(e);console.log("CLICKED WHEEL LINK:",t),window.open(t,"_blank","noopener,noreferrer")}export{a as P,h as g,m as o};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
<script setup lang="ts">
import {
ApartmentOutlined,
AuditOutlined,
DashboardOutlined,
LineChartOutlined,
@@ -8,6 +9,8 @@ import {
MoreOutlined,
PieChartOutlined,
ReloadOutlined,
SafetyCertificateOutlined,
TeamOutlined,
TrophyOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
@@ -21,32 +24,38 @@ const authStore = useAuthStore();
const moreOpen = ref(false);
const tabs = [
const allTabs = [
{ key: 'm-dashboard', label: 'داشبورد', icon: DashboardOutlined, path: '/m/dashboard' },
{ key: 'm-campaigns', label: 'کمپین‌ها', icon: PieChartOutlined, path: '/m/campaigns' },
{ key: 'm-users', label: 'کاربران', icon: UserOutlined, path: '/m/users' },
{ key: 'm-winners', label: 'برندگان', icon: TrophyOutlined, path: '/m/winners' },
{ key: 'm-campaigns', label: 'کمپین‌ها', icon: PieChartOutlined, path: '/m/campaigns', permission: 'campaigns:read' },
{ key: 'm-users', label: 'کاربران', icon: UserOutlined, path: '/m/users', permission: 'users:read' },
{ key: 'm-winners', label: 'برندگان', icon: TrophyOutlined, path: '/m/winners', permission: 'winners:read' },
];
const moreItems = [
{ key: 'm-spins', label: 'تاریخچه اسپین', icon: ReloadOutlined, path: '/m/spins' },
{ key: 'm-sms', label: 'لاگ پیامک', icon: MessageOutlined, path: '/m/sms' },
{ key: 'm-reports', label: 'گزارش‌ها', icon: LineChartOutlined, path: '/m/reports' },
{ key: 'm-audit', label: 'لاگ بازرسی', icon: AuditOutlined, path: '/m/audit' },
const allMoreItems = [
{ key: 'm-spins', label: 'تاریخچه اسپین', icon: ReloadOutlined, path: '/m/spins', permission: 'spins:read' },
{ key: 'm-sms', label: 'لاگ پیامک', icon: MessageOutlined, path: '/m/sms', permission: 'sms:read' },
{ key: 'm-reports', label: 'گزارش‌ها', icon: LineChartOutlined, path: '/m/reports', permission: 'reports:read' },
{ key: 'm-audit', label: 'لاگ بازرسی', icon: AuditOutlined, path: '/m/audit', permission: 'audit:read' },
{ key: 'm-managers', label: 'مدیریت مدیران', icon: TeamOutlined, path: '/m/managers', permission: 'admins:read' },
{ key: 'm-roles', label: 'نقش‌ها و دسترسی‌ها', icon: SafetyCertificateOutlined, path: '/m/roles', permission: 'roles:manage' },
{ key: 'm-workspaces', label: 'فضاهای کاری', icon: ApartmentOutlined, path: '/m/workspaces', permission: 'workspaces:manage' },
];
const tabs = computed(() => allTabs.filter(t => !t.permission || authStore.can(t.permission)));
const moreItems = computed(() => allMoreItems.filter(i => !i.permission || authStore.can(i.permission)));
const activeKey = computed(() => {
const name = String(route.name || '');
if (tabs.some((t) => t.key === name || name.startsWith(t.key))) {
if (tabs.value.some((t) => t.key === name || name.startsWith(t.key))) {
if (name.startsWith('m-campaign')) return 'm-campaigns';
if (name.startsWith('m-user')) return 'm-users';
return name;
}
if (moreItems.some((t) => t.key === name)) return 'more';
if (moreItems.value.some((t) => t.key === name)) return 'more';
return name;
});
const moreActive = computed(() => moreItems.some((i) => i.key === String(route.name)));
const moreActive = computed(() => moreItems.value.some((i) => i.key === String(route.name)));
function go(path: string) {
moreOpen.value = false;
@@ -64,7 +64,9 @@ function handleBack() {
.m-topbar {
position: sticky;
top: 0;
z-index: 100;
z-index: 1000;
width: 100%;
flex-shrink: 0;
background: linear-gradient(135deg, #4f46e5 0%, #6366f1 55%, #7c3aed 100%);
color: #fff;
box-shadow: 0 4px 20px rgba(79, 70, 229, 0.28);
+5 -1
View File
@@ -34,6 +34,10 @@ const desktopToMobileRoute: Record<string, string> = {
sms: 'm-sms',
reports: 'm-reports',
audit: 'm-audit',
managers: 'm-managers',
roles: 'm-roles',
workspaces: 'm-workspaces',
forbidden: 'm-forbidden',
};
const mobileToDesktopRoute: Record<string, string> = Object.fromEntries(
@@ -49,7 +53,7 @@ function syncRouteToDevice() {
isMobile.value = mobile;
const route = boundRouter.currentRoute.value;
const isMobileRoute = route.path.startsWith('/m') || !!route.meta.mobile;
const isMobileRoute = route.path === '/m' || route.path.startsWith('/m/') || !!route.meta.mobile;
const name = String(route.name || '');
if (mobile && !isMobileRoute) {
@@ -0,0 +1,30 @@
import { computed } from 'vue';
import { useAuthStore } from '../stores/auth.store';
export function usePermission() {
const authStore = useAuthStore();
const isSuperAdmin = computed(() => authStore.isSuperAdmin);
const permissions = computed(() => authStore.userPermissions);
function can(permissionCode: string): boolean {
return authStore.can(permissionCode);
}
function canAny(permissionCodes: string[]): boolean {
return authStore.canAny(permissionCodes);
}
function canAll(permissionCodes: string[]): boolean {
if (authStore.isSuperAdmin) return true;
return permissionCodes.every((code) => authStore.can(code));
}
return {
isSuperAdmin,
permissions,
can,
canAny,
canAll,
};
}
@@ -0,0 +1,33 @@
import type { App, DirectiveBinding } from 'vue';
import { useAuthStore } from '../stores/auth.store';
export const permissionDirective = {
mounted(el: HTMLElement, binding: DirectiveBinding<string | string[]>) {
const authStore = useAuthStore();
const value = binding.value;
if (!value) return;
let hasPermission = false;
if (Array.isArray(value)) {
hasPermission = authStore.canAny(value);
} else {
hasPermission = authStore.can(value);
}
if (!hasPermission) {
if (binding.modifiers.disable) {
el.setAttribute('disabled', 'true');
el.classList.add('ant-btn-disabled', 'is-permission-disabled');
el.style.pointerEvents = 'none';
el.style.opacity = '0.5';
} else {
el.parentNode?.removeChild(el);
}
}
},
};
export function setupPermissionDirective(app: App) {
app.directive('permission', permissionDirective);
}
+78 -14
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import {
ApartmentOutlined,
AuditOutlined,
CloseCircleOutlined,
DashboardOutlined,
@@ -14,12 +15,14 @@ import {
MessageOutlined,
PieChartOutlined,
ReloadOutlined,
SafetyCertificateOutlined,
SearchOutlined,
TeamOutlined,
TrophyOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import { computed, createVNode, reactive, ref, watch } from 'vue';
import { computed, createVNode, onMounted, reactive, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useAuthStore } from '../stores/auth.store';
@@ -36,32 +39,52 @@ interface NavItem {
label: string;
icon: any;
path: string;
permission?: string;
}
const mainItems: NavItem[] = [
{ key: 'dashboard', label: 'داشبورد آماری', icon: DashboardOutlined, path: '/dashboard' },
{ key: 'campaigns', label: 'مدیریت کمپین‌ها', icon: PieChartOutlined, path: '/campaigns' },
{ key: 'users', label: 'مدیریت کاربران', icon: UserOutlined, path: '/users' },
{ key: 'winners', label: 'مدیریت برندگان', icon: TrophyOutlined, path: '/winners' },
{ key: 'spins', label: 'تاریخچه اسپین‌ها', icon: ReloadOutlined, path: '/spins' },
{ key: 'campaigns', label: 'مدیریت کمپین‌ها', icon: PieChartOutlined, path: '/campaigns', permission: 'campaigns:read' },
{ key: 'users', label: 'مدیریت کاربران', icon: UserOutlined, path: '/users', permission: 'users:read' },
{ key: 'winners', label: 'مدیریت برندگان', icon: TrophyOutlined, path: '/winners', permission: 'winners:read' },
{ key: 'spins', label: 'تاریخچه اسپین‌ها', icon: ReloadOutlined, path: '/spins', permission: 'spins:read' },
];
const utilityItems: NavItem[] = [
{ key: 'sms', label: 'لاگ‌های پیامک', icon: MessageOutlined, path: '/sms' },
{ key: 'reports', label: 'گزارش‌ها و KPI', icon: LineChartOutlined, path: '/reports' },
{ key: 'audit', label: 'لاگ بازرسی (Audit)', icon: AuditOutlined, path: '/audit' },
{ key: 'sms', label: 'لاگ‌های پیامک', icon: MessageOutlined, path: '/sms', permission: 'sms:read' },
{ key: 'reports', label: 'گزارش‌ها و KPI', icon: LineChartOutlined, path: '/reports', permission: 'reports:read' },
{ key: 'audit', label: 'لاگ بازرسی (Audit)', icon: AuditOutlined, path: '/audit', permission: 'audit:read' },
];
const accessControlItems: NavItem[] = [
{ key: 'managers', label: 'مدیریت مدیران', icon: TeamOutlined, path: '/managers', permission: 'admins:read' },
{ key: 'roles', label: 'نقش‌ها و دسترسی‌ها', icon: SafetyCertificateOutlined, path: '/roles', permission: 'roles:manage' },
{ key: 'workspaces', label: 'فضاهای کاری', icon: ApartmentOutlined, path: '/workspaces', permission: 'workspaces:manage' },
];
const filteredMainItems = computed(() => {
if (!searchQuery.value.trim()) return mainItems;
const allowed = mainItems.filter(item => !item.permission || authStore.can(item.permission));
if (!searchQuery.value.trim()) return allowed;
const q = searchQuery.value.toLowerCase().trim();
return mainItems.filter(item => item.label.toLowerCase().includes(q));
return allowed.filter(item => item.label.toLowerCase().includes(q));
});
const filteredUtilityItems = computed(() => {
if (!searchQuery.value.trim()) return utilityItems;
const allowed = utilityItems.filter(item => !item.permission || authStore.can(item.permission));
if (!searchQuery.value.trim()) return allowed;
const q = searchQuery.value.toLowerCase().trim();
return utilityItems.filter(item => item.label.toLowerCase().includes(q));
return allowed.filter(item => item.label.toLowerCase().includes(q));
});
const filteredAccessControlItems = computed(() => {
const allowed = accessControlItems.filter(item => !item.permission || authStore.can(item.permission));
if (!searchQuery.value.trim()) return allowed;
const q = searchQuery.value.toLowerCase().trim();
return allowed.filter(item => item.label.toLowerCase().includes(q));
});
onMounted(() => {
authStore.fetchMe();
});
function navigateTo(item: NavItem) {
@@ -267,9 +290,31 @@ function confirmLogout() {
</div>
</div>
<!-- Access Control Section -->
<div v-if="filteredAccessControlItems.length > 0" class="nav-group nav-group-secondary">
<div v-if="!collapsed" class="nav-group-header">مدیریت دسترسی و امنیت</div>
<div class="nav-group-items">
<a-tooltip
v-for="item in filteredAccessControlItems"
:key="item.key"
:title="collapsed ? item.label : ''"
placement="left"
>
<div
class="nav-link-item"
:class="{ 'is-active': selectedKeys.includes(item.key) }"
@click="navigateTo(item)"
>
<component :is="item.icon" class="nav-link-icon" />
<span v-if="!collapsed" class="nav-link-text">{{ item.label }}</span>
</div>
</a-tooltip>
</div>
</div>
<!-- Empty search state -->
<div
v-if="!collapsed && filteredMainItems.length === 0 && filteredUtilityItems.length === 0"
v-if="!collapsed && filteredMainItems.length === 0 && filteredUtilityItems.length === 0 && filteredAccessControlItems.length === 0"
class="empty-search-text"
>
موردی یافت نشد
@@ -532,8 +577,24 @@ function confirmLogout() {
/* Navigation Area */
.menu-scroll-wrap {
flex: 1;
overflow: hidden;
overflow-y: auto;
overflow-x: hidden;
padding: 6px 10px 16px;
scrollbar-width: thin;
scrollbar-color: #cbd5e1 transparent;
}
.menu-scroll-wrap::-webkit-scrollbar {
width: 4px;
}
.menu-scroll-wrap::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border-radius: 4px;
}
.menu-scroll-wrap::-webkit-scrollbar-thumb:hover {
background-color: #94a3b8;
}
.sider-nav {
@@ -641,6 +702,9 @@ function confirmLogout() {
}
.app-header {
position: sticky;
top: 0;
z-index: 99;
background: #ffffff;
padding: 0 28px;
height: 64px;
+18 -3
View File
@@ -19,14 +19,28 @@ const titles: Record<string, string> = {
'm-sms': 'لاگ پیامک',
'm-reports': 'گزارش‌ها',
'm-audit': 'لاگ بازرسی',
'm-managers': 'مدیریت مدیران',
'm-managers-create': 'افزودن مدیر جدید',
'm-managers-edit': 'ویرایش اطلاعات مدیر',
'm-roles': 'نقش‌ها و دسترسی‌ها',
'm-workspaces': 'فضاهای کاری',
'm-forbidden': 'عدم دسترسی',
};
const hideBottomNavRoutes = new Set(['m-campaign-create', 'm-campaign-edit', 'm-user-detail']);
const hideBottomNavRoutes = new Set([
'm-campaign-create',
'm-campaign-edit',
'm-user-detail',
'm-managers-create',
'm-managers-edit',
]);
const showBackRoutes = new Set([
'm-campaign-create',
'm-campaign-edit',
'm-campaign-prizes',
'm-user-detail',
'm-managers-create',
'm-managers-edit',
]);
const pageTitle = computed(() => {
@@ -79,8 +93,9 @@ watch(
<style scoped>
.mobile-shell {
min-height: 100vh;
min-height: 100dvh;
height: 100vh;
height: 100dvh;
max-height: 100dvh;
background: #f4f6fa;
display: flex;
flex-direction: column;
+2
View File
@@ -5,6 +5,7 @@ import { createPinia } from 'pinia';
import { createApp } from 'vue';
import App from './App.vue';
import { bindDeviceRouter } from './composables/useDevice';
import { setupPermissionDirective } from './directives/permission.directive';
import { router } from './router';
const app = createApp(App);
@@ -13,6 +14,7 @@ const pinia = createPinia();
app.use(pinia);
app.use(router);
app.use(Antd);
setupPermissionDirective(app);
bindDeviceRouter(router);
+78 -1
View File
@@ -86,6 +86,29 @@ const routes: RouteRecordRaw[] = [
name: 'audit',
component: () => import('../views/audit/AuditLogView.vue'),
},
{
path: 'managers',
name: 'managers',
component: () => import('../views/managers/AdminListView.vue'),
meta: { permission: 'admins:read' },
},
{
path: 'roles',
name: 'roles',
component: () => import('../views/roles/RoleMatrixView.vue'),
meta: { permission: 'roles:manage' },
},
{
path: 'workspaces',
name: 'workspaces',
component: () => import('../views/workspaces/WorkspaceListView.vue'),
meta: { permission: 'workspaces:manage' },
},
{
path: '403',
name: 'forbidden',
component: () => import('../views/errors/ForbiddenView.vue'),
},
],
},
{
@@ -154,6 +177,41 @@ const routes: RouteRecordRaw[] = [
name: 'm-audit',
component: () => import('../views/mobile/MAuditLogView.vue'),
},
{
path: 'managers',
name: 'm-managers',
component: () => import('../views/mobile/MAdminListView.vue'),
meta: { permission: 'admins:read' },
},
{
path: 'managers/create',
name: 'm-managers-create',
component: () => import('../views/mobile/MAdminFormView.vue'),
meta: { permission: 'admins:manage' },
},
{
path: 'managers/:id/edit',
name: 'm-managers-edit',
component: () => import('../views/mobile/MAdminFormView.vue'),
meta: { permission: 'admins:manage' },
},
{
path: 'roles',
name: 'm-roles',
component: () => import('../views/mobile/MRoleMatrixView.vue'),
meta: { permission: 'roles:manage' },
},
{
path: 'workspaces',
name: 'm-workspaces',
component: () => import('../views/mobile/MWorkspaceListView.vue'),
meta: { permission: 'workspaces:manage' },
},
{
path: '403',
name: 'm-forbidden',
component: () => import('../views/mobile/MForbiddenView.vue'),
},
],
},
{
@@ -184,7 +242,7 @@ function toDesktopPath(to: { name?: string | symbol | null; params: Record<strin
router.beforeEach(async (to, _from, next) => {
const token = localStorage.getItem('admin_access_token');
const mobile = checkIsMobile();
const isMobileRoute = to.path.startsWith('/m') || !!to.meta.mobile;
const isMobileRoute = to.path === '/m' || to.path.startsWith('/m/') || !!to.meta.mobile;
if (to.meta.requiresAuth && !token) {
next(mobile ? '/m/login' : '/login');
@@ -227,5 +285,24 @@ router.beforeEach(async (to, _from, next) => {
}
}
// Permission authorization check
if (token && to.meta.permission) {
const adminInfoStr = localStorage.getItem('admin_info');
if (adminInfoStr) {
try {
const adminInfo = JSON.parse(adminInfoStr);
const isSuper = !!adminInfo.isSuperAdmin || adminInfo.role === 'SUPER_ADMIN';
const permissions: string[] = adminInfo.permissions || [];
const requiredPerm = to.meta.permission as string;
if (!isSuper && !permissions.includes(requiredPerm)) {
next(mobile ? '/m/403' : '/403');
return;
}
} catch {
// Fallback
}
}
}
next();
});
+50 -2
View File
@@ -9,6 +9,19 @@ export interface AdminUser {
email: string;
mobile: string;
role: string;
isSuperAdmin?: boolean;
permissions?: string[];
roles?: Array<{
id: string;
code: string;
name: string;
}>;
workspaces?: Array<{
workspaceId: string;
workspaceName: string;
scopeType: string;
specificCampaignIds?: string[] | null;
}>;
}
export const useAuthStore = defineStore('adminAuth', () => {
@@ -21,6 +34,33 @@ export const useAuthStore = defineStore('adminAuth', () => {
const loading = ref(false);
const isAuthenticated = computed(() => !!token.value);
const isSuperAdmin = computed(() => !!admin.value?.isSuperAdmin || admin.value?.role === 'SUPER_ADMIN');
const userPermissions = computed(() => admin.value?.permissions || []);
function can(permissionCode: string): boolean {
if (!isAuthenticated.value) return false;
if (isSuperAdmin.value) return true;
return userPermissions.value.includes(permissionCode);
}
function canAny(permissionCodes: string[]): boolean {
if (!isAuthenticated.value) return false;
if (isSuperAdmin.value) return true;
return permissionCodes.some((code) => userPermissions.value.includes(code));
}
async function fetchMe() {
if (!token.value) return null;
try {
const response = await apiClient.get('/admin/auth/me');
const data = response.data;
admin.value = data;
localStorage.setItem('admin_info', JSON.stringify(data));
return data;
} catch (err) {
return null;
}
}
async function login(credentials: { email: string; password: string }) {
loading.value = true;
@@ -59,8 +99,11 @@ export const useAuthStore = defineStore('adminAuth', () => {
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));
admin.value = {
...admin.value,
...response.data,
};
localStorage.setItem('admin_info', JSON.stringify(admin.value));
return response.data;
} finally {
loading.value = false;
@@ -72,6 +115,11 @@ export const useAuthStore = defineStore('adminAuth', () => {
admin,
loading,
isAuthenticated,
isSuperAdmin,
userPermissions,
can,
canAny,
fetchMe,
login,
logout,
updateProfile,
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
function goHome() {
router.push('/dashboard');
}
</script>
<template>
<div class="forbidden-page">
<a-card :bordered="false" class="forbidden-card">
<a-result
status="403"
title="۴۰۳ — دسترسی غیرمجاز"
sub-title="شما مجوز لازم برای مشاهده این بخش یا انجام این عملیات را در سامانه ندارید."
>
<template #extra>
<a-button type="primary" @click="goHome">
بازگشت به داشبورد
</a-button>
</template>
</a-result>
</a-card>
</div>
</template>
<style scoped>
.forbidden-page {
padding: 40px 24px;
display: flex;
justify-content: center;
align-items: center;
min-height: 70vh;
}
.forbidden-card {
max-width: 600px;
width: 100%;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
}
</style>
@@ -0,0 +1,389 @@
<script setup lang="ts">
import {
ApartmentOutlined,
KeyOutlined,
SafetyCertificateOutlined,
SaveOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { apiClient } from '../../services/api/client';
const props = defineProps<{
open: boolean;
adminId?: string | null;
}>();
const emit = defineEmits<{
(e: 'update:open', val: boolean): void;
(e: 'saved'): void;
}>();
const loading = ref(false);
const submitting = ref(false);
const rolesList = ref<any[]>([]);
const workspacesList = ref<any[]>([]);
const campaignsList = ref<any[]>([]);
const formState = reactive({
name: '',
email: '',
mobile: '',
password: '',
status: 'ACTIVE',
isSuperAdmin: false,
roleIds: [] as string[],
workspaceId: '',
scopeType: 'ALL',
specificCampaignIds: [] as string[],
});
const isEdit = computed(() => !!props.adminId);
async function loadMetadata() {
try {
const [rolesRes, wsRes, campRes] = await Promise.all([
apiClient.get('/admin/roles'),
apiClient.get('/admin/workspaces'),
apiClient.get('/admin/campaigns', { params: { pageSize: 100 } }),
]);
rolesList.value = Array.isArray(rolesRes.data) ? rolesRes.data : rolesRes.data?.data || [];
workspacesList.value = Array.isArray(wsRes.data) ? wsRes.data : wsRes.data?.data || [];
campaignsList.value = Array.isArray(campRes.data) ? campRes.data : campRes.data?.data || [];
} catch (err) {
// Handled
}
}
async function loadAdminData(id: string) {
loading.value = true;
try {
const res = await apiClient.get(`/admin/managers/${id}`);
const a = res.data;
formState.name = a.name || '';
formState.email = a.email || '';
formState.mobile = a.mobile || '';
formState.password = '';
formState.status = a.status || 'ACTIVE';
formState.isSuperAdmin = !!a.isSuperAdmin;
formState.roleIds = a.roles ? a.roles.map((r: any) => r.role?.id || r.roleId) : [];
if (a.workspaceAssignments && a.workspaceAssignments.length > 0) {
const wa = a.workspaceAssignments[0];
formState.workspaceId = wa.workspaceId || wa.workspace?.id || '';
formState.scopeType = wa.scopeType || 'ALL';
formState.specificCampaignIds = Array.isArray(wa.specificCampaignIds) ? wa.specificCampaignIds : [];
} else {
formState.workspaceId = workspacesList.value[0]?.id || '';
formState.scopeType = 'ALL';
formState.specificCampaignIds = [];
}
} finally {
loading.value = false;
}
}
function resetForm() {
formState.name = '';
formState.email = '';
formState.mobile = '';
formState.password = '';
formState.status = 'ACTIVE';
formState.isSuperAdmin = false;
formState.roleIds = [];
formState.workspaceId = workspacesList.value[0]?.id || '';
formState.scopeType = 'ALL';
formState.specificCampaignIds = [];
}
watch(
() => props.open,
(val) => {
if (val) {
loadMetadata();
if (props.adminId) {
loadAdminData(props.adminId);
} else {
resetForm();
}
}
},
);
async function onSubmit() {
if (!formState.name.trim()) {
message.error('لطفاً نام و نام خانوادگی مدیر را وارد کنید.');
return;
}
if (!formState.email.trim() || !formState.email.includes('@')) {
message.error('لطفاً یک آدرس ایمیل معتبر وارد کنید.');
return;
}
if (!formState.mobile.trim() || !/^09\d{9}$/.test(formState.mobile.trim())) {
message.error('شماره موبایل باید ۱۱ رقمی بوده و با ۰۹ شروع شود.');
return;
}
if (!isEdit.value && (!formState.password || formState.password.length < 6)) {
message.error('کلمه عبور باید حداقل ۶ کاراکتر باشد.');
return;
}
submitting.value = true;
try {
const payload: any = {
name: formState.name.trim(),
email: formState.email.trim().toLowerCase(),
mobile: formState.mobile.trim(),
status: formState.status,
isSuperAdmin: formState.isSuperAdmin,
roleIds: formState.roleIds,
};
if (formState.password) {
payload.password = formState.password;
}
if (formState.workspaceId) {
payload.workspaceAssignments = [
{
workspaceId: formState.workspaceId,
scopeType: formState.scopeType,
specificCampaignIds:
formState.scopeType === 'CUSTOM_CAMPAIGNS' ? formState.specificCampaignIds : undefined,
},
];
}
if (isEdit.value && props.adminId) {
await apiClient.patch(`/admin/managers/${props.adminId}`, payload);
message.success('مشخصات مدیر با موفقیت به‌روزرسانی شد.');
} else {
await apiClient.post('/admin/managers', payload);
message.success('مدیر جدید با موفقیت ایجاد شد.');
}
emit('saved');
emit('update:open', false);
} finally {
submitting.value = false;
}
}
</script>
<template>
<a-drawer
:open="open"
:title="isEdit ? 'ویرایش اطلاعات مدیر' : 'افزودن مدیر جدید به سامانه'"
:width="540"
placement="right"
:maskClosable="false"
rootClassName="admin-form-drawer-rtl"
@close="emit('update:open', false)"
>
<a-spin :spinning="loading">
<a-form layout="vertical">
<a-tabs defaultActiveKey="1" class="admin-form-tabs">
<!-- TAB 1: اطلاعات فردی و ورود -->
<a-tab-pane key="1">
<template #tab>
<span><UserOutlined /> اطلاعات هویتی</span>
</template>
<a-form-item label="نام و نام خانوادگی" required>
<a-input
v-model:value="formState.name"
placeholder="مثال: علی رضایی"
size="large"
/>
</a-form-item>
<a-form-item label="شماره موبایل سازمانی" required>
<a-input
v-model:value="formState.mobile"
placeholder="09xxxxxxxxx"
dir="ltr"
size="large"
maxLength="11"
/>
</a-form-item>
<a-form-item label="ایمیل سازمانی (نام کاربری ورود)" required>
<a-input
v-model:value="formState.email"
placeholder="admin@example.com"
dir="ltr"
size="large"
/>
</a-form-item>
<a-form-item
:label="isEdit ? 'کلمه عبور جدید (در صورت نیاز به تغییر)' : 'کلمه عبور ورود'"
:required="!isEdit"
>
<a-input-password
v-model:value="formState.password"
placeholder="حداقل ۶ کاراکتر"
size="large"
dir="ltr"
/>
</a-form-item>
<a-form-item label="وضعیت حساب کاربری">
<a-radio-group v-model:value="formState.status" buttonStyle="solid">
<a-radio-button value="ACTIVE">فعال</a-radio-button>
<a-radio-button value="DISABLED">غیرفعال / مسدود</a-radio-button>
</a-radio-group>
</a-form-item>
</a-tab-pane>
<!-- TAB 2: نقش‌ها و اختیارات -->
<a-tab-pane key="2">
<template #tab>
<span><SafetyCertificateOutlined /> نقش‌ها و اختیارات</span>
</template>
<div class="super-admin-banner">
<a-checkbox v-model:checked="formState.isSuperAdmin">
<strong style="color: #7c3aed; font-size: 15px">مدیر ارشد سامانه (Super Admin)</strong>
</a-checkbox>
<p class="banner-sub">
مدیر ارشد دسترسی کامل و بی‌قیدوشرط به تمامی بخش‌ها، ایجاد مدیران و تنظیمات کلی سامانه دارد.
</p>
</div>
<a-divider style="margin: 16px 0" />
<a-form-item label="نقش‌های سازمانی (می‌توانید چند نقش همزمان انتخاب کنید)">
<div v-if="rolesList.length === 0" style="color: #94a3b8">
در حال بارگذاری نقش‌ها...
</div>
<a-checkbox-group
v-else
v-model:value="formState.roleIds"
style="width: 100%"
:disabled="formState.isSuperAdmin"
>
<a-row :gutter="[12, 12]">
<a-col :span="24" v-for="r in rolesList" :key="r.id">
<div class="role-item-card" :class="{ 'is-selected': formState.roleIds.includes(r.id) }">
<a-checkbox :value="r.id">
<span class="role-name">{{ r.name }}</span>
<a-tag v-if="r.isSystem" color="blue" size="small" style="margin-right: 6px">سیستمی</a-tag>
</a-checkbox>
<div class="role-desc">{{ r.description || 'بدون توضیح' }}</div>
</div>
</a-col>
</a-row>
</a-checkbox-group>
</a-form-item>
</a-tab-pane>
<!-- TAB 3: فضاهای کاری و اسکوپ دسترسی -->
<a-tab-pane key="3">
<template #tab>
<span><ApartmentOutlined /> قلمرو و فضای کاری</span>
</template>
<a-form-item label="فضای کاری مجاز">
<a-select
v-model:value="formState.workspaceId"
placeholder="انتخاب فضای کاری"
size="large"
style="width: 100%"
>
<a-select-option v-for="ws in workspacesList" :key="ws.id" :value="ws.id">
{{ ws.name }} ({{ ws.slug }})
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="قلمرو دسترسی کمپین‌ها (Scope)">
<a-radio-group v-model:value="formState.scopeType" style="margin-bottom: 12px">
<a-radio value="ALL">دسترسی به تمامی کمپین‌های این فضای کاری</a-radio>
<a-radio value="CUSTOM_CAMPAIGNS">محدود به کمپین‌های خاص</a-radio>
</a-radio-group>
<div v-if="formState.scopeType === 'CUSTOM_CAMPAIGNS'" style="margin-top: 8px">
<a-select
v-model:value="formState.specificCampaignIds"
mode="multiple"
placeholder="کمپین‌های مجاز را انتخاب کنید"
size="large"
style="width: 100%"
>
<a-select-option v-for="c in campaignsList" :key="c.id" :value="c.id">
{{ c.title }}
</a-select-option>
</a-select>
</div>
</a-form-item>
</a-tab-pane>
</a-tabs>
</a-form>
</a-spin>
<template #footer>
<div style="display: flex; justify-content: flex-end; gap: 8px">
<a-button @click="emit('update:open', false)">انصراف</a-button>
<a-button type="primary" :loading="submitting" @click="onSubmit">
<template #icon><SaveOutlined /></template>
{{ isEdit ? 'ذخیره تغییرات' : 'ثبت مدیر' }}
</a-button>
</div>
</template>
</a-drawer>
</template>
<style scoped>
.super-admin-banner {
background: #f5f3ff;
border: 1px solid #ddd6fe;
border-radius: 8px;
padding: 12px 16px;
}
.banner-sub {
margin: 6px 0 0 24px;
font-size: 12px;
color: #6b7280;
line-height: 1.6;
}
.role-item-card {
padding: 10px 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
transition: all 0.2s ease;
background: #fff;
}
.role-item-card.is-selected {
border-color: #3b82f6;
background: #eff6ff;
}
.role-name {
font-weight: 600;
font-size: 14px;
color: #1e293b;
}
.role-desc {
margin-right: 24px;
font-size: 12px;
color: #64748b;
margin-top: 2px;
}
:deep(.ant-drawer-content) {
direction: rtl;
text-align: right;
}
:deep(.ant-drawer-header) {
direction: rtl;
text-align: right;
}
:deep(.ant-drawer-title) {
text-align: right;
}
:deep(.ant-drawer-close) {
margin-right: 0;
margin-left: var(--ant-margin-xs, 8px);
}
</style>
@@ -0,0 +1,518 @@
<script setup lang="ts">
import {
CheckCircleOutlined,
CloseCircleOutlined,
CrownOutlined,
DeleteOutlined,
EditOutlined,
KeyOutlined,
MoreOutlined,
PlusOutlined,
SearchOutlined,
StopOutlined,
TeamOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import { onMounted, reactive, ref } from 'vue';
import { toFarsiDigits } from '@lucky-wheel/shared-types';
import { useAuthStore } from '../../stores/auth.store';
import { apiClient } from '../../services/api/client';
import AdminFormDrawer from './AdminFormDrawer.vue';
const authStore = useAuthStore();
const loading = ref(false);
const admins = ref<any[]>([]);
const total = ref(0);
const rolesList = ref<any[]>([]);
const filters = reactive({
page: 1,
pageSize: 10,
search: '',
roleCode: undefined as string | undefined,
status: undefined as string | undefined,
});
// Drawer state
const isDrawerOpen = ref(false);
const editingAdminId = ref<string | null>(null);
// Reset password modal state
const isPasswordModalOpen = ref(false);
const resettingAdmin = ref<any>(null);
const newPassword = ref('');
const isSubmittingPassword = ref(false);
async function loadRoles() {
try {
const res = await apiClient.get('/admin/roles');
rolesList.value = Array.isArray(res.data) ? res.data : res.data?.data || [];
} catch {
// Handled
}
}
async function loadAdmins() {
loading.value = true;
try {
const res = await apiClient.get('/admin/managers', {
params: {
page: filters.page,
pageSize: filters.pageSize,
search: filters.search || undefined,
roleCode: filters.roleCode || undefined,
status: filters.status || undefined,
},
});
const items = Array.isArray(res.data) ? res.data : res.data?.data || [];
admins.value = items;
total.value = res.meta?.total || res.data?.meta?.total || items.length;
} finally {
loading.value = false;
}
}
function openCreateDrawer() {
editingAdminId.value = null;
isDrawerOpen.value = true;
}
function openEditDrawer(record: any) {
editingAdminId.value = record.id;
isDrawerOpen.value = true;
}
function openResetPasswordModal(record: any) {
resettingAdmin.value = record;
newPassword.value = '';
isPasswordModalOpen.value = true;
}
async function handleResetPassword() {
if (!newPassword.value || newPassword.value.length < 6) {
message.error('کلمه عبور جدید باید حداقل ۶ کاراکتر باشد.');
return;
}
isSubmittingPassword.value = true;
try {
await apiClient.patch(`/admin/managers/${resettingAdmin.value.id}/password`, {
password: newPassword.value,
});
message.success(`کلمه عبور مدیر ${resettingAdmin.value.name} با موفقیت بازنشانی شد.`);
isPasswordModalOpen.value = false;
} finally {
isSubmittingPassword.value = false;
}
}
function confirmToggleStatus(record: any) {
const isCurrentlyActive = record.status === 'ACTIVE';
const newStatus = isCurrentlyActive ? 'DISABLED' : 'ACTIVE';
Modal.confirm({
title: isCurrentlyActive ? 'غیرفعال‌سازی حساب مدیر' : 'فعال‌سازی مجدد حساب مدیر',
content: isCurrentlyActive
? `آیا از غیرفعال‌سازی دسترسی مدیر ${record.name} اطمینان دارید؟ دسترسی ایشان به پنل بلافاصله مسدود می‌شود.`
: `آیا از فعال‌سازی مجدد حساب کاربری ${record.name} اطمینان دارید؟`,
okText: isCurrentlyActive ? 'غیرفعال کن' : 'فعال کن',
okType: isCurrentlyActive ? 'danger' : 'primary',
cancelText: 'انصراف',
onOk: async () => {
try {
await apiClient.patch(`/admin/managers/${record.id}/status`, { status: newStatus });
message.success(isCurrentlyActive ? 'حساب کاربری غیرفعال شد.' : 'حساب کاربری فعال شد.');
loadAdmins();
} catch {
// Handled by client interceptor
}
},
});
}
function confirmDeleteAdmin(record: any) {
Modal.confirm({
title: 'حذف مدیر از سامانه',
content: `آیا از حذف دائم حساب مدیر ${record.name} اطمینان دارید؟ این عملیات غیرقابل بازگشت است.`,
okText: 'حذف مدیر',
okType: 'danger',
cancelText: 'انصراف',
onOk: async () => {
try {
await apiClient.delete(`/admin/managers/${record.id}`);
message.success('مدیر با موفقیت حذف شد.');
loadAdmins();
} catch {
// Handled by client interceptor
}
},
});
}
const columns = [
{ title: 'مشخصات مدیر', key: 'adminProfile', width: 220 },
{ title: 'شماره موبایل', dataIndex: 'mobile', key: 'mobile', width: 140 },
{ title: 'نقش‌ها و سطح اختیارات', key: 'roles', minWidth: 200 },
{ title: 'فضای کاری و اسکوپ', key: 'workspaces', minWidth: 160 },
{ title: 'وضعیت', dataIndex: 'status', key: 'status', align: 'center' as const, width: 100 },
{ title: 'آخرین ورود', dataIndex: 'lastLoginAt', key: 'lastLoginAt', width: 130 },
{ title: 'عملیات', key: 'actions', width: 70, align: 'center' as const },
];
onMounted(() => {
loadRoles();
loadAdmins();
});
</script>
<template>
<div class="admin-list-page">
<div class="page-header-title">
<div>
<h1 class="title">مدیریت مدیران و دسترسی‌ها</h1>
<p class="subtitle">مشاهده لیست مدیران، تخصیص نقش‌ها، فضاهای کاری و اسکوپ دسترسی</p>
</div>
<div class="page-actions" v-if="authStore.can('admins:manage')">
<a-button type="primary" size="large" @click="openCreateDrawer">
<template #icon><PlusOutlined /></template>
افزودن مدیر جدید
</a-button>
</div>
</div>
<a-card :bordered="false" class="main-card">
<div class="table-header">
<div class="filters">
<a-input-search
v-model:value="filters.search"
placeholder="جستجوی نام، ایمیل یا موبایل..."
style="width: 280px"
allowClear
@search="() => { filters.page = 1; loadAdmins(); }"
>
<template #prefix><SearchOutlined style="color: #94a3b8; margin-left: 4px" /></template>
</a-input-search>
<a-select
v-model:value="filters.roleCode"
placeholder="فیلتر بر اساس نقش"
allowClear
style="width: 180px"
@change="() => { filters.page = 1; loadAdmins(); }"
>
<a-select-option v-for="r in rolesList" :key="r.id" :value="r.code">
{{ r.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="filters.status"
placeholder="وضعیت حساب"
allowClear
style="width: 140px"
@change="() => { filters.page = 1; loadAdmins(); }"
>
<a-select-option value="ACTIVE">فعال</a-select-option>
<a-select-option value="DISABLED">غیرفعال</a-select-option>
</a-select>
</div>
</div>
<a-table
:dataSource="admins"
:columns="columns"
:loading="loading"
rowKey="id"
size="middle"
:pagination="{
current: filters.page,
pageSize: filters.pageSize,
total: total,
showTotal: (total: number) => `مجموع: ${toFarsiDigits(total)} مدیر`,
onChange: (page: number) => {
filters.page = page;
loadAdmins();
},
}"
>
<template #bodyCell="{ column, record }">
<!-- Profile Column -->
<template v-if="column.key === 'adminProfile'">
<div class="admin-profile-cell">
<a-avatar
:style="{
backgroundColor: record.isSuperAdmin ? '#7c3aed' : '#2563eb',
verticalAlign: 'middle',
}"
>
{{ record.name?.charAt(0) || 'U' }}
</a-avatar>
<div class="admin-details">
<span class="admin-name">
{{ record.name }}
<a-tooltip v-if="record.isSuperAdmin" title="مدیر ارشد سامانه">
<CrownOutlined style="color: #f59e0b; margin-right: 4px" />
</a-tooltip>
</span>
<span class="admin-email">{{ record.email }}</span>
</div>
</div>
</template>
<!-- Mobile -->
<template v-else-if="column.key === 'mobile'">
<span class="mobile-cell">{{ toFarsiDigits(record.mobile) }}</span>
</template>
<!-- Roles -->
<template v-else-if="column.key === 'roles'">
<div class="roles-tag-group">
<a-tag v-if="record.isSuperAdmin" color="purple" class="role-pill">
مدیر ارشد (Super Admin)
</a-tag>
<template v-if="record.roles && record.roles.length > 0">
<a-tag
v-for="ra in record.roles"
:key="ra.role?.id"
color="blue"
class="role-pill"
>
{{ ra.role?.name || ra.role?.code }}
</a-tag>
</template>
<span v-else-if="!record.isSuperAdmin" style="color: #94a3b8; font-size: 13px">
بدون نقش
</span>
</div>
</template>
<!-- Workspaces & Scopes -->
<template v-else-if="column.key === 'workspaces'">
<div class="workspaces-tag-group">
<template v-if="record.workspaceAssignments && record.workspaceAssignments.length > 0">
<a-tag
v-for="(wa, idx) in record.workspaceAssignments"
:key="idx"
color="cyan"
class="scope-pill"
>
{{ wa.workspace?.name || 'فضای اصلی' }}
<span v-if="wa.scopeType === 'CUSTOM_CAMPAIGNS'" style="font-size: 11px; opacity: 0.8">
(کمپین‌های خاص)
</span>
</a-tag>
</template>
<span v-else style="color: #94a3b8; font-size: 13px">
سراسری
</span>
</div>
</template>
<!-- Status -->
<template v-else-if="column.key === 'status'">
<a-tag :color="record.status === 'DISABLED' ? 'red' : 'green'" class="status-tag">
<span
class="status-dot"
:style="{ backgroundColor: record.status === 'DISABLED' ? '#ef4444' : '#10b981' }"
/>
{{ record.status === 'DISABLED' ? 'غیرفعال' : 'فعال' }}
</a-tag>
</template>
<!-- Last Login -->
<template v-else-if="column.key === 'lastLoginAt'">
<span class="date-cell">
{{ record.lastLoginAt ? toFarsiDigits(new Date(record.lastLoginAt).toLocaleDateString('fa-IR')) : '—' }}
</span>
</template>
<!-- Actions -->
<template v-else-if="column.key === 'actions'">
<a-dropdown :trigger="['click']" placement="bottomLeft">
<a-button type="text" shape="circle" class="action-more-btn">
<MoreOutlined style="font-size: 18px" />
</a-button>
<template #overlay>
<a-menu class="action-dropdown-menu">
<a-menu-item
v-if="authStore.can('admins:manage')"
key="edit"
@click="openEditDrawer(record)"
>
<EditOutlined style="margin-left: 8px" />
ویرایش مشخصات و دسترسی‌ها
</a-menu-item>
<a-menu-item
v-if="authStore.can('admins:manage')"
key="resetPassword"
@click="openResetPasswordModal(record)"
>
<KeyOutlined style="margin-left: 8px" />
بازنشانی کلمه عبور
</a-menu-item>
<a-menu-divider v-if="authStore.can('admins:manage') && record.id !== authStore.admin?.id" />
<a-menu-item
v-if="authStore.can('admins:manage') && record.id !== authStore.admin?.id"
key="toggleStatus"
@click="confirmToggleStatus(record)"
>
<template v-if="record.status === 'ACTIVE'">
<StopOutlined style="margin-left: 8px; color: #ef4444" />
<span style="color: #ef4444">غیرفعال‌سازی دسترسی</span>
</template>
<template v-else>
<CheckCircleOutlined style="margin-left: 8px; color: #10b981" />
<span style="color: #10b981">فعال‌سازی مجدد</span>
</template>
</a-menu-item>
<a-menu-item
v-if="authStore.can('admins:manage') && record.id !== authStore.admin?.id"
key="delete"
danger
@click="confirmDeleteAdmin(record)"
>
<DeleteOutlined style="margin-left: 8px" />
حذف مدیر
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</template>
</a-table>
</a-card>
<!-- Create / Edit Drawer -->
<AdminFormDrawer
v-model:open="isDrawerOpen"
:adminId="editingAdminId"
@saved="loadAdmins"
/>
<!-- Reset Password Modal -->
<a-modal
v-model:open="isPasswordModalOpen"
title="بازنشانی کلمه عبور مدیر"
okText="تغییر کلمه عبور"
cancelText="انصراف"
:confirmLoading="isSubmittingPassword"
@ok="handleResetPassword"
>
<div style="margin-bottom: 16px">
کلمه عبور جدید را برای <strong>{{ resettingAdmin?.name }}</strong> وارد کنید:
</div>
<a-input-password
v-model:value="newPassword"
placeholder="حداقل ۶ کاراکتر"
size="large"
dir="ltr"
/>
</a-modal>
</div>
</template>
<style scoped>
.admin-list-page {
padding-bottom: 24px;
}
.page-header-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title {
font-size: 22px;
font-weight: 700;
color: #1e293b;
margin: 0;
}
.subtitle {
font-size: 13px;
color: #64748b;
margin-top: 4px;
margin-bottom: 0;
}
.main-card {
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.table-header {
margin-bottom: 16px;
}
.filters {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.admin-profile-cell {
display: flex;
align-items: center;
gap: 12px;
}
.admin-details {
display: flex;
flex-direction: column;
}
.admin-name {
font-weight: 600;
font-size: 14px;
color: #1e293b;
}
.admin-email {
font-size: 12px;
color: #64748b;
direction: ltr;
text-align: right;
}
.mobile-cell {
font-weight: 500;
color: #334155;
direction: ltr;
display: inline-block;
}
.roles-tag-group,
.workspaces-tag-group {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.role-pill,
.scope-pill {
border-radius: 6px;
font-size: 12px;
padding: 2px 8px;
}
.status-tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-block;
}
.date-cell {
font-size: 13px;
color: #64748b;
}
.action-more-btn {
color: #64748b;
}
.action-more-btn:hover {
background-color: #f1f5f9;
color: #1e293b;
}
</style>
@@ -0,0 +1,495 @@
<script setup lang="ts">
import {
ApartmentOutlined,
CrownOutlined,
SafetyCertificateOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { apiClient } from '../../services/api/client';
const route = useRoute();
const router = useRouter();
const isEdit = computed(() => !!route.params.id);
const adminId = computed(() => route.params.id as string | undefined);
const loading = ref(false);
const submitting = ref(false);
const step = ref(0);
const rolesList = ref<any[]>([]);
const workspacesList = ref<any[]>([]);
const campaignsList = ref<any[]>([]);
const formState = reactive({
name: '',
email: '',
mobile: '',
password: '',
status: 'ACTIVE',
isSuperAdmin: false,
roleIds: [] as string[],
workspaceId: '',
scopeType: 'ALL',
specificCampaignIds: [] as string[],
});
async function loadMetadata() {
try {
const [rolesRes, wsRes, campRes] = await Promise.all([
apiClient.get('/admin/roles'),
apiClient.get('/admin/workspaces'),
apiClient.get('/admin/campaigns', { params: { pageSize: 100 } }),
]);
rolesList.value = Array.isArray(rolesRes.data) ? rolesRes.data : rolesRes.data?.data || [];
workspacesList.value = Array.isArray(wsRes.data) ? wsRes.data : wsRes.data?.data || [];
campaignsList.value = Array.isArray(campRes.data) ? campRes.data : campRes.data?.data || [];
if (!formState.workspaceId && workspacesList.value.length > 0) {
formState.workspaceId = workspacesList.value[0].id;
}
} catch (err) {
// Handled
}
}
async function loadAdminData(id: string) {
loading.value = true;
try {
const res = await apiClient.get(`/admin/managers/${id}`);
const a = res.data;
formState.name = a.name || '';
formState.email = a.email || '';
formState.mobile = a.mobile || '';
formState.password = '';
formState.status = a.status || 'ACTIVE';
formState.isSuperAdmin = !!a.isSuperAdmin;
formState.roleIds = a.roles ? a.roles.map((r: any) => r.role?.id || r.roleId) : [];
if (a.workspaceAssignments && a.workspaceAssignments.length > 0) {
const wa = a.workspaceAssignments[0];
formState.workspaceId = wa.workspaceId || wa.workspace?.id || '';
formState.scopeType = wa.scopeType || 'ALL';
formState.specificCampaignIds = Array.isArray(wa.specificCampaignIds) ? wa.specificCampaignIds : [];
} else if (workspacesList.value.length > 0) {
formState.workspaceId = workspacesList.value[0].id;
}
} finally {
loading.value = false;
}
}
function nextStep() {
if (step.value === 0) {
if (!formState.name.trim()) {
message.error('لطفاً نام و نام خانوادگی مدیر را وارد کنید.');
return;
}
if (!formState.mobile.trim() || !/^09\d{9}$/.test(formState.mobile.trim())) {
message.error('شماره موبایل باید ۱۱ رقمی باشد و با ۰۹ شروع شود.');
return;
}
if (!formState.email.trim() || !/^\S+@\S+\.\S+$/.test(formState.email.trim())) {
message.error('لطفاً یک آدرس ایمیل معتبر وارد کنید.');
return;
}
if (!isEdit.value && (!formState.password || formState.password.length < 6)) {
message.error('کلمه عبور ورود باید حداقل ۶ کاراکتر باشد.');
return;
}
}
if (step.value === 1) {
if (!formState.isSuperAdmin && formState.roleIds.length === 0) {
message.error('لطفاً حداقل یک نقش سازمانی انتخاب کنید یا وضعیت مدیر ارشد را فعال نمایید.');
return;
}
}
step.value += 1;
}
function toggleRole(roleId: string) {
const idx = formState.roleIds.indexOf(roleId);
if (idx > -1) {
formState.roleIds.splice(idx, 1);
} else {
formState.roleIds.push(roleId);
}
}
async function onSubmit() {
if (!formState.name.trim()) {
message.error('نام الزامی است');
return;
}
submitting.value = true;
try {
const payload: any = {
name: formState.name.trim(),
email: formState.email.trim().toLowerCase(),
mobile: formState.mobile.trim(),
status: formState.status,
isSuperAdmin: formState.isSuperAdmin,
roleIds: formState.isSuperAdmin ? [] : formState.roleIds,
};
if (formState.password) {
payload.password = formState.password;
}
if (formState.workspaceId) {
payload.workspaceAssignments = [
{
workspaceId: formState.workspaceId,
scopeType: formState.scopeType,
specificCampaignIds:
formState.scopeType === 'CUSTOM_CAMPAIGNS' ? formState.specificCampaignIds : undefined,
},
];
}
if (isEdit.value && adminId.value) {
await apiClient.patch(`/admin/managers/${adminId.value}`, payload);
message.success('مشخصات مدیر با موفقیت به‌روزرسانی شد.');
} else {
await apiClient.post('/admin/managers', payload);
message.success('مدیر جدید با موفقیت ایجاد شد.');
}
router.push('/m/managers');
} catch (err: any) {
const errorMsg = err.response?.data?.message || 'خطا در ثبت اطلاعات مدیر';
message.error(Array.isArray(errorMsg) ? errorMsg.join(' | ') : errorMsg);
} finally {
submitting.value = false;
}
}
onMounted(async () => {
await loadMetadata();
if (adminId.value) {
await loadAdminData(adminId.value);
}
});
</script>
<template>
<div class="m-form-page">
<a-spin :spinning="loading">
<!-- Steps Indicator -->
<div class="m-steps">
<div class="m-step" :class="{ active: step === 0, done: step > 0 }">
<UserOutlined /> ۱. هویتی
</div>
<div class="m-step" :class="{ active: step === 1, done: step > 1 }">
<SafetyCertificateOutlined /> ۲. نقش‌ها
</div>
<div class="m-step" :class="{ active: step === 2 }">
<ApartmentOutlined /> ۳. دسترسی
</div>
</div>
<!-- STEP 1: اطلاعات هویتی و ورود -->
<div v-show="step === 0" class="m-card">
<a-form layout="vertical">
<a-form-item label="نام و نام خانوادگی" required>
<a-input
v-model:value="formState.name"
size="large"
placeholder="مثال: علی رضایی"
/>
</a-form-item>
<a-form-item label="شماره موبایل سازمانی" required>
<a-input
v-model:value="formState.mobile"
size="large"
placeholder="09xxxxxxxxx"
dir="ltr"
maxLength="11"
/>
</a-form-item>
<a-form-item label="ایمیل سازمانی (نام کاربری)" required>
<a-input
v-model:value="formState.email"
size="large"
placeholder="admin@example.com"
dir="ltr"
/>
</a-form-item>
<a-form-item
:label="isEdit ? 'کلمه عبور جدید (اختیاری)' : 'کلمه عبور ورود'"
:required="!isEdit"
>
<a-input-password
v-model:value="formState.password"
size="large"
placeholder="حداقل ۶ کاراکتر"
dir="ltr"
/>
</a-form-item>
<a-form-item label="وضعیت حساب کاربری">
<a-radio-group v-model:value="formState.status" buttonStyle="solid" class="m-radio-full">
<a-radio-button value="ACTIVE">فعال</a-radio-button>
<a-radio-button value="DISABLED">غیرفعال / مسدود</a-radio-button>
</a-radio-group>
</a-form-item>
</a-form>
</div>
<!-- STEP 2: نقش‌ها و اختیارات -->
<div v-show="step === 1" class="m-card">
<div class="m-super-admin-card" :class="{ 'is-selected': formState.isSuperAdmin }">
<a-checkbox v-model:checked="formState.isSuperAdmin">
<span class="m-super-title">
<CrownOutlined style="color: #f59e0b; margin-left: 6px" />
مدیر ارشد سامانه (Super Admin)
</span>
</a-checkbox>
<p class="m-super-desc">
مدیر ارشد به تمامی ماژول‌ها، نقش‌ها، گزارش‌ها و فضاهای کاری دسترسی کامل و بی‌قیدوشرط دارد.
</p>
</div>
<div v-if="!formState.isSuperAdmin" class="m-roles-container">
<div class="m-section-label">انتخاب نقش‌های سازمانی مدیر:</div>
<div class="m-roles-list">
<div
v-for="role in rolesList.filter(r => r.code !== 'SUPER_ADMIN')"
:key="role.id"
class="m-role-pill"
:class="{ 'is-active': formState.roleIds.includes(role.id) }"
@click="toggleRole(role.id)"
>
<div class="m-role-pill-top">
<a-checkbox :checked="formState.roleIds.includes(role.id)" />
<span class="m-role-name">{{ role.name }}</span>
</div>
<p class="m-role-desc">{{ role.description }}</p>
</div>
</div>
</div>
</div>
<!-- STEP 3: فضای کاری و اسکوپ دسترسی -->
<div v-show="step === 2" class="m-card">
<a-form layout="vertical">
<a-form-item label="انتخاب فضای کاری سازمانی" required>
<a-select v-model:value="formState.workspaceId" size="large" style="width: 100%">
<a-select-option v-for="ws in workspacesList" :key="ws.id" :value="ws.id">
{{ ws.name }} ({{ ws.slug }})
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="اسکوپ دسترسی کمپین‌ها">
<a-radio-group v-model:value="formState.scopeType" buttonStyle="solid" class="m-radio-full">
<a-radio-button value="ALL">همه کمپین‌ها</a-radio-button>
<a-radio-button value="CUSTOM_CAMPAIGNS">کمپین‌های مشخص</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item
v-if="formState.scopeType === 'CUSTOM_CAMPAIGNS'"
label="کمپین‌های مجاز برای این مدیر"
required
>
<a-select
v-model:value="formState.specificCampaignIds"
mode="multiple"
size="large"
placeholder="کمپین‌ها را انتخاب کنید"
style="width: 100%"
>
<a-select-option v-for="c in campaignsList" :key="c.id" :value="c.id">
{{ c.title }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</div>
<!-- Actions -->
<div class="m-form-actions">
<a-button v-if="step > 0" size="large" block @click="step -= 1">
مرحله قبلی
</a-button>
<a-button
v-if="step < 2"
type="primary"
size="large"
block
@click="nextStep"
>
مرحله بعدی
</a-button>
<a-button
v-else
type="primary"
size="large"
block
:loading="submitting"
@click="onSubmit"
>
{{ isEdit ? 'ذخیره تغییرات مدیر' : 'ایجاد مدیر جدید' }}
</a-button>
</div>
</a-spin>
</div>
</template>
<style scoped>
.m-form-page {
padding: 14px 14px 28px;
direction: rtl;
}
.m-steps {
display: flex;
gap: 6px;
margin-bottom: 14px;
}
.m-step {
flex: 1;
text-align: center;
font-size: 11px;
font-weight: 700;
padding: 10px 4px;
border-radius: 10px;
background: #e2e8f0;
color: #64748b;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
transition: all 0.2s ease;
}
.m-step.active {
background: #4f46e5;
color: #ffffff;
}
.m-step.done {
background: #c7d2fe;
color: #3730a3;
}
.m-card {
background: #ffffff;
border-radius: 16px;
padding: 18px 16px;
border: 1px solid #f1f5f9;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
}
.m-radio-full {
display: flex;
width: 100%;
}
.m-radio-full :deep(.ant-radio-button-wrapper) {
flex: 1;
text-align: center;
}
.m-super-admin-card {
background: #fdf4ff;
border: 1px solid #f0abfc;
border-radius: 12px;
padding: 14px;
margin-bottom: 16px;
transition: all 0.2s ease;
}
.m-super-admin-card.is-selected {
background: #fae8ff;
border-color: #d946ef;
box-shadow: 0 0 0 2px rgba(217, 70, 239, 0.15);
}
.m-super-title {
font-weight: 700;
font-size: 14px;
color: #86198f;
}
.m-super-desc {
font-size: 11.5px;
color: #701a75;
margin: 6px 24px 0 0;
line-height: 1.5;
}
.m-section-label {
font-size: 13px;
font-weight: 700;
color: #334155;
margin-bottom: 10px;
}
.m-roles-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.m-role-pill {
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 12px;
background: #f8fafc;
cursor: pointer;
transition: all 0.2s ease;
}
.m-role-pill.is-active {
border-color: #4f46e5;
background: #eef2ff;
}
.m-role-pill-top {
display: flex;
align-items: center;
gap: 8px;
}
.m-role-name {
font-weight: 700;
font-size: 13.5px;
color: #1e293b;
}
.m-role-desc {
margin: 4px 28px 0 0;
font-size: 11.5px;
color: #64748b;
line-height: 1.5;
}
.m-form-actions {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
}
.m-form-actions :deep(.ant-btn) {
height: 48px;
border-radius: 14px;
font-weight: 700;
font-size: 14px;
}
</style>
@@ -0,0 +1,387 @@
<script setup lang="ts">
import {
CheckCircleOutlined,
CrownOutlined,
DeleteOutlined,
EditOutlined,
KeyOutlined,
PlusOutlined,
SearchOutlined,
StopOutlined,
} from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import { onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { toFarsiDigits } from '@lucky-wheel/shared-types';
import { useAuthStore } from '../../stores/auth.store';
import { apiClient } from '../../services/api/client';
const router = useRouter();
const authStore = useAuthStore();
const loading = ref(false);
const admins = ref<any[]>([]);
const total = ref(0);
const rolesList = ref<any[]>([]);
const filters = reactive({
page: 1,
pageSize: 10,
search: '',
roleCode: undefined as string | undefined,
status: undefined as string | undefined,
});
const isDrawerOpen = ref(false);
const editingAdminId = ref<string | null>(null);
const isPasswordModalOpen = ref(false);
const resettingAdmin = ref<any>(null);
const newPassword = ref('');
const isSubmittingPassword = ref(false);
async function loadRoles() {
try {
const res = await apiClient.get('/admin/roles');
rolesList.value = Array.isArray(res.data) ? res.data : res.data?.data || [];
} catch {}
}
async function loadAdmins() {
loading.value = true;
try {
const res = await apiClient.get('/admin/managers', {
params: {
page: filters.page,
pageSize: filters.pageSize,
search: filters.search || undefined,
roleCode: filters.roleCode || undefined,
status: filters.status || undefined,
},
});
const items = Array.isArray(res.data) ? res.data : res.data?.data || [];
admins.value = items;
total.value = res.meta?.total || res.data?.meta?.total || items.length;
} finally {
loading.value = false;
}
}
function openCreate() {
router.push('/m/managers/create');
}
function openEdit(admin: any) {
router.push(`/m/managers/${admin.id}/edit`);
}
function openResetPassword(admin: any) {
resettingAdmin.value = admin;
newPassword.value = '';
isPasswordModalOpen.value = true;
}
async function handleResetPassword() {
if (!newPassword.value || newPassword.value.length < 6) {
message.error('کلمه عبور باید حداقل ۶ کاراکتر باشد.');
return;
}
isSubmittingPassword.value = true;
try {
await apiClient.patch(`/admin/managers/${resettingAdmin.value.id}/password`, {
password: newPassword.value,
});
message.success('کلمه عبور با موفقیت تغییر یافت.');
isPasswordModalOpen.value = false;
} finally {
isSubmittingPassword.value = false;
}
}
function confirmToggleStatus(record: any) {
const isCurrentlyActive = record.status === 'ACTIVE';
const newStatus = isCurrentlyActive ? 'DISABLED' : 'ACTIVE';
Modal.confirm({
title: isCurrentlyActive ? 'غیرفعال‌سازی مدیر' : 'فعال‌سازی مجدد مدیر',
content: `آیا از تغییر وضعیت حساب ${record.name} اطمینان دارید؟`,
okText: isCurrentlyActive ? 'غیرفعال کن' : 'فعال کن',
okType: isCurrentlyActive ? 'danger' : 'primary',
cancelText: 'انصراف',
onOk: async () => {
try {
await apiClient.patch(`/admin/managers/${record.id}/status`, { status: newStatus });
message.success('وضعیت به‌روزرسانی شد.');
loadAdmins();
} catch {}
},
});
}
function confirmDelete(record: any) {
Modal.confirm({
title: 'حذف مدیر',
content: `آیا از حذف دائم ${record.name} اطمینان دارید؟`,
okText: 'حذف',
okType: 'danger',
cancelText: 'انصراف',
onOk: async () => {
try {
await apiClient.delete(`/admin/managers/${record.id}`);
message.success('مدیر حذف شد.');
loadAdmins();
} catch {}
},
});
}
onMounted(() => {
loadRoles();
loadAdmins();
});
</script>
<template>
<div class="m-admin-page">
<!-- Top Bar with Search & Action -->
<div class="m-search-box">
<a-input-search
v-model:value="filters.search"
placeholder="جستجوی مدیر..."
allowClear
@search="() => { filters.page = 1; loadAdmins(); }"
>
<template #prefix><SearchOutlined style="color: #94a3b8" /></template>
</a-input-search>
</div>
<div class="m-add-bar" v-if="authStore.can('admins:manage')">
<a-button type="primary" block size="large" @click="openCreate">
<template #icon><PlusOutlined /></template>
افزودن مدیر جدید
</a-button>
</div>
<!-- Cards List -->
<a-spin :spinning="loading">
<div v-if="admins.length === 0 && !loading" class="m-empty">
مدیری یافت نشد.
</div>
<div class="m-cards-list">
<div v-for="admin in admins" :key="admin.id" class="m-admin-card">
<div class="m-card-header">
<div class="m-user-info">
<a-avatar :style="{ backgroundColor: admin.isSuperAdmin ? '#7c3aed' : '#2563eb' }">
{{ admin.name?.charAt(0) || 'U' }}
</a-avatar>
<div class="m-name-box">
<span class="m-name">
{{ admin.name }}
<CrownOutlined v-if="admin.isSuperAdmin" style="color: #f59e0b; margin-right: 4px" />
</span>
<span class="m-email">{{ admin.email }}</span>
</div>
</div>
<a-tag :color="admin.status === 'DISABLED' ? 'red' : 'green'" class="m-status-tag">
{{ admin.status === 'DISABLED' ? 'غیرفعال' : 'فعال' }}
</a-tag>
</div>
<div class="m-card-body">
<div class="m-info-row">
<span class="m-info-label">شماره موبایل:</span>
<span class="m-info-val">{{ toFarsiDigits(admin.mobile) }}</span>
</div>
<div class="m-info-row">
<span class="m-info-label">نقش‌ها:</span>
<div class="m-tags-wrap">
<a-tag v-if="admin.isSuperAdmin" color="purple" size="small">مدیر ارشد</a-tag>
<template v-if="admin.roles && admin.roles.length > 0">
<a-tag v-for="ra in admin.roles" :key="ra.role?.id" color="blue" size="small">
{{ ra.role?.name || ra.role?.code }}
</a-tag>
</template>
<span v-else-if="!admin.isSuperAdmin" style="color: #94a3b8; font-size: 11px">بدون نقش</span>
</div>
</div>
<div class="m-info-row">
<span class="m-info-label">فضای کاری:</span>
<div class="m-tags-wrap">
<template v-if="admin.workspaceAssignments && admin.workspaceAssignments.length > 0">
<a-tag v-for="(wa, i) in admin.workspaceAssignments" :key="i" color="cyan" size="small">
{{ wa.workspace?.name || 'اصلی' }}
</a-tag>
</template>
<span v-else style="color: #94a3b8; font-size: 11px">سراسری</span>
</div>
</div>
</div>
<!-- Actions -->
<div class="m-card-actions" v-if="authStore.can('admins:manage')">
<a-button type="default" size="small" @click="openEdit(admin)">
<template #icon><EditOutlined /></template>
ویرایش
</a-button>
<a-button type="default" size="small" @click="openResetPassword(admin)">
<template #icon><KeyOutlined /></template>
رمز
</a-button>
<template v-if="admin.id !== authStore.admin?.id">
<a-button
:danger="admin.status === 'ACTIVE'"
size="small"
@click="confirmToggleStatus(admin)"
>
<template #icon>
<StopOutlined v-if="admin.status === 'ACTIVE'" />
<CheckCircleOutlined v-else />
</template>
{{ admin.status === 'ACTIVE' ? 'غیرفعال' : 'فعال' }}
</a-button>
<a-button type="text" danger size="small" @click="confirmDelete(admin)">
<DeleteOutlined />
</a-button>
</template>
</div>
</div>
</div>
<!-- Pagination -->
<div class="m-pagination" v-if="total > filters.pageSize">
<a-pagination
v-model:current="filters.page"
:pageSize="filters.pageSize"
:total="total"
size="small"
simple
@change="loadAdmins"
/>
</div>
</a-spin>
<a-modal
v-model:open="isPasswordModalOpen"
title="تغییر کلمه عبور مدیر"
okText="تغییر"
cancelText="انصراف"
:confirmLoading="isSubmittingPassword"
@ok="handleResetPassword"
>
<a-input-password
v-model:value="newPassword"
placeholder="کلمه عبور جدید (حداقل ۶ کاراکتر)"
size="large"
dir="ltr"
style="margin-top: 12px"
/>
</a-modal>
</div>
</template>
<style scoped>
.m-admin-page {
padding: 12px;
}
.m-search-box {
margin-bottom: 10px;
}
.m-add-bar {
margin-bottom: 12px;
}
.m-cards-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.m-admin-card {
background: #fff;
border-radius: 10px;
border: 1px solid #e2e8f0;
padding: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.m-card-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f1f5f9;
padding-bottom: 8px;
margin-bottom: 8px;
}
.m-user-info {
display: flex;
align-items: center;
gap: 8px;
}
.m-name-box {
display: flex;
flex-direction: column;
}
.m-name {
font-weight: 600;
font-size: 13px;
color: #1e293b;
}
.m-email {
font-size: 11px;
color: #94a3b8;
direction: ltr;
}
.m-status-tag {
border-radius: 10px;
font-size: 11px;
}
.m-card-body {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 12px;
}
.m-info-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.m-info-label {
color: #64748b;
}
.m-info-val {
font-weight: 500;
color: #334155;
direction: ltr;
}
.m-tags-wrap {
display: flex;
flex-wrap: wrap;
gap: 2px;
}
.m-card-actions {
display: flex;
align-items: center;
gap: 6px;
margin-top: 10px;
padding-top: 8px;
border-top: 1px dashed #e2e8f0;
}
.m-pagination {
display: flex;
justify-content: center;
margin: 16px 0;
}
.m-empty {
text-align: center;
padding: 40px;
color: #94a3b8;
}
</style>
@@ -0,0 +1,36 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
function goHome() {
router.push('/m/dashboard');
}
</script>
<template>
<div class="m-forbidden-page">
<a-result
status="403"
title="۴۰۳ — دسترسی غیرمجاز"
sub-title="شما مجوز دسترسی به این بخش را ندارید."
>
<template #extra>
<a-button type="primary" block @click="goHome">
بازگشت به داشبورد
</a-button>
</template>
</a-result>
</div>
</template>
<style scoped>
.m-forbidden-page {
padding: 32px 16px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
}
</style>
@@ -0,0 +1,304 @@
<script setup lang="ts">
import {
PlusOutlined,
SafetyCertificateOutlined,
SaveOutlined,
TeamOutlined,
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { toFarsiDigits } from '@lucky-wheel/shared-types';
import { useAuthStore } from '../../stores/auth.store';
import { apiClient } from '../../services/api/client';
const authStore = useAuthStore();
const loading = ref(false);
const saving = ref(false);
const roles = ref<any[]>([]);
const permissions = ref<any[]>([]);
const selectedRoleId = ref<string | null>(null);
const activeRolePermIds = ref<string[]>([]);
const isRoleDirty = ref(false);
const isCreateModalOpen = ref(false);
const isSubmittingNewRole = ref(false);
const newRoleForm = reactive({
code: '',
name: '',
description: '',
permissionIds: [] as string[],
});
const selectedRole = computed(() => {
return roles.value.find((r) => r.id === selectedRoleId.value) || null;
});
const moduleLabels: Record<string, string> = {
campaigns: 'کمپین‌ها',
prizes: 'جوایز',
users: 'کاربران',
chances: 'شانس‌ها',
spins: 'اسپین‌ها',
winners: 'برندگان',
sms: 'پیامک',
audit: 'بازرسی',
reports: 'گزارش‌ها',
admins: 'مدیران',
roles: 'نقش‌ها',
workspaces: 'فضاهای کاری',
};
const groupedPermissions = computed(() => {
const groups: Record<string, any[]> = {};
permissions.value.forEach((p) => {
const mod = p.module || 'other';
if (!groups[mod]) groups[mod] = [];
groups[mod].push(p);
});
return groups;
});
async function loadData() {
loading.value = true;
try {
const [rolesRes, permsRes] = await Promise.all([
apiClient.get('/admin/roles'),
apiClient.get('/admin/permissions'),
]);
roles.value = Array.isArray(rolesRes.data) ? rolesRes.data : rolesRes.data?.data || [];
permissions.value = Array.isArray(permsRes.data) ? permsRes.data : permsRes.data?.data || [];
if (!selectedRoleId.value && roles.value.length > 0) {
selectRole(roles.value[0].id);
} else if (selectedRoleId.value) {
selectRole(selectedRoleId.value);
}
} finally {
loading.value = false;
}
}
function selectRole(roleId: string) {
selectedRoleId.value = roleId;
const role = roles.value.find((r) => r.id === roleId);
if (role) {
activeRolePermIds.value = role.permissions ? role.permissions.map((rp: any) => rp.permissionId || rp.permission?.id) : [];
} else {
activeRolePermIds.value = [];
}
isRoleDirty.value = false;
}
function togglePermission(permId: string) {
if (selectedRole.value?.code === 'SUPER_ADMIN') return;
const idx = activeRolePermIds.value.indexOf(permId);
if (idx > -1) {
activeRolePermIds.value.splice(idx, 1);
} else {
activeRolePermIds.value.push(permId);
}
isRoleDirty.value = true;
}
async function saveRolePermissions() {
if (!selectedRoleId.value) return;
saving.value = true;
try {
await apiClient.patch(`/admin/roles/${selectedRoleId.value}`, {
permissionIds: activeRolePermIds.value,
});
message.success('مجوزهای نقش با موفقیت به‌روزرسانی شد.');
isRoleDirty.value = false;
loadData();
} finally {
saving.value = false;
}
}
async function handleCreateRole() {
if (!newRoleForm.name.trim() || !newRoleForm.code.trim()) {
message.error('نام و کد انگلیسی نقش الزامی است.');
return;
}
isSubmittingNewRole.value = true;
try {
const res = await apiClient.post('/admin/roles', {
name: newRoleForm.name.trim(),
code: newRoleForm.code.trim().toUpperCase(),
description: newRoleForm.description?.trim() || undefined,
permissionIds: newRoleForm.permissionIds,
});
message.success('نقش ایجاد شد.');
isCreateModalOpen.value = false;
await loadData();
if (res.data?.id) selectRole(res.data.id);
} finally {
isSubmittingNewRole.value = false;
}
}
onMounted(() => {
loadData();
});
</script>
<template>
<div class="m-roles-page">
<!-- Select role dropdown -->
<div class="m-role-selector-bar">
<div style="flex: 1">
<a-select
v-model:value="selectedRoleId"
style="width: 100%"
size="large"
@change="selectRole(selectedRoleId!)"
>
<a-select-option v-for="r in roles" :key="r.id" :value="r.id">
{{ r.name }} ({{ toFarsiDigits(r._count?.admins ?? 0) }} نفر)
</a-select-option>
</a-select>
</div>
<a-button
v-if="authStore.can('roles:manage')"
type="primary"
size="large"
@click="isCreateModalOpen = true"
>
<template #icon><PlusOutlined /></template>
</a-button>
</div>
<!-- Save button if dirty -->
<div class="m-save-bar" v-if="isRoleDirty && !selectedRole?.isSystem">
<a-button type="primary" block size="large" :loading="saving" @click="saveRolePermissions">
<template #icon><SaveOutlined /></template>
ذخیره تغییرات مجوزها
</a-button>
</div>
<!-- Super admin note -->
<div v-if="selectedRole?.code === 'SUPER_ADMIN'" class="m-notice">
<SafetyCertificateOutlined style="color: #7c3aed; font-size: 18px" />
<span>دسترسی‌های مدیر ارشد دائمی و کامل است.</span>
</div>
<!-- Permissions accordion/list -->
<a-spin :spinning="loading">
<div class="m-modules-list">
<div v-for="(perms, mod) in groupedPermissions" :key="mod" class="m-mod-card">
<div class="m-mod-title">
<strong>{{ moduleLabels[mod] || mod }}</strong>
<span class="m-mod-count">
{{ toFarsiDigits(perms.filter((p) => activeRolePermIds.includes(p.id)).length) }}/{{ toFarsiDigits(perms.length) }}
</span>
</div>
<div class="m-mod-perms">
<div
v-for="p in perms"
:key="p.id"
class="m-perm-row"
:class="{ 'is-selected': activeRolePermIds.includes(p.id) }"
@click="togglePermission(p.id)"
>
<a-checkbox
:checked="activeRolePermIds.includes(p.id)"
:disabled="selectedRole?.code === 'SUPER_ADMIN' || !authStore.can('roles:manage')"
/>
<span class="m-p-name">{{ p.name }}</span>
</div>
</div>
</div>
</div>
</a-spin>
<a-modal
v-model:open="isCreateModalOpen"
title="تعریف نقش جدید"
okText="ایجاد"
cancelText="انصراف"
:confirmLoading="isSubmittingNewRole"
@ok="handleCreateRole"
>
<a-form layout="vertical" style="margin-top: 12px">
<a-form-item label="نام نقش" required>
<a-input v-model:value="newRoleForm.name" placeholder="مثال: سرپرست بازاریابی" />
</a-form-item>
<a-form-item label="کد انگلیسی" required>
<a-input v-model:value="newRoleForm.code" placeholder="MARKETING_LEAD" dir="ltr" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style scoped>
.m-roles-page {
padding: 12px;
}
.m-role-selector-bar {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.m-save-bar {
margin-bottom: 12px;
}
.m-notice {
background: #f5f3ff;
border: 1px solid #ddd6fe;
border-radius: 8px;
padding: 10px 12px;
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #6b7280;
margin-bottom: 12px;
}
.m-modules-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.m-mod-card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
}
.m-mod-title {
background: #f8fafc;
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e2e8f0;
font-size: 13px;
}
.m-mod-count {
font-size: 11px;
color: #64748b;
}
.m-mod-perms {
padding: 6px 12px;
}
.m-perm-row {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid #f8fafc;
}
.m-perm-row:last-child {
border-bottom: none;
}
.m-p-name {
font-size: 13px;
color: #1e293b;
}
</style>
@@ -0,0 +1,205 @@
<script setup lang="ts">
import {
ApartmentOutlined,
EditOutlined,
PlusOutlined,
ProjectOutlined,
TeamOutlined,
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { onMounted, reactive, ref } from 'vue';
import { toFarsiDigits } from '@lucky-wheel/shared-types';
import { useAuthStore } from '../../stores/auth.store';
import { apiClient } from '../../services/api/client';
const authStore = useAuthStore();
const loading = ref(false);
const workspaces = ref<any[]>([]);
const isModalOpen = ref(false);
const isEditing = ref(false);
const editingWorkspaceId = ref<string | null>(null);
const isSubmitting = ref(false);
const formState = reactive({
name: '',
slug: '',
description: '',
});
async function loadWorkspaces() {
loading.value = true;
try {
const res = await apiClient.get('/admin/workspaces');
workspaces.value = Array.isArray(res.data) ? res.data : res.data?.data || [];
} finally {
loading.value = false;
}
}
function openCreate() {
isEditing.value = false;
editingWorkspaceId.value = null;
formState.name = '';
formState.slug = '';
formState.description = '';
isModalOpen.value = true;
}
function openEdit(ws: any) {
isEditing.value = true;
editingWorkspaceId.value = ws.id;
formState.name = ws.name;
formState.slug = ws.slug;
formState.description = ws.description || '';
isModalOpen.value = true;
}
async function onSubmit() {
if (!formState.name.trim()) {
message.error('نام فضای کاری الزامی است.');
return;
}
isSubmitting.value = true;
try {
if (isEditing.value && editingWorkspaceId.value) {
await apiClient.patch(`/admin/workspaces/${editingWorkspaceId.value}`, {
name: formState.name.trim(),
description: formState.description?.trim() || undefined,
});
message.success('به‌روزرسانی شد.');
} else {
await apiClient.post('/admin/workspaces', {
name: formState.name.trim(),
slug: formState.slug.trim().toLowerCase(),
description: formState.description?.trim() || undefined,
});
message.success('فضای کاری ایجاد شد.');
}
isModalOpen.value = false;
loadWorkspaces();
} finally {
isSubmitting.value = false;
}
}
onMounted(() => {
loadWorkspaces();
});
</script>
<template>
<div class="m-workspaces-page">
<div class="m-add-bar" v-if="authStore.can('workspaces:manage')">
<a-button type="primary" block size="large" @click="openCreate">
<template #icon><PlusOutlined /></template>
تعریف فضای کاری جدید
</a-button>
</div>
<a-spin :spinning="loading">
<div class="m-ws-list">
<div v-for="ws in workspaces" :key="ws.id" class="m-ws-card">
<div class="m-ws-header">
<div style="display: flex; align-items: center; gap: 8px">
<ApartmentOutlined style="color: #2563eb; font-size: 16px" />
<strong>{{ ws.name }}</strong>
</div>
<a-button
v-if="authStore.can('workspaces:manage')"
type="text"
size="small"
@click="openEdit(ws)"
>
<EditOutlined style="color: #2563eb" />
</a-button>
</div>
<div class="m-ws-slug">
{{ ws.slug }}
</div>
<div class="m-ws-desc" v-if="ws.description">
{{ ws.description }}
</div>
<div class="m-ws-footer">
<a-tag color="purple" size="small">
<ProjectOutlined style="margin-left: 4px" />
{{ toFarsiDigits(ws._count?.campaigns ?? 0) }} کمپین
</a-tag>
<a-tag color="cyan" size="small">
<TeamOutlined style="margin-left: 4px" />
{{ toFarsiDigits(ws._count?.adminAssignments ?? 0) }} مدیر
</a-tag>
</div>
</div>
</div>
</a-spin>
<a-modal
v-model:open="isModalOpen"
:title="isEditing ? 'ویرایش فضای کاری' : 'فضای کاری جدید'"
okText="ذخیره"
cancelText="انصراف"
:confirmLoading="isSubmitting"
@ok="onSubmit"
>
<a-form layout="vertical" style="margin-top: 12px">
<a-form-item label="نام فضای کاری" required>
<a-input v-model:value="formState.name" placeholder="مثال: شعبه مشهد" />
</a-form-item>
<a-form-item label="شناسه انگلیسی (Slug)" :required="!isEditing">
<a-input v-model:value="formState.slug" placeholder="mashhad-branch" dir="ltr" :disabled="isEditing" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style scoped>
.m-workspaces-page {
padding: 12px;
}
.m-add-bar {
margin-bottom: 12px;
}
.m-ws-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.m-ws-card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
}
.m-ws-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.m-ws-slug {
font-size: 11px;
color: #94a3b8;
direction: ltr;
text-align: right;
margin: 4px 0;
}
.m-ws-desc {
font-size: 12px;
color: #64748b;
margin-bottom: 8px;
}
.m-ws-footer {
display: flex;
gap: 6px;
margin-top: 8px;
padding-top: 6px;
border-top: 1px dashed #f1f5f9;
}
</style>
@@ -0,0 +1,558 @@
<script setup lang="ts">
import {
CheckOutlined,
DeleteOutlined,
EditOutlined,
LockOutlined,
PlusOutlined,
SafetyCertificateOutlined,
SaveOutlined,
TeamOutlined,
} from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { toFarsiDigits } from '@lucky-wheel/shared-types';
import { useAuthStore } from '../../stores/auth.store';
import { apiClient } from '../../services/api/client';
const authStore = useAuthStore();
const loading = ref(false);
const saving = ref(false);
const roles = ref<any[]>([]);
const permissions = ref<any[]>([]);
const selectedRoleId = ref<string | null>(null);
// Form state for creating a new role
const isCreateModalOpen = ref(false);
const isSubmittingNewRole = ref(false);
const newRoleForm = reactive({
code: '',
name: '',
description: '',
permissionIds: [] as string[],
});
// Permission IDs assigned to the currently selected role in edit matrix
const activeRolePermIds = ref<string[]>([]);
const isRoleDirty = ref(false);
const selectedRole = computed(() => {
return roles.value.find((r) => r.id === selectedRoleId.value) || null;
});
// Group permissions by module
const moduleLabels: Record<string, string> = {
campaigns: 'مدیریت کمپین‌ها',
prizes: 'مدیریت جوایز',
users: 'کاربران سامانه',
chances: 'تنظیم دستی شانس‌ها',
spins: 'تاریخچه اسپین‌ها',
winners: 'مدیریت برندگان و کدهای هدیه',
sms: 'لاگ و ارسال پیامک',
audit: 'لاگ بازرسی (Audit)',
reports: 'گزارش‌ها و KPI',
admins: 'مدیریت مدیران و همکاران',
roles: 'مدیریت نقش‌ها و دسترسی‌ها',
workspaces: 'فضاهای کاری سازمانی',
};
const groupedPermissions = computed(() => {
const groups: Record<string, any[]> = {};
permissions.value.forEach((p) => {
const mod = p.module || 'other';
if (!groups[mod]) groups[mod] = [];
groups[mod].push(p);
});
return groups;
});
async function loadData() {
loading.value = true;
try {
const [rolesRes, permsRes] = await Promise.all([
apiClient.get('/admin/roles'),
apiClient.get('/admin/permissions'),
]);
roles.value = Array.isArray(rolesRes.data) ? rolesRes.data : rolesRes.data?.data || [];
permissions.value = Array.isArray(permsRes.data) ? permsRes.data : permsRes.data?.data || [];
if (!selectedRoleId.value && roles.value.length > 0) {
selectRole(roles.value[0].id);
} else if (selectedRoleId.value) {
selectRole(selectedRoleId.value);
}
} finally {
loading.value = false;
}
}
function selectRole(roleId: string) {
selectedRoleId.value = roleId;
const role = roles.value.find((r) => r.id === roleId);
if (role) {
activeRolePermIds.value = role.permissions ? role.permissions.map((rp: any) => rp.permissionId || rp.permission?.id) : [];
} else {
activeRolePermIds.value = [];
}
isRoleDirty.value = false;
}
function togglePermission(permId: string) {
if (selectedRole.value?.isSystem && selectedRole.value?.code === 'SUPER_ADMIN') {
message.warning('مجوزهای نقش مدیر ارشد به صورت ذاتی کامل و غیرقابل تغییر است.');
return;
}
const idx = activeRolePermIds.value.indexOf(permId);
if (idx > -1) {
activeRolePermIds.value.splice(idx, 1);
} else {
activeRolePermIds.value.push(permId);
}
isRoleDirty.value = true;
}
function toggleModuleAll(mod: string) {
if (selectedRole.value?.isSystem && selectedRole.value?.code === 'SUPER_ADMIN') return;
const perms = groupedPermissions.value[mod] || [];
const ids = perms.map((p) => p.id);
const allSelected = ids.every((id) => activeRolePermIds.value.includes(id));
if (allSelected) {
activeRolePermIds.value = activeRolePermIds.value.filter((id) => !ids.includes(id));
} else {
ids.forEach((id) => {
if (!activeRolePermIds.value.includes(id)) {
activeRolePermIds.value.push(id);
}
});
}
isRoleDirty.value = true;
}
async function saveRolePermissions() {
if (!selectedRoleId.value) return;
saving.value = true;
try {
await apiClient.patch(`/admin/roles/${selectedRoleId.value}`, {
permissionIds: activeRolePermIds.value,
});
message.success('مجوزهای نقش با موفقیت به‌روزرسانی شد.');
isRoleDirty.value = false;
loadData();
} finally {
saving.value = false;
}
}
function openCreateModal() {
newRoleForm.code = '';
newRoleForm.name = '';
newRoleForm.description = '';
newRoleForm.permissionIds = [];
isCreateModalOpen.value = true;
}
async function handleCreateRole() {
if (!newRoleForm.name.trim()) {
message.error('نام فارسی نقش الزامی است.');
return;
}
if (!newRoleForm.code.trim()) {
message.error('کد انگلیسی نقش الزامی است.');
return;
}
isSubmittingNewRole.value = true;
try {
const res = await apiClient.post('/admin/roles', {
name: newRoleForm.name.trim(),
code: newRoleForm.code.trim().toUpperCase(),
description: newRoleForm.description?.trim() || undefined,
permissionIds: newRoleForm.permissionIds,
});
message.success('نقش جدید با موفقیت ایجاد شد.');
isCreateModalOpen.value = false;
await loadData();
if (res.data?.id) selectRole(res.data.id);
} finally {
isSubmittingNewRole.value = false;
}
}
function confirmDeleteRole(role: any) {
Modal.confirm({
title: 'حذف نقش سفارشی',
content: `آیا از حذف نقش «${role.name}» اطمینان دارید؟`,
okText: 'حذف نقش',
okType: 'danger',
cancelText: 'انصراف',
onOk: async () => {
try {
await apiClient.delete(`/admin/roles/${role.id}`);
message.success('نقش با موفقیت حذف شد.');
selectedRoleId.value = null;
loadData();
} catch {
// Handled
}
},
});
}
onMounted(() => {
loadData();
});
</script>
<template>
<div class="roles-matrix-page">
<div class="page-header-title">
<div>
<h1 class="title">مدیریت نقش‌ها و ماتریس مجوزها</h1>
<p class="subtitle">تعریف نقش‌های سازمانی و انتساب دانه‌بندی‌شده مجوزهای دسترسی به ماژول‌های سامانه</p>
</div>
<div class="page-actions" v-if="authStore.can('roles:manage')">
<a-button type="primary" size="large" @click="openCreateModal">
<template #icon><PlusOutlined /></template>
تعریف نقش جدید
</a-button>
</div>
</div>
<a-spin :spinning="loading">
<a-row :gutter="20">
<!-- سمت راست: لیست نقش‌ها -->
<a-col :xs="24" :md="8" :lg="7">
<a-card :bordered="false" class="roles-sidebar-card" title="نقش‌های سامانه">
<div class="roles-list">
<div
v-for="r in roles"
:key="r.id"
class="role-item"
:class="{ 'is-active': r.id === selectedRoleId }"
@click="selectRole(r.id)"
>
<div class="role-item-header">
<div class="role-title">
<SafetyCertificateOutlined
:style="{ color: r.isSystem ? '#7c3aed' : '#2563eb', marginLeft: '6px' }"
/>
<strong>{{ r.name }}</strong>
</div>
<a-tag v-if="r.isSystem" color="purple" size="small">سیستمی</a-tag>
</div>
<div class="role-code">{{ r.code }}</div>
<div class="role-desc">{{ r.description || 'بدون توضیح' }}</div>
<div class="role-footer">
<span class="user-count">
<TeamOutlined style="margin-left: 4px" />
{{ toFarsiDigits(r._count?.admins ?? 0) }} مدیر منتسب
</span>
<a-button
v-if="!r.isSystem && authStore.can('roles:manage')"
type="text"
danger
size="small"
@click.stop="confirmDeleteRole(r)"
>
<DeleteOutlined />
</a-button>
</div>
</div>
</div>
</a-card>
</a-col>
<!-- سمت چپ: ماتریس مجوزهای نقش انتخاب‌شده -->
<a-col :xs="24" :md="16" :lg="17">
<a-card :bordered="false" class="matrix-card">
<template #title>
<div class="matrix-header">
<div>
<h3 style="margin: 0; font-size: 16px">
ماتریس دسترسی‌های:
<span style="color: #2563eb">{{ selectedRole?.name || 'انتخاب کنید' }}</span>
</h3>
<span style="font-size: 12px; color: #64748b">
{{ selectedRole?.code }}
{{ selectedRole?.isSystem ? '• نقش سیستمی پایه' : '• نقش سفارشی' }}
</span>
</div>
<a-button
v-if="authStore.can('roles:manage') && !selectedRole?.isSystem"
type="primary"
:loading="saving"
:disabled="!isRoleDirty"
@click="saveRolePermissions"
>
<template #icon><SaveOutlined /></template>
ذخیره تغییرات مجوزها
</a-button>
</div>
</template>
<div v-if="selectedRole?.code === 'SUPER_ADMIN'" class="super-admin-notice">
<SafetyCertificateOutlined style="font-size: 20px; color: #7c3aed" />
<div>
<strong>نقش مدیر ارشد سامانه (Super Admin)</strong>
<p style="margin: 4px 0 0; color: #6b7280; font-size: 13px">
تمامی مجوزهای سامانه به صورت خودکار و دائمی برای مدیر ارشد فعال است و نیازی به تغییر دستی ندارد.
</p>
</div>
</div>
<div class="modules-grid">
<div
v-for="(perms, modKey) in groupedPermissions"
:key="modKey"
class="module-perm-block"
>
<div class="module-header">
<div class="module-title">
<h4>{{ moduleLabels[modKey] || modKey }}</h4>
<span class="count-label">
({{ toFarsiDigits(perms.filter((p) => activeRolePermIds.includes(p.id)).length) }} از {{ toFarsiDigits(perms.length) }})
</span>
</div>
<a-button
type="link"
size="small"
:disabled="selectedRole?.code === 'SUPER_ADMIN' || !authStore.can('roles:manage')"
@click="toggleModuleAll(modKey)"
>
تغییر انتخاب همه
</a-button>
</div>
<div class="perms-list">
<div
v-for="p in perms"
:key="p.id"
class="perm-checkbox-item"
:class="{ 'is-checked': activeRolePermIds.includes(p.id) }"
@click="togglePermission(p.id)"
>
<a-checkbox
:checked="activeRolePermIds.includes(p.id)"
:disabled="selectedRole?.code === 'SUPER_ADMIN' || !authStore.can('roles:manage')"
>
<strong class="perm-name">{{ p.name }}</strong>
</a-checkbox>
<div class="perm-code">{{ p.code }}</div>
<div class="perm-desc" v-if="p.description">{{ p.description }}</div>
</div>
</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
</a-spin>
<!-- Create Role Modal -->
<a-modal
v-model:open="isCreateModalOpen"
title="تعریف نقش سازمانی جدید"
okText="ایجاد نقش"
cancelText="انصراف"
:confirmLoading="isSubmittingNewRole"
@ok="handleCreateRole"
>
<a-form layout="vertical" style="margin-top: 16px">
<a-form-item label="نام فارسی نقش" required>
<a-input
v-model:value="newRoleForm.name"
placeholder="مثال: سرپرست بازاریابی"
size="large"
/>
</a-form-item>
<a-form-item label="کد انگلیسی نقش (فقط حروف بزرگ و خط زیر)" required>
<a-input
v-model:value="newRoleForm.code"
placeholder="MARKETING_SUPERVISOR"
dir="ltr"
size="large"
/>
</a-form-item>
<a-form-item label="توضیحات و شرح اختیارات">
<a-textarea
v-model:value="newRoleForm.description"
placeholder="شرح وظایف و اختیارات این نقش..."
:rows="3"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style scoped>
.roles-matrix-page {
padding-bottom: 24px;
}
.page-header-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title {
font-size: 22px;
font-weight: 700;
color: #1e293b;
margin: 0;
}
.subtitle {
font-size: 13px;
color: #64748b;
margin-top: 4px;
margin-bottom: 0;
}
.roles-sidebar-card,
.matrix-card {
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.roles-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.role-item {
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
background: #fff;
}
.role-item:hover {
border-color: #93c5fd;
background: #f8fafc;
}
.role-item.is-active {
border-color: #2563eb;
background: #eff6ff;
}
.role-item-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.role-code {
font-size: 11px;
color: #94a3b8;
direction: ltr;
text-align: right;
margin: 2px 0 6px;
}
.role-desc {
font-size: 12px;
color: #64748b;
line-height: 1.5;
}
.role-footer {
margin-top: 10px;
padding-top: 8px;
border-top: 1px dashed #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #64748b;
}
.matrix-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.super-admin-notice {
display: flex;
gap: 12px;
align-items: center;
background: #f5f3ff;
border: 1px solid #ddd6fe;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 20px;
}
.modules-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.module-perm-block {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
}
.module-header {
background: #f8fafc;
padding: 10px 14px;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.module-title h4 {
margin: 0;
font-size: 14px;
font-weight: 600;
display: inline-block;
color: #1e293b;
}
.count-label {
font-size: 12px;
color: #64748b;
margin-right: 6px;
}
.perms-list {
padding: 12px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 10px;
}
.perm-checkbox-item {
padding: 8px 10px;
border: 1px solid #f1f5f9;
border-radius: 6px;
transition: all 0.15s ease;
cursor: pointer;
background: #fff;
}
.perm-checkbox-item:hover {
border-color: #cbd5e1;
}
.perm-checkbox-item.is-checked {
background: #eff6ff;
border-color: #bfdbfe;
}
.perm-name {
font-size: 13px;
color: #1e293b;
}
.perm-code {
font-size: 11px;
color: #94a3b8;
direction: ltr;
text-align: right;
margin: 2px 24px 0 0;
}
.perm-desc {
font-size: 11px;
color: #64748b;
margin: 2px 24px 0 0;
}
</style>

Some files were not shown because too many files have changed in this diff Show More