ali 2 anos atrás
pai
commit
ae48c8d113
28 arquivos alterados com 1295 adições e 7 exclusões
  1. 2 0
      src/api/APIGateway.js
  2. 84 0
      src/api/models/Notice.js
  3. 6 1
      src/components/EntityIndexGridItem.vue
  4. 5 0
      src/components/Template/Header/UserPanelHeader.vue
  5. 8 0
      src/components/Template/SideBard/AdminPanelSideBar.vue
  6. 89 0
      src/components/Widgets/Notice/AdminNoticeCreate/AdminNoticeCreate.vue
  7. 21 0
      src/components/Widgets/Notice/AdminNoticeCreate/OptionPanel.vue
  8. 98 0
      src/components/Widgets/Notice/AdminNoticeList/AdminNoticeList.vue
  9. 21 0
      src/components/Widgets/Notice/AdminNoticeList/OptionPanel.vue
  10. 121 0
      src/components/Widgets/Notice/AdminNoticeShow/AdminNoticeShow.vue
  11. 21 0
      src/components/Widgets/Notice/AdminNoticeShow/OptionPanel.vue
  12. 79 0
      src/components/Widgets/Notice/LastNotices/LastNotices.vue
  13. 39 0
      src/components/Widgets/Notice/LastNotices/NoticeItem.vue
  14. 21 0
      src/components/Widgets/Notice/LastNotices/OptionPanel.vue
  15. 258 0
      src/components/Widgets/Notice/NoticeList/NoticeList.vue
  16. 21 0
      src/components/Widgets/Notice/NoticeList/OptionPanel.vue
  17. 172 0
      src/components/Widgets/Notice/NoticeShow/NoticeShow.vue
  18. 21 0
      src/components/Widgets/Notice/NoticeShow/OptionPanel.vue
  19. 106 0
      src/models/Notice.js
  20. 23 0
      src/pages/Admin/Notice/Create.vue
  21. 23 0
      src/pages/Admin/Notice/List.vue
  22. 23 0
      src/pages/Admin/Notice/Show.vue
  23. 6 6
      src/router/routes.js
  24. 22 0
      src/router/routes/Admin.js
  25. 0 0
      src/router/routes/Auth.js
  26. 0 0
      src/router/routes/Document.js
  27. 0 0
      src/router/routes/Public.js
  28. 5 0
      src/router/routes/UserPanel.js

+ 2 - 0
src/api/APIGateway.js

@@ -7,6 +7,7 @@ import TeamAPI from 'src/api/models/Team.js'
 import TestAPI from 'src/api/models/Test.js'
 import EventAPI from 'src/api/models/Event.js'
 import MediaAPI from 'src/api/models/Media.js'
+import NoticeAPI from 'src/api/models/Notice.js'
 import TicketAPI from 'src/api/models/Ticket.js'
 import WalletAPI from 'src/api/models/Wallet.js'
 import TestSetAPI from 'src/api/models/TestSet.js'
