mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-28 12:11:08 -04:00
v5.0.0 update
This commit is contained in:
181
resources/scripts/components/modal-components/BackupModal.vue
Normal file
181
resources/scripts/components/modal-components/BackupModal.vue
Normal file
@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="onCancel" @open="loadData">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="onCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form @submit.prevent="createNewBackup">
|
||||
<div class="p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.backup.select_backup_type')"
|
||||
:error="
|
||||
v$.currentBackupData.option.$error &&
|
||||
v$.currentBackupData.option.$errors[0].$message
|
||||
"
|
||||
horizontal
|
||||
required
|
||||
class="py-2"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="backupStore.currentBackupData.option"
|
||||
:options="options"
|
||||
:can-deselect="false"
|
||||
:placeholder="$t('settings.backup.select_backup_type')"
|
||||
searchable
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.select_disk')"
|
||||
:error="
|
||||
v$.currentBackupData.selected_disk.$error &&
|
||||
v$.currentBackupData.selected_disk.$errors[0].$message
|
||||
"
|
||||
horizontal
|
||||
required
|
||||
class="py-2"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="backupStore.currentBackupData.selected_disk"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="getDisksOptions"
|
||||
:searchable="true"
|
||||
:allow-empty="false"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
:placeholder="$t('settings.disk.select_disk')"
|
||||
track-by="id"
|
||||
object
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="onCancel"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isCreateLoading"
|
||||
:disabled="isCreateLoading"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isCreateLoading"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.create') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useBackupStore } from '@/scripts/stores/backup'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useDiskStore } from '@/scripts/stores/disk'
|
||||
import { required, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
|
||||
let table = ref(null)
|
||||
let isSaving = ref(false)
|
||||
let isCreateLoading = ref(false)
|
||||
let isFetchingInitialData = ref(false)
|
||||
const options = reactive(['full', 'only-db', 'only-files'])
|
||||
|
||||
const backupStore = useBackupStore()
|
||||
const modalStore = useModalStore()
|
||||
const diskStore = useDiskStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'BackupModal'
|
||||
})
|
||||
|
||||
const getDisksOptions = computed(() => {
|
||||
return diskStore.disks.map((disk) => {
|
||||
return {
|
||||
...disk,
|
||||
name: disk.name + ' — ' + '[' + disk.driver + ']',
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentBackupData: {
|
||||
option: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_disk: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => backupStore)
|
||||
)
|
||||
|
||||
async function createNewBackup() {
|
||||
v$.value.currentBackupData.$touch()
|
||||
if (v$.value.currentBackupData.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = {
|
||||
option: backupStore.currentBackupData.option,
|
||||
file_disk_id: backupStore.currentBackupData.selected_disk.id,
|
||||
}
|
||||
try {
|
||||
isCreateLoading.value = true
|
||||
let res = await backupStore.createBackup(data)
|
||||
if (res.data) {
|
||||
isCreateLoading.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
modalStore.closeModal()
|
||||
}
|
||||
} catch (e) {
|
||||
isCreateLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
isFetchingInitialData.value = true
|
||||
let res = await diskStore.fetchDisks({ limit: 'all' })
|
||||
backupStore.currentBackupData.selected_disk = res.data.data[0]
|
||||
isFetchingInitialData.value = false
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
backupStore.$reset()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
161
resources/scripts/components/modal-components/CategoryModal.vue
Normal file
161
resources/scripts/components/modal-components/CategoryModal.vue
Normal file
@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeCategoryModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeCategoryModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form action="" @submit.prevent="submitCategoryData">
|
||||
<div class="p-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('expenses.category')"
|
||||
:error="
|
||||
v$.currentCategory.name.$error &&
|
||||
v$.currentCategory.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="categoryStore.currentCategory.name"
|
||||
:invalid="v$.currentCategory.name.$error"
|
||||
type="text"
|
||||
@input="v$.currentCategory.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('expenses.description')"
|
||||
:error="
|
||||
v$.currentCategory.description.$error &&
|
||||
v$.currentCategory.description.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="categoryStore.currentCategory.description"
|
||||
rows="4"
|
||||
cols="50"
|
||||
@input="v$.currentCategory.description.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-gray-200 border-solid border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
type="button"
|
||||
variant="primary-outline"
|
||||
class="mr-3 text-sm"
|
||||
@click="closeCategoryModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ categoryStore.isEdit ? $t('general.update') : $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useCategoryStore } from '@/scripts/stores/category'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, ref } from 'vue'
|
||||
import { required, minLength, maxLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const categoryStore = useCategoryStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentCategory: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
description: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.description_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => categoryStore)
|
||||
)
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'CategoryModal'
|
||||
})
|
||||
|
||||
async function submitCategoryData() {
|
||||
v$.value.currentCategory.$touch()
|
||||
|
||||
if (v$.value.currentCategory.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
const action = categoryStore.isEdit
|
||||
? categoryStore.updateCategory
|
||||
: categoryStore.addCategory
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
await action(categoryStore.currentCategory)
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
|
||||
closeCategoryModal()
|
||||
}
|
||||
|
||||
function closeCategoryModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
categoryStore.$reset()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
253
resources/scripts/components/modal-components/CompanyModal.vue
Normal file
253
resources/scripts/components/modal-components/CompanyModal.vue
Normal file
@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeCompanyModal" @open="getInitials">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeCompanyModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="submitCompanyData">
|
||||
<div class="p-4 mb-16 sm:p-6 space-y-4">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:content-loading="isFetchingInitialData"
|
||||
:label="$tc('settings.company_info.company_logo')"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isFetchingInitialData">
|
||||
<BaseContentPlaceholdersBox :rounded="true" class="w-full h-24" />
|
||||
</BaseContentPlaceholders>
|
||||
<div v-else class="flex flex-col items-center">
|
||||
<BaseFileUploader
|
||||
:preview-image="previewLogo"
|
||||
base64
|
||||
@remove="onFileInputRemove"
|
||||
@change="onFileInputChange"
|
||||
/>
|
||||
</div>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.company_info.company_name')"
|
||||
:error="
|
||||
v$.newCompanyForm.name.$error &&
|
||||
v$.newCompanyForm.name.$errors[0].$message
|
||||
"
|
||||
:content-loading="isFetchingInitialData"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="newCompanyForm.name"
|
||||
:invalid="v$.newCompanyForm.name.$error"
|
||||
:content-loading="isFetchingInitialData"
|
||||
@input="v$.newCompanyForm.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:content-loading="isFetchingInitialData"
|
||||
:label="$tc('settings.company_info.country')"
|
||||
:error="
|
||||
v$.newCompanyForm.address.country_id.$error &&
|
||||
v$.newCompanyForm.address.country_id.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="newCompanyForm.address.country_id"
|
||||
:content-loading="isFetchingInitialData"
|
||||
label="name"
|
||||
:invalid="v$.newCompanyForm.address.country_id.$error"
|
||||
:options="globalStore.countries"
|
||||
value-prop="id"
|
||||
:can-deselect="true"
|
||||
:can-clear="false"
|
||||
searchable
|
||||
track-by="name"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('wizard.currency')"
|
||||
:error="
|
||||
v$.newCompanyForm.currency.$error &&
|
||||
v$.newCompanyForm.currency.$errors[0].$message
|
||||
"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:help-text="$t('wizard.currency_set_alert')"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="newCompanyForm.currency"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="globalStore.currencies"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
:searchable="true"
|
||||
track-by="name"
|
||||
:placeholder="$tc('settings.currencies.select_currency')"
|
||||
:invalid="v$.newCompanyForm.currency.$error"
|
||||
class="w-full"
|
||||
>
|
||||
</BaseMultiselect>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
|
||||
<div class="z-0 flex justify-end p-4 bg-gray-50 border-modal-bg">
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
outline
|
||||
type="button"
|
||||
@click="closeCompanyModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onMounted, ref, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
import { useGlobalStore } from '@/scripts/stores/global'
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const modalStore = useModalStore()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
let isSaving = ref(false)
|
||||
let previewLogo = ref(null)
|
||||
let isFetchingInitialData = ref(false)
|
||||
let companyLogoFileBlob = ref(null)
|
||||
let companyLogoName = ref(null)
|
||||
|
||||
const newCompanyForm = reactive({
|
||||
name: null,
|
||||
currency: '',
|
||||
address: {
|
||||
country_id: null,
|
||||
},
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'CompanyModal'
|
||||
})
|
||||
|
||||
const rules = {
|
||||
newCompanyForm: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
address: {
|
||||
country_id: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
currency: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(rules, { newCompanyForm })
|
||||
|
||||
async function getInitials() {
|
||||
isFetchingInitialData.value = true
|
||||
await globalStore.fetchCurrencies()
|
||||
await globalStore.fetchCountries()
|
||||
|
||||
newCompanyForm.currency = companyStore.selectedCompanyCurrency.id
|
||||
newCompanyForm.address.country_id =
|
||||
companyStore.selectedCompany.address.country_id
|
||||
|
||||
isFetchingInitialData.value = false
|
||||
}
|
||||
|
||||
function onFileInputChange(fileName, file) {
|
||||
companyLogoName.value = fileName
|
||||
companyLogoFileBlob.value = file
|
||||
}
|
||||
|
||||
function onFileInputRemove() {
|
||||
companyLogoName.value = null
|
||||
companyLogoFileBlob.value = null
|
||||
}
|
||||
|
||||
async function submitCompanyData() {
|
||||
v$.value.newCompanyForm.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
const res = await companyStore.addNewCompany(newCompanyForm)
|
||||
if (res.data.data) {
|
||||
await companyStore.setSelectedCompany(res.data.data)
|
||||
if (companyLogoFileBlob && companyLogoFileBlob.value) {
|
||||
let logoData = new FormData()
|
||||
|
||||
logoData.append(
|
||||
'company_logo',
|
||||
JSON.stringify({
|
||||
name: companyLogoName.value,
|
||||
data: companyLogoFileBlob.value,
|
||||
})
|
||||
)
|
||||
|
||||
await companyStore.updateCompanyLogo(logoData)
|
||||
}
|
||||
await globalStore.setIsAppLoaded(false)
|
||||
await globalStore.bootstrap()
|
||||
closeCompanyModal()
|
||||
}
|
||||
isSaving.value = false
|
||||
}
|
||||
|
||||
function resetNewCompanyForm() {
|
||||
newCompanyForm.name = ''
|
||||
newCompanyForm.currency = ''
|
||||
newCompanyForm.address.country_id = ''
|
||||
|
||||
v$.value.$reset()
|
||||
}
|
||||
|
||||
function closeCompanyModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
resetNewCompanyForm()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
528
resources/scripts/components/modal-components/CustomerModal.vue
Normal file
528
resources/scripts/components/modal-components/CustomerModal.vue
Normal file
@ -0,0 +1,528 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeCustomerModal"
|
||||
@open="setInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeCustomerModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="submitCustomerData">
|
||||
<div class="px-6 pb-3">
|
||||
<BaseTabGroup>
|
||||
<BaseTab :title="$t('customers.basic_info')" class="!mt-2">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.display_name')"
|
||||
required
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.name"
|
||||
type="text"
|
||||
name="name"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.name.$error"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.currencies.currency')"
|
||||
required
|
||||
:error="
|
||||
v$.currency_id.$error && v$.currency_id.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="customerStore.currentCustomer.currency_id"
|
||||
:options="globalStore.currencies"
|
||||
value-prop="id"
|
||||
searchable
|
||||
:placeholder="$t('customers.select_currency')"
|
||||
:max-height="200"
|
||||
class="mt-1 md:mt-0"
|
||||
track-by="name"
|
||||
:invalid="v$.currency_id.$error"
|
||||
label="name"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.primary_contact_name')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.contact_name"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('login.email')"
|
||||
:error="v$.email.$error && v$.email.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.email"
|
||||
type="text"
|
||||
name="email"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.email.$error"
|
||||
@input="v$.email.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.prefix')"
|
||||
:error="v$.prefix.$error && v$.prefix.$errors[0].$message"
|
||||
:content-loading="isFetchingInitialData"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.prefix"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="name"
|
||||
class=""
|
||||
:invalid="v$.prefix.$error"
|
||||
@input="v$.prefix.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup :label="$t('customers.phone')">
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.website')"
|
||||
:error="v$.website.$error && v$.website.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.website"
|
||||
type="url"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.website.$error"
|
||||
@input="v$.website.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</BaseInputGrid>
|
||||
</BaseTab>
|
||||
|
||||
<BaseTab :title="$t('customers.billing_address')" class="!mt-2">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup :label="$t('customers.name')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.billing.name"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.country')">
|
||||
<BaseMultiselect
|
||||
v-model="customerStore.currentCustomer.billing.country_id"
|
||||
:options="globalStore.countries"
|
||||
searchable
|
||||
:show-labels="false"
|
||||
:placeholder="$t('general.select_country')"
|
||||
:allow-empty="false"
|
||||
track-by="name"
|
||||
class="mt-1 md:mt-0"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.state')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.billing.state"
|
||||
type="text"
|
||||
name="billingState"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.city')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.billing.city"
|
||||
type="text"
|
||||
name="billingCity"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.address')"
|
||||
:error="
|
||||
v$.billing.address_street_1.$error &&
|
||||
v$.billing.address_street_1.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="
|
||||
customerStore.currentCustomer.billing.address_street_1
|
||||
"
|
||||
:placeholder="$t('general.street_1')"
|
||||
rows="2"
|
||||
cols="50"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.billing.address_street_1.$error"
|
||||
@input="v$.billing.address_street_1.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:error="
|
||||
v$.billing.address_street_2.$error &&
|
||||
v$.billing.address_street_2.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="
|
||||
customerStore.currentCustomer.billing.address_street_2
|
||||
"
|
||||
:placeholder="$t('general.street_2')"
|
||||
rows="2"
|
||||
cols="50"
|
||||
:invalid="v$.billing.address_street_2.$error"
|
||||
@input="v$.billing.address_street_2.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.phone')">
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.billing.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.zip_code')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.billing.zip"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</BaseTab>
|
||||
|
||||
<BaseTab :title="$t('customers.shipping_address')" class="!mt-2">
|
||||
<div class="grid md:grid-cols-12">
|
||||
<div class="flex justify-end col-span-12">
|
||||
<BaseButton
|
||||
variant="primary"
|
||||
type="button"
|
||||
size="xs"
|
||||
@click="copyAddress(true)"
|
||||
>
|
||||
{{ $t('customers.copy_billing_address') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup :label="$t('customers.name')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.shipping.name"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.country')">
|
||||
<BaseMultiselect
|
||||
v-model="customerStore.currentCustomer.shipping.country_id"
|
||||
:options="globalStore.countries"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:placeholder="$t('general.select_country')"
|
||||
track-by="name"
|
||||
class="mt-1 md:mt-0"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.state')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.shipping.state"
|
||||
type="text"
|
||||
name="shippingState"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.city')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.shipping.city"
|
||||
type="text"
|
||||
name="shippingCity"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.address')"
|
||||
:error="
|
||||
v$.shipping.address_street_1.$error &&
|
||||
v$.shipping.address_street_1.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="
|
||||
customerStore.currentCustomer.shipping.address_street_1
|
||||
"
|
||||
:placeholder="$t('general.street_1')"
|
||||
rows="2"
|
||||
cols="50"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.shipping.address_street_1.$error"
|
||||
@input="v$.shipping.address_street_1.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:error="
|
||||
v$.shipping.address_street_2.$error &&
|
||||
v$.shipping.address_street_2.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="
|
||||
customerStore.currentCustomer.shipping.address_street_2
|
||||
"
|
||||
:placeholder="$t('general.street_2')"
|
||||
rows="2"
|
||||
cols="50"
|
||||
:invalid="v$.shipping.address_street_1.$error"
|
||||
@input="v$.shipping.address_street_2.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.phone')">
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.shipping.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.zip_code')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.shipping.zip"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</BaseTab>
|
||||
</BaseTabGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
type="button"
|
||||
variant="primary-outline"
|
||||
@click="closeCustomerModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton :loading="isLoading" variant="primary" type="submit">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
required,
|
||||
minLength,
|
||||
maxLength,
|
||||
email,
|
||||
alpha,
|
||||
url,
|
||||
helpers,
|
||||
} from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useEstimateStore } from '@/scripts/stores/estimate'
|
||||
import { useCustomerStore } from '@/scripts/stores/customer'
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
import { useGlobalStore } from '@/scripts/stores/global'
|
||||
import { useInvoiceStore } from '@/scripts/stores/invoice'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
const customerStore = useCustomerStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const invoiceStore = useInvoiceStore()
|
||||
|
||||
let isFetchingInitialData = ref(false)
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const isEdit = ref(false)
|
||||
const isLoading = ref(false)
|
||||
|
||||
const modalActive = computed(
|
||||
() => modalStore.active && modalStore.componentName === 'CustomerModal'
|
||||
)
|
||||
|
||||
const rules = {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
currency_id: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
email: {
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
prefix: {
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
website: {
|
||||
url: helpers.withMessage(t('validation.invalid_url'), url),
|
||||
},
|
||||
|
||||
billing: {
|
||||
address_street_1: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.address_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
address_street_2: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.address_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
shipping: {
|
||||
address_street_1: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.address_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
address_street_2: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.address_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => customerStore.currentCustomer)
|
||||
)
|
||||
|
||||
function copyAddress() {
|
||||
customerStore.copyAddress()
|
||||
}
|
||||
|
||||
async function setInitialData() {
|
||||
if (!customerStore.isEdit) {
|
||||
customerStore.currentCustomer.currency_id =
|
||||
companyStore.selectedCompanyCurrency.id
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCustomerData() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$error) {
|
||||
return true
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
let data = {
|
||||
...customerStore.currentCustomer,
|
||||
}
|
||||
|
||||
try {
|
||||
let response = null
|
||||
if (customerStore.isEdit) {
|
||||
response = await customerStore.updateCustomer(data)
|
||||
} else {
|
||||
response = await customerStore.addCustomer(data)
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
isLoading.value = false
|
||||
// Automatically create newly created customer
|
||||
if (route.name === 'invoices.create' || route.name === 'invoices.edit') {
|
||||
invoiceStore.selectCustomer(response.data.data.id)
|
||||
}
|
||||
if (
|
||||
route.name === 'estimates.create' ||
|
||||
route.name === 'estimates.edit'
|
||||
) {
|
||||
estimateStore.selectCustomer(response.data.data.id)
|
||||
}
|
||||
closeCustomerModal()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeCustomerModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
customerStore.resetCurrentCustomer()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeCompanyModal">
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="px-6 pt-6">
|
||||
<h6 class="font-medium text-lg text-left">
|
||||
{{ modalStore.title }}
|
||||
</h6>
|
||||
<p
|
||||
class="mt-2 text-sm leading-snug text-gray-500"
|
||||
style="max-width: 680px"
|
||||
>
|
||||
{{
|
||||
$t('settings.company_info.delete_company_modal_desc', {
|
||||
company: companyStore.selectedCompany.name,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form action="" @submit.prevent="submitCompanyData">
|
||||
<div class="p-4 sm:p-6 space-y-4">
|
||||
<BaseInputGroup
|
||||
:label="
|
||||
$t('settings.company_info.delete_company_modal_label', {
|
||||
company: companyStore.selectedCompany.name,
|
||||
})
|
||||
"
|
||||
:error="
|
||||
v$.formData.name.$error && v$.formData.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="formData.name"
|
||||
:invalid="v$.formData.name.$error"
|
||||
@input="v$.formData.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
|
||||
<div class="z-0 flex justify-end p-4 bg-gray-50 border-modal-bg">
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
outline
|
||||
type="button"
|
||||
@click="closeCompanyModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isDeleting"
|
||||
:disabled="isDeleting"
|
||||
variant="danger"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isDeleting"
|
||||
name="TrashIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onMounted, ref, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers, sameAs } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
import { useGlobalStore } from '@/scripts/stores/global'
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const modalStore = useModalStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
let isDeleting = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
id: companyStore.selectedCompany.id,
|
||||
name: null,
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'DeleteCompanyModal'
|
||||
})
|
||||
|
||||
const rules = {
|
||||
formData: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
sameAsName: helpers.withMessage(
|
||||
t('validation.company_name_not_same'),
|
||||
sameAs(companyStore.selectedCompany.name)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
{ formData },
|
||||
{
|
||||
$scope: false,
|
||||
}
|
||||
)
|
||||
|
||||
async function submitCompanyData() {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
const company = companyStore.companies[0]
|
||||
|
||||
isDeleting.value = true
|
||||
try {
|
||||
const res = await companyStore.deleteCompany(formData)
|
||||
console.log(res.data.success)
|
||||
if (res.data.success) {
|
||||
closeCompanyModal()
|
||||
await companyStore.setSelectedCompany(company)
|
||||
router.push('/admin/dashboard')
|
||||
await globalStore.setIsAppLoaded(false)
|
||||
await globalStore.bootstrap()
|
||||
}
|
||||
isDeleting.value = false
|
||||
} catch {
|
||||
isDeleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetNewCompanyForm() {
|
||||
formData.id = null
|
||||
formData.name = ''
|
||||
|
||||
v$.value.$reset()
|
||||
}
|
||||
|
||||
function closeCompanyModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
resetNewCompanyForm()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive">
|
||||
<ExchangeRateBulkUpdate @update="closeModal()" />
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import ExchangeRateBulkUpdate from '@/scripts/components/currency-exchange-rate/ExchangeRateBulkUpdate.vue'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return (
|
||||
modalStore.active &&
|
||||
modalStore.componentName === 'ExchangeRateBulkUpdateModal'
|
||||
)
|
||||
})
|
||||
|
||||
function closeModal() {
|
||||
modalStore.closeModal()
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,482 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeExchangeRateModal"
|
||||
@open="fetchInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeExchangeRateModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form @submit.prevent="submitExchangeRate">
|
||||
<div class="px-4 md:px-8 py-8 overflow-y-auto sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.exchange_rate.driver')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
required
|
||||
:error="
|
||||
v$.currentExchangeRate.driver.$error &&
|
||||
v$.currentExchangeRate.driver.$errors[0].$message
|
||||
"
|
||||
:help-text="driverSite"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="exchangeRateStore.currentExchangeRate.driver"
|
||||
:options="driversLists"
|
||||
:content-loading="isFetchingInitialData"
|
||||
value-prop="value"
|
||||
:can-deselect="true"
|
||||
label="key"
|
||||
:searchable="true"
|
||||
:invalid="v$.currentExchangeRate.driver.$error"
|
||||
@update:modelValue="resetCurrency"
|
||||
@input="v$.currentExchangeRate.driver.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
v-if="isCurrencyConverter"
|
||||
required
|
||||
:label="$t('settings.exchange_rate.server')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.currencyConverter.type.$error &&
|
||||
v$.currencyConverter.type.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="exchangeRateStore.currencyConverter.type"
|
||||
:content-loading="isFetchingInitialData"
|
||||
value-prop="value"
|
||||
searchable
|
||||
:options="serverOptions"
|
||||
:invalid="v$.currencyConverter.type.$error"
|
||||
label="value"
|
||||
@update:modelValue="resetCurrency"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.exchange_rate.key')"
|
||||
required
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.currentExchangeRate.key.$error &&
|
||||
v$.currentExchangeRate.key.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="exchangeRateStore.currentExchangeRate.key"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="key"
|
||||
:loading="isFetchingCurrencies"
|
||||
loading-position="right"
|
||||
:invalid="v$.currentExchangeRate.key.$error"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="exchangeRateStore.supportedCurrencies.length"
|
||||
:label="$t('settings.exchange_rate.currency')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.currentExchangeRate.currencies.$error &&
|
||||
v$.currentExchangeRate.currencies.$errors[0].$message
|
||||
"
|
||||
:help-text="$t('settings.exchange_rate.currency_help_text')"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="exchangeRateStore.currentExchangeRate.currencies"
|
||||
:content-loading="isFetchingInitialData"
|
||||
value-prop="code"
|
||||
mode="tags"
|
||||
searchable
|
||||
:options="exchangeRateStore.supportedCurrencies"
|
||||
:invalid="v$.currentExchangeRate.currencies.$error"
|
||||
label="code"
|
||||
track-by="code"
|
||||
@input="v$.currentExchangeRate.currencies.$touch()"
|
||||
openDirection="top"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<!-- For Currency Converter -->
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="isDedicatedServer"
|
||||
:label="$t('settings.exchange_rate.url')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.currencyConverter.url.$error &&
|
||||
v$.currencyConverter.url.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="exchangeRateStore.currencyConverter.url"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="url"
|
||||
:invalid="v$.currencyConverter.url.$error"
|
||||
@input="v$.currencyConverter.url.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseSwitch
|
||||
v-model="exchangeRateStore.currentExchangeRate.active"
|
||||
class="flex"
|
||||
:label-right="$t('settings.exchange_rate.active')"
|
||||
/>
|
||||
</BaseInputGrid>
|
||||
|
||||
<BaseInfoAlert
|
||||
v-if="
|
||||
currenciesAlredayInUsed.length &&
|
||||
exchangeRateStore.currentExchangeRate.active
|
||||
"
|
||||
class="mt-5"
|
||||
:title="$t('settings.exchange_rate.currency_in_used')"
|
||||
:lists="[currenciesAlredayInUsed.toString()]"
|
||||
:actions="['Remove']"
|
||||
@hide="dismiss"
|
||||
@Remove="removeUsedSelectedCurrencies"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
:disabled="isSaving"
|
||||
@click="closeExchangeRateModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving || isFetchingCurrencies"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{
|
||||
exchangeRateStore.isEdit ? $t('general.update') : $t('general.save')
|
||||
}}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useExchangeRateStore } from '@/scripts/stores/exchange-rate'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
required,
|
||||
minLength,
|
||||
helpers,
|
||||
requiredIf,
|
||||
url,
|
||||
} from '@vuelidate/validators'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
let isFetchingInitialData = ref(false)
|
||||
let isFetchingCurrencies = ref(false)
|
||||
let currenciesAlredayInUsed = ref([])
|
||||
let currenctPorivderOldCurrencies = ref([])
|
||||
const modalStore = useModalStore()
|
||||
const exchangeRateStore = useExchangeRateStore()
|
||||
|
||||
let serverOptions = ref([])
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentExchangeRate: {
|
||||
key: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
currencies: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
currencyConverter: {
|
||||
type: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(isCurrencyConverter)
|
||||
),
|
||||
},
|
||||
url: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(isDedicatedServer)
|
||||
),
|
||||
url: helpers.withMessage(t('validation.invalid_url'), url),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
const driversLists = computed(() => {
|
||||
return exchangeRateStore.drivers.map((item) => {
|
||||
return Object.assign({}, item, {
|
||||
key: t(item.key),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return (
|
||||
modalStore.active &&
|
||||
modalStore.componentName === 'ExchangeRateProviderModal'
|
||||
)
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return modalStore.title
|
||||
})
|
||||
|
||||
const isCurrencyConverter = computed(() => {
|
||||
return exchangeRateStore.currentExchangeRate.driver === 'currency_converter'
|
||||
})
|
||||
|
||||
const isDedicatedServer = computed(() => {
|
||||
return (
|
||||
exchangeRateStore.currencyConverter &&
|
||||
exchangeRateStore.currencyConverter.type === 'DEDICATED'
|
||||
)
|
||||
})
|
||||
|
||||
const driverSite = computed(() => {
|
||||
switch (exchangeRateStore.currentExchangeRate.driver) {
|
||||
case 'currency_converter':
|
||||
return `https://www.currencyconverterapi.com`
|
||||
|
||||
case 'currency_freak':
|
||||
return 'https://currencyfreaks.com'
|
||||
|
||||
case 'currency_layer':
|
||||
return 'https://currencylayer.com'
|
||||
|
||||
case 'open_exchange_rate':
|
||||
return 'https://openexchangerates.org'
|
||||
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => exchangeRateStore)
|
||||
)
|
||||
|
||||
function dismiss() {
|
||||
currenciesAlredayInUsed.value = []
|
||||
}
|
||||
function removeUsedSelectedCurrencies() {
|
||||
const { currencies } = exchangeRateStore.currentExchangeRate
|
||||
currenciesAlredayInUsed.value.forEach((uc) => {
|
||||
currencies.forEach((c, i) => {
|
||||
if (c === uc) {
|
||||
currencies.splice(i, 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
currenciesAlredayInUsed.value = []
|
||||
}
|
||||
|
||||
function resetCurrency() {
|
||||
exchangeRateStore.currentExchangeRate.key = null
|
||||
exchangeRateStore.currentExchangeRate.currencies = []
|
||||
exchangeRateStore.supportedCurrencies = []
|
||||
}
|
||||
|
||||
function resetModalData() {
|
||||
exchangeRateStore.supportedCurrencies = []
|
||||
currenctPorivderOldCurrencies.value = []
|
||||
exchangeRateStore.currentExchangeRate = {
|
||||
id: null,
|
||||
name: '',
|
||||
driver: '',
|
||||
key: '',
|
||||
active: true,
|
||||
currencies: [],
|
||||
}
|
||||
|
||||
exchangeRateStore.currencyConverter = {
|
||||
type: '',
|
||||
url: '',
|
||||
}
|
||||
currenciesAlredayInUsed.value = []
|
||||
}
|
||||
|
||||
async function fetchInitialData() {
|
||||
exchangeRateStore.currentExchangeRate.driver = 'currency_converter'
|
||||
let params = {}
|
||||
if (exchangeRateStore.isEdit) {
|
||||
params.provider_id = exchangeRateStore.currentExchangeRate.id
|
||||
}
|
||||
isFetchingInitialData.value = true
|
||||
await exchangeRateStore.fetchDefaultProviders()
|
||||
await exchangeRateStore.fetchActiveCurrency(params)
|
||||
|
||||
currenctPorivderOldCurrencies.value =
|
||||
exchangeRateStore.currentExchangeRate.currencies
|
||||
|
||||
isFetchingInitialData.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => isCurrencyConverter.value,
|
||||
(newVal, oldValue) => {
|
||||
if (newVal) {
|
||||
fetchServers()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => exchangeRateStore.currentExchangeRate.key,
|
||||
(newVal, oldValue) => {
|
||||
if (newVal) {
|
||||
fetchCurrencies()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => exchangeRateStore?.currencyConverter?.type,
|
||||
(newVal, oldValue) => {
|
||||
if (newVal) {
|
||||
fetchCurrencies()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
fetchCurrencies = debounce(fetchCurrencies, 500)
|
||||
|
||||
function validate() {
|
||||
v$.value.$touch()
|
||||
checkingIsActiveCurrencies()
|
||||
if (
|
||||
v$.value.$invalid ||
|
||||
(currenciesAlredayInUsed.value.length &&
|
||||
exchangeRateStore.currentExchangeRate.active)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function submitExchangeRate() {
|
||||
if (validate()) {
|
||||
return true
|
||||
}
|
||||
let data = {
|
||||
...exchangeRateStore.currentExchangeRate,
|
||||
}
|
||||
if (isCurrencyConverter.value) {
|
||||
data.driver_config = {
|
||||
...exchangeRateStore.currencyConverter,
|
||||
}
|
||||
if (!isDedicatedServer.value) {
|
||||
data.driver_config.url = ''
|
||||
}
|
||||
}
|
||||
const action = exchangeRateStore.isEdit
|
||||
? exchangeRateStore.updateProvider
|
||||
: exchangeRateStore.addProvider
|
||||
isSaving.value = true
|
||||
|
||||
await action(data)
|
||||
.then((res) => {
|
||||
isSaving.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closeExchangeRateModal()
|
||||
})
|
||||
.catch((err) => {
|
||||
isSaving.value = false
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchServers() {
|
||||
let res = await exchangeRateStore.getCurrencyConverterServers()
|
||||
serverOptions.value = res.data.currency_converter_servers
|
||||
exchangeRateStore.currencyConverter.type = 'FREE'
|
||||
}
|
||||
|
||||
function fetchCurrencies() {
|
||||
const { driver, key } = exchangeRateStore.currentExchangeRate
|
||||
if (driver && key) {
|
||||
isFetchingCurrencies.value = true
|
||||
let data = {
|
||||
driver: driver,
|
||||
key: key,
|
||||
}
|
||||
if (
|
||||
isCurrencyConverter.value &&
|
||||
!exchangeRateStore.currencyConverter.type
|
||||
) {
|
||||
isFetchingCurrencies.value = false
|
||||
return
|
||||
}
|
||||
if (exchangeRateStore?.currencyConverter?.type) {
|
||||
data.type = exchangeRateStore.currencyConverter.type
|
||||
}
|
||||
|
||||
exchangeRateStore
|
||||
.fetchCurrencies(data)
|
||||
.then((res) => {
|
||||
isFetchingCurrencies.value = false
|
||||
})
|
||||
.catch((err) => {
|
||||
isFetchingCurrencies.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function checkingIsActiveCurrencies(showError = true) {
|
||||
currenciesAlredayInUsed.value = []
|
||||
const { currencies } = exchangeRateStore.currentExchangeRate
|
||||
|
||||
if (currencies.length && exchangeRateStore.activeUsedCurrencies?.length) {
|
||||
currencies.forEach((curr) => {
|
||||
if (exchangeRateStore.activeUsedCurrencies.includes(curr)) {
|
||||
currenciesAlredayInUsed.value.push(curr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeExchangeRateModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
resetModalData()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
151
resources/scripts/components/modal-components/FileDiskModal.vue
Normal file
151
resources/scripts/components/modal-components/FileDiskModal.vue
Normal file
@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeDiskModal" @open="loadData">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeDiskModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="file-disk-modal">
|
||||
<component
|
||||
:is="diskStore.selected_driver"
|
||||
:loading="isLoading"
|
||||
:disks="diskStore.getDiskDrivers"
|
||||
:is-edit="isEdit"
|
||||
@onChangeDisk="(val) => diskChange(val)"
|
||||
@submit="createNewDisk"
|
||||
>
|
||||
<template #default="slotProps">
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-solid border-gray-light
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeDiskModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isRequestFire(slotProps)"
|
||||
:disabled="isRequestFire(slotProps)"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<BaseIcon
|
||||
v-if="!isRequestFire(slotProps)"
|
||||
name="SaveIcon"
|
||||
class="w-6 mr-2"
|
||||
/>
|
||||
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
</component>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
import Dropbox from './disks/DropboxDisk.vue'
|
||||
import Local from './disks/LocalDisk.vue'
|
||||
import S3 from './disks/S3Disk.vue'
|
||||
import DoSpaces from './disks/DoSpacesDisk.vue'
|
||||
export default {
|
||||
components: {
|
||||
Dropbox,
|
||||
Local,
|
||||
S3,
|
||||
DoSpaces,
|
||||
},
|
||||
setup() {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
let isLoading = ref(false)
|
||||
let isEdit = ref(false)
|
||||
|
||||
watchEffect(() => {
|
||||
if (modalStore.id) {
|
||||
isEdit.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'FileDiskModal'
|
||||
})
|
||||
|
||||
function isRequestFire(slotProps) {
|
||||
return (
|
||||
slotProps && (slotProps.diskData.isLoading.value || isLoading.value)
|
||||
)
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
let res = await diskStore.fetchDiskDrivers()
|
||||
if (isEdit.value) {
|
||||
diskStore.selected_driver = modalStore.data.driver
|
||||
} else {
|
||||
diskStore.selected_driver = res.data.drivers[0].value
|
||||
}
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
async function createNewDisk(data) {
|
||||
Object.assign(diskStore.diskConfigData, data)
|
||||
isLoading.value = true
|
||||
|
||||
let formData = {
|
||||
id: modalStore.id,
|
||||
...data,
|
||||
}
|
||||
|
||||
let response = null
|
||||
const action = isEdit.value ? diskStore.updateDisk : diskStore.createDisk
|
||||
response = await action(formData)
|
||||
isLoading.value = false
|
||||
modalStore.refreshData()
|
||||
closeDiskModal()
|
||||
}
|
||||
|
||||
function closeDiskModal() {
|
||||
modalStore.closeModal()
|
||||
}
|
||||
|
||||
function diskChange(value) {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.diskConfigData.selected_driver = value
|
||||
}
|
||||
|
||||
return {
|
||||
isEdit,
|
||||
createNewDisk,
|
||||
isRequestFire,
|
||||
diskStore,
|
||||
closeDiskModal,
|
||||
loadData,
|
||||
diskChange,
|
||||
modalStore,
|
||||
isLoading,
|
||||
modalActive,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
262
resources/scripts/components/modal-components/ItemModal.vue
Normal file
262
resources/scripts/components/modal-components/ItemModal.vue
Normal file
@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeItemModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeItemModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="item-modal">
|
||||
<form action="" @submit.prevent="submitItemData">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('items.name')"
|
||||
required
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="itemStore.currentItem.name"
|
||||
type="text"
|
||||
:invalid="v$.name.$error"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('items.price')">
|
||||
<BaseMoney
|
||||
:key="companyStore.selectedCompanyCurrency"
|
||||
v-model="price"
|
||||
:currency="companyStore.selectedCompanyCurrency"
|
||||
class="
|
||||
relative
|
||||
w-full
|
||||
focus:border focus:border-solid focus:border-primary
|
||||
"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('items.unit')">
|
||||
<BaseMultiselect
|
||||
v-model="itemStore.currentItem.unit_id"
|
||||
label="name"
|
||||
:options="itemStore.itemUnits"
|
||||
value-prop="id"
|
||||
:can-deselect="false"
|
||||
:can-clear="false"
|
||||
:placeholder="$t('items.select_a_unit')"
|
||||
searchable
|
||||
track-by="id"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="isTaxPerItemEnabled"
|
||||
:label="$t('items.taxes')"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="taxes"
|
||||
:options="getTaxTypes"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
class="w-full"
|
||||
:can-deselect="false"
|
||||
:can-clear="false"
|
||||
searchable
|
||||
track-by="id"
|
||||
object
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('items.description')"
|
||||
:error="
|
||||
v$.description.$error && v$.description.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="itemStore.currentItem.description"
|
||||
rows="4"
|
||||
cols="50"
|
||||
:invalid="v$.description.$error"
|
||||
@input="v$.description.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeItemModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ itemStore.isEdit ? $t('general.update') : $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
required,
|
||||
minLength,
|
||||
maxLength,
|
||||
minValue,
|
||||
helpers,
|
||||
alpha,
|
||||
} from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
import { useItemStore } from '@/scripts/stores/item'
|
||||
import { useTaxTypeStore } from '@/scripts/stores/tax-type'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useEstimateStore } from '@/scripts/stores/estimate'
|
||||
import { useInvoiceStore } from '@/scripts/stores/invoice'
|
||||
|
||||
const emit = defineEmits(['newItem'])
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const itemStore = useItemStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const isLoading = ref(false)
|
||||
const taxPerItemSetting = ref(companyStore.selectedCompanySettings.tax_per_item)
|
||||
|
||||
const modalActive = computed(
|
||||
() => modalStore.active && modalStore.componentName === 'ItemModal'
|
||||
)
|
||||
|
||||
const price = computed({
|
||||
get: () => itemStore.currentItem.price / 100,
|
||||
set: (value) => {
|
||||
itemStore.currentItem.price = Math.round(value * 100)
|
||||
},
|
||||
})
|
||||
|
||||
const taxes = computed({
|
||||
get: () =>
|
||||
itemStore.currentItem.taxes.map((tax) => {
|
||||
if (tax) {
|
||||
return {
|
||||
...tax,
|
||||
tax_type_id: tax.id,
|
||||
tax_name: tax.name + ' (' + tax.percent + '%)',
|
||||
}
|
||||
}
|
||||
}),
|
||||
set: (value) => {
|
||||
itemStore.$patch((state) => {
|
||||
state.currentItem.taxes = value
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const isTaxPerItemEnabled = computed(() => {
|
||||
return taxPerItemSetting.value === 'YES'
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
|
||||
description: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.description_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => itemStore.currentItem)
|
||||
)
|
||||
|
||||
const getTaxTypes = computed(() => {
|
||||
return taxTypeStore.taxTypes.map((tax) => {
|
||||
return { ...tax, tax_name: tax.name + ' (' + tax.percent + '%)' }
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
v$.value.$reset()
|
||||
itemStore.fetchItemUnits({ limit: 'all' })
|
||||
})
|
||||
|
||||
async function submitItemData() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = {
|
||||
...itemStore.currentItem,
|
||||
taxes: itemStore.currentItem.taxes.map((tax) => {
|
||||
return {
|
||||
tax_type_id: tax.id,
|
||||
amount: (price.value * tax.percent) / 100,
|
||||
percent: tax.percent,
|
||||
name: tax.name,
|
||||
collective_tax: 0,
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
const action = itemStore.isEdit ? itemStore.updateItem : itemStore.addItem
|
||||
|
||||
await action(data).then((res) => {
|
||||
isLoading.value = false
|
||||
if (res.data.data) {
|
||||
if (modalStore.data) {
|
||||
modalStore.refreshData(res.data.data)
|
||||
}
|
||||
}
|
||||
closeItemModal()
|
||||
})
|
||||
}
|
||||
|
||||
function closeItemModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
itemStore.resetCurrentItem()
|
||||
modalStore.$reset()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
143
resources/scripts/components/modal-components/ItemUnitModal.vue
Normal file
143
resources/scripts/components/modal-components/ItemUnitModal.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalStore.active && modalStore.componentName === 'ItemUnitModal'"
|
||||
@close="closeItemUnitModal"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeItemUnitModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form action="" @submit.prevent="submitItemUnit">
|
||||
<div class="p-8 sm:p-6">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.items.unit_name')"
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
variant="horizontal"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="itemStore.currentItemUnit.name"
|
||||
:invalid="v$.name.$error"
|
||||
type="text"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-gray-200 border-solid border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
type="button"
|
||||
variant="primary-outline"
|
||||
class="mr-3 text-sm"
|
||||
@click="closeItemUnitModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{
|
||||
itemStore.isItemUnitEdit ? $t('general.update') : $t('general.save')
|
||||
}}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useItemStore } from '@/scripts/stores/item'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { required, minLength, maxLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const itemStore = useItemStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
let isSaving = ref(false)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => itemStore.currentItemUnit)
|
||||
)
|
||||
|
||||
async function submitItemUnit() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const action = itemStore.isItemUnitEdit
|
||||
? itemStore.updateItemUnit
|
||||
: itemStore.addItemUnit
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
await action(itemStore.currentItemUnit)
|
||||
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
|
||||
closeItemUnitModal()
|
||||
isSaving.value = false
|
||||
} catch (err) {
|
||||
isSaving.value = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function closeItemUnitModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
itemStore.currentItemUnit = {
|
||||
id: null,
|
||||
name: '',
|
||||
}
|
||||
|
||||
modalStore.$reset()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
169
resources/scripts/components/modal-components/MailTestModal.vue
Normal file
169
resources/scripts/components/modal-components/MailTestModal.vue
Normal file
@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeTestModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeTestModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="onTestMailSend">
|
||||
<div class="p-4 md:p-8">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('general.to')"
|
||||
:error="v$.formData.to.$error && v$.formData.to.$errors[0].$message"
|
||||
variant="horizontal"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
ref="to"
|
||||
v-model="formData.to"
|
||||
type="text"
|
||||
:invalid="v$.formData.to.$error"
|
||||
@input="v$.formData.to.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.subject')"
|
||||
:error="
|
||||
v$.formData.subject.$error &&
|
||||
v$.formData.subject.$errors[0].$message
|
||||
"
|
||||
variant="horizontal"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="formData.subject"
|
||||
type="text"
|
||||
:invalid="v$.formData.subject.$error"
|
||||
@input="v$.formData.subject.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.message')"
|
||||
:error="
|
||||
v$.formData.message.$error &&
|
||||
v$.formData.message.$errors[0].$message
|
||||
"
|
||||
variant="horizontal"
|
||||
required
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="formData.message"
|
||||
rows="4"
|
||||
cols="50"
|
||||
:invalid="v$.formData.message.$error"
|
||||
@input="v$.formData.message.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
class="mr-3"
|
||||
@click="closeTestModal()"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton :loading="isSaving" variant="primary" type="submit">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="PaperAirplaneIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.send') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, email, maxLength, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useMailDriverStore } from '@/scripts/stores/mail-driver'
|
||||
|
||||
let isSaving = ref(false)
|
||||
let formData = reactive({
|
||||
to: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
})
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'MailTestModal'
|
||||
})
|
||||
|
||||
const rules = {
|
||||
formData: {
|
||||
to: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
subject: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.subject_maxlength'),
|
||||
maxLength(100)
|
||||
),
|
||||
},
|
||||
message: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.message_maxlength'),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(rules, { formData })
|
||||
|
||||
function resetFormData() {
|
||||
formData.id = ''
|
||||
formData.to = ''
|
||||
formData.subject = ''
|
||||
formData.message = ''
|
||||
|
||||
v$.value.$reset()
|
||||
}
|
||||
|
||||
async function onTestMailSend() {
|
||||
v$.value.formData.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
let response = await mailDriverStore.sendTestMail(formData)
|
||||
if (response.data) {
|
||||
closeTestModal()
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
function closeTestModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
modalStore.resetModalData()
|
||||
resetFormData()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
281
resources/scripts/components/modal-components/NoteModal.vue
Normal file
281
resources/scripts/components/modal-components/NoteModal.vue
Normal file
@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeNoteModal" @open="setFields">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeNoteModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="submitNote">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.notes.name')"
|
||||
variant="vertical"
|
||||
:error="
|
||||
v$.currentNote.name.$error &&
|
||||
v$.currentNote.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="noteStore.currentNote.name"
|
||||
:invalid="v$.currentNote.name.$error"
|
||||
type="text"
|
||||
@input="v$.currentNote.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.notes.type')"
|
||||
:error="
|
||||
v$.currentNote.type.$error &&
|
||||
v$.currentNote.type.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="noteStore.currentNote.type"
|
||||
:options="types"
|
||||
value-prop="type"
|
||||
class="mt-2"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.notes.notes')"
|
||||
:error="
|
||||
v$.currentNote.notes.$error &&
|
||||
v$.currentNote.notes.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="noteStore.currentNote.notes"
|
||||
:invalid="v$.currentNote.notes.$error"
|
||||
:fields="fields"
|
||||
@input="v$.currentNote.notes.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
px-4
|
||||
py-4
|
||||
border-t border-solid border-gray-light
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-2"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeNoteModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ noteStore.isEdit ? $t('general.update') : $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useNotesStore } from '@/scripts/stores/note'
|
||||
import { useInvoiceStore } from '@/scripts/stores/invoice'
|
||||
import { usePaymentStore } from '@/scripts/stores/payment'
|
||||
import { useEstimateStore } from '@/scripts/stores/estimate'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const noteStore = useNotesStore()
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const paymentStore = usePaymentStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
const types = reactive(['Invoice', 'Estimate', 'Payment'])
|
||||
let fields = ref(['customer', 'customerCustom'])
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'NoteModal'
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentNote: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
notes: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
type: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => noteStore)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => noteStore.currentNote.type,
|
||||
(val) => {
|
||||
setFields()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (route.name === 'estimates.create') {
|
||||
noteStore.currentNote.type = 'Estimate'
|
||||
} else if (route.name === 'invoices.create') {
|
||||
noteStore.currentNote.type = 'Invoice'
|
||||
} else {
|
||||
noteStore.currentNote.type = 'Payment'
|
||||
}
|
||||
})
|
||||
|
||||
function setFields() {
|
||||
fields.value = ['customer', 'customerCustom']
|
||||
|
||||
if (noteStore.currentNote.type == 'Invoice') {
|
||||
fields.value.push('invoice', 'invoiceCustom')
|
||||
}
|
||||
|
||||
if (noteStore.currentNote.type == 'Estimate') {
|
||||
fields.value.push('estimate', 'estimateCustom')
|
||||
}
|
||||
|
||||
if (noteStore.currentNote.type == 'Payment') {
|
||||
fields.value.push('payment', 'paymentCustom')
|
||||
}
|
||||
}
|
||||
|
||||
async function submitNote() {
|
||||
v$.value.currentNote.$touch()
|
||||
if (v$.value.currentNote.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
if (noteStore.isEdit) {
|
||||
let data = {
|
||||
id: noteStore.currentNote.id,
|
||||
...noteStore.currentNote,
|
||||
}
|
||||
await noteStore
|
||||
.updateNote(data)
|
||||
.then((res) => {
|
||||
isSaving.value = false
|
||||
if (res.data) {
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('settings.customization.notes.note_updated'),
|
||||
})
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closeNoteModal()
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
isSaving.value = false
|
||||
})
|
||||
} else {
|
||||
await noteStore
|
||||
.addNote(noteStore.currentNote)
|
||||
.then((res) => {
|
||||
isSaving.value = false
|
||||
if (res.data) {
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('settings.customization.notes.note_added'),
|
||||
})
|
||||
|
||||
if (
|
||||
(route.name === 'invoices.create' &&
|
||||
res.data.data.type === 'Invoice') ||
|
||||
(route.name === 'invoices.edit' && res.data.data.type === 'Invoice')
|
||||
) {
|
||||
invoiceStore.selectNote(res.data.data)
|
||||
}
|
||||
|
||||
if (
|
||||
(route.name === 'estimates.create' &&
|
||||
res.data.data.type === 'Estimate') ||
|
||||
(route.name === 'estimates.edit' &&
|
||||
res.data.data.type === 'Estimate')
|
||||
) {
|
||||
estimateStore.selectNote(res.data.data)
|
||||
}
|
||||
|
||||
if (
|
||||
(route.name === 'payments.create' &&
|
||||
res.data.data.type === 'Payment') ||
|
||||
(route.name === 'payments.edit' && res.data.data.type === 'Payment')
|
||||
) {
|
||||
paymentStore.selectNote(res.data.data)
|
||||
}
|
||||
}
|
||||
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closeNoteModal()
|
||||
})
|
||||
.catch((err) => {
|
||||
isSaving.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeNoteModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
noteStore.resetCurrentNote()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.note-modal {
|
||||
.header-editior .editor-menu-bar {
|
||||
margin-left: 0.5px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closePaymentModeModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closePaymentModeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form action="" @submit.prevent="submitPaymentMode">
|
||||
<div class="p-4 sm:p-6">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.payment_modes.mode_name')"
|
||||
:error="
|
||||
v$.currentPaymentMode.name.$error &&
|
||||
v$.currentPaymentMode.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="paymentStore.currentPaymentMode.name"
|
||||
:invalid="v$.currentPaymentMode.name.$error"
|
||||
@input="v$.currentPaymentMode.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
variant="primary-outline"
|
||||
class="mr-3"
|
||||
type="button"
|
||||
@click="closePaymentModeModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{
|
||||
paymentStore.currentPaymentMode.id
|
||||
? $t('general.update')
|
||||
: $t('general.save')
|
||||
}}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { usePaymentStore } from '@/scripts/stores/payment'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const paymentStore = usePaymentStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const isSaving = ref(false)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentPaymentMode: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => paymentStore)
|
||||
)
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'PaymentModeModal'
|
||||
})
|
||||
|
||||
async function submitPaymentMode() {
|
||||
v$.value.currentPaymentMode.$touch()
|
||||
|
||||
if (v$.value.currentPaymentMode.$invalid) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const action = paymentStore.currentPaymentMode.id
|
||||
? paymentStore.updatePaymentMode
|
||||
: paymentStore.addPaymentMode
|
||||
isSaving.value = true
|
||||
await action(paymentStore.currentPaymentMode)
|
||||
isSaving.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closePaymentModeModal()
|
||||
} catch (err) {
|
||||
isSaving.value = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function closePaymentModeModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
paymentStore.currentPaymentMode = {
|
||||
id: '',
|
||||
name: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
299
resources/scripts/components/modal-components/RolesModal.vue
Normal file
299
resources/scripts/components/modal-components/RolesModal.vue
Normal file
@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeRolesModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeRolesModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form @submit.prevent="submitRoleData">
|
||||
<div class="px-4 md:px-8 py-4 md:py-6">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.roles.name')"
|
||||
class="mt-3"
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
required
|
||||
:content-loading="isFetchingInitialData"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="roleStore.currentRole.name"
|
||||
:invalid="v$.name.$error"
|
||||
type="text"
|
||||
:content-loading="isFetchingInitialData"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<h6
|
||||
class="
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
text-primary-800
|
||||
px-4
|
||||
md:px-8
|
||||
py-1.5
|
||||
"
|
||||
>
|
||||
{{ $tc('settings.roles.permission', 2) }}
|
||||
<span class="text-sm text-red-500"> *</span>
|
||||
</h6>
|
||||
<div
|
||||
class="
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
text-gray-300
|
||||
px-4
|
||||
md:px-8
|
||||
py-1.5
|
||||
"
|
||||
>
|
||||
<a
|
||||
class="cursor-pointer text-primary-400"
|
||||
@click="setSelectAll(true)"
|
||||
>
|
||||
{{ $t('settings.roles.select_all') }}
|
||||
</a>
|
||||
/
|
||||
<a
|
||||
class="cursor-pointer text-primary-400"
|
||||
@click="setSelectAll(false)"
|
||||
>
|
||||
{{ $t('settings.roles.none') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 py-3">
|
||||
<div
|
||||
class="
|
||||
grid grid-cols-1
|
||||
sm:grid-cols-2
|
||||
md:grid-cols-3
|
||||
lg:grid-cols-4
|
||||
gap-4
|
||||
px-8
|
||||
sm:px-8
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(abilityGroup, gIndex) in roleStore.abilitiesList"
|
||||
:key="gIndex"
|
||||
class="flex flex-col space-y-1"
|
||||
>
|
||||
<p class="text-sm text-gray-500 border-b border-gray-200 pb-1 mb-2">
|
||||
{{ gIndex }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(ability, index) in abilityGroup"
|
||||
:key="index"
|
||||
class="flex"
|
||||
>
|
||||
<BaseCheckbox
|
||||
v-model="roleStore.currentRole.abilities"
|
||||
:set-initial-value="true"
|
||||
variant="primary"
|
||||
:disabled="ability.disabled"
|
||||
:label="ability.name"
|
||||
:value="ability"
|
||||
@update:modelValue="onUpdateAbility(ability)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="v$.abilities.$error"
|
||||
class="block mt-0.5 text-sm text-red-500"
|
||||
>
|
||||
{{ v$.abilities.$errors[0].$message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-solid border--200 border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeRolesModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ !roleStore.isEdit ? $t('general.save') : $t('general.update') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useRoleStore } from '@/scripts/stores/role'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const roleStore = useRoleStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
let isFetchingInitialData = ref(false)
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'RolesModal'
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
abilities: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.at_least_one_ability'),
|
||||
required
|
||||
),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => roleStore.currentRole)
|
||||
)
|
||||
|
||||
async function submitRoleData() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const action = roleStore.isEdit ? roleStore.updateRole : roleStore.addRole
|
||||
isSaving.value = true
|
||||
await action(roleStore.currentRole)
|
||||
isSaving.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closeRolesModal()
|
||||
} catch (error) {
|
||||
isSaving.value = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function onUpdateAbility(currentAbility) {
|
||||
const fd = roleStore.currentRole.abilities.find(
|
||||
(_abl) => _abl.ability === currentAbility.ability
|
||||
)
|
||||
|
||||
if (!fd && currentAbility?.depends_on?.length) {
|
||||
enableAbilities(currentAbility)
|
||||
return
|
||||
}
|
||||
|
||||
currentAbility?.depends_on?.forEach((_d) => {
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
if (_d === _a.ability) {
|
||||
_a.disabled = true
|
||||
|
||||
let found = roleStore.currentRole.abilities.find(
|
||||
(_af) => _af.ability === _d
|
||||
)
|
||||
|
||||
if (!found) {
|
||||
roleStore.currentRole.abilities.push(_a)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function setSelectAll(checked) {
|
||||
let dependList = []
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
_a?.depends_on && (dependList = [...dependList, ..._a.depends_on])
|
||||
})
|
||||
})
|
||||
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
if (dependList.includes(_a.ability)) {
|
||||
checked ? (_a.disabled = true) : (_a.disabled = false)
|
||||
}
|
||||
roleStore.currentRole.abilities.push(_a)
|
||||
})
|
||||
})
|
||||
|
||||
if (!checked) roleStore.currentRole.abilities = []
|
||||
}
|
||||
|
||||
function enableAbilities(ability) {
|
||||
ability.depends_on.forEach((_d) => {
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
// CHECK IF EXISTS IN CURRENT ROLE ABILITIES
|
||||
let found = roleStore.currentRole.abilities.find((_r) =>
|
||||
_r.depends_on?.includes(_a.ability)
|
||||
)
|
||||
|
||||
if (_d === _a.ability && !found) {
|
||||
_a.disabled = false
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function closeRolesModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
roleStore.currentRole = {
|
||||
id: null,
|
||||
name: '',
|
||||
abilities: [],
|
||||
}
|
||||
|
||||
// Enable all disabled ability
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
_a.disabled = false
|
||||
})
|
||||
})
|
||||
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeModal" @open="setData">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalTitle }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<div
|
||||
v-if="modalStore.data"
|
||||
class="grid grid-cols-3 gap-2 p-1 overflow-x-auto"
|
||||
>
|
||||
<div
|
||||
v-for="(template, index) in modalStore.data.templates"
|
||||
:key="index"
|
||||
:class="{
|
||||
'border border-solid border-primary-500':
|
||||
selectedTemplate === template.name,
|
||||
}"
|
||||
class="
|
||||
relative
|
||||
flex flex-col
|
||||
m-2
|
||||
border border-gray-200 border-solid
|
||||
cursor-pointer
|
||||
hover:border-primary-300
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="template.path"
|
||||
:alt="template.name"
|
||||
class="w-full"
|
||||
@click="selectedTemplate = template.name"
|
||||
/>
|
||||
<img
|
||||
v-if="selectedTemplate === template.name"
|
||||
:alt="template.name"
|
||||
class="absolute z-10 w-5 h-5 text-primary-500"
|
||||
style="top: -6px; right: -5px"
|
||||
src="/img/tick.png"
|
||||
/>
|
||||
<span
|
||||
:class="[
|
||||
'w-full p-1 bg-gray-200 text-sm text-center absolute bottom-0 left-0',
|
||||
{
|
||||
'text-primary-500 bg-primary-100':
|
||||
selectedTemplate === template.name,
|
||||
'text-gray-600': selectedTemplate != template.name,
|
||||
},
|
||||
]"
|
||||
>
|
||||
{{ template.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid">
|
||||
<BaseButton class="mr-3" variant="primary-outline" @click="closeModal">
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton variant="primary" @click="chooseTemplate()">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $t('general.choose') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const selectedTemplate = ref('')
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'SelectTemplate'
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return modalStore.title
|
||||
})
|
||||
|
||||
function setData() {
|
||||
if (modalStore.data.store[modalStore.data.storeProp].template_name) {
|
||||
selectedTemplate.value =
|
||||
modalStore.data.store[modalStore.data.storeProp].template_name
|
||||
} else {
|
||||
selectedTemplate.value = modalStore.data.templates[0]
|
||||
}
|
||||
}
|
||||
|
||||
async function chooseTemplate() {
|
||||
await modalStore.data.store.setTemplate(selectedTemplate.value)
|
||||
closeModal()
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
modalStore.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeSendEstimateModal"
|
||||
@open="setInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeSendEstimateModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form v-if="!isPreview" action="">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('general.from')"
|
||||
required
|
||||
:error="v$.from.$error && v$.from.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="estimateMailForm.from"
|
||||
type="text"
|
||||
:invalid="v$.from.$error"
|
||||
@input="v$.from.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.to')"
|
||||
required
|
||||
:error="v$.to.$error && v$.to.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="estimateMailForm.to"
|
||||
type="text"
|
||||
:invalid="v$.to.$error"
|
||||
@input="v$.to.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.subject')"
|
||||
required
|
||||
:error="v$.subject.$error && v$.subject.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="estimateMailForm.subject"
|
||||
type="text"
|
||||
:invalid="v$.subject.$error"
|
||||
@input="v$.subject.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup :label="$t('general.body')" required>
|
||||
<BaseCustomInput
|
||||
v-model="estimateMailForm.body"
|
||||
:fields="estimateMailFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendEstimateModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
class="mr-3"
|
||||
@click="submitForm"
|
||||
>
|
||||
<BaseIcon v-if="!isLoading" name="PhotographIcon" class="h-5 mr-2" />
|
||||
{{ $t('general.preview') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
<div class="my-6 mx-4 border border-gray-200 relative">
|
||||
<BaseButton
|
||||
class="absolute top-4 right-4"
|
||||
:disabled="isLoading"
|
||||
variant="primary-outline"
|
||||
@click="cancelPreview"
|
||||
>
|
||||
<BaseIcon name="PencilIcon" class="h-5 mr-2" />
|
||||
Edit
|
||||
</BaseButton>
|
||||
<iframe
|
||||
:src="templateUrl"
|
||||
frameborder="0"
|
||||
class="w-full"
|
||||
style="min-height: 500px"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendEstimateModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
@click="submitForm"
|
||||
>
|
||||
<BaseIcon v-if="!isLoading" name="PaperAirplaneIcon" class="mr-2" />
|
||||
{{ $t('general.send') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watchEffect, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useEstimateStore } from '@/scripts/stores/estimate'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
import { useMailDriverStore } from '@/scripts/stores/mail-driver'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const isLoading = ref(false)
|
||||
const templateUrl = ref('')
|
||||
const isPreview = ref(false)
|
||||
|
||||
const estimateMailFields = ref([
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'estimate',
|
||||
'estimateCustom',
|
||||
'company',
|
||||
])
|
||||
|
||||
let estimateMailForm = reactive({
|
||||
id: null,
|
||||
from: null,
|
||||
to: null,
|
||||
subject: 'New Estimate',
|
||||
body: null,
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'SendEstimateModal'
|
||||
})
|
||||
|
||||
const modalData = computed(() => {
|
||||
return modalStore.data
|
||||
})
|
||||
|
||||
const rules = {
|
||||
from: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
to: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
subject: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
body: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => estimateMailForm)
|
||||
)
|
||||
|
||||
function cancelPreview() {
|
||||
isPreview.value = false
|
||||
}
|
||||
|
||||
async function setInitialData() {
|
||||
let admin = await companyStore.fetchBasicMailConfig()
|
||||
|
||||
estimateMailForm.id = modalStore.id
|
||||
|
||||
if (admin.data) {
|
||||
estimateMailForm.from = admin.data.from_mail
|
||||
}
|
||||
|
||||
if (modalData.value) {
|
||||
estimateMailForm.to = modalData.value.customer.email
|
||||
}
|
||||
|
||||
estimateMailForm.body =
|
||||
companyStore.selectedCompanySettings.estimate_mail_body
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
if (!isPreview.value) {
|
||||
const previewResponse = await estimateStore.previewEstimate(
|
||||
estimateMailForm
|
||||
)
|
||||
isLoading.value = false
|
||||
|
||||
isPreview.value = true
|
||||
var blob = new Blob([previewResponse.data], { type: 'text/html' })
|
||||
templateUrl.value = URL.createObjectURL(blob)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const response = await estimateStore.sendEstimate(estimateMailForm)
|
||||
|
||||
isLoading.value = false
|
||||
|
||||
if (response.data.success) {
|
||||
closeSendEstimateModal()
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
isLoading.value = false
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: t('estimates.something_went_wrong'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeSendEstimateModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
isPreview.value = false
|
||||
templateUrl.value = null
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeSendInvoiceModal"
|
||||
@open="setInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalTitle }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeSendInvoiceModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form v-if="!isPreview" action="">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column" class="col-span-7">
|
||||
<BaseInputGroup
|
||||
:label="$t('general.from')"
|
||||
required
|
||||
:error="v$.from.$error && v$.from.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="invoiceMailForm.from"
|
||||
type="text"
|
||||
:invalid="v$.from.$error"
|
||||
@input="v$.from.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.to')"
|
||||
required
|
||||
:error="v$.to.$error && v$.to.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="invoiceMailForm.to"
|
||||
type="text"
|
||||
:invalid="v$.to.$error"
|
||||
@input="v$.to.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:error="v$.subject.$error && v$.subject.$errors[0].$message"
|
||||
:label="$t('general.subject')"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="invoiceMailForm.subject"
|
||||
type="text"
|
||||
:invalid="v$.subject.$error"
|
||||
@input="v$.subject.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.body')"
|
||||
:error="v$.body.$error && v$.body.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="invoiceMailForm.body"
|
||||
:fields="invoiceMailFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendInvoiceModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
class="mr-3"
|
||||
@click="submitForm"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
:class="slotProps.class"
|
||||
name="PhotographIcon"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.preview') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
<div class="my-6 mx-4 border border-gray-200 relative">
|
||||
<BaseButton
|
||||
class="absolute top-4 right-4"
|
||||
:disabled="isLoading"
|
||||
variant="primary-outline"
|
||||
@click="cancelPreview"
|
||||
>
|
||||
<BaseIcon name="PencilIcon" class="h-5 mr-2" />
|
||||
Edit
|
||||
</BaseButton>
|
||||
|
||||
<iframe
|
||||
:src="templateUrl"
|
||||
frameborder="0"
|
||||
class="w-full"
|
||||
style="min-height: 500px"
|
||||
></iframe>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendInvoiceModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
@click="submitForm()"
|
||||
>
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
name="PaperAirplaneIcon"
|
||||
class="h-5 mr-2"
|
||||
/>
|
||||
{{ $t('general.send') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive, onMounted } from 'vue'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useInvoiceStore } from '@/scripts/stores/invoice'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import { useMailDriverStore } from '@/scripts/stores/mail-driver'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
let isLoading = ref(false)
|
||||
const templateUrl = ref('')
|
||||
const isPreview = ref(false)
|
||||
|
||||
const invoiceMailFields = ref([
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'invoice',
|
||||
'invoiceCustom',
|
||||
'company',
|
||||
])
|
||||
|
||||
const invoiceMailForm = reactive({
|
||||
id: null,
|
||||
from: null,
|
||||
to: null,
|
||||
subject: 'New Invoice',
|
||||
body: null,
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'SendInvoiceModal'
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return modalStore.title
|
||||
})
|
||||
|
||||
const modalData = computed(() => {
|
||||
return modalStore.data
|
||||
})
|
||||
|
||||
const rules = {
|
||||
from: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
to: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
subject: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
body: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => invoiceMailForm)
|
||||
)
|
||||
|
||||
function cancelPreview() {
|
||||
isPreview.value = false
|
||||
}
|
||||
|
||||
async function setInitialData() {
|
||||
let admin = await companyStore.fetchBasicMailConfig()
|
||||
|
||||
invoiceMailForm.id = modalStore.id
|
||||
|
||||
if (admin.data) {
|
||||
invoiceMailForm.from = admin.data.from_mail
|
||||
}
|
||||
|
||||
if (modalData.value) {
|
||||
invoiceMailForm.to = modalData.value.customer.email
|
||||
}
|
||||
|
||||
invoiceMailForm.body = companyStore.selectedCompanySettings.invoice_mail_body
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
if (!isPreview.value) {
|
||||
const previewResponse = await invoiceStore.previewInvoice(invoiceMailForm)
|
||||
isLoading.value = false
|
||||
|
||||
isPreview.value = true
|
||||
var blob = new Blob([previewResponse.data], { type: 'text/html' })
|
||||
templateUrl.value = URL.createObjectURL(blob)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const response = await invoiceStore.sendInvoice(invoiceMailForm)
|
||||
|
||||
if (response.data.success) {
|
||||
closeSendInvoiceModal()
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
isLoading.value = false
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: t('invoices.something_went_wrong'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeSendInvoiceModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
isPreview.value = false
|
||||
templateUrl.value = null
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeSendPaymentModal"
|
||||
@open="setInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalTitle }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeSendPaymentModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form v-if="!isPreview" action="">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column" class="col-span-7">
|
||||
<BaseInputGroup
|
||||
:label="$t('general.from')"
|
||||
required
|
||||
:error="v$.from.$error && v$.from.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="paymentMailForm.from"
|
||||
type="text"
|
||||
:invalid="v$.from.$error"
|
||||
@input="v$.from.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.to')"
|
||||
required
|
||||
:error="v$.to.$error && v$.to.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="paymentMailForm.to"
|
||||
type="text"
|
||||
:invalid="v$.to.$error"
|
||||
@input="v$.to.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:error="v$.subject.$error && v$.subject.$errors[0].$message"
|
||||
:label="$t('general.subject')"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="paymentMailForm.subject"
|
||||
type="text"
|
||||
:invalid="v$.subject.$error"
|
||||
@input="v$.subject.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.body')"
|
||||
:error="v$.body.$error && v$.body.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="paymentMailForm.body"
|
||||
:fields="paymentMailFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendPaymentModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
class="mr-3"
|
||||
@click="sendPaymentData"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
:class="slotProps.class"
|
||||
name="PhotographIcon"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.preview') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
<div class="my-6 mx-4 border border-gray-200 relative">
|
||||
<BaseButton
|
||||
class="absolute top-4 right-4"
|
||||
:disabled="isLoading"
|
||||
variant="primary-outline"
|
||||
@click="cancelPreview"
|
||||
>
|
||||
<BaseIcon name="PencilIcon" class="h-5 mr-2" />
|
||||
Edit
|
||||
</BaseButton>
|
||||
|
||||
<iframe
|
||||
:src="templateUrl"
|
||||
frameborder="0"
|
||||
class="w-full"
|
||||
style="min-height: 500px"
|
||||
></iframe>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendPaymentModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
@click="sendPaymentData()"
|
||||
>
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
name="PaperAirplaneIcon"
|
||||
class="h-5 mr-2"
|
||||
/>
|
||||
{{ $t('general.send') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { ref, reactive, computed, watch, watchEffect } from 'vue'
|
||||
import { usePaymentStore } from '@/scripts/stores/payment'
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useMailDriverStore } from '@/scripts/stores/mail-driver'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
|
||||
const paymentStore = usePaymentStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const mailDriversStore = useMailDriverStore()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
let isLoading = ref(false)
|
||||
const templateUrl = ref('')
|
||||
const isPreview = ref(false)
|
||||
|
||||
const paymentMailFields = ref([
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'payments',
|
||||
'paymentsCustom',
|
||||
'company',
|
||||
])
|
||||
|
||||
const paymentMailForm = reactive({
|
||||
id: null,
|
||||
from: null,
|
||||
to: null,
|
||||
subject: 'New Payment',
|
||||
body: null,
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'SendPaymentModal'
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return modalStore.title
|
||||
})
|
||||
|
||||
const modalData = computed(() => {
|
||||
return modalStore.data
|
||||
})
|
||||
|
||||
const rules = {
|
||||
from: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
to: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
subject: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
body: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(rules, paymentMailForm)
|
||||
|
||||
function cancelPreview() {
|
||||
isPreview.value = false
|
||||
}
|
||||
|
||||
async function setInitialData() {
|
||||
let admin = await companyStore.fetchBasicMailConfig()
|
||||
paymentMailForm.id = modalStore.id
|
||||
|
||||
if (admin.data) {
|
||||
paymentMailForm.from = admin.data.from_mail
|
||||
}
|
||||
|
||||
if (modalData.value) {
|
||||
paymentMailForm.to = modalData.value.customer.email
|
||||
}
|
||||
|
||||
paymentMailForm.body = companyStore.selectedCompanySettings.payment_mail_body
|
||||
}
|
||||
|
||||
async function sendPaymentData() {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
if (!isPreview.value) {
|
||||
const previewResponse = await paymentStore.previewPayment(paymentMailForm)
|
||||
isLoading.value = false
|
||||
|
||||
isPreview.value = true
|
||||
var blob = new Blob([previewResponse.data], { type: 'text/html' })
|
||||
templateUrl.value = URL.createObjectURL(blob)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const response = await paymentStore.sendEmail(paymentMailForm)
|
||||
|
||||
if (response.data.success) {
|
||||
closeSendPaymentModal()
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
isLoading.value = false
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: t('payments.something_went_wrong'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeSendPaymentModal() {
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
isPreview.value = false
|
||||
templateUrl.value = null
|
||||
modalStore.resetModalData()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
259
resources/scripts/components/modal-components/TaxTypeModal.vue
Normal file
259
resources/scripts/components/modal-components/TaxTypeModal.vue
Normal file
@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalStore.active && modalStore.componentName === 'TaxTypeModal'"
|
||||
@close="closeTaxTypeModal"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeTaxTypeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="submitTaxTypeData">
|
||||
<div class="p-4 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('tax_types.name')"
|
||||
variant="horizontal"
|
||||
:error="
|
||||
v$.currentTaxType.name.$error &&
|
||||
v$.currentTaxType.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="taxTypeStore.currentTaxType.name"
|
||||
:invalid="v$.currentTaxType.name.$error"
|
||||
type="text"
|
||||
@input="v$.currentTaxType.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('tax_types.percent')"
|
||||
variant="horizontal"
|
||||
:error="
|
||||
v$.currentTaxType.percent.$error &&
|
||||
v$.currentTaxType.percent.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMoney
|
||||
v-model="taxTypeStore.currentTaxType.percent"
|
||||
:currency="{
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
symbol: '% ',
|
||||
precision: 2,
|
||||
masked: false,
|
||||
}"
|
||||
:invalid="v$.currentTaxType.percent.$error"
|
||||
class="
|
||||
relative
|
||||
w-full
|
||||
focus:border focus:border-solid focus:border-primary
|
||||
"
|
||||
@input="v$.currentTaxType.percent.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('tax_types.description')"
|
||||
:error="
|
||||
v$.currentTaxType.description.$error &&
|
||||
v$.currentTaxType.description.$errors[0].$message
|
||||
"
|
||||
variant="horizontal"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="taxTypeStore.currentTaxType.description"
|
||||
:invalid="v$.currentTaxType.description.$error"
|
||||
rows="4"
|
||||
cols="50"
|
||||
@input="v$.currentTaxType.description.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('tax_types.compound_tax')"
|
||||
variant="horizontal"
|
||||
class="flex flex-row-reverse"
|
||||
>
|
||||
<BaseSwitch
|
||||
v-model="taxTypeStore.currentTaxType.compound_tax"
|
||||
class="flex items-center"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-solid border--200 border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeTaxTypeModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ taxTypeStore.isEdit ? $t('general.update') : $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTaxTypeStore } from '@/scripts/stores/tax-type'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useEstimateStore } from '@/scripts/stores/estimate'
|
||||
import { useInvoiceStore } from '@/scripts/stores/invoice'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Guid from 'guid'
|
||||
import TaxStub from '@/scripts/stub/tax'
|
||||
import {
|
||||
required,
|
||||
minLength,
|
||||
maxLength,
|
||||
between,
|
||||
helpers,
|
||||
} from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
|
||||
const { t, tm } = useI18n()
|
||||
let isSaving = ref(false)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentTaxType: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
percent: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
between: helpers.withMessage(
|
||||
t('validation.enter_valid_tax_rate'),
|
||||
between(0, 100)
|
||||
),
|
||||
},
|
||||
description: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.description_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => taxTypeStore)
|
||||
)
|
||||
|
||||
async function submitTaxTypeData() {
|
||||
v$.value.currentTaxType.$touch()
|
||||
if (v$.value.currentTaxType.$invalid) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const action = taxTypeStore.isEdit
|
||||
? taxTypeStore.updateTaxType
|
||||
: taxTypeStore.addTaxType
|
||||
isSaving.value = true
|
||||
let res = await action(taxTypeStore.currentTaxType)
|
||||
isSaving.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData(res.data.data) : ''
|
||||
closeTaxTypeModal()
|
||||
} catch (err) {
|
||||
isSaving.value = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function SelectTax(taxData) {
|
||||
let amount = 0
|
||||
if (taxData.compound_tax && estimateStore.getSubtotalWithDiscount) {
|
||||
amount = Math.round(
|
||||
((estimateStore.getSubtotalWithDiscount +
|
||||
estimateStore.getTotalSimpleTax) *
|
||||
taxData.percent) /
|
||||
100
|
||||
)
|
||||
} else if (estimateStore.getSubtotalWithDiscount && taxData.percent) {
|
||||
amount = Math.round(
|
||||
(estimateStore.getSubtotalWithDiscount * taxData.percent) / 100
|
||||
)
|
||||
}
|
||||
let data = {
|
||||
...TaxStub,
|
||||
id: Guid.raw(),
|
||||
name: taxData.name,
|
||||
percent: taxData.percent,
|
||||
compound_tax: taxData.compound_tax,
|
||||
tax_type_id: taxData.id,
|
||||
amount,
|
||||
}
|
||||
estimateStore.$patch((state) => {
|
||||
state.newEstimate.taxes.push({ ...data })
|
||||
})
|
||||
}
|
||||
|
||||
function selectItemTax(taxData) {
|
||||
if (modalStore.data) {
|
||||
let data = {
|
||||
...TaxStub,
|
||||
id: Guid.raw(),
|
||||
name: taxData.name,
|
||||
percent: taxData.percent,
|
||||
compound_tax: taxData.compound_tax,
|
||||
tax_type_id: taxData.id,
|
||||
}
|
||||
modalStore.refreshData(data)
|
||||
}
|
||||
}
|
||||
|
||||
function closeTaxTypeModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
taxTypeStore.resetCurrentTaxType()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,422 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @open="setData">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeCustomFieldModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form action="" @submit.prevent="submitCustomFieldData">
|
||||
<div class="overflow-y-auto max-h-[550px]">
|
||||
<div class="px-4 md:px-8 py-8 overflow-y-auto sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.name')"
|
||||
required
|
||||
:error="
|
||||
v$.currentCustomField.name.$error &&
|
||||
v$.currentCustomField.name.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
ref="name"
|
||||
v-model="customFieldStore.currentCustomField.name"
|
||||
:invalid="v$.currentCustomField.name.$error"
|
||||
@input="v$.currentCustomField.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.model')"
|
||||
:error="
|
||||
v$.currentCustomField.model_type.$error &&
|
||||
v$.currentCustomField.model_type.$errors[0].$message
|
||||
"
|
||||
:help-text="
|
||||
customFieldStore.currentCustomField.in_use
|
||||
? $t('settings.custom_fields.model_in_use')
|
||||
: ''
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="customFieldStore.currentCustomField.model_type"
|
||||
:options="modelTypes"
|
||||
:can-deselect="false"
|
||||
:invalid="v$.currentCustomField.model_type.$error"
|
||||
:searchable="true"
|
||||
:disabled="customFieldStore.currentCustomField.in_use"
|
||||
@input="v$.currentCustomField.model_type.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
class="flex items-center space-x-4"
|
||||
:label="$t('settings.custom_fields.required')"
|
||||
>
|
||||
<BaseSwitch v-model="isRequiredField" />
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.type')"
|
||||
:error="
|
||||
v$.currentCustomField.type.$error &&
|
||||
v$.currentCustomField.type.$errors[0].$message
|
||||
"
|
||||
:help-text="
|
||||
customFieldStore.currentCustomField.in_use
|
||||
? $t('settings.custom_fields.type_in_use')
|
||||
: ''
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selectedType"
|
||||
:options="dataTypes"
|
||||
:invalid="v$.currentCustomField.type.$error"
|
||||
:disabled="customFieldStore.currentCustomField.in_use"
|
||||
:searchable="true"
|
||||
:can-deselect="false"
|
||||
object
|
||||
@update:modelValue="onSelectedTypeChange"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.label')"
|
||||
required
|
||||
:error="
|
||||
v$.currentCustomField.label.$error &&
|
||||
v$.currentCustomField.label.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customFieldStore.currentCustomField.label"
|
||||
:invalid="v$.currentCustomField.label.$error"
|
||||
@input="v$.currentCustomField.label.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="isDropdownSelected"
|
||||
:label="$t('settings.custom_fields.options')"
|
||||
>
|
||||
<OptionCreate @onAdd="addNewOption" />
|
||||
|
||||
<div
|
||||
v-for="(option, index) in customFieldStore.currentCustomField
|
||||
.options"
|
||||
:key="index"
|
||||
class="flex items-center mt-5"
|
||||
>
|
||||
<BaseInput v-model="option.name" class="w-64" />
|
||||
|
||||
<BaseIcon
|
||||
name="MinusCircleIcon"
|
||||
class="ml-1 cursor-pointer"
|
||||
:class="
|
||||
customFieldStore.currentCustomField.in_use
|
||||
? 'text-gray-300'
|
||||
: 'text-red-300'
|
||||
"
|
||||
@click="removeOption(index)"
|
||||
/>
|
||||
</div>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.default_value')"
|
||||
class="relative"
|
||||
>
|
||||
<component
|
||||
:is="defaultValueComponent"
|
||||
v-model="customFieldStore.currentCustomField.default_answer"
|
||||
:options="customFieldStore.currentCustomField.options"
|
||||
:default-date-time="
|
||||
customFieldStore.currentCustomField.dateTimeValue
|
||||
"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="!isSwitchTypeSelected"
|
||||
:label="$t('settings.custom_fields.placeholder')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customFieldStore.currentCustomField.placeholder"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.order')"
|
||||
:error="
|
||||
v$.currentCustomField.order.$error &&
|
||||
v$.currentCustomField.order.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customFieldStore.currentCustomField.order"
|
||||
type="number"
|
||||
:invalid="v$.currentCustomField.order.$error"
|
||||
@input="v$.currentCustomField.order.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-solid border-gray-light border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
type="button"
|
||||
variant="primary-outline"
|
||||
@click="closeCustomFieldModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
variant="primary"
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
:class="slotProps.class"
|
||||
name="SaveIcon"
|
||||
/>
|
||||
</template>
|
||||
{{
|
||||
!customFieldStore.isEdit ? $t('general.save') : $t('general.update')
|
||||
}}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, computed, defineAsyncComponent } from 'vue'
|
||||
import OptionCreate from './OptionsCreate.vue'
|
||||
import moment from 'moment'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, numeric, helpers } from '@vuelidate/validators'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useCustomFieldStore } from '@/scripts/stores/custom-field'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const customFieldStore = useCustomFieldStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
|
||||
const modelTypes = reactive([
|
||||
'Customer',
|
||||
'Invoice',
|
||||
'Estimate',
|
||||
'Expense',
|
||||
'Payment',
|
||||
])
|
||||
|
||||
const dataTypes = reactive([
|
||||
{ label: 'Text', value: 'Input' },
|
||||
{ label: 'Textarea', value: 'TextArea' },
|
||||
{ label: 'Phone', value: 'Phone' },
|
||||
{ label: 'URL', value: 'Url' },
|
||||
{ label: 'Number', value: 'Number' },
|
||||
{ label: 'Select Field', value: 'Dropdown' },
|
||||
{ label: 'Switch Toggle', value: 'Switch' },
|
||||
{ label: 'Date', value: 'Date' },
|
||||
{ label: 'Time', value: 'Time' },
|
||||
{ label: 'Date & Time', value: 'DateTime' },
|
||||
])
|
||||
|
||||
let selectedType = ref(dataTypes[0])
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'CustomFieldModal'
|
||||
})
|
||||
|
||||
const isSwitchTypeSelected = computed(
|
||||
() => selectedType.value && selectedType.value.label === 'Switch Toggle'
|
||||
)
|
||||
|
||||
const isDropdownSelected = computed(
|
||||
() => selectedType.value && selectedType.value.label === 'Select Field'
|
||||
)
|
||||
|
||||
const defaultValueComponent = computed(() => {
|
||||
if (customFieldStore.currentCustomField.type) {
|
||||
return defineAsyncComponent(() =>
|
||||
import(
|
||||
`../../custom-fields/types/${customFieldStore.currentCustomField.type}Type.vue`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
const isRequiredField = computed({
|
||||
get: () => customFieldStore.currentCustomField.is_required === 1,
|
||||
set: (value) => {
|
||||
const intVal = value ? 1 : 0
|
||||
customFieldStore.currentCustomField.is_required = intVal
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentCustomField: {
|
||||
type: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
label: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
model_type: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
order: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
numeric: helpers.withMessage(t('validation.numbers_only'), numeric),
|
||||
},
|
||||
type: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => customFieldStore)
|
||||
)
|
||||
|
||||
function setData() {
|
||||
if (customFieldStore.isEdit) {
|
||||
selectedType.value = dataTypes.find(
|
||||
(type) => type.value == customFieldStore.currentCustomField.type
|
||||
)
|
||||
} else {
|
||||
customFieldStore.currentCustomField.model_type = modelTypes[0]
|
||||
|
||||
customFieldStore.currentCustomField.type = dataTypes[0].value
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCustomFieldData() {
|
||||
v$.value.currentCustomField.$touch()
|
||||
|
||||
if (v$.value.currentCustomField.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
let data = {
|
||||
...customFieldStore.currentCustomField,
|
||||
}
|
||||
|
||||
if (customFieldStore.currentCustomField.options) {
|
||||
data.options = customFieldStore.currentCustomField.options.map(
|
||||
(option) => option.name
|
||||
)
|
||||
}
|
||||
|
||||
if (data.type == 'Time' && typeof data.default_answer == 'object') {
|
||||
let HH =
|
||||
data && data.default_answer && data.default_answer.HH
|
||||
? data.default_answer.HH
|
||||
: null
|
||||
let mm =
|
||||
data && data.default_answer && data.default_answer.mm
|
||||
? data.default_answer.mm
|
||||
: null
|
||||
let ss =
|
||||
data && data.default_answer && data.default_answer.ss
|
||||
? data.default_answer.ss
|
||||
: null
|
||||
|
||||
data.default_answer = `${HH}:${mm}`
|
||||
}
|
||||
|
||||
const action = customFieldStore.isEdit
|
||||
? customFieldStore.updateCustomField
|
||||
: customFieldStore.addCustomField
|
||||
|
||||
await action(data)
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
|
||||
closeCustomFieldModal()
|
||||
}
|
||||
|
||||
function addNewOption(option) {
|
||||
customFieldStore.currentCustomField.options = [
|
||||
{ name: option },
|
||||
...customFieldStore.currentCustomField.options,
|
||||
]
|
||||
}
|
||||
|
||||
function removeOption(index) {
|
||||
if (customFieldStore.isEdit && customFieldStore.currentCustomField.in_use) {
|
||||
return
|
||||
}
|
||||
|
||||
const option = customFieldStore.currentCustomField.options[index]
|
||||
|
||||
if (option.name === customFieldStore.currentCustomField.default_answer) {
|
||||
customFieldStore.currentCustomField.default_answer = null
|
||||
}
|
||||
|
||||
customFieldStore.currentCustomField.options.splice(index, 1)
|
||||
}
|
||||
|
||||
function onChangeReset() {
|
||||
customFieldStore.$patch((state) => {
|
||||
state.currentCustomField.default_answer = null
|
||||
state.currentCustomField.is_required = false
|
||||
state.currentCustomField.placeholder = null
|
||||
state.currentCustomField.options = []
|
||||
})
|
||||
|
||||
v$.value.$reset()
|
||||
}
|
||||
|
||||
function onSelectedTypeChange(data) {
|
||||
customFieldStore.currentCustomField.type = data.value
|
||||
}
|
||||
|
||||
function closeCustomFieldModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
customFieldStore.resetCurrentCustomField()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="flex items-center mt-1">
|
||||
<BaseInput
|
||||
v-model="option"
|
||||
type="text"
|
||||
class="w-full md:w-96"
|
||||
:placeholder="$t('settings.custom_fields.press_enter_to_add')"
|
||||
@click="onAddOption"
|
||||
@keydown.enter.prevent.stop="onAddOption"
|
||||
/>
|
||||
|
||||
<BaseIcon
|
||||
name="PlusCircleIcon"
|
||||
class="ml-1 text-primary-500 cursor-pointer"
|
||||
@click="onAddOption"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const emit = defineEmits(['onAdd'])
|
||||
|
||||
const option = ref(null)
|
||||
|
||||
function onAddOption() {
|
||||
if (option.value == null || option.value == '' || option.value == undefined) {
|
||||
return true
|
||||
}
|
||||
|
||||
emit('onAdd', option.value)
|
||||
|
||||
option.value = null
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitData">
|
||||
<div class="px-8 py-6">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.name')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.name.$error &&
|
||||
v$.doSpaceDiskConfig.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="diskStore.doSpaceDiskConfig.name"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.doSpaceDiskConfig.name.$error"
|
||||
@input="v$.doSpaceDiskConfig.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.disk.driver')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.selected_driver.$error &&
|
||||
v$.doSpaceDiskConfig.selected_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selected_driver"
|
||||
:invalid="v$.doSpaceDiskConfig.selected_driver.$error"
|
||||
value-prop="value"
|
||||
:options="disks"
|
||||
searchable
|
||||
label="name"
|
||||
:can-deselect="false"
|
||||
@update:modelValue="onChangeDriver(data)"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_root')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.root.$error &&
|
||||
v$.doSpaceDiskConfig.root.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.root"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. /user/root/"
|
||||
:invalid="v$.doSpaceDiskConfig.root.$error"
|
||||
@input="v$.doSpaceDiskConfig.root.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_key')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.key.$error &&
|
||||
v$.doSpaceDiskConfig.key.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.key"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. KEIS4S39SERSDS"
|
||||
:invalid="v$.doSpaceDiskConfig.key.$error"
|
||||
@input="v$.doSpaceDiskConfig.key.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_secret')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.secret.$error &&
|
||||
v$.doSpaceDiskConfig.secret.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.secret"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. ********"
|
||||
:invalid="v$.doSpaceDiskConfig.secret.$error"
|
||||
@input="v$.doSpaceDiskConfig.secret.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_region')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.region.$error &&
|
||||
v$.doSpaceDiskConfig.region.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.region"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. nyc3"
|
||||
:invalid="v$.doSpaceDiskConfig.region.$error"
|
||||
@input="v$.doSpaceDiskConfig.region.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_endpoint')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.endpoint.$error &&
|
||||
v$.doSpaceDiskConfig.endpoint.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.endpoint"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. https://nyc3.digitaloceanspaces.com"
|
||||
:invalid="v$.doSpaceDiskConfig.endpoint.$error"
|
||||
@input="v$.doSpaceDiskConfig.endpoint.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_bucket')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.bucket.$error &&
|
||||
v$.doSpaceDiskConfig.bucket.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.bucket"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. my-new-space"
|
||||
:invalid="v$.doSpaceDiskConfig.bucket.$error"
|
||||
@input="v$.doSpaceDiskConfig.bucket.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div v-if="!isDisabled" class="flex items-center mt-6">
|
||||
<div class="relative flex items-center w-12">
|
||||
<BaseSwitch v-model="set_as_default" class="flex" />
|
||||
</div>
|
||||
<div class="ml-4 right">
|
||||
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
|
||||
{{ $t('settings.disk.is_default') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot :disk-data="{ isLoading, submitData }" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, url, helpers } from '@vuelidate/validators'
|
||||
export default {
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
disks: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['submit', 'onChangeDisk'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isLoading = ref(false)
|
||||
let set_as_default = ref(false)
|
||||
let selected_disk = ref('')
|
||||
let is_current_disk = ref(null)
|
||||
|
||||
const selected_driver = computed({
|
||||
get: () => diskStore.selected_driver,
|
||||
set: (value) => {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.doSpaceDiskConfig.selected_driver = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
doSpaceDiskConfig: {
|
||||
root: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
key: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
secret: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
region: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
endpoint: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
url: helpers.withMessage(t('validation.invalid_url'), url),
|
||||
},
|
||||
bucket: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => diskStore)
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
diskStore.doSpaceDiskConfig = {
|
||||
name: null,
|
||||
selected_driver: 'doSpaces',
|
||||
key: null,
|
||||
secret: null,
|
||||
region: null,
|
||||
bucket: null,
|
||||
endpoint: null,
|
||||
root: null,
|
||||
}
|
||||
})
|
||||
|
||||
loadData()
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
let data = reactive({
|
||||
disk: 'doSpaces',
|
||||
})
|
||||
|
||||
if (props.isEdit) {
|
||||
Object.assign(
|
||||
diskStore.doSpaceDiskConfig,
|
||||
JSON.parse(modalStore.data.credentials)
|
||||
)
|
||||
set_as_default.value = modalStore.data.set_as_default
|
||||
|
||||
if (set_as_default.value) {
|
||||
is_current_disk.value = true
|
||||
}
|
||||
} else {
|
||||
let diskData = await diskStore.fetchDiskEnv(data)
|
||||
Object.assign(diskStore.doSpaceDiskConfig, diskData.data)
|
||||
}
|
||||
selected_disk.value = props.disks.find((v) => v.value == 'doSpaces')
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return props.isEdit && set_as_default.value && is_current_disk.value
|
||||
? true
|
||||
: false
|
||||
})
|
||||
|
||||
async function submitData() {
|
||||
v$.value.doSpaceDiskConfig.$touch()
|
||||
if (v$.value.doSpaceDiskConfig.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = {
|
||||
credentials: diskStore.doSpaceDiskConfig,
|
||||
name: diskStore.doSpaceDiskConfig.name,
|
||||
driver: selected_disk.value.value,
|
||||
set_as_default: set_as_default.value,
|
||||
}
|
||||
emit('submit', data)
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
emit('onChangeDisk', diskStore.doSpaceDiskConfig.selected_driver)
|
||||
}
|
||||
|
||||
return {
|
||||
v$,
|
||||
diskStore,
|
||||
selected_driver,
|
||||
isLoading,
|
||||
set_as_default,
|
||||
selected_disk,
|
||||
is_current_disk,
|
||||
loadData,
|
||||
submitData,
|
||||
onChangeDriver,
|
||||
isDisabled,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitData">
|
||||
<div class="px-8 py-6">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.name')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.name.$error &&
|
||||
v$.dropBoxDiskConfig.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="diskStore.dropBoxDiskConfig.name"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.dropBoxDiskConfig.name.$error"
|
||||
@input="v$.dropBoxDiskConfig.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.driver')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.selected_driver.$error &&
|
||||
v$.dropBoxDiskConfig.selected_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selected_driver"
|
||||
:invalid="v$.dropBoxDiskConfig.selected_driver.$error"
|
||||
value-prop="value"
|
||||
:options="disks"
|
||||
searchable
|
||||
label="name"
|
||||
:can-deselect="false"
|
||||
@update:modelValue="onChangeDriver(data)"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_root')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.root.$error &&
|
||||
v$.dropBoxDiskConfig.root.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.root"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. /user/root/"
|
||||
:invalid="v$.dropBoxDiskConfig.root.$error"
|
||||
@input="v$.dropBoxDiskConfig.root.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_token')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.token.$error &&
|
||||
v$.dropBoxDiskConfig.token.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.token"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.dropBoxDiskConfig.token.$error"
|
||||
@input="v$.dropBoxDiskConfig.token.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_key')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.key.$error &&
|
||||
v$.dropBoxDiskConfig.key.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.key"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. KEIS4S39SERSDS"
|
||||
:invalid="v$.dropBoxDiskConfig.key.$error"
|
||||
@input="v$.dropBoxDiskConfig.key.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_secret')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.secret.$error &&
|
||||
v$.dropBoxDiskConfig.secret.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.secret"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. ********"
|
||||
:invalid="v$.dropBoxDiskConfig.secret.$error"
|
||||
@input="v$.dropBoxDiskConfig.secret.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_app')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.app.$error &&
|
||||
v$.dropBoxDiskConfig.app.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.app"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.dropBoxDiskConfig.app.$error"
|
||||
@input="v$.dropBoxDiskConfig.app.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div v-if="!isDisabled" class="flex items-center mt-6">
|
||||
<div class="relative flex items-center w-12">
|
||||
<BaseSwitch v-model="set_as_default" class="flex" />
|
||||
</div>
|
||||
<div class="ml-4 right">
|
||||
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
|
||||
{{ $t('settings.disk.is_default') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot :disk-data="{ isLoading, submitData }" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { reactive, ref, computed, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, helpers } from '@vuelidate/validators'
|
||||
export default {
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
disks: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['submit', 'onChangeDisk'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let set_as_default = ref(false)
|
||||
let isLoading = ref(false)
|
||||
let is_current_disk = ref(null)
|
||||
let selected_disk = ref(null)
|
||||
|
||||
const selected_driver = computed({
|
||||
get: () => diskStore.selected_driver,
|
||||
set: (value) => {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.dropBoxDiskConfig.selected_driver = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
dropBoxDiskConfig: {
|
||||
root: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
key: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
secret: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
token: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
app: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => diskStore)
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
diskStore.dropBoxDiskConfig = {
|
||||
name: null,
|
||||
selected_driver: 'dropbox',
|
||||
token: null,
|
||||
key: null,
|
||||
secret: null,
|
||||
app: null,
|
||||
}
|
||||
})
|
||||
|
||||
loadData()
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
let data = reactive({
|
||||
disk: 'dropbox',
|
||||
})
|
||||
|
||||
if (props.isEdit) {
|
||||
Object.assign(diskStore.dropBoxDiskConfig, modalStore.data)
|
||||
set_as_default.value = modalStore.data.set_as_default
|
||||
|
||||
if (set_as_default.value) {
|
||||
is_current_disk.value = true
|
||||
}
|
||||
} else {
|
||||
let diskData = await diskStore.fetchDiskEnv(data)
|
||||
Object.assign(diskStore.dropBoxDiskConfig, diskData.data)
|
||||
}
|
||||
selected_disk.value = props.disks.find((v) => v.value == 'dropbox')
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return props.isEdit && set_as_default.value && is_current_disk.value
|
||||
? true
|
||||
: false
|
||||
})
|
||||
|
||||
async function submitData() {
|
||||
v$.value.dropBoxDiskConfig.$touch()
|
||||
if (v$.value.dropBoxDiskConfig.$invalid) {
|
||||
return true
|
||||
}
|
||||
let data = {
|
||||
credentials: diskStore.dropBoxDiskConfig,
|
||||
name: diskStore.dropBoxDiskConfig.name,
|
||||
driver: selected_disk.value.value,
|
||||
set_as_default: set_as_default.value,
|
||||
}
|
||||
|
||||
emit('submit', data)
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
emit('onChangeDisk', diskStore.dropBoxDiskConfig.selected_driver)
|
||||
}
|
||||
|
||||
return {
|
||||
v$,
|
||||
diskStore,
|
||||
selected_driver,
|
||||
set_as_default,
|
||||
isLoading,
|
||||
is_current_disk,
|
||||
selected_disk,
|
||||
isDisabled,
|
||||
loadData,
|
||||
submitData,
|
||||
onChangeDriver,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<form action="" @submit.prevent="submitData">
|
||||
<div class="px-4 sm:px-8 py-6">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.name')"
|
||||
:error="
|
||||
v$.localDiskConfig.name.$error &&
|
||||
v$.localDiskConfig.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="diskStore.localDiskConfig.name"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.localDiskConfig.name.$error"
|
||||
@input="v$.localDiskConfig.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.disk.driver')"
|
||||
:error="
|
||||
v$.localDiskConfig.selected_driver.$error &&
|
||||
v$.localDiskConfig.selected_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selected_driver"
|
||||
value-prop="value"
|
||||
:invalid="v$.localDiskConfig.selected_driver.$error"
|
||||
:options="disks"
|
||||
searchable
|
||||
label="name"
|
||||
:can-deselect="false"
|
||||
@update:modelValue="onChangeDriver(data)"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.local_root')"
|
||||
:error="
|
||||
v$.localDiskConfig.root.$error &&
|
||||
v$.localDiskConfig.root.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.localDiskConfig.root"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.localDiskConfig.root.$error"
|
||||
placeholder="Ex./user/root/"
|
||||
@input="v$.localDiskConfig.root.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div v-if="!isDisabled" class="flex items-center mt-6">
|
||||
<div class="relative flex items-center w-12">
|
||||
<BaseSwitch v-model="set_as_default" class="flex" />
|
||||
</div>
|
||||
|
||||
<div class="ml-4 right">
|
||||
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
|
||||
{{ $t('settings.disk.is_default') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot :disk-data="{ isLoading, submitData }" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, helpers } from '@vuelidate/validators'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
disks: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['submit', 'onChangeDisk'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isLoading = ref(false)
|
||||
let set_as_default = ref(false)
|
||||
let selected_disk = ref('')
|
||||
let is_current_disk = ref(null)
|
||||
|
||||
const selected_driver = computed({
|
||||
get: () => diskStore.selected_driver,
|
||||
set: (value) => {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.localDiskConfig.selected_driver = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
localDiskConfig: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
root: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => diskStore)
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
diskStore.localDiskConfig = {
|
||||
name: null,
|
||||
selected_driver: 'local',
|
||||
root: null,
|
||||
}
|
||||
})
|
||||
|
||||
loadData()
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
|
||||
let data = reactive({
|
||||
disk: 'local',
|
||||
})
|
||||
|
||||
if (props.isEdit) {
|
||||
Object.assign(diskStore.localDiskConfig, modalStore.data)
|
||||
diskStore.localDiskConfig.root = modalStore.data.credentials
|
||||
set_as_default.value = modalStore.data.set_as_default
|
||||
|
||||
if (set_as_default.value) {
|
||||
is_current_disk.value = true
|
||||
}
|
||||
} else {
|
||||
let diskData = await diskStore.fetchDiskEnv(data)
|
||||
Object.assign(diskStore.localDiskConfig, diskData.data)
|
||||
}
|
||||
|
||||
selected_disk.value = props.disks.find((v) => v.value == 'local')
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return props.isEdit && set_as_default.value && is_current_disk.value
|
||||
? true
|
||||
: false
|
||||
})
|
||||
|
||||
async function submitData() {
|
||||
v$.value.localDiskConfig.$touch()
|
||||
|
||||
if (v$.value.localDiskConfig.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = reactive({
|
||||
credentials: diskStore.localDiskConfig.root,
|
||||
name: diskStore.localDiskConfig.name,
|
||||
driver: diskStore.localDiskConfig.selected_driver,
|
||||
set_as_default: set_as_default.value,
|
||||
})
|
||||
|
||||
emit('submit', data)
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
emit('onChangeDisk', diskStore.localDiskConfig.selected_driver)
|
||||
}
|
||||
|
||||
return {
|
||||
v$,
|
||||
diskStore,
|
||||
modalStore,
|
||||
selected_driver,
|
||||
selected_disk,
|
||||
isLoading,
|
||||
set_as_default,
|
||||
is_current_disk,
|
||||
submitData,
|
||||
onChangeDriver,
|
||||
isDisabled,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
304
resources/scripts/components/modal-components/disks/S3Disk.vue
Normal file
304
resources/scripts/components/modal-components/disks/S3Disk.vue
Normal file
@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitData">
|
||||
<div class="px-8 py-6">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.name')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.name.$error &&
|
||||
v$.s3DiskConfigData.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="diskStore.s3DiskConfigData.name"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.s3DiskConfigData.name.$error"
|
||||
@input="v$.s3DiskConfigData.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.disk.driver')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.selected_driver.$error &&
|
||||
v$.s3DiskConfigData.selected_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selected_driver"
|
||||
:invalid="v$.s3DiskConfigData.selected_driver.$error"
|
||||
value-prop="value"
|
||||
:options="disks"
|
||||
searchable
|
||||
label="name"
|
||||
:can-deselect="false"
|
||||
@update:modelValue="onChangeDriver(data)"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_root')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.root.$error &&
|
||||
v$.s3DiskConfigData.root.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.root"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. /user/root/"
|
||||
:invalid="v$.s3DiskConfigData.root.$error"
|
||||
@input="v$.s3DiskConfigData.root.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_key')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.key.$error &&
|
||||
v$.s3DiskConfigData.key.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.key"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. KEIS4S39SERSDS"
|
||||
:invalid="v$.s3DiskConfigData.key.$error"
|
||||
@input="v$.s3DiskConfigData.key.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_secret')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.secret.$error &&
|
||||
v$.s3DiskConfigData.secret.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.secret"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. ********"
|
||||
:invalid="v$.s3DiskConfigData.secret.$error"
|
||||
@input="v$.s3DiskConfigData.secret.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_region')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.region.$error &&
|
||||
v$.s3DiskConfigData.region.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.region"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. us-west"
|
||||
:invalid="v$.s3DiskConfigData.region.$error"
|
||||
@input="v$.s3DiskConfigData.region.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_bucket')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.bucket.$error &&
|
||||
v$.s3DiskConfigData.bucket.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.bucket"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. AppName"
|
||||
:invalid="v$.s3DiskConfigData.bucket.$error"
|
||||
@input="v$.s3DiskConfigData.bucket.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div v-if="!isDisabled" class="flex items-center mt-6">
|
||||
<div class="relative flex items-center w-12">
|
||||
<BaseSwitch v-model="set_as_default" class="flex" />
|
||||
</div>
|
||||
<div class="ml-4 right">
|
||||
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
|
||||
{{ $t('settings.disk.is_default') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot :disk-data="{ isLoading, submitData }" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, helpers } from '@vuelidate/validators'
|
||||
export default {
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
disks: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['submit', 'onChangeDisk'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let set_as_default = ref(false)
|
||||
let isLoading = ref(false)
|
||||
let selected_disk = ref(null)
|
||||
let is_current_disk = ref(null)
|
||||
|
||||
const selected_driver = computed({
|
||||
get: () => diskStore.selected_driver,
|
||||
set: (value) => {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.s3DiskConfigData.selected_driver = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
s3DiskConfigData: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
root: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
key: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
secret: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
region: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
bucket: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => diskStore)
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
diskStore.s3DiskConfigData = {
|
||||
name: null,
|
||||
selected_driver: 's3',
|
||||
key: null,
|
||||
secret: null,
|
||||
region: null,
|
||||
bucket: null,
|
||||
root: null,
|
||||
}
|
||||
})
|
||||
|
||||
loadData()
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
let data = reactive({
|
||||
disk: 's3',
|
||||
})
|
||||
|
||||
if (props.isEdit) {
|
||||
Object.assign(diskStore.s3DiskConfigData, modalStore.data)
|
||||
set_as_default.value = modalStore.data.set_as_default
|
||||
|
||||
if (set_as_default.value) {
|
||||
is_current_disk.value = true
|
||||
}
|
||||
} else {
|
||||
let diskData = await diskStore.fetchDiskEnv(data)
|
||||
Object.assign(diskStore.s3DiskConfigData, diskData.data)
|
||||
}
|
||||
selected_disk.value = props.disks.find((v) => v.value == 's3')
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return props.isEdit && set_as_default.value && is_current_disk.value
|
||||
? true
|
||||
: false
|
||||
})
|
||||
|
||||
async function submitData() {
|
||||
v$.value.s3DiskConfigData.$touch()
|
||||
if (v$.value.s3DiskConfigData.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = {
|
||||
credentials: diskStore.s3DiskConfigData,
|
||||
name: diskStore.s3DiskConfigData.name,
|
||||
driver: selected_disk.value.value,
|
||||
set_as_default: set_as_default.value,
|
||||
}
|
||||
|
||||
emit('submit', data)
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
emit('onChangeDisk', diskStore.s3DiskConfigData.selected_driver)
|
||||
}
|
||||
|
||||
return {
|
||||
v$,
|
||||
diskStore,
|
||||
modalStore,
|
||||
set_as_default,
|
||||
isLoading,
|
||||
selected_disk,
|
||||
selected_driver,
|
||||
is_current_disk,
|
||||
loadData,
|
||||
submitData,
|
||||
onChangeDriver,
|
||||
isDisabled,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user