ali 3 سال پیش
والد
کامیت
4691b7e924

+ 2 - 0
src/api/APIGateway.js

@@ -10,6 +10,7 @@ import PaymentAPI from 'src/api/models/Payment.js'
 import SessionAPI from 'src/api/models/Session.js'
 import ContentAPI from 'src/api/models/Content.js'
 import ClassroomAPI from 'src/api/models/Classroom.js'
+import InstalmentAPI from 'src/api/models/Instalment.js'
 import TransactionAPI from 'src/api/models/Transaction.js'
 import PageSettingAPI from 'src/api/models/PageSetting.js'
 import MediaHashtagAPI from 'src/api/models/MediaHashtag.js'
@@ -36,6 +37,7 @@ export const APIGateway = {
   session: new SessionAPI(),
   content: new ContentAPI(),
   classroom: new ClassroomAPI(),
+  instalment: new InstalmentAPI(),
   transaction: new TransactionAPI(),
   pageSetting: new PageSettingAPI(),
   registration: new RegistrationAPI(),

+ 4 - 2
src/api/classes/APIInstanceWrapper.js

@@ -24,9 +24,11 @@ export default class APIInstanceWrapper {
         serverURL,
         headers: { 'Content-Type': 'application/json', common: { Authorization: null } }
       }
-      const get = (url) => new Promise((resolve, reject) => {
+      const get = (url, data) => new Promise((resolve, reject) => {
         const requestAddress = defaults.serverURL + url
+        axios.defaults.headers.common.Authorization = defaults.headers.common.Authorization
         axios.get(requestAddress, {
+          params: data.params,
           proxy: {
             // protocol: 'http',
             host,
@@ -221,7 +223,7 @@ export default class APIInstanceWrapper {
     } else if (req === 'put') {
       return option.api.put(option.request, option.data)
     } else if (req === 'delete') {
-      return option.api.delete(option.request)
+      return option.api.delete(option.request, { params: option.data })
     }
   }
 

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

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

+ 0 - 33
src/api/models/Invoice.js

@@ -7,7 +7,6 @@ export default class InvoiceAPI extends APIRepository {
     super('invoice', appApi)
     this.APIAdresses = {
       base: '/accounting/invoices',
-      instalments: '/accounting/instalments',
       byId: (id) => '/accounting/invoices/' + id,
       pay: (id) => '/accounting/invoices/' + id + '/pay'
     }
@@ -55,38 +54,6 @@ export default class InvoiceAPI extends APIRepository {
     })
   }
 
-  instalments(data) {
-    return this.sendRequest({
-      apiMethod: 'get',
-      api: this.api,
-      request: this.APIAdresses.base,
-      data: this.getNormalizedSendData({
-        type: null, // String -> overdue - ...
-        page: 1 // Number
-      }, data),
-      resolveCallback: (response) => {
-        const paginate = response.data
-        const results = response.data.results
-        delete paginate.results
-        return {
-          list: new InvoiceList(results),
-          paginate
-          // {
-          //   "count": 1,
-          //   "num_pages": 1,
-          //   "per_page": 10,
-          //   "current": 1,
-          //   "next": null,
-          //   "previous": null,
-          // }
-        }
-      },
-      rejectCallback: (error) => {
-        return error
-      }
-    })
-  }
-
   pay(invoiceId) {
     return this.sendRequest({
       apiMethod: 'put',

+ 9 - 1
src/api/models/Wallet.js

@@ -54,11 +54,19 @@ export default class WalletAPI extends APIRepository {
   deposit(data) {
     const amount = data.amount ? data.amount : 0
     const invoice = data.invoice ? data.invoice : null
+    const instalment = data.instalment ? data.instalment : null
+    const sendData = { amount }
+    if (invoice) {
+      sendData.invoice = invoice
+    }
+    if (instalment) {
+      sendData.instalment = instalment
+    }
     return this.sendRequest({
       apiMethod: 'post',
       api: this.api,
       request: this.APIAdresses.deposit,
-      data: { amount, invoice },
+      data: sendData,
       resolveCallback: (response) => {
         return response.data.url // String -> redirect url // thankyou
       },

+ 17 - 1
src/components/InvoicePaymentCard.vue → src/components/InvoicePaymentCard/InvoicePaymentCard.vue

@@ -59,10 +59,12 @@
           </div>
         </div>
       </div>
-      <div class="action-area">
+      <div v-if="!invoice.instalments_info || invoice.instalments_info.length === 0"
+           class="action-area">
         <q-btn color="red"
                class="btn-cancel"
                outline
+               :disable="invoice.status==='PAYING_IN_INSTALMENT'"
                :loading="invoice.loading || wallet.loading"
                @click="onCancel">
           انصراف
@@ -88,6 +90,12 @@
           مشاهده صورتحساب
         </q-btn>
       </div>
+
+      <div>
+        <instalments :wallet="wallet"
+                     :invoice="invoice"
+                     @onInstalmentPayed="onInstalmentPayed" />
+      </div>
     </div>
   </div>
 </template>
@@ -95,9 +103,13 @@
 <script>
 import { Wallet } from 'src/models/Wallet.js'
 import { Invoice } from 'src/models/Invoice.js'
+import Instalments from './components/Instalments.vue'
 
 export default {
   name: 'InvoicePaymentCard',
+  components: {
+    Instalments
+  },
   props: {
     invoice: {
       type: Invoice,
@@ -108,6 +120,7 @@ export default {
       default: new Wallet()
     }
   },
+  emits: ['onCancel', 'onAccept', 'onInstalmentPayed'],
   data: () => {
     return {
       walletLoaded: false,
@@ -137,6 +150,9 @@ export default {
     onAccept () {
       this.$emit('onAccept')
     },
+    onInstalmentPayed () {
+      this.$emit('onInstalmentPayed')
+    },
     checkForNeedToDeposit () {
       this.needToDeposit = this.invoice.amount > this.wallet.inventory
     },

+ 154 - 0
src/components/InvoicePaymentCard/components/Instalments.vue

@@ -0,0 +1,154 @@
+<template>
+  <div class="Instalments">
+    <template v-if="invoice.instalments_info && invoice.instalments_info.length > 0">
+      <q-banner class="q-mb-md">
+        لیست اقساط
+      </q-banner>
+      <q-card class="q-mb-sm sm-hide">
+        <q-card-section>
+          <div class="Instalment row">
+            <div class="Instalments-col col-md-1">
+              شماره
+            </div>
+            <div class="Instalments-col col-md-1">
+              شناسه
+            </div>
+            <div class="Instalments-col col-md-2">
+              مبلغ(ریال)
+            </div>
+            <div class="Instalments-col col-md-2">
+              تاریخ سررسید
+            </div>
+            <div class="Instalments-col col-md-3">
+              تاریخ پرداخت
+            </div>
+          </div>
+        </q-card-section>
+      </q-card>
+      <q-card v-for="(instalment, instalmentIndex) in invoice.instalments_info"
+              :key="instalment.id"
+              class="Instalment q-mb-sm">
+        <q-card-section>
+          <div class="row">
+            <div class="Instalments-col col-md-1 col-12">
+              <span class="lt-md">
+                شماره:
+              </span>
+              {{ instalmentIndex + 1 }}
+            </div>
+            <div class="Instalments-col col-md-1 col-12">
+              <span class="lt-md">
+                شناسه:
+              </span>
+              {{ instalment.id }}
+            </div>
+            <div class="Instalments-col col-md-2 col-12">
+              <span class="lt-md">
+                مبلغ(ریال):
+              </span>
+              {{ instalment.amount.toLocaleString('fa') }}
+            </div>
+            <div class="Instalments-col col-md-2 col-12">
+              <span class="lt-md">
+                تاریخ سررسید:
+              </span>
+              {{ getDueDate(instalment.due_date) }}
+            </div>
+            <div class="Instalments-col col-md-3 col-12">
+              <span class="lt-md">
+                تاریخ پرداخت:
+              </span>
+              {{ getPaidTime(instalment.paid_time) }}
+            </div>
+            <div class="Instalments-col col-md-3 col-12">
+              <q-btn v-if="!instalment.paid_time"
+                     color="primary"
+                     :loading="payLoading"
+                     @click="pay(instalment)">
+                پرداخت
+              </q-btn>
+            </div>
+          </div>
+        </q-card-section>
+      </q-card>
+    </template>
+  </div>
+</template>
+
+<script>
+import Assist from 'src/assets/js/Assist.js'
+import { Wallet } from 'src/models/Wallet.js'
+import { Invoice } from 'src/models/Invoice.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'Instalments',
+  props: {
+    invoice: {
+      type: Invoice,
+      default: new Invoice()
+    },
+    wallet: {
+      type: Wallet,
+      default: new Wallet()
+    }
+  },
+  emits: ['onInstalmentPayed'],
+  data: () => {
+    return {
+      payLoading: false
+    }
+  },
+  methods: {
+    getDueDate (dueDate) {
+      return Assist.miladiToShamsi(dueDate, 'jYYYY/jMM/jDD')
+    },
+    getPaidTime (paidTime) {
+      return Assist.miladiToShamsi(paidTime)
+    },
+    amountOfDepositWalletNeeded (instalmentAmount) {
+      return instalmentAmount - this.wallet.inventory
+    },
+    pay (instalment) {
+      if (this.amountOfDepositWalletNeeded(instalment.amount) > 0) {
+        this.payInstalmentByDepositWallet(instalment.id, instalment.amount)
+      } else {
+        this.payInstalmentByWallet(instalment.id)
+      }
+    },
+    payInstalmentByWallet (instalmentId) {
+      this.payLoading = true
+      APIGateway.instalment.pay(instalmentId)
+        .then((message) => {
+          this.payLoading = false
+          this.$emit('onInstalmentPayed')
+          this.$q.notify({
+            message,
+            type: 'positive'
+          })
+        })
+        .catch(() => {
+          this.payLoading = false
+        })
+    },
+    payInstalmentByDepositWallet (instalmentId, instalmentAmount) {
+      this.payLoading = true
+      APIGateway.wallet.deposit({
+        amount: instalmentAmount,
+        instalment: instalmentId
+      })
+        .then((url) => {
+          window.location.href = url
+        })
+        .catch(() => {
+          this.payLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.Instalments {
+}
+</style>

+ 195 - 0
src/components/Widgets/Invoice/Instalments/Instalments.vue

@@ -0,0 +1,195 @@
+<template>
+  <div class="Instalments"
+       :style="localOptions.style">
+
+    <q-banner inline-actions
+              class="text-white bg-red q-mb-md text-center">
+      لطفا اقساط معوقه خود را پرداخت کنید.
+    </q-banner>
+    <entity-index v-if="mounted"
+                  ref="entityIndex"
+                  v-model:value="inputs"
+                  title="لیست اقساط معوقه"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :create-route-name="createRouteName"
+                  :table-grid-size="$q.screen.lt.sm"
+                  :show-search-button="false"
+                  :show-expand-button="false"
+                  :show-reload-button="false">
+      <template #entity-index-table-cell="{inputData}">
+        <template v-if="inputData.col.name === 'number'">
+          {{ inputData.rowNumber }}
+        </template>
+        <template v-else-if="inputData.col.name === 'action'">
+          <q-btn color="primary"
+                 :to="{name: 'UserPanel.Invoice.Show', params: {id: inputData.props.row.invoice}}">
+            مشاهده جزییات
+          </q-btn>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+      <template #entity-index-table-item-cell="{inputData}">
+        <entity-index-grid-item :input-data="inputData">
+          <template #col="{col, row}">
+            <template v-if="col.name === 'number'">
+              {{ inputData.rowNumber }}
+            </template>
+            <template v-else-if="col.name === 'action'">
+              <q-btn color="primary"
+                     :to="{name: 'UserPanel.Invoice.Show', params: {id: row.invoice}}">
+                مشاهده جزییات
+              </q-btn>
+            </template>
+          </template>
+        </entity-index-grid-item>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import Assist from 'assets/js/Assist.js'
+import { EntityIndex } from 'quasar-crud'
+import { Invoice } from 'src/models/Invoice.js'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import EntityIndexGridItem from 'src/components/EntityIndexGridItem.vue'
+
+export default {
+  name: 'Instalments',
+  components: { EntityIndex, EntityIndexGridItem },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      api: APIGateway.instalment.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        // { type: 'hidden', name: 'type', value: 'overdue' }
+      ],
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'id',
+            required: true,
+            label: 'شناسه',
+            align: 'left',
+            field: row => row.id
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان',
+            align: 'left',
+            field: row => row.invoice_info.title
+          },
+          {
+            name: 'status',
+            required: true,
+            label: 'وضعیت',
+            align: 'left',
+            field: row => (new Invoice(row.invoice_info)).status_info.label
+          },
+          {
+            name: 'amount',
+            required: true,
+            label: 'مبلغ صورتحساب(ریال)',
+            align: 'left',
+            field: row => row.invoice_info.amount.toLocaleString('fa')
+          },
+          {
+            name: 'amount',
+            required: true,
+            label: 'مبلغ قسط(ریال)',
+            align: 'left',
+            field: row => row.amount.toLocaleString('fa')
+          },
+          {
+            name: 'creation_time',
+            required: true,
+            label: 'زمان ثبت',
+            align: 'left',
+            field: row => Assist.miladiToShamsi(row.creation_time)
+          },
+          {
+            name: 'creation_time',
+            required: true,
+            label: 'تاریخ سررسید',
+            align: 'left',
+            field: row => Assist.miladiToShamsi(row.due_date, 'jYYYY/jMM/jDD')
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.setOwner()
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setOwner () {
+      const user = this.$store.getters['Auth/user']
+      FormBuilderAssist.setAttributeByName(this.inputs, 'owner', 'value', user.id)
+    },
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.search)
+    },
+    search () {
+      this.$refs.entityIndex.search()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.Instalments {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+  }
+  :deep(.list) {
+    .quasar-crud-index-table {
+      padding: 0;
+      .q-table__container {
+        background-color: transparent;
+        box-shadow: none;
+        border: none;
+        .q-table__top {
+          display: none;
+        }
+      }
+    }
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Invoice/Instalments/OptionPanel.vue

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

+ 7 - 2
src/components/Widgets/Invoice/InvoiceShow/InvoiceShow.vue

@@ -98,7 +98,8 @@
                                 :invoice="invoice"
                                 class="payment-card"
                                 @onCancel="onCancel"
-                                @onAccept="onAccept" />
+                                @onAccept="onAccept"
+                                @onInstalmentPayed="onInstalmentPayed" />
         </q-card-section>
       </q-card>
 
@@ -113,7 +114,7 @@ import { Wallet } from 'src/models/Wallet.js'
 import { Invoice } from 'src/models/Invoice.js'
 import { mixinWidget } from 'src/mixin/Mixins.js'
 import { APIGateway } from 'src/api/APIGateway.js'
-import InvoicePaymentCard from 'src/components/InvoicePaymentCard.vue'
+import InvoicePaymentCard from 'src/components/InvoicePaymentCard/InvoicePaymentCard.vue'
 
 export default {
   name: 'InvoiceShow',
@@ -154,6 +155,10 @@ export default {
     onAccept () {
       this.payInvoice()
     },
+    onInstalmentPayed () {
+      this.getMyWallet()
+      this.getInvoice()
+    },
     getMyWallet () {
       this.wallet.loading = true
       APIGateway.wallet.getMyWallet(this.user.id)

+ 95 - 8
src/components/Widgets/Invoice/OverdueInstalments/OverdueInstalments.vue

@@ -6,6 +6,18 @@
               class="text-white bg-red q-mb-md text-center">
       لطفا اقساط معوقه خود را پرداخت کنید.
     </q-banner>
+    <q-banner class="q-mb-md">
+      موجودی کیف پول:
+      <template v-if="!wallet.loading">
+        {{ wallet.inventory ? wallet.inventory.toLocaleString('fa') : 0 }}
+      </template>
+      <template v-else>
+        <q-skeleton type="text"
+                    width="100px"
+                    class="inline-block" />
+      </template>
+      ریال
+    </q-banner>
     <entity-index v-if="mounted"
                   ref="entityIndex"
                   v-model:value="inputs"
@@ -17,15 +29,17 @@
                   :table-grid-size="$q.screen.lt.sm"
                   :show-search-button="false"
                   :show-expand-button="false"
-                  :show-reload-button="false">
+                  :show-reload-button="false"
+                  @on-page-changed="onPageChanged">
       <template #entity-index-table-cell="{inputData}">
         <template v-if="inputData.col.name === 'number'">
           {{ inputData.rowNumber }}
         </template>
         <template v-else-if="inputData.col.name === 'action'">
           <q-btn color="primary"
-                 :to="{name: 'UserPanel.Invoice.Show', params: {id: inputData.props.row.invoice}}">
-            مشاهده جزییات
+                 :loading="payLoading || wallet.loading"
+                 @click="pay(inputData.props.row)">
+            پرداخت
           </q-btn>
         </template>
         <template v-else>
@@ -40,8 +54,9 @@
             </template>
             <template v-else-if="col.name === 'action'">
               <q-btn color="primary"
-                     :to="{name: 'UserPanel.Invoice.Show', params: {id: row.invoice}}">
-                مشاهده جزییات
+                     :loading="payLoading || wallet.loading"
+                     @click="pay(row)">
+                پرداخت
               </q-btn>
             </template>
           </template>
@@ -54,11 +69,13 @@
 <script>
 import Assist from 'assets/js/Assist.js'
 import { EntityIndex } from 'quasar-crud'
+import { Wallet } from 'src/models/Wallet.js'
 import { Invoice } from 'src/models/Invoice.js'
 import { mixinWidget } from 'src/mixin/Mixins.js'
 import { APIGateway } from 'src/api/APIGateway.js'
 import { FormBuilderAssist } from 'quasar-form-builder'
 import EntityIndexGridItem from 'src/components/EntityIndexGridItem.vue'
+import { User } from 'src/models/User'
 
 export default {
   name: 'InvoiceList',
@@ -66,7 +83,9 @@ export default {
   mixins: [mixinWidget],
   data: () => {
     return {
-      api: APIGateway.invoice.APIAdresses.instalments,
+      user: new User(),
+      wallet: new Wallet(),
+      api: APIGateway.instalment.APIAdresses.base,
       tableKeys: {
         data: 'results',
         total: 'count',
@@ -145,24 +164,92 @@ export default {
         ]
       },
       mounted: false,
+      payLoading: false,
       createRouteName: ''
     }
   },
   mounted() {
+    this.loadAuthData()
     this.setOwner()
     this.setActionBtn()
+    this.getMyWallet()
     this.mounted = true
   },
   methods: {
+    loadAuthData() { // prevent Hydration node mismatch
+      this.user = this.$store.getters['Auth/user']
+    },
     setOwner () {
-      const user = this.$store.getters['Auth/user']
-      FormBuilderAssist.setAttributeByName(this.inputs, 'owner', 'value', user.id)
+      FormBuilderAssist.setAttributeByName(this.inputs, 'owner', 'value', this.user.id)
     },
     setActionBtn () {
       FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.search)
     },
     search () {
       this.$refs.entityIndex.search()
+    },
+    onPageChanged (response) {
+      if (response.data.results.length === 0) {
+        this.redirectTo()
+      }
+    },
+    redirectTo () {
+      let redirectTo = this.$store.getters['Auth/redirectTo']
+      if (redirectTo === null || typeof redirectTo !== 'object') {
+        redirectTo = { name: 'Public.Home' }
+      }
+      this.$router.push(redirectTo)
+      this.$store.commit('Auth/updateRedirectTo', null)
+    },
+    getMyWallet () {
+      this.wallet.loading = true
+      APIGateway.wallet.getMyWallet(this.user.id)
+        .then((wallet) => {
+          this.wallet = new Wallet(wallet)
+          this.wallet.loading = false
+          this.walletLoaded = true
+        })
+        .catch(() => {
+          this.wallet.loading = false
+        })
+    },
+    amountOfDepositWalletNeeded (instalmentAmount) {
+      return instalmentAmount - this.wallet.inventory
+    },
+    pay (instalment) {
+      if (this.amountOfDepositWalletNeeded(instalment.amount) > 0) {
+        this.payInstalmentByDepositWallet(instalment.id, instalment.amount)
+      } else {
+        this.payInstalmentByWallet(instalment.id)
+      }
+    },
+    payInstalmentByWallet (instalmentId) {
+      this.payLoading = true
+      APIGateway.instalment.pay(instalmentId)
+        .then((message) => {
+          this.payLoading = false
+          this.search()
+          this.$q.notify({
+            message,
+            type: 'positive'
+          })
+        })
+        .catch(() => {
+          this.payLoading = false
+        })
+    },
+    payInstalmentByDepositWallet (instalmentId, instalmentAmount) {
+      this.payLoading = true
+      APIGateway.wallet.deposit({
+        amount: instalmentAmount,
+        instalment: instalmentId
+      })
+        .then((url) => {
+          window.location.href = url
+        })
+        .catch(() => {
+          this.payLoading = false
+        })
     }
   }
 }

+ 47 - 0
src/models/Instalment.js

@@ -0,0 +1,47 @@
+import { User } from 'src/models/User.js'
+import { Model, Collection } from 'js-abstract-model'
+import { InstalmentOffer } from 'src/models/InstalmentOffer.js'
+
+class Instalment extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      {
+        key: 'amount',
+        default: 0
+      },
+      { key: 'creator' },
+      {
+        key: 'creator_info',
+        relatedModel: User
+      },
+      { key: 'creation_time' },
+      { key: 'last_modification_time' },
+      { key: 'due_date' },
+      { key: 'instalment_offer' },
+      {
+        key: 'instalment_offer_info',
+        relatedModel: InstalmentOffer
+      },
+      { key: 'invoice' },
+      { key: 'invoice_info' },
+      { key: 'is_active' },
+      { key: 'is_deleted' },
+      { key: 'paid_time' },
+      { key: 'payer' },
+      {
+        key: 'payer_info',
+        relatedModel: User
+      },
+      { key: 'wallet_inventory' }
+    ])
+  }
+}
+
+class InstalmentList extends Collection {
+  model() {
+    return Instalment
+  }
+}
+
+export { Instalment, InstalmentList }

+ 2 - 0
src/models/Invoice.js

@@ -22,6 +22,8 @@ class Invoice extends Model {
       { key: 'is_deleted' },
       { key: 'item_id' },
       { key: 'item_type' }, // CLASSROOM
+      { key: 'instalments' },
+      { key: 'instalments_info' },
       {
         key: 'item_info',
         relatedModel: Classroom

+ 22 - 0
src/pages/User/Profile/OverdueInstalment.vue

@@ -0,0 +1,22 @@
+<template>
+  <overdue-instalments v-if="mounted" />
+</template>
+
+<script>
+import OverdueInstalments from 'src/components/Widgets/Invoice/OverdueInstalments/OverdueInstalments.vue'
+
+export default {
+  name: 'UserPanel.Profile.AllClassrooms',
+  components: {
+    OverdueInstalments
+  },
+  data () {
+    return {
+      mounted: false
+    }
+  },
+  mounted () {
+    this.mounted = true
+  }
+}
+</script>

+ 39 - 46
src/pages/User/Profile/classrooms.vue

@@ -1,51 +1,46 @@
 <template>
   <breadcrumbs style="margin-top: 29px; margin-bottom: 19px;" />
-  <template v-if="user.has_overdue_instalment">
-    <overdue-instalments />
-  </template>
-  <template v-else>
-    <entity-index v-if="mounted"
-                  ref="entityIndex"
-                  v-model:value="inputs"
-                  title="لیست دوره ها"
-                  :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-else-if="inputData.col.name === 'action'">
-          <q-btn size="md"
-                 color="primary"
-                 label="جزییات"
-                 :to="{name: 'UserPanel.Profile.ClassroomInfo', params: {id: inputData.props.row.classroom_info.id}}" />
-        </template>
-        <template v-else>
-          {{ inputData.col.value }}
-        </template>
+  <entity-index v-if="mounted"
+                ref="entityIndex"
+                v-model:value="inputs"
+                title="لیست دوره ها"
+                :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 #entity-index-table-item-cell="{inputData}">
-        <entity-index-grid-item :input-data="inputData">
-          <template #col="{col, row}">
-            <template v-if="col.name === 'number'">
-              {{ inputData.rowNumber }}
-            </template>
-            <template v-else-if="col.name === 'action'">
-              <q-btn size="md"
-                     color="primary"
-                     label="جزییات"
-                     :to="{name: 'UserPanel.Profile.ClassroomInfo', params: {id: row.classroom_info.id}}" />
-            </template>
-          </template>
-        </entity-index-grid-item>
+      <template v-else-if="inputData.col.name === 'action'">
+        <q-btn size="md"
+               color="primary"
+               label="جزییات"
+               :to="{name: 'UserPanel.Profile.ClassroomInfo', params: {id: inputData.props.row.classroom_info.id}}" />
       </template>
-    </entity-index>
-  </template>
+      <template v-else>
+        {{ inputData.col.value }}
+      </template>
+    </template>
+    <template #entity-index-table-item-cell="{inputData}">
+      <entity-index-grid-item :input-data="inputData">
+        <template #col="{col, row}">
+          <template v-if="col.name === 'number'">
+            {{ inputData.rowNumber }}
+          </template>
+          <template v-else-if="col.name === 'action'">
+            <q-btn size="md"
+                   color="primary"
+                   label="جزییات"
+                   :to="{name: 'UserPanel.Profile.ClassroomInfo', params: {id: row.classroom_info.id}}" />
+          </template>
+        </template>
+      </entity-index-grid-item>
+    </template>
+  </entity-index>
 </template>
 
 <script>
@@ -61,7 +56,6 @@ import BtnControl from 'src/components/Control/btn.vue'
 import { Registration } from 'src/models/Registration.js'
 import EntityIndexGridItem from 'src/components/EntityIndexGridItem.vue'
 import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
-import OverdueInstalments from 'src/components/Widgets/Invoice/OverdueInstalments/OverdueInstalments.vue'
 
 const BtnControlComp = shallowRef(BtnControl)
 
@@ -70,7 +64,6 @@ export default {
   components: {
     EntityIndex,
     Breadcrumbs,
-    OverdueInstalments,
     EntityIndexGridItem
   },
   mixins: [mixinAuth],

+ 14 - 0
src/router/middleware/HasOverdueInstalment.js

@@ -0,0 +1,14 @@
+import { User } from 'src/models/User.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default async function HasOverdueInstalment ({ next, store, to }) {
+  const user = await APIGateway.user.getCurrent()
+  store.commit('Auth/updateUser', user)
+
+  if ((new User(user)).has_overdue_instalment) {
+    store.commit('Auth/updateRedirectTo', { name: to.name, params: to.params, query: to.query })
+    return next({ name: 'UserPanel.Profile.OverdueInstalments' })
+  }
+
+  return next()
+}

+ 2 - 1
src/router/middleware/middleware.js

@@ -2,5 +2,6 @@ import notLogedIn from './notLogedIn.js'
 import Authenticated from './Authenticated.js'
 import canSeeAdminPanel from './canSeeAdminPanel.js'
 import canSeeUserProfilePanel from './canSeeUserProfilePanel.js'
+import HasOverdueInstalment from './HasOverdueInstalment.js'
 
-export { notLogedIn, Authenticated, canSeeAdminPanel, canSeeUserProfilePanel }
+export { notLogedIn, Authenticated, canSeeAdminPanel, canSeeUserProfilePanel, HasOverdueInstalment }

+ 31 - 6
src/router/routes.js

@@ -1,4 +1,4 @@
-import { /* notLogedIn, */Authenticated, canSeeAdminPanel, canSeeUserProfilePanel } from './middleware/middleware.js'
+import { /* notLogedIn, */Authenticated, canSeeAdminPanel, canSeeUserProfilePanel, HasOverdueInstalment } from './middleware/middleware.js'
 
 const routes = [
   {
@@ -167,6 +167,11 @@ const routes = [
             component: () => import('src/layouts/bareLayout.vue'),
             meta: { middlewares: [Authenticated] },
             children: [
+              {
+                name: 'UserPanel.Profile.OverdueInstalments',
+                path: 'overdue-instalments',
+                component: () => import('src/pages/User/Profile/OverdueInstalment.vue')
+              },
               {
                 name: 'UserPanel.Profile.AllClassrooms',
                 path: 'all-classrooms',
@@ -181,7 +186,8 @@ const routes = [
                         to: { name: 'UserPanel.Profile.AllClassrooms' }
                       }
                     ]
-                  }
+                  },
+                  middlewares: [HasOverdueInstalment]
                 }
               },
               {
@@ -198,12 +204,31 @@ const routes = [
                         to: { name: 'UserPanel.Profile.AllDiscussionCirclesClassrooms' }
                       }
                     ]
-                  }
+                  },
+                  middlewares: [HasOverdueInstalment]
+                }
+              },
+              {
+                name: 'UserPanel.Profile.ClassroomInfo',
+                path: 'classroom/:id',
+                component: () => import('src/pages/User/Profile/classroomInfo.vue'),
+                meta: {
+                  middlewares: [HasOverdueInstalment]
+                }
+              },
+              {
+                name: 'UserPanel.Profile.SessionInfo',
+                path: 'session/:id',
+                component: () => import('src/pages/User/Profile/sessionInfo.vue'),
+                meta: {
+                  middlewares: [HasOverdueInstalment]
                 }
               },
-              { name: 'UserPanel.Profile.ClassroomInfo', path: 'classroom/:id', component: () => import('src/pages/User/Profile/classroomInfo.vue') },
-              { name: 'UserPanel.Profile.SessionInfo', path: 'session/:id', component: () => import('src/pages/User/Profile/sessionInfo.vue') },
-              { name: 'UserPanel.Profile.UserInfo', path: 'user-info', component: () => import('src/pages/User/Profile/profile.vue') }
+              {
+                name: 'UserPanel.Profile.UserInfo',
+                path: 'user-info',
+                component: () => import('src/pages/User/Profile/profile.vue')
+              }
             ]
           },
           {

+ 12 - 0
src/store/Auth/actions.js

@@ -110,3 +110,15 @@ export function logOut (context, payload) {
     this.$router.push({ name: 'Login' })
   }
 }
+
+export function updateCurrent (context) {
+  return new Promise((resolve, reject) => {
+    APIGateway.user.getCurrent()
+      .then((user) => {
+        context.commit('updateUser', user)
+      })
+      .catch(() => {
+        reject()
+      })
+  })
+}