@@ -54,6 +55,7 @@ export const APIGateway = {
   test: new TestAPI(),
   event: new EventAPI(),
   media: new MediaAPI(),
+  notice: new NoticeAPI(),
   ticket: new TicketAPI(),
   wallet: new WalletAPI(),
   testSet: new TestSetAPI(),

+ 84 - 0
src/api/models/Notice.js

@@ -0,0 +1,84 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { Notice, NoticeList } from 'src/models/Notice.js'
+
+export default class NoticeAPI extends APIRepository {
+  constructor() {
+    super('ticket', appApi)
+    this.APIAdresses = {
+      base: '/mma/notices',
+      myNotSeenNotificationsCount: '/mma/notices/my_not_seen_notifications_count',
+      byId: (id) => '/mma/notices/' + id
+    }
+    this.restUrl = (id) => this.url + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new Notice(response.data) },
+      post: (response) => { return new Notice(response.data) },
+      put: (response) => { return new Notice(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 NoticeList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  myNotSeenNotificationsCount(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.myNotSeenNotificationsCount,
+      resolveCallback: (response) => {
+        return response.data.count // number
+      },
+      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 Notice(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 6 - 1
src/components/EntityIndexGridItem.vue

@@ -1,5 +1,6 @@
 <template>
-  <q-card class="EntityIndexGridItem">
+  <q-card :flat="flat"
+          class="EntityIndexGridItem">
     <q-card-section v-if="inputData.props?.cols && inputData.props.cols.length > 0">
       <div v-for="(col, colIndex) in inputData.props.cols"
            :key="colIndex"
@@ -26,6 +27,10 @@ export default {
       default () {
         return {}
       }
+    },
+    flat: {
+      type: Boolean,
+      default: false
     }
   },
   methods: {}

+ 5 - 0
src/components/Template/Header/UserPanelHeader.vue

@@ -32,6 +32,11 @@
           </q-btn>
         </div>
         <div class="left-side">
+          <q-btn flat
+                 :to="{name: 'UserPanel.Notice.List'}">
+            <q-icon name="notifications_none" />
+            پیام ها
+          </q-btn>
           <q-btn flat>
             <svg id="_000000ff"
                  data-name="#000000ff"

+ 8 - 0
src/components/Template/SideBard/AdminPanelSideBar.vue

@@ -219,6 +219,14 @@ export default {
             }
           ]
         },
+        {
+          title: 'اعلان‌ها',
+          roles: ['superuser'],
+          routeName: 'Admin.Notice.List',
+          icon: 'notifications',
+          show: true,
+          active: false
+        },
         {
           title: 'تمام درخواست ها',
           roles: ['superuser'],

+ 89 - 0
src/components/Widgets/Notice/AdminNoticeCreate/AdminNoticeCreate.vue

@@ -0,0 +1,89 @@
+<template>
+  <div class="AdminNoticeCreate"
+       :style="localOptions.style">
+    <entity-create v-if="mounted"
+                   ref="entityCreate"
+                   v-model:value="inputs"
+                   title="ارسال اعلان"
+                   :api="api"
+                   :entity-id-key-in-response="entityIdKeyInResponse"
+                   :show-route-param-key="showRouteParamKey"
+                   :index-route-name="indexRouteName"
+                   :show-route-name="showRouteName"
+                   :show-close-button="false"
+                   :show-save-button="false"
+                   :show-expand-button="false" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'src/components/Control/btn.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'AdminNoticeCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.notice.APIAdresses.base,
+      entityIdKeyInResponse: 'id',
+      showRouteParamKey: 'id',
+      showRouteName: 'AdminPanel.Notice.Show',
+      indexRouteName: 'AdminPanel.Notice.List',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-12' },
+        { type: 'input', name: 'url', responseKey: 'url', label: 'لینک', placeholder: ' ', col: 'col-12' },
+        { type: 'inputEditor', name: 'body', responseKey: 'body', label: 'متن', placeholder: ' ', col: 'col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ارسال', placeholder: ' ', atClick: this.create, col: 'col-md-6' }
+      ]
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.mounted = true
+    })
+  },
+  methods: {
+    create() {
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity()
+        .then(() => {
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminNoticeCreate {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    position: relative;
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Notice/AdminNoticeCreate/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </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]
+})
+</script>

+ 98 - 0
src/components/Widgets/Notice/AdminNoticeList/AdminNoticeList.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="AdminNoticeList"
+       :style="localOptions.style">
+    <entity-index v-if="mounted"
+                  v-model:value="inputs"
+                  title="لیست اعلان ها"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :create-route-name="createRouteName"
+                  :show-search-button="false"
+                  :show-expand-button="false"
+                  :show-reload-button="false">
+      <template #entity-index-table-cell="{inputData}">
+        <template v-if="inputData.col.name === 'action'">
+          <q-btn flat
+                 color="primary"
+                 :to="{name: 'Admin.Notice.Show', params: {id: inputData.props.row.id}}">
+            مشاهده جزییات
+          </q-btn>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+// import { Notice } from 'src/models/Notice.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'AdminNoticeList',
+  components: { EntityIndex },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      api: APIGateway.notice.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [],
+      table: {
+        columns: [
+          {
+            name: 'title',
+            required: true,
+            label: 'موضوع اعلان',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'sender',
+            required: true,
+            label: 'فرستنده',
+            align: 'left',
+            field: row => row.creator_info ? row.creator_info.firstname + ' ' + row.creator_info.lastname : ''
+          },
+          {
+            name: 'status',
+            required: true,
+            label: 'وضعیت',
+            align: 'left',
+            // field: row => (new Notice(row)).status_info.label
+            field: row => row.state
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.mounted = true
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminNoticeList {
+
+}
+</style>

+ 21 - 0
src/components/Widgets/Notice/AdminNoticeList/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </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]
+})
+</script>

+ 121 - 0
src/components/Widgets/Notice/AdminNoticeShow/AdminNoticeShow.vue

@@ -0,0 +1,121 @@
+<template>
+  <div class="AdminNoticeShow"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-edit v-if="mounted"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 :title="'ویرایش' + ' ' + entityTitle "
+                 :api="api"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :show-route-name="showRouteName"
+                 :show-close-button="false"
+                 :show-edit-button="false"
+                 :show-expand-button="false"
+                 :show-save-button="false"
+                 :show-reload-button="false"
+                 :redirect-after-edit="false"
+                 :after-load-input-data="afterLoadInputData" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityEdit } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'src/components/Control/btn.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'AdminNoticeShow',
+  components: {
+    EntityEdit
+  },
+  mixins: [mixinWidget],
+  data () {
+    const noticeId = this.$route.params.id
+    return {
+      mounted: false,
+      entityLoading: true,
+      entityTitle: '',
+      api: APIGateway.notice.APIAdresses.byId(noticeId),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'AdminPanel.Notice.Show',
+      indexRouteName: 'AdminPanel.Notice.List',
+      inputs: [
+        {
+          type: 'input',
+          name: 'title',
+          responseKey: 'title',
+          label: 'عنوان',
+          placeholder: ' ',
+          col: 'col-12'
+        },
+        {
+          type: 'input',
+          name: 'url',
+          responseKey: 'url',
+          label: 'لینک',
+          placeholder: ' ',
+          col: 'col-12'
+        },
+        {
+          type: 'inputEditor',
+          name: 'body',
+          responseKey: 'body',
+          label: 'متن',
+          placeholder: ' ',
+          col: 'col-12'
+        },
+        {
+          type: 'hidden',
+          name: 'id',
+          responseKey: 'id'
+        },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ثبت تغییرات', placeholder: ' ', atClick: this.edit, col: 'col-md-6' }
+      ]
+    }
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    onUpdateServices () {
+      this.$refs.entityEdit.getData()
+    },
+    afterLoadInputData (response) {
+      this.entityTitle = response.title
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity()
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminNoticeShow {
+
+}
+</style>

+ 21 - 0
src/components/Widgets/Notice/AdminNoticeShow/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </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]
+})
+</script>

