build version 400

This commit is contained in:
Mohit Panjwani
2020-12-02 17:54:08 +05:30
parent 326508e567
commit 89ee58590c
963 changed files with 62887 additions and 48868 deletions

View File

@ -0,0 +1,233 @@
<template>
<div class="relative setting-main-container backup">
<sw-card variant="setting-card">
<div slot="header" class="flex flex-wrap justify-between lg:flex-no-wrap">
<div>
<h6 class="sw-section-title">
{{ $tc('settings.backup.title', 1) }}
</h6>
<p
class="mt-2 text-sm leading-snug text-gray-500"
style="max-width: 680px"
>
{{ $t('settings.backup.description') }}
</p>
</div>
<div class="mt-4 lg:mt-0 lg:ml-2">
<sw-button
variant="primary-outline"
size="lg"
@click="onCreateNewBackup"
>
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('settings.backup.new_backup') }}
</sw-button>
</div>
</div>
<div class="grid mb-8 md:grid-cols-3">
<sw-input-group :label="$t('settings.disk.select_disk')">
<sw-select
v-model="filters.selected_disk"
:options="getDisks"
:searchable="true"
:show-labels="false"
:placeholder="$t('settings.disk.select_disk')"
:allow-empty="false"
track-by="id"
label="name"
:custom-label="getCustomLabel"
@select="refreshTable"
/>
</sw-input-group>
</div>
<sw-table-component
ref="table"
variant="gray"
:show-filter="false"
:data="fetchBackupsData"
>
<sw-table-column :label="$t('settings.backup.path')" show="path">
<template slot-scope="row">
<span>{{ $t('settings.backup.path') }}</span>
<span class="mt-6">{{ row.path }}</span>
</template>
</sw-table-column>
<sw-table-column
:label="$t('settings.backup.created_at')"
show="created_at"
/>
<sw-table-column :label="$t('settings.backup.size')" show="size" />
<sw-table-column
:sortable="false"
:filterable="false"
:data="fetchBackupsData"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span>{{ $t('settings.backup.action') }}</span>
<sw-dropdown>
<dot-icon slot="activator" />
<sw-dropdown-item @click="onDownloadBckup(row)">
<cloud-download-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.download') }}
</sw-dropdown-item>
<sw-dropdown-item @click="onRemoveBackup(row)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</sw-table-column>
</sw-table-component>
</sw-card>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import { TrashIcon, CloudDownloadIcon, PlusIcon } from '@vue-hero-icons/solid'
export default {
components: {
PlusIcon,
TrashIcon,
CloudDownloadIcon,
},
data() {
return {
isRequestOngoing: true,
filters: {
selected_disk: { driver: 'local' },
},
}
},
computed: {
...mapGetters('disks', ['getDisks']),
},
created() {
this.loadDisksData()
},
methods: {
...mapActions('backup', ['fetchBackups', 'downloadBackup', 'removeBackup']),
...mapActions('disks', ['fetchDisks']),
...mapActions('modal', ['openModal']),
getCustomLabel({ driver, name }) {
if (!name) {
return
}
return `${name} — [${driver}]`
},
async onRemoveBackup(backup) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('settings.backup.backup_confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
}).then(async (value) => {
if (value) {
let data = {
disk: this.filters.selected_disk.driver,
file_disk_id: this.filters.selected_disk.id,
path: backup.path,
}
let response = await this.removeBackup(data)
if (response.data.success) {
window.toastr['success'](this.$t('settings.backup.deleted_message'))
this.$refs.table.refresh()
return true
}
}
})
},
async loadDisksData() {
this.isRequestOngoing = true
let res = await this.fetchDisks({ limit: 'all' })
this.filters.selected_disk = res.data.disks.data.find(
(disk) => disk.set_as_default == 1
)
this.isRequestOngoing = false
},
async fetchBackupsData({ page, filter, sort }) {
let data = {
disk: this.filters.selected_disk.driver,
file_disk_id: this.filters.selected_disk.id,
}
this.isRequestOngoing = true
let response = await this.fetchBackups(data)
if (response.data.error) {
window.toastr['error'](
this.$t('settings.backup.' + response.data.error)
)
}
this.isRequestOngoing = false
return {
data: response.data.backups,
pagination: {
totalPages: 1,
currentPage: 1,
},
}
this.$refs.table.refresh()
},
refreshTable() {
setTimeout(() => {
this.$refs.table.refresh()
}, 100)
},
async onCreateNewBackup() {
this.openModal({
title: this.$t('settings.backup.create_backup'),
componentName: 'BackupModal',
refreshData: this.refreshTable,
})
},
async onDownloadBckup(backup) {
this.isRequestOngoing = true
window
.axios({
method: 'GET',
url: '/api/v1/download-backup',
responseType: 'blob', // important
params: {
disk: this.filters.selected_disk.driver,
file_disk_id: this.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()
this.isRequestOngoing = false
})
.catch((e) => {
this.isRequestOngoing = false
window.toastr['error'](e.response.data.message)
})
},
},
}
</script>

View File

@ -1,278 +0,0 @@
<template>
<div class="setting-main-container">
<form action="" @submit.prevent="updateCompany">
<div class="card setting-card">
<div class="page-header">
<h3 class="page-title">{{ $t('settings.company_info.company_info') }}</h3>
<p class="page-sub-title">
{{ $t('settings.company_info.section_description') }}
</p>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="input-label">{{ $tc('settings.company_info.company_logo') }}</label>
<div id="pick-avatar" class="image-upload-box">
<div class="overlay">
<font-awesome-icon class="white-icon" icon="camera"/>
</div>
<img v-if="previewLogo" :src="previewLogo" class="preview-logo">
<div v-else class="upload-content">
<font-awesome-icon class="upload-icon" icon="cloud-upload-alt"/>
<p class="upload-text"> {{ $tc('general.choose_file') }} </p>
</div>
</div>
</div>
<avatar-cropper
:labels="{ submit: 'Submit', cancel: 'Cancel'}"
:cropper-options="cropperOptions"
:output-options="cropperOutputOptions"
:output-quality="0.8"
:upload-handler="cropperHandler"
trigger="#pick-avatar"
@changed="setFileObject"
@error="handleUploadError"
/>
</div>
<div class="row">
<div class="col-md-6 mb-4">
<label class="input-label">{{ $tc('settings.company_info.company_name') }}</label> <span class="text-danger"> * </span>
<base-input
v-model="formData.name"
:invalid="$v.formData.name.$error"
:placeholder="$t('settings.company_info.company_name')"
@input="$v.formData.name.$touch()"
/>
<div v-if="$v.formData.name.$error">
<span v-if="!$v.formData.name.required" class="text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
<div class="col-md-6 mb-4">
<label class="input-label">{{ $tc('settings.company_info.phone') }}</label>
<base-input
v-model="formData.phone"
:placeholder="$t('settings.company_info.phone')"
/>
</div>
<div class="col-md-6 mb-4">
<label class="input-label">{{ $tc('settings.company_info.country') }}</label><span class="text-danger"> * </span>
<base-select
v-model="country"
:options="countries"
:class="{'error': $v.formData.country_id.$error }"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:placeholder="$t('general.select_country')"
label="name"
track-by="id"
/>
<div v-if="$v.formData.country_id.$error">
<span v-if="!$v.formData.country_id.required" class="text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
<div class="col-md-6 mb-4">
<label class="input-label">{{ $tc('settings.company_info.state') }}</label>
<base-input
v-model="formData.state"
:placeholder="$tc('settings.company_info.state')"
name="state"
type="text"
/>
</div>
<div class="col-md-6 mb-4">
<label class="input-label">{{ $tc('settings.company_info.city') }}</label>
<base-input
v-model="formData.city"
:placeholder="$tc('settings.company_info.city')"
name="city"
type="text"
/>
</div>
<div class="col-md-6 mb-4">
<label class="input-label">{{ $tc('settings.company_info.zip') }}</label>
<base-input
v-model="formData.zip"
:placeholder="$tc('settings.company_info.zip')"
/>
</div>
<div class="col-md-6 mb-4">
<label class="input-label">{{ $tc('settings.company_info.address') }}</label>
<base-text-area
v-model="formData.address_street_1"
:placeholder="$tc('general.street_1')"
:class="{'invalid': $v.formData.address_street_1.$error }"
rows="2"
@input="$v.formData.address_street_1.$touch()"
/>
<div v-if="$v.formData.address_street_1.$error">
<span v-if="!$v.formData.address_street_1.maxLength" class="text-danger">{{ $tc('validation.address_maxlength') }}</span>
</div>
<base-text-area
v-model="formData.address_street_2"
:placeholder="$tc('general.street_2')"
:class="{'invalid': $v.formData.address_street_2.$error }"
rows="2"
@input="$v.formData.address_street_2.$touch()"
/>
<div v-if="$v.formData.address_street_2.$error">
<span v-if="!$v.formData.address_street_2.maxLength" class="text-danger">{{ $tc('validation.address_maxlength') }}</span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<base-button
:loading="isLoading"
:disabled="isLoading"
icon="save"
color="theme"
type="submit"
>
{{ $tc('settings.company_info.save') }}
</base-button>
</div>
</div>
</div>
</form>
</div>
</template>
<script>
import IconUpload from '../../components/icon/upload'
import ImageBox from '../components/ImageBox.vue'
import AvatarCropper from 'vue-avatar-cropper'
import { validationMixin } from 'vuelidate'
import { mapActions } from 'vuex'
const { required, email, maxLength } = require('vuelidate/lib/validators')
export default {
components: { AvatarCropper, IconUpload, ImageBox },
mixins: [validationMixin],
data () {
return {
cropperOutputOptions: {
width: 150,
height: 150
},
cropperOptions: {
autoCropArea: 1,
viewMode: 0,
movable: true,
zoomable: true
},
isFetchingData: false,
formData: {
name: null,
email: '',
phone: '',
zip: '',
address_street_1: '',
address_street_2: '',
website: '',
country_id: null,
state: '',
city: ''
},
isLoading: false,
isHidden: false,
country: null,
previewLogo: null,
countries: [],
passData: [],
fileSendUrl: '/api/settings/company',
fileObject: null
}
},
watch: {
country (newCountry) {
this.formData.country_id = newCountry.id
if (this.isFetchingData) {
return true
}
}
},
validations: {
formData: {
name: {
required
},
country_id: {
required
},
email: {
email
},
address_street_1: {
maxLength: maxLength(255)
},
address_street_2: {
maxLength: maxLength(255)
}
}
},
mounted () {
this.fetchCountry()
this.setInitialData()
},
methods: {
...mapActions('companyInfo', [
'loadData',
'editCompany',
'getFile'
]),
cropperHandler (cropper) {
this.previewLogo = cropper.getCroppedCanvas().toDataURL(this.cropperOutputMime)
},
setFileObject (file) {
this.fileObject = file
},
handleUploadError (message, type, xhr) {
window.toastr['error']('Oops! Something went wrong...')
},
async setInitialData () {
let response = await this.loadData()
this.isFetchingData = true
this.formData.name = response.data.user.company.name
this.formData.address_street_1 = response.data.user.addresses[0].address_street_1
this.formData.address_street_2 = response.data.user.addresses[0].address_street_2
this.formData.zip = response.data.user.addresses[0].zip
this.formData.phone = response.data.user.addresses[0].phone
this.formData.state = response.data.user.addresses[0].state
this.formData.city = response.data.user.addresses[0].city
this.country = response.data.user.addresses[0].country
this.previewLogo = response.data.user.company.logo
},
async updateCompany () {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let response = await this.editCompany(this.formData)
if (response.data.success) {
this.isLoading = false
if (this.fileObject && this.previewLogo) {
let logoData = new FormData()
logoData.append('company_logo', JSON.stringify({
name: this.fileObject.name,
data: this.previewLogo
}))
await axios.post('/api/settings/company/upload-logo', logoData)
}
this.isLoading = false
window.toastr['success'](this.$t('settings.company_info.updated_message'))
return true
}
this.isLoading = false
window.toastr['error'](response.data.error)
return true
},
async fetchCountry () {
let res = await window.axios.get('/api/countries')
if (res) {
this.countries = res.data.countries
}
}
}
}
</script>

View File

@ -0,0 +1,336 @@
<template>
<form @submit.prevent="updateCompanyData" class="relative h-full">
<base-loader v-if="isRequestOnGoing" :show-bg-overlay="true" />
<sw-card variant="setting-card">
<template slot="header">
<h6 class="sw-section-title">
{{ $t('settings.company_info.company_info') }}
</h6>
<p
class="mt-2 text-sm leading-snug text-gray-500"
style="max-width: 680px"
>
{{ $t('settings.company_info.section_description') }}
</p>
</template>
<div class="grid mb-6 md:grid-cols-2">
<sw-input-group :label="$tc('settings.company_info.company_logo')">
<div
id="logo-box"
class="relative flex items-center justify-center h-24 p-5 mt-2 bg-transparent border-2 border-gray-200 border-dashed rounded-md image-upload-box"
>
<img
v-if="previewLogo"
:src="previewLogo"
class="absolute opacity-100 preview-logo"
style="max-height: 80%; animation: fadeIn 2s ease"
/>
<div v-else class="flex flex-col items-center">
<cloud-upload-icon
class="h-5 mb-2 text-xl leading-6 text-gray-400"
/>
<p class="text-xs leading-4 text-center text-gray-400">
Drag a file here or
<span id="pick-avatar" class="cursor-pointer text-primary-500">
browse
</span>
to choose a file
</p>
</div>
</div>
<sw-avatar
trigger="#logo-box"
:preview-avatar="previewLogo"
@changed="onChange"
@uploadHandler="onUploadHandler"
@handleUploadError="onHandleUploadError"
>
<template v-slot:icon>
<cloud-upload-icon
class="h-5 mb-2 text-xl leading-6 text-gray-400"
/>
</template>
</sw-avatar>
</sw-input-group>
</div>
<div class="grid gap-6 sm:grid-col-1 md:grid-cols-2">
<sw-input-group
:label="$tc('settings.company_info.company_name')"
:error="nameError"
required
>
<sw-input
v-model="formData.name"
:invalid="$v.formData.name.$error"
:placeholder="$t('settings.company_info.company_name')"
class="mt-2"
@input="$v.formData.name.$touch()"
/>
</sw-input-group>
<sw-input-group :label="$tc('settings.company_info.phone')">
<sw-input
v-model="formData.phone"
class="mt-2"
:placeholder="$t('settings.company_info.phone')"
/>
</sw-input-group>
<sw-input-group
:label="$tc('settings.company_info.country')"
:error="countryError"
required
>
<sw-select
v-model="country"
:options="countries"
:class="{ error: $v.formData.country_id.$error }"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:placeholder="$t('general.select_country')"
class="mt-2"
label="name"
track-by="id"
/>
</sw-input-group>
<sw-input-group :label="$tc('settings.company_info.state')">
<sw-input
v-model="formData.state"
:placeholder="$tc('settings.company_info.state')"
name="state"
class="mt-2"
type="text"
/>
</sw-input-group>
<sw-input-group :label="$tc('settings.company_info.city')">
<sw-input
v-model="formData.city"
:placeholder="$tc('settings.company_info.city')"
name="city"
class="mt-2"
type="text"
/>
</sw-input-group>
<sw-input-group :label="$tc('settings.company_info.zip')">
<sw-input
v-model="formData.zip"
:placeholder="$tc('settings.company_info.zip')"
class="mt-2"
/>
</sw-input-group>
<div>
<sw-input-group
:label="$tc('settings.company_info.address')"
:error="address1Error"
>
<sw-textarea
v-model="formData.address_street_1"
:placeholder="$tc('general.street_1')"
:class="{ invalid: $v.formData.address_street_1.$error }"
rows="2"
@input="$v.formData.address_street_1.$touch()"
/>
</sw-input-group>
<sw-input-group :error="address2Error" class="my-2">
<sw-textarea
v-model="formData.address_street_2"
:placeholder="$tc('general.street_2')"
:class="{ invalid: $v.formData.address_street_2.$error }"
rows="2"
@input="$v.formData.address_street_2.$touch()"
/>
</sw-input-group>
</div>
</div>
<sw-button
class="mt-4"
:loading="isLoading"
:disabled="isLoading"
variant="primary"
>
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
{{ $tc('settings.company_info.save') }}
</sw-button>
</sw-card>
</form>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { CloudUploadIcon } from '@vue-hero-icons/solid'
const { required, email, maxLength } = require('vuelidate/lib/validators')
export default {
components: {
CloudUploadIcon,
},
data() {
return {
isFetchingData: false,
formData: {
name: null,
email: '',
phone: '',
zip: '',
address_street_1: '',
address_street_2: '',
website: '',
country_id: null,
state: '',
city: '',
},
isLoading: false,
country: null,
passData: [],
fileSendUrl: '/api/v1/settings/company',
previewLogo: null,
fileObject: null,
cropperOutputMime: '',
isRequestOnGoing: false,
}
},
watch: {
country(newCountry) {
this.formData.country_id = newCountry.id
if (this.isFetchingData) {
return true
}
},
},
validations: {
formData: {
name: {
required,
},
country_id: {
required,
},
email: {
email,
},
address_street_1: {
maxLength: maxLength(255),
},
address_street_2: {
maxLength: maxLength(255),
},
},
},
computed: {
...mapGetters(['countries']),
nameError() {
if (!this.$v.formData.name.$error) {
return ''
}
if (!this.$v.formData.name.required) {
return this.$tc('validation.required')
}
},
countryError() {
if (!this.$v.formData.country_id.$error) {
return ''
}
if (!this.$v.formData.country_id.required) {
return this.$tc('validation.required')
}
},
address1Error() {
if (!this.$v.formData.address_street_1.$error) {
return ''
}
if (!this.$v.formData.address_street_1.maxLength) {
return this.$tc('validation.address_maxlength')
}
},
address2Error() {
if (!this.$v.formData.address_street_2.$error) {
return ''
}
if (!this.$v.formData.address_street_2.maxLength) {
return this.$tc('validation.address_maxlength')
}
},
},
mounted() {
this.setInitialData()
},
methods: {
...mapActions('company', ['updateCompany', 'updateCompanyLogo']),
...mapActions('user', ['fetchCurrentUser']),
onUploadHandler(cropper) {
this.previewLogo = cropper
.getCroppedCanvas()
.toDataURL(this.cropperOutputMime)
},
onHandleUploadError() {
window.toastr['error']('Oops! Something went wrong...')
},
onChange(file) {
this.cropperOutputMime = file.type
this.fileObject = file
},
async setInitialData() {
this.isRequestOnGoing = true
let response = await this.fetchCurrentUser()
this.isFetchingData = true
if (response.data.user) {
this.formData.name = response.data.user.company.name
this.formData.address_street_1 =
response.data.user.company.address.address_street_1
this.formData.address_street_2 =
response.data.user.company.address.address_street_2
this.formData.zip = response.data.user.company.address.zip
this.formData.phone = response.data.user.company.address.phone
this.formData.state = response.data.user.company.address.state
this.formData.city = response.data.user.company.address.city
this.country = response.data.user.company.address.country
this.previewLogo = response.data.user.company.logo
}
this.isRequestOnGoing = false
},
async updateCompanyData() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let response = await this.updateCompany(this.formData)
if (response.data.success) {
this.isLoading = false
if (this.fileObject && this.previewLogo) {
let logoData = new FormData()
logoData.append(
'company_logo',
JSON.stringify({
name: this.fileObject.name,
data: this.previewLogo,
})
)
await this.updateCompanyLogo(logoData)
}
this.isLoading = false
window.toastr['success'](
this.$t('settings.company_info.updated_message')
)
return true
}
this.isLoading = false
window.toastr['error'](response.data.error)
return true
},
},
}
</script>

View File

@ -0,0 +1,183 @@
<template>
<sw-card variant="setting-card">
<div slot="header" class="flex flex-wrap justify-between lg:flex-no-wrap">
<div>
<h6 class="sw-section-title">
{{ $t('settings.menu_title.custom_fields') }}
</h6>
<p
class="mt-2 text-sm leading-snug text-gray-500"
style="max-width: 680px"
>
{{ $t('settings.custom_fields.section_description') }}
</p>
</div>
<div class="mt-4 lg:mt-0 lg:ml-2">
<sw-button variant="primary-outline" size="lg" @click="addCustomField">
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('settings.custom_fields.add_custom_field') }}
</sw-button>
</div>
</div>
<sw-table-component
ref="table"
variant="gray"
:show-filter="false"
:data="fetchData"
>
<sw-table-column
:sortable="true"
:label="$t('settings.custom_fields.name')"
show="name"
/>
<sw-table-column
:sortable="true"
:label="$t('settings.custom_fields.label')"
show="label"
/>
<sw-table-column
:sortable="true"
:label="$t('settings.custom_fields.model')"
show="model_type"
/>
<sw-table-column
:sortable="true"
:label="$t('settings.custom_fields.type')"
show="type.label"
/>
<sw-table-column
:sortable="true"
:filterable="true"
:label="$t('settings.custom_fields.required')"
show="is_required"
>
<template slot-scope="row">
<span>{{ $t('settings.custom_fields.required') }}</span>
<sw-badge
:bg-color="
$utils.getBadgeStatusColor(row.is_required ? 'YES' : 'NO').bgColor
"
:color="
$utils.getBadgeStatusColor(row.is_required ? 'YES' : 'NO').color
"
>
{{
row.is_required
? $t('settings.custom_fields.yes')
: $t('settings.custom_fields.no').replace('_', ' ')
}}
</sw-badge>
</template>
</sw-table-column>
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.action') }}</span>
<sw-dropdown>
<dot-icon slot="activator" />
<sw-dropdown-item @click="editCustomField(row.id)">
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removeCustomField(row.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</sw-table-column>
</sw-table-component>
</sw-card>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { PencilIcon, TrashIcon, PlusIcon } from '@vue-hero-icons/solid'
export default {
components: {
PencilIcon,
TrashIcon,
PlusIcon,
},
methods: {
...mapActions('customFields', ['fetchCustomFields', 'deleteCustomFields']),
...mapActions('modal', ['openModal']),
async fetchData({ page, filter, sort }) {
let data = {
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page,
}
let response = await this.fetchCustomFields(data)
return {
data: response.data.customFields.data,
pagination: {
totalPages: response.data.customFields.last_page,
currentPage: page,
count: response.data.customFields.count,
},
}
},
async removeCustomField(id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('settings.custom_fields.custom_field_confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
}).then(async (value) => {
if (value) {
let response = await this.deleteCustomFields(id)
if (response.data.success) {
window.toastr['success'](
this.$t('settings.custom_fields.deleted_message')
)
this.id = null
this.$refs.table.refresh()
return true
}
window.toastr['error'](
this.$t('settings.custom_fields.already_in_use')
)
}
})
},
addCustomField() {
this.openModal({
title: this.$t('settings.custom_fields.add_custom_field'),
componentName: 'CustomFieldModal',
refreshData: this.$refs.table.refresh,
})
},
editCustomField(id) {
this.openModal({
title: this.$t('settings.custom_fields.edit_custom_field'),
componentName: 'CustomFieldModal',
id: id,
refreshData: this.$refs.table.refresh,
})
},
},
}
</script>

View File

