ali 3 лет назад
Родитель
Сommit
8b80acc85f
24 измененных файлов с 1684 добавлено и 0 удалено
  1. 2 0
      src/api/APIGateway.js
  2. 70 0
      src/api/models/Ticket.js
  3. 130 0
      src/components/Widgets/Ticket/AdminTicketCreate/AdminTicketCreate.vue
  4. 21 0
      src/components/Widgets/Ticket/AdminTicketCreate/OptionPanel.vue
  5. 124 0
      src/components/Widgets/Ticket/AdminTicketList/AdminTicketList.vue
  6. 21 0
      src/components/Widgets/Ticket/AdminTicketList/OptionPanel.vue
  7. 280 0
      src/components/Widgets/Ticket/AdminTicketShow/AdminTicketShow.vue
  8. 21 0
      src/components/Widgets/Ticket/AdminTicketShow/OptionPanel.vue
  9. 79 0
      src/components/Widgets/Ticket/LastTickets/LastTickets.vue
  10. 21 0
      src/components/Widgets/Ticket/LastTickets/OptionPanel.vue
  11. 65 0
      src/components/Widgets/Ticket/LastTickets/TicketItem.vue
  12. 21 0
      src/components/Widgets/Ticket/TicketCreate/OptionPanel.vue
  13. 128 0
      src/components/Widgets/Ticket/TicketCreate/TicketCreate.vue
  14. 21 0
      src/components/Widgets/Ticket/TicketList/OptionPanel.vue
  15. 136 0
      src/components/Widgets/Ticket/TicketList/TicketList.vue
  16. 21 0
      src/components/Widgets/Ticket/TicketShow/OptionPanel.vue
  17. 273 0
      src/components/Widgets/Ticket/TicketShow/TicketShow.vue
  18. 108 0
      src/models/Ticket.js
  19. 18 0
      src/models/TicketCategory.js
  20. 30 0
      src/models/TicketReply.js
  21. 24 0
      src/pages/User/Ticket/Create.vue
  22. 24 0
      src/pages/User/Ticket/List.vue
  23. 24 0
      src/pages/User/Ticket/Show.vue
  24. 22 0
      src/router/routes.js

+ 2 - 0
src/api/APIGateway.js

@@ -2,6 +2,7 @@ import SeoAPI from 'src/api/models/Seo.js'
 import AuthAPI from 'src/api/models/Auth.js'
 import UserAPI from 'src/api/models/User.js'
 import UnitAPI from 'src/api/models/Unit.js'
+import TicketAPI from 'src/api/models/Ticket.js'
 import WalletAPI from 'src/api/models/Wallet.js'
 import InvoiceAPI from 'src/api/models/Invoice.js'
 import SessionAPI from 'src/api/models/Session.js'
