ali vor 2 Jahren
Ursprung
Commit
160b6876e4

+ 138 - 297
src/components/Widgets/Store/Shop/Shop.vue

@@ -6,95 +6,17 @@
     </q-banner>
     <div class="shop-widget__content">
       <div class="shop-widget__sidebar">
-        <div class="shop-widget__sidebar-header">
-          <div class="shop-widget__sidebar-header-title">
-            فیلترها
-          </div>
-          <q-btn flat
-                 class="shop-widget__sidebar-header-remove-filters">
-            <q-icon name="delete_outline" />
-            حذف فیلترها
-          </q-btn>
-        </div>
-        <div class="shop-widget__sidebar-selected-filters">
-          <div class="shop-widget__sidebar-selected-filter">
-            <div class="shop-widget__sidebar-selected-filter-title">
-              استاد مطهری
-            </div>
-            <q-btn class="shop-widget__sidebar-selected-filter-btn-remove"
-                   flat
-                   icon="close" />
-          </div>
-          <div class="shop-widget__sidebar-selected-filter">
-            <div class="shop-widget__sidebar-selected-filter-title">
-              سیر مطالعاتی
-            </div>
-            <q-btn class="shop-widget__sidebar-selected-filter-btn-remove"
-                   flat
-                   icon="close" />
-          </div>
-        </div>
-        <q-separator />
-        <div class="shop-widget__sidebar-price-filter">
-          <q-range v-model="filters.price"
-                   :min="0"
-                   :step="100"
-                   :max="1500000" />
-          <div class="shop-widget__sidebar-price-label">
-            <div class="shop-widget__sidebar-price-label-min">
-              از
-              {{ parseInt(filters.price.min.toString()).toLocaleString('fa') }}
-              تومان
-            </div>
-            <div class="shop-widget__sidebar-price-label-max">
-              تا
-              {{ parseInt(filters.price.max.toString()).toLocaleString('fa') }}
-              تومان
-            </div>
-          </div>
-        </div>
-        <q-separator />
-        <div class="shop-widget__sidebar-exist-filter">
-          <div class="shop-widget__sidebar-exist-label">
-            فقط محصولات موجود
-          </div>
-          <q-toggle v-model="filters.exist" />
-        </div>
-        <q-separator />
-        <div class="shop-widget__sidebar-category-filter">
-          <q-expansion-item expand-separator
-                            label="دسته بندی ها">
-            <q-tree v-model:ticked="filters.categories"
-                    class="col-12 col-sm-6"
-                    :nodes="simple"
-                    children-key="children"
-                    label-key="label"
-                    node-key="id"
-                    tick-strategy="leaf" />
-          </q-expansion-item>
-
-        </div>
-        <q-separator />
-        <div class="shop-widget__sidebar-type-filter">
-          <q-expansion-item expand-separator
-                            label="نوع">
-            <q-tree v-model:ticked="filters.categories"
-                    class="col-12 col-sm-6"
-                    :nodes="types"
-                    children-key="children"
-                    label-key="label"
-                    node-key="value"
-                    tick-strategy="leaf" />
-          </q-expansion-item>
-
-        </div>
+        <sidebar :type="searchTab"
+                 @onChangeFilters="onChangeSidebarFilters" />
       </div>
       <div class="shop-widget__main">
         <div class="shop-widget__main-top-filter">
           <div class="shop-widget__main-top-filter-tools">
             <div class="shop-widget__main-top-filter-txt">
-              <q-input v-model="filters.search"
-                       label="جستجوی محصولات">
+              <q-input v-model="topFilters.search"
+                       :debounce="300"
+                       placeholder="جستجوی محصولات"
+                       @update:model-value="search">
                 <template #prepend>
                   <q-icon name="search" />
                 </template>
@@ -105,10 +27,11 @@
                 ترتیب نمایش
               </div>
               <div class="shop-widget__main-top-filter-sort-select">
-                <q-select v-model="filters.sort"
+                <q-select v-model="topFilters.sort"
                           emit-value
                           map-options
-                          :options="sortOptions" />
+                          :options="sortOptions"
+                          @update:model-value="search" />
               </div>
             </div>
           </div>
@@ -124,23 +47,45 @@
             </q-tabs>
           </div>
         </div>
-        <div class="shop-widget__result">
-          <entity-index v-if="mounted"
-                        ref="entityIndex"
-                        v-model:value="inputs"
-                        :default-layout="false"
-                        :api="api"
-                        :table="table"
-                        :table-keys="tableKeys"
-                        :table-grid-size="true"
-                        :show-expand-button="false"
-                        :show-reload-button="false"
-                        :show-search-button="false">
-            <template #entity-index-table-item-cell="{inputData}">
-              <product-item class="shop-widget__result-product-item"
-                            :product="getProduct(inputData.props.row)" />
-            </template>
-          </entity-index>
+        <div v-if="mounted"
+             class="shop-widget__result">
+          <q-tab-panels v-model="searchTab"
+                        animated>
+            <q-tab-panel name="product">
+              <entity-index ref="productEntityIndex"
+                            v-model:value="productInputs"
+                            :default-layout="false"
+                            :api="productApi"
+                            :table="tableColumns"
+                            :table-keys="tableKeys"
+                            :table-grid-size="true"
+                            :show-expand-button="false"
+                            :show-reload-button="false"
+                            :show-search-button="false">
+                <template #entity-index-table-item-cell="{inputData}">
+                  <product-item class="shop-widget__result-product-item"
+                                :product="getProduct(inputData.props.row)" />
+                </template>
+              </entity-index>
+            </q-tab-panel>
+            <q-tab-panel name="package">
+              <entity-index ref="packageEntityIndex"
+                            v-model:value="packageInputs"
+                            :default-layout="false"
+                            :api="packageApi"
+                            :table="tableColumns"
+                            :table-keys="tableKeys"
+                            :table-grid-size="true"
+                            :show-expand-button="false"
+                            :show-reload-button="false"
+                            :show-search-button="false">
+                <template #entity-index-table-item-cell="{inputData}">
+                  <package-item class="shop-widget__result-product-item"
+                                :package-item="getPackage(inputData.props.row)" />
+                </template>
+              </entity-index>
+            </q-tab-panel>
+          </q-tab-panels>
         </div>
       </div>
     </div>