@ -1,599 +0,0 @@
<template>
<div class="setting-main-container customization">
<div class="card setting-card">
<ul class="tabs">
<li class="tab" @click="setActiveTab('INVOICES')">
<a :class="['tab-link', {'a-active': activeTab === 'INVOICES'}]" href="#">{{ $t('settings.customization.invoices.title') }}</a>
</li>
<li class="tab" @click="setActiveTab('ESTIMATES')">
<a :class="['tab-link', {'a-active': activeTab === 'ESTIMATES'}]" href="#">{{ $t('settings.customization.estimates.title') }}</a>
</li>
<li class="tab" @click="setActiveTab('PAYMENTS')">
<a :class="['tab-link', {'a-active': activeTab === 'PAYMENTS'}]" href="#">{{ $t('settings.customization.payments.title') }}</a>
</li>
<li class="tab" @click="setActiveTab('ITEMS')">
<a :class="['tab-link', {'a-active': activeTab === 'ITEMS'}]" href="#">{{ $t('settings.customization.items.title') }}</a>
</li>
</ul>
<!-- Invoices Tab -->
<transition name="fade-customize">
<div v-if="activeTab === 'INVOICES'" class="invoice-tab">
<form action="" class="mt-3" @submit.prevent="updateInvoiceSetting">
<div class="row">
<div class="col-md-12 mb-4">
<label class="input-label">{{ $t('settings.customization.invoices.invoice_prefix') }}</label>
<base-input
v-model="invoices.invoice_prefix"
:invalid="$v.invoices.invoice_prefix.$error"
class="prefix-input"
@input="$v.invoices.invoice_prefix.$touch()"
@keyup="changeToUppercase('INVOICES')"
/>
<span v-show="!$v.invoices.invoice_prefix.required" class="text-danger mt-1">{{ $t('validation.required') }}</span>
<span v-if="!$v.invoices.invoice_prefix.maxLength" class="text-danger">{{ $t('validation.prefix_maxlength') }}</span>
<span v-if="!$v.invoices.invoice_prefix.alpha" class="text-danger">{{ $t('validation.characters_only') }}</span>
</div>
</div>
<div class="row pb-3">
<div class="col-md-12">
<base-button
icon="save"
color="theme"
type="submit"
>
{{ $t('settings.customization.save') }}
</base-button>
</div>
</div>
</form>
<hr>
<div class="page-header pt-3">
<h3 class="page-title">
{{ $t('settings.customization.invoices.invoice_settings') }}
</h3>
<div class="flex-box">
<div class="left">
<base-switch
v-model="invoiceAutogenerate"
class="btn-switch"
@change="setInvoiceSetting"
/>
</div>
<div class="right ml-15">
<p class="box-title"> {{ $t('settings.customization.invoices.autogenerate_invoice_number') }} </p>
<p class="box-desc"> {{ $t('settings.customization.invoices.invoice_setting_description') }} </p>
</div>
</div>
</div>
</div>
</transition>
<!-- Estimates Tab -->
<transition name="fade-customize">
<div v-if="activeTab === 'ESTIMATES'" class="estimate-tab">
<form action="" class="mt-3" @submit.prevent="updateEstimateSetting">
<div class="row">
<div class="col-md-12 mb-4">
<label class="input-label">{{ $t('settings.customization.estimates.estimate_prefix') }}</label>
<base-input
v-model="estimates.estimate_prefix"
:invalid="$v.estimates.estimate_prefix.$error"
class="prefix-input"
@input="$v.estimates.estimate_prefix.$touch()"
@keyup="changeToUppercase('ESTIMATES')"
/>
<span v-show="!$v.estimates.estimate_prefix.required" class="text-danger mt-1">{{ $t('validation.required') }}</span>
<span v-if="!$v.estimates.estimate_prefix.maxLength" class="text-danger">{{ $t('validation.prefix_maxlength') }}</span>
<span v-if="!$v.estimates.estimate_prefix.alpha" class="text-danger">{{ $t('validation.characters_only') }}</span>
</div>
</div>
<div class="row pb-3">
<div class="col-md-12">
<base-button
icon="save"
color="theme"
type="submit"
>
{{ $t('settings.customization.save') }}
</base-button>
</div>
</div>
<hr>
</form>
<div class="page-header pt-3">
<h3 class="page-title">
{{ $t('settings.customization.estimates.estimate_settings') }}
</h3>
<div class="flex-box">
<div class="left">
<base-switch
v-model="estimateAutogenerate"
class="btn-switch"
@change="setEstimateSetting"
/>
</div>
<div class="right ml-15">
<p class="box-title"> {{ $t('settings.customization.estimates.autogenerate_estimate_number') }} </p>
<p class="box-desc"> {{ $t('settings.customization.estimates.estimate_setting_description') }} </p>
</div>
</div>
</div>
</div>
</transition>
<!-- Payments Tab -->
<transition name="fade-customize">
<div v-if="activeTab === 'PAYMENTS'" class="payment-tab">
<div class="page-header">
<div class="row">
<div class="col-md-8">
<!-- <h3 class="page-title">
{{ $t('settings.customization.payments.payment_mode') }}
</h3> -->
</div>
<div class="col-md-4 d-flex flex-row-reverse">
<base-button
outline
class="add-new-tax"
color="theme"
@click="addPaymentMode"
>
{{ $t('settings.customization.payments.add_payment_mode') }}
</base-button>
</div>
</div>
</div>
<table-component
ref="table"
:show-filter="false"
:data="paymentModes"
table-class="table tax-table"
class="mb-3"
>
<table-column
:sortable="true"
:label="$t('settings.customization.payments.payment_mode')"
show="name"
/>
<table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.action') }}</span>
<v-dropdown>
<a slot="activator" href="#">
<dot-icon />
</a>
<v-dropdown-item>
<div class="dropdown-item" @click="editPaymentMode(row)">
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
{{ $t('general.edit') }}
</div>
</v-dropdown-item>
<v-dropdown-item>
<div class="dropdown-item" @click="removePaymentMode(row.id)">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
</template>
</table-column>
</table-component>
<hr>
<form action="" class="pt-3" @submit.prevent="updatePaymentSetting">
<div class="row">
<div class="col-md-12 mb-4">
<label class="input-label">{{ $t('settings.customization.payments.payment_prefix') }}</label>
<base-input
v-model="payments.payment_prefix"
:invalid="$v.payments.payment_prefix.$error"
class="prefix-input"
@input="$v.payments.payment_prefix.$touch()"
@keyup="changeToUppercase('PAYMENTS')"
/>
<span v-show="!$v.payments.payment_prefix.required" class="text-danger mt-1">{{ $t('validation.required') }}</span>
<span v-if="!$v.payments.payment_prefix.maxLength" class="text-danger">{{ $t('validation.prefix_maxlength') }}</span>
<span v-if="!$v.payments.payment_prefix.alpha" class="text-danger">{{ $t('validation.characters_only') }}</span>
</div>
</div>
<div class="row pb-3">
<div class="col-md-12">
<base-button
icon="save"
color="theme"
type="submit"
>
{{ $t('settings.customization.save') }}
</base-button>
</div>
</div>
</form>
<hr>
<div class="page-header pt-3">
<h3 class="page-title">
{{ $t('settings.customization.payments.payment_settings') }}
</h3>
<div class="flex-box">
<div class="left">
<base-switch
v-model="paymentAutogenerate"
class="btn-switch"
@change="setPaymentSetting"
/>
</div>
<div class="right ml-15">
<p class="box-title"> {{ $t('settings.customization.payments.autogenerate_payment_number') }} </p>
<p class="box-desc"> {{ $t('settings.customization.payments.payment_setting_description') }} </p>
</div>
</div>
</div>
</div>
</transition>
<!-- Items Tab -->
<transition name="fade-customize">
<div v-if="activeTab === 'ITEMS'" class="item-tab">
<div class="page-header">
<div class="row">
<div class="col-md-8">
<!-- <h3 class="page-title">
{{ $t('settings.customization.items.title') }}
</h3> -->
</div>
<div class="col-md-4 d-flex flex-row-reverse">
<base-button
outline
class="add-new-tax"
color="theme"
@click="addItemUnit"
>
{{ $t('settings.customization.items.add_item_unit') }}
</base-button>
</div>
</div>
</div>
<table-component
ref="itemTable"
:show-filter="false"
:data="itemUnits"
table-class="table tax-table"
class="mb-3"
>
<table-column
:sortable="true"
:label="$t('settings.customization.items.units')"
show="name"
/>
<table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.action') }}</span>
<v-dropdown>
<a slot="activator" href="#">
<dot-icon />
</a>
<v-dropdown-item>
<div class="dropdown-item" @click="editItemUnit(row)">
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
{{ $t('general.edit') }}
</div>
</v-dropdown-item>
<v-dropdown-item>
<div class="dropdown-item" @click="removeItemUnit(row.id)">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
</template>
</table-column>
</table-component>
</div>
</transition>
</div>
</div>
</template>
<script>
import { validationMixin } from 'vuelidate'
import { mapActions, mapGetters } from 'vuex'
const { required, maxLength, alpha } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
data () {
return {
activeTab: 'INVOICES',
invoiceAutogenerate: false,
estimateAutogenerate: false,
paymentAutogenerate: false,
invoices: {
invoice_prefix: null,
invoice_notes: null,
invoice_terms_and_conditions: null
},
estimates: {
estimate_prefix: null,
estimate_notes: null,
estimate_terms_and_conditions: null
},
payments: {
payment_prefix: null
},
items: {
units: []
},
currentData: null
}
},
computed: {
...mapGetters('item', [
'itemUnits'
]),
...mapGetters('payment', [
'paymentModes'
])
},
watch: {
activeTab () {
this.loadData()
}
},
validations: {
invoices: {
invoice_prefix: {
required,
maxLength: maxLength(5),
alpha
}
},
estimates: {
estimate_prefix: {
required,
maxLength: maxLength(5),
alpha
}
},
payments: {
payment_prefix: {
required,
maxLength: maxLength(5),
alpha
}
}
},
created () {
this.loadData()
},
methods: {
...mapActions('modal', [
'openModal'
]),
...mapActions('payment', [
'deletePaymentMode'
]),
...mapActions('item', [
'deleteItemUnit'
]),
async setInvoiceSetting () {
let data = {
key: 'invoice_auto_generate',
value: this.invoiceAutogenerate ? 'YES' : 'NO'
}
let response = await window.axios.put('/api/settings/update-setting', data)
if (response.data) {
window.toastr['success'](this.$t('general.setting_updated'))
}
},
async setEstimateSetting () {
let data = {
key: 'estimate_auto_generate',
value: this.estimateAutogenerate ? 'YES' : 'NO'
}
let response = await window.axios.put('/api/settings/update-setting', data)
if (response.data) {
window.toastr['success'](this.$t('general.setting_updated'))
}
},
async addItemUnit () {
this.openModal({
'title': this.$t('settings.customization.items.add_item_unit'),
'componentName': 'ItemUnit'
})
this.$refs.itemTable.refresh()
},
async editItemUnit (data) {
this.openModal({
'title': this.$t('settings.customization.items.edit_item_unit'),
'componentName': 'ItemUnit',
'id': data.id,
'data': data
})
this.$refs.itemTable.refresh()
},
async removeItemUnit (id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('settings.customization.items.item_unit_confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
let response = await this.deleteItemUnit(id)
if (response.data.success) {
window.toastr['success'](this.$t('settings.customization.items.deleted_message'))
this.id = null
this.$refs.itemTable.refresh()
return true
}
window.toastr['error'](this.$t('settings.customization.items.already_in_use'))
}
})
},
async addPaymentMode () {
this.openModal({
'title': this.$t('settings.customization.payments.add_payment_mode'),
'componentName': 'PaymentMode'
})
this.$refs.table.refresh()
},
async editPaymentMode (data) {
this.openModal({
'title': this.$t('settings.customization.payments.edit_payment_mode'),
'componentName': 'PaymentMode',
'id': data.id,
'data': data
})
this.$refs.table.refresh()
},
removePaymentMode (id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('settings.customization.payments.payment_mode_confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
let response = await this.deletePaymentMode(id)
if (response.data.success) {
window.toastr['success'](this.$t('settings.customization.payments.deleted_message'))
this.id = null
this.$refs.table.refresh()
return true
}
window.toastr['error'](this.$t('settings.customization.payments.already_in_use'))
}
})
},
changeToUppercase (currentTab) {
if (currentTab === 'INVOICES') {
this.invoices.invoice_prefix = this.invoices.invoice_prefix.toUpperCase()
return true
}
if (currentTab === 'ESTIMATES') {
this.estimates.estimate_prefix = this.estimates.estimate_prefix.toUpperCase()
return true
}
if (currentTab === 'PAYMENTS') {
this.payments.payment_prefix = this.payments.payment_prefix.toUpperCase()
return true
}
},
async setPaymentSetting () {
let data = {
key: 'payment_auto_generate',
value: this.paymentAutogenerate ? 'YES' : 'NO'
}
let response = await window.axios.put('/api/settings/update-setting', data)
if (response.data) {
window.toastr['success'](this.$t('general.setting_updated'))
}
},
async loadData () {
let res = await window.axios.get('/api/settings/get-customize-setting')
if (res.data) {
this.invoices.invoice_prefix = res.data.invoice_prefix
this.invoices.invoice_notes = res.data.invoice_notes
this.invoices.invoice_terms_and_conditions = res.data.invoice_terms_and_conditions
this.estimates.estimate_prefix = res.data.estimate_prefix
this.estimates.estimate_notes = res.data.estimate_notes
this.estimates.estimate_terms_and_conditions = res.data.estimate_terms_and_conditions
this.payments.payment_prefix = res.data.payment_prefix
if (res.data.invoice_auto_generate === 'YES') {
this.invoiceAutogenerate = true
} else {
this.invoiceAutogenerate = false
}
if (res.data.estimate_auto_generate === 'YES') {
this.estimateAutogenerate = true
} else {
this.estimateAutogenerate = false
}
if (res.data.payment_auto_generate === 'YES') {
this.paymentAutogenerate = true
} else {
this.paymentAutogenerate = false
}
}
},
async updateInvoiceSetting () {
this.$v.invoices.$touch()
if (this.$v.invoices.$invalid) {
return false
}
let data = {type: 'INVOICES', ...this.invoices}
if (this.updateSetting(data)) {
window.toastr['success'](this.$t('settings.customization.invoices.invoice_setting_updated'))
}
},
async updateEstimateSetting () {
this.$v.estimates.$touch()
if (this.$v.estimates.$invalid) {
return false
}
let data = {type: 'ESTIMATES', ...this.estimates}
if (this.updateSetting(data)) {
window.toastr['success'](this.$t('settings.customization.estimates.estimate_setting_updated'))
}
},
async updatePaymentSetting () {
this.$v.payments.$touch()
if (this.$v.payments.$invalid) {
return false
}
let data = {type: 'PAYMENTS', ...this.payments}
if (this.updateSetting(data)) {
window.toastr['success'](this.$t('settings.customization.payments.payment_setting_updated'))
}
},
async updateSetting (data) {
let res = await window.axios.put('/api/settings/update-customize-setting', data)
if (res.data.success) {
return true
}
return false
},
setActiveTab (val) {
this.activeTab = val
}
}
}
</script>
<style>
.fade-customize-enter-active {
transition: opacity 0.9s;
}
.fade-customize-leave-active {
transition: opacity 0s;
}
.fade-customize-enter, .fade-customize-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<div class="relative">
<base-loader v-if="isRequestOnGoing" :show-bg-overlay="true" />
<sw-card>
<sw-tabs class="p-2">
<!-- Invoices -->
<sw-tab-item :title="$t('settings.customization.invoices.title')">
<invoices-tab :settings="settings" />
</sw-tab-item>
<!-- Estimates -->
<sw-tab-item :title="$t('settings.customization.estimates.title')">
<estimates-tab :settings="settings" />
</sw-tab-item>
<!-- Payments -->
<sw-tab-item :title="$t('settings.customization.payments.title')">
<payments-tab :settings="settings" />
</sw-tab-item>
<!-- Items -->
<sw-tab-item :title="$t('settings.customization.items.title')">
<items-tab />
</sw-tab-item>
</sw-tabs>
</sw-card>
</div>
</template>
<script>
import InvoicesTab from './customization-tabs/InvoicesTab'
import EstimatesTab from './customization-tabs/EstimatesTab'
import PaymentsTab from './customization-tabs/PaymentsTab'
import ItemsTab from './customization-tabs/ItemsTab'
import { mapActions } from 'vuex'
export default {
data() {
return {
settings: {},
isRequestOnGoing: false,
}
},
components: {
InvoicesTab,
EstimatesTab,
PaymentsTab,
ItemsTab,
},
created() {
this.fetchSettings()
},
methods: {
...mapActions('company', ['fetchCompanySettings']),
async fetchSettings() {
this.isRequestOnGoing = true
let res = await this.fetchCompanySettings([
'payment_auto_generate',
'payment_prefix',
'payment_mail_body',
'invoice_auto_generate',
'invoice_prefix',
'invoice_mail_body',
'estimate_auto_generate',
'estimate_prefix',
'estimate_mail_body',
'invoice_billing_address_format',
'invoice_shipping_address_format',
'invoice_company_address_format',
'invoice_mail_body',
'payment_mail_body',
'payment_company_address_format',
'payment_from_customer_address_format',
'estimate_company_address_format',
'estimate_billing_address_format',
'estimate_shipping_address_format',
])
this.settings = res.data
this.isRequestOnGoing = false
},
},
}
</script>

View File

@ -1,141 +0,0 @@
<template>
<div class="setting-main-container">
<div class="card setting-card">
<div class="page-header d-flex justify-content-between">
<div>
<h3 class="page-title">{{ $t('settings.expense_category.title') }}</h3>
<p class="page-sub-title">
{{ $t('settings.expense_category.description') }}
</p>
</div>
<base-button
outline
class="add-new-tax"
color="theme"
@click="openCategoryModal"
>
{{ $t('settings.expense_category.add_new_category') }}
</base-button>
</div>
<table-component
ref="table"
:show-filter="false"
:data="categories"
table-class="table expense-category"
>
<table-column
:label="$t('settings.expense_category.category_name')"
show="name"
/>
<table-column
:sortable="true"
:filterable="true"
:label="$t('settings.expense_category.category_description')"
>
<template slot-scope="row">
<span>{{ $t('settings.expense_category.category_description') }}</span>
<div class="notes">
<div class="note">{{ row.description }}</div>
</div>
</template>
</table-column>
<table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span>{{ $t('settings.expense_category.action') }}</span>
<v-dropdown>
<a slot="activator" href="#">
<dot-icon />
</a>
<v-dropdown-item>
<div class="dropdown-item" @click="EditCategory(row.id)">
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
{{ $t('general.edit') }}
</div>
</v-dropdown-item>
<v-dropdown-item>
<div class="dropdown-item" @click="removeExpenseCategory(row.id)">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
</template>
</table-column>
</table-component>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
data () {
return {
id: null
}
},
computed: {
...mapGetters('category', [
'categories',
'getCategoryById'
])
},
mounted () {
this.fetchCategories()
},
methods: {
...mapActions('modal', [
'openModal'
]),
...mapActions('category', [
'fetchCategories',
'fetchCategory',
'deleteCategory'
]),
async removeExpenseCategory (id, index) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('settings.expense_category.confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
}).then(async (willDelete) => {
if (willDelete) {
let response = await this.deleteCategory(id)
if (response.data.success) {
window.toastr['success'](this.$tc('settings.expense_category.deleted_message'))
this.id = null
this.$refs.table.refresh()
return true
} window.toastr['error'](this.$t('settings.expense_category.already_in_use'))
}
})
},
openCategoryModal () {
this.openModal({
'title': this.$t('settings.expense_category.add_category'),
'componentName': 'CategoryModal'
})
this.$refs.table.refresh()
},
async EditCategory (id) {
let response = await this.fetchCategory(id)
this.openModal({
'title': this.$t('settings.expense_category.edit_category'),
'componentName': 'CategoryModal',
'id': id,
'data': response.data.category
})
this.$refs.table.refresh()
}
}
}
</script>

View File

@ -0,0 +1,179 @@
<template>
<sw-card variant="setting-card">
<div slot="header" class="flex flex-wrap justify-between lg:flex-no-wrap">
<div>
<h6 class="sw-section-title">
{{ $t('settings.expense_category.title') }}
</h6>
<p
class="mt-2 text-sm leading-snug text-gray-500"
style="max-width: 680px"
>
{{ $t('settings.expense_category.description') }}
</p>
</div>
<div class="mt-4 lg:mt-0 lg:ml-2">
<sw-button
variant="primary-outline"
size="lg"
@click="addExpenseCategory"
>
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('settings.expense_category.add_new_category') }}
</sw-button>
</div>
</div>
<sw-table-component
ref="table"
:show-filter="false"
:data="fetchData"
variant="gray"
>
<sw-table-column
:label="$t('settings.expense_category.category_name')"
show="name"
>
<template slot-scope="row">
<span>{{ $t('settings.expense_category.category_name') }}}</span>
<span class="mt-6">{{ row.name }}</span>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:filterable="true"
:label="$t('settings.expense_category.category_description')"
>
<template slot-scope="row">
<span>{{
$t('settings.expense_category.category_description')
}}</span>
<div class="w-48 overflow-hidden notes">
<div
class="overflow-hidden whitespace-no-wrap"
style="text-overflow: ellipsis"
>
{{ row.description }}
</div>
</div>
</template>
</sw-table-column>
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span>{{ $t('settings.expense_category.action') }}</span>
<sw-dropdown>
<dot-icon slot="activator" class="h-5" />
<sw-dropdown-item @click="editExpenseCategory(row.id)">
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removeExpenseCategory(row.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</sw-table-column>
</sw-table-component>
</sw-card>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { TrashIcon, PencilIcon, PlusIcon } from '@vue-hero-icons/solid'
export default {
components: {
TrashIcon,
PencilIcon,
PlusIcon,
},
computed: {
...mapGetters('category', ['categories', 'getCategoryById']),
},
methods: {
...mapActions('modal', ['openModal']),
...mapActions('category', [
'fetchCategories',
'fetchCategory',
'deleteCategory',
]),
async fetchData({ page, filter, sort }) {
let data = {
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page,
}
this.isRequestOngoing = true
let response = await this.fetchCategories(data)
this.isRequestOngoing = false
return {
data: response.data.categories.data,
pagination: {
totalPages: response.data.categories.last_page,
currentPage: page,
count: response.data.categories.count,
},
}
},
async removeExpenseCategory(id, index) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('settings.expense_category.confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
}).then(async (willDelete) => {
if (willDelete) {
let response = await this.deleteCategory(id)
if (response.data.success) {
window.toastr['success'](
this.$tc('settings.expense_category.deleted_message')
)
this.id = null
this.$refs.table.refresh()
return true
}
window.toastr['error'](
this.$t('settings.expense_category.already_in_use')
)
}
})
},
addExpenseCategory() {
this.openModal({
title: this.$t('settings.expense_category.add_category'),
componentName: 'CategoryModal',
refreshData: this.$refs.table.refresh,
})
},
async editExpenseCategory(id) {
let response = await this.fetchCategory(id)
this.openModal({
title: this.$t('settings.expense_category.edit_category'),
componentName: 'CategoryModal',
id: id,
data: response.data.category,
refreshData: this.$refs.table.refresh,
})
},
},
}
</script>

View File

