File "UpdateAppSetting-20250318162429.vue"

Full Path: /home/pulsehostuk9/public_html/invoicer.pulsehost.co.uk/resources/scripts/admin/views/settings/UpdateAppSetting-20250318162429.vue
File size: 12.49 KB
MIME-type: text/html
Charset: utf-8

<template>
  <BaseSettingCard
    :title="$t('settings.update_app.title')"
    :description="$t('settings.update_app.description')"
  >
    <div class="pb-8 ml-0">
      <label class="text-sm not-italic font-medium input-label">
        {{ $t('settings.update_app.current_version') }}
      </label>

      <div class="w-full border-b-2 border-gray-100 border-solid pb-4">
        <div
          class="
          box-border
          inline-block
          w-auto
          p-3
          my-2
          text-sm text-gray-600
          bg-gray-200
          border border-gray-200 border-solid
          rounded-md
          version
        "
        >
          {{ currentVersion }}
        </div>
      </div>

      <div class="w-full pt-4">
        <BaseCheckbox v-model="insiderChannel" :label="$t('settings.update_app.insider_consent')"/>
      </div>

      <BaseButton
        :loading="isCheckingforUpdate"
        :disabled="isCheckingforUpdate || isUpdating"
        variant="primary-outline"
        class="mt-6"
        @click="checkUpdate"
      >
        {{ $t('settings.update_app.check_update') }}
      </BaseButton>

      <BaseDivider v-if="isUpdateAvailable" class="mt-6 mb-4" />

      <div v-show="!isUpdating" v-if="isUpdateAvailable" class="mt-4 content">
        <BaseHeading type="heading-title" class="mb-2">
          {{ $t('settings.update_app.avail_update') }}
        </BaseHeading>

        <div class="rounded-md bg-primary-50 p-4 mb-3">
          <div class="flex">
            <div class="shrink-0">
              <BaseIcon
                name="InformationCircleIcon"
                class="h-5 w-5 text-primary-400"
                aria-hidden="true"
              />
            </div>
            <div class="ml-3">
              <h3 class="text-sm font-medium text-primary-800">
                {{ $t('general.note') }}
              </h3>
              <div class="mt-2 text-sm text-primary-700">
                <p>
                  {{ $t('settings.update_app.update_warning') }}
                </p>
              </div>
            </div>
          </div>
        </div>

        <label class="text-sm not-italic font-medium input-label">
          {{ $t('settings.update_app.next_version') }}
        </label>
        <br />
        <div
          class="
            box-border
            inline-block
            w-auto
            p-3
            my-2
            text-sm text-gray-600
            bg-gray-200
            border border-gray-200 border-solid
            rounded-md
            version
          "
        >
          {{ updateData.version }}
        </div>

        <div
          class="
            pl-5
            mt-4
            mb-2
            text-sm
            leading-snug
            text-gray-500
            update-description
          "
          style="white-space: pre-wrap; max-width: 480px"
          v-if="description"
          v-html="description"
        ></div>

        <div
          class="
            pl-5
            mt-2
            mb-8
            text-sm
            leading-snug
            text-gray-500
            update-changelog
          "
          style="white-space: pre-wrap; max-width: 480px"
          v-if="changelog"
          v-html="changelog"
        ></div>

        <label class="text-sm not-italic font-medium input-label">
          {{ $t('settings.update_app.requirements') }}
        </label>

        <table class="w-1/2 mt-2 border-2 border-gray-200 BaseTable-fixed">
          <tr
            v-for="(ext, i) in requiredExtentions"
            :key="i"
            class="p-2 border-2 border-gray-200"
          >
            <td width="70%" class="p-2 text-sm truncate">
              {{ i }}
            </td>
            <td width="30%" class="p-2 text-sm text-right">
              <span
                v-if="ext"
                class="inline-block w-4 h-4 ml-3 mr-2 bg-green-500 rounded-full"
              />
              <span
                v-else
                class="inline-block w-4 h-4 ml-3 mr-2 bg-red-500 rounded-full"
              />
            </td>
          </tr>
        </table>

        <BaseButton class="mt-10" variant="primary" @click="onUpdateApp">
          {{ $t('settings.update_app.update') }}
        </BaseButton>
      </div>

      <div v-if="isUpdating" class="relative flex justify-between mt-4 content">
        <div>
          <h6 class="m-0 mb-3 font-medium sw-section-title">
            {{ $t('settings.update_app.update_progress') }}
          </h6>
          <p
            class="mb-8 text-sm leading-snug text-gray-500"
            style="max-width: 480px"
          >
            {{ $t('settings.update_app.progress_text') }}
          </p>
        </div>
        <LoadingIcon
          class="absolute right-0 h-6 m-1 animate-spin text-primary-400"
        />
      </div>
      <ul v-if="isUpdating" class="w-full p-0 list-none">
        <li
          v-for="step in updateSteps"
          :key="step.stepUrl"
          class="
            flex
            justify-between
            w-full
            py-3
            border-b border-gray-200 border-solid
            last:border-b-0
          "
        >
          <p class="m-0 text-sm leading-8">{{ $t(step.translationKey) }}</p>
          <div class="flex flex-row items-center">
            <span v-if="step.time" class="mr-3 text-xs text-gray-500">
              {{ step.time }}
            </span>
            <span
              :class="statusClass(step)"
              class="block py-1 text-sm text-center uppercase rounded-full"
              style="width: 88px"
            >
              {{ getStatus(step) }}
            </span>
          </div>
        </li>
      </ul>
    </div>
  </BaseSettingCard>
