فهرست منبع

Merge branch 'master' of http://git.hghweb.ir/ali.s/palizvani-front-vite into production

hgh 1 سال پیش
والد
کامیت
3a782a1476

BIN
public/assets/images/auth/login-image-right.png


+ 2 - 0
src/api/APIGateway.js

@@ -57,11 +57,13 @@ import ContentTopicCategoryAPI from 'src/api/models/ContentTopicCategory.js'
 import ClassroomRegistrationAPI from 'src/api/models/ClassroomRegistration.js'
 import ProjectAttendanceSheetsAPI from 'src/api/models/ProjectAttendanceSheets.js'
 import SessionAttendanceSheetsAPI from 'src/api/models/SessionAttendanceSheets.js'
+import SubscriptionAPI from 'src/api/models/Subscription'
 
 /* Exporting the APIGateway object. */
 export const APIGateway = {
   seo: new SeoAPI(),
   auth: new AuthAPI(),
+  subscription: new SubscriptionAPI(),
   user: new UserAPI(),
   unit: new UnitAPI(),
   post: new PostAPI(),

+ 44 - 1
src/api/models/Auth.js

@@ -8,6 +8,7 @@ export default class AuthAPI extends APIRepository {
     this.APIAdresses = {
       login: '/uma/users/login',
       sendOtp: '/uma/users/send_otp',
+      loginOtp: '/uma/users/login_otp',
       signUp: '/uma/users/sign_up',
       verify: '/uma/users/verify_input',
       setPass: '/uma/users/set_password',
@@ -15,7 +16,7 @@ export default class AuthAPI extends APIRepository {
       loginAdmin: '/auth/login',
       refreshToken: '/auth/refresh-token',
       changePass: '/auth/change-pass',
-      loginOtp: '/auth/login-otp'
+      loginOtp2: '/auth/login-otp'
     }
     this.CacheList = {}
   }
@@ -44,6 +45,24 @@ export default class AuthAPI extends APIRepository {
     })
   }
 