@ -0,0 +1,297 @@
<template>
<div class="setting-main-container backup">
<sw-card variant="setting-card">
<div slot="header" class="flex flex-wrap justify-between lg:flex-no-wrap">
<div>
<h6 class="sw-section-title">
{{ $tc('settings.disk.title', 1) }}
</h6>
<p
class="mt-2 text-sm leading-snug text-gray-500"
style="max-width: 680px"
>
{{ $t('settings.disk.description') }}
</p>
</div>
<div class="mt-4 lg:mt-0 lg:ml-2">
<sw-button
variant="primary-outline"
size="lg"
@click="openCreateDiskModal"
>
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('settings.disk.new_disk') }}
</sw-button>
</div>
</div>
<sw-table-component
ref="table"
variant="gray"
:show-filter="false"
:data="fetchData"
table-class="table tax-table"
class="mt-0 mb-3"
>
<sw-table-column :label="$t('settings.disk.disk_name')" show="name">
<template slot-scope="row">
<span>{{ $t('settings.disk.disk_name') }}</span>
<span class="mt-6">{{ row.name }}</span>
</template>
</sw-table-column>
<sw-table-column
:label="$t('settings.disk.filesystem_driver')"
show="driver"
/>
<sw-table-column :label="$t('settings.disk.disk_type')" show="type" />
<sw-table-column
:sortable="false"
:filterable="false"
:label="$t('settings.disk.is_default')"
>
<template slot-scope="row">
<span>{{ $t('settings.disk.is_default') }}</span>
<sw-badge
:bg-color="
$utils.getBadgeStatusColor(row.set_as_default ? 'YES' : 'NO')
.bgColor
"
:color="
$utils.getBadgeStatusColor(row.set_as_default ? 'YES' : 'NO')
.color
"
>
{{ row.set_as_default ? 'Yes' : 'No'.replace('_', ' ') }}
</sw-badge>
</template>
</sw-table-column>
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown no-click"
>
<template slot-scope="row">
<span>{{ $t('settings.disk.action') }}</span>
<sw-dropdown v-if="isShowAction(row)">
<a slot="activator" href="#">
<dot-icon />
</a>
<sw-dropdown-item
v-if="!row.set_as_default"
@click="setDefaultDiskData(row.id)"
>
<check-circle-icon class="h-5 mr-3 text-gray-600" />
{{ $t('settings.disk.set_default_disk') }}
</sw-dropdown-item>
<sw-dropdown-item
v-if="row.type !== 'SYSTEM'"
@click="openEditDiskModal(row)"
>
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item
v-if="row.type !== 'SYSTEM' && !row.set_as_default"
@click="removeDisk(row.id)"
>
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</sw-table-column>
</sw-table-component>
<sw-divider class="mt-6 mb-4" />
<h3 class="mb-5 text-lg font-medium text-black">
{{ $t('settings.disk.disk_settings') }}
</h3>
<div class="flex">
<div class="relative w-12">
<sw-switch
v-model="save_pdf_to_disk"
class="absolute"
style="top: -18px"
@change="setDiskSettings"
/>
</div>
<div class="ml-4">
<p class="p-0 mb-1 text-base leading-snug text-black">
{{ $t('settings.disk.save_pdf_to_disk') }}
</p>
<p class="max-w-lg p-0 m-0 text-xs leading-tight text-gray-500">
{{ $t('settings.disk.disk_setting_description') }}
</p>
</div>
</div>
</sw-card>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import {
CheckCircleIcon,
PlusIcon,
TrashIcon,
PencilIcon,
} from '@vue-hero-icons/solid'
export default {
components: {
CheckCircleIcon,
PlusIcon,
TrashIcon,
PencilIcon,
},
data() {
return {
disk: 'local',
save_pdf_to_disk: true,
loading: false,
disks: [],
}
},
mounted() {
this.getDiskSetting()
},
methods: {
...mapActions('modal', ['openModal']),
...mapActions('disks', ['fetchDisks', 'updateDisk', 'deleteFileDisk']),
...mapActions('company', ['updateCompanySettings', 'fetchCompanySettings']),
async fetchData({ page, filter, sort }) {
let data = {
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page,
}
let response = await this.fetchDisks(data)
return {
data: response.data.disks.data,
pagination: {
totalPages: response.data.disks.last_page,
currentPage: page,
count: response.data.disks.count,
},
}
},
isShowAction(disk) {
if (!disk.set_as_default) return true
if (disk.type == 'SYSTEM' && disk.set_as_default) return false
return true
},
openCreateDiskModal() {
this.openModal({
title: this.$t('settings.disk.new_disk'),
componentName: 'FileDiskModal',
variant: 'lg',
refreshData: this.refreshTable,
})
},
openEditDiskModal(data) {
this.openModal({
title: this.$t('settings.disk.edit_file_disk'),
componentName: 'FileDiskModal',
variant: 'lg',
id: data.id,
data,
refreshData: this.refreshTable,
})
},
refreshTable() {
this.$refs.table.refresh()
},
async getDiskSetting(val) {
let response = await this.fetchCompanySettings(['save_pdf_to_disk'])
if (response.data) {
this.save_pdf_to_disk =
response.data.save_pdf_to_disk === 'YES' ? true : false
}
},
async setDiskSettings() {
let data = {
settings: {
save_pdf_to_disk: this.save_pdf_to_disk ? 'YES' : 'NO',
},
}
let response = await this.updateCompanySettings(data)
if (response.data.success) {
window.toastr['success'](this.$t('general.setting_updated'))
}
this.$refs.table.refresh()
},
async setDefaultDiskData(id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('settings.disk.set_default_disk_confirm'),
icon: '/assets/icon/check-circle-solid.svg',
buttons: true,
dangerMode: true,
}).then(async (value) => {
if (value) {
this.loading = true
let data = {
set_as_default: true,
id,
}
let response = await this.updateDisk(data)
if (response.data.success) {
this.refreshTable()
window.toastr['success'](
this.$t('settings.disk.success_set_default_disk')
)
}
}
})
},
async removeDisk(id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('settings.disk.confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
}).then(async (value) => {
if (value) {
let response = await this.deleteFileDisk(id)
if (response.data.success) {
window.toastr['success'](this.$t('settings.disk.deleted_message'))
this.refreshTable()
return true
}
}
})
},
},
}
</script>

View File

@ -1,102 +0,0 @@
<template>
<div class="main-content">
<div class="card setting-card">
<div class="page-header">
<h3 class="page-title">{{ $t('settings.title') }}</h3>
<ol class="breadcrumb">
<li class="breadcrumb-item"><router-link slot="item-title" to="/admin/dashboard">{{ $t('general.home') }}</router-link></li>
<li class="breadcrumb-item"><router-link slot="item-title" to="#">{{ $t('settings.general') }}</router-link></li>
</ol>
</div>
<form action="" @submit.prevent="submitData">
<div class="row">
<div class="col-sm-8">
<div class="card">
<div class="card-header">
<div class="caption">
<h6>{{ $t('settings.general') }}</h6>
</div>
<div class="actions">
<base-button icon="backward" color="theme" size="small" type="submit">
{{ $t('general.save') }}
</base-button>
</div>
</div>
<div class="card-body">
<div class="form-group row">
<label class="col-md-2 form-control-label">{{ $t('settings.language') }}: </label>
<div class="col-md-10">
<setting-dropdown
:options="languages"
:get-data="settings"
:current-data="settings.language"
type="languages"
/>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 form-control-label">{{ $t('settings.primary_currency') }}: </label>
<div class="col-md-10">
<setting-dropdown
:options="currencies"
:get-data="settings"
:current-data="settings.currency"
type="currencies"
/>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 form-control-label">{{ $t('settings.timezone') }}: </label>
<div class="col-md-10">
<setting-dropdown
:options="time_zones"
:get-data="settings"
:current-data="settings.time_zone"
type="time_zones"
/>
</div>
</div>
<div class="form-body">
<div class="form-group row">
<label class="col-md-2 form-control-label">{{ $t('settings.date_format') }}: </label>
<div class="col-md-10">
<setting-dropdown
:options="date_formats"
:get-data="settings"
:current-data="settings.date_format"
type="date_formats"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</template>
<script>
import SettingDropdown from '../components/SettingListBox.vue'
import { mapActions } from 'vuex'
export default {
components: {
'setting-dropdown': SettingDropdown
},
data () {
return this.$store.state.general
},
mounted () {
this.loadData()
},
methods: {
...mapActions('general', [
'loadData',
'submitData'
])
}
}
</script>

View File

@ -1,107 +0,0 @@
<template>
<div class="setting-main-container">
<div class="card setting-card">
<div class="page-header">
<h3 class="page-title">{{ $t('settings.mail.mail_config') }}</h3>
<p class="page-sub-title">
{{ $t('settings.mail.mail_config_desc') }}
</p>
</div>
<div v-if="mailConfigData">
<component
:is="mail_driver"
:config-data="mailConfigData"
:loading="loading"
:mail-drivers="mail_drivers"
@on-change-driver="(val) => mail_driver = mailConfigData.mail_driver = val"
@submit-data="saveEmailConfig"
>
<base-button
:loading="loading"
outline
class="pull-right mt-4 ml-2"
icon="check"
color="theme"
type="button"
@click="openMailTestModal"
>
{{ $t('general.test_mail_conf') }}
</base-button>
</component>
</div>
</div>
</div>
</template>
<script>
import MultiSelect from 'vue-multiselect'
import { validationMixin } from 'vuelidate'
import Smtp from './mailDriver/Smtp'
import Mailgun from './mailDriver/Mailgun'
import Ses from './mailDriver/Ses'
import Basic from './mailDriver/Basic'
import { mapActions } from 'vuex'
export default {
components: {
MultiSelect,
Smtp,
Mailgun,
Ses,
sendmail: Basic,
mail: Basic
},
mixins: [validationMixin],
data () {
return {
mailConfigData: null,
mail_driver: 'smtp',
loading: false,
mail_drivers: []
}
},
mounted () {
this.loadData()
},
methods: {
...mapActions('modal', [
'openModal'
]),
async loadData () {
this.loading = true
let mailDrivers = await window.axios.get('/api/settings/environment/mail')
let mailData = await window.axios.get('/api/settings/environment/mail-env')
if (mailDrivers.data) {
this.mail_drivers = mailDrivers.data
}
if (mailData.data) {
this.mailConfigData = mailData.data
this.mail_driver = mailData.data.mail_driver
}
this.loading = false
},
async saveEmailConfig (mailConfigData) {
this.loading = true
try {
let response = await window.axios.post('/api/settings/environment/mail', mailConfigData)
if (response.data.success) {
window.toastr['success'](this.$t('wizard.success.' + response.data.success))
} else {
window.toastr['error'](this.$t('wizard.errors.' + response.data.error))
}
this.loading = false
return true
} catch (e) {
window.toastr['error']('Something went wrong')
}
},
openMailTestModal () {
this.openModal({
'title': 'Test Mail Configuration',
'componentName': 'MailTestModal'
})
}
}
}
</script>

View File

@ -0,0 +1,125 @@
<template>
<div class="relative">
<base-loader v-if="isRequestOnGoing" :show-bg-overlay="true" />
<sw-card variant="setting-card">
<template slot="header">
<h6 class="sw-section-title">
{{ $t('settings.mail.mail_config') }}
</h6>
<p
class="mt-2 text-sm leading-snug text-gray-500"
style="max-width: 680px"
>
{{ $t('settings.mail.mail_config_desc') }}
</p>
</template>
<div v-if="mailConfigData">
<component
:is="mail_driver"
:config-data="mailConfigData"
:loading="isLoading"
:mail-drivers="mail_drivers"
@on-change-driver="
(val) => (mail_driver = mailConfigData.mail_driver = val)
"
@submit-data="saveEmailConfig"
>
<sw-button
variant="primary-outline"
type="button"
class="ml-2"
@click="openMailTestModal"
>
{{ $t('general.test_mail_conf') }}
</sw-button>
</component>
</div>
</sw-card>
</div>
</template>
<script>
import Smtp from './mail-driver/SmtpMailDriver'
import Mailgun from './mail-driver/MailgunMailDriver'
import Ses from './mail-driver/SesMailDriver'
import Basic from './mail-driver/BasicMailDriver'
import { mapActions } from 'vuex'
export default {
components: {
Smtp,
Mailgun,
Ses,
sendmail: Basic,
mail: Basic,
},
data() {
return {
mailConfigData: null,
mail_driver: 'smtp',
isLoading: false,
isRequestOnGoing: false,
mail_drivers: [],
}
},
mounted() {
this.loadData()
},
methods: {
...mapActions('modal', ['openModal']),
...mapActions('company', [
'fetchMailDrivers',
'fetchMailConfig',
'updateMailConfig',
]),
async loadData() {
this.isRequestOnGoing = true
let mailDrivers = await this.fetchMailDrivers()
let mailData = await this.fetchMailConfig()
if (mailDrivers.data) {
this.mail_drivers = mailDrivers.data
}
if (mailData.data) {
this.mailConfigData = mailData.data
this.mail_driver = mailData.data.mail_driver
}
this.isRequestOnGoing = false
},
async saveEmailConfig(mailConfigData) {
try {
this.isLoading = true
let response = await this.updateMailConfig(mailConfigData)
if (response.data.success) {
this.isLoading = false
window.toastr['success'](
this.$t('wizard.success.' + response.data.success)
)
} else {
window.toastr['error'](
this.$t('wizard.errors.' + response.data.error)
)
}
return true
} catch (e) {
window.toastr['error']('Something went wrong')
}
},
openMailTestModal() {
this.openModal({
title: 'Test Mail Configuration',
componentName: 'MailTestModal',
})
},
},
}
</script>

View File

@ -0,0 +1,160 @@
<template>
<sw-card variant="setting-card">
<div slot="header" class="flex flex-wrap justify-between lg:flex-no-wrap">
<div>
<h6 class="sw-section-title">
{{ $t('settings.customization.notes.title') }}
</h6>
<p
class="mt-2 text-sm leading-snug text-gray-500"
style="max-width: 680px"
>
{{ $t('settings.customization.notes.description') }}
</p>
</div>
<div class="mt-4 lg:mt-0 lg:ml-2">
<sw-button
size="lg"
variant="primary-outline"
@click="openNoteSelectModal"
>
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('settings.customization.notes.add_note') }}
</sw-button>
</div>
</div>
<sw-table-component
ref="table"
variant="gray"
:show-filter="false"
:data="fetchData"
>
<sw-table-column
:label="$t('settings.customization.notes.name')"
show="name"
>
<template slot-scope="row">
<span>{{ $t('settings.customization.notes.name') }}</span>
<span class="mt-6">{{ row.name }}</span>
</template>
</sw-table-column>
<sw-table-column
:label="$t('settings.customization.notes.type')"
show="type"
>
<template slot-scope="row">
<span>{{ $t('settings.customization.notes.type') }}</span>
<span class="mt-6">{{ row.type }}</span>
</template>
</sw-table-column>
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.action') }}</span>
<sw-dropdown>
<dot-icon slot="activator" class="h-5" />
<sw-dropdown-item @click="editNote(row)">
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removeNote(row.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</sw-table-column>
</sw-table-component>
</sw-card>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
const { required, maxLength, alpha } = require('vuelidate/lib/validators')
import { TrashIcon, PencilIcon, PlusIcon } from '@vue-hero-icons/solid'
export default {
components: {
TrashIcon,
PencilIcon,
PlusIcon,
},
methods: {
...mapActions('modal', ['openModal']),
...mapActions('notes', ['fetchNotes', 'deleteNote']),
async fetchData({ page, filter, sort }) {
let data = {
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page,
}
let response = await this.fetchNotes(data)
return {
data: response.data.notes.data,
pagination: {
totalPages: response.data.notes.last_page,
currentPage: page,
count: response.data.notes.count,
},
}
},
removeNote(id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('settings.customization.notes.note_confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
}).then(async (value) => {
if (value) {
let response = await this.deleteNote(id)
if (response.data.success) {
window.toastr['success'](
this.$t('settings.customization.notes.deleted_message')
)
this.$refs.table.refresh()
return true
}
window.toastr['error'](
this.$t('settings.customization.notes.already_in_use')
)
}
})
},
editNote(data) {
this.openModal({
title: this.$t('settings.customization.notes.edit_note'),
componentName: 'NoteSelectModal',
id: data.id,
data: data,
variant: 'lg',
refreshData: this.$refs.table.refresh,
})
},
openNoteSelectModal() {
this.openModal({
title: this.$t('settings.customization.notes.add_note'),
componentName: 'NoteSelectModal',
variant: 'lg',
refreshData: this.$refs.table.refresh,
})
},
},
}
</script>

View File

@ -1,153 +0,0 @@
<template>
<div class="setting-main-container">
<div class="card setting-card">
<div class="page-header">
<h3 class="page-title">{{ $t('settings.notification.title') }}</h3>
<p class="page-sub-title">
{{ $t('settings.notification.description') }}
</p>
</div>
<form action="" @submit.prevent="saveEmail()">
<div class="form-group">
<label class="form-label">{{ $t('settings.notification.email') }}</label><span class="text-danger"> *</span>
<base-input
:invalid="$v.notification_email.$error"
v-model.trim="notification_email"
:placeholder="$tc('settings.notification.please_enter_email')"
type="text"
name="notification_email"
icon="envelope"
input-class="col-md-6"
@input="$v.notification_email.$touch()"
/>
<div v-if="$v.notification_email.$error">
<span v-if="!$v.notification_email.required" class="text-danger">{{ $tc('validation.required') }}</span>
<span v-if="!$v.notification_email.email" class="text-danger"> {{ $tc('validation.email_incorrect') }} </span>
</div>
<base-button
:loading="isLoading"
:disabled="isLoading"
class="mt-4"
icon="save"
color="theme"
type="submit"
> {{ $tc('settings.notification.save') }} </base-button>
</div>
</form>
<hr>
<div class="flex-box mt-3 mb-4">
<div class="left">
<base-switch v-model="notify_invoice_viewed" class="btn-switch" @change="setInvoiceViewd"/>
</div>
<div class="right ml-15">
<p class="box-title"> {{ $t('settings.notification.invoice_viewed') }} </p>
<p class="box-desc"> {{ $t('settings.notification.invoice_viewed_desc') }} </p>
</div>
</div>
<div class="flex-box mb-2">
<div class="left">
<base-switch v-model="notify_estimate_viewed" class="btn-switch" @change="setEstimateViewd"/>
</div>
<div class="right ml-15">
<p class="box-title"> {{ $t('settings.notification.estimate_viewed') }} </p>
<p class="box-desc"> {{ $t('settings.notification.estimate_viewed_desc') }} </p>
</div>
</div>
</div>
</div>
</template>
<script>
import { validationMixin } from 'vuelidate'
const { required, email } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
data () {
return {
isLoading: false,
notification_email: null,
notify_invoice_viewed: null,
notify_estimate_viewed: null
}
},
validations: {
notification_email: {
required,
email
}
},
mounted () {
this.fetchData()
},
methods: {
async fetchData () {
let response1 = await axios.get('/api/settings/get-setting?key=notify_invoice_viewed')
if (response1.data) {
let data = response1.data
data.notify_invoice_viewed === 'YES' ?
this.notify_invoice_viewed = true :
this.notify_invoice_viewed = null
}
let response2 = await axios.get('/api/settings/get-setting?key=notify_estimate_viewed')
if (response2.data) {
let data = response2.data
data.notify_estimate_viewed === 'YES' ?
this.notify_estimate_viewed = true :
this.notify_estimate_viewed = null
}
let response3 = await axios.get('/api/settings/get-setting?key=notification_email')
if (response3.data) {
this.notification_email = response3.data.notification_email
}
},
async saveEmail () {
this.$v.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let data = {
key: 'notification_email',
value: this.notification_email
}
let response = await axios.put('/api/settings/update-setting', data)
if (response.data.success) {
this.isLoading = false
window.toastr['success'](this.$tc('settings.notification.email_save_message'))
}
},
async setInvoiceViewd (val) {
this.$v.$touch()
if (this.$v.$invalid) {
this.notify_invoice_viewed = !this.notify_invoice_viewed
return true
}
let data = {
key: 'notify_invoice_viewed',
value: this.notify_invoice_viewed ? 'YES' : 'NO'
}
let response = await axios.put('/api/settings/update-setting', data)
if (response.data.success) {
window.toastr['success'](this.$tc('general.setting_updated'))
}
},
async setEstimateViewd (val) {
this.$v.$touch()
if (this.$v.$invalid) {
this.notify_estimate_viewed = !this.notify_estimate_viewed
return true
}
let data = {
key: 'notify_estimate_viewed',
value: this.notify_estimate_viewed ? 'YES' : 'NO'
}
let response = await axios.put('/api/settings/update-setting', data)
if (response.data) {
window.toastr['success'](this.$tc('general.setting_updated'))
}
}
}
}
</script>

View File

@ -0,0 +1,228 @@
<template>
<div class="relative">
<base-loader v-if="isRequestOnGoing" :show-bg-overlay="true" />
<sw-card variant="setting-card">
<template slot="header">
<h6 class="sw-section-title">
{{ $t('settings.notification.title') }}
</h6>
<p
class="mt-2 text-sm leading-snug text-gray-500"
style="max-width: 680px"
>
{{ $t('settings.notification.description') }}
</p>
</template>
<form action="" @submit.prevent="saveEmail()">
<div class="grid-cols-2 col-span-1">
<sw-input-group
:label="$t('settings.notification.email')"
:error="notificationEmailError"
class="my-2"
required
>
<sw-input
:invalid="$v.notification_email.$error"
v-model.trim="notification_email"
:placeholder="$tc('settings.notification.please_enter_email')"
type="text"
name="notification_email"
icon="envelope"
@input="$v.notification_email.$touch()"
/>
</sw-input-group>
<sw-button
:disabled="isLoading"
:loading="isLoading"
variant="primary"
type="submit"
class="my-6"
>
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
{{ $tc('settings.notification.save') }}
</sw-button>
</div>
</form>
<sw-divider class="mt-1 mb-6" />
<div class="flex mt-3 mb-4">
<div class="relative w-12">
<sw-switch
v-model="notify_invoice_viewed"
class="absolute"
style="top: -20px"
@change="setInvoiceViewd"
/>
</div>
<div class="ml-4">
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
{{ $t('settings.notification.invoice_viewed') }}
</p>
<p
class="p-0 m-0 text-xs leading-tight text-gray-500"
style="max-width: 480px"
>
{{ $t('settings.notification.invoice_viewed_desc') }}
</p>
</div>
</div>
<div class="flex mb-2">
<div class="relative w-12">
<sw-switch
v-model="notify_estimate_viewed"
class="absolute"
style="top: -20px"
@change="setEstimateViewd"
/>
</div>
<div class="ml-4">
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
{{ $t('settings.notification.estimate_viewed') }}
</p>
<p
class="p-0 m-0 text-xs leading-tight text-gray-500"
style="max-width: 480px"
>
{{ $t('settings.notification.estimate_viewed_desc') }}
</p>
</div>
</div>
</sw-card>
</div>
</template>
<script>
import { mapActions } from 'vuex'
const { required, email } = require('vuelidate/lib/validators')
export default {
data() {
return {
isLoading: false,
notification_email: null,
notify_invoice_viewed: null,
notify_estimate_viewed: null,
isRequestOnGoing: false,
}
},
validations: {
notification_email: {
required,
email,
},
},
computed: {
notificationEmailError() {
if (!this.$v.notification_email.$error) {
return ''
}
if (!this.$v.notification_email.required) {
return this.$tc('validation.required')
}
if (!this.$v.notification_email.email) {
return this.$tc('validation.email_incorrect')
}
},
},
mounted() {
this.fetchData()
},
methods: {
...mapActions('company', ['fetchCompanySettings', 'updateCompanySettings']),
async fetchData() {
this.isRequestOnGoing = true
let response = await this.fetchCompanySettings([
'notify_invoice_viewed',
'notify_estimate_viewed',
'notification_email',
])
if (response.data) {
this.notification_email = response.data.notification_email
response.data.notify_invoice_viewed === 'YES'
? (this.notify_invoice_viewed = true)
: (this.notify_invoice_viewed = false)
response.data.notify_estimate_viewed === 'YES'
? (this.notify_estimate_viewed = true)
: (this.notify_estimate_viewed = false)
}
this.isRequestOnGoing = false
},
async saveEmail() {
this.$v.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let data = {
settings: {
notification_email: this.notification_email,
},
}
let response = await this.updateCompanySettings(data)
if (response.data.success) {
this.isLoading = false
window.toastr['success'](
this.$tc('settings.notification.email_save_message')
)
}
},
async setInvoiceViewd(val) {
this.$v.$touch()
if (this.$v.$invalid) {
this.notify_invoice_viewed = !this.notify_invoice_viewed
return true
}
let data = {
settings: {
notify_invoice_viewed: this.notify_invoice_viewed ? 'YES' : 'NO',
},
}
let response = await this.updateCompanySettings(data)
if (response.data.success) {
window.toastr['success'](this.$tc('general.setting_updated'))
}
},
async setEstimateViewd(val) {
this.$v.$touch()
if (this.$v.$invalid) {
this.notify_estimate_viewed = !this.notify_estimate_viewed
return true
}
let data = {
settings: {
notify_estimate_viewed: this.notify_estimate_viewed ? 'YES' : 'NO',
},
}
let response = await this.updateCompanySettings(data)
if (response.data) {
window.toastr['success'](this.$tc('general.setting_updated'))
}
},
},
}
</script>

