mirror of
				https://github.com/crater-invoice/crater.git
				synced 2025-10-28 12:11:08 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			437 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			437 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <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="
 | |
|           box-border
 | |
|           flex
 | |
|           w-16
 | |
|           p-3
 | |
|           my-2
 | |
|           text-sm text-gray-600
 | |
|           bg-gray-200
 | |
|           border border-gray-200 border-solid
 | |
|           rounded-md
 | |
|           version
 | |
|         "
 | |
|       >
 | |
|         {{ currentVersion }}
 | |
|       </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
 | |
|             flex
 | |
|             w-16
 | |
|             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-8
 | |
|             text-sm
 | |
|             leading-snug
 | |
|             text-gray-500
 | |
|             update-description
 | |
|           "
 | |
|           style="white-space: pre-wrap; max-width: 480px"
 | |
|           v-html="description"
 | |
|         ></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'
 | |
| 
 | |
| 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 currentVersion = 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
 | |
| })
 | |
| 
 | |
| // 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')
 | |
|     isCheckingforUpdate.value = false
 | |
|     if (!response.data.version) {
 | |
|       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.version.version
 | |
|       description.value = response.data.version.description
 | |
|       requiredExtentions.value = response.data.version.extensions
 | |
|       isUpdateAvailable.value = true
 | |
|       minPhpVesrion.value = response.data.version.minimum_php_version
 | |
|       deletedFiles.value = response.data.version.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,
 | |
|               deleted_files: deletedFiles.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-description ul {
 | |
|   list-style: disc !important;
 | |
| }
 | |
| 
 | |
| .update-description li {
 | |
|   margin-bottom: 4px;
 | |
| }
 | |
| </style>
 |