@@ -18,6 +19,7 @@ export const APIGateway = {
   auth: new AuthAPI(),
   user: new UserAPI(),
   unit: new UnitAPI(),
+  ticket: new TicketAPI(),
   wallet: new WalletAPI(),
   invoice: new InvoiceAPI(),
   session: new SessionAPI(),

+ 70 - 0
src/api/models/Ticket.js

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

+ 130 - 0
src/components/Widgets/Ticket/AdminTicketCreate/AdminTicketCreate.vue

@@ -0,0 +1,130 @@
+<template>
+  <div class="AdminTicketCreate"
+       :style="localOptions.style">
+    <div class="title">
+      ارسال پیام
+    </div>
+    <q-card class="form"
+            flat>
+      <entity-create v-if="mounted"
+                     ref="entityCreate"
+                     v-model:value="inputs"
+                     title="نقش جدید"
+                     :api="api"
+                     :default-layout="false"
+                     :entity-id-key-in-response="entityIdKeyInResponse"
+                     :show-route-param-key="showRouteParamKey"
+                     :index-route-name="indexRouteName"
+                     :show-route-name="showRouteName" />
+      <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="create">
+              ارسال
+            </q-btn>
+          </div>
+        </div>
+      </div>
+    </q-card>
+  </div>
+</template>
+
+<script>
+import { EntityCreate } from 'quasar-crud'
+import { Ticket } from 'src/models/Ticket.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'AdminTicketCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.ticket.APIAdresses.base,
+      entityIdKeyInResponse: 'id',
+      showRouteParamKey: 'id',
+      showRouteName: 'AdminPanel.Ticket.Show',
+      indexRouteName: 'AdminPanel.Ticket.List',
+      inputs: [
+        { type: 'select', name: 'category', responseKey: 'category', options: [], label: 'دسته', col: 'col-md-6 col-12' },
+        {
+          type: 'select',
+          name: 'state',
+          responseKey: 'state',
+          options: (new Ticket()).statusEnums,
+          multiple: false,
+          label: 'وضعیت',
+          col: 'col-md-3 col-12'
+        },
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', col: 'col-md-12 col-12' },
+        { type: 'inputEditor', name: 'body', responseKey: 'body', label: 'متن', col: 'col-md-12 col-12' },
+        { type: 'hidden', name: 'owner', responseKey: 'owner', value: 1, label: 'owner', col: 'col-md-12 col-12' }
+      ]
+    }
+  },
+  mounted() {
+    this.loadOptions()
+    this.$nextTick(() => {
+      this.mounted = true
+    })
+  },
+  methods: {
+    create() {
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity()
+        .then(() => {
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    },
+    loadOptions () {
+      this.loadCategories()
+    },
+    loadCategories () {
+      APIGateway.ticketCategory.index()
+        .then(({ list }) => {
+          const ticketCategoryList = list
+          this.setInputOptions('category', ticketCategoryList.list.map(item => {
+            return {
+              value: item.id,
+              label: item.title
+            }
+          }))
+        })
+    },
+    setInputOptions (name, options) {
+      this.$refs.entityCreate.setInputAttributeByName(name, 'options', options)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminTicketCreate {
+  .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/Ticket/AdminTicketCreate/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>

+ 124 - 0
src/components/Widgets/Ticket/AdminTicketList/AdminTicketList.vue

@@ -0,0 +1,124 @@
+<template>
+  <div class="AdminTicketList"
+       :style="localOptions.style">
+    <div class="title">
+      پیام‌های دریافتی
+    </div>
+    <q-card class="list"
+            flat>
+      <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"
+                    :default-layout="false">
+        <template #entity-index-table-cell="{inputData}">
+          <template v-if="inputData.col.name === 'action'">
+            <q-btn flat
+                   color="primary"
+                   :to="{name: 'AdminPanel.Ticket.Show', params: {id: inputData.props.row.id}}">
+              مشاهده جزییات
+            </q-btn>
+          </template>
+          <template v-else>
+            {{ inputData.col.value }}
+          </template>
+        </template>
+      </entity-index>
+    </q-card>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { Ticket } from 'src/models/Ticket.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'AdminTicketList',
+  components: { EntityIndex },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      api: APIGateway.ticket.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.firstname + ' ' + row.creator_info.lastname
+          },
+          {
+            name: 'status',
+            required: true,
+            label: 'وضعیت',
+            align: 'left',
+            field: row => (new Ticket(row)).status_info.label
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.mounted = true
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminTicketList {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+  }
+  :deep(.list) {
+    .quasar-crud-index-table {
+      padding: 0;
+      .q-table__container {
+        background-color: transparent;
+        box-shadow: none;
+        border: none;
+        .q-table__top {
+          display: none;
+        }
+      }
+    }
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Ticket/AdminTicketList/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>

+ 280 - 0
src/components/Widgets/Ticket/AdminTicketShow/AdminTicketShow.vue

@@ -0,0 +1,280 @@
+<template>
+  <div class="AdminTicketShow"
+       :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>
+          {{ packageTitle }}
+        </template>
+      </div>
+      <div class="back-action">
+        <q-btn flat
+               :to="{name: 'AdminPanel.Ticket.List'}"
+               color="grey">
+          بازگشت
+          >
+        </q-btn>
+      </div>
+    </div>
+    <q-card class="form"
+            flat>
+      <entity-edit 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="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>
+
+      <q-card v-if="mounted"
+              class="q-mt-md">
+        <q-card-section>
+          <div class="row justify-center">
+            <div style="width: 100%;">
+              <q-chat-message v-for="message in repliesInfo"
+                              :key="message.id"
+                              :avatar="message.creator_info.picture ? message.creator_info.picture : 'assets/images/web/sample-avatar.svg'"
+                              :name="message.creator_info.firstname + ' ' + message.creator_info.lastname"
+                              :text="[message.body]"
+                              :sent="authenticatedUser.id !== message.creator_info.id" />
+              <!--                  <div>-->
+              <!--                    <q-input v-model="replyBody"-->
+              <!--                             bottom-slots-->
+              <!--                             :loading="replyBodyLoading">-->
+              <!--                      <template v-slot:after>-->
+              <!--                        <q-btn round-->
+              <!--                               dense-->
+              <!--                               flat-->
+              <!--                               icon="send"-->
+              <!--                               :loading="replyBodyLoading"-->
+              <!--                               @click="sendReplyBody" />-->
+              <!--                      </template>-->
+              <!--                    </q-input>-->
+              <!--                  </div>-->
+            </div>
+          </div>
+        </q-card-section>
+      </q-card>
+
+      <div class="action">
+        <div class="row q-mt-lg justify-end">
+          <div class="col-md-12 col-12">
+            متن پیام
+          </div>
+          <div class="col-md-12 col-12 q-mb-md">
+            <q-input v-model="replyText"
+                     type="textarea" />
+          </div>
+          <div class="col-md-4 col-12">
+            <q-btn color="primary"
+                   class="full-width"
+                   :loading="entityLoading"
+                   @click="sendReply">
+              ارسال پاسخ
+            </q-btn>
+          </div>
+        </div>
+      </div>
+    </q-card>
+  </div>
+</template>
+
+<script>
+import { EntityEdit } from 'quasar-crud'
+import { User } from 'src/models/User.js'
+import { Ticket } from 'src/models/Ticket.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { TicketCategoryList } from 'src/models/TicketCategory'
+
+export default {
+  name: 'AdminTicketShow',
+  components: {
+    EntityEdit
+  },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      replyText: null,
+      mounted: false,
+      entityLoading: true,
+      packageTitle: '',
+      authenticatedUser: new User(),
+      api: APIGateway.ticket.APIAdresses.base,
+      ticketCategoryList: new TicketCategoryList(),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'AdminPanel.Ticket.Show',
+      indexRouteName: 'AdminPanel.Ticket.List',
+      inputs: [
+        {
+          type: 'select',
+          name: 'category',
+          responseKey: 'category',
+          options: [],
+          label: 'دسته',
+          col: 'col-md-6 col-12'
+        },
+        {
+          type: 'select',
+          name: 'state',
+          responseKey: 'state',
+          options: (new Ticket()).statusEnums,
+          multiple: false,
+          label: 'وضعیت',
+          col: 'col-md-6 col-12'
+        },
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', col: 'col-12' },
+        { type: 'inputEditor', name: 'body', responseKey: 'body', label: 'متن', col: 'col-12' },
+        { type: 'hidden', name: 'id', responseKey: 'id' },
+        { type: 'hidden', name: 'owner', responseKey: 'owner' },
+        { type: 'hidden', name: 'replies_info', responseKey: 'replies_info' }
+      ]
+    }
+  },
+  computed: {
+    repliesInfo () {
+      return this.inputs.find(input => input.name === 'replies_info').value
+    }
+  },
+  created() {
+    this.api = this.api + '/' + this.$route.params.id
+  },
+  mounted() {
+    this.loadOptions()
+      .then(() => {
+        this.mounted = true
+        this.loadAuthData()
+        this.$nextTick(() => {
+          this.setInputOptions('category', this.ticketCategoryList.list.map(item => {
+            return {
+              value: item.id,
+              label: item.title
+            }
+          }))
+        })
+      })
+      .catch(() => {
+        this.mounted = true
+      })
+  },
+  methods: {
+    loadAuthData () {
+      this.authenticatedUser = this.$store.getters['Auth/user']
+    },
+    afterLoadInputData () {
+      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
+        })
+    },
+    loadOptions () {
+      return this.loadCategories()
+    },
+    loadCategories () {
+      return new Promise((resolve, reject) => {
+        APIGateway.ticketCategory.index()
+          .then(({ list }) => {
+            this.ticketCategoryList = list
+            resolve()
+          })
+          .catch(() => {
+            reject()
+          })
+      })
+    },
+    setInputOptions (name, options) {
+      this.$refs.entityEdit.setInputAttributeByName(name, 'options', options)
+    },
+    sendReply() {
+      this.entityLoading = true
+      APIGateway.ticket.reply({
+        id: this.$route.params.id,
+        body: this.replyText
+      })
+        .then(() => {
+          this.$refs.entityEdit.editEntity()
+            .then(() => {
+              this.$refs.entityEdit.getData()
+              this.entityLoading = false
+            })
+            .catch(() => {
+              this.$refs.entityEdit.getData()
+              this.entityLoading = false
+            })
+        })
+        .catch(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminTicketShow {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    display: flex;
+    flex-flow: row;
+    position: relative;
+    .static-title {
+
+    }
+    .dynamic-title {
+
+    }
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Ticket/AdminTicketShow/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/Ticket/LastTickets/LastTickets.vue

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

+ 21 - 0
src/components/Widgets/Ticket/LastTickets/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>

+ 65 - 0
src/components/Widgets/Ticket/LastTickets/TicketItem.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="TicketItem">
+    <div class="bullet"
+         :style="{background: ticket.status_info.color}" />
+    <div class="title ellipsis">
+      {{ ticket.title }}
+    </div>
+    <div class="status ellipsis"
+         :style="{color: ticket.status_info.color}">
+      {{ ticket.status_info.label }}
+    </div>
+  </div>
+</template>
+
+<script>
+import { Ticket } from 'src/models/Ticket.js'
+
+export default {
+  name: 'TicketItem',
+  props: {
+    ticket: {
+      type: Ticket,
+      default: new Ticket()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.TicketItem {
+  border-bottom: solid 1px #EDEDED;
+  padding: 16px 0;
+  display: flex;
+  flex-flow: row;
+  justify-content: flex-start;
+  align-items: center;
+  .bullet {
+    position: relative;
+    width: 10px;
+    height: 10px;
+    margin-right: 8px;
+    border-radius: 100%;
+    &:before {
+      position: absolute;
+      content: ' ';
+      left: 0;
+      top: 0;
+      width: 10px;
+      height: 10px;
+      //background: $primary;
+      border-radius: 100%;
+    }
+  }
+  .title {
+    width: calc( 100% - 118px);
+    color: #747474;
+    font-size: 14px;
+    font-weight: 500;
+    line-height: 140%;
+  }
+  .status {
+    width: 100px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Ticket/TicketCreate/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>

+ 128 - 0
src/components/Widgets/Ticket/TicketCreate/TicketCreate.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="TicketCreate"
+       :style="localOptions.style">
+    <div class="title">
+      ارسال پیام
+      <div class="back-action">
+        <q-btn flat
+               :to="{name: 'UserPanel.Ticket.List'}"
+               color="grey">
+          بازگشت
+          >
+        </q-btn>
+      </div>
+    </div>
+    <q-card class="form"
+            flat>
+      <entity-create v-if="mounted"
+                     ref="entityCreate"
+                     v-model:value="inputs"
+                     title="نقش جدید"
+                     :api="api"
+                     :default-layout="false"
+                     :entity-id-key-in-response="entityIdKeyInResponse"
+                     :show-route-param-key="showRouteParamKey"
+                     :index-route-name="indexRouteName"
+                     :show-route-name="showRouteName" />
+      <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="create">
+              ارسال
+            </q-btn>
+          </div>
+        </div>
+      </div>
+    </q-card>
+  </div>
+</template>
+
+<script>
+import { EntityCreate } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'TicketCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.ticket.APIAdresses.base,
+      entityIdKeyInResponse: 'id',
+      showRouteParamKey: 'id',
+      showRouteName: 'AdminPanel.Ticket.Show',
+      indexRouteName: 'AdminPanel.Ticket.List',
+      inputs: [
+        { type: 'select', name: 'category', responseKey: 'category', options: [], label: 'دسته', col: 'col-md-12 col-12' },
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', col: 'col-md-12 col-12' },
+        { type: 'inputEditor', name: 'body', responseKey: 'body', label: 'متن', col: 'col-md-12 col-12' },
+        { type: 'hidden', name: 'owner', responseKey: 'owner', value: 1, label: 'owner', col: 'col-md-12 col-12' }
+      ]
+    }
+  },
+  mounted() {
+    this.loadOptions()
+    this.$nextTick(() => {
+      this.mounted = true
+    })
+  },
+  methods: {
+    create() {
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity()
+        .then(() => {
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    },
+    loadOptions () {
+      this.loadCategories()
+    },
+    loadCategories () {
+      APIGateway.ticketCategory.index()
+        .then(({ list }) => {
+          const ticketCategoryList = list
+          this.setInputOptions('category', ticketCategoryList.list.map(item => {
+            return {
+              value: item.id,
+              label: item.title
+            }
+          }))
+        })
+    },
+    setInputOptions (name, options) {
+      this.$refs.entityCreate.setInputAttributeByName(name, 'options', options)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.TicketCreate {
+  .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/Ticket/TicketList/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>

+ 136 - 0
src/components/Widgets/Ticket/TicketList/TicketList.vue

@@ -0,0 +1,136 @@
+<template>
+  <div class="TicketList"
+       :style="localOptions.style">
+    <div class="title">
+      پیام‌های دریافتی
+      <div class="back-action">
+        <q-btn flat
+               :to="{name: 'UserPanel.Ticket.Create'}"
+               color="grey">
+          ارسال پیام جدید
+        </q-btn>
+      </div>
+    </div>
+    <q-card class="list"
+            flat>
+      <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"
+                    :default-layout="false">
+        <template #entity-index-table-cell="{inputData}">
+          <template v-if="inputData.col.name === 'action'">
+            <q-btn flat
+                   color="primary"
+                   :to="{name: 'UserPanel.Ticket.Show', params: {id: inputData.props.row.id}}">
+              مشاهده جزییات
+            </q-btn>
+          </template>
+          <template v-else>
+            {{ inputData.col.value }}
+          </template>
+        </template>
+      </entity-index>
+    </q-card>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { Ticket } from 'src/models/Ticket.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'TicketList',
+  components: { EntityIndex },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      api: APIGateway.ticket.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.firstname + ' ' + row.creator_info.lastname
+          },
+          {
+            name: 'status',
+            required: true,
+            label: 'وضعیت',
+            align: 'left',
+            field: row => (new Ticket(row)).status_info.label
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.mounted = true
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.TicketList {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.list) {
+    .quasar-crud-index-table {
+      padding: 0;
+      .q-table__container {
+        background-color: transparent;
+        box-shadow: none;
+        border: none;
+        .q-table__top {
+          display: none;
+        }
+      }
+    }
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Ticket/TicketShow/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>

+ 273 - 0
src/components/Widgets/Ticket/TicketShow/TicketShow.vue

@@ -0,0 +1,273 @@
+<template>
+  <div class="TicketShow"
+       :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>
+          {{ packageTitle }}
+        </template>
+      </div>
+      <div class="back-action">
+        <q-btn flat
+               :to="{name: 'UserPanel.Ticket.List'}"
+               color="grey">
+          بازگشت
+          >
+        </q-btn>
+      </div>
+    </div>
+    <q-card class="form"
+            flat>
+      <entity-show v-if="mounted"
+                   ref="entityShow"
+                   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" />
+
+      <q-card v-if="mounted"
+              class="q-mt-md">
+        <q-card-section>
+          <div class="row justify-center">
+            <div style="width: 100%;">
+              <q-chat-message v-for="message in repliesInfo"
+                              :key="message.id"
+                              :avatar="message.creator_info.picture ? message.creator_info.picture : 'assets/images/web/sample-avatar.svg'"
+                              :name="message.creator_info.firstname + ' ' + message.creator_info.lastname"
+                              :text="[message.body]"
+                              :sent="authenticatedUser.id !== message.creator_info.id" />
+              <!--                  <div>-->
+              <!--                    <q-input v-model="replyBody"-->
+              <!--                             bottom-slots-->
+              <!--                             :loading="replyBodyLoading">-->
+              <!--                      <template v-slot:after>-->
+              <!--                        <q-btn round-->
+              <!--                               dense-->
+              <!--                               flat-->
+              <!--                               icon="send"-->
+              <!--                               :loading="replyBodyLoading"-->
+              <!--                               @click="sendReplyBody" />-->
+              <!--                      </template>-->
+              <!--                    </q-input>-->
+              <!--                  </div>-->
+            </div>
+          </div>
+        </q-card-section>
+      </q-card>
+
+      <div class="action">
+        <div class="row q-mt-lg justify-end">
+          <div class="col-md-12 col-12">
+            متن پیام
+          </div>
+          <div class="col-md-12 col-12 q-mb-md">
+            <q-input v-model="replyText"
+                     type="textarea" />
+          </div>
+          <div class="col-md-4 col-12">
+            <q-btn color="primary"
+                   class="full-width"
+                   :loading="entityLoading"
+                   @click="sendReply">
+              ارسال پاسخ
+            </q-btn>
+          </div>
+        </div>
+      </div>
+    </q-card>
+  </div>
+</template>
+
+<script>
+import { EntityShow } from 'quasar-crud'
+import { User } from 'src/models/User.js'
+import { Ticket } from 'src/models/Ticket.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { TicketCategoryList } from 'src/models/TicketCategory.js'
+
+export default {
+  name: 'TicketShow',
+  components: {
+    EntityShow
+  },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      replyText: null,
+      mounted: false,
+      entityLoading: true,
+      packageTitle: '',
+      authenticatedUser: new User(),
+      api: APIGateway.ticket.APIAdresses.base,
+      ticketCategoryList: new TicketCategoryList(),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'AdminPanel.Ticket.Show',
+      indexRouteName: 'AdminPanel.Ticket.List',
+      inputs: [
+        {
+          type: 'select',
+          name: 'category',
+          responseKey: 'category',
+          placeholder: ' ',
+          options: [],
+          label: 'دسته',
+          col: 'col-md-6 col-12'
+        },
+        {
+          type: 'select',
+          name: 'status',
+          responseKey: 'status',
+          options: (new Ticket()).statusEnums,
+          multiple: false,
+          label: 'وضعیت',
+          placeholder: ' ',
+          col: 'col-md-6 col-12'
+        },
+        {
+          type: 'input',
+          name: 'title',
+          responseKey: 'title',
+          label: 'عنوان',
+          placeholder: ' ',
+          col: 'col-md-12 col-12'
+        },
+        {
+          type: 'inputEditor',
+          name: 'body',
+          responseKey: 'body',
+          label: 'متن',
+          placeholder: ' ',
+          col: 'col-md-12 col-12'
+        },
+        { type: 'hidden', name: 'id', responseKey: 'id', label: 'شناسه', col: 'col-md-12' },
+        { type: 'hidden', name: 'owner', responseKey: 'owner', label: 'owner', col: 'col-md-12' },
+        { type: 'hidden', name: 'replies_info', responseKey: 'replies_info', label: 'replies_info', col: 'col-md-12' }
+      ]
+    }
+  },
+  computed: {
+    repliesInfo () {
+      return this.inputs.find(input => input.name === 'replies_info').value
+    }
+  },
+  created() {
+    this.api = this.api + '/' + this.$route.params.id
+  },
+  mounted() {
+    this.loadOptions()
+      .then(() => {
+        this.mounted = true
+        this.loadAuthData()
+        this.$nextTick(() => {
+          this.setInputOptions('category', this.ticketCategoryList.list.map(item => {
+            return {
+              value: item.id,
+              label: item.title
+            }
+          }))
+        })
+      })
+      .catch(() => {
+        this.mounted = true
+      })
+  },
+  methods: {
+    loadAuthData () {
+      this.authenticatedUser = this.$store.getters['Auth/user']
+    },
+    afterLoadInputData () {
+      this.entityLoading = false
+    },
+    sendReply() {
+      this.entityLoading = true
+      APIGateway.ticket.reply({
+        id: this.$route.params.id,
+        body: this.replyText
+      })
+        .then(() => {
+          this.$refs.entityShow.editEntity()
+            .then(() => {
+              this.$refs.entityShow.getData()
+              this.entityLoading = false
+            })
+            .catch(() => {
+              this.$refs.entityShow.getData()
+              this.entityLoading = false
+            })
+        })
+        .catch(() => {
+          this.$refs.entityShow.getData()
+          this.entityLoading = false
+        })
+    },
+    loadOptions () {
+      return this.loadCategories()
+    },
+    loadCategories () {
+      return new Promise((resolve, reject) => {
+        APIGateway.ticketCategory.index()
+          .then(({ list }) => {
+            this.ticketCategoryList = list
+            resolve()
+          })
+          .catch(() => {
+            reject()
+          })
+      })
+    },
+    setInputOptions (name, options) {
+      this.$refs.entityCreate.setInputAttributeByName(name, 'options', options)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.TicketShow {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    display: flex;
+    flex-flow: row;
+    position: relative;
+    .static-title {
+
+    }
+    .dynamic-title {
+
+    }
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 108 - 0
src/models/Ticket.js

@@ -0,0 +1,108 @@
+import { User } from 'src/models/User.js'
+import { Collection, Model } from 'js-abstract-model'
+import { TicketReplyList } from 'src/models/TicketReply.js'
+import { TicketCategory } from 'src/models/TicketCategory.js'
+
+class Ticket extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'title' },
+      { key: 'body' }, //
+      { key: 'priority' }, // HIGH - ...
+      { key: 'status' }, // ADMIN_REPLIED - ...
+      { key: 'category' },
+      {
+        key: 'category_info',
+        relatedModel: TicketCategory
+      },
+      {
+        key: 'replies_info',
+        relatedModel: TicketReplyList
+      },
+      { key: 'creator' },
+      {
+        key: 'creator_info',
+        relatedModel: User
+      },
+      { key: 'owner' },
+      {
+        key: 'owner_info',
+        relatedModel: User
+      }
+    ])
+
+    this.priorityEnums = [
+      {
+        label: 'بالا',
+        value: 'HIGH'
+      }
+    ]
+
+    this.statusEnums = [
+      {
+        label: 'باز شده',
+        value: 'OPENED',
+        color: '#2FA84A'
+      },
+      {
+        label: 'پاسخ داده شده',
+        value: 'ADMIN_REPLIED',
+        color: '#0B6AB1'
+      },
+      {
+        label: 'در حال انتظار',
+        value: 'PENDING',
+        color: '#2FA84A'
+      },
+      {
+        label: 'خوانده نشده',
+        value: 'UNREAD',
+        color: '#2FA84A'
+      },
+      {
+        label: 'خوانده شده',
+        value: 'READ',
+        color: '#2FA84A'
+      },
+      {
+        label: 'بسته شده',
+        value: 'CLOSED',
+        color: '#989898'
+      }
+    ]
+
+    this.loadStatusInfo()
+    this.loadPriorityInfo()
+  }
+
+  loadPriorityInfo () {
+    const target = this.priorityEnums.find(type => type.value === this.priority)
+    if (!target) {
+      this.priority_info = null
+    } else {
+      this.priority_info = target
+    }
+  }
+
+  loadStatusInfo () {
+    const target = this.statusEnums.find(type => type.value === this.status)
+    if (!target) {
+      this.status_info = {
+        label: null,
+        value: null,
+        color: ''
+      }
+    } else {
+      this.status_info = target
+    }
+  }
+}
+
+class TicketList extends Collection {
+  model () {
+    return Ticket
+  }
+}
+
+export { Ticket, TicketList }

+ 18 - 0
src/models/TicketCategory.js

@@ -0,0 +1,18 @@
+import { Collection, Model } from 'js-abstract-model'
+
+class TicketCategory extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'name' }
+    ])
+  }
+}
+
+class TicketCategoryList extends Collection {
+  model () {
+    return TicketCategory
+  }
+}
+
+export { TicketCategory, TicketCategoryList }

+ 30 - 0
src/models/TicketReply.js

@@ -0,0 +1,30 @@
+import { User } from 'src/models/User.js'
+import { Collection, Model } from 'js-abstract-model'
+
+class TicketReply extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'body' },
+      { key: 'ticket' },
+      { key: 'read' },
+      { key: 'creation_time' },
+      { key: 'last_modification_time' },
+      { key: 'is_active' },
+      { key: 'is_deleted' },
+      { key: 'creator' },
+      {
+        key: 'creator_info',
+        relatedModel: User
+      }
+    ])
+  }
+}
+
+class TicketReplyList extends Collection {
+  model () {
+    return TicketReply
+  }
+}
+
+export { TicketReply, TicketReplyList }

+ 24 - 0
src/pages/User/Ticket/Create.vue

@@ -0,0 +1,24 @@
+<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: 'TicketCreate' }], options: { style: { marginTop: '124px' } } }], options: {} }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+    this.pageBuilderLoading = false
+  }
+}
+</script>