View File

@ -0,0 +1,148 @@
<template>
<sw-card variant="setting-card">
<div slot="header" class="flex flex-wrap justify-between lg:flex-no-wrap">
<div>
<h6 class="sw-section-title">
{{ $t('settings.customization.payments.payment_modes') }}
</h6>
<p
class="mt-2 text-sm leading-snug text-gray-500"
style="max-width: 680px"
>
{{ $t('settings.customization.payments.description') }}
</p>
</div>
<div class="mt-4 lg:mt-0 lg:ml-2">
<sw-button variant="primary-outline" size="lg" @click="addPaymentMode">
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('settings.customization.payments.add_payment_mode') }}
</sw-button>
</div>
</div>
<sw-table-component
ref="table"
variant="gray"
:show-filter="false"
:data="fetchData"
>
<sw-table-column
:label="$t('settings.customization.payments.mode_name')"
show="name"
>
<template slot-scope="row">
<span>{{ $t('settings.customization.payments.mode_name') }}</span>
<span class="mt-6"> {{ row.name }}</span>
</template>
</sw-table-column>
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.action') }}</span>
<sw-dropdown>
<dot-icon slot="activator" class="h-5" />
<sw-dropdown-item @click="editPaymentMode(row)">
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removePaymentMode(row.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</sw-table-column>
</sw-table-component>
</sw-card>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
const { required, maxLength, alpha } = require('vuelidate/lib/validators')
import { TrashIcon, PencilIcon, PlusIcon } from '@vue-hero-icons/solid'
export default {
components: {
TrashIcon,
PencilIcon,
PlusIcon,
},
methods: {
...mapActions('modal', ['openModal']),
...mapActions('payment', ['deletePaymentMode', 'fetchPaymentModes']),
async fetchData({ page, filter, sort }) {
let data = {
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page,
}
let response = await this.fetchPaymentModes(data)
return {
data: response.data.paymentMethods.data,
pagination: {
totalPages: response.data.paymentMethods.last_page,
currentPage: page,
count: response.data.paymentMethods.count,
},
}
},
addPaymentMode() {
this.openModal({
title: this.$t('settings.customization.payments.add_payment_mode'),
componentName: 'PaymentMode',
refreshData: this.$refs.table.refresh,
})
},
editPaymentMode(data) {
this.openModal({
title: this.$t('settings.customization.payments.edit_payment_mode'),
componentName: 'PaymentMode',
id: data.id,
data: data,
refreshData: this.$refs.table.refresh,
})
},
removePaymentMode(id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t(
'settings.customization.payments.payment_mode_confirm_delete'
),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
}).then(async (value) => {
if (value) {
let response = await this.deletePaymentMode(id)
if (response.data.success) {
window.toastr['success'](
this.$t('settings.customization.payments.deleted_message')
)
this.id = null
this.$refs.table.refresh()
return true
}
window.toastr['error'](
this.$t('settings.customization.payments.already_in_use')
)
}
})
},
},
}
</script>

View File

@ -1,251 +0,0 @@
<template>
<div class="setting-main-container">
<div class="card setting-card">
<div class="page-header">
<h3 class="page-title">{{ $tc('settings.preferences.preference',2) }}</h3>
<p class="page-sub-title">
{{ $t('settings.preferences.general_settings') }}
</p>
</div>
<form action="" @submit.prevent="updatePreferencesData">
<div class="row">
<div class="col-md-6 mb-4 form-group">
<label class="input-label">{{ $tc('settings.preferences.currency') }}</label><span class="text-danger"> * </span>
<base-select
v-model="formData.currency"
:options="currencies"
:custom-label="currencyNameWithCode"
:class="{'error': $v.formData.currency.$error }"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:placeholder="$tc('settings.currencies.select_currency')"
label="name"
track-by="id"
/>
<div v-if="$v.formData.currency.$error">
<span v-if="!$v.formData.currency.required" class="text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
<div class="col-md-6 mb-4 form-group">
<label class="input-label">{{ $tc('settings.preferences.language') }}</label><span class="text-danger"> * </span>
<base-select
v-model="formData.language"
:options="languages"
:class="{'error': $v.formData.language.$error }"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:placeholder="$tc('settings.preferences.select_language')"
label="name"
track-by="code"
/>
<div v-if="$v.formData.language.$error">
<span v-if="!$v.formData.language.required" class="text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
<div class="col-md-6 mb-4 form-group">
<label class="input-label">{{ $tc('settings.preferences.time_zone') }}</label><span class="text-danger"> * </span>
<base-select
v-model="formData.timeZone"
:options="timeZones"
:class="{'error': $v.formData.timeZone.$error }"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:placeholder="$tc('settings.preferences.select_time_zone')"
label="key"
track-by="key"
/>
<div v-if="$v.formData.timeZone.$error">
<span v-if="!$v.formData.timeZone.required" class="text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
<div class="col-md-6 mb-4 form-group">
<label class="input-label">{{ $tc('settings.preferences.date_format') }}</label><span class="text-danger"> * </span>
<base-select
v-model="formData.dateFormat"
:options="dateFormats"
:class="{'error': $v.formData.dateFormat.$error }"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:placeholder="$tc('settings.preferences.select_date_formate')"
label="display_date"
/>
<div v-if="$v.formData.dateFormat.$error">
<span v-if="!$v.formData.dateFormat.required" class="text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
<div class="col-md-6 mb-4 form-group">
<label class="input-label">{{ $tc('settings.preferences.fiscal_year') }}</label><span class="text-danger"> * </span>
<base-select
v-model="formData.fiscalYear"
:options="fiscalYears"
:class="{'error': $v.formData.fiscalYear.$error }"
:show-labels="false"
:allow-empty="false"
:searchable="true"
:placeholder="$tc('settings.preferences.select_financial_year')"
label="key"
track-by="value"
/>
<div v-if="$v.formData.fiscalYear.$error">
<span v-if="!$v.formData.fiscalYear.required" class="text-danger">{{ $tc('settings.company_info.errors.required') }}</span>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-12 input-group">
<base-button
:loading="isLoading"
:disabled="isLoading"
icon="save"
color="theme"
type="submit"
>
{{ $tc('settings.company_info.save') }}
</base-button>
</div>
</div>
</form>
<hr>
<div class="page-header mt-3">
<h3 class="page-title">{{ $t('settings.preferences.discount_setting') }}</h3>
<div class="flex-box">
<div class="left">
<base-switch
v-model="discount_per_item"
class="btn-switch"
@change="setDiscount"
/>
</div>
<div class="right ml-15">
<p class="box-title"> {{ $t('settings.preferences.discount_per_item') }} </p>
<p class="box-desc"> {{ $t('settings.preferences.discount_setting_description') }} </p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import MultiSelect from 'vue-multiselect'
import { validationMixin } from 'vuelidate'
import { mapActions } from 'vuex'
const { required } = require('vuelidate/lib/validators')
export default {
components: { MultiSelect },
mixins: [validationMixin],
data () {
return {
isLoading: false,
formData: {
language: null,
currency: null,
timeZone: null,
dateFormat: null,
fiscalYear: null
},
discount_per_item: null,
languages: [],
currencies: [],
timeZones: [],
dateFormats: [],
fiscalYears: []
}
},
validations: {
formData: {
currency: {
required
},
language: {
required
},
dateFormat: {
required
},
timeZone: {
required
},
fiscalYear: {
required
}
}
},
mounted () {
this.setInitialData()
this.getDiscountSettings()
},
methods: {
currencyNameWithCode ({name, code}) {
return `${code} - ${name}`
},
...mapActions('currency', [
'setDefaultCurrency'
]),
...mapActions('preferences', [
'loadData',
'editPreferences'
]),
async setInitialData () {
let response = await this.loadData()
this.languages = [...response.data.languages]
this.currencies = response.data.currencies
this.dateFormats = response.data.date_formats
this.timeZones = response.data.time_zones
this.fiscalYears = [...response.data.fiscal_years]
this.formData.currency = response.data.currencies.find(currency => currency.id == response.data.selectedCurrency)
this.formData.language = response.data.languages.find(language => language.code == response.data.selectedLanguage)
this.formData.timeZone = response.data.time_zones.find(timeZone => timeZone.value == response.data.time_zone)
this.formData.fiscalYear = response.data.fiscal_years.find(fiscalYear => fiscalYear.value == response.data.fiscal_year)
this.formData.dateFormat = response.data.date_formats.find(dateFormat => dateFormat.carbon_format_value == response.data.carbon_date_format)
},
async updatePreferencesData () {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let data = {
currency: this.formData.currency.id,
time_zone: this.formData.timeZone.value,
fiscal_year: this.formData.fiscalYear.value,
language: this.formData.language.code,
carbon_date_format: this.formData.dateFormat.carbon_format_value,
moment_date_format: this.formData.dateFormat.moment_format_value
}
let response = await this.editPreferences(data)
if (response.data.success) {
this.isLoading = false
window.i18n.locale = this.formData.language.code
this.setDefaultCurrency(this.formData.currency)
window.toastr['success'](this.$t('settings.preferences.updated_message'))
return true
}
window.toastr['error'](response.data.error)
return true
},
async getDiscountSettings () {
let response = await axios.get('/api/settings/get-setting?key=discount_per_item')
if (response.data) {
response.data.discount_per_item === 'YES' ?
this.discount_per_item = true :
this.discount_per_item = false
}
},
async setDiscount () {
let data = {
key: 'discount_per_item',
value: this.discount_per_item ? 'YES' : 'NO'
}
let response = await axios.put('/api/settings/update-setting', data)
if (response.data.success) {
window.toastr['success'](this.$t('general.setting_updated'))
}
}
}
}
</script>

View File

@ -0,0 +1,365 @@
<template>
<form action="" @submit.prevent="updatePreferencesData" class="relative">
<base-loader v-if="isRequestOnGoing" :show-bg-overlay="true" />
<sw-card variant="setting-card">
<template slot="header">
<h6 class="sw-section-title">
{{ $t('settings.menu_title.preferences') }}
</h6>
<p
class="mt-2 text-sm leading-snug text-gray-500"
style="max-width: 680px"
>
{{ $t('settings.preferences.general_settings') }}
</p>
</template>
<div class="grid gap-6 sm:grid-col-1 md:grid-cols-2">
<sw-input-group
:label="$tc('settings.preferences.currency')"
:error="currencyError"
required
>
<sw-select
v-model="formData.currency"
:options="currencies"
:custom-label="currencyNameWithCode"
:class="{ error: $v.formData.currency.$error }"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:placeholder="$tc('settings.currencies.select_currency')"
class="mt-2"
label="name"
track-by="id"
/>
</sw-input-group>
<sw-input-group
:label="$tc('settings.preferences.default_language')"
:error="languageError"
required
>
<sw-select
v-model="formData.language"
:options="languages"
:class="{ error: $v.formData.language.$error }"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:placeholder="$tc('settings.preferences.select_language')"
class="mt-2"
label="name"
track-by="code"
/>
</sw-input-group>
<sw-input-group
:label="$tc('settings.preferences.time_zone')"
:error="timeZoneError"
required
>
<sw-select
v-model="formData.timeZone"
:options="timeZones"
:class="{ error: $v.formData.timeZone.$error }"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:placeholder="$tc('settings.preferences.select_time_zone')"
class="mt-2"
label="key"
track-by="key"
/>
</sw-input-group>
<sw-input-group
:label="$tc('settings.preferences.date_format')"
:error="dateFormatError"
required
>
<sw-select
v-model="formData.dateFormat"
:options="dateFormats"
:class="{ error: $v.formData.dateFormat.$error }"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:placeholder="$tc('settings.preferences.select_date_format')"
class="mt-2"
label="display_date"
/>
</sw-input-group>
<sw-input-group
:label="$tc('settings.preferences.fiscal_year')"
:error="fiscalYearError"
class="mb-2"
required
>
<sw-select
v-model="formData.fiscalYear"
:options="fiscalYears"
:class="{ error: $v.formData.fiscalYear.$error }"
:show-labels="false"
:allow-empty="false"
:searchable="true"
:placeholder="$tc('settings.preferences.select_financial_year')"
label="key"
track-by="value"
/>
</sw-input-group>
</div>
<sw-button
class="mt-6"
variant="primary"
type="submit"
:disabled="isLoading"
:loading="isLoading"
>
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
{{ $tc('settings.company_info.save') }}
</sw-button>
<sw-divider class="mt-6 mb-8" />
<div class="flex">
<div class="relative w-12">
<sw-switch
v-model="discount_per_item"
class="absolute"
style="top: -18px"
@change="setDiscount"
/>
</div>
<div class="ml-15">
<p class="p-0 mb-1 text-base leading-snug text-black">
{{ $t('settings.preferences.discount_per_item') }}
</p>
<p
class="p-0 m-0 text-xs leading-tight text-gray-500"
style="max-width: 480px"
>
{{ $t('settings.preferences.discount_setting_description') }}
</p>
</div>
</div>
</sw-card>
</form>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
const { required } = require('vuelidate/lib/validators')
export default {
data() {
return {
isLoading: false,
formData: {
language: null,
currency: null,
timeZone: null,
dateFormat: null,
fiscalYear: null,
},
isRequestOnGoing: false,
discount_per_item: null,
}
},
validations: {
formData: {
currency: {
required,
},
language: {
required,
},
dateFormat: {
required,
},
timeZone: {
required,
},
fiscalYear: {
required,
},
},
},
computed: {
...mapGetters([
'currencies',
'timeZones',
'dateFormats',
'fiscalYears',
'languages',
]),
...mapGetters('company', ['defaultFiscalYear', 'defaultTimeZone']),
currencyError() {
if (!this.$v.formData.currency.$error) {
return ''
}
if (!this.$v.formData.currency.required) {
return this.$tc('validation.required')
}
},
languageError() {
if (!this.$v.formData.language.$error) {
return ''
}
if (!this.$v.formData.language.required) {
return this.$tc('validation.required')
}
},
timeZoneError() {
if (!this.$v.formData.timeZone.$error) {
return ''
}
if (!this.$v.formData.timeZone.required) {
return this.$tc('validation.required')
}
},
fiscalYearError() {
if (!this.$v.formData.fiscalYear.$error) {
return ''
}
if (!this.$v.formData.fiscalYear.required) {
return this.$tc('settings.company_info.errors.required')
}
},
dateFormatError() {
if (!this.$v.formData.dateFormat.$error) {
return ''
}
if (!this.$v.formData.dateFormat.required) {
return this.$tc('validation.required')
}
},
},
async mounted() {
this.setInitialData()
},
methods: {
...mapActions('company', [
'setDefaultCurrency',
'fetchCompanySettings',
'updateCompanySettings',
]),
...mapActions([
'fetchLanguages',
'fetchFiscalYears',
'fetchDateFormats',
'fetchTimeZones',
]),
currencyNameWithCode({ name, code }) {
return `${code} - ${name}`
},
async setInitialData() {
this.isRequestOnGoing = true
await this.fetchDateFormats()
await this.fetchLanguages()
await this.fetchFiscalYears()
await this.fetchTimeZones()
let response = await this.fetchCompanySettings([
'currency',
'time_zone',
'language',
'fiscal_year',
'carbon_date_format',
'moment_date_format',
'discount_per_item',
])
if (response.data) {
response.data.discount_per_item === 'YES'
? (this.discount_per_item = true)
: (this.discount_per_item = false)
this.formData.currency = this.currencies.find(
(currency) => currency.id == response.data.currency
)
this.formData.language = this.languages.find(
(language) => language.code == response.data.language
)
this.formData.timeZone = this.timeZones.find(
(timeZone) => timeZone.value == this.defaultTimeZone
)
this.formData.fiscalYear = this.fiscalYears.find(
(fiscalYear) => fiscalYear.value == this.defaultFiscalYear
)
this.formData.dateFormat = this.dateFormats.find(
(dateFormat) =>
dateFormat.carbon_format_value == response.data.carbon_date_format
)
}
this.isRequestOnGoing = false
},
async updatePreferencesData() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let data = {
settings: {
currency: this.formData.currency.id,
time_zone: this.formData.timeZone.value,
fiscal_year: this.formData.fiscalYear.value,
language: this.formData.language.code,
carbon_date_format: this.formData.dateFormat.carbon_format_value,
moment_date_format: this.formData.dateFormat.moment_format_value,
},
}
let response = await this.updateCompanySettings(data)
if (response.data.success) {
this.isLoading = false
// window.i18n.locale = this.formData.language.code
this.setDefaultCurrency(this.formData.currency)
window.toastr['success'](
this.$t('settings.preferences.updated_message')
)
return true
}
window.toastr['error'](response.data.error)
return true
},
async setDiscount() {
let data = {
settings: {
discount_per_item: this.discount_per_item ? 'YES' : 'NO',
},
}
let response = await this.updateCompanySettings(data)
if (response.data.success) {
window.toastr['success'](this.$t('general.setting_updated'))
}
},
},
}
</script>

View File

@ -0,0 +1,213 @@
<template>
<base-page>
<div class="pb-6">
<sw-page-header :title="$tc('settings.setting', 1)">
<sw-breadcrumb slot="breadcrumbs">
<sw-breadcrumb-item
:title="$t('general.home')"
to="/admin/dashboard"
/>
<sw-breadcrumb-item
:title="$tc('settings.setting', 2)"
to="/admin/settings/user-profile"
active
/>
</sw-breadcrumb>
</sw-page-header>
</div>
<div class="w-full mb-6 select-wrapper xl:hidden">
<sw-select
:options="menuItems"
v-model="currentSetting"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:custom-label="getCustomLabel"
@input="navigateToSetting"
/>
</div>
<div class="grid md:grid-cols-12">
<div class="hidden col-span-3 mt-1 xl:block">
<sw-list>
<sw-list-item
v-for="(menuItem, index) in menuItems"
:title="$t(menuItem.title)"
:key="index"
:to="menuItem.link"
:active="hasActiveUrl(menuItem.link)"
tag-name="router-link"
class="py-3"
>
<component slot="icon" :is="menuItem.icon" class="h-5" />
</sw-list-item>
</sw-list>
</div>
<div class="col-span-12 xl:col-span-9">
<transition name="fade" mode="out-in">
<router-view />
</transition>
</div>
</div>
</base-page>
</template>
<script>
import {
UserIcon,
OfficeBuildingIcon,
BellIcon,
CheckCircleIcon,
ClipboardListIcon,
CubeIcon,
ClipboardCheckIcon,
} from '@vue-hero-icons/outline'
import {
RefreshIcon,
CogIcon,
MailIcon,
PencilAltIcon,
CloudUploadIcon,
FolderIcon,
DatabaseIcon,
CreditCardIcon,
} from '@vue-hero-icons/solid'
export default {
components: {
UserIcon,
OfficeBuildingIcon,
PencilAltIcon,
CogIcon,
CheckCircleIcon,
ClipboardListIcon,
MailIcon,
BellIcon,
FolderIcon,
RefreshIcon,
CubeIcon,
CloudUploadIcon,
DatabaseIcon,
CreditCardIcon,
ClipboardCheckIcon,
},
data() {
return {
currentSetting: {
link: '/admin/settings/user-profile',
title: 'settings.menu_title.account_settings',
icon: 'user-icon',
},
menuItems: [
{
link: '/admin/settings/user-profile',
title: 'settings.menu_title.account_settings',
icon: 'user-icon',
},
{
link: '/admin/settings/company-info',
title: 'settings.menu_title.company_information',
icon: 'office-building-icon',
},
{
link: '/admin/settings/preferences',
title: 'settings.menu_title.preferences',
icon: 'cog-icon',
},
{
link: '/admin/settings/customization',
title: 'settings.menu_title.customization',
icon: 'pencil-alt-icon',
},
{
link: '/admin/settings/notifications',
title: 'settings.menu_title.notifications',
icon: 'bell-icon',
},
{
link: '/admin/settings/tax-types',
title: 'settings.menu_title.tax_types',
icon: 'check-circle-icon',
},
{
link: '/admin/settings/payment-mode',
title: 'settings.menu_title.payment_modes',
icon: 'credit-card-icon',
},
{
link: '/admin/settings/custom-fields',
title: 'settings.menu_title.custom_fields',
icon: 'cube-icon',
},
{
link: '/admin/settings/notes',
title: 'settings.menu_title.notes',
icon: 'clipboard-check-icon',
},
{
link: '/admin/settings/expense-category',
title: 'settings.menu_title.expense_category',
icon: 'clipboard-list-icon',
},
{
link: '/admin/settings/mail-configuration',
title: 'settings.mail.mail_config',
icon: 'mail-icon',
},
{
link: '/admin/settings/file-disk',
title: 'settings.menu_title.file_disk',
icon: 'folder-icon',
},
{
link: '/admin/settings/backup',
title: 'settings.menu_title.backup',
icon: 'database-icon',
},
{
link: '/admin/settings/update-app',
title: 'settings.menu_title.update_app',
icon: 'refresh-icon',
},
],
}
},
watch: {
'$route.path'(newValue) {
if (newValue === '/admin/settings') {
this.$router.push('/admin/settings/user-profile')
}
},
},
mounted() {
this.currentSetting = this.menuItems.find(
(item) => item.link == this.$route.path
)
},
created() {
if (this.$route.path === '/admin/settings') {
this.$router.push('/admin/settings/user-profile')
}
},
methods: {
getCustomLabel({ title }) {
return this.$t(title)
},
hasActiveUrl(url) {
return this.$route.path.indexOf(url) > -1
},
navigateToSetting(setting) {
this.$router.push(setting.link)
},
},
}
</script>

View File