</template>

<script setup>
import { useNotificationStore } from '@/scripts/stores/notification'
import axios from 'axios'
import LoadingIcon from '@/scripts/components/icons/LoadingIcon.vue'
import { reactive, ref, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { handleError } from '@/scripts/helpers/error-handling'
import { useCompanyStore } from '@/scripts/admin/stores/company'
import { useExchangeRateStore } from '@/scripts/admin/stores/exchange-rate'
import { useDialogStore } from '@/scripts/stores/dialog'
import BaseCheckbox from "@/scripts/components/base/BaseCheckbox.vue";

const notificationStore = useNotificationStore()
const dialogStore = useDialogStore()
const { t, tm } = useI18n()
const comapnyStore = useCompanyStore()
const exchangeRateStore = useExchangeRateStore()

let isUpdateAvailable = ref(false)
let isCheckingforUpdate = ref(false)
let description = ref('')
let changelog = ref('');
let currentVersion = ref('')
let insiderChannel = ref('')
let requiredExtentions = ref(null)
let deletedFiles = ref(null)
let isUpdating = ref(false)

const updateSteps = reactive([
  {
    translationKey: 'settings.update_app.download_zip_file',
    stepUrl: '/api/v1/update/download',
    time: null,
    started: false,
    completed: false,
  },
  {
    translationKey: 'settings.update_app.unzipping_package',
    stepUrl: '/api/v1/update/unzip',
    time: null,
    started: false,
    completed: false,
  },
  {
    translationKey: 'settings.update_app.copying_files',
    stepUrl: '/api/v1/update/copy',
    time: null,
    started: false,
    completed: false,
  },
  {
    translationKey: 'settings.update_app.deleting_files',
    stepUrl: '/api/v1/update/delete',
    time: null,
    started: false,
    completed: false,
  },
  {
    translationKey: 'settings.update_app.running_migrations',
    stepUrl: '/api/v1/update/migrate',
    time: null,
    started: false,
    completed: false,
  },
  {
    translationKey: 'settings.update_app.finishing_update',
    stepUrl: '/api/v1/update/finish',
    time: null,
    started: false,
    completed: false,
  },
])

const updateData = reactive({
  isMinor: Boolean,
  installed: '',
  version: '',
})

let minPhpVesrion = ref(null)

window.addEventListener('beforeunload', (event) => {
  if (isUpdating.value) {
    event.returnValue = 'Update is in progress!'
  }
})

// Created

axios.get('/api/v1/app/version').then((res) => {
  currentVersion.value = res.data.version
  insiderChannel.value = res.data.channel === 'insider'
})

// comapnyStore
//   .fetchCompanySettings(['bulk_exchange_rate_configured'])
//   .then((res) => {
//     isExchangeRateUpdated.value =
//       res.data.bulk_exchange_rate_configured === 'YES'
//   })

// Comuted props

const allowToUpdate = computed(() => {
  if (requiredExtentions.value !== null) {
    return Object.keys(requiredExtentions.value).every((k) => {
      return requiredExtentions.value[k]
    })
  }
  return true
})

function statusClass(step) {
  const status = getStatus(step)

  switch (status) {
    case 'pending':
      return 'text-primary-800 bg-gray-200'
    case 'finished':
      return 'text-teal-500 bg-teal-100'
    case 'running':
      return 'text-blue-400 bg-blue-100'
    case 'error':
      return 'text-danger bg-red-200'
    default:
      return ''
  }
}

async function checkUpdate() {
  try {
    isCheckingforUpdate.value = true
    let response = await axios.get('/api/v1/check/update', {
      params: {
        channel: insiderChannel ? 'insider' : ''
      }
    });
    isCheckingforUpdate.value = false
    if (!response.data.release) {
      notificationStore.showNotification({
        title: 'Info!',
        type: 'info',
        message: t('settings.update_app.latest_message'),
      })
      return;
    }

    if (response.data) {
      updateData.isMinor = response.data.is_minor
      updateData.version = response.data.release.version
      description.value = response.data.release.description
      changelog.value = response.data.release.changelog
      requiredExtentions.value = response.data.release.extensions
      isUpdateAvailable.value = true
      minPhpVesrion.value = response.data.release.min_php_version
      deletedFiles.value = response.data.release.deleted_files
    }
  } catch (e) {
    isUpdateAvailable.value = false
    isCheckingforUpdate.value = false
    handleError(e)
  }
}

function onUpdateApp() {
  dialogStore
    .openDialog({
      title: t('general.are_you_sure'),
      message: t('settings.update_app.update_warning'),
      yesLabel: t('general.ok'),
      noLabel: t('general.cancel'),
      variant: 'danger',
      hideNoButton: false,
      size: 'lg',
    })
    .then(async (res) => {
      if (res) {
        let path = null
        if (!allowToUpdate.value) {
          notificationStore.showNotification({
            type: 'error',
            message:
              'Your current configuration does not match the update requirements. Please try again after all the requirements are fulfilled.',
          })
          return true
        }
        for (let index = 0; index < updateSteps.length; index++) {
          let currentStep = updateSteps[index]
          try {
            isUpdating.value = true
            currentStep.started = true
            let updateParams = {
              version: updateData.version,
              installed: currentVersion.value,
              path: path || null,
            }

            let requestResponse = await axios.post(
              currentStep.stepUrl,
              updateParams
            )
            currentStep.completed = true
            if (requestResponse.data && requestResponse.data.path) {
              path = requestResponse.data.path
            }
            // on finish

            if (currentStep.translationKey == 'settings.update_app.finishing_update') {
              isUpdating.value = false
              notificationStore.showNotification({
                type: 'success',
                message: t('settings.update_app.update_success'),
              })

              setTimeout(() => {
                location.reload()
              }, 3000)
            }
          } catch (error) {
            currentStep.started = false
            currentStep.completed = true
            handleError(error)
            onUpdateFailed(currentStep.translationKey)
            return false
          }
        }
      }
    })
}

function onUpdateFailed(translationKey) {
  let stepName = t(translationKey)
  if (stepName.value) {
    onUpdateApp()
    return
  }
  isUpdating.value = false
}

function getStatus(step) {
  if (step.started && step.completed) {
    return 'finished'
  } else if (step.started && !step.completed) {
    return 'running'
  } else if (!step.started && !step.completed) {
    return 'pending'
  } else {
    return 'error'
  }
}
</script>

<style>
.update-changelog ul {
  list-style: disc!important;
  margin-left: 30px;
}
.update-changelog li {
  margin-bottom: 4px;
}

</style>