+  sendOtpByAction (data) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.sendOtp,
+      data: this.getNormalizedSendData({
+        input: '', // String
+        action: 'SIGN_UP' // String
+      }, data),
+      resolveCallback: (response) => {
+        return response.data.message // String (ok)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
   sendOtpSignUp (data) {
     return this.sendRequest({
       apiMethod: 'post',
@@ -80,6 +99,30 @@ export default class AuthAPI extends APIRepository {
     })
   }
 
+  loginOtp (data) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.loginOtp,
+      data: this.getNormalizedSendData({
+        input: '', // String
+        otp: '' // String
+      }, data),
+      resolveCallback: (response) => {
+        const accessToken = response.data.token.access_token
+        const ca = accessToken
+        const base64Url = ca.split('.')[1]
+        const decodedValue = JSON.parse(window.atob(base64Url))
+        const user = new User(decodedValue)
+
+        return { accessToken, user }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
   verify (data) {
     return this.sendRequest({
       apiMethod: 'put',

+ 158 - 0
src/api/models/Subscription.js

@@ -0,0 +1,158 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { Subscription, SubscriptionList } from 'src/models/Subscription.js'
+
+export default class SubscriptionAPI extends APIRepository {
+  constructor() {
+    super('subscription', appApi)
+    this.APIAdresses = {
+      base: '/mma/subscriptions',
+      byId: (id) => '/mma/subscriptions/' + id,
+      myCorrespondingSubscription: '/mma/subscriptions/my_corresponding_subscription',
+      sendMessage: '/mma/subscriptions/send_message',
+      exportReport: '/mma/subscriptions/export_report',
+      deleteAll: '/mma/subscriptions/delete_all'
+    }
+    this.restUrl = (id) => this.url + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new Subscription(response.data) },
+      post: (response) => { return new Subscription(response.data) },
+      put: (response) => { return new Subscription(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new SubscriptionList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  getMyCorrespondingSubscription(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.myCorrespondingSubscription,
+      data: this.getNormalizedSendData({
+        source_type: 'UNIT' //
+      }, data),
+      resolveCallback: (response) => {
+        return new Subscription(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  deleteAllSubscriptions(data) {
+    return this.sendRequest({
+      apiMethod: 'delete',
+      api: this.api,
+      request: this.APIAdresses.deleteAll,
+      data: this.getNormalizedSendData({
+      }, data),
+      resolveCallback: (response) => {
+        return response.data
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  exportSubscriptionsListReport(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.exportReport,
+      data: this.getNormalizedSendData({
+        source_type: 'UNIT' //
+      }, data),
+      responseType: 'blob',
+      resolveCallback: (response) => {
+        return response.data // xlsxData
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  submit (data) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        source_type: 'UNIT'
+      }, data),
+      resolveCallback: (response) => {
+        return new Subscription(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  sendMessage (data) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.sendMessage,
+      data: this.getNormalizedSendData({
+        source_type: 'UNIT'
+      }, data),
+      resolveCallback: (response) => {
+        return new Subscription(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  seen(id) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      data: {
+        state: 'READ'
+      },
+      resolveCallback: (response) => {
+        return new Subscription(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 7 - 2
src/components/Auth/ForgetStep.vue

@@ -230,7 +230,9 @@ export default {
       })
         .then(() => {
           this.loading = false
-          this.redirectTo()
+          if (this.redirect) {
+            this.redirectTo()
+          }
           // this.$store.commit('Auth/updateAccessToken', response.data.token.access_token)
           // this.$store.commit('Auth/setAccessToken', response.data.token.access_token)
           // this.$emit('register')
@@ -283,7 +285,10 @@ export default {
       })
         .then(() => {
           this.loading = false
-          this.redirectTo()
+          this.$emit('login-success', { user: 'ali' })
+          if (this.redirect) {
+            this.redirectTo()
+          }
           // this.setRegisterStep('newPassword')
         })
         .catch(() => {

+ 4 - 1
src/components/Auth/InformationStep.vue

@@ -119,7 +119,10 @@ export default {
         .then(() => {
           this.loadingList = false
           this.$axios.defaults.headers.common.Authorization = 'Bearer ' + this.$store.getters['Auth/accessToken']
-          this.redirectTo()
+          this.$emit('login-success', { user: 'ali' })
+          if (this.redirect) {
+            this.redirectTo()
+          }
         })
         .catch(() => {
           this.loadingList = false

+ 28 - 7
src/components/Auth/LoginComponent.vue

@@ -1,17 +1,27 @@
 <template>
-  <template-component>
+  <template-component :show-header="redirect">
     <template #body>
       <login-step v-if="step === 'login'"
+                  :redirect="redirect"
                   @goToRegisterStep="goToRegisterStep"
-                  @goToForgetStep="goToForgetStep" />
-      <register-step v-if="step === 'register'"
+                  @goToSendOtpStep="goToSendOtpStep"
+                  @goToForgetStep="goToForgetStep"
+                  @login-success="emitLoginSuccess" />
+      <register-step v-if="showRegisterComponent"
+                     :register-step-prop="step"
+                     :redirect="redirect"
                      @goToLoginStep="goToLoginStep"
-                     @register="onRegister" />
+                     @register="onRegister"
+                     @login-success="emitLoginSuccess" />
       <forget-step v-if="step === 'forget'"
+                   :redirect="redirect"
                    @goToLoginStep="goToLoginStep"
-                   @register="onRegister" />
+                   @register="onRegister"
+                   @login-success="emitLoginSuccess" />
       <information-step v-if="step === 'information'"
-                        @goToLoginStep="goToLoginStep" />
+                        :redirect="redirect"
+                        @goToLoginStep="goToLoginStep"
+                        @login-success="emitLoginSuccess" />
     </template>
   </template-component>
 </template>
@@ -37,9 +47,17 @@ export default {
       step: 'login'
     }
   },
+  computed: {
+    showRegisterComponent() {
+      return this.step === 'getUsername' || this.step === 'sendOtp'
+    }
+  },
   methods: {
     goToRegisterStep () {
-      this.step = 'register'
+      this.step = 'getUsername'
+    },
+    goToSendOtpStep () {
+      this.step = 'sendOtp'
     },
     goToForgetStep () {
       this.step = 'forget'
@@ -49,6 +67,9 @@ export default {
     },
     onRegister () {
       // console.log('onRegister')
+    },
+    emitLoginSuccess (payload) {
+      this.$emit('login-success', payload)
     }
   }
 }

+ 43 - 7
src/components/Auth/LoginStep.vue

@@ -47,12 +47,17 @@
            label="ورود به سامانه"
            :loading="loadingList"
            @click="login" />
-    <div>
+    <div class="full-width div-btn-register-login-opt">
       حساب کاربری ندارید؟
-      <span class="btn-register-span"
-            @click="goToRegisterStep">
-        ثبت نام کنید
-      </span>
+      <q-btn class="full-width btn-login-otp"
+             color="secondary"
+             label="ثبت نام کنید"
+             :loading="loadingList"
+             @click="goToRegisterStep" />
+      <div class="btn-otp-div"
+           @click="goToSendOtpStep">
+        ورود با رمز یکبار مصرف
+      </div>
     </div>
   </div>
 </template>
@@ -128,12 +133,18 @@ export default {
         .then(() => {
           this.loadingList = false
           this.$axios.defaults.headers.common.Authorization = 'Bearer ' + this.$store.getters['Auth/accessToken']
-          this.redirectTo()
+          this.$emit('login-success', { user: 'ali' })
+          if (this.redirect) {
+            this.redirectTo()
+          }
         })
         .catch(() => {
           this.loadingList = false
         })
     },
+    goToSendOtpStep () {
+      this.$emit('goToSendOtpStep')
+    },
     goToRegisterStep () {
       this.$emit('goToRegisterStep')
     },
@@ -158,7 +169,11 @@ margin-bottom: 30px;
   margin-bottom: 50px;
 }
 .btn-login {
-  margin-bottom: 40px;
+  margin-bottom: 16px;
+  font-size: 18px;
+}
+
+.btn-login-otp {
   font-size: 18px;
 }
 .password-label {
@@ -175,4 +190,25 @@ margin-bottom: 30px;
   font-size: 16px;
   cursor: pointer;
 }
+
+.btn-register-span {
+  color: #475F4A;
+  font-size: 16px;
+  cursor: pointer;
+}
+
+.btn-otp-div {
+  display: flex;
+  font-size: 18px;
+  cursor: pointer;
+  justify-content: center;
+  align-self: stretch;
+}
+.div-btn-register-login-opt{
+  display: flex;
+  flex-direction: column;
+  padding: 16px 0px;
+  align-items: flex-start;
+  gap: 16px;
+}
 </style>

+ 136 - 79
src/components/Auth/RegisterStep.vue

@@ -3,7 +3,7 @@
     <q-banner class="banner">
       {{ bannerMessage }}
     </q-banner>
-    <template v-if="registerStep === 'getUsername'">
+    <template v-if="showUsernameInput">
       <div class="hello">
         سلام
       </div>
@@ -23,7 +23,7 @@
         </template>
       </q-input>
       <div class="accept-rules-and-conditions-message">
-        با ثبت نام در سامانه
+        با ورود در سامانه
         <span class="accept-rules-and-conditions-message-btn"
               @click="showRulesAndConditions">
           قوانین و شرایط استفاده
@@ -44,7 +44,7 @@
         </span>
       </div>
     </template>
-    <template v-else-if="registerStep === 'verify'">
+    <template v-else-if="showVerifyForm">
       <label for="verifyNumber">
         .لطفا کد تایید ارسال شده به شماره
         {{ username }}
@@ -77,82 +77,93 @@
         </template>
       </div>
 
-      <div class="information-top-message">
-        لطفا اطلاعات کاربری خود را وارد کنید.
+      <div v-if="registerStep === 'verify'"
+           id="information-form">
+        <div class="information-top-message">
+          لطفا اطلاعات کاربری خود را وارد کنید.
+        </div>
+        <label for="name">
+          نام
+        </label>
+        <q-input id="name"
+                 v-model="firstname"
+                 outlined
+                 class="name-input"
+                 placeholder="نام خود را وارد کنید"
+                 @keydown.enter="verify">
+          <template v-slot:prepend>
+            <q-icon name="person" />
+          </template>
+        </q-input>
+        <label for="lastname">
+          نام خانوادگی
+        </label>
+        <q-input id="lastname"
+                 v-model="lastname"
+                 outlined
+                 class="lastname-input"
+                 placeholder="نام خانوادگی خود را وارد کنید"
+                 @keydown.enter="verify">
+          <template v-slot:prepend>
+            <q-icon name="person" />
+          </template>
+        </q-input>
+        <label class="password-label"
+               for="password">
+          گذرواژه
+        </label>
+        <q-input id="password"
+                 v-model="password"
+                 outlined
+                 class="password-input"
+                 placeholder="گذرواژه خود را وارد کنید"
+                 :type="visibilityIcon === 'visibility' ? 'password' : 'text'"
+                 @keydown.enter="verify">
+          <template v-slot:prepend>
+            <q-icon name="lock_open" />
+          </template>
+          <template v-slot:append>
+            <q-icon :name="visibilityIcon"
+                    class="cursor-pointer"
+                    @click="toggleVisibilityIcon" />
+          </template>
+        </q-input>
+        <label class="password-label"
+               for="password">
+          تکرار گذرواژه
+        </label>
+        <q-input id="rePassword"
+                 v-model="rePassword"
+                 outlined
+                 class="password-input"
+                 placeholder="مجدد گذرواژه خود را وارد کنید"
+                 :type="visibilityIcon === 'visibility' ? 'password' : 'text'"
+                 @keydown.enter="verify">
+          <template v-slot:prepend>
+            <q-icon name="lock_open" />
+          </template>
+          <template v-slot:append>
+            <q-icon :name="visibilityIcon"
+                    class="cursor-pointer"
+                    @click="toggleVisibilityIcon" />
+          </template>
+        </q-input>
       </div>
-      <label for="name">
-        نام
-      </label>
-      <q-input id="name"
-               v-model="firstname"
-               outlined
-               class="name-input"
-               placeholder="نام خود را وارد کنید"
-               @keydown.enter="verify">
-        <template v-slot:prepend>
-          <q-icon name="person" />
-        </template>
-      </q-input>
-      <label for="lastname">
-        نام خانوادگی
-      </label>
-      <q-input id="lastname"
-               v-model="lastname"
-               outlined
-               class="lastname-input"
-               placeholder="نام خانوادگی خود را وارد کنید"
-               @keydown.enter="verify">
-        <template v-slot:prepend>
-          <q-icon name="person" />
-        </template>
-      </q-input>
-      <label class="password-label"
-             for="password">
-        گذرواژه
-      </label>
-      <q-input id="password"
-               v-model="password"
-               outlined
-               class="password-input"
-               placeholder="گذرواژه خود را وارد کنید"
-               :type="visibilityIcon === 'visibility' ? 'password' : 'text'"
-               @keydown.enter="verify">
-        <template v-slot:prepend>
-          <q-icon name="lock_open" />
-        </template>
-        <template v-slot:append>
-          <q-icon :name="visibilityIcon"
-                  class="cursor-pointer"
-                  @click="toggleVisibilityIcon" />
-        </template>
-      </q-input>
-      <label class="password-label"
-             for="password">
-        تکرار گذرواژه
-      </label>
-      <q-input id="rePassword"
-               v-model="rePassword"
-               outlined
-               class="password-input"
-               placeholder="مجدد گذرواژه خود را وارد کنید"
-               :type="visibilityIcon === 'visibility' ? 'password' : 'text'"
-               @keydown.enter="verify">
-        <template v-slot:prepend>
-          <q-icon name="lock_open" />
-        </template>
-        <template v-slot:append>
-          <q-icon :name="visibilityIcon"
-                  class="cursor-pointer"
-                  @click="toggleVisibilityIcon" />
-        </template>
-      </q-input>
 
-      <q-btn class="full-width btn-verifyUsername"
+      <q-btn v-if="registerStep==='verify'"
+             class="full-width btn-verifyUsername"
              color="primary"
              label="ثبت نام"
              :disable="loading"
              :loading="loading"
              @click="verify" />
+      <q-btn v-if="registerStep==='loginOtp'"
+             class="full-width btn-verifyUsername"
+             color="primary"
+             label=" ورود"
+             :disable="loading"
+             :loading="loading"
+             @click="loginOtp" />
       <div class="go-to-set-username-message"
            @click="goToGetUsernameStep">
         تغییر شماره تلفن همراه یا استفاده از آدرس ایمیل
@@ -177,6 +188,10 @@ export default {
   components: { Timer },
   mixins: [mixinAuth],
   props: {
+    registerStepProp: {
+      type: String,
+      default: 'getUsername'
+    },
     redirect: {
       type: Boolean,
       default: false
@@ -210,6 +225,12 @@ export default {
     user () {
       return this.$store.getters['Auth/user']
     },
+    showUsernameInput () {
+      return this.registerStep === 'getUsername' || this.registerStep === 'sendOtp'
+    },
+    showVerifyForm () {
+      return this.registerStep === 'verify' || this.registerStep === 'loginOtp'
+    },
     formatedTimer () {
       const second = this.otpTimer % 60
       const minute = Math.floor(this.otpTimer / 60)
@@ -221,7 +242,7 @@ export default {
     // if (this.getToken()) {
     //   this.getUserData(() => { this.redirectTo() })
     // }
-    this.setRegisterStep('getUsername')
+    this.setRegisterStep(this.registerStepProp)
   },
   methods: {
     toggleVisibilityIcon () {
@@ -275,7 +296,11 @@ export default {
       this.setBannerMessageFromStep(step)
     },
     setBannerMessageFromStep (step) {
-      if (step === 'getUsername') {
+      if (step === 'loginOtp') {
+        this.bannerMessage = 'ورود با رمز یکبار مصرف به سامانه'
+      } else if (step === 'sendOtp') {
+        this.bannerMessage = 'ارسال رمز یکبار مصرف'
+      } else if (step === 'getUsername') {
         this.bannerMessage = 'ثبت نام در سامانه'
       } else if (step === 'verify') {
         this.bannerMessage = 'کد تایید'
@@ -320,7 +345,10 @@ export default {
       })
         .then(() => {
           this.loading = false
-          this.$router.push({ name: 'Public.Home' })
+          this.$emit('login-success', { user: 'ali' })
+          if (this.redirect) {
+            this.$router.push({ name: 'Public.Home' })
+          }
           // this.$store.commit('Auth/updateAccessToken', response.data.token.access_token)
           // this.$store.commit('Auth/setAccessToken', response.data.token.access_token)
           // this.$emit('register')
@@ -344,13 +372,15 @@ export default {
         return
       }
       this.loading = true
-      APIGateway.auth.sendOtpSignUp({ input: this.username })
+      const action = this.registerStep === 'sendOtp' ? 'LOGIN_OTP' : 'SIGN_UP'
+      const nextStep = this.registerStep === 'sendOtp' ? 'loginOtp' : 'verify'
+      APIGateway.auth.sendOtpByAction({ input: this.username, action: action })
         .then(() => {
           this.loading = false
           this.restartOtpInterval()
           // this.$axios.defaults.headers.common.Authorization = 'Bearer ' + this.$store.getters['Auth/accessToken']
           // this.redirectTo()
-          this.setRegisterStep('verify')
+          this.setRegisterStep(nextStep)
         })
         .catch(() => {
           this.loading = false
@@ -395,7 +425,34 @@ export default {
       })
         .then(() => {
           this.loading = false
-          this.$router.push({ name: 'Public.Home' })
+          if (this.redirect) {
+            this.$router.push({ name: 'Public.Home' })
+          }
+          // this.setRegisterStep('information')
+        })
+        .catch(() => {
+          this.loading = false
+        })
+    },
+    loginOtp () {
+      if (!this.verifyNumber) {
+        this.$q.notify({
+          type: 'negative',
+          message: 'کد تایید شماره همراه را وارد نکرده اید'
+        })
+        return
+      }
+      this.loading = true
+      this.$store.dispatch('Auth/loginOtp', {
+        input: this.username,
+        otp: this.verifyNumber
+      })
+        .then(() => {
+          this.loading = false
+          this.$emit('login-success', { user: 'ali' })
+          if (this.redirect) {
+            this.$router.push({ name: 'Public.Home' })
+          }
           // this.setRegisterStep('information')
         })
         .catch(() => {
@@ -403,7 +460,7 @@ export default {
         })
     },
     goToGetUsernameStep () {
-      this.setRegisterStep('getUsername')
+      this.setRegisterStep(this.registerStepProp)
     }
   }
 }

+ 19 - 7
src/components/Auth/TemplateComponent.vue

@@ -1,6 +1,7 @@
 <template>
   <q-card class="auth-template-card">
-    <div class="header">
+    <div v-if="showHeader"
+         class="header">
       <div class="title-image">
         <router-link :to="{name: 'Public.Home'}">
           <q-img src="/assets/images/auth/login-image-right.png" />
@@ -18,13 +19,19 @@
 
 <script>
 export default {
-  name: 'TemplateComponent'
+  name: 'TemplateComponent',
+  props: {
+    showHeader: {
+      type: Boolean,
+      default: true
+    }
+  }
 }
 </script>
 
 <style lang="scss" scoped>
 .auth-template-card {
-  min-width: 524px;
+  min-width: 448px;
   background: #F9F9F9 0 0 no-repeat padding-box;
   box-shadow: 0 0 6px #00000029;
   border-radius: 10px;
@@ -38,19 +45,24 @@ export default {
   .header {
     display: flex;
     justify-content: space-between;
-    padding-top: 36px;
+    padding-top: 16px;
+    padding-left: 60px;
     .image {
       width: 59px;
     }
     .title-image {
       width: 217px;
     }
+    @media screen and (max-width: 599px) {
+      $padding: 20px;
+      padding-left: $padding;
+    }
   }
   .body {
     margin-top: 20px;
-    padding-right: 67px;
-    padding-left: 67px;
-    padding-bottom: 61px;
+    padding-right: 36px;
+    padding-left: 36px;
+    padding-bottom: 8px;
     @media screen and (max-width: 599px) {
       $padding: 20px;
       padding-right: $padding;

+ 24 - 9
src/components/Template/Header/MainHeader.vue

@@ -115,13 +115,27 @@
             <q-btn stretch
                    flat
                    label="درباره ما"
-                   :to="{ name: 'Public.AboutUs' }"
-                   class="top-menu-item" />
-            <q-btn stretch
-                   flat
-                   label="تماس با ما"
-                   :to="{ name: 'Public.ContactUs' }"
-                   class="top-menu-item" />
+                   class="top-menu-item">
+              <q-menu>
+                <q-list style="min-width: 100px">
+                  <q-item v-close-popup
+                          :to="{ name: 'Public.AboutUs' }"
+                          clickable>
+                    <q-item-section>معرفی مجموعه</q-item-section>
+                  </q-item>
+                  <q-item v-close-popup
+                          :to="{ name: 'Public.ContactUs' }"
+                          clickable>
+                    <q-item-section>تماس با ما</q-item-section>
+                  </q-item>
+                  <q-item v-close-popup
+                          :to="{ name: 'Public.Landing.Show', params: { slug: 'courses' } }"
+                          clickable>
+                    <q-item-section>معرفی دوره ها</q-item-section>
+                  </q-item>
+                </q-list>
+              </q-menu>
+            </q-btn>
             <q-btn stretch
                    flat
                    label="سوالات متداول"
@@ -232,7 +246,6 @@
 </template>
 
 <script>
-import { User } from 'src/models/User.js'
 import { Basket } from 'src/models/Basket.js'
 import { mapGetters, mapMutations } from 'vuex'
 import { APIGateway } from 'src/api/APIGateway.js'
@@ -241,12 +254,14 @@ export default {
   name: 'templateHeader',
   data () {
     return {
-      user: new User(),
       topMenuItems: [
       ]
     }
   },
   computed: {
+    user () {
+      return this.$store.getters['Auth/user']
+    },
     cartItemCount () {
       return this.basket.items_info.list.length
     },

+ 26 - 11
src/components/Template/SideBar/Main.vue

@@ -134,17 +134,32 @@ export default {
         },
         {
           title: 'درباره ما',
-          route: { name: 'Public.AboutUs' },
-          type: 'itemMenu',
-          permission: 'all',
-          show: true
-        },
-        {
-          title: 'تماس با ما',
-          route: { name: 'Public.ContactUs' },
-          type: 'itemMenu',
-          permission: 'all',
-          show: true
+          type: 'simpleMenu',
+          children: [
+            {
+              title: 'معرفی مجموعه',
+              route: { name: 'Public.AboutUs' },
+              type: 'itemMenu',
+              permission: 'all',
+              show: true
+            },
+            {
+              title: 'تماس با ما',
+              route: { name: 'Public.ContactUs' },
+              type: 'itemMenu',
+              permission: 'all',
+              show: true
+            },
+            {
+              title: 'معرفی دوره ها',
+              route: { name: 'Public.Landing.Show', params: { slug: 'courses' } },
+              type: 'itemMenu',
+              permission: 'all',
+              show: true
+            }
+          ],
+          show: false,
+          mobileMode: true
         },
         {
           title: 'سوالات متداول',

+ 143 - 0
src/components/Widgets/AlertMe/AlertMe.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="notification-toggle-wrapper">
+    <q-toggle v-model="is_enabled"
+              class="large-toggle"
+              checked-icon="notifications"
+              unchecked-icon="notifications_off"
+              color="primary"
+              :label="localOptions.sourceTitle"
+              left-label
+              :disable="loading"
+              @update:model-value="onToggle">
+      <q-tooltip anchor="top middle"
+                 self="bottom middle"
+                 :offset="[10, 10]">
+        {{ is_enabled ? 'غیرفعال کردن اطلاع‌رسانی' : 'فعال‌سازی اطلاع‌رسانی' }}
+      </q-tooltip>
+
+      <template v-slot:loading>
+        <q-spinner color="primary" />
+      </template>
+    </q-toggle>
+
+    <!-- دیالوگ لاگین -->
+    <q-dialog v-model="showLoginDialog">
+      <login-component @login-success="onLoginSuccess" />
+    </q-dialog>
+  </div>
+</template>
+
+<script>
+import LoginComponent from 'src/components/Auth/LoginComponent.vue'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway'
+
+export default {
+  name: 'NotificationToggle',
+  components: { LoginComponent },
+  mixins: [mixinWidget],
+  props: {
+    initialState: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data () {
+    return {
+      // is_enabled: this.initialState,
+      is_enabled: false,
+      loading: false,
+      showLoginDialog: false,
+      pendingToggleValue: null
+    }
+  },
+  computed: {
+    user () {
+      return this.$store.getters['Auth/user']
+    },
+    isUserLogin() {
+      return this.$store.getters['Auth/isUserLogin']
+    },
+    sourceType() {
+      return this.localOptions.sourceType
+    },
+    sourceId() {
+      return this.localOptions.sourceId
+    }
+  },
+  watch: {
+    showLoginDialog (oldVal, newVal) {
+      if (oldVal === true && newVal === false && !this.isUserLogin) {
+        // یعنی کاربر دیالوگ رو بست (لاگین نکرد)
+        this.is_enabled = false
+      }
+    }
+  },
+  mounted () {
+    if (this.isUserLogin) {
+      this.fetchNotificationStatus()
+    }
+  },
+
+  methods: {
+    fetchNotificationStatus () {
+      this.loading = true
+      APIGateway.subscription.getMyCorrespondingSubscription(
+        {
+          source_id: this.sourceId,
+          source_type: this.sourceType
+        }).then((subscription) => {
+        this.is_enabled = subscription?.is_enabled
+      }).catch(() => {
+        this.is_enabled = false
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    onToggle (val) {
+      if (!this.isUserLogin) {
+        this.showLoginDialog = true
+        // برگردوندن toggle به حالت قبلی تا لاگین انجام بشه
+        this.pendingToggleValue = val
+        return
+      }
+      this.toggleNotification(val)
+    },
+
+    toggleNotification (val) {
+      this.loading = true
+      APIGateway.subscription.submit({
+        source_id: this.sourceId,
+        source_type: this.sourceType,
+        is_enabled: val,
+        owner: this.user.id
+      }).then((data) => {
+        this.is_enabled = data.is_enabled
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    onLoginSuccess () {
+      this.showLoginDialog = false
+      if (this.pendingToggleValue) {
+        this.toggleNotification(this.pendingToggleValue)
+        this.pendingToggleValue = null
+      }
+      this.fetchNotificationStatus()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.notification-toggle-wrapper {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 16px;
+  font-size: 18px;
+}
+:deep(.q-toggle__inner) {
+  font-size: 64px;
+}
+</style>

+ 55 - 0
src/components/Widgets/AlertMe/OptionPanel.vue

@@ -0,0 +1,55 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      <div class="q-mt-md">
+        عنوان
+      </div>
+      <div class="row">
+        <div class="col-md-8 col-12">
+          <q-input v-model="localOptions.sourceTitle" />
+        </div>
+      </div>
+      <div class="q-mt-md">
+        نوع منبع
+      </div>
+      <div class="row">
+        <div class="col-md-8 col-12">
+          <q-select v-model="localOptions.sourceType"
+                    :options="sourceTypeOptions" />
+        </div>
+      </div>
+      <div class="q-mt-md">
+        شناسه منبع
+      </div>
+      <div class="row">
+        <div class="col-md-8 col-12">
+          <q-input v-model="localOptions.sourceId" />
+        </div>
+      </div>
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel],
+  data () {
+    return {
+      sourceTypeOptions: ['UNIT'],
+      defaultOptions: {
+        sourceTitle: '',
+        sourceType: null,
+        sourceId: null
+      }
+    }
+  }
+})
+</script>

+ 4 - 4
src/components/Widgets/Payment/AdminPaymentShow/AdminPaymentShow.vue

@@ -112,10 +112,10 @@ export default {
       indexRouteName: 'Admin.Content.List',
       inputs: [
         { type: 'separator', name: 'space', label: 'مشخصات کاربر', className: 'custom-separator', col: 'col-12' },
-        { type: 'input', name: 'creator_info.firstname', responseKey: 'creator_info.firstname', label: 'نام', placeholder: ' ', col: 'col-md-3 col-12' },
-        { type: 'input', name: 'creator_info.lastname', responseKey: 'creator_info.lastname', label: 'نام خانوادگی', placeholder: ' ', col: 'col-md-3 col-12' },
-        { type: 'input', name: 'creator_info.mobile_number', responseKey: 'creator_info.mobile_number', label: 'تلفن همراه', placeholder: ' ', col: 'col-md-3 col-12' },
-        { type: 'input', name: 'creator_info.email', responseKey: 'creator_info.email', label: 'ایمیل', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'wallet_info.owner_info.firstname', responseKey: 'wallet_info.owner_info.firstname', label: 'نام', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'wallet_info.owner_info.lastname', responseKey: 'wallet_info.owner_info.lastname', label: 'نام خانوادگی', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'wallet_info.owner_info.mobile_number', responseKey: 'wallet_info.owner_info.mobile_number', label: 'تلفن همراه', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'wallet_info.owner_info.email', responseKey: 'wallet_info.owner_info.email', label: 'ایمیل', placeholder: ' ', col: 'col-md-3 col-12' },
 
         { type: 'separator', name: 'space', label: 'مشخصات حساب', className: 'custom-separator', col: 'col-12' },
         { type: 'input', name: 'id', responseKey: 'id', label: 'شناسه', placeholder: ' ', col: 'col-md-2 col-12' },

+ 1 - 1
src/css/Theme/sizes.scss

@@ -1,3 +1,3 @@
 $radius-15 : 15px;
 $radius-10 : 10px;
-$mobileFooterHeight: 72px;
+$mobileFooterHeight: 0px;

+ 38 - 0
src/models/Subscription.js

@@ -0,0 +1,38 @@
+import { User } from 'src/models/User.js'
+import { Collection, Model } from 'js-abstract-model'
+
+class Subscription extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'source_type' },
+      { key: 'source_id' },
+      { key: 'creation_time' },
+      { key: 'owner' },
+      { key: 'is_enabled' },
+      {
+        key: 'owner_info',
+        relatedModel: User
+      }
+    ], {
+      source_type: {
+        infoKey: 'source_type',
+        enumListKey: 'sourceTypeEnums',
+        enums: [
+          {
+            label: 'درس',
+            value: 'UNIT'
+          }
+        ]
+      }
+    })
+  }
+}
+
+class SubscriptionList extends Collection {
+  model () {
+    return Subscription
+  }
+}
+
+export { Subscription, SubscriptionList }

+ 0 - 1
src/models/TicketCategory.js

@@ -13,7 +13,6 @@ class TicketCategory extends Model {
         enums: [
           { label: 'معاونت آموزش', value: 'EDUCATIONAL' },
           { label: 'معاونت مالی', value: 'FINANCIAL' },
-          { label: 'معاونت محتوا', value: 'CONTENT' },
           { label: 'معاونت پژوهش', value: 'RESEARCH' },
           { label: 'معاونت سرمایه انسانی', value: 'HR' },
           { label: 'معاونت روابط عمومی', value: 'GENERAL' }

+ 284 - 2
src/pages/Admin/Unit/show.vue

@@ -23,6 +23,8 @@
              label="آزمون" />
       <q-tab name="transcriptSheetHistory"
              label="تاریخچه کارنامه" />
+      <q-tab name="subscriptions"
+             label="اطلاع رسانی" />
     </q-tabs>
     <q-separator />
     <q-tab-panels v-if="mounted"
@@ -187,8 +189,134 @@
           </template>
         </entity-index>
       </q-tab-panel>
+      <q-tab-panel name="subscriptions"
+                   class="q-pa-none">
+        <entity-index v-if="mounted"
+                      ref="subscriptionsEntityIndex"
+                      v-model:value="subscriptionsFilterInputs"
+                      title="لیست دنبال کنندگان"
+                      :api="subscriptionsApi"
+                      :table="subscriptionsTable"
+                      :table-keys="subscriptionsTableKeys"
+                      :show-reload-button="false"
+                      :show-search-button="false"
+                      :show-expand-button="false">
+          <template #toolbar>
+            <div class="flex flex-nowrap justify-end">
+              <q-btn class="q-ml-sm"
+                     color="primary"
+                     outline
+                     :loading="exportReportLoading"
+                     @click="getSubscriptionsListExcel">
+                خروجی اکسل
+              </q-btn>
+              <q-btn class="q-ml-sm"
+                     color="primary"
+                     label="ارسال پیام"
+                     @click="showAlertDialog = true" />
+            </div>
+          </template>
+          <template #before-index-table>
+            <div class="flex flex-nowrap justify-end"
+                 style="margin-bottom: 16px; margin-left: 16px">
+              <q-btn color="red"
+                     outline
+                     :loading="deleteAllLoading"
+                     @click="confirmRemoveAllDialog = true">
+                حذف همه دنبال کنندگان
+              </q-btn>
+            </div>
+          </template>
+          <template #after-index-table>
+            <q-separator class="q-my-lg" />
+          </template>
+          <template #entity-index-table-cell="{inputData, showConfirmRemoveDialog}">
+            <template v-if="inputData.col.name === 'actions'">
+              <div class="q-gutter-md">
+                <div class="action-column-entity-index">
+                  <delete-btn @click="showConfirmRemoveDialog(inputData.props.row, 'id', getRemoveMessage(inputData.props.row))" />
+                </div>
+              </div>
+            </template>
+            <template v-else>
+              {{ inputData.col.value }}
+            </template>
+          </template>
+        </entity-index>
+      </q-tab-panel>
     </q-tab-panels>
-  </q-card>
+  </q-card>    <!-- دیالوگ -->
+  <q-dialog v-model="showAlertDialog">
+    <q-card class="my-dialog-card q-pa-md">
+      <q-card-section class="text-center">
+        <div class="text-h6">ارسال پیام</div>
+      </q-card-section>
+      <q-card-section>
+        <q-input v-model="alertMessageText"
+                 type="textarea"
+                 label="متن پیام خود را وارد کنید"
+                 autogrow
+                 :disable="sendingAlertConfirmation"
+                 dense />
+      </q-card-section>
+
+      <q-separator />
+
+      <q-card-actions class="q-gutter-sm"
+                      align="center">
+        <template v-if="!sendingAlertConfirmation">
+          <q-btn v-close-popup
+                 class="full-width-button"
+                 flat
+                 label="انصراف"
+                 color="primary" />
+          <!-- مرحله اول: دکمه ارسال -->
+          <q-btn v-if="!sendingAlertConfirmation"
+                 class="full-width-button"
+                 label="ادامه به ارسال"
+                 color="primary"
+                 :disable="!alertMessageText.trim()"
+                 @click="sendingAlertConfirmation = true" />
+        </template>
+
+        <template v-else>
+          <!-- مرحله دوم: تأیید نهایی -->
+          <div class="text-body2 q-mb-md">آیا مطمئن هستید که می‌خواهید پیام را ارسال کنید؟</div>
+          <q-btn class="full-width-button"
+                 flat
+                 color="grey"
+                 label="خیر"
+                 @click="sendingAlertConfirmation = false" />
+          <q-btn class="full-width-button"
+                 color="red"
+                 label="بله، ارسال کن"
+                 :loading="sendingAlertMessage"
+                 @click="sendAlertMessage" />
+        </template>
+      </q-card-actions>
+    </q-card>
+  </q-dialog>
+  <q-dialog v-model="confirmRemoveAllDialog"
+            persistent>
+    <q-card>
+      <q-card-section class="row items-center">
+        <q-icon name="warning"
+                color="primary"
+                size="md" />
+        <span class="q-ml-sm">{{ confirmRemoveAllMessage }}</span>
+      </q-card-section>
+      <q-card-actions align="right">
+        <q-btn flat
+               label="انصراف"
+               color="primary"
+               @click="confirmRemoveAllDialog = false" />
+        <q-btn flat
+               label="تایید"
+               color="primary"
+               @click="deleteAllSubscriptions" />
+      </q-card-actions>
+    </q-card>
+  </q-dialog>
 </template>
 
 <script>
@@ -202,6 +330,7 @@ import PostRequisites from 'src/components/Control/PostRequisites.vue'
 import FormBuilderInputEditor from 'src/components/FormBuilderCustumComponents/FormBuilderInputEditor.vue'
 import { FormBuilderAssist } from 'quasar-form-builder'
 import BtnControl from 'src/components/Control/btn.vue'
+import Assist from 'assets/js/Assist'
 
 const PostRequisitesComp = shallowRef(PostRequisites)
 const FormBuilderInputEditorComp = shallowRef(FormBuilderInputEditor)
@@ -219,7 +348,15 @@ export default {
     return {
       tab: 'UnitInfo',
       mounted: false,
+      exportReportLoading: false,
+      deleteAllLoading: false,
+      showAlertDialog: false,
+      confirmRemoveAllDialog: false,
+      alertMessageText: '',
+      sendingAlertConfirmation: false,
+      sendingAlertMessage: false,
       newTranscriptSheetHistoryLoading: false,
+      subscriptionsLoading: false,
       newSessionLoading: false,
       newSessionName: '-',
       newSessionSessionCount: null,
@@ -417,15 +554,152 @@ export default {
         currentPage: 'current',
         perPage: 'per_page',
         pageKey: 'page'
+      },
+
+      subscriptionsApi: APIGateway.subscription.APIAdresses.base,
+      subscriptionsTable: {
+        columns: [
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'mobile_number',
+            required: true,
+            label: 'تلفن همراه',
+            align: 'left',
+            field: row => row.owner_info.mobile_number
+          },
+          {
+            name: 'email',
+            required: true,
+            label: 'ایمیل',
+            align: 'left',
+            field: row => row.owner_info.email
+          },
+          {
+            name: 'is_enabled',
+            required: true,
+            label: 'وضعیت',
+            align: 'left',
+            field: row => row.is_enabled ? 'فعال' : 'غیرفعال'
+          },
+          {
+            name: 'creation_time',
+            required: true,
+            label: 'تاریخ ایجاد',
+            align: 'left',
+            field: row => ShamsiDate.getDateTime(row.creation_time)
+          },
+          {
+            name: 'last_modification_time',
+            required: true,
+            label: 'تاریخ آخرین تغییر',
+            align: 'left',
+            field: row => ShamsiDate.getDateTime(row.last_modification_time)
+          },
+          {
+            name: 'actions',
+            required: true,
+            label: 'عملیات',
+            align: 'left',
+            field: ''
+          }
+        ],
+        data: []
+      },
+      subscriptionsFilterInputs: [
+        { type: 'hidden', name: 'source_type', value: 'UNIT' },
+        { type: 'hidden', name: 'source_id', value: this.$route.params.id }
+      ],
+      subscriptionsTableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
       }
     }
   },
+  computed: {
+    confirmRemoveAllMessage () {
+      return 'آیا از حذف همه دنبال کنندگان اطمینان دارید؟'
+    }
+  },
   mounted () {
     // this.getCategories()
     this.setActionBtn()
     this.mounted = true
   },
   methods: {
+    deleteAllSubscriptions () {
+      this.deleteAllLoading = true
+      APIGateway.subscription.deleteAllSubscriptions({
+        source_type: 'UNIT',
+        source_id: this.$route.params.id
+      })
+        .then(() => {
+          this.$refs.subscriptionsEntityIndex.search()
+          this.$q.notify({
+            type: 'positive',
+            message: 'دنبال کنندگان حذف شدند',
+            position: 'center'
+          })
+          this.deleteAllLoading = false
+          this.confirmRemoveAllDialog = false
+        })
+        .catch((e) => {
+          this.deleteAllLoading = false
+          this.confirmRemoveAllDialog = false
+        })
+    },
+    getSubscriptionsListExcel () {
+      this.exportReportLoading = true
+      const status = null
+      APIGateway.subscription.exportSubscriptionsListReport({
+        source_type: 'UNIT',
+        source_id: this.$route.params.id,
+        report_type: 'subscriptions_list',
+        status
+      })
+        .then((xlsxData) => {
+          Assist.saveXlsx(xlsxData, this.$route.params.id)
+          this.exportReportLoading = false
+        })
+        .catch((e) => {
+          this.exportReportLoading = false
+        })
+    },
+    sendAlertMessage() {
+      if (!this.alertMessageText.trim()) {
+        this.$q.notify({
+          type: 'negative',
+          message: 'متن پاسخ را کامل کنید.',
+          position: 'top'
+        })
+        return
+      }
+      this.sendingAlertMessage = true
+      APIGateway.subscription.sendMessage({
+        source_type: 'UNIT',
+        source_id: this.$route.params.id,
+        message: this.alertMessageText
+      })
+        .then(() => {
+          this.alertMessageText = ''
+          this.showAlertDialog = false
+          this.sendingAlertConfirmation = false
+        })
+        .catch((err) => {
+          console.error('خطا در ارسال:', err)
+        })
+        .finally(() => {
+          this.sendingAlertMessage = false
+        })
+    },
     getCategories () {
       this.categories.loading = true
       APIGateway.unitCategory.index()
@@ -452,7 +726,10 @@ export default {
       this.$refs.categoryEntityEdit.editEntity()
     },
     getRemoveMessage (row) {
-      return 'آیا از حذف ' + row.title + ' اطمینان دارید؟'
+      if (row.title) {
+        return 'آیا از حذف ' + row.title + ' اطمینان دارید؟'
+      }
+      return 'آیا از حذف اطمینان دارید؟'
     },
     reloadSessions () {
       this.$refs.sessionEntityIndex.reload()
@@ -494,4 +771,9 @@ export default {
 .btn-go-to-question-bank {
   margin-right: 20px;
 }
+.my-dialog-card {
+  width: 90vw; /* همیشه ۹۰٪ عرض صفحه */
+  max-width: 400px; /* ولی بیشتر از ۴۰۰px نشه */
+  min-width: 300px; /* و حداقل ۳۰۰px باشه که خیلی کوچیک نشه */
+}
 </style>

+ 4 - 1
src/pages/Auth/Login.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="login-page">
     <!--    <auth-login />-->
-    <login-component />
+    <login-component :redirect="redirect" />
   </div>
 </template>
 
@@ -13,6 +13,9 @@ export default {
   name: 'Login',
   // components: { AuthLogin },
   components: { LoginComponent },
+  data: () => ({
+    redirect: true
+  }),
   computed: {
     isUserLogin () {
       return this.$store.getters['Auth/isUserLogin']

+ 66 - 0
src/pages/Public/landing.vue

@@ -0,0 +1,66 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { APIGateway } from 'src/api/APIGateway.js'
+import { PageSetting } from 'src/models/PageSetting.js'
+import { mixinSEO, mixinPageOptions, mixinPrefetchServerData, mixinAuth } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'Landing',
+  mixins: [mixinPrefetchServerData, mixinPageOptions, mixinSEO, mixinAuth],
+  computed: {
+    hasDynamicSetting () {
+      return !!this.$route.meta?.hasDynamicSetting
+    },
+    hasDynamicSettingWithParams () {
+      return !!this.$route.meta?.hasDynamicSettingWithParams
+    }
+  },
+  methods: {
+    onFailedFetchData () {
+      setTimeout(() => {
+        this.checkNewLanding()
+      }, 1000)
+    },
+    goToNotFoundLanding () {
+      // this.$router.push({ name: 'NotFound' })
+      this.$router.push({ name: 'Public.Home' })
+    },
+    checkNewLanding () {
+      if (!this.isUserLogin || !this.user.isSuperUser()) {
+        this.goToNotFoundLanding()
+        return
+      }
+
+      this.$q.dialog({
+        title: 'ساخت صفحه جدید',
+        message: 'از نام لندینگ ' + this.$route.params.slug + ' اطمینان دارید؟ ',
+        cancel: true,
+        persistent: true
+      }).onOk(() => {
+        this.createNewLanding()
+      }).onCancel(() => {
+        // this.$router.push({ name: 'Public.Home' })
+        this.goToNotFoundLanding()
+      })
+    },
+    createNewLanding () {
+      const params = JSON.stringify(this.$route.params)
+      const routeName = this.$route.name
+      const key = 'route_name:' + routeName + '-params:' + params
+      const value = new PageSetting({
+        value: { sections: [], seo: {}, layoutConfig: {} }
+      }).getStringifyValue()
+      APIGateway.pageSetting.create({ key, value })
+        .then(() => {
+          this.currenSections = []
+        })
+    }
+  }
+}
+</script>

+ 12 - 1
src/pages/User/Profile/projectInfo.vue

@@ -41,6 +41,14 @@
                      :show-edit-button="false"
                      :show-index-button="false"
                      :show-reload-button="false" />
+        <entity-show v-if="projectLoaded && Object.keys(currentUserAttendanceSheet).length > 0 && currentUserAttendanceSheet.notation!==null && currentUserAttendanceSheet.notation!=='' && currentUserAttendanceSheet.notation!=='<br>'"
+                     v-model:value="graderShowInputs"
+                     :loaded-data="currentUserAttendanceSheet"
+                     :default-layout="false"
+                     :show-expand-button="false"
+                     :show-edit-button="false"
+                     :show-index-button="false"
+                     :show-reload-button="false" />
       </template>
     </entity-show>
   </div>
@@ -114,7 +122,10 @@ export default {
       attendanceSheetsShowInputs: [
         { type: 'separator', name: 'space', label: 'انجام پروژه', className: 'custom-separator', col: 'col-12' },
         { type: 'inputEditor', name: 'answer_text', responseKey: 'answer_text', label: 'متن پاسخ', placeholder: ' ', col: 'col-12' },
-        { type: 'file', name: 'answer_attachment', responseKey: 'answer_attachment', label: 'فایل ضمیمه', placeholder: ' ', col: 'col-md-4 col-12' },
+        { type: 'file', name: 'answer_attachment', responseKey: 'answer_attachment', label: 'فایل ضمیمه', placeholder: ' ', col: 'col-md-4 col-12' }
+      ],
+
+      graderShowInputs: [
         { type: 'separator', name: 'space', label: 'توضیحات مصحح', className: 'custom-separator', col: 'col-12' },
         { type: 'inputEditor', name: 'notation', responseKey: 'notation', label: '', placeholder: ' ', col: 'col-12' }
       ]

+ 24 - 0
src/router/routes/Public.js

@@ -203,6 +203,30 @@ export default {
         }
       ]
     },
+    {
+      name: 'Public.Landing',
+      path: 'landing',
+      meta: {
+        hasDynamicSetting: true
+      },
+      component: () => import('layouts/bareLayout.vue'),
+      children: [
+        {
+          path: '',
+          name: 'Public.Landing.List',
+          component: () => import('src/pages/BasicDynamicPage.vue')
+        },
+        {
+          path: ':slug',
+          name: 'Public.Landing.Show',
+          meta: {
+            hasDynamicSetting: true,
+            hasDynamicSettingWithParams: true
+          },
+          component: () => import('src/pages/Public/landing.vue')
+        }
+      ]
+    },
     {
       name: 'Public.Rules',
       path: 'rules',

+ 26 - 0
src/store/Auth/actions.js

@@ -60,6 +60,32 @@ export function signUp (context, data) {
   })
 }
 
+export function loginOtp (context, data) {
+  const setVars = (user, accessToken) => {
+    context.commit('updateUser', user)
+    context.commit('updateAccessToken', accessToken)
+    if (typeof window !== 'undefined') {
+      Cookies.set('BearerAccessToken', accessToken, {
+        // domain: '.' + window.location.host,
+        path: '/',
+        expires: '365d'
+      })
+    }
+    context.commit('updateAxiosAuthorization', accessToken)
+  }
+  return new Promise((resolve, reject) => {
+    APIGateway.auth.loginOtp(data)
+      .then(({ user, accessToken }) => {
+        setVars(user, accessToken)
+        context.commit('updateUser', user)
+        resolve({ accessToken, user })
+      })
+      .catch(() => {
+        reject()
+      })
+  })
+}
+
 export function setPassword (context, data) {
   const setVars = (user, accessToken) => {
     context.commit('updateUser', user)

+ 0 - 1
src/store/Auth/mutations.js

@@ -4,7 +4,6 @@ export function updateUser(state, newInfo) {
 export function updateAxiosAuthorization (state, accessToken) {
   const tokenType = 'Bearer'
   const token = accessToken ? (tokenType + ' ' + accessToken) : null
-
   this.$accessToken = accessToken
   if (this.$axios) {
     this.$axios.defaults.headers.common.Authorization = token