mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-28 04:01:10 -04:00
v6 update
This commit is contained in:
228
resources/scripts/admin/views/settings/AccountSetting.vue
Normal file
228
resources/scripts/admin/views/settings/AccountSetting.vue
Normal file
@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<form class="relative" @submit.prevent="updateUserData">
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.account_settings.account_settings')"
|
||||
:description="$t('settings.account_settings.section_description')"
|
||||
>
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.account_settings.profile_picture')"
|
||||
>
|
||||
<BaseFileUploader
|
||||
v-model="imgFiles"
|
||||
:avatar="true"
|
||||
accept="image/*"
|
||||
@change="onFileInputChange"
|
||||
@remove="onFileInputRemove"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<!-- Empty Column -->
|
||||
<span></span>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.account_settings.name')"
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="userForm.name"
|
||||
:invalid="v$.name.$error"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.account_settings.email')"
|
||||
:error="v$.email.$error && v$.email.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="userForm.email"
|
||||
:invalid="v$.email.$error"
|
||||
@input="v$.email.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:error="v$.password.$error && v$.password.$errors[0].$message"
|
||||
:label="$tc('settings.account_settings.password')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="userForm.password"
|
||||
type="password"
|
||||
@input="v$.password.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.account_settings.confirm_password')"
|
||||
:error="
|
||||
v$.confirm_password.$error &&
|
||||
v$.confirm_password.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="userForm.confirm_password"
|
||||
type="password"
|
||||
@input="v$.confirm_password.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$tc('settings.language')">
|
||||
<BaseMultiselect
|
||||
v-model="userForm.language"
|
||||
:options="globalStore.config.languages"
|
||||
label="name"
|
||||
value-prop="code"
|
||||
track-by="code"
|
||||
open-direction="top"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
|
||||
<BaseButton :loading="isSaving" :disabled="isSaving" class="mt-6">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
></BaseIcon>
|
||||
</template>
|
||||
{{ $tc('settings.company_info.save') }}
|
||||
</BaseButton>
|
||||
</BaseSettingCard>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive } from 'vue'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
helpers,
|
||||
sameAs,
|
||||
email,
|
||||
required,
|
||||
minLength,
|
||||
} from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
let avatarFileBlob = ref(null)
|
||||
let imgFiles = ref([])
|
||||
|
||||
if (userStore.currentUser.avatar) {
|
||||
imgFiles.value.push({
|
||||
image: userStore.currentUser.avatar,
|
||||
})
|
||||
}
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
email: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
password: {
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.password_length', { count: 8 }),
|
||||
minLength(8)
|
||||
),
|
||||
},
|
||||
confirm_password: {
|
||||
sameAsPassword: helpers.withMessage(
|
||||
t('validation.password_incorrect'),
|
||||
sameAs(userForm.password)
|
||||
),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const userForm = reactive({
|
||||
name: userStore.currentUser.name,
|
||||
email: userStore.currentUser.email,
|
||||
language:
|
||||
userStore.currentUserSettings.language ||
|
||||
companyStore.selectedCompanySettings.language,
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => userForm)
|
||||
)
|
||||
|
||||
function onFileInputChange(fileName, file) {
|
||||
avatarFileBlob.value = file
|
||||
}
|
||||
|
||||
function onFileInputRemove() {
|
||||
avatarFileBlob.value = null
|
||||
}
|
||||
|
||||
async function updateUserData() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
let data = {
|
||||
name: userForm.name,
|
||||
email: userForm.email,
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
userForm.password != null &&
|
||||
userForm.password !== undefined &&
|
||||
userForm.password !== ''
|
||||
) {
|
||||
data = { ...data, password: userForm.password }
|
||||
}
|
||||
// Update Language if changed
|
||||
|
||||
if (userStore.currentUserSettings.language !== userForm.language) {
|
||||
await userStore.updateUserSettings({
|
||||
settings: {
|
||||
language: userForm.language,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let response = await userStore.updateCurrentUser(data)
|
||||
|
||||
if (response.data.data) {
|
||||
isSaving.value = false
|
||||
|
||||
if (avatarFileBlob.value) {
|
||||
let avatarData = new FormData()
|
||||
|
||||
avatarData.append('admin_avatar', avatarFileBlob.value)
|
||||
|
||||
await userStore.uploadAvatar(avatarData)
|
||||
}
|
||||
|
||||
userForm.password = ''
|
||||
userForm.confirm_password = ''
|
||||
}
|
||||
} catch (error) {
|
||||
isSaving.value = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
230
resources/scripts/admin/views/settings/BackupSetting.vue
Normal file
230
resources/scripts/admin/views/settings/BackupSetting.vue
Normal file
@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<BackupModal />
|
||||
|
||||
<BaseSettingCard
|
||||
:title="$tc('settings.backup.title', 1)"
|
||||
:description="$t('settings.backup.description')"
|
||||
>
|
||||
<template #action>
|
||||
<BaseButton variant="primary-outline" @click="onCreateNewBackup">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon :class="slotProps.class" name="PlusIcon" />
|
||||
</template>
|
||||
{{ $t('settings.backup.new_backup') }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<div class="grid my-14 md:grid-cols-3">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.select_disk')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="filters.selected_disk"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="getDisksOptions"
|
||||
track-by="id"
|
||||
:placeholder="$t('settings.disk.select_disk')"
|
||||
label="name"
|
||||
:searchable="true"
|
||||
object
|
||||
class="w-full"
|
||||
value-prop="id"
|
||||
@select="refreshTable"
|
||||
>
|
||||
</BaseMultiselect>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
|
||||
<BaseTable
|
||||
ref="table"
|
||||
class="mt-10"
|
||||
:show-filter="false"
|
||||
:data="fetchBackupsData"
|
||||
:columns="backupColumns"
|
||||
>
|
||||
<template #cell-actions="{ row }">
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<div class="inline-block">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="text-gray-500" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<BaseDropdownItem @click="onDownloadBckup(row.data)">
|
||||
<BaseIcon name="CloudDownloadIcon" class="mr-3 text-gray-600" />
|
||||
|
||||
{{ $t('general.download') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<BaseDropdownItem @click="onRemoveBackup(row.data)">
|
||||
<BaseIcon name="TrashIcon" class="mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</BaseSettingCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useBackupStore } from '@/scripts/admin/stores/backup'
|
||||
import { computed, ref, reactive, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDiskStore } from '@/scripts/admin/stores/disk'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import BackupModal from '@/scripts/admin/components/modal-components/BackupModal.vue'
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const backupStore = useBackupStore()
|
||||
const modalStore = useModalStore()
|
||||
const diskStore = useDiskStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const filters = reactive({
|
||||
selected_disk: { driver: 'local' },
|
||||
})
|
||||
|
||||
const table = ref('')
|
||||
let isFetchingInitialData = ref(true)
|
||||
|
||||
const backupColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'path',
|
||||
label: t('settings.backup.path'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
label: t('settings.backup.created_at'),
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'size',
|
||||
label: t('settings.backup.size'),
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'text-right text-sm font-medium',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const getDisksOptions = computed(() => {
|
||||
return diskStore.disks.map((disk) => {
|
||||
return {
|
||||
...disk,
|
||||
name: disk.name + ' — ' + '[' + disk.driver + ']',
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
loadDisksData()
|
||||
|
||||
function onRemoveBackup(backup) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.backup.backup_confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
let data = {
|
||||
disk: filters.selected_disk.driver,
|
||||
file_disk_id: filters.selected_disk.id,
|
||||
path: backup.path,
|
||||
}
|
||||
|
||||
let response = await backupStore.removeBackup(data)
|
||||
|
||||
if (response.data.success || response.data.backup) {
|
||||
table.value && table.value.refresh()
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function refreshTable() {
|
||||
setTimeout(() => {
|
||||
table.value.refresh()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
async function loadDisksData() {
|
||||
isFetchingInitialData.value = true
|
||||
let res = await diskStore.fetchDisks({ limit: 'all' })
|
||||
if (res.data.error) {
|
||||
}
|
||||
filters.selected_disk = res.data.data.find((disk) => disk.set_as_default == 0)
|
||||
isFetchingInitialData.value = false
|
||||
}
|
||||
|
||||
async function fetchBackupsData({ page, filter, sort }) {
|
||||
let data = {
|
||||
disk: filters.selected_disk.driver,
|
||||
filed_disk_id: filters.selected_disk.id,
|
||||
}
|
||||
|
||||
isFetchingInitialData.value = true
|
||||
|
||||
let response = await backupStore.fetchBackups(data)
|
||||
|
||||
isFetchingInitialData.value = false
|
||||
|
||||
return {
|
||||
data: response.data.backups,
|
||||
pagination: {
|
||||
totalPages: 1,
|
||||
currentPage: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function onCreateNewBackup() {
|
||||
modalStore.openModal({
|
||||
title: t('settings.backup.create_backup'),
|
||||
componentName: 'BackupModal',
|
||||
refreshData: table.value && table.value.refresh,
|
||||
size: 'sm',
|
||||
})
|
||||
}
|
||||
|
||||
async function onDownloadBckup(backup) {
|
||||
isFetchingInitialData.value = true
|
||||
window
|
||||
.axios({
|
||||
method: 'GET',
|
||||
url: '/api/v1/download-backup',
|
||||
responseType: 'blob',
|
||||
params: {
|
||||
disk: filters.selected_disk.driver,
|
||||
file_disk_id: filters.selected_disk.id,
|
||||
path: backup.path,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', backup.path.split('/')[1])
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
isFetchingInitialData.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
isFetchingInitialData.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
260
resources/scripts/admin/views/settings/CompanyInfoSettings.vue
Normal file
260
resources/scripts/admin/views/settings/CompanyInfoSettings.vue
Normal file
@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<form @submit.prevent="updateCompanyData">
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.company_info.company_info')"
|
||||
:description="$t('settings.company_info.section_description')"
|
||||
>
|
||||
<BaseInputGrid class="mt-5">
|
||||
<BaseInputGroup :label="$tc('settings.company_info.company_logo')">
|
||||
<BaseFileUploader
|
||||
v-model="previewLogo"
|
||||
base64
|
||||
@change="onFileInputChange"
|
||||
@remove="onFileInputRemove"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
|
||||
<BaseInputGrid class="mt-5">
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.company_info.company_name')"
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="companyForm.name"
|
||||
:invalid="v$.name.$error"
|
||||
@blur="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$tc('settings.company_info.phone')">
|
||||
<BaseInput v-model="companyForm.address.phone" />
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.company_info.country')"
|
||||
:error="
|
||||
v$.address.country_id.$error &&
|
||||
v$.address.country_id.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="companyForm.address.country_id"
|
||||
label="name"
|
||||
:invalid="v$.address.country_id.$error"
|
||||
:options="globalStore.countries"
|
||||
value-prop="id"
|
||||
:can-deselect="true"
|
||||
:can-clear="false"
|
||||
searchable
|
||||
track-by="name"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$tc('settings.company_info.state')">
|
||||
<BaseInput
|
||||
v-model="companyForm.address.state"
|
||||
name="state"
|
||||
type="text"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$tc('settings.company_info.city')">
|
||||
<BaseInput v-model="companyForm.address.city" type="text" />
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$tc('settings.company_info.zip')">
|
||||
<BaseInput v-model="companyForm.address.zip" />
|
||||
</BaseInputGroup>
|
||||
|
||||
<div>
|
||||
<BaseInputGroup :label="$tc('settings.company_info.address')">
|
||||
<BaseTextarea
|
||||
v-model="companyForm.address.address_street_1"
|
||||
rows="2"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseTextarea
|
||||
v-model="companyForm.address.address_street_2"
|
||||
rows="2"
|
||||
:row="2"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
</BaseInputGrid>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
type="submit"
|
||||
class="mt-6"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" :class="slotProps.class" name="SaveIcon" />
|
||||
</template>
|
||||
{{ $tc('settings.company_info.save') }}
|
||||
</BaseButton>
|
||||
|
||||
<div v-if="companyStore.companies.length !== 1" class="py-5">
|
||||
<BaseDivider class="my-4" />
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||
{{ $tc('settings.company_info.delete_company') }}
|
||||
</h3>
|
||||
<div class="mt-2 max-w-xl text-sm text-gray-500">
|
||||
<p>
|
||||
{{ $tc('settings.company_info.delete_company_description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<button
|
||||
type="button"
|
||||
class="
|
||||
inline-flex
|
||||
items-center
|
||||
justify-center
|
||||
px-4
|
||||
py-2
|
||||
border border-transparent
|
||||
font-medium
|
||||
rounded-md
|
||||
text-red-700
|
||||
bg-red-100
|
||||
hover:bg-red-200
|
||||
focus:outline-none
|
||||
focus:ring-2
|
||||
focus:ring-offset-2
|
||||
focus:ring-red-500
|
||||
sm:text-sm
|
||||
"
|
||||
@click="removeCompany"
|
||||
>
|
||||
{{ $tc('general.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseSettingCard>
|
||||
</form>
|
||||
<DeleteCompanyModal />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, inject, computed } from 'vue'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import DeleteCompanyModal from '@/scripts/admin/components/modal-components/DeleteCompanyModal.vue'
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
const utils = inject('utils')
|
||||
|
||||
let isSaving = ref(false)
|
||||
|
||||
const companyForm = reactive({
|
||||
name: null,
|
||||
logo: null,
|
||||
address: {
|
||||
address_street_1: '',
|
||||
address_street_2: '',
|
||||
website: '',
|
||||
country_id: null,
|
||||
state: '',
|
||||
city: '',
|
||||
phone: '',
|
||||
zip: '',
|
||||
},
|
||||
})
|
||||
|
||||
utils.mergeSettings(companyForm, {
|
||||
...companyStore.selectedCompany,
|
||||
})
|
||||
|
||||
let previewLogo = ref([])
|
||||
let logoFileBlob = ref(null)
|
||||
let logoFileName = ref(null)
|
||||
|
||||
if (companyForm.logo) {
|
||||
previewLogo.value.push({
|
||||
image: companyForm.logo,
|
||||
})
|
||||
}
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length'),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
address: {
|
||||
country_id: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => companyForm)
|
||||
)
|
||||
|
||||
globalStore.fetchCountries()
|
||||
|
||||
function onFileInputChange(fileName, file, fileCount, fileList) {
|
||||
logoFileName.value = fileList.name
|
||||
logoFileBlob.value = file
|
||||
}
|
||||
|
||||
function onFileInputRemove() {
|
||||
logoFileBlob.value = null
|
||||
}
|
||||
|
||||
async function updateCompanyData() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
const res = await companyStore.updateCompany(companyForm)
|
||||
|
||||
if (res.data.data) {
|
||||
if (logoFileBlob.value) {
|
||||
let logoData = new FormData()
|
||||
|
||||
logoData.append(
|
||||
'company_logo',
|
||||
JSON.stringify({
|
||||
name: logoFileName.value,
|
||||
data: logoFileBlob.value,
|
||||
})
|
||||
)
|
||||
|
||||
await companyStore.updateCompanyLogo(logoData)
|
||||
}
|
||||
|
||||
isSaving.value = false
|
||||
}
|
||||
isSaving.value = false
|
||||
}
|
||||
function removeCompany(id) {
|
||||
modalStore.openModal({
|
||||
title: t('settings.company_info.are_you_absolutely_sure'),
|
||||
componentName: 'DeleteCompanyModal',
|
||||
size: 'sm',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
150
resources/scripts/admin/views/settings/CustomFieldsSetting.vue
Normal file
150
resources/scripts/admin/views/settings/CustomFieldsSetting.vue
Normal file
@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.menu_title.custom_fields')"
|
||||
:description="$t('settings.custom_fields.section_description')"
|
||||
>
|
||||
<template #action>
|
||||
<BaseButton
|
||||
v-if="userStore.hasAbilities(abilities.CREATE_CUSTOM_FIELDS)"
|
||||
variant="primary-outline"
|
||||
@click="addCustomField"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon :class="slotProps.class" name="PlusIcon" />
|
||||
|
||||
{{ $t('settings.custom_fields.add_custom_field') }}
|
||||
</template>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<CustomFieldModal />
|
||||
|
||||
<BaseTable
|
||||
ref="table"
|
||||
:data="fetchData"
|
||||
:columns="customFieldsColumns"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #cell-name="{ row }">
|
||||
{{ row.data.name }}
|
||||
<span class="text-xs text-gray-500"> ({{ row.data.slug }})</span>
|
||||
</template>
|
||||
|
||||
<template #cell-is_required="{ row }">
|
||||
<BaseBadge
|
||||
:bg-color="
|
||||
utils.getBadgeStatusColor(row.data.is_required ? 'YES' : 'NO')
|
||||
.bgColor
|
||||
"
|
||||
:color="
|
||||
utils.getBadgeStatusColor(row.data.is_required ? 'YES' : 'NO').color
|
||||
"
|
||||
>
|
||||
{{
|
||||
row.data.is_required
|
||||
? $t('settings.custom_fields.yes')
|
||||
: $t('settings.custom_fields.no').replace('_', ' ')
|
||||
}}
|
||||
</BaseBadge>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="
|
||||
userStore.hasAbilities([
|
||||
abilities.DELETE_CUSTOM_FIELDS,
|
||||
abilities.EDIT_CUSTOM_FIELDS,
|
||||
])
|
||||
"
|
||||
#cell-actions="{ row }"
|
||||
>
|
||||
<CustomFieldDropdown
|
||||
:row="row.data"
|
||||
:table="table"
|
||||
:load-data="refreshTable"
|
||||
/>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</BaseSettingCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useCustomFieldStore } from '@/scripts/admin/stores/custom-field'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import CustomFieldDropdown from '@/scripts/admin/components/dropdowns/CustomFieldIndexDropdown.vue'
|
||||
import CustomFieldModal from '@/scripts/admin/components/modal-components/custom-fields/CustomFieldModal.vue'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const customFieldStore = useCustomFieldStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const utils = inject('utils')
|
||||
const { t } = useI18n()
|
||||
|
||||
const table = ref(null)
|
||||
|
||||
const customFieldsColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('settings.custom_fields.name'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'model_type',
|
||||
label: t('settings.custom_fields.model'),
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: t('settings.custom_fields.type'),
|
||||
},
|
||||
{
|
||||
key: 'is_required',
|
||||
label: t('settings.custom_fields.required'),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'text-right text-sm font-medium',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
async function fetchData({ page, filter, sort }) {
|
||||
let data = {
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page,
|
||||
}
|
||||
|
||||
let response = await customFieldStore.fetchCustomFields(data)
|
||||
|
||||
return {
|
||||
data: response.data.data,
|
||||
pagination: {
|
||||
totalPages: response.data.meta.last_page,
|
||||
currentPage: page,
|
||||
limit: 5,
|
||||
totalCount: response.data.meta.total,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function addCustomField() {
|
||||
modalStore.openModal({
|
||||
title: t('settings.custom_fields.add_custom_field'),
|
||||
componentName: 'CustomFieldModal',
|
||||
size: 'sm',
|
||||
refreshData: table.value && table.value.refresh,
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshTable() {
|
||||
table.value && table.value.refresh()
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<ExchangeRateProviderModal />
|
||||
<BaseCard>
|
||||
<div slot="header" class="flex flex-wrap justify-between lg:flex-nowrap">
|
||||
<div>
|
||||
<h6 class="text-lg font-medium text-left">
|
||||
{{ $t('settings.menu_title.exchange_rate') }}
|
||||
</h6>
|
||||
<p
|
||||
class="mt-2 text-sm leading-snug text-left text-gray-500"
|
||||
style="max-width: 680px"
|
||||
>
|
||||
{{ $t('settings.exchange_rate.providers_description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 lg:mt-0 lg:ml-2">
|
||||
<BaseButton
|
||||
variant="primary-outline"
|
||||
size="lg"
|
||||
@click="addExchangeRate"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<PlusIcon :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $t('settings.exchange_rate.new_driver') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseTable ref="table" class="mt-16" :data="fetchData" :columns="drivers">
|
||||
<template #cell-driver="{ row }">
|
||||
<span class="capitalize">{{ row.data.driver.replace('_', ' ') }}</span>
|
||||
</template>
|
||||
<template #cell-active="{ row }">
|
||||
<BaseBadge
|
||||
:bg-color="
|
||||
utils.getBadgeStatusColor(row.data.active ? 'YES' : 'NO').bgColor
|
||||
"
|
||||
:color="
|
||||
utils.getBadgeStatusColor(row.data.active ? 'YES' : 'NO').color
|
||||
"
|
||||
>
|
||||
{{ row.data.active ? 'YES' : 'NO' }}
|
||||
</BaseBadge>
|
||||
</template>
|
||||
<template #cell-actions="{ row }">
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<div class="inline-block">
|
||||
<DotsHorizontalIcon class="w-5 text-gray-500" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<BaseDropdownItem @click="editExchangeRate(row.data.id)">
|
||||
<PencilIcon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<BaseDropdownItem @click="removeExchangeRate(row.data.id)">
|
||||
<TrashIcon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useExchangeRateStore } from '@/scripts/admin/stores/exchange-rate'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { SaveIcon } from '@heroicons/vue/outline'
|
||||
import { ref, computed, inject, reactive } from 'vue'
|
||||
import ExchangeRateProviderModal from '@/scripts/admin/components/modal-components/ExchangeRateProviderModal.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
PlusIcon,
|
||||
DotsHorizontalIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
} from '@heroicons/vue/outline'
|
||||
import BaseTable from '@/scripts/components/base/base-table/BaseTable.vue'
|
||||
|
||||
// store
|
||||
|
||||
const { tm, t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
const exchangeRateStore = useExchangeRateStore()
|
||||
const modalStore = useModalStore()
|
||||
const dialogStore = useDialogStore()
|
||||
//created
|
||||
|
||||
// local state
|
||||
|
||||
let table = ref('')
|
||||
const utils = inject('utils')
|
||||
const drivers = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'driver',
|
||||
label: t('settings.exchange_rate.driver'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
label: t('settings.exchange_rate.key'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'active',
|
||||
label: t('settings.exchange_rate.active'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'text-right text-sm font-medium',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
async function fetchData({ page, sort }) {
|
||||
let data = reactive({
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page,
|
||||
})
|
||||
|
||||
let response = await exchangeRateStore.fetchProviders(data)
|
||||
|
||||
return {
|
||||
data: response.data.data,
|
||||
pagination: {
|
||||
totalPages: response.data.meta.last_page,
|
||||
currentPage: page,
|
||||
totalCount: response.data.meta.total,
|
||||
limit: 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
async function updateRate() {
|
||||
await exchangeRateStore.updateExchangeRate(
|
||||
exchangeRateStore.currentExchangeRate.rate
|
||||
)
|
||||
}
|
||||
|
||||
function addExchangeRate() {
|
||||
modalStore.openModal({
|
||||
title: t('settings.exchange_rate.new_driver'),
|
||||
componentName: 'ExchangeRateProviderModal',
|
||||
size: 'md',
|
||||
refreshData: table.value && table.value.refresh,
|
||||
})
|
||||
}
|
||||
|
||||
function editExchangeRate(data) {
|
||||
exchangeRateStore.fetchProvider(data)
|
||||
modalStore.openModal({
|
||||
title: t('settings.exchange_rate.edit_driver'),
|
||||
componentName: 'ExchangeRateProviderModal',
|
||||
size: 'md',
|
||||
data: data,
|
||||
refreshData: table.value && table.value.refresh,
|
||||
})
|
||||
}
|
||||
|
||||
function removeExchangeRate(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.exchange_rate.exchange_rate_confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
await exchangeRateStore.deleteExchangeRate(id)
|
||||
table.value && table.value.refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<CategoryModal />
|
||||
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.expense_category.title')"
|
||||
:description="$t('settings.expense_category.description')"
|
||||
>
|
||||
<template #action>
|
||||
<BaseButton
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="openCategoryModal"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon :class="slotProps.class" name="PlusIcon" />
|
||||
</template>
|
||||
|
||||
{{ $t('settings.expense_category.add_new_category') }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<BaseTable
|
||||
ref="table"
|
||||
:data="fetchData"
|
||||
:columns="ExpenseCategoryColumns"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #cell-description="{ row }">
|
||||
<div class="w-64">
|
||||
<p class="truncate">{{ row.data.description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<ExpenseCategoryDropdown
|
||||
:row="row.data"
|
||||
:table="table"
|
||||
:load-data="refreshTable"
|
||||
/>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</BaseSettingCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useCategoryStore } from '@/scripts/admin/stores/category'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ExpenseCategoryDropdown from '@/scripts/admin/components/dropdowns/ExpenseCategoryIndexDropdown.vue'
|
||||
import CategoryModal from '@/scripts/admin/components/modal-components/CategoryModal.vue'
|
||||
|
||||
const categoryStore = useCategoryStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const table = ref(null)
|
||||
|
||||
const ExpenseCategoryColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('settings.expense_category.category_name'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: t('settings.expense_category.category_description'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'text-right text-sm font-medium',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
async function fetchData({ page, filter, sort }) {
|
||||
let data = {
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page,
|
||||
}
|
||||
|
||||
let response = await categoryStore.fetchCategories(data)
|
||||
return {
|
||||
data: response.data.data,
|
||||
pagination: {
|
||||
totalPages: response.data.meta.last_page,
|
||||
currentPage: page,
|
||||
totalCount: response.data.meta.total,
|
||||
limit: 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function openCategoryModal() {
|
||||
modalStore.openModal({
|
||||
title: t('settings.expense_category.add_category'),
|
||||
componentName: 'CategoryModal',
|
||||
size: 'sm',
|
||||
refreshData: table.value && table.value.refresh,
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshTable() {
|
||||
table.value && table.value.refresh()
|
||||
}
|
||||
</script>
|
||||
257
resources/scripts/admin/views/settings/FileDiskSetting.vue
Normal file
257
resources/scripts/admin/views/settings/FileDiskSetting.vue
Normal file
@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<FileDiskModal />
|
||||
|
||||
<BaseSettingCard
|
||||
:title="$tc('settings.disk.title', 1)"
|
||||
:description="$t('settings.disk.description')"
|
||||
>
|
||||
<template #action>
|
||||
<BaseButton variant="primary-outline" @click="openCreateDiskModal">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon :class="slotProps.class" name="PlusIcon" />
|
||||
</template>
|
||||
{{ $t('settings.disk.new_disk') }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<BaseTable
|
||||
ref="table"
|
||||
class="mt-16"
|
||||
:data="fetchData"
|
||||
:columns="fileDiskColumns"
|
||||
>
|
||||
<template #cell-set_as_default="{ row }">
|
||||
<BaseBadge
|
||||
:bg-color="
|
||||
utils.getBadgeStatusColor(row.data.set_as_default ? 'YES' : 'NO')
|
||||
.bgColor
|
||||
"
|
||||
:color="
|
||||
utils.getBadgeStatusColor(row.data.set_as_default ? 'YES' : 'NO')
|
||||
.color
|
||||
"
|
||||
>
|
||||
{{ row.data.set_as_default ? 'Yes' : 'No'.replace('_', ' ') }}
|
||||
</BaseBadge>
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<BaseDropdown v-if="isNotSystemDisk(row.data)">
|
||||
<template #activator>
|
||||
<div class="inline-block">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="text-gray-500" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<BaseDropdownItem
|
||||
v-if="!row.data.set_as_default"
|
||||
@click="setDefaultDiskData(row.data.id)"
|
||||
>
|
||||
<BaseIcon class="mr-3 tetx-gray-600" name="CheckCircleIcon" />
|
||||
|
||||
{{ $t('settings.disk.set_default_disk') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<BaseDropdownItem
|
||||
v-if="row.data.type !== 'SYSTEM'"
|
||||
@click="openEditDiskModal(row.data)"
|
||||
>
|
||||
<BaseIcon name="PencilIcon" class="mr-3 text-gray-600" />
|
||||
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<BaseDropdownItem
|
||||
v-if="row.data.type !== 'SYSTEM' && !row.data.set_as_default"
|
||||
@click="removeDisk(row.data.id)"
|
||||
>
|
||||
<BaseIcon name="TrashIcon" class="mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
</BaseTable>
|
||||
|
||||
<BaseDivider class="mt-8 mb-2" />
|
||||
|
||||
<BaseSwitchSection
|
||||
v-model="savePdfToDiskField"
|
||||
:title="$t('settings.disk.save_pdf_to_disk')"
|
||||
:description="$t('settings.disk.disk_setting_description')"
|
||||
/>
|
||||
</BaseSettingCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDiskStore } from '@/scripts/admin/stores/disk'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { ref, computed, reactive, onMounted, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import FileDiskModal from '@/scripts/admin/components/modal-components/FileDiskModal.vue'
|
||||
|
||||
const utils = inject('utils')
|
||||
|
||||
const modelStore = useModalStore()
|
||||
const diskStore = useDiskStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let disk = 'local'
|
||||
let loading = ref(false)
|
||||
let table = ref('')
|
||||
|
||||
const fileDiskColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('settings.disk.disk_name'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'driver',
|
||||
label: t('settings.disk.filesystem_driver'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: t('settings.disk.disk_type'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
|
||||
{
|
||||
key: 'set_as_default',
|
||||
label: t('settings.disk.is_default'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'text-right text-sm font-medium',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const savePdfToDisk = ref(companyStore.selectedCompanySettings.save_pdf_to_disk)
|
||||
|
||||
const savePdfToDiskField = computed({
|
||||
get: () => {
|
||||
return savePdfToDisk.value === 'YES'
|
||||
},
|
||||
set: async (newValue) => {
|
||||
const value = newValue ? 'YES' : 'NO'
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
save_pdf_to_disk: value,
|
||||
},
|
||||
}
|
||||
|
||||
savePdfToDisk.value = value
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'general.setting_updated',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
async function fetchData({ page, filter, sort }) {
|
||||
let data = reactive({
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page,
|
||||
})
|
||||
|
||||
let response = await diskStore.fetchDisks(data)
|
||||
|
||||
return {
|
||||
data: response.data.data,
|
||||
pagination: {
|
||||
totalPages: response.data.meta.last_page,
|
||||
currentPage: page,
|
||||
totalCount: response.data.meta.total,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function isNotSystemDisk(disk) {
|
||||
if (!disk.set_as_default) return true
|
||||
if (disk.type == 'SYSTEM' && disk.set_as_default) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function openCreateDiskModal() {
|
||||
modelStore.openModal({
|
||||
title: t('settings.disk.new_disk'),
|
||||
componentName: 'FileDiskModal',
|
||||
variant: 'lg',
|
||||
refreshData: table.value && table.value.refresh,
|
||||
})
|
||||
}
|
||||
|
||||
function openEditDiskModal(data) {
|
||||
modelStore.openModal({
|
||||
title: t('settings.disk.edit_file_disk'),
|
||||
componentName: 'FileDiskModal',
|
||||
variant: 'lg',
|
||||
id: data.id,
|
||||
data: data,
|
||||
refreshData: table.value && table.value.refresh,
|
||||
})
|
||||
}
|
||||
|
||||
function setDefaultDiskData(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.disk.set_default_disk_confirm'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
loading.value = true
|
||||
let data = reactive({
|
||||
set_as_default: true,
|
||||
id,
|
||||
})
|
||||
await diskStore.updateDisk(data).then(() => {
|
||||
table.value && table.value.refresh()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function removeDisk(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.disk.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
let response = await diskStore.deleteFileDisk(id)
|
||||
if (response.data.success) {
|
||||
table.value && table.value.refresh()
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
93
resources/scripts/admin/views/settings/MailConfigSetting.vue
Normal file
93
resources/scripts/admin/views/settings/MailConfigSetting.vue
Normal file
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<MailTestModal />
|
||||
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.mail.mail_config')"
|
||||
:description="$t('settings.mail.mail_config_desc')"
|
||||
>
|
||||
<div v-if="mailDriverStore && mailDriverStore.mailConfigData" class="mt-14">
|
||||
<component
|
||||
:is="mailDriver"
|
||||
:config-data="mailDriverStore.mailConfigData"
|
||||
:is-saving="isSaving"
|
||||
:mail-drivers="mailDriverStore.mail_drivers"
|
||||
:is-fetching-initial-data="isFetchingInitialData"
|
||||
@on-change-driver="(val) => changeDriver(val)"
|
||||
@submit-data="saveEmailConfig"
|
||||
>
|
||||
<BaseButton
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
class="ml-2"
|
||||
:content-loading="isFetchingInitialData"
|
||||
@click="openMailTestModal"
|
||||
>
|
||||
{{ $t('general.test_mail_conf') }}
|
||||
</BaseButton>
|
||||
</component>
|
||||
</div>
|
||||
</BaseSettingCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Smtp from '@/scripts/admin/views/settings/mail-driver/SmtpMailDriver.vue'
|
||||
import Mailgun from '@/scripts/admin/views/settings/mail-driver/MailgunMailDriver.vue'
|
||||
import Ses from '@/scripts/admin/views/settings/mail-driver/SesMailDriver.vue'
|
||||
import Basic from '@/scripts/admin/views/settings/mail-driver/BasicMailDriver.vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import MailTestModal from '@/scripts/admin/components/modal-components/MailTestModal.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
let isSaving = ref(false)
|
||||
let isFetchingInitialData = ref(false)
|
||||
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
loadData()
|
||||
function changeDriver(value) {
|
||||
mailDriverStore.mail_driver = value
|
||||
mailDriverStore.mailConfigData.mail_driver = value
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
isFetchingInitialData.value = true
|
||||
Promise.all([
|
||||
await mailDriverStore.fetchMailDrivers(),
|
||||
await mailDriverStore.fetchMailConfig(),
|
||||
]).then(([res1]) => {
|
||||
isFetchingInitialData.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const mailDriver = computed(() => {
|
||||
if (mailDriverStore.mail_driver == 'smtp') return Smtp
|
||||
if (mailDriverStore.mail_driver == 'mailgun') return Mailgun
|
||||
if (mailDriverStore.mail_driver == 'sendmail') return Basic
|
||||
if (mailDriverStore.mail_driver == 'ses') return Ses
|
||||
if (mailDriverStore.mail_driver == 'mail') return Basic
|
||||
return Smtp
|
||||
})
|
||||
|
||||
async function saveEmailConfig(value) {
|
||||
try {
|
||||
isSaving.value = true
|
||||
await mailDriverStore.updateMailConfig(value)
|
||||
isSaving.value = false
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function openMailTestModal() {
|
||||
modalStore.openModal({
|
||||
title: t('general.test_mail_conf'),
|
||||
componentName: 'MailTestModal',
|
||||
size: 'sm',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
116
resources/scripts/admin/views/settings/NotesSetting.vue
Normal file
116
resources/scripts/admin/views/settings/NotesSetting.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<NoteModal />
|
||||
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.customization.notes.title')"
|
||||
:description="$t('settings.customization.notes.description')"
|
||||
>
|
||||
<template #action>
|
||||
<BaseButton
|
||||
v-if="userStore.hasAbilities(abilities.MANAGE_NOTE)"
|
||||
variant="primary-outline"
|
||||
@click="openNoteSelectModal"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon :class="slotProps.class" name="PlusIcon" />
|
||||
</template>
|
||||
{{ $t('settings.customization.notes.add_note') }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<BaseTable
|
||||
ref="table"
|
||||
:data="fetchData"
|
||||
:columns="notesColumns"
|
||||
class="mt-14"
|
||||
>
|
||||
<template #cell-actions="{ row }">
|
||||
<NoteDropdown
|
||||
:row="row.data"
|
||||
:table="table"
|
||||
:load-data="refreshTable"
|
||||
/>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</BaseSettingCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotesStore } from '@/scripts/admin/stores/note'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import NoteDropdown from '@/scripts/admin/components/dropdowns/NoteIndexDropdown.vue'
|
||||
import NoteModal from '@/scripts/admin/components/modal-components/NoteModal.vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const noteStore = useNotesStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const table = ref('')
|
||||
|
||||
const notesColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('settings.customization.notes.name'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: t('settings.customization.notes.type'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'text-right text-sm font-medium',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
async function fetchData({ page, filter, sort }) {
|
||||
let data = reactive({
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page,
|
||||
})
|
||||
|
||||
let response = await noteStore.fetchNotes(data)
|
||||
|
||||
return {
|
||||
data: response.data.data,
|
||||
pagination: {
|
||||
totalPages: response.data.meta.last_page,
|
||||
currentPage: page,
|
||||
totalCount: response.data.meta.total,
|
||||
limit: 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function openNoteSelectModal() {
|
||||
await modalStore.openModal({
|
||||
title: t('settings.customization.notes.add_note'),
|
||||
componentName: 'NoteModal',
|
||||
size: 'md',
|
||||
refreshData: table.value && table.value.refresh,
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshTable() {
|
||||
table.value && table.value.refresh()
|
||||
}
|
||||
</script>
|
||||
162
resources/scripts/admin/views/settings/NotificationsSetting.vue
Normal file
162
resources/scripts/admin/views/settings/NotificationsSetting.vue
Normal file
@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.notification.title')"
|
||||
:description="$t('settings.notification.description')"
|
||||
>
|
||||
<form action="" @submit.prevent="submitForm">
|
||||
<div class="grid-cols-2 col-span-1 mt-14">
|
||||
<BaseInputGroup
|
||||
:error="
|
||||
v$.notification_email.$error &&
|
||||
v$.notification_email.$errors[0].$message
|
||||
"
|
||||
:label="$t('settings.notification.email')"
|
||||
class="my-2"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="settingsForm.notification_email"
|
||||
:invalid="v$.notification_email.$error"
|
||||
type="email"
|
||||
@input="v$.notification_email.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton
|
||||
:disabled="isSaving"
|
||||
:loading="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
class="mt-6"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
:class="slotProps.class"
|
||||
name="SaveIcon"
|
||||
/>
|
||||
</template>
|
||||
|
||||
{{ $tc('settings.notification.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
|
||||
<ul class="divide-y divide-gray-200">
|
||||
<BaseSwitchSection
|
||||
v-model="invoiceViewedField"
|
||||
:title="$t('settings.notification.invoice_viewed')"
|
||||
:description="$t('settings.notification.invoice_viewed_desc')"
|
||||
/>
|
||||
|
||||
<BaseSwitchSection
|
||||
v-model="estimateViewedField"
|
||||
:title="$t('settings.notification.estimate_viewed')"
|
||||
:description="$t('settings.notification.estimate_viewed_desc')"
|
||||
/>
|
||||
</ul>
|
||||
</BaseSettingCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
let isSaving = ref(false)
|
||||
const { t } = useI18n()
|
||||
|
||||
const settingsForm = reactive({
|
||||
notify_invoice_viewed:
|
||||
companyStore.selectedCompanySettings.notify_invoice_viewed,
|
||||
notify_estimate_viewed:
|
||||
companyStore.selectedCompanySettings.notify_estimate_viewed,
|
||||
notification_email: companyStore.selectedCompanySettings.notification_email,
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
notification_email: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => settingsForm)
|
||||
)
|
||||
|
||||
const invoiceViewedField = computed({
|
||||
get: () => {
|
||||
return settingsForm.notify_invoice_viewed === 'YES'
|
||||
},
|
||||
set: async (newValue) => {
|
||||
const value = newValue ? 'YES' : 'NO'
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
notify_invoice_viewed: value,
|
||||
},
|
||||
}
|
||||
|
||||
settingsForm.notify_invoice_viewed = value
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'general.setting_updated',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const estimateViewedField = computed({
|
||||
get: () => {
|
||||
return settingsForm.notify_estimate_viewed === 'YES'
|
||||
},
|
||||
set: async (newValue) => {
|
||||
const value = newValue ? 'YES' : 'NO'
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
notify_estimate_viewed: value,
|
||||
},
|
||||
}
|
||||
|
||||
settingsForm.notify_estimate_viewed = value
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'general.setting_updated',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
async function submitForm() {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
const data = {
|
||||
settings: {
|
||||
notification_email: settingsForm.notification_email,
|
||||
},
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.notification.email_save_message',
|
||||
})
|
||||
|
||||
isSaving.value = false
|
||||
}
|
||||
</script>
|
||||
104
resources/scripts/admin/views/settings/PaymentsModeSetting.vue
Normal file
104
resources/scripts/admin/views/settings/PaymentsModeSetting.vue
Normal file
@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<PaymentModeModal />
|
||||
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.payment_modes.title')"
|
||||
:description="$t('settings.payment_modes.description')"
|
||||
>
|
||||
<template #action>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
variant="primary-outline"
|
||||
@click="addPaymentMode"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon :class="slotProps.class" name="PlusIcon" />
|
||||
</template>
|
||||
{{ $t('settings.payment_modes.add_payment_mode') }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<BaseTable
|
||||
ref="table"
|
||||
:data="fetchData"
|
||||
:columns="paymentColumns"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #cell-actions="{ row }">
|
||||
<PaymentModeDropdown
|
||||
:row="row.data"
|
||||
:table="table"
|
||||
:load-data="refreshTable"
|
||||
/>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</BaseSettingCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { usePaymentStore } from '@/scripts/admin/stores/payment'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import PaymentModeModal from '@/scripts/admin/components/modal-components/PaymentModeModal.vue'
|
||||
import PaymentModeDropdown from '@/scripts/admin/components/dropdowns/PaymentModeIndexDropdown.vue'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const paymentStore = usePaymentStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const table = ref(null)
|
||||
|
||||
const paymentColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('settings.payment_modes.mode_name'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'text-right text-sm font-medium',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
async function refreshTable() {
|
||||
table.value && table.value.refresh()
|
||||
}
|
||||
|
||||
async function fetchData({ page, filter, sort }) {
|
||||
let data = {
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page,
|
||||
}
|
||||
|
||||
let response = await paymentStore.fetchPaymentModes(data)
|
||||
|
||||
return {
|
||||
data: response.data.data,
|
||||
pagination: {
|
||||
totalPages: response.data.meta.last_page,
|
||||
currentPage: page,
|
||||
totalCount: response.data.meta.total,
|
||||
limit: 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function addPaymentMode() {
|
||||
modalStore.openModal({
|
||||
title: t('settings.payment_modes.add_payment_mode'),
|
||||
componentName: 'PaymentModeModal',
|
||||
refreshData: table.value && table.value.refresh,
|
||||
size: 'sm',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
331
resources/scripts/admin/views/settings/PreferencesSetting.vue
Normal file
331
resources/scripts/admin/views/settings/PreferencesSetting.vue
Normal file
@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<form action="" class="relative" @submit.prevent="updatePreferencesData">
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.menu_title.preferences')"
|
||||
:description="$t('settings.preferences.general_settings')"
|
||||
>
|
||||
<BaseInputGrid class="mt-5">
|
||||
<BaseInputGroup
|
||||
:content-loading="isFetchingInitialData"
|
||||
:label="$tc('settings.preferences.currency')"
|
||||
:help-text="$t('settings.preferences.company_currency_unchangeable')"
|
||||
:error="v$.currency.$error && v$.currency.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="settingsForm.currency"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="globalStore.currencies"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
:searchable="true"
|
||||
track-by="name"
|
||||
:invalid="v$.currency.$error"
|
||||
disabled
|
||||
class="w-full"
|
||||
>
|
||||
</BaseMultiselect>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.preferences.default_language')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="v$.language.$error && v$.language.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="settingsForm.language"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="globalStore.config.languages"
|
||||
label="name"
|
||||
value-prop="code"
|
||||
class="w-full"
|
||||
track-by="code"
|
||||
:searchable="true"
|
||||
:invalid="v$.language.$error"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.preferences.time_zone')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="v$.time_zone.$error && v$.time_zone.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="settingsForm.time_zone"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="globalStore.timeZones"
|
||||
label="key"
|
||||
value-prop="value"
|
||||
track-by="key"
|
||||
:searchable="true"
|
||||
:invalid="v$.time_zone.$error"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.preferences.date_format')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.carbon_date_format.$error &&
|
||||
v$.carbon_date_format.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="settingsForm.carbon_date_format"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="globalStore.dateFormats"
|
||||
label="display_date"
|
||||
value-prop="carbon_format_value"
|
||||
track-by="carbon_format_value"
|
||||
searchable
|
||||
:invalid="v$.carbon_date_format.$error"
|
||||
class="w-full"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="v$.fiscal_year.$error && v$.fiscal_year.$errors[0].$message"
|
||||
:label="$tc('settings.preferences.fiscal_year')"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="settingsForm.fiscal_year"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="globalStore.config.fiscal_years"
|
||||
label="key"
|
||||
value-prop="value"
|
||||
:invalid="v$.fiscal_year.$error"
|
||||
track-by="key"
|
||||
:searchable="true"
|
||||
class="w-full"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
|
||||
<BaseButton
|
||||
:content-loading="isFetchingInitialData"
|
||||
:disabled="isSaving"
|
||||
:loading="isSaving"
|
||||
type="submit"
|
||||
class="mt-6"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $tc('settings.company_info.save') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
|
||||
<ul>
|
||||
<form @submit.prevent="submitData">
|
||||
<BaseSwitchSection
|
||||
v-model="expirePdfField"
|
||||
:title="$t('settings.preferences.expire_public_links')"
|
||||
:description="$t('settings.preferences.expire_setting_description')"
|
||||
/>
|
||||
|
||||
<!--pdf_link_expiry_days -->
|
||||
<BaseInputGroup
|
||||
v-if="expirePdfField"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:label="$t('settings.preferences.expire_public_links')"
|
||||
class="mt-2 mb-4"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="settingsForm.link_expiry_days"
|
||||
:disabled="
|
||||
settingsForm.automatically_expire_public_links === 'NO'
|
||||
"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="number"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton
|
||||
:content-loading="isFetchingInitialData"
|
||||
:disabled="isDataSaving"
|
||||
:loading="isDataSaving"
|
||||
type="submit"
|
||||
class="mt-6"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $tc('general.save') }}
|
||||
</BaseButton>
|
||||
</form>
|
||||
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
|
||||
<BaseSwitchSection
|
||||
v-model="discountPerItemField"
|
||||
:title="$t('settings.preferences.discount_per_item')"
|
||||
:description="$t('settings.preferences.discount_setting_description')"
|
||||
/>
|
||||
</ul>
|
||||
</BaseSettingCard>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, reactive } from 'vue'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const { t, tm } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
let isDataSaving = ref(false)
|
||||
let isFetchingInitialData = ref(false)
|
||||
|
||||
const settingsForm = reactive({ ...companyStore.selectedCompanySettings })
|
||||
|
||||
const retrospectiveEditOptions = computed(() => {
|
||||
return globalStore.config.retrospective_edits.map((option) => {
|
||||
option.title = t(option.key)
|
||||
return option
|
||||
})
|
||||
})
|
||||
|
||||
watch(
|
||||
() => settingsForm.carbon_date_format,
|
||||
(val) => {
|
||||
if (val) {
|
||||
const dateFormatObject = globalStore.dateFormats.find((d) => {
|
||||
return d.carbon_format_value === val
|
||||
})
|
||||
|
||||
settingsForm.moment_date_format = dateFormatObject.moment_format_value
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const discountPerItemField = computed({
|
||||
get: () => {
|
||||
return settingsForm.discount_per_item === 'YES'
|
||||
},
|
||||
set: async (newValue) => {
|
||||
const value = newValue ? 'YES' : 'NO'
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
discount_per_item: value,
|
||||
},
|
||||
}
|
||||
|
||||
settingsForm.discount_per_item = value
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'general.setting_updated',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const expirePdfField = computed({
|
||||
get: () => {
|
||||
return settingsForm.automatically_expire_public_links === 'YES'
|
||||
},
|
||||
set: async (newValue) => {
|
||||
const value = newValue ? 'YES' : 'NO'
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
automatically_expire_public_links: value,
|
||||
},
|
||||
}
|
||||
|
||||
settingsForm.automatically_expire_public_links = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currency: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
language: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
carbon_date_format: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
moment_date_format: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
time_zone: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
fiscal_year: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => settingsForm)
|
||||
)
|
||||
|
||||
setInitialData()
|
||||
|
||||
async function setInitialData() {
|
||||
isFetchingInitialData.value = true
|
||||
Promise.all([
|
||||
globalStore.fetchCurrencies(),
|
||||
globalStore.fetchDateFormats(),
|
||||
globalStore.fetchTimeZones(),
|
||||
]).then(([res1]) => {
|
||||
isFetchingInitialData.value = false
|
||||
})
|
||||
}
|
||||
|
||||
async function updatePreferencesData() {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return
|
||||
}
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
...settingsForm,
|
||||
},
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
delete data.settings.link_expiry_days
|
||||
let res = await companyStore.updateCompanySettings({
|
||||
data: data,
|
||||
message: 'settings.preferences.updated_message',
|
||||
})
|
||||
|
||||
isSaving.value = false
|
||||
}
|
||||
|
||||
async function submitData() {
|
||||
isDataSaving.value = true
|
||||
|
||||
let res = await companyStore.updateCompanySettings({
|
||||
data: {
|
||||
settings: {
|
||||
link_expiry_days: settingsForm.link_expiry_days,
|
||||
automatically_expire_public_links:
|
||||
settingsForm.automatically_expire_public_links,
|
||||
},
|
||||
},
|
||||
message: 'settings.preferences.updated_message',
|
||||
})
|
||||
|
||||
isDataSaving.value = false
|
||||
}
|
||||
</script>
|
||||
112
resources/scripts/admin/views/settings/RolesSettings.vue
Normal file
112
resources/scripts/admin/views/settings/RolesSettings.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<RolesModal />
|
||||
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.roles.title')"
|
||||
:description="$t('settings.roles.description')"
|
||||
>
|
||||
<template v-if="userStore.currentUser.is_owner" #action>
|
||||
<BaseButton variant="primary-outline" @click="openRoleModal">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="PlusIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $t('settings.roles.add_new_role') }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<BaseTable
|
||||
ref="table"
|
||||
:data="fetchData"
|
||||
:columns="roleColumns"
|
||||
class="mt-14"
|
||||
>
|
||||
<!-- Added on -->
|
||||
<template #cell-created_at="{ row }">
|
||||
{{ row.data.formatted_created_at }}
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<RoleDropdown
|
||||
v-if="
|
||||
userStore.currentUser.is_owner && row.data.name !== 'super admin'
|
||||
"
|
||||
:row="row.data"
|
||||
:table="table"
|
||||
:load-data="refreshTable"
|
||||
/>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</BaseSettingCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RoleDropdown from '@/scripts/admin/components/dropdowns/RoleIndexDropdown.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoleStore } from '@/scripts/admin/stores/role'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import RolesModal from '@/scripts/admin/components/modal-components/RolesModal.vue'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const roleStore = useRoleStore()
|
||||
const userStore = useUserStore()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const table = ref(null)
|
||||
|
||||
const roleColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('settings.roles.role_name'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
label: t('settings.roles.added_on'),
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'text-right text-sm font-medium',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
async function fetchData({ page, filter, sort }) {
|
||||
let data = {
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
company_id: companyStore.selectedCompany.id,
|
||||
}
|
||||
|
||||
let response = await roleStore.fetchRoles(data)
|
||||
|
||||
return {
|
||||
data: response.data.data,
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTable() {
|
||||
table.value && table.value.refresh()
|
||||
}
|
||||
|
||||
async function openRoleModal() {
|
||||
await roleStore.fetchAbilities()
|
||||
|
||||
modalStore.openModal({
|
||||
title: t('settings.roles.add_role'),
|
||||
componentName: 'RolesModal',
|
||||
size: 'lg',
|
||||
refreshData: table.value && table.value.refresh,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
95
resources/scripts/admin/views/settings/SettingsIndex.vue
Normal file
95
resources/scripts/admin/views/settings/SettingsIndex.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<BasePage>
|
||||
<BasePageHeader :title="$tc('settings.setting', 1)" class="mb-6">
|
||||
<BaseBreadcrumb>
|
||||
<BaseBreadcrumbItem :title="$t('general.home')" to="/admin/dashboard" />
|
||||
<BaseBreadcrumbItem
|
||||
:title="$tc('settings.setting', 2)"
|
||||
to="/admin/settings/account-settings"
|
||||
active
|
||||
/>
|
||||
</BaseBreadcrumb>
|
||||
</BasePageHeader>
|
||||
|
||||
<div class="w-full mb-6 select-wrapper xl:hidden">
|
||||
<BaseMultiselect
|
||||
v-model="currentSetting"
|
||||
:options="dropdownMenuItems"
|
||||
:can-deselect="false"
|
||||
value-prop="title"
|
||||
track-by="title"
|
||||
label="title"
|
||||
object
|
||||
@update:modelValue="navigateToSetting"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="hidden mt-1 xl:block min-w-[240px]">
|
||||
<BaseList>
|
||||
<BaseListItem
|
||||
v-for="(menuItem, index) in globalStore.settingMenu"
|
||||
:key="index"
|
||||
:title="$t(menuItem.title)"
|
||||
:to="menuItem.link"
|
||||
:active="hasActiveUrl(menuItem.link)"
|
||||
:index="index"
|
||||
class="py-3"
|
||||
>
|
||||
<template #icon>
|
||||
<BaseIcon :name="menuItem.icon"></BaseIcon>
|
||||
</template>
|
||||
</BaseListItem>
|
||||
</BaseList>
|
||||
</div>
|
||||
|
||||
<div class="w-full overflow-hidden">
|
||||
<RouterView />
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watchEffect, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
import BaseList from '@/scripts/components/list/BaseList.vue'
|
||||
import BaseListItem from '@/scripts/components/list/BaseListItem.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
|
||||
let currentSetting = ref({})
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const dropdownMenuItems = computed(() => {
|
||||
return globalStore.settingMenu.map((item) => {
|
||||
return Object.assign({}, item, {
|
||||
title: t(item.title),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (route.path === '/admin/settings') {
|
||||
router.push('/admin/settings/account-settings')
|
||||
}
|
||||
|
||||
const item = dropdownMenuItems.value.find((item) => {
|
||||
return item.link === route.path
|
||||
})
|
||||
|
||||
currentSetting.value = item
|
||||
})
|
||||
|
||||
function hasActiveUrl(url) {
|
||||
return route.path.indexOf(url) > -1
|
||||
}
|
||||
|
||||
function navigateToSetting(setting) {
|
||||
return router.push(setting.link)
|
||||
}
|
||||
</script>
|
||||
182
resources/scripts/admin/views/settings/TaxTypesSetting.vue
Normal file
182
resources/scripts/admin/views/settings/TaxTypesSetting.vue
Normal file
@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.tax_types.title')"
|
||||
:description="$t('settings.tax_types.description')"
|
||||
>
|
||||
<TaxTypeModal />
|
||||
|
||||
<template v-if="userStore.hasAbilities(abilities.CREATE_TAX_TYPE)" #action>
|
||||
<BaseButton type="submit" variant="primary-outline" @click="openTaxModal">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon :class="slotProps.class" name="PlusIcon" />
|
||||
</template>
|
||||
{{ $t('settings.tax_types.add_new_tax') }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<BaseTable
|
||||
ref="table"
|
||||
class="mt-16"
|
||||
:data="fetchData"
|
||||
:columns="taxTypeColumns"
|
||||
>
|
||||
<template #cell-compound_tax="{ row }">
|
||||
<BaseBadge
|
||||
:bg-color="
|
||||
utils.getBadgeStatusColor(row.data.compound_tax ? 'YES' : 'NO')
|
||||
.bgColor
|
||||
"
|
||||
:color="
|
||||
utils.getBadgeStatusColor(row.data.compound_tax ? 'YES' : 'NO')
|
||||
.color
|
||||
"
|
||||
>
|
||||
{{ row.data.compound_tax ? 'Yes' : 'No'.replace('_', ' ') }}
|
||||
</BaseBadge>
|
||||
</template>
|
||||
|
||||
<template #cell-percent="{ row }"> {{ row.data.percent }} % </template>
|
||||
|
||||
<template v-if="hasAtleastOneAbility()" #cell-actions="{ row }">
|
||||
<TaxTypeDropdown
|
||||
:row="row.data"
|
||||
:table="table"
|
||||
:load-data="refreshTable"
|
||||
/>
|
||||
</template>
|
||||
</BaseTable>
|
||||
<div v-if="userStore.currentUser.is_owner">
|
||||
<BaseDivider class="mt-8 mb-2" />
|
||||
|
||||
<BaseSwitchSection
|
||||
v-model="taxPerItemField"
|
||||
:disabled="salesTaxEnabled"
|
||||
:title="$t('settings.tax_types.tax_per_item')"
|
||||
:description="$t('settings.tax_types.tax_setting_description')"
|
||||
/>
|
||||
</div>
|
||||
</BaseSettingCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, reactive, ref, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useModuleStore } from '@/scripts/admin/stores/module'
|
||||
|
||||
import TaxTypeDropdown from '@/scripts/admin/components/dropdowns/TaxTypeIndexDropdown.vue'
|
||||
import TaxTypeModal from '@/scripts/admin/components/modal-components/TaxTypeModal.vue'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const { t } = useI18n()
|
||||
const utils = inject('utils')
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const modalStore = useModalStore()
|
||||
const userStore = useUserStore()
|
||||
const moduleStore = useModuleStore()
|
||||
|
||||
const table = ref(null)
|
||||
const taxPerItemSetting = ref(companyStore.selectedCompanySettings.tax_per_item)
|
||||
|
||||
const taxTypeColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('settings.tax_types.tax_name'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'compound_tax',
|
||||
label: t('settings.tax_types.compound_tax'),
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'percent',
|
||||
label: t('settings.tax_types.percent'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'text-right text-sm font-medium',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const salesTaxEnabled = computed(() => {
|
||||
return (
|
||||
companyStore.selectedCompanySettings.sales_tax_us_enabled === 'YES' &&
|
||||
moduleStore.salesTaxUSEnabled
|
||||
)
|
||||
})
|
||||
|
||||
const taxPerItemField = computed({
|
||||
get: () => {
|
||||
return taxPerItemSetting.value === 'YES'
|
||||
},
|
||||
set: async (newValue) => {
|
||||
const value = newValue ? 'YES' : 'NO'
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
tax_per_item: value,
|
||||
},
|
||||
}
|
||||
|
||||
taxPerItemSetting.value = value
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'general.setting_updated',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
function hasAtleastOneAbility() {
|
||||
return userStore.hasAbilities([
|
||||
abilities.DELETE_TAX_TYPE,
|
||||
abilities.EDIT_TAX_TYPE,
|
||||
])
|
||||
}
|
||||
|
||||
async function fetchData({ page, filter, sort }) {
|
||||
let data = {
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page,
|
||||
}
|
||||
|
||||
let response = await taxTypeStore.fetchTaxTypes(data)
|
||||
|
||||
return {
|
||||
data: response.data.data,
|
||||
pagination: {
|
||||
totalPages: response.data.meta.last_page,
|
||||
currentPage: page,
|
||||
totalCount: response.data.meta.total,
|
||||
limit: 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTable() {
|
||||
table.value && table.value.refresh()
|
||||
}
|
||||
|
||||
function openTaxModal() {
|
||||
modalStore.openModal({
|
||||
title: t('settings.tax_types.add_tax'),
|
||||
componentName: 'TaxTypeModal',
|
||||
size: 'sm',
|
||||
refreshData: table.value && table.value.refresh,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
436
resources/scripts/admin/views/settings/UpdateAppSetting.vue
Normal file
436
resources/scripts/admin/views/settings/UpdateAppSetting.vue
Normal file
@ -0,0 +1,436 @@
|
||||
<template>
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.update_app.title')"
|
||||
:description="$t('settings.update_app.description')"
|
||||
>
|
||||
<div class="pb-8 ml-0">
|
||||
<label class="text-sm not-italic font-medium input-label">
|
||||
{{ $t('settings.update_app.current_version') }}
|
||||
</label>
|
||||
|
||||
<div
|
||||
class="
|
||||
box-border
|
||||
flex
|
||||
w-16
|
||||
p-3
|
||||
my-2
|
||||
text-sm text-gray-600
|
||||
bg-gray-200
|
||||
border border-gray-200 border-solid
|
||||
rounded-md
|
||||
version
|
||||
"
|
||||
>
|
||||
{{ currentVersion }}
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
:loading="isCheckingforUpdate"
|
||||
:disabled="isCheckingforUpdate || isUpdating"
|
||||
variant="primary-outline"
|
||||
class="mt-6"
|
||||
@click="checkUpdate"
|
||||
>
|
||||
{{ $t('settings.update_app.check_update') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseDivider v-if="isUpdateAvailable" class="mt-6 mb-4" />
|
||||
|
||||
<div v-show="!isUpdating" v-if="isUpdateAvailable" class="mt-4 content">
|
||||
<BaseHeading type="heading-title" class="mb-2">
|
||||
{{ $t('settings.update_app.avail_update') }}
|
||||
</BaseHeading>
|
||||
|
||||
<div class="rounded-md bg-primary-50 p-4 mb-3">
|
||||
<div class="flex">
|
||||
<div class="shrink-0">
|
||||
<BaseIcon
|
||||
name="InformationCircleIcon"
|
||||
class="h-5 w-5 text-primary-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-primary-800">
|
||||
{{ $t('general.note') }}
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-primary-700">
|
||||
<p>
|
||||
{{ $t('settings.update_app.update_warning') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="text-sm not-italic font-medium input-label">
|
||||
{{ $t('settings.update_app.next_version') }}
|
||||
</label>
|
||||
<br />
|
||||
<div
|
||||
class="
|
||||
box-border
|
||||
flex
|
||||
w-16
|
||||
p-3
|
||||
my-2
|
||||
text-sm text-gray-600
|
||||
bg-gray-200
|
||||
border border-gray-200 border-solid
|
||||
rounded-md
|
||||
version
|
||||
"
|
||||
>
|
||||
{{ updateData.version }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
pl-5
|
||||
mt-4
|
||||
mb-8
|
||||
text-sm
|
||||
leading-snug
|
||||
text-gray-500
|
||||
update-description
|
||||
"
|
||||
style="white-space: pre-wrap; max-width: 480px"
|
||||
v-html="description"
|
||||
></div>
|
||||
|
||||
<label class="text-sm not-italic font-medium input-label">
|
||||
{{ $t('settings.update_app.requirements') }}
|
||||
</label>
|
||||
|
||||
<table class="w-1/2 mt-2 border-2 border-gray-200 BaseTable-fixed">
|
||||
<tr
|
||||
v-for="(ext, i) in requiredExtentions"
|
||||
:key="i"
|
||||
class="p-2 border-2 border-gray-200"
|
||||
>
|
||||
<td width="70%" class="p-2 text-sm truncate">
|
||||
{{ i }}
|
||||
</td>
|
||||
<td width="30%" class="p-2 text-sm text-right">
|
||||
<span
|
||||
v-if="ext"
|
||||
class="inline-block w-4 h-4 ml-3 mr-2 bg-green-500 rounded-full"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="inline-block w-4 h-4 ml-3 mr-2 bg-red-500 rounded-full"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<BaseButton class="mt-10" variant="primary" @click="onUpdateApp">
|
||||
{{ $t('settings.update_app.update') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div v-if="isUpdating" class="relative flex justify-between mt-4 content">
|
||||
<div>
|
||||
<h6 class="m-0 mb-3 font-medium sw-section-title">
|
||||
{{ $t('settings.update_app.update_progress') }}
|
||||
</h6>
|
||||
<p
|
||||
class="mb-8 text-sm leading-snug text-gray-500"
|
||||
style="max-width: 480px"
|
||||
>
|
||||
{{ $t('settings.update_app.progress_text') }}
|
||||
</p>
|
||||
</div>
|
||||
<LoadingIcon
|
||||
class="absolute right-0 h-6 m-1 animate-spin text-primary-400"
|
||||
/>
|
||||
</div>
|
||||
<ul v-if="isUpdating" class="w-full p-0 list-none">
|
||||
<li
|
||||
v-for="step in updateSteps"
|
||||
:key="step.stepUrl"
|
||||
class="
|
||||
flex
|
||||
justify-between
|
||||
w-full
|
||||
py-3
|
||||
border-b border-gray-200 border-solid
|
||||
last:border-b-0
|
||||
"
|
||||
>
|
||||
<p class="m-0 text-sm leading-8">{{ $t(step.translationKey) }}</p>
|
||||
<div class="flex flex-row items-center">
|
||||
<span v-if="step.time" class="mr-3 text-xs text-gray-500">
|
||||
{{ step.time }}
|
||||
</span>
|
||||
<span
|
||||
:class="statusClass(step)"
|
||||
class="block py-1 text-sm text-center uppercase rounded-full"
|
||||
style="width: 88px"
|
||||
>
|
||||
{{ getStatus(step) }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</BaseSettingCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import axios from 'axios'
|
||||
import LoadingIcon from '@/scripts/components/icons/LoadingIcon.vue'
|
||||
import { reactive, ref, onMounted, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { handleError } from '@/scripts/helpers/error-handling'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useExchangeRateStore } from '@/scripts/admin/stores/exchange-rate'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
|
||||
const notificationStore = useNotificationStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const { t, tm } = useI18n()
|
||||
const comapnyStore = useCompanyStore()
|
||||
const exchangeRateStore = useExchangeRateStore()
|
||||
|
||||
let isUpdateAvailable = ref(false)
|
||||
let isCheckingforUpdate = ref(false)
|
||||
let description = ref('')
|
||||
let currentVersion = ref('')
|
||||
let requiredExtentions = ref(null)
|
||||
let deletedFiles = ref(null)
|
||||
let isUpdating = ref(false)
|
||||
|
||||
const updateSteps = reactive([
|
||||
{
|
||||
translationKey: 'settings.update_app.download_zip_file',
|
||||
stepUrl: '/api/v1/update/download',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
translationKey: 'settings.update_app.unzipping_package',
|
||||
stepUrl: '/api/v1/update/unzip',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
translationKey: 'settings.update_app.copying_files',
|
||||
stepUrl: '/api/v1/update/copy',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
translationKey: 'settings.update_app.deleting_files',
|
||||
stepUrl: '/api/v1/update/delete',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
translationKey: 'settings.update_app.running_migrations',
|
||||
stepUrl: '/api/v1/update/migrate',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
translationKey: 'settings.update_app.finishing_update',
|
||||
stepUrl: '/api/v1/update/finish',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
])
|
||||
|
||||
const updateData = reactive({
|
||||
isMinor: Boolean,
|
||||
installed: '',
|
||||
version: '',
|
||||
})
|
||||
|
||||
let minPhpVesrion = ref(null)
|
||||
|
||||
window.addEventListener('beforeunload', (event) => {
|
||||
if (isUpdating.value) {
|
||||
event.returnValue = 'Update is in progress!'
|
||||
}
|
||||
})
|
||||
|
||||
// Created
|
||||
|
||||
axios.get('/api/v1/app/version').then((res) => {
|
||||
currentVersion.value = res.data.version
|
||||
})
|
||||
|
||||
// comapnyStore
|
||||
// .fetchCompanySettings(['bulk_exchange_rate_configured'])
|
||||
// .then((res) => {
|
||||
// isExchangeRateUpdated.value =
|
||||
// res.data.bulk_exchange_rate_configured === 'YES'
|
||||
// })
|
||||
|
||||
// Comuted props
|
||||
|
||||
const allowToUpdate = computed(() => {
|
||||
if (requiredExtentions.value !== null) {
|
||||
return Object.keys(requiredExtentions.value).every((k) => {
|
||||
return requiredExtentions.value[k]
|
||||
})
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
function statusClass(step) {
|
||||
const status = getStatus(step)
|
||||
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'text-primary-800 bg-gray-200'
|
||||
case 'finished':
|
||||
return 'text-teal-500 bg-teal-100'
|
||||
case 'running':
|
||||
return 'text-blue-400 bg-blue-100'
|
||||
case 'error':
|
||||
return 'text-danger bg-red-200'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUpdate() {
|
||||
try {
|
||||
isCheckingforUpdate.value = true
|
||||
let response = await axios.get('/api/v1/check/update')
|
||||
isCheckingforUpdate.value = false
|
||||
if (!response.data.version) {
|
||||
notificationStore.showNotification({
|
||||
title: 'Info!',
|
||||
type: 'info',
|
||||
message: t('settings.update_app.latest_message'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
updateData.isMinor = response.data.is_minor
|
||||
updateData.version = response.data.version.version
|
||||
description.value = response.data.version.description
|
||||
requiredExtentions.value = response.data.version.extensions
|
||||
isUpdateAvailable.value = true
|
||||
minPhpVesrion.value = response.data.version.minimum_php_version
|
||||
deletedFiles.value = response.data.version.deleted_files
|
||||
}
|
||||
} catch (e) {
|
||||
isUpdateAvailable.value = false
|
||||
isCheckingforUpdate.value = false
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
function onUpdateApp() {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.update_app.update_warning'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
let path = null
|
||||
if (!allowToUpdate.value) {
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'Your current configuration does not match the update requirements. Please try again after all the requirements are fulfilled.',
|
||||
})
|
||||
return true
|
||||
}
|
||||
for (let index = 0; index < updateSteps.length; index++) {
|
||||
let currentStep = updateSteps[index]
|
||||
try {
|
||||
isUpdating.value = true
|
||||
currentStep.started = true
|
||||
let updateParams = {
|
||||
version: updateData.version,
|
||||
installed: currentVersion.value,
|
||||
deleted_files: deletedFiles.value,
|
||||
path: path || null,
|
||||
}
|
||||
|
||||
let requestResponse = await axios.post(
|
||||
currentStep.stepUrl,
|
||||
updateParams
|
||||
)
|
||||
currentStep.completed = true
|
||||
if (requestResponse.data && requestResponse.data.path) {
|
||||
path = requestResponse.data.path
|
||||
}
|
||||
// on finish
|
||||
|
||||
if (
|
||||
currentStep.translationKey ==
|
||||
'settings.update_app.finishing_update'
|
||||
) {
|
||||
isUpdating.value = false
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('settings.update_app.update_success'),
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload()
|
||||
}, 3000)
|
||||
}
|
||||
} catch (error) {
|
||||
currentStep.started = false
|
||||
currentStep.completed = true
|
||||
handleError(error)
|
||||
onUpdateFailed(currentStep.translationKey)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onUpdateFailed(translationKey) {
|
||||
let stepName = t(translationKey)
|
||||
if (stepName.value) {
|
||||
onUpdateApp()
|
||||
return
|
||||
}
|
||||
isUpdating.value = false
|
||||
}
|
||||
|
||||
function getStatus(step) {
|
||||
if (step.started && step.completed) {
|
||||
return 'finished'
|
||||
} else if (step.started && !step.completed) {
|
||||
return 'running'
|
||||
} else if (!step.started && !step.completed) {
|
||||
return 'pending'
|
||||
} else {
|
||||
return 'error'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.update-description ul {
|
||||
list-style: disc !important;
|
||||
}
|
||||
|
||||
.update-description li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<BaseCard container-class="px-4 py-5 sm:px-8 sm:py-2">
|
||||
<BaseTabGroup>
|
||||
<BaseTab
|
||||
tab-panel-container="py-4 mt-px"
|
||||
:title="$t('settings.customization.invoices.title')"
|
||||
>
|
||||
<InvoicesTab />
|
||||
</BaseTab>
|
||||
|
||||
<BaseTab
|
||||
tab-panel-container="py-4 mt-px"
|
||||
:title="$t('settings.customization.estimates.title')"
|
||||
>
|
||||
<EstimatesTab />
|
||||
</BaseTab>
|
||||
|
||||
<BaseTab
|
||||
tab-panel-container="py-4 mt-px"
|
||||
:title="$t('settings.customization.payments.title')"
|
||||
>
|
||||
<PaymentsTab />
|
||||
</BaseTab>
|
||||
|
||||
<BaseTab
|
||||
tab-panel-container="py-4 mt-px"
|
||||
:title="$t('settings.customization.items.title')"
|
||||
>
|
||||
<ItemsTab />
|
||||
</BaseTab>
|
||||
</BaseTabGroup>
|
||||
</BaseCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import InvoicesTab from '@/scripts/admin/views/settings/customization/invoices/InvoicesTab.vue'
|
||||
import EstimatesTab from '@/scripts/admin/views/settings/customization/estimates/EstimatesTab.vue'
|
||||
import PaymentsTab from '@/scripts/admin/views/settings/customization/payments/PaymentsTab.vue'
|
||||
import ItemsTab from '@/scripts/admin/views/settings/customization/items/ItemsTab.vue'
|
||||
</script>
|
||||
@ -0,0 +1,453 @@
|
||||
<template>
|
||||
<h6 class="text-gray-900 text-lg font-medium">
|
||||
{{ $t(`settings.customization.${type}s.${type}_number_format`) }}
|
||||
</h6>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
{{
|
||||
$t(`settings.customization.${type}s.${type}_number_format_description`)
|
||||
}}
|
||||
</p>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full mt-6 table-fixed">
|
||||
<colgroup>
|
||||
<col style="width: 4%" />
|
||||
<col style="width: 45%" />
|
||||
<col style="width: 27%" />
|
||||
<col style="width: 24%" />
|
||||
</colgroup>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-left text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
></th>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-left text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
Component
|
||||
</th>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-left text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
Parameter
|
||||
</th>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-left text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<draggable
|
||||
v-model="selectedFields"
|
||||
class="divide-y divide-gray-200"
|
||||
item-key="id"
|
||||
tag="tbody"
|
||||
handle=".handle"
|
||||
filter=".ignore-element"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<tr class="relative">
|
||||
<td class="text-gray-300 cursor-move handle align-middle">
|
||||
<DragIcon />
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<label
|
||||
class="
|
||||
block
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
text-primary-800
|
||||
whitespace-nowrap
|
||||
mr-2
|
||||
min-w-[200px]
|
||||
"
|
||||
>
|
||||
{{ element.label }}
|
||||
</label>
|
||||
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{{ element.description }}
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-left align-middle">
|
||||
<BaseInputGroup
|
||||
:label="element.paramLabel"
|
||||
class="lg:col-span-3"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="element.value"
|
||||
:disabled="element.inputDisabled"
|
||||
:type="element.inputType"
|
||||
@update:modelValue="onUpdate($event, element)"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</td>
|
||||
|
||||
<td class="px-5 py-4 text-right align-middle pt-10">
|
||||
<BaseButton
|
||||
variant="white"
|
||||
@click.prevent="removeComponent(element)"
|
||||
>
|
||||
Remove
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="!sm:m-0"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
</BaseButton>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template #footer>
|
||||
<tr>
|
||||
<td colspan="2" class="px-5 py-4">
|
||||
<BaseInputGroup
|
||||
:label="
|
||||
$t(`settings.customization.${type}s.preview_${type}_number`)
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="nextNumber"
|
||||
disabled
|
||||
:loading="isFetchingNextNumber"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-right align-middle" colspan="2">
|
||||
<BaseDropdown wrapper-class="flex items-center justify-end mt-5">
|
||||
<template #activator>
|
||||
<BaseButton variant="primary-outline">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon :class="slotProps.class" name="PlusIcon" />
|
||||
</template>
|
||||
|
||||
{{ $t('settings.customization.add_new_component') }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<BaseDropdownItem
|
||||
v-for="field in computedFields"
|
||||
:key="field.label"
|
||||
@click.prevent="onSelectField(field)"
|
||||
>
|
||||
{{ field.label }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</draggable>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
class="mt-4"
|
||||
@click="submitForm"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" :class="slotProps.class" name="SaveIcon" />
|
||||
</template>
|
||||
{{ $t('settings.customization.save') }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive, watch } from 'vue'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import draggable from 'vuedraggable'
|
||||
import Guid from 'guid'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
|
||||
import DragIcon from '@/scripts/components/icons/DragIcon.vue'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
typeStore: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
defaultSeries: {
|
||||
type: String,
|
||||
default: 'INV',
|
||||
},
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
const selectedFields = ref([])
|
||||
const isSaving = ref(false)
|
||||
|
||||
const allFields = ref([
|
||||
{
|
||||
label: t('settings.customization.series'),
|
||||
description: t('settings.customization.series_description'),
|
||||
name: 'SERIES',
|
||||
paramLabel: t('settings.customization.series_param_label'),
|
||||
value: props.defaultSeries,
|
||||
inputDisabled: false,
|
||||
inputType: 'text',
|
||||
allowMultiple: false,
|
||||
},
|
||||
{
|
||||
label: t('settings.customization.sequence'),
|
||||
description: t('settings.customization.sequence_description'),
|
||||
name: 'SEQUENCE',
|
||||
paramLabel: t('settings.customization.sequence_param_label'),
|
||||
value: '6',
|
||||
inputDisabled: false,
|
||||
inputType: 'number',
|
||||
allowMultiple: false,
|
||||
},
|
||||
{
|
||||
label: t('settings.customization.delimiter'),
|
||||
description: t('settings.customization.delimiter_description'),
|
||||
name: 'DELIMITER',
|
||||
paramLabel: t('settings.customization.delimiter_param_label'),
|
||||
value: '-',
|
||||
inputDisabled: false,
|
||||
inputType: 'text',
|
||||
allowMultiple: true,
|
||||
},
|
||||
{
|
||||
label: t('settings.customization.customer_series'),
|
||||
description: t('settings.customization.customer_series_description'),
|
||||
name: 'CUSTOMER_SERIES',
|
||||
paramLabel: '',
|
||||
value: '',
|
||||
inputDisabled: true,
|
||||
inputType: 'text',
|
||||
allowMultiple: false,
|
||||
},
|
||||
{
|
||||
label: t('settings.customization.customer_sequence'),
|
||||
description: t('settings.customization.customer_sequence_description'),
|
||||
name: 'CUSTOMER_SEQUENCE',
|
||||
paramLabel: t('settings.customization.customer_sequence_param_label'),
|
||||
value: '6',
|
||||
inputDisabled: false,
|
||||
inputType: 'number',
|
||||
allowMultiple: false,
|
||||
},
|
||||
{
|
||||
label: t('settings.customization.date_format'),
|
||||
description: t('settings.customization.date_format_description'),
|
||||
name: 'DATE_FORMAT',
|
||||
paramLabel: t('settings.customization.date_format_param_label'),
|
||||
value: 'Y',
|
||||
inputDisabled: false,
|
||||
inputType: 'text',
|
||||
allowMultiple: true,
|
||||
},
|
||||
{
|
||||
label: t('settings.customization.random_sequence'),
|
||||
description: t('settings.customization.random_sequence_description'),
|
||||
name: 'RANDOM_SEQUENCE',
|
||||
paramLabel: t('settings.customization.random_sequence_param_label'),
|
||||
value: '6',
|
||||
inputDisabled: false,
|
||||
inputType: 'number',
|
||||
allowMultiple: false,
|
||||
},
|
||||
])
|
||||
|
||||
const computedFields = computed(() => {
|
||||
return allFields.value.filter(function (obj) {
|
||||
return !selectedFields.value.some(function (obj2) {
|
||||
if (obj.allowMultiple) {
|
||||
return false
|
||||
}
|
||||
|
||||
return obj.name == obj2.name
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const nextNumber = ref('')
|
||||
const isFetchingNextNumber = ref(false)
|
||||
const isLoadingPlaceholders = ref(false)
|
||||
|
||||
const getNumberFormat = computed(() => {
|
||||
let format = ''
|
||||
|
||||
selectedFields.value.forEach((field) => {
|
||||
let fieldString = `{{${field.name}`
|
||||
|
||||
if (field.value) {
|
||||
fieldString += `:${field.value}`
|
||||
}
|
||||
|
||||
format += `${fieldString}}}`
|
||||
})
|
||||
|
||||
return format
|
||||
})
|
||||
|
||||
watch(selectedFields, (val) => {
|
||||
fetchNextNumber()
|
||||
})
|
||||
|
||||
setInitialFields()
|
||||
|
||||
async function setInitialFields() {
|
||||
let data = {
|
||||
format: companyStore.selectedCompanySettings[`${props.type}_number_format`],
|
||||
}
|
||||
|
||||
isLoadingPlaceholders.value = true
|
||||
|
||||
let res = await globalStore.fetchPlaceholders(data)
|
||||
|
||||
res.data.placeholders.forEach((placeholder) => {
|
||||
let found = allFields.value.find((field) => {
|
||||
return field.name === placeholder.name
|
||||
})
|
||||
|
||||
const value = placeholder.value ?? ''
|
||||
|
||||
selectedFields.value.push({ ...found, value, id: Guid.raw() })
|
||||
})
|
||||
|
||||
isLoadingPlaceholders.value = false
|
||||
|
||||
fetchNextNumber()
|
||||
}
|
||||
|
||||
function isFieldAdded(field) {
|
||||
return selectedFields.value.find((v) => v.name === field.name)
|
||||
}
|
||||
|
||||
function onSelectField(field) {
|
||||
if (isFieldAdded(field) && !field.allowMultiple) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedFields.value.push({ ...field, id: Guid.raw() })
|
||||
|
||||
fetchNextNumber()
|
||||
}
|
||||
|
||||
function removeComponent(component) {
|
||||
selectedFields.value = selectedFields.value.filter(function (el) {
|
||||
return component.id !== el.id
|
||||
})
|
||||
}
|
||||
|
||||
function onUpdate(val, element) {
|
||||
switch (element.name) {
|
||||
case 'SERIES':
|
||||
if (val.length >= 6) {
|
||||
val = val.substring(0, 6)
|
||||
}
|
||||
break
|
||||
case 'DELIMITER':
|
||||
if (val.length >= 1) {
|
||||
val = val.substring(0, 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
element.value = val
|
||||
|
||||
fetchNextNumber()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const fetchNextNumber = useDebounceFn(() => {
|
||||
getNextNumber()
|
||||
}, 500)
|
||||
|
||||
async function getNextNumber() {
|
||||
if (!getNumberFormat.value) {
|
||||
nextNumber.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
let data = {
|
||||
key: props.type,
|
||||
format: getNumberFormat.value,
|
||||
}
|
||||
|
||||
isFetchingNextNumber.value = true
|
||||
|
||||
let res = await props.typeStore.getNextNumber(data)
|
||||
|
||||
isFetchingNextNumber.value = false
|
||||
|
||||
if (res.data) {
|
||||
nextNumber.value = res.data.nextNumber
|
||||
}
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (isFetchingNextNumber.value || isLoadingPlaceholders.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
let data = { settings: {} }
|
||||
|
||||
data.settings[props.type + '_number_format'] = getNumberFormat.value
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: `settings.customization.${props.type}s.${props.type}_settings_updated`,
|
||||
})
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<EstimatesTabEstimateNumber />
|
||||
|
||||
<BaseDivider class="my-8" />
|
||||
|
||||
<EstimatesTabExpiryDate />
|
||||
|
||||
<BaseDivider class="my-8" />
|
||||
|
||||
<EstimatesTabConvertEstimate />
|
||||
|
||||
<BaseDivider class="my-8" />
|
||||
|
||||
<EstimatesTabDefaultFormats />
|
||||
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
|
||||
<ul class="divide-y divide-gray-200">
|
||||
<BaseSwitchSection
|
||||
v-model="sendAsAttachmentField"
|
||||
:title="$t('settings.customization.estimates.estimate_email_attachment')"
|
||||
:description="
|
||||
$t(
|
||||
'settings.customization.estimates.estimate_email_attachment_setting_description'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, inject } from 'vue'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
|
||||
import EstimatesTabEstimateNumber from './EstimatesTabEstimateNumber.vue'
|
||||
import EstimatesTabExpiryDate from './EstimatesTabExpiryDate.vue'
|
||||
import EstimatesTabDefaultFormats from './EstimatesTabDefaultFormats.vue'
|
||||
import EstimatesTabConvertEstimate from './EstimatesTabConvertEstimate.vue'
|
||||
|
||||
const utils = inject('utils')
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const estimateSettings = reactive({
|
||||
estimate_email_attachment: null,
|
||||
})
|
||||
|
||||
utils.mergeSettings(estimateSettings, {
|
||||
...companyStore.selectedCompanySettings,
|
||||
})
|
||||
|
||||
const sendAsAttachmentField = computed({
|
||||
get: () => {
|
||||
return estimateSettings.estimate_email_attachment === 'YES'
|
||||
},
|
||||
set: async (newValue) => {
|
||||
const value = newValue ? 'YES' : 'NO'
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
estimate_email_attachment: value,
|
||||
},
|
||||
}
|
||||
|
||||
estimateSettings.estimate_email_attachment = value
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'general.setting_updated',
|
||||
})
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<h6 class="text-gray-900 text-lg font-medium">
|
||||
{{ $tc('settings.customization.estimates.convert_estimate_options') }}
|
||||
</h6>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
{{ $t('settings.customization.estimates.convert_estimate_description') }}
|
||||
</p>
|
||||
|
||||
<BaseInputGroup required>
|
||||
<BaseRadio
|
||||
id="no_action"
|
||||
v-model="settingsForm.estimate_convert_action"
|
||||
:label="$t('settings.customization.estimates.no_action')"
|
||||
size="sm"
|
||||
name="filter"
|
||||
value="no_action"
|
||||
class="mt-2"
|
||||
@update:modelValue="submitForm"
|
||||
/>
|
||||
<BaseRadio
|
||||
id="delete_estimate"
|
||||
v-model="settingsForm.estimate_convert_action"
|
||||
:label="$t('settings.customization.estimates.delete_estimate')"
|
||||
size="sm"
|
||||
name="filter"
|
||||
value="delete_estimate"
|
||||
class="my-2"
|
||||
@update:modelValue="submitForm"
|
||||
/>
|
||||
<BaseRadio
|
||||
id="mark_estimate_as_accepted"
|
||||
v-model="settingsForm.estimate_convert_action"
|
||||
:label="$t('settings.customization.estimates.mark_estimate_as_accepted')"
|
||||
size="sm"
|
||||
name="filter"
|
||||
value="mark_estimate_as_accepted"
|
||||
@update:modelValue="submitForm"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, computed, ref, inject } from 'vue'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { required, helpers } from '@vuelidate/validators'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
|
||||
const { t, tm } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
const utils = inject('utils')
|
||||
|
||||
const settingsForm = reactive({ estimate_convert_action: null })
|
||||
|
||||
utils.mergeSettings(settingsForm, {
|
||||
...companyStore.selectedCompanySettings,
|
||||
})
|
||||
|
||||
const retrospectiveEditOptions = computed(() => {
|
||||
return globalStore.config.estimate_convert_action.map((option) => {
|
||||
option.title = t(option.key)
|
||||
return option
|
||||
})
|
||||
})
|
||||
|
||||
async function submitForm() {
|
||||
let data = {
|
||||
settings: {
|
||||
...settingsForm,
|
||||
},
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.customization.estimates.estimate_settings_updated',
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<h6 class="text-gray-900 text-lg font-medium">
|
||||
{{ $t('settings.customization.estimates.default_formats') }}
|
||||
</h6>
|
||||
<p class="mt-1 text-sm text-gray-500 mb-2">
|
||||
{{ $t('settings.customization.estimates.default_formats_description') }}
|
||||
</p>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="
|
||||
$t('settings.customization.estimates.default_estimate_email_body')
|
||||
"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.estimate_mail_body"
|
||||
:fields="estimateMailFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.estimates.company_address_format')"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.estimate_company_address_format"
|
||||
:fields="companyFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.estimates.shipping_address_format')"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.estimate_shipping_address_format"
|
||||
:fields="shippingFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.estimates.billing_address_format')"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.estimate_billing_address_format"
|
||||
:fields="billingFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
class="mt-4"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" :class="slotProps.class" name="SaveIcon" />
|
||||
</template>
|
||||
{{ $t('settings.customization.save') }}
|
||||
</BaseButton>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject } from 'vue'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const utils = inject('utils')
|
||||
|
||||
const estimateMailFields = ref([
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'estimate',
|
||||
'estimateCustom',
|
||||
'company',
|
||||
])
|
||||
|
||||
const billingFields = ref([
|
||||
'billing',
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'estimateCustom',
|
||||
])
|
||||
|
||||
const shippingFields = ref([
|
||||
'shipping',
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'estimateCustom',
|
||||
])
|
||||
|
||||
const companyFields = ref(['company', 'estimateCustom'])
|
||||
|
||||
let isSaving = ref(false)
|
||||
|
||||
const formatSettings = reactive({
|
||||
estimate_mail_body: null,
|
||||
estimate_company_address_format: null,
|
||||
estimate_shipping_address_format: null,
|
||||
estimate_billing_address_format: null,
|
||||
})
|
||||
|
||||
utils.mergeSettings(formatSettings, {
|
||||
...companyStore.selectedCompanySettings,
|
||||
})
|
||||
|
||||
async function submitForm() {
|
||||
isSaving.value = true
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
...formatSettings,
|
||||
},
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.customization.estimates.estimate_settings_updated',
|
||||
})
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<NumberCustomizer
|
||||
type="estimate"
|
||||
:type-store="estimateStore"
|
||||
default-series="EST"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import NumberCustomizer from '../NumberCustomizer.vue'
|
||||
|
||||
const estimateStore = useEstimateStore()
|
||||
</script>
|
||||
@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<h6 class="text-gray-900 text-lg font-medium">
|
||||
{{ $t('settings.customization.estimates.expiry_date') }}
|
||||
</h6>
|
||||
<p class="mt-1 text-sm text-gray-500 mb-2">
|
||||
{{ $t('settings.customization.estimates.expiry_date_description') }}
|
||||
</p>
|
||||
|
||||
<BaseSwitchSection
|
||||
v-model="expiryDateAutoField"
|
||||
:title="
|
||||
$t('settings.customization.estimates.set_expiry_date_automatically')
|
||||
"
|
||||
:description="
|
||||
$t(
|
||||
'settings.customization.estimates.set_expiry_date_automatically_description'
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="expiryDateAutoField"
|
||||
:label="$t('settings.customization.estimates.expiry_date_days')"
|
||||
:error="
|
||||
v$.expiryDateSettings.estimate_expiry_date_days.$error &&
|
||||
v$.expiryDateSettings.estimate_expiry_date_days.$errors[0].$message
|
||||
"
|
||||
class="mt-2 mb-4"
|
||||
>
|
||||
<div class="w-full sm:w-1/2 md:w-1/4 lg:w-1/5">
|
||||
<BaseInput
|
||||
v-model="expiryDateSettings.estimate_expiry_date_days"
|
||||
:invalid="v$.expiryDateSettings.estimate_expiry_date_days.$error"
|
||||
type="number"
|
||||
@input="v$.expiryDateSettings.estimate_expiry_date_days.$touch()"
|
||||
/>
|
||||
</div>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
class="mt-4"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" :class="slotProps.class" name="SaveIcon" />
|
||||
</template>
|
||||
{{ $t('settings.customization.save') }}
|
||||
</BaseButton>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, reactive, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { numeric, helpers, requiredIf } from '@vuelidate/validators'
|
||||
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const utils = inject('utils')
|
||||
|
||||
let isSaving = ref(false)
|
||||
|
||||
const expiryDateSettings = reactive({
|
||||
estimate_set_expiry_date_automatically: null,
|
||||
estimate_expiry_date_days: null,
|
||||
})
|
||||
|
||||
utils.mergeSettings(expiryDateSettings, {
|
||||
...companyStore.selectedCompanySettings,
|
||||
})
|
||||
|
||||
const expiryDateAutoField = computed({
|
||||
get: () => {
|
||||
return expiryDateSettings.estimate_set_expiry_date_automatically === 'YES'
|
||||
},
|
||||
set: async (newValue) => {
|
||||
const value = newValue ? 'YES' : 'NO'
|
||||
|
||||
expiryDateSettings.estimate_set_expiry_date_automatically = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
expiryDateSettings: {
|
||||
estimate_expiry_date_days: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(expiryDateAutoField.value)
|
||||
),
|
||||
numeric: helpers.withMessage(t('validation.numbers_only'), numeric),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(rules, { expiryDateSettings })
|
||||
|
||||
async function submitForm() {
|
||||
v$.value.expiryDateSettings.$touch()
|
||||
|
||||
if (v$.value.expiryDateSettings.$invalid) {
|
||||
return false
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
...expiryDateSettings,
|
||||
},
|
||||
}
|
||||
// Don't pass expiry_date_days if setting is not enabled
|
||||
|
||||
if (!expiryDateAutoField.value) {
|
||||
delete data.settings.estimate_expiry_date_days
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.customization.estimates.estimate_settings_updated',
|
||||
})
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<InvoicesTabInvoiceNumber />
|
||||
|
||||
<BaseDivider class="my-8" />
|
||||
|
||||
<InvoicesTabDueDate />
|
||||
|
||||
<BaseDivider class="my-8" />
|
||||
|
||||
<InvoicesTabRetrospective />
|
||||
|
||||
<BaseDivider class="my-8" />
|
||||
|
||||
<InvoicesTabDefaultFormats />
|
||||
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
|
||||
<ul class="divide-y divide-gray-200">
|
||||
<BaseSwitchSection
|
||||
v-model="sendAsAttachmentField"
|
||||
:title="$t('settings.customization.invoices.invoice_email_attachment')"
|
||||
:description="
|
||||
$t(
|
||||
'settings.customization.invoices.invoice_email_attachment_setting_description'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, inject } from 'vue'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import InvoicesTabInvoiceNumber from './InvoicesTabInvoiceNumber.vue'
|
||||
import InvoicesTabRetrospective from './InvoicesTabRetrospective.vue'
|
||||
import InvoicesTabDueDate from './InvoicesTabDueDate.vue'
|
||||
import InvoicesTabDefaultFormats from './InvoicesTabDefaultFormats.vue'
|
||||
|
||||
const utils = inject('utils')
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const invoiceSettings = reactive({
|
||||
invoice_email_attachment: null,
|
||||
})
|
||||
|
||||
utils.mergeSettings(invoiceSettings, {
|
||||
...companyStore.selectedCompanySettings,
|
||||
})
|
||||
|
||||
const sendAsAttachmentField = computed({
|
||||
get: () => {
|
||||
return invoiceSettings.invoice_email_attachment === 'YES'
|
||||
},
|
||||
set: async (newValue) => {
|
||||
const value = newValue ? 'YES' : 'NO'
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
invoice_email_attachment: value,
|
||||
},
|
||||
}
|
||||
|
||||
invoiceSettings.invoice_email_attachment = value
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'general.setting_updated',
|
||||
})
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<h6 class="text-gray-900 text-lg font-medium">
|
||||
{{ $t('settings.customization.invoices.default_formats') }}
|
||||
</h6>
|
||||
<p class="mt-1 text-sm text-gray-500 mb-2">
|
||||
{{ $t('settings.customization.invoices.default_formats_description') }}
|
||||
</p>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.invoices.default_invoice_email_body')"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.invoice_mail_body"
|
||||
:fields="invoiceMailFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.invoices.company_address_format')"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.invoice_company_address_format"
|
||||
:fields="companyFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.invoices.shipping_address_format')"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.invoice_shipping_address_format"
|
||||
:fields="shippingFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.invoices.billing_address_format')"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.invoice_billing_address_format"
|
||||
:fields="billingFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
class="mt-4"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" :class="slotProps.class" name="SaveIcon" />
|
||||
</template>
|
||||
{{ $t('settings.customization.save') }}
|
||||
</BaseButton>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject } from 'vue'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const utils = inject('utils')
|
||||
|
||||
const invoiceMailFields = ref([
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'invoice',
|
||||
'invoiceCustom',
|
||||
'company',
|
||||
])
|
||||
|
||||
const billingFields = ref([
|
||||
'billing',
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'invoiceCustom',
|
||||
])
|
||||
|
||||
const shippingFields = ref([
|
||||
'shipping',
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'invoiceCustom',
|
||||
])
|
||||
|
||||
const companyFields = ref(['company', 'invoiceCustom'])
|
||||
|
||||
let isSaving = ref(false)
|
||||
|
||||
const formatSettings = reactive({
|
||||
invoice_mail_body: null,
|
||||
invoice_company_address_format: null,
|
||||
invoice_shipping_address_format: null,
|
||||
invoice_billing_address_format: null,
|
||||
})
|
||||
|
||||
utils.mergeSettings(formatSettings, {
|
||||
...companyStore.selectedCompanySettings,
|
||||
})
|
||||
|
||||
async function submitForm() {
|
||||
isSaving.value = true
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
...formatSettings,
|
||||
},
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.customization.invoices.invoice_settings_updated',
|
||||
})
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<h6 class="text-gray-900 text-lg font-medium">
|
||||
{{ $t('settings.customization.invoices.due_date') }}
|
||||
</h6>
|
||||
<p class="mt-1 text-sm text-gray-500 mb-2">
|
||||
{{ $t('settings.customization.invoices.due_date_description') }}
|
||||
</p>
|
||||
|
||||
<BaseSwitchSection
|
||||
v-model="dueDateAutoField"
|
||||
:title="$t('settings.customization.invoices.set_due_date_automatically')"
|
||||
:description="
|
||||
$t(
|
||||
'settings.customization.invoices.set_due_date_automatically_description'
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="dueDateAutoField"
|
||||
:label="$t('settings.customization.invoices.due_date_days')"
|
||||
:error="
|
||||
v$.dueDateSettings.invoice_due_date_days.$error &&
|
||||
v$.dueDateSettings.invoice_due_date_days.$errors[0].$message
|
||||
"
|
||||
class="mt-2 mb-4"
|
||||
>
|
||||
<div class="w-full sm:w-1/2 md:w-1/4 lg:w-1/5">
|
||||
<BaseInput
|
||||
v-model="dueDateSettings.invoice_due_date_days"
|
||||
:invalid="v$.dueDateSettings.invoice_due_date_days.$error"
|
||||
type="number"
|
||||
@input="v$.dueDateSettings.invoice_due_date_days.$touch()"
|
||||
/>
|
||||
</div>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
class="mt-4"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" :class="slotProps.class" name="SaveIcon" />
|
||||
</template>
|
||||
{{ $t('settings.customization.save') }}
|
||||
</BaseButton>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, reactive, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { numeric, helpers, requiredIf } from '@vuelidate/validators'
|
||||
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const utils = inject('utils')
|
||||
|
||||
let isSaving = ref(false)
|
||||
|
||||
const dueDateSettings = reactive({
|
||||
invoice_set_due_date_automatically: null,
|
||||
invoice_due_date_days: null,
|
||||
})
|
||||
|
||||
utils.mergeSettings(dueDateSettings, {
|
||||
...companyStore.selectedCompanySettings,
|
||||
})
|
||||
|
||||
const dueDateAutoField = computed({
|
||||
get: () => {
|
||||
return dueDateSettings.invoice_set_due_date_automatically === 'YES'
|
||||
},
|
||||
set: async (newValue) => {
|
||||
const value = newValue ? 'YES' : 'NO'
|
||||
|
||||
dueDateSettings.invoice_set_due_date_automatically = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
dueDateSettings: {
|
||||
invoice_due_date_days: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(dueDateAutoField.value)
|
||||
),
|
||||
numeric: helpers.withMessage(t('validation.numbers_only'), numeric),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(rules, { dueDateSettings })
|
||||
|
||||
async function submitForm() {
|
||||
v$.value.dueDateSettings.$touch()
|
||||
|
||||
if (v$.value.dueDateSettings.$invalid) {
|
||||
return false
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
...dueDateSettings,
|
||||
},
|
||||
}
|
||||
// Don't pass due_date_days if setting is not enabled
|
||||
|
||||
if (!dueDateAutoField.value) {
|
||||
delete data.settings.invoice_due_date_days
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.customization.invoices.invoice_settings_updated',
|
||||
})
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<NumberCustomizer
|
||||
type="invoice"
|
||||
:type-store="invoiceStore"
|
||||
default-series="INV"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import NumberCustomizer from '../NumberCustomizer.vue'
|
||||
|
||||
const invoiceStore = useInvoiceStore()
|
||||
</script>
|
||||
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<h6 class="text-gray-900 text-lg font-medium">
|
||||
{{ $tc('settings.customization.invoices.retrospective_edits') }}
|
||||
</h6>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
{{ $t('settings.customization.invoices.retrospective_edits_description') }}
|
||||
</p>
|
||||
|
||||
<BaseInputGroup required>
|
||||
<BaseRadio
|
||||
id="allow"
|
||||
v-model="settingsForm.retrospective_edits"
|
||||
:label="$t('settings.customization.invoices.allow')"
|
||||
size="sm"
|
||||
name="filter"
|
||||
value="allow"
|
||||
class="mt-2"
|
||||
@update:modelValue="submitForm"
|
||||
/>
|
||||
|
||||
<BaseRadio
|
||||
id="disable_on_invoice_partial_paid"
|
||||
v-model="settingsForm.retrospective_edits"
|
||||
:label="
|
||||
$t('settings.customization.invoices.disable_on_invoice_partial_paid')
|
||||
"
|
||||
size="sm"
|
||||
name="filter"
|
||||
value="disable_on_invoice_partial_paid"
|
||||
class="mt-2"
|
||||
@update:modelValue="submitForm"
|
||||
/>
|
||||
<BaseRadio
|
||||
id="disable_on_invoice_paid"
|
||||
v-model="settingsForm.retrospective_edits"
|
||||
:label="$t('settings.customization.invoices.disable_on_invoice_paid')"
|
||||
size="sm"
|
||||
name="filter"
|
||||
value="disable_on_invoice_paid"
|
||||
class="my-2"
|
||||
@update:modelValue="submitForm"
|
||||
/>
|
||||
<BaseRadio
|
||||
id="disable_on_invoice_sent"
|
||||
v-model="settingsForm.retrospective_edits"
|
||||
:label="$t('settings.customization.invoices.disable_on_invoice_sent')"
|
||||
size="sm"
|
||||
name="filter"
|
||||
value="disable_on_invoice_sent"
|
||||
@update:modelValue="submitForm"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, computed, ref, inject } from 'vue'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
|
||||
const { t, tm } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const utils = inject('utils')
|
||||
|
||||
const settingsForm = reactive({ retrospective_edits: null })
|
||||
|
||||
utils.mergeSettings(settingsForm, {
|
||||
...companyStore.selectedCompanySettings,
|
||||
})
|
||||
|
||||
const retrospectiveEditOptions = computed(() => {
|
||||
return globalStore.config.retrospective_edits.map((option) => {
|
||||
option.title = t(option.key)
|
||||
return option
|
||||
})
|
||||
})
|
||||
|
||||
async function submitForm() {
|
||||
let data = {
|
||||
settings: {
|
||||
...settingsForm,
|
||||
},
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.customization.invoices.invoice_settings_updated',
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<ItemUnitModal />
|
||||
|
||||
<div class="flex flex-wrap justify-end mt-2 lg:flex-nowrap">
|
||||
<BaseButton variant="primary-outline" @click="addItemUnit">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon :class="slotProps.class" name="PlusIcon" />
|
||||
</template>
|
||||
{{ $t('settings.customization.items.add_item_unit') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<BaseTable ref="table" class="mt-10" :data="fetchData" :columns="columns">
|
||||
<template #cell-actions="{ row }">
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<div class="inline-block">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="text-gray-500" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<BaseDropdownItem @click="editItemUnit(row)">
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
<BaseDropdownItem @click="removeItemUnit(row)">
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useItemStore } from '@/scripts/admin/stores/item'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import ItemUnitModal from '@/scripts/admin/components/modal-components/ItemUnitModal.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const table = ref(null)
|
||||
|
||||
const itemStore = useItemStore()
|
||||
const modalStore = useModalStore()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
const columns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('settings.customization.items.unit_name'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'text-right text-sm font-medium',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
async function fetchData({ page, filter, sort }) {
|
||||
let data = {
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page,
|
||||
}
|
||||
let response = await itemStore.fetchItemUnits(data)
|
||||
|
||||
return {
|
||||
data: response.data.data,
|
||||
pagination: {
|
||||
totalPages: response.data.meta.last_page,
|
||||
currentPage: page,
|
||||
totalCount: response.data.meta.total,
|
||||
limit: 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function addItemUnit() {
|
||||
modalStore.openModal({
|
||||
title: t('settings.customization.items.add_item_unit'),
|
||||
componentName: 'ItemUnitModal',
|
||||
refreshData: table.value.refresh,
|
||||
size: 'sm',
|
||||
})
|
||||
}
|
||||
|
||||
async function editItemUnit(row) {
|
||||
itemStore.fetchItemUnit(row.data.id)
|
||||
modalStore.openModal({
|
||||
title: t('settings.customization.items.edit_item_unit'),
|
||||
componentName: 'ItemUnitModal',
|
||||
id: row.data.id,
|
||||
data: row.data,
|
||||
refreshData: table.value && table.value.refresh,
|
||||
})
|
||||
}
|
||||
|
||||
function removeItemUnit(row) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.customization.items.item_unit_confirm_delete'),
|
||||
yesLabel: t('general.yes'),
|
||||
noLabel: t('general.no'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
await itemStore.deleteItemUnit(row.data.id)
|
||||
table.value && table.value.refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<PaymentsTabPaymentNumber />
|
||||
|
||||
<BaseDivider class="my-8" />
|
||||
|
||||
<PaymentsTabDefaultFormats />
|
||||
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
|
||||
<ul class="divide-y divide-gray-200">
|
||||
<BaseSwitchSection
|
||||
v-model="sendAsAttachmentField"
|
||||
:title="$t('settings.customization.payments.payment_email_attachment')"
|
||||
:description="
|
||||
$t(
|
||||
'settings.customization.payments.payment_email_attachment_setting_description'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, inject } from 'vue'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import PaymentsTabPaymentNumber from './PaymentsTabPaymentNumber.vue'
|
||||
import PaymentsTabDefaultFormats from './PaymentsTabDefaultFormats.vue'
|
||||
|
||||
const utils = inject('utils')
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const paymentSettings = reactive({
|
||||
payment_email_attachment: null,
|
||||
})
|
||||
|
||||
utils.mergeSettings(paymentSettings, {
|
||||
...companyStore.selectedCompanySettings,
|
||||
})
|
||||
|
||||
const sendAsAttachmentField = computed({
|
||||
get: () => {
|
||||
return paymentSettings.payment_email_attachment === 'YES'
|
||||
},
|
||||
set: async (newValue) => {
|
||||
const value = newValue ? 'YES' : 'NO'
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
payment_email_attachment: value,
|
||||
},
|
||||
}
|
||||
|
||||
paymentSettings.payment_email_attachment = value
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'general.setting_updated',
|
||||
})
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<h6 class="text-gray-900 text-lg font-medium">
|
||||
{{ $t('settings.customization.payments.default_formats') }}
|
||||
</h6>
|
||||
<p class="mt-1 text-sm text-gray-500 mb-2">
|
||||
{{ $t('settings.customization.payments.default_formats_description') }}
|
||||
</p>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.payments.default_payment_email_body')"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.payment_mail_body"
|
||||
:fields="mailFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.payments.company_address_format')"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.payment_company_address_format"
|
||||
:fields="companyFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="
|
||||
$t('settings.customization.payments.from_customer_address_format')
|
||||
"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.payment_from_customer_address_format"
|
||||
:fields="customerAddressFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
class="mt-4"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" :class="slotProps.class" name="SaveIcon" />
|
||||
</template>
|
||||
{{ $t('settings.customization.save') }}
|
||||
</BaseButton>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject } from 'vue'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const utils = inject('utils')
|
||||
|
||||
const mailFields = ref([
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'company',
|
||||
'payment',
|
||||
'paymentCustom',
|
||||
])
|
||||
|
||||
const customerAddressFields = ref([
|
||||
'billing',
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'paymentCustom',
|
||||
])
|
||||
|
||||
const companyFields = ref(['company', 'paymentCustom'])
|
||||
|
||||
let isSaving = ref(false)
|
||||
|
||||
const formatSettings = reactive({
|
||||
payment_mail_body: null,
|
||||
payment_company_address_format: null,
|
||||
payment_from_customer_address_format: null,
|
||||
})
|
||||
|
||||
utils.mergeSettings(formatSettings, {
|
||||
...companyStore.selectedCompanySettings,
|
||||
})
|
||||
|
||||
async function submitForm() {
|
||||
isSaving.value = true
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
...formatSettings,
|
||||
},
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.customization.payments.payment_settings_updated',
|
||||
})
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<NumberCustomizer
|
||||
type="payment"
|
||||
:type-store="paymentStore"
|
||||
default-series="PAY"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { usePaymentStore } from '@/scripts/admin/stores/payment'
|
||||
import NumberCustomizer from '../NumberCustomizer.vue'
|
||||
|
||||
const paymentStore = usePaymentStore()
|
||||
</script>
|
||||
@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveEmailConfig">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.driver')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.basicMailConfig.mail_driver.$error &&
|
||||
v$.basicMailConfig.mail_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="mailDriverStore.basicMailConfig.mail_driver"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="mailDrivers"
|
||||
:can-deselect="false"
|
||||
:invalid="v$.basicMailConfig.mail_driver.$error"
|
||||
@update:modelValue="onChangeDriver"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.from_mail')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.basicMailConfig.from_mail.$error &&
|
||||
v$.basicMailConfig.from_mail.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.basicMailConfig.from_mail"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="from_mail"
|
||||
:invalid="v$.basicMailConfig.from_mail.$error"
|
||||
@input="v$.basicMailConfig.from_mail.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.from_name')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.basicMailConfig.from_name.$error &&
|
||||
v$.basicMailConfig.from_name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.basicMailConfig.from_name"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.basicMailConfig.from_name.$error"
|
||||
@input="v$.basicMailConfig.from_name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div class="flex mt-8">
|
||||
<BaseButton
|
||||
:content-loading="isFetchingInitialData"
|
||||
:disabled="isSaving"
|
||||
:loading="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" :class="slotProps.class" name="SaveIcon" />
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
<slot />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
|
||||
|
||||
const props = defineProps({
|
||||
configData: {
|
||||
type: Object,
|
||||
require: true,
|
||||
default: Object,
|
||||
},
|
||||
isSaving: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
isFetchingInitialData: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
mailDrivers: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['submit-data', 'on-change-driver'])
|
||||
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
basicMailConfig: {
|
||||
mail_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
from_mail: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
from_name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => mailDriverStore)
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
for (const key in mailDriverStore.basicMailConfig) {
|
||||
if (props.configData.hasOwnProperty(key)) {
|
||||
mailDriverStore.$patch((state) => {
|
||||
state.basicMailConfig[key] = props.configData[key]
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function saveEmailConfig() {
|
||||
v$.value.basicMailConfig.$touch()
|
||||
if (!v$.value.basicMailConfig.$invalid) {
|
||||
emit('submit-data', mailDriverStore.basicMailConfig)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
v$.value.basicMailConfig.mail_driver.$touch()
|
||||
emit('on-change-driver', mailDriverStore.basicMailConfig.mail_driver)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveEmailConfig">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.driver')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.mailgunConfig.mail_driver.$error &&
|
||||
v$.mailgunConfig.mail_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="mailDriverStore.mailgunConfig.mail_driver"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="mailDrivers"
|
||||
:can-deselect="false"
|
||||
:invalid="v$.mailgunConfig.mail_driver.$error"
|
||||
@update:modelValue="onChangeDriver"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.mailgun_domain')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.mailgunConfig.mail_mailgun_domain.$error &&
|
||||
v$.mailgunConfig.mail_mailgun_domain.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.mailgunConfig.mail_mailgun_domain"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="mailgun_domain"
|
||||
:invalid="v$.mailgunConfig.mail_mailgun_domain.$error"
|
||||
@input="v$.mailgunConfig.mail_mailgun_domain.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.mailgun_secret')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.mailgunConfig.mail_mailgun_secret.$error &&
|
||||
v$.mailgunConfig.mail_mailgun_secret.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.mailgunConfig.mail_mailgun_secret"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:type="getInputType"
|
||||
name="mailgun_secret"
|
||||
autocomplete="off"
|
||||
:invalid="v$.mailgunConfig.mail_mailgun_secret.$error"
|
||||
@input="v$.mailgunConfig.mail_mailgun_secret.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<BaseIcon
|
||||
v-if="isShowPassword"
|
||||
class="mr-1 text-gray-500 cursor-pointer"
|
||||
name="EyeOffIcon"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="mr-1 text-gray-500 cursor-pointer"
|
||||
name="EyeIcon"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/>
|
||||
</template>
|
||||
</BaseInput>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.mailgun_endpoint')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.mailgunConfig.mail_mailgun_endpoint.$error &&
|
||||
v$.mailgunConfig.mail_mailgun_endpoint.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.mailgunConfig.mail_mailgun_endpoint"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="mailgun_endpoint"
|
||||
:invalid="v$.mailgunConfig.mail_mailgun_endpoint.$error"
|
||||
@input="v$.mailgunConfig.mail_mailgun_endpoint.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.from_mail')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.mailgunConfig.from_mail.$error &&
|
||||
v$.mailgunConfig.from_mail.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.mailgunConfig.from_mail"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="from_mail"
|
||||
:invalid="v$.mailgunConfig.from_mail.$error"
|
||||
@input="v$.mailgunConfig.from_mail.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.from_name')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.mailgunConfig.from_name.$error &&
|
||||
v$.mailgunConfig.from_name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.mailgunConfig.from_name"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="from_name"
|
||||
:invalid="v$.mailgunConfig.from_name.$error"
|
||||
@input="v$.mailgunConfig.from_name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div class="flex my-10">
|
||||
<BaseButton
|
||||
:disabled="isSaving"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:loading="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
<slot />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
|
||||
|
||||
const props = defineProps({
|
||||
configData: {
|
||||
type: Object,
|
||||
require: true,
|
||||
default: Object,
|
||||
},
|
||||
isSaving: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
isFetchingInitialData: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
mailDrivers: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['submit-data', 'on-change-driver'])
|
||||
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isShowPassword = ref(false)
|
||||
|
||||
const getInputType = computed(() => {
|
||||
if (isShowPassword.value) {
|
||||
return 'text'
|
||||
}
|
||||
return 'password'
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
mailgunConfig: {
|
||||
mail_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
mail_mailgun_domain: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
mail_mailgun_endpoint: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
mail_mailgun_secret: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
from_mail: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email,
|
||||
},
|
||||
from_name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => mailDriverStore)
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
for (const key in mailDriverStore.mailgunConfig) {
|
||||
if (props.configData.hasOwnProperty(key)) {
|
||||
mailDriverStore.mailgunConfig[key] = props.configData[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function saveEmailConfig() {
|
||||
v$.value.mailgunConfig.$touch()
|
||||
if (!v$.value.mailgunConfig.$invalid) {
|
||||
emit('submit-data', mailDriverStore.mailgunConfig)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
v$.value.mailgunConfig.mail_driver.$touch()
|
||||
emit('on-change-driver', mailDriverStore.mailgunConfig.mail_driver)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveEmailConfig">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.driver')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.sesConfig.mail_driver.$error &&
|
||||
v$.sesConfig.mail_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="mailDriverStore.sesConfig.mail_driver"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="mailDrivers"
|
||||
:can-deselect="false"
|
||||
:invalid="v$.sesConfig.mail_driver.$error"
|
||||
@update:modelValue="onChangeDriver"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.host')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.sesConfig.mail_host.$error &&
|
||||
v$.sesConfig.mail_host.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.sesConfig.mail_host"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="mail_host"
|
||||
:invalid="v$.sesConfig.mail_host.$error"
|
||||
@input="v$.sesConfig.mail_host.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.port')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.sesConfig.mail_port.$error &&
|
||||
v$.sesConfig.mail_port.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.sesConfig.mail_port"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="mail_port"
|
||||
:invalid="v$.sesConfig.mail_port.$error"
|
||||
@input="v$.sesConfig.mail_port.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.encryption')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.sesConfig.mail_encryption.$error &&
|
||||
v$.sesConfig.mail_encryption.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model.trim="mailDriverStore.sesConfig.mail_encryption"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="encryptions"
|
||||
:invalid="v$.sesConfig.mail_encryption.$error"
|
||||
placeholder="Select option"
|
||||
@input="v$.sesConfig.mail_encryption.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.from_mail')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.sesConfig.from_mail.$error &&
|
||||
v$.sesConfig.from_mail.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.sesConfig.from_mail"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="from_mail"
|
||||
:invalid="v$.sesConfig.from_mail.$error"
|
||||
@input="v$.sesConfig.from_mail.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.from_name')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.sesConfig.from_name.$error &&
|
||||
v$.sesConfig.from_name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.sesConfig.from_name"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.sesConfig.from_name.$error"
|
||||
@input="v$.sesConfig.from_name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.ses_key')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.sesConfig.mail_ses_key.$error &&
|
||||
v$.sesConfig.mail_ses_key.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.sesConfig.mail_ses_key"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="mail_ses_key"
|
||||
:invalid="v$.sesConfig.mail_ses_key.$error"
|
||||
@input="v$.sesConfig.mail_ses_key.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.ses_secret')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.sesConfig.mail_ses_secret.$error &&
|
||||
v$.mail_ses_secret.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.sesConfig.mail_ses_secret"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:type="getInputType"
|
||||
name="mail_ses_secret"
|
||||
autocomplete="off"
|
||||
:invalid="v$.sesConfig.mail_ses_secret.$error"
|
||||
@input="v$.sesConfig.mail_ses_secret.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<BaseIcon
|
||||
v-if="isShowPassword"
|
||||
class="mr-1 text-gray-500 cursor-pointer"
|
||||
name="EyeOffIcon"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="mr-1 text-gray-500 cursor-pointer"
|
||||
name="EyeIcon"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/>
|
||||
</template>
|
||||
</BaseInput>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
|
||||
<div class="flex my-10">
|
||||
<BaseButton
|
||||
:disabled="isSaving"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:loading="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
<slot />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { required, email, numeric, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
|
||||
|
||||
const props = defineProps({
|
||||
configData: {
|
||||
type: Object,
|
||||
require: true,
|
||||
default: Object,
|
||||
},
|
||||
isSaving: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
isFetchingInitialData: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
mailDrivers: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['submit-data', 'on-change-driver'])
|
||||
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isShowPassword = ref(false)
|
||||
const encryptions = reactive(['tls', 'ssl', 'starttls'])
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
sesConfig: {
|
||||
mail_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
mail_host: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
mail_port: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
numeric,
|
||||
},
|
||||
mail_ses_key: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
mail_ses_secret: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
mail_encryption: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
from_mail: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
from_name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => mailDriverStore)
|
||||
)
|
||||
|
||||
const getInputType = computed(() => {
|
||||
if (isShowPassword.value) {
|
||||
return 'text'
|
||||
}
|
||||
return 'password'
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
for (const key in mailDriverStore.sesConfig) {
|
||||
if (props.configData.hasOwnProperty(key)) {
|
||||
mailDriverStore.sesConfig[key] = props.configData[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function saveEmailConfig() {
|
||||
v$.value.sesConfig.$touch()
|
||||
if (!v$.value.sesConfig.$invalid) {
|
||||
emit('submit-data', mailDriverStore.sesConfig)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
v$.value.sesConfig.mail_driver.$touch()
|
||||
emit('on-change-driver', mailDriverStore.sesConfig.mail_driver)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveEmailConfig">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.driver')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.smtpConfig.mail_driver.$error &&
|
||||
v$.smtpConfig.mail_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="mailDriverStore.smtpConfig.mail_driver"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="mailDrivers"
|
||||
:can-deselect="false"
|
||||
:invalid="v$.smtpConfig.mail_driver.$error"
|
||||
@update:modelValue="onChangeDriver"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.host')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.smtpConfig.mail_host.$error &&
|
||||
v$.smtpConfig.mail_host.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.smtpConfig.mail_host"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="mail_host"
|
||||
:invalid="v$.smtpConfig.mail_host.$error"
|
||||
@input="v$.smtpConfig.mail_host.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:content-loading="isFetchingInitialData"
|
||||
:label="$t('settings.mail.username')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.smtpConfig.mail_username"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="db_name"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:content-loading="isFetchingInitialData"
|
||||
:label="$t('settings.mail.password')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.smtpConfig.mail_password"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:type="getInputType"
|
||||
name="password"
|
||||
>
|
||||
<template #right>
|
||||
<BaseIcon
|
||||
v-if="isShowPassword"
|
||||
class="mr-1 text-gray-500 cursor-pointer"
|
||||
name="EyeOffIcon"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="mr-1 text-gray-500 cursor-pointer"
|
||||
name="EyeIcon"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/>
|
||||
</template>
|
||||
</BaseInput>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.port')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.smtpConfig.mail_port.$error &&
|
||||
v$.smtpConfig.mail_port.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.smtpConfig.mail_port"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="mail_port"
|
||||
:invalid="v$.smtpConfig.mail_port.$error"
|
||||
@input="v$.smtpConfig.mail_port.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.encryption')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.smtpConfig.mail_encryption.$error &&
|
||||
v$.smtpConfig.mail_encryption.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model.trim="mailDriverStore.smtpConfig.mail_encryption"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="encryptions"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
placeholder="Select option"
|
||||
:invalid="v$.smtpConfig.mail_encryption.$error"
|
||||
@input="v$.smtpConfig.mail_encryption.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.from_mail')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.smtpConfig.from_mail.$error &&
|
||||
v$.smtpConfig.from_mail.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.smtpConfig.from_mail"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="from_mail"
|
||||
:invalid="v$.smtpConfig.from_mail.$error"
|
||||
@input="v$.smtpConfig.from_mail.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.mail.from_name')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.smtpConfig.from_name.$error &&
|
||||
v$.smtpConfig.from_name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="mailDriverStore.smtpConfig.from_name"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="from_name"
|
||||
:invalid="v$.smtpConfig.from_name.$error"
|
||||
@input="v$.smtpConfig.from_name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
|
||||
<div class="flex my-10">
|
||||
<BaseButton
|
||||
:disabled="isSaving"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:loading="isSaving"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
<slot />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, onMounted, ref, computed } from 'vue'
|
||||
import { required, email, numeric, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
|
||||
|
||||
const props = defineProps({
|
||||
configData: {
|
||||
type: Object,
|
||||
require: true,
|
||||
default: Object,
|
||||
},
|
||||
isSaving: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
isFetchingInitialData: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
mailDrivers: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['submit-data', 'on-change-driver'])
|
||||
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isShowPassword = ref(false)
|
||||
const encryptions = reactive(['tls', 'ssl', 'starttls'])
|
||||
|
||||
const getInputType = computed(() => {
|
||||
if (isShowPassword.value) {
|
||||
return 'text'
|
||||
}
|
||||
return 'password'
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
smtpConfig: {
|
||||
mail_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
mail_host: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
mail_port: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
numeric: helpers.withMessage(t('validation.numbers_only'), numeric),
|
||||
},
|
||||
mail_encryption: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
from_mail: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
from_name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => mailDriverStore)
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
for (const key in mailDriverStore.smtpConfig) {
|
||||
if (props.configData.hasOwnProperty(key)) {
|
||||
mailDriverStore.smtpConfig[key] = props.configData[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function saveEmailConfig() {
|
||||
v$.value.smtpConfig.$touch()
|
||||
if (!v$.value.smtpConfig.$invalid) {
|
||||
emit('submit-data', mailDriverStore.smtpConfig)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
v$.value.smtpConfig.mail_driver.$touch()
|
||||
emit('on-change-driver', mailDriverStore.smtpConfig.mail_driver)
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user