v5.0.0 update

This commit is contained in:
Mohit Panjwani
2021-11-30 18:58:19 +05:30
parent d332712c22
commit 082d5cacf2
1253 changed files with 88309 additions and 71741 deletions

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>