+ 24 - 0
src/pages/User/Ticket/List.vue

@@ -0,0 +1,24 @@
+<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: 'TicketList' }], options: { style: { paddingTop: '124px' } } }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '30px', style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+    this.pageBuilderLoading = false
+  }
+}
+</script>

+ 24 - 0
src/pages/User/Ticket/Show.vue

@@ -0,0 +1,24 @@
+<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: 'TicketShow' }], options: { style: { paddingTop: '124px' } } }], options: {} }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+    this.pageBuilderLoading = false
+  }
+}
+</script>

+ 22 - 0
src/router/routes.js

@@ -165,6 +165,28 @@ const routes = [
                 component: () => import('src/pages/User/Invoice/Show.vue')
               }
             ]
+          },
+          {
+            path: 'ticket',
+            name: 'UserPanel.Ticket',
+            component: () => import('layouts/bareLayout.vue'),
+            children: [
+              {
+                path: '',
+                name: 'UserPanel.Ticket.List',
+                component: () => import('src/pages/User/Ticket/List.vue')
+              },
+              {
+                path: ':id',
+                name: 'UserPanel.Ticket.Show',
+                component: () => import('src/pages/User/Ticket/Show.vue')
+              },
+              {
+                path: 'create',
+                name: 'UserPanel.Ticket.Create',
+                component: () => import('src/pages/User/Ticket/Create.vue')
+              }
+            ]
           }
         ]
       },