@ -1,195 +0,0 @@
<template>
<div class="setting-main-container">
<div class="card setting-card">
<div class="page-header d-flex justify-content-between">
<div>
<h3 class="page-title">
{{ $t('settings.tax_types.title') }}
</h3>
<p class="page-sub-title">
{{ $t('settings.tax_types.description') }}
</p>
</div>
<base-button
outline
class="add-new-tax"
color="theme"
@click="openTaxModal"
>
{{ $t('settings.tax_types.add_new_tax') }}
</base-button>
</div>
<table-component
ref="table"
:show-filter="false"
:data="taxTypes"
table-class="table tax-table"
class="mb-3"
>
<table-column
:sortable="true"
:label="$t('settings.tax_types.tax_name')"
show="name"
/>
<table-column
:sortable="true"
:filterable="true"
:label="$t('settings.tax_types.compound_tax')"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.compound_tax') }}</span>
<div class="compound-tax">
{{ row.compound_tax ? 'Yes' : 'No' }}
</div>
</template>
</table-column>
<table-column
:sortable="true"
:filterable="true"
:label="$t('settings.tax_types.percent')"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.percent') }}</span>
{{ row.percent }} %
</template>
</table-column>
<table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.action') }}</span>
<v-dropdown>
<a slot="activator" href="#">
<dot-icon />
</a>
<v-dropdown-item>
<div class="dropdown-item" @click="EditTax(row.id)">
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
{{ $t('general.edit') }}
</div>
</v-dropdown-item>
<v-dropdown-item>
<div class="dropdown-item" @click="removeTax(row.id)">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
</template>
</table-column>
</table-component>
<hr>
<div class="page-header mt-3">
<h3 class="page-title">
{{ $t('settings.tax_types.tax_settings') }}
</h3>
<div class="flex-box">
<div class="left">
<base-switch
v-model="formData.tax_per_item"
class="btn-switch"
@change="setTax"
/>
</div>
<div class="right ml-15">
<p class="box-title"> {{ $t('settings.tax_types.tax_per_item') }} </p>
<p class="box-desc"> {{ $t('settings.tax_types.tax_setting_description') }} </p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
data () {
return {
id: null,
formData: {
tax_per_item: false
}
}
},
computed: {
...mapGetters('taxType', [
'taxTypes',
'getTaxTypeById'
])
},
mounted () {
this.getTaxSetting()
},
methods: {
...mapActions('modal', [
'openModal'
]),
...mapActions('taxType', [
'indexLoadData',
'deleteTaxType',
'fetchTaxType'
]),
async getTaxSetting (val) {
let response = await axios.get('/api/settings/get-setting?key=tax_per_item')
if (response.data && response.data.tax_per_item === 'YES') {
this.formData.tax_per_item = true
} else {
this.formData.tax_per_item = false
}
},
async setTax (val) {
let data = {
key: 'tax_per_item',
value: this.formData.tax_per_item ? 'YES' : 'NO'
}
let response = await axios.put('/api/settings/update-setting', data)
if (response.data) {
window.toastr['success'](this.$t('general.setting_updated'))
}
},
async removeTax (id, index) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('settings.tax_types.confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
let response = await this.deleteTaxType(id)
if (response.data.success) {
window.toastr['success'](this.$t('settings.tax_types.deleted_message'))
this.id = null
this.$refs.table.refresh()
return true
}
window.toastr['error'](this.$t('settings.tax_types.already_in_use'))
}
})
},
openTaxModal () {
this.openModal({
'title': this.$t('settings.tax_types.add_tax'),
'componentName': 'TaxTypeModal'
})
this.$refs.table.refresh()
},
async EditTax (id) {
let response = await this.fetchTaxType(id)
this.openModal({
'title': this.$t('settings.tax_types.edit_tax'),
'componentName': 'TaxTypeModal',
'id': id,
'data': response.data.taxType
})
this.$refs.table.refresh()
}
}
}
</script>

View File

@ -0,0 +1,243 @@
<template>
<sw-card variant="setting-card">
<div slot="header" class="flex flex-wrap justify-between lg:flex-no-wrap">
<div>
<h6 class="sw-section-title">
{{ $t('settings.tax_types.title') }}
</h6>
<p
class="mt-2 text-sm leading-snug text-gray-500"
style="max-width: 680px"
>
{{ $t('settings.tax_types.description') }}
</p>
</div>
<div class="mt-4 lg:mt-0 lg:ml-2">
<sw-button size="lg" variant="primary-outline" @click="openTaxModal">
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('settings.tax_types.add_new_tax') }}
</sw-button>
</div>
</div>
<sw-table-component
ref="table"
:show-filter="false"
:data="fetchData"
table-class="table"
variant="gray"
>
<sw-table-column
:sortable="true"
:label="$t('settings.tax_types.tax_name')"
show="name"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.tax_name') }}</span>
<span class="mt-6">{{ row.name }}</span>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:filterable="true"
:label="$t('settings.tax_types.compound_tax')"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.compound_tax') }}</span>
<sw-badge
:bg-color="
$utils.getBadgeStatusColor(row.compound_tax ? 'YES' : 'NO')
.bgColor
"
:color="
$utils.getBadgeStatusColor(row.compound_tax ? 'YES' : 'NO').color
"
>
{{ row.compound_tax ? 'Yes' : 'No'.replace('_', ' ') }}
</sw-badge>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:filterable="true"
:label="$t('settings.tax_types.percent')"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.percent') }}</span>
{{ row.percent }} %
</template>
</sw-table-column>
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.action') }}</span>
<sw-dropdown>
<dot-icon slot="activator" />
<sw-dropdown-item @click="editTax(row.id)">
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removeTax(row.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</sw-table-column>
</sw-table-component>
<sw-divider class="my-8" />
<div class="flex mt-2">
<div class="relative w-12">
<sw-switch
v-model="formData.tax_per_item"
class="absolute"
style="top: -20px"
@change="setTax"
/>
</div>
<div class="ml-4">
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
{{ $t('settings.tax_types.tax_per_item') }}
</p>
<p
class="p-0 m-0 text-xs leading-4 text-gray-500"
style="max-width: 480px"
>
{{ $t('settings.tax_types.tax_setting_description') }}
</p>
</div>
</div>
</sw-card>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { TrashIcon, PencilIcon, PlusIcon } from '@vue-hero-icons/solid'
export default {
components: {
TrashIcon,
PencilIcon,
PlusIcon,
},
data() {
return {
formData: {
tax_per_item: false,
},
isRequestOnGoing: false,
}
},
computed: {
...mapGetters('taxType', ['taxTypes', 'getTaxTypeById']),
},
mounted() {
this.getTaxSetting()
},
methods: {
...mapActions('modal', ['openModal']),
...mapActions('taxType', [
'indexLoadData',
'deleteTaxType',
'fetchTaxType',
'fetchTaxTypes',
]),
...mapActions('company', ['fetchCompanySettings', 'updateCompanySettings']),
async fetchData({ page, filter, sort }) {
let data = {
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page,
}
let response = await this.fetchTaxTypes(data)
return {
data: response.data.taxTypes.data,
pagination: {
totalPages: response.data.taxTypes.last_page,
currentPage: page,
count: response.data.taxTypes.count,
},
}
},
async getTaxSetting() {
let response = await this.fetchCompanySettings(['tax_per_item'])
if (response.data) {
this.formData.tax_per_item = response.data.tax_per_item === 'YES'
}
},
async setTax(val) {
let data = {
settings: {
tax_per_item: this.formData.tax_per_item ? 'YES' : 'NO',
},
}
let response = await this.updateCompanySettings(data)
if (response.data) {
window.toastr['success'](this.$t('general.setting_updated'))
}
},
async removeTax(id, index) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('settings.tax_types.confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
}).then(async (value) => {
if (value) {
let response = await this.deleteTaxType(id)
if (response.data.success) {
window.toastr['success'](
this.$t('settings.tax_types.deleted_message')
)
this.$refs.table.refresh()
return true
}
window.toastr['error'](this.$t('settings.tax_types.already_in_use'))
}
})
},
openTaxModal() {
this.openModal({
title: this.$t('settings.tax_types.add_tax'),
componentName: 'TaxTypeModal',
refreshData: this.$refs.table.refresh,
})
},
async editTax(id) {
let response = await this.fetchTaxType(id)
this.openModal({
title: this.$t('settings.tax_types.edit_tax'),
componentName: 'TaxTypeModal',
id: id,
data: response.data.taxType,
refreshData: this.$refs.table.refresh,
})
},
},
}
</script>

View File

