Browse Source

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

# Conflicts:
#	package.json
#	src/components/ClassroomItem.vue
#	src/components/EventItem.vue
#	yarn.lock
hgh 2 years ago
parent
commit
94f8bce2ec
100 changed files with 8096 additions and 1913 deletions
  1. 17 0
      docker-compose-production.yml
  2. 6 15
      package.json
  3. BIN
      public/assets/images/web/today-hadith-thumbnail.png
  4. 6 1
      quasar.config.js
  5. 1 3
      src/App.vue
  6. 30 0
      src/api/APIGateway.js
  7. 207 0
      src/api/models/Basket.js
  8. 224 0
      src/api/models/BasketItem.js
  9. 66 0
      src/api/models/Classroom.js
  10. 16 1
      src/api/models/ClassroomGraders.js
  11. 16 1
      src/api/models/ClassroomLeaders.js
  12. 14 0
      src/api/models/Content.js
  13. 64 0
      src/api/models/ContentTopic.js
  14. 57 0
      src/api/models/ContentTopicCategory.js
  15. 101 0
      src/api/models/Discount.js
  16. 71 0
      src/api/models/Donation.js
  17. 71 0
      src/api/models/DonationCategory.js
  18. 93 0
      src/api/models/Hadith.js
  19. 88 0
      src/api/models/Hashtag.js
  20. 20 1
      src/api/models/Invoice.js
  21. 1 0
      src/api/models/MediaHashtag.js
  22. 84 0
      src/api/models/Notice.js
  23. 85 0
      src/api/models/NoticeReceiver.js
  24. 100 0
      src/api/models/Package.js
  25. 20 1
      src/api/models/Payment.js
  26. 101 0
      src/api/models/Product.js
  27. 1 0
      src/api/models/Project.js
  28. 2 1
      src/api/models/Session.js
  29. 57 0
      src/api/models/StoreCategory.js
  30. 3 3
      src/api/models/TicketCategory.js
  31. 78 0
      src/api/models/TranscriptSheet.js
  32. 3 1
      src/api/models/User.js
  33. 2 0
      src/assets/js/jalali-moment.browser.js
  34. 10 4
      src/components/Auth/ForgetStep.vue
  35. 18 9
      src/components/Auth/LoginStep.vue
  36. 54 20
      src/components/Auth/RegisterStep.vue
  37. 49 47
      src/components/ClassroomItem.vue
  38. 0 297
      src/components/ContentVideoPlayer.vue
  39. 6 1
      src/components/EntityIndexGridItem.vue
  40. 54 51
      src/components/EventItem.vue
  41. 105 50
      src/components/FormBuilderCustumComponents/ContentCategorySelector.vue
  42. 11 8
      src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue
  43. 293 0
      src/components/FormBuilderCustumComponents/FormBuilderDateTime.vue
  44. 6 6
      src/components/FormBuilderCustumComponents/FormBuilderInputEditor.vue
  45. 85 0
      src/components/FormBuilderCustumComponents/Hashtags/HashtagItem.vue
  46. 150 0
      src/components/FormBuilderCustumComponents/Hashtags/HashtagSelector.vue
  47. 147 0
      src/components/FormBuilderCustumComponents/ProductSelector.vue
  48. 13 2
      src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/CreateQuestionForm.vue
  49. 5 1
      src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/QuestionsCreateAndAppend.vue
  50. 6 1
      src/components/FormBuilderCustumComponents/QuestionsSelector/QuestionsSelector.vue
  51. 109 0
      src/components/FormBuilderCustumComponents/TicketCategorySelector.vue
  52. 1 1
      src/components/PostItem.vue
  53. 120 28
      src/components/Template/Header/MainHeader.vue
  54. 57 13
      src/components/Template/Header/UserPanelHeader.vue
  55. 95 30
      src/components/Template/SideBar/AdminPanelSideBar.vue
  56. 52 27
      src/components/Template/SideBar/Main.vue
  57. 3 3
      src/components/Template/SideBar/TemplateSideBar.vue
  58. 6 0
      src/components/Template/SideBar/UserPanelSideBar.vue
  59. 0 829
      src/components/VideoPlayer.vue
  60. 16 14
      src/components/Widgets/Classroom/AdminClassroomCreate/AdminClassroomCreate.vue
  61. 967 0
      src/components/Widgets/Classroom/AdminClassroomInfoOfUser/AdminClassroomInfoOfUser.vue
  62. 518 0
      src/components/Widgets/Classroom/AdminClassroomInfoOfUser/components/ShowClassroomInfo.vue
  63. 10 6
      src/components/Widgets/Classroom/AdminClassroomList/AdminClassroomList.vue
  64. 29 4
      src/components/Widgets/Classroom/AdminClassroomShow/AdminClassroomShow.vue
  65. 91 36
      src/components/Widgets/Classroom/AdminClassroomShow/components/ActivitySheetList.vue
  66. 418 0
      src/components/Widgets/Classroom/AdminClassroomShow/components/AnnounceResultList.vue
  67. 15 13
      src/components/Widgets/Classroom/AdminClassroomShow/components/ClassroomInfo.vue
  68. 52 7
      src/components/Widgets/Classroom/AdminClassroomShow/components/GraderList.vue
  69. 53 8
      src/components/Widgets/Classroom/AdminClassroomShow/components/LeaderList.vue
  70. 7 0
      src/components/Widgets/Classroom/AdminClassroomShow/components/MembersList.vue
  71. 1 1
      src/components/Widgets/Classroom/ClassroomAnswerBookList/ClassroomAnswerBookList.vue
  72. 46 32
      src/components/Widgets/Classroom/ClassroomList/ClassroomList.vue
  73. 12 3
      src/components/Widgets/Classroom/ClassroomShow/ClassroomShow.vue
  74. 171 0
      src/components/Widgets/Content/AdminContentCreate-new/AdminContentCreate.vue
  75. 21 0
      src/components/Widgets/Content/AdminContentCreate-new/OptionPanel.vue
  76. 148 0
      src/components/Widgets/Content/AdminContentList-new/AdminContentList.vue
  77. 21 0
      src/components/Widgets/Content/AdminContentList-new/OptionPanel.vue
  78. 40 8
      src/components/Widgets/Content/AdminContentList/AdminContentList.vue
  79. 180 0
      src/components/Widgets/Content/AdminContentShow-new/AdminContentShow.vue
  80. 21 0
      src/components/Widgets/Content/AdminContentShow-new/OptionPanel.vue
  81. 62 34
      src/components/Widgets/Content/AdminContentShow/AdminContentShow.vue
  82. 110 0
      src/components/Widgets/Content/AdminContentTopicCreate/AdminContentTopicCreate.vue
  83. 21 0
      src/components/Widgets/Content/AdminContentTopicCreate/OptionPanel.vue
  84. 142 0
      src/components/Widgets/Content/AdminContentTopicList/AdminContentTopicList.vue
  85. 21 0
      src/components/Widgets/Content/AdminContentTopicList/OptionPanel.vue
  86. 112 0
      src/components/Widgets/Content/AdminContentTopicShow/AdminContentTopicShow.vue
  87. 21 0
      src/components/Widgets/Content/AdminContentTopicShow/OptionPanel.vue
  88. 116 53
      src/components/Widgets/Content/ContentList/ContentList.vue
  89. 67 2
      src/components/Widgets/Content/ContentList/OptionPanel.vue
  90. 71 0
      src/components/Widgets/Content/ContentMixin.js
  91. 397 234
      src/components/Widgets/Content/ContentShow/ContentShow.vue
  92. 10 2
      src/components/Widgets/Content/ContentShow/OptionPanel.vue
  93. 71 0
      src/components/Widgets/Content/ContentShow/components/MediaCategory.vue
  94. 77 0
      src/components/Widgets/Content/ContentShow/components/MediaItem.vue
  95. 91 0
      src/components/Widgets/Content/ContentShow/components/Medias.vue
  96. 133 0
      src/components/Widgets/Content/ContentShow/components/productItem.vue
  97. 206 0
      src/components/Widgets/Content/ContentTopicList/ContentTopicList.vue
  98. 21 0
      src/components/Widgets/Content/ContentTopicList/OptionPanel.vue
  99. 156 0
      src/components/Widgets/Content/ContentTopicList/components/TopicItem.vue
  100. 194 0
      src/components/Widgets/Content/ContentTopicList/components/packageItem.vue

+ 17 - 0
docker-compose-production.yml

@@ -0,0 +1,17 @@
+version: "3.3"
+services:
+  nextjs:
+    build: ./
+    network_mode: "host"
+    restart: "always"
+#  nginx:
+#    build: ./nginx
+#    network_mode: "host"
+#    volumes:
+#      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
+#    depends_on:
+#      - nextjs
+#    ports:
+#      - "8081:80"
+#    ports:
+#      - "${NGINX_PORT}:80"

+ 6 - 15
package.json

@@ -1,8 +1,8 @@
 {
-  "name": "alaatv",
+  "name": "palizvani",
   "version": "0.0.1",
-  "description": "alaatv with vite",
-  "productName": "alaatv",
+  "description": "palizvani with vite",
+  "productName": "palizvani",
   "author": "Ali <aliesmaeeli20@gmail.com>",
   "private": true,
   "scripts": {
@@ -20,12 +20,11 @@
     "start:ssr": "yarn ci && yarn build:ssr && cd dist/ssr && yarn install && yarn start"
   },
   "dependencies": {
-    "@capacitor/cli": "^5.0.4",
-    "@capacitor/core": "^5.0.4",
     "@gtm-support/vue-gtm": "^2.0.0",
     "@nodeguy/server-date": "^4.0.3",
     "@quasar/app-vite": "^1.7.1",
     "@quasar/extras": "^1.16.9",
+    "@splidejs/splide": "^4.1.4",
     "autoprefixer": "^10.4.14",
     "axios": "^1.6.2",
     "cropperjs": "^1.5.13",
@@ -48,21 +47,13 @@
     "process": "^0.11.10",
     "quasar": "^2.14.2",
     "quasar-app-extension-q-page-builder": "^0.4.4",
-    "quasar-crud": "^1.0.9",
-    "quasar-form-builder": "^1.0.6",
+    "quasar-crud": "^2.2.4",
+    "quasar-form-builder": "^3.2.0",
     "quasar-template-builder": "^1.0.23",
     "quasar-ui-q-page-builder": "^0.4.4",
     "socket.io-client": "^4.6.1",
-    "sticky-sidebar-v2": "^1.1.1",
-    "video.js": "^8.3.0",
-    "videojs-brand": "^0.0.4",
-    "videojs-contrib-quality-levels": "^3.0.0",
-    "videojs-hls-quality-selector": "^1.1.4",
-    "vue-advanced-cropper": "^2.8.8",
-    "vue-dragscroll": "^4.0.5",
     "vue-router": "^4.1.6",
     "vue-tiptap-katex-core": "^2.1.5",
-    "vue3-carousel": "0.3.1",
     "vue3-otp-input": "^0.4.1",
     "vue3-tiptap-katex": "^1.1.27",
     "vuex": "^4.1.0",

BIN
public/assets/images/web/today-hadith-thumbnail.png


+ 6 - 1
quasar.config.js

@@ -84,6 +84,9 @@ module.exports = configure(function (ctx) {
       transpileDependencies: [
         'js-abstract-model',
         'quasar-template-builder',
+        'quasar-app-extension-q-page-builder',
+        'quasar-crud',
+        'quasar-form-builder',
         'quasar-ui-q-page-builder'
       ],
 
@@ -270,7 +273,9 @@ module.exports = configure(function (ctx) {
       // you can manually specify Quasar components/directives to be available everywhere:
       //
       // components: [],
-      // directives: [],
+      directives: [
+        'ClosePopup'
+      ],
 
       // Quasar plugins
       plugins: [

+ 1 - 3
src/App.vue

@@ -1,7 +1,5 @@
 <template>
-  <div id="q-app">
-    <router-view :key="$route.path" />
-  </div>
+  <router-view :key="$route.path" />
 </template>
 <script>
 import process from 'process'

+ 30 - 0
src/api/APIGateway.js

@@ -7,18 +7,27 @@ 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 BasketAPI from 'src/api/models/Basket.js'
+import HadithAPI from 'src/api/models/Hadith.js'
+import HashtagAPI from 'src/api/models/Hashtag.js'
 import TestSetAPI from 'src/api/models/TestSet.js'
 import InvoiceAPI from 'src/api/models/Invoice.js'
 import PaymentAPI from 'src/api/models/Payment.js'
 import SessionAPI from 'src/api/models/Session.js'
 import ContentAPI from 'src/api/models/Content.js'
 import ProjectAPI from 'src/api/models/Project.js'
+import PackageAPI from 'src/api/models/Package.js'
+import ProductAPI from 'src/api/models/Product.js'
+import DonationAPI from 'src/api/models/Donation.js'
 import QuestionApi from 'src/api/models/Question.js'
+import DiscountAPI from 'src/api/models/Discount.js'
 import ClassroomAPI from 'src/api/models/Classroom.js'
 import AnswerBookApi from 'src/api/models/AnswerBook.js'
 import InstalmentAPI from 'src/api/models/Instalment.js'
+import BasketItemAPI from 'src/api/models/BasketItem.js'
 import AnswerSheetApi from 'src/api/models/AnswerSheet.js'
 import TransactionAPI from 'src/api/models/Transaction.js'
 import PageSettingAPI from 'src/api/models/PageSetting.js'
@@ -26,16 +35,22 @@ import PostCategoryAPI from 'src/api/models/PostCategory.js'
 import MediaHashtagAPI from 'src/api/models/MediaHashtag.js'
 import RegistrationAPI from 'src/api/models/Registration.js'
 import UnitCategoryAPI from 'src/api/models/UnitCategory.js'
+import ContentTopicAPI from 'src/api/models/ContentTopic.js'
+import StoreCategoryAPI from 'src/api/models/StoreCategory.js'
 import MediaCategoryAPI from 'src/api/models/MediaCategory.js'
+import NoticeReceiverAPI from 'src/api/models/NoticeReceiver.js'
 import QuestionChoiceApi from 'src/api/models/QuestionChoice.js'
 import TicketCategoryAPI from 'src/api/models/TicketCategory.js'
 import SessionTemplateAPI from 'src/api/models/SessionTemplate.js'
+import TranscriptSheetAPI from 'src/api/models/TranscriptSheet.js'
 import ContentCategoryAPI from 'src/api/models/ContentCategory.js'
 import InstalmentOfferAPI from 'src/api/models/InstalmentOffer.js'
 import ClassroomGraderAPI from 'src/api/models/ClassroomGraders.js'
 import ClassroomLeadersAPI from 'src/api/models/ClassroomLeaders.js'
+import DonationCategoryApi from 'src/api/models/DonationCategory.js'
 import TeamRegistrationsAPI from 'src/api/models/TeamRegistrations.js'
 import EventRegistrationApi from 'src/api/models/EventRegistration.js'
+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'
@@ -51,35 +66,50 @@ export const APIGateway = {
   test: new TestAPI(),
   event: new EventAPI(),
   media: new MediaAPI(),
+  notice: new NoticeAPI(),
   ticket: new TicketAPI(),
   wallet: new WalletAPI(),
+  basket: new BasketAPI(),
+  hadith: new HadithAPI(),
+  hashtag: new HashtagAPI(),
   testSet: new TestSetAPI(),
   project: new ProjectAPI(),
+  package: new PackageAPI(),
+  product: new ProductAPI(),
   invoice: new InvoiceAPI(),
   payment: new PaymentAPI(),
   session: new SessionAPI(),
   content: new ContentAPI(),
+  donation: new DonationAPI(),
   question: new QuestionApi(),
+  discount: new DiscountAPI(),
   classroom: new ClassroomAPI(),
   answerBook: new AnswerBookApi(),
   instalment: new InstalmentAPI(),
+  basketItem: new BasketItemAPI(),
   answerSheet: new AnswerSheetApi(),
   transaction: new TransactionAPI(),
   pageSetting: new PageSettingAPI(),
   registration: new RegistrationAPI(),
   postCategory: new PostCategoryAPI(),
   unitCategory: new UnitCategoryAPI(),
+  contentTopic: new ContentTopicAPI(),
   mediaHashtag: new MediaHashtagAPI(),
+  storeCategory: new StoreCategoryAPI(),
   mediaCategory: new MediaCategoryAPI(),
+  noticeReceiver: new NoticeReceiverAPI(),
   ticketCategory: new TicketCategoryAPI(),
   questionChoice: new QuestionChoiceApi(),
+  transcriptSheet: new TranscriptSheetAPI(),
   sessionTemplate: new SessionTemplateAPI(),
   contentCategory: new ContentCategoryAPI(),
   instalmentOffer: new InstalmentOfferAPI(),
   classroomGrader: new ClassroomGraderAPI(),
   classroomLeaders: new ClassroomLeadersAPI(),
+  donationCategory: new DonationCategoryApi(),
   teamRegistrations: new TeamRegistrationsAPI(),
   eventRegistration: new EventRegistrationApi(),
+  contentTopicCategory: new ContentTopicCategoryAPI(),
   classroomRegistration: new ClassroomRegistrationAPI(),
   projectAttendanceSheets: new ProjectAttendanceSheetsAPI(),
   sessionAttendanceSheets: new SessionAttendanceSheetsAPI()

+ 207 - 0
src/api/models/Basket.js

@@ -0,0 +1,207 @@
+import { appApi } from 'src/boot/axios.js'
+import { Invoice } from 'src/models/Invoice.js'
+import APIRepository from '../classes/APIRepository.js'
+import { Basket, BasketList } from 'src/models/Basket.js'
+
+export default class BasketAPI extends APIRepository {
+  constructor() {
+    super('package', appApi)
+    this.APIAdresses = {
+      base: '/store/baskets',
+      checkoutReview: '/store/baskets/checkout_review',
+      listOrders: '/store/baskets/list_orders',
+      byId: (id) => '/store/baskets/' + id,
+      send: (id) => '/store/baskets/' + id + '/send',
+      createInvoice: (id) => '/store/baskets/' + id + '/create_invoice',
+      submitDiscountCode: (id) => '/store/baskets/' + id + '/submit_discount_code',
+      removeDiscountCode: (id) => '/store/baskets/' + id + '/remove_discount_code'
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new Basket(response.data) },
+      post: (response) => { return new Basket(response.data) },
+      put: (response) => { return new Basket(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new BasketList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  listOrders(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.listOrders,
+      data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new BasketList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new Basket(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  checkoutReview () {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.checkoutReview,
+      resolveCallback: (response) => {
+        return new Basket(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  setAddress (basketId, addressData) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.byId(basketId),
+      data: this.getNormalizedSendData({
+        province: null, // String
+        city: null, // String
+        address: null, // String
+        postal_code: null // String
+      }, addressData),
+      resolveCallback: (response) => {
+        return new Basket(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  submitDiscountCode (basketId, code) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.submitDiscountCode(basketId),
+      data: this.getNormalizedSendData({
+        code: null // String
+      }, { code }),
+      resolveCallback: (response) => {
+        return new Basket(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  removeDiscountCode (basketId) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.removeDiscountCode(basketId),
+      resolveCallback: (response) => {
+        return new Basket(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  send (basketId) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.send(basketId),
+      resolveCallback: (response) => {
+        return new Basket(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  createInvoice (basketId, owner) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.createInvoice(basketId),
+      data: this.getNormalizedSendData({
+        owner: null // String
+      }, { owner }),
+      resolveCallback: (response) => {
+        return new Invoice(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 224 - 0
src/api/models/BasketItem.js

@@ -0,0 +1,224 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { BasketItem, BasketItemList } from 'src/models/BasketItem.js'
+
+export default class BasketItemAPI extends APIRepository {
+  constructor() {
+    super('package', appApi)
+    this.APIAdresses = {
+      base: '/store/items',
+      decrement: '/store/items/decrement',
+      increment: '/store/items/increment',
+      byId: (id) => '/store/items/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new BasketItem(response.data) },
+      post: (response) => { return new BasketItem(response.data) },
+      put: (response) => { return new BasketItem(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new BasketItemList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new BasketItem(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  remove (id) {
+    return this.sendRequest({
+      apiMethod: 'delete',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new BasketItem(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  addProduct (productId, count) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        product: null, // Number
+        count: null // Number
+      }, {
+        product: parseInt(productId),
+        count
+      }),
+      resolveCallback: (response) => {
+        return new BasketItem(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  addPackage (packageId, count) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        package: null, // Number
+        count: null // Number
+      }, {
+        package: packageId,
+        count
+      }),
+      resolveCallback: (response) => {
+        return new BasketItem(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  decrementPackage (packageId) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.decrement,
+      data: this.getNormalizedSendData({
+        package: null // Number
+      }, {
+        package: packageId
+      }),
+      resolveCallback: (response) => {
+        return new BasketItem(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  decrementProduct (productId) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.decrement,
+      data: this.getNormalizedSendData({
+        product: null // Number
+      }, {
+        product: productId
+      }),
+      resolveCallback: (response) => {
+        return new BasketItem(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  incrementPackage (packageId) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.increment,
+      data: this.getNormalizedSendData({
+        package: null // Number
+      }, {
+        package: packageId
+      }),
+      resolveCallback: (response) => {
+        return new BasketItem(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  incrementProduct (productId) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.increment,
+      data: this.getNormalizedSendData({
+        product: null // Number
+      }, {
+        product: productId
+      }),
+      resolveCallback: (response) => {
+        return new BasketItem(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  update (basketItemId, count) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.byId(basketItemId),
+      data: this.getNormalizedSendData({
+        count: null // Number
+      }, {
+        count
+      }),
+      resolveCallback: (response) => {
+        return new BasketItem(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 66 - 0
src/api/models/Classroom.js

@@ -11,13 +11,17 @@ export default class ClassroomAPI extends APIRepository {
       base: '/lma/classrooms',
       myAsGraderClassrooms: '/lma/classrooms/my_as_a_grader_classrooms',
       byId: (id) => '/lma/classrooms/' + id,
+      toggleOngoingStatus: (id) => '/lma/classrooms/' + id + '/toggle_ongoing_status',
       enroll: (id) => '/lma/classrooms/' + id + '/enroll',
       graders: (id) => '/lma/classrooms/' + id + '/graders',
       drop: (id) => '/lma/classrooms/' + id + '/drop',
       register: (id) => '/lma/classrooms/' + id + '/register',
       leaders: (id) => '/lma/classrooms/' + id + '/leaders',
+      announceResult: (id) => '/lma/classrooms/' + id + '/announce_result',
+      getClassroomAndCorrespondingRegistration: (classroomId, ownerId) => '/lma/classrooms/' + classroomId + '/get_classroom_and_corresponding_registration/' + ownerId,
       members: '/lma/registrations',
       activitySheet: '/lma/registrations/activity_sheet',
+      exportActivitySheetReport: '/lma/registrations/export_report',
       enrollByAdmin: (classroomId, userId) => '/lma/classrooms/' + classroomId + '/enrolll?user_id=' + userId,
       dropByAdmin: (classroomId) => '/lma/classrooms/' + classroomId + '/drop',
       createInvoice: (id) => '/lma/classrooms/' + id + '/create_invoice'
@@ -61,6 +65,26 @@ export default class ClassroomAPI extends APIRepository {
     })
   }
 
+  exportActivitySheetReport(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.exportActivitySheetReport,
+      data: this.getNormalizedSendData({
+        classroom: null, // Number
+        type: null, // String
+        status: null // String
+      }, data),
+      responseType: 'blob',
+      resolveCallback: (response) => {
+        return response.data // xlsxData
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
   get (id) {
     return this.sendRequest({
       apiMethod: 'get',
@@ -75,6 +99,20 @@ export default class ClassroomAPI extends APIRepository {
     })
   }
 
+  getClassroomAndCorrespondingRegistration (classroomId, ownerId) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.getClassroomAndCorrespondingRegistration(classroomId, ownerId),
+      resolveCallback: (response) => {
+        return new Classroom(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
   register (id) {
     return this.sendRequest({
       apiMethod: 'put',
@@ -101,6 +139,20 @@ export default class ClassroomAPI extends APIRepository {
     })
   }
 
+  toggleOngoingStatus (id) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.toggleOngoingStatus(id),
+      resolveCallback: (response) => {
+        return new Classroom(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
   enrollByUser (id) {
     return this.sendRequest({
       apiMethod: 'put',
@@ -209,4 +261,18 @@ export default class ClassroomAPI extends APIRepository {
       }
     })
   }
+
+  notifyAnnounceResult (classroomId) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.announceResult(classroomId),
+      resolveCallback: (response) => {
+        return response.data
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
 }

+ 16 - 1
src/api/models/ClassroomGraders.js

@@ -6,11 +6,26 @@ export default class ClassroomGraderAPI extends APIRepository {
     super('classroom_graders', appApi, '/lma/classroom_graders')
     this.APIAdresses = {
       base: '/lma/classroom_graders',
-      byId: (id) => '/lma/classroom_graders/' + id
+      byId: (id) => '/lma/classroom_graders/' + id,
+      withLastPassedUnit: '/lma/classroom_graders/graders_with_last_passed_unit'
     }
     this.CacheList = {
       base: this.name + this.APIAdresses.base,
       byId: (id) => this.name + this.APIAdresses.byId(id)
     }
   }
+
+  deleteById (id) {
+    return this.sendRequest({
+      apiMethod: 'delete',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return response.data
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
 }

+ 16 - 1
src/api/models/ClassroomLeaders.js

@@ -6,11 +6,26 @@ export default class ClassroomLeaderAPI extends APIRepository {
     super('classroom_leaders', appApi, '/lma/classroom_leaders')
     this.APIAdresses = {
       base: '/lma/classroom_leaders',
-      byId: (id) => '/lma/classroom_leaders/' + id
+      byId: (id) => '/lma/classroom_leaders/' + id,
+      withLastPassedUnit: '/lma/classroom_leaders/leaders_with_last_passed_unit'
     }
     this.CacheList = {
       base: this.name + this.APIAdresses.base,
       byId: (id) => this.name + this.APIAdresses.byId(id)
     }
   }
+
+  deleteById (id) {
+    return this.sendRequest({
+      apiMethod: 'delete',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return response.data
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
 }

+ 14 - 0
src/api/models/Content.js

@@ -54,4 +54,18 @@ export default class ContentAPI extends APIRepository {
       }
     })
   }
+
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new Content(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
 }

+ 64 - 0
src/api/models/ContentTopic.js

@@ -0,0 +1,64 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { ContentTopic, ContentTopicList } from 'src/models/ContentTopic.js'
+
+export default class ContentTopicAPI extends APIRepository {
+  constructor() {
+    super('content-topic', appApi)
+    this.APIAdresses = {
+      base: '/cma/topics',
+      byId: (id) => '/cma/topics/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new ContentTopic(response.data) },
+      post: (response) => { return new ContentTopic(response.data) },
+      put: (response) => { return new ContentTopic(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        content: null, // Number
+        creator: null, // Number
+        category: null, // Number
+        tile: null, // String
+        category__parent__parent: null, // Number
+        category__in: null, // Number[]
+        hashtags__id__in: null, // Number[]
+        hashtags__in: null, // Number[]
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new ContentTopicList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 57 - 0
src/api/models/ContentTopicCategory.js

@@ -0,0 +1,57 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { ContentTopicCategory, ContentTopicCategoryList } from 'src/models/ContentTopicCategory.js'
+
+export default class ContentTopicCategoryAPI extends APIRepository {
+  constructor() {
+    super('content-topic-category', appApi)
+    this.APIAdresses = {
+      base: '/cma/topics/categories',
+      byId: (id) => '/cma/topics/categories/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new ContentTopicCategory(response.data) },
+      post: (response) => { return new ContentTopicCategory(response.data) },
+      put: (response) => { return new ContentTopicCategory(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        parent__isnull: null, // String (true, false)
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new ContentTopicCategoryList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 101 - 0
src/api/models/Discount.js

@@ -0,0 +1,101 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { Discount, DiscountList } from 'src/models/Discount.js'
+
+export default class DiscountAPI extends APIRepository {
+  constructor() {
+    super('discounts', appApi)
+    this.APIAdresses = {
+      base: '/store/discounts',
+      byId: (id) => '/store/discounts/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new Discount(response.data) },
+      post: (response) => { return new Discount(response.data) },
+      put: (response) => { return new Discount(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new DiscountList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new Discount(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  add (data) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        store_category: null, // Number
+        content_category: null, // Number
+        inventory: 0, // Number
+        unit_price: 0, // Number
+        discount_percent: 0, // Number
+        maximum_order_count: 0, // Number
+        weight: 0, // Number
+        thumbnail: null, // String
+        title: null, // String
+        description: null, // String
+        physical_type: null, // String
+        specification: null, // String
+        is_physical: false, // Boolean
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        return new Discount(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 71 - 0
src/api/models/Donation.js

@@ -0,0 +1,71 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { Donation, DonationList } from 'src/models/Donation.js'
+
+export default class DonationAPI extends APIRepository {
+  constructor() {
+    super('donation', appApi)
+    this.APIAdresses = {
+      base: '/dma/donations',
+      byId: (id) => '/dma/donations/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new Donation(response.data) },
+      post: (response) => { return new Donation(response.data) },
+      put: (response) => { return new Donation(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new DonationList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new Donation(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 71 - 0
src/api/models/DonationCategory.js

@@ -0,0 +1,71 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { DonationCategory, DonationCategoryList } from 'src/models/DonationCategory.js'
+
+export default class DonationCategoryAPI extends APIRepository {
+  constructor() {
+    super('donation-category', appApi)
+    this.APIAdresses = {
+      base: '/dma/donations/categories',
+      byId: (id) => '/dma/donations/categories/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new DonationCategory(response.data) },
+      post: (response) => { return new DonationCategory(response.data) },
+      put: (response) => { return new DonationCategory(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        parent__isnull: null, // String (true, false)
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new DonationCategoryList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new DonationCategory(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 93 - 0
src/api/models/Hadith.js

@@ -0,0 +1,93 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { Hadith, HadithList } from 'src/models/Hadith.js'
+
+export default class HadithAPI extends APIRepository {
+  constructor() {
+    super('hadithes', appApi)
+    this.APIAdresses = {
+      base: '/cma/hadithes',
+      byId: (id) => '/cma/hadithes/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new Hadith(response.data) },
+      post: (response) => { return new Hadith(response.data) },
+      put: (response) => { return new Hadith(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        status: null, // String
+        per_page: 10, // Number
+        page: 1, // Number
+        ordering: '-publish_date'
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new HadithList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new Hadith(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  add (data) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        persian_text: null, // String
+        arabian_text: null, // String
+        narrator: null, // String
+        source: null, // String
+        publish_date: null // String
+      }, data),
+      resolveCallback: (response) => {
+        return new Hadith(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 88 - 0
src/api/models/Hashtag.js

@@ -0,0 +1,88 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { Hashtag, HashtagList } from 'src/models/Hashtag.js'
+
+export default class HashtagAPI extends APIRepository {
+  constructor() {
+    super('hashtags', appApi)
+    this.APIAdresses = {
+      base: '/cma/hashtags',
+      byId: (id) => '/cma/hashtags/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new Hashtag(response.data) },
+      post: (response) => { return new Hashtag(response.data) },
+      put: (response) => { return new Hashtag(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new HashtagList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new Hashtag(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  create (title) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        title: null // String
+      }, { title }),
+      resolveCallback: (response) => {
+        return new Hashtag(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 20 - 1
src/api/models/Invoice.js

@@ -9,7 +9,8 @@ export default class InvoiceAPI extends APIRepository {
       base: '/accounting/invoices',
       byId: (id) => '/accounting/invoices/' + id,
       pay: (id) => '/accounting/invoices/' + id + '/pay',
-      cancel: (id) => '/accounting/invoices/' + id + '/cancel'
+      cancel: (id) => '/accounting/invoices/' + id + '/cancel',
+      exportInvoiceReport: '/accounting/invoices/export_report'
     }
     this.CacheList = {
       base: this.name + this.APIAdresses.base
@@ -142,4 +143,22 @@ export default class InvoiceAPI extends APIRepository {
       }
     })
   }
+
+  exportInvoiceReport(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.exportInvoiceReport,
+      data: this.getNormalizedSendData({
+        report_type: null // String
+      }, data),
+      responseType: 'blob',
+      resolveCallback: (response) => {
+        return response.data // xlsxData
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
 }

+ 1 - 0
src/api/models/MediaHashtag.js

@@ -27,6 +27,7 @@ export default class MediaHashtagAPI extends APIRepository {
       api: this.api,
       request: this.APIAdresses.base,
       data: this.getNormalizedSendData({
+        search: null, // String
         page: 1 // Number
       }, data),
       resolveCallback: (response) => {

+ 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
+      }
+    })
+  }
+}

+ 85 - 0
src/api/models/NoticeReceiver.js

@@ -0,0 +1,85 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { NoticeReceiver, NoticeReceiverList } from 'src/models/NoticeReceiver.js'
+
+export default class NoticeReceiverAPI extends APIRepository {
+  constructor() {
+    super('ticket', appApi)
+    this.APIAdresses = {
+      base: '/mma/notice_receivers',
+      myNotSeenNotificationsCount: '/mma/notice_receivers/my_not_seen_notifications_count',
+      byId: (id) => '/mma/notice_receivers/' + id,
+      readNotification: (id) => '/mma/notice_receivers/' + id + '/read'
+    }
+    this.restUrl = (id) => this.url + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new NoticeReceiver(response.data) },
+      post: (response) => { return new NoticeReceiver(response.data) },
+      put: (response) => { return new NoticeReceiver(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 NoticeReceiverList(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.readNotification(id),
+      data: {
+
+      },
+      resolveCallback: (response) => {
+        return new NoticeReceiver(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 100 - 0
src/api/models/Package.js

@@ -0,0 +1,100 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { Package, PackageList } from 'src/models/Package.js'
+
+export default class PackageAPI extends APIRepository {
+  constructor() {
+    super('package', appApi)
+    this.APIAdresses = {
+      base: '/store/packages',
+      byId: (id) => '/store/packages/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new Package(response.data) },
+      post: (response) => { return new Package(response.data) },
+      put: (response) => { return new Package(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new PackageList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new Package(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  add (data) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        store_category: null, // Number
+        content_category: null, // Number
+        inventory: 0, // Number
+        unit_price: 0, // Number
+        discount_percent: 0, // Number
+        maximum_order_count: 0, // Number
+        weight: 0, // Number
+        thumbnail: null, // String
+        title: null, // String
+        description: null, // String
+        specification: null, // String
+        products: [], // Array of Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        return new Package(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 20 - 1
src/api/models/Payment.js

@@ -10,7 +10,8 @@ export default class PaymentAPI extends APIRepository {
       requestWithdraw: '/accounting/payments/request_withdraw',
       confirmWithdraw: (id) => '/accounting/payments/' + id + '/confirm_withdraw',
       rejectWithdraw: (id) => '/accounting/payments/' + id + '/reject_withdraw',
-      byId: (id) => '/accounting/payments/' + id
+      byId: (id) => '/accounting/payments/' + id,
+      exportPaymentReport: '/accounting/payments/export_report'
     }
     this.CacheList = {
       base: this.name + this.APIAdresses.base
@@ -108,4 +109,22 @@ export default class PaymentAPI extends APIRepository {
       }
     })
   }
+
+  exportPaymentReport(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.exportPaymentReport,
+      data: this.getNormalizedSendData({
+        report_type: null // String
+      }, data),
+      responseType: 'blob',
+      resolveCallback: (response) => {
+        return response.data // xlsxData
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
 }

+ 101 - 0
src/api/models/Product.js

@@ -0,0 +1,101 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { Product, ProductList } from 'src/models/Product.js'
+
+export default class ProductAPI extends APIRepository {
+  constructor() {
+    super('product', appApi)
+    this.APIAdresses = {
+      base: '/store/products',
+      byId: (id) => '/store/products/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new Product(response.data) },
+      post: (response) => { return new Product(response.data) },
+      put: (response) => { return new Product(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new ProductList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new Product(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  add (data) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        store_category: null, // Number
+        content_category: null, // Number
+        inventory: 0, // Number
+        unit_price: 0, // Number
+        discount_percent: 0, // Number
+        maximum_order_count: 0, // Number
+        weight: 0, // Number
+        thumbnail: null, // String
+        title: null, // String
+        description: null, // String
+        physical_type: null, // String
+        specification: null, // String
+        is_physical: false, // Boolean
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        return new Product(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 1 - 0
src/api/models/Project.js

@@ -10,6 +10,7 @@ export default class ProjectAPI extends APIRepository {
       attendanceSheetsList: '/lma/projects/attendance_sheets',
       byId: (id) => '/lma/projects/' + id,
       projectAndCurrentUserAttendanceSheet: (id) => '/lma/projects/' + id + '/project_and_current_user_attendance_sheet',
+      projectAndCurrentUserAttendanceSheetOfUser: (projectId, userId) => '/lma/projects/' + projectId + '/project_and_owner_user_attendance_sheet/' + userId,
       attendanceSheets: (id) => '/lma/projects/' + id + '/attendance_sheets'
     }
     this.CacheList = {

+ 2 - 1
src/api/models/Session.js

@@ -9,7 +9,8 @@ export default class SessionAPI extends APIRepository {
       base: '/lma/sessions',
       attendanceSheets: '/lma/sessions/attendance_sheets',
       byId: (id) => '/lma/sessions/' + id,
-      sessionAndCurrentUserAttendanceSheet: (id) => '/lma/sessions/' + id + '/session_and_current_user_attendance_sheet'
+      sessionAndCurrentUserAttendanceSheet: (id) => '/lma/sessions/' + id + '/session_and_current_user_attendance_sheet',
+      sessionAndCurrentUserAttendanceSheetOfUser: (sessionId, userId) => '/lma/sessions/' + sessionId + '/session_and_owner_user_attendance_sheet/' + userId
     }
     this.CacheList = {
       base: this.name + this.APIAdresses.base,

+ 57 - 0
src/api/models/StoreCategory.js

@@ -0,0 +1,57 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { StoreCategory, StoreCategoryList } from 'src/models/StoreCategory.js'
+
+export default class StoreCategoryAPI extends APIRepository {
+  constructor() {
+    super('store-category', appApi)
+    this.APIAdresses = {
+      base: '/store/categories',
+      byId: (id) => '/store/categories/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new StoreCategory(response.data) },
+      post: (response) => { return new StoreCategory(response.data) },
+      put: (response) => { return new StoreCategory(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        parent__isnull: null, // String (true, false)
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new StoreCategoryList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 3 - 3
src/api/models/TicketCategory.js

@@ -1,7 +1,7 @@
 import { appApi } from 'src/boot/axios.js'
+import { Ticket } from 'src/models/Ticket.js'
 import APIRepository from '../classes/APIRepository.js'
-import { Ticket, TicketList } from 'src/models/Ticket.js'
-import { TicketCategory } from 'src/models/TicketCategory.js'
+import { TicketCategory, TicketCategoryList } from 'src/models/TicketCategory.js'
 
 export default class TicketCategoryAPI extends APIRepository {
   constructor() {
@@ -34,7 +34,7 @@ export default class TicketCategoryAPI extends APIRepository {
         const results = response.data.results
         delete paginate.results
         return {
-          list: new TicketList(results),
+          list: new TicketCategoryList(results),
           paginate
           // {
           //   "count": 1,

+ 78 - 0
src/api/models/TranscriptSheet.js

@@ -0,0 +1,78 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+
+export default class TranscriptSheetAPI extends APIRepository {
+  constructor() {
+    super('transcript_sheets', appApi)
+    this.APIAdresses = {
+      base: '/lma/transcript_sheets',
+      exportReport: '/lma/transcript_sheets/export_report',
+      byId: (id) => '/lma/transcript_sheets/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+  }
+
+  notifyAnnounceResult (data) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.announceResult(data.classroomId),
+      resolveCallback: (response) => {
+        return response.data
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  deleteById (transcriptSheetId) {
+    return this.sendRequest({
+      apiMethod: 'delete',
+      api: this.api,
+      request: this.APIAdresses.byId(transcriptSheetId),
+      resolveCallback: (response) => {
+        return response.data
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  update (transcriptSheetId, data) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.byId(transcriptSheetId),
+      data,
+      resolveCallback: (response) => {
+        return response.data
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  exportReport(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.exportReport,
+      data: this.getNormalizedSendData({
+        registration__classroom: null, // Number
+        page: null // Number
+      }, data),
+      responseType: 'blob',
+      resolveCallback: (response) => {
+        return response.data // xlsxData
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 3 - 1
src/api/models/User.js

@@ -12,7 +12,9 @@ export default class UserAPI extends APIRepository {
       verify: '/uma/users/verify',
       current: '/uma/users/current',
       byId: (id) => '/uma/users/' + id,
-      changeRole: (id) => '/uma/users/' + id + '/change_role'
+      changeRole: (id) => '/uma/users/' + id + '/change_role',
+      unassignedGradersWithLastPassedUnit: '/uma/users/unassigned_graders_with_last_passed_unit',
+      unassignedLeadersWithLastPassedUnit: '/uma/users/unassigned_leaders_with_last_passed_unit'
     }
     this.CacheList = {
       base: this.name + this.APIAdresses.base,

File diff suppressed because it is too large
+ 2 - 0
src/assets/js/jalali-moment.browser.js


+ 10 - 4
src/components/Auth/ForgetStep.vue

@@ -35,7 +35,7 @@
     </template>
     <template v-else-if="registerStep === 'verify'">
       <label for="verifyNumber">
-        .لطفا کد تایید ارسال شده به شماره
+        .لطفا کد تایید ارسال شده به
         {{ username }}
         را وارد کنید
       </label>
@@ -72,14 +72,16 @@
                v-model="password"
                outlined
                class="password-input"
-               type="password"
+               :type="visibilityIcon === 'visibility' ? 'password' : 'text'"
                placeholder="گذرواژه جدید خود را وارد کنید"
                @keydown.enter="verify">
         <template v-slot:prepend>
-          <q-icon name="person" />
+          <q-icon name="lock_open" />
         </template>
         <template v-slot:append>
-          <q-icon name="person" />
+          <q-icon :name="visibilityIcon"
+                  class="cursor-pointer"
+                  @click="toggleVisibilityIcon" />
         </template>
       </q-input>
       <q-btn class="full-width btn-verifyUsername"
@@ -122,6 +124,7 @@ export default {
     sendOtpLoading: false,
     timerEnded: false,
     loading: false,
+    visibilityIcon: 'visibility',
     bannerMessage: 'بازیابی گذرواژه',
     registerStep: 'getUsername',
     username: null,
@@ -157,6 +160,9 @@ export default {
     this.$store.dispatch('Auth/logOut')
   },
   methods: {
+    toggleVisibilityIcon () {
+      this.visibilityIcon = this.visibilityIcon === 'visibility' ? 'visibility_off' : 'visibility'
+    },
     onStopTimer () {
       this.timerEnded = true
     },

+ 18 - 9
src/components/Auth/LoginStep.vue

@@ -69,15 +69,17 @@ export default {
       default: false
     }
   },
-  data: () => ({
-    userLogin: false,
-    loadingList: false,
-    visibilityIcon: 'visibility',
-    otpStep: 'sendOtp',
-    username: null,
-    password: null,
-    otp: null
-  }),
+  data () {
+    return {
+      userLogin: false,
+      loadingList: false,
+      visibilityIcon: 'visibility',
+      otpStep: 'sendOtp',
+      username: null,
+      password: null,
+      otp: null
+    }
+  },
   created () {
     // if (this.getToken()) {
     //   this.getUserData(() => { this.redirectTo() })
@@ -111,6 +113,13 @@ export default {
     },
 
     login () {
+      if (!this.username || !this.password) {
+        this.$q.notify({
+          type: 'negative',
+          message: 'نام کاربری و کلمه عبور را وارد کنید.'
+        })
+        return
+      }
       this.loadingList = true
       this.$store.dispatch('Auth/login', {
         input: this.username,

+ 54 - 20
src/components/Auth/RegisterStep.vue

@@ -182,28 +182,30 @@ export default {
       default: false
     }
   },
-  data: () => ({
-    sendOtpLoading: false,
-    timerEnded: false,
-    loading: false,
-    visibilityIcon: 'visibility',
-    bannerMessage: 'ثبت نام در سامانه',
-    registerStep: 'getUsername',
-    username: null,
-    verifyNumber: null,
-    firstname: null,
-    lastname: null,
-    password: null,
-    rePassword: null,
-    otpInterval: null,
-    otpTimer: 120,
+  data () {
+    return {
+      sendOtpLoading: false,
+      timerEnded: false,
+      loading: false,
+      visibilityIcon: 'visibility',
+      bannerMessage: 'ثبت نام در سامانه',
+      registerStep: 'getUsername',
+      username: null,
+      verifyNumber: null,
+      firstname: null,
+      lastname: null,
+      password: null,
+      rePassword: null,
+      otpInterval: null,
+      otpTimer: 120,
 
-    userLogin: false,
-    loadingList: false,
-    otpStep: 'sendOtp',
+      userLogin: false,
+      loadingList: false,
+      otpStep: 'sendOtp',
 
-    otp: null
-  }),
+      otp: null
+    }
+  },
   computed: {
     user () {
       return this.$store.getters['Auth/user']
@@ -232,6 +234,13 @@ export default {
       this.sendOtpSignUp()
     },
     sendOtpSignUp () {
+      if (!this.username) {
+        this.$q.notify({
+          type: 'negative',
+          message: 'ایمیل یا شماره تلفن همراه را وارد کنید.'
+        })
+        return
+      }
       this.sendOtpLoading = true
       APIGateway.auth.sendOtpSignUp({ input: this.username })
         .then(() => {
@@ -297,6 +306,13 @@ export default {
     },
 
     register () {
+      if (!this.firstname || !this.lastname || !this.password) {
+        this.$q.notify({
+          type: 'negative',
+          message: 'نام و نام خانوادگی و کلمه عبور را وارد کنید.'
+        })
+        return
+      }
       this.$axios.post(API_ADDRESS.auth.setPass, {
         new_password: this.password,
         firstname: this.firstname,
@@ -321,6 +337,10 @@ export default {
     },
     goToVerifyStep () {
       if (!this.username) {
+        this.$q.notify({
+          type: 'negative',
+          message: 'ایمیل یا شماره تلفن همراه را وارد کنید.'
+        })
         return
       }
       this.loading = true
@@ -344,6 +364,20 @@ export default {
         })
         return
       }
+      if (!this.firstname) {
+        this.$q.notify({
+          type: 'negative',
+          message: 'نام را وارد نکرده اید'
+        })
+        return
+      }
+      if (!this.lastname) {
+        this.$q.notify({
+          type: 'negative',
+          message: ' نام خانوادگی را وارد نکرده اید'
+        })
+        return
+      }
       if (this.rePassword !== this.password) {
         this.$q.notify({
           type: 'negative',

+ 49 - 47
src/components/ClassroomItem.vue

@@ -3,55 +3,54 @@
           class="classroom-item"
           :class="{'isRegistered' : classroom.is_current_user_registered }">
     <q-card-section class="thumbnail">
-      <router-link :to="{name: 'Public.ShowClassroomInfo', params: {id: classroom.id}}">
-        <div class="RegisteredSign">
-          <svg class="ribbon"
-               xmlns="http://www.w3.org/2000/svg"
-               width="123.945"
-               height="123.422"
-               viewBox="0 0 123.945 123.422">
-            <g id="Group_11524"
-               data-name="Group 11524"
-               transform="translate(-361 -1111)">
-              <g id="Group_7903"
-                 data-name="Group 7903"
-                 transform="translate(361 1111)">
-                <g id="Group_7901"
-                   data-name="Group 7901"
-                   transform="translate(0 0)">
-                  <rect id="Rectangle_1771"
-                        data-name="Rectangle 1771"
-                        width="13.986"
-                        height="39.668"
-                        transform="translate(0 83.754)"
-                        fill="#cfb184" />
-                  <rect id="Rectangle_1772"
-                        data-name="Rectangle 1772"
-                        width="39.668"
-                        height="13.986"
-                        transform="translate(84.277 0)"
-                        fill="#cfb184" />
-                  <path id="Path_18668"
-                        data-name="Path 18668"
-                        d="M1938.931,282.651l123.945-123.406-.016-.016h-44a9.041,9.041,0,0,0-6.379,2.631l-70.907,70.6a8.962,8.962,0,0,0-2.642,6.352Z"
-                        transform="translate(-1938.931 -159.229)"
-                        fill="#eac38a" />
-                </g>
+      <div class="RegisteredSign">
+        <svg class="ribbon"
+             xmlns="http://www.w3.org/2000/svg"
+             width="123.945"
+             height="123.422"
+             viewBox="0 0 123.945 123.422">
+          <g id="Group_11524"
+             data-name="Group 11524"
+             transform="translate(-361 -1111)">
+            <g id="Group_7903"
+               data-name="Group 7903"
+               transform="translate(361 1111)">
+              <g id="Group_7901"
+                 data-name="Group 7901"
+                 transform="translate(0 0)">
+                <rect id="Rectangle_1771"
+                      data-name="Rectangle 1771"
+                      width="13.986"
+                      height="39.668"
+                      transform="translate(0 83.754)"
+                      fill="#cfb184" />
+                <rect id="Rectangle_1772"
+                      data-name="Rectangle 1772"
+                      width="39.668"
+                      height="13.986"
+                      transform="translate(84.277 0)"
+                      fill="#cfb184" />
+                <path id="Path_18668"
+                      data-name="Path 18668"
+                      d="M1938.931,282.651l123.945-123.406-.016-.016h-44a9.041,9.041,0,0,0-6.379,2.631l-70.907,70.6a8.962,8.962,0,0,0-2.642,6.352Z"
+                      transform="translate(-1938.931 -159.229)"
+                      fill="#eac38a" />
               </g>
-              <text id="دوره_های_من"
-                    data-name="دوره‌های من"
-                    transform="translate(445.063 1135.174) rotate(-45)"
-                    fill="#fff"
-                    font-size="18"
-                    font-family="SegoeUI-Bold, Segoe UI"
-                    font-weight="700"><tspan x="0"
-                                             y="0">دوره‌های من</tspan></text>
             </g>
-          </svg>
-        </div>
-        <img class="thumbnail-img q-ma-xs"
-             :src="classroom.thumbnail">
-      </router-link>
+            <text id="دوره_های_من"
+                  data-name="دوره‌های من"
+                  transform="translate(445.063 1135.174) rotate(-45)"
+                  fill="#fff"
+                  font-size="18"
+                  font-family="SegoeUI-Bold, Segoe UI"
+                  font-weight="700"><tspan x="0"
+                                           y="0">دوره‌های من</tspan></text>
+          </g>
+        </svg>
+      </div>
+      <img class="thumbnail-img q-ma-xs"
+           :src="classroom.thumbnail"
+           @click="goToPage">
     </q-card-section>
     <q-card-section class="title">
       <router-link :to="{name: 'Public.ShowClassroomInfo', params: {id: classroom.id}}">
@@ -196,6 +195,9 @@ export default defineComponent({
     }
   },
   methods: {
+    goToPage () {
+      this.$router.push({ name: 'Public.ShowClassroomInfo', params: { id: this.classroom.id } })
+    },
     getHoldingType (classroom) {
       return new Classroom(classroom).holding_type_info.label
     }

+ 0 - 297
src/components/ContentVideoPlayer.vue

@@ -1,297 +0,0 @@
-<template>
-  <div ref="videoPlayerWrapper"
-       class="vPlayer">
-    <video-player v-if="content.photo && content.isVideo() && content.hasVideoSource()"
-                  ref="videoPlayer"
-                  :key="playerKey"
-                  :source="content.getVideoSource()"
-                  :poster="content.photo"
-                  :over-player="hasTimepoint"
-                  :over-player-width="'250px'"
-                  :has-vast="canInitVAST && contentHasVast"
-                  :use-over-player="hasTimepoint"
-                  @adStarted="adStarted">
-      <template #overPlayer>
-        <div class="timepoint-list">
-          <q-banner class="timepoint-list-title">
-            زمان کوب ها
-            ({{ currentContent.timepoints.list.length }})
-          </q-banner>
-          <q-list class="timepoint-list-items">
-            <q-item v-for="timepoint in currentContent.timepoints.list"
-                    :key="timepoint.id"
-                    v-ripple
-                    clickable
-                    @click="goToTimpoint(timepoint)">
-              <q-item-section avatar>
-                <bookmark color="white"
-                          size="30"
-                          :is-favored="timepoint.is_favored"
-                          :loading="timepoint.loading"
-                          @clicked="handleTimepointBookmark(timepoint.id)" />
-              </q-item-section>
-              <q-item-section class="text-section">
-                <span>{{ timepoint.title }}</span>
-                <span v-if="currentContent.can_user_use_timepoint">
-                  {{ timepoint.formattedTime() }}
-                </span>
-              </q-item-section>
-            </q-item>
-          </q-list>
-        </div>
-      </template>
-    </video-player>
-    <div v-else>
-      <q-card class="flex justify-center items-center">
-        <q-img v-if="content.photo"
-               :src="content.photo" />
-        <div v-else
-             class="flex justify-center items-center"
-             style="height: 420px; justify-content: center">
-          ویدیویی وجود ندارد!
-        </div>
-      </q-card>
-    </div>
-  </div>
-</template>
-
-<script>
-import { Content } from 'src/models/Content.js'
-import Bookmark from 'src/components/Bookmark.vue'
-import VideoPlayer from 'src/components/VideoPlayer.vue'
-import TimeElapsedSinceLastEvent from 'src/assets/js/TimeElapsedSinceLastEvent.js'
-
-export default {
-  name: 'ContentVideoPlayer',
-  components: { VideoPlayer, Bookmark },
-  props: {
-    content: {
-      type: Content
-    },
-    showTimePoints: {
-      type: Boolean,
-      default: true
-    },
-    poster: {
-      type: String,
-      default () {
-        return ''
-      }
-    },
-    keepCalculating: {
-      type: Boolean,
-      default () {
-        return true
-      }
-    },
-    currentTimed: {
-      type: Number
-    }
-  },
-  emits: ['seeked'],
-  data() {
-    return {
-      canInitVAST: false,
-      playerKey: Date.now(),
-      currentContent: new Content()
-    }
-  },
-  computed: {
-    contentHasVast () {
-      return this.content.has_vast
-    },
-    hasTimepoint () {
-      return this.content.timepoints.list.length > 0
-    }
-  },
-  watch: {
-    content: {
-      handler (newVal) {
-        this.playerKey = Date.now()
-        this.currentContent = newVal
-        if (!this.currentContent.can_user_use_timepoint) {
-          this.currentContent.timepoints.removeAllTimes()
-        }
-      },
-      immediate: true,
-      deep: true
-    }
-  },
-  beforeUnmount() {
-    if (this.player) {
-      this.player.dispose()
-    }
-  },
-  beforeMount () {
-    this.canInitVAST = TimeElapsedSinceLastEvent.canInitVAST()
-  },
-  methods: {
-    adStarted () {
-      TimeElapsedSinceLastEvent.setEventOccurrenceTime()
-    },
-    getCurrentContentTimepoint (timepointId) {
-      return this.currentContent.timepoints.list.find(item => item.id === timepointId)
-    },
-    handleTimepointBookmark (timepointId) {
-      const timepointIndex = this.currentContent.timepoints.list.findIndex(item => item.id === timepointId)
-      const currentContentTimepoint = this.getCurrentContentTimepoint(timepointId)
-      this.currentContent.timepoints.list[timepointIndex].loading = true
-      const isFavoredStatus = currentContentTimepoint.is_favored ? 'unfavored' : 'favored'
-      this.changeTimepointStatus({
-        timepointId,
-        isFavoredStatus,
-        timepointIndex,
-        currentContentTimepoint
-      })
-    },
-    changeTimepointStatus (data) {
-      this.$apiGateway.content.setBookmarkTimepointFavoredStatus({
-        id: data.timepointId,
-        status: data.isFavoredStatus
-      })
-        .then(() => {
-          this.currentContent.timepoints.list[data.timepointIndex].is_favored = !data.currentContentTimepoint.is_favored
-          this.toggleFavorite(this.content.id)
-          this.currentContent.timepoints.list[data.timepointIndex].loading = false
-        })
-        .catch(() => {
-          this.currentContent.timepoints.list[data.timepointIndex].loading = false
-        })
-    },
-    goToTimpoint (timepoint) {
-      if (!this.$refs.videoPlayer) {
-        return
-      }
-      if (!this.currentContent.can_user_use_timepoint) {
-        this.$refs.videoPlayer.toggleFullScreen()
-        this.$q.dialog({
-          title: 'استفاده از زمان کوب',
-          message: 'جهت استفاده از زمان کوب می بایست اشتراک خریداری کنید.',
-          cancel: true,
-          persistent: true
-        }).onOk(() => {
-          this.$router.push({ name: 'Public.Landing.DynamicName', params: { landing_name: 'timepoint' } })
-        }).onCancel(() => {
-          // this.$router.push({ name: 'Public.Home' })
-        })
-        return
-      }
-      this.$refs.videoPlayer.changeCurrentTime(timepoint.time)
-    },
-    activate(time) {
-      this.player.currentTime(time)
-      this.player.play()
-      const requiredElement = document.querySelector('.video-js')
-      requiredElement.focus()
-    },
-    setSources() {
-      this.options.sources = this.source
-    },
-    setPoster() {
-      this.options.poster = this.poster
-    },
-    reInitVideo() {
-      this.player.src(this.source)
-      this.player.poster(this.poster)
-    },
-    toggleFavorite(id) {
-      const that = this
-      let count = -1
-      // let currentTimepointIndex = null
-      this.timePoints.forEach(function (item) {
-        count++
-        if (parseInt(item.id) === parseInt(id)) {
-          // currentTimepointIndex = index
-          item.loading = true
-          item.is_favored = !item.is_favored
-          that.postIsFavored = {
-            id: item.id,
-            isFavored: item.is_favored,
-            numberOfTimestamp: count
-          }
-        }
-      })
-      const requiredElement = document.querySelector('.video-js')
-      requiredElement.focus()
-      this.$emit('toggleBookmark', this.postIsFavored)
-      // setTimeout(function() { that.timePoints[currentTimepointIndex].loading = false }, 200) // vue/no-mutating-props
-    },
-    // postIsFavored(timeStampData){
-    //     var postStatus = 'unfavored'
-    //     if (timeStampData.isFavored){
-    //         postStatus = 'favored'
-    //     }
-    //     // /api/v2/timepoint/{timepoint_id}/favored
-    //     axios.post('/api/v2/c/timepoint/' + parseInt(timeStampData.id) + '/'+ postStatus)
-    //         .then(response => {
-    //             if (response.status === 200){
-    //                 this.timePoints.forEach( function (item) {
-    //                     if (parseInt(item.id) === parseInt(timeStampData.id)) {
-    //                         item.loading = false
-    //                     }
-    //                 })
-    //             }
-    //         })
-    //         .catch(err => console.error(err));
-    // },
-    calcWatchedPercentage(currentTime, duration) {
-      const watchedPercentage = ((currentTime / duration) * 100)
-      const videoPlayerTimeData = {
-        currentTime,
-        duration,
-        watchedPercentage
-      }
-      this.$emit('calcTimeData', videoPlayerTimeData)
-    },
-    videoStatus(val) {
-      this.videoIsPlaying = val
-    }
-  }
-}
-</script>
-
-<style scoped lang="scss">
-.vPlayer {
-  width: 100%;
-  .timepoint-list {
-    direction: ltr;
-    width: 100%;
-    color: white;
-    height: 100%;
-    padding-bottom: 30px;
-    background: rgba(0,0,0,0.4);
-    .timepoint-list-title {
-      text-align: center;
-      background: rgba(0,0,0,0.7);
-    }
-    .timepoint-list-items {
-      .text-section {
-        display: flex;
-        flex-flow: row;
-        font-size: 0.7rem;
-        font-weight: bold;
-        align-items: center;
-        justify-content: space-between;
-      }
-    }
-    :deep(.q-list) {
-      height: calc(100% - 54px);
-      overflow: auto;
-      .bookmark-btn.q-btn {
-        width: 26px;
-        height: 26px;
-        padding: 0;
-        font-size: 10px;
-        color: $primary !important;
-        .q-btn__content {
-          margin: 3px;
-          svg {
-            width: 20px;
-            height: 20px;
-          }
-        }
-      }
-    }
-  }
-}
-</style>

+ 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: {}

+ 54 - 51
src/components/EventItem.vue

@@ -3,55 +3,54 @@
           class="event-item"
           :class="{'isRegistered' : event.is_current_user_registered }">
     <q-card-section class="thumbnail">
-      <router-link :to="{name: 'Public.ShowEventInfo', params: {id: event.id}}">
-        <div class="RegisteredSign">
-          <svg class="ribbon"
-               xmlns="http://www.w3.org/2000/svg"
-               width="123.945"
-               height="123.422"
-               viewBox="0 0 123.945 123.422">
-            <g id="Group_11524"
-               data-name="Group 11524"
-               transform="translate(-361 -1111)">
-              <g id="Group_7903"
-                 data-name="Group 7903"
-                 transform="translate(361 1111)">
-                <g id="Group_7901"
-                   data-name="Group 7901"
-                   transform="translate(0 0)">
-                  <rect id="Rectangle_1771"
-                        data-name="Rectangle 1771"
-                        width="13.986"
-                        height="39.668"
-                        transform="translate(0 83.754)"
-                        fill="#cfb184" />
-                  <rect id="Rectangle_1772"
-                        data-name="Rectangle 1772"
-                        width="39.668"
-                        height="13.986"
-                        transform="translate(84.277 0)"
-                        fill="#cfb184" />
-                  <path id="Path_18668"
-                        data-name="Path 18668"
-                        d="M1938.931,282.651l123.945-123.406-.016-.016h-44a9.041,9.041,0,0,0-6.379,2.631l-70.907,70.6a8.962,8.962,0,0,0-2.642,6.352Z"
-                        transform="translate(-1938.931 -159.229)"
-                        fill="#eac38a" />
-                </g>
+      <div class="RegisteredSign">
+        <svg class="ribbon"
+             xmlns="http://www.w3.org/2000/svg"
+             width="123.945"
+             height="123.422"
+             viewBox="0 0 123.945 123.422">
+          <g id="Group_11524"
+             data-name="Group 11524"
+             transform="translate(-361 -1111)">
+            <g id="Group_7903"
+               data-name="Group 7903"
+               transform="translate(361 1111)">
+              <g id="Group_7901"
+                 data-name="Group 7901"
+                 transform="translate(0 0)">
+                <rect id="Rectangle_1771"
+                      data-name="Rectangle 1771"
+                      width="13.986"
+                      height="39.668"
+                      transform="translate(0 83.754)"
+                      fill="#cfb184" />
+                <rect id="Rectangle_1772"
+                      data-name="Rectangle 1772"
+                      width="39.668"
+                      height="13.986"
+                      transform="translate(84.277 0)"
+                      fill="#cfb184" />
+                <path id="Path_18668"
+                      data-name="Path 18668"
+                      d="M1938.931,282.651l123.945-123.406-.016-.016h-44a9.041,9.041,0,0,0-6.379,2.631l-70.907,70.6a8.962,8.962,0,0,0-2.642,6.352Z"
+                      transform="translate(-1938.931 -159.229)"
+                      fill="#eac38a" />
               </g>
-              <text id="رویداد_های_من"
-                    data-name="رویداد‌های من"
-                    transform="translate(445.063 1135.174) rotate(-45)"
-                    fill="#fff"
-                    font-size="18"
-                    font-family="SegoeUI-Bold, Segoe UI"
-                    font-weight="700"><tspan x="0"
-                                             y="0">رویداد‌های من</tspan></text>
             </g>
-          </svg>
-        </div>
-        <img class="thumbnail-img"
-             :src="event.thumbnail">
-      </router-link>
+            <text id="رویداد_های_من"
+                  data-name="رویداد‌های من"
+                  transform="translate(445.063 1135.174) rotate(-45)"
+                  fill="#fff"
+                  font-size="18"
+                  font-family="SegoeUI-Bold, Segoe UI"
+                  font-weight="700"><tspan x="0"
+                                           y="0">رویداد‌های من</tspan></text>
+          </g>
+        </svg>
+      </div>
+      <img class="thumbnail-img"
+           :src="event.thumbnail"
+           @click="goToPage">
     </q-card-section>
     <div class="infos">
       <div class="title ellipsis-2-lines">
@@ -87,15 +86,16 @@
 
 <script>
 import { defineComponent } from 'vue'
-import { Event } from 'src/models/Event.js'
+// import { Event } from 'src/models/Event.js'
+import { Classroom } from 'src/models/Classroom' //  using Classroom instead of Event to prevent props warning
 import Assist from 'src/assets/js/Assist.js'
 
 export default defineComponent({
   name: 'OptionPanel',
   props: {
     event: {
-      type: Event,
-      default: new Event()
+      type: Classroom,
+      default: new Classroom()
     }
   },
   data() {
@@ -150,8 +150,11 @@ export default defineComponent({
     }
   },
   methods: {
+    goToPage () {
+      this.$router.push({ name: 'Public.ShowEventInfo', params: { id: this.event.id } })
+    },
     getHoldingType (event) {
-      return new Event(event).holding_type_info.label
+      return new Classroom(event).holding_type_info.label
     },
     miladiToShamsi () {
       if (!this.event.publish_time) {

+ 105 - 50
src/components/FormBuilderCustumComponents/ContentCategorySelector.vue

@@ -1,50 +1,79 @@
 <template>
-  <div class="ContentCategorySelector row q-col-gutter-md">
-    <div class="col-md-4 col-12">
-      <div>
-        دسته بندی اصلی
+  <q-card>
+    <q-card-section>
+      <span v-if="categoryType === 'content'">
+        انتخاب دسته محتوا
+      </span>
+      <span v-if="categoryType === 'contentTopic'">
+        انتخاب دسته موضوع
+      </span>
+      <span v-else-if="categoryType === 'store'">
+        انتخاب دسته فروشگاه
+      </span>
+    </q-card-section>
+    <q-separator />
+    <q-card-section>
+      <div class="ContentCategorySelector row q-col-gutter-md">
+        <div class="col-md-4 col-12">
+          <div>
+            دسته بندی اصلی
+          </div>
+          <q-select v-model="mainCategory"
+                    :options="mainCategoryOptions"
+                    :loading="categories.laoding" />
+        </div>
+        <div class="col-md-4 col-12">
+          <div>
+            دسته بندی جزیی
+          </div>
+          <q-select v-model="subCategory"
+                    :options="subCategoryOptions"
+                    :loading="categories.laoding" />
+        </div>
+        <div class="col-md-4 col-12">
+          <div>
+            بخش
+          </div>
+          <q-select v-model="bakhshCategory"
+                    :options="bakhshCategoryOptions"
+                    :loading="categories.laoding" />
+        </div>
       </div>
-      <q-select v-model="mainCategory"
-                :options="mainCategoryOptions"
-                :loading="contentCategories.laoding" />
-    </div>
-    <div class="col-md-4 col-12">
-      <div>
-        دسته بندی جزیی
-      </div>
-      <q-select v-model="subCategory"
-                :options="subCategoryOptions"
-                :loading="contentCategories.laoding" />
-    </div>
-    <div class="col-md-4 col-12">
-      <div>
-        بخش
-      </div>
-      <q-select v-model="bakhshCategory"
-                :options="bakhshCategoryOptions"
-                :loading="contentCategories.laoding" />
-    </div>
-  </div>
+    </q-card-section>
+  </q-card>
 </template>
 
 <script>
 import { APIGateway } from 'src/api/APIGateway.js'
+import { StoreCategoryList } from 'src/models/StoreCategory.js'
 import { ContentCategoryList } from 'src/models/ContentCategory.js'
+import { ContentTopicCategoryList } from 'src/models/ContentTopicCategory.js'
 
 export default {
   name: 'ContentCategorySelector',
   props: {
     value: {
       default: () => [],
-      type: [Array, String, Number, Boolean]
+      type: [Array, String, Number, Boolean, Object]
     },
     disable: {
       default: false,
       type: Boolean
+    },
+    categoryType: {
+      type: String,
+      default: 'content' // 'content', 'contentTopic', 'store'
     }
   },
   emits: ['update:value'],
   data () {
+    let categories = new ContentCategoryList()
+    if (this.categoryType === 'store') {
+      categories = new StoreCategoryList()
+    }
+    if (this.categoryType === 'contentTopic') {
+      categories = new ContentTopicCategoryList()
+    }
     return {
       mainCategory: null,
       subCategory: null,
@@ -52,20 +81,24 @@ export default {
       mainCategoryOptions: [],
       subCategoryOptions: [],
       bakhshCategoryOptions: [],
-      contentCategories: new ContentCategoryList()
+      categories
     }
   },
   watch: {
     value () {
-      if (this.value.id) {
+      if (this.value?.id) {
         this.getCategories()
-      } else {
+      } else if (this.value) {
         this.inputData = this.value
       }
     },
     mainCategory (newValue) {
       this.subCategory = null
       this.subCategoryOptions = []
+      if (!newValue.item.children) {
+        this.$emit('update:value', newValue.value)
+        return
+      }
       this.subCategoryOptions = newValue.item.children.map(item => {
         return {
           item,
@@ -77,6 +110,10 @@ export default {
     subCategory (newValue) {
       this.bakhshCategory = null
       this.bakhshCategoryOptions = []
+      if (!newValue?.item?.children) {
+        this.$emit('update:value', null)
+        return
+      }
       this.bakhshCategoryOptions = newValue.item.children.map(item => {
         return {
           item,
@@ -94,31 +131,48 @@ export default {
     this.getCategories()
   },
   methods: {
-    setMainCategory () {
-      if (!this.value?.parent?.parent?.id) {
+    setMainCategory (category) {
+      if (!category?.parent?.parent?.id) {
         return
       }
-      this.mainCategory = this.mainCategoryOptions.find(item => item.value === this.value.parent.parent.id)
+      this.mainCategory = this.mainCategoryOptions.find(item => item.value === category.parent.parent.id)
     },
-    setSubCategory () {
-      if (!this.value?.parent?.id) {
+    setSubCategory (category) {
+      if (!category?.parent?.id) {
         return
       }
-      this.subCategory = this.subCategoryOptions.find(item => item.value === this.value.parent.id)
+      this.subCategory = this.subCategoryOptions.find(item => item.value === category.parent.id)
     },
-    setBakhshCategory () {
-      if (!this.value?.id) {
+    setBakhshCategory (category) {
+      if (!category?.id) {
         return
       }
-      this.bakhshCategory = this.bakhshCategoryOptions.find(item => item.value === this.value.id)
+      this.bakhshCategory = this.bakhshCategoryOptions.find(item => item.value === category.id)
     },
     getCategories () {
-      this.contentCategories.laoding = true
-      APIGateway.contentCategory.index({ parent__isnull: 'true', per_page: 99999 })
-        .then(contentCategories => {
-          this.contentCategories = new ContentCategoryList(contentCategories.list)
-          this.contentCategories.laoding = false
-          this.mainCategoryOptions = this.contentCategories.list.map(item => {
+      this.categories.laoding = true
+      const args = { parent__isnull: 'true', per_page: 99999 }
+
+      let apiGateway = APIGateway.contentCategory.index(args)
+      if (this.categoryType === 'store') {
+        apiGateway = APIGateway.storeCategory.index(args)
+      }
+      if (this.categoryType === 'contentTopic') {
+        apiGateway = APIGateway.contentTopicCategory.index(args)
+      }
+
+      apiGateway
+        .then(categories => {
+          if (this.categoryType === 'content') {
+            this.categories = new ContentCategoryList(categories.list)
+          } else if (this.categoryType === 'contentTopic') {
+            this.categories = new ContentTopicCategoryList(categories.list)
+          } else if (this.categoryType === 'store') {
+            this.categories = new StoreCategoryList(categories.list)
+          }
+
+          this.categories.laoding = false
+          this.mainCategoryOptions = this.categories.list.map(item => {
             return {
               item,
               label: item.title,
@@ -126,21 +180,22 @@ export default {
             }
           })
 
-          if (this.value?.parent?.parent?.id && this.value.id) {
+          if (this.value?.parent?.parent?.id && this.value?.id) {
+            const category = this.value
             this.$nextTick(() => {
-              this.setMainCategory()
+              this.setMainCategory(category)
               this.$nextTick(() => {
-                this.setSubCategory()
+                this.setSubCategory(category)
                 this.$nextTick(() => {
-                  this.setBakhshCategory()
-                  this.$emit('update:value', this.value.id)
+                  this.setBakhshCategory(category)
+                  this.$emit('update:value', category.id)
                 })
               })
             })
           }
         })
         .catch(() => {
-          this.contentCategories.laoding = false
+          this.categories.laoding = false
         })
     }
   }

+ 11 - 8
src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue

@@ -14,13 +14,16 @@
                      :show-edit-button="false"
                      :show-expand-button="false"
                      :show-save-button="false"
-                     :show-reload-button="false" />
-      <q-btn color="primary"
-             class="full-width q-mt-md"
-             :loading="entityCreateLoading"
-             @click="entityCreate">
-        افزودن چند رسانه ای
-      </q-btn>
+                     :show-reload-button="false">
+        <template #after-form-builder>
+          <q-btn color="primary"
+                 class="full-width q-mt-md"
+                 :loading="entityCreateLoading"
+                 @click="entityCreate">
+            افزودن چند رسانه ای
+          </q-btn>
+        </template>
+      </entity-create>
     </div>
     <media-category v-if="imageMedias.list.length > 0"
                     :medias="imageMedias"
@@ -139,7 +142,7 @@ export default {
     value: {
       handler () {
         this.inputData = this.value
-        if (this.value.length > 0 && this.value[0].id) {
+        if (this.value && this.value.length > 0 && this.value[0].id) {
           this.medias = new MediaList(this.value)
           this.$emit('update:value', this.medias.list.map(item => item.id))
         }

+ 293 - 0
src/components/FormBuilderCustumComponents/FormBuilderDateTime.vue

@@ -0,0 +1,293 @@
+<template>
+  <div class="form-builder-date-time"
+       :class="customClass">
+    <div v-if="outsideLabel"
+         class="outside-label">
+      {{ outsideLabel }}
+    </div>
+    <div class="inputs">
+      <q-input ref="input"
+               :model-value="displayTime"
+               :name="name"
+               :loading="loading"
+               :filled="filled"
+               readonly
+               dir="ltr"
+               :disable="disable"
+               :label="placeholder ? null : label"
+               :stack-label="!!placeholder"
+               :placeholder="placeholder"
+               :rules="rules"
+               :lazy-rules="lazyRules"
+               :outlined="outlined"
+               :class="customClass"
+               :input-class="customClass"
+               @click="onClickInput('time')">
+        <template v-slot:append>
+          <q-icon name="access_time"
+                  class="cursor-pointer">
+            <q-popup-proxy v-model="popupTime"
+                           cover
+                           transition-show="scale"
+                           transition-hide="scale">
+              <q-time v-model="dateTime.time"
+                      mask="HH:mm:00"
+                      format24h
+                      :disable="disable"
+                      :title="title ? title : label"
+                      :now-btn="nowBtn"
+                      @update:model-value="onChangeTime">
+                <div class="row items-center justify-end">
+                  <q-btn v-close-popup
+                         label="بستن"
+                         color="primary"
+                         flat />
+                </div>
+              </q-time>
+            </q-popup-proxy>
+          </q-icon>
+          <q-btn v-if="clearable"
+                 icon="close"
+                 flat
+                 round
+                 class="cursor-pointer"
+                 @click="onClear" />
+        </template>
+      </q-input>
+      <q-input ref="input"
+               :model-value="displayDate"
+               :name="name"
+               :loading="loading"
+               :filled="filled"
+               readonly
+               dir="ltr"
+               :disable="disable"
+               :label="placeholder ? null : label"
+               :stack-label="!!placeholder"
+               :placeholder="placeholder"
+               :rules="rules"
+               :lazy-rules="lazyRules"
+               :outlined="outlined"
+               :class="customClass"
+               :input-class="customClass"
+               @click="onClickInput('date')">
+        <template v-slot:append>
+          <q-icon name="event"
+                  class="cursor-pointer">
+            <q-popup-proxy v-model="popupDate"
+                           cover
+                           transition-show="scale"
+                           transition-hide="scale">
+              <q-date v-model="dateTime.date"
+                      :calendar="calendar"
+                      mask="YYYY/MM/DD"
+                      :range="range"
+                      :multiple="multiple"
+                      :disable="disable"
+                      :title="title ? title : label"
+                      :today-btn="todayBtn"
+                      @update:model-value="onChangeDate">
+                <div class="row items-center justify-end">
+                  <q-btn v-close-popup
+                         label="بستن"
+                         color="primary"
+                         flat />
+                </div>
+              </q-date>
+            </q-popup-proxy>
+          </q-icon>
+        </template>
+        <template #prepend>
+          <q-btn icon="close"
+                 flat
+                 round
+                 class="clear-btn"
+                 @click="onClear" />
+        </template>
+      </q-input>
+    </div>
+  </div>
+</template>
+
+<script>
+import moment from 'moment-jalaali'
+import { inputMixin } from 'quasar-form-builder'
+
+moment.loadPersian()
+
+export default {
+  name: 'FormBuilderDateTime',
+  mixins: [inputMixin],
+  props: {
+    name: {
+      default: '',
+      type: String
+    },
+    outsideLabel: {
+      default: null,
+      type: String
+    },
+    calendar: {
+      default: 'persian',
+      type: String
+    },
+    calendarIcon: {
+      default: 'event',
+      type: String
+    },
+    clockIcon: {
+      default: 'access_time',
+      type: String
+    },
+    title: {
+      default: '',
+      type: String
+    },
+    placeholder: {
+      default: '',
+      type: String
+    },
+    value: {
+      default: '',
+      type: String
+    },
+    nowBtn: {
+      default: false,
+      type: Boolean
+    },
+    iso8601: {
+      default: true,
+      type: Boolean
+    },
+    todayBtn: {
+      default: false,
+      type: Boolean
+    }
+  },
+  data() {
+    return {
+      displayDateTime: '',
+      popupDate: false,
+      popupTime: false,
+      dateTime: {
+        date: '',
+        time: ''
+      },
+      rendrKey: Date.now(),
+      showDate: false,
+      showTime: false
+    }
+  },
+  computed: {
+    displayDate () {
+      if (!this.displayDateTime) {
+        return ''
+      }
+
+      const dataArr = this.displayDateTime.split(' ')
+      if (dataArr.length > 0) {
+        return dataArr[0]
+      }
+      return ''
+    },
+    displayTime () {
+      if (!this.displayDateTime) {
+        return ''
+      }
+
+      const dataArr = this.displayDateTime.split(' ')
+      if (dataArr.length === 2) {
+        return dataArr[1]
+      }
+      return ''
+    }
+  },
+  watch: {
+    value: {
+      handler(newValue) {
+        if (!newValue) {
+          this.inputData = null
+          this.displayDateTime = ''
+          return
+        }
+        const newDate = moment(newValue.toString()).format('YYYY-MM-DD')
+        const newTime = moment(newValue.toString()).format('HH:mm:00')
+        this.updateDateTime(newDate, 'date')
+        this.updateDateTime(newTime, 'time')
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    onClickInput (type) {
+      if (type === 'date') {
+        this.popupDate = true
+      } else {
+        this.popupTime = true
+      }
+    },
+    onClear () {
+      this.displayDateTime = ''
+      this.inputData = null
+      this.change(this.inputData)
+    },
+    onChangeDate (newValue) {
+      let gregorianDate = newValue
+      if (this.calendar === 'persian') {
+        gregorianDate = this.shamsiToMiladiDate(newValue.toString())
+      }
+      this.updateDateTime(gregorianDate, 'date')
+    },
+    onChangeTime (newValue) {
+      this.updateDateTime(newValue, 'time')
+    },
+    updateDateTime (newValue, updateType = 'date') {
+      const defaultDate = moment(Date.now()).format('YYYY-MM-DD')
+      const defaultTime = moment(Date.now()).format('HH:mm:00')
+      const inputData = this.inputData ? this.inputData.replace('T', ' ') : defaultDate + ' ' + defaultTime
+      const arrValue = inputData.toString().trim().split(' ')
+      let arrDisplay = new Array(arrValue)
+      if (updateType === 'date') {
+        arrDisplay = [newValue.toString(), arrValue[1]]
+        // if (this.calendar === 'persian') {
+        //   arrValue[0] = this.shamsiToMiladiDate(newValue.toString())
+        // } else {
+        //   arrValue[0] = newValue.toString()
+        // }
+        arrValue[0] = newValue.toString()
+      } else if (updateType === 'time') {
+        const timeWithoutSecond = arrValue[1].split(':').splice(0, 2).join(':')
+        if (this.calendar === 'persian') {
+          arrDisplay = [this.miladiToShamsiDate(arrValue[0]), timeWithoutSecond]
+        } else {
+          arrDisplay = [arrValue[0], timeWithoutSecond]
+        }
+        arrValue[1] = newValue.toString()
+      }
+
+      const delimiter = this.iso8601 ? 'T' : ' '
+      this.displayDateTime = arrDisplay.join(' ').replace(',', ' ')
+      this.inputData = arrValue.join(delimiter).replace(',', ' ')
+      this.change(this.inputData)
+    },
+
+    shamsiToMiladiDate(date) {
+      return moment(date, 'jYYYY/jMM/jDD').format('YYYY-MM-DD')
+    },
+    miladiToShamsiDate(date) {
+      return moment(date, 'YYYY-MM-DD').format('jYYYY/jMM/jDD')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.inputs {
+  display: flex;
+  justify-content: flex-start;
+  flex-wrap: nowrap;
+  .q-field {
+    width: 50%;
+  }
+}
+</style>

+ 6 - 6
src/components/FormBuilderCustumComponents/FormBuilderInputEditor.vue

@@ -1,12 +1,9 @@
 <template>
-  <div class="form-builder-editor"
-       :class="customClass">
+  <div class="form-builder-editor">
     <p v-text="label" />
     <q-editor v-if="!disable && !readonly"
               ref="editor"
               v-model="inputData"
-              :class="customClass"
-              :content-class="customClass"
               :name="name"
               :dense="$q.screen.lt.md"
               :toolbar="[
@@ -79,8 +76,7 @@
                 times_new_roman: 'Times New Roman',
                 verdana: 'Verdana',
               }"
-              @update:model-value="change($event)"
-              @click="onClick"
+              @update:model-value="onChangeText"
               @paste="onPaste">
       <template v-slot:colorpicker>
         <q-btn-dropdown ref="token"
@@ -176,6 +172,10 @@ export default {
     onPaste () {
       this.inputData = this.inputData.replaceAll(/<!--\[if .*endif\]-->/gms, 'ferret')
     },
+    onChangeText (event) {
+      this.change(event)
+      this.$emit('update:value', event)
+    },
     setColor (cmd, name) {
       const edit = this.$refs.editor
       this.$refs.token.hide()

+ 85 - 0
src/components/FormBuilderCustumComponents/Hashtags/HashtagItem.vue

@@ -0,0 +1,85 @@
+<template>
+  <div class="HashtagItam">
+    <div class="title ellipsis">
+      {{ hashtag.title }}
+    </div>
+    <div class="actions">
+      <q-btn v-if="canDelete"
+             class="deleteBtn"
+             icon="clear"
+             outline
+             dense
+             round
+             color="red"
+             size="sm"
+             @click="onDelete" />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'HashtagItam',
+  props: {
+    hashtag: {
+      type: Object,
+      default: () => {
+        return {
+          title: ''
+        }
+      }
+    },
+    canDelete: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['update:value', 'onView', 'onDelete'],
+  data () {
+    return {
+      entityCreateLoading: false
+    }
+  },
+  methods: {
+    onView () {
+      this.$emit('onView')
+    },
+    onDelete () {
+      this.$emit('onDelete')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.HashtagItam {
+  background: #F9F9F9;
+  border: 1px solid #DFE1EC;
+  border-radius: 50px;
+  padding: 8px 12px 8px 12px;
+  display: flex;
+  flex-flow: row;
+  justify-content: space-between;
+  align-items: center;
+  .title {
+    font-size: 15px;
+    letter-spacing: 0;
+    color: #212121;
+  }
+  .actions {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    .q-btn {
+      width: 28px;
+      height: 28px;
+      border-radius: 100%;
+      &.viewBtn {
+      }
+      &.deleteBtn {
+        margin-left: 16px;
+      }
+    }
+  }
+}
+</style>

+ 150 - 0
src/components/FormBuilderCustumComponents/Hashtags/HashtagSelector.vue

@@ -0,0 +1,150 @@
+<template>
+  <div class="ContentMedias">
+    <q-select v-model="selectedHashtags"
+              use-input
+              hide-selected
+              map-options
+              multiple
+              option-value="id"
+              option-label="title"
+              input-debounce="300"
+              label="کلیدواژه"
+              :loading="fetchLoading"
+              :options="filteredHashtags"
+              @new-value="addHashtag"
+              @update:model-value="onChangeSelected"
+              @filter="filterFn">
+      <template #prepend>
+        <q-icon name="search" />
+      </template>
+      <template v-slot:no-option>
+        <span v-if="fetchLoading">
+          کمی صبر کنید...
+        </span>
+        <span v-else>
+          موردی یافت نشد
+        </span>
+      </template>
+    </q-select>
+    <div class="selected-hastags">
+      <hashtag-item v-for="(hashtag, hashtagIndex) in selectedHashtags"
+                    :key="hashtagIndex"
+                    :hashtag="hashtag"
+                    :can-delete="!readonly"
+                    @onDelete="onDeleteHashtag(hashtagIndex)" />
+    </div>
+  </div>
+</template>
+
+<script>
+import HashtagItem from './HashtagItem.vue'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'HashtagSelector',
+  components: { HashtagItem },
+  props: {
+    value: {
+      default: () => [],
+      type: [Array, String, Number, Boolean]
+    },
+    disable: {
+      default: false,
+      type: Boolean
+    },
+    readonly: {
+      default: false,
+      type: Boolean
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      selectedHashtags: [],
+      filteredHashtags: [],
+      selectInput: null,
+      fetchLoading: false
+    }
+  },
+  watch: {
+    value: {
+      handler () {
+        if (this.value && this.value.length > 0 && this.value[0].id) {
+          this.selectedHashtags = this.value
+          this.emitUpdateValue()
+        }
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    addHashtag (val, done) {
+      this.fetchLoading = true
+      APIGateway.hashtag.create(val)
+        .then((hashtag) => {
+          done(hashtag)
+        })
+        .finally(() => {
+          this.fetchLoading = false
+        })
+    },
+    onChangeSelected () {
+      this.emitUpdateValue()
+    },
+    getHashtags (search) {
+      return new Promise((resolve, reject) => {
+        APIGateway.mediaHashtag.index({ search })
+          .then((hashtagList) => {
+            resolve(hashtagList.list.list)
+          })
+          .catch(() => {
+            reject()
+          })
+      })
+    },
+    filterFn (val, update) {
+      this.selectInput = val
+      if (!val) {
+        this.filteredHashtags = []
+        return
+      }
+
+      this.fetchLoading = true
+      this.getHashtags(val)
+        .then((hashtagList) => {
+          update(() => {
+            this.filteredHashtags = hashtagList
+          })
+        })
+        .catch(() => {
+          update(() => {
+            this.filteredHashtags = []
+          })
+        })
+        .finally(() => {
+          this.fetchLoading = false
+        })
+    },
+    onDeleteHashtag(hashtagIndex) {
+      this.selectedHashtags.splice(hashtagIndex, 1)
+      this.emitUpdateValue()
+    },
+    emitUpdateValue () {
+      const selectedIds = this.selectedHashtags.map(item => item.id)
+      this.$emit('update:value', selectedIds)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.ContentMedias {
+  width: 100%;
+  .selected-hastags {
+    margin-top: 8px;
+    display: flex;
+    justify-content: flex-start;
+    gap: 8px;
+  }
+}
+</style>

+ 147 - 0
src/components/FormBuilderCustumComponents/ProductSelector.vue

@@ -0,0 +1,147 @@
+<template>
+  <entity-index v-model:table-selected-values="selected"
+                :value="inputs"
+                :title="label"
+                :api="api"
+                :table="table"
+                :table-keys="tableKeys"
+                :table-selection-mode="selectionMode"
+                :show-close-button="false"
+                :show-expand-button="false"
+                :show-reload-button="false"
+                :show-search-button="true"
+                :row-key="itemIdentifyKey"
+                :item-indicator-key="itemIndicatorKey"
+                @onInputClick="onInputClick"
+                @update:table-selected-values="onSelectedUpdate" />
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityIndex } from 'quasar-crud'
+import { Product } from 'src/models/Product.js'
+import { inputMixin } from 'quasar-form-builder'
+import { APIGateway } from 'src/api/APIGateway.js'
+import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+
+const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
+
+export default {
+  name: 'EntityInput',
+  components: {
+    EntityIndex
+  },
+  mixins: [inputMixin],
+  props: {
+    value: {
+      default: null
+    },
+    selectionMode: {
+      type: String,
+      default: 'single'
+    }
+  },
+  emits: ['onInputClick'],
+  data () {
+    return {
+      selected: [],
+      api: APIGateway.product.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: ContentCategorySelectorComp, name: 'store_category', responseKey: 'store_category_info', categoryType: 'store', col: 'col-md-12 col-12' },
+        { type: 'select', name: 'is_physical', label: 'جنس محصول', options: (new Product()).is_physicalEnums, placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'select', name: 'physical_type', label: 'نوع محصول فیزیکی', options: (new Product()).physical_typeEnums, placeholder: ' ', col: 'col-md-6 col-12' }
+      ],
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'نام محصول',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'is_physical_info',
+            required: true,
+            label: 'جنس',
+            align: 'left',
+            field: row => (new Product(row)).is_physical_info.label
+          },
+          {
+            name: 'content_category',
+            required: true,
+            label: 'دسته فروشگاه',
+            align: 'left',
+            field: row => row.store_category_info?.parent?.parent?.title + '، ' + row.store_category_info?.parent?.title + '، ' + row.store_category_info?.title
+          },
+          {
+            name: 'unit_price',
+            required: true,
+            label: 'قیمت',
+            align: 'left',
+            field: row => parseInt(row.unit_price.toString()).toLocaleString('fa')
+          // },
+          // {
+          //   name: 'is_visible',
+          //   required: true,
+          //   label: 'نمایش',
+          //   align: 'left',
+          //   field: '-'
+          }
+        ]
+      },
+      itemIdentifyKey: 'id',
+      itemIndicatorKey: (product) => product ? product.title + '(' + product.id + ')' : '-'
+    }
+  },
+  watch: {
+    value: {
+      handler (newValue) {
+        if (newValue) {
+          this.selected = newValue
+        }
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    onInputClick(event) {
+      this.$emit('onInputClick', event)
+    },
+    onSelectedUpdate (values) {
+      let selected = (this.selectionMode === 'multiple') ? [] : null
+      values.forEach(vlue => {
+        if (this.selectionMode === 'multiple') {
+          selected.push(vlue)
+        } else {
+          selected = vlue
+        }
+      })
+      this.selected = selected
+      this.inputData = selected
+      if (this.selectionMode === 'multiple') {
+        this.$emit('update:value', selected.map(product => product.id))
+      } else {
+        this.$emit('update:value', selected.id)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+</style>

+ 13 - 2
src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/CreateQuestionForm.vue

@@ -44,11 +44,11 @@ export default {
   },
   props: {
     testId: {
-      type: Number,
+      type: [Number, String, Boolean],
       default: null
     },
     testSetId: {
-      type: Number,
+      type: [Number, String, Boolean],
       default: null
     },
     questionType: {
@@ -78,6 +78,14 @@ export default {
       ]
     }
   },
+  watch: {
+    questionType: {
+      handler (newValue) {
+        FormBuilderAssist.setAttributeByName(this.inputs, 'type', 'value', newValue)
+      },
+      immediate: true
+    }
+  },
   created() {
     if (this.testId) {
       this.api = APIGateway.test.APIAdresses.appendQuestion(this.testId)
@@ -110,6 +118,9 @@ export default {
       this.entityLoading = false
     },
     edit() {
+      // console.log(this.$refs.entityEdit.getFormData())
+      FormBuilderAssist.setAttributeByName(this.inputs, 'type', 'value', this.questionType)
+      // console.log(this.$refs.entityEdit.getFormData())
       this.entityLoading = true
       this.$refs.entityEdit.editEntity(false)
         .then((response) => {

+ 5 - 1
src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/QuestionsCreateAndAppend.vue

@@ -2,6 +2,10 @@
   <div class="ContentsSelector q-mt-md">
     <q-table :rows="testQuestionList"
              :columns="table.columns"
+             hide-pagination
+             :pagination="{
+               rowsPerPage: 0
+             }"
              row-key="name">
       <template #top>
         <q-banner class="full-width">
@@ -159,7 +163,7 @@ export default {
       type: [Number, String, Boolean]
     },
     testSetId: {
-      type: Number,
+      type: [Number, String, Boolean],
       default: null
     },
     unitId: {

+ 6 - 1
src/components/FormBuilderCustumComponents/QuestionsSelector/QuestionsSelector.vue

@@ -14,7 +14,9 @@
                       :options="sessionOptions"
                       :loading="sessions.loading"
                       emit-value
-                      map-options />
+                      clearable
+                      map-options
+                      @clear="clearFn()" />
           </div>
         </div>
         <div class="col-md-4 flex items-end">
@@ -395,6 +397,9 @@ export default {
           this.sessions.loading = false
         })
     },
+    clearFn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'session_template_questions__session_template', 'value', null)
+    },
     onChangeQuestionListPage (response) {
       this.updateloadedQuestionsByArray(response.data.results)
     },

+ 109 - 0
src/components/FormBuilderCustumComponents/TicketCategorySelector.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="TicketCategorySelector row q-col-gutter-md">
+    <div class="col-md-6 col-12">
+      <div>
+        معاونت
+      </div>
+      <q-select v-model="ticketCategoryType"
+                map-options
+                emit-value
+                :options="ticketCategoryTypeOptions"
+                :loading="ticketCategoryTypeOptions.laoding" />
+    </div>
+    <div class="col-md-6 col-12">
+      <div>
+        دسته
+      </div>
+      <q-select v-model="ticketCategory"
+                map-options
+                emit-value
+                :options="ticketCategoryOptions"
+                :loading="ticketCategoryTypeOptions.laoding" />
+    </div>
+  </div>
+</template>
+
+<script>
+import { APIGateway } from 'src/api/APIGateway.js'
+import { TicketCategory } from 'src/models/TicketCategory.js'
+
+export default {
+  name: 'TicketCategorySelector',
+  props: {
+    value: {
+      default: () => [],
+      type: [Array, String, Number, Boolean, Object]
+    },
+    disable: {
+      default: false,
+      type: Boolean
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      ticketCategoryTypeFirstChanged: false,
+      ticketCategoryType: null,
+      ticketCategory: null,
+      ticketCategoryTypeOptions: (new TicketCategory()).typeEnums,
+      ticketCategoryOptions: []
+    }
+  },
+  watch: {
+    value: {
+      handler: function (newValue) {
+        if (newValue?.type && newValue.id) {
+          this.ticketCategoryType = newValue.type
+          this.getCategories(this.ticketCategoryType)
+        } else {
+          this.inputData = this.value
+        }
+      },
+      immediate: true
+    },
+    ticketCategoryType (newValue) {
+      this.ticketCategory = null
+      this.ticketCategoryOptions = []
+      if (this.ticketCategoryTypeFirstChanged && newValue) {
+        this.getCategories(newValue)
+      } else {
+        this.ticketCategoryTypeFirstChanged = true
+      }
+    },
+    ticketCategory (newValue) {
+      this.$emit('update:value', newValue)
+    }
+  },
+  methods: {
+    getCategories (type) {
+      this.ticketCategoryTypeOptions.laoding = true
+
+      APIGateway.ticketCategory.index({ type, per_page: 99999 })
+        .then(categories => {
+          this.ticketCategoryOptions = categories.list.list.map(item => {
+            return {
+              item,
+              label: item.title,
+              value: item.id
+            }
+          })
+
+          let ticketCategory = null
+          if (this.value?.type && this.value.id) {
+            ticketCategory = this.value.id
+          }
+          if (this.value && !isNaN(this.value)) {
+            ticketCategory = this.value
+          }
+
+          this.$emit('update:value', ticketCategory)
+          this.ticketCategory = ticketCategory
+        })
+        .finally(() => {
+          this.ticketCategoryTypeOptions.laoding = false
+        })
+    }
+  }
+
+}
+</script>

+ 1 - 1
src/components/PostItem.vue

@@ -13,7 +13,7 @@
         </router-link>
       </div>
       <div class="child-category ellipsis-2-lines">
-        <router-link :to="{name: 'Public.PostCategory.Show', params: {category_id: post.category_info?.id}}">
+        <router-link :to="{name: 'Public.ContentCategory.Show', params: {category_id: post.category_info?.id}}">
           <q-icon name="account_tree" />
           دسته:
           {{post.category_info?.title}}

+ 120 - 28
src/components/Template/Header/MainHeader.vue

@@ -33,6 +33,16 @@
                    label="راهنما"
                    :to="{ name: 'Public.Post.List.Guide' }"
                    class="top-menu-item" />
+            <!--<q-btn stretch-->
+            <!--flat-->
+            <!--label="Shop"-->
+            <!--:to="{ name: 'Public.Shop' }"-->
+            <!--class="top-menu-item" />-->
+            <q-btn stretch
+                   flat
+                   label="آرشیو محتوا"
+                   :to="{ name: 'Public.ContentCategoryParentParent.List' }"
+                   class="top-menu-item" />
             <q-btn stretch
                    flat
                    label="فروشگاه"
@@ -51,6 +61,12 @@
                           clickable>
                     <q-item-section>کتب سطح دو</q-item-section>
                   </q-item>
+                  <q-item v-close-popup
+                          href="https://formafzar.com/form/9949w"
+                          target="_blank"
+                          clickable>
+                    <q-item-section>کتب سطح سه</q-item-section>
+                  </q-item>
                   <q-item v-close-popup
                           href="https://formafzar.com/form/dttpf"
                           target="_blank"
@@ -124,6 +140,20 @@
       </q-btn>
 
       <div class="left-side">
+        <q-btn flat
+               :to="{ name: 'Public.PostTopic.List' }"
+               class="btn-cart">
+          <q-icon name="search" />
+        </q-btn>
+        <q-btn v-if="isUserLogin"
+               flat
+               :to="{ name: 'Public.Cart' }"
+               class="btn-cart">
+          <q-badge>
+            {{ cartItemCount }}
+          </q-badge>
+          <q-icon name="shopping_cart" />
+        </q-btn>
         <q-btn v-if="user && user.id !== null"
                color="primary"
                @click="goToProperPanel">
@@ -203,7 +233,9 @@
 
 <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'
 
 export default {
   name: 'templateHeader',
@@ -215,6 +247,20 @@ export default {
     }
   },
   computed: {
+    cartItemCount () {
+      return this.basket.items_info.list.length
+    },
+    basket: {
+      get () {
+        return this.$store.getters['Shop/basket']
+      },
+      set (newValue) {
+        this.$store.commit('Shop/updateBasket', newValue)
+      }
+    },
+    isUserLogin () {
+      return this.user && this.user.id !== null
+    },
     canSeeAdminPanel () {
       return this.user.isAdmin() || this.user.isSuperUser() || this.user.isEducationalDepartmentHead() || this.user.isFinancialDepartmentHead()
     },
@@ -234,11 +280,24 @@ export default {
   },
   mounted () {
     this.loadAuthData()
+    if (this.isUserLogin) {
+      this.checkoutReview()
+    }
   },
   methods: {
     ...mapMutations('AppLayout', [
       'updateLayoutLeftDrawerVisible'
     ]),
+    checkoutReview () {
+      this.basket.loading = true
+      APIGateway.basket.checkoutReview()
+        .then((basket) => {
+          this.basket = new Basket(basket)
+        })
+        .finally(() => {
+          this.basket.loading = false
+        })
+    },
     loadAuthData () {
       this.user = this.$store.getters['Auth/user']
     },
@@ -288,23 +347,20 @@ export default {
 .drawer-btn {
   display: none;
   @media screen and (max-width: 1439px) {
-    display: block;
+    & {
+      display: block;
+    }
   }
   @media screen and (max-width: 599px) {
-    margin-bottom: 10px;
+    & {
+      margin-bottom: 10px;
+    }
   }
 }
 
 .right-side {
   display: flex;
   align-items: center;
-  @media screen and (max-width: 1023px) {
-    margin-left: 42px;
-  }
-  @media screen and (max-width: 599px) {
-    margin-left: 0;
-  }
-
   .breadcrumbs {
     &:deep(> *) {
       font-style: normal;
@@ -319,14 +375,27 @@ export default {
       }
     }
   }
+  @media screen and (max-width: 1023px) {
+    & {
+      margin-left: 42px;
+    }
+  }
+  @media screen and (max-width: 599px) {
+    & {
+      margin-left: 0;
+    }
+  }
+
 }
 
 .left-side {
   display: flex;
   justify-content: flex-end;
   @media screen and (max-width: 1439px) {
-    position: absolute;
-    right: 0;
+    & {
+      position: absolute;
+      right: 0;
+    }
   }
   @media screen and (max-width: 1023px) {
     .q-btn {
@@ -338,8 +407,10 @@ export default {
     }
   }
   @media screen and (max-width: 599px) {
-    right: 16px;
-    margin-bottom: 10px;
+    & {
+      right: 16px;
+      margin-bottom: 10px;
+    }
   }
 }
 </style>
@@ -349,14 +420,13 @@ export default {
   height: 120px;
   background-color: #FBF4EA;
   color: #212121;
-  @media only screen and (max-width: 1023px) {
-    height: 70px;
-  }
   //border-bottom: solid 1px #DFE1EC;
   &.isHomePage {
     background: transparent;
     @media only screen and (max-width: 1023px) {
-      background-color: #FBF4EA;
+      & {
+        background-color: #FBF4EA;
+      }
     }
   }
   .admin-top-menu {
@@ -407,15 +477,6 @@ export default {
     margin-left: 100px;
     margin-bottom: 22px;
 
-    @media only screen and (max-width: 1500px) {
-      margin-right: 10px;
-      margin-left: 10px;
-    }
-    @media only screen and (max-width: 1023px) {
-      padding-top: 0;
-      margin-bottom: 0;
-    }
-
     display: flex;
     flex-flow: row;
     justify-content: space-between;
@@ -454,10 +515,14 @@ export default {
       align-items: center;
       padding-right: 50px;
       @media only screen and (max-width: 1023px) {
-        height: 100%;
+        & {
+          height: 100%;
+        }
       }
       @media only screen and (max-width: 599px) {
-        padding-right: 0;
+        & {
+          padding-right: 0;
+        }
       }
     }
 
@@ -468,6 +533,24 @@ export default {
         }
       }
     }
+
+    @media only screen and (max-width: 1500px) {
+      & {
+        margin-right: 10px;
+        margin-left: 10px;
+      }
+    }
+    @media only screen and (max-width: 1023px) {
+      & {
+        padding-top: 0;
+        margin-bottom: 0;
+      }
+    }
+  }
+  @media only screen and (max-width: 1023px) {
+    & {
+      height: 70px;
+    }
   }
 }
 
@@ -530,6 +613,15 @@ export default {
         }
       }
     }
+    &.btn-cart {
+      background: white;
+      color: $primary;
+      margin-right: 20px;
+      .q-badge {
+        background: #FBF4EA;
+        color: $primary;
+      }
+    }
   }
 }
 </style>

+ 57 - 13
src/components/Template/Header/UserPanelHeader.vue

@@ -32,6 +32,15 @@
           </q-btn>
         </div>
         <div class="left-side">
+          <q-btn flat
+                 :to="{name: 'UserPanel.Notice.List'}">
+            <q-icon name="notifications_none" />
+            پیام ها
+            <q-badge v-if="myNotSeenNotificationsCount !== 0"
+                     color="warning">
+              {{ myNotSeenNotificationsCount }}
+            </q-badge>
+          </q-btn>
           <q-btn flat>
             <svg id="_000000ff"
                  data-name="#000000ff"
@@ -80,6 +89,7 @@
 <script>
 import { mapMutations } from 'vuex'
 import { User } from 'src/models/User.js'
+import { APIGateway } from 'src/api/APIGateway.js'
 
 export default {
   name: 'UserPanelHeader',
@@ -121,6 +131,14 @@ export default {
     }
   },
   computed: {
+    myNotSeenNotificationsCount: {
+      get () {
+        return this.$store.getters['Auth/myNotSeenNotificationsCount']
+      },
+      set (newValue) {
+        return this.$store.commit('Auth/updateMyNotSeenNotificationsCount', newValue)
+      }
+    },
     isAdminPage () {
       return this.$route.name && this.$route.name.includes('Admin.')
     },
@@ -135,6 +153,12 @@ export default {
     this.loadAuthData()
   },
   methods: {
+    getMyNotSeenNotificationsCount () {
+      APIGateway.noticeReceiver.myNotSeenNotificationsCount()
+        .then((count) => {
+          this.myNotSeenNotificationsCount = count
+        })
+    },
     loadAuthData () {
       this.user = this.$store.getters['Auth/user']
     },
@@ -189,9 +213,6 @@ export default {
       width: calc( 100% - 240px );
       display: flex;
       flex-flow: row;
-      @media screen and (max-width: 1023px) {
-        width: 100%;
-      }
       justify-content: space-between;
       .q-btn {
         height: 40px;
@@ -205,7 +226,9 @@ export default {
         align-items: center;
         padding-left: 50px;
         @media screen and (max-width: 1023px) {
-          padding-left: 0;
+          & {
+            padding-left: 0;
+          }
         }
       }
       .left-side {
@@ -214,7 +237,14 @@ export default {
         align-items: center;
         padding-right: 50px;
         @media screen and (max-width: 1023px) {
-          padding-right: 0;
+          & {
+            padding-right: 0;
+          }
+        }
+      }
+      @media screen and (max-width: 1023px) {
+        & {
+          width: 100%;
         }
       }
     }
@@ -237,7 +267,9 @@ export default {
 .top-menu-items {
   border-radius: 10px;
   @media screen and (max-width: 1023px) {
-    margin-left: 0;
+    & {
+      margin-left: 0;
+    }
   }
   @media only screen and (max-width: 500px) {
     .q-btn__content {
@@ -297,10 +329,14 @@ export default {
 .drawer-btn {
   display: none;
   @media screen and (max-width: 1439px) {
-    display: block;
+    & {
+      display: block;
+    }
   }
   @media screen and (max-width: 599px) {
-    margin-bottom: 10px;
+    & {
+      margin-bottom: 10px;
+    }
   }
 }
 
@@ -308,13 +344,19 @@ export default {
   display: flex;
   align-items: center;
   @media screen and (max-width: 1439px) {
-    margin-left: 78px;
+    & {
+      margin-left: 78px;
+    }
   }
   @media screen and (max-width: 1023px) {
-    margin-left: 42px;
+    & {
+      margin-left: 42px;
+    }
   }
   @media screen and (max-width: 599px) {
-    margin-left: 0;
+    & {
+      margin-left: 0;
+    }
   }
 
   .breadcrumbs {
@@ -341,8 +383,10 @@ export default {
     //right: 30px;
   }
   @media screen and (max-width: 599px) {
-    right: 16px;
-    margin-bottom: 10px;
+    & {
+      right: 16px;
+      margin-bottom: 10px;
+    }
   }
 }
 </style>

+ 95 - 30
src/components/Template/SideBard/AdminPanelSideBar.vue → src/components/Template/SideBar/AdminPanelSideBar.vue

@@ -42,7 +42,7 @@ export default {
           icon: 'import_contacts',
           routeName: null,
           show: true,
-          open: true,
+          open: false,
           active: false,
           children: [
             {
@@ -66,7 +66,7 @@ export default {
           icon: 'import_contacts',
           routeName: null,
           show: true,
-          open: true,
+          open: false,
           active: false,
           children: [
             {
@@ -83,31 +83,6 @@ export default {
             }
           ]
         },
-        {
-          title: 'حلقه های مباحثاتی',
-          roles: ['superuser', 'educational_department_head'],
-          icon: 'diversity_2',
-          routeName: null,
-          show: true,
-          open: true,
-          active: false,
-          children: [
-            {
-              title: 'درس ها و چارت آموزشی',
-              roles: ['superuser', 'educational_department_head'],
-              routeName: 'Admin.Category.DiscussionCircles.Index',
-              show: true,
-              active: false
-            },
-            {
-              title: 'حلقه های مباحثاتی',
-              roles: ['superuser', 'educational_department_head'],
-              routeName: 'Admin.Classroom.DiscussionCircles.Index',
-              show: true,
-              active: false
-            }
-          ]
-        },
         {
           title: 'درخواست های آموزش',
           roles: ['superuser', 'educational_department_head'],
@@ -122,7 +97,7 @@ export default {
           icon: 'live_tv',
           routeName: null,
           show: true,
-          open: true,
+          open: false,
           active: false,
           children: [
             {
@@ -139,6 +114,20 @@ export default {
               show: true,
               active: false
             },
+            {
+              title: 'دسته بندی موضوعی',
+              roles: ['superuser'],
+              routeName: 'Admin.ContentTopicCategory.List',
+              show: true,
+              active: false
+            },
+            {
+              title: 'لیست هشتگ ها',
+              roles: ['superuser'],
+              routeName: 'Admin.Hashtag.List',
+              show: true,
+              active: false
+            },
             {
               title: 'درخواست ها',
               roles: ['superuser'],
@@ -154,7 +143,7 @@ export default {
           icon: 'rss_feed',
           routeName: null,
           show: true,
-          open: true,
+          open: false,
           active: false,
           children: [
             {
@@ -173,13 +162,21 @@ export default {
             }
           ]
         },
+        {
+          title: 'احادیث',
+          roles: ['superuser'],
+          routeName: 'Admin.Hadith.List',
+          icon: 'person_add',
+          show: true,
+          active: false
+        },
         {
           title: 'مدیریت مالی',
           roles: ['superuser', 'financial_department_head'],
           icon: 'payments',
           routeName: null,
           show: true,
-          open: true,
+          open: false,
           active: false,
           children: [
             {
@@ -202,9 +199,77 @@ export default {
               routeName: 'Admin.Invoice.Ticket.List',
               show: true,
               active: false
+            },
+            {
+              title: 'دسته های کمک های مالی',
+              roles: ['superuser', 'financial_department_head'],
+              routeName: 'Admin.DonationCategory.List',
+              show: true,
+              active: false
+            },
+            {
+              title: 'لیست کمک های مالی',
+              roles: ['superuser', 'financial_department_head'],
+              routeName: 'Admin.Donation.List',
+              show: true,
+              active: false
             }
           ]
         },
+        {
+          title: 'فروشگاه',
+          roles: ['superuser'],
+          icon: 'storefront',
+          routeName: null,
+          show: true,
+          open: false,
+          active: false,
+          children: [
+            {
+              title: 'دسته بندی ها',
+              roles: ['superuser'],
+              routeName: 'Admin.Store.Category.List',
+              show: true,
+              active: false
+            },
+            {
+              title: 'محصولات',
+              roles: ['superuser'],
+              routeName: 'Admin.Store.Package.List',
+              show: true,
+              active: false
+            },
+            {
+              title: 'سفارشات',
+              roles: ['superuser'],
+              routeName: 'Admin.Store.Basket.List',
+              show: true,
+              active: false
+            },
+            {
+              title: 'کد تخفیف',
+              roles: ['superuser'],
+              routeName: 'Admin.Store.Discount.List',
+              show: true,
+              active: false
+            }
+            // {
+            //   title: 'تنظیمات',
+            //   roles: ['superuser'],
+            //   routeName: 'Admin.Store.Package.List',
+            //   show: true,
+            //   active: false
+            // }
+          ]
+        },
+        {
+          title: 'اعلان‌ها',
+          roles: ['superuser'],
+          routeName: 'Admin.Notice.List',
+          icon: 'notifications',
+          show: true,
+          active: false
+        },
         {
           title: 'تمام درخواست ها',
           roles: ['superuser'],

+ 52 - 27
src/components/Template/SideBard/Main.vue → src/components/Template/SideBar/Main.vue

@@ -60,6 +60,13 @@ export default {
           permission: 'all',
           show: true
         },
+        // {
+        //   title: 'Shop',
+        //   route: { name: 'Public.Shop' },
+        //   type: 'itemMenu',
+        //   permission: 'all',
+        //   show: true
+        // },
         {
           title: 'فروشگاه',
           type: 'simpleMenu',
@@ -74,6 +81,11 @@ export default {
               externalLink: 'https://formafzar.com/form/zmvhh',
               show: true
             },
+            {
+              title: 'کتب سطح سه',
+              externalLink: 'https://formafzar.com/form/9949w',
+              show: true
+            },
             {
               title: 'کتب سطح چهار',
               externalLink: 'https://formafzar.com/form/dttpf',
@@ -203,26 +215,13 @@ export default {
       width: 100%;
     }
   }
-
   .side-menu-body {
     display: grid;
     grid-template-rows: auto;
     .q-list {
       padding: 0;
       &.side-menu-list {
-        .search-input {
-          margin-bottom: 30px;
-        }
         margin: 0 24px 109px 24px;
-        @media screen and (max-width: 1919px) {
-          margin: 0 24px 34px 24px;
-        }
-        @media screen and (max-width: 1439px) {
-          margin: 0 21px 26px 21px;
-        }
-        @media screen and (max-width: 599px) {
-          margin: 0 18px 8px 18px;
-        }
 
         .top-separator {
           margin: 0 40px 32px 40px;
@@ -238,6 +237,24 @@ export default {
           padding: 0;
           min-height: 0;
         }
+        .search-input {
+          margin-bottom: 30px;
+        }
+        @media screen and (max-width: 1919px) {
+           & {
+             margin: 0 24px 34px 24px;
+           }
+        }
+        @media screen and (max-width: 1439px) {
+          & {
+            margin: 0 21px 26px 21px;
+          }
+        }
+        @media screen and (max-width: 599px) {
+          & {
+            margin: 0 18px 8px 18px;
+          }
+        }
       }
     }
     .log-out {
@@ -252,13 +269,6 @@ export default {
       align-items: center;
       padding: 0 14px 0 10px;
       margin: 0 0 36px 27px;
-      @media screen and (max-width: 1439px) {
-        margin: 0 31px 33px 31px;
-      }
-      @media screen and (max-width: 599px) {
-        margin: 0 30px 30px 30px;
-        //padding: 0 0 0 10px;
-      }
       &:hover {
         background-color: rgba(255, 255, 255, 0.1);
       }
@@ -268,6 +278,17 @@ export default {
         margin-right: 12px;
         transform: matrix(-1, 0, 0, 1, 0, 0);
       }
+      @media screen and (max-width: 1439px) {
+        & {
+          margin: 0 31px 33px 31px;
+        }
+      }
+      @media screen and (max-width: 599px) {
+        & {
+          margin: 0 30px 30px 30px;
+          //padding: 0 0 0 10px;
+        }
+      }
     }
   }
   .log-out {
@@ -281,13 +302,6 @@ export default {
     align-items: center;
     padding: 0 14px 0 10px;
     margin: 0 0 36px 27px;
-    @media screen and (max-width: 1439px) {
-      margin: 0 31px 33px 31px;
-    }
-    @media screen and (max-width: 599px) {
-      margin: 0 30px 30px 30px;
-      //padding: 0 0 0 10px;
-    }
     &:hover {
       background-color: rgba(255, 255, 255, 0.1);
     }
@@ -297,6 +311,17 @@ export default {
       margin-right: 12px;
       transform: matrix(-1, 0, 0, 1, 0, 0);
     }
+    @media screen and (max-width: 1439px) {
+      & {
+        margin: 0 31px 33px 31px;
+      }
+    }
+    @media screen and (max-width: 599px) {
+      & {
+        margin: 0 30px 30px 30px;
+        //padding: 0 0 0 10px;
+      }
+    }
   }
   &:deep(.side-menu-main-layout) {
     .q-expansion-item__container {

+ 3 - 3
src/components/Template/SideBard/TemplateSideBar.vue → src/components/Template/SideBar/TemplateSideBar.vue

@@ -5,9 +5,9 @@
 </template>
 
 <script>
-import MainSideBarTemplate from 'src/components/Template/SideBard/Main.vue'
-import UserPanelSideBar from 'src/components/Template/SideBard/UserPanelSideBar.vue'
-import AdminPanelSideBar from 'src/components/Template/SideBard/AdminPanelSideBar.vue'
+import MainSideBarTemplate from 'src/components/Template/SideBar/Main.vue'
+import UserPanelSideBar from 'src/components/Template/SideBar/UserPanelSideBar.vue'
+import AdminPanelSideBar from 'src/components/Template/SideBar/AdminPanelSideBar.vue'
 
 export default {
   name: 'TemplateSideBar',

+ 6 - 0
src/components/Template/SideBard/UserPanelSideBar.vue → src/components/Template/SideBar/UserPanelSideBar.vue

@@ -114,6 +114,12 @@ export default {
               show: true,
               active: false
             },
+            {
+              title: 'سفارش های من',
+              routeName: 'UserPanel.MyOrders.List',
+              show: true,
+              active: false
+            },
             {
               title: 'تاریخچه اتصال به درگاه بانک',
               routeName: 'UserPanel.Payment.TransactionList',

+ 0 - 829
src/components/VideoPlayer.vue

@@ -1,829 +0,0 @@
-<template>
-  <div ref="videoPlayerWrapper"
-       class="vPlayer">
-    <video ref="videoPlayer"
-           dir="ltr"
-           class="video-js vjs-fluid vjs-big-play-centered vjs-show-big-play-button-on-pause"
-           controls
-           preload="none">
-      <p class="vjs-no-js">
-        To view this video please enable JavaScript, and consider upgrading to a web browser that
-        <a href="https://videojs.com/html5-video-support/"
-           target="_blank">
-          supports HTML5 video
-        </a>
-      </p>
-    </video>
-
-    <div class="VastElements">
-      <div ref="VastTimerBtn"
-           class="VastElement VastTimerBtn hide" />
-      <div ref="VastSkipAdBtn"
-           class="VastElement VastSkipAdBtn hide">
-        رد کردن
-      </div>
-      <div ref="VastLinkBtn"
-           class="VastElement VastLinkBtn hide">
-        اطلاعات بیشتر
-      </div>
-    </div>
-
-    <div v-if="useOverPlayer && !isInVastMode"
-         ref="overPlayer"
-         class="over-player-wrapper">
-      <q-btn icon-right="isax:menu-1"
-             size="sm"
-             color="primary"
-             class="toggleSideBarBtn"
-             @click="toggleSideBar" />
-      <div class="over-player-slot"
-           :class="{'show': localOverPlayer, 'hide': !localOverPlayer}"
-           :style="{width: overPlayerWidth}">
-        <slot name="overPlayer" />
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import videojs from 'video.js'
-import 'videojs-hls-quality-selector'
-import videojsBrand from 'videojs-brand'
-import fa from 'video.js/dist/lang/fa.json'
-import { APIGateway } from 'src/api/APIGateway.js'
-// import { mixinAbrisham } from 'src/mixin/Mixins.js'
-import { PlayerSourceList } from 'src/models/PlayerSource.js'
-import videoJsResolutionSwitcher from 'src/assets/js/videoJsResolutionSwitcher.js'
-
-// https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8 (Live)
-// https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8
-
-export default {
-  name: 'VideoPlayer',
-  // mixins: [mixinAbrisham],
-  props: {
-    hasVast: {
-      type: Boolean,
-      default: false
-    },
-    source: {
-      type: [String, PlayerSourceList],
-      default: null
-    },
-    useOverPlayer: {
-      type: Boolean,
-      default: false
-    },
-    poster: {
-      type: String,
-      default () {
-        return ''
-      }
-    },
-    overPlayer: {
-      type: Boolean,
-      default: true
-    },
-    overPlayerWidth: {
-      type: String,
-      default: '100%'
-    },
-    keepCalculating: {
-      type: Boolean,
-      default () {
-        return true
-      }
-    },
-    bookmarkLoading: {
-      type: Boolean,
-      default () {
-        return false
-      }
-    },
-    currentTimed: {
-      type: Number
-    }
-  },
-  emits: ['seeked', 'adStarted', 'update:sideBar'],
-  data() {
-    return {
-      needReInitVideo: false,
-      isInVastMode: false,
-      vastSrc: null,
-      vastLink: null,
-      vastSkipOffset: null,
-      vastStartOffset: null,
-      vastTimerInterval: null,
-      width: '',
-      showVastElements: false,
-      drawer: false,
-      player: null,
-      localOverPlayer: true,
-      favLoading: false,
-      options: {
-        myItems: [{
-          name: 'test'
-        }],
-        controlBar: {
-          // currentTimeDisplay: true,
-          TimeDivider: true,
-          children: [
-            'playToggle',
-            'PlaybackRateMenuButton',
-            'CurrentTimeDisplay',
-            'progressControl',
-            'TimeDivider',
-            'RemainingTimeDisplay',
-            'volumePanel',
-            'SubtitlesButton',
-            'QualitySelector',
-            'fullscreenToggle',
-            'PictureInPictureToggle'
-          ],
-          volumePanel: {
-            inline: true,
-            vertical: true
-          }
-        },
-        language: 'fa',
-        languages: {
-          fa
-        },
-        responsive: true,
-        autoplay: false,
-        controls: true,
-        playbackRates: [0.5, 1, 1.5, 2, 3, 4],
-        nativeControlsForTouch: false,
-        sources: [],
-        poster: null,
-        plugins: {
-          // hotkeys: {
-          //   enableModifiersForNumbers: false,
-          //   seekStep: 5,
-          //   enableMute: true,
-          //   enableVolumeScroll: true,
-          //   enableHoverScroll: true,
-          //   enableFullscreen: true
-          // }
-        }
-      },
-      videoIsPlaying: false,
-      postIsFavored: {}
-    }
-  },
-  computed: {
-    calcTheHeight() {
-      return '100%'
-    },
-    calcTheWidth() {
-      return '100%'
-    },
-    currentTime() {
-      return this.currentTimed
-    }
-  },
-  watch: {
-    source: {
-      handler () {
-        if (typeof window === 'undefined') {
-          return
-        }
-        this.$nextTick(() => {
-          if (this.$refs.videoPlayer) {
-            this.reInitVideo()
-          } else {
-            this.needReInitVideo = true
-          }
-        })
-      },
-      immediate: true
-    },
-    currentTime(time) {
-      this.player.currentTime(time)
-    },
-    overPlayer(newValue) {
-      this.localOverPlayer = newValue
-    },
-    useOverPlayer (newValue) {
-      if (!newValue) {
-        return
-      }
-
-      this.$nextTick(() => {
-        this.moveSideBarElementIntoVideoPlayerElements()
-      })
-    }
-  },
-  created() {
-    this.width = this.overPlayerWidth
-    this.setPoster()
-    this.setSources()
-  },
-  mounted() {
-    if (this.needReInitVideo) {
-      this.$nextTick(() => {
-        this.reInitVideo()
-      })
-    }
-    if (this.useOverPlayer) {
-      this.$nextTick(() => {
-        this.moveSideBarElementIntoVideoPlayerElements()
-      })
-    }
-  },
-  beforeUnmount() {
-    if (this.player) {
-      this.player.dispose()
-    }
-  },
-  methods: {
-    getVast () {
-      return APIGateway.vast.getXml()
-        .then((vastXml) => {
-          this.loadVast(vastXml)
-        })
-        .catch(() => {
-        })
-    },
-    showVastElement (vastClassName) {
-      this.removeVastClass(vastClassName, 'hide')
-      this.addVastClass(vastClassName, 'show')
-    },
-    hideVastElement (vastClassName) {
-      this.removeVastClass(vastClassName, 'show')
-      this.addVastClass(vastClassName, 'hide')
-    },
-    addVastClass (vastClassName, classValue) {
-      const vastElement = this.vastElementExist(vastClassName)
-      if (!vastElement) {
-        return false
-      }
-
-      vastElement.classList.add(classValue)
-    },
-    removeVastClass (vastClassName, classValue) {
-      const vastElement = this.vastElementExist(vastClassName)
-      if (!vastElement) {
-        return false
-      }
-
-      vastElement.classList.remove(classValue)
-    },
-    vastElementExist (vastClassName) {
-      const vastElement = this.$refs.videoPlayerWrapper.getElementsByClassName(vastClassName)[0]
-      if (!vastElement) {
-        return false
-      }
-
-      return vastElement
-    },
-    loadVastDomElements () {
-      this.injectDomeElement(this.$refs.VastTimerBtn)
-      this.injectDomeElement(this.$refs.VastSkipAdBtn)
-      this.injectDomeElement(this.$refs.VastLinkBtn)
-    },
-    showVastLinkBtn (link, title = 'اطلاعات بیشتر') {
-      this.showVastElement('VastLinkBtn')
-      this.updateVastElementInnerHTML('VastLinkBtn', '<a href="' + link + '" target="_blank">' + title + '</a>')
-    },
-    stopVastTimer () {
-      if (this.vastTimerInterval) {
-        clearInterval(this.vastTimerInterval)
-      }
-      this.updateVastTimer(0)
-      this.hideVastElement('VastTimerBtn')
-    },
-    startVastTimer (endTimerCallback) {
-      this.showVastElement('VastTimerBtn')
-      if (this.vastTimerInterval) {
-        clearInterval(this.vastTimerInterval)
-      }
-      let seconds = this.getVastTimerSeconds()
-      if (seconds === 0) {
-        this.stopVastTimer()
-        return
-      }
-      this.updateVastTimer(seconds--)
-      this.vastTimerInterval = setInterval(() => {
-        this.updateVastTimer(seconds--)
-        if (seconds < 0) {
-          this.stopVastTimer()
-          endTimerCallback()
-        }
-      }, 1000)
-    },
-    getVastTimerSeconds () {
-      if (!this.vastSkipOffset) {
-        return 0
-      }
-
-      const array = this.vastSkipOffset.split(':')
-      return (array[0] * 60 * 60) + (array[1] * 60) + array[2]
-    },
-    updateVastTimer (timer) {
-      this.updateVastElementInnerHTML('VastTimerBtn', timer + ' ثانیه')
-    },
-    updateVastElementInnerHTML (vastClassName, innerHTML) {
-      const vastElement = this.vastElementExist(vastClassName)
-      if (!vastElement) {
-        return false
-      }
-
-      vastElement.innerHTML = innerHTML
-    },
-    sowVastSkipAdBtn () {
-      const vastClassName = 'VastSkipAdBtn'
-      this.showVastElement(vastClassName)
-      const vastElement = this.vastElementExist(vastClassName)
-      if (!vastElement) {
-        return false
-      }
-      vastElement.addEventListener('click', (event) => {
-        this.endVast()
-      })
-    },
-    loadVast (vastXml) {
-      const parser = new DOMParser()
-      const xmlDoc = parser.parseFromString(vastXml, 'text/xml')
-      this.vastSrc = xmlDoc.getElementsByTagName('MediaFile')[0].childNodes[0].nodeValue
-      this.vastLink = xmlDoc.getElementsByTagName('ClickThrough')[0].childNodes[0].nodeValue
-      this.vastStartOffset = xmlDoc.getElementsByTagName('Linear')[0].getAttribute('startoffset')
-      this.vastSkipOffset = xmlDoc.getElementsByTagName('Linear')[0].getAttribute('skipoffset')
-
-      this.player.addClass('vjs-ad-playing')
-
-      setTimeout(() => {
-        this.isInVastMode = true
-        const playerSourceList = new PlayerSourceList([{
-          src: this.vastSrc,
-          type: 'video/mp4',
-          label: 'کیفیت عالی',
-          caption: 'کیفیت عالی',
-          link: this.vastSrc,
-          res: 720,
-          selected: false
-        }])
-        const source = this.isPlayerSourceList() ? playerSourceList.list : this.vastSrc
-        this.player.src(source)
-      }, 100)
-
-      this.player.one('play', () => {
-        this.player.controlBar.progressControl.disable()
-        this.loadVastDomElements()
-
-        this.showVastLinkBtn(this.vastLink)
-        this.startVastTimer(() => {
-          this.sowVastSkipAdBtn()
-        })
-
-        this.$emit('adStarted')
-      })
-      this.player.one('ended', () => {
-        if (!this.isInVastMode) {
-          return
-        }
-        this.endVast()
-      })
-    },
-    startVast (vastXml) {
-      const parser = new DOMParser()
-      const xmlDoc = parser.parseFromString(vastXml, 'text/xml')
-      this.vastSrc = xmlDoc.getElementsByTagName('MediaFile')[0].childNodes[0].nodeValue
-      this.vastLink = xmlDoc.getElementsByTagName('ClickThrough')[0].childNodes[0].nodeValue
-      this.vastStartOffset = xmlDoc.getElementsByTagName('Linear')[0].getAttribute('startoffset')
-      this.vastSkipOffset = xmlDoc.getElementsByTagName('Linear')[0].getAttribute('skipoffset')
-
-      // in a real plugin, you might fetch ad inventory here
-      this.player.trigger('adsready')
-    },
-    endVast () {
-      this.isInVastMode = false
-      this.player.controlBar.progressControl.enable()
-      this.hideVastElement('VastTimerBtn')
-      this.hideVastElement('VastSkipAdBtn')
-      this.hideVastElement('VastLinkBtn')
-      this.player.removeClass('vjs-ad-playing')
-      // setTimeout(() => {
-      this.setPoster()
-      this.setSources()
-      const source = this.isPlayerSourceList() ? this.source.list : this.source
-      this.player.src(source)
-      this.player.poster(this.poster)
-      // this.player.reset()
-      this.player.play()
-      // }, 100)
-      this.$emit('adEnded')
-    },
-
-    focusOnPlayer () {
-      this.player.el().focus()
-    },
-    setPlayerBrand () {
-      this.player.brand({
-        image: 'https://nodes.alaatv.com/upload/landing/chatr/alaa%20logo.png?w=30&h=30',
-        title: 'پالیزوانی',
-        destination: '/',
-        destinationTarget: '_blank'
-      })
-    },
-    redefineTap () {
-      this.player.on('touchend', function(e) { // tap
-        if (this.player().controls()) {
-          const classes = [
-            'vjs-tech'
-          ]
-          const canDoAction = !classes.find(className => e.target.classList.contains(className))
-          if (canDoAction) {
-            return
-          }
-          if (this.player().paused()) {
-            this.player().play()
-          } else {
-            this.player().pause()
-          }
-        }
-      })
-    },
-    hasPlugin (pluginName) {
-      return Object.keys(videojs.getPlugins()).includes(pluginName)
-    },
-    initPlayer (withVast) {
-      if (!this.hasPlugin('brand')) {
-        videojs.registerPlugin('brand', videojsBrand)
-      }
-      // if (this.isPlayerSourceList(this.source) && !this.hasPlugin('videoJsResolutionSwitcher')) { // old multiple quality type
-      if (this.isPlayerSourceList(this.source)) { // old multiple quality type
-        videoJsResolutionSwitcher(videojs)
-        this.options.plugins.videoJsResolutionSwitcher = {
-          default: 'کیفیت بالا',
-          dynamicLabel: true
-        }
-      }
-
-      this.player = videojs(this.$refs.videoPlayer, this.options)
-      if (this.hasVast && (typeof withVast === 'undefined' || withVast === true)) {
-        // this.loadVast()
-        this.getVast()
-      }
-
-      this.player.ready(() => {
-        this.setPlayerBrand()
-        this.focusOnPlayer()
-        this.redefineTap()
-        // this.on('timeupdate', function () {
-        //   if (that.keepCalculating) {
-        //     that.calcWatchedPercentage(this.currentTime(), this.duration())
-        //   }
-        //   // document.querySelector('.video-js').focus()
-        //   if (that.$refs.videoPlayer) {
-        //     that.$refs.videoPlayer.focus()
-        //   }
-
-        //   if (!that.player.paused() && !that.player.userActive()) {
-        //     that.videoStatus(false)
-        //   } else if (!that.player.paused()) {
-        //     that.videoStatus(true)
-        //   }
-        // })
-      })
-
-      if (typeof this.source === 'string' && this.source.includes('.m3u8')) { // hls type
-        this.player.hlsQualitySelector()
-      }
-
-      this.player.on('seeked', () => {
-        this.$emit('seeked', this.player.currentTime())
-      })
-
-      // const events = [
-      //   'loadstart',
-      //   'progress',
-      //   'suspend',
-      //   'abort',
-      //   'error',
-      //   'emptied',
-      //   'stalled',
-      //   'loadedmetadata',
-      //   'loadeddata',
-      //   'canplay',
-      //   'canplaythrough',
-      //   'playing',
-      //   'waiting',
-      //   'seeking',
-      //   'seeked',
-      //   'ended',
-      //   'durationchange',
-      //   'timeupdate',
-      //   'play',
-      //   'pause',
-      //   'ratechange',
-      //   'resize',
-      //   'volumechange'
-      // ]
-      //
-      // events.forEach(event => {
-      //   this.player.on(event, () => {
-      //     console.log('event: ', event)
-      //     console.log('sources: ', this.player.sources)
-      //   })
-      // })
-    },
-    changeCurrentTime (time) {
-      if (!this.player) {
-        return
-      }
-      this.player.currentTime(time)
-    },
-    injectDomeElement (element) {
-      this.$refs.videoPlayerWrapper.querySelector('.video-js').appendChild(element)
-    },
-    createDomeElement (elementClass, refKey) {
-      const div = document.createElement('div')
-      div.classList = elementClass
-      const child = this.$refs[refKey].$el ? this.$refs[refKey].$el : this.$refs[refKey]
-      div.appendChild(child)
-      this.$refs.videoPlayerWrapper.querySelector('.video-js').appendChild(div)
-    },
-    moveSideBarElementIntoVideoPlayerElements () {
-      this.createDomeElement('over-player-wrapper-div', 'overPlayer')
-    },
-    toggleSideBar () {
-      this.localOverPlayer = !this.localOverPlayer
-      // this.$emit('update:sideBar', this.localOverPlayer)
-    },
-    activate(time) {
-      this.player.currentTime(time)
-      this.player.play()
-      const requiredElement = document.querySelector('.video-js')
-      requiredElement.focus()
-    },
-    setSources(sources) {
-      this.options.sources = sources || (this.isPlayerSourceList() ? this.source.list : this.source)
-    },
-    setPoster(poster) {
-      this.options.poster = poster || this.poster
-    },
-    reInitVideo(withVast) {
-      if (this.player?.reset) {
-        this.player.reset()
-      }
-      // this.player.dispose()
-      this.setPoster()
-      this.setSources()
-      this.initPlayer(withVast)
-      const source = this.isPlayerSourceList() ? this.source.list : this.source
-      this.player.src(source)
-      this.player.poster(this.poster)
-    },
-    isPlayerSourceList () {
-      return (this.source && this.source.list && Array.isArray(this.source.list))
-    },
-    calcWatchedPercentage(currentTime, duration) {
-      const watchedPercentage = ((currentTime / duration) * 100)
-      const videoPlayerTimeData = {
-        currentTime,
-        duration,
-        watchedPercentage
-      }
-      this.$emit('calcTimeData', videoPlayerTimeData)
-    },
-    videoStatus(val) {
-      this.videoIsPlaying = val
-    },
-    toggleFullScreen() {
-      if (document.exitFullscreen) {
-        document.exitFullscreen()
-      } else if (document.webkitExitFullscreen) { /* Safari */
-        document.webkitExitFullscreen()
-      } else if (document.msExitFullscreen) { /* IE11 */
-        document.msExitFullscreen()
-      }
-    }
-  }
-}
-</script>
-
-<style lang="scss">
-@import "video.js/dist/video-js.css";
-.vPlayer {
-  width: 100%;
-  overflow: hidden;
-  display: contents;
-  .VastElements {
-    display: none;
-  }
-  .VastElement {
-    position: absolute;
-    bottom: 60px;
-    height: 50px;
-    border: solid 2px $primary;
-    border-radius: 8px;
-    background: rgba(0, 0, 0, 0.3);
-    display: flex;
-    flex-flow: row;
-    justify-content: center;
-    align-items: center;
-    cursor: pointer;
-    transition: all 1s;
-    $width: 150px;
-    &.VastTimerBtn {
-      border-top-right-radius: 0;
-      border-bottom-right-radius: 0;
-      border-right: none;
-      width: $width;
-      cursor: not-allowed;
-      &.show {
-        right: 0;
-      }
-      &.hide {
-        right: -$width;
-      }
-    }
-    &.VastSkipAdBtn {
-      right: 0;
-      border-top-right-radius: 0;
-      border-bottom-right-radius: 0;
-      border-right: none;
-      width: $width;
-      color: white;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      font-size: 0.8rem;
-      &.show {
-        right: 0;
-      }
-      &.hide {
-        right: -$width;
-      }
-    }
-    &.VastLinkBtn {
-      left: 0;
-      border-top-left-radius: 0;
-      border-bottom-left-radius: 0;
-      border-left: none;
-      width: $width;
-      &.show {
-        left: 0;
-      }
-      &.hide {
-        left: -$width;
-      }
-      a {
-        width: 100%;
-        height: 100%;
-        color: white;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        font-size: 0.8rem;
-      }
-    }
-    @media screen and (max-width: 600px) {
-      height: 35px;
-      $screen-max-600-width: 100px;
-      &.VastTimerBtn {
-        width: $screen-max-600-width;
-        font-size: 0.7rem;
-        &.hide {
-          left: -$screen-max-600-width;
-        }
-      }
-      &.VastSkipAdBtn {
-        width: $screen-max-600-width;
-        font-size: 0.7rem;
-        &.hide {
-          left: -$screen-max-600-width;
-        }
-      }
-      &.VastLinkBtn {
-        width: $screen-max-600-width;
-        &.hide {
-          right: -$screen-max-600-width;
-        }
-        a {
-          font-size: 0.7rem;
-        }
-      }
-    }
-
-  }
-  .over-player-wrapper-div {
-    position: absolute;
-    top: 0;
-    left: 0;
-    height: 100%;
-    z-index: 1;
-    .over-player-wrapper {
-      position: absolute;
-      top: 0;
-      left: 0;
-      height: 100%;
-      .over-player-slot {
-        left: 2000px;
-        height: 100%;
-        color: initial;
-        transition: 0.4s;
-        direction: ltr;
-        position: relative;
-        z-index: 1;
-        &.show {
-          right: 0;
-        }
-        &.hide {
-          width: 0 !important;
-          right: 2500px;
-        }
-      }
-    }
-  }
-  .toggleSideBarBtn {
-    position: absolute;
-    top: 5px;
-    left: 5px;
-    z-index: 2;
-  }
-  .video-js {
-    overflow: hidden;
-    background-color: transparent;
-    &.vjs-ad-playing {
-      .vjs-resolution-button {
-        display: none;
-      }
-    }
-    .vjs-loading-spinner {
-      right: 50%;
-      margin: -25px -25px 0 0;
-      text-align: right;
-    }
-    .vjs-big-play-button {
-      color: white;
-      width: 80px;
-      height: 80px;
-      border-radius: 100%;
-      background: $primary;
-      border-color: $primary;
-
-      @media screen and (max-width: 600px) {
-        margin-top: -60px;
-      }
-      .vjs-icon-placeholder:before {
-        display: flex;
-        font-size: 65px;
-        align-items: center;
-        justify-content: center;
-      }
-    }
-    .vjs-control-bar {
-      z-index: 2;
-      .vjs-volume-panel {
-        .vjs-volume-control {
-          // right: -3.5em;
-          // margin-right: -1px;
-        }
-      }
-      .vjs-resolution-button {
-        .vjs-menu-button {
-          .vjs-icon-placeholder {
-            &:before {
-              content: "\f114";
-              font-style: normal;
-              font-weight: normal;
-              font-family: VideoJS, Arial, serif;
-            }
-          }
-        }
-      }
-    }
-    .vjs-play-progress {
-      &:before {
-        /*rtl:ignore*/
-        right: -0.5em;
-        /*rtl:ignore*/
-        left: auto;
-      }
-    }
-    .vjs-volume-tooltip {
-      float: right;
-    }
-    .vjs-volume-level {
-      right: 0;
-    }
-  }
-}
-
-.vjs-slider-horizontal .vjs-volume-level:before {
-  left: 0;
-  right: auto;
-}
-.video-js .vjs-volume-level {
-    /* right: 0; */
-}
-.video-js .vjs-volume-tooltip {
-    /* float: left; */
-}
-.vPlayer .video-js .vjs-control-bar .vjs-volume-panel .vjs-volume-control {
-    /* left: -3.5em; */
-    /* margin-left: -1px; */
-}
-</style>

+ 16 - 14
src/components/Widgets/Classroom/AdminClassroomCreate/AdminClassroomCreate.vue

@@ -27,7 +27,7 @@
       <template #after-form-builder>
         <div class="flex justify-end q-mt-md">
           <q-btn color="primary"
-                 :label="'ایجاد ' + classroomTypeTitle + 'جدید'"
+                 :label="'ایجاد ' + classroomTypeTitle + ' جدید'"
                  @click="createClassroom" />
         </div>
       </template>
@@ -43,8 +43,10 @@ import { mixinWidget } from 'src/mixin/Mixins.js'
 import { APIGateway } from 'src/api/APIGateway.js'
 import { FormBuilderAssist } from 'quasar-form-builder'
 import { UnitCategory } from 'src/models/UnitCategory.js'
+import FormBuilderDateTime from 'src/components/FormBuilderCustumComponents/FormBuilderDateTime.vue'
 import FormBuilderInputEditor from 'src/components/FormBuilderCustumComponents/FormBuilderInputEditor.vue'
 
+const FormBuilderDateTimeComp = shallowRef(FormBuilderDateTime)
 const FormBuilderInputEditorComp = shallowRef(FormBuilderInputEditor)
 
 export default {
@@ -112,16 +114,16 @@ export default {
         { type: 'select', name: 'professor', responseKey: 'professor', options: [], value: null, placeholder: ' ', label: 'استاد', col: 'col-md-3 col-12' },
 
         { type: 'separator', name: 'space', label: 'مشخصات اندیشه جو', className: 'custom-separator', col: 'col-12' },
-        { type: 'select', name: 'audience_status_wwwww', responseKey: 'audience_status_wwwww', options: [], placeholder: ' ', label: 'وضعیت(؟)', col: 'col-md-3 col-12' },
-        { type: 'select', name: 'category1', responseKey: 'category1', options: [], placeholder: ' ', label: 'مجموعه(؟)', col: 'col-md-3 col-12' },
+        // { type: 'select', name: 'audience_status_wwwww', responseKey: 'audience_status_wwwww', options: [], placeholder: ' ', label: 'وضعیت(؟)', col: 'col-md-3 col-12' },
+        // { type: 'select', name: 'category1', responseKey: 'category1', options: [], placeholder: ' ', label: 'مجموعه(؟)', col: 'col-md-3 col-12' },
         { type: 'select', name: 'audience_gender_type', responseKey: 'audience_gender_type', options: Enums.genders, placeholder: ' ', label: 'جنسیت', col: 'col-md-3 col-12' },
-        { type: 'select', name: 'audience_bashgah_wwwww', responseKey: 'audience_bashgah_wwwww', options: [], placeholder: ' ', label: 'باشگاه(؟)', col: 'col-md-3 col-12' },
+        // { type: 'select', name: 'audience_bashgah_wwwww', responseKey: 'audience_bashgah_wwwww', options: [], placeholder: ' ', label: 'باشگاه(؟)', col: 'col-md-3 col-12' },
 
         { type: 'separator', name: 'space', label: 'تنظیمات', className: 'custom-separator', col: 'col-12' },
         { type: 'input', name: 'capacity', responseKey: 'capacity', placeholder: ' ', label: 'ظرفیت گروه درسی', col: 'col-md-3 col-12' },
 
         { type: 'input', name: 'allowed_absence_count', responseKey: 'allowed_absence_count', placeholder: ' ', label: 'تعداد مجاز غیبت', col: 'col-md-3 col-12' },
-        { type: 'input', name: 'effective_absence_coefficient', responseKey: 'effective_absence_coefficient', placeholder: ' ', label: 'ضریب نمره حضور و غیاب', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'effective_absence_coefficient', responseKey: 'effective_absence_coefficient', placeholder: ' ', label: 'وزن نمره حضور و غیاب ', col: 'col-md-3 col-12' },
         { type: 'input', name: 'mandatory_assignment_count', responseKey: 'mandatory_assignment_count', placeholder: ' ', label: 'تعداد تکالیف اجباری', col: 'col-md-3 col-12' },
 
         { type: 'input', name: 'special_passing_mark', responseKey: 'special_passing_mark', placeholder: ' ', label: 'نمره قبولی مشروطی ها', col: 'col-md-3 col-12' },
@@ -129,24 +131,24 @@ export default {
         { type: 'input', name: 'minimum_clean_passing_mark', responseKey: 'minimum_clean_passing_mark', placeholder: ' ', label: 'نمره قبولی', col: 'col-md-3 col-12' },
 
         { type: 'separator', name: 'space', label: 'ثبت نام و حذف و اضافه', className: 'custom-separator', col: 'col-12' },
-        { type: 'dateTime', name: 'beginning_enrollment_period', responseKey: 'beginning_enrollment_period', placeholder: ' ', label: 'تاریخ شروع پیش ثبت نام', col: 'col-md-6 col-12' },
-        { type: 'dateTime', name: 'ending_enrollment_period', responseKey: 'ending_enrollment_period', placeholder: ' ', label: 'تاریخ پایان پیش ثبت نام', col: 'col-md-6 col-12' },
-        { type: 'dateTime', name: 'beginning_registration_period', responseKey: 'beginning_registration_period', placeholder: ' ', label: 'تاریخ شروع ثبت نام', col: 'col-md-6 col-12' },
-        { type: 'dateTime', name: 'ending_registration_period', responseKey: 'ending_registration_period', placeholder: ' ', label: 'تاریخ پایان ثبت نام', col: 'col-md-6 col-12' },
+        { type: FormBuilderDateTimeComp, name: 'beginning_enrollment_period', responseKey: 'beginning_enrollment_period', outsideLabel: 'تاریخ شروع پیش ثبت نام', col: 'col-md-6 col-12' },
+        { type: FormBuilderDateTimeComp, name: 'ending_enrollment_period', responseKey: 'ending_enrollment_period', outsideLabel: 'تاریخ پایان پیش ثبت نام', col: 'col-md-6 col-12' },
+        { type: FormBuilderDateTimeComp, name: 'beginning_registration_period', responseKey: 'beginning_registration_period', outsideLabel: 'تاریخ شروع ثبت نام', col: 'col-md-6 col-12' },
+        { type: FormBuilderDateTimeComp, name: 'ending_registration_period', responseKey: 'ending_registration_period', outsideLabel: 'تاریخ پایان ثبت نام', col: 'col-md-6 col-12' },
 
         { type: 'input', name: 'registration_period_refund_percent', responseKey: 'registration_period_refund_percent', placeholder: ' ', label: 'درصد برگشت هزینه انصراف در بازه ثبت نام', col: 'col-md-3 col-12' },
         { type: 'checkbox', name: 'allow_dropping_on_enrollment_period', responseKey: 'allow_dropping_on_enrollment_period', label: 'امکان حذف در بازه پیش ثبت نام', value: false, col: 'col-md-3 col-12' },
         { type: 'checkbox', name: 'allow_adding_on_registration_period', responseKey: 'allow_adding_on_registration_period', label: 'امکان اضافه کردن در بازه ثبت نام', value: false, col: 'col-md-3 col-12' },
         { type: 'checkbox', name: 'allow_dropping_on_registration_period', responseKey: 'allow_dropping_on_registration_period', label: 'امکان حذف ثبت نام در بازه ثبت نام', value: false, col: 'col-md-3 col-12' },
 
-        { type: 'dateTime', name: 'beginning_drop_add_period', responseKey: 'beginning_drop_add_period', placeholder: ' ', label: 'تاریخ شروع حذف و اضافه', col: 'col-md-6 col-12' },
-        { type: 'dateTime', name: 'ending_drop_add_period', responseKey: 'ending_drop_add_period', placeholder: ' ', label: 'تاریخ پایان حذف و اضافه', col: 'col-md-6 col-12' },
+        { type: FormBuilderDateTimeComp, name: 'beginning_drop_add_period', responseKey: 'beginning_drop_add_period', outsideLabel: 'تاریخ شروع حذف و اضافه', col: 'col-md-6 col-12' },
+        { type: FormBuilderDateTimeComp, name: 'ending_drop_add_period', responseKey: 'ending_drop_add_period', outsideLabel: 'تاریخ پایان حذف و اضافه', col: 'col-md-6 col-12' },
 
         { type: 'input', name: 'drop_add_period_refund_percent', responseKey: 'drop_add_period_refund_percent', placeholder: ' ', label: 'درصد برگشت هزینه انصراف در بازه حذف و اضافه', col: 'col-md-3 col-12' },
         { type: 'checkbox', name: 'allow_adding_on_drop_add_period', responseKey: 'allow_adding_on_drop_add_period', label: 'امکان اضافه شدن در بازه حذف و اضافه', value: false, col: 'col-md-3 col-12' },
         { type: 'checkbox', name: 'allow_dropping_on_drop_add_period', responseKey: 'allow_dropping_on_drop_add_period', label: 'امکان حذف در بازه حذف و اضافه', value: false, col: 'col-md-3 col-12' },
 
-        { type: 'dateTime', name: 'publish_time', responseKey: 'publish_time', placeholder: ' ', label: 'تاریخ انتشار', col: 'col-md-6 col-12' },
+        { type: FormBuilderDateTimeComp, name: 'publish_time', responseKey: 'publish_time', outsideLabel: 'تاریخ انتشار', col: 'col-md-6 col-12' },
 
         { type: 'separator', name: 'space', label: 'قوانین', className: 'custom-separator', col: 'col-12' },
         { type: FormBuilderInputEditorComp, name: 'rules', responseKey: 'rules', label: 'قوانین', col: 'col-12' },
@@ -157,8 +159,8 @@ export default {
 
         // -----------------------------------------------------------------------------------------------------------
 
-        { type: 'separator', name: 'space', label: 'موارد زیر در طرح نبودند', className: 'custom-separator', col: 'col-12' },
-        { type: 'select', name: 'audience_role', responseKey: 'audience_role', options: Enums.groups, placeholder: ' ', label: 'نقش', col: 'col-md-4 col-12' },
+        // { type: 'separator', name: 'space', label: 'موارد زیر در طرح نبودند', className: 'custom-separator', col: 'col-12' },
+        // { type: 'select', name: 'audience_role', responseKey: 'audience_role', options: Enums.groups, placeholder: ' ', label: 'نقش', col: 'col-md-4 col-12' },
 
         { type: 'hidden', name: 'id', responseKey: 'id', label: 'id', col: 'col-md-3 col-12' }
       ]

+ 967 - 0
src/components/Widgets/Classroom/AdminClassroomInfoOfUser/AdminClassroomInfoOfUser.vue

@@ -0,0 +1,967 @@
+<template>
+  <breadcrumbs style="margin-top: 29px; margin-bottom: 19px;" />
+  <div class="flex justify-end">
+    <q-btn flat
+           color="grey"
+           @click="$router.go(-1)">
+      بازگشت
+      >
+    </q-btn>
+  </div>
+  <q-card class="q-mb-md q-pa-sm">
+    <q-linear-progress v-show="ownerInfo.loading"
+                       indeterminate />
+    <div class="row items-center justify-center">
+      <div class="col-md-1">
+        <q-img :src="ownerInfo.picture" />
+      </div>
+      <div class="col-md-11">
+        <div class="row items-center">
+          <div class="col-md-4">
+            {{ ownerInfo.gender === 'MALE' ? 'آقای' : 'خانم' }}
+            {{ ownerInfo.fullname() }}
+            <br>
+            کد ملی:
+            {{ ownerInfo.national_code }}
+          </div>
+          <div class="col-md-4">
+            تلفن همراه جهت تماس:
+            {{ ownerInfo.mobile_number }}
+            <br>
+            ایمیل:
+            {{ ownerInfo.email }}
+          </div>
+          <div class="col-md-4">
+            <q-btn color="primary"
+                   :to="{ name: 'Admin.User.Show', params: { id: ownerId }}"
+                   target="_blank">
+              مشاهده پروفایل
+            </q-btn>
+          </div>
+        </div>
+      </div>
+    </div>
+  </q-card>
+  <q-card>
+    <q-tabs v-model="tab"
+            dense
+            class="text-grey"
+            active-color="primary"
+            indicator-color="primary"
+            align="justify"
+            narrow-indicator>
+      <q-tab name="classroomInfo"
+             label="اطلاعات دوره آموزشی" />
+      <q-tab v-if="canShowOtherTabsOfClassroom"
+             name="educations"
+             label="جدول آموزشی" />
+      <q-tab v-if="canShowOtherTabsOfClassroom"
+             name="team"
+             label="گروه" />
+      <q-tab v-if="canShowOtherTabsOfClassroom"
+             name="tests"
+             label="آزمون" />
+      <q-tab v-if="canShowOtherTabsOfClassroom"
+             name="projects"
+             label="پروژه" />
+      <q-tab v-if="canShowOtherTabsOfClassroom && classroom.live_streaming_url"
+             name="live_streaming_url"
+             label="بخش آنلاین" />
+    </q-tabs>
+
+    <q-separator />
+
+    <q-tab-panels v-model="tab"
+                  animated>
+      <q-tab-panel name="classroomInfo">
+        <div v-if="mounted && classroom.user_register_info.transcript_sheet_info.status"
+             class="bg-light-green-1 q-pa-md q-mb-md">
+          <div class="row q-col-gutter-md">
+            <div class="col-md-6 col-12">
+              نمره حضور و غیاب: {{ classroom.user_register_info.transcript_sheet_info.attendance_score }}
+              <br>
+              نمره آزمون: {{ classroom.user_register_info.transcript_sheet_info.highest_test_score }}
+              <br>
+              نمره نهایی: {{ classroom.user_register_info.transcript_sheet_info.final_score }}
+            </div>
+            <div class="col-md-6 col-12">
+              وضعیت نهایی: {{ getTranscriptSheetStatus(classroom.user_register_info.transcript_sheet_info) }}
+              <br>
+              <q-badge :color="classroom.user_register_info.transcript_sheet_info.is_sent_by_post ? 'green' : 'red'"
+                       outline>
+                <template v-if="classroom.user_register_info.transcript_sheet_info.is_sent_by_post">
+                  ارسال شده با پست
+                </template>
+                <template v-else>
+                  پست نشده است
+                </template>
+              </q-badge>
+              <br>
+              <a v-if="classroom.user_register_info.transcript_sheet_info.certification"
+                 :href="classroom.user_register_info.transcript_sheet_info.certification"
+                 target="_blank">
+                <q-btn outline
+                       color="primary">
+                  دانلود مدرک
+                </q-btn>
+              </a>
+            </div>
+          </div>
+        </div>
+        <show-classroom-info v-if="classroom.id"
+                             :options="{ profileMode: true }"
+                             :classroom="classroom"
+                             @onDrop="getClassroom" />
+        <q-skeleton v-else
+                    type="circle"
+                    size="100px"
+                    class="q-mt-md" />
+      </q-tab-panel>
+
+      <q-tab-panel name="educations"
+                   class="q-pa-none">
+        <entity-index v-if="mounted"
+                      ref="entityIndex"
+                      v-model:value="sessionsInputs"
+                      title="لیست جلسات"
+                      :api="sessionsApi"
+                      :table="sessionsTable"
+                      :table-keys="sessionsTableKeys"
+                      :table-grid-size="$q.screen.lt.sm"
+                      :show-expand-button="false"
+                      :show-reload-button="false"
+                      :show-search-button="false">
+          <template #entity-index-table-cell="{inputData}">
+            <template v-if="inputData.col.name === 'number'">
+              {{ inputData.rowNumber }}
+            </template>
+            <template v-else-if="inputData.col.name === 'action'">
+              <q-btn size="md"
+                     color="primary"
+                     label="جزییات"
+                     :to="{name: 'Admin.Session.UserClass', params: {session_id: inputData.props.row.id, user_id: $route.params.user_id}}" />
+            </template>
+            <template v-else>
+              {{ inputData.col.value }}
+            </template>
+          </template>
+          <template #entity-index-table-item-cell="{inputData}">
+            <entity-index-grid-item :input-data="inputData">
+              <template #col="{col, row}">
+                <template v-if="col.name === 'number'">
+                  {{ inputData.rowNumber }}
+                </template>
+                <template v-else-if="col.name === 'action'">
+                  <q-btn size="md"
+                         color="primary"
+                         label="جزییات"
+                         :to="{name: 'UserPanel.Profile.SessionInfo', params: {id: row.id}}" />
+                </template>
+              </template>
+            </entity-index-grid-item>
+          </template>
+        </entity-index>
+      </q-tab-panel>
+
+      <q-tab-panel name="team"
+                   class="q-pa-none">
+        <entity-index v-if="mounted"
+                      ref="teamsList"
+                      v-model:value="teamsListInputs"
+                      title="لیست گروه ها"
+                      :api="teamsListApi"
+                      :table="teamsListTable"
+                      :table-keys="teamsListTableKeys"
+                      :show-search-button="false"
+                      :show-reload-button="false"
+                      :show-expand-button="false">
+          <template v-slot:entity-index-table-cell="{inputData}">
+            <template v-if="inputData.col.name === 'number'">
+              {{ inputData.rowNumber }}
+            </template>
+            <template v-else-if="inputData.col.name === 'action'">
+              <div class="action-column-entity-index">
+                <q-btn size="md"
+                       color="primary"
+                       :flat="inputData.props.row.join_status !== 'ENABLED'"
+                       :disable="inputData.props.row.join_status !== 'ENABLED'"
+                       :label="getTeamJoinStatusLabel(inputData.props.row)"
+                       class="q-mr-md"
+                       :loading="joinTeamLoading"
+                       @click="joinTeam(inputData.props.row)" />
+              </div>
+            </template>
+            <template v-else>
+              {{ inputData.col.value }}
+            </template>
+          </template>
+        </entity-index>
+        <q-skeleton v-else
+                    type="rect"
+                    height="200px" />
+      </q-tab-panel>
+
+      <q-tab-panel name="tests"
+                   class="q-pa-none">
+        <entity-index v-if="mounted"
+                      ref="entityIndex"
+                      v-model:value="testListInputs"
+                      title="آزمون ها"
+                      :api="testListApi"
+                      :table="testListTable"
+                      :table-keys="testListTableKeys"
+                      :table-grid-size="$q.screen.lt.sm"
+                      :show-expand-button="false"
+                      :show-reload-button="false"
+                      :show-search-button="false">
+          <template #entity-index-table-cell="{inputData}">
+            <template v-if="inputData.col.name === 'number'">
+              {{ inputData.rowNumber }}
+            </template>
+            <template v-else-if="inputData.col.name === 'action'">
+              <q-btn v-if="inputData.props.row.is_enabled_attending"
+                     size="md"
+                     color="primary"
+                     label="شرکت در آزمون"
+                     :to="{name: 'UserPanel.Test.AnswerBook.Confirmation', params: {test_id: inputData.props.row.test, answer_book_id: inputData.props.row.id}}" />
+              <q-btn v-if="inputData.props.row.is_enabled_continuing"
+                     size="md"
+                     color="primary"
+                     label="ادامه آزمون"
+                     @click="showParticipateTypeDialog(inputData.props.row.test, inputData.props.row.id)" />
+              <q-btn v-if="inputData.props.row.is_enabled_objecting"
+                     size="md"
+                     color="primary"
+                     label="ثبت اعتراض"
+                     :to="{name: 'UserPanel.Test.AnswerBook.Show', params: {test_id: inputData.props.row.test, answer_book_id: inputData.props.row.id}, query: { send_objection: 1}}" />
+              <q-btn v-if="inputData.props.row.is_enabled_viewing"
+                     size="md"
+                     color="primary"
+                     label="مشاهده"
+                     :to="{name: 'UserPanel.Test.AnswerBook.Show', params: {test_id: inputData.props.row.test, answer_book_id: inputData.props.row.id}}" />
+            </template>
+            <template v-else>
+              {{ inputData.col.value }}
+            </template>
+          </template>
+          <template #entity-index-table-item-cell="{inputData}">
+            <entity-index-grid-item :input-data="inputData">
+              <template #col="{col, row}">
+                <template v-if="col.name === 'number'">
+                  {{ inputData.rowNumber }}
+                </template>
+                <template v-else-if="col.name === 'action'">
+                  <q-btn v-if="row.is_enabled_attending"
+                         size="md"
+                         color="primary"
+                         label="شرکت در آزمون"
+                         :to="{name: 'UserPanel.Test.AnswerBook.Confirmation', params: {test_id: row.test, answer_book_id: row.id}}" />
+                  <q-btn v-if="row.is_enabled_continuing"
+                         size="md"
+                         color="primary"
+                         label="ادامه آزمون"
+                         @click="showParticipateTypeDialog(row.test, row.id)" />
+                  <q-btn v-if="row.is_enabled_objecting"
+                         size="md"
+                         color="primary"
+                         label="ثبت اعتراض"
+                         :to="{name: 'UserPanel.Test.AnswerBook.Show', params: {test_id: row.test, answer_book_id: row.id}, query: { send_objection: 1}}" />
+                  <q-btn v-if="row.is_enabled_viewing"
+                         size="md"
+                         color="primary"
+                         label="مشاهده"
+                         :to="{name: 'UserPanel.Test.AnswerBook.Show', params: {test_id: row.test, answer_book_id: row.id}}" />
+                </template>
+              </template>
+            </entity-index-grid-item>
+          </template>
+        </entity-index>
+        <q-skeleton v-else
+                    type="rect"
+                    height="200px" />
+        <q-dialog v-model="participateTypeDialog">
+          <q-card>
+            <q-card-actions>
+              نحوه پاسخ به آزمون
+            </q-card-actions>
+            <q-separator />
+            <q-card-actions>
+              <div>
+                اندیشه جوی عزیز لطفا نحوه پاسخ به سوالات آزمون را مشخص کنید
+              </div>
+              <div>
+                <q-radio v-model="participateType"
+                         val="UserPanel.Test.AnswerBook.Participate.AllQuestions"
+                         label="مایلم پاسخ سوالات را به صورت یک فایل جامع بارگذاری کنم" />
+                <q-radio v-model="participateType"
+                         val="UserPanel.Test.AnswerBook.Participate.SingleQuestion"
+                         label="مایلم پاسخ سوالات را به صورت جداگانه ذیل همان سوال وارد کنم" />
+              </div>
+            </q-card-actions>
+            <q-card-actions>
+              <q-btn v-close-popup
+                     outline
+                     label="انصراف از آزمون"
+                     color="red" />
+              <q-btn :disable="!participateType"
+                     label="تایید و شروع آزمون"
+                     color="primary"
+                     @click="startTest" />
+            </q-card-actions>
+          </q-card>
+        </q-dialog>
+      </q-tab-panel>
+
+      <q-tab-panel name="projects"
+                   class="q-pa-none">
+        <entity-index v-if="mounted"
+                      ref="projectList"
+                      v-model:value="projectListInputs"
+                      title="لیست پروژه ها"
+                      :api="projectListApi"
+                      :table="projectListTable"
+                      :table-keys="projectListTableKeys"
+                      :show-search-button="false"
+                      :show-reload-button="false"
+                      :show-expand-button="false">
+          <template v-slot:entity-index-table-cell="{inputData}">
+            <template v-if="inputData.col.name === 'number'">
+              {{ inputData.rowNumber }}
+            </template>
+            <template v-else-if="inputData.col.name === 'actions'">
+              <div class="action-column-entity-index">
+                <q-btn size="md"
+                       color="primary"
+                       label="جزییات"
+                       :to="{name: 'Admin.Classroom.Project.UserInfo', params: {classroom_id: classroomId, project_id: inputData.props.row.id, user_id: $route.params.user_id}}"
+                       class="q-mr-md" />
+              </div>
+            </template>
+            <template v-else>
+              {{ inputData.col.value }}
+            </template>
+          </template>
+        </entity-index>
+        <q-skeleton v-else
+                    type="rect"
+                    height="200px" />
+      </q-tab-panel>
+      <q-tab-panel v-if="classroom.live_streaming_url"
+                   name="live_streaming_url">
+        <q-btn class="full-width q-mb-lg"
+               color="primary"
+               @click="goToLiveStreamUrl">
+          رفتن به کلاس
+        </q-btn>
+        <q-banner>
+          <div class="q-mb-md">
+            نام کاربری اسکای روم:
+            {{ user.national_code }}
+          </div>
+          <div>
+            کلمه عبور اسکای روم:
+            1234
+          </div>
+        </q-banner>
+        <iframe :src="classroom.live_streaming_url"
+                title="W3Schools Free Online Web Tutorials"
+                style="height: 80vh"
+                class="full-width" />
+      </q-tab-panel>
+    </q-tab-panels>
+  </q-card>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { Team } from 'src/models/Team.js'
+import { Test } from 'src/models/Test.js'
+import Enums from 'src/assets/Enums/Enums.js'
+import { mixinAuth } from 'src/mixin/Mixins.js'
+import ShamsiDate from 'src/assets/ShamsiDate.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { Classroom } from 'src/models/Classroom.js'
+import { AnswerBook } from 'src/models/AnswerBook.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import ShowClassroomInfo from './components/ShowClassroomInfo.vue'
+import EntityIndexGridItem from 'src/components/EntityIndexGridItem.vue'
+import { RegistrationList } from 'src/models/Registration.js'
+import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
+import { ProjectAttendanceSheets } from 'src/models/ProjectAttendanceSheets.js'
+import { SessionAttendanceSheets } from 'src/models/SessionAttendanceSheets.js'
+import { TranscriptSheet } from 'src/models/TranscriptSheet'
+import { User } from 'src/models/User'
+
+export default {
+  name: 'UserPanel.Profile.ClassroomInfo',
+  components: {
+    EntityIndex,
+    Breadcrumbs,
+    ShowClassroomInfo,
+    EntityIndexGridItem
+  },
+  mixins: [mixinAuth],
+  data () {
+    const userId = this.$route.params.user_id
+    const classroomId = this.$route.params.classroom_id
+    return {
+      mounted: false,
+      ownerInfo: new User(),
+      selectedTest: {
+        testId: null,
+        answerBookId: null
+      },
+      participateTypeDialog: false,
+      participateType: null,
+      tab: 'classroomInfo',
+      inputs: [],
+      sessionsInputs: [
+        { type: 'hidden', name: 'owner', value: userId },
+        { type: 'hidden', name: 'classroom', value: classroomId }
+      ],
+      api: APIGateway.classroom.get(classroomId),
+      sessionsApi: APIGateway.session.APIAdresses.attendanceSheets,
+      classroom: new Classroom(),
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'زمان شروع',
+            align: 'left',
+            field: row => row.beginning_time ? ShamsiDate.getDateTime(row.beginning_time) : '-'
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'زمان پایان',
+            align: 'left',
+            field: row => row.ending_time ? ShamsiDate.getDateTime(row.ending_time) : '-'
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: ''
+          }
+        ]
+      },
+      sessionsTable: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'زمان شروع',
+            align: 'left',
+            field: row => row.beginning_time ? ShamsiDate.getDateTime(row.beginning_time) : '-'
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'زمان پایان',
+            align: 'left',
+            field: row => row.ending_time ? ShamsiDate.getDateTime(row.ending_time) : '-'
+          },
+          {
+            name: 'assignment_status',
+            required: true,
+            label: 'وضعیت تکلیف',
+            align: 'left',
+            field: row => (new SessionAttendanceSheets(this.getCurrentUserAttendanceSheet(row))).assignment_status_info.label
+          },
+          {
+            name: 'attendance_status',
+            required: true,
+            label: 'وضعیت حضور و غیاب',
+            align: 'left',
+            field: row => (new SessionAttendanceSheets(this.getCurrentUserAttendanceSheet(row))).attendance_status_info.label
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: ''
+          }
+        ]
+      },
+      sessionsTableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+
+      projectListInputs: [
+        { type: 'hidden', name: 'owner', value: userId },
+        { type: 'hidden', name: 'classroom', value: classroomId }
+      ],
+      // projectListApi: APIGateway.projectAttendanceSheets.APIAdresses.base,
+      projectListApi: APIGateway.project.APIAdresses.attendanceSheetsList,
+      projectListTable: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان پروژه',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'creation_time',
+            required: true,
+            label: 'زمان شروع',
+            align: 'left',
+            field: row => row?.beginning_doing_period ? ShamsiDate.getDateTime(row.beginning_doing_period) : '-'
+          },
+          {
+            name: 'creation_time',
+            required: true,
+            label: 'زمان پایان',
+            align: 'left',
+            field: row => row?.ending_doing_period ? ShamsiDate.getDateTime(row.ending_doing_period) : '-'
+          },
+          {
+            name: 'status',
+            required: true,
+            label: 'وضعیت',
+            align: 'left',
+            field: row => (new ProjectAttendanceSheets(this.getProjectAttendanceSheets(row))).answer_status_info.label
+          },
+          {
+            name: 'actions',
+            required: true,
+            label: 'عملیات',
+            align: 'left',
+            field: ''
+          }
+        ]
+      },
+      projectListTableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+
+      registrations: new RegistrationList(),
+
+      joinTeamLoading: false,
+      teamsListInputs: [
+        { type: 'hidden', name: 'owner', value: userId },
+        { type: 'hidden', name: 'classroom', value: classroomId }
+      ],
+      teamsListApi: APIGateway.team.APIAdresses.classroomTeamsWithJoinStatus,
+      teamsListTable: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان گروه',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'leader_info',
+            required: true,
+            label: 'سرگروه',
+            align: 'left',
+            field: row => row.leader_info?.firstname + ' ' + row.leader_info?.lastname
+          },
+          {
+            name: 'used_capacity',
+            required: true,
+            label: 'ظرفیت استفاده شده',
+            align: 'left',
+            field: row => row.used_capacity
+          },
+          {
+            name: 'capacity',
+            required: true,
+            label: 'ظرفیت',
+            align: 'left',
+            field: row => row.capacity
+          },
+          {
+            name: 'action',
+            required: true,
+            label: ' ',
+            align: 'left',
+            field: ''
+          }
+        ],
+        data: []
+      },
+      teamsListTableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+
+      testListInputs: [
+        { type: 'hidden', name: 'owner', value: userId },
+        { type: 'hidden', name: 'test__classroom', value: classroomId }
+      ],
+      testListApi: APIGateway.answerBook.APIAdresses.base,
+      testListTable: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه آزمون',
+            align: 'left',
+            field: row => row.test_info.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان آزمون',
+            align: 'left',
+            field: row => row.test_info?.title
+          },
+          {
+            name: 'start_time',
+            required: true,
+            label: 'زمان شروع',
+            align: 'left',
+            field: row => row.test_info?.start_time ? ShamsiDate.getDateTime(row.test_info.start_time) : '-'
+          },
+          {
+            name: 'end_time',
+            required: true,
+            label: 'زمان پایان',
+            align: 'left',
+            field: row => row.test_info?.end_time ? ShamsiDate.getDateTime(row.test_info.end_time) : '-'
+          },
+          {
+            name: 'duration_deadline',
+            required: true,
+            label: 'مدت آزمون(دقیقه)',
+            align: 'left',
+            field: row => row.test_info.duration_deadline
+          },
+          {
+            name: 'test_questions.length',
+            required: true,
+            label: 'تعداد سوالات',
+            align: 'left',
+            field: row => row.test_info?.test_questions_length
+          },
+          {
+            name: 'grade',
+            required: true,
+            label: 'نمره نهایی',
+            align: 'left',
+            field: row => (row.status === 'GRADED' || row.status === 'OBJECTED' || row.status === 'FINALIZED') ? row.total_score : ''
+          },
+          {
+            name: 'test_info_status',
+            required: true,
+            label: 'وضعیت آزمون',
+            align: 'left',
+            field: row => (new Test(row.test_info)).status_info.label
+          },
+          {
+            name: 'result_status',
+            required: true,
+            label: 'وضعیت تصحیح',
+            align: 'left',
+            field: row => (new AnswerBook(row)).status_info.label
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'عملیات',
+            align: 'left',
+            field: ''
+          }
+        ]
+      },
+      testListTableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      }
+    }
+  },
+  computed: {
+    ownerId () {
+      return this.$route.params.user_id
+    },
+    classroomId () {
+      return this.$route.params.classroom_id
+    },
+    canShowOtherTabsOfClassroom () {
+      return !this.classroom.user_register_info.loading && (this.classroom.user_register_info.status === 'REGISTERED' || this.classroom.user_register_info.status === 'ENROLLED')
+    },
+    selectedCategoryId () {
+      return FormBuilderAssist.getInputsByName(this.inputs, 'category')?.value
+    }
+  },
+  watch: {
+    selectedCategoryId () {
+      this.setInputValue('unit', null)
+      this.getUnits(this.selectedCategoryId)
+    }
+  },
+  mounted () {
+    this.$store.commit('Test/updateAnswerBook', null)
+    this.getUserInfo()
+    this.getClassroom()
+    this.mounted = true
+  },
+  methods: {
+    getClassroom () {
+      this.classroom.loading = true
+      APIGateway.classroom.getClassroomAndCorrespondingRegistration(this.classroomId, this.ownerId)
+        .then((classroom) => {
+          this.classroom = new Classroom(classroom)
+          if (!this.classroom.user_register_info.id) {
+            this.getRegistrationInfo()
+          }
+          this.onloadnClassroom()
+        })
+        .finally(() => {
+          this.classroom.loading = false
+        })
+    },
+    getUserInfo () {
+      this.ownerInfo.loading = true
+      APIGateway.user.get({ data: { id: this.ownerId } })
+        .then((user) => {
+          this.ownerInfo = new User(user)
+        })
+        .finally(() => {
+          this.ownerInfo.loading = false
+        })
+    },
+    showParticipateTypeDialog (testId, answerBookId) {
+      this.$store.commit('Test/updateAnswerBook', null)
+      this.selectedTest.testId = testId
+      this.selectedTest.answerBookId = answerBookId
+      this.participateTypeDialog = true
+    },
+    startTest () {
+      this.$store.commit('Test/updateAnswerBook', null)
+      this.$router.push({ name: this.participateType, params: { test_id: this.selectedTest.testId, answer_book_id: this.selectedTest.answerBookId, question_number: 1 } })
+    },
+    getTeamJoinStatusLabel (team) {
+      if (team.join_status === 'ENABLED') {
+        return 'عضو شوید'
+      }
+      return (new Team(team)).join_status_info.label
+    },
+    joinTeam (team) {
+      this.joinTeamLoading = true
+      APIGateway.team.register(team.id)
+        .then(() => {
+          this.joinTeamLoading = false
+          this.$refs.teamsList.search()
+        })
+        .catch(() => {
+          this.joinTeamLoading = false
+          this.$refs.teamsList.search()
+        })
+    },
+    getRegistrationInfo () {
+      this.registrations.loading = true
+      this.classroom.user_register_info.loading = true
+      APIGateway.registration.index({ classroom: this.classroomId })
+        .then(({ list }) => {
+          this.registrations = new RegistrationList(list)
+          if (this.registrations.list.length > 0) {
+            this.classroom.user_register_info = this.registrations.list[0]
+          }
+          this.registrations.loading = false
+          this.classroom.user_register_info.loading = false
+          this.onloadnClassroom()
+        })
+        .catch(() => {
+          this.registrations.loading = false
+          this.classroom.user_register_info.loading = false
+        })
+    },
+    getCurrentUserAttendanceSheet (session) {
+      if (!session?.session_attendance_sheets || session.session_attendance_sheets.length === 0) {
+        return null
+      }
+
+      return session.session_attendance_sheets[0]
+    },
+    getProjectAttendanceSheets (project) {
+      if (!project?.project_attendance_sheets || project.project_attendance_sheets.length === 0) {
+        return null
+      }
+
+      return project.project_attendance_sheets[0]
+    },
+    goToLiveStreamUrl () {
+      window.open(this.classroom.live_streaming_url, '_blank')
+    },
+    onloadnClassroom () {
+      this.$store.commit('AppLayout/updateBreadcrumbs', {
+        visible: true,
+        loading: false,
+        path: [
+          {
+            label: 'دوره های کاربر',
+            to: { name: 'UserPanel.Profile.AllClassrooms' }
+          },
+          {
+            label: this.classroom.title,
+            to: { name: 'UserPanel.Profile.ClassroomInfo', params: { id: this.classroom.id } }
+          }
+        ]
+      })
+    },
+    search () {
+      this.$refs.entityIndex.search()
+    },
+    getClassroomHoldingTypeTitle (type) {
+      const target = Enums.classroomHoldingTypes.find(item => item.value === type)
+      if (!target) {
+        return '-'
+      }
+
+      return target.label
+    },
+    getClassroomStatusTitle (type) {
+      const target = Enums.classroomStatuses.find(item => item.value === type)
+      if (!target) {
+        return '-'
+      }
+
+      return target.label
+    },
+    getInvoiceStatusTitle (type) {
+      const target = Enums.invoiceStatus.find(item => item.value === type)
+      if (!target) {
+        return '-'
+      }
+
+      return target.label
+    },
+    loadInputDataOptions () {
+      this.getCategories()
+      this.getUnits()
+    },
+    setInputValue (name, value) {
+      const inputIndex = this.inputs.findIndex(input => input.name === name)
+      this.inputs[inputIndex].value = value
+    },
+    getSelectOptions (result, value, label) {
+      return result.map(item => {
+        return {
+          value: item[value],
+          label: item[label]
+        }
+      })
+    },
+    loadSelectOptions (name, value) {
+      const inputIndex = this.inputs.findIndex(input => input.name === name)
+      this.inputs[inputIndex].options = value
+    },
+    getTranscriptSheetStatus (transcriptSheet) {
+      return new TranscriptSheet(transcriptSheet).status_info.label
+    }
+  }
+}
+</script>

+ 518 - 0
src/components/Widgets/Classroom/AdminClassroomInfoOfUser/components/ShowClassroomInfo.vue

@@ -0,0 +1,518 @@
+<template>
+  <div class="classroom">
+    <q-banner v-if="!defaultOptions.profileMode"
+              class="banner classroom-banner">
+      {{ classroomTypeTitle }}
+      {{classroom.title}}
+    </q-banner>
+    <q-card flat
+            class="classroom-card">
+      <q-card-section class="info-box">
+
+        <q-banner v-if="classroom.getUserRegisterInfoLabel()"
+                  inline-actions
+                  rounded
+                  class="bg-blue-3 text-white text-center q-mb-md">
+          {{ classroom.getUserRegisterInfoLabel() }}
+        </q-banner>
+
+        <div class="row">
+          <div class="col-md-4 col-12 classroom-thumbnail">
+            <q-img :src="classroom.thumbnail" />
+          </div>
+          <div class="col-md-4 col-12 classroom-top-attributes">
+            <div class="title">
+              {{ classroomTypeTitle }}
+              {{classroom.title}}:
+            </div>
+            <div class="attributes">
+              <div class="attribute-item">{{getAudienceGenderType(classroom.audience_gender_type)}}</div>
+              <!--              <div class="attribute-item">{{getHoldingType(classroom.holding_type)}}</div>-->
+            </div>
+          </div>
+          <div class="col-md-4 col-12 classroom-action-col">
+            <q-btn v-if="classroom.is_enabled_adding"
+                   color="primary"
+                   class="btn-register q-ml-md"
+                   @click="openRegisterDialog">
+              <svg data-name="Add User"
+                   xmlns="http://www.w3.org/2000/svg"
+                   width="20"
+                   height="19.795"
+                   viewBox="0 0 20 19.795"
+                   class="q-mr-md">
+                <path id="Combined_Shape"
+                      data-name="Combined Shape"
+                      d="M0,16.115c0-3.281,4.5-3.659,7.877-3.659,1.949,0,7.877,0,7.877,3.681,0,3.279-4.5,3.658-7.877,3.658C5.928,19.795,0,19.795,0,16.115Zm1.5,0c0,1.446,2.146,2.18,6.377,2.18s6.377-.726,6.377-2.158c0-1.447-2.146-2.181-6.377-2.181S1.5,14.683,1.5,16.115Zm14.954-5.436V9.424H15.16a.75.75,0,0,1,0-1.5h1.294V6.669a.75.75,0,1,1,1.5,0V7.924h1.3a.75.75,0,0,1,0,1.5h-1.3v1.255a.75.75,0,1,1-1.5,0Zm-8.609-.043a5.334,5.334,0,1,1,.031,0ZM4.06,5.318A3.8,3.8,0,0,0,7.849,9.136l.028.75v-.75A3.818,3.818,0,1,0,4.06,5.318Z"
+                      transform="translate(0)"
+                      fill="#fff" />
+              </svg>
+              ثبت نام
+            </q-btn>
+            <q-btn v-if="classroom.is_enabled_enrolment"
+                   color="primary"
+                   class="btn-register q-ml-md"
+                   @click="onEnrolment">
+              <svg data-name="Add User"
+                   xmlns="http://www.w3.org/2000/svg"
+                   width="20"
+                   height="19.795"
+                   viewBox="0 0 20 19.795"
+                   class="q-mr-md">
+                <path id="Combined_Shape"
+                      data-name="Combined Shape"
+                      d="M0,16.115c0-3.281,4.5-3.659,7.877-3.659,1.949,0,7.877,0,7.877,3.681,0,3.279-4.5,3.658-7.877,3.658C5.928,19.795,0,19.795,0,16.115Zm1.5,0c0,1.446,2.146,2.18,6.377,2.18s6.377-.726,6.377-2.158c0-1.447-2.146-2.181-6.377-2.181S1.5,14.683,1.5,16.115Zm14.954-5.436V9.424H15.16a.75.75,0,0,1,0-1.5h1.294V6.669a.75.75,0,1,1,1.5,0V7.924h1.3a.75.75,0,0,1,0,1.5h-1.3v1.255a.75.75,0,1,1-1.5,0Zm-8.609-.043a5.334,5.334,0,1,1,.031,0ZM4.06,5.318A3.8,3.8,0,0,0,7.849,9.136l.028.75v-.75A3.818,3.818,0,1,0,4.06,5.318Z"
+                      transform="translate(0)"
+                      fill="#fff" />
+              </svg>
+              پیش ثبت نام
+            </q-btn>
+            <q-btn v-if="!defaultOptions.profileMode && classroom.is_enabled_viewing"
+                   color="primary"
+                   class="btn-register q-ml-md"
+                   :to="{name: 'UserPanel.Profile.ClassroomInfo', params: {id: classroom.id}}">
+              مشاهده
+              {{ classroomTypeTitle }}
+            </q-btn>
+            <q-btn v-if="classroom.is_enabled_dropping"
+                   outline
+                   color="red"
+                   class="btn-register q-ml-md"
+                   :loading="dropLoading"
+                   @click="onDropping">
+              انصراف
+            </q-btn>
+            <q-btn v-if="classroom.current_user_register_info?.status !== 'REGISTERED' && classroom.current_user_register_info?.status !== 'ENROLLED'"
+                   outline
+                   color="primary"
+                   class="btn-register q-ml-md"
+                   :loading="fetchCodesLoading"
+                   @click="openCodeLink">
+              آیین نامه
+              {{ classroomTypeTitle }}
+            </q-btn>
+          </div>
+        </div>
+      </q-card-section>
+      <q-card-section class="info-box">
+        <div class="row second-section">
+          <div class="col-md-4 col-sm-6 col-12 attribute-column">
+            <div class="icon">
+              <svg xmlns="http://www.w3.org/2000/svg"
+                   width="17.058"
+                   height="21.399"
+                   viewBox="0 0 17.058 21.399">
+                <path id="Combined_Shape"
+                      data-name="Combined Shape"
+                      d="M0,17.418c0-3.55,4.868-3.96,8.53-3.96,2.109,0,8.528,0,8.528,3.982,0,3.548-4.867,3.959-8.528,3.959C6.42,21.4,0,21.4,0,17.418Zm1.616,0c0,1.57,2.326,2.366,6.914,2.366s6.913-.789,6.913-2.345-2.326-2.366-6.913-2.366S1.616,15.862,1.616,17.418ZM8.5,11.436A5.718,5.718,0,0,1,8.53,0a5.718,5.718,0,0,1,0,11.436ZM4.349,5.718A4.171,4.171,0,0,0,8.5,9.9l.031.77V9.9a4.18,4.18,0,1,0-4.181-4.18Z"
+                      transform="translate(0 0)"
+                      fill="#aaa095" />
+              </svg>
+            </div>
+            <div class="title">
+              {{ classroom.professor_info.firstname }}
+              {{ classroom.professor_info.lastname }}
+            </div>
+          </div>
+          <div class="col-md-4 col-sm-6 col-12 attribute-column">
+            <div class="icon">
+              <q-icon name="cast_for_education"
+                      size="20px"
+                      color="grey" />
+            </div>
+            <div class="title">
+              <!--              {{ getTerm(classroom) }}-->
+              {{getHoldingType(classroom.holding_type)}}
+            </div>
+          </div>
+          <div class="col-md-4 col-sm-6 col-12 attribute-column">
+            <div class="icon">
+              <svg data-name="Group 11306"
+                   xmlns="http://www.w3.org/2000/svg"
+                   width="21.539"
+                   height="21.044"
+                   viewBox="0 0 21.539 21.044">
+                <g id="Buy"
+                   transform="translate(0 0)">
+                  <path id="Combined_Shape"
+                        data-name="Combined Shape"
+                        d="M16.3,19.649A1.395,1.395,0,1,1,17.7,21.044,1.4,1.4,0,0,1,16.3,19.649Zm-12.147,0a1.394,1.394,0,1,1,1.394,1.395A1.4,1.4,0,0,1,4.157,19.649ZM17.77,16.139H6.014A2.76,2.76,0,0,1,3.28,13.615L2.294,1.883.671,1.6A.812.812,0,0,1,.012.668.811.811,0,0,1,.815,0a.829.829,0,0,1,.13.011L3.186.4a.808.808,0,0,1,.666.729L4.1,4.143c15.367.007,15.424.014,15.5.022a2.254,2.254,0,0,1,1.912,2.553l-1.023,7.062a2.76,2.76,0,0,1-2.717,2.358ZM4.89,13.483a1.123,1.123,0,0,0,1.127,1.04H17.771a1.139,1.139,0,0,0,1.122-.972l1.024-7.064a.632.632,0,0,0-.121-.472.625.625,0,0,0-.418-.249c-.052,0-.6,0-1.677,0-1.693,0-4.455,0-7.291,0l-6.17,0ZM13.059,9.74a.808.808,0,0,1,0-1.616h2.985a.808.808,0,0,1,0,1.616Z"
+                        transform="translate(0)"
+                        fill="#aaa095" />
+                </g>
+              </svg>
+            </div>
+            <div class="title">
+              {{ classroom.price ? classroom.price.toLocaleString('fa') : 0 }}
+              ریال
+            </div>
+          </div>
+        </div>
+      </q-card-section>
+      <div class="row q-col-gutter-lg four-sections">
+        <div class="col-md-6 col-12">
+          <div class="info-box detailes">
+            <div class="info-box-title">
+              <svg xmlns="http://www.w3.org/2000/svg"
+                   width="17.054"
+                   height="19.866"
+                   viewBox="0 0 17.054 19.866">
+                <path id="Combined_Shape"
+                      data-name="Combined Shape"
+                      d="M4.475,19.866A4.585,4.585,0,0,1,0,15.188V4.491A4.606,4.606,0,0,1,4.57.012h6.271a.758.758,0,0,1,.265,0h.131a.755.755,0,0,1,.541.23l5.065,5.277a.753.753,0,0,1,.209.519V15.2A4.615,4.615,0,0,1,12.6,19.865H4.475ZM1.5,4.509V15.2a3.091,3.091,0,0,0,3.009,3.162h8.064a3.109,3.109,0,0,0,2.979-3.155V6.984h-2.01a3.332,3.332,0,0,1-3.318-3.325V1.512H4.572A3.089,3.089,0,0,0,1.5,4.509Zm10.224-.85a1.828,1.828,0,0,0,1.82,1.824H14.73L11.724,2.353ZM5.387,14.108a.75.75,0,0,1,0-1.5h5.4a.75.75,0,0,1,0,1.5Zm0-3.752a.75.75,0,0,1,0-1.5H8.743a.75.75,0,0,1,0,1.5Z"
+                      transform="translate(0.001)"
+                      fill="#aaa095" />
+              </svg>
+              مشخصات
+            </div>
+            <div class="info-box-body"
+                 v-html="classroom.specification" />
+          </div>
+          <div class="info-box rules">
+            <div class="info-box-title">
+              <svg xmlns="http://www.w3.org/2000/svg"
+                   width="16.968"
+                   height="20.356"
+                   viewBox="0 0 16.968 20.356">
+                <path id="Combined_Shape"
+                      data-name="Combined Shape"
+                      d="M8.266,20.324c-.335-.1-8.205-2.55-8.205-9.445C.06,9,.036,7.621.019,6.616l0-.058c-.046-2.544-.055-3.074.6-3.73C1.4,2.04,7.349,0,8.483,0s7.078,2.04,7.865,2.828c.656.656.646,1.184.6,3.722l0,.062C16.929,7.619,16.9,9,16.9,10.879c0,6.895-7.87,9.344-8.205,9.445a.711.711,0,0,1-.217.032A.719.719,0,0,1,8.266,20.324ZM1.668,3.9c-.182.184-.189.5-.15,2.63l0,.065c.017,1.011.041,2.395.041,4.286,0,5.217,5.765,7.525,6.921,7.934C9.637,18.4,15.4,16.086,15.4,10.879c0-1.959.027-3.4.044-4.353.038-2.127.031-2.445-.162-2.637A30.988,30.988,0,0,0,8.483,1.5,31.027,31.027,0,0,0,1.668,3.9Zm5.577,8.4L5.353,10.406A.751.751,0,0,1,6.415,9.345l1.361,1.363,3.369-3.368A.75.75,0,0,1,12.2,8.4l-3.9,3.9a.749.749,0,0,1-1.061,0Z"
+                      transform="translate(0.001)"
+                      fill="#aaa095" />
+              </svg>
+              قوانین
+            </div>
+            <div class="info-box-body"
+                 v-html="classroom.rules" />
+          </div>
+        </div>
+        <div class="col-md-6 col-12">
+          <div class="info-box class-time">
+            <div class="info-box-title">
+              <svg id="Time_Circle"
+                   data-name="Time Circle"
+                   xmlns="http://www.w3.org/2000/svg"
+                   width="20"
+                   height="20"
+                   viewBox="0 0 20 20">
+                <path id="Combined_Shape"
+                      data-name="Combined Shape"
+                      d="M0,10A10,10,0,1,1,10,20,10.011,10.011,0,0,1,0,10Zm1.5,0A8.5,8.5,0,1,0,10,1.5,8.51,8.51,0,0,0,1.5,10Zm11.547,3.587-3.77-2.249a.754.754,0,0,1-.366-.644V5.845a.75.75,0,0,1,1.5,0v4.422l3.4,2.03a.751.751,0,0,1-.769,1.29Z"
+                      fill="#aaa095" />
+              </svg>
+              تابلو اعلانات
+            </div>
+            <div class="info-box-body"
+                 v-html="classroom.bulletin" />
+          </div>
+          <div class="info-box books">
+            <div class="info-box-title">
+              <svg xmlns="http://www.w3.org/2000/svg"
+                   width="21.968"
+                   height="15.702"
+                   viewBox="0 0 21.968 15.702">
+                <path id="import_contacts_FILL0_wght400_GRAD0_opsz48"
+                      d="M7.192,20.482a10.817,10.817,0,0,1,2.609.312,11.579,11.579,0,0,1,2.484.936V11.07A9.5,9.5,0,0,0,9.851,9.91,8.819,8.819,0,0,0,7.192,9.5a7.353,7.353,0,0,0-1.86.237,17.989,17.989,0,0,0-1.835.587h0V21.156h0a7.045,7.045,0,0,1,1.76-.512A11.9,11.9,0,0,1,7.192,20.482Zm6.59,1.248a12.562,12.562,0,0,1,2.446-.936,10.07,10.07,0,0,1,2.546-.312,13.37,13.37,0,0,1,1.96.15,9.769,9.769,0,0,1,1.735.4h0V10.322h0a7.418,7.418,0,0,0-1.8-.624,9.187,9.187,0,0,0-1.9-.2,8.346,8.346,0,0,0-2.609.412,9.727,9.727,0,0,0-2.384,1.161ZM13.034,23.7a1.613,1.613,0,0,1-.362-.037.708.708,0,0,1-.287-.137,10.528,10.528,0,0,0-2.5-1.123,9.28,9.28,0,0,0-2.7-.4,7.188,7.188,0,0,0-1.8.225,17.621,17.621,0,0,0-1.747.549A1.07,1.07,0,0,1,2.537,22.7,1.122,1.122,0,0,1,2,21.7V10.147a1.381,1.381,0,0,1,.175-.686A1.192,1.192,0,0,1,2.7,8.974a9.65,9.65,0,0,1,2.184-.736A11.268,11.268,0,0,1,7.192,8a11.077,11.077,0,0,1,3.058.424,9.758,9.758,0,0,1,2.783,1.3,9.477,9.477,0,0,1,2.733-1.3A10.725,10.725,0,0,1,18.775,8a11.088,11.088,0,0,1,2.3.237,9.691,9.691,0,0,1,2.172.736,1.272,1.272,0,0,1,.537.487,1.309,1.309,0,0,1,.187.686V21.7a1.183,1.183,0,0,1-.562,1.061.956.956,0,0,1-1.111.012,11.887,11.887,0,0,0-1.722-.562,7.589,7.589,0,0,0-1.8-.212,8.949,8.949,0,0,0-2.646.4,10.183,10.183,0,0,0-2.446,1.123.708.708,0,0,1-.287.137A1.613,1.613,0,0,1,13.034,23.7ZM7.891,15.764Z"
+                      transform="translate(-2 -8)"
+                      fill="#aaa095" />
+              </svg>
+              توضیحات
+            </div>
+            <div class="info-box-body"
+                 v-html="classroom.description" />
+          </div>
+        </div>
+      </div>
+    </q-card>
+
+    <q-dialog v-model="rulesDialog"
+              class="rulesDialog"
+              full-width>
+      <q-card>
+        <q-card-section>
+          <div class="text-h6">قوانین</div>
+        </q-card-section>
+
+        <q-separator />
+
+        <q-card-section style="max-height: 50vh"
+                        class="scroll">
+          <div v-html="classroom.rules" />
+          <div v-if="classroom.unit_info && classroom.unit_info.codes"
+               v-html="classroom.unit_info.codes" />
+        </q-card-section>
+
+        <q-separator />
+
+        <q-card-actions class="rulesDialog-actions">
+          <div v-if="(!classroom.is_enabled_enrolment || !classroom.is_enabled_adding) && acceptClassType !== 'rules'"
+               class="rulesDialog-accept-chk">
+            <q-checkbox v-model="rulesAccept"
+                        :label="'قوانین '+classroomTypeTitle+' را خواندم و قصد ثبت نام در '+classroomTypeTitle+' را دارم'" />
+          </div>
+          <div class="rulesDialog-btns q-gutter-md">
+            <q-btn v-if="acceptClassType === 'rules'"
+                   v-close-popup
+                   label="بستن"
+                   color="primary"
+                   outline />
+            <q-btn v-if="(!classroom.is_enabled_enrolment || !classroom.is_enabled_adding) && acceptClassType !== 'rules'"
+                   v-close-popup
+                   label="انصراف"
+                   color="primary"
+                   outline />
+            <q-btn v-if="(!classroom.is_enabled_enrolment || !classroom.is_enabled_adding) && acceptClassType !== 'rules'"
+                   label="تایید و ادامه"
+                   color="primary"
+                   :disable="!rulesAccept"
+                   @click="acceptAndContinue" />
+          </div>
+        </q-card-actions>
+      </q-card>
+    </q-dialog>
+  </div>
+</template>
+
+<script>
+import Enums from 'assets/Enums/Enums.js'
+import ShamsiDate from 'assets/ShamsiDate.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { Classroom } from 'src/models/Classroom.js'
+import { UnitCategory } from 'src/models/UnitCategory'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'ShowClassroomInfo',
+  mixins: [mixinWidget],
+  props: {
+    options: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    },
+    classroom: {
+      type: Classroom,
+      default: new Classroom()
+    },
+    classroomId: {
+      type: Number,
+      default: null
+    },
+    ownerId: {
+      type: Number,
+      default: null
+    },
+    classroomType: {
+      type: String,
+      default: 'TRAINING'
+    }
+  },
+  emits: ['onDrop'],
+  data: () => ({
+    defaultOptions: {
+      profileMode: false,
+      classroomType: 'TRAINING'
+    },
+    acceptClassType: 'register', // accept - enrollment - rules
+    fetchCodesLoading: false,
+    dropLoading: false,
+    rulesAccept: false,
+    rulesDialog: false,
+    loading: true
+  }),
+  computed: {
+    classroomTypeTitle () {
+      const unitCategory = new UnitCategory({ type: this.localOptions.classroomType })
+      return unitCategory.type_info.label
+    }
+  },
+  methods: {
+    getTitledDateTime (dateTime) {
+      return ShamsiDate.getTitledDateTime(dateTime)
+    },
+    acceptAndContinue () {
+      this.$store.commit('Shop/updateOnRegisterClassroom', this.classroom)
+      this.$router.push({ name: 'UserPanel.ShopCompleteInfo', query: { type: this.acceptClassType, source: this.classroomType === 'EVENT' ? 'event' : 'classroom' } })
+    },
+    onEnrolment () {
+      this.acceptClassType = 'enrollment'
+      this.rulesDialog = true
+    },
+    onDropping () {
+      this.$q.dialog({
+        title: 'توجه',
+        message: 'آیا از حذف این دوره اطمینان دارید؟',
+        cancel: true,
+        persistent: true
+      }).onOk(() => {
+        this.dropLoading = true
+        APIGateway.classroom.dropByUser(this.classroom.id)
+          .then(() => {
+            this.dropLoading = false
+            this.$emit('onDrop')
+          })
+          .catch(() => {
+            this.dropLoading = false
+          })
+      })
+    },
+    openRulesDialog () {
+      this.acceptClassType = 'rules'
+      this.rulesDialog = true
+    },
+    openCodeLink () {
+      if (!this.classroom.codes) {
+        return
+      }
+
+      const fileName = (this.classroom.codes.match(/(?<=\/)[^/?#]+(?=[^/]*$)/gm) || [this.classroom.title])[0]
+      this.fetchCodesLoading = true
+      fetch(this.classroom.codes)
+        .then(response => response.blob())
+        .then(blob => {
+          const link = document.createElement('a')
+          link.href = URL.createObjectURL(blob)
+          link.download = fileName
+          link.click()
+          this.fetchCodesLoading = false
+        })
+        .catch(() => {
+          this.fetchCodesLoading = false
+        })
+    },
+    openRegisterDialog () {
+      this.acceptClassType = 'register'
+      this.rulesDialog = true
+    },
+    getHoldingType (type) {
+      const target = Enums.classroomHoldingTypes.find(item => item.value === type)
+      if (target) {
+        return target.label
+      }
+      return '-'
+    },
+    getAudienceGenderType (type) {
+      const target = Enums.classroomGenders.find(item => item.value === type)
+      if (target) {
+        return target.label
+      }
+      return '-'
+    },
+    getTime (time) {
+      return ShamsiDate.getDateTime(time)
+    },
+    getTerm (classroom) {
+      if (!classroom.beginning_registration_period) {
+        return '-'
+      }
+      return ShamsiDate.getTerm(classroom.beginning_registration_period)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.classroom {
+  .classroom-banner {
+    margin-top: 52px;
+    margin-bottom: 42px;
+  }
+  .classroom-card {
+    background-color: #F5F5F5;
+    padding: 20px 30px 40px 30px;
+    .four-sections {
+      .info-box {
+        //height: 353px;
+        //overflow: auto;
+        &.detailes {
+          //margin-right: 24px;
+        }
+        &.rules {
+          //margin-right: 24px;
+        }
+        .info-box-title {
+          margin-bottom: 29px;
+          display: flex;
+          flex-flow: row;
+          svg {
+            margin-right: 8px;
+          }
+        }
+        .info-box-body {
+          .info-item {
+            margin-bottom: 27px;
+          }
+        }
+      }
+    }
+    .info-box {
+      background-color: #FFFFFF;
+      border-radius: 8px;
+      padding: 26px;
+      margin-bottom: 24px;
+      .classroom-thumbnail {
+        padding-right: 42px;
+      }
+      .classroom-top-attributes {
+        .attributes {
+          padding-top: 27px;
+          .attribute-item {
+            margin-bottom: 20px;
+            position: relative;
+            padding-left: 15px;
+            &:before {
+              content: ' ';
+              background-color: #EAC38A;
+              width: 8px;
+              height: 8px;
+              border-radius: 100%;
+              position: absolute;
+              left: 0;
+              top: 10px;
+            }
+          }
+        }
+      }
+      .classroom-action-col {
+        display: flex;
+        justify-content: flex-end;
+        align-items: flex-end;
+        flex-wrap: wrap;
+        .btn-register {
+          width: 220px;
+        }
+        @media screen and (max-width: 700px) {
+          width: 100%;
+          .q-btn {
+            width: 100% !important;
+            margin-left: 0;
+            margin-bottom: 8px;
+          }
+        }
+      }
+      .second-section {
+        .attribute-column {
+          display: flex;
+          flex-flow: row;
+          .icon {
+            margin-right: 20px;
+          }
+        }
+      }
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+.rulesDialog {
+  .rulesDialog-actions {
+    display: flex;
+    flex-flow: row;
+    justify-content: space-between;
+    .rulesDialog-accept-chk {}
+    .rulesDialog-btns {}
+  }
+}
+</style>

+ 10 - 6
src/components/Widgets/Classroom/AdminClassroomList/AdminClassroomList.vue

@@ -78,19 +78,19 @@ export default {
       inputs: [
         {
           type: 'select',
-          name: 'status',
-          options: Enums.classroomStatuses,
+          name: 'unit',
+          options: [],
           value: null,
-          label: 'وضعیت ',
+          label: 'unit',
           placeholder: ' ',
           col: 'col-md-2 col-12'
         },
         {
           type: 'select',
-          name: 'unit',
-          options: [],
+          name: 'status',
+          options: Enums.classroomStatuses,
           value: null,
-          label: 'درس',
+          label: 'وضعیت ',
           placeholder: ' ',
           col: 'col-md-2 col-12'
         },
@@ -225,6 +225,7 @@ export default {
   methods: {
     setClassroomTypeOfInputs () {
       FormBuilderAssist.setAttributeByName(this.inputs, 'status', 'label', 'وضعیت ' + this.classroomTypeTitle)
+      FormBuilderAssist.setAttributeByName(this.inputs, 'unit', 'label', this.getUnitFilterTitle())
       FormBuilderAssist.setAttributeByName(this.inputs, 'unit__category__type', 'value', this.localOptions.classroomType)
       this.table.columns.forEach(col => {
         if (col.name === 'title') {
@@ -280,6 +281,9 @@ export default {
       }
 
       return target.label
+    },
+    getUnitFilterTitle () {
+      return (this.localOptions.classroomType === 'TRAINING') ? 'درس' : 'دسته'
     }
   }
 }

+ 29 - 4
src/components/Widgets/Classroom/AdminClassroomShow/AdminClassroomShow.vue

@@ -25,30 +25,43 @@
              name="classroomInfo"
              label="مشخصات" />
       <q-tab v-if="canShowTabPage('sessions')"
+             :disable="!classroom.id"
              name="sessions"
              :label="sessionsTabTitle" />
       <q-tab v-if="canShowTabPage('projects')"
+             :disable="!classroom.id"
              name="projects"
              label="پروژه‌ها" />
       <q-tab v-if="canShowTabPage('exams')"
+             :disable="!classroom.id"
              name="exams"
              :label="localOptions.classroomType === 'TRAINING' ? 'آزمون' : 'پرسشنامه'" />
       <q-tab v-if="canShowTabPage('members')"
+             :disable="!classroom.id"
              name="members"
              label="اندیشه جویان" />
       <q-tab v-if="canShowTabPage('teams')"
+             :disable="!classroom.id"
              name="teams"
              label="گروه" />
       <q-tab v-if="canShowTabPage('graders')"
+             :disable="!classroom.id"
              name="graders"
              label="مصححین" />
       <q-tab v-if="canShowTabPage('leaders')"
+             :disable="!classroom.id"
              name="leaders"
              label="سرگروه ها" />
       <q-tab v-if="canShowTabPage('activity_sheet')"
+             :disable="!classroom.id"
              name="activity_sheet"
              label="فعالیت کلاسی" />
+      <q-tab v-if="canShowTabPage('announce_result')"
+             :disable="!classroom.id"
+             name="announce_result"
+             label="کارنامه" />
       <q-tab v-if="canShowTabPage('live_streaming_url')"
+             :disable="!classroom.id"
              name="live_streaming_url"
              label="بخش آنلاین" />
     </q-tabs>
@@ -75,7 +88,8 @@
       </q-tab-panel>
       <q-tab-panel name="exams"
                    class="q-pa-none">
-        <test-list :classroom="classroom"
+        <test-list v-if="classroom.id"
+                   :classroom="classroom"
                    :classroom-id="parseInt($route.params.id)"
                    :classroom-type="localOptions.classroomType" />
       </q-tab-panel>
@@ -97,10 +111,18 @@
       </q-tab-panel>
       <q-tab-panel name="activity_sheet"
                    class="q-pa-none">
-        <activity-sheet-list :classroom-id="parseInt($route.params.id)" />
+        <activity-sheet-list v-if="classroom.id"
+                             :classroom="classroom"
+                             :classroom-id="parseInt($route.params.id)" />
+      </q-tab-panel>
+      <q-tab-panel name="announce_result"
+                   class="q-pa-none">
+        <announce-result-list v-if="classroom.id"
+                              :classroom="classroom" />
       </q-tab-panel>
       <q-tab-panel name="live_streaming_url">
-        <live-streaming :classroom="classroom" />
+        <live-streaming v-if="classroom.id"
+                        :classroom="classroom" />
       </q-tab-panel>
     </q-tab-panels>
   </q-card>
@@ -120,6 +142,7 @@ import { UnitCategory } from 'src/models/UnitCategory.js'
 import ClassroomInfo from './components/ClassroomInfo.vue'
 import LiveStreaming from './components/LiveStreaming.vue'
 import ActivitySheetList from './components/ActivitySheetList.vue'
+import AnnounceResultList from './components/AnnounceResultList.vue'
 import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
 
 export default {
@@ -135,7 +158,8 @@ export default {
     Breadcrumbs,
     ClassroomInfo,
     LiveStreaming,
-    ActivitySheetList
+    ActivitySheetList,
+    AnnounceResultList
   },
   mixins: [mixinWidget],
   data () {
@@ -193,6 +217,7 @@ export default {
             'graders',
             'leaders',
             'activity_sheet',
+            'announce_result',
             'live_streaming_url'
           ],
           EVENT: [

+ 91 - 36
src/components/Widgets/Classroom/AdminClassroomShow/components/ActivitySheetList.vue

@@ -9,14 +9,44 @@
                 :show-search-button="false"
                 :show-reload-button="false"
                 :show-expand-button="false">
+    <template #toolbar>
+      <q-btn color="primary"
+             :loading="exportReportLoading"
+             @click="getActivitySheetExcel">
+        خروجی اکسل
+      </q-btn>
+    </template>
     <template v-slot:entity-index-table-cell="{inputData, showConfirmRemoveDialog}">
-      <template v-if="inputData.col.name === 'actions'">
+      <template v-if="inputData.col.name === 'is_project_done'">
+        <q-badge rounded
+                 :color="inputData.col.value ? 'green' : 'red'">
+          <q-icon :name="inputData.col.value ? 'check' : 'close'"
+                  size="20px" />
+        </q-badge>
+      </template>
+      <template v-else-if="inputData.col.name === 'is_passed_assignment_condition'">
+        <q-badge rounded
+                 :color="inputData.col.value ? 'green' : 'red'">
+          <q-icon :name="inputData.col.value ? 'check' : 'close'"
+                  size="20px" />
+        </q-badge>
+      </template>
+      <template v-else-if="inputData.col.name === 'is_allowed_absence_count'">
+        <q-badge rounded
+                 :color="inputData.col.value ? 'green' : 'red'">
+          <q-icon :name="inputData.col.value ? 'check' : 'close'"
+                  size="20px" />
+        </q-badge>
+      </template>
+      <template v-else-if="inputData.col.name === 'actions'">
         <div class="action-column-entity-index">
           <q-btn size="md"
                  color="primary"
                  outline
-                 label="سابقه آموزشی"
-                 class="q-mr-md" />
+                 class="q-mr-md"
+                 :to="{ name: 'Admin.Classroom.UserClass', params: { classroom_id: classroomId, user_id: inputData.props.row.owner }}">
+            سابقه آموزشی
+          </q-btn>
           <!--                :to="{name: 'Admin.Session.Show', params: {id: inputData.props.row.id}}"-->
           <delete-btn @click="showConfirmRemoveDialog(inputData.props.row, 'id', getRemoveMessage(inputData.props.row))" />
         </div>
@@ -35,9 +65,10 @@
 import { shallowRef } from 'vue'
 import { EntityIndex } from 'quasar-crud'
 import { APIGateway } from 'src/api/APIGateway.js'
-import { FormBuilderAssist } from 'quasar-form-builder'
 import BtnControl from 'src/components/Control/btn.vue'
 import DeleteBtn from 'src/components/Control/DeleteBtn.vue'
+import Assist from 'assets/js/Assist'
+import { Classroom } from 'src/models/Classroom'
 
 const BtnControlComp = shallowRef(BtnControl)
 
@@ -51,6 +82,10 @@ export default {
     classroomId: {
       type: Number,
       default: null
+    },
+    classroom: {
+      type: Classroom,
+      default: new Classroom()
     }
   },
   data () {
@@ -59,10 +94,11 @@ export default {
       mounted: false,
       createInvoiceLoading: false,
 
+      exportReportLoading: false,
       activitySheetListInputs: [
         { type: 'hidden', name: 'classroom', value: classroomId },
-        { type: 'select', name: 'status', value: null, label: 'وضعیت', placeholder: ' ', col: 'col-md-3 col-12' },
-        { type: BtnControlComp, name: 'btn', label: 'جستجو', placeholder: ' ', atClick: () => {}, col: 'col-md-2 col-12' }
+        { type: 'input', name: 'owner__national_code', value: null, label: 'کد ملی', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: BtnControlComp, name: 'btn', label: 'جستجو', placeholder: ' ', atClick: this.searchActivitySheetList, col: 'col-md-2 col-12' }
       ],
       activitySheetListApi: APIGateway.classroom.APIAdresses.activitySheet,
       activitySheetListTable: {
@@ -88,48 +124,55 @@ export default {
             align: 'left',
             field: row => row.owner_info.national_code
           },
-          {
-            name: 'is_project_done',
-            required: true,
-            label: 'پروژه',
-            align: 'left',
-            field: row => row.is_project_done
-          },
-          {
-            name: 'done_assignment_count',
-            required: true,
-            label: 'تکالیف انجام شده',
-            align: 'left',
-            field: row => row.done_assignment_count
-          },
           {
             name: 'is_allowed_absence_count',
             required: true,
             label: 'حضور و غیاب',
             align: 'left',
-            field: row => row.is_allowed_absence_count // bool
+            field: row => row.is_passed_attendance_condition
           },
           {
-            name: 'attendance_score',
+            name: 'is_project_done',
             required: true,
-            label: 'نمره حضور و غیاب',
+            label: 'پروژه',
             align: 'left',
-            field: row => row.attendance_score
+            field: row => row.is_passed_project_condition
           },
           {
-            name: 'final_test',
+            name: 'is_passed_assignment_condition',
             required: true,
-            label: 'آزمون',
+            label: 'تکالیف',
             align: 'left',
-            field: row => row.final_test // number - null
+            field: row => row.is_passed_assignment_condition
           },
           {
-            name: 'final_status',
+            name: 'done_assignment_count',
             required: true,
-            label: 'وضعیت نهایی',
+            label: 'تکالیف انجام شده',
             align: 'left',
-            field: row => row.final_status
+            field: row => row.done_assignment_count
           },
+          // {
+          //   name: 'attendance_score',
+          //   required: true,
+          //   label: 'نمره حضور و غیاب',
+          //   align: 'left',
+          //   field: row => row.attendance_score
+          // },
+          // {
+          //   name: 'final_test',
+          //   required: true,
+          //   label: 'آزمون',
+          //   align: 'left',
+          //   field: row => row.final_test // number - null
+          // },
+          // {
+          //   name: 'final_status',
+          //   required: true,
+          //   label: 'وضعیت نهایی',
+          //   align: 'left',
+          //   field: row => row.final_status
+          // },
           {
             name: 'actions',
             required: true,
@@ -150,18 +193,30 @@ export default {
     }
   },
   mounted () {
-    this.setActivitySheetListActionBtn()
     this.mounted = true
   },
   methods: {
-    setActivitySheetListActionBtn () {
-      FormBuilderAssist.setAttributeByName(this.activitySheetListInputs, 'btn', 'atClick', this.searchActivitySheetList)
-    },
     searchActivitySheetList () {
-      this.$refs.activitySheet.search()
+      this.$refs.activitySheetList.search()
     },
     getRemoveMessage (row) {
-      return 'آیا از حذف ' + row.title + ' اطمینان دارید؟'
+      return 'آیا از حذف ' + row.owner_info.firstname + ' ' + row.owner_info.lastname + ' اطمینان دارید؟'
+    },
+    getActivitySheetExcel () {
+      this.exportReportLoading = true
+      const status = null
+      APIGateway.classroom.exportActivitySheetReport({
+        classroom: this.$route.params.id,
+        type: 'activity_sheet_export',
+        status
+      })
+        .then((xlsxData) => {
+          Assist.saveXlsx(xlsxData, this.classroom.title)
+          this.exportReportLoading = false
+        })
+        .catch((e) => {
+          this.exportReportLoading = false
+        })
     }
   }
 }

+ 418 - 0
src/components/Widgets/Classroom/AdminClassroomShow/components/AnnounceResultList.vue

@@ -0,0 +1,418 @@
+<template>
+  <entity-index v-if="mounted"
+                ref="announceResultList"
+                v-model:value="announceResultListInputs"
+                title="کارنامه"
+                :api="announceResultListApi"
+                :table="announceResultListTable"
+                :table-keys="announceResultListTableKeys"
+                :show-search-button="false"
+                :show-reload-button="false"
+                :show-expand-button="false">
+    <template #toolbar>
+      <q-btn label="خروجی اکسل"
+             color="primary"
+             :loading="exportReportLoading"
+             outline
+             @click="getExcel" />
+      <q-btn label="اعلام نتایج"
+             color="primary"
+             :loading="notifyAnnounceResultLoading"
+             outline
+             class="q-ml-md"
+             @click="notifyAnnounceResult" />
+      <q-btn :label="classroomOngoingStatus ? 'بستن کلاس' : 'فعالسازی کلاس'"
+             color="primary"
+             outline
+             class="q-ml-md"
+             @click="toggleOngoingStatus" />
+    </template>
+    <template v-slot:entity-index-table-cell="{inputData}">
+      <template v-if="inputData.col.name === 'actions'">
+        <q-btn color="primary"
+               outline
+               label="عملیات"
+               :loading="updateTranscriptSheetLoading">
+          <q-menu>
+            <q-list style="min-width: 100px">
+              <q-item v-close-popup
+                      clickable
+                      @click="updateTranscriptSheetsFinalScore(inputData.props.row)">
+                <q-item-section>تغییر نمره نهایی</q-item-section>
+              </q-item>
+              <q-item v-if="!inputData.props.row.certification"
+                      v-close-popup
+                      clickable
+                      @click="updateTranscriptSheetsUploadCertification(inputData.props.row)">
+                <q-item-section>آپلود فایل مدرک</q-item-section>
+              </q-item>
+              <q-item v-if="inputData.props.row.certification"
+                      v-close-popup
+                      clickable
+                      @click="updateTranscriptSheetsRemoveCertification(inputData.props.row)">
+                <q-item-section>حذف فایل مدرک</q-item-section>
+              </q-item>
+              <q-item v-close-popup
+                      clickable
+                      @click="updateTranscriptSheetsUploadIsSentByPost(inputData.props.row)">
+                <q-item-section>تغییر وضعیت پست شده</q-item-section>
+              </q-item>
+              <q-item v-close-popup
+                      clickable
+                      @click="deleteTranscriptSheet(inputData.props.row)">
+                <q-item-section>حذف ردیف جاری</q-item-section>
+              </q-item>
+            </q-list>
+          </q-menu>
+        </q-btn>
+      </template>
+      <template v-else-if="inputData.col.name === 'is_sent_by_post'">
+        <q-badge :color="inputData.col.value ? 'green' : 'red'"
+                 outline>
+          <template v-if="inputData.col.value">
+            پست شده
+          </template>
+          <template v-else>
+            پست نشده
+          </template>
+        </q-badge>
+      </template>
+      <template v-else-if="inputData.col.name === 'certification'">
+        <a v-if="inputData.col.value"
+           :href="inputData.col.value"
+           target="_blank">
+          <q-btn color="primary"
+                 label="دانلود مدرک" />
+        </a>
+        <q-icon v-else
+                name="close"
+                color="red" />
+      </template>
+      <template v-else>
+        {{ inputData.col.value }}
+      </template>
+    </template>
+  </entity-index>
+  <q-skeleton v-else
+              type="rect"
+              height="200px" />
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityIndex } from 'quasar-crud'
+import Assist from 'assets/js/Assist.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { Classroom } from 'src/models/Classroom.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import { Registration } from 'src/models/Registration.js'
+import { TranscriptSheet } from 'src/models/TranscriptSheet.js'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'ActivitySheetList',
+  components: {
+    EntityIndex
+  },
+  props: {
+    classroom: {
+      type: Classroom,
+      default: new Classroom()
+    }
+  },
+  data () {
+    const classroomId = this.classroom.id
+    return {
+      mounted: false,
+      exportReportLoading: false,
+      classroomOngoingStatus: this.classroom.is_ongoing,
+      toggleOngoingStatusLoading: false,
+      notifyAnnounceResultLoading: false,
+      updateTranscriptSheetLoading: false,
+
+      announceResultListInputs: [
+        { type: 'select', name: 'status', value: null, label: 'وضعیت نهایی', options: (new TranscriptSheet()).statusEnums, placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'select', name: 'registration__status', value: null, label: 'وضعیت ثبت نام', options: (new Registration()).statusEnums, placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'registration__owner__national_code', value: null, label: 'کد ملی', placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'optionGroupRadio', name: 'is_sent_by_post', value: null, label: 'پست', options: [{ label: 'همه موارد', value: null }, { label: 'پست نشده', value: false }, { label: 'پست شده', value: true }], placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'optionGroupRadio', name: 'certification__isnull', value: null, label: 'مدرک', options: [{ label: 'همه موارد', value: null }, { label: 'آپلود شده', value: false }, { label: 'آپلود نشده', value: true }], placeholder: ' ', col: 'col-md-3 col-12' },
+        { type: 'hidden', name: 'registration__classroom', value: classroomId },
+        {
+          type: BtnControlComp,
+          name: 'btn',
+          label: 'جستجو',
+          placeholder: ' ',
+          atClick: () => {
+            this.$refs.announceResultList.search()
+          },
+          col: 'col-md-2 col-12'
+        }
+      ],
+      announceResultListApi: APIGateway.transcriptSheet.APIAdresses.base,
+      announceResultListTable: {
+        columns: [
+          // {
+          //   name: 'id',
+          //   required: true,
+          //   label: 'شماره',
+          //   align: 'left',
+          //   field: row => row.id
+          // },
+          {
+            name: 'owner',
+            required: true,
+            label: 'شناسه کاربر',
+            align: 'left',
+            field: row => row.registration_info.owner
+          },
+          {
+            name: 'owner_info.fullname',
+            required: true,
+            label: 'نام و نام خانوادگی',
+            align: 'left',
+            field: row => row.registration_info.owner_info.firstname + ' ' + row.registration_info.owner_info.lastname
+          },
+          {
+            name: 'owner_info.national_code',
+            required: true,
+            label: 'کد ملی',
+            align: 'left',
+            field: row => row.registration_info.owner_info.national_code
+          },
+          {
+            name: 'attendance_score',
+            required: true,
+            label: 'نمره حضور و غیاب',
+            align: 'left',
+            field: row => row.attendance_score
+          },
+          {
+            name: 'highest_test_score',
+            required: true,
+            label: 'نمره آزمون',
+            align: 'left',
+            field: row => row.highest_test_score
+          },
+          {
+            name: 'final_score',
+            required: true,
+            label: 'نمره نهایی',
+            align: 'left',
+            field: row => row.final_score
+          },
+          {
+            name: 'status',
+            required: true,
+            label: 'وضعیت نهایی',
+            align: 'left',
+            field: row => new TranscriptSheet(row).status_info.label
+          },
+          {
+            name: 'is_sent_by_post',
+            required: true,
+            label: 'ارسال شده با پست',
+            align: 'left',
+            field: row => row.is_sent_by_post
+          },
+          {
+            name: 'certification',
+            required: true,
+            label: 'مدرک',
+            align: 'left',
+            field: row => row.certification
+          },
+          {
+            name: 'actions',
+            required: true,
+            label: 'عملیات',
+            align: 'left',
+            field: () => ''
+          }
+        ]
+      },
+      announceResultListTableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      }
+    }
+  },
+  mounted () {
+    this.mounted = true
+  },
+  methods: {
+    toggleOngoingStatus () {
+      this.toggleOngoingStatusLoading = true
+      APIGateway.classroom.toggleOngoingStatus(this.classroom.id)
+        .then((response) => {
+          this.classroomOngoingStatus = response.is_ongoing
+          this.toggleOngoingStatusLoading = false
+        })
+        .catch(() => {
+          this.toggleOngoingStatusLoading = false
+        })
+    },
+    notifyAnnounceResult () {
+      this.notifyAnnounceResultLoading = true
+      APIGateway.classroom.notifyAnnounceResult(this.classroom.id)
+        .then(() => {
+          this.notifyAnnounceResultLoading = false
+          this.$refs.announceResultList.search()
+        })
+        .catch(() => {
+          this.notifyAnnounceResultLoading = false
+        })
+    },
+    searchActivitySheetList () {
+      this.$refs.activitySheet.search()
+    },
+    getRemoveMessage (row) {
+      return 'آیا از حذف ' + row.title + ' اطمینان دارید؟'
+    },
+    updateTranscriptSheetsFinalScore (transcriptSheet) {
+      this.$q.dialog({
+        title: 'تغییر نمره نهایی',
+        message: 'نمره نهایی مورد نظر را وارد کنید:',
+        prompt: {
+          model: null,
+          type: 'text'
+        },
+        cancel: true,
+        persistent: true
+      }).onOk(data => {
+        this.updateTranscriptSheetLoading = true
+        APIGateway.transcriptSheet.update(transcriptSheet.id, {
+          final_score: data
+        })
+          .then(() => {
+            this.$refs.announceResultList.search()
+            this.updateTranscriptSheetLoading = false
+          })
+          .catch(() => {
+            this.updateTranscriptSheetLoading = false
+          })
+      }).onCancel(() => {
+        // console.log('>>>> Cancel')
+      }).onDismiss(() => {
+        // console.log('I am triggered on both OK and Cancel')
+      })
+    },
+    updateTranscriptSheetsUploadCertification (transcriptSheet) {
+      this.$q.dialog({
+        title: 'آپلود فایل مدرک',
+        message: 'فایل مدرک را انتخاب کنید:',
+        prompt: {
+          model: null,
+          type: 'file'
+        },
+        cancel: true,
+        persistent: true
+      }).onOk(data => {
+        this.updateTranscriptSheetLoading = true
+        const formData = new FormData()
+        formData.append('certification', data[0])
+        APIGateway.transcriptSheet.update(transcriptSheet.id, formData)
+          .then(() => {
+            this.$refs.announceResultList.search()
+            this.updateTranscriptSheetLoading = false
+          })
+          .catch(() => {
+            this.updateTranscriptSheetLoading = false
+          })
+      }).onCancel(() => {
+        // console.log('>>>> Cancel')
+      }).onDismiss(() => {
+        // console.log('I am triggered on both OK and Cancel')
+      })
+    },
+    updateTranscriptSheetsRemoveCertification (transcriptSheet) {
+      this.$q.dialog({
+        title: 'حذف فایل مدرک',
+        message: 'آیا از حذف فایل مدرک اطمینان دارید؟',
+        cancel: true,
+        persistent: true
+      }).onOk(() => {
+        this.updateTranscriptSheetLoading = true
+        APIGateway.transcriptSheet.update(transcriptSheet.id, {
+          certification: null
+        })
+          .then(() => {
+            this.$refs.announceResultList.search()
+            this.updateTranscriptSheetLoading = false
+          })
+          .catch(() => {
+            this.updateTranscriptSheetLoading = false
+          })
+      }).onCancel(() => {
+        // console.log('>>>> Cancel')
+      }).onDismiss(() => {
+        // console.log('I am triggered on both OK and Cancel')
+      })
+    },
+    updateTranscriptSheetsUploadIsSentByPost (transcriptSheet) {
+      const state = transcriptSheet.is_sent_by_post ? 'نشده' : 'شده'
+      this.$q.dialog({
+        title: 'ارسال شده با پست',
+        message: 'آیا از تغییر به حالت ارسال ' + state + ' با پست اطمینان دارید؟',
+        cancel: true,
+        persistent: true
+      }).onOk(() => {
+        this.updateTranscriptSheetLoading = true
+        APIGateway.transcriptSheet.update(transcriptSheet.id, {
+          is_sent_by_post: transcriptSheet.is_sent_by_post ? 0 : 1
+        })
+          .then(() => {
+            this.$refs.announceResultList.search()
+            this.updateTranscriptSheetLoading = false
+          })
+          .catch(() => {
+            this.updateTranscriptSheetLoading = false
+          })
+      }).onCancel(() => {
+        // console.log('>>>> Cancel')
+      }).onDismiss(() => {
+        // console.log('I am triggered on both OK and Cancel')
+      })
+    },
+    deleteTranscriptSheet(transcriptSheet) {
+      this.$q.dialog({
+        title: 'حذف ردیف جاری کارنامه',
+        message: 'آیا از حذف ردیف جاری(کارنامه کاربر متناظر) اطمینان دارید؟',
+        cancel: true,
+        persistent: true
+      }).onOk(() => {
+        this.updateTranscriptSheetLoading = true
+        APIGateway.transcriptSheet.deleteById(transcriptSheet.id)
+          .then(() => {
+            this.$refs.announceResultList.search()
+            this.updateTranscriptSheetLoading = false
+          })
+          .catch(() => {
+            this.updateTranscriptSheetLoading = false
+          })
+      }).onCancel(() => {
+        // console.log('>>>> Cancel')
+      }).onDismiss(() => {
+        // console.log('I am triggered on both OK and Cancel')
+      })
+    },
+    getExcel () {
+      this.exportReportLoading = true
+      const filterData = this.$refs.announceResultList.createParams()
+      filterData.type = 'list'
+      filterData.registration__classroom = this.classroom.id
+      APIGateway.transcriptSheet.exportReport(filterData)
+        .then((xlsxData) => {
+          Assist.saveXlsx(xlsxData, this.classroom.title)
+          this.exportReportLoading = false
+        })
+        .catch(() => {
+          this.exportReportLoading = false
+        })
+    }
+  }
+}
+</script>

+ 15 - 13
src/components/Widgets/Classroom/AdminClassroomShow/components/ClassroomInfo.vue

@@ -32,8 +32,10 @@ import { APIGateway } from 'src/api/APIGateway.js'
 import { Classroom } from 'src/models/Classroom.js'
 import { FormBuilderAssist } from 'quasar-form-builder'
 import { UnitCategory } from 'src/models/UnitCategory.js'
+import FormBuilderDateTime from 'src/components/FormBuilderCustumComponents/FormBuilderDateTime.vue'
 import FormBuilderInputEditor from 'src/components/FormBuilderCustumComponents/FormBuilderInputEditor.vue'
 
+const FormBuilderDateTimeComp = shallowRef(FormBuilderDateTime)
 const FormBuilderInputEditorComp = shallowRef(FormBuilderInputEditor)
 
 export default {
@@ -106,15 +108,15 @@ export default {
         { type: 'select', name: 'professor', responseKey: 'professor', options: [], value: null, placeholder: ' ', label: 'استاد دوره', col: 'col-md-3 col-12' },
 
         { type: 'separator', name: 'space', label: 'مشخصات اندیشه جو', className: 'custom-separator', col: 'col-12' },
-        { type: 'select', name: 'audience_status_wwwww', responseKey: 'audience_status_wwwww', options: [], placeholder: ' ', label: 'وضعیت(؟)', col: 'col-md-3 col-12' },
-        { type: 'select', name: 'category', responseKey: 'category', options: [], placeholder: ' ', label: 'مجموعه(؟)', col: 'col-md-3 col-12' },
+        // { type: 'select', name: 'audience_status_wwwww', responseKey: 'audience_status_wwwww', options: [], placeholder: ' ', label: 'وضعیت(؟)', col: 'col-md-3 col-12' },
+        // { type: 'select', name: 'category', responseKey: 'category', options: [], placeholder: ' ', label: 'مجموعه(؟)', col: 'col-md-3 col-12' },
         { type: 'select', name: 'audience_gender_type', responseKey: 'audience_gender_type', options: Enums.classroomGenders, placeholder: ' ', label: 'جنسیت', col: 'col-md-3 col-12' },
-        { type: 'select', name: 'audience_bashgah_wwwww', responseKey: 'audience_bashgah_wwwww', options: [], placeholder: ' ', label: 'باشگاه(؟)', col: 'col-md-3 col-12' },
+        // { type: 'select', name: 'audience_bashgah_wwwww', responseKey: 'audience_bashgah_wwwww', options: [], placeholder: ' ', label: 'باشگاه(؟)', col: 'col-md-3 col-12' },
 
         { type: 'separator', name: 'space', label: 'تنظیمات دوره', className: 'custom-separator', col: 'col-12' },
         { type: 'input', name: 'capacity', responseKey: 'capacity', placeholder: ' ', label: 'ظرفیت گروه درسی', col: 'col-md-3 col-12' },
         { type: 'input', name: 'allowed_absence_count', responseKey: 'allowed_absence_count', placeholder: ' ', label: 'تعداد مجاز غیبت', col: 'col-md-3 col-12' },
-        { type: 'input', name: 'effective_absence_coefficient', responseKey: 'effective_absence_coefficient', placeholder: ' ', label: 'ضریب نمره حضور و غیاب', col: 'col-md-3 col-12' },
+        { type: 'input', name: 'effective_absence_coefficient', responseKey: 'effective_absence_coefficient', placeholder: ' ', label: 'وزن نمره حضور و غیاب ', col: 'col-md-3 col-12' },
         { type: 'input', name: 'mandatory_assignment_count', responseKey: 'mandatory_assignment_count', placeholder: ' ', label: 'تعداد تکالیف اجباری', col: 'col-md-3 col-12' },
 
         { type: 'input', name: 'special_passing_mark', responseKey: 'special_passing_mark', placeholder: ' ', label: 'نمره قبولی مشروطی ها', col: 'col-md-3 col-12' },
@@ -122,24 +124,24 @@ export default {
         { type: 'input', name: 'minimum_clean_passing_mark', responseKey: 'minimum_clean_passing_mark', placeholder: ' ', label: 'نمره قبولی', col: 'col-md-3 col-12' },
 
         { type: 'separator', name: 'space', label: 'ثبت نام و حذف و اضافه', className: 'custom-separator', col: 'col-12' },
-        { type: 'dateTime', name: 'beginning_enrollment_period', responseKey: 'beginning_enrollment_period', placeholder: ' ', label: 'تاریخ شروع پیش ثبت نام', col: 'col-md-6 col-12' },
-        { type: 'dateTime', name: 'ending_enrollment_period', responseKey: 'ending_enrollment_period', placeholder: ' ', label: 'تاریخ پایان پیش ثبت نام', col: 'col-md-6 col-12' },
-        { type: 'dateTime', name: 'beginning_registration_period', responseKey: 'beginning_registration_period', placeholder: ' ', label: 'تاریخ شروع ثبت نام', col: 'col-md-6 col-12' },
-        { type: 'dateTime', name: 'ending_registration_period', responseKey: 'ending_registration_period', placeholder: ' ', label: 'تاریخ پایان ثبت نام', col: 'col-md-6 col-12' },
+        { type: FormBuilderDateTimeComp, name: 'beginning_enrollment_period', responseKey: 'beginning_enrollment_period', outsideLabel: 'تاریخ شروع پیش ثبت نام', col: 'col-md-6 col-12' },
+        { type: FormBuilderDateTimeComp, name: 'ending_enrollment_period', responseKey: 'ending_enrollment_period', outsideLabel: 'تاریخ پایان پیش ثبت نام', col: 'col-md-6 col-12' },
+        { type: FormBuilderDateTimeComp, name: 'beginning_registration_period', responseKey: 'beginning_registration_period', outsideLabel: 'تاریخ شروع ثبت نام', col: 'col-md-6 col-12' },
+        { type: FormBuilderDateTimeComp, name: 'ending_registration_period', responseKey: 'ending_registration_period', outsideLabel: 'تاریخ پایان ثبت نام', col: 'col-md-6 col-12' },
 
         { type: 'input', name: 'registration_period_refund_percent', responseKey: 'registration_period_refund_percent', placeholder: ' ', label: 'درصد برگشت هزینه انصراف در بازه ثبت نام', col: 'col-md-3 col-12' },
         { type: 'checkbox', name: 'allow_dropping_on_enrollment_period', responseKey: 'allow_dropping_on_enrollment_period', label: 'امکان حذف در بازه پیش ثبت نام', col: 'col-md-3 col-12' },
         { type: 'checkbox', name: 'allow_adding_on_registration_period', responseKey: 'allow_adding_on_registration_period', label: 'امکان اضافه کردن در بازه ثبت نام', col: 'col-md-3 col-12' },
         { type: 'checkbox', name: 'allow_dropping_on_registration_period', responseKey: 'allow_dropping_on_registration_period', label: 'امکان حذف ثبت نام در بازه ثبت نام', col: 'col-md-3 col-12' },
 
-        { type: 'dateTime', name: 'beginning_drop_add_period', responseKey: 'beginning_drop_add_period', placeholder: ' ', label: 'تاریخ شروع حذف و اضافه', col: 'col-md-6 col-12' },
-        { type: 'dateTime', name: 'ending_drop_add_period', responseKey: 'ending_drop_add_period', placeholder: ' ', label: 'تاریخ پایان حذف و اضافه', col: 'col-md-6 col-12' },
+        { type: FormBuilderDateTimeComp, name: 'beginning_drop_add_period', responseKey: 'beginning_drop_add_period', outsideLabel: 'تاریخ شروع حذف و اضافه', col: 'col-md-6 col-12' },
+        { type: FormBuilderDateTimeComp, name: 'ending_drop_add_period', responseKey: 'ending_drop_add_period', outsideLabel: 'تاریخ پایان حذف و اضافه', col: 'col-md-6 col-12' },
 
         { type: 'input', name: 'drop_add_period_refund_percent', responseKey: 'drop_add_period_refund_percent', placeholder: ' ', label: 'درصد برگشت هزینه انصراف در بازه حذف و اضافه', col: 'col-md-3 col-12' },
         { type: 'checkbox', name: 'allow_adding_on_drop_add_period', responseKey: 'allow_adding_on_drop_add_period', label: 'امکان اضافه شدن در بازه حذف و اضافه', col: 'col-md-3 col-12' },
         { type: 'checkbox', name: 'allow_dropping_on_drop_add_period', responseKey: 'allow_dropping_on_drop_add_period', label: 'امکان حذف در بازه حذف و اضافه', col: 'col-md-3 col-12' },
 
-        { type: 'dateTime', name: 'publish_time', responseKey: 'publish_time', placeholder: ' ', label: 'تاریخ انتشار', col: 'col-md-6 col-12' },
+        { type: FormBuilderDateTimeComp, name: 'publish_time', responseKey: 'publish_time', outsideLabel: 'تاریخ انتشار', col: 'col-md-6 col-12' },
 
         { type: 'separator', name: 'space', label: 'قوانین', className: 'custom-separator', col: 'col-12' },
         { type: FormBuilderInputEditorComp, name: 'rules', responseKey: 'rules', label: 'قوانین دوره آموزشی', col: 'col-12' },
@@ -150,8 +152,8 @@ export default {
 
         // -----------------------------------------------------------------------------------------------------------
 
-        { type: 'separator', name: 'space', label: 'موارد زیر در طرح نبودند', className: 'custom-separator', col: 'col-12' },
-        { type: 'select', name: 'audience_role', responseKey: 'audience_role', options: Enums.groups, placeholder: ' ', label: 'نقش', col: 'col-md-4 col-12' },
+        // { type: 'separator', name: 'space', label: 'موارد زیر در طرح نبودند', className: 'custom-separator', col: 'col-12' },
+        // { type: 'select', name: 'audience_role', responseKey: 'audience_role', options: Enums.groups, placeholder: ' ', label: 'نقش', col: 'col-md-4 col-12' },
 
         { type: 'hidden', name: 'id', responseKey: 'id', label: 'id', col: 'col-md-3 col-12' }
       ],

+ 52 - 7
src/components/Widgets/Classroom/AdminClassroomShow/components/GraderList.vue

@@ -14,13 +14,14 @@
                     v-model:value="addGraderInputs"
                     :loading="addNewGraderLoading" />
     </template>
-    <template v-slot:entity-index-table-cell="{inputData, showConfirmRemoveDialog}">
+    <template v-slot:entity-index-table-cell="{inputData}">
       <template v-if="inputData.col.name === 'number'">
         {{ inputData.rowNumber }}
       </template>
       <template v-else-if="inputData.col.name === 'actions'">
         <div class="action-column-entity-index">
-          <delete-btn @click="showConfirmRemoveDialog(inputData.props.row, 'id', getRemoveMessage(inputData.props.row))" />
+          <delete-btn :loading="deletingLoading"
+                      @click="onDeletingClassroomGrader(inputData.props.row)" />
         </div>
       </template>
       <template v-else-if="inputData.col.name === 'members'">
@@ -48,10 +49,12 @@ import { shallowRef } from 'vue'
 import { EntityIndex } from 'quasar-crud'
 import { APIGateway } from 'src/api/APIGateway.js'
 import DeleteBtn from 'src/components/Control/DeleteBtn.vue'
-import { FormBuilder, inputMixin } from 'quasar-form-builder'
+import { FormBuilder, FormBuilderAssist, inputMixin } from 'quasar-form-builder'
 import EntityInput from 'quasar-crud/src/components/Entity/Attachment/EntityInput.vue'
+import BtnControl from 'src/components/Control/btn.vue'
 
 const EntityInputComp = shallowRef(EntityInput)
+const BtnControlComp = shallowRef(BtnControl)
 
 export default {
   name: 'GraderList',
@@ -92,7 +95,7 @@ export default {
             buttonBadgeColor: 'pink'
           },
 
-          apiAddress: APIGateway.user.APIAdresses.base,
+          apiAddress: APIGateway.user.APIAdresses.unassignedGradersWithLastPassedUnit,
           tableTitle: 'لیست کل مصححین',
           showTableItemsRouteName: 'Admin.BlockManagement.Show',
           tableKeys: {
@@ -118,6 +121,13 @@ export default {
                 align: 'left',
                 field: row => row.firstname + ' ' + row.lastname
               },
+              {
+                name: 'last_passed_unit_title',
+                required: true,
+                label: 'آخرین دوره',
+                align: 'left',
+                field: row => row.last_passed_unit_title
+              },
               {
                 name: 'national_code',
                 required: true,
@@ -171,9 +181,11 @@ export default {
       ],
 
       graderListInputs: [
-        { type: 'hidden', name: 'classroom', value: classroomId }
+        { type: 'hidden', name: 'classroom', value: classroomId },
+        { type: 'input', name: 'search', value: null, col: 'col-md-3 col-12', label: 'جست و جو', placeholder: ' ' },
+        { type: BtnControlComp, name: 'btn', label: 'جستجو', placeholder: ' ', atClick: () => {}, col: 'col-md-2 col-12' }
       ],
-      graderListApi: APIGateway.classroomGrader.APIAdresses.base,
+      graderListApi: APIGateway.classroomGrader.APIAdresses.withLastPassedUnit,
       graderListTable: {
         columns: [
           {
@@ -190,6 +202,13 @@ export default {
             align: 'left',
             field: row => row.grader_info.id
           },
+          {
+            name: 'last_passed_unit_title',
+            required: true,
+            label: 'آخرین دوره',
+            align: 'left',
+            field: row => row.last_passed_unit_title
+          },
           {
             name: 'creation_time',
             required: true,
@@ -240,7 +259,8 @@ export default {
         currentPage: 'current',
         perPage: 'per_page',
         pageKey: 'page'
-      }
+      },
+      deletingLoading: false
     }
   },
   computed: {
@@ -255,9 +275,34 @@ export default {
     }
   },
   mounted () {
+    this.setGraderListActionBtn()
     this.mounted = true
   },
   methods: {
+    setGraderListActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.graderListInputs, 'btn', 'atClick', this.searchGraderList)
+    },
+    searchGraderList () {
+      this.$refs.graderList.search()
+    },
+    onDeletingClassroomGrader (row) {
+      this.$q.dialog({
+        title: 'توجه',
+        message: 'آیا از حذف ' + row.grader_info.firstname + ' ' + row.grader_info.lastname + ' اطمینان دارید؟',
+        cancel: true,
+        persistent: true
+      }).onOk(() => {
+        this.deletingLoading = true
+        APIGateway.classroomGrader.deleteById(row.id)
+          .then(() => {
+            this.deletingLoading = false
+            this.$refs.graderList.search()
+          })
+          .catch(() => {
+            this.deletingLoading = false
+          })
+      })
+    },
     onConfirmedSelected (data) {
       const graders = data.map(item => item.id)
       this.addNewGraderLoading = true

+ 53 - 8
src/components/Widgets/Classroom/AdminClassroomShow/components/LeaderList.vue

@@ -14,13 +14,14 @@
                     v-model:value="addLeaderInputs"
                     :loading="addNewLeaderLoading" />
     </template>
-    <template v-slot:entity-index-table-cell="{inputData, showConfirmRemoveDialog}">
+    <template v-slot:entity-index-table-cell="{inputData}">
       <template v-if="inputData.col.name === 'number'">
         {{ inputData.rowNumber }}
       </template>
       <template v-else-if="inputData.col.name === 'actions'">
         <div class="action-column-entity-index">
-          <delete-btn @click="showConfirmRemoveDialog(inputData.props.row, 'id', getRemoveMessage(inputData.props.row))" />
+          <delete-btn :loading="deletingLoading"
+                      @click="onDeletingClassroomLeader(inputData.props.row)" />
         </div>
       </template>
       <template v-else-if="inputData.col.name === 'members'">
@@ -48,10 +49,12 @@ import { shallowRef } from 'vue'
 import { EntityIndex } from 'quasar-crud'
 import { APIGateway } from 'src/api/APIGateway.js'
 import DeleteBtn from 'src/components/Control/DeleteBtn.vue'
-import { FormBuilder, inputMixin } from 'quasar-form-builder'
+import { FormBuilder, FormBuilderAssist, inputMixin } from 'quasar-form-builder'
 import EntityInput from 'quasar-crud/src/components/Entity/Attachment/EntityInput.vue'
+import BtnControl from 'src/components/Control/btn.vue'
 
 const EntityInputComp = shallowRef(EntityInput)
+const BtnControlComp = shallowRef(BtnControl)
 
 export default {
   name: 'LeaderList',
@@ -92,7 +95,7 @@ export default {
             buttonBadgeColor: 'pink'
           },
 
-          apiAddress: APIGateway.user.APIAdresses.base,
+          apiAddress: APIGateway.user.APIAdresses.unassignedLeadersWithLastPassedUnit,
           tableTitle: 'لیست کل سرگروه ها',
           showTableItemsRouteName: 'Admin.BlockManagement.Show',
           tableKeys: {
@@ -118,6 +121,13 @@ export default {
                 align: 'left',
                 field: row => row.firstname + ' ' + row.lastname
               },
+              {
+                name: 'last_passed_unit_title',
+                required: true,
+                label: 'آخرین دوره',
+                align: 'left',
+                field: row => row.last_passed_unit_title
+              },
               {
                 name: 'national_code',
                 required: true,
@@ -157,9 +167,11 @@ export default {
       ],
 
       leaderListInputs: [
-        { type: 'hidden', name: 'classroom', value: classroomId }
+        { type: 'hidden', name: 'classroom', value: classroomId },
+        { type: 'input', name: 'search', value: null, col: 'col-md-3 col-12', label: 'جست و جو', placeholder: ' ' },
+        { type: BtnControlComp, name: 'btn', label: 'جستجو', placeholder: ' ', atClick: () => {}, col: 'col-md-2 col-12' }
       ],
-      leaderListApi: APIGateway.classroomLeaders.APIAdresses.base,
+      leaderListApi: APIGateway.classroomLeaders.APIAdresses.withLastPassedUnit,
       leaderListTable: {
         columns: [
           {
@@ -176,12 +188,19 @@ export default {
             align: 'left',
             field: row => row.leader_info.id
           },
+          {
+            name: 'last_passed_unit_title',
+            required: true,
+            label: 'آخرین دوره',
+            align: 'left',
+            field: row => row.last_passed_unit_title
+          },
           {
             name: 'title',
             required: true,
             label: 'جنسیت',
             align: 'left',
-            field: row => (row.leader_info.gender === 1) ? 'مذکر' : ((row.leader_info.gender === 2) ? 'مونث' : '-')
+            field: row => (row.leader_info.gender === 'MALE') ? 'آقا' : ((row.leader_info.gender === 'FEMALE') ? 'خانم' : '-')
           },
           {
             name: 'creation_time',
@@ -233,7 +252,8 @@ export default {
         currentPage: 'current',
         perPage: 'per_page',
         pageKey: 'page'
-      }
+      },
+      deletingLoading: false
     }
   },
   computed: {
@@ -248,9 +268,34 @@ export default {
     }
   },
   mounted () {
+    this.setLeaderListActionBtn()
     this.mounted = true
   },
   methods: {
+    setLeaderListActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.leaderListInputs, 'btn', 'atClick', this.searchLeaderList)
+    },
+    searchLeaderList () {
+      this.$refs.leaderList.search()
+    },
+    onDeletingClassroomLeader (row) {
+      this.$q.dialog({
+        title: 'توجه',
+        message: 'آیا از حذف ' + row.leader_info.firstname + ' ' + row.leader_info.lastname + ' اطمینان دارید؟',
+        cancel: true,
+        persistent: true
+      }).onOk(() => {
+        this.deletingLoading = true
+        APIGateway.classroomLeaders.deleteById(row.id)
+          .then(() => {
+            this.deletingLoading = false
+            this.$refs.leaderList.search()
+          })
+          .catch(() => {
+            this.deletingLoading = false
+          })
+      })
+    },
     onConfirmedSelected (data) {
       const leaders = data.map(item => item.id)
       this.addNewLeaderLoading = true

+ 7 - 0
src/components/Widgets/Classroom/AdminClassroomShow/components/MembersList.vue

@@ -22,6 +22,13 @@
       </template>
       <template v-else-if="inputData.col.name === 'action'">
         <div class="action-column-entity-index">
+          <q-btn size="md"
+                 color="primary"
+                 outline
+                 class="q-mr-md"
+                 :to="{ name: 'Admin.Classroom.UserClass', params: { classroom_id: classroomId, user_id: inputData.props.row.owner }}">
+            سابقه آموزشی
+          </q-btn>
           <q-btn v-if="inputData.props.row.status === 'DROPPED_FOR_CONDITION'"
                  color="primary"
                  class="q-mr-md"

+ 1 - 1
src/components/Widgets/Classroom/ClassroomAnswerBookList/ClassroomAnswerBookList.vue

@@ -213,7 +213,7 @@ export default {
   },
   mounted () {
     this.setActionBtn()
-    this.setClassroomTypeOfInputs()
+    // this.setClassroomTypeOfInputs() // for what?
     this.mounted = true
   },
   methods: {

+ 46 - 32
src/components/Widgets/Classroom/ClassroomList/ClassroomList.vue

@@ -15,6 +15,11 @@
       <template v-if="inputData.col.name === 'number'">
         {{ inputData.rowNumber }}
       </template>
+      <template v-if="inputData.col.name === 'status'">
+        <div :class="this.getRegistrationStatusTitleClass(inputData.props.row)">
+          {{ this.getRegistrationStatusTitle(inputData.props.row) }}
+        </div>
+      </template>
       <template v-else-if="inputData.col.name === 'action'">
         <q-btn size="md"
                color="primary"
@@ -31,6 +36,11 @@
           <template v-if="col.name === 'number'">
             {{ inputData.rowNumber }}
           </template>
+          <template v-if="col.name === 'status'">
+            <div :class="this.getRegistrationStatusTitleClass(row)">
+              {{ this.getRegistrationStatusTitle(row) }}
+            </div>
+          </template>
           <template v-else-if="col.name === 'action'">
             <q-btn size="md"
                    color="primary"
@@ -57,6 +67,7 @@ import { Registration } from 'src/models/Registration.js'
 import { mixinAuth, mixinWidget } from 'src/mixin/Mixins.js'
 import EntityIndexGridItem from 'src/components/EntityIndexGridItem.vue'
 import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
+import { TranscriptSheet } from 'src/models/TranscriptSheet'
 
 const BtnControlComp = shallowRef(BtnControl)
 
@@ -72,33 +83,6 @@ export default {
     return {
       mounted: false,
       inputs: [
-        {
-          type: 'select',
-          name: 'classroom__unit__category__type',
-          options: [
-            {
-              label: 'دوره های آموزشی',
-              value: 'TRAINING'
-            },
-            {
-              label: 'حلقه های مباحثاتی',
-              value: 'DISCUSSION_CIRCLE'
-            }
-          ],
-          value: 'TRAINING',
-          label: 'نوع',
-          placeholder: ' ',
-          col: 'col-md-2 col-12'
-        },
-        {
-          type: 'select',
-          name: 'status',
-          options: Enums.classroomStatuses,
-          value: null,
-          label: 'وضعیت دوره',
-          placeholder: ' ',
-          col: 'col-md-2 col-12'
-        },
         {
           type: 'select',
           name: 'category',
@@ -118,6 +102,7 @@ export default {
           placeholder: ' ',
           col: 'col-md-2 col-12'
         },
+        { type: 'hidden', name: 'classroom__unit__category__type', value: 'TRAINING' },
         { type: 'hidden', name: 'owner', value: null },
         { type: 'input', name: 'search', label: 'جستجو', placeholder: ' ', col: 'col-md-2 col-12' },
         { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'جستجو', placeholder: ' ', atClick: () => {}, col: 'col-md-2 col-12' }
@@ -172,15 +157,15 @@ export default {
             required: true,
             label: 'تعداد جلسات',
             align: 'left',
-            field: row => row.classroom_info?.sessions_info?.length
+            field: row => row.classroom_info?.sessions_count
           },
           {
             name: 'status',
             required: true,
             label: 'وضعیت',
             align: 'left',
-            field: row => (new Registration(row)).status_info.label
-            // field: row => this.getClassroomStatusTitle(row.classroom_info.status)
+            // field: row => (new Registration(row)).status_info.label
+            field: row => this.getRegistrationStatusTitle(row)
           },
           {
             name: 'title',
@@ -190,7 +175,7 @@ export default {
             field: row => ShamsiDate.getDateTime(row.creation_time)
           },
           {
-            name: 'status',
+            name: 'invoice_status',
             required: true,
             label: 'وضعیت مالی',
             align: 'left',
@@ -263,7 +248,7 @@ export default {
         FormBuilderAssist.setAttributeByName(this.inputs, 'classroom__unit__category__type', 'type', 'hidden')
         FormBuilderAssist.setAttributeByName(this.inputs, 'category', 'type', 'hidden')
         FormBuilderAssist.setAttributeByName(this.inputs, 'unit', 'label', 'دسته')
-        FormBuilderAssist.setAttributeByName(this.inputs, 'status', 'label', 'وضعیت رویداد')
+        FormBuilderAssist.setAttributeByName(this.inputs, 'status', 'label', 'مرحله رویداد')
 
         this.table.columns.forEach(col => {
           if (col.name === 'professor_info') {
@@ -293,6 +278,19 @@ export default {
 
       return target.label
     },
+    getRegistrationStatusTitle (registration) {
+      if (registration.status === 'REGISTERED' && registration.transcript_sheet_info) {
+        return (new TranscriptSheet(registration.transcript_sheet_info)).status_info.label
+      } else {
+        return (new Registration(registration)).status_info.label
+      }
+    },
+    getRegistrationStatusTitleClass (registration) {
+      if (registration.status === 'REGISTERED' && registration.transcript_sheet_info) {
+        return (new TranscriptSheet(registration.transcript_sheet_info)).status_info.value
+      }
+      return null
+    },
     getClassroomStatusTitle (type) {
       const target = Enums.classroomStatuses.find(item => item.value === type)
       if (!target) {
@@ -359,3 +357,19 @@ export default {
   }
 }
 </script>
+
+<style scoped lang="scss">
+.CLEAN_PASSED {
+  //font-size: 1.1em;
+  color: green;
+}
+.CONDITIONAL_PASSED{
+  color: yellow;
+}
+.FAILED{
+  color: darkred;
+}
+.FAILED_DUE_TO_PRE_CONDITIONAL{
+  color: red;
+}
+</style>

+ 12 - 3
src/components/Widgets/Classroom/ClassroomShow/ClassroomShow.vue

@@ -18,7 +18,7 @@
             narrow-indicator>
       <q-tab v-if="canShowTabPageForClassroomType('classroomInfo')"
              name="classroomInfo"
-             label="اطلاعات دوره آموزشی" />
+             :label="` اطلاعات ${classroomTypeTitle}`" />
       <q-tab v-if="canShowOtherTabsOfClassroom && canShowTabPageForClassroomType('educations')"
              name="educations"
              :label="educationsTabTitle" />
@@ -493,6 +493,15 @@ export default {
     }
   },
   computed: {
+    classroomTypeTitle () {
+      if (this.localOptions.classroomType === 'TRAINING') {
+        return 'دوره'
+      }
+      if (this.localOptions.classroomType === 'EVENT') {
+        return 'رویداد'
+      }
+      return 'دوره'
+    },
     listTitle () {
       if (this.localOptions.classroomType === 'TRAINING') {
         return 'لیست دوره های آموزشی'
@@ -612,8 +621,8 @@ export default {
         loading: false,
         path: [
           {
-            label: 'دوره های من',
-            to: { name: 'UserPanel.Profile.AllClassrooms' }
+            label: this.classroomTypeTitle + 'های من',
+            to: { name: this.localOptions.classroomType === 'TRAINING' ? 'UserPanel.Profile.AllClassrooms' : 'UserPanel.Profile.AllEvents' }
           },
           {
             label: this.classroom.title,

+ 171 - 0
src/components/Widgets/Content/AdminContentCreate-new/AdminContentCreate.vue

@@ -0,0 +1,171 @@
+<template>
+  <div class="AdminPostCreate"
+       :style="localOptions.style">
+    <div class="title">
+      نوشته جدید
+      <div class="back-action">
+        <q-btn flat
+               :to="{name: 'AdminPanel.Post.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"
+                     :entity-id-key="entityIdKey"
+                     :entity-param-key="entityParamKey"
+                     :show-route-name="showRouteName"
+                     :default-layout="false"
+                     :show-close-button="false"
+                     :show-edit-button="false"
+                     :show-expand-button="false"
+                     :show-save-button="false"
+                     :show-reload-button="false" />
+      <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 { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import PostMixin from 'src/components/Widgets/Post/PostMixin.js'
+import PostCategorySelector from 'src/components/FormBuilderCustumComponents/PostCategorySelector.vue'
+import FormBuilderTiptapEditor from 'src/components/FormBuilderCustumComponents/FormBuilderTiptapEditor.vue'
+// import { FormBuilderAssist } from 'quasar-form-builder'
+
+const PostCategorySelectorComp = shallowRef(PostCategorySelector)
+const FormBuilderTiptapEditorComp = shallowRef(FormBuilderTiptapEditor)
+
+export default {
+  name: 'AdminPostCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget, PostMixin],
+  data () {
+    const authorization = 'Bearer ' + this.$store.getters['Auth/accessToken']
+    const uploadVideo = this.uploadVideo
+    const uploadAudio = this.uploadAudio
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.content.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'AdminPanel.Post.Show',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-12' },
+        { type: 'input', name: 'order', responseKey: 'order', label: 'ترتیب', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: PostCategorySelectorComp, name: 'category', responseKey: 'category', col: 'col-12' },
+        {
+          // type: 'tiptap-editor',
+          type: FormBuilderTiptapEditorComp,
+          name: 'summary',
+          responseKey: 'summary',
+          label: 'خلاصه',
+          options: {
+            bubbleMenu: false,
+            floatingMenu: false,
+            poem: false,
+            reading: false,
+            loadBareHtml: true,
+            persianKeyboard: true,
+            mathliveOptions: { smartFence: false },
+            uploadServer: {
+              url: '/api' + APIGateway.media.APIAdresses.base,
+              instantUpload: true,
+              responseKey: 'file',
+              headers: { Authorization: authorization }
+            },
+            uploadVideo,
+            uploadAudio
+          },
+          col: 'col-md-12 col-12'
+        },
+        {
+          // type: 'tiptap-editor',
+          type: FormBuilderTiptapEditorComp,
+          name: 'text',
+          responseKey: 'text',
+          label: 'متن',
+          options: {
+            bubbleMenu: false,
+            floatingMenu: false,
+            poem: false,
+            reading: false,
+            loadBareHtml: true,
+            persianKeyboard: true,
+            mathliveOptions: { smartFence: false },
+            uploadServer: {
+              url: '/api' + APIGateway.media.APIAdresses.base,
+              instantUpload: true,
+              responseKey: 'file',
+              headers: { Authorization: authorization }
+            },
+            uploadVideo,
+            uploadAudio
+          },
+          col: 'col-md-12 col-12'
+        }
+      ]
+    }
+  },
+  mounted () {
+    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">
+.AdminPostCreate {
+  .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/Content/AdminContentCreate-new/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>

+ 148 - 0
src/components/Widgets/Content/AdminContentList-new/AdminContentList.vue

@@ -0,0 +1,148 @@
+<template>
+  <div class="AdminPostList"
+       :style="localOptions.style">
+    <div class="title">
+      نوشته ها
+      <div class="back-action">
+        <q-btn :to="{name: 'Admin.Content.Create'}"
+               color="primary">
+          ایجاد محتوای جدید
+        </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"
+                    :default-layout="false"
+                    :show-search-button="false"
+                    :show-expand-button="false"
+                    :show-reload-button="false">
+        <template #entity-index-table-cell="{inputData}">
+          <template v-if="inputData.col.name === 'number'">
+            {{ inputData.rowNumber }}
+          </template>
+          <template v-else-if="inputData.col.name === 'thumbnail'">
+            <q-img :src="inputData.col.value"
+                   width="100px" />
+          </template>
+          <template v-else-if="inputData.col.name === 'action'">
+            <q-btn color="primary"
+                   :to="{name: 'AdminPanel.Post.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 { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'AdminPostList',
+  components: { EntityIndex },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      api: APIGateway.content.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [],
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'thumbnail',
+            required: true,
+            label: 'تصویر',
+            align: 'left',
+            field: row => row.thumbnail
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'category_info',
+            required: true,
+            label: 'دسته',
+            align: 'left',
+            field: row => row.category_info?.title
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.mounted = true
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminPostList {
+  .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(.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/Content/AdminContentList-new/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>

+ 40 - 8
src/components/Widgets/Content/AdminContentList/AdminContentList.vue

@@ -1,12 +1,15 @@
 <template>
   <div class="AdminContentList"
        :style="localOptions.style">
+    <q-linear-progress v-if="loading"
+                       indeterminate />
     <div class="more-action">
       <q-btn label="محتوای جدید"
              color="primary"
              :to="{name: 'Admin.Content.Create'}" />
     </div>
     <entity-index v-if="mounted"
+                  ref="entityIndex"
                   v-model:value="inputs"
                   title="لیست محتواها"
                   :api="api"
@@ -16,7 +19,7 @@
                   :show-search-button="false"
                   :show-expand-button="false"
                   :show-reload-button="false">
-      <template #entity-index-table-cell="{inputData}">
+      <template #entity-index-table-cell="{inputData, showConfirmRemoveDialog}">
         <template v-if="inputData.col.name === 'number'">
           {{ inputData.rowNumber }}
         </template>
@@ -25,10 +28,14 @@
                  width="100px" />
         </template>
         <template v-else-if="inputData.col.name === 'action'">
-          <q-btn color="primary"
-                 :to="{name: 'Admin.Content.Show', params: {id: inputData.props.row.id}}">
-            مشاهده جزییات
-          </q-btn>
+          <div class="action-column-entity-index">
+            <q-btn color="primary"
+                   :to="{name: 'Admin.Content.Show', params: {id: inputData.props.row.id}}">
+              مشاهده جزییات
+            </q-btn>
+            <delete-btn @click="showConfirmRemoveDialog(inputData.props.row, 'id', getRemoveMessage(inputData.props.row))" />
+          </div>
+
         </template>
         <template v-else>
           {{ inputData.col.value }}
@@ -39,16 +46,24 @@
 </template>
 
 <script>
+import { shallowRef } from 'vue'
 import { EntityIndex } 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'
+import DeleteBtn from 'src/components/Control/DeleteBtn.vue'
+import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
 
 export default {
   name: 'AdminContentList',
-  components: { EntityIndex },
+  components: { DeleteBtn, EntityIndex },
   mixins: [mixinWidget],
-  data: () => {
+  data () {
     return {
+      loading: false,
       api: APIGateway.content.APIAdresses.base,
       tableKeys: {
         data: 'results',
@@ -57,7 +72,11 @@ export default {
         perPage: 'per_page',
         pageKey: 'page'
       },
-      inputs: [],
+      inputs: [
+        { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category', col: 'col-12' },
+        { type: 'input', name: 'search', value: null, col: 'col-md-3 col-12', label: 'جست و جو', placeholder: ' ' },
+        { type: BtnControlComp, name: 'btn', label: 'جستجو', placeholder: ' ', atClick: this.search, col: 'col-md-2 col-12' }
+      ],
       table: {
         columns: [
           {
@@ -103,6 +122,14 @@ export default {
   },
   mounted() {
     this.mounted = true
+  },
+  methods: {
+    search () {
+      this.$refs.entityIndex.search()
+    },
+    getRemoveMessage (row) {
+      return 'آیا از حذف ' + row.title + ' اطمینان دارید؟'
+    }
   }
 }
 </script>
@@ -115,5 +142,10 @@ export default {
     justify-content: flex-end;
     margin-bottom: 10px;
   }
+  .action-column-entity-index {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+  }
 }
 </style>

+ 180 - 0
src/components/Widgets/Content/AdminContentShow-new/AdminContentShow.vue

@@ -0,0 +1,180 @@
+<template>
+  <div class="AdminPostShow"
+       :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="اطلاعات محتوا"
+                 :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'
+import PostMixin from 'src/components/Widgets/Post/PostMixin.js'
+import PostCategorySelector from 'src/components/FormBuilderCustumComponents/PostCategorySelector.vue'
+import FormBuilderTiptapEditor from 'src/components/FormBuilderCustumComponents/FormBuilderTiptapEditor.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const PostCategorySelectorComp = shallowRef(PostCategorySelector)
+const FormBuilderTiptapEditorComp = shallowRef(FormBuilderTiptapEditor)
+
+export default {
+  name: 'AdminPostShow',
+  components: {
+    EntityEdit
+  },
+  mixins: [mixinWidget, PostMixin],
+  data () {
+    const postId = this.$route.params.id
+    const uploadVideo = this.uploadVideo
+    const uploadPhoto = this.uploadPhoto
+    const uploadAudio = this.uploadAudio
+    return {
+      mounted: false,
+      entityLoading: true,
+      api: APIGateway.content.APIAdresses.byId(postId),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'AdminPanel.Post.Show',
+      indexRouteName: 'AdminPanel.Post.List',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-12' },
+        { type: 'input', name: 'order', responseKey: 'order', label: 'ترتیب', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: PostCategorySelectorComp, name: 'category', responseKey: 'category_info', col: 'col-md-12 col-12' },
+        {
+          // type: 'tiptap-editor',
+          type: FormBuilderTiptapEditorComp,
+          name: 'summary',
+          responseKey: 'summary',
+          label: 'خلاصه',
+          options: {
+            loadBareHtml: true,
+            persianKeyboard: true,
+            uploadPhoto,
+            uploadVideo,
+            uploadAudio
+          },
+          col: 'col-md-12 col-12'
+        },
+        {
+          // type: 'tiptap-editor',
+          type: FormBuilderTiptapEditorComp,
+          name: 'text',
+          responseKey: 'text',
+          label: 'متن',
+          options: {
+            loadBareHtml: true,
+            persianKeyboard: true,
+            uploadPhoto,
+            uploadVideo,
+            uploadAudio
+          },
+          col: 'col-md-12 col-12'
+        },
+        {
+          type: BtnControlComp,
+          name: 'btn',
+          responseKey: 'btn',
+          label: 'ویرایش پست',
+          placeholder: ' ',
+          ignoreValue: true,
+          atClick: () => {
+            this.edit()
+          },
+          col: 'col-md-6'
+        },
+        {
+          type: BtnControlComp,
+          name: 'btnShowPost',
+          responseKey: 'btn',
+          label: 'مشاهده پست',
+          placeholder: ' ',
+          ignoreValue: true,
+          atClick: () => {
+            this.showPost()
+          },
+          col: 'col-md-6'
+        }
+      ]
+    }
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    showPost () {
+      const routeData = this.$router.resolve({ name: 'Public.Post.Show', params: { id: this.$route.params.id } })
+      window.open(routeData.href, '_blank')
+    },
+    afterLoadInputData () {
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity(false)
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminPostShow {
+  .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/Content/AdminContentShow-new/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>

+ 62 - 34
src/components/Widgets/Content/AdminContentShow/AdminContentShow.vue

@@ -9,21 +9,44 @@
         >
       </q-btn>
     </div>
-    <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"
-                 :redirect-after-edit="false"
-                 :after-load-input-data="afterLoadInputData" />
+    <q-card flat>
+      <q-tabs v-model="tab"
+              align="left">
+        <q-tab name="content"
+               label="جزییات محتوا" />
+        <q-tab name="contentTag"
+               label="فیش برداری" />
+      </q-tabs>
+    </q-card>
+    <q-tab-panels v-if="mounted"
+                  v-model="tab">
+      <q-tab-panel name="content"
+                   class="q-pa-none">
+        <entity-edit 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"
+                     :redirect-after-edit="false"
+                     :after-load-input-data="afterLoadInputData" />
+      </q-tab-panel>
+      <q-tab-panel name="contentTag"
+                   class="q-pa-none">
+        <admin-content-topic-create :content="contentId"
+                                    :redirect="false"
+                                    @created="onTopicCreated" />
+        <q-separator class="q-my-md" />
+        <admin-content-topic-list :key="topicListKey"
+                                  :content-id="contentId" />
+      </q-tab-panel>
+    </q-tab-panels>
   </div>
 </template>
 
@@ -35,6 +58,8 @@ import { APIGateway } from 'src/api/APIGateway.js'
 import BtnControl from 'src/components/Control/btn.vue'
 import ContentMedias from 'src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue'
 import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+import AdminContentTopicList from 'src/components/Widgets/Content/AdminContentTopicList/AdminContentTopicList.vue'
+import AdminContentTopicCreate from 'src/components/Widgets/Content/AdminContentTopicCreate/AdminContentTopicCreate.vue'
 
 const BtnControlComp = shallowRef(BtnControl)
 const ContentMediasComp = shallowRef(ContentMedias)
@@ -43,14 +68,18 @@ const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
 export default {
   name: 'AdminContentShow',
   components: {
-    EntityEdit
+    EntityEdit,
+    AdminContentTopicList,
+    AdminContentTopicCreate
   },
   mixins: [mixinWidget],
-  data: () => {
+  data () {
     return {
       mounted: false,
       entityLoading: true,
-      api: null,
+      topicListKey: Date.now(),
+      api: APIGateway.content.APIAdresses.byId(this.$route.params.id),
+      tab: 'content',
       entityIdKey: 'id',
       entityParamKey: 'id',
       showRouteName: 'Admin.Content.Show',
@@ -61,39 +90,32 @@ export default {
         { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category_info', col: 'col-md-12 col-12' },
         { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-md-12 col-12' },
         { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', col: 'col-md-12 col-12' },
-        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ویرایش محتوا', placeholder: ' ', ignoreValue: true, atClick: () => {}, col: 'col-md-6' }
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ویرایش محتوا', placeholder: ' ', ignoreValue: true, atClick: this.edit, col: 'col-md-6' }
       ]
     }
   },
-  created() {
-    this.api = APIGateway.content.APIAdresses.byId(this.$route.params.id)
+  computed: {
+    contentId () {
+      return parseInt(this.$route.params.id)
+    }
   },
   mounted() {
-    this.setActionBtn()
     this.mounted = true
   },
   methods: {
-    setActionBtn () {
-      this.inputs.forEach((item, index) => {
-        if (item.name === 'btn') {
-          this.inputs[index].atClick = this.edit
-        }
-      })
-    },
     afterLoadInputData () {
       this.entityLoading = false
     },
     edit() {
       this.entityLoading = true
       this.$refs.entityEdit.editEntity(false)
-        .then(() => {
-          this.$refs.entityEdit.getData()
-          this.entityLoading = false
-        })
-        .catch(() => {
+        .finally(() => {
           this.$refs.entityEdit.getData()
           this.entityLoading = false
         })
+    },
+    onTopicCreated () {
+      this.topicListKey = Date.now()
     }
   }
 }
@@ -101,6 +123,12 @@ export default {
 
 <style scoped lang="scss">
 .AdminContentShow {
+  .more-action {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-start;
+    margin-bottom: 10px;
+  }
   .title {
     font-style: normal;
     font-weight: 700;

+ 110 - 0
src/components/Widgets/Content/AdminContentTopicCreate/AdminContentTopicCreate.vue

@@ -0,0 +1,110 @@
+<template>
+  <div class="AdminContentTopicCreate"
+       :style="localOptions.style">
+    <entity-create v-if="mounted"
+                   ref="entityCreate"
+                   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" />
+  </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'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import HashtagSelector from 'src/components/FormBuilderCustumComponents/Hashtags/HashtagSelector.vue'
+import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const HashtagSelectorComp = shallowRef(HashtagSelector)
+const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
+
+export default {
+  name: 'AdminContentCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  props: {
+    content: {
+      type: Number,
+      default: null
+    },
+    redirect: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['created'],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.contentTopic.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.ContentTopicCategory.Show',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان فیش', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', value: null, placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category_info', categoryType: 'contentTopic', col: 'col-12' },
+        { type: HashtagSelectorComp, name: 'hashtags', responseKey: 'hashtags_info', col: 'col-12' },
+        { type: 'inputEditor', name: 'text', responseKey: 'text', label: 'متن', placeholder: ' ', col: 'col-md-12 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ایجاد فیش', placeholder: ' ', atClick: this.create, col: 'col-md-6' },
+        { type: 'hidden', name: 'content', responseKey: 'content' }
+      ]
+    }
+  },
+  created() {
+    FormBuilderAssist.setAttributeByName(this.inputs, 'content', 'value', this.content)
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    create() {
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity(this.redirect)
+        .then(() => {
+          this.$emit('created')
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentTopicCreate {
+  .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/Content/AdminContentTopicCreate/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>

+ 142 - 0
src/components/Widgets/Content/AdminContentTopicList/AdminContentTopicList.vue

@@ -0,0 +1,142 @@
+<template>
+  <div class="AdminContentTopicList"
+       :style="localOptions.style">
+    <div class="more-action">
+      <q-btn label="فیش جدید"
+             color="primary"
+             :to="{name: 'Admin.ContentTopic.Create'}" />
+    </div>
+    <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, showConfirmRemoveDialog}">
+        <template v-if="inputData.col.name === 'number'">
+          {{ inputData.rowNumber }}
+        </template>
+        <template v-else-if="inputData.col.name === 'thumbnail'">
+          <q-img :src="inputData.col.value"
+                 width="100px" />
+        </template>
+        <template v-else-if="inputData.col.name === 'action'">
+          <div class="action-column-entity-index">
+            <q-btn color="primary"
+                   :to="{name: 'Admin.ContentTopic.Show', params: {id: inputData.props.row.id}}">
+              مشاهده جزییات
+            </q-btn>
+            <delete-btn @click="showConfirmRemoveDialog(inputData.props.row, 'id', getRemoveMessage(inputData.props.row))" />
+          </div>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import DeleteBtn from 'src/components/Control/DeleteBtn.vue'
+
+export default {
+  name: 'AdminContentTopicList',
+  components: { EntityIndex, DeleteBtn },
+  mixins: [mixinWidget],
+  props: {
+    contentId: {
+      type: Number,
+      default: null
+    }
+  },
+  data () {
+    return {
+      api: APIGateway.contentTopic.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: 'hidden', name: 'content', value: null }
+      ],
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'thumbnail',
+            required: true,
+            label: 'تصویر',
+            align: 'left',
+            field: row => row.thumbnail
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'category_info',
+            required: true,
+            label: 'دسته',
+            align: 'left',
+            field: row => row.category_info?.title
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  created() {
+    if (this.contentId) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'content', 'value', this.contentId)
+    }
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    getRemoveMessage (row) {
+      return 'آیا از حذف ' + row.title + ' اطمینان دارید؟'
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentTopicList {
+  .more-action {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Content/AdminContentTopicList/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>

+ 112 - 0
src/components/Widgets/Content/AdminContentTopicShow/AdminContentTopicShow.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="AdminContentTopicShow"
+       :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="مشخصات دسته بندی کلی"
+                 :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'
+import { ContentTopic } from 'src/models/ContentTopic.js'
+import HashtagSelector from 'src/components/FormBuilderCustumComponents/Hashtags/HashtagSelector.vue'
+import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const HashtagSelectorComp = shallowRef(HashtagSelector)
+const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
+
+export default {
+  name: 'AdminContentTopicShow',
+  components: {
+    EntityEdit
+  },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      listKey: Date.now(),
+      replyText: null,
+      mounted: false,
+      entityLoading: true,
+      contentTopicCategory: new ContentTopic(),
+      api: APIGateway.contentTopic.APIAdresses.byId(this.$route.params.id),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.ContentTopic.Show',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان فیش', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', value: null, placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category_info', categoryType: 'contentTopic', col: 'col-12' },
+        { type: HashtagSelectorComp, name: 'hashtags', responseKey: 'hashtags_info', col: 'col-12' },
+        { type: 'inputEditor', name: 'text', responseKey: 'text', label: 'متن', placeholder: ' ', col: 'col-md-12 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ویرایش فیش', placeholder: ' ', atClick: this.edit, col: 'col-md-6' },
+        { type: 'hidden', name: 'content', responseKey: 'content' },
+        { type: 'hidden', name: 'id', responseKey: 'id', value: null }
+      ]
+    }
+  },
+  computed: {
+    repliesInfo () {
+      return this.inputs.find(input => input.name === 'replies_info').value
+    }
+  },
+  created() {
+    this.contentTopicCategory.loading = true
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    afterLoadInputData (data) {
+      this.contentTopicCategory = new ContentTopic(data)
+      this.contentTopicCategory.loading = false
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity(false)
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+    },
+    onCreated () {
+      this.listKey = Date.now()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentTopicShow {
+}
+</style>

+ 21 - 0
src/components/Widgets/Content/AdminContentTopicShow/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>

+ 116 - 53
src/components/Widgets/Content/ContentList/ContentList.vue

@@ -1,32 +1,23 @@
 <template>
-  <div class="ContentList"
+  <div class="PostList"
        :style="localOptions.style">
-    <div class="more-action">
-      <q-btn label="ایجاد درخواست"
-             color="primary"
-             :to="{name: 'UserPanel.Ticket.Create'}" />
-    </div>
+    <breadcrumbs class="q-mb-xl"
+                 style="margin-top: 29px; margin-bottom: 19px;" />
     <entity-index v-if="mounted"
                   ref="entityIndex"
                   v-model:value="inputs"
-                  title="لیست درخواست ها"
                   :api="api"
                   :table="table"
                   :table-keys="tableKeys"
-                  :create-route-name="createRouteName"
+                  :table-grid-size="true"
+                  :default-layout="false"
                   :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 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-table-item-cell="{inputData}">
+        <post-item :post="getPost(inputData.props.row)"
+                   :post-show-route-name="localOptions.postShowRouteName"
+                   :btn-show-post-class-name="this.localOptions.btnShowPostClassName" />
       </template>
     </entity-index>
   </div>
@@ -34,17 +25,21 @@
 
 <script>
 import { EntityIndex } from 'quasar-crud'
-import { Ticket } from 'src/models/Ticket.js'
+import { Post } from 'src/models/Post.js'
 import { mixinWidget } from 'src/mixin/Mixins.js'
 import { APIGateway } from 'src/api/APIGateway.js'
+import PostItem from 'src/components/PostItem.vue'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import { ContentCategory } from 'src/models/ContentCategory.js'
+import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
 
 export default {
-  name: 'ContentList',
-  components: { EntityIndex },
+  name: 'PostList',
+  components: { PostItem, EntityIndex, Breadcrumbs },
   mixins: [mixinWidget],
-  data: () => {
+  data () {
     return {
-      api: APIGateway.ticket.APIAdresses.base,
+      api: APIGateway.content.APIAdresses.base,
       tableKeys: {
         data: 'results',
         total: 'count',
@@ -52,51 +47,119 @@ export default {
         perPage: 'per_page',
         pageKey: 'page'
       },
-      inputs: [],
+      inputs: [
+        { type: 'hidden', name: 'category', value: null },
+        { type: 'hidden', name: 'category__parent', value: null },
+        { type: 'hidden', name: 'category__parent__parent', 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.firstname + ' ' + row.creator_info.lastname
-          },
+        columns: []
+      },
+      mounted: false,
+      contentCategory: new ContentCategory(),
+      breadcrumbs: {
+        visible: true,
+        loading: false,
+        path: [
           {
-            name: 'status',
-            required: true,
-            label: 'وضعیت',
-            align: 'left',
-            field: row => (new Ticket(row)).status_info.label
+            label: 'خانه',
+            to: { name: 'Public.Home' }
           },
           {
-            name: 'action',
-            required: true,
-            label: 'جزییات',
-            align: 'left',
-            field: row => ''
+            label: 'یادداشت ها و مقالات',
+            to: { name: 'Public.Post.List' }
           }
         ]
       },
-      mounted: false,
-      createRouteName: ''
+      defaultOptions: {
+        postShowRouteName: 'Public.Post.Show',
+        categoryId: null,
+        categoryCategoryId: null,
+        categoryCategoryCategoryId: null,
+        btnShowPostClassName: null,
+        categoryFromQueryParams: false,
+        categoryCategoryFromQueryParams: false,
+        categoryCategoryCategoryFromQueryParams: false
+      }
+    }
+  },
+  computed: {
+    contentCategoryId () {
+      if (this.localOptions.categoryFromQueryParams && this.$route.params?.category_id) {
+        return this.$route.params.category_id
+      }
+
+      if (this.localOptions.categoryId) {
+        return this.localOptions.categoryId
+      }
+
+      return null
     }
   },
-  mounted() {
+  mounted () {
     this.mounted = true
+    this.$store.commit('AppLayout/updateBreadcrumbs', this.breadcrumbs)
+
+    if (this.localOptions.categoryCategoryCategoryFromQueryParams) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'category__parent__parent', 'value', this.$route.params.category_parent_parent_id)
+    } else if (this.localOptions.categoryCategoryCategoryId) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'category__parent__parent', 'value', this.localOptions.categoryCategoryCategoryId)
+    }
+
+    if (this.localOptions.categoryCategoryFromQueryParams) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'category__parent', 'value', this.$route.params.category_parent_id)
+    } else if (this.localOptions.categoryCategoryId) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'category__parent', 'value', this.localOptions.categoryCategoryId)
+    }
+
+    if (this.localOptions.categoryFromQueryParams) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'category', 'value', this.$route.params.category_id)
+    } else if (this.localOptions.categoryId) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'category', 'value', this.localOptions.categoryId)
+    }
+
+    if (this.contentCategoryId) {
+      this.getContentCategoryInfo(this.contentCategoryId)
+    }
+  },
+  methods: {
+    getPost (data) {
+      return new Post(data)
+    },
+    getContentCategoryInfo (contentCategoryId) {
+      this.contentCategory.loading = true
+      APIGateway.contentCategory.get({ data: { id: contentCategoryId } })
+        .then((contentCategory) => {
+          this.contentCategory = new ContentCategory(contentCategory)
+          this.breadcrumbs.path.push({
+            label: this.contentCategory.title,
+            to: { name: 'Public.ContentCategory.Show', params: { category_id: contentCategoryId } }
+          })
+          this.$store.commit('AppLayout/updateBreadcrumbs', this.breadcrumbs)
+          this.contentCategory.loading = false
+        })
+        .catch(() => {
+          this.contentCategory.loading = false
+        })
+    }
   }
 }
 </script>
 
 <style scoped lang="scss">
-.ContentList {
+.PostList {
+  :deep(.entity-index) {
+    .quasar-crud-index-table {
+      .q-table__top {
+        display: none;
+      }
+      .q-table__bottom {
+        .q-table__control {
+          display: none;
+        }
+      }
+    }
+  }
   .more-action {
     display: flex;
     flex-flow: row;

+ 67 - 2
src/components/Widgets/Content/ContentList/OptionPanel.vue

@@ -1,7 +1,58 @@
 <template>
   <option-panel-tabs v-model:options="localOptions">
     <template #main-tab>
-      ...
+      <div class="q-mt-md">
+        کد دسته پدربزرگ
+      </div>
+      <div class="row">
+        <div class="col-md-4 col-12">
+          <q-checkbox v-model="localOptions.categoryCategoryCategoryFromQueryParams"
+                      label="استفاده از کوری پارام صفحه" />
+        </div>
+        <div v-if="!localOptions.categoryCategoryCategoryFromQueryParams"
+             class="col-md-8 col-12">
+          <q-input v-model="localOptions.categoryCategoryCategoryId" />
+        </div>
+      </div>
+      <div>
+        کد دسته پدر
+      </div>
+      <div class="row">
+        <div class="col-md-4 col-12">
+          <q-checkbox v-model="localOptions.categoryCategoryFromQueryParams"
+                      label="استفاده از کوری پارام صفحه" />
+        </div>
+        <div v-if="!localOptions.categoryCategoryFromQueryParams"
+             class="col-md-8 col-12">
+          <q-input v-model="localOptions.categoryCategoryId" />
+        </div>
+      </div>
+      <div>
+        کد دسته فرزند
+      </div>
+      <div class="row">
+        <div class="col-md-4 col-12">
+          <q-checkbox v-model="localOptions.categoryFromQueryParams"
+                      label="استفاده از کوری پارام صفحه" />
+        </div>
+        <div v-if="!localOptions.categoryFromQueryParams"
+             class="col-md-8 col-12">
+          <q-input v-model="localOptions.categoryId" />
+        </div>
+      </div>
+      <div>
+        نام کلاس دکمه های موجود در کامپوننت
+      </div>
+      <div class="row">
+        <div class="col-md-8 col-12">
+          <q-input v-model="localOptions.btnShowPostClassName" />
+        </div>
+      </div>
+      <div class="q-mt-md">
+        <q-input v-model="localOptions.postShowRouteName"
+                 label="post show Route name" />
+      </div>
+
     </template>
   </option-panel-tabs>
 </template>
@@ -16,6 +67,20 @@ export default defineComponent({
   components: {
     OptionPanelTabs
   },
-  mixins: [mixinOptionPanel]
+  mixins: [mixinOptionPanel],
+  data () {
+    return {
+      defaultOptions: {
+        postShowRouteName: 'Public.Post.Show',
+        btnShowPostClassName: null,
+        categoryId: null,
+        categoryCategoryId: null,
+        categoryCategoryCategoryId: null,
+        categoryFromQueryParams: false,
+        categoryCategoryFromQueryParams: false,
+        categoryCategoryCategoryFromQueryParams: false
+      }
+    }
+  }
 })
 </script>

+ 71 - 0
src/components/Widgets/Content/ContentMixin.js

@@ -0,0 +1,71 @@
+import { APIGateway } from 'src/api/APIGateway.js'
+
+const mixinAuth = {
+  methods: {
+    uploadAudio (files, updateUploadingProgressBar, onFileUpload, onError) {
+      const formData = new FormData()
+      formData.append('file', files[0])
+      formData.append('type', 'SOUND')
+      this.$axios.post(APIGateway.media.APIAdresses.base, formData, {
+        headers: {
+          'Content-Type': 'multipart/form-data'
+        },
+        onUploadProgress: progressEvent => {
+          const { loaded, total } = progressEvent
+          const percentage = Math.floor((loaded * 100) / total)
+          updateUploadingProgressBar(percentage + '%')
+        }
+      })
+        .then((response) => {
+          onFileUpload(response.data.file)
+        })
+        .catch(() => {
+          onError()
+        })
+    },
+    uploadPhoto (files, updateUploadingProgressBar, onFileUpload, onError) {
+      const formData = new FormData()
+      formData.append('file', files[0])
+      formData.append('type', 'IMAGE')
+      this.$axios.post(APIGateway.media.APIAdresses.base, formData, {
+        headers: {
+          'Content-Type': 'multipart/form-data'
+        },
+        onUploadProgress: progressEvent => {
+          const { loaded, total } = progressEvent
+          const percentage = Math.floor((loaded * 100) / total)
+          updateUploadingProgressBar(percentage + '%')
+        }
+      })
+        .then((response) => {
+          onFileUpload(response.data.file)
+        })
+        .catch(() => {
+          onError()
+        })
+    },
+    uploadVideo (files, updateUploadingProgressBar, onFileUpload, onError) {
+      const formData = new FormData()
+      formData.append('file', files[0])
+      formData.append('type', 'VIDEO')
+      this.$axios.post(APIGateway.media.APIAdresses.base, formData, {
+        headers: {
+          'Content-Type': 'multipart/form-data'
+        },
+        onUploadProgress: progressEvent => {
+          const { loaded, total } = progressEvent
+          const percentage = Math.floor((loaded * 100) / total)
+          updateUploadingProgressBar(percentage + '%')
+        }
+      })
+        .then((response) => {
+          onFileUpload(response.data.file)
+        })
+        .catch(() => {
+          onError()
+        })
+    }
+  }
+}
+
+export default mixinAuth

+ 397 - 234
src/components/Widgets/Content/ContentShow/ContentShow.vue

@@ -1,277 +1,440 @@
 <template>
-  <div class="ContentShow"
+  <div class="content-show"
        :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>
-          ({{ ticketTitle }})
-        </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/default-avatar.png'"
-                              :name="message.creator_info.firstname + ' ' + message.creator_info.lastname"
-                              :text="[message.body]"
-                              text-html
-                              :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" />
+    <breadcrumbs class="q-mb-md" />
+    <div class="content-show__summery">
+      <div class="content-show__summery-info">
+        <div class="content-show__thumbnail">
+          <q-img :src="content.thumbnail" />
+        </div>
+        <div class="content-show__meta">
+          <div class="content-show__title">
+            {{ content.title }}
           </div>
-          <div class="col-md-4 col-12">
-            <q-btn color="primary"
-                   class="full-width"
-                   :loading="entityLoading"
-                   @click="sendReply">
-              ارسال پاسخ
-            </q-btn>
+          <div class="content-show__attrs">
+            <div class="content-show__attrs-title">
+              ویژگی ها:
+            </div>
+            <div class="content-show__attrs-items">
+              <div v-if="content.is_physical"
+                   class="content-show__attrs-item">
+                <div class="content-show__attrs-item-label">
+                  نوع:
+                </div>
+                <div class="content-show__attrs-item-value">
+                  {{ content.is_physical_info.label }}
+                </div>
+              </div>
+              <div v-if="content.physical_type"
+                   class="content-show__attrs-item">
+                <div class="content-show__attrs-item-label">
+                  جنس:
+                </div>
+                <div class="content-show__attrs-item-value">
+                  {{ content.physical_type_info.label }}
+                </div>
+              </div>
+              <div v-if="content.weight"
+                   class="content-show__attrs-item">
+                <div class="content-show__attrs-item-label">
+                  وزن:
+                </div>
+                <div class="content-show__attrs-item-value">
+                  {{ content.weight }}
+                  گرم
+                </div>
+              </div>
+            </div>
           </div>
         </div>
       </div>
-    </q-card>
+    </div>
+    <div v-if="content.description"
+         class="content-show__descriptions">
+      <q-banner class="content-show__descriptions-title">
+        توضیحات:
+      </q-banner>
+      <div class="content-show__descriptions-data"
+           v-html="content.description" />
+    </div>
+    <div v-if="content.medias_info"
+         class="content-show__medias">
+      <medias-component :medias="content.medias_info" />
+    </div>
   </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 ShamsiDate from 'assets/ShamsiDate'
+import { Content } from 'src/models/Content.js'
 import { APIGateway } from 'src/api/APIGateway.js'
-import { TicketCategoryList } from 'src/models/TicketCategory.js'
+import MediasComponent from './components/Medias.vue'
+import { ContentCategory } from 'src/models/ContentCategory.js'
+import { mixinPrefetchServerData, mixinWidget } from 'src/mixin/Mixins.js'
+import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
+
+let html2pdf
 
 export default {
   name: 'ContentShow',
-  components: {
-    EntityShow
-  },
-  mixins: [mixinWidget],
-  data: () => {
+  components: { Breadcrumbs, MediasComponent },
+  mixins: [mixinPrefetchServerData, mixinWidget],
+  data () {
     return {
-      replyText: null,
-      mounted: false,
-      entityLoading: true,
-      ticketTitle: '',
-      authenticatedUser: new User(),
-      api: APIGateway.ticket.APIAdresses.base,
-      ticketCategoryList: new TicketCategoryList(),
-      entityIdKey: 'id',
-      entityParamKey: 'id',
-      showRouteName: 'Admin.Ticket.Show',
-      indexRouteName: 'Admin.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'
-        },
+      content: new Content(),
+      contentCategory: new ContentCategory(),
+      contentCategoryParent: new ContentCategory(),
+      contentCategoryParentParent: new ContentCategory(),
+      addToCartLoading: false
+    }
+  },
+  computed: {
+    contentId () {
+      return this.$route.params.id
+    },
+    breadcrumbsPath () {
+      const path = [
         {
-          type: 'input',
-          name: 'title',
-          responseKey: 'title',
-          label: 'عنوان',
-          placeholder: ' ',
-          col: 'col-md-12 col-12'
+          label: 'خانه',
+          to: { name: 'Public.Home' }
         },
         {
-          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' }
+          label: 'آرشیو محتوا',
+          to: { name: 'Public.ContentCategoryParentParent.List' }
+        }
       ]
-    }
-  },
-  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
-            }
-          }))
+
+      if (this.contentCategoryParentParent.id) {
+        path.push({
+          label: this.contentCategoryParentParent.title,
+          to: { name: 'Public.ContentCategoryParentParent.Show', params: { category_id: this.contentCategoryParentParent.id } }
         })
-      })
-      .catch(() => {
-        this.mounted = true
-      })
+      }
+      if (this.contentCategoryParent.id) {
+        path.push({
+          label: this.contentCategoryParent.title,
+          to: { name: 'Public.ContentCategoryParent.Show', params: { category_id: this.contentCategoryParent.id } }
+        })
+      }
+      if (this.contentCategory.id) {
+        path.push({
+          label: this.contentCategory.title,
+          to: { name: 'Public.ContentCategory.Show', params: { category_id: this.contentCategory.id } }
+        })
+      }
+      if (this.content.id) {
+        path.push({
+          label: this.content.title,
+          to: { name: 'Public.Content.Show', params: { id: this.content.id } }
+        })
+      }
+      return path
+    }
   },
   methods: {
-    loadAuthData () {
-      this.authenticatedUser = this.$store.getters['Auth/user']
+
+    prefetchServerDataPromise () {
+      return this.getContent()
     },
-    afterLoadInputData (response) {
-      this.ticketTitle = response.title
-      this.entityLoading = false
+    prefetchServerDataPromiseThen (content) {
+      this.content = new Content(content)
+      this.content.content = false
+      this.loadContentCategory()
     },
-    sendReply() {
-      this.entityLoading = true
-      const body = this.replyText.replace(/\n/g, '<br>\n')
-      APIGateway.ticket.reply({
-        id: this.$route.params.id,
-        body
-      })
-        .then(() => {
-          this.$refs.entityShow.editEntity()
-            .then(() => {
-              this.replyText = null
-              this.$refs.entityShow.getData()
-              this.entityLoading = false
-            })
-            .catch(() => {
-              this.$refs.entityShow.getData()
-              this.entityLoading = false
-            })
-        })
-        .catch(() => {
-          this.$refs.entityShow.getData()
-          this.entityLoading = false
+    prefetchServerDataPromiseCatch () {
+      this.content.content = false
+    },
+
+    getTitledDateTime (dateTime) {
+      return ShamsiDate.getTitledDateTime(dateTime)
+    },
+    getTime (time) {
+      return ShamsiDate.getDateTime(time)
+    },
+    getContent () {
+      this.content.content = true
+      return APIGateway.content.get(this.contentId)
+    },
+    print () {
+      this.convertPhotosToBase64()
+      const html2pdfConfig = {
+        margin: [0, 0, 0, 0],
+        image: {
+          type: 'jpeg',
+          quality: 0.6
+        },
+        filename: this.content.title,
+        html2canvas: {
+          dpi: 1,
+          scale: 2.5,
+          letterRendering: true,
+          useCORS: true
+        },
+        jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
+      }
+      html2pdf()
+        .set(html2pdfConfig)
+        .from(this.$refs.printArea)
+        .save()
+        .thenExternal(() => {
+          this.downloadLoading = false
         })
     },
-    loadOptions () {
-      return this.loadCategories()
+    toDataURL(src, callback, outputFormat) {
+      const img = new Image()
+      img.crossOrigin = 'Anonymous'
+      img.onload = function() {
+        const canvas = document.createElement('CANVAS')
+        const ctx = canvas.getContext('2d')
+        canvas.height = this.naturalHeight
+        canvas.width = this.naturalWidth
+        ctx.drawImage(this, 0, 0)
+        const dataURL = canvas.toDataURL(outputFormat)
+        callback(dataURL)
+      }
+      if (src.includes('?')) {
+        img.src = src
+      } else {
+        img.src = src + '?test=123'
+      }
+      if (img.complete || img.complete === undefined) {
+        img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
+        img.src = src
+      }
+    },
+    convertPhotosToBase64 () {
+      this.convertSvgToBase64()
+      this.convertImagesToBase64()
     },
-    loadCategories () {
-      return new Promise((resolve, reject) => {
-        APIGateway.ticketCategory.index()
-          .then(({ list }) => {
-            this.ticketCategoryList = list
-            resolve()
-          })
-          .catch(() => {
-            reject()
-          })
+    convertImagesToBase64 () {
+      if (!this.$refs.printArea) {
+        return
+      }
+      const images = this.$refs.printArea.getElementsByTagName('img')
+      images.forEach(image => {
+        this.toDataURL(image.src, function(dataUrl) {
+          image.src = dataUrl
+        })
+      })
+    },
+    convertSvgToBase64 () {
+      if (!this.$refs.printArea) {
+        return
+      }
+      const svgs = this.$refs.printArea.getElementsByTagName('svg')
+      svgs.forEach(svg => {
+        // Convert the SVG node to HTML.
+        const div = document.createElement('div')
+        div.appendChild(svg.cloneNode(true))
+
+        // Encode the SVG as base64
+        const b64 = 'data:image/svg+xml;base64,' + window.btoa(div.innerHTML)
+        const url = 'url("' + b64 + '")'
+        svg.style.backgroundImage = url
+        svg.style.backgroundSize = 'cover'
+        if (svg.parentElement.classList.contains('brace-left')) {
+          svg.style.backgroundPosition = 'left'
+        }
+        if (svg.parentElement.classList.contains('brace-center')) {
+          svg.style.backgroundPosition = 'bottom'
+        }
+        if (svg.parentElement.classList.contains('brace-right')) {
+          svg.style.backgroundPosition = 'right'
+        }
       })
     },
-    setInputOptions (name, options) {
-      this.$refs.entityCreate.setInputAttributeByName(name, 'options', options)
+    getContentCategoryInfo (contentCategoryId) {
+      return APIGateway.contentCategory.get({ data: { id: contentCategoryId } })
+    },
+    loadContentCategory () {
+      this.contentCategory.loading = true
+      this.getContentCategoryInfo(this.content.category)
+        .then((contentCategory) => {
+          this.contentCategory = new ContentCategory(contentCategory)
+          this.updateBreadcrumbs()
+          if (this.contentCategory.parent) {
+            this.getContentCategoryInfo(this.contentCategory.parent)
+              .then((parentContentCategory) => {
+                this.contentCategoryParent = new ContentCategory(parentContentCategory)
+                this.updateBreadcrumbs()
+                if (this.contentCategoryParent.parent) {
+                  this.getContentCategoryInfo(this.contentCategoryParent.parent)
+                    .then((parentParentContentCategory) => {
+                      this.contentCategoryParentParent = new ContentCategory(parentParentContentCategory)
+                      this.updateBreadcrumbs()
+                    })
+                    .finally(() => {
+                      this.contentCategoryParentParent.loading = false
+                    })
+                }
+              })
+              .finally(() => {
+                this.contentCategoryParent.loading = false
+              })
+          }
+        })
+        .finally(() => {
+          this.contentCategory.loading = false
+        })
+    },
+    updateBreadcrumbs () {
+      const breadcrumbs = {
+        visible: true,
+        loading: false,
+        path: this.breadcrumbsPath
+      }
+
+      this.$store.commit('AppLayout/updateBreadcrumbs', breadcrumbs)
     }
   }
 }
 </script>
 
 <style scoped lang="scss">
-.ContentShow {
-  .title {
-    font-style: normal;
-    font-weight: 700;
-    font-size: 24px;
-    line-height: 140%;
-    color: #424242;
-    margin-bottom: 27px;
+.content-show {
+  $price-section-with: 312px;
+  .content-show__summery {
+    $gap: 24px;
     display: flex;
-    flex-flow: row;
-    position: relative;
-    .static-title {
-
+    flex-wrap: wrap;
+    gap: $gap;
+    align-items: stretch;
+    justify-content: flex-start;
+    margin-bottom: 24px;
+    .content-show__summery-info {
+      border-radius: 8px;
+      background: #F6F6F6;
+      border: 1px solid #DFE1EC;
+      padding: 24px;
+      width: 100%;
+      display: flex;
+      flex-wrap: wrap;
+      gap: 74px;
+      align-items: flex-start;
+      justify-content: flex-start;
+      .content-show__thumbnail {
+        width: 239px;
+        @media screen and (max-width: 600px) {
+          & {
+            width: 100%;
+          }
+        }
+      }
+      .content-show__meta {
+        .content-show__title {
+          font-size: 22px;
+          margin-bottom: 35px;
+        }
+        .content-show__attrs {
+          .content-show__attrs-title {
+            color: #727272;
+            font-size: 16px;
+            margin-bottom: 16px;
+          }
+          .content-show__attrs-items {
+            .content-show__attrs-item {
+              font-size: 16px;
+              display: flex;
+              gap: 4px;
+              align-items: center;
+              margin-bottom: 20px;
+              justify-content: flex-start;
+              &:before {
+                content: ' ';
+                width: 8px;
+                height: 8px;
+                border-radius: 100%;
+                background: #475F4A;
+                margin-right: 6px;
+              }
+              .content-show__attrs-item-label {
+                color: #797979;
+              }
+              .content-show__attrs-item-value {
+                color: #212121;
+              }
+            }
+          }
+        }
+        @media screen and (max-width: 600px) {
+          & {
+            width: 100%;
+          }
+        }
+      }
+      @media screen and (max-width: 900px) {
+        & {
+          width: 100%;
+        }
+      }
+      @media screen and (max-width: 600px) {
+        & {
+          gap: 16px;
+          padding-bottom: 0;
+        }
+      }
     }
-    .dynamic-title {
-
+  }
+  .content-show__contents {
+    margin-bottom: 47px;
+    :deep(.q-expansion-item) {
+      .q-expansion-item__container {
+        .q-item {
+          background: #F6F6F6;
+          border: 1px solid #DFE1EC;
+          border-radius: 8px;
+          .q-item__section--avatar {
+            padding-right: 8px;
+            font-size: 36px;
+            min-width: 36px;
+            color: #AAA095;
+          }
+          .q-item__label {
+            font-size: 18px;
+            color: #212121;
+          }
+        }
+        .q-expansion-item__content {
+          border: 1px solid #DFE1EC;
+          border-top: none;
+        }
+      }
+      &.q-expansion-item--expanded {
+        .q-expansion-item__container {
+          .q-item {
+            border-radius: 8px 8px 0 0;
+          }
+        }
+      }
+    }
+    .content-item-component {
+      border-bottom: 1px solid #DFE1EC;
+      &:last-child {
+        border: none;
+      }
+    }
+  }
+  .content-show__descriptions {
+    padding: 32px 32px 70px 32px;
+    border: 1px solid #DFE1EC;
+    border-radius: 8px;
+    background: #F6F6F6;
+    margin-bottom: 20px;
+    .content-show__descriptions-title{
+      color: #727272;
+      font-size: 16px;
+      margin-bottom: 21px;
     }
-    .back-action {
-      position: absolute;
-      right: 0;
-      top: 0;
+    .content-show__descriptions-data {
+      color: #212121;
+      font-size: 16px;
     }
   }
-  :deep(.form) {
-    padding: 24px;
+  .content-show__medias {
+    background: #FFFFFF;
+    border: 1px solid #DFE1EC;
+    border-radius: 8px;
+    padding: 36px 46px;
   }
 }
 </style>

+ 10 - 2
src/components/Widgets/Content/ContentShow/OptionPanel.vue

@@ -1,7 +1,8 @@
 <template>
   <option-panel-tabs v-model:options="localOptions">
     <template #main-tab>
-      ...
+      <q-input v-model="localOptions.postShowRouteName"
+               label="post show Route name" />
     </template>
   </option-panel-tabs>
 </template>
@@ -16,6 +17,13 @@ export default defineComponent({
   components: {
     OptionPanelTabs
   },
-  mixins: [mixinOptionPanel]
+  mixins: [mixinOptionPanel],
+  data () {
+    return {
+      defaultOptions: {
+        postShowRouteName: 'Public.Post.Show'
+      }
+    }
+  }
 })
 </script>

+ 71 - 0
src/components/Widgets/Content/ContentShow/components/MediaCategory.vue

@@ -0,0 +1,71 @@
+<template>
+  <div class="MediaCategory">
+    <div class="title">
+      <q-icon :name="icon"
+              size="19px" />
+      {{ title }}
+    </div>
+    <div class="media-list row q-col-gutter-md">
+      <div v-for="media in medias.list"
+           :key="media.id"
+           class="media-item col-md-3 col-sm-6 col-12">
+        <media-item :media="media"
+                    :media-type="mediaType"
+                    @onView="onView(media)" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { MediaList } from 'src/models/Media.js'
+import MediaItem from './MediaItem.vue'
+
+export default {
+  name: 'MediaCategory',
+  components: { MediaItem },
+  props: {
+    medias: {
+      type: MediaList,
+      default: new MediaList()
+    },
+    title: {
+      type: String,
+      default: ''
+    },
+    icon: {
+      type: String,
+      default: ''
+    },
+    mediaType: {
+      type: String,
+      default: null
+    }
+  },
+  emits: ['update:value', 'onView', 'onDelete'],
+  data () {
+    return {
+      entityCreateLoading: false
+    }
+  },
+  methods: {
+    onView (media) {
+      this.$emit('onView', media)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.MediaCategory {
+  .title {
+    font-size: 16px;
+    letter-spacing: 0;
+    color: #212121;
+    margin-bottom: 26px;
+    .q-icon {
+      margin-left: 9px;
+    }
+  }
+}
+</style>

+ 77 - 0
src/components/Widgets/Content/ContentShow/components/MediaItem.vue

@@ -0,0 +1,77 @@
+<template>
+  <div class="MediaItem">
+    <div class="title ellipsis">
+      {{ media.title }}
+    </div>
+    <div class="actions">
+      <q-btn class="viewBtn"
+             color="primary"
+             @click="onView">
+        <template v-if="mediaType === 'video'">
+          مشاهده
+        </template>
+        <template v-else>
+          <q-icon name="file_download" />
+          دانلود
+        </template>
+      </q-btn>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Media } from 'src/models/Media.js'
+
+export default {
+  name: 'MediaItem',
+  props: {
+    media: {
+      type: Media,
+      default: new Media()
+    },
+    mediaType: {
+      type: String,
+      default: null
+    }
+  },
+  emits: ['update:value', 'onView', 'onDelete'],
+  data () {
+    return {
+      entityCreateLoading: false
+    }
+  },
+  methods: {
+    onView () {
+      this.$emit('onView')
+    },
+    onDelete () {
+      this.$emit('onDelete')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.MediaItem {
+  background: #F9F9F9;
+  border: 1px solid #DFE1EC;
+  border-radius: 10px;
+  padding: 10px 16px 10px 21px;
+  display: flex;
+  flex-flow: row;
+  justify-content: space-between;
+  align-items: center;
+  .title {
+    font-size: 15px;
+    letter-spacing: 0;
+    color: #212121;
+  }
+  .actions {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    .q-btn {
+    }
+  }
+}
+</style>

+ 91 - 0
src/components/Widgets/Content/ContentShow/components/Medias.vue

@@ -0,0 +1,91 @@
+<template>
+  <div class="Medias">
+    <media-category v-if="documentMedias.list.length > 0"
+                    :medias="documentMedias"
+                    icon="receipt_long"
+                    title="فایل"
+                    class="q-mt-lg"
+                    @onView="onView" />
+    <media-category v-if="soundMedias.list.length > 0"
+                    :medias="soundMedias"
+                    icon="mic_none"
+                    title="صوت"
+                    class="q-mt-lg"
+                    @onView="onView" />
+    <media-category v-if="videoMedias.list.length > 0"
+                    :medias="videoMedias"
+                    icon="play_circle_outline"
+                    title="ویدیو"
+                    class="q-mt-lg"
+                    media-type="video"
+                    @onView="onView" />
+    <media-category v-if="imageMedias.list.length > 0"
+                    :medias="imageMedias"
+                    icon="visibility"
+                    title="تصویر"
+                    class="q-mt-lg"
+                    @onView="onView" />
+  </div>
+</template>
+
+<script>
+import { MediaList } from 'src/models/Media.js'
+import MediaCategory from './MediaCategory.vue'
+
+export default {
+  name: 'Medias',
+  components: { MediaCategory },
+  props: {
+    medias: {
+      type: MediaList,
+      default: new MediaList()
+    },
+    sourceType: {
+      type: String,
+      default: 'CONTENT'
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      fetchLoading: false
+    }
+  },
+  computed: {
+    videoMedias () {
+      return this.filterMediasByType('VIDEO')
+    },
+    soundMedias () {
+      return this.filterMediasByType('SOUND')
+    },
+    imageMedias () {
+      return this.filterMediasByType('IMAGE')
+    },
+    documentMedias () {
+      return this.filterMediasByType('DOCUMENT')
+    }
+  },
+  methods: {
+    filterMediasByType (type) {
+      if (typeof this.medias.getType !== 'function') {
+        return new MediaList()
+      }
+      return new MediaList(this.medias.getType(type))
+    },
+    onView (media) {
+      if (media.url) {
+        window.open(media.url, '_blank')
+      }
+      if (media.file) {
+        window.open(media.file, '_blank')
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.Medias {
+  width: 100%;
+}
+</style>

+ 133 - 0
src/components/Widgets/Content/ContentShow/components/productItem.vue

@@ -0,0 +1,133 @@
+<template>
+  <div class="product-item">
+    <div class="product-item__meta">
+      <div class="product-item__meta-thumbnail">
+        <q-img :src="product.thumbnail" />
+      </div>
+      <div class="product-item__meta-info">
+        <div class="product-item__product-title">
+          {{ product.title }}
+        </div>
+        <div class="product-item__product-atrs">
+          <div class="product-item__product-atr">
+            <div class="product-item__product-atr-label">
+              {{ product.title }}
+            </div>
+            <div class="product-item__product-atr-value">
+              {{ product.title }}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="product-item__price">
+      <div class="product-item__price-info">
+        <div class="product-item__price-info-icon">
+          <q-icon name="shopping_cart" />
+        </div>
+        <div class="product-item__price-info-label">
+          قیمت محصول:
+        </div>
+        <div class="product-item__price-info-value">
+          {{ product.getFinalPrice() }}
+          تومان
+        </div>
+      </div>
+      <q-btn color="primary"
+             :to="{ name: 'Public.Product', params: { id: product.id }}"
+             class="product-item__price-action">
+        مشاهده
+      </q-btn>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Product } from 'src/models/Product.js'
+
+export default {
+  name: 'productItem',
+  props: {
+    product: {
+      type: Product,
+      default: new Product()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.product-item {
+  padding: 24px;
+  display: flex;
+  justify-content: space-between;
+  .product-item__meta {
+    display: flex;
+    gap: 32px;
+    justify-content: flex-start;
+    .product-item__meta-thumbnail {
+      width: 123px;
+    }
+    .product-item__meta-info {
+      .product-item__product-title {
+        color: #212121;
+        font-size: 18px;
+        margin-bottom: 30px;
+      }
+      .product-item__product-atrs {
+        .product-item__product-atr {
+          font-size: 16px;
+          display: flex;
+          gap: 4px;
+          align-items: center;
+          margin-bottom: 31px;
+          justify-content: flex-start;
+          &:before {
+            content: ' ';
+            width: 8px;
+            height: 8px;
+            border-radius: 100%;
+            background: #475F4A;
+            margin-right: 6px;
+          }
+          .product-item__product-atr-label {
+            color: #797979;
+          }
+          .product-item__product-atr-value {
+            color: #212121;
+          }
+        }
+      }
+    }
+  }
+  .product-item__price {
+    display: flex;
+    flex-direction: column;
+    gap: 29px;
+    justify-content: center;
+    align-items: center;
+    .product-item__price-info {
+      display: flex;
+      align-items: center;
+      justify-content: flex-start;
+      .product-item__price-info-icon {
+        font-size: 20px;
+        color: #AAA095;
+        margin-right: 10px;
+      }
+      .product-item__price-info-label {
+        font-size: 16px;
+        color: #616161;
+        margin-right: 25px;
+      }
+      .product-item__price-info-value {
+        font-size: 24px;
+        color: #475F4A;
+      }
+    }
+    .product-item__price-action {
+      width: 252px;
+    }
+  }
+}
+</style>

+ 206 - 0
src/components/Widgets/Content/ContentTopicList/ContentTopicList.vue

@@ -0,0 +1,206 @@
+<template>
+  <div class="ContentTopicList"
+       :style="localOptions.style">
+    <template v-if="mounted">
+      <breadcrumbs class="q-mb-lg" />
+      <div class="ContentTopicList__header">
+        <q-banner>
+          جستجوی پیشرفته محتوایی
+        </q-banner>
+        <!--        <q-btn color="primary"-->
+        <!--               @click="downloadExcel">-->
+        <!--          <q-icon name="file_download" />-->
+        <!--          دریافت-->
+        <!--        </q-btn>-->
+      </div>
+      <div class="ContentTopicList__top-filter">
+        <form-builder v-model:value="topFilterInputs"
+                      @update:value="onTopFilterChange" />
+      </div>
+      <div class="ContentTopicList__main">
+        <div class="ContentTopicList__sidebar">
+          <sidebar @onChangeFilters="onChangeSidebarFilters" />
+        </div>
+        <div class="ContentTopicList__result">
+          <entity-index v-if="mounted"
+                        ref="entityIndex"
+                        v-model:value="inputs"
+                        :api="api"
+                        :table="table"
+                        :table-keys="tableKeys"
+                        :table-grid-size="true"
+                        :default-layout="false"
+                        :create-route-name="createRouteName"
+                        :show-search-button="false"
+                        :show-expand-button="false"
+                        :show-reload-button="false">
+            <template #entity-index-table-item-cell="{inputData}">
+              <topic-item class="topic-item"
+                          :topic="inputData.props.row" />
+            </template>
+          </entity-index>
+        </div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityIndex } from 'quasar-crud'
+import { Ticket } from 'src/models/Ticket.js'
+import Sidebar from './components/sidebar.vue'
+import TopicItem from './components/TopicItem.vue'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilder, FormBuilderAssist } from 'quasar-form-builder'
+import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
+import HashtagSelector from 'src/components/FormBuilderCustumComponents/Hashtags/HashtagSelector.vue'
+
+const HashtagSelectorComp = shallowRef(HashtagSelector)
+
+export default {
+  name: 'ContentList',
+  components: { EntityIndex, Breadcrumbs, Sidebar, FormBuilder, TopicItem },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      breadcrumbs: {
+        visible: true,
+        loading: false,
+        path: [
+          {
+            label: 'خانه',
+            to: { name: 'Public.Home' }
+          },
+          {
+            label: 'جستجوی پیشرفته محتوایی',
+            to: { name: 'Public.PostTopic.List' }
+          }
+        ]
+      },
+      api: APIGateway.contentTopic.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: 'hidden', name: 'category__in', value: null },
+        { type: 'hidden', name: 'hashtags__id__in', value: null }
+      ],
+      topFilterInputs: [
+        { type: HashtagSelectorComp, name: 'hashtags', responseKey: 'hashtags_info', col: 'col-12' }
+      ],
+      topFilter: {
+        hashtags: 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.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,
+      sidebarFilters: { categories: [] },
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.$store.commit('AppLayout/updateBreadcrumbs', this.breadcrumbs)
+    this.mounted = true
+  },
+  methods: {
+    onChangeSidebarFilters (filters) {
+      this.sidebarFilters = Object.assign(filters, { categories: [] })
+      this.search()
+    },
+    onTopFilterChange () {
+      this.topFilter = FormBuilderAssist.getFormData(this.topFilterInputs)
+      this.search()
+    },
+    search () {
+      this.updateFilters()
+      if (!this.$refs.entityIndex) {
+        return
+      }
+
+      this.$refs.entityIndex.search()
+    },
+    updateFilters () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'category__in', 'value', this.sidebarFilters.category__in?.join())
+      FormBuilderAssist.setAttributeByName(this.inputs, 'hashtags__id__in', 'value', this.topFilter.hashtags?.join())
+    },
+    downloadExcel () {
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.ContentTopicList {
+  .ContentTopicList__header {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 24px;
+  }
+  .ContentTopicList__top-filter {
+    margin-bottom: 24px;
+  }
+  .ContentTopicList__main {
+    $gap: 24px;
+    display: flex;
+    gap: $gap;
+    flex-wrap: wrap;
+    justify-content: flex-start;
+    $sidebar-width: 312px;
+    .ContentTopicList__sidebar {
+      width: $sidebar-width;
+      @media screen and (max-width: 800px) {
+        & {
+          width: 100%;
+        }
+      }
+    }
+    .ContentTopicList__result {
+      width: calc( 100% - #{$sidebar-width} - #{$gap} );
+      .topic-item {
+        width: 100%;
+      }
+      @media screen and (max-width: 800px) {
+        & {
+          width: 100%;
+        }
+      }
+    }
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Content/ContentTopicList/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>

+ 156 - 0
src/components/Widgets/Content/ContentTopicList/components/TopicItem.vue

@@ -0,0 +1,156 @@
+<template>
+  <div class="topic-item-wrapper">
+    <div class="topic-item-side">
+      <div class="topic-item-side__line" />
+      <div class="topic-item-side__dot" />
+      <div class="topic-item-side__arrow" />
+    </div>
+    <div class="topic-item">
+      <div class="topic-item__title">
+        {{ topic.title }}
+      </div>
+      <div class="topic-item__text"
+           v-html="topic.text" />
+      <div class="topic-item__content">
+        <div class="topic-item__content-label">
+          <q-icon name="description"
+                  size="20px"
+                  color="primary" />
+          محتوا:
+        </div>
+        <div class="topic-item__content-title">
+          <q-btn flat
+                 color="primary"
+                 :to="{name: 'Public.Content.Show', params: { id: topic.content_info.id }}">
+            {{ topic.content_info.title }}
+          </q-btn>
+        </div>
+      </div>
+      <div class="topic-item__hashtags">
+        <div class="topic-item__hashtags-label">
+          <q-icon name="layers"
+                  size="20px"
+                  color="primary" />
+          برچسب ها:
+        </div>
+        <div class="topic-item__hashtags-items">
+          <div v-for="(hashtag, hashtagIndex) in topic.hashtags_info"
+               :key="hashtagIndex"
+               class="topic-item__hashtags-item">
+            {{ hashtag.title }}
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ContentTopic } from 'src/models/ContentTopic.js'
+
+export default {
+  name: 'TopicItem',
+  props: {
+    topic: {
+      type: Object,
+      default: new ContentTopic()
+    }
+  },
+  data () {
+    return {
+      addToCartLoading: false
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.topic-item-wrapper {
+  display: flex;
+  flex-flow: row;
+  justify-content: flex-start;
+  $side-width: 32px;
+  .topic-item-side {
+    width: $side-width;
+    position: relative;
+    .topic-item-side__line {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 4px;
+      height: 100%;
+      background: $primary;
+    }
+    .topic-item-side__dot {
+      width: 16px;
+      height: 16px;
+      background: white;
+      position: absolute;
+      top: 32px;
+      left: 0;
+      transform: translateX(calc( -50% + 2px ));
+      &:after {
+        content: ' ';
+        position: absolute;
+        left: 50%;
+        top: 50%;
+        width: 8px;
+        height: 8px;
+        border-radius: 100%;
+        background: $primary;
+        transform: translateX(-50%) translateY(-50%);
+      }
+    }
+    .topic-item-side__arrow {
+      width: 16px;
+      height: 16px;
+      background: white;
+      position: absolute;
+      top: 24px;
+      right: 0;
+      border-left: solid 1px $primary;
+      border-bottom: solid 1px $primary;
+      transform: rotate( 45deg ) translateX(12px);
+    }
+  }
+  .topic-item {
+    width: calc( 100% - #{$side-width} );
+    border: 1px solid $primary;
+    border-radius: 8px;
+    padding: 16px;
+    margin: 8px 0;
+    .topic-item__title {
+      color: $primary;
+      margin-bottom: 16px;
+    }
+    .topic-item__text {
+      margin-bottom: 32px;
+    }
+    .topic-item__content {
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+    }
+    .topic-item__hashtags {
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+      .topic-item__hashtags-label {
+        margin-right: 8px;
+      }
+      .topic-item__hashtags-items {
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: flex-start;
+        align-items: center;
+        gap: 8px;
+        .topic-item__hashtags-item {
+          padding: 6px 10px;
+          border: solid 1px #b9b9b9;
+          border-radius: 50px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 194 - 0
src/components/Widgets/Content/ContentTopicList/components/packageItem.vue

@@ -0,0 +1,194 @@
+<template>
+  <div class="package-item">
+    <div class="package-item__badges">
+      <div class="package-item__badge">
+        <div class="package-item__badge-title">
+          بسته
+        </div>
+        <div class="package-item__badge-icon">
+          <q-icon name="inventory_2" />
+        </div>
+      </div>
+    </div>
+    <div class="package-item__thumbnail">
+      <q-img :src="packageItem.thumbnail" />
+    </div>
+    <div class="package-item__title">
+      {{ packageItem.title }}
+    </div>
+    <div class="package-item__price">
+      <div class="package-item__price-base">
+        {{ parseInt(packageItem.unit_price).toLocaleString('fa') }}
+        تومان
+      </div>
+      <div class="package-item__price-final">
+        {{ packageItem.getFinalPrice() }}
+        تومان
+      </div>
+    </div>
+    <div class="package-item__actions">
+      <q-btn class="package-item__actions-view"
+             :to="{ name: 'Public.Package', params: { id: packageItem.id } }"
+             color="primary"
+             outline>
+        مشاهده جزییات
+      </q-btn>
+      <q-btn class="package-item__actions-add-to-cart"
+             :loading="addToCartLoading"
+             color="primary"
+             @click="addToCart">
+        افزودن به سبد
+      </q-btn>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Basket } from 'src/models/Basket.js'
+import { Package } from 'src/models/Package.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'PackageItem',
+  props: {
+    packageItem: {
+      type: Package,
+      default: new Package()
+    }
+  },
+  data () {
+    return {
+      addToCartLoading: false
+    }
+  },
+  methods: {
+    addToCart () {
+      this.addToCartLoading = true
+      APIGateway.basketItem.addPackage(this.packageItem.id, 1)
+        .finally(() => {
+          this.addToCartLoading = false
+          this.checkoutReview()
+        })
+    },
+    checkoutReview () {
+      this.addToCartLoading = true
+      APIGateway.basket.checkoutReview()
+        .then((basket) => {
+          this.$store.commit('Shop/updateBasket', new Basket(basket))
+        })
+        .finally(() => {
+          this.addToCartLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.package-item {
+  $transition-time: 0.2s;
+  background: #F6F6F6;
+  border: 1px solid #DFE1EC;
+  border-radius: 8px;
+  position: relative;
+  .package-item__badges {
+    position: absolute;
+    top: 12px;
+    right: 12px;
+    display: flex;
+    flex-flow: column;
+    align-items: flex-end;
+    gap: 10px;
+    .package-item__badge {
+      transition: all $transition-time;
+      width: max-content;
+      background: #FFFFFF;
+      border: 1px solid #DFE1EC;
+      border-radius: 20px;
+      height: 30px;
+      padding: 4px 8px;
+      text-align: center;
+      display: flex;
+      justify-content: space-between;
+      gap: 4px;
+      align-items: center;
+      .package-item__badge-title {
+        display: none;
+        font-size: 13px;
+        color: #212121;
+      }
+      .package-item__badge-icon {
+        color: $primary;
+      }
+    }
+  }
+  :deep(.package-item__thumbnail) {
+    transition: all $transition-time;
+    margin: 15px 58px 12px 58px;
+    .q-img {
+      width: 100%;
+    }
+  }
+  .package-item__title {
+    color: #212121;
+    width: 100%;
+    padding: 0 4px;
+    text-align: center;
+    font-size: 18px;
+    margin-bottom: 52px;
+  }
+  .package-item__price {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 20px;
+    margin-bottom: 43px;
+    .package-item__price-final {
+      font-size: 20px;
+      color: #475F4A;
+    }
+    .package-item__price-base {
+      font-size: 16px;
+      color: #FF3D3D;
+      text-decoration: line-through;
+    }
+  }
+  .package-item__actions {
+    display: none;
+    margin: 0 25px 25px 25px;
+    .package-item__actions-view {
+      background: white;
+      margin-bottom: 20px;
+      width: 100%;
+    }
+    .package-item__actions-add-to-cart {
+      width: 100%;
+    }
+  }
+  &:hover {
+    .package-item__badges {
+      .package-item__badge {
+        .package-item__badge-title {
+          display: block;
+        }
+      }
+    }
+    :deep(.package-item__thumbnail) {
+      margin: 10px 105px 4px 105px;
+      .q-img {
+        width: 100%;
+      }
+    }
+    .package-item__title {
+      margin-bottom: 44px;
+    }
+    .package-item__price {
+      margin-bottom: 27px;
+    }
+    .package-item__actions {
+      display: flex;
+      flex-flow: column;
+    }
+  }
+}
+</style>

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