@@ -149,143 +94,45 @@
 
 <script>
 import { EntityIndex } from 'quasar-crud'
+import Sidebar from './components/sidebar.vue'
 import { Product } from 'src/models/Product.js'
+import { Package } from 'src/models/Package.js'
 import { mixinWidget } from 'src/mixin/Mixins.js'
 import { APIGateway } from 'src/api/APIGateway.js'
 import ProductItem from './components/productItem.vue'
+import PackageItem from './components/packageItem.vue'
+import { FormBuilderAssist } from 'quasar-form-builder'
 
 export default {
   name: 'ContentList',
-  components: { EntityIndex, ProductItem },
+  components: { EntityIndex, ProductItem, PackageItem, Sidebar },
   mixins: [mixinWidget],
   data () {
     return {
-      searchTab: 'product',
-      filters: {
+      mounted: false,
+      searchTab: 'package', // product - package
+      topFilters: {
         search: null,
-        exist: false,
-        sort: 'sort1',
-        categories: [], // content_category
-        price: {
-          min: 100, // unit_price__gte
-          max: 1000000 // unit_price__lt
-        }
+        sort: 'BEST_SELLING'
       },
-      simple: [
-        {
-          label: 'شهید مطهری',
-          id: 1,
-          children: [
-            {
-              label: 'سطح 6',
-              id: 11
-            },
-            {
-              label: 'سطح ۱',
-              id: 3,
-              children: [
-                {
-                  label: 'سطح ۲',
-                  id: 4,
-                  children: [
-                    {
-                      label: 'سطح ۳',
-                      id: 5
-                    },
-                    {
-                      label: 'سطح 4',
-                      id: 9
-                    },
-                    {
-                      label: 'سطح 5',
-                      id: 10
-                    }
-                  ]
-                }
-              ]
-            }
-          ]
-        },
-        {
-          label: 'جریان شناسی',
-          id: 2,
-          children: [
-            {
-              label: 'سطح ۱',
-              id: 6,
-              children: [
-                {
-                  label: 'سطح ۲',
-                  id: 7,
-                  children: [
-                    {
-                      label: 'سطح ۳',
-                      id: 8
-                    }
-                  ]
-                }
-              ]
-            }
-          ]
-        }
-      ],
-      types: [
-        {
-          label: 'مجازی',
-          value: 'virtual',
-          children: [
-            {
-              label: 'ویدیو',
-              value: 'VIDEO'
-            },
-            {
-              label: 'صوت',
-              value: 'SOUND'
-            },
-            {
-              label: 'متن',
-              value: 'TEXT'
-            },
-            {
-              label: 'تصویر',
-              value: 'IMAGE'
-            }
-          ]
-        },
-        {
-          label: 'فیزیکی',
-          value: 'physical',
-          children: [
-            {
-              label: 'کتاب',
-              value: 'BOOK'
-            },
-            {
-              label: 'جزوه',
-              value: 'MANUSCRIPT'
-            },
-            {
-              label: 'لوج فشرده',
-              value: 'CD'
-            }
-          ]
-        }
-      ],
+      sidebarFilters: {},
       sortOptions: [
         {
           label: 'پرفروش ترین',
-          value: 'sort1'
+          value: 'BEST_SELLING'
         },
         {
           label: 'گران ترین',
-          value: 'sort2'
+          value: 'MOST_EXPENSIVE'
         },
         {
           label: 'ارزان ترین',
-          value: 'sort3'
+          value: 'LEAST_EXPENSIVE'
         }
       ],
-      api: APIGateway.product.APIAdresses.base,
+      tableColumns: {
+        columns: []
+      },
       tableKeys: {
         data: 'results',
         total: 'count',
@@ -293,20 +140,88 @@ export default {
         perPage: 'per_page',
         pageKey: 'page'
       },
-      inputs: [],
-      table: {
-        columns: []
-      },
-      mounted: false,
-      createRouteName: ''
+      productApi: APIGateway.product.APIAdresses.base,
+      packageApi: APIGateway.package.APIAdresses.base,
+      productInputs: [
+        { type: 'hidden', name: 'search', value: null },
+        { type: 'hidden', name: 'sort', value: null },
+        { type: 'hidden', name: 'unit_price__lt', value: null },
+        { type: 'hidden', name: 'unit_price__gte', value: null },
+        { type: 'hidden', name: 'inventory__gte', value: null },
+        { type: 'hidden', name: 'physical_type__in', value: null },
+        { type: 'hidden', name: 'medias__type__in', value: null }
+      ],
+      packageInputs: [
+        { type: 'hidden', name: 'search', value: null },
+        { type: 'hidden', name: 'sort', value: null },
+        { type: 'hidden', name: 'unit_price__lt', value: null },
+        { type: 'hidden', name: 'unit_price__gte', value: null },
+        { type: 'hidden', name: 'inventory__gte', value: null }
+      ]
     }
   },
   mounted() {
     this.mounted = true
   },
   methods: {
+    onChangeSidebarFilters (filters) {
+      this.sidebarFilters = filters
+      this.search()
+    },
+    search () {
+      this.updateFilters()
+      if (this.searchTab === 'product') {
+        this.searchProducts()
+      }
+      if (this.searchTab === 'package') {
+        this.searchPackage()
+      }
+    },
+    updateFilters () {
+      this.updateProductFilters()
+      this.updatePackageFilters()
+    },
+    updateProductFilters () {
+      // topFilters
+      FormBuilderAssist.setAttributeByName(this.productInputs, 'sort', 'value', this.topFilters.sort)
+      FormBuilderAssist.setAttributeByName(this.productInputs, 'search', 'value', this.topFilters.search)
+
+      // sidebarFilters
+      FormBuilderAssist.setAttributeByName(this.productInputs, 'unit_price__lt', 'value', this.sidebarFilters.unit_price__lt)
+      FormBuilderAssist.setAttributeByName(this.productInputs, 'unit_price__gte', 'value', this.sidebarFilters.unit_price__gte)
+      FormBuilderAssist.setAttributeByName(this.productInputs, 'inventory__gte', 'value', this.sidebarFilters.inventory__gte)
+      FormBuilderAssist.setAttributeByName(this.productInputs, 'physical_type__in', 'value', this.sidebarFilters.physical_type__in)
+      FormBuilderAssist.setAttributeByName(this.productInputs, 'medias__type__in', 'value', this.sidebarFilters.medias__type__in)
+    },
+    updatePackageFilters () {
+      // topFilters
+      FormBuilderAssist.setAttributeByName(this.packageInputs, 'sort', 'value', this.topFilters.sort)
+      FormBuilderAssist.setAttributeByName(this.packageInputs, 'search', 'value', this.topFilters.search)
+
+      // sidebarFilters
+      FormBuilderAssist.setAttributeByName(this.packageInputs, 'unit_price__lt', 'value', this.sidebarFilters.unit_price__lt)
+      FormBuilderAssist.setAttributeByName(this.packageInputs, 'unit_price__gte', 'value', this.sidebarFilters.unit_price__gte)
+      FormBuilderAssist.setAttributeByName(this.packageInputs, 'inventory__gte', 'value', this.sidebarFilters.inventory__gte)
+    },
+    searchProducts () {
+      if (!this.$refs.productEntityIndex) {
+        return
+      }
+
+      this.$refs.productEntityIndex.search()
+    },
+    searchPackage () {
+      if (!this.$refs.packageEntityIndex) {
+        return
+      }
+
+      this.$refs.packageEntityIndex.search()
+    },
     getProduct (data) {
       return new Product(data)
+    },
+    getPackage (data) {
+      return new Package(data)
     }
   }
 }
@@ -321,80 +236,6 @@ export default {
     $sidebar-width: 312px;
     .shop-widget__sidebar {
       width: $sidebar-width;
-      background: #F6F6F6;
-      border-radius: 20px;
-      padding: 24px;
-      .shop-widget__sidebar-header {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-        margin-bottom: 15px;
-        .shop-widget__sidebar-header-title {
-          font: normal normal medium 16px/21px AzarMehr;
-          letter-spacing: 0;
-        }
-        .shop-widget__sidebar-header-remove-filters {
-          color: #FF3D3D;
-        }
-      }
-      .shop-widget__sidebar-selected-filters {
-        display: flex;
-        gap: 8px;
-        justify-content: flex-start;
-        .shop-widget__sidebar-selected-filter {
-          border: 1px solid #ABABAB;
-          border-radius: 20px;
-          padding: 8px;
-          display: flex;
-          justify-content: space-between;
-          gap: 8px;
-          align-items: center;
-          background: #FFFFFF;
-          .shop-widget__sidebar-selected-filter-title {
-            font-size: 14px;
-            color: #212121;
-          }
-          :deep(.shop-widget__sidebar-selected-filter-btn-remove) {
-            @mixin set-btn-min-max () {
-              $btn-size: 20px;
-              max-height: $btn-size;
-              max-width: $btn-size;
-              min-width: $btn-size;
-              min-height: $btn-size;
-            }
-            padding: 1px;
-            @include set-btn-min-max;
-            .q-focus-helper {
-              @include set-btn-min-max;
-            }
-            .q-btn__content {
-              @include set-btn-min-max;
-              .q-icon {
-                font-size: 18px;
-              }
-            }
-          }
-        }
-      }
-      .q-separator {
-        margin: 18px 0;
-      }
-      .shop-widget__sidebar-price-filter {
-        .shop-widget__sidebar-price-label {
-          display: flex;
-          justify-content: space-between;
-          align-items: center;
-          font-size: 14px;
-          color: #212121;
-          .shop-widget__sidebar-price-label-min {}
-          .shop-widget__sidebar-price-label-max {}
-        }
-      }
-      .shop-widget__sidebar-exist-filter {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-      }
     }
     .shop-widget__main {
       width: calc( 100% - #{$sidebar-width} );

+ 161 - 0
src/components/Widgets/Store/Shop/components/packageItem.vue

@@ -0,0 +1,161 @@
+<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"
+             color="primary">
+        افزودن به سبد
+      </q-btn>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Package } from 'src/models/Package.js'
+
+export default {
+  name: 'PackageItem',
+  props: {
+    packageItem: {
+      type: Package,
+      default: new Package()
+    }
+  }
+}
+</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;
+    .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: 16px;
+        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>

+ 48 - 2
src/components/Widgets/Store/Shop/components/productItem.vue

@@ -1,5 +1,15 @@
 <template>
   <div class="product-item">
+    <div class="product-item__badges">
+      <div class="product-item__badge">
+        <div class="product-item__badge-title">
+          بسته
+        </div>
+        <div class="product-item__badge-icon">
+          <q-icon name="inventory_2" />
+        </div>
+      </div>
+    </div>
     <div class="product-item__thumbnail">
       <q-img :src="product.thumbnail" />
     </div>
@@ -34,7 +44,7 @@
 import { Product } from 'src/models/Product.js'
 
 export default {
-  name: 'productItem',
+  name: 'ProductItem',
   props: {
     product: {
       type: Product,
@@ -46,11 +56,39 @@ export default {
 
 <style scoped lang="scss">
 .product-item {
+  $transition-time: 0.2s;
   background: #F6F6F6;
   border: 1px solid #DFE1EC;
   border-radius: 8px;
+  .product-item__badges {
+    position: absolute;
+    top: 12px;
+    right: 12px;
+    .product-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;
+      .product-item__badge-title {
+        display: none;
+        font-size: 16px;
+        color: #212121;
+      }
+      .product-item__badge-icon {
+        color: $primary;
+      }
+    }
+  }
   :deep(.product-item__thumbnail) {
-    transition: all 0.5s;
+    transition: all $transition-time;
     margin: 15px 58px 12px 58px;
     .q-img {
       width: 100%;
@@ -77,6 +115,7 @@ export default {
     .product-item__price-base {
       font-size: 16px;
       color: #FF3D3D;
+      text-decoration: line-through;
     }
   }
   .product-item__actions {
@@ -92,6 +131,13 @@ export default {
     }
   }
   &:hover {
+    .product-item__badges {
+      .product-item__badge {
+        .product-item__badge-title {
+          display: block;
+        }
+      }
+    }
     :deep(.product-item__thumbnail) {
       margin: 10px 105px 4px 105px;
       .q-img {

+ 336 - 0
src/components/Widgets/Store/Shop/components/sidebar.vue

@@ -0,0 +1,336 @@
+<template>
+  <div class="sidebar">
+    <div class="sidebar__header">
+      <div class="sidebar__header-title">
+        فیلترها
+      </div>
+      <q-btn flat
+             class="sidebar__header-remove-filters"
+             @click="clearFilters">
+        <q-icon name="delete_outline" />
+        حذف فیلترها
+      </q-btn>
+    </div>
+    <div class="sidebar__selected-filters">
+      <div v-for="(tag, tagIndex) in selectedFilters"
+           :key="tagIndex"
+           class="sidebar__selected-filter">
+        <div class="sidebar__selected-filter-title">
+          {{ tag.label }}
+        </div>
+        <q-btn class="sidebar__selected-filter-btn-remove"
+               flat
+               icon="close"
+               @click="removeFilterTag(tag)" />
+      </div>
+    </div>
+    <q-separator />
+    <div class="sidebar__price-filter">
+      <q-range v-model="filters.price"
+               :min="minPrice"
+               :step="100"
+               :max="maxPrice"
+               @update:model-value="updateFilter" />
+      <div class="sidebar__price-label">
+        <div class="sidebar__price-label-min">
+          از
+          {{ parseInt(filters.price.min.toString()).toLocaleString('fa') }}
+          تومان
+        </div>
+        <div class="sidebar__price-label-max">
+          تا
+          {{ parseInt(filters.price.max.toString()).toLocaleString('fa') }}
+          تومان
+        </div>
+      </div>
+    </div>
+    <q-separator />
+    <div class="sidebar__exist-filter">
+      <div class="sidebar__exist-label">
+        فقط محصولات موجود
+      </div>
+      <q-toggle v-model="filters.exist"
+                @update:model-value="updateFilter" />
+    </div>
+    <template v-if="type === 'product'">
+      <q-separator />
+      <div class="sidebar__category-filter">
+        <q-expansion-item expand-separator
+                          label="دسته بندی ها">
+          <q-linear-progress v-if="storeCategories.loading"
+                             indeterminate />
+          <q-tree v-if="!storeCategories.loading"
+                  v-model:ticked="filters.categories"
+                  class="col-12 col-sm-6"
+                  :nodes="storeCategories.list"
+                  children-key="children"
+                  label-key="title"
+                  node-key="id"
+                  tick-strategy="leaf"
+                  @update:ticked="updateFilter" />
+        </q-expansion-item>
+      </div>
+      <q-separator />
+      <div class="sidebar__type-filter">
+        <q-expansion-item expand-separator
+                          label="نوع">
+          <q-tree v-model:ticked="filters.types"
+                  class="col-12 col-sm-6"
+                  :nodes="types"
+                  children-key="children"
+                  label-key="label"
+                  node-key="value"
+                  tick-strategy="leaf"
+                  @update:ticked="updateFilter" />
+        </q-expansion-item>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import { Media } from 'src/models/Media.js'
+import { APIGateway } from 'src/api/APIGateway'
+import { StoreCategoryList } from 'src/models/StoreCategory.js'
+import { Product } from 'src/models/Product.js'
+
+export default {
+  name: 'sidebar',
+  props: {
+    type: {
+      type: String,
+      default: 'product'
+    }
+  },
+  emits: ['onChangeFilters'],
+  data () {
+    return {
+      searchTab: 'product',
+      filters: {
+        exist: false,
+        categories: [], // content_category
+        types: [],
+        price: {
+          min: 100, // unit_price__gte
+          max: 1000000 // unit_price__lt
+        }
+      },
+      minPrice: 0,
+      maxPrice: 1500000,
+      types: [
+        {
+          label: 'مجازی',
+          value: 'virtual',
+          children: (new Media()).typeEnums
+        },
+        {
+          label: 'فیزیکی',
+          value: 'physical',
+          children: (new Product()).physical_typeEnums
+        }
+      ],
+      api: APIGateway.product.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [],
+      storeCategories: new StoreCategoryList(),
+      table: {
+        columns: []
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  computed: {
+    selectedFilters () {
+      const tags = []
+
+      if (this.filters.exist) {
+        tags.push({
+          label: 'کالاهای موجود',
+          filterKey: 'exist'
+        })
+      }
+
+      if (this.filters.categories.length > 0) {
+        this.filters.categories.map(categoryId => this.getCategory(categoryId)).filter(item => item).forEach(item => {
+          tags.push({
+            label: item.title,
+            filterKey: 'storeCategories',
+            value: item
+          })
+        })
+      }
+
+      if (this.filters.types.length > 0) {
+        this.filters.types.map(typeValue => this.getType(typeValue)).filter(item => item).forEach(item => {
+          tags.push({
+            label: item.label,
+            filterKey: 'types',
+            value: item
+          })
+        })
+      }
+
+      return tags
+    }
+  },
+  mounted() {
+    this.getStoreCategories()
+  },
+  methods: {
+    updateFilter () {
+      const productFilters = {
+        unit_price__lt: this.filters.price.max,
+        unit_price__gte: this.filters.price.min,
+        inventory__gte: this.filters.exist ? 1 : null,
+        physical_type__in: this.filters.categories,
+        medias__type__in: this.filters.types
+      }
+
+      const filters = this.type === 'product' ? productFilters : null
+
+      this.$emit('onChangeFilters', filters)
+    },
+    removeCategoryById(categories, idKey, categoryValue) {
+      const targetIndex = categories.findIndex(item => item[idKey] === categoryValue)
+      categories.splice(targetIndex, 1)
+    },
+    getCategoryById(categories, idKey, categoryValue) {
+      for (const category of categories) {
+        if (category[idKey] === categoryValue) {
+          return category
+        }
+        if (category.children && category.children.length > 0) {
+          const result = this.getCategoryById(category.children, idKey, categoryValue)
+          if (result) {
+            return result
+          }
+        }
+      }
+      return null // Return null if category with given id is not found
+    },
+    getCategory (categoryId) {
+      return this.getCategoryById(this.storeCategories.list, 'id', categoryId)
+    },
+    getType (typeEnum) {
+      return this.getCategoryById(this.types, 'value', typeEnum)
+    },
+    removeFilterTag (filterTagItem) {
+      if (filterTagItem.filterKey === 'exist') {
+        this.filters.exist = false
+      }
+      if (filterTagItem.filterKey === 'storeCategories') {
+        this.removeCategoryById(this.filters.categories, 'id', filterTagItem.value.id)
+      }
+      if (filterTagItem.filterKey === 'types') {
+        this.removeCategoryById(this.filters.types, 'value', filterTagItem.value.value)
+      }
+      this.updateFilter()
+    },
+    clearFilters () {
+      this.filters.price.min = this.minPrice
+      this.filters.price.max = this.maxPrice
+      this.filters.exist = false
+      this.filters.categories = []
+      this.filters.types = []
+      this.updateFilter()
+    },
+    getStoreCategories () {
+      this.storeCategories.loading = true
+      APIGateway.storeCategory.index({ parent__isnull: 'true', per_page: 99999 })
+        .then(({ list }) => {
+          this.storeCategories = list
+        })
+        .finally(() => {
+          this.storeCategories.loading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.sidebar {
+  background: #F6F6F6;
+  border-radius: 20px;
+  padding: 24px;
+  .sidebar__header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 15px;
+    .sidebar__header-title {
+      font: normal normal medium 16px/21px AzarMehr;
+      letter-spacing: 0;
+    }
+    .sidebar__header-remove-filters {
+      color: #FF3D3D;
+    }
+  }
+  .sidebar__selected-filters {
+    display: flex;
+    gap: 8px;
+    flex-wrap: wrap;
+    justify-content: flex-start;
+    .sidebar__selected-filter {
+      border: 1px solid #ABABAB;
+      border-radius: 20px;
+      padding: 8px;
+      display: flex;
+      justify-content: space-between;
+      gap: 8px;
+      align-items: center;
+      background: #FFFFFF;
+      .sidebar__selected-filter-title {
+        font-size: 14px;
+        color: #212121;
+      }
+      :deep(.sidebar__selected-filter-btn-remove) {
+        @mixin set-btn-min-max () {
+          $btn-size: 20px;
+          max-height: $btn-size;
+          max-width: $btn-size;
+          min-width: $btn-size;
+          min-height: $btn-size;
+        }
+        padding: 1px;
+        @include set-btn-min-max;
+        .q-focus-helper {
+          @include set-btn-min-max;
+        }
+        .q-btn__content {
+          @include set-btn-min-max;
+          .q-icon {
+            font-size: 18px;
+          }
+        }
+      }
+    }
+  }
+  .q-separator {
+    margin: 18px 0;
+  }
+  .sidebar__price-filter {
+    .sidebar__price-label {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      font-size: 14px;
+      color: #212121;
+      .sidebar__price-label-min {}
+      .sidebar__price-label-max {}
+    }
+  }
+  .sidebar__exist-filter {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+}
+</style>

+ 253 - 233
src/components/Widgets/Store/StorePackage/StorePackageShow/StorePackageShow.vue

@@ -1,277 +1,297 @@
 <template>
-  <div class="ContentShow"
+  <div class="package-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="package-show__summery">
+      <div class="package-show__summery-info">
+        <div class="package-show__thumbnail">
+          <q-img :src="storePackage.thumbnail" />
+        </div>
+        <div class="package-show__meta">
+          <div class="package-show__title">
+            {{ storePackage.title }}
           </div>
-          <div class="col-md-4 col-12">
-            <q-btn color="primary"
-                   class="full-width"
-                   :loading="entityLoading"
-                   @click="sendReply">
-              ارسال پاسخ
-            </q-btn>
+          <div class="package-show__attrs">
+            <div class="package-show__attrs-title">
+              ویژگی ها:
+            </div>
+            <div class="package-show__attrs-items">
+              <div class="package-show__attrs-item">
+                <div class="package-show__attrs-item-label">
+                  وزن:
+                </div>
+                <div class="package-show__attrs-item-value">
+                  ۲۰۰ گرم
+                </div>
+              </div>
+              <div class="package-show__attrs-item">
+                <div class="package-show__attrs-item-label">
+                  وزن:
+                </div>
+                <div class="package-show__attrs-item-value">
+                  ۲۰۰ گرم
+                </div>
+              </div>
+            </div>
           </div>
         </div>
       </div>
-    </q-card>
+      <div class="package-show__summery-price">
+        <div class="package-show__summery-price-title">
+          <q-icon name="shopping_cart" />
+          قیمت محصول
+        </div>
+        <div class="package-show__summery-price-base">
+          {{ storePackage.unit_price }}
+          تومان
+        </div>
+        <div class="package-show__summery-price-final">
+          {{ storePackage.getFinalPrice() }}
+          تومان
+        </div>
+        <q-btn label="افزودن به سبد"
+               outline
+               color="primary"
+               class="package-show__summery-price-action" />
+      </div>
+    </div>
+    <div class="package-show__products">
+      <q-expansion-item expand-separator
+                        default-opened
+                        icon="inventory_2"
+                        label="اقلام داخل بسته">
+        <product-item v-for="(product, productIndex) in storePackage.products_info.list"
+                      :key="productIndex"
+                      :product="product"
+                      class="product-item-component" />
+      </q-expansion-item>
+
+    </div>
+    <div v-if="storePackage.description"
+         class="package-show__descriptions">
+      <q-banner class="package-show__descriptions-title">
+        توضیحات:
+      </q-banner>
+      <div class="package-show__descriptions-data"
+           v-html="storePackage.description" />
+    </div>
   </div>
 </template>
 
 <script>
-import { EntityShow } from 'quasar-crud'
-import { User } from 'src/models/User.js'
-import { Ticket } from 'src/models/Ticket.js'
+import { Package } from 'src/models/Package.js'
 import { mixinWidget } from 'src/mixin/Mixins.js'
 import { APIGateway } from 'src/api/APIGateway.js'
-import { TicketCategoryList } from 'src/models/TicketCategory.js'
+import ProductItem from './components/productItem.vue'
+import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
 
 export default {
   name: 'ContentShow',
-  components: {
-    EntityShow
-  },
+  components: { Breadcrumbs, ProductItem },
   mixins: [mixinWidget],
-  data: () => {
+  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'
-        },
-        {
-          type: 'input',
-          name: 'title',
-          responseKey: 'title',
-          label: 'عنوان',
-          placeholder: ' ',
-          col: 'col-md-12 col-12'
-        },
-        {
-          type: 'inputEditor',
-          name: 'body',
-          responseKey: 'body',
-          label: 'متن',
-          placeholder: ' ',
-          col: 'col-md-12 col-12'
-        },
-        { type: 'hidden', name: 'id', responseKey: 'id', label: 'شناسه', col: 'col-md-12' },
-        { type: 'hidden', name: 'owner', responseKey: 'owner', label: 'owner', col: 'col-md-12' },
-        { type: 'hidden', name: 'replies_info', responseKey: 'replies_info', label: 'replies_info', col: 'col-md-12' }
-      ]
+      storePackage: new Package()
     }
   },
   computed: {
-    repliesInfo () {
-      return this.inputs.find(input => input.name === 'replies_info').value
+    packageId () {
+      return this.$route.params.id
     }
   },
-  created() {
-    this.api = this.api + '/' + this.$route.params.id
-  },
   mounted() {
-    this.loadOptions()
-      .then(() => {
-        this.mounted = true
-        this.loadAuthData()
-        this.$nextTick(() => {
-          this.setInputOptions('category', this.ticketCategoryList.list.map(item => {
-            return {
-              value: item.id,
-              label: item.title
-            }
-          }))
-        })
-      })
-      .catch(() => {
-        this.mounted = true
-      })
+    this.setBreadcrumbsLoading()
+    this.getPackage()
   },
   methods: {
-    loadAuthData () {
-      this.authenticatedUser = this.$store.getters['Auth/user']
-    },
-    afterLoadInputData (response) {
-      this.ticketTitle = response.title
-      this.entityLoading = false
-    },
-    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
-            })
+    getPackage () {
+      this.storePackage.loading = true
+      APIGateway.package.get(this.packageId)
+        .then((storePackage) => {
+          this.storePackage = new Package(storePackage)
+          this.updateBreadcrumbs()
         })
-        .catch(() => {
-          this.$refs.entityShow.getData()
-          this.entityLoading = false
+        .finally(() => {
+          this.storePackage.loading = false
         })
     },
-    loadOptions () {
-      return this.loadCategories()
-    },
-    loadCategories () {
-      return new Promise((resolve, reject) => {
-        APIGateway.ticketCategory.index()
-          .then(({ list }) => {
-            this.ticketCategoryList = list
-            resolve()
-          })
-          .catch(() => {
-            reject()
-          })
+    setBreadcrumbsLoading () {
+      this.$store.commit('AppLayout/updateBreadcrumbs', {
+        visible: false,
+        loading: true
       })
     },
-    setInputOptions (name, options) {
-      this.$refs.entityCreate.setInputAttributeByName(name, 'options', options)
+    updateBreadcrumbs () {
+      this.$store.commit('AppLayout/updateBreadcrumbs', {
+        visible: true,
+        loading: false,
+        path: [
+          {
+            label: 'خانه',
+            to: { name: 'Public.Home' }
+          },
+          {
+            label: 'فروشگاه',
+            to: { name: 'Public.Shop' }
+          },
+          {
+            label: this.storePackage.title,
+            to: { name: 'Public.Package', params: { id: this.storePackage } }
+          }
+        ]
+      })
     }
   }
 }
 </script>
 
 <style scoped lang="scss">
-.ContentShow {
-  .title {
-    font-style: normal;
-    font-weight: 700;
-    font-size: 24px;
-    line-height: 140%;
-    color: #424242;
-    margin-bottom: 27px;
+.package-show {
+  $price-section-with: 312px;
+  .package-show__summery {
     display: flex;
-    flex-flow: row;
-    position: relative;
-    .static-title {
-
+    gap: 24px;
+    align-items: stretch;
+    justify-content: flex-start;
+    margin-bottom: 24px;
+    .package-show__summery-info {
+      border-radius: 8px;
+      background: #F6F6F6;
+      border: 1px solid #DFE1EC;
+      padding: 24px;
+      width: calc( 100% - #{$price-section-with} );
+      display: flex;
+      gap: 74px;
+      align-items: flex-start;
+      justify-content: flex-start;
+      .package-show__thumbnail {
+        width: 239px;
+      }
+      .package-show__meta {
+        .package-show__title {
+          font-size: 22px;
+          margin-bottom: 35px;
+        }
+        .package-show__attrs {
+          .package-show__attrs-title {
+            color: #727272;
+            font-size: 16px;
+            margin-bottom: 16px;
+          }
+          .package-show__attrs-items {
+            .package-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;
+              }
+              .package-show__attrs-item-label {
+                color: #797979;
+              }
+              .package-show__attrs-item-value {
+                color: #212121;
+              }
+            }
+          }
+        }
+      }
     }
-    .dynamic-title {
-
+    .package-show__summery-price {
+      width: $price-section-with;
+      border-radius: 8px;
+      background: #F6F6F6;
+      border: 1px solid #DFE1EC;
+      padding: 76px 20px 46px 20px;
+      gap: 10px;
+      display: flex;
+      flex-flow: column;
+      align-items: center;
+      justify-content: flex-start;
+      .package-show__summery-price-title {
+        font-size: 16px;
+        color: #616161;
+        .q-icon {
+          font-size: 20px;
+          color: #AAA095;
+        }
+      }
+      .package-show__summery-price-base {
+        font-size: 18px;
+        color: #FF3D3D;
+        text-decoration: line-through;
+      }
+      .package-show__summery-price-final {
+        font-size: 24px;
+        color: #475F4A;
+      }
+      .package-show__summery-price-action {
+        width: 100%;
+        background: white !important;
+      }
     }
-    .back-action {
-      position: absolute;
-      right: 0;
-      top: 0;
+  }
+  .package-show__products {
+    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;
+          }
+        }
+      }
+    }
+    .product-item-component {
+      border-bottom: 1px solid #DFE1EC;
+      &:last-child {
+        border: none;
+      }
     }
   }
-  :deep(.form) {
-    padding: 24px;
+  .package-show__descriptions {
+    padding: 32px 32px 70px 32px;
+    border: 1px solid #DFE1EC;
+    border-radius: 8px;
+    background: #F6F6F6;
+    .package-show__descriptions-title{
+      color: #727272;
+      font-size: 16px;
+      margin-bottom: 21px;
+    }
+    .package-show__descriptions-data {
+      color: #212121;
+      font-size: 16px;
+    }
   }
 }
 </style>

+ 133 - 0
src/components/Widgets/Store/StorePackage/StorePackageShow/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>

+ 260 - 233
src/components/Widgets/Store/StoreProduct/StoreProductShow/StoreProductShow.vue

@@ -1,277 +1,304 @@
 <template>
-  <div class="ContentShow"
+  <div class="product-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="product-show__summery">
+      <div class="product-show__summery-info">
+        <div class="product-show__thumbnail">
+          <q-img :src="product.thumbnail" />
+        </div>
+        <div class="product-show__meta">
+          <div class="product-show__title">
+            {{ product.title }}
           </div>
-          <div class="col-md-4 col-12">
-            <q-btn color="primary"
-                   class="full-width"
-                   :loading="entityLoading"
-                   @click="sendReply">
-              ارسال پاسخ
-            </q-btn>
+          <div class="product-show__attrs">
+            <div class="product-show__attrs-title">
+              ویژگی ها:
+            </div>
+            <div class="product-show__attrs-items">
+              <div v-if="product.is_physical !== null"
+                   class="product-show__attrs-item">
+                <div class="product-show__attrs-item-label">
+                  نوع:
+                </div>
+                <div class="product-show__attrs-item-value">
+                  {{ product.is_physical ? 'فیزیکی' : 'مجازی' }}
+                  <template v-if="product.is_physical">
+                    ({{ product.physical_type_info.label }})
+                  </template>
+                </div>
+              </div>
+              <div v-if="product.weight !== null"
+                   class="product-show__attrs-item">
+                <div class="product-show__attrs-item-label">
+                  وزن:
+                </div>
+                <div class="product-show__attrs-item-value">
+                  {{ product.weight }}
+                  گرم
+                </div>
+              </div>
+            </div>
           </div>
         </div>
       </div>
-    </q-card>
+      <div class="product-show__summery-price">
+        <div class="product-show__summery-price-title">
+          <q-icon name="shopping_cart" />
+          قیمت محصول
+        </div>
+        <div class="product-show__summery-price-base">
+          {{ product.unit_price }}
+          تومان
+        </div>
+        <div class="product-show__summery-price-final">
+          {{ product.getFinalPrice() }}
+          تومان
+        </div>
+        <q-btn label="افزودن به سبد"
+               outline
+               color="primary"
+               class="product-show__summery-price-action" />
+      </div>
+    </div>
+    <div v-if="false"
+         class="product-show__products">
+      <q-expansion-item expand-separator
+                        default-opened
+                        icon="inventory_2"
+                        label="اقلام داخل بسته">
+        <product-item v-for="(product, productIndex) in product.products_info.list"
+                      :key="productIndex"
+                      :product="product"
+                      class="product-item-component" />
+      </q-expansion-item>
+
+    </div>
+    <div v-if="product.description"
+         class="product-show__descriptions">
+      <q-banner class="product-show__descriptions-title">
+        توضیحات:
+      </q-banner>
+      <div class="product-show__descriptions-data"
+           v-html="product.description" />
+    </div>
   </div>
 </template>
 
 <script>
-import { EntityShow } from 'quasar-crud'
-import { User } from 'src/models/User.js'
-import { Ticket } from 'src/models/Ticket.js'
+import { Product } from 'src/models/Product.js'
 import { mixinWidget } from 'src/mixin/Mixins.js'
 import { APIGateway } from 'src/api/APIGateway.js'
-import { TicketCategoryList } from 'src/models/TicketCategory.js'
+import ProductItem from './components/productItem.vue'
+import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
 
 export default {
   name: 'ContentShow',
-  components: {
-    EntityShow
-  },
+  components: { Breadcrumbs, ProductItem },
   mixins: [mixinWidget],
-  data: () => {
+  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'
-        },
-        {
-          type: 'input',
-          name: 'title',
-          responseKey: 'title',
-          label: 'عنوان',
-          placeholder: ' ',
-          col: 'col-md-12 col-12'
-        },
-        {
-          type: 'inputEditor',
-          name: 'body',
-          responseKey: 'body',
-          label: 'متن',
-          placeholder: ' ',
-          col: 'col-md-12 col-12'
-        },
-        { type: 'hidden', name: 'id', responseKey: 'id', label: 'شناسه', col: 'col-md-12' },
-        { type: 'hidden', name: 'owner', responseKey: 'owner', label: 'owner', col: 'col-md-12' },
-        { type: 'hidden', name: 'replies_info', responseKey: 'replies_info', label: 'replies_info', col: 'col-md-12' }
-      ]
+      product: new Product()
     }
   },
   computed: {
-    repliesInfo () {
-      return this.inputs.find(input => input.name === 'replies_info').value
+    productId () {
+      return this.$route.params.id
     }
   },
-  created() {
-    this.api = this.api + '/' + this.$route.params.id
-  },
   mounted() {
-    this.loadOptions()
-      .then(() => {
-        this.mounted = true
-        this.loadAuthData()
-        this.$nextTick(() => {
-          this.setInputOptions('category', this.ticketCategoryList.list.map(item => {
-            return {
-              value: item.id,
-              label: item.title
-            }
-          }))
-        })
-      })
-      .catch(() => {
-        this.mounted = true
-      })
+    this.setBreadcrumbsLoading()
+    this.getPackage()
   },
   methods: {
-    loadAuthData () {
-      this.authenticatedUser = this.$store.getters['Auth/user']
-    },
-    afterLoadInputData (response) {
-      this.ticketTitle = response.title
-      this.entityLoading = false
-    },
-    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
-            })
+    getPackage () {
+      this.product.loading = true
+      APIGateway.product.get(this.productId)
+        .then((product) => {
+          this.product = new Product(product)
+          this.updateBreadcrumbs()
         })
-        .catch(() => {
-          this.$refs.entityShow.getData()
-          this.entityLoading = false
+        .finally(() => {
+          this.product.loading = false
         })
     },
-    loadOptions () {
-      return this.loadCategories()
-    },
-    loadCategories () {
-      return new Promise((resolve, reject) => {
-        APIGateway.ticketCategory.index()
-          .then(({ list }) => {
-            this.ticketCategoryList = list
-            resolve()
-          })
-          .catch(() => {
-            reject()
-          })
+    setBreadcrumbsLoading () {
+      this.$store.commit('AppLayout/updateBreadcrumbs', {
+        visible: false,
+        loading: true
       })
     },
-    setInputOptions (name, options) {
-      this.$refs.entityCreate.setInputAttributeByName(name, 'options', options)
+    updateBreadcrumbs () {
+      this.$store.commit('AppLayout/updateBreadcrumbs', {
+        visible: true,
+        loading: false,
+        path: [
+          {
+            label: 'خانه',
+            to: { name: 'Public.Home' }
+          },
+          {
+            label: 'فروشگاه',
+            to: { name: 'Public.Shop' }
+          },
+          {
+            label: this.product.title,
+            to: { name: 'Public.Package', params: { id: this.product } }
+          }
+        ]
+      })
     }
   }
 }
 </script>
 
 <style scoped lang="scss">
-.ContentShow {
-  .title {
-    font-style: normal;
-    font-weight: 700;
-    font-size: 24px;
-    line-height: 140%;
-    color: #424242;
-    margin-bottom: 27px;
+.product-show {
+  $price-section-with: 312px;
+  .product-show__summery {
     display: flex;
-    flex-flow: row;
-    position: relative;
-    .static-title {
-
+    gap: 24px;
+    align-items: stretch;
+    justify-content: flex-start;
+    margin-bottom: 24px;
+    .product-show__summery-info {
+      border-radius: 8px;
+      background: #F6F6F6;
+      border: 1px solid #DFE1EC;
+      padding: 24px;
+      width: calc( 100% - #{$price-section-with} );
+      display: flex;
+      gap: 74px;
+      align-items: flex-start;
+      justify-content: flex-start;
+      .product-show__thumbnail {
+        width: 239px;
+      }
+      .product-show__meta {
+        .product-show__title {
+          font-size: 22px;
+          margin-bottom: 35px;
+        }
+        .product-show__attrs {
+          .product-show__attrs-title {
+            color: #727272;
+            font-size: 16px;
+            margin-bottom: 16px;
+          }
+          .product-show__attrs-items {
+            .product-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;
+              }
+              .product-show__attrs-item-label {
+                color: #797979;
+              }
+              .product-show__attrs-item-value {
+                color: #212121;
+              }
+            }
+          }
+        }
+      }
     }
-    .dynamic-title {
-
+    .product-show__summery-price {
+      width: $price-section-with;
+      border-radius: 8px;
+      background: #F6F6F6;
+      border: 1px solid #DFE1EC;
+      padding: 76px 20px 46px 20px;
+      gap: 10px;
+      display: flex;
+      flex-flow: column;
+      align-items: center;
+      justify-content: flex-start;
+      .product-show__summery-price-title {
+        font-size: 16px;
+        color: #616161;
+        .q-icon {
+          font-size: 20px;
+          color: #AAA095;
+        }
+      }
+      .product-show__summery-price-base {
+        font-size: 18px;
+        color: #FF3D3D;
+        text-decoration: line-through;
+      }
+      .product-show__summery-price-final {
+        font-size: 24px;
+        color: #475F4A;
+      }
+      .product-show__summery-price-action {
+        width: 100%;
+        background: white !important;
+      }
     }
-    .back-action {
-      position: absolute;
-      right: 0;
-      top: 0;
+  }
+  .product-show__products {
+    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;
+          }
+        }
+      }
+    }
+    .product-item-component {
+      border-bottom: 1px solid #DFE1EC;
+      &:last-child {
+        border: none;
+      }
     }
   }
-  :deep(.form) {
-    padding: 24px;
+  .product-show__descriptions {
+    padding: 32px 32px 70px 32px;
+    border: 1px solid #DFE1EC;
+    border-radius: 8px;
+    background: #F6F6F6;
+    .product-show__descriptions-title{
+      color: #727272;
+      font-size: 16px;
+      margin-bottom: 21px;
+    }
+    .product-show__descriptions-data {
+      color: #212121;
+      font-size: 16px;
+    }
   }
 }
 </style>

+ 133 - 0
src/components/Widgets/Store/StoreProduct/StoreProductShow/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>

+ 4 - 0
src/models/Package.js

@@ -43,6 +43,10 @@ class Package extends Model {
       this.thumbnail = '/img/sample-post-thumbnail.jpg'
     }
   }
+
+  getFinalPrice () {
+    return parseInt(this.unit_price * (100 - this.discount_percent)).toLocaleString('fa')
+  }
 }
 
 class PackageList extends Collection {

+ 1 - 1
src/models/Product.js

@@ -43,7 +43,7 @@ class Product extends Model {
         enumListKey: 'physical_typeEnums',
         enums: [
           {
-            label: 'لوج فشرده',
+            label: 'لوح فشرده',
             value: 'CD'
           },
           {

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

@@ -88,6 +88,22 @@ export default {
       },
       component: () => import('src/pages/BasicDynamicPage.vue')
     },
+    {
+      name: 'Public.Package',
+      path: 'package/:id',
+      meta: {
+        hasDynamicSetting: true
+      },
+      component: () => import('src/pages/BasicDynamicPage.vue')
+    },
+    {
+      name: 'Public.Product',
+      path: 'product/:id',
+      meta: {
+        hasDynamicSetting: true
+      },
+      component: () => import('src/pages/BasicDynamicPage.vue')
+    },
     {
       name: 'Public.Rules',
       path: 'rules',