@ -1,108 +1,141 @@
<template>
<div class="setting-main-container update-container">
<div class="card setting-card">
<div class="page-header">
<h3 class="page-title">{{ $t('settings.update_app.title') }}</h3>
<p class="page-sub-title">
{{ $t('settings.update_app.description') }}
</p>
<label class="input-label">{{
$t('settings.update_app.current_version')
}}</label
<sw-card variant="setting-card">
<template slot="header">
<h6 class="sw-section-title">
{{ $t('settings.update_app.title') }}
</h6>
<p
class="mt-2 text-sm leading-snug text-gray-500"
style="max-width: 680px"
>
{{ $t('settings.update_app.description') }}
</p>
</template>
<div class="m-0">
<label class="text-sm not-italic font-medium input-label">
{{ $t('settings.update_app.current_version') }}
</label>
<label
class="box-border flex w-16 p-3 my-2 text-sm text-gray-500 bg-gray-200 border border-gray-200 border-solid rounded-md version"
>
{{ currentVersion }}
</label>
<sw-button
:loading="isCheckingforUpdate"
:disabled="isCheckingforUpdate || isUpdating"
variant="primary-outline"
class="mt-6"
@click="checkUpdate"
>
{{ $t('settings.update_app.check_update') }}
</sw-button>
<sw-divider v-if="isUpdateAvailable" class="mt-2 mb-4" />
<div v-show="!isUpdating" v-if="isUpdateAvailable" class="mt-4 content">
<h6 class="mb-8 sw-section-title">
{{ $t('settings.update_app.avail_update') }}
</h6>
<label class="text-sm not-italic font-medium input-label">
{{ $t('settings.update_app.next_version') }} </label
><br />
<label class="version mb-4">{{ currentVersion }}</label>
<base-button
:outline="true"
:disabled="isCheckingforUpdate || isUpdating"
size="large"
color="theme"
class="mb-4"
@click="checkUpdate"
<label
class="box-border flex w-16 p-3 my-2 text-sm text-gray-500 bg-gray-200 border border-gray-200 border-solid rounded-md version"
>
<font-awesome-icon
:class="{ update: isCheckingforUpdate }"
style="margin-right: 10px;"
icon="sync-alt"
/>
{{ $t('settings.update_app.check_update') }}
</base-button>
<hr />
<div v-show="!isUpdating" v-if="isUpdateAvailable" class="mt-4 content">
<h3 class="page-title mb-3">
{{ $t('settings.update_app.avail_update') }}
</h3>
<label class="input-label">{{
$t('settings.update_app.next_version')
}}</label
><br />
<label class="version">{{ updateData.version }}</label>
<p
class="page-sub-title"
style="white-space: pre-wrap;"
v-html="description"
>
</p>
<label class="input-label">
{{ $t('settings.update_app.requirements') }}
</label>
<div
{{ updateData.version }}
</label>
<p
class="mb-8 text-sm leading-snug text-gray-500"
style="white-space: pre-wrap; max-width: 480px"
v-html="description"
>
</p>
<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 table-fixed">
<tr
class="p-2 border-2 border-gray-200"
v-for="(ext, i) in requiredExtentions"
:key="i"
class="col-md-8 p-0"
>
<div class="update-requirements">
<div class="d-flex justify-content-between">
<span>{{ i }}</span>
<span v-if="ext" class="verified" />
<span v-else class="not-verified" />
</div>
</div>
</div>
<base-button
size="large"
icon="rocket"
color="theme"
class="mt-5"
v-if="hasUiUpdate"
@click="onUpdateApp"
>
{{ $t('settings.update_app.update') }}
</base-button>
</div>
<div v-if="isUpdating" class="mt-4 content">
<div class="d-flex flex-row justify-content-between">
<div>
<h3 class="page-title">
{{ $t('settings.update_app.update_progress') }}
</h3>
<p class="page-sub-title">
{{ $t('settings.update_app.progress_text') }}
</p>
</div>
<font-awesome-icon icon="spinner" class="update-spinner fa-spin" />
</div>
<ul class="update-steps-container">
<li class="update-step" v-for="step in updateSteps">
<p class="update-step-text">{{ $t(step.translationKey) }}</p>
<div class="update-status-container">
<span v-if="step.time" class="update-time">
{{step.time}}
</span>
<span :class="'update-status status-' + getStatus(step)">
{{getStatus(step)}}
</span>
</div>
</li>
</ul>
</div>
<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 rounded-full bg-success"
/>
<span
v-else
class="inline-block w-4 h-4 ml-3 mr-2 rounded-full bg-danger"
/>
</td>
</tr>
</table>
<sw-button
size="lg"
class="mt-10"
variant="primary"
@click="onUpdateApp"
>
{{ $t('settings.update_app.update') }}
</sw-button>
</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>
<loading-icon
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
class="flex justify-between w-full py-3 border-b border-gray-200 border-solid last:border-b-0"
v-for="step in updateSteps"
>
<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="block py-1 text-sm text-center uppercase rounded-full"
:class="statusClass(step)"
style="width: 88px"
>
{{ getStatus(step) }}
</span>
</div>
</li>
</ul>
</div>
</div>
</sw-card>
</template>
<script>
import LoadingIcon from '../../components/icon/LoadingIcon'
export default {
components: {
LoadingIcon,
},
data() {
return {
isShowProgressBar: false,
@ -113,38 +146,39 @@ export default {
interval: null,
description: '',
currentVersion: '',
requiredExtentions: null,
updateSteps: [
{
translationKey: 'settings.update_app.download_zip_file',
stepUrl: '/api/update/download',
stepUrl: '/api/v1/update/download',
time: null,
started: false,
completed: false,
},
{
translationKey: 'settings.update_app.unzipping_package',
stepUrl: '/api/update/unzip',
stepUrl: '/api/v1/update/unzip',
time: null,
started: false,
completed: false,
},
{
translationKey: 'settings.update_app.copying_files',
stepUrl: '/api/update/copy',
stepUrl: '/api/v1/update/copy',
time: null,
started: false,
completed: false,
},
{
translationKey: 'settings.update_app.running_migrations',
stepUrl: '/api/update/migrate',
stepUrl: '/api/v1/update/migrate',
time: null,
started: false,
completed: false,
},
{
translationKey: 'settings.update_app.finishing_update',
stepUrl: '/api/update/finish',
stepUrl: '/api/v1/update/finish',
time: null,
started: false,
completed: false,
@ -178,15 +212,14 @@ export default {
}
})
},
mounted() {
window.axios.get('/api/settings/app/version').then((res) => {
window.axios.get('/api/v1/app/version').then((res) => {
this.currentVersion = res.data.version
})
},
methods: {
closeHandler() {
console.log('closing')
},
getStatus(step) {
if (step.started && step.completed) {
return 'finished'
@ -198,15 +231,34 @@ export default {
return 'error'
}
},
statusClass(step) {
const status = this.getStatus(step)
switch (status) {
case 'pending':
return 'text-primary-800 bg-gray-200'
break
case 'finished':
return 'text-teal-500 bg-teal-100'
break
case 'running':
return 'text-blue-400 bg-blue-100'
break
case 'error':
return 'text-danger bg-red-200'
break
}
},
async checkUpdate() {
try {
this.isCheckingforUpdate = true
let response = await window.axios.get('/api/check/update')
let response = await window.axios.get('/api/v1/check/update')
this.isCheckingforUpdate = false
if (!response.data.version) {
window.toastr['info'](this.$t('settings.update_app.latest_message'))
return
}
@ -214,6 +266,7 @@ export default {
this.updateData.isMinor = response.data.is_minor
this.updateData.version = response.data.version.version
this.description = response.data.version.description
this.requiredExtentions = response.data.version.extensions
this.isUpdateAvailable = true
this.requiredExtentions = response.data.version.extensions
this.minPhpVesrion = response.data.version.minimum_php_version
@ -224,6 +277,7 @@ export default {
window.toastr['error']('Something went wrong')
}
},
async onUpdateApp() {
let path = null
if (!this.allowToUpdate) {
@ -274,6 +328,7 @@ export default {
}
}
},
onUpdateFailed(translationKey) {
let stepName = this.$t(translationKey)
swal({
@ -293,25 +348,3 @@ export default {
},
}
</script>
<style scoped>
.update-requirements {
/* display: flex;
justify-content: space-between; */
padding: 10px;
border: 1px solid #eaf1fb;
}
.update {
transform: rotate(360deg);
animation: rotating 1s linear infinite;
}
@keyframes rotating {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@ -1,224 +0,0 @@
<template>
<div class="setting-main-container">
<form action="" @submit.prevent="updateUserData">
<div class="card setting-card">
<div class="page-header">
<h3 class="page-title">{{ $t('settings.account_settings.account_settings') }}</h3>
<p class="page-sub-title">
{{ $t('settings.account_settings.section_description') }}
</p>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="input-label">{{ $tc('settings.account_settings.profile_picture') }}</label>
<div id="pick-avatar" class="image-upload-box avatar-upload">
<div class="overlay">
<font-awesome-icon class="white-icon" icon="camera"/>
</div>
<img v-if="previewAvatar" :src="previewAvatar" class="preview-logo">
<div v-if="!previewAvatar" class="upload-content">
<font-awesome-icon class="upload-icon" icon="cloud-upload-alt"/>
<p class="upload-text"> {{ $tc('general.choose_file') }} </p>
</div>
</div>
</div>
<avatar-cropper
:labels="{ submit: 'Submit', cancel: 'Cancel'}"
:cropper-options="cropperOptions"
:output-options="cropperOutputOptions"
:output-quality="0.8"
:upload-handler="cropperHandler"
trigger="#pick-avatar"
@changed="setFileObject"
@error="handleUploadError"
/>
</div>
<div class="row">
<div class="col-md-6 mb-4 form-group">
<label class="input-label">{{ $tc('settings.account_settings.name') }}</label>
<base-input
v-model="formData.name"
:invalid="$v.formData.name.$error"
:placeholder="$t('settings.user_profile.name')"
@input="$v.formData.name.$touch()"
/>
<div v-if="$v.formData.name.$error">
<span v-if="!$v.formData.name.required" class="text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
<div class="col-md-6 mb-4 form-group">
<label class="input-label">{{ $tc('settings.account_settings.email') }}</label>
<base-input
v-model="formData.email"
:invalid="$v.formData.email.$error"
:placeholder="$t('settings.user_profile.email')"
@input="$v.formData.email.$touch()"
/>
<div v-if="$v.formData.email.$error">
<span v-if="!$v.formData.email.required" class="text-danger">{{ $tc('validation.required') }}</span>
<span v-if="!$v.formData.email.email" class="text-danger">{{ $tc('validation.email_incorrect') }}</span>
</div>
</div>
<div class="col-md-6 mb-4 form-group">
<label class="input-label">{{ $tc('settings.account_settings.password') }}</label>
<base-input
v-model="formData.password"
:invalid="$v.formData.password.$error"
:placeholder="$t('settings.user_profile.password')"
type="password"
@input="$v.formData.password.$touch()"
/>
<div v-if="$v.formData.password.$error">
<span v-if="!$v.formData.password.minLength" class="text-danger"> {{ $tc('validation.password_min_length', $v.formData.password.$params.minLength.min, {count: $v.formData.password.$params.minLength.min}) }} </span>
</div>
</div>
<div class="col-md-6 mb-4 form-group">
<label class="input-label">{{ $tc('settings.account_settings.confirm_password') }}</label>
<base-input
v-model="formData.confirm_password"
:invalid="$v.formData.confirm_password.$error"
:placeholder="$t('settings.user_profile.confirm_password')"
type="password"
@input="$v.formData.confirm_password.$touch()"
/>
<div v-if="$v.formData.confirm_password.$error">
<span v-if="!$v.formData.confirm_password.sameAsPassword" class="text-danger">{{ $tc('validation.password_incorrect') }}</span>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-12 input-group">
<base-button
:loading="isLoading"
:disabled="isLoading"
icon="save"
color="theme"
type="submit"
>
{{ $tc('settings.account_settings.save') }}
</base-button>
</div>
</div>
</div>
</form>
</div>
</template>
<script>
import { validationMixin } from 'vuelidate'
import { mapActions } from 'vuex'
import AvatarCropper from 'vue-avatar-cropper'
const { required, requiredIf, sameAs, email, minLength } = require('vuelidate/lib/validators')
export default {
components: { AvatarCropper },
mixins: [validationMixin],
data () {
return {
cropperOutputOptions: {
width: 150,
height: 150
},
cropperOptions: {
autoCropArea: 1,
viewMode: 0,
movable: true,
zoomable: true
},
formData: {
name: null,
email: null,
password: null,
confirm_password: null
},
isLoading: false,
previewAvatar: null,
fileObject: null
}
},
validations: {
formData: {
name: {
required
},
email: {
required,
email
},
password: {
minLength: minLength(5)
},
confirm_password: {
required: requiredIf('isRequired'),
sameAsPassword: sameAs('password')
}
}
},
computed: {
isRequired () {
if (this.formData.password === null || this.formData.password === undefined || this.formData.password === '') {
return false
}
return true
}
},
mounted () {
this.setInitialData()
},
methods: {
...mapActions('userProfile', [
'loadData',
'editUser',
'uploadAvatar'
]),
cropperHandler (cropper) {
this.previewAvatar = cropper.getCroppedCanvas().toDataURL(this.cropperOutputMime)
},
setFileObject (file) {
this.fileObject = file
},
handleUploadError (message, type, xhr) {
window.toastr['error']('Oops! Something went wrong...')
},
async setInitialData () {
let response = await this.loadData()
this.formData.name = response.data.name
this.formData.email = response.data.email
if (response.data.avatar) {
this.previewAvatar = response.data.avatar
} else {
this.previewAvatar = '/images/default-avatar.jpg'
}
},
async updateUserData () {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let data = {
name: this.formData.name,
email: this.formData.email
}
if (this.formData.password != null && this.formData.password !== undefined && this.formData.password !== '') {
data = { ...data, password: this.formData.password }
}
let response = await this.editUser(data)
if (response.data.success) {
this.isLoading = false
if (this.fileObject && this.previewAvatar) {
let avatarData = new FormData()
avatarData.append('admin_avatar', JSON.stringify({
name: this.fileObject.name,
data: this.previewAvatar
}))
this.uploadAvatar(avatarData)
}
window.toastr['success'](this.$t('settings.account_settings.updated_message'))
return true
}
window.toastr['error'](response.data.error)
return true
}
}
}
</script>

View File

@ -0,0 +1,372 @@
<template>
<form @submit.prevent="updateUserData" class="relative h-full">
<base-loader v-if="isRequestOnGoing" :show-bg-overlay="true" />
<sw-card variant="setting-card">
<template slot="header">
<h6 class="sw-section-title">
{{ $t('settings.account_settings.account_settings') }}
</h6>
<p
class="mt-2 text-sm leading-snug text-gray-500"
style="max-width: 680px"
>
{{ $t('settings.account_settings.section_description') }}
</p>
</template>
<div class="grid mb-4 md:grid-cols-6">
<div>
<label
class="text-sm not-italic font-medium leading-4 text-black whitespace-no-wrap"
>
{{ $tc('settings.account_settings.profile_picture') }}
</label>
<sw-avatar
:preview-avatar="previewAvatar"
:label="$tc('general.choose_file')"
@changed="onChange"
@uploadHandler="onUploadHandler"
@handleUploadError="onHandleUploadError"
>
<template v-slot:icon>
<cloud-upload-icon
class="h-5 mb-2 text-xl leading-6 text-gray-400"
/>
</template>
</sw-avatar>
</div>
</div>
<div class="grid gap-6 sm:grid-col-1 md:grid-cols-2">
<sw-input-group
:label="$tc('settings.account_settings.name')"
:error="nameError"
>
<sw-input
v-model="formData.name"
:invalid="$v.formData.name.$error"
:placeholder="$t('settings.user_profile.name')"
class="mt-2"
@input="$v.formData.name.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$tc('settings.account_settings.email')"
:error="emailError"
>
<sw-input
v-model="formData.email"
:invalid="$v.formData.email.$error"
:placeholder="$t('settings.user_profile.email')"
class="mt-2"
@input="$v.formData.email.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$tc('settings.account_settings.password')"
:error="passwordError"
>
<sw-input
v-model="formData.password"
:invalid="$v.formData.password.$error"
:placeholder="$t('settings.user_profile.password')"
type="password"
class="mt-2"
@input="$v.formData.password.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$tc('settings.account_settings.confirm_password')"
:error="confirmPasswordError"
class="mt-1 mb-2"
>
<sw-input
v-model="formData.confirm_password"
:invalid="$v.formData.confirm_password.$error"
:placeholder="$t('settings.user_profile.confirm_password')"
type="password"
@input="$v.formData.confirm_password.$touch()"
/>
</sw-input-group>
</div>
<div class="grid gap-6 mt-4 sm:grid-col-1 md:grid-cols-2">
<sw-input-group
:label="$tc('settings.language')"
:error="languageError"
>
<sw-select
v-model="language"
:options="languages"
:class="{ error: $v.language.$error }"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:placeholder="$tc('settings.preferences.select_language')"
class="mt-2"
label="name"
track-by="code"
/>
</sw-input-group>
</div>
<sw-button
class="mt-6"
:loading="isLoading"
:disabled="isLoading"
variant="primary"
>
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
{{ $tc('settings.account_settings.save') }}
</sw-button>
</sw-card>
</form>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex'
import { CloudUploadIcon } from '@vue-hero-icons/solid'
import BaseLoader from '../../components/base/BaseLoader.vue'
const {
required,
requiredIf,
sameAs,
email,
minLength,
} = require('vuelidate/lib/validators')
export default {
components: {
CloudUploadIcon,
BaseLoader,
},
data() {
return {
formData: {
name: null,
email: null,
password: null,
confirm_password: null,
},
isLoading: false,
previewAvatar: null,
cropperOutputMime: '',
fileObject: null,
language: null,
isRequestOnGoing: false,
}
},
validations: {
formData: {
name: {
required,
},
email: {
required,
email,
},
password: {
minLength: minLength(8),
},
confirm_password: {
sameAsPassword: sameAs('password'),
},
},
language: {
required,
},
},
computed: {
...mapGetters(['languages']),
emailError() {
if (!this.$v.formData.email.$error) {
return ''
}
if (!this.$v.formData.email.required) {
return this.$tc('validation.required')
}
if (!this.$v.formData.email.email) {
return this.$tc('validation.email_incorrect')
}
},
passwordError() {
if (!this.$v.formData.password.$error) {
return ''
}
if (!this.$v.formData.password.minLength) {
return this.$tc(
'validation.password_min_length',
this.$v.formData.password.$params.minLength.min,
{ count: this.$v.formData.password.$params.minLength.min }
)
}
},
nameError() {
if (!this.$v.formData.name.$error) {
return ''
}
if (!this.$v.formData.name.required) {
return this.$tc('validation.required')
}
},
confirmPasswordError() {
if (!this.$v.formData.confirm_password.$error) {
return ''
}
if (!this.$v.formData.confirm_password.sameAsPassword) {
return this.$tc('validation.password_incorrect')
}
},
languageError() {
if (!this.$v.language.$error) {
return ''
}
if (!this.$v.language.required) {
return this.$tc('validation.required')
}
},
},
watch: {
'formData.password'(val) {
if (!val) {
this.formData.confirm_password = ''
}
},
},
mounted() {
this.setInitialData()
this.fetchLanguages()
},
methods: {
...mapActions('user', [
'fetchCurrentUser',
'updateCurrentUser',
'fetchUserSettings',
'updateUserSettings',
'uploadAvatar',
]),
...mapActions(['fetchLanguages']),
onUploadHandler(cropper) {
this.previewAvatar = cropper
.getCroppedCanvas()
.toDataURL(this.cropperOutputMime)
},
onHandleUploadError() {
window.toastr['error']('Oops! Something went wrong...')
},
onChange(file) {
this.cropperOutputMime = file.type
this.fileObject = file
},
async setInitialData() {
this.isRequestOnGoing = true
let response = await this.fetchCurrentUser()
this.formData.name = response.data.user.name
this.formData.email = response.data.user.email
if (response.data.user.avatar) {
this.previewAvatar = response.data.user.avatar
} else {
this.previewAvatar = '/images/default-avatar.jpg'
}
let res = await this.fetchUserSettings(['language'])
this.language = this.languages.find(
(language) => language.code == res.data.language
)
this.isRequestOnGoing = false
},
async updateUserData() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let data = {
name: this.formData.name,
email: this.formData.email,
}
if (
this.formData.password != null &&
this.formData.password !== undefined &&
this.formData.password !== ''
) {
data = { ...data, password: this.formData.password }
}
let response = await this.updateCurrentUser(data)
let languageData = {
settings: {
language: this.language.code,
},
}
let languageRes = await this.updateUserSettings(languageData)
// if(languageRes) {
// window.i18n.locale = this.language.code
// }
if (response.data.success) {
this.isLoading = false
if (this.fileObject && this.previewAvatar) {
let avatarData = new FormData()
avatarData.append(
'admin_avatar',
JSON.stringify({
name: this.fileObject.name,
data: this.previewAvatar,
})
)
this.uploadAvatar(avatarData)
}
window.toastr['success'](
this.$t('settings.account_settings.updated_message')
)
this.formData.password = ''
this.formData.confirm_password = ''
return true
}
window.toastr['error'](response.data.error)
this.isLoading = false
return true
},
},
}
</script>

View File

@ -0,0 +1,148 @@
<template>
<transition name="fade">
<div class="address-tab">
<form action="" class="px-4 py-2" @submit.prevent="updateAddressSetting">
<div class="grid grid-cols-12 mt-6">
<div class="col-span-12 mb-6">
<label class="text-sm font-medium leading-5 text-dark non-italic">
{{
$t('settings.customization.addresses.customer_billing_address')
}}
</label>
<base-custom-input
v-model="addresses.billing_address_format"
:types="billingAddressType"
class="mt-2"
/>
</div>
<div class="col-span-12 mb-6">
<label class="text-sm font-medium leading-5 text-dark non-italic">
{{
$t('settings.customization.addresses.customer_shipping_address')
}}
</label>
<base-custom-input
v-model="addresses.shipping_address_format"
:types="shippingAddressType"
class="mt-2"
/>
</div>
<div class="col-span-12 mb-6">
<label class="text-sm font-medium leading-5 text-dark non-italic">
{{ $t('settings.customization.addresses.company_address') }}
</label>
<base-custom-input
v-model="addresses.company_address_format"
:types="companyAddressType"
class="mt-2"
/>
</div>
</div>
<div class="grid grid-cols-12">
<div class="col-span-12">
<sw-button
:disabled="isLoading"
:loading="isLoading"
variant="primary"
type="submit"
>
<save-icon v-if="!isLoading" class="mr-2" />
{{ $t('settings.customization.save') }}
</sw-button>
</div>
</div>
</form>
</div>
</transition>
</template>
<script>
export default {
data() {
return {
isLoading: false,
addresses: {
billing_address_format: '',
shipping_address_format: '',
company_address_format: '',
},
billingAddressType: [
{
label: 'Customer',
fields: [
{ label: 'Display Name', value: 'CONTACT_DISPLAY_NAME' },
{ label: 'Contact Name', value: 'PRIMARY_CONTACT_NAME' },
{ label: 'Email', value: 'CONTACT_EMAIL' },
{ label: 'Phone', value: 'CONTACT_PHONE' },
{ label: 'Website', value: 'CONTACT_WEBSITE' },
],
},
{
label: 'Billing Address',
fields: [
{ label: 'Adddress name', value: 'BILLING_ADDRESS_NAME' },
{ label: 'Country', value: 'BILLING_COUNTRY' },
{ label: 'State', value: 'BILLING_STATE' },
{ label: 'City', value: 'BILLING_CITY' },
{ label: 'Address Street 1', value: 'BILLING_ADDRESS_STREET_1' },
{ label: 'Address Street 2', value: 'BILLING_ADDRESS_STREET_2' },
{ label: 'Phone', value: 'BILLING_PHONE' },
{ label: 'Zip Code', value: 'BILLING_ZIP_CODE' },
],
},
],
shippingAddressType: [
{
label: 'Customer',
fields: [
{ label: 'Display Name', value: 'CONTACT_DISPLAY_NAME' },
{ label: 'Contact Name', value: 'PRIMARY_CONTACT_NAME' },
{ label: 'Email', value: 'CONTACT_EMAIL' },
{ label: 'Phone', value: 'CONTACT_PHONE' },
{ label: 'Website', value: 'CONTACT_WEBSITE' },
],
},
{
label: 'Shipping Address',
fields: [
{ label: 'Adddress name', value: 'SHIPPING_ADDRESS_NAME' },
{ label: 'Country', value: 'SHIPPING_COUNTRY' },
{ label: 'State', value: 'SHIPPING_STATE' },
{ label: 'City', value: 'SHIPPING_CITY' },
{ label: 'Address Street 1', value: 'SHIPPING_ADDRESS_STREET_1' },
{ label: 'Address Street 2', value: 'SHIPPING_ADDRESS_STREET_2' },
{ label: 'Phone', value: 'SHIPPING_PHONE' },
{ label: 'Zip Code', value: 'SHIPPING_ZIP_CODE' },
],
},
],
companyAddressType: [
{
label: 'Company Address',
fields: [
{ label: 'Company Name', value: 'COMPANY_NAME' },
{ label: 'Address street 1', value: 'COMPANY_ADDRESS_STREET_1' },
{ label: 'Address Street 2', value: 'COMPANY_ADDRESS_STREET_2' },
{ label: 'Country', value: 'COMPANY_COUNTRY' },
{ label: 'State', value: 'COMPANY_STATE' },
{ label: 'City', value: 'COMPANY_CITY' },
{ label: 'Zip Code', value: 'COMPANY_ZIP_CODE' },
{ label: 'Phone', value: 'COMPANY_PHONE' },
],
},
],
}
},
methods: {
async updateAddressSetting() {
let data = { type: 'ADDRESSES', ...this.addresses, large: true }
// if (this.updateSetting(data)) {
window.toastr['success'](
this.$t('settings.customization.addresses.address_setting_updated')
)
// }
},
},
}
</script>

View File

@ -0,0 +1,272 @@
<template>
<div>
<form action="" class="mt-6" @submit.prevent="updateEstimateSetting">
<sw-input-group
:label="$t('settings.customization.estimates.estimate_prefix')"
:error="estimatePrefixError"
>
<sw-input
v-model="estimates.estimate_prefix"
:invalid="$v.estimates.estimate_prefix.$error"
style="max-width: 30%"
@input="$v.estimates.estimate_prefix.$touch()"
@keyup="changeToUppercase('ESTIMATES')"
/>
</sw-input-group>
<sw-input-group
:label="
$t('settings.customization.estimates.default_estimate_email_body')
"
class="mt-6 mb-4"
>
<base-custom-input
v-model="estimates.estimate_mail_body"
:fields="mailFields"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.customization.estimates.company_address_format')"
class="mt-6 mb-4"
>
<base-custom-input
v-model="estimates.company_address_format"
:fields="companyFields"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.customization.estimates.shipping_address_format')"
class="mt-6 mb-4"
>
<base-custom-input
v-model="estimates.shipping_address_format"
:fields="shippingFields"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.customization.estimates.billing_address_format')"
class="mt-6 mb-4"
>
<base-custom-input
v-model="estimates.billing_address_format"
:fields="billingFields"
/>
</sw-input-group>
<sw-button
:disabled="isLoading"
:loading="isLoading"
variant="primary"
type="submit"
class="mt-4"
>
<save-icon v-if="!isLoading" class="mr-2" />
{{ $t('settings.customization.save') }}
</sw-button>
</form>
<sw-divider class="mt-6 mb-8" />
<div class="flex">
<div class="relative w-12">
<sw-switch
v-model="estimateAutogenerate"
class="absolute"
style="top: -20px"
@change="setEstimateSetting"
/>
</div>
<div class="ml-4">
<p class="p-0 mb-1 text-base leading-snug text-black">
{{
$t('settings.customization.estimates.autogenerate_estimate_number')
}}
</p>
<p
class="p-0 m-0 text-xs leading-tight text-gray-500"
style="max-width: 480px"
>
{{
$t('settings.customization.estimates.estimate_setting_description')
}}
</p>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
const { required, maxLength, alpha } = require('vuelidate/lib/validators')
export default {
props: {
settings: {
type: Object,
require: true,
default: false,
},
},
data() {
return {
estimateAutogenerate: false,
estimates: {
estimate_prefix: null,
estimate_mail_body: null,
estimate_terms_and_conditions: null,
company_address_format: null,
shipping_address_format: null,
billing_address_format: null,
},
billingFields: [
'billing',
'customer',
'customerCustom',
'estimateCustom',
],
shippingFields: [
'shipping',
'customer',
'customerCustom',
'estimateCustom',
],
mailFields: [
'customer',
'estimate',
'company',
'customerCustom',
'estimateCustom',
],
companyFields: ['company', 'estimateCustom'],
isLoading: false,
}
},
computed: {
estimatePrefixError() {
if (!this.$v.estimates.estimate_prefix.$error) {
return ''
}
if (!this.$v.estimates.estimate_prefix.required) {
return this.$t('validation.required')
}
if (!this.$v.estimates.estimate_prefix.maxLength) {
return this.$t('validation.prefix_maxlength')
}
if (!this.$v.estimates.estimate_prefix.alpha) {
return this.$t('validation.characters_only')
}
},
},
validations: {
estimates: {
estimate_prefix: {
required,
maxLength: maxLength(5),
alpha,
},
},
},
watch: {
settings(val) {
this.estimates.estimate_prefix = val ? val.estimate_prefix : ''
this.estimates.estimate_mail_body = val ? val.estimate_mail_body : ''
this.estimates.company_address_format = val
? val.estimate_company_address_format
: ''
this.estimates.shipping_address_format = val
? val.estimate_shipping_address_format
: ''
this.estimates.billing_address_format = val
? val.estimate_billing_address_format
: ''
this.estimates.estimate_terms_and_conditions = val
? val.estimate_terms_and_conditions
: ''
this.estimate_auto_generate = val ? val.estimate_auto_generate : ''
if (this.estimate_auto_generate === 'YES') {
this.estimateAutogenerate = true
} else {
this.estimateAutogenerate = false
}
},
},
methods: {
...mapActions('company', ['updateCompanySettings']),
async setEstimateSetting() {
let data = {
settings: {
estimate_auto_generate: this.estimateAutogenerate ? 'YES' : 'NO',
},
}
let response = await this.updateCompanySettings(data)
if (response.data) {
window.toastr['success'](this.$t('general.setting_updated'))
}
},
changeToUppercase(currentTab) {
if (currentTab === 'ESTIMATES') {
this.estimates.estimate_prefix = this.estimates.estimate_prefix.toUpperCase()
return true
}
},
async updateEstimateSetting() {
this.$v.estimates.$touch()
if (this.$v.estimates.$invalid) {
return false
}
let data = {
settings: {
estimate_prefix: this.estimates.estimate_prefix,
estimate_mail_body: this.estimates.estimate_mail_body,
estimate_company_address_format: this.estimates
.company_address_format,
estimate_shipping_address_format: this.estimates
.shipping_address_format,
estimate_billing_address_format: this.estimates
.billing_address_format,
},
}
if (this.updateSetting(data)) {
window.toastr['success'](
this.$t('settings.customization.estimates.estimate_setting_updated')
)
}
},
async updateSetting(data) {
this.isLoading = true
let res = await this.updateCompanySettings(data)
if (res.data.success) {
this.isLoading = false
return true
}
return false
},
},
}
</script>

View File

@ -0,0 +1,268 @@
<template>
<div>
<form action="" class="mt-6" @submit.prevent="updateInvoiceSetting">
<sw-input-group
:label="$t('settings.customization.invoices.invoice_prefix')"
:error="invoicePrefixError"
>
<sw-input
v-model="invoices.invoice_prefix"
:invalid="$v.invoices.invoice_prefix.$error"
style="max-width: 30%"
@input="$v.invoices.invoice_prefix.$touch()"
@keyup="changeToUppercase('INVOICES')"
/>
</sw-input-group>
<sw-input-group
:label="
$t('settings.customization.invoices.default_invoice_email_body')
"
class="mt-6 mb-4"
>
<base-custom-input
v-model="invoices.invoice_mail_body"
:fields="InvoiceMailFields"
class="mt-2"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.customization.invoices.company_address_format')"
class="mt-6 mb-4"
>
<base-custom-input
v-model="invoices.company_address_format"
:fields="companyFields"
class="mt-2"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.customization.invoices.shipping_address_format')"
class="mt-6 mb-4"
>
<base-custom-input
v-model="invoices.shipping_address_format"
:fields="shippingFields"
class="mt-2"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.customization.invoices.billing_address_format')"
class="mt-6 mb-4"
>
<base-custom-input
v-model="invoices.billing_address_format"
:fields="billingFields"
class="mt-2"
/>
</sw-input-group>
<sw-button
:loading="isLoading"
:disabled="isLoading"
variant="primary"
type="submit"
class="mt-4"
>
<save-icon v-if="!isLoading" class="mr-2" />
{{ $t('settings.customization.save') }}
</sw-button>
</form>
<sw-divider class="mt-6 mb-8" />
<div class="flex">
<div class="relative w-12">
<sw-switch
v-model="invoiceAutogenerate"
class="absolute"
style="top: -20px"
@change="setInvoiceSetting"
/>
</div>
<div class="ml-4">
<p class="p-0 mb-1 text-base leading-snug text-black">
{{
$t('settings.customization.invoices.autogenerate_invoice_number')
}}
</p>
<p
class="p-0 m-0 text-xs leading-tight text-gray-500"
style="max-width: 480px"
>
{{
$t('settings.customization.invoices.invoice_setting_description')
}}
</p>
</div>
</div>
</div>
</template>
<script>
const { required, maxLength, alpha } = require('vuelidate/lib/validators')
import { mapActions, mapGetters } from 'vuex'
export default {
props: {
settings: {
type: Object,
require: true,
default: false,
},
},
data() {
return {
invoiceAutogenerate: false,
invoices: {
invoice_prefix: null,
invoice_mail_body: null,
company_address_format: null,
shipping_address_format: null,
billing_address_format: null,
},
isLoading: false,
InvoiceMailFields: [
'customer',
'customerCustom',
'invoice',
'invoiceCustom',
'company',
],
billingFields: ['billing', 'customer', 'customerCustom', 'invoiceCustom'],
shippingFields: [
'shipping',
'customer',
'customerCustom',
'invoiceCustom',
],
companyFields: ['company', 'invoiceCustom'],
}
},
computed: {
invoicePrefixError() {
if (!this.$v.invoices.invoice_prefix.$error) {
return ''
}
if (!this.$v.invoices.invoice_prefix.required) {
return this.$t('validation.required')
}
if (!this.$v.invoices.invoice_prefix.maxLength) {
return this.$t('validation.prefix_maxlength')
}
if (!this.$v.invoices.invoice_prefix.alpha) {
return this.$t('validation.characters_only')
}
},
},
watch: {
settings(val) {
this.invoices.invoice_prefix = val ? val.invoice_prefix : ''
this.invoices.invoice_mail_body = val ? val.invoice_mail_body : null
this.invoices.company_address_format = val
? val.invoice_company_address_format
: null
this.invoices.shipping_address_format = val
? val.invoice_shipping_address_format
: null
this.invoices.billing_address_format = val
? val.invoice_billing_address_format
: null
this.invoice_auto_generate = val ? val.invoice_auto_generate : ''
if (this.invoice_auto_generate === 'YES') {
this.invoiceAutogenerate = true
} else {
this.invoiceAutogenerate = false
}
},
},
validations: {
invoices: {
invoice_prefix: {
required,
maxLength: maxLength(5),
alpha,
},
},
},
methods: {
...mapActions('company', ['updateCompanySettings']),
async setInvoiceSetting() {
let data = {
settings: {
invoice_auto_generate: this.invoiceAutogenerate ? 'YES' : 'NO',
},
}
let response = await this.updateCompanySettings(data)
if (response.data) {
window.toastr['success'](this.$t('general.setting_updated'))
}
},
changeToUppercase(currentTab) {
if (currentTab === 'INVOICES') {
this.invoices.invoice_prefix = this.invoices.invoice_prefix.toUpperCase()
return true
}
},
async updateInvoiceSetting() {
this.$v.invoices.$touch()
if (this.$v.invoices.$invalid) {
return false
}
let data = {
settings: {
invoice_prefix: this.invoices.invoice_prefix,
invoice_mail_body: this.invoices.invoice_mail_body,
invoice_company_address_format: this.invoices.company_address_format,
invoice_billing_address_format: this.invoices.billing_address_format,
invoice_shipping_address_format: this.invoices
.shipping_address_format,
},
}
if (this.updateSetting(data)) {
window.toastr['success'](
this.$t('settings.customization.invoices.invoice_setting_updated')
)
}
},
async updateSetting(data) {
this.isLoading = true
let res = await this.updateCompanySettings(data)
if (res.data.success) {
this.isLoading = false
return true
}
return false
},
},
}
</script>

View File

@ -0,0 +1,132 @@
<template>
<div>
<div class="flex flex-wrap justify-end mt-8 lg:flex-no-wrap">
<sw-button size="lg" variant="primary-outline" @click="addItemUnit">
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('settings.customization.items.add_item_unit') }}
</sw-button>
</div>
<sw-table-component
ref="table"
variant="gray"
:data="fetchData"
:show-filter="false"
>
<sw-table-column
:sortable="true"
:label="$t('settings.customization.items.unit_name')"
show="name"
>
<template slot-scope="row">
<span>{{ $t('settings.customization.items.unit_name') }}</span>
<span class="mt-6">{{ row.name }}</span>
</template>
</sw-table-column>
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.action') }}</span>
<sw-dropdown>
<dot-icon slot="activator" class="h-5 mr-3 text-primary-800" />
<sw-dropdown-item @click="editItemUnit(row)">
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removeItemUnit(row.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</sw-table-column>
</sw-table-component>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { TrashIcon, PencilIcon, PlusIcon } from '@vue-hero-icons/solid'
const { required, maxLength, alpha } = require('vuelidate/lib/validators')
export default {
components: {
TrashIcon,
PlusIcon,
PencilIcon,
},
methods: {
...mapActions('modal', ['openModal']),
...mapActions('item', ['deleteItemUnit', 'fetchItemUnits']),
async fetchData({ page, filter, sort }) {
let data = {
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page,
}
let response = await this.fetchItemUnits(data)
return {
data: response.data.units.data,
pagination: {
totalPages: response.data.units.last_page,
currentPage: page,
count: response.data.units.count,
},
}
},
async addItemUnit() {
this.openModal({
title: this.$t('settings.customization.items.add_item_unit'),
componentName: 'ItemUnit',
refreshData: this.$refs.table.refresh,
})
},
async editItemUnit(data) {
this.openModal({
title: this.$t('settings.customization.items.edit_item_unit'),
componentName: 'ItemUnit',
id: data.id,
data: data,
refreshData: this.$refs.table.refresh,
})
},
async removeItemUnit(id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('settings.customization.items.item_unit_confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
}).then(async (value) => {
if (value) {
let response = await this.deleteItemUnit(id)
if (response.data.success) {
window.toastr['success'](
this.$t('settings.customization.items.deleted_message')
)
this.$refs.table.refresh()
return true
}
window.toastr['error'](
this.$t('settings.customization.items.already_in_use')
)
}
})
},
},
}
</script>

View File

@ -0,0 +1,251 @@
<template>
<div>
<form action="" class="mt-6" @submit.prevent="updatePaymentSetting">
<sw-input-group
:label="$t('settings.customization.payments.payment_prefix')"
:error="paymentPrefixError"
>
<sw-input
v-model="payments.payment_prefix"
:invalid="$v.payments.payment_prefix.$error"
class="mt-2"
style="max-width: 30%"
@input="$v.payments.payment_prefix.$touch()"
@keyup="changeToUppercase('PAYMENTS')"
/>
</sw-input-group>
<sw-input-group
:label="
$t('settings.customization.payments.default_payment_email_body')
"
class="mt-6 mb-4"
>
<base-custom-input
v-model="payments.payment_mail_body"
:fields="mailFields"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.customization.payments.company_address_format')"
class="mt-6 mb-4"
>
<base-custom-input
v-model="payments.company_address_format"
:fields="companyFields"
/>
</sw-input-group>
<sw-input-group
:label="
$t('settings.customization.payments.from_customer_address_format')
"
class="mt-6 mb-4"
>
<base-custom-input
v-model="payments.from_customer_address_format"
:fields="customerAddressFields"
/>
</sw-input-group>
<sw-button
:loading="isLoading"
:disabled="isLoading"
variant="primary"
type="submit"
class="my-4"
>
<save-icon v-if="!isLoading" class="mr-2" />
{{ $t('settings.customization.save') }}
</sw-button>
</form>
<sw-divider class="mt-6 mb-8" />
<div class="flex">
<div class="relative w-12">
<sw-switch
v-model="paymentAutogenerate"
class="absolute"
style="top: -20px"
@change="setPaymentSetting"
/>
</div>
<div class="ml-4">
<p class="p-0 mb-1 text-base leading-snug text-black">
{{
$t('settings.customization.payments.autogenerate_payment_number')
}}
</p>
<p
class="p-0 m-0 text-xs leading-tight text-gray-500"
style="max-width: 480px"
>
{{
$t('settings.customization.payments.payment_setting_description')
}}
</p>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
const { required, maxLength, alpha } = require('vuelidate/lib/validators')
export default {
props: {
settings: {
type: Object,
require: true,
default: false,
},
},
data() {
return {
paymentAutogenerate: false,
payments: {
payment_prefix: null,
payment_mail_body: null,
from_customer_address_format: null,
company_address_format: null,
},
mailFields: [
'customer',
'customerCustom',
'company',
'payment',
'paymentCustom',
],
customerAddressFields: [
'billing',
'customer',
'customerCustom',
'paymentCustom',
],
companyFields: ['company', 'paymentCustom'],
isLoading: false,
}
},
computed: {
paymentPrefixError() {
if (!this.$v.payments.payment_prefix.$error) {
return ''
}
if (!this.$v.payments.payment_prefix.required) {
return this.$t('validation.required')
}
if (!this.$v.payments.payment_prefix.maxLength) {
return this.$t('validation.prefix_maxlength')
}
if (!this.$v.payments.payment_prefix.alpha) {
return this.$t('validation.characters_only')
}
},
},
validations: {
payments: {
payment_prefix: {
required,
maxLength: maxLength(5),
alpha,
},
},
},
watch: {
settings(val) {
this.payments.payment_prefix = val ? val.payment_prefix : ''
this.payments.payment_mail_body = val ? val.payment_mail_body : ''
this.payments.company_address_format = val
? val.payment_company_address_format
: ''
this.payments.from_customer_address_format = val
? val.payment_from_customer_address_format
: ''
this.payment_auto_generate = val ? val.payment_auto_generate : ''
if (this.payment_auto_generate === 'YES') {
this.paymentAutogenerate = true
} else {
this.paymentAutogenerate = false
}
},
},
methods: {
...mapActions('modal', ['openModal']),
...mapActions('company', ['updateCompanySettings']),
changeToUppercase(currentTab) {
if (currentTab === 'PAYMENTS') {
this.payments.payment_prefix = this.payments.payment_prefix.toUpperCase()
return true
}
},
async setPaymentSetting() {
let data = {
settings: {
payment_auto_generate: this.paymentAutogenerate ? 'YES' : 'NO',
},
}
let response = await this.updateCompanySettings(data)
if (response.data) {
window.toastr['success'](this.$t('general.setting_updated'))
}
},
async updatePaymentSetting() {
this.$v.payments.$touch()
if (this.$v.payments.$invalid) {
return false
}
let data = {
settings: {
payment_prefix: this.payments.payment_prefix,
payment_mail_body: this.payments.payment_mail_body,
payment_company_address_format: this.payments.company_address_format,
payment_from_customer_address_format: this.payments
.from_customer_address_format,
},
}
if (this.updateSetting(data)) {
window.toastr['success'](
this.$t('settings.customization.payments.payment_setting_updated')
)
}
},
async updateSetting(data) {
this.isLoading = true
let res = await this.updateCompanySettings(data)
if (res.data.success) {
this.isLoading = false
return true
}
return false
},
},
}
</script>

View File

@ -1,111 +0,0 @@
<template>
<div class="invoice-create-page main-content">
<div class="page-header">
<h3 class="page-title">{{ $tc('settings.setting',1) }}</h3>
<ol class="breadcrumb">
<li class="breadcrumb-item"><router-link slot="item-title" to="/admin/dashboard">{{ $t('general.home') }}</router-link></li>
<li class="breadcrumb-item"><router-link slot="item-title" to="/admin/settings/user-profile">{{ $tc('settings.setting', 2) }}</router-link></li>
</ol>
</div>
<div class="row settings-container">
<div class="col-lg-3 settings-sidebar-container">
<ol class="settings-sidebar">
<li v-for="(menuItem, index) in menuItems" :key="index" class="settings-menu-item">
<router-link :class="['link-color', {'active-setting': hasActiveUrl(menuItem.link)}]" :to="menuItem.link">
<font-awesome-icon :icon="[menuItem.iconType, menuItem.icon]" class="setting-icon"/>
<span class="menu-title ml-3">{{ $t(menuItem.title) }}</span>
</router-link>
</li>
</ol>
</div>
<div class="col-lg-9">
<transition
name="fade"
mode="out-in">
<router-view/>
</transition>
</div>
</div>
</div>
</template>
<script>
export default {
data () {
return {
menuItems: [
{
link: '/admin/settings/user-profile',
title: 'settings.menu_title.account_settings',
icon: 'user',
iconType: 'far'
},
{
link: '/admin/settings/company-info',
title: 'settings.menu_title.company_information',
icon: 'building',
iconType: 'far'
},
{
link: '/admin/settings/customization',
title: 'settings.menu_title.customization',
icon: 'edit',
iconType: 'fa'
},
{
link: '/admin/settings/preferences',
title: 'settings.menu_title.preferences',
icon: 'cog',
iconType: 'fas'
},
{
link: '/admin/settings/tax-types',
title: 'settings.menu_title.tax_types',
icon: 'check-circle',
iconType: 'far'
},
{
link: '/admin/settings/expense-category',
title: 'settings.menu_title.expense_category',
icon: 'list-alt',
iconType: 'far'
},
{
link: '/admin/settings/mail-configuration',
title: 'settings.mail.mail_config',
icon: 'envelope',
iconType: 'fa'
},
{
link: '/admin/settings/notifications',
title: 'settings.menu_title.notifications',
icon: 'bell',
iconType: 'far'
},
{
link: '/admin/settings/update-app',
title: 'settings.menu_title.update_app',
icon: 'sync-alt',
iconType: 'fas'
}
]
}
},
watch: {
'$route.path' (newValue) {
if (newValue === '/admin/settings') {
this.$router.push('/admin/settings/user-profile')
}
}
},
created () {
if (this.$route.path === '/admin/settings') {
this.$router.push('/admin/settings/user-profile')
}
},
methods: {
hasActiveUrl (url) {
return this.$route.path.indexOf(url) > -1
}
}
}
</script>

View File

@ -0,0 +1,165 @@
<template>
<form @submit.prevent="saveEmailConfig">
<div class="grid gap-6 grid-col-1 md:grid-cols-2">
<sw-input-group
:label="$t('settings.mail.driver')"
:error="driverError"
required
>
<sw-select
v-model="mailConfigData.mail_driver"
:invalid="$v.mailConfigData.mail_driver.$error"
:options="mailDrivers"
:searchable="true"
:allow-empty="false"
:show-labels="false"
class="mt-2"
@input="onChangeDriver"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.from_mail')"
:error="fromEmailError"
required
>
<sw-input
:invalid="$v.mailConfigData.from_mail.$error"
v-model.trim="mailConfigData.from_mail"
type="text"
name="from_mail"
class="mt-2"
@input="$v.mailConfigData.from_mail.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.from_name')"
:error="fromNameError"
required
>
<sw-input
:invalid="$v.mailConfigData.from_name.$error"
v-model.trim="mailConfigData.from_name"
type="text"
name="name"
class="mt-2"
@input="$v.mailConfigData.from_name.$touch()"
/>
</sw-input-group>
</div>
<div class="flex mt-8">
<sw-button
:disabled="loading"
:loading="loading"
variant="primary"
type="submit"
>
<save-icon class="mr-2" />
{{ $t('general.save') }}
</sw-button>
<slot />
</div>
</form>
</template>
<script>
const { required, email } = require('vuelidate/lib/validators')
export default {
props: {
configData: {
type: Object,
require: true,
default: Object,
},
loading: {
type: Boolean,
require: true,
default: false,
},
mailDrivers: {
type: Array,
require: true,
default: Array,
},
},
data() {
return {
mailConfigData: {
mail_driver: '',
mail_host: '',
from_mail: '',
from_name: '',
},
}
},
validations: {
mailConfigData: {
mail_driver: {
required,
},
from_mail: {
required,
email,
},
from_name: {
required,
},
},
},
computed: {
driverError() {
if (!this.$v.mailConfigData.mail_driver.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_driver.required) {
return this.$tc('validation.required')
}
},
fromEmailError() {
if (!this.$v.mailConfigData.from_mail.$error) {
return ''
}
if (!this.$v.mailConfigData.from_mail.required) {
return this.$tc('validation.required')
}
if (!this.$v.mailConfigData.from_mail.email) {
return this.$tc('validation.email_incorrect')
}
},
fromNameError() {
if (!this.$v.mailConfigData.from_name.$error) {
return ''
}
if (!this.$v.mailConfigData.from_name.required) {
return this.$tc('validation.required')
}
},
},
mounted() {
for (const key in this.mailConfigData) {
if (this.configData.hasOwnProperty(key)) {
this.mailConfigData[key] = this.configData[key]
}
}
},
methods: {
async saveEmailConfig() {
this.$v.mailConfigData.$touch()
if (!this.$v.mailConfigData.$invalid) {
this.$emit('submit-data', this.mailConfigData)
}
return false
},
onChangeDriver() {
this.$v.mailConfigData.mail_driver.$touch()
this.$emit('on-change-driver', this.mailConfigData.mail_driver)
},
},
}
</script>

View File

@ -0,0 +1,278 @@
<template>
<form @submit.prevent="saveEmailConfig">
<div class="grid gap-6 sm:grid-col-1 md:grid-cols-2">
<sw-input-group
:label="$t('settings.mail.driver')"
:error="driverError"
required
>
<sw-select
v-model="mailConfigData.mail_driver"
:invalid="$v.mailConfigData.mail_driver.$error"
:options="mailDrivers"
:allow-empty="false"
:searchable="true"
:show-labels="false"
class="mt-2"
@input="onChangeDriver"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.mailgun_domain')"
:error="domainError"
required
>
<sw-input
:invalid="$v.mailConfigData.mail_mailgun_domain.$error"
v-model.trim="mailConfigData.mail_mailgun_domain"
type="text"
name="mailgun_domain"
class="mt-2"
@input="$v.mailConfigData.mail_mailgun_domain.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.mailgun_secret')"
:error="secretError"
required
>
<sw-input
:invalid="$v.mailConfigData.mail_mailgun_secret.$error"
v-model.trim="mailConfigData.mail_mailgun_secret"
:type="getInputType"
name="mailgun_secret"
class="mt-2"
@input="$v.mailConfigData.mail_mailgun_secret.$touch()"
>
<template v-slot:rightIcon>
<eye-off-icon
v-if="isShowPassword"
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
<eye-icon
v-else
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
</template>
</sw-input>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.mailgun_endpoint')"
:error="endpointError"
required
>
<sw-input
:invalid="$v.mailConfigData.mail_mailgun_endpoint.$error"
v-model.trim="mailConfigData.mail_mailgun_endpoint"
type="text"
name="mailgun_endpoint"
class="mt-2"
@input="$v.mailConfigData.mail_mailgun_endpoint.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.from_mail')"
:error="fromEmailError"
required
>
<sw-input
:invalid="$v.mailConfigData.from_mail.$error"
v-model.trim="mailConfigData.from_mail"
type="text"
name="from_mail"
class="mt-2"
@input="$v.mailConfigData.from_mail.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.from_name')"
:error="fromNameError"
required
>
<sw-input
:invalid="$v.mailConfigData.from_name.$error"
v-model.trim="mailConfigData.from_name"
type="text"
name="from_name"
class="mt-2"
@input="$v.mailConfigData.from_name.$touch()"
/>
</sw-input-group>
</div>
<div class="flex my-10">
<sw-button
:disabled="loading"
:loading="loading"
variant="primary"
type="submit"
>
<save-icon class="mr-2" />
{{ $t('general.save') }}
</sw-button>
<slot />
</div>
</form>
</template>
<script>
const { required, email, numeric } = require('vuelidate/lib/validators')
import { EyeIcon, EyeOffIcon } from '@vue-hero-icons/outline'
export default {
props: {
configData: {
type: Object,
require: true,
default: Object,
},
loading: {
type: Boolean,
require: true,
default: false,
},
mailDrivers: {
type: Array,
require: true,
default: Array,
},
},
components: {
EyeIcon,
EyeOffIcon,
},
data() {
return {
isShowPassword: false,
mailConfigData: {
mail_driver: '',
mail_mailgun_domain: '',
mail_mailgun_secret: '',
mail_mailgun_endpoint: '',
from_mail: '',
from_name: '',
},
}
},
validations: {
mailConfigData: {
mail_driver: {
required,
},
mail_mailgun_domain: {
required,
},
mail_mailgun_endpoint: {
required,
},
mail_mailgun_secret: {
required,
},
from_mail: {
required,
email,
},
from_name: {
required,
},
},
},
computed: {
driverError() {
if (!this.$v.mailConfigData.mail_driver.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_driver.required) {
return this.$tc('validation.required')
}
},
domainError() {
if (!this.$v.mailConfigData.mail_mailgun_domain.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_mailgun_domain.required) {
return this.$tc('validation.required')
}
},
secretError() {
if (!this.$v.mailConfigData.mail_mailgun_secret.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_mailgun_secret.required) {
return this.$tc('validation.required')
}
},
endpointError() {
if (!this.$v.mailConfigData.mail_mailgun_endpoint.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_mailgun_endpoint.required) {
return this.$tc('validation.required')
}
if (!this.$v.mailConfigData.mail_mailgun_endpoint.numeric) {
return this.$tc('validation.numbers_only')
}
},
fromEmailError() {
if (!this.$v.mailConfigData.from_mail.$error) {
return ''
}
if (!this.$v.mailConfigData.from_mail.required) {
return this.$tc('validation.required')
}
if (!this.$v.mailConfigData.from_mail.email) {
return this.$tc('validation.email_incorrect')
}
},
fromNameError() {
if (!this.$v.mailConfigData.from_name.$error) {
return ''
}
if (!this.$v.mailConfigData.from_name.required) {
return this.$tc('validation.required')
}
},
getInputType() {
if (this.isShowPassword) {
return 'text'
}
return 'password'
},
},
mounted() {
for (const key in this.mailConfigData) {
if (this.configData.hasOwnProperty(key)) {
this.mailConfigData[key] = this.configData[key]
}
}
},
methods: {
async saveEmailConfig() {
this.$v.mailConfigData.$touch()
if (!this.$v.mailConfigData.$invalid) {
this.$emit('submit-data', this.mailConfigData)
}
return false
},
onChangeDriver() {
this.$v.mailConfigData.mail_driver.$touch()
this.$emit('on-change-driver', this.mailConfigData.mail_driver)
},
},
}
</script>

View File

@ -0,0 +1,334 @@
<template>
<form @submit.prevent="saveEmailConfig">
<div class="grid gap-6 sm:grid-col-1 md:grid-cols-2">
<sw-input-group
:label="$t('settings.mail.driver')"
:error="driverError"
required
>
<sw-select
v-model="mailConfigData.mail_driver"
:invalid="$v.mailConfigData.mail_driver.$error"
:options="mailDrivers"
:allow-empty="false"
:searchable="true"
:show-labels="false"
class="mt-2"
@input="onChangeDriver"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.host')"
:error="hostError"
required
>
<sw-input
:invalid="$v.mailConfigData.mail_host.$error"
v-model.trim="mailConfigData.mail_host"
type="text"
name="mail_host"
class="mt-2"
@input="$v.mailConfigData.mail_host.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.port')"
:error="portError"
required
>
<sw-input
:invalid="$v.mailConfigData.mail_port.$error"
v-model.trim="mailConfigData.mail_port"
type="text"
name="mail_port"
class="mt-2"
@input="$v.mailConfigData.mail_port.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.encryption')"
:error="encryptionError"
required
>
<sw-select
v-model.trim="mailConfigData.mail_encryption"
:invalid="$v.mailConfigData.mail_encryption.$error"
:options="encryptions"
:searchable="true"
:show-labels="false"
class="mt-2"
@input="$v.mailConfigData.mail_encryption.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.from_mail')"
:error="fromEmailError"
required
>
<sw-input
:invalid="$v.mailConfigData.from_mail.$error"
v-model.trim="mailConfigData.from_mail"
type="text"
name="from_mail"
class="mt-2"
@input="$v.mailConfigData.from_mail.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.from_name')"
:error="fromNameError"
required
>
<sw-input
:invalid="$v.mailConfigData.from_name.$error"
v-model.trim="mailConfigData.from_name"
type="text"
name="name"
class="mt-2"
@input="$v.mailConfigData.from_name.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.ses_key')"
:error="keyError"
required
>
<sw-input
:invalid="$v.mailConfigData.mail_ses_key.$error"
v-model.trim="mailConfigData.mail_ses_key"
type="text"
name="mail_ses_key"
class="mt-2"
@input="$v.mailConfigData.mail_ses_key.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.ses_secret')"
:error="secretError"
required
>
<sw-input
:invalid="$v.mailConfigData.mail_ses_secret.$error"
v-model.trim="mailConfigData.mail_ses_secret"
:type="getInputType"
name="mail_ses_secret"
class="mt-2"
@input="$v.mailConfigData.mail_ses_secret.$touch()"
>
<template v-slot:rightIcon>
<eye-off-icon
v-if="isShowPassword"
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
<eye-icon
v-else
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
</template>
</sw-input>
</sw-input-group>
</div>
<div class="flex my-10">
<sw-button
:disabled="loading"
:loading="loading"
variant="primary"
type="submit"
>
<save-icon class="mr-2" />
{{ $t('general.save') }}
</sw-button>
<slot />
</div>
</form>
</template>
<script>
const { required, email, numeric } = require('vuelidate/lib/validators')
import { EyeIcon, EyeOffIcon } from '@vue-hero-icons/outline'
export default {
props: {
configData: {
type: Object,
require: true,
default: Object,
},
loading: {
type: Boolean,
require: true,
default: false,
},
mailDrivers: {
type: Array,
require: true,
default: Array,
},
},
components: {
EyeOffIcon,
EyeIcon,
},
data() {
return {
isShowPassword: false,
mailConfigData: {
mail_driver: '',
mail_host: '',
mail_port: null,
mail_ses_key: '',
mail_ses_secret: '',
mail_encryption: 'tls',
from_mail: '',
from_name: '',
},
encryptions: ['tls', 'ssl', 'starttls'],
}
},
validations: {
mailConfigData: {
mail_driver: {
required,
},
mail_host: {
required,
},
mail_port: {
required,
numeric,
},
mail_ses_key: {
required,
},
mail_ses_secret: {
required,
},
mail_encryption: {
required,
},
from_mail: {
required,
email,
},
from_name: {
required,
},
},
},
computed: {
secretError() {
if (!this.$v.mailConfigData.mail_ses_secret.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_ses_secret.required) {
return this.$tc('validation.required')
}
},
keyError() {
if (!this.$v.mailConfigData.mail_ses_key.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_ses_key.required) {
return this.$tc('validation.required')
}
},
fromNameError() {
if (!this.$v.mailConfigData.from_name.$error) {
return ''
}
if (!this.$v.mailConfigData.from_name.required) {
return this.$tc('validation.required')
}
},
fromEmailError() {
if (!this.$v.mailConfigData.from_mail.$error) {
return ''
}
if (!this.$v.mailConfigData.from_mail.required) {
return this.$tc('validation.required')
}
if (!this.$v.mailConfigData.from_mail.email) {
return this.$tc('validation.email_incorrect')
}
},
encryptionError() {
if (!this.$v.mailConfigData.mail_encryption.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_encryption.required) {
return this.$tc('validation.required')
}
},
portError() {
if (!this.$v.mailConfigData.mail_port.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_port.required) {
return this.$tc('validation.required')
}
if (!this.$v.mailConfigData.mail_port.numeric) {
return this.$tc('validation.numbers_only')
}
},
hostError() {
if (!this.$v.mailConfigData.mail_host.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_host.required) {
return this.$tc('validation.required')
}
},
driverError() {
if (!this.$v.mailConfigData.mail_driver.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_driver.required) {
return this.$tc('validation.required')
}
},
getInputType() {
if (this.isShowPassword) {
return 'text'
}
return 'password'
},
},
mounted() {
for (const key in this.mailConfigData) {
if (this.configData.hasOwnProperty(key)) {
this.mailConfigData[key] = this.configData[key]
}
}
},
methods: {
async saveEmailConfig() {
this.$v.mailConfigData.$touch()
if (!this.$v.mailConfigData.$invalid) {
this.$emit('submit-data', this.mailConfigData)
}
return false
},
onChangeDriver() {
this.$v.mailConfigData.mail_driver.$touch()
this.$emit('on-change-driver', this.mailConfigData.mail_driver)
},
},
}
</script>

View File

@ -0,0 +1,335 @@
<template>
<form @submit.prevent="saveEmailConfig">
<div class="grid gap-6 grid-col-1 md:grid-cols-2">
<sw-input-group
:label="$t('settings.mail.driver')"
:error="driverError"
required
>
<sw-select
v-model="mailConfigData.mail_driver"
:invalid="$v.mailConfigData.mail_driver.$error"
:options="mailDrivers"
:searchable="true"
:allow-empty="false"
:show-labels="false"
class="mt-2"
@input="onChangeDriver"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.host')"
:error="hostError"
required
>
<sw-input
:invalid="$v.mailConfigData.mail_host.$error"
v-model.trim="mailConfigData.mail_host"
type="text"
name="mail_host"
class="mt-2"
@input="$v.mailConfigData.mail_host.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.username')"
:error="usernameError"
required
>
<sw-input
:invalid="$v.mailConfigData.mail_username.$error"
v-model.trim="mailConfigData.mail_username"
type="text"
name="db_name"
class="mt-2"
@input="$v.mailConfigData.mail_username.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.password')"
:error="passwordError"
required
>
<sw-input
:invalid="$v.mailConfigData.mail_password.$error"
v-model.trim="mailConfigData.mail_password"
:type="getInputType"
name="password"
class="mt-2"
@input="$v.mailConfigData.mail_password.$touch()"
>
<template v-slot:rightIcon>
<eye-off-icon
v-if="isShowPassword"
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
<eye-icon
v-else
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
</template>
</sw-input>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.port')"
:error="portError"
required
>
<sw-input
:invalid="$v.mailConfigData.mail_port.$error"
v-model.trim="mailConfigData.mail_port"
type="text"
name="mail_port"
class="mt-2"
@input="$v.mailConfigData.mail_port.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.encryption')"
:error="encryptionError"
required
>
<sw-select
v-model.trim="mailConfigData.mail_encryption"
:invalid="$v.mailConfigData.mail_encryption.$error"
:options="encryptions"
:searchable="true"
:show-labels="false"
class="mt-2"
@input="$v.mailConfigData.mail_encryption.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.from_mail')"
:error="fromEmailError"
required
>
<sw-input
:invalid="$v.mailConfigData.from_mail.$error"
v-model.trim="mailConfigData.from_mail"
type="text"
name="from_mail"
class="mt-2"
@input="$v.mailConfigData.from_mail.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.mail.from_name')"
:error="fromNameError"
required
>
<sw-input
:invalid="$v.mailConfigData.from_name.$error"
v-model.trim="mailConfigData.from_name"
type="text"
name="from_name"
class="mt-2"
@input="$v.mailConfigData.from_name.$touch()"
/>
</sw-input-group>
</div>
<div class="flex my-10">
<sw-button
:disabled="loading"
:loading="loading"
type="submit"
variant="primary"
>
<save-icon class="mr-2" />
{{ $t('general.save') }}
</sw-button>
<slot />
</div>
</form>
</template>
<script>
const { required, email, numeric } = require('vuelidate/lib/validators')
import { EyeIcon, EyeOffIcon } from '@vue-hero-icons/outline'
export default {
props: {
configData: {
type: Object,
require: true,
default: Object,
},
loading: {
type: Boolean,
require: true,
default: false,
},
mailDrivers: {
type: Array,
require: true,
default: Array,
},
},
components: {
EyeIcon,
EyeOffIcon,
},
data() {
return {
mailConfigData: {
mail_driver: '',
mail_host: '',
mail_port: null,
mail_username: '',
mail_password: '',
mail_encryption: 'tls',
from_mail: '',
from_name: '',
},
isShowPassword: false,
encryptions: ['tls', 'ssl', 'starttls'],
}
},
validations: {
mailConfigData: {
mail_driver: {
required,
},
mail_host: {
required,
},
mail_port: {
required,
numeric,
},
mail_username: {
required,
},
mail_password: {
required,
},
mail_encryption: {
required,
},
from_mail: {
required,
email,
},
from_name: {
required,
},
},
},
computed: {
driverError() {
if (!this.$v.mailConfigData.mail_driver.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_driver.required) {
return this.$tc('validation.required')
}
},
hostError() {
if (!this.$v.mailConfigData.mail_host.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_host.required) {
return this.$tc('validation.required')
}
},
usernameError() {
if (!this.$v.mailConfigData.mail_username.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_username.required) {
return this.$tc('validation.required')
}
},
passwordError() {
if (!this.$v.mailConfigData.mail_password.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_password.required) {
return this.$tc('validation.required')
}
},
portError() {
if (!this.$v.mailConfigData.mail_port.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_port.required) {
return this.$tc('validation.required')
}
if (!this.$v.mailConfigData.mail_port.numeric) {
return this.$tc('validation.numbers_only')
}
},
encryptionError() {
if (!this.$v.mailConfigData.mail_encryption.$error) {
return ''
}
if (!this.$v.mailConfigData.mail_encryption.required) {
return this.$tc('validation.required')
}
},
fromEmailError() {
if (!this.$v.mailConfigData.from_mail.$error) {
return ''
}
if (!this.$v.mailConfigData.from_mail.required) {
return this.$tc('validation.required')
}
if (!this.$v.mailConfigData.from_mail.email) {
return this.$tc('validation.email_incorrect')
}
},
fromNameError() {
if (!this.$v.mailConfigData.from_name.$error) {
return ''
}
if (!this.$v.mailConfigData.from_name.required) {
return this.$tc('validation.required')
}
},
getInputType() {
if (this.isShowPassword) {
return 'text'
}
return 'password'
},
},
mounted() {
for (const key in this.mailConfigData) {
if (this.configData.hasOwnProperty(key)) {
this.mailConfigData[key] = this.configData[key]
}
}
},
methods: {
async saveEmailConfig() {
this.$v.mailConfigData.$touch()
if (!this.$v.mailConfigData.$invalid) {
this.$emit('submit-data', this.mailConfigData)
}
return false
},
onChangeDriver() {
this.$v.mailConfigData.mail_driver.$touch()
this.$emit('on-change-driver', this.mailConfigData.mail_driver)
},
},
}
</script>

View File

@ -1,163 +0,0 @@
<template>
<form @submit.prevent="saveEmailConfig()">
<div class="row">
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.driver') }}</label>
<span class="text-danger"> *</span>
<base-select
v-model="mailConfigData.mail_driver"
:invalid="$v.mailConfigData.mail_driver.$error"
:options="mailDrivers"
:searchable="true"
:allow-empty="false"
:show-labels="false"
@input="onChangeDriver"
/>
<div v-if="$v.mailConfigData.mail_driver.$error">
<span v-if="!$v.mailConfigData.mail_driver.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
<!-- <div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.host') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.mail_host.$error"
v-model.trim="mailConfigData.mail_host"
type="text"
name="mail_host"
@input="$v.mailConfigData.mail_host.$touch()"
/>
<div v-if="$v.mailConfigData.mail_host.$error">
<span v-if="!$v.mailConfigData.mail_host.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div> -->
</div>
<div class="row my-2">
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.from_mail') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.from_mail.$error"
v-model.trim="mailConfigData.from_mail"
type="text"
name="from_mail"
@input="$v.mailConfigData.from_mail.$touch()"
/>
<div v-if="$v.mailConfigData.from_mail.$error">
<span v-if="!$v.mailConfigData.from_mail.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
<span v-if="!$v.mailConfigData.from_mail.email" class="text-danger">
{{ $tc('validation.email_incorrect') }}
</span>
</div>
</div>
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.from_name') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.from_name.$error"
v-model.trim="mailConfigData.from_name"
type="text"
name="name"
@input="$v.mailConfigData.from_name.$touch()"
/>
<div v-if="$v.mailConfigData.from_name.$error">
<span v-if="!$v.mailConfigData.from_name.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
</div>
<div class="d-flex">
<base-button
:loading="loading"
class="pull-right mt-4"
icon="save"
color="theme"
type="submit"
>
{{ $t('general.save') }}
</base-button>
<slot/>
</div>
</form>
</template>
<script>
import MultiSelect from 'vue-multiselect'
import { validationMixin } from 'vuelidate'
const { required, email } = require('vuelidate/lib/validators')
export default {
components: {
MultiSelect
},
mixins: [validationMixin],
props: {
configData: {
type: Object,
require: true,
default: Object
},
loading: {
type: Boolean,
require: true,
default: false
},
mailDrivers: {
type: Array,
require: true,
default: Array
}
},
data () {
return {
mailConfigData: {
mail_driver: '',
mail_host: '',
from_mail: '',
from_name: ''
}
}
},
validations: {
mailConfigData: {
mail_driver: {
required
},
from_mail: {
required,
email
},
from_name: {
required
}
}
},
mounted () {
for (const key in this.mailConfigData) {
if (this.configData.hasOwnProperty(key)) {
this.mailConfigData[key] = this.configData[key]
}
}
},
methods: {
async saveEmailConfig () {
this.$v.mailConfigData.$touch()
if (!this.$v.mailConfigData.$invalid) {
this.$emit('submit-data', this.mailConfigData)
}
return false
},
onChangeDriver () {
this.$v.mailConfigData.mail_driver.$touch()
this.$emit('on-change-driver', this.mailConfigData.mail_driver)
}
}
}
</script>

View File

@ -1,212 +0,0 @@
<template>
<form @submit.prevent="saveEmailConfig()">
<div class="row">
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.driver') }}</label>
<span class="text-danger"> *</span>
<base-select
v-model="mailConfigData.mail_driver"
:invalid="$v.mailConfigData.mail_driver.$error"
:options="mailDrivers"
:allow-empty="false"
:searchable="true"
:show-labels="false"
@input="onChangeDriver"
/>
<div v-if="$v.mailConfigData.mail_driver.$error">
<span v-if="!$v.mailConfigData.mail_driver.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.mailgun_domain') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.mail_mailgun_domain.$error"
v-model.trim="mailConfigData.mail_mailgun_domain"
type="text"
name="mailgun_domain"
@input="$v.mailConfigData.mail_mailgun_domain.$touch()"
/>
<div v-if="$v.mailConfigData.mail_mailgun_domain.$error">
<span v-if="!$v.mailConfigData.mail_mailgun_domain.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
</div>
<div class="row my-2">
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.mailgun_secret') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.mail_mailgun_secret.$error"
v-model.trim="mailConfigData.mail_mailgun_secret"
type="password"
name="mailgun_secret"
show-password
@input="$v.mailConfigData.mail_mailgun_secret.$touch()"
/>
<div v-if="$v.mailConfigData.mail_mailgun_secret.$error">
<span v-if="!$v.mailConfigData.mail_mailgun_secret.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.mailgun_endpoint') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.mail_mailgun_endpoint.$error"
v-model.trim="mailConfigData.mail_mailgun_endpoint"
type="text"
name="mailgun_endpoint"
@input="$v.mailConfigData.mail_mailgun_endpoint.$touch()"
/>
<div v-if="$v.mailConfigData.mail_mailgun_endpoint.$error">
<span v-if="!$v.mailConfigData.mail_mailgun_endpoint.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
<span v-if="!$v.mailConfigData.mail_mailgun_endpoint.numeric" class="text-danger">
{{ $tc('validation.numbers_only') }}
</span>
</div>
</div>
</div>
<div class="row my-2">
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.from_mail') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.from_mail.$error"
v-model.trim="mailConfigData.from_mail"
type="text"
name="from_mail"
@input="$v.mailConfigData.from_mail.$touch()"
/>
<div v-if="$v.mailConfigData.from_mail.$error">
<span v-if="!$v.mailConfigData.from_mail.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
<span v-if="!$v.mailConfigData.from_mail.email" class="text-danger">
{{ $tc('validation.email_incorrect') }}
</span>
</div>
</div>
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.from_name') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.from_name.$error"
v-model.trim="mailConfigData.from_name"
type="text"
name="from_name"
@input="$v.mailConfigData.from_name.$touch()"
/>
<div v-if="$v.mailConfigData.from_name.$error">
<span v-if="!$v.mailConfigData.from_name.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
</div>
<div class="d-flex">
<base-button
:loading="loading"
class="pull-right mt-4"
icon="save"
color="theme"
type="submit"
>
{{ $t('general.save') }}
</base-button>
<slot/>
</div>
</form>
</template>
<script>
import MultiSelect from 'vue-multiselect'
import { validationMixin } from 'vuelidate'
const { required, email, numeric } = require('vuelidate/lib/validators')
export default {
components: {
MultiSelect
},
mixins: [validationMixin],
props: {
configData: {
type: Object,
require: true,
default: Object
},
loading: {
type: Boolean,
require: true,
default: false
},
mailDrivers: {
type: Array,
require: true,
default: Array
}
},
data () {
return {
mailConfigData: {
mail_driver: '',
mail_mailgun_domain: '',
mail_mailgun_secret: '',
mail_mailgun_endpoint: '',
from_mail: '',
from_name: ''
}
}
},
validations: {
mailConfigData: {
mail_driver: {
required
},
mail_mailgun_domain: {
required
},
mail_mailgun_endpoint: {
required
},
mail_mailgun_secret: {
required
},
from_mail: {
required,
email
},
from_name: {
required
}
}
},
mounted () {
for (const key in this.mailConfigData) {
if (this.configData.hasOwnProperty(key)) {
this.mailConfigData[key] = this.configData[key]
}
}
},
methods: {
async saveEmailConfig () {
this.$v.mailConfigData.$touch()
if (!this.$v.mailConfigData.$invalid) {
this.$emit('submit-data', this.mailConfigData)
}
return false
},
onChangeDriver () {
this.$v.mailConfigData.mail_driver.$touch()
this.$emit('on-change-driver', this.mailConfigData.mail_driver)
}
}
}
</script>

View File

@ -1,257 +0,0 @@
<template>
<form @submit.prevent="saveEmailConfig()">
<div class="row">
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.driver') }}</label>
<span class="text-danger"> *</span>
<base-select
v-model="mailConfigData.mail_driver"
:invalid="$v.mailConfigData.mail_driver.$error"
:options="mailDrivers"
:allow-empty="false"
:searchable="true"
:show-labels="false"
@input="onChangeDriver"
/>
<div v-if="$v.mailConfigData.mail_driver.$error">
<span v-if="!$v.mailConfigData.mail_driver.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.host') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.mail_host.$error"
v-model.trim="mailConfigData.mail_host"
type="text"
name="mail_host"
@input="$v.mailConfigData.mail_host.$touch()"
/>
<div v-if="$v.mailConfigData.mail_host.$error">
<span v-if="!$v.mailConfigData.mail_host.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
</div>
<div class="row my-2">
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.port') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.mail_port.$error"
v-model.trim="mailConfigData.mail_port"
type="text"
name="mail_port"
@input="$v.mailConfigData.mail_port.$touch()"
/>
<div v-if="$v.mailConfigData.mail_port.$error">
<span v-if="!$v.mailConfigData.mail_port.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
<span v-if="!$v.mailConfigData.mail_port.numeric" class="text-danger">
{{ $tc('validation.numbers_only') }}
</span>
</div>
</div>
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.encryption') }}</label>
<span class="text-danger"> *</span>
<base-select
v-model.trim="mailConfigData.mail_encryption"
:invalid="$v.mailConfigData.mail_encryption.$error"
:options="encryptions"
:searchable="true"
:show-labels="false"
@input="$v.mailConfigData.mail_encryption.$touch()"
/>
<div v-if="$v.mailConfigData.mail_encryption.$error">
<span v-if="!$v.mailConfigData.mail_encryption.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
</div>
<div class="row my-2">
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.from_mail') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.from_mail.$error"
v-model.trim="mailConfigData.from_mail"
type="text"
name="from_mail"
@input="$v.mailConfigData.from_mail.$touch()"
/>
<div v-if="$v.mailConfigData.from_mail.$error">
<span v-if="!$v.mailConfigData.from_mail.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
<span v-if="!$v.mailConfigData.from_mail.email" class="text-danger">
{{ $tc('validation.email_incorrect') }}
</span>
</div>
</div>
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.from_name') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.from_name.$error"
v-model.trim="mailConfigData.from_name"
type="text"
name="name"
@input="$v.mailConfigData.from_name.$touch()"
/>
<div v-if="$v.mailConfigData.from_name.$error">
<span v-if="!$v.mailConfigData.from_name.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
</div>
<div class="row my-2">
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.ses_key') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.mail_ses_key.$error"
v-model.trim="mailConfigData.mail_ses_key"
type="text"
name="mail_ses_key"
@input="$v.mailConfigData.mail_ses_key.$touch()"
/>
<div v-if="$v.mailConfigData.mail_ses_key.$error">
<span v-if="!$v.mailConfigData.mail_ses_key.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.ses_secret') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.mail_ses_secret.$error"
v-model.trim="mailConfigData.mail_ses_secret"
type="password"
name="mail_ses_secret"
show-password
@input="$v.mailConfigData.mail_ses_secret.$touch()"
/>
<div v-if="$v.mailConfigData.mail_ses_secret.$error">
<span v-if="!$v.mailConfigData.mail_ses_secret.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
</div>
<div class="d-flex">
<base-button
:loading="loading"
class="pull-right mt-4"
icon="save"
color="theme"
type="submit"
>
{{ $t('general.save') }}
</base-button>
<slot/>
</div>
</form>
</template>
<script>
import MultiSelect from 'vue-multiselect'
import { validationMixin } from 'vuelidate'
const { required, email, numeric } = require('vuelidate/lib/validators')
export default {
components: {
MultiSelect
},
mixins: [validationMixin],
props: {
configData: {
type: Object,
require: true,
default: Object
},
loading: {
type: Boolean,
require: true,
default: false
},
mailDrivers: {
type: Array,
require: true,
default: Array
}
},
data () {
return {
mailConfigData: {
mail_driver: '',
mail_host: '',
mail_port: null,
mail_ses_key: '',
mail_ses_secret: '',
mail_encryption: 'tls',
from_mail: '',
from_name: ''
},
encryptions: ['tls', 'ssl', 'starttls']
}
},
validations: {
mailConfigData: {
mail_driver: {
required
},
mail_host: {
required
},
mail_port: {
required,
numeric
},
mail_ses_key: {
required
},
mail_ses_secret: {
required
},
mail_encryption: {
required
},
from_mail: {
required,
email
},
from_name: {
required
}
}
},
mounted () {
for (const key in this.mailConfigData) {
if (this.configData.hasOwnProperty(key)) {
this.mailConfigData[key] = this.configData[key]
}
}
},
methods: {
async saveEmailConfig () {
this.$v.mailConfigData.$touch()
if (!this.$v.mailConfigData.$invalid) {
this.$emit('submit-data', this.mailConfigData)
}
return false
},
onChangeDriver () {
this.$v.mailConfigData.mail_driver.$touch()
this.$emit('on-change-driver', this.mailConfigData.mail_driver)
}
}
}
</script>

View File

@ -1,257 +0,0 @@
<template>
<form @submit.prevent="saveEmailConfig()">
<div class="row">
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.driver') }}</label>
<span class="text-danger"> *</span>
<base-select
v-model="mailConfigData.mail_driver"
:invalid="$v.mailConfigData.mail_driver.$error"
:options="mailDrivers"
:searchable="true"
:allow-empty="false"
:show-labels="false"
@input="onChangeDriver"
/>
<div v-if="$v.mailConfigData.mail_driver.$error">
<span v-if="!$v.mailConfigData.mail_driver.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.host') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.mail_host.$error"
v-model.trim="mailConfigData.mail_host"
type="text"
name="mail_host"
@input="$v.mailConfigData.mail_host.$touch()"
/>
<div v-if="$v.mailConfigData.mail_host.$error">
<span v-if="!$v.mailConfigData.mail_host.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
</div>
<div class="row my-2">
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.username') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.mail_username.$error"
v-model.trim="mailConfigData.mail_username"
type="text"
name="db_name"
@input="$v.mailConfigData.mail_username.$touch()"
/>
<div v-if="$v.mailConfigData.mail_username.$error">
<span v-if="!$v.mailConfigData.mail_username.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.password') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.mail_password.$error"
v-model.trim="mailConfigData.mail_password"
type="password"
name="name"
show-password
@input="$v.mailConfigData.mail_password.$touch()"
/>
<div v-if="$v.mailConfigData.mail_password.$error">
<span v-if="!$v.mailConfigData.mail_password.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
</div>
<div class="row my-2">
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.port') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.mail_port.$error"
v-model.trim="mailConfigData.mail_port"
type="text"
name="mail_port"
@input="$v.mailConfigData.mail_port.$touch()"
/>
<div v-if="$v.mailConfigData.mail_port.$error">
<span v-if="!$v.mailConfigData.mail_port.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
<span v-if="!$v.mailConfigData.mail_port.numeric" class="text-danger">
{{ $tc('validation.numbers_only') }}
</span>
</div>
</div>
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.encryption') }}</label>
<span class="text-danger"> *</span>
<base-select
v-model.trim="mailConfigData.mail_encryption"
:invalid="$v.mailConfigData.mail_encryption.$error"
:options="encryptions"
:searchable="true"
:show-labels="false"
@input="$v.mailConfigData.mail_encryption.$touch()"
/>
<div v-if="$v.mailConfigData.mail_encryption.$error">
<span v-if="!$v.mailConfigData.mail_encryption.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
</div>
<div class="row my-2">
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.from_mail') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.from_mail.$error"
v-model.trim="mailConfigData.from_mail"
type="text"
name="from_mail"
@input="$v.mailConfigData.from_mail.$touch()"
/>
<div v-if="$v.mailConfigData.from_mail.$error">
<span v-if="!$v.mailConfigData.from_mail.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
<span v-if="!$v.mailConfigData.from_mail.email" class="text-danger">
{{ $tc('validation.email_incorrect') }}
</span>
</div>
</div>
<div class="col-md-6 my-2">
<label class="form-label">{{ $t('settings.mail.from_name') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.mailConfigData.from_name.$error"
v-model.trim="mailConfigData.from_name"
type="text"
name="from_name"
@input="$v.mailConfigData.from_name.$touch()"
/>
<div v-if="$v.mailConfigData.from_name.$error">
<span v-if="!$v.mailConfigData.from_name.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
</div>
<div class="d-flex">
<base-button
:loading="loading"
class="pull-right mt-4"
icon="save"
color="theme"
type="submit"
>
{{ $t('general.save') }}
</base-button>
<slot/>
</div>
</form>
</template>
<script>
import MultiSelect from 'vue-multiselect'
import { validationMixin } from 'vuelidate'
const { required, email, numeric } = require('vuelidate/lib/validators')
export default {
components: {
MultiSelect
},
mixins: [validationMixin],
props: {
configData: {
type: Object,
require: true,
default: Object
},
loading: {
type: Boolean,
require: true,
default: false
},
mailDrivers: {
type: Array,
require: true,
default: Array
}
},
data () {
return {
mailConfigData: {
mail_driver: '',
mail_host: '',
mail_port: null,
mail_username: '',
mail_password: '',
mail_encryption: 'tls',
from_mail: '',
from_name: ''
},
encryptions: ['tls', 'ssl', 'starttls']
}
},
validations: {
mailConfigData: {
mail_driver: {
required
},
mail_host: {
required
},
mail_port: {
required,
numeric
},
mail_username: {
required
},
mail_password: {
required
},
mail_encryption: {
required
},
from_mail: {
required,
email
},
from_name: {
required
}
}
},
mounted () {
for (const key in this.mailConfigData) {
if (this.configData.hasOwnProperty(key)) {
this.mailConfigData[key] = this.configData[key]
}
}
},
methods: {
async saveEmailConfig () {
this.$v.mailConfigData.$touch()
if (!this.$v.mailConfigData.$invalid) {
this.$emit('submit-data', this.mailConfigData)
}
return false
},
onChangeDriver () {
this.$v.mailConfigData.mail_driver.$touch()
this.$emit('on-change-driver', this.mailConfigData.mail_driver)
}
}
}
</script>