Sfoglia il codice sorgente

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

hgh 1 anno fa
parent
commit
c01f3ea0ae

+ 2 - 1
quasar.config.js

@@ -70,7 +70,8 @@ module.exports = configure(function (ctx) {
       // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
 
       // 'roboto-font', // optional, you are not bound to it
-      'material-icons' // optional, you are not bound to it
+      'material-icons', // optional, you are not bound to it
+      'material-icons-outlined' // optional, you are not bound to it
     ],
 
     // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build

+ 7 - 3
src/assets/Enums/invoiceStatus.js

@@ -1,8 +1,12 @@
 const invoiceStatus = [
+  { label: 'در حال تأیید', value: 'VERIFYING' },
   { label: 'لغو شده', value: 'CANCELED' },
-  { label: 'در حال پرداخت', value: 'IN_PROGRESS' },
-  { label: 'پرداخت نقدی', value: 'PAID' },
-  { label: 'پرداخت قسطی', value: 'PAY_IN_INSTALMENT' }
+  { label: 'تأیید نشده', value: 'NOT_VERIFIED' },
+  { label: 'در حال پرداخت', value: 'PAYING' },
+  { label: 'درحال تایید پرداخت', value: 'TRANSACTION_VERIFYING' },
+  { label: 'پرداخت شده', value: 'PAID_FULL' },
+  { label: 'درحال پرداخت اقساط', value: 'PAYING_IN_INSTALMENT' },
+  { label: 'اقساط پرداخت شده', value: 'PAID_IN_INSTALMENT' }
 ]
 
 export default invoiceStatus

+ 1 - 1
src/components/EntityIndexGridItem.vue

@@ -40,7 +40,7 @@ export default {
 <style scoped lang="scss">
 .EntityIndexGridItem {
   width: 100%;
-  margin-bottom: 8px;
+
   .table-column {
     margin-bottom: 16px;
     .label {

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

@@ -14,20 +14,10 @@
                  class="lt-md"
                  @click="updateLayoutLeftDrawerVisible(true)" />
           <q-btn flat
+                 class="items-center"
                  :to="{name: 'Public.Home'}">
-            <svg xmlns="http://www.w3.org/2000/svg"
-                 width="20.5"
-                 height="21.505"
-                 viewBox="0 0 20.5 21.505">
-              <g id="Home"
-                 transform="translate(0 0)">
-                <path id="Fill_1"
-                      data-name="Fill 1"
-                      d="M16.277,21.506H14.353A1.973,1.973,0,0,1,12.4,19.536V16.46a.677.677,0,0,0-.684-.669H8.831a.675.675,0,0,0-.674.669v3.066a.742.742,0,0,1-.031.215,1.983,1.983,0,0,1-1.979,1.764H4.223A4.211,4.211,0,0,1,0,17.317V8.6A3.158,3.158,0,0,1,1.259,6.1L7.794.855a3.913,3.913,0,0,1,4.88,0L19.256,6.1A3.154,3.154,0,0,1,20.5,8.582v8.734A4.211,4.211,0,0,1,16.277,21.506ZM8.826,14.291h2.891A2.179,2.179,0,0,1,13.9,16.46v3.076a.472.472,0,0,0,.47.469h1.906A2.709,2.709,0,0,0,19,17.317V8.593a1.672,1.672,0,0,0-.667-1.309L11.74,2.026a2.416,2.416,0,0,0-3.01,0L2.181,7.282A1.684,1.684,0,0,0,1.5,8.61v8.707a2.709,2.709,0,0,0,2.723,2.688H6.147a.486.486,0,0,0,.491-.479.766.766,0,0,1,.019-.171v-2.9A2.182,2.182,0,0,1,8.826,14.291Z"
-                      transform="translate(0 0)"
-                      fill="#aaa095" />
-              </g>
-            </svg>
+            <q-icon name="home"
+                    class="material-icons-outlined" />
             صفحه اصلی
           </q-btn>
         </div>
@@ -42,27 +32,8 @@
             </q-badge>
           </q-btn>
           <q-btn flat>
-            <svg id="_000000ff"
-                 data-name="#000000ff"
-                 xmlns="http://www.w3.org/2000/svg"
-                 width="30"
-                 height="30"
-                 viewBox="0 0 30 30">
-              <path id="Path_29101"
-                    data-name="Path 29101"
-                    d="M8.807,0H21.19a9.015,9.015,0,0,1,8.266,5.926A9.2,9.2,0,0,1,30,8.811V21.135a9.019,9.019,0,0,1-6.97,8.631A9.442,9.442,0,0,1,21.19,30H8.811A9.013,9.013,0,0,1,0,21.133V8.81A9.021,9.021,0,0,1,8.807,0M8.291,2.034A7.012,7.012,0,0,0,2,9.025q0,6.006,0,12.011A7.016,7.016,0,0,0,9.026,28q6,0,12.007,0A7.014,7.014,0,0,0,28,21.035Q28,15,28,8.961a7.012,7.012,0,0,0-5.962-6.885A12.06,12.06,0,0,0,20.154,2H9.434A10.63,10.63,0,0,0,8.291,2.034Z"
-                    fill="#aaa095" />
-              <path id="Path_29102"
-                    data-name="Path 29102"
-                    d="M159.237,85.32A6,6,0,1,1,155.26,87.2a6.006,6.006,0,0,1,3.977-1.876m-.343,2.06a4,4,0,1,0,2.664.429A4.011,4.011,0,0,0,158.895,87.38Z"
-                    transform="translate(-144.628 -80.308)"
-                    fill="#aaa095" />
-              <path id="Path_29103"
-                    data-name="Path 29103"
-                    d="M124.8,324.32c1.223-.023,2.446-.006,3.669-.01a8.967,8.967,0,0,1,6.065,2.409,1,1,0,0,1,.218,1.1.976.976,0,0,1-1.124.578,1.694,1.694,0,0,1-.749-.488,6.993,6.993,0,0,0-4.412-1.622q-1.64,0-3.281,0a6.992,6.992,0,0,0-4.654,1.845,1.041,1.041,0,0,1-.792.32,1,1,0,0,1-.685-1.682A8.994,8.994,0,0,1,124.8,324.32Z"
-                    transform="translate(-111.831 -305.305)"
-                    fill="#aaa095" />
-            </svg>
+            <q-icon name="person"
+                    class="material-icons-outlined" />
             {{ user.fullname() }}
             <q-menu>
               <q-list style="min-width: 100px">
@@ -216,6 +187,7 @@ export default {
       justify-content: space-between;
       .q-btn {
         height: 40px;
+        padding: 4px 4px;
         svg {
           margin-right: 10px;
         }
@@ -314,6 +286,10 @@ export default {
   }
 }
 
+.q-icon {
+  padding: 2px;
+}
+
 .left-side {
   .q-btn {
     &.toolbar-button {
@@ -355,7 +331,7 @@ export default {
   }
   @media screen and (max-width: 599px) {
     & {
-      margin-left: 0;
+      margin-left: 16px;
     }
   }
 
@@ -384,8 +360,8 @@ export default {
   }
   @media screen and (max-width: 599px) {
     & {
-      right: 16px;
-      margin-bottom: 10px;
+      margin-right: 16px;
+      align-items: center;
     }
   }
 }

+ 95 - 91
src/components/Widgets/Classroom/ClassroomList/ClassroomList.vue

@@ -1,85 +1,75 @@
-<template>
-  <breadcrumbs style="margin-top: 29px; margin-bottom: 19px;" />
-  <template v-if="mounted">
-    <q-card class="barcode-section">
-      <q-card-section>
-        شناسه ورود به جلسه
-      </q-card-section>
-      <q-separator />
-      <form-builder-bar-code lable=""
-                             :value="this.national_code" />
-    </q-card>
-  </template>
-  <entity-index v-if="mounted"
-                ref="entityIndex"
-                v-model:value="inputs"
-                :title="listTitle"
-                :api="api"
-                :table="table"
-                :table-keys="tableKeys"
-                :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-if="inputData.col.name === 'status'">
-        <div :class="this.getRegistrationStatusTitleClass(inputData.props.row)">
-          {{ this.getRegistrationStatusTitle(inputData.props.row) }}
-        </div>
+<template class="my-container">
+  <div class="my-container">
+    <breadcrumbs style="margin-top: 29px; margin-bottom: 19px;" />
+    <template v-if="mounted">
+      <q-card class="barcode-section">
+        <q-card-section>
+          شناسه ورود به جلسه
+        </q-card-section>
+        <q-separator />
+        <form-builder-bar-code lable=""
+                               :value="this.national_code" />
+      </q-card>
+    </template>
+    <entity-index v-if="mounted"
+                  ref="entityIndex"
+                  v-model:value="inputs"
+                  :title="listTitle"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :table-grid-size="$q.screen.lt.sm"
+                  :show-expand-button="false"
+                  :show-reload-button="false"
+                  :show-search-button="false">
+      <template #before-form-builder>
+        <filters :unitCategories="unitCategories"
+                 :units="units"
+                 @onUnitCategoriesFilterChanged="onUnitCategoriesFilterChanged"
+                 @onUnitsFilterChanged="onUnitsFilterChanged" />
       </template>
-      <template v-else-if="inputData.col.name === 'action'">
-        <q-btn size="md"
-               color="primary"
-               label="جزییات"
-               :to="{name: showPageRouteName, params: {id: inputData.props.row.classroom_info.id}}" />
+      <template #entity-index-table-cell="{inputData}">
+        <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"
+                 label="جزییات"
+                 :to="{name: showPageRouteName, params: {id: inputData.props.row.classroom_info.id}}" />
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
       </template>
-      <template v-else>
-        {{ inputData.col.value }}
+      <template #entity-index-table-item-cell="{inputData}">
+        <q-card class="user-classroom-card-list">
+          <user-classroom-card :registration="inputData.props.row" />
+        </q-card>
       </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-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"
-                   label="جزییات"
-                   :to="{name: showPageRouteName, params: {id: row.classroom_info.id}}" />
-          </template>
-        </template>
-      </entity-index-grid-item>
-    </template>
-  </entity-index>
+    </entity-index>
+  </div>
 </template>
 
 <script>
-import { shallowRef } from 'vue'
 import { EntityIndex } from 'quasar-crud'
 import Enums from 'src/assets/Enums/Enums.js'
 import { Invoice } from 'src/models/Invoice.js'
 import ShamsiDate from 'src/assets/ShamsiDate.js'
 import { APIGateway } from 'src/api/APIGateway.js'
 import { FormBuilderAssist } from 'quasar-form-builder'
-import BtnControl from 'src/components/Control/btn.vue'
 import { Registration } from 'src/models/Registration.js'
 import { mixinAuth, mixinWidget } from 'src/mixin/Mixins.js'
-import EntityIndexGridItem from 'src/components/EntityIndexGridItem.vue'
+import UserClassroomCard from 'src/components/Widgets/Classroom/UserClassroomCard/UserClassroomCard.vue'
 import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
 import { TranscriptSheet } from 'src/models/TranscriptSheet'
 import FormBuilderBarCode from 'src/components/FormBuilderCustumComponents/FormBuilderBarCode.vue'
-
-const BtnControlComp = shallowRef(BtnControl)
+import Filters from './filters.vue'
 
 export default {
   name: 'UserPanel.Profile.AllClassrooms',
@@ -87,36 +77,21 @@ export default {
     FormBuilderBarCode,
     EntityIndex,
     Breadcrumbs,
-    EntityIndexGridItem
+    UserClassroomCard,
+    Filters
   },
   mixins: [mixinAuth, mixinWidget],
   data () {
     return {
       mounted: false,
+      unitCategories: [],
+      units: [],
       national_code: null,
       inputs: [
-        {
-          type: 'select',
-          name: 'category',
-          options: [],
-          value: null,
-          label: 'گروه آموزشی',
-          placeholder: ' ',
-          col: 'col-md-2 col-12'
-        },
-        {
-          type: 'select',
-          name: 'unit',
-          responseKey: 'unit',
-          options: [],
-          value: null,
-          label: 'درس',
-          placeholder: ' ',
-          col: 'col-md-2 col-12'
-        },
+        { type: 'hidden', name: 'classroom__unit__category', value: null },
+        { type: 'hidden', name: 'classroom__unit', value: null },
         { type: 'hidden', name: 'classroom__unit__category__type', value: 'TRAINING' },
-        { type: 'hidden', name: 'owner', value: null },
-        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'جستجو', placeholder: ' ', atClick: () => {}, col: 'col-md-2 col-12' }
+        { type: 'hidden', name: 'owner', value: null }
       ],
       api: APIGateway.registration.APIAdresses.base,
       table: {
@@ -219,12 +194,13 @@ export default {
       return 'UserPanel.Profile.ClassroomInfo'
     },
     selectedCategoryId () {
-      return this.getInputValue('category')
+      // return this.getInputValue('category')
+      return 1 // TODO:
     }
   },
   watch: {
     selectedCategoryId () {
-      this.setInputValue('unit', null)
+      // this.setInputValue('unit', null)
       this.getUnits(this.selectedCategoryId)
     }
   },
@@ -236,6 +212,20 @@ export default {
     this.mounted = true
   },
   methods: {
+    onUnitCategoriesFilterChanged (selectedId) {
+      if (selectedId) {
+        this.getUnits(selectedId)
+      } else {
+        this.units = []
+      }
+      FormBuilderAssist.setAttributeByName(this.inputs, 'classroom__unit__category', 'value', selectedId)
+      FormBuilderAssist.setAttributeByName(this.inputs, 'classroom__unit', 'value', null)
+      this.search()
+    },
+    onUnitsFilterChanged (selectedId) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'classroom__unit', 'value', selectedId)
+      this.search()
+    },
     setClassroomTypeOfInputs () {
       FormBuilderAssist.setAttributeByName(this.inputs, 'classroom__unit__category__type', 'value', this.localOptions.classroomType)
 
@@ -307,7 +297,7 @@ export default {
     },
     loadInputDataOptions () {
       this.getCategories()
-      this.getUnits()
+      // this.getUnits()
     },
     getCategories () {
       APIGateway.unitCategory.index({ per_page: 9999, type: this.localOptions.classroomType })
@@ -318,6 +308,7 @@ export default {
               label: item.title
             }
           }))
+          this.unitCategories = categories.list
         })
         .catch(() => {})
     },
@@ -327,7 +318,8 @@ export default {
         category__type: this.localOptions.classroomType
       })
         .then(unitList => {
-          this.loadSelectOptions('unit', this.getSelectOptions(unitList.list.list, 'id', 'title'))
+          // this.loadSelectOptions('unit', this.getSelectOptions(unitList.list.list, 'id', 'title'))
+          this.units = unitList.list
         })
         .catch(() => {
         })
@@ -361,6 +353,15 @@ export default {
   margin-bottom: 8px;
   padding-bottom: 8px;
 }
+
+.my-container {
+  :deep(.entity-index) {
+    .entity-crud-formBuilder {
+      padding: 8px 0 0 8px;
+    }
+  }
+}
+
 .CLEAN_PASSED {
   //font-size: 1.1em;
   color: green;
@@ -374,4 +375,7 @@ export default {
 .FAILED_DUE_TO_PRE_CONDITIONAL{
   color: red;
 }
+.user-classroom-card-list {
+  margin: 8px 0;
+}
 </style>

+ 200 - 0
src/components/Widgets/Classroom/ClassroomList/filters.vue

@@ -0,0 +1,200 @@
+<template>
+  <div class="filters">
+    <div class="main-filters">
+      <div class="unit-category-chips">
+        <q-btn v-if="unitCategories?.list?.length > 0"
+               flat
+               class="filter-btn unitCategory "
+               :class="{ 'filter-btn__selected': selectedUnitCategoryId===null}"
+               @click="onUnitCategoriesFilterChanged(null)">
+          <div class="filter-btn-title">
+            همه
+          </div>
+        </q-btn>
+        <q-btn v-for="(unitCategory, unitCategoryIndex) in unitCategories.list"
+               :key="unitCategoryIndex"
+               flat
+               class="filter-btn unitCategory"
+               :class="{ 'filter-btn__selected': selectedUnitCategoryId===unitCategory.id}"
+               @click="onUnitCategoriesFilterChanged(unitCategory.id)">
+          <div class="filter-btn-title">
+            {{ unitCategory.title }}
+          </div>
+        </q-btn>
+      </div>
+      <div v-if="units?.list?.length > 0"
+           class="unit-chips">
+        <q-btn flat
+               class="filter-btn unitCategory"
+               :class="{ 'filter-btn__selected': selectedUnitId===null}"
+               @click="onUnitsFilterChanged(null)">
+          <div class="filter-btn-title">
+            همه
+          </div>
+        </q-btn>
+        <q-btn v-for="(unit, unitIndex) in units.list"
+               :key="unitIndex"
+               flat
+               class="filter-btn unitCategory"
+               :class="{ 'filter-btn__selected': selectedUnitId===unit.id}"
+               @click="onUnitsFilterChanged(unit.id)">
+          <div class="filter-btn-title">
+            {{ unit.title }}
+          </div>
+        </q-btn>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Media } from 'src/models/Media.js'
+import { UnitCategory, UnitCategoryList } from 'src/models/UnitCategory'
+import { UnitList } from 'src/models/Unit'
+
+export default {
+  name: 'UnitCategoryItem',
+  props: {
+    unitCategories: {
+      type: UnitCategoryList,
+      default: new UnitCategoryList()
+    },
+    units: {
+      type: UnitList,
+      default: new UnitList()
+    },
+    unitCategory: {
+      type: UnitCategory,
+      default: new UnitCategory()
+    },
+    showCategoryRouteName: {
+      type: String,
+      default: 'Public.UnitCategory.Show'
+    },
+    listType: {
+      type: String,
+      default: 'category_parent_parent'
+    }
+  },
+  emits: ['onUnitCategoriesFilterChanged', 'onUnitsFilterChanged'],
+  data () {
+    return {
+      media: new Media(),
+      searchText: null,
+      selectedUnitCategoryId: null,
+      selectedUnitId: null,
+      subFilterExpanded: false
+    }
+  },
+  computed: {
+    mediaTypes () {
+      return [{
+        label: 'همه',
+        value: null
+      }].concat(this.media.typeEnums)
+    },
+    screenGtMd () {
+      if (typeof window === 'undefined') {
+        return true
+      }
+      return this.$q.screen.gt.md
+    }
+  },
+  watch: {
+    searchText () {
+      this.emitFilters()
+    },
+    screenGtMd: {
+      handler (newValue) {
+        if (!newValue) {
+          this.$nextTick(() => {
+            this.subFilterExpanded = true
+          })
+        }
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    filterMediaType (mediaType) {
+      this.selectedMediaType = mediaType
+      this.emitFilters()
+    },
+    onUnitsFilterChanged (selectedId) {
+      this.selectedUnitId = selectedId
+      this.$emit('onUnitsFilterChanged', selectedId)
+    },
+    onUnitCategoriesFilterChanged (selectedId) {
+      this.selectedUnitCategoryId = selectedId
+      this.$emit('onUnitCategoriesFilterChanged', selectedId)
+    },
+    emitFilters () {
+      this.$emit('filter', this.getFilterObject())
+    },
+    getFilterObject () {
+      return {
+        medias__type: this.selectedMediaType,
+        search: this.searchText
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.filters {
+  .main-filters {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    margin-bottom: 12px;
+
+    .unit-category-chips {
+      display: flex;
+      gap: 8px;
+      flex-wrap: nowrap;
+      max-width: 100%;
+      overflow-x: auto;
+      justify-content: flex-start;
+      scrollbar-width: none; // فایرفاکس
+      -ms-overflow-style: none; // IE و Edge قدیمی
+    }
+    .unit-chips {
+      display: flex;
+      gap: 8px;
+      flex-wrap: nowrap;
+      max-width: 100%;
+      overflow-x: auto;
+      justify-content: flex-start;
+      scrollbar-width: none; // فایرفاکس
+      -ms-overflow-style: none; // IE و Edge قدیمی
+    }
+  }
+  .filter-btn {
+    border: 1px solid #000000;
+    border-radius: 20px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background: #FFFFFF;
+    height: 40px;
+    padding: 0 8px;
+    white-space: nowrap;
+    .filter-btn-title {
+      font-size: 14px;
+      color: #212121;
+    }
+    &.filter-btn__selected {
+      background: #444444;
+      .filter-btn-title {
+        color: white;
+      }
+    }
+  }
+}
+.unitCategories::-webkit-scrollbar {
+  display: none;               // کروم و سافاری
+}
+</style>

+ 28 - 0
src/components/Widgets/Classroom/UserClassroomCard/OptionPanel.vue

@@ -0,0 +1,28 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      <q-option-group v-model="localOptions.classroomType"
+                      :options="classroomTypeOptions" />
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { UnitCategory } from 'src/models/UnitCategory.js'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel],
+  data () {
+    return {
+      classroomTypeOptions: (new UnitCategory()).typeEnums
+    }
+  }
+})
+</script>

+ 349 - 0
src/components/Widgets/Classroom/UserClassroomCard/UserClassroomCard.vue

@@ -0,0 +1,349 @@
+<template>
+  <q-card-section class="classroom-card row">
+    <div class="right-side col-6">
+      <img class="thumbnail"
+           :src="registration.classroom_info.thumbnail ? registration.classroom_info.thumbnail: '/img/sample-post-thumbnail.jpg'">
+      <q-btn color="primary"
+             :to="{name: showPageRouteName, params: {id: registration.classroom_info.id}}">
+        مشاهده
+      </q-btn>
+    </div>
+    <div class="left-side col-6">
+      <div class="title-box">
+        <q-banner class="banner">{{ convertClassroomTitle(registration.classroom_info.title) }}</q-banner>
+        <div>
+          {{ getClassroomTitleCaption(registration.classroom_info.title) }}
+        </div>
+        <div>
+          {{ getHoldingType(registration.classroom_info.holding_type) }}
+          |
+          {{ registration.classroom_info.sessions_count }} جلسه
+        </div>
+      </div>
+      <div class="status-box">
+        <!--        <div :class="this.getRegistrationStatusTitleClass(row)" class="registration-status-box">-->
+        <div :class="['registration-status-box', getRegistrationStatusTitleClass(registration)]">
+          <q-icon name="event_note"
+                  class="material-icons-outlined" />
+          {{ this.getRegistrationStatusTitle(registration) }}
+        </div>
+        <div v-if="registration.invoice_info"
+             class="financial-status-box">
+          <q-icon name="credit_card"
+                  class="material-icons-outlined" />
+          {{ this.getInvoiceStatusTitle(registration.invoice_info.status) }}
+        </div>
+      </div>
+    </div>
+  </q-card-section>
+</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 { Registration } from 'src/models/Registration.js'
+import { UnitCategory } from 'src/models/UnitCategory'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { TranscriptSheet } from 'src/models/TranscriptSheet'
+
+export default {
+  name: 'UserClassroomCard',
+  mixins: [mixinWidget],
+  props: {
+    registration: {
+      type: Registration,
+      default: new Registration()
+    },
+    classroom: {
+      type: Classroom,
+      default: new Classroom()
+    },
+    classroomId: {
+      type: Number,
+      default: null
+    },
+    ownerId: {
+      type: Number,
+      default: null
+    },
+    classroomType: {
+      type: String,
+      default: 'TRAINING'
+    }
+  },
+  emits: [],
+  data: () => ({
+    defaultOptions: {
+      profileMode: false,
+      classroomType: 'TRAINING'
+    },
+    acceptClassType: 'register', // accept - enrollment - rules
+    fetchCodesLoading: false,
+    dropLoading: false,
+    rulesAccept: false,
+    rulesDialog: false,
+    loading: true
+  }),
+  computed: {
+    listTitle () {
+      if (this.localOptions.classroomType === 'TRAINING') {
+        return 'لیست دوره های آموزشی'
+      }
+      if (this.localOptions.classroomType === 'EVENT') {
+        return 'لیست رویدادها'
+      }
+      return 'لیست دوره های آموزشی'
+    },
+    showPageRouteName () {
+      if (this.localOptions.classroomType === 'TRAINING') {
+        return 'UserPanel.Profile.ClassroomInfo'
+      }
+      if (this.localOptions.classroomType === 'EVENT') {
+        return 'UserPanel.Profile.EventInfo'
+      }
+      return 'UserPanel.Profile.ClassroomInfo'
+    },
+    classroomTypeTitle () {
+      const unitCategory = new UnitCategory({ type: this.localOptions.classroomType })
+      return unitCategory.type_info.label
+    }
+  },
+  methods: {
+    getClassroomTitleCaption(title) {
+      const match = title.match(/^(.*?)\s*\((.*?)\)$/)
+      if (match) {
+        const insideParenthesis = match[2].split('-')[0].trim()
+        return `${insideParenthesis}`
+      } else {
+        return ''
+      }
+    },
+    convertClassroomTitle(title) {
+      const match = title.match(/^(.*?)\s*\((.*?)\)$/)
+      if (match) {
+        const beforeParenthesis = match[1]
+        return `${beforeParenthesis}`
+      } else {
+        return title
+      }
+    },
+    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 '-'
+    },
+    getClassroomHoldingTypeTitle (type) {
+      const target = Enums.classroomHoldingTypes.find(item => item.value === type)
+      if (!target) {
+        return '-'
+      }
+
+      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) {
+        return '-'
+      }
+
+      return target.label
+    },
+    getInvoiceStatusTitle (type) {
+      const target = Enums.invoiceStatus.find(item => item.value === type)
+      if (!target) {
+        return '-'
+      }
+
+      return target.label
+    },
+    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-card {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  align-self: stretch;
+  padding: 0;
+  :deep(.q-card__section--vert){
+    padding: 0;
+  }
+  .q-card__section.q-card__section--vert.classroom-card.row {
+    padding: 0;
+  }
+  .right-side {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    padding: 0 4px 0 0;
+
+    .thumbnail {
+      max-width: 100%;
+      height: auto;
+      display: block;
+    }
+
+    .q-btn {
+      display: flex;
+      height: 48px;
+      justify-content: center;
+      align-items: center;
+      align-self: stretch;
+      border-radius: 0 0 0 4px;
+    }
+  }
+  .left-side {
+    display: flex;
+
+    flex-direction: column;
+    justify-content: center;
+    align-items: flex-end;
+    gap: 16px;
+    align-self: stretch;
+
+    .title-box {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: flex-start;
+      align-self: stretch;
+      gap: 8px;
+
+      :deep(.q-banner) {
+        padding: 8px 4px;
+        .q-banner__content {
+          font-size: 16px;
+        }
+      }
+    }
+
+    .status-box {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: flex-start;
+      align-self: stretch;
+
+      .registration-status-box {
+        display: flex;
+        height: 30px;
+        align-items: center;
+        gap: 4px;
+        align-self: stretch;
+      }
+
+      .financial-status-box {
+        display: flex;
+        align-items: center;
+        gap: 4px;
+        align-self: stretch;
+      }
+    }
+  }
+}
+.CLEAN_PASSED {
+  //font-size: 1.1em;
+  color: green;
+}
+.CONDITIONAL_PASSED{
+  color: yellow;
+}
+.FAILED{
+  color: darkred;
+}
+.FAILED_DUE_TO_PRE_CONDITIONAL{
+  color: red;
+}
+</style>

+ 3 - 3
src/components/Widgets/Store/Shop/components/sidebar.vue

@@ -111,12 +111,12 @@ export default {
         categories: [], // content_category
         types: [],
         price: {
-          min: 100, // unit_price__gte
-          max: 1000000 // unit_price__lt
+          min: 0, // unit_price__gte
+          max: 20000000 // unit_price__lt
         }
       },
       minPrice: 0,
-      maxPrice: 1500000,
+      maxPrice: 20000000,
       types: [
         {
           label: 'مجازی',

+ 6 - 4
src/components/Widgets/Test/TestParticipation/TestParticipation.vue

@@ -203,17 +203,19 @@ export default {
           .catch(() => {})
       }).onCancel(() => {})
     },
-    getRemainingTimeInSeconds (serverTimeStr, attendingStartTimeStr, durationInMinutes) {
+    getRemainingTimeInSeconds (serverTimeStr, attendingStartTimeStr, durationInMinutes, testEndTimeStr) {
       // Convert the strings to Date objects
       const serverTime = new Date(serverTimeStr)
       const attendingStartTime = new Date(attendingStartTimeStr)
+      const testEndTime = new Date(testEndTimeStr)
 
       // Calculate the difference in milliseconds
       const timeDiffInMilliseconds = serverTime - attendingStartTime
-
       // Convert the difference to seconds
       const timeDiffInSeconds = timeDiffInMilliseconds / 1000
-
+      if (new Date(serverTime.getTime() + durationInMinutes * 1000 * 60) > testEndTime) {
+        return Math.floor((testEndTime - serverTime) / 1000)
+      }
       // Calculate the remaining time in seconds considering the duration
       return Math.floor((durationInMinutes * 60) - timeDiffInSeconds)
     },
@@ -267,7 +269,7 @@ export default {
       this.answerBook = new AnswerBook(answerBook)
       this.answerBook.loading = false
       if (this.answerBook.remainingTime === null) {
-        this.answerBook.remainingTime = this.getRemainingTimeInSeconds(this.answerBook.server_time, this.answerBook.attending_start_time, this.answerBook.test_info.duration_deadline)
+        this.answerBook.remainingTime = this.getRemainingTimeInSeconds(this.answerBook.server_time, this.answerBook.attending_start_time, this.answerBook.test_info.duration_deadline, this.answerBook.test_info.end_time)
       }
       this.remainingTime = this.answerBook.remainingTime
       this.$store.commit('Test/updateAnswerBook', this.answerBook)