+ 79 - 0
src/components/Widgets/Notice/LastNotices/LastNotices.vue

@@ -0,0 +1,79 @@
+<template>
+  <q-card class="LastNotices">
+    <div class="title">
+      اعلان‌ها
+    </div>
+    <div v-if="mounted"
+         class="items">
+      <template v-if="notices.loading">
+        کمی صبر کنید ...
+      </template>
+      <template v-else-if="notices.list.length > 0">
+        <notice-item v-for="notice in notices.list"
+                     :key="notice.id"
+                     :notice="notice" />
+      </template>
+      <template v-else>
+        موردی وجود ندارد
+      </template>
+    </div>
+  </q-card>
+</template>
+
+<script>
+import { NoticeList } from 'src/models/Notice.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import NoticeItem from 'src/components/Widgets/Notice/LastNotices/NoticeItem.vue'
+
+export default {
+  name: 'LastNotices',
+  components: { NoticeItem },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      mounted: false,
+      notices: new NoticeList()
+    }
+  },
+  mounted() {
+    this.mounted = true
+    this.loadData()
+  },
+  methods: {
+    loadData () {
+      this.getApiRequest()
+        .then((data) => {
+          this.prefetchServerDataPromiseThen(data)
+        })
+        .catch(() => {
+          this.prefetchServerDataPromiseCatch()
+        })
+    },
+    prefetchServerDataPromiseThen ({ list }) {
+      this.notices = new NoticeList(list)
+      this.notices.loading = false
+    },
+    prefetchServerDataPromiseCatch () {
+      this.notices.loading = false
+    },
+
+    getApiRequest() {
+      this.notices.loading = true
+      return APIGateway.notice.index()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.LastNotices {
+  padding: 27px 24px;
+  .title {
+    color: #2E2E2E;
+    font-size: 20px;
+    font-weight: 700;
+    line-height: 140%;
+  }
+}
+</style>

+ 39 - 0
src/components/Widgets/Notice/LastNotices/NoticeItem.vue

@@ -0,0 +1,39 @@
+<template>
+  <div class="NoticeItem">
+    <div class="title ellipsis-3-lines">
+      {{ notice.title }}
+    </div>
+  </div>
+</template>
+
+<script>
+import { Notice } from 'src/models/Notice.js'
+
+export default {
+  name: 'NoticeItem',
+  props: {
+    notice: {
+      type: Notice,
+      default: new Notice()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.NoticeItem {
+  border-bottom: solid 1px #EDEDED;
+  padding: 16px 0;
+  display: flex;
+  flex-flow: row;
+  justify-content: flex-start;
+  align-items: center;
+  .title {
+    width: 100%;
+    color: #747474;
+    font-size: 14px;
+    font-weight: 500;
+    line-height: 140%;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Notice/LastNotices/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </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]
+})
+</script>

+ 258 - 0
src/components/Widgets/Notice/NoticeList/NoticeList.vue

@@ -0,0 +1,258 @@
+<template>
+  <div class="NoticeList"
+       :style="localOptions.style">
+    <div class="more-action">
+      <breadcrumbs />
+    </div>
+    <q-card flat>
+      <q-card-section>
+        <q-btn :flat="filterType !== 'all'"
+               color="primary"
+               @click="setFilterToAll">
+          همه پیام ها
+        </q-btn>
+        <q-btn :flat="filterType !== 'read'"
+               color="primary"
+               @click="setFilterToRead">
+          خوانده نشده
+          <q-badge v-if="myNotSeenNotificationsCount !== 0"
+                   color="warning">
+            {{ myNotSeenNotificationsCount }}
+          </q-badge>
+        </q-btn>
+        <q-btn :flat="filterType !== 'not-read'"
+               color="primary"
+               @click="setFilterToNotRead">
+          خوانده شده
+        </q-btn>
+      </q-card-section>
+      <q-separator />
+      <div class="entity-index-wrapper">
+        <entity-index v-if="mounted"
+                      ref="entityIndex"
+                      v-model:value="inputs"
+                      title=""
+                      :api="api"
+                      :table="table"
+                      :table-keys="tableKeys"
+                      :table-grid-size="true"
+                      :create-route-name="createRouteName"
+                      :show-search-button="false"
+                      :show-expand-button="false"
+                      :show-reload-button="false"
+                      :default-layout="false">
+          <template #entity-index-table-cell="{inputData}">
+            <template v-if="inputData.col.name === 'action'">
+              <q-btn flat
+                     color="primary"
+                     :to="{name: 'UserPanel.Notice.Show', params: {id: inputData.props.row.id}}">
+                مشاهده جزییات
+              </q-btn>
+            </template>
+            <template v-else>
+              {{ inputData.col.value }}
+            </template>
+          </template>
+          <template #entity-index-table-item-cell="{inputData}">
+            <div class="entity-index-grid-item"
+                 :class="{'notice--read': getNoticeModel(inputData.props.row).state === 'READ'}">
+              <div class="entity-index-grid-item__header">
+                <div class="entity-index-grid-item__header-title">
+                  {{ inputData.props.row.title }}
+                </div>
+                <div class="entity-index-grid-item__header-meta">
+                  <div v-if="getNoticeModel(inputData.props.row).state"
+                       class="entity-index-grid-item__header-state"
+                       :class="getNoticeModel(inputData.props.row).state">
+                    {{ getNoticeModel(inputData.props.row).state_info.label }}
+                  </div>
+                  <div class="entity-index-grid-item__header-creation-time">
+                    {{ getNoticeModel(inputData.props.row).shamsiDate('creation_time').dateTime }}
+                  </div>
+                </div>
+              </div>
+              <div class="entity-index-grid-item__content"
+                   v-html="inputData.props.row.body" />
+              <div v-if="getNoticeModel(inputData.props.row).state !== 'READ'"
+                   class="entity-index-grid-item__action">
+                <q-btn color="primary"
+                       outline
+                       @click="updateNoticeToSeen(inputData.props.row.id)">
+                  خوانده شود
+                </q-btn>
+              </div>
+            </div>
+          </template>
+        </entity-index>
+      </div>
+    </q-card>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { Notice } from 'src/models/Notice.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
+
+export default {
+  name: 'NoticeList',
+  components: { EntityIndex, Breadcrumbs },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      filterType: 'all',
+      myNotSeenNotificationsCount: 0,
+      api: APIGateway.notice.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: 'hidden', name: 'state', value: null }
+      ],
+      table: {
+        columns: [
+          {
+            name: 'title',
+            required: true,
+            label: 'موضوع اعلان',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'sender',
+            required: true,
+            label: 'فرستنده',
+            align: 'left',
+            field: row => row.creator_info ? row.creator_info.firstname + ' ' + row.creator_info.lastname : ''
+          },
+          {
+            name: 'status',
+            required: true,
+            label: 'وضعیت',
+            align: 'left',
+            field: row => (new Notice(row)).status_info.label
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.mounted = true
+    this.updateBreadcrumbs()
+    this.getMyNotSeenNotificationsCount()
+  },
+  methods: {
+    updateBreadcrumbs () {
+      this.$store.commit('AppLayout/updateBreadcrumbs', {
+        visible: true,
+        loading: false,
+        path: [
+          {
+            label: 'پیام ها',
+            to: { name: 'UserPanel.Notice.List' }
+          }
+        ]
+      })
+    },
+    getNoticeModel (data) {
+      return new Notice(data)
+    },
+    updateNoticeToSeen (noticeId) {
+      APIGateway.notice.seen(noticeId)
+        .finally(() => {
+          this.$refs.entityIndex.search()
+        })
+    },
+    getMyNotSeenNotificationsCount () {
+      APIGateway.notice.myNotSeenNotificationsCount()
+        .then((count) => {
+          this.myNotSeenNotificationsCount = count
+        })
+        .finally(() => {
+          // this.$refs.entityIndex.search()
+        })
+    },
+    setFilterToRead () {
+      this.filterType = 'read'
+      FormBuilderAssist.setAttributeByName(this.inputs, 'state', 'value', 'READ')
+      this.$refs.entityIndex.search()
+    },
+    setFilterToNotRead () {
+      this.filterType = 'not-read'
+      FormBuilderAssist.setAttributeByName(this.inputs, 'state', 'value', 'NOT-READ')
+      this.$refs.entityIndex.search()
+    },
+    setFilterToAll () {
+      this.filterType = 'all'
+      FormBuilderAssist.setAttributeByName(this.inputs, 'state', 'value', null)
+      this.$refs.entityIndex.search()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.NoticeList {
+  & > .q-card {
+    border: solid 1px #DFE1EC;
+    .entity-index-wrapper {
+      padding-top: 12px;
+      margin-right: -16px;
+      margin-left: -16px;
+      .entity-index-grid-item {
+        width: 100%;
+        padding: 20px;
+        border-bottom: solid 1px #DFE1EC;
+        &.notice--read {
+          color: #A5A4A4;
+        }
+        .entity-index-grid-item__header {
+          display: flex;
+          justify-content: space-between;
+          .entity-index-grid-item__header-title {}
+          .entity-index-grid-item__header-meta {
+            display: flex;
+            align-items: center;
+            justify-content: flex-end;
+            gap: 24px;
+            .entity-index-grid-item__header-state {
+              padding: 4px 8px;
+              border-radius: 15px;
+              color: #FF9100;
+              background: #ffead1;
+              &.READ {
+                color: #10D078;
+                background: #adffd8;
+              }
+            }
+            .entity-index-grid-item__header-creation-time {}
+          }
+        }
+        .entity-index-grid-item__content {
+
+        }
+        .entity-index-grid-item__action {
+          display: flex;
+          justify-content: flex-end;
+        }
+      }
+    }
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Notice/NoticeList/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </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]
+})
+</script>

+ 172 - 0
src/components/Widgets/Notice/NoticeShow/NoticeShow.vue

@@ -0,0 +1,172 @@
+<template>
+  <div class="NoticeShow"
+       :style="localOptions.style">
+    <div class="title">
+      <div class="static-title">
+        مشاهده
+      </div>
+      <div class="dynamic-title">
+        <template v-if="entityLoading">
+          <q-skeleton type="text"
+                      class="q-ml-lg"
+                      width="100px" />
+        </template>
+        <template v-else>
+          {{ entityTitle }}
+        </template>
+      </div>
+      <div class="back-action">
+        <q-btn flat
+               :to="{name: 'UserPanel.Notice.List'}"
+               color="grey">
+          بازگشت
+          >
+        </q-btn>
+      </div>
+    </div>
+    <q-card class="form"
+            flat>
+      <entity-show v-if="mounted"
+                   ref="entityEdit"
+                   v-model:value="inputs"
+                   title="اطلاعات کاربری"
+                   :api="api"
+                   :entity-id-key="entityIdKey"
+                   :entity-param-key="entityParamKey"
+                   :show-route-name="showRouteName"
+                   :show-close-button="false"
+                   :show-edit-button="false"
+                   :show-expand-button="false"
+                   :show-save-button="false"
+                   :show-reload-button="false"
+                   :default-layout="false"
+                   :redirect-after-edit="false"
+                   :after-load-input-data="afterLoadInputData" />
+      <div class="action">
+        <div class="row q-mt-lg justify-end">
+          <div class="col-md-4 col-12">
+            <q-btn color="primary"
+                   class="full-width"
+                   :loading="entityLoading"
+                   @click="edit">
+              ثبت تغییرات
+            </q-btn>
+          </div>
+        </div>
+      </div>
+    </q-card>
+  </div>
+</template>
+
+<script>
+import { EntityShow } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'NoticeShow',
+  components: {
+    EntityShow
+  },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      mounted: false,
+      entityLoading: true,
+      entityTitle: '',
+      api: APIGateway.notice.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'AdminPanel.Notice.Show',
+      indexRouteName: 'AdminPanel.Notice.List',
+      inputs: [
+        {
+          type: 'input',
+          name: 'title',
+          responseKey: 'title',
+          label: 'عنوان',
+          placeholder: ' ',
+          col: 'col-12'
+        },
+        {
+          type: 'input',
+          name: 'url',
+          responseKey: 'url',
+          label: 'لینک',
+          placeholder: ' ',
+          col: 'col-12'
+        },
+        {
+          type: 'inputEditor',
+          name: 'body',
+          responseKey: 'body',
+          label: 'متن',
+          placeholder: ' ',
+          col: 'col-12'
+        },
+        {
+          type: 'hidden',
+          name: 'id',
+          responseKey: 'id'
+        }
+      ]
+    }
+  },
+  created() {
+    this.api = this.api + '/' + this.$route.params.id
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    onUpdateServices () {
+      this.$refs.entityEdit.getData()
+    },
+    afterLoadInputData (response) {
+      this.entityTitle = response.title
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity()
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.NoticeShow {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    display: flex;
+    flex-flow: row;
+    .static-title {
+
+    }
+    .dynamic-title {
+
+    }
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Notice/NoticeShow/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </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]
+})
+</script>

+ 106 - 0
src/models/Notice.js

@@ -0,0 +1,106 @@
+import { User } from 'src/models/User.js'
+import { Collection, Model } from 'js-abstract-model'
+import { TicketCategory } from 'src/models/TicketCategory.js'
+
+class Notice extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'title' },
+      { key: 'body' }, //
+      { key: 'priority' }, // HIGH - ...
+      { key: 'status' }, // ADMIN_REPLIED - ...
+      { key: 'state' },
+      { key: 'creation_time' },
+      { key: 'category' },
+      {
+        key: 'category_info',
+        relatedModel: TicketCategory
+      },
+      { key: 'owner' },
+      {
+        key: 'owner_info',
+        relatedModel: User
+      }
+    ], {
+      status: {
+        infoKey: 'status_info',
+        enumListKey: 'statusEnums',
+        enums: [
+          {
+            label: 'باز شده',
+            value: 'OPENED'
+          },
+          {
+            label: 'پاسخ داده شده',
+            value: 'ADMIN_REPLIED'
+          },
+          {
+            label: 'در حال انتظار',
+            value: 'PENDING'
+          },
+          {
+            label: 'خوانده نشده',
+            value: 'UNREAD'
+          },
+          {
+            label: 'خوانده شده',
+            value: 'READ'
+          },
+          {
+            label: 'بسته شده',
+            value: 'CLOSED'
+          }
+        ]
+      },
+      state: {
+        infoKey: 'state_info',
+        enumListKey: 'stateEnums',
+        enums: [
+          {
+            label: 'باز شده',
+            value: 'OPENED'
+          },
+          {
+            label: 'پاسخ داده شده',
+            value: 'ADMIN_REPLIED'
+          },
+          {
+            label: 'در حال انتظار',
+            value: 'PENDING'
+          },
+          {
+            label: 'خوانده نشده',
+            value: 'UNREAD'
+          },
+          {
+            label: 'خوانده شده',
+            value: 'READ'
+          },
+          {
+            label: 'بسته شده',
+            value: 'CLOSED'
+          }
+        ]
+      },
+      priority: {
+        infoKey: 'priority_info',
+        enumListKey: 'priorityEnums',
+        enums: [
+          {
+            label: 'بالا',
+            value: 'HIGH'
+          }
+        ]
+      }
+    })
+  }
+}
+
+class NoticeList extends Collection {
+  model () {
+    return Notice
+  }
+}
+
+export { Notice, NoticeList }

