Compare commits
3
Commits
a70a5445eb
...
d406d286ec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d406d286ec | ||
|
|
127a1f91e8 | ||
|
|
c5a841c35f |
File diff suppressed because one or more lines are too long
@@ -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}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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};
|
||||
@@ -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}
|
||||
@@ -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};
|
||||
@@ -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};
|
||||
@@ -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}
|
||||
@@ -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
@@ -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
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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
@@ -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}
|
||||
@@ -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
@@ -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
@@ -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}
|
||||
@@ -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};
|
||||
@@ -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};
|
||||
@@ -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}
|
||||
@@ -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
@@ -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};
|
||||
@@ -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}
|
||||
@@ -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
@@ -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};
|
||||
@@ -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}
|
||||
@@ -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};
|
||||
@@ -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
@@ -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
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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};
|
||||
@@ -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};
|
||||
@@ -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
@@ -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}
|
||||
@@ -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
@@ -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
@@ -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}
|
||||
@@ -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};
|
||||
@@ -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
@@ -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
@@ -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};
|
||||
@@ -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};
|
||||
@@ -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
@@ -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}
|
||||
@@ -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
@@ -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}
|
||||
@@ -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
@@ -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};
|
||||
+515
File diff suppressed because one or more lines are too long
+60
File diff suppressed because one or more lines are too long
+1
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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user