+ 23 - 0
src/pages/Admin/Notice/Create.vue

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'AdminPanel.Package.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminBlogCreate', options: { style: { marginTop: '124px' } } }], options: {} }], options: {} }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/Notice/List.vue

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'AdminPanel.Package.List',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminNoticeList', options: { style: { marginBottom: '24px' } } }, { name: 'AdminNoticeCreate' }], options: {} }], options: {} }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/Notice/Show.vue

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'AdminPanel.Package.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminNoticeShow' }] }], options: {} }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 6 - 6
src/router/routes.js

@@ -1,8 +1,8 @@
-import Auth from 'src/router/public/Auth.js'
-import Admin from 'src/router/public/Admin.js'
-import Public from 'src/router/public/Public.js'
-import Document from 'src/router/public/Document.js'
-import UserPanel from 'src/router/public/UserPanel.js'
+import Auth from 'src/router/routes/Auth.js'
+import Admin from 'src/router/routes/Admin.js'
+import Public from 'src/router/routes/Public.js'
+import Document from 'src/router/routes/Document.js'
+import UserPanel from 'src/router/routes/UserPanel.js'
 
 const routes = [
   {
@@ -38,7 +38,7 @@ const routes = [
     },
     component: () => import('layouts/MainLayout.vue'),
     meta: {
-      hasDynamicSetting: true
+      hasDynamicSetting: false
     },
     children: [
       Auth,

+ 22 - 0
src/router/public/Admin.js → src/router/routes/Admin.js

@@ -239,6 +239,28 @@ export default {
         // { name: 'Admin.Content.Ticket', path: 'ticket', component: () => import('src/pages/Admin/Classroom/Ticket.vue'), breadcrumbs: { title: 'درخواست ها دوره های آموزشی' } }
       ]
     },
+    {
+      path: 'notice',
+      name: 'Admin.Notice',
+      component: () => import('layouts/bareLayout.vue'),
+      children: [
+        {
+          path: '',
+          name: 'Admin.Notice.List',
+          component: () => import('src/pages/Admin/Notice/List.vue')
+        },
+        {
+          path: 'create',
+          name: 'Admin.Notice.Create',
+          component: () => import('src/pages/Admin/Notice/Create.vue')
+        },
+        {
+          path: ':id',
+          name: 'Admin.Notice.Show',
+          component: () => import('src/pages/Admin/Notice/Show.vue')
+        }
+      ]
+    },
     {
       path: 'content-category',
       name: 'Admin.ContentCategory',

+ 0 - 0
src/router/public/Auth.js → src/router/routes/Auth.js


+ 0 - 0
src/router/public/Document.js → src/router/routes/Document.js


+ 0 - 0
src/router/public/Public.js → src/router/routes/Public.js


+ 5 - 0
src/router/public/UserPanel.js → src/router/routes/UserPanel.js

@@ -303,6 +303,11 @@ export default {
         }
       ]
     },
+    {
+      path: 'notice',
+      name: 'UserPanel.Notice.List',
+      component: () => import('src/pages/User/Notice/List.vue')
+    },
     {
       path: 'wallet',
       name: 'UserPanel.Wallet',