mirror of
https://github.com/crater-invoice/crater.git
synced 2026-02-10 12:52:41 -05:00
build version 400
This commit is contained in:
@@ -1,11 +1,7 @@
|
||||
<template>
|
||||
<form
|
||||
id="loginForm"
|
||||
@submit.prevent="validateBeforeSubmit"
|
||||
>
|
||||
|
||||
<div :class="{'form-group' : true }">
|
||||
<base-input
|
||||
<form id="loginForm" @submit.prevent="validateBeforeSubmit">
|
||||
<div class="mb-4">
|
||||
<sw-input
|
||||
:invalid="$v.formData.email.$error"
|
||||
v-model.lazy="formData.email"
|
||||
:disabled="isSent"
|
||||
@@ -15,22 +11,27 @@
|
||||
@blur="$v.formData.email.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.email.$error">
|
||||
<span v-if="!$v.formData.email.required" class="help-block text-danger">
|
||||
<span v-if="!$v.formData.email.required" class="text-sm text-danger">
|
||||
{{ $t('validation.required') }}
|
||||
</span>
|
||||
<span v-if="!$v.formData.email.email" class="help-block text-danger">
|
||||
<span v-if="!$v.formData.email.email" class="text-sm text-danger">
|
||||
{{ $t('validation.email_incorrect') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<base-button v-if="!isSent" :loading="isLoading" :disabled="isLoading" type="submit" color="theme">
|
||||
<sw-button
|
||||
v-if="!isSent"
|
||||
:disabled="isLoading"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
{{ $t('validation.send_reset_link') }}
|
||||
</base-button>
|
||||
<base-button v-else :loading="isLoading" :disabled="isLoading" color="theme" type="submit">
|
||||
</sw-button>
|
||||
<sw-button v-else :disabled="isLoading" variant="primary" type="submit">
|
||||
{{ $t('validation.not_yet') }}
|
||||
</base-button>
|
||||
</sw-button>
|
||||
|
||||
<div class="other-actions mb-4">
|
||||
<div class="mt-4 mb-4 text-sm">
|
||||
<router-link to="/login">
|
||||
{{ $t('general.back_to_login') }}
|
||||
</router-link>
|
||||
@@ -39,43 +40,45 @@
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import { async } from 'q'
|
||||
import { mapActions } from 'vuex'
|
||||
const { required, email } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
email: ''
|
||||
email: '',
|
||||
},
|
||||
isSent: false,
|
||||
isLoading: false,
|
||||
isRegisteredUser: false
|
||||
isRegisteredUser: false,
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
formData: {
|
||||
email: {
|
||||
email,
|
||||
required
|
||||
}
|
||||
}
|
||||
required,
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
async validateBeforeSubmit (e) {
|
||||
...mapActions('auth', ['checkMail']),
|
||||
async validateBeforeSubmit(e) {
|
||||
this.$v.formData.$touch()
|
||||
|
||||
if (await this.checkMail() === false) {
|
||||
let { data } = await this.checkMail()
|
||||
if (data === false) {
|
||||
toastr['error'](this.$t('validation.email_does_not_exist'))
|
||||
return
|
||||
}
|
||||
if (!this.$v.formData.$invalid) {
|
||||
try {
|
||||
this.isLoading = true
|
||||
let res = await axios.post('/api/auth/password/email', this.formData)
|
||||
let res = await axios.post(
|
||||
'/api/v1/auth/password/email',
|
||||
this.formData
|
||||
)
|
||||
|
||||
if (res.data) {
|
||||
toastr['success']('Mail sent successfuly!', 'Success')
|
||||
@@ -90,10 +93,13 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
async checkMail () {
|
||||
let response = await window.axios.post('/api/is-registered', this.formData)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
// async checkMail() {
|
||||
// let response = await window.axios.post(
|
||||
// '/api/v1/is-registered',
|
||||
// this.formData
|
||||
// )
|
||||
// return response.data
|
||||
// },
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,126 +1,168 @@
|
||||
<template>
|
||||
<form
|
||||
id="loginForm"
|
||||
@submit.prevent="validateBeforeSubmit"
|
||||
>
|
||||
<div :class="{'form-group' : true }">
|
||||
<p class="input-label">{{ $t('login.email') }} <span class="text-danger"> * </span></p>
|
||||
<base-input
|
||||
<form id="loginForm" @submit.prevent="validateBeforeSubmit">
|
||||
<sw-input-group
|
||||
:label="$t('login.email')"
|
||||
:error="emailError"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<sw-input
|
||||
:invalid="$v.loginData.email.$error"
|
||||
v-model="loginData.email"
|
||||
:placeholder="$t(login.login_placeholder)"
|
||||
v-model="loginData.email"
|
||||
focus
|
||||
type="email"
|
||||
name="email"
|
||||
@input="$v.loginData.email.$touch()"
|
||||
/>
|
||||
<div v-if="$v.loginData.email.$error">
|
||||
<span v-if="!$v.loginData.email.required" class="text-danger">
|
||||
{{ $tc('validation.required') }}
|
||||
</span>
|
||||
<span v-if="!$v.loginData.email.email" class="text-danger">
|
||||
{{ $tc('validation.email_incorrect') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p class="input-label">{{ $t('login.password') }} <span class="text-danger"> * </span></p>
|
||||
<base-input
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('login.password')"
|
||||
:error="passwordError"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<sw-input
|
||||
v-model="loginData.password"
|
||||
:invalid="$v.loginData.password.$error"
|
||||
type="password"
|
||||
:type="getInputType"
|
||||
name="password"
|
||||
show-password
|
||||
@input="$v.loginData.password.$touch()"
|
||||
/>
|
||||
<div v-if="$v.loginData.password.$error">
|
||||
<span v-if="!$v.loginData.password.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
<span v-if="!$v.loginData.password.minLength" class="text-danger"> {{ $tc('validation.password_min_length', $v.loginData.password.$params.minLength.min, {count: $v.loginData.password.$params.minLength.min}) }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="other-actions row">
|
||||
<div class="col-sm-12 text-sm-left mb-4">
|
||||
<router-link to="forgot-password" class="forgot-link">
|
||||
>
|
||||
<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 class="mt-5 mb-8">
|
||||
<div class="mb-4">
|
||||
<router-link
|
||||
to="forgot-password"
|
||||
class="text-sm text-primary-400 hover:text-gray-700"
|
||||
>
|
||||
{{ $t('login.forgot_password') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<base-button :loading="isLoading" type="submit" color="theme">{{ $t('login.login') }}</base-button>
|
||||
|
||||
<!-- <div class="social-links">
|
||||
|
||||
<span class="link-text">{{ $t('login.or_signIn_with') }}</span>
|
||||
|
||||
<div class="social-logo">
|
||||
<icon-facebook class="icon"/>
|
||||
<icon-twitter class="icon"/>
|
||||
<icon-google class="icon"/>
|
||||
</div>
|
||||
|
||||
</div> -->
|
||||
|
||||
<sw-button
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
{{ $t('login.login') }}
|
||||
</sw-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
import { EyeIcon, EyeOffIcon } from '@vue-hero-icons/outline'
|
||||
import IconFacebook from '../../components/icon/facebook'
|
||||
import IconTwitter from '../../components/icon/twitter'
|
||||
import IconGoogle from '../../components/icon/google'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
const { required, email, minLength } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
|
||||
components: {
|
||||
IconFacebook,
|
||||
IconTwitter,
|
||||
IconGoogle
|
||||
IconGoogle,
|
||||
EyeIcon,
|
||||
EyeOffIcon,
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
loginData: {
|
||||
email: '',
|
||||
password: '',
|
||||
remember: ''
|
||||
remember: '',
|
||||
},
|
||||
submitted: false,
|
||||
isLoading: false
|
||||
isLoading: false,
|
||||
isShowPassword: false,
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
loginData: {
|
||||
email: {
|
||||
required,
|
||||
email
|
||||
email,
|
||||
},
|
||||
password: {
|
||||
required,
|
||||
minLength: minLength(8)
|
||||
minLength: minLength(8),
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
emailError() {
|
||||
if (!this.$v.loginData.email.$error) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
if (!this.$v.loginData.email.required) {
|
||||
return this.$tc('validation.required')
|
||||
}
|
||||
if (!this.$v.loginData.email.email) {
|
||||
return this.$tc('validation.email_incorrect')
|
||||
}
|
||||
},
|
||||
|
||||
passwordError() {
|
||||
if (!this.$v.loginData.password.$error) {
|
||||
return ''
|
||||
}
|
||||
if (!this.$v.loginData.password.required) {
|
||||
return this.$tc('validation.required')
|
||||
}
|
||||
if (!this.$v.loginData.password.minLength) {
|
||||
return this.$tc(
|
||||
'validation.password_min_length',
|
||||
this.$v.loginData.password.$params.minLength.min,
|
||||
{ count: this.$v.loginData.password.$params.minLength.min }
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
getInputType() {
|
||||
if (this.isShowPassword) {
|
||||
return 'text'
|
||||
}
|
||||
return 'password'
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('auth', [
|
||||
'login'
|
||||
]),
|
||||
async validateBeforeSubmit () {
|
||||
...mapActions('auth', ['login']),
|
||||
async validateBeforeSubmit() {
|
||||
axios.defaults.withCredentials = true
|
||||
|
||||
this.$v.loginData.$touch()
|
||||
if (this.$v.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
this.isLoading = true
|
||||
this.login(this.loginData).then((res) => {
|
||||
|
||||
try {
|
||||
await this.login(this.loginData)
|
||||
this.$router.push('/admin/dashboard')
|
||||
this.isLoading = false
|
||||
}).catch(() => {
|
||||
} catch (error) {
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<form
|
||||
id="registerForm"
|
||||
action=""
|
||||
method="post"
|
||||
>
|
||||
<form id="registerForm" action="" method="post">
|
||||
<!-- {{ csrf_field() }} -->
|
||||
<div class="form-group">
|
||||
<input
|
||||
@@ -11,7 +7,7 @@
|
||||
type="email"
|
||||
class="form-control form-control-danger"
|
||||
name="email"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
@@ -20,7 +16,7 @@
|
||||
class="form-control form-control-danger"
|
||||
placeholder="Enter Password"
|
||||
name="password"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
@@ -28,30 +24,32 @@
|
||||
class="form-control form-control-danger"
|
||||
placeholder="Retype Password"
|
||||
name="password_confirmation"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
<base-button class="btn btn-login btn-full">{{ $t('login.register') }}</base-button>
|
||||
<sw-button class="btn btn-login btn-full">{{
|
||||
$t('login.register')
|
||||
}}</sw-button>
|
||||
</form>
|
||||
</template>
|
||||
<script type="text/babel">
|
||||
export default {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: ''
|
||||
password_confirmation: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateBeforeSubmit (e) {
|
||||
validateBeforeSubmit(e) {
|
||||
this.$validator.validateAll().then((result) => {
|
||||
if (result) {
|
||||
// eslint-disable-next-line
|
||||
alert('Form Submitted!')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<template>
|
||||
<form
|
||||
id="loginForm"
|
||||
@submit.prevent="validateBeforeSubmit"
|
||||
>
|
||||
<form id="loginForm" @submit.prevent="validateBeforeSubmit">
|
||||
<div class="form-group">
|
||||
<base-input
|
||||
<sw-input
|
||||
v-model.trim="formData.email"
|
||||
:invalid="$v.formData.email.$error"
|
||||
:placeholder="$t('login.enter_email')"
|
||||
@@ -22,7 +19,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<base-input
|
||||
<sw-input
|
||||
id="password"
|
||||
v-model.trim="formData.password"
|
||||
:invalid="$v.formData.password.$error"
|
||||
@@ -32,16 +29,28 @@
|
||||
@input="$v.formData.password.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.password.$error">
|
||||
<span v-if="!$v.formData.password.required" class="help-block text-danger">
|
||||
<span
|
||||
v-if="!$v.formData.password.required"
|
||||
class="help-block text-danger"
|
||||
>
|
||||
{{ $t('validation.required') }}
|
||||
</span>
|
||||
<span v-if="!$v.formData.password.minLength" class="help-block text-danger">
|
||||
{{ $tc('validation.password_length', $v.formData.password.minLength.min, { count: $v.formData.password.$params.minLength.min }) }}
|
||||
<span
|
||||
v-if="!$v.formData.password.minLength"
|
||||
class="help-block text-danger"
|
||||
>
|
||||
{{
|
||||
$tc(
|
||||
'validation.password_length',
|
||||
$v.formData.password.minLength.min,
|
||||
{ count: $v.formData.password.$params.minLength.min }
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<base-input
|
||||
<sw-input
|
||||
v-model.trim="formData.password_confirmation"
|
||||
:invalid="$v.formData.password_confirmation.$error"
|
||||
:placeholder="$t('login.retype_password')"
|
||||
@@ -50,50 +59,56 @@
|
||||
@input="$v.formData.password_confirmation.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.password_confirmation.$error">
|
||||
<span v-if="!$v.formData.password_confirmation.sameAsPassword" class="help-block text-danger">
|
||||
<span
|
||||
v-if="!$v.formData.password_confirmation.sameAsPassword"
|
||||
class="help-block text-danger"
|
||||
>
|
||||
{{ $t('validation.password_incorrect') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<base-button :loading="isLoading" type="submit" color="theme">
|
||||
<sw-button type="submit" variant="primary">
|
||||
{{ $t('login.reset_password') }}
|
||||
</base-button>
|
||||
</sw-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
import { validationMixin } from 'vuelidate'
|
||||
const { required, email, sameAs, minLength } = require('vuelidate/lib/validators')
|
||||
const {
|
||||
required,
|
||||
email,
|
||||
sameAs,
|
||||
minLength,
|
||||
} = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: ''
|
||||
password_confirmation: '',
|
||||
},
|
||||
isLoading: false
|
||||
isLoading: false,
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
formData: {
|
||||
email: {
|
||||
required,
|
||||
email
|
||||
email,
|
||||
},
|
||||
password: {
|
||||
required,
|
||||
minLength: minLength(8)
|
||||
minLength: minLength(8),
|
||||
},
|
||||
password_confirmation: {
|
||||
sameAsPassword: sameAs('password')
|
||||
}
|
||||
}
|
||||
sameAsPassword: sameAs('password'),
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async validateBeforeSubmit (e) {
|
||||
async validateBeforeSubmit(e) {
|
||||
this.$v.formData.$touch()
|
||||
|
||||
if (!this.$v.formData.$invalid) {
|
||||
@@ -102,23 +117,29 @@ export default {
|
||||
email: this.formData.email,
|
||||
password: this.formData.password,
|
||||
password_confirmation: this.formData.password_confirmation,
|
||||
token: this.$route.params.token
|
||||
token: this.$route.params.token,
|
||||
}
|
||||
this.isLoading = true
|
||||
let res = await axios.post('/api/auth/reset/password', data)
|
||||
let res = await axios.post('/api/v1/auth/reset/password', data)
|
||||
this.isLoading = false
|
||||
if (res.data) {
|
||||
toastr['success'](this.$t('login.password_reset_successfully'), 'Success')
|
||||
toastr['success'](
|
||||
this.$t('login.password_reset_successfully'),
|
||||
'Success'
|
||||
)
|
||||
this.$router.push('/login')
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 403) {
|
||||
toastr['error'](err.response.data, this.$t('validation.email_incorrect'))
|
||||
toastr['error'](
|
||||
err.response.data,
|
||||
this.$t('validation.email_incorrect')
|
||||
)
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
<template>
|
||||
<div class="imgbox">
|
||||
<vue-dropzone
|
||||
id="dropzone"
|
||||
ref="myVueDropzone"
|
||||
:include-styling="true"
|
||||
:options="dropzoneOptions"
|
||||
@vdropzone-sending="sendingEvent"
|
||||
@vdropzone-success="successEvent"
|
||||
@vdropzone-max-files-exceeded="maximum"
|
||||
@vdropzone-file-added="getCustomeFile"
|
||||
@vdropzone-removed-file="removeFile"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import vue2Dropzone from 'vue2-dropzone'
|
||||
import 'vue2-dropzone/dist/vue2Dropzone.min.css'
|
||||
export default {
|
||||
components: {
|
||||
vueDropzone: vue2Dropzone
|
||||
},
|
||||
props: {
|
||||
additionaldata: {
|
||||
type: Array,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default () {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
router: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
paramname: {
|
||||
type: String,
|
||||
default () {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
acceptedfiles: {
|
||||
type: String,
|
||||
default () {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
dictdefaultmessage: {
|
||||
type: String,
|
||||
default () {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
autoprocessqueue: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
method: {
|
||||
type: String,
|
||||
default: 'POST'
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
dropzoneOptions: {
|
||||
autoProcessQueue: this.autoprocessqueue,
|
||||
url: this.url,
|
||||
thumbnailWidth: 110,
|
||||
maxFiles: 1,
|
||||
paramName: this.paramname,
|
||||
acceptedFiles: this.acceptedfiles,
|
||||
uploadMultiple: false,
|
||||
dictDefaultMessage: '<font-awesome-icon icon="trash"/> ' + this.dictdefaultmessage,
|
||||
dictInvalidFileType: 'This file type is not supported.',
|
||||
dictFileTooBig: 'File size too Big',
|
||||
addRemoveLinks: true,
|
||||
method: this.method,
|
||||
headers: { 'Authorization': `Bearer ${window.Ls.get('auth.token')}`, 'Company': `${window.Ls.get('selectedCompany')}` }
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
url (newURL) {
|
||||
this.$refs.myVueDropzone.options.url = newURL
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.hub.$on('sendFile', this.customeSend)
|
||||
},
|
||||
methods: {
|
||||
sendingEvent (file, xhr, formData) {
|
||||
var i
|
||||
for (i = 0; i < this.additionaldata.length; i++) {
|
||||
for (var key in this.additionaldata[i]) {
|
||||
formData.append(key, this.additionaldata[i][key])
|
||||
}
|
||||
}
|
||||
},
|
||||
successEvent (file, response) {
|
||||
// window.toastr['success'](response.success)
|
||||
},
|
||||
maximum (file) {
|
||||
this.$refs.myVueDropzone.removeFile(file)
|
||||
},
|
||||
getCustomeFile (file) {
|
||||
this.$emit('takefile', true)
|
||||
},
|
||||
removeFile (file, error, xhr) {
|
||||
this.$emit('takefile', false)
|
||||
},
|
||||
customeSend () {
|
||||
this.$refs.myVueDropzone.processQueue()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,71 +0,0 @@
|
||||
<template>
|
||||
<div class="form-group image-radio">
|
||||
<div
|
||||
v-for="(pdfStyleList, index) in pdfStyleLists"
|
||||
:key="index"
|
||||
class="radio"
|
||||
>
|
||||
<label :for="pdfStyleList.val">
|
||||
<input
|
||||
v-model="checkedID"
|
||||
:value="pdfStyleList.val"
|
||||
:id="pdfStyleList.val"
|
||||
:checked="pdfStyleList.val == checkedID"
|
||||
type="radio"
|
||||
name="pdfSet"
|
||||
class="hidden"
|
||||
>
|
||||
<img
|
||||
:src="srcMaker(pdfStyleList.src)"
|
||||
alt="No Image"
|
||||
class="special-img"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default{
|
||||
props: {
|
||||
currentPDF: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
pdfStyleLists: [
|
||||
{src: 'assets/img/PDF/Invoice1.png', val: '1'},
|
||||
{src: 'assets/img/PDF/Invoice2.png', val: '2'},
|
||||
{src: 'assets/img/PDF/Invoice3.png', val: '3'},
|
||||
{src: 'assets/img/PDF/Invoice4.png', val: '4'}
|
||||
],
|
||||
checkedID: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
checkedID (newID) {
|
||||
if (newID !== null) {
|
||||
this.$emit('selectedPDF', newID)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
setTimeout(() => {
|
||||
if (this.currentPDF === '') {
|
||||
this.checkedID = null
|
||||
} else {
|
||||
this.checkedID = this.currentPDF
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
methods: {
|
||||
srcMaker (file) {
|
||||
var url = '/'
|
||||
var full = url + '' + file
|
||||
return full
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,129 +0,0 @@
|
||||
<template>
|
||||
<div class="setting-list-box">
|
||||
<div id="myApp list-box-container">
|
||||
<!-- <v-select
|
||||
:value.sync="selected"
|
||||
:options="list"
|
||||
:on-change ="setValue"
|
||||
/> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
// import vSelect from 'vue-select'
|
||||
export default {
|
||||
// components: {vSelect},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
Options: {
|
||||
type: [Array, Object],
|
||||
required: false,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
getData: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
currentData: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
selected: null,
|
||||
list: []
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
window.setTimeout(() => {
|
||||
this.setList()
|
||||
if (this.currentData !== null || this.currentData !== '') {
|
||||
this.defaultValue(this.currentData)
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
methods: {
|
||||
setList () {
|
||||
if (this.type === 'currencies') {
|
||||
for (let i = 0; i < this.Options.length; i++) {
|
||||
this.list.push(this.Options[i].name + ' - ' + this.Options[i].code)
|
||||
}
|
||||
} else if (this.type === 'time_zones' || this.type === 'languages' || this.type === 'date_formats') {
|
||||
for (let key in this.Options) {
|
||||
this.list.push(this.Options[key])
|
||||
}
|
||||
}
|
||||
},
|
||||
setValue (val) {
|
||||
if (this.type === 'currencies') {
|
||||
for (let i = 0; i < this.Options.length; i++) {
|
||||
if (val === this.Options[i].name + ' - ' + this.Options[i].code) {
|
||||
this.getData.currency = this.Options[i].id
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (this.type === 'time_zones') {
|
||||
for (let key in this.Options) {
|
||||
if (val === this.Options[key]) {
|
||||
this.getData.time_zone = key
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (this.type === 'languages') {
|
||||
for (let key in this.Options) {
|
||||
if (val === this.Options[key]) {
|
||||
this.getData.language = key
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (this.type === 'date_formats') {
|
||||
for (let key in this.Options) {
|
||||
if (val === this.Options[key]) {
|
||||
this.getData.date_format = key
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
defaultValue (val) {
|
||||
if (this.type === 'currencies') {
|
||||
for (let i = 0; i < this.Options.length; i++) {
|
||||
if (Number(val) === this.Options[i].id) {
|
||||
this.selected = this.Options[i].name + ' - ' + this.Options[i].code
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (this.type === 'time_zones') {
|
||||
for (let key in this.Options) {
|
||||
if (val === key) {
|
||||
this.selected = this.Options[key]
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (this.type === 'languages') {
|
||||
for (let key in this.Options) {
|
||||
if (val === key) {
|
||||
this.selected = this.Options[key]
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (this.type === 'date_formats') {
|
||||
for (let key in this.Options) {
|
||||
if (val === key) {
|
||||
this.selected = this.Options[key]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,258 +1,322 @@
|
||||
<template>
|
||||
<div class="customer-create main-content">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ $t('customers.title') }}</h3>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="dashboard">
|
||||
{{ $t('general.home') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="#">
|
||||
{{ $tc('customers.customer',2) }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="page-actions row">
|
||||
<div class="col-xs-2 mr-4">
|
||||
<base-button
|
||||
v-show="totalCustomers || filtersApplied"
|
||||
:outline="true"
|
||||
:icon="filterIcon"
|
||||
size="large"
|
||||
color="theme"
|
||||
right-icon
|
||||
@click="toggleFilter"
|
||||
>
|
||||
{{ $t('general.filter') }}
|
||||
</base-button>
|
||||
</div>
|
||||
<router-link slot="item-title" class="col-xs-2" to="customers/create">
|
||||
<base-button
|
||||
size="large"
|
||||
icon="plus"
|
||||
color="theme">
|
||||
{{ $t('customers.new_customer') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<base-page class="customer-create">
|
||||
<sw-page-header :title="$t('customers.title')">
|
||||
<sw-breadcrumb slot="breadcrumbs">
|
||||
<sw-breadcrumb-item to="dashboard" :title="$t('general.home')" />
|
||||
<sw-breadcrumb-item
|
||||
to="#"
|
||||
:title="$tc('customers.customer', 2)"
|
||||
active
|
||||
/>
|
||||
</sw-breadcrumb>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-show="showFilters" class="filter-section">
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label">{{ $t('customers.display_name') }}</label>
|
||||
<base-input
|
||||
v-model="filters.display_name"
|
||||
type="text"
|
||||
name="name"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label">{{ $t('customers.contact_name') }}</label>
|
||||
<base-input
|
||||
v-model="filters.contact_name"
|
||||
type="text"
|
||||
name="address_name"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label">{{ $t('customers.phone') }}</label>
|
||||
<base-input
|
||||
v-model="filters.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<label class="clear-filter" @click="clearFilter">{{ $t('general.clear_all') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div v-cloak v-show="showEmptyScreen" class="col-xs-1 no-data-info" align="center">
|
||||
<astronaut-icon class="mt-5 mb-4"/>
|
||||
<div class="row" align="center">
|
||||
<label class="col title">{{ $t('customers.no_customers') }}</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="description col mt-1" align="center">{{ $t('customers.list_of_customers') }}</label>
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<base-button
|
||||
:outline="true"
|
||||
color="theme"
|
||||
class="mt-3"
|
||||
size="large"
|
||||
@click="$router.push('customers/create')"
|
||||
<template slot="actions">
|
||||
<sw-button
|
||||
v-show="totalCustomers"
|
||||
size="lg"
|
||||
variant="primary-outline"
|
||||
@click="toggleFilter"
|
||||
>
|
||||
{{ $t('customers.add_new_customer') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
{{ $t('general.filter') }}
|
||||
<component :is="filterIcon" class="h-4 ml-1 -mr-1 font-bold" />
|
||||
</sw-button>
|
||||
|
||||
<div v-show="!showEmptyScreen" class="table-container">
|
||||
<div class="table-actions mt-5">
|
||||
<p class="table-stats">{{ $t('general.showing') }}: <b>{{ customers.length }}</b> {{ $t('general.of') }} <b>{{ totalCustomers }}</b></p>
|
||||
<sw-button
|
||||
tag-name="router-link"
|
||||
to="customers/create"
|
||||
size="lg"
|
||||
variant="primary"
|
||||
class="ml-4"
|
||||
>
|
||||
<plus-sm-icon class="h-6 mr-1 -ml-2 font-bold" />
|
||||
{{ $t('customers.new_customer') }}
|
||||
</sw-button>
|
||||
</template>
|
||||
</sw-page-header>
|
||||
|
||||
<transition name="fade">
|
||||
<v-dropdown v-if="selectedCustomers.length" :show-arrow="false">
|
||||
<span slot="activator" href="#" class="table-actions-button dropdown-toggle">
|
||||
<slide-y-up-transition>
|
||||
<sw-filter-wrapper v-show="showFilters">
|
||||
<sw-input-group
|
||||
:label="$t('customers.display_name')"
|
||||
class="flex-1 mt-2"
|
||||
>
|
||||
<sw-input
|
||||
v-model="filters.display_name"
|
||||
type="text"
|
||||
name="name"
|
||||
class="mt-2"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('customers.contact_name')"
|
||||
class="flex-1 mt-2 ml-0 lg:ml-6"
|
||||
>
|
||||
<sw-input
|
||||
v-model="filters.contact_name"
|
||||
type="text"
|
||||
name="address_name"
|
||||
class="mt-2"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('customers.phone')"
|
||||
class="flex-1 mt-2 ml-0 lg:ml-6"
|
||||
>
|
||||
<sw-input
|
||||
v-model="filters.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
class="mt-2"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<label
|
||||
class="absolute text-sm leading-snug text-black cursor-pointer"
|
||||
style="top: 10px; right: 15px"
|
||||
@click="clearFilter"
|
||||
>{{ $t('general.clear_all') }}</label
|
||||
>
|
||||
</sw-filter-wrapper>
|
||||
</slide-y-up-transition>
|
||||
|
||||
<sw-empty-table-placeholder
|
||||
v-show="showEmptyScreen"
|
||||
:title="$t('customers.no_customers')"
|
||||
:description="$t('customers.list_of_customers')"
|
||||
>
|
||||
<astronaut-icon class="mt-5 mb-4" />
|
||||
|
||||
<sw-button
|
||||
slot="actions"
|
||||
tag-name="router-link"
|
||||
to="/admin/customers/create"
|
||||
size="lg"
|
||||
variant="primary-outline"
|
||||
>
|
||||
{{ $t('customers.add_new_customer') }}
|
||||
</sw-button>
|
||||
</sw-empty-table-placeholder>
|
||||
|
||||
<div v-show="!showEmptyScreen" class="relative table-container">
|
||||
<div
|
||||
class="relative flex items-center justify-between h-10 mt-5 border-b-2 border-gray-200 border-solid"
|
||||
>
|
||||
<p class="text-sm">
|
||||
{{ $t('general.showing') }}: <b>{{ customers.length }}</b>
|
||||
{{ $t('general.of') }} <b>{{ totalCustomers }}</b>
|
||||
</p>
|
||||
|
||||
<sw-transition type="fade">
|
||||
<sw-dropdown v-if="selectedCustomers.length">
|
||||
<span
|
||||
slot="activator"
|
||||
class="flex block text-sm font-medium cursor-pointer select-none text-primary-400"
|
||||
>
|
||||
{{ $t('general.actions') }}
|
||||
<chevron-down-icon class="h-5" />
|
||||
</span>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeMultipleCustomers">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
</transition>
|
||||
|
||||
<sw-dropdown-item @click="removeMultipleCustomers">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</sw-transition>
|
||||
</div>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="select-all"
|
||||
<div class="absolute z-10 items-center pl-4 mt-2 select-none md:mt-12">
|
||||
<sw-checkbox
|
||||
v-model="selectAllFieldStatus"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="hidden md:inline"
|
||||
@change="selectAllCustomers"
|
||||
>
|
||||
<label for="select-all" class="custom-control-label selectall">
|
||||
<span class="select-all-label">{{ $t('general.select_all') }} </span>
|
||||
</label>
|
||||
/>
|
||||
|
||||
<sw-checkbox
|
||||
v-model="selectAllFieldStatus"
|
||||
:label="$t('general.select_all')"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="md:hidden"
|
||||
@change="selectAllCustomers"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<table-component
|
||||
<sw-table-component
|
||||
ref="table"
|
||||
:show-filter="false"
|
||||
:data="fetchData"
|
||||
table-class="table"
|
||||
>
|
||||
<table-column
|
||||
<sw-table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
cell-class="no-click"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
:id="row.id"
|
||||
v-model="selectField"
|
||||
:value="row.id"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
>
|
||||
<label :for="row.id" class="custom-control-label" />
|
||||
</div>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
<div class="relative block" slot-scope="row">
|
||||
<sw-checkbox
|
||||
:id="row.id"
|
||||
v-model="selectField"
|
||||
:value="row.id"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:filterable="true"
|
||||
:label="$t('customers.display_name')"
|
||||
show="name"
|
||||
/>
|
||||
<table-column
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('customers.display_name') }}</span>
|
||||
<router-link
|
||||
:to="{ path: `customers/${row.id}/view` }"
|
||||
class="font-medium text-primary-500"
|
||||
>
|
||||
{{ row.name }}
|
||||
</router-link>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('customers.contact_name')"
|
||||
show="contact_name"
|
||||
/>
|
||||
<table-column
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('customers.contact_name') }}</span>
|
||||
<span>
|
||||
{{ row.contact_name ? row.contact_name : 'No Contact Name' }}
|
||||
</span>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('customers.phone')"
|
||||
show="phone"
|
||||
/>
|
||||
<table-column
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('customers.phone') }}</span>
|
||||
<span>
|
||||
{{ row.phone ? row.phone : 'No Contact' }}
|
||||
</span>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('customers.amount_due')"
|
||||
show="due_amount"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span> {{ $t('customers.amount_due') }} </span>
|
||||
<div v-html="$utils.formatMoney(row.due_amount, row.currency)"/>
|
||||
<div v-html="$utils.formatMoney(row.due_amount, row.currency)" />
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('customers.added_on')"
|
||||
sort-as="created_at"
|
||||
show="formattedCreatedAt"
|
||||
/>
|
||||
<table-column
|
||||
|
||||
<sw-table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
cell-class="action-dropdown"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span> {{ $t('customers.action') }} </span>
|
||||
<v-dropdown>
|
||||
<a slot="activator" href="#">
|
||||
<dot-icon />
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
|
||||
<router-link :to="{path: `customers/${row.id}/edit`}" class="dropdown-item">
|
||||
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon"/>
|
||||
{{ $t('general.edit') }}
|
||||
</router-link>
|
||||
<sw-dropdown>
|
||||
<dot-icon slot="activator" />
|
||||
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeCustomer(row.id)">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`customers/${row.id}/edit`"
|
||||
>
|
||||
<pencil-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.edit') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`customers/${row.id}/view`"
|
||||
>
|
||||
<eye-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.view') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="removeCustomer(row.id)">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</template>
|
||||
</table-column>
|
||||
</table-component>
|
||||
</sw-table-column>
|
||||
</sw-table-component>
|
||||
</div>
|
||||
</div>
|
||||
</base-page>
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { SweetModal, SweetModalTab } from 'sweet-modal-vue'
|
||||
import DotIcon from '../../components/icon/DotIcon'
|
||||
import { PlusSmIcon } from '@vue-hero-icons/solid'
|
||||
import {
|
||||
FilterIcon,
|
||||
XIcon,
|
||||
ChevronDownIcon,
|
||||
TrashIcon,
|
||||
PencilIcon,
|
||||
EyeIcon,
|
||||
} from '@vue-hero-icons/solid'
|
||||
import AstronautIcon from '../../components/icon/AstronautIcon'
|
||||
import BaseButton from '../../../js/components/base/BaseButton'
|
||||
import { request } from 'http'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DotIcon,
|
||||
AstronautIcon,
|
||||
SweetModal,
|
||||
SweetModalTab,
|
||||
BaseButton
|
||||
ChevronDownIcon,
|
||||
PlusSmIcon,
|
||||
FilterIcon,
|
||||
XIcon,
|
||||
TrashIcon,
|
||||
PencilIcon,
|
||||
EyeIcon,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
showFilters: false,
|
||||
filtersApplied: false,
|
||||
isRequestOngoing: true,
|
||||
filters: {
|
||||
display_name: '',
|
||||
contact_name: '',
|
||||
phone: ''
|
||||
}
|
||||
phone: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showEmptyScreen () {
|
||||
return !this.totalCustomers && !this.isRequestOngoing && !this.filtersApplied
|
||||
showEmptyScreen() {
|
||||
return !this.totalCustomers && !this.isRequestOngoing
|
||||
},
|
||||
filterIcon () {
|
||||
return (this.showFilters) ? 'times' : 'filter'
|
||||
filterIcon() {
|
||||
return this.showFilters ? 'x-icon' : 'filter-icon'
|
||||
},
|
||||
...mapGetters('customer', [
|
||||
'customers',
|
||||
'selectedCustomers',
|
||||
'totalCustomers',
|
||||
'selectAllField'
|
||||
'selectAllField',
|
||||
]),
|
||||
selectField: {
|
||||
get: function () {
|
||||
@@ -260,7 +324,7 @@ export default {
|
||||
},
|
||||
set: function (val) {
|
||||
this.selectCustomer(val)
|
||||
}
|
||||
},
|
||||
},
|
||||
selectAllFieldStatus: {
|
||||
get: function () {
|
||||
@@ -268,16 +332,16 @@ export default {
|
||||
},
|
||||
set: function (val) {
|
||||
this.setSelectAllState(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
filters: {
|
||||
handler: 'setFilters',
|
||||
deep: true
|
||||
}
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
destroyed () {
|
||||
destroyed() {
|
||||
if (this.selectAllField) {
|
||||
this.selectAllCustomers()
|
||||
}
|
||||
@@ -289,19 +353,19 @@ export default {
|
||||
'selectCustomer',
|
||||
'deleteCustomer',
|
||||
'deleteMultipleCustomers',
|
||||
'setSelectAllState'
|
||||
'setSelectAllState',
|
||||
]),
|
||||
refreshTable () {
|
||||
refreshTable() {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
async fetchData ({ page, filter, sort }) {
|
||||
async fetchData({ page, filter, sort }) {
|
||||
let data = {
|
||||
display_name: this.filters.display_name,
|
||||
contact_name: this.filters.contact_name,
|
||||
phone: this.filters.phone,
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page
|
||||
page,
|
||||
}
|
||||
|
||||
this.isRequestOngoing = true
|
||||
@@ -312,60 +376,58 @@ export default {
|
||||
data: response.data.customers.data,
|
||||
pagination: {
|
||||
totalPages: response.data.customers.last_page,
|
||||
currentPage: page
|
||||
}
|
||||
currentPage: page,
|
||||
},
|
||||
}
|
||||
},
|
||||
setFilters () {
|
||||
this.filtersApplied = true
|
||||
setFilters() {
|
||||
this.refreshTable()
|
||||
},
|
||||
clearFilter () {
|
||||
clearFilter() {
|
||||
this.filters = {
|
||||
display_name: '',
|
||||
contact_name: '',
|
||||
phone: ''
|
||||
phone: '',
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.filtersApplied = false
|
||||
})
|
||||
},
|
||||
toggleFilter () {
|
||||
if (this.showFilters && this.filtersApplied) {
|
||||
toggleFilter() {
|
||||
if (this.showFilters) {
|
||||
this.clearFilter()
|
||||
this.refreshTable()
|
||||
}
|
||||
|
||||
this.showFilters = !this.showFilters
|
||||
},
|
||||
async removeCustomer (id) {
|
||||
|
||||
async removeCustomer(id) {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('customers.confirm_delete'),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
dangerMode: true,
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteCustomer(id)
|
||||
let res = await this.deleteCustomer({ ids: [id] })
|
||||
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$tc('customers.deleted_message'))
|
||||
this.refreshTable()
|
||||
window.toastr['success'](this.$tc('customers.deleted_message', 1))
|
||||
this.$refs.table.refresh()
|
||||
return true
|
||||
} else if (request.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
|
||||
window.toastr['error'](res.data.message)
|
||||
return true
|
||||
}
|
||||
})
|
||||
},
|
||||
async removeMultipleCustomers () {
|
||||
|
||||
async removeMultipleCustomers() {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('customers.confirm_delete', 2),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
dangerMode: true,
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let request = await this.deleteMultipleCustomers()
|
||||
@@ -377,7 +439,7 @@ export default {
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
140
resources/assets/js/views/customers/View.vue
Normal file
140
resources/assets/js/views/customers/View.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<base-page class="xl:pl-96">
|
||||
<sw-page-header :title="pageTitle">
|
||||
<template slot="actions">
|
||||
<sw-button
|
||||
tag-name="router-link"
|
||||
:to="`/admin/customers/${$route.params.id}/edit`"
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
>
|
||||
{{ $t('general.edit') }}
|
||||
</sw-button>
|
||||
<sw-dropdown position="bottom-end">
|
||||
<sw-button slot="activator" class="mr-3" variant="primary">
|
||||
{{ $t('customers.new_transaction') }}
|
||||
</sw-button>
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`/admin/estimates/create?customer=${$route.params.id}`"
|
||||
>
|
||||
<document-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('estimates.new_estimate') }}
|
||||
</sw-dropdown-item>
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`/admin/invoices/create?customer=${$route.params.id}`"
|
||||
>
|
||||
<document-text-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('invoices.new_invoice') }}
|
||||
</sw-dropdown-item>
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`/admin/payments/create?customer=${$route.params.id}`"
|
||||
>
|
||||
<credit-card-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('payments.new_payment') }}
|
||||
</sw-dropdown-item>
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`/admin/expenses/create?customer=${$route.params.id}`"
|
||||
>
|
||||
<calculator-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('expenses.new_expense') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
<sw-dropdown>
|
||||
<sw-button slot="activator" variant="primary">
|
||||
<dots-horizontal-icon class="h-5 -ml-1 -mr-1" />
|
||||
</sw-button>
|
||||
|
||||
<sw-dropdown-item @click="removeCustomer($route.params.id)">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</template>
|
||||
</sw-page-header>
|
||||
|
||||
<!-- sidebar -->
|
||||
<customer-view-sidebar />
|
||||
|
||||
<!-- Chart -->
|
||||
<customer-chart />
|
||||
</base-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
DotsHorizontalIcon,
|
||||
TrashIcon,
|
||||
DocumentIcon,
|
||||
DocumentTextIcon,
|
||||
CreditCardIcon,
|
||||
CalculatorIcon,
|
||||
} from '@vue-hero-icons/solid'
|
||||
import LineChart from '../../components/chartjs/LineChart'
|
||||
import CustomerViewSidebar from './partials/CustomerViewSidebar'
|
||||
import CustomerChart from './partials/CustomerChart'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LineChart,
|
||||
DotsHorizontalIcon,
|
||||
CustomerViewSidebar,
|
||||
DocumentIcon,
|
||||
DocumentTextIcon,
|
||||
CreditCardIcon,
|
||||
CalculatorIcon,
|
||||
CustomerChart,
|
||||
TrashIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
customer: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('customer', ['selectedViewCustomer']),
|
||||
pageTitle() {
|
||||
return this.selectedViewCustomer.customer
|
||||
? this.selectedViewCustomer.customer.name
|
||||
: ''
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchViewCustomer({ id: this.$route.params.id })
|
||||
},
|
||||
methods: {
|
||||
...mapActions('customer', [
|
||||
'fetchViewCustomer',
|
||||
'selectCustomer',
|
||||
'deleteMultipleCustomers',
|
||||
]),
|
||||
|
||||
async removeCustomer(id) {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('customers.confirm_delete'),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let data = [id]
|
||||
this.selectCustomer(data)
|
||||
let res = await this.deleteMultipleCustomers()
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$tc('customers.deleted_message'))
|
||||
this.$router.push('/admin/customers')
|
||||
return true
|
||||
} else if (request.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
219
resources/assets/js/views/customers/partials/CustomerChart.vue
Normal file
219
resources/assets/js/views/customers/partials/CustomerChart.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<sw-card v-if="chartData" class="flex flex-col mt-6">
|
||||
<div class="grid grid-cols-12">
|
||||
<div class="col-span-12 xl:col-span-9 xxl:col-span-10">
|
||||
<div class="flex justify-between mt-1 mb-6">
|
||||
<h6 class="flex items-center sw-section-title">
|
||||
<chart-square-bar-icon class="h-5 text-primary-400" />{{
|
||||
$t('dashboard.monthly_chart.title')
|
||||
}}
|
||||
</h6>
|
||||
<div class="w-40 h-10">
|
||||
<sw-select
|
||||
v-model="selectedYear"
|
||||
:options="years"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('dashboard.select_year')"
|
||||
@select="onChangeYear"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<line-chart
|
||||
:format-money="$utils.formatMoney"
|
||||
:format-graph-money="$utils.formatGraphMoney"
|
||||
:invoices="getChartInvoices"
|
||||
:expenses="getChartExpenses"
|
||||
:receipts="getReceiptTotals"
|
||||
:income="getNetProfits"
|
||||
:labels="getChartMonths"
|
||||
class="sm:w-full"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="grid col-span-12 mt-6 text-center xl:mt-0 sm:grid-cols-4 xl:text-right xl:col-span-3 xl:grid-cols-1 xxl:col-span-2"
|
||||
>
|
||||
<div class="px-6 py-2">
|
||||
<span class="text-xs leading-5 lg:text-sm">
|
||||
{{ $t('dashboard.chart_info.total_sales') }}
|
||||
</span>
|
||||
<br />
|
||||
<span class="block mt-1 text-xl font-semibold leading-8">
|
||||
<div v-html="getFormattedSalesTotal" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="px-6 py-2">
|
||||
<span class="text-xs leading-5 lg:text-sm">
|
||||
{{ $t('dashboard.chart_info.total_receipts') }}
|
||||
</span>
|
||||
<br />
|
||||
<span
|
||||
class="block mt-1 text-xl font-semibold leading-8"
|
||||
style="color: #00c99c"
|
||||
>
|
||||
<div v-html="getFormattedTotalReceipts" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="px-6 py-2">
|
||||
<span class="text-xs leading-5 lg:text-sm">
|
||||
{{ $t('dashboard.chart_info.total_expense') }}
|
||||
</span>
|
||||
<br />
|
||||
<span
|
||||
class="block mt-1 text-xl font-semibold leading-8"
|
||||
style="color: #fb7178"
|
||||
>
|
||||
<div v-html="getFormattedTotalExpenses" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="px-6 py-2">
|
||||
<span class="text-xs leading-5 lg:text-sm">
|
||||
{{ $t('dashboard.chart_info.net_income') }}
|
||||
</span>
|
||||
<br />
|
||||
<span
|
||||
class="block mt-1 text-xl font-semibold leading-8"
|
||||
style="color: #5851d8"
|
||||
>
|
||||
<div v-html="getFormattedTotalNetProfit" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- basic info -->
|
||||
<customer-info />
|
||||
</sw-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomerInfo from './CustomerInfo'
|
||||
import LineChart from '../../../components/chartjs/LineChart'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { ChartSquareBarIcon } from '@vue-hero-icons/outline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LineChart,
|
||||
CustomerInfo,
|
||||
ChartSquareBarIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
customers: [],
|
||||
isLoaded: false,
|
||||
chartData: null,
|
||||
years: ['This year', 'Previous year'],
|
||||
selectedYear: 'This year',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('company', ['defaultCurrency']),
|
||||
getChartInvoices() {
|
||||
if (this.chartData && this.chartData.invoiceTotals) {
|
||||
return this.chartData.invoiceTotals
|
||||
}
|
||||
return []
|
||||
},
|
||||
getChartExpenses() {
|
||||
if (this.chartData && this.chartData.expenseTotals) {
|
||||
return this.chartData.expenseTotals
|
||||
}
|
||||
return []
|
||||
},
|
||||
getReceiptTotals() {
|
||||
if (this.chartData && this.chartData.receiptTotals) {
|
||||
return this.chartData.receiptTotals
|
||||
}
|
||||
return []
|
||||
},
|
||||
getNetProfits() {
|
||||
if (this.chartData && this.chartData.netProfits) {
|
||||
return this.chartData.netProfits
|
||||
}
|
||||
return []
|
||||
},
|
||||
getChartMonths() {
|
||||
if (this.chartData && this.chartData.months) {
|
||||
return this.chartData.months
|
||||
}
|
||||
return []
|
||||
},
|
||||
getFormattedSalesTotal() {
|
||||
if (this.chartData && this.chartData.salesTotal) {
|
||||
return this.$utils.formatMoney(
|
||||
this.chartData.salesTotal,
|
||||
this.defaultCurrency
|
||||
)
|
||||
}
|
||||
return 0
|
||||
},
|
||||
getFormattedTotalReceipts() {
|
||||
if (this.chartData && this.chartData.totalReceipts) {
|
||||
return this.$utils.formatMoney(
|
||||
this.chartData.totalReceipts,
|
||||
this.defaultCurrency
|
||||
)
|
||||
}
|
||||
return 0
|
||||
},
|
||||
getFormattedTotalExpenses() {
|
||||
if (this.chartData && this.chartData.totalExpenses) {
|
||||
return this.$utils.formatMoney(
|
||||
this.chartData.totalExpenses,
|
||||
this.defaultCurrency
|
||||
)
|
||||
}
|
||||
return 0
|
||||
},
|
||||
getFormattedTotalNetProfit() {
|
||||
if (this.chartData && this.chartData.netProfit) {
|
||||
return this.$utils.formatMoney(
|
||||
this.chartData.netProfit,
|
||||
this.defaultCurrency
|
||||
)
|
||||
}
|
||||
return 0
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.loadCustomer()
|
||||
this.selectedYear = 'This year'
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadCustomer()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('customer', ['fetchViewCustomer']),
|
||||
|
||||
async loadCustomer() {
|
||||
this.isLoaded = false
|
||||
let response = await this.fetchViewCustomer({ id: this.$route.params.id })
|
||||
if (response.data) {
|
||||
this.chartData = response.data.chartData
|
||||
}
|
||||
this.isLoaded = false
|
||||
},
|
||||
async onChangeYear(data) {
|
||||
if (data == 'Previous year') {
|
||||
let response = await this.fetchViewCustomer({
|
||||
id: this.$route.params.id,
|
||||
previous_year: true,
|
||||
})
|
||||
if (response.data) {
|
||||
this.chartData = response.data.chartData
|
||||
}
|
||||
return true
|
||||
}
|
||||
let response = await this.fetchViewCustomer({ id: this.$route.params.id })
|
||||
if (response.data) {
|
||||
this.chartData = response.data.chartData
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
273
resources/assets/js/views/customers/partials/CustomerInfo.vue
Normal file
273
resources/assets/js/views/customers/partials/CustomerInfo.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<div
|
||||
class="pt-6 mt-5 border-t-2 border-solid lg:pt-8 md:pt-4"
|
||||
style="border-top-color: #f9fbff"
|
||||
>
|
||||
<div class="col-span-12">
|
||||
<p class="text-gray-500 uppercase sw-section-title">
|
||||
{{ $t('customers.basic_info') }}
|
||||
</p>
|
||||
<div
|
||||
class="grid grid-cols-1 gap-4 mt-5 lg:grid-cols-3 md:grid-cols-2 sm:grid-cols-1"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
|
||||
>
|
||||
{{ $t('customers.display_name') }}
|
||||
</p>
|
||||
<p class="text-sm font-bold leading-5 text-black non-italic">
|
||||
{{
|
||||
selectedViewCustomer.customer &&
|
||||
selectedViewCustomer.customer.name
|
||||
? selectedViewCustomer.customer.name
|
||||
: ''
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p
|
||||
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
|
||||
>
|
||||
{{ $t('customers.primary_contact_name') }}
|
||||
</p>
|
||||
<p class="text-sm font-bold leading-5 text-black non-italic">
|
||||
{{
|
||||
selectedViewCustomer.customer &&
|
||||
selectedViewCustomer.customer.contact_name
|
||||
? selectedViewCustomer.customer.contact_name
|
||||
: ''
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p
|
||||
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
|
||||
>
|
||||
{{ $t('customers.email') }}
|
||||
</p>
|
||||
<p class="text-sm font-bold leading-5 text-black non-italic">
|
||||
{{
|
||||
selectedViewCustomer.customer &&
|
||||
selectedViewCustomer.customer.email
|
||||
? selectedViewCustomer.customer.email
|
||||
: ''
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-1 gap-4 mt-5 lg:grid-cols-3 md:grid-cols-2 sm:grid-cols-1"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
|
||||
>
|
||||
{{ $t('wizard.currency') }}
|
||||
</p>
|
||||
<p class="text-sm font-bold leading-5 text-black non-italic">
|
||||
{{
|
||||
selectedViewCustomer.customer.currency
|
||||
? `${selectedViewCustomer.customer.currency.code} (${selectedViewCustomer.customer.currency.symbol})`
|
||||
: ''
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p
|
||||
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
|
||||
>
|
||||
{{ $t('customers.phone_number') }}
|
||||
</p>
|
||||
<p class="text-sm font-bold leading-5 text-black non-italic">
|
||||
{{
|
||||
selectedViewCustomer.customer &&
|
||||
selectedViewCustomer.customer.phone
|
||||
? selectedViewCustomer.customer.phone
|
||||
: ''
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p
|
||||
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
|
||||
>
|
||||
{{ $t('customers.website') }}
|
||||
</p>
|
||||
<p class="text-sm font-bold leading-5 text-black non-italic">
|
||||
{{
|
||||
selectedViewCustomer.customer &&
|
||||
selectedViewCustomer.customer.website
|
||||
? selectedViewCustomer.customer.website
|
||||
: ''
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="
|
||||
getFormattedShippingAddress.length ||
|
||||
getFormattedBillingAddress.length
|
||||
"
|
||||
class="mt-8 text-gray-500 uppercase sw-section-title"
|
||||
>
|
||||
{{ $t('customers.address') }}
|
||||
</p>
|
||||
<div
|
||||
class="grid grid-cols-1 gap-4 md:grid-cols-2 sm:grid-cols-1 lg:grid-cols-2"
|
||||
>
|
||||
<div v-if="getFormattedBillingAddress.length" class="mt-5">
|
||||
<p
|
||||
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
|
||||
>
|
||||
{{ $t('customers.billing_address') }}
|
||||
</p>
|
||||
<p
|
||||
class="text-sm font-bold leading-5 text-black non-italic"
|
||||
v-html="getFormattedBillingAddress"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="getFormattedShippingAddress.length" class="mt-5">
|
||||
<p
|
||||
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
|
||||
>
|
||||
{{ $t('customers.shipping_address') }}
|
||||
</p>
|
||||
<p
|
||||
class="text-sm font-bold leading-5 text-black non-italic"
|
||||
v-html="getFormattedShippingAddress"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Fields -->
|
||||
<p
|
||||
v-if="getCustomField.length > 0"
|
||||
class="mt-8 text-gray-500 uppercase sw-section-title"
|
||||
>
|
||||
{{ $t('settings.custom_fields.title') }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 gap-4 mt-5 lg:grid-cols-3 md:grid-cols-2 sm:grid-cols-1"
|
||||
>
|
||||
<div
|
||||
v-for="(field, index) in getCustomField"
|
||||
:key="index"
|
||||
:required="field.is_required ? true : false"
|
||||
>
|
||||
<p
|
||||
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
|
||||
>
|
||||
{{ field.custom_field.label }}
|
||||
</p>
|
||||
<p
|
||||
v-if="field.type === 'Switch'"
|
||||
class="text-sm font-bold leading-5 text-black non-italic"
|
||||
>
|
||||
<span v-if="field.defaultAnswer === 1"> Yes </span>
|
||||
<span v-else> No </span>
|
||||
</p>
|
||||
<p v-else class="text-sm font-bold leading-5 text-black non-italic">
|
||||
{{ field.defaultAnswer }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
customer: null,
|
||||
customFields: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getFormattedBillingAddress() {
|
||||
let billingAddress = ``
|
||||
|
||||
if (!this.selectedViewCustomer.customer) {
|
||||
return billingAddress
|
||||
}
|
||||
|
||||
if (!this.selectedViewCustomer.customer.billing_address) {
|
||||
return billingAddress
|
||||
}
|
||||
|
||||
if (this.selectedViewCustomer.customer.billing_address.address_street_1) {
|
||||
billingAddress += `<span>${this.selectedViewCustomer.customer.billing_address.address_street_1},</span><br>`
|
||||
}
|
||||
if (this.selectedViewCustomer.customer.billing_address.address_street_2) {
|
||||
billingAddress += `<span>${this.selectedViewCustomer.customer.billing_address.address_street_2},</span><br>`
|
||||
}
|
||||
if (this.selectedViewCustomer.customer.billing_address.city) {
|
||||
billingAddress += `<span>${this.selectedViewCustomer.customer.billing_address.city},</span> `
|
||||
}
|
||||
if (this.selectedViewCustomer.customer.billing_address.state) {
|
||||
billingAddress += `<span>${this.selectedViewCustomer.customer.billing_address.state},</span><br>`
|
||||
}
|
||||
if (this.selectedViewCustomer.customer.billing_address.country) {
|
||||
billingAddress += `<span>${this.selectedViewCustomer.customer.billing_address.country.name}.</span> `
|
||||
}
|
||||
if (this.selectedViewCustomer.customer.billing_address.zip) {
|
||||
billingAddress += `<span>${this.selectedViewCustomer.customer.billing_address.zip}.</span> `
|
||||
}
|
||||
return billingAddress
|
||||
},
|
||||
getFormattedShippingAddress() {
|
||||
let shippingAddress = ``
|
||||
|
||||
if (!this.selectedViewCustomer.customer) {
|
||||
return shippingAddress
|
||||
}
|
||||
|
||||
if (!this.selectedViewCustomer.customer.shipping_address) {
|
||||
return shippingAddress
|
||||
}
|
||||
|
||||
if (
|
||||
this.selectedViewCustomer.customer.shipping_address.address_street_1
|
||||
) {
|
||||
shippingAddress += `<span>${this.selectedViewCustomer.customer.shipping_address.address_street_1},</span><br>`
|
||||
}
|
||||
if (
|
||||
this.selectedViewCustomer.customer.shipping_address.address_street_2
|
||||
) {
|
||||
shippingAddress += `<span>${this.selectedViewCustomer.customer.shipping_address.address_street_2},</span><br>`
|
||||
}
|
||||
if (this.selectedViewCustomer.customer.shipping_address.city) {
|
||||
shippingAddress += `<span>${this.selectedViewCustomer.customer.shipping_address.city},</span> `
|
||||
}
|
||||
if (this.selectedViewCustomer.customer.shipping_address.state) {
|
||||
shippingAddress += `<span>${this.selectedViewCustomer.customer.shipping_address.state},</span><br>`
|
||||
}
|
||||
if (this.selectedViewCustomer.customer.shipping_address.country) {
|
||||
shippingAddress += `<span>${this.selectedViewCustomer.customer.shipping_address.country.name}.</span> `
|
||||
}
|
||||
if (this.selectedViewCustomer.customer.shipping_address.zip) {
|
||||
shippingAddress += `<span>${this.selectedViewCustomer.customer.shipping_address.zip}.</span> `
|
||||
}
|
||||
return shippingAddress
|
||||
},
|
||||
|
||||
getCustomField() {
|
||||
if (this.selectedViewCustomer.customer.fields) {
|
||||
return this.selectedViewCustomer.customer.fields
|
||||
}
|
||||
return []
|
||||
},
|
||||
|
||||
...mapGetters('customer', ['selectedViewCustomer']),
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.customer = this.selectedViewCustomer
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed top-0 left-0 hidden h-full pt-16 pb-4 ml-56 bg-white xl:ml-64 w-88 xl:block"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between px-4 pt-8 pb-2 border border-gray-200 border-solid height-full"
|
||||
>
|
||||
<sw-input
|
||||
v-model="searchData.searchText"
|
||||
:placeholder="$t('general.search')"
|
||||
class="mb-6"
|
||||
type="text"
|
||||
variant="gray"
|
||||
@input="onSearch()"
|
||||
>
|
||||
<search-icon slot="rightIcon" class="h-5" />
|
||||
</sw-input>
|
||||
|
||||
<div class="flex mb-6 ml-3" role="group" aria-label="First group">
|
||||
<sw-dropdown
|
||||
:close-on-select="false"
|
||||
align="left"
|
||||
position="bottom-start"
|
||||
>
|
||||
<sw-button slot="activator" size="md" variant="gray-light">
|
||||
<filter-icon class="h-5" />
|
||||
</sw-button>
|
||||
|
||||
<div
|
||||
class="px-2 py-1 pb-2 mb-2 text-sm border-b border-gray-200 border-solid"
|
||||
>
|
||||
{{ $t('general.sort_by') }}
|
||||
</div>
|
||||
|
||||
<sw-dropdown-item class="flex cursor-pointer">
|
||||
<sw-input-group class="-mt-3 font-normal">
|
||||
<sw-radio
|
||||
:label="$t('customers.create_date')"
|
||||
size="sm"
|
||||
id="filter_create_date"
|
||||
v-model="searchData.orderByField"
|
||||
name="filter"
|
||||
value="invoices.created_at"
|
||||
@change="onSearch"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item class="flex cursor-pointer">
|
||||
<sw-input-group class="-mt-3 font-normal">
|
||||
<sw-radio
|
||||
:label="$t('customers.display_name')"
|
||||
size="sm"
|
||||
id="filter_display_name"
|
||||
v-model="searchData.orderByField"
|
||||
name="filter"
|
||||
value="users.name"
|
||||
@change="onSearch"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
|
||||
<sw-button
|
||||
class="ml-1"
|
||||
v-tooltip.top-center="{ content: getOrderName }"
|
||||
size="md"
|
||||
variant="gray-light"
|
||||
@click="sortData"
|
||||
>
|
||||
<sort-ascending-icon v-if="getOrderBy" class="h-5" />
|
||||
<sort-descending-icon v-else class="h-5" />
|
||||
</sw-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<base-loader v-if="isSearching" :show-bg-overlay="true" />
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="h-full pb-32 overflow-y-scroll border-l border-gray-200 border-solid sidebar sw-scroll"
|
||||
>
|
||||
<router-link
|
||||
v-for="(customer, index) in customers"
|
||||
:to="`/admin/customers/${customer.id}/view`"
|
||||
:key="index"
|
||||
:id="'customer-' + customer.id"
|
||||
:class="[
|
||||
'flex justify-between p-4 items-center cursor-pointer hover:bg-gray-100 border-l-4 border-transparent',
|
||||
{
|
||||
'bg-gray-100 border-l-4 border-primary-500 border-solid': hasActiveUrl(
|
||||
customer.id
|
||||
),
|
||||
},
|
||||
]"
|
||||
style="border-top: 1px solid rgba(185, 193, 209, 0.41)"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="pr-2 text-sm not-italic font-normal leading-5 text-black capitalize truncate"
|
||||
>
|
||||
{{ customer.name }}
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 text-xs not-italic font-medium leading-5 text-gray-600"
|
||||
v-if="customer.contact_name"
|
||||
>
|
||||
{{ customer.contact_name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 whitespace-no-wrap right">
|
||||
<div
|
||||
class="text-xl not-italic font-semibold leading-8 text-right text-gray-900"
|
||||
v-html="$utils.formatMoney(customer.due_amount, customer.currency)"
|
||||
/>
|
||||
</div>
|
||||
</router-link>
|
||||
<p
|
||||
v-if="!customers.length"
|
||||
class="flex justify-center px-4 mt-5 text-sm text-gray-600"
|
||||
>
|
||||
{{ $t('customers.no_matching_customers') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
FilterIcon,
|
||||
SortAscendingIcon,
|
||||
SortDescendingIcon,
|
||||
SearchIcon,
|
||||
} from '@vue-hero-icons/solid'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
const _ = require('lodash')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterIcon,
|
||||
SortAscendingIcon,
|
||||
SortDescendingIcon,
|
||||
SearchIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
customers: [],
|
||||
customer: null,
|
||||
currency: null,
|
||||
searchData: {
|
||||
orderBy: null,
|
||||
orderByField: null,
|
||||
searchText: null,
|
||||
},
|
||||
isSearching: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getOrderBy() {
|
||||
if (
|
||||
this.searchData.orderBy === 'asc' ||
|
||||
this.searchData.orderBy == null
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
getOrderName() {
|
||||
if (this.getOrderBy) {
|
||||
return this.$t('general.ascending')
|
||||
}
|
||||
return this.$t('general.descending')
|
||||
},
|
||||
...mapGetters('company', ['defaultCurrency']),
|
||||
|
||||
...mapGetters('customer', ['selectedViewCustomer']),
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.loadCustomer()
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.loadCustomers()
|
||||
this.loadCustomer()
|
||||
this.onSearch = _.debounce(this.onSearch, 500)
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('customer', ['fetchCustomers']),
|
||||
|
||||
hasActiveUrl(id) {
|
||||
return this.$route.params.id == id
|
||||
},
|
||||
|
||||
async loadCustomers() {
|
||||
let response = await this.fetchCustomers({
|
||||
limit: 'all',
|
||||
})
|
||||
|
||||
if (response.data.customers) {
|
||||
this.customers = response.data.customers.data
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.scrollToCustomer()
|
||||
}, 500)
|
||||
},
|
||||
|
||||
scrollToCustomer() {
|
||||
const el = document.getElementById(`customer-${this.$route.params.id}`)
|
||||
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth' })
|
||||
el.classList.add('shake')
|
||||
}
|
||||
},
|
||||
|
||||
async loadCustomer() {
|
||||
this.customer = this.selectedViewCustomer
|
||||
this.currency = this.selectedViewCustomer.currency
|
||||
},
|
||||
async onSearch() {
|
||||
let data = {}
|
||||
if (
|
||||
this.searchData.searchText !== '' &&
|
||||
this.searchData.searchText !== null &&
|
||||
this.searchData.searchText !== undefined
|
||||
) {
|
||||
data.display_name = this.searchData.searchText
|
||||
}
|
||||
|
||||
if (
|
||||
this.searchData.orderBy !== null &&
|
||||
this.searchData.orderBy !== undefined
|
||||
) {
|
||||
data.orderBy = this.searchData.orderBy
|
||||
}
|
||||
|
||||
if (
|
||||
this.searchData.orderByField !== null &&
|
||||
this.searchData.orderByField !== undefined
|
||||
) {
|
||||
data.orderByField = this.searchData.orderByField
|
||||
}
|
||||
|
||||
this.isSearching = true
|
||||
try {
|
||||
let response = await this.fetchCustomers({ ...data })
|
||||
this.isSearching = false
|
||||
if (response.data) {
|
||||
this.customers = response.data.customers.data
|
||||
}
|
||||
} catch (error) {
|
||||
this.isSearching = false
|
||||
}
|
||||
},
|
||||
sortData() {
|
||||
if (this.searchData.orderBy === 'asc') {
|
||||
this.searchData.orderBy = 'desc'
|
||||
this.onSearch()
|
||||
return true
|
||||
}
|
||||
this.searchData.orderBy = 'asc'
|
||||
this.onSearch()
|
||||
return true
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -1,642 +1,21 @@
|
||||
<template>
|
||||
<div id="app" class="main-content dashboard">
|
||||
<div class="row">
|
||||
<div class="dash-item col-sm-6">
|
||||
<router-link slot="item-title" to="/admin/invoices">
|
||||
<div class="dashbox">
|
||||
<div class="desc">
|
||||
<span
|
||||
v-if="isLoaded"
|
||||
class="amount"
|
||||
>
|
||||
<div v-html="$utils.formatMoney(getTotalDueAmount, defaultCurrency)"/>
|
||||
</span>
|
||||
<span class="title">
|
||||
{{ $t('dashboard.cards.due_amount') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<dollar-icon class="card-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="dash-item col-sm-6">
|
||||
<router-link slot="item-title" to="/admin/customers">
|
||||
<div class="dashbox">
|
||||
<div class="desc">
|
||||
<span v-if="isLoaded"
|
||||
class="amount" >
|
||||
{{ getContacts }}
|
||||
</span>
|
||||
<span class="title">
|
||||
{{ $t('dashboard.cards.customers') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<contact-icon class="card-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="dash-item col-sm-6">
|
||||
<router-link slot="item-title" to="/admin/invoices">
|
||||
<div class="dashbox">
|
||||
<div class="desc">
|
||||
<span v-if="isLoaded"
|
||||
class="amount">
|
||||
{{ getInvoices }}
|
||||
</span>
|
||||
<span class="title">
|
||||
{{ $t('dashboard.cards.invoices') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<invoice-icon class="card-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="dash-item col-sm-6">
|
||||
<router-link slot="item-title" to="/admin/estimates">
|
||||
<div class="dashbox">
|
||||
<div class="desc">
|
||||
<span v-if="isLoaded"
|
||||
class="amount">
|
||||
{{ getEstimates }}
|
||||
</span>
|
||||
<span class="title">
|
||||
{{ $t('dashboard.cards.estimates') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<estimate-icon class="card-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 mt-2">
|
||||
<div class="card dashboard-card">
|
||||
<div class="graph-body">
|
||||
<div class="card-body col-md-12 col-lg-12 col-xl-10">
|
||||
<div class="card-header">
|
||||
<h6><i class="fa fa-line-chart text-primary"/>{{ $t('dashboard.monthly_chart.title') }} </h6>
|
||||
<div class="year-selector">
|
||||
<base-select
|
||||
v-model="selectedYear"
|
||||
:options="years"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('dashboard.select_year')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<line-chart
|
||||
v-if="isLoaded"
|
||||
:format-money="$utils.formatMoney"
|
||||
:format-graph-money="$utils.formatGraphMoney"
|
||||
:invoices="getChartInvoices"
|
||||
:expenses="getChartExpenses"
|
||||
:receipts="getReceiptTotals"
|
||||
:income="getNetProfits"
|
||||
:labels="getChartMonths"
|
||||
class=""
|
||||
/>
|
||||
</div>
|
||||
<div class="chart-desc col-md-12 col-lg-12 col-xl-2">
|
||||
<div class="stats">
|
||||
<div class="description">
|
||||
<span class="title"> {{ $t('dashboard.chart_info.total_sales') }} </span>
|
||||
<br>
|
||||
<span v-if="isLoaded" class="total">
|
||||
<div v-html="$utils.formatMoney(getTotalSales, defaultCurrency)"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<span class="title"> {{ $t('dashboard.chart_info.total_receipts') }} </span>
|
||||
<br>
|
||||
<span v-if="isLoaded" class="total" style="color:#00C99C;">
|
||||
<div v-html="$utils.formatMoney(getTotalReceipts, defaultCurrency)"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<span class="title"> {{ $t('dashboard.chart_info.total_expense') }} </span>
|
||||
<br>
|
||||
<span v-if="isLoaded" class="total" style="color:#FB7178;">
|
||||
<div v-html="$utils.formatMoney(getTotalExpenses, defaultCurrency)"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<span class="title"> {{ $t('dashboard.chart_info.net_income') }} </span>
|
||||
<br>
|
||||
<span class="total" style="color:#5851D8;">
|
||||
<div v-html="$utils.formatMoney(getNetProfit, defaultCurrency)"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<base-loader v-if="!getLoadedData"/>
|
||||
<div class="row table-row">
|
||||
<div class="col-lg-12 col-xl-6 mt-2">
|
||||
<div class="table-header">
|
||||
<h6 class="table-title">
|
||||
{{ $t('dashboard.recent_invoices_card.title') }}
|
||||
</h6>
|
||||
<router-link to="/admin/invoices">
|
||||
<base-button
|
||||
:outline="true"
|
||||
color="theme"
|
||||
class="btn-sm"
|
||||
>
|
||||
{{ $t('dashboard.recent_invoices_card.view_all') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="dashboard-table">
|
||||
<table-component
|
||||
ref="inv_table"
|
||||
:data="getDueInvoices"
|
||||
:show-filter="false"
|
||||
table-class="table"
|
||||
class="dashboard"
|
||||
>
|
||||
<table-column :label="$t('dashboard.recent_invoices_card.due_on')" show="formattedDueDate" />
|
||||
<table-column :label="$t('dashboard.recent_invoices_card.customer')" show="user.name" />
|
||||
<table-column
|
||||
:label="$t('invoices.status')"
|
||||
sort-as="status"
|
||||
>
|
||||
<template slot-scope="row" >
|
||||
<span> {{ $t('invoices.status') }}</span>
|
||||
<span :class="'inv-status-'+row.status.toLowerCase()">{{ (row.status != 'PARTIALLY_PAID')? row.status : row.status.replace('_', ' ') }}</span>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column :label="$t('dashboard.recent_invoices_card.amount_due')" show="due_amount" sort-as="due_amount">
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('dashboard.recent_invoices_card.amount_due') }}</span>
|
||||
<div v-html="$utils.formatMoney(row.due_amount, row.user.currency)"/>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
cell-class="action-dropdown dashboard-recent-invoice-options no-click"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<v-dropdown>
|
||||
<a slot="activator" href="#/">
|
||||
<dot-icon />
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
<router-link :to="{path: `invoices/${row.id}/edit`}" class="dropdown-item">
|
||||
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon"/>
|
||||
{{ $t('general.edit') }}
|
||||
</router-link>
|
||||
<router-link :to="{path: `invoices/${row.id}/view`}" class="dropdown-item">
|
||||
<font-awesome-icon icon="eye" class="dropdown-item-icon" />
|
||||
{{ $t('invoices.view') }}
|
||||
</router-link>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item v-if="row.status == 'DRAFT'">
|
||||
<a class="dropdown-item" href="#/" @click="sendInvoice(row.id)" >
|
||||
<font-awesome-icon icon="envelope" class="dropdown-item-icon" />
|
||||
{{ $t('invoices.send_invoice') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item v-if="row.status === 'DRAFT'">
|
||||
<a class="dropdown-item" href="#/" @click="sentInvoice(row.id)">
|
||||
<font-awesome-icon icon="check-circle" class="dropdown-item-icon" />
|
||||
{{ $t('invoices.mark_as_sent') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeInvoice(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>
|
||||
<div class="col-lg-12 col-xl-6 mt-2 mob-table">
|
||||
<div class="table-header">
|
||||
<h6 class="table-title">
|
||||
{{ $t('dashboard.recent_estimate_card.title') }}
|
||||
</h6>
|
||||
<router-link to="/admin/estimates">
|
||||
<base-button
|
||||
:outline="true"
|
||||
color="theme"
|
||||
class="btn-sm"
|
||||
>
|
||||
{{ $t('dashboard.recent_estimate_card.view_all') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="dashboard-table">
|
||||
<table-component
|
||||
ref="est_table"
|
||||
:data="getRecentEstimates"
|
||||
:show-filter="false"
|
||||
table-class="table"
|
||||
>
|
||||
<table-column :label="$t('dashboard.recent_estimate_card.date')" show="formattedExpiryDate" />
|
||||
<table-column :label="$t('dashboard.recent_estimate_card.customer')" show="user.name" />
|
||||
<table-column
|
||||
:label="$t('estimates.status')"
|
||||
show="status" >
|
||||
<template slot-scope="row" >
|
||||
<span> {{ $t('estimates.status') }}</span>
|
||||
<span :class="'est-status-'+row.status.toLowerCase()">{{ row.status }}</span>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column :label="$t('dashboard.recent_estimate_card.amount_due')" show="total" sort-as="total">
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('dashboard.recent_estimate_card.amount_due') }}</span>
|
||||
<div v-html="$utils.formatMoney(row.total, row.user.currency)"/>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
cell-class="action-dropdown no-click"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<v-dropdown>
|
||||
<a slot="activator" href="#/">
|
||||
<dot-icon />
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
<router-link :to="{path: `estimates/${row.id}/edit`}" class="dropdown-item">
|
||||
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
|
||||
{{ $t('general.edit') }}
|
||||
</router-link>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeEstimate(row.id)">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<router-link :to="{path: `estimates/${row.id}/view`}" class="dropdown-item">
|
||||
<font-awesome-icon icon="eye" class="dropdown-item-icon" />
|
||||
{{ $t('general.view') }}
|
||||
</router-link>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<a class="dropdown-item" href="#/" @click="convertInToinvoice(row.id)">
|
||||
<font-awesome-icon icon="file-alt" class="dropdown-item-icon" />
|
||||
{{ $t('estimates.convert_to_invoice') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item v-if="row.status === 'DRAFT'">
|
||||
<a class="dropdown-item" href="#/" @click.self="onMarkAsSent(row.id)">
|
||||
<font-awesome-icon icon="check-circle" class="dropdown-item-icon" />
|
||||
{{ $t('estimates.mark_as_sent') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item v-if="row.status !== 'SENT'">
|
||||
<a class="dropdown-item" href="#/" @click.self="sendEstimate(row.id)">
|
||||
<font-awesome-icon icon="paper-plane" class="dropdown-item-icon" />
|
||||
{{ $t('estimates.send_estimate') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item v-if="row.status !== 'ACCEPTED'">
|
||||
<a class="dropdown-item" href="#/" @click.self="onMarkAsAccepted(row.id)">
|
||||
<font-awesome-icon icon="check-circle" class="dropdown-item-icon" />
|
||||
{{ $t('estimates.mark_as_accepted') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item v-if="row.status !== 'REJECTED'">
|
||||
<a class="dropdown-item" href="#/" @click.self="onMarkAsRejected(row.id)">
|
||||
<font-awesome-icon icon="times-circle" class="dropdown-item-icon" />
|
||||
{{ $t('estimates.mark_as_rejected') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
</template>
|
||||
</table-column>
|
||||
</table-component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<base-page>
|
||||
<dashboard-stats />
|
||||
<dashboard-chart />
|
||||
<dashboard-table />
|
||||
</base-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import { SweetModal, SweetModalTab } from 'sweet-modal-vue'
|
||||
import LineChart from '../../components/chartjs/LineChart'
|
||||
import DollarIcon from '../../components/icon/DollarIcon'
|
||||
import ContactIcon from '../../components/icon/ContactIcon'
|
||||
import InvoiceIcon from '../../components/icon/InvoiceIcon'
|
||||
import EstimateIcon from '../../components/icon/EstimateIcon'
|
||||
import DashboardStats from '../dashboard/DashboardStats'
|
||||
import DashboardChart from '../dashboard/DashboardChart'
|
||||
import DashboardTable from '../dashboard/DashboardTable'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LineChart,
|
||||
DollarIcon,
|
||||
ContactIcon,
|
||||
InvoiceIcon,
|
||||
EstimateIcon
|
||||
DashboardStats,
|
||||
DashboardChart,
|
||||
DashboardTable,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
incomeTotal: null,
|
||||
...this.$store.state.dashboard,
|
||||
currency: { precision: 2, thousand_separator: ',', decimal_separator: '.', symbol: '$' },
|
||||
isLoaded: false,
|
||||
fetching: false,
|
||||
years: ['This year', 'Previous year'],
|
||||
selectedYear: 'This year'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('user', {
|
||||
'user': 'currentUser'
|
||||
}),
|
||||
...mapGetters('dashboard', [
|
||||
'getChartMonths',
|
||||
'getChartInvoices',
|
||||
'getChartExpenses',
|
||||
'getNetProfits',
|
||||
'getReceiptTotals',
|
||||
'getContacts',
|
||||
'getInvoices',
|
||||
'getEstimates',
|
||||
'getTotalDueAmount',
|
||||
'getTotalSales',
|
||||
'getTotalReceipts',
|
||||
'getTotalExpenses',
|
||||
'getNetProfit',
|
||||
'getLoadedData',
|
||||
'getDueInvoices',
|
||||
'getRecentEstimates'
|
||||
]),
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrency'
|
||||
])
|
||||
},
|
||||
watch: {
|
||||
selectedYear (val) {
|
||||
if (val === 'Previous year') {
|
||||
let params = {previous_year: true}
|
||||
this.loadData(params)
|
||||
} else {
|
||||
this.loadData()
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('dashboard', [
|
||||
'getChart',
|
||||
'loadData'
|
||||
]),
|
||||
...mapActions('invoice', [
|
||||
'deleteInvoice',
|
||||
'sendEmail',
|
||||
'markAsSent'
|
||||
]),
|
||||
...mapActions('estimate', [
|
||||
'deleteEstimate',
|
||||
'markAsAccepted',
|
||||
'markAsRejected',
|
||||
'convertToInvoice'
|
||||
]),
|
||||
...mapActions('estimate', {
|
||||
'sendEstimateEmail': 'sendEmail',
|
||||
'markEstimateAsSent': 'markAsSent'
|
||||
}),
|
||||
|
||||
async loadData (params) {
|
||||
await this.$store.dispatch('dashboard/loadData', params)
|
||||
this.isLoaded = true
|
||||
},
|
||||
|
||||
async removeEstimate (id) {
|
||||
this.id = id
|
||||
window.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('estimates.confirm_delete', 1),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteEstimate(this.id)
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$tc('estimates.deleted_message', 1))
|
||||
this.refreshEstTable()
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
refreshInvTable () {
|
||||
this.$refs.inv_table.refresh()
|
||||
},
|
||||
|
||||
refreshEstTable () {
|
||||
this.$refs.est_table.refresh()
|
||||
},
|
||||
|
||||
async convertInToinvoice (id) {
|
||||
window.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_conversion'),
|
||||
icon: '/assets/icon/file-alt-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.convertToInvoice(id)
|
||||
this.selectAllField = false
|
||||
if (res.data) {
|
||||
window.toastr['success'](this.$t('estimates.conversion_message'))
|
||||
this.$router.push(`invoices/${res.data.invoice.id}/edit`)
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async onMarkAsSent (id) {
|
||||
window.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_mark_as_sent'),
|
||||
icon: '/assets/icon/check-circle-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willMarkAsSent) => {
|
||||
if (willMarkAsSent) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.markEstimateAsSent(data)
|
||||
this.refreshEstTable()
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('estimates.mark_as_sent_successfully'))
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async removeInvoice (id) {
|
||||
this.id = id
|
||||
window.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('invoices.confirm_delete'),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteInvoice(this.id)
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$tc('invoices.deleted_message'))
|
||||
this.refreshInvTable()
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async sendInvoice (id) {
|
||||
window.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('invoices.confirm_send'),
|
||||
icon: '/assets/icon/paper-plane-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willSendInvoice) => {
|
||||
if (willSendInvoice) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.sendEmail(data)
|
||||
this.refreshInvTable()
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](this.$tc('invoices.send_invoice_successfully'))
|
||||
return true
|
||||
}
|
||||
if (response.data.error === 'user_email_does_not_exist') {
|
||||
window.toastr['error'](this.$tc('invoices.user_email_does_not_exist'))
|
||||
return false
|
||||
}
|
||||
window.toastr['error'](this.$tc('invoices.something_went_wrong'))
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async sentInvoice (id) {
|
||||
window.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('invoices.invoice_mark_as_sent'),
|
||||
icon: '/assets/icon/check-circle-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willMarkAsSend) => {
|
||||
if (willMarkAsSend) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.markAsSent(data)
|
||||
this.refreshInvTable()
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('invoices.mark_as_sent_successfully'))
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async onMarkAsAccepted (id) {
|
||||
window.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_mark_as_accepted'),
|
||||
icon: '/assets/icon/check-circle-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (markedAsRejected) => {
|
||||
if (markedAsRejected) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.markAsAccepted(data)
|
||||
this.refreshEstTable()
|
||||
if (response.data) {
|
||||
this.refreshEstTable()
|
||||
window.toastr['success'](this.$tc('estimates.marked_as_accepted_message'))
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async onMarkAsRejected (id) {
|
||||
window.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_mark_as_rejected'),
|
||||
icon: '/assets/icon/times-circle-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (markedAsRejected) => {
|
||||
if (markedAsRejected) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.markAsRejected(data)
|
||||
this.refreshEstTable()
|
||||
if (response.data) {
|
||||
this.refreshEstTable()
|
||||
window.toastr['success'](this.$tc('estimates.marked_as_rejected_message'))
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async sendEstimate (id) {
|
||||
window.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_send_estimate'),
|
||||
icon: '/assets/icon/paper-plane-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willSendEstimate) => {
|
||||
if (willSendEstimate) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.sendEstimateEmail(data)
|
||||
this.refreshEstTable()
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](this.$tc('estimates.send_estimate_successfully'))
|
||||
return true
|
||||
}
|
||||
if (response.data.error === 'user_email_does_not_exist') {
|
||||
window.toastr['error'](this.$tc('estimates.user_email_does_not_exist'))
|
||||
return true
|
||||
}
|
||||
window.toastr['error'](this.$tc('estimates.something_went_wrong'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
152
resources/assets/js/views/dashboard/DashboardChart.vue
Normal file
152
resources/assets/js/views/dashboard/DashboardChart.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-10 mt-8 bg-white rounded shadow">
|
||||
<!-- Chart -->
|
||||
<div
|
||||
class="grid grid-cols-1 col-span-10 px-4 py-5 lg:col-span-7 xl:col-span-8 sm:p-6"
|
||||
>
|
||||
<div class="flex justify-between mt-1 mb-6">
|
||||
<h6 class="flex items-center sw-section-title">
|
||||
<chart-square-bar-icon class="h-5 text-primary-400" />{{ $t('dashboard.monthly_chart.title') }}
|
||||
</h6>
|
||||
<div class="w-40 h-10" style="z-index: 0">
|
||||
<sw-select
|
||||
v-model="selectedYear"
|
||||
:options="years"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('dashboard.select_year')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<line-chart
|
||||
v-if="isLoaded"
|
||||
:format-money="$utils.formatMoney"
|
||||
:format-graph-money="$utils.formatGraphMoney"
|
||||
:invoices="getChartInvoices"
|
||||
:expenses="getChartExpenses"
|
||||
:receipts="getReceiptTotals"
|
||||
:income="getNetProfits"
|
||||
:labels="getChartMonths"
|
||||
class="sm:w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Chart Labels -->
|
||||
<div
|
||||
class="grid grid-cols-1 grid-cols-3 col-span-10 text-center border-t border-l border-gray-200 border-solid lg:border-t-0 lg:text-right lg:col-span-3 xl:col-span-2 lg:grid-cols-1"
|
||||
>
|
||||
<div class="p-6">
|
||||
<span class="text-xs leading-5 lg:text-sm">
|
||||
{{ $t('dashboard.chart_info.total_sales') }}
|
||||
</span>
|
||||
<br />
|
||||
<span
|
||||
v-if="isLoaded"
|
||||
class="block mt-1 text-xl font-semibold leading-8 lg:text-2xl"
|
||||
>
|
||||
<div v-html="$utils.formatMoney(getTotalSales, defaultCurrency)" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<span class="text-xs leading-5 lg:text-sm">
|
||||
{{ $t('dashboard.chart_info.total_receipts') }}
|
||||
</span>
|
||||
<br />
|
||||
<span
|
||||
v-if="isLoaded"
|
||||
class="block mt-1 text-xl font-semibold leading-8 lg:text-2xl"
|
||||
style="color: #00c99c"
|
||||
>
|
||||
<div v-html="$utils.formatMoney(getTotalReceipts, defaultCurrency)" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<span class="text-xs leading-5 lg:text-sm">
|
||||
{{ $t('dashboard.chart_info.total_expense') }}
|
||||
</span>
|
||||
<br />
|
||||
<span
|
||||
v-if="isLoaded"
|
||||
class="block mt-1 text-xl font-semibold leading-8 lg:text-2xl"
|
||||
style="color: #fb7178"
|
||||
>
|
||||
<div v-html="$utils.formatMoney(getTotalExpenses, defaultCurrency)" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="col-span-3 p-6 border-t border-gray-200 border-solid lg:col-span-1"
|
||||
>
|
||||
<span class="text-xs leading-5 lg:text-sm">
|
||||
{{ $t('dashboard.chart_info.net_income') }}
|
||||
</span>
|
||||
<br />
|
||||
<span
|
||||
class="block mt-1 text-xl font-semibold leading-8 lg:text-2xl"
|
||||
style="color: #5851d8"
|
||||
>
|
||||
<div v-html="$utils.formatMoney(getNetProfit, defaultCurrency)" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import { SweetModal, SweetModalTab } from 'sweet-modal-vue'
|
||||
import LineChart from '../../components/chartjs/LineChart'
|
||||
import { ChartSquareBarIcon } from "@vue-hero-icons/outline"
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LineChart,
|
||||
ChartSquareBarIcon
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
...this.$store.state.dashboard,
|
||||
isLoaded: false,
|
||||
years: ['This year', 'Previous year'],
|
||||
selectedYear: 'This year',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('user', {
|
||||
user: 'currentUser',
|
||||
}),
|
||||
...mapGetters('dashboard', [
|
||||
'getChartMonths',
|
||||
'getChartInvoices',
|
||||
'getChartExpenses',
|
||||
'getNetProfits',
|
||||
'getReceiptTotals',
|
||||
'getTotalSales',
|
||||
'getTotalReceipts',
|
||||
'getTotalExpenses',
|
||||
'getNetProfit',
|
||||
]),
|
||||
...mapGetters('company', ['defaultCurrency']),
|
||||
},
|
||||
watch: {
|
||||
selectedYear(val) {
|
||||
if (val === 'Previous year') {
|
||||
let params = { previous_year: true }
|
||||
this.loadData(params)
|
||||
} else {
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('dashboard', ['loadData']),
|
||||
|
||||
async loadData(params) {
|
||||
await this.$store.dispatch('dashboard/loadData', params)
|
||||
this.isLoaded = true
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
128
resources/assets/js/views/dashboard/DashboardStats.vue
Normal file
128
resources/assets/js/views/dashboard/DashboardStats.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-9 xl:gap-8">
|
||||
<!-- Amount Due -->
|
||||
<router-link
|
||||
slot="item-title"
|
||||
class="relative flex justify-between p-3 bg-white rounded shadow hover:bg-gray-100 lg:col-span-3 xl:p-4"
|
||||
to="/admin/invoices"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
v-if="getDashboardDataLoaded"
|
||||
class="text-xl font-semibold leading-tight text-black xl:text-3xl"
|
||||
>
|
||||
<span
|
||||
v-html="$utils.formatMoney(getTotalDueAmount, defaultCurrency)"
|
||||
/>
|
||||
</span>
|
||||
<span class="block mt-1 text-sm leading-tight text-gray-500 xl:text-lg">
|
||||
{{ $t('dashboard.cards.due_amount') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<dollar-icon class="w-10 h-10 xl:w-12 xl:h-12" />
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<!-- Customers -->
|
||||
<router-link
|
||||
slot="item-title"
|
||||
class="relative flex justify-between p-3 bg-white rounded shadow hover:bg-gray-100 lg:col-span-2 xl:p-4"
|
||||
to="/admin/customers"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
v-if="getDashboardDataLoaded"
|
||||
class="text-xl font-semibold leading-tight text-black xl:text-3xl"
|
||||
>
|
||||
{{ getContacts }}
|
||||
</span>
|
||||
<span class="block mt-1 text-sm leading-tight text-gray-500 xl:text-lg">
|
||||
{{ $t('dashboard.cards.customers') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<contact-icon class="w-10 h-10 xl:w-12 xl:h-12" />
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<!-- Invoices -->
|
||||
<router-link
|
||||
slot="item-title"
|
||||
class="relative flex justify-between p-3 bg-white rounded shadow hover:bg-gray-100 lg:col-span-2 xl:p-4"
|
||||
to="/admin/invoices"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
v-if="getDashboardDataLoaded"
|
||||
class="text-xl font-semibold leading-tight text-black xl:text-3xl"
|
||||
>
|
||||
{{ getInvoices }}
|
||||
</span>
|
||||
<span class="block mt-1 text-sm leading-tight text-gray-500 xl:text-lg">
|
||||
{{ $t('dashboard.cards.invoices') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<invoice-icon class="w-10 h-10 xl:w-12 xl:h-12" />
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<!-- Estimates -->
|
||||
<router-link
|
||||
slot="item-title"
|
||||
class="relative flex justify-between p-3 bg-white rounded shadow hover:bg-gray-100 lg:col-span-2 xl:p-4"
|
||||
to="/admin/estimates"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
v-if="getDashboardDataLoaded"
|
||||
class="text-xl font-semibold leading-tight text-black xl:text-3xl"
|
||||
>
|
||||
{{ getEstimates }}
|
||||
</span>
|
||||
<span class="block mt-1 text-sm leading-tight text-gray-500 xl:text-lg">
|
||||
{{ $t('dashboard.cards.estimates') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<estimate-icon class="w-10 h-10 xl:w-12 xl:h-12" />
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import DollarIcon from '../../components/icon/DollarIcon'
|
||||
import ContactIcon from '../../components/icon/ContactIcon'
|
||||
import InvoiceIcon from '../../components/icon/InvoiceIcon'
|
||||
import EstimateIcon from '../../components/icon/EstimateIcon'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DollarIcon,
|
||||
ContactIcon,
|
||||
InvoiceIcon,
|
||||
EstimateIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
...this.$store.state.dashboard,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('user', {
|
||||
user: 'currentUser',
|
||||
}),
|
||||
...mapGetters('dashboard', [
|
||||
'getContacts',
|
||||
'getInvoices',
|
||||
'getEstimates',
|
||||
'getTotalDueAmount',
|
||||
'getDashboardDataLoaded',
|
||||
]),
|
||||
...mapGetters('company', ['defaultCurrency']),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
619
resources/assets/js/views/dashboard/DashboardTable.vue
Normal file
619
resources/assets/js/views/dashboard/DashboardTable.vue
Normal file
@@ -0,0 +1,619 @@
|
||||
<template>
|
||||
<div>
|
||||
<base-loader v-if="!getDashboardDataLoaded" />
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 mt-10 xl:grid-cols-2">
|
||||
<!-- Due Invoices -->
|
||||
<div class="due-invoices">
|
||||
<div class="relative z-10 flex items-center justify-between">
|
||||
<h6 class="mb-0 text-xl font-semibold leading-normal">
|
||||
{{ $t('dashboard.recent_invoices_card.title') }}
|
||||
</h6>
|
||||
|
||||
<sw-button
|
||||
tag-name="router-link"
|
||||
to="/admin/invoices"
|
||||
variant="primary-outline"
|
||||
>
|
||||
{{ $t('dashboard.recent_invoices_card.view_all') }}
|
||||
</sw-button>
|
||||
</div>
|
||||
|
||||
<sw-table-component
|
||||
ref="inv_table"
|
||||
:data="getDueInvoices"
|
||||
:show-filter="false"
|
||||
table-class="table"
|
||||
>
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('dashboard.recent_invoices_card.due_on')"
|
||||
show="formattedDueDate"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('dashboard.recent_invoices_card.due_on') }}</span>
|
||||
<span class="mt-6">{{ row.formattedDueDate }}</span>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('dashboard.recent_invoices_card.customer')"
|
||||
show="user.name"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('dashboard.recent_invoices_card.customer') }}</span>
|
||||
<router-link
|
||||
:to="{ path: `invoices/${row.id}/view` }"
|
||||
class="font-medium text-primary-500"
|
||||
>
|
||||
{{ row.user.name }}
|
||||
</router-link>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('invoices.status')"
|
||||
sort-as="status"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span> {{ $t('invoices.status') }}</span>
|
||||
|
||||
<sw-badge
|
||||
:bg-color="$utils.getBadgeStatusColor(row.status).bgColor"
|
||||
:color="$utils.getBadgeStatusColor(row.status).color"
|
||||
>
|
||||
{{
|
||||
row.status != 'PARTIALLY_PAID'
|
||||
? row.status
|
||||
: row.status.replace('_', ' ')
|
||||
}}
|
||||
</sw-badge>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('dashboard.recent_invoices_card.amount_due')"
|
||||
show="due_amount"
|
||||
sort-as="due_amount"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('dashboard.recent_invoices_card.amount_due') }}</span>
|
||||
|
||||
<div
|
||||
v-html="$utils.formatMoney(row.due_amount, row.user.currency)"
|
||||
/>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
cell-class="action-dropdown dashboard-recent-invoice-options no-click"
|
||||
>
|
||||
<sw-dropdown slot-scope="row">
|
||||
<dot-icon slot="activator" />
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`invoices/${row.id}/edit`"
|
||||
>
|
||||
<pencil-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.edit') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`invoices/${row.id}/view`"
|
||||
>
|
||||
<eye-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('invoices.view') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<!-- <sw-dropdown-item
|
||||
v-if="row.status == 'DRAFT'"
|
||||
@click="sendInvoice(row.id)"
|
||||
>
|
||||
<paper-airplane-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('invoices.send_invoice') }}
|
||||
</sw-dropdown-item> -->
|
||||
|
||||
<sw-dropdown-item
|
||||
v-if="row.status === 'DRAFT'"
|
||||
@click="sentInvoice(row.id)"
|
||||
>
|
||||
<check-circle-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('invoices.mark_as_sent') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="removeInvoice(row.id)">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</sw-table-column>
|
||||
</sw-table-component>
|
||||
</div>
|
||||
|
||||
<!-- Recent Estimates -->
|
||||
<div class="recent-estimates">
|
||||
<div class="relative z-10 flex items-center justify-between">
|
||||
<h6 class="mb-0 text-xl font-semibold leading-normal">
|
||||
{{ $t('dashboard.recent_estimate_card.title') }}
|
||||
</h6>
|
||||
|
||||
<sw-button
|
||||
tag-name="router-link"
|
||||
to="/admin/estimates"
|
||||
variant="primary-outline"
|
||||
>
|
||||
{{ $t('dashboard.recent_estimate_card.view_all') }}
|
||||
</sw-button>
|
||||
</div>
|
||||
|
||||
<sw-table-component
|
||||
ref="est_table"
|
||||
:data="getRecentEstimates"
|
||||
:show-filter="false"
|
||||
table-class="table"
|
||||
>
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('dashboard.recent_estimate_card.date')"
|
||||
show="formattedExpiryDate"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('dashboard.recent_estimate_card.date') }}</span>
|
||||
<span class="mt-6">{{ row.formattedExpiryDate }}</span>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('dashboard.recent_estimate_card.customer')"
|
||||
show="user.name"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('dashboard.recent_estimate_card.customer') }}</span>
|
||||
<router-link
|
||||
:to="{ path: `estimates/${row.id}/view` }"
|
||||
class="font-medium text-primary-500"
|
||||
>
|
||||
{{ row.user.name }}
|
||||
</router-link>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('estimates.status')"
|
||||
show="status"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span> {{ $t('estimates.status') }}</span>
|
||||
|
||||
<sw-badge
|
||||
:bg-color="$utils.getBadgeStatusColor(row.status).bgColor"
|
||||
:color="$utils.getBadgeStatusColor(row.status).color"
|
||||
class="px-3 py-1"
|
||||
>
|
||||
{{ row.status }}
|
||||
</sw-badge>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('dashboard.recent_estimate_card.amount_due')"
|
||||
show="total"
|
||||
sort-as="total"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('dashboard.recent_estimate_card.amount_due') }}</span>
|
||||
|
||||
<div v-html="$utils.formatMoney(row.total, row.user.currency)" />
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
cell-class="action-dropdown no-click"
|
||||
>
|
||||
<sw-dropdown slot-scope="row">
|
||||
<dot-icon slot="activator" />
|
||||
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`estimates/${row.id}/edit`"
|
||||
>
|
||||
<pencil-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.edit') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`estimates/${row.id}/view`"
|
||||
>
|
||||
<eye-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.view') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="convertInToinvoice(row.id)">
|
||||
<document-text-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('estimates.convert_to_invoice') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="onMarkAsSent(row.id)">
|
||||
<check-circle-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('estimates.mark_as_sent') }}
|
||||
</sw-dropdown-item>
|
||||
<!--
|
||||
<sw-dropdown-item
|
||||
v-if="row.status !== 'SENT'"
|
||||
@click="sendEstimate(row.id)"
|
||||
>
|
||||
<paper-airplane-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('estimates.send_estimate') }}
|
||||
</sw-dropdown-item> -->
|
||||
|
||||
<sw-dropdown-item
|
||||
v-if="row.status !== 'ACCEPTED'"
|
||||
@click="onMarkAsAccepted(row.id)"
|
||||
>
|
||||
<check-circle-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('estimates.mark_as_accepted') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item
|
||||
v-if="row.status !== 'REJECTED'"
|
||||
@click="onMarkAsRejected(row.id)"
|
||||
>
|
||||
<x-circle-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('estimates.mark_as_rejected') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="removeEstimate(row.id)">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</sw-table-column>
|
||||
</sw-table-component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import { SweetModal, SweetModalTab } from 'sweet-modal-vue'
|
||||
import {
|
||||
PencilIcon,
|
||||
EyeIcon,
|
||||
PaperAirplaneIcon,
|
||||
CheckCircleIcon,
|
||||
TrashIcon,
|
||||
DocumentTextIcon,
|
||||
XCircleIcon,
|
||||
} from '@vue-hero-icons/solid'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PencilIcon,
|
||||
EyeIcon,
|
||||
PaperAirplaneIcon,
|
||||
CheckCircleIcon,
|
||||
TrashIcon,
|
||||
DocumentTextIcon,
|
||||
XCircleIcon,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
...this.$store.state.dashboard,
|
||||
currency: {
|
||||
precision: 2,
|
||||
thousand_separator: ',',
|
||||
decimal_separator: '.',
|
||||
symbol: '$',
|
||||
},
|
||||
isLoaded: false,
|
||||
fetching: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters('user', {
|
||||
user: 'currentUser',
|
||||
}),
|
||||
|
||||
...mapGetters('dashboard', [
|
||||
'getDashboardDataLoaded',
|
||||
'getDueInvoices',
|
||||
'getRecentEstimates',
|
||||
]),
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('dashboard', ['loadData']),
|
||||
|
||||
...mapActions('invoice', ['deleteInvoice', 'sendEmail', 'markAsSent']),
|
||||
|
||||
...mapActions('estimate', [
|
||||
'deleteEstimate',
|
||||
'markAsAccepted',
|
||||
'markAsRejected',
|
||||
'convertToInvoice',
|
||||
]),
|
||||
|
||||
...mapActions('estimate', {
|
||||
sendEstimateEmail: 'sendEmail',
|
||||
markEstimateAsSent: 'markAsSent',
|
||||
}),
|
||||
|
||||
async loadData(params) {
|
||||
await this.$store.dispatch('dashboard/loadData', params)
|
||||
this.isLoaded = true
|
||||
},
|
||||
|
||||
async removeEstimate(id) {
|
||||
this.id = id
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('estimates.confirm_delete', 1),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteEstimate({ ids: [this.id] })
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$tc('estimates.deleted_message', 1))
|
||||
this.refreshEstTable()
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
refreshInvTable() {
|
||||
this.$refs.inv_table.refresh()
|
||||
},
|
||||
|
||||
refreshEstTable() {
|
||||
this.$refs.est_table.refresh()
|
||||
},
|
||||
|
||||
async convertInToinvoice(id) {
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_conversion'),
|
||||
icon: '/assets/icon/file-alt-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.convertToInvoice(id)
|
||||
this.selectAllField = false
|
||||
|
||||
if (res.data) {
|
||||
window.toastr['success'](this.$t('estimates.conversion_message'))
|
||||
this.$router.push(`invoices/${res.data.invoice.id}/edit`)
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async onMarkAsSent(id) {
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_mark_as_sent'),
|
||||
icon: '/assets/icon/check-circle-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (willMarkAsSent) => {
|
||||
if (willMarkAsSent) {
|
||||
const data = {
|
||||
id: id,
|
||||
status: 'SENT',
|
||||
}
|
||||
|
||||
let response = await this.markEstimateAsSent(data)
|
||||
this.refreshEstTable()
|
||||
|
||||
if (response.data) {
|
||||
window.toastr['success'](
|
||||
this.$tc('estimates.mark_as_sent_successfully')
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async removeInvoice(id) {
|
||||
this.id = id
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('invoices.confirm_delete'),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteInvoice({ ids: [this.id] })
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$tc('invoices.deleted_message'))
|
||||
this.refreshInvTable()
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async sendInvoice(id) {
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('invoices.confirm_send'),
|
||||
icon: '/assets/icon/paper-plane-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (willSendInvoice) => {
|
||||
if (willSendInvoice) {
|
||||
const data = {
|
||||
id: id,
|
||||
}
|
||||
let response = await this.sendEmail(data)
|
||||
this.refreshInvTable()
|
||||
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](
|
||||
this.$tc('invoices.send_invoice_successfully')
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
if (response.data.error === 'user_email_does_not_exist') {
|
||||
window.toastr['error'](
|
||||
this.$tc('invoices.user_email_does_not_exist')
|
||||
)
|
||||
return false
|
||||
}
|
||||
window.toastr['error'](this.$tc('invoices.something_went_wrong'))
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async sentInvoice(id) {
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('invoices.invoice_mark_as_sent'),
|
||||
icon: '/assets/icon/check-circle-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (willMarkAsSend) => {
|
||||
if (willMarkAsSend) {
|
||||
const data = {
|
||||
id: id,
|
||||
status: 'SENT',
|
||||
}
|
||||
let response = await this.markAsSent(data)
|
||||
|
||||
this.refreshInvTable()
|
||||
if (response.data) {
|
||||
window.toastr['success'](
|
||||
this.$tc('invoices.mark_as_sent_successfully')
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async onMarkAsAccepted(id) {
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_mark_as_accepted'),
|
||||
icon: '/assets/icon/check-circle-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (markedAsRejected) => {
|
||||
if (markedAsRejected) {
|
||||
const data = {
|
||||
id: id,
|
||||
}
|
||||
let response = await this.markAsAccepted(data)
|
||||
this.refreshEstTable()
|
||||
|
||||
if (response.data) {
|
||||
this.refreshEstTable()
|
||||
window.toastr['success'](
|
||||
this.$tc('estimates.marked_as_accepted_message')
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async onMarkAsRejected(id) {
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_mark_as_rejected'),
|
||||
icon: '/assets/icon/times-circle-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (markedAsRejected) => {
|
||||
if (markedAsRejected) {
|
||||
const data = {
|
||||
id: id,
|
||||
}
|
||||
let response = await this.markAsRejected(data)
|
||||
this.refreshEstTable()
|
||||
|
||||
if (response.data) {
|
||||
this.refreshEstTable()
|
||||
window.toastr['success'](
|
||||
this.$tc('estimates.marked_as_rejected_message')
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async sendEstimate(id) {
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_send_estimate'),
|
||||
icon: '/assets/icon/paper-plane-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (willSendEstimate) => {
|
||||
if (willSendEstimate) {
|
||||
const data = {
|
||||
id: id,
|
||||
}
|
||||
let response = await this.sendEstimateEmail(data)
|
||||
this.refreshEstTable()
|
||||
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](
|
||||
this.$tc('estimates.send_estimate_successfully')
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
if (response.data.error === 'user_email_does_not_exist') {
|
||||
window.toastr['error'](
|
||||
this.$tc('estimates.user_email_does_not_exist')
|
||||
)
|
||||
return true
|
||||
}
|
||||
window.toastr['error'](this.$tc('estimates.something_went_wrong'))
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -1,13 +1,19 @@
|
||||
<template>
|
||||
<div class="error-box">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 text-sm-center">
|
||||
<h1>{{ $t('general.four_zero_four') }}</h1>
|
||||
<h5>{{ $t('general.you_got_lost') }}</h5>
|
||||
<div class="w-full h-full">
|
||||
<div class="flex items-center justify-center w-full h-full">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<h1 class="text-primary-500" style="font-size: 10rem">
|
||||
{{ $t('general.four_zero_four') }}
|
||||
</h1>
|
||||
<h5 class="mb-10 text-3xl text-primary-500">
|
||||
{{ $t('general.you_got_lost') }}
|
||||
</h5>
|
||||
<router-link
|
||||
class="btn btn-lg bg-yellow text-white"
|
||||
to="/">
|
||||
<font-awesome-icon icon="arrow-left" class="icon text-white mr-2"/> {{ $t('general.go_home') }}
|
||||
class="flex items-center w-32 h-12 px-2 py-1 text-base font-medium leading-none text-center text-white whitespace-no-wrap rounded bg-primary-500 btn-lg hover:text-white"
|
||||
to="/admin/dashboard"
|
||||
>
|
||||
<arrow-left-icon class="mr-2 text-white icon" />
|
||||
{{ $t('general.go_home') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -15,18 +21,23 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowLeftIcon } from '@vue-hero-icons/solid'
|
||||
export default {
|
||||
mounted () {
|
||||
components: {
|
||||
ArrowLeftIcon,
|
||||
},
|
||||
mounted() {
|
||||
this.setLayoutClasses()
|
||||
},
|
||||
destroyed () {
|
||||
$('body').removeClass('page-error-404')
|
||||
destroyed() {
|
||||
let body = document.getElementsByTagName('body')
|
||||
body[0].classList -= ' bg-black'
|
||||
},
|
||||
methods: {
|
||||
setLayoutClasses () {
|
||||
let body = $('body')
|
||||
body.addClass('page-error-404')
|
||||
}
|
||||
}
|
||||
setLayoutClasses() {
|
||||
let body = document.getElementsByTagName('body')
|
||||
body[0].classList += ' bg-black'
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
211
resources/assets/js/views/estimates/CustomerSelect.vue
Normal file
211
resources/assets/js/views/estimates/CustomerSelect.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div class="col-span-5 pr-0">
|
||||
<div
|
||||
v-if="selectedCustomer"
|
||||
class="flex flex-col p-4 bg-white border border-gray-200 border-solid"
|
||||
style="min-height: 170px"
|
||||
>
|
||||
<div class="relative flex justify-between mb-1">
|
||||
<label class="flex-1 font-medium">{{ selectedCustomer.name }}</label>
|
||||
<a
|
||||
class="relative my-0 ml-0 mr-6 text-sm font-medium cursor-pointer text-primary-500"
|
||||
@click.prevent="editCustomer"
|
||||
>
|
||||
{{ $t('general.edit') }}
|
||||
</a>
|
||||
<a
|
||||
class="relative my-0 ml-0 mr-6 text-sm font-medium cursor-pointer text-primary-500"
|
||||
@click.prevent="resetSelectedCustomer"
|
||||
>
|
||||
{{ $t('general.deselect') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mt-1">
|
||||
<div v-if="selectedCustomer.billing_address">
|
||||
<div class="flex flex-col">
|
||||
<label
|
||||
class="mb-1 text-sm font-medium text-gray-500 uppercase whitespace-no-wrap"
|
||||
>
|
||||
{{ $t('general.bill_to') }}
|
||||
</label>
|
||||
<div class="flex flex-col flex-1 p-0">
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.name"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.name }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.address_street_1"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.address_street_1 }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.address_street_2"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.address_street_2 }}
|
||||
</label>
|
||||
<label
|
||||
v-if="
|
||||
selectedCustomer.billing_address.city &&
|
||||
selectedCustomer.billing_address.state
|
||||
"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.city }},
|
||||
{{ selectedCustomer.billing_address.state }}
|
||||
{{ selectedCustomer.billing_address.zip }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.country"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.country.name }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.phone"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.phone }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedCustomer.shipping_address">
|
||||
<div class="flex flex-col">
|
||||
<label
|
||||
class="mb-1 text-sm font-medium text-gray-500 uppercase whitespace-no-wrap"
|
||||
>
|
||||
{{ $t('general.ship_to') }}
|
||||
</label>
|
||||
<div class="flex flex-col flex-1 p-0">
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.name"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.name }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.address_street_1"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.address_street_1 }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.address_street_2"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.address_street_2 }}
|
||||
</label>
|
||||
<label
|
||||
v-if="
|
||||
selectedCustomer.shipping_address.city &&
|
||||
selectedCustomer.shipping_address
|
||||
"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.city }},
|
||||
{{ selectedCustomer.shipping_address.state }}
|
||||
{{ selectedCustomer.shipping_address.zip }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.country"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.country.name }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.phone"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.phone }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<sw-popup
|
||||
:class="[
|
||||
'p-0',
|
||||
{
|
||||
'border border-solid border-danger rounded': valid.$error,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div
|
||||
slot="activator"
|
||||
class="relative flex justify-center px-0 py-16 bg-white border border-gray-200 border-solid rounded"
|
||||
style="min-height: 170px"
|
||||
>
|
||||
<user-icon
|
||||
class="flex justify-center w-10 h-10 p-2 mr-5 text-sm text-white bg-gray-200 rounded-full font-base"
|
||||
/>
|
||||
<div class="mt-1">
|
||||
<label class="text-lg">
|
||||
{{ $t('customers.new_customer') }}
|
||||
<span class="text-danger"> * </span>
|
||||
</label>
|
||||
<p v-if="valid.$error && !valid.required" class="text-danger">
|
||||
{{ $t('estimates.errors.required') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<customer-select-popup :user-id="customerId" type="estimate" />
|
||||
</sw-popup>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { UserIcon } from '@vue-hero-icons/solid'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserIcon,
|
||||
},
|
||||
props: {
|
||||
valid: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
customerId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters('estimate', ['getTemplateId', 'selectedCustomer']),
|
||||
},
|
||||
|
||||
created() {
|
||||
this.resetSelectedCustomer()
|
||||
if (this.customerId) {
|
||||
this.selectCustomer(this.customerId)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('estimate', ['resetSelectedCustomer', 'selectCustomer']),
|
||||
|
||||
...mapActions('modal', ['openModal']),
|
||||
|
||||
editCustomer() {
|
||||
this.openModal({
|
||||
title: this.$t('customers.edit_customer'),
|
||||
componentName: 'CustomerModal',
|
||||
id: this.selectedCustomer.id,
|
||||
data: this.selectedCustomer,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -1,50 +1,49 @@
|
||||
<template>
|
||||
<div class="section mt-2">
|
||||
<label class="estimate-label">
|
||||
<div class="flex items-center justify-between w-full mt-2 text-sm">
|
||||
<label class="font-semibold leading-5 text-gray-500 uppercase">
|
||||
{{ tax.name }} ({{ tax.percent }}%)
|
||||
</label>
|
||||
<label class="estimate-amount">
|
||||
<label class="flex items-center justify-center text-lg text-black">
|
||||
<div v-html="$utils.formatMoney(tax.amount, currency)" />
|
||||
|
||||
<font-awesome-icon
|
||||
class="ml-2"
|
||||
icon="trash-alt"
|
||||
@click="$emit('remove', index)"
|
||||
/>
|
||||
<trash-icon class="h-5 ml-2" @click="$emit('remove', index)" />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { TrashIcon } from '@vue-hero-icons/solid'
|
||||
export default {
|
||||
components: {
|
||||
TrashIcon,
|
||||
},
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
tax: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0,
|
||||
},
|
||||
totalTax: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
taxAmount () {
|
||||
taxAmount() {
|
||||
if (this.tax.compound_tax && this.total) {
|
||||
return ((this.total + this.totalTax) * this.tax.percent) / 100
|
||||
}
|
||||
@@ -54,30 +53,26 @@ export default {
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
total: {
|
||||
handler: 'updateTax'
|
||||
handler: 'updateTax',
|
||||
},
|
||||
totalTax: {
|
||||
handler: 'updateTax'
|
||||
}
|
||||
handler: 'updateTax',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateTax () {
|
||||
updateTax() {
|
||||
this.$emit('update', {
|
||||
'index': this.index,
|
||||
'item': {
|
||||
index: this.index,
|
||||
item: {
|
||||
...this.tax,
|
||||
amount: this.taxAmount
|
||||
}
|
||||
amount: this.taxAmount,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,22 @@
|
||||
<template>
|
||||
<tr class="item-row estimate-item-row">
|
||||
<td colspan="5">
|
||||
<table class="full-width">
|
||||
<tr class="box-border bg-white border border-gray-200 border-solid rounded-b">
|
||||
<td colspan="5" class="p-0 text-left align-top">
|
||||
<table class="w-full">
|
||||
<colgroup>
|
||||
<col style="width: 40%;">
|
||||
<col style="width: 10%;">
|
||||
<col style="width: 15%;">
|
||||
<col v-if="discountPerItem === 'YES'" style="width: 15%;">
|
||||
<col style="width: 15%;">
|
||||
<col style="width: 40%" />
|
||||
<col style="width: 10%" />
|
||||
<col style="width: 15%" />
|
||||
<col v-if="discountPerItem === 'YES'" style="width: 15%" />
|
||||
<col style="width: 15%" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="">
|
||||
<div class="item-select-wrapper">
|
||||
<div class="sort-icon-wrapper handle">
|
||||
<font-awesome-icon
|
||||
class="sort-icon"
|
||||
icon="grip-vertical"
|
||||
/>
|
||||
<td class="px-5 py-4 text-left align-top">
|
||||
<div class="flex justify-start">
|
||||
<div
|
||||
class="flex items-center justify-center w-12 h-5 mt-2 text-gray-400 cursor-move handle"
|
||||
>
|
||||
<drag-icon />
|
||||
</div>
|
||||
<item-select
|
||||
ref="itemSelect"
|
||||
@@ -34,87 +33,94 @@
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<base-input
|
||||
<td class="px-5 py-4 text-right align-top">
|
||||
<sw-input
|
||||
v-model="item.quantity"
|
||||
:invalid="$v.item.quantity.$error"
|
||||
:is-input-group="!!item.unit_name"
|
||||
:input-group-text="item.unit_name"
|
||||
type="text"
|
||||
small
|
||||
@keyup="updateItem"
|
||||
@input="$v.item.quantity.$touch()"
|
||||
/>
|
||||
<div v-if="$v.item.quantity.$error">
|
||||
<span v-if="!$v.item.quantity.maxLength" class="text-danger">{{ $t('validation.quantity_maxlength') }}</span>
|
||||
<span v-if="!$v.item.quantity.maxLength" class="text-danger">
|
||||
{{ $t('validation.quantity_maxlength') }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-left">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="flex-fillbd-highlight">
|
||||
<div class="base-input">
|
||||
<money
|
||||
<td class="px-5 py-4 text-left align-top">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex-auto flex-fill bd-highlight">
|
||||
<div class="relative w-full">
|
||||
<sw-money
|
||||
v-model="price"
|
||||
v-bind="customerCurrency"
|
||||
class="input-field"
|
||||
:currency="customerCurrency"
|
||||
:invalid="$v.item.price.$error"
|
||||
@input="$v.item.price.$touch()"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="$v.item.price.$error">
|
||||
<span v-if="!$v.item.price.maxLength" class="text-danger">{{ $t('validation.price_maxlength') }}</span>
|
||||
<span v-if="!$v.item.price.maxLength" class="text-danger">
|
||||
{{ $t('validation.price_maxlength') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td v-if="discountPerItem === 'YES'" class="">
|
||||
<div class="d-flex flex-column bd-highlight">
|
||||
<div
|
||||
class="btn-group flex-fill bd-highlight"
|
||||
role="group"
|
||||
>
|
||||
<base-input
|
||||
<td
|
||||
v-if="discountPerItem === 'YES'"
|
||||
class="px-5 py-4 text-left align-top"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-auto" role="group">
|
||||
<sw-input
|
||||
v-model="discount"
|
||||
:invalid="$v.item.discount_val.$error"
|
||||
input-class="item-discount"
|
||||
class="border-r-0 rounded-tr-none rounded-br-none"
|
||||
@input="$v.item.discount_val.$touch()"
|
||||
/>
|
||||
<v-dropdown :show-arrow="false" theme-light>
|
||||
<button
|
||||
|
||||
<sw-dropdown>
|
||||
<sw-button
|
||||
slot="activator"
|
||||
type="button"
|
||||
class="btn item-dropdown dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
style="height: 43px; padding: 6px"
|
||||
variant="white"
|
||||
>
|
||||
{{ item.discount_type == 'fixed' ? currency.symbol : '%' }}
|
||||
</button>
|
||||
<v-dropdown-item>
|
||||
<a class="dropdown-item" href="#" @click.prevent="selectFixed" >
|
||||
{{ $t('general.fixed') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<a class="dropdown-item" href="#" @click.prevent="selectPercentage">
|
||||
{{ $t('general.percentage') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<span class="flex">
|
||||
{{
|
||||
item.discount_type == 'fixed' ? currency.symbol : '%'
|
||||
}}
|
||||
<chevron-down-icon class="h-5" />
|
||||
</span>
|
||||
</sw-button>
|
||||
|
||||
<sw-dropdown-item @click="selectFixed">
|
||||
{{ $t('general.fixed') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="selectPercentage">
|
||||
{{ $t('general.percentage') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</div>
|
||||
<!-- <div v-if="$v.item.discount.$error"> discount error </div> -->
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="item-amount">
|
||||
<td class="px-5 py-4 text-right align-top">
|
||||
<div class="flex items-center justify-end text-sm">
|
||||
<span>
|
||||
<div v-html="$utils.formatMoney(total, currency)" />
|
||||
</span>
|
||||
|
||||
<div class="remove-icon-wrapper">
|
||||
<font-awesome-icon
|
||||
<div
|
||||
class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer"
|
||||
>
|
||||
<trash-icon
|
||||
v-if="isShowRemoveItemIcon"
|
||||
class="remove-icon"
|
||||
icon="trash-alt"
|
||||
class="h-5 text-gray-700"
|
||||
@click="removeItem"
|
||||
/>
|
||||
</div>
|
||||
@@ -122,8 +128,8 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="taxPerItem === 'YES'" class="tax-tr">
|
||||
<td />
|
||||
<td colspan="4">
|
||||
<td class="px-5 py-4 text-left align-top" />
|
||||
<td colspan="4" class="px-5 py-4 text-left align-top">
|
||||
<tax
|
||||
v-for="(tax, index) in item.taxes"
|
||||
:key="tax.id"
|
||||
@@ -146,96 +152,100 @@
|
||||
</template>
|
||||
<script>
|
||||
import Guid from 'guid'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import { mapGetters } from 'vuex'
|
||||
import TaxStub from '../../stub/tax'
|
||||
import EstimateStub from '../../stub/estimate'
|
||||
import ItemSelect from './ItemSelect'
|
||||
import Tax from './Tax'
|
||||
const { required, minValue, between, maxLength } = require('vuelidate/lib/validators')
|
||||
import { TrashIcon, ChevronDownIcon } from '@vue-hero-icons/solid'
|
||||
import DragIcon from '@/components/icon/DragIcon'
|
||||
|
||||
const {
|
||||
required,
|
||||
minValue,
|
||||
between,
|
||||
maxLength,
|
||||
} = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tax,
|
||||
ItemSelect
|
||||
ItemSelect,
|
||||
TrashIcon,
|
||||
ChevronDownIcon,
|
||||
DragIcon,
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
|
||||
props: {
|
||||
itemData: {
|
||||
type: Object,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
taxPerItem: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
discountPerItem: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
estimateItems: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
isClosePopup: false,
|
||||
itemSelect: null,
|
||||
item: {...this.itemData},
|
||||
item: { ...this.itemData },
|
||||
maxDiscount: 0,
|
||||
money: {
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
prefix: '$ ',
|
||||
precision: 2,
|
||||
masked: false
|
||||
masked: false,
|
||||
},
|
||||
isSelected: false
|
||||
isSelected: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('item', [
|
||||
'items'
|
||||
]),
|
||||
...mapGetters('modal', [
|
||||
'modalActive'
|
||||
]),
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrencyForInput'
|
||||
]),
|
||||
customerCurrency () {
|
||||
...mapGetters('item', ['items']),
|
||||
...mapGetters('modal', ['modalActive']),
|
||||
...mapGetters('company', ['defaultCurrencyForInput']),
|
||||
customerCurrency() {
|
||||
if (this.currency) {
|
||||
return {
|
||||
decimal: this.currency.decimal_separator,
|
||||
thousands: this.currency.thousand_separator,
|
||||
prefix: this.currency.symbol + ' ',
|
||||
precision: this.currency.precision,
|
||||
masked: false
|
||||
masked: false,
|
||||
}
|
||||
} else {
|
||||
return this.defaultCurrencyForInput
|
||||
}
|
||||
},
|
||||
isShowRemoveItemIcon () {
|
||||
isShowRemoveItemIcon() {
|
||||
if (this.estimateItems.length == 1) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
subtotal () {
|
||||
subtotal() {
|
||||
return this.item.price * this.item.quantity
|
||||
},
|
||||
discount: {
|
||||
@@ -250,12 +260,12 @@ export default {
|
||||
}
|
||||
|
||||
this.item.discount = newValue
|
||||
}
|
||||
},
|
||||
},
|
||||
total () {
|
||||
total() {
|
||||
return this.subtotal - this.item.discount_val
|
||||
},
|
||||
totalSimpleTax () {
|
||||
totalSimpleTax() {
|
||||
return window._.sumBy(this.item.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
return tax.amount
|
||||
@@ -264,7 +274,7 @@ export default {
|
||||
return 0
|
||||
})
|
||||
},
|
||||
totalCompoundTax () {
|
||||
totalCompoundTax() {
|
||||
return window._.sumBy(this.item.taxes, function (tax) {
|
||||
if (tax.compound_tax) {
|
||||
return tax.amount
|
||||
@@ -273,7 +283,7 @@ export default {
|
||||
return 0
|
||||
})
|
||||
},
|
||||
totalTax () {
|
||||
totalTax() {
|
||||
return this.totalSimpleTax + this.totalCompoundTax
|
||||
},
|
||||
price: {
|
||||
@@ -291,51 +301,51 @@ export default {
|
||||
} else {
|
||||
this.item.price = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
item: {
|
||||
handler: 'updateItem',
|
||||
deep: true
|
||||
deep: true,
|
||||
},
|
||||
subtotal (newValue) {
|
||||
subtotal(newValue) {
|
||||
if (this.item.discount_type === 'percentage') {
|
||||
this.item.discount_val = (this.item.discount * newValue) / 100
|
||||
}
|
||||
},
|
||||
modalActive (val) {
|
||||
modalActive(val) {
|
||||
if (!val) {
|
||||
this.isSelected = false
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
validations () {
|
||||
validations() {
|
||||
return {
|
||||
item: {
|
||||
name: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
quantity: {
|
||||
required,
|
||||
minValue: minValue(1),
|
||||
maxLength: maxLength(20)
|
||||
minValue: minValue(0),
|
||||
maxLength: maxLength(20),
|
||||
},
|
||||
price: {
|
||||
required,
|
||||
minValue: minValue(1),
|
||||
maxLength: maxLength(20)
|
||||
maxLength: maxLength(20),
|
||||
},
|
||||
discount_val: {
|
||||
between: between(0, this.maxDiscount)
|
||||
between: between(0, this.maxDiscount),
|
||||
},
|
||||
description: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
}
|
||||
maxLength: maxLength(255),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
window.hub.$on('checkItems', this.validateItem)
|
||||
window.hub.$on('newItem', (val) => {
|
||||
if (this.taxPerItem === 'YES') {
|
||||
@@ -346,36 +356,43 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
mounted() {
|
||||
this.$v.item.$reset()
|
||||
},
|
||||
methods: {
|
||||
updateTax (data) {
|
||||
updateTax(data) {
|
||||
this.$set(this.item.taxes, data.index, data.item)
|
||||
|
||||
let lastTax = this.item.taxes[this.item.taxes.length - 1]
|
||||
|
||||
if (lastTax.tax_type_id !== 0) {
|
||||
this.item.taxes.push({...TaxStub, id: Guid.raw()})
|
||||
this.item.taxes.push({ ...TaxStub, id: Guid.raw() })
|
||||
}
|
||||
|
||||
this.updateItem()
|
||||
},
|
||||
removeTax (index) {
|
||||
removeTax(index) {
|
||||
this.item.taxes.splice(index, 1)
|
||||
|
||||
this.updateItem()
|
||||
},
|
||||
taxWithPercentage ({ name, percent }) {
|
||||
taxWithPercentage({ name, percent }) {
|
||||
return `${name} (${percent}%)`
|
||||
},
|
||||
searchVal (val) {
|
||||
searchVal(val) {
|
||||
this.item.name = val
|
||||
},
|
||||
deselectItem () {
|
||||
this.item = {...EstimateStub, id: this.item.id, taxes: [{...TaxStub, id: Guid.raw()}]}
|
||||
deselectItem() {
|
||||
this.item = {
|
||||
...EstimateStub,
|
||||
id: this.item.id,
|
||||
taxes: [{ ...TaxStub, id: Guid.raw() }],
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.$refs.itemSelect.$refs.baseSelect.$refs.search.focus()
|
||||
})
|
||||
},
|
||||
onSelectItem (item) {
|
||||
onSelectItem(item) {
|
||||
this.item.name = item.name
|
||||
this.item.price = item.price
|
||||
this.item.item_id = item.id
|
||||
@@ -383,16 +400,13 @@ export default {
|
||||
this.item.unit_name = item.unit_name
|
||||
if (this.taxPerItem === 'YES' && item.taxes) {
|
||||
let index = 0
|
||||
item.taxes.forEach(tax => {
|
||||
this.updateTax({index, item: { ...tax }})
|
||||
item.taxes.forEach((tax) => {
|
||||
this.updateTax({ index, item: { ...tax } })
|
||||
index++
|
||||
})
|
||||
}
|
||||
// if (this.item.taxes.length) {
|
||||
// this.item.taxes = {...item.taxes}
|
||||
// }
|
||||
},
|
||||
selectFixed () {
|
||||
selectFixed() {
|
||||
if (this.item.discount_type === 'fixed') {
|
||||
return
|
||||
}
|
||||
@@ -400,7 +414,7 @@ export default {
|
||||
this.item.discount_val = this.item.discount * 100
|
||||
this.item.discount_type = 'fixed'
|
||||
},
|
||||
selectPercentage () {
|
||||
selectPercentage() {
|
||||
if (this.item.discount_type === 'percentage') {
|
||||
return
|
||||
}
|
||||
@@ -409,24 +423,24 @@ export default {
|
||||
|
||||
this.item.discount_type = 'percentage'
|
||||
},
|
||||
updateItem () {
|
||||
updateItem() {
|
||||
this.$emit('update', {
|
||||
'index': this.index,
|
||||
'item': {
|
||||
index: this.index,
|
||||
item: {
|
||||
...this.item,
|
||||
total: this.total,
|
||||
totalSimpleTax: this.totalSimpleTax,
|
||||
totalCompoundTax: this.totalCompoundTax,
|
||||
totalTax: this.totalTax,
|
||||
tax: this.totalTax,
|
||||
taxes: [...this.item.taxes]
|
||||
}
|
||||
taxes: [...this.item.taxes],
|
||||
},
|
||||
})
|
||||
},
|
||||
removeItem () {
|
||||
removeItem() {
|
||||
this.$emit('remove', this.index)
|
||||
},
|
||||
validateItem () {
|
||||
validateItem() {
|
||||
this.$v.item.$touch()
|
||||
|
||||
if (this.item !== null) {
|
||||
@@ -434,7 +448,7 @@ export default {
|
||||
} else {
|
||||
this.$emit('itemValidate', this.index, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
<template>
|
||||
<div class="item-selector">
|
||||
<div v-if="item.item_id" class="selected-item">
|
||||
<div class="flex-1 text-sm">
|
||||
<div
|
||||
v-if="item.item_id"
|
||||
class="relative flex items-center h-10 pl-2 bg-gray-200 border border-gray-200 border-solid rounded"
|
||||
>
|
||||
{{ item.name }}
|
||||
|
||||
<span class="deselect-icon" @click="deselectItem">
|
||||
<font-awesome-icon icon="times-circle" />
|
||||
<span
|
||||
class="absolute text-gray-400 cursor-pointer"
|
||||
style="top: 8px; right: 10px"
|
||||
@click="deselectItem"
|
||||
>
|
||||
<x-circle-icon class="h-5" />
|
||||
</span>
|
||||
</div>
|
||||
<base-select
|
||||
<sw-select
|
||||
v-else
|
||||
ref="baseSelect"
|
||||
v-model="itemSelect"
|
||||
@@ -24,25 +31,34 @@
|
||||
@select="onSelect"
|
||||
>
|
||||
<div slot="afterList">
|
||||
<button type="button" class="list-add-button" @click="openItemModal">
|
||||
<font-awesome-icon class="icon" icon="cart-plus" />
|
||||
<label>{{ $t('general.add_new_item') }}</label>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center w-full p-3 bg-gray-200 border-none outline-none"
|
||||
@click="openItemModal"
|
||||
>
|
||||
<shopping-cart-icon class="h-5 text-primary-400" />
|
||||
<label class="ml-2 text-sm leading-none text-primary-400">{{
|
||||
$t('general.add_new_item')
|
||||
}}</label>
|
||||
</button>
|
||||
</div>
|
||||
</base-select>
|
||||
<div class="item-description">
|
||||
<base-text-area
|
||||
</sw-select>
|
||||
<div class="w-full pt-1 text-xs text-light">
|
||||
<sw-textarea
|
||||
v-autoresize
|
||||
v-model.trim="item.description"
|
||||
:invalid-description="invalidDescription"
|
||||
:placeholder="$t('estimates.item.type_item_description')"
|
||||
type="text"
|
||||
rows="1"
|
||||
class="description-input"
|
||||
variant="inv-desc"
|
||||
class="w-full text-gray-600 border-none resize-none"
|
||||
@input="$emit('onDesriptionInput')"
|
||||
/>
|
||||
<div v-if="invalidDescription">
|
||||
<span class="text-danger">{{ $tc('validation.description_maxlength') }}</span>
|
||||
<span class="text-danger">{{
|
||||
$tc('validation.description_maxlength')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,78 +66,79 @@
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import { XCircleIcon, ShoppingCartIcon } from '@vue-hero-icons/solid'
|
||||
const { maxLength } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
components: {
|
||||
XCircleIcon,
|
||||
ShoppingCartIcon,
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
invalid: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
invalidDescription: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
taxPerItem: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
itemSelect: null,
|
||||
loading: false
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
validations () {
|
||||
validations() {
|
||||
return {
|
||||
item: {
|
||||
description: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
}
|
||||
maxLength: maxLength(255),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('item', [
|
||||
'items'
|
||||
])
|
||||
...mapGetters('item', ['items']),
|
||||
},
|
||||
watch: {
|
||||
invalidDescription (newValue) {
|
||||
invalidDescription(newValue) {
|
||||
console.log(newValue)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
...mapActions('item', [
|
||||
'fetchItems'
|
||||
]),
|
||||
async searchItems (search) {
|
||||
...mapActions('modal', ['openModal']),
|
||||
...mapActions('item', ['fetchItems']),
|
||||
async searchItems(search) {
|
||||
let data = {
|
||||
search,
|
||||
filter: {
|
||||
name: search,
|
||||
unit: '',
|
||||
price: ''
|
||||
price: '',
|
||||
},
|
||||
orderByField: '',
|
||||
orderBy: '',
|
||||
page: 1
|
||||
page: 1,
|
||||
}
|
||||
|
||||
if (this.item) {
|
||||
data.item_id = this.item.item_id
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
@@ -130,27 +147,27 @@ export default {
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
onTextChange (val) {
|
||||
onTextChange(val) {
|
||||
this.searchItems(val)
|
||||
|
||||
this.$emit('search', val)
|
||||
},
|
||||
openItemModal () {
|
||||
openItemModal() {
|
||||
this.$emit('onSelectItem')
|
||||
this.openModal({
|
||||
'title': this.$t('items.add_item'),
|
||||
'componentName': 'ItemModal',
|
||||
'data': {taxPerItem: this.taxPerItem, taxes: this.taxes}
|
||||
title: this.$t('items.add_item'),
|
||||
componentName: 'ItemModal',
|
||||
data: { taxPerItem: this.taxPerItem, taxes: this.taxes },
|
||||
})
|
||||
},
|
||||
onSelect(val) {
|
||||
this.$emit('select', val)
|
||||
this.fetchItems()
|
||||
},
|
||||
deselectItem () {
|
||||
},
|
||||
deselectItem() {
|
||||
this.itemSelect = null
|
||||
this.$emit('deselect')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="tax-row">
|
||||
<div class="d-flex align-items-center tax-select">
|
||||
<label class="bd-highlight pr-2 mb-0" align="right">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center text-base" style="flex: 4">
|
||||
<label class="pr-2 mb-0" align="right">
|
||||
{{ $t('estimates.tax') }}
|
||||
</label>
|
||||
<base-select
|
||||
<sw-select
|
||||
v-model="selectedTax"
|
||||
:options="filteredTypes"
|
||||
:allow-empty="false"
|
||||
@@ -16,20 +16,27 @@
|
||||
@select="(val) => onSelectTax(val)"
|
||||
>
|
||||
<div slot="afterList">
|
||||
<button type="button" class="list-add-button" @click="openTaxModal">
|
||||
<font-awesome-icon class="icon" icon="check-circle" />
|
||||
<label>{{ $t('estimates.add_new_tax') }}</label>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center w-full px-2 py-2 bg-gray-200 border-none outline-none"
|
||||
@click="openTaxModal"
|
||||
>
|
||||
<check-circle-icon class="h-5 text-primary-400" />
|
||||
<label class="ml-2 text-sm leading-none text-primary-400">{{
|
||||
$t('estimates.add_new_tax')
|
||||
}}</label>
|
||||
</button>
|
||||
</div>
|
||||
</base-select> <br>
|
||||
</sw-select>
|
||||
<br />
|
||||
</div>
|
||||
<div class="text-right tax-amount">
|
||||
<div class="text-sm text-right" style="flex: 3">
|
||||
<div v-html="$utils.formatMoney(taxAmount, currency)" />
|
||||
</div>
|
||||
<div class="remove-icon-wrapper">
|
||||
<font-awesome-icon
|
||||
<div class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer">
|
||||
<trash-icon
|
||||
v-if="taxes.length && index !== taxes.length - 1"
|
||||
class="remove-icon"
|
||||
class="h-5 text-gray-700"
|
||||
icon="trash-alt"
|
||||
@click="removeTax"
|
||||
/>
|
||||
@@ -39,49 +46,52 @@
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { CheckCircleIcon, TrashIcon } from '@vue-hero-icons/solid'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CheckCircleIcon,
|
||||
TrashIcon,
|
||||
},
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
taxData: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
default: []
|
||||
default: [],
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0,
|
||||
},
|
||||
totalTax: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
tax: {...this.taxData},
|
||||
selectedTax: null
|
||||
tax: { ...this.taxData },
|
||||
selectedTax: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('taxType', [
|
||||
'taxTypes'
|
||||
]),
|
||||
filteredTypes () {
|
||||
const clonedTypes = this.taxTypes.map(a => ({...a}))
|
||||
...mapGetters('taxType', ['taxTypes']),
|
||||
filteredTypes() {
|
||||
const clonedTypes = this.taxTypes.map((a) => ({ ...a }))
|
||||
|
||||
return clonedTypes.map((taxType) => {
|
||||
let found = this.taxes.find(tax => tax.tax_type_id === taxType.id)
|
||||
let found = this.taxes.find((tax) => tax.tax_type_id === taxType.id)
|
||||
|
||||
if (found) {
|
||||
taxType.$isDisabled = true
|
||||
@@ -92,7 +102,7 @@ export default {
|
||||
return taxType
|
||||
})
|
||||
},
|
||||
taxAmount () {
|
||||
taxAmount() {
|
||||
if (this.tax.compound_tax && this.total) {
|
||||
return ((this.total + this.totalTax) * this.tax.percent) / 100
|
||||
}
|
||||
@@ -102,19 +112,21 @@ export default {
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
total: {
|
||||
handler: 'updateTax'
|
||||
handler: 'updateTax',
|
||||
},
|
||||
totalTax: {
|
||||
handler: 'updateTax'
|
||||
}
|
||||
handler: 'updateTax',
|
||||
},
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
if (this.taxData.tax_type_id > 0) {
|
||||
this.selectedTax = this.taxTypes.find(_type => _type.id === this.taxData.tax_type_id)
|
||||
this.selectedTax = this.taxTypes.find(
|
||||
(_type) => _type.id === this.taxData.tax_type_id
|
||||
)
|
||||
}
|
||||
|
||||
window.hub.$on('newTax', (val) => {
|
||||
@@ -127,13 +139,11 @@ export default {
|
||||
this.updateTax()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
customLabel ({ name, percent }) {
|
||||
...mapActions('modal', ['openModal']),
|
||||
customLabel({ name, percent }) {
|
||||
return `${name} - ${percent}%`
|
||||
},
|
||||
onSelectTax (val) {
|
||||
onSelectTax(val) {
|
||||
this.tax.percent = val.percent
|
||||
this.tax.tax_type_id = val.id
|
||||
this.tax.compound_tax = val.compound_tax
|
||||
@@ -141,28 +151,28 @@ export default {
|
||||
|
||||
this.updateTax()
|
||||
},
|
||||
updateTax () {
|
||||
updateTax() {
|
||||
if (this.tax.tax_type_id === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$emit('update', {
|
||||
'index': this.index,
|
||||
'item': {
|
||||
index: this.index,
|
||||
item: {
|
||||
...this.tax,
|
||||
amount: this.taxAmount
|
||||
}
|
||||
amount: this.taxAmount,
|
||||
},
|
||||
})
|
||||
},
|
||||
removeTax () {
|
||||
removeTax() {
|
||||
this.$emit('remove', this.index, this.tax)
|
||||
},
|
||||
openTaxModal () {
|
||||
openTaxModal() {
|
||||
this.openModal({
|
||||
'title': this.$t('settings.tax_types.add_tax'),
|
||||
'componentName': 'TaxTypeModal'
|
||||
title: this.$t('settings.tax_types.add_tax'),
|
||||
componentName: 'TaxTypeModal',
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,202 +1,244 @@
|
||||
<template>
|
||||
<div v-if="estimate" class="main-content estimate-view-page">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ estimate.estimate_number }}</h3>
|
||||
<div class="page-actions row">
|
||||
<div class="col-xs-2 mr-3">
|
||||
<base-button
|
||||
<base-page v-if="estimate" class="xl:pl-96">
|
||||
<sw-page-header :title="pageTitle">
|
||||
<template slot="actions">
|
||||
<div class="mr-3 text-sm">
|
||||
<sw-button
|
||||
v-if="estimate.status === 'DRAFT'"
|
||||
:loading="isMarkAsSent"
|
||||
:disabled="isMarkAsSent"
|
||||
:outline="true"
|
||||
color="theme"
|
||||
variant="primary-outline"
|
||||
@click="onMarkAsSent"
|
||||
>
|
||||
{{ $t('estimates.mark_as_sent') }}
|
||||
</base-button>
|
||||
</sw-button>
|
||||
</div>
|
||||
<div class="col-xs-2">
|
||||
<base-button
|
||||
v-if="estimate.status === 'DRAFT'"
|
||||
:loading="isSendingEmail"
|
||||
:disabled="isSendingEmail"
|
||||
color="theme"
|
||||
@click="onSendEstimate"
|
||||
>
|
||||
{{ $t('estimates.send_estimate') }}
|
||||
</base-button>
|
||||
</div>
|
||||
<v-dropdown
|
||||
:close-on-select="true"
|
||||
align="left"
|
||||
class="filter-container"
|
||||
<sw-button
|
||||
v-if="estimate.status === 'DRAFT'"
|
||||
:disabled="isSendingEmail"
|
||||
variant="primary"
|
||||
class="text-sm"
|
||||
@click="onSendEstimate"
|
||||
>
|
||||
<a slot="activator" href="#">
|
||||
<base-button color="theme">
|
||||
<font-awesome-icon icon="ellipsis-h" />
|
||||
</base-button>
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="copyPdfUrl()">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'link']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</div>
|
||||
<router-link
|
||||
:to="{ path: `/admin/estimates/${$route.params.id}/edit` }"
|
||||
class="dropdown-item"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'pencil-alt']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</router-link>
|
||||
<div
|
||||
class="dropdown-item"
|
||||
@click="removeEstimate($route.params.id)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'trash']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="estimate-sidebar">
|
||||
<base-loader v-if="isSearching" />
|
||||
<div v-else class="side-header">
|
||||
<base-input
|
||||
{{ $t('estimates.send_estimate') }}
|
||||
</sw-button>
|
||||
<sw-dropdown class="ml-3">
|
||||
<sw-button slot="activator" variant="primary">
|
||||
<dots-horizontal-icon class="h-5" />
|
||||
</sw-button>
|
||||
|
||||
<sw-dropdown-item @click="copyPdfUrl">
|
||||
<link-icon class="h-5 mr-3 text-primary-800" />
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`/admin/estimates/${$route.params.id}/edit`"
|
||||
>
|
||||
<pencil-icon class="h-5 mr-3 text-primary-800" />
|
||||
{{ $t('general.edit') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="removeEstimate($route.params.id)">
|
||||
<trash-icon class="h-5 mr-3 text-primary-800" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</template>
|
||||
</sw-page-header>
|
||||
|
||||
<!-- sidebar -->
|
||||
<div
|
||||
class="fixed top-0 left-0 hidden h-full pt-16 pb-4 ml-56 bg-white xl:ml-64 w-88 xl:block"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between px-4 pt-8 pb-2 border border-gray-200 border-solid height-full"
|
||||
>
|
||||
<sw-input
|
||||
v-model="searchData.searchText"
|
||||
:placeholder="$t('general.search')"
|
||||
input-class="inv-search"
|
||||
icon="search"
|
||||
class="mb-6"
|
||||
type="text"
|
||||
align-icon="right"
|
||||
variant="gray"
|
||||
@input="onSearched()"
|
||||
/>
|
||||
<div class="btn-group ml-3" role="group" aria-label="First group">
|
||||
<v-dropdown
|
||||
:close-on-select="false"
|
||||
align="left"
|
||||
class="filter-container"
|
||||
>
|
||||
<a slot="activator" href="#">
|
||||
<base-button
|
||||
class="inv-button inv-filter-fields-btn"
|
||||
color="default"
|
||||
size="medium"
|
||||
>
|
||||
<font-awesome-icon icon="filter" />
|
||||
</base-button>
|
||||
</a>
|
||||
<div class="filter-title">
|
||||
>
|
||||
<search-icon slot="rightIcon" class="h-5" />
|
||||
</sw-input>
|
||||
|
||||
<div class="flex mb-6 ml-3" role="group" aria-label="First group">
|
||||
<sw-dropdown class="ml-3" position="bottom-start">
|
||||
<sw-button slot="activator" size="md" variant="gray-light">
|
||||
<filter-icon class="h-5" />
|
||||
</sw-button>
|
||||
|
||||
<div
|
||||
class="px-2 py-1 pb-2 mb-1 mb-2 text-sm border-b border-gray-200 border-solid"
|
||||
>
|
||||
{{ $t('general.sort_by') }}
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
id="filter_estimate_date"
|
||||
v-model="searchData.orderByField"
|
||||
type="radio"
|
||||
name="filter"
|
||||
class="inv-radio"
|
||||
value="estimate_date"
|
||||
@change="onSearched"
|
||||
/>
|
||||
<label class="inv-label" for="filter_estimate_date">{{
|
||||
$t('reports.estimates.estimate_date')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
id="filter_due_date"
|
||||
v-model="searchData.orderByField"
|
||||
type="radio"
|
||||
name="filter"
|
||||
class="inv-radio"
|
||||
value="expiry_date"
|
||||
@change="onSearched"
|
||||
/>
|
||||
<label class="inv-label" for="filter_due_date">{{
|
||||
$t('estimates.due_date')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
id="filter_estimate_number"
|
||||
v-model="searchData.orderByField"
|
||||
type="radio"
|
||||
name="filter"
|
||||
class="inv-radio"
|
||||
value="estimate_number"
|
||||
@change="onSearched"
|
||||
/>
|
||||
<label class="inv-label" for="filter_estimate_number">{{
|
||||
$t('estimates.estimate_number')
|
||||
}}</label>
|
||||
</div>
|
||||
</v-dropdown>
|
||||
<base-button
|
||||
|
||||
<sw-dropdown-item class="flex px-1 py-2 cursor-pointer">
|
||||
<sw-input-group class="-mt-3 font-normal">
|
||||
<sw-radio
|
||||
id="filter_estimate_date"
|
||||
v-model="searchData.orderByField"
|
||||
:label="$t('reports.estimates.estimate_date')"
|
||||
size="sm"
|
||||
name="filter"
|
||||
value="estimate_date"
|
||||
@change="onSearched"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item class="flex px-1 py-2 cursor-pointer">
|
||||
<sw-input-group class="-mt-3 font-normal">
|
||||
<sw-radio
|
||||
id="filter_due_date"
|
||||
v-model="searchData.orderByField"
|
||||
value="expiry_date"
|
||||
:label="$t('estimates.due_date')"
|
||||
size="sm"
|
||||
name="filter"
|
||||
@change="onSearched"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item class="flex px-1 py-2 cursor-pointer">
|
||||
<sw-input-group class="-mt-3 font-normal">
|
||||
<sw-radio
|
||||
id="filter_estimate_number"
|
||||
v-model="searchData.orderByField"
|
||||
value="estimate_number"
|
||||
:label="$t('estimates.estimate_number')"
|
||||
size="sm"
|
||||
name="filter"
|
||||
@change="onSearched"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
|
||||
<sw-button
|
||||
class="ml-1"
|
||||
v-tooltip.top-center="{ content: getOrderName }"
|
||||
class="inv-button inv-filter-sorting-btn"
|
||||
color="default"
|
||||
size="medium"
|
||||
size="md"
|
||||
variant="gray-light"
|
||||
@click="sortData"
|
||||
>
|
||||
<font-awesome-icon v-if="getOrderBy" icon="sort-amount-up" />
|
||||
<font-awesome-icon v-else icon="sort-amount-down" />
|
||||
</base-button>
|
||||
<sort-ascending-icon v-if="getOrderBy" class="h-5" />
|
||||
<sort-descending-icon v-else class="h-5" />
|
||||
</sw-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-content">
|
||||
|
||||
<base-loader v-if="isSearching" :show-bg-overlay="true" />
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="h-full pb-32 overflow-y-scroll border-l border-gray-200 border-solid sw-scroll"
|
||||
>
|
||||
<router-link
|
||||
v-for="(estimate, index) in estimates"
|
||||
:to="`/admin/estimates/${estimate.id}/view`"
|
||||
:id="'estimate-' + estimate.id"
|
||||
:key="index"
|
||||
class="side-estimate"
|
||||
:class="[
|
||||
'flex justify-between side-estimate p-4 cursor-pointer hover:bg-gray-100 items-center border-l-4 border-transparent',
|
||||
{
|
||||
'bg-gray-100 border-l-4 border-primary-500 border-solid': hasActiveUrl(
|
||||
estimate.id
|
||||
),
|
||||
},
|
||||
]"
|
||||
style="border-bottom: 1px solid rgba(185, 193, 209, 0.41)"
|
||||
>
|
||||
<div class="left">
|
||||
<div class="inv-name">{{ estimate.user.name }}</div>
|
||||
<div class="inv-number">{{ estimate.estimate_number }}</div>
|
||||
<div class="flex-2">
|
||||
<div
|
||||
:class="'est-status-' + estimate.status.toLowerCase()"
|
||||
class="inv-status"
|
||||
class="pr-2 mb-2 text-sm not-italic font-normal leading-5 text-black capitalize truncate"
|
||||
>
|
||||
{{ estimate.user.name }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-1 mb-2 text-xs not-italic font-medium leading-5 text-gray-600"
|
||||
>
|
||||
{{ estimate.estimate_number }}
|
||||
</div>
|
||||
|
||||
<sw-badge
|
||||
class="px-1 text-xs"
|
||||
:bg-color="$utils.getBadgeStatusColor(estimate.status).bgColor"
|
||||
:color="$utils.getBadgeStatusColor(estimate.status).color"
|
||||
>
|
||||
{{ estimate.status }}
|
||||
</div>
|
||||
</sw-badge>
|
||||
</div>
|
||||
<div class="right">
|
||||
|
||||
<div class="flex-1 whitespace-no-wrap right">
|
||||
<div
|
||||
class="inv-amount"
|
||||
class="mb-2 text-xl not-italic font-semibold leading-8 text-right text-gray-900"
|
||||
v-html="
|
||||
$utils.formatMoney(estimate.total, estimate.user.currency)
|
||||
"
|
||||
/>
|
||||
<div class="inv-date">{{ estimate.formattedEstimateDate }}</div>
|
||||
|
||||
<div
|
||||
class="text-sm not-italic font-normal leading-5 text-right text-gray-600 est-date"
|
||||
>
|
||||
{{ estimate.formattedEstimateDate }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<p v-if="!estimates.length" class="no-result">
|
||||
|
||||
<p
|
||||
v-if="!estimates.length"
|
||||
class="flex justify-center px-4 mt-5 text-sm text-gray-600"
|
||||
>
|
||||
{{ $t('estimates.no_matching_estimates') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="estimate-view-page-container">
|
||||
<iframe :src="`${shareableLink}`" class="frame-style" />
|
||||
|
||||
<div
|
||||
class="flex flex-col min-h-0 mt-8 overflow-hidden sw-scroll"
|
||||
style="height: 75vh"
|
||||
>
|
||||
<iframe
|
||||
:src="`${shareableLink}`"
|
||||
class="flex-1 border border-gray-400 border-solid rounded-md frame-style"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</base-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
import {
|
||||
DotsHorizontalIcon,
|
||||
FilterIcon,
|
||||
SortAscendingIcon,
|
||||
SortDescendingIcon,
|
||||
SearchIcon,
|
||||
LinkIcon,
|
||||
TrashIcon,
|
||||
PencilIcon,
|
||||
} from '@vue-hero-icons/solid'
|
||||
const _ = require('lodash')
|
||||
|
||||
export default {
|
||||
data () {
|
||||
components: {
|
||||
DotsHorizontalIcon,
|
||||
FilterIcon,
|
||||
SortAscendingIcon,
|
||||
SortDescendingIcon,
|
||||
SearchIcon,
|
||||
LinkIcon,
|
||||
TrashIcon,
|
||||
PencilIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
count: null,
|
||||
@@ -206,38 +248,50 @@ export default {
|
||||
searchData: {
|
||||
orderBy: null,
|
||||
orderByField: null,
|
||||
searchText: null
|
||||
searchText: null,
|
||||
},
|
||||
status: ['DRAFT', 'SENT', 'VIEWED', 'EXPIRED', 'ACCEPTED', 'REJECTED'],
|
||||
isMarkAsSent: false,
|
||||
isSendingEmail: false,
|
||||
isRequestOnGoing: false,
|
||||
isSearching: false
|
||||
isSearching: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getOrderBy () {
|
||||
if (this.searchData.orderBy === 'asc' || this.searchData.orderBy == null) {
|
||||
pageTitle() {
|
||||
return this.estimate.estimate_number
|
||||
},
|
||||
getOrderBy() {
|
||||
if (
|
||||
this.searchData.orderBy === 'asc' ||
|
||||
this.searchData.orderBy == null
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
getOrderName () {
|
||||
getOrderName() {
|
||||
if (this.getOrderBy) {
|
||||
return this.$t('general.ascending')
|
||||
}
|
||||
return this.$t('general.descending')
|
||||
},
|
||||
shareableLink () {
|
||||
shareableLink() {
|
||||
return `/estimates/pdf/${this.estimate.unique_hash}`
|
||||
}
|
||||
},
|
||||
getCurrentEstimateId() {
|
||||
if (this.estimate && this.estimate.id) {
|
||||
return this.estimate.id
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route (to, from) {
|
||||
$route(to, from) {
|
||||
this.loadEstimate()
|
||||
}
|
||||
},
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
this.loadEstimates()
|
||||
this.loadEstimate()
|
||||
this.onSearched = _.debounce(this.onSearched, 500)
|
||||
@@ -251,32 +305,67 @@ export default {
|
||||
'sendEmail',
|
||||
'deleteEstimate',
|
||||
'selectEstimate',
|
||||
'fetchViewEstimate'
|
||||
'fetchViewEstimate',
|
||||
]),
|
||||
async loadEstimates () {
|
||||
let response = await this.fetchEstimates()
|
||||
|
||||
...mapActions('modal', ['openModal']),
|
||||
|
||||
hasActiveUrl(id) {
|
||||
return this.$route.params.id == id
|
||||
},
|
||||
|
||||
async loadEstimates() {
|
||||
let response = await this.fetchEstimates({ limit: 'all' })
|
||||
if (response.data) {
|
||||
this.estimates = response.data.estimates.data
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.scrollToEstimate()
|
||||
}, 500)
|
||||
},
|
||||
async loadEstimate () {
|
||||
scrollToEstimate() {
|
||||
const el = document.getElementById(`estimate-${this.$route.params.id}`)
|
||||
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth' })
|
||||
el.classList.add('shake')
|
||||
}
|
||||
},
|
||||
async loadEstimate() {
|
||||
let response = await this.fetchViewEstimate(this.$route.params.id)
|
||||
|
||||
if (response.data) {
|
||||
this.estimate = response.data.estimate
|
||||
this.estimate = { ...response.data.estimate }
|
||||
}
|
||||
},
|
||||
async onSearched () {
|
||||
copyPdfUrl() {
|
||||
let pdfUrl = `${window.location.origin}/estimates/pdf/${this.estimate.unique_hash}`
|
||||
|
||||
let response = this.$utils.copyTextToClipboard(pdfUrl)
|
||||
|
||||
window.toastr['success'](this.$tc('general.copied_pdf_url_clipboard'))
|
||||
},
|
||||
async onSearched() {
|
||||
let data = ''
|
||||
if (this.searchData.searchText !== '' && this.searchData.searchText !== null && this.searchData.searchText !== undefined) {
|
||||
if (
|
||||
this.searchData.searchText !== '' &&
|
||||
this.searchData.searchText !== null &&
|
||||
this.searchData.searchText !== undefined
|
||||
) {
|
||||
data += `search=${this.searchData.searchText}&`
|
||||
}
|
||||
|
||||
if (this.searchData.orderBy !== null && this.searchData.orderBy !== undefined) {
|
||||
if (
|
||||
this.searchData.orderBy !== null &&
|
||||
this.searchData.orderBy !== undefined
|
||||
) {
|
||||
data += `orderBy=${this.searchData.orderBy}&`
|
||||
}
|
||||
|
||||
if (this.searchData.orderByField !== null && this.searchData.orderByField !== undefined) {
|
||||
if (
|
||||
this.searchData.orderByField !== null &&
|
||||
this.searchData.orderByField !== undefined
|
||||
) {
|
||||
data += `orderByField=${this.searchData.orderByField}`
|
||||
}
|
||||
this.isSearching = true
|
||||
@@ -286,7 +375,7 @@ export default {
|
||||
this.estimates = response.data.estimates.data
|
||||
}
|
||||
},
|
||||
sortData () {
|
||||
sortData() {
|
||||
if (this.searchData.orderBy === 'asc') {
|
||||
this.searchData.orderBy = 'desc'
|
||||
this.onSearched()
|
||||
@@ -296,74 +385,68 @@ export default {
|
||||
this.onSearched()
|
||||
return true
|
||||
},
|
||||
async onMarkAsSent () {
|
||||
window.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_mark_as_sent'),
|
||||
icon: '/assets/icon/check-circle-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (value) => {
|
||||
if (value) {
|
||||
this.isMarkAsSent = true
|
||||
let response = await this.markAsSent({id: this.estimate.id})
|
||||
this.isMarkAsSent = false
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('estimates.mark_as_sent_successfully'))
|
||||
async onMarkAsSent() {
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_mark_as_sent'),
|
||||
icon: '/assets/icon/check-circle-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (value) => {
|
||||
if (value) {
|
||||
this.isMarkAsSent = true
|
||||
let response = await this.markAsSent({
|
||||
id: this.estimate.id,
|
||||
status: 'SENT',
|
||||
})
|
||||
this.isMarkAsSent = false
|
||||
if (response.data) {
|
||||
this.estimate.status = 'SENT'
|
||||
window.toastr['success'](
|
||||
this.$tc('estimates.mark_as_sent_successfully')
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async onSendEstimate(id) {
|
||||
this.openModal({
|
||||
title: this.$t('estimates.send_estimate'),
|
||||
componentName: 'SendEstimateModal',
|
||||
id: this.estimate.id,
|
||||
data: this.estimate,
|
||||
})
|
||||
},
|
||||
async onSendEstimate (id) {
|
||||
window.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_send_estimate'),
|
||||
icon: '/assets/icon/paper-plane-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (value) => {
|
||||
if (value) {
|
||||
this.isSendingEmail = true
|
||||
let response = await this.sendEmail({id: this.estimate.id})
|
||||
this.isSendingEmail = false
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](this.$tc('estimates.send_estimate_successfully'))
|
||||
return true
|
||||
}
|
||||
if (response.data.error === 'user_email_does_not_exist') {
|
||||
window.toastr['error'](this.$tc('estimates.user_email_does_not_exist'))
|
||||
return true
|
||||
}
|
||||
window.toastr['error'](this.$tc('estimates.something_went_wrong'))
|
||||
}
|
||||
})
|
||||
},
|
||||
copyPdfUrl () {
|
||||
copyPdfUrl() {
|
||||
let pdfUrl = `${window.location.origin}/estimates/pdf/${this.estimate.unique_hash}`
|
||||
|
||||
let response = this.$utils.copyTextToClipboard(pdfUrl)
|
||||
|
||||
window.toastr['success'](this.$tc('Copied PDF url to clipboard!'))
|
||||
window.toastr['success'](this.$tc('general.copied_pdf_url_clipboard'))
|
||||
},
|
||||
async removeEstimate (id) {
|
||||
window.swal({
|
||||
title: 'Deleted',
|
||||
text: 'you will not be able to recover this estimate!',
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (value) => {
|
||||
if (value) {
|
||||
let request = await this.deleteEstimate(id)
|
||||
if (request.data.success) {
|
||||
window.toastr['success'](this.$tc('estimates.deleted_message', 1))
|
||||
this.$router.push('/admin/estimates')
|
||||
} else if (request.data.error) {
|
||||
window.toastr['error'](request.data.message)
|
||||
async removeEstimate(id) {
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: 'you will not be able to recover this estimate!',
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (value) => {
|
||||
if (value) {
|
||||
let request = await this.deleteEstimate({ ids: [id] })
|
||||
if (request.data.success) {
|
||||
window.toastr['success'](this.$tc('estimates.deleted_message', 1))
|
||||
this.$router.push('/admin/estimates')
|
||||
} else if (request.data.error) {
|
||||
window.toastr['error'](request.data.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,307 +1,437 @@
|
||||
<template>
|
||||
<div class="main-content expenses">
|
||||
<base-page class="relative">
|
||||
<form action="" @submit.prevent="sendData">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ isEdit ? $t('expenses.edit_expense') : $t('expenses.new_expense') }}</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/expenses">{{ $tc('expenses.expense', 2) }}</router-link></li>
|
||||
<li class="breadcrumb-item"><a href="#">{{ isEdit ? $t('expenses.edit_expense') : $t('expenses.new_expense') }}</a></li>
|
||||
</ol>
|
||||
<div class="page-actions row header-button-container">
|
||||
<div v-if="isReceiptAvailable" class="col-xs-2 mr-4">
|
||||
<a :href="getReceiptUrl">
|
||||
<base-button
|
||||
:loading="isLoading"
|
||||
icon="download"
|
||||
color="theme"
|
||||
outline
|
||||
>
|
||||
{{ $t('expenses.download_receipt') }}
|
||||
</base-button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-2">
|
||||
<base-button
|
||||
<!-- Page Header -->
|
||||
<sw-page-header class="mb-5" :title="pageTitle">
|
||||
<sw-breadcrumb slot="breadcrumbs">
|
||||
<sw-breadcrumb-item
|
||||
to="/admin/dashboard"
|
||||
:title="$t('general.home')"
|
||||
/>
|
||||
|
||||
<sw-breadcrumb-item
|
||||
to="/admin/expenses"
|
||||
:title="$tc('expenses.expense', 2)"
|
||||
/>
|
||||
|
||||
<sw-breadcrumb-item
|
||||
v-if="$route.name === 'expenses.edit'"
|
||||
to="#"
|
||||
:title="$t('expenses.edit_expense')"
|
||||
active
|
||||
/>
|
||||
|
||||
<sw-breadcrumb-item
|
||||
v-else
|
||||
to="#"
|
||||
:title="$t('expenses.new_expense')"
|
||||
active
|
||||
/>
|
||||
</sw-breadcrumb>
|
||||
|
||||
<template slot="actions">
|
||||
<sw-button
|
||||
v-if="isReceiptAvailable"
|
||||
tag-name="a"
|
||||
:href="getReceiptUrl"
|
||||
variant="primary"
|
||||
outline
|
||||
size="lg"
|
||||
class="mr-2"
|
||||
>
|
||||
<download-icon class="h-5 mr-2 -ml-1" />
|
||||
{{ $t('expenses.download_receipt') }}
|
||||
</sw-button>
|
||||
|
||||
<div class="hidden md:block">
|
||||
<sw-button
|
||||
:loading="isLoading"
|
||||
icon="save"
|
||||
color="theme"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
size="lg"
|
||||
>
|
||||
{{ isEdit ? $t('expenses.update_expense') : $t('expenses.save_expense') }}
|
||||
</base-button>
|
||||
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
|
||||
{{
|
||||
isEdit
|
||||
? $t('expenses.update_expense')
|
||||
: $t('expenses.save_expense')
|
||||
}}
|
||||
</sw-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<!-- <div class="form-group col-sm-6">
|
||||
<label class="control-label">{{ $t('expenses.expense_title') }}</label>
|
||||
<input v-model="formData.title" type="text" name="name" class="form-control">
|
||||
</div> -->
|
||||
<div class="form-group col-sm-6">
|
||||
<label class="control-label">{{ $t('expenses.category') }}</label><span class="text-danger"> * </span>
|
||||
<base-select
|
||||
ref="baseSelect"
|
||||
v-model="category"
|
||||
:options="categories"
|
||||
:invalid="$v.category.$error"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('expenses.categories.select_a_category')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
@input="$v.category.$touch()"
|
||||
</template>
|
||||
</sw-page-header>
|
||||
|
||||
<base-loader v-if="isRequestOnGoing" :show-bg-overlay="true" />
|
||||
|
||||
<sw-card v-else>
|
||||
<div class="grid gap-6 grid-col-1 md:grid-cols-2">
|
||||
<sw-input-group
|
||||
:label="$t('expenses.category')"
|
||||
:error="categoryError"
|
||||
required
|
||||
>
|
||||
<sw-select
|
||||
ref="baseSelect"
|
||||
v-model="category"
|
||||
:options="categories"
|
||||
:invalid="$v.category.$error"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('expenses.categories.select_a_category')"
|
||||
class="mt-2"
|
||||
label="name"
|
||||
track-by="id"
|
||||
@input="$v.category.$touch()"
|
||||
>
|
||||
<sw-button
|
||||
slot="afterList"
|
||||
type="button"
|
||||
variant="gray-light"
|
||||
class="flex items-center justify-center w-full px-4 py-3 bg-gray-200 border-none outline-none"
|
||||
@click="openCategoryModal"
|
||||
>
|
||||
<shopping-cart-icon class="h-5 text-center text-primary-400" />
|
||||
<label class="ml-2 text-xs leading-none text-primary-400">{{
|
||||
$t('settings.expense_category.add_new_category')
|
||||
}}</label>
|
||||
</sw-button>
|
||||
</sw-select>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('expenses.expense_date')"
|
||||
:error="dateError"
|
||||
required
|
||||
>
|
||||
<base-date-picker
|
||||
v-model="formData.expense_date"
|
||||
:invalid="$v.formData.expense_date.$error"
|
||||
:calendar-button="true"
|
||||
class="mt-2"
|
||||
calendar-button-icon="calendar"
|
||||
@change="$v.formData.expense_date.$touch()"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('expenses.amount')"
|
||||
:error="amountError"
|
||||
required
|
||||
>
|
||||
<sw-money
|
||||
v-model="amount"
|
||||
:currency="defaultCurrencyForInput"
|
||||
class="focus:border focus:border-solid focus:border-primary-500"
|
||||
:invalid="$v.formData.amount.$error"
|
||||
@input="$v.formData.amount.$touch()"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group :label="$t('expenses.customer')">
|
||||
<sw-select
|
||||
ref="baseSelect"
|
||||
v-model="customer"
|
||||
:options="customers"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('customers.select_a_customer')"
|
||||
class="mt-1"
|
||||
label="name"
|
||||
track-by="id"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group :label="$t('expenses.note')" :error="notesError">
|
||||
<sw-textarea
|
||||
v-model="formData.notes"
|
||||
rows="4"
|
||||
@input="$v.formData.notes.$touch()"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group :label="$t('expenses.receipt')">
|
||||
<div
|
||||
id="receipt-box"
|
||||
class="relative flex items-center justify-center h-24 p-6 bg-transparent border-2 border-gray-200 border-dashed rounded-md image-upload-box"
|
||||
>
|
||||
<img
|
||||
v-if="previewReceipt"
|
||||
:src="previewReceipt"
|
||||
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
|
||||
>
|
||||
<div slot="afterList">
|
||||
<button type="button" class="list-add-button" @click="openCategoryModal">
|
||||
<font-awesome-icon class="icon" icon="cart-plus" />
|
||||
<label>{{ $t('settings.expense_category.add_new_category') }}</label>
|
||||
</button>
|
||||
</div>
|
||||
</base-select>
|
||||
<div v-if="$v.category.$error">
|
||||
<span v-if="!$v.category.required" class="text-danger">{{ $t('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="form-group col-sm-6">
|
||||
<label>{{ $t('expenses.contact') }}</label>
|
||||
<select v-model="formData.contact" name="contact" class="form-control ls-select2">
|
||||
<option v-for="(contact, index) in contacts" :key="index" :value="contact.id"> {{ contact.name }}</option>
|
||||
</select>
|
||||
</div> -->
|
||||
<div class="form-group col-sm-6">
|
||||
<label>{{ $t('expenses.expense_date') }}</label><span class="text-danger"> * </span>
|
||||
<base-date-picker
|
||||
v-model="formData.expense_date"
|
||||
:invalid="$v.formData.expense_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@change="$v.formData.expense_date.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.expense_date.$error">
|
||||
<span v-if="!$v.formData.expense_date.required" class="text-danger">{{ $t('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-sm-6">
|
||||
<label>{{ $t('expenses.amount') }}</label> <span class="text-danger"> * </span>
|
||||
<div class="base-input">
|
||||
<money
|
||||
:class="{'invalid' : $v.formData.amount.$error}"
|
||||
v-model="amount"
|
||||
v-bind="defaultCurrencyForInput"
|
||||
class="input-field"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="$v.formData.amount.$error">
|
||||
<span v-if="!$v.formData.amount.required" class="text-danger">{{ $t('validation.required') }} </span>
|
||||
<span v-if="!$v.formData.amount.maxLength" class="text-danger">{{ $t('validation.price_maxlength') }}</span>
|
||||
<span v-if="!$v.formData.amount.minValue" class="text-danger">{{ $t('validation.price_minvalue') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-sm-6">
|
||||
<label class="form-label">{{ $t('expenses.customer') }}</label>
|
||||
<base-select
|
||||
ref="baseSelect"
|
||||
v-model="customer"
|
||||
:options="customerList"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('customers.select_a_customer')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group col-sm-6">
|
||||
<label for="description">{{ $t('expenses.note') }}</label>
|
||||
<base-text-area
|
||||
v-model="formData.notes"
|
||||
@input="$v.formData.notes.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.notes.$error">
|
||||
<span v-if="!$v.formData.notes.maxLength" class="text-danger">{{ $t('validation.notes_maxlength') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="description">{{ $t('expenses.receipt') }} : </label>
|
||||
<div class="image-upload-box" @click="$refs.file.click()">
|
||||
<input ref="file" class="d-none" type="file" @change="onFileChange">
|
||||
<img v-if="previewReceipt" :src="previewReceipt" class="preview-logo">
|
||||
<div v-else class="upload-content">
|
||||
<font-awesome-icon class="upload-icon" icon="cloud-upload-alt"/>
|
||||
<p class="upload-text"> {{ $t('general.choose_file') }} </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<div class="form-group collapse-button-container">
|
||||
<base-button
|
||||
:loading="isLoading"
|
||||
icon="save"
|
||||
color="theme"
|
||||
type="submit"
|
||||
class="collapse-button"
|
||||
>
|
||||
{{ isEdit ? $t('expenses.update_expense') : $t('expenses.save_expense') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
to choose a file
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<sw-avatar
|
||||
trigger="#receipt-box"
|
||||
:preview-avatar="previewReceipt"
|
||||
:enable-cropper="false"
|
||||
@changed="onChange"
|
||||
>
|
||||
<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 v-if="customFields.length > 0">
|
||||
<div class="grid gap-6 mt-6 grid-col-1 md:grid-cols-2">
|
||||
<sw-input-group
|
||||
v-for="(field, index) in customFields"
|
||||
:label="field.label"
|
||||
:required="field.is_required ? true : false"
|
||||
:key="index"
|
||||
>
|
||||
<component
|
||||
:type="field.type.label"
|
||||
:field="field"
|
||||
:isEdit="isEdit"
|
||||
:is="field.type + 'Field'"
|
||||
:invalid-fields="invalidFields"
|
||||
@update="setCustomFieldValue"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block mt-2 md:hidden">
|
||||
<sw-button
|
||||
:disabled="isLoading"
|
||||
:loading="isLoading"
|
||||
:tabindex="6"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
size="lg"
|
||||
class="flex w-full"
|
||||
>
|
||||
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
|
||||
{{
|
||||
isEdit
|
||||
? $t('expenses.update_expense')
|
||||
: $t('expenses.save_expense')
|
||||
}}
|
||||
</sw-button>
|
||||
</div>
|
||||
</sw-card>
|
||||
</form>
|
||||
</div>
|
||||
</base-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MultiSelect from 'vue-multiselect'
|
||||
import moment from 'moment'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
const { required, minValue, maxLength } = require('vuelidate/lib/validators')
|
||||
import { DownloadIcon } from '@vue-hero-icons/outline'
|
||||
import { CloudUploadIcon, ShoppingCartIcon } from '@vue-hero-icons/solid'
|
||||
import CustomFieldsMixin from '../../mixins/customFields'
|
||||
|
||||
export default {
|
||||
mixins: [CustomFieldsMixin],
|
||||
|
||||
components: {
|
||||
MultiSelect
|
||||
CloudUploadIcon,
|
||||
ShoppingCartIcon,
|
||||
DownloadIcon,
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
|
||||
props: {
|
||||
addname: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data () {
|
||||
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
expense_category_id: null,
|
||||
expense_date: new Date(),
|
||||
amount: null,
|
||||
amount: 100,
|
||||
notes: '',
|
||||
user_id: null
|
||||
user_id: null,
|
||||
},
|
||||
|
||||
money: {
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
prefix: '$ ',
|
||||
precision: 2,
|
||||
masked: false
|
||||
masked: false,
|
||||
},
|
||||
isRequestOnGoing: false,
|
||||
isReceiptAvailable: false,
|
||||
isLoading: false,
|
||||
file: null,
|
||||
category: null,
|
||||
passData: [],
|
||||
contacts: [],
|
||||
previewReceipt: null,
|
||||
fileSendUrl: '/api/expenses',
|
||||
fileSendUrl: '/api/v1/expenses',
|
||||
customer: null,
|
||||
customerList: []
|
||||
fileObject: null,
|
||||
}
|
||||
},
|
||||
|
||||
validations: {
|
||||
category: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
|
||||
formData: {
|
||||
expense_date: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
|
||||
amount: {
|
||||
required,
|
||||
minValue: minValue(0.1),
|
||||
maxLength: maxLength(20)
|
||||
maxLength: maxLength(20),
|
||||
},
|
||||
|
||||
notes: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
}
|
||||
maxLength: maxLength(255),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrencyForInput'
|
||||
]),
|
||||
...mapGetters('company', ['defaultCurrencyForInput']),
|
||||
|
||||
amount: {
|
||||
get: function () {
|
||||
return this.formData.amount / 100
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.formData.amount = newValue * 100
|
||||
}
|
||||
},
|
||||
},
|
||||
isEdit () {
|
||||
|
||||
pageTitle() {
|
||||
if (this.$route.name === 'expenses.edit') {
|
||||
return this.$t('expenses.edit_expense')
|
||||
}
|
||||
return this.$t('expenses.new_expense')
|
||||
},
|
||||
|
||||
isEdit() {
|
||||
if (this.$route.name === 'expenses.edit') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
...mapGetters('category', [
|
||||
'categories'
|
||||
]),
|
||||
...mapGetters('company', [
|
||||
'getSelectedCompany'
|
||||
]),
|
||||
getReceiptUrl () {
|
||||
|
||||
...mapGetters('category', ['categories']),
|
||||
|
||||
...mapGetters('customer', ['customers']),
|
||||
|
||||
...mapGetters('company', ['getSelectedCompany']),
|
||||
|
||||
getReceiptUrl() {
|
||||
if (this.isEdit) {
|
||||
return `/expenses/${this.$route.params.id}/receipt/${this.getSelectedCompany.unique_hash}`
|
||||
return `/expenses/${this.$route.params.id}/receipt`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
categoryError() {
|
||||
if (!this.$v.category.$error) {
|
||||
return ''
|
||||
}
|
||||
if (!this.$v.category.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
|
||||
dateError() {
|
||||
if (!this.$v.formData.expense_date.$error) {
|
||||
return ''
|
||||
}
|
||||
if (!this.$v.formData.expense_date.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
|
||||
amountError() {
|
||||
if (!this.$v.formData.amount.$error) {
|
||||
return ''
|
||||
}
|
||||
if (!this.$v.formData.amount.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
if (!this.$v.formData.amount.maxLength) {
|
||||
return this.$t('validation.price_maxlength')
|
||||
}
|
||||
if (!this.$v.formData.amount.minValue) {
|
||||
return this.$t('validation.price_minvalue')
|
||||
}
|
||||
},
|
||||
|
||||
notesError() {
|
||||
if (!this.$v.formData.notes.$error) {
|
||||
return ''
|
||||
}
|
||||
if (!this.$v.formData.notes.maxLength) {
|
||||
return this.$t('validation.notes_maxlength')
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
category (newValue) {
|
||||
category(newValue) {
|
||||
this.formData.expense_category_id = newValue.id
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
// this.$refs.baseSelect.$refs.search.focus()
|
||||
this.fetchInitialData()
|
||||
if (this.isEdit) {
|
||||
this.getReceipt()
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.$v.formData.$reset()
|
||||
|
||||
this.loadData()
|
||||
|
||||
window.hub.$on('newCategory', (val) => {
|
||||
this.category = val
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('expense', [
|
||||
'fetchCreateExpense',
|
||||
'getFile',
|
||||
'sendFileWithData',
|
||||
'getExpenseReceipt',
|
||||
'addExpense',
|
||||
'updateExpense',
|
||||
'fetchExpense'
|
||||
'fetchExpense',
|
||||
]),
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
...mapActions('category', [
|
||||
'fetchCategories'
|
||||
]),
|
||||
openCategoryModal () {
|
||||
|
||||
...mapActions('modal', ['openModal']),
|
||||
|
||||
...mapActions('category', ['fetchCategories']),
|
||||
|
||||
...mapActions('customer', ['fetchCustomers']),
|
||||
|
||||
openCategoryModal() {
|
||||
this.openModal({
|
||||
'title': 'Add Category',
|
||||
'componentName': 'CategoryModal'
|
||||
title: this.$t('settings.expense_category.add_category'),
|
||||
componentName: 'CategoryModal',
|
||||
})
|
||||
// this.$refs.table.refresh()
|
||||
},
|
||||
onFileChange (e) {
|
||||
var input = event.target
|
||||
this.file = input.files[0]
|
||||
if (input.files && input.files[0]) {
|
||||
var reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
this.previewReceipt = e.target.result
|
||||
}
|
||||
reader.readAsDataURL(input.files[0])
|
||||
}
|
||||
|
||||
onChange(data) {
|
||||
this.previewReceipt = data.image
|
||||
this.fileObject = data.file
|
||||
},
|
||||
async getReceipt () {
|
||||
let res = await axios.get(`/api/expenses/${this.$route.params.id}/show/receipt`)
|
||||
|
||||
async getReceipt() {
|
||||
let res = await this.getExpenseReceipt(this.$route.params.id)
|
||||
|
||||
if (res.data.error) {
|
||||
this.isReceiptAvailable = false
|
||||
@@ -311,47 +441,99 @@ export default {
|
||||
this.isReceiptAvailable = true
|
||||
this.previewReceipt = res.data.image
|
||||
},
|
||||
async fetchInitialData () {
|
||||
this.fetchCategories()
|
||||
let fetchData = await this.fetchCreateExpense()
|
||||
this.customerList = fetchData.data.customers
|
||||
if (this.isEdit) {
|
||||
let response = await this.fetchExpense(this.$route.params.id)
|
||||
this.category = response.data.expense.category
|
||||
this.formData = { ...response.data.expense }
|
||||
this.formData.expense_date = moment(this.formData.expense_date).toString()
|
||||
this.formData.amount = (response.data.expense.amount)
|
||||
this.fileSendUrl = `/api/expenses/${this.$route.params.id}`
|
||||
if (response.data.expense.user_id) {
|
||||
this.customer = this.customerList.find(customer => customer.id === response.data.expense.user_id)
|
||||
}
|
||||
}
|
||||
|
||||
setExpenseCustomer(id) {
|
||||
this.customer = this.customers.find((c) => {
|
||||
return c.id == id
|
||||
})
|
||||
},
|
||||
async sendData () {
|
||||
|
||||
async loadData() {
|
||||
this.isRequestOnGoing = true
|
||||
await this.fetchCategories({ limit: 'all' })
|
||||
await this.fetchCustomers({ limit: 'all' })
|
||||
if (this.isEdit) {
|
||||
this.isRequestOnGoing = true
|
||||
let response = await this.fetchExpense(this.$route.params.id)
|
||||
|
||||
this.formData = { ...this.formData, ...response.data.expense }
|
||||
|
||||
this.formData.expense_date = moment(
|
||||
this.formData.expense_date
|
||||
).toString()
|
||||
|
||||
this.formData.amount = response.data.expense.amount
|
||||
|
||||
this.fileSendUrl = `/api/v1/expenses/${this.$route.params.id}`
|
||||
|
||||
if (response.data.expense.expense_category_id) {
|
||||
this.category = this.categories.find(
|
||||
(category) =>
|
||||
category.id === response.data.expense.expense_category_id
|
||||
)
|
||||
}
|
||||
|
||||
if (response.data.expense.user_id) {
|
||||
this.customer = this.customers.find(
|
||||
(customer) => customer.id === response.data.expense.user_id
|
||||
)
|
||||
}
|
||||
|
||||
let res = await this.fetchCustomFields({
|
||||
type: 'Expense',
|
||||
limit: 'all',
|
||||
})
|
||||
|
||||
this.setEditCustomFields(
|
||||
response.data.expense.fields,
|
||||
res.data.customFields.data
|
||||
)
|
||||
|
||||
this.getReceipt()
|
||||
this.isRequestOnGoing = false
|
||||
return true
|
||||
}
|
||||
await this.setInitialCustomFields('Expense')
|
||||
if (this.$route.query.customer) {
|
||||
this.setExpenseCustomer(parseInt(this.$route.query.customer))
|
||||
}
|
||||
this.isRequestOnGoing = false
|
||||
},
|
||||
|
||||
async sendData() {
|
||||
let validate = await this.touchCustomField()
|
||||
this.$v.category.$touch()
|
||||
this.$v.formData.$touch()
|
||||
if (this.$v.$invalid) {
|
||||
if (this.$v.$invalid || validate.error) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = new FormData()
|
||||
|
||||
if (this.file) {
|
||||
data.append('attachment_receipt', this.file)
|
||||
if (this.fileObject) {
|
||||
data.append('attachment_receipt', this.fileObject)
|
||||
}
|
||||
data.append('expense_category_id', this.formData.expense_category_id)
|
||||
data.append('expense_date', moment(this.formData.expense_date).format('DD/MM/YYYY'))
|
||||
data.append('amount', (this.formData.amount))
|
||||
data.append(
|
||||
'expense_date',
|
||||
moment(this.formData.expense_date).format('YYYY-MM-DD')
|
||||
)
|
||||
data.append('amount', this.formData.amount)
|
||||
data.append('notes', this.formData.notes ? this.formData.notes : '')
|
||||
data.append('user_id', this.customer ? this.customer.id : '')
|
||||
data.append('customFields', JSON.stringify(this.formData.customFields))
|
||||
|
||||
if (this.isEdit) {
|
||||
this.isLoading = true
|
||||
data.append('_method', 'PUT')
|
||||
let response = await this.updateExpense({id: this.$route.params.id, editData: data})
|
||||
let response = await this.updateExpense({
|
||||
id: this.$route.params.id,
|
||||
editData: data,
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](this.$t('expenses.updated_message'))
|
||||
this.isLoading = false
|
||||
window.toastr['success'](this.$t('expenses.updated_message'))
|
||||
this.$router.push('/admin/expenses')
|
||||
return true
|
||||
}
|
||||
@@ -359,16 +541,16 @@ export default {
|
||||
} else {
|
||||
this.isLoading = true
|
||||
let response = await this.addExpense(data)
|
||||
this.isLoading = false
|
||||
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](this.$t('expenses.created_message'))
|
||||
this.isLoading = false
|
||||
this.$router.push('/admin/expenses')
|
||||
this.isLoading = false
|
||||
return true
|
||||
}
|
||||
window.toastr['success'](response.data.success)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,200 +1,232 @@
|
||||
<template>
|
||||
<div class="expenses main-content">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ $t('expenses.title') }}</h3>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="dashboard">
|
||||
{{ $t('general.home') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="#">
|
||||
{{ $tc('expenses.expense',2) }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="page-actions row">
|
||||
<div class="col-xs-2 mr-4">
|
||||
<base-button
|
||||
v-show="totalExpenses || filtersApplied"
|
||||
:outline="true"
|
||||
:icon="filterIcon"
|
||||
size="large"
|
||||
right-icon
|
||||
color="theme"
|
||||
@click="toggleFilter"
|
||||
>
|
||||
{{ $t('general.filter') }}
|
||||
</base-button>
|
||||
</div>
|
||||
<router-link slot="item-title" class="col-xs-2" to="expenses/create">
|
||||
<base-button size="large" icon="plus" color="theme">
|
||||
{{ $t('expenses.add_expense') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<base-page>
|
||||
<!-- Page Header -->
|
||||
<sw-page-header :title="$t('expenses.title')">
|
||||
<sw-breadcrumb slot="breadcrumbs">
|
||||
<sw-breadcrumb-item to="dashboard" :title="$t('general.home')" />
|
||||
|
||||
<transition name="fade">
|
||||
<div v-show="showFilters" class="filter-section">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<label>{{ $t('expenses.customer') }}</label>
|
||||
<base-select
|
||||
v-model="filters.user"
|
||||
:options="customers"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('expenses.select_a_customer')"
|
||||
label="name"
|
||||
@click="filter = ! filter"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label>{{ $t('expenses.category') }}</label>
|
||||
<base-select
|
||||
v-model="filters.category"
|
||||
:options="categories"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('expenses.categories.select_a_category')"
|
||||
label="name"
|
||||
@click="filter = ! filter"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label>{{ $t('expenses.from_date') }}</label>
|
||||
<base-date-picker
|
||||
v-model="filters.from_date"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label>{{ $t('expenses.to_date') }}</label>
|
||||
<base-date-picker
|
||||
v-model="filters.to_date"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label class="clear-filter" @click="clearFilter">{{ $t('general.clear_all') }}</label>
|
||||
</div>
|
||||
</transition>
|
||||
<sw-breadcrumb-item to="#" :title="$tc('expenses.expense', 2)" active />
|
||||
</sw-breadcrumb>
|
||||
|
||||
<div v-cloak v-show="showEmptyScreen" class="col-xs-1 no-data-info" align="center">
|
||||
<observatory-icon class="mt-5 mb-4"/>
|
||||
<div class="row" align="center">
|
||||
<label class="col title">{{ $t('expenses.no_expenses') }}</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="description col mt-1" align="center">{{ $t('expenses.list_of_expenses') }}</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<base-button
|
||||
:outline="true"
|
||||
color="theme"
|
||||
class="mt-3"
|
||||
size="large"
|
||||
@click="$router.push('expenses/create')"
|
||||
>
|
||||
{{ $t('expenses.add_new_expense') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="!showEmptyScreen" class="table-container">
|
||||
|
||||
<div class="table-actions mt-5">
|
||||
<p class="table-stats">{{ $t('general.showing') }}: <b>{{ expenses.length }}</b> {{ $t('general.of') }} <b>{{ totalExpenses }}</b></p>
|
||||
<transition name="fade">
|
||||
<v-dropdown v-if="selectedExpenses.length" :show-arrow="false" theme-light class="action mr-5">
|
||||
<span slot="activator" href="#" class="table-actions-button dropdown-toggle">
|
||||
{{ $t('general.actions') }}
|
||||
</span>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeMultipleExpenses">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="select-all"
|
||||
v-model="selectAllFieldStatus"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
@change="selectAllExpenses"
|
||||
<template slot="actions">
|
||||
<sw-button
|
||||
v-show="totalExpenses"
|
||||
size="lg"
|
||||
variant="primary-outline"
|
||||
@click="toggleFilter"
|
||||
>
|
||||
<label v-show="!isRequestOngoing" for="select-all" class="custom-control-label selectall">
|
||||
<span class="select-all-label">{{ $t('general.select_all') }} </span>
|
||||
</label>
|
||||
{{ $t('general.filter') }}
|
||||
<component :is="filterIcon" class="w-4 h-4 ml-2 -mr-1" />
|
||||
</sw-button>
|
||||
|
||||
<sw-button
|
||||
tag-name="router-link"
|
||||
to="expenses/create"
|
||||
class="ml-4"
|
||||
size="lg"
|
||||
variant="primary"
|
||||
>
|
||||
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
|
||||
{{ $t('expenses.add_expense') }}
|
||||
</sw-button>
|
||||
</template>
|
||||
</sw-page-header>
|
||||
|
||||
<!--Filter Wrapper -->
|
||||
<slide-y-up-transition>
|
||||
<sw-filter-wrapper v-show="showFilters" class="mt-3">
|
||||
<sw-input-group :label="$t('expenses.customer')" class="flex-1 mt-3">
|
||||
<base-customer-select
|
||||
ref="customerSelect"
|
||||
@select="onSelectCustomer"
|
||||
@deselect="clearCustomerSearch"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('expenses.category')"
|
||||
class="flex-1 mt-2 ml-0 lg:ml-6"
|
||||
>
|
||||
<sw-select
|
||||
v-model="filters.category"
|
||||
:options="categories"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('expenses.categories.select_a_category')"
|
||||
label="name"
|
||||
class="mt-2"
|
||||
@click="filter = !filter"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('expenses.from_date')"
|
||||
class="flex-1 mt-2 ml-0 lg:ml-6"
|
||||
>
|
||||
<base-date-picker
|
||||
v-model="filters.from_date"
|
||||
:calendar-button="true"
|
||||
class="mt-2"
|
||||
calendar-button-icon="calendar"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('expenses.to_date')"
|
||||
class="flex-1 mt-2 ml-0 lg:ml-6"
|
||||
>
|
||||
<base-date-picker
|
||||
v-model="filters.to_date"
|
||||
:calendar-button="true"
|
||||
class="mt-2"
|
||||
calendar-button-icon="calendar"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<label
|
||||
class="absolute text-sm leading-snug text-black cursor-pointer"
|
||||
style="top: 10px; right: 15px"
|
||||
@click="clearFilter"
|
||||
>{{ $t('general.clear_all') }}</label
|
||||
>
|
||||
</sw-filter-wrapper>
|
||||
</slide-y-up-transition>
|
||||
|
||||
<!-- Empty Table Placeholder -->
|
||||
<sw-empty-table-placeholder
|
||||
v-show="showEmptyScreen"
|
||||
:title="$t('expenses.no_expenses')"
|
||||
:description="$t('expenses.list_of_expenses')"
|
||||
>
|
||||
<observatory-icon class="mt-5 mb-4" />
|
||||
|
||||
<sw-button
|
||||
slot="actions"
|
||||
tag-name="router-link"
|
||||
to="/admin/expenses/create"
|
||||
size="lg"
|
||||
variant="primary-outline"
|
||||
>
|
||||
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
|
||||
{{ $t('expenses.add_new_expense') }}
|
||||
</sw-button>
|
||||
</sw-empty-table-placeholder>
|
||||
|
||||
<div v-show="!showEmptyScreen" class="relative table-container">
|
||||
<div
|
||||
class="relative flex items-center justify-between h-10 mt-5 list-none border-b-2 border-gray-200 border-solid"
|
||||
>
|
||||
<p class="text-sm">
|
||||
{{ $t('general.showing') }}: <b>{{ expenses.length }}</b>
|
||||
|
||||
{{ $t('general.of') }} <b>{{ totalExpenses }}</b>
|
||||
</p>
|
||||
|
||||
<sw-transition type="fade">
|
||||
<sw-dropdown v-if="selectedExpenses.length">
|
||||
<span
|
||||
slot="activator"
|
||||
class="flex block text-sm font-medium cursor-pointer select-none text-primary-400"
|
||||
>
|
||||
{{ $t('general.actions') }}
|
||||
<chevron-down-icon class="h-5" />
|
||||
</span>
|
||||
|
||||
<sw-dropdown-item @click="removeMultipleExpenses">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</sw-transition>
|
||||
</div>
|
||||
|
||||
<table-component
|
||||
ref="table"
|
||||
:show-filter="false"
|
||||
:data="fetchData"
|
||||
table-class="table"
|
||||
>
|
||||
<div class="absolute z-10 items-center pl-4 mt-2 select-none md:mt-12">
|
||||
<sw-checkbox
|
||||
v-model="selectAllFieldStatus"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="hidden md:inline"
|
||||
@change="selectAllExpenses"
|
||||
/>
|
||||
|
||||
<table-column
|
||||
<sw-checkbox
|
||||
v-model="selectAllFieldStatus"
|
||||
:label="$t('general.select_all')"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="md:hidden"
|
||||
@change="selectAllExpenses"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<sw-table-component ref="table" :show-filter="false" :data="fetchData">
|
||||
<sw-table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
cell-class="no-click"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
:id="row.id"
|
||||
v-model="selectField"
|
||||
:value="row.id"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
>
|
||||
<label :for="row.id" class="custom-control-label" />
|
||||
</div>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
:label="$tc('expenses.categories.category', 1)"
|
||||
sort-as="name"
|
||||
show="category.name"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('expenses.customer')"
|
||||
sort-as="user_name"
|
||||
show="user_name"
|
||||
/>
|
||||
<table-column
|
||||
<div slot-scope="row" class="relative block">
|
||||
<sw-checkbox
|
||||
:id="row.id"
|
||||
v-model="selectField"
|
||||
:value="row.id"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('expenses.date')"
|
||||
sort-as="expense_date"
|
||||
show="formattedExpenseDate"
|
||||
/>
|
||||
<table-column
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$tc('expenses.categories.category', 1)"
|
||||
sort-as="name"
|
||||
show="category.name"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $tc('expenses.categories.category', 1) }}</span>
|
||||
<router-link
|
||||
:to="{ path: `expenses/${row.id}/edit` }"
|
||||
class="font-medium text-primary-500"
|
||||
>
|
||||
{{ row.category.name }}
|
||||
</router-link>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('expenses.customer')"
|
||||
sort-as="user_name"
|
||||
show="user_name"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('expenses.customer') }}</span>
|
||||
<span> {{ row.user_name ? row.user_name : 'Not selected' }} </span>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('expenses.note')"
|
||||
sort-as="expense_date"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('expenses.note') }}</span>
|
||||
<div class="notes">
|
||||
<div class="note">{{ row.notes }}</div>
|
||||
<div class="truncate note w-60">{{ row.notes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('expenses.amount')"
|
||||
sort-as="amount"
|
||||
show="category.amount"
|
||||
@@ -203,116 +235,133 @@
|
||||
<span>{{ $t('expenses.amount') }}</span>
|
||||
<div v-html="$utils.formatMoney(row.amount, defaultCurrency)" />
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
cell-class="action-dropdown no-click"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('expenses.action') }}</span>
|
||||
<v-dropdown>
|
||||
<a slot="activator" href="#">
|
||||
<dot-icon />
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
<router-link :to="{path: `expenses/${row.id}/edit`}" class="dropdown-item">
|
||||
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
|
||||
{{ $t('general.edit') }}
|
||||
</router-link>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeExpense(row.id)">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<sw-dropdown>
|
||||
<dot-icon slot="activator" />
|
||||
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`expenses/${row.id}/edit`"
|
||||
>
|
||||
<pencil-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.edit') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="removeExpense(row.id)">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</template>
|
||||
</table-column>
|
||||
</table-component>
|
||||
</sw-table-column>
|
||||
</sw-table-component>
|
||||
</div>
|
||||
</div>
|
||||
</base-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { SweetModal, SweetModalTab } from 'sweet-modal-vue'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import ObservatoryIcon from '../../components/icon/ObservatoryIcon'
|
||||
import MultiSelect from 'vue-multiselect'
|
||||
import moment, { invalid } from 'moment'
|
||||
import {
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
FilterIcon,
|
||||
XIcon,
|
||||
ChevronDownIcon,
|
||||
PlusIcon,
|
||||
} from '@vue-hero-icons/solid'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MultiSelect,
|
||||
'observatory-icon': ObservatoryIcon,
|
||||
'SweetModal': SweetModal,
|
||||
'SweetModalTab': SweetModalTab
|
||||
ObservatoryIcon,
|
||||
PlusIcon,
|
||||
FilterIcon,
|
||||
XIcon,
|
||||
ChevronDownIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
},
|
||||
data () {
|
||||
|
||||
data() {
|
||||
return {
|
||||
showFilters: false,
|
||||
filtersApplied: false,
|
||||
isRequestOngoing: true,
|
||||
customers: [],
|
||||
filters: {
|
||||
category: null,
|
||||
from_date: '',
|
||||
to_date: '',
|
||||
user: ''
|
||||
}
|
||||
user: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
showEmptyScreen () {
|
||||
return !this.totalExpenses && !this.isRequestOngoing && !this.filtersApplied
|
||||
showEmptyScreen() {
|
||||
return !this.totalExpenses && !this.isRequestOngoing
|
||||
},
|
||||
filterIcon () {
|
||||
return (this.showFilters) ? 'times' : 'filter'
|
||||
|
||||
filterIcon() {
|
||||
return this.showFilters ? 'x-icon' : 'filter-icon'
|
||||
},
|
||||
...mapGetters('category', [
|
||||
'categories'
|
||||
]),
|
||||
|
||||
...mapGetters('category', ['categories']),
|
||||
|
||||
...mapGetters('expense', [
|
||||
'selectedExpenses',
|
||||
'totalExpenses',
|
||||
'expenses',
|
||||
'selectAllField'
|
||||
]),
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrency'
|
||||
'selectAllField',
|
||||
]),
|
||||
|
||||
...mapGetters('company', ['defaultCurrency']),
|
||||
|
||||
...mapGetters('customer', ['customers']),
|
||||
|
||||
selectField: {
|
||||
get: function () {
|
||||
return this.selectedExpenses
|
||||
},
|
||||
set: function (val) {
|
||||
this.selectExpense(val)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
selectAllFieldStatus: {
|
||||
get: function () {
|
||||
return this.selectAllField
|
||||
},
|
||||
set: function (val) {
|
||||
this.setSelectAllState(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
filters: {
|
||||
handler: 'setFilters',
|
||||
deep: true
|
||||
}
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
destroyed () {
|
||||
|
||||
destroyed() {
|
||||
if (this.selectAllField) {
|
||||
this.selectAllExpenses()
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchCategories()
|
||||
|
||||
created() {
|
||||
this.fetchCategories({ limit: 'all' })
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('expense', [
|
||||
'fetchExpenses',
|
||||
@@ -320,76 +369,104 @@ export default {
|
||||
'deleteExpense',
|
||||
'deleteMultipleExpenses',
|
||||
'selectAllExpenses',
|
||||
'setSelectAllState'
|
||||
'setSelectAllState',
|
||||
]),
|
||||
...mapActions('category', [
|
||||
'fetchCategories'
|
||||
]),
|
||||
async fetchData ({ page, filter, sort }) {
|
||||
|
||||
...mapActions('category', ['fetchCategories']),
|
||||
|
||||
async fetchData({ page, filter, sort }) {
|
||||
let data = {
|
||||
user_id: this.filters.user ? this.filters.user.id : null,
|
||||
expense_category_id: this.filters.category !== null ? this.filters.category.id : '',
|
||||
from_date: this.filters.from_date === '' ? this.filters.from_date : moment(this.filters.from_date).format('DD/MM/YYYY'),
|
||||
to_date: this.filters.to_date === '' ? this.filters.to_date : moment(this.filters.to_date).format('DD/MM/YYYY'),
|
||||
|
||||
expense_category_id:
|
||||
this.filters.category !== null ? this.filters.category.id : '',
|
||||
|
||||
from_date:
|
||||
this.filters.from_date === ''
|
||||
? this.filters.from_date
|
||||
: this.filters.from_date,
|
||||
|
||||
to_date:
|
||||
this.filters.to_date === ''
|
||||
? this.filters.to_date
|
||||
: this.filters.to_date,
|
||||
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
|
||||
orderBy: sort.order || 'desc',
|
||||
page
|
||||
|
||||
page,
|
||||
}
|
||||
|
||||
this.isRequestOngoing = true
|
||||
let response = await this.fetchExpenses(data)
|
||||
this.customers = response.data.customers
|
||||
this.isRequestOngoing = false
|
||||
|
||||
return {
|
||||
data: response.data.expenses.data,
|
||||
|
||||
pagination: {
|
||||
totalPages: response.data.expenses.last_page,
|
||||
currentPage: page,
|
||||
count: response.data.expenses.count
|
||||
}
|
||||
count: response.data.expenses.count,
|
||||
},
|
||||
}
|
||||
},
|
||||
refreshTable () {
|
||||
|
||||
onSelectCustomer(customer) {
|
||||
this.filters.user = customer
|
||||
},
|
||||
|
||||
refreshTable() {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
setFilters () {
|
||||
this.filtersApplied = true
|
||||
|
||||
setFilters() {
|
||||
this.refreshTable()
|
||||
},
|
||||
clearFilter () {
|
||||
|
||||
clearFilter() {
|
||||
if (this.filters.user) {
|
||||
this.$refs.customerSelect.$refs.baseSelect.removeElement(
|
||||
this.filters.user
|
||||
)
|
||||
}
|
||||
|
||||
this.filters = {
|
||||
category: null,
|
||||
from_date: '',
|
||||
to_date: '',
|
||||
user: null
|
||||
user: null,
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.filtersApplied = false
|
||||
})
|
||||
},
|
||||
toggleFilter () {
|
||||
if (this.showFilters && this.filtersApplied) {
|
||||
|
||||
async clearCustomerSearch(removedOption, id) {
|
||||
this.filters.user = ''
|
||||
this.refreshTable()
|
||||
},
|
||||
|
||||
toggleFilter() {
|
||||
if (this.showFilters) {
|
||||
this.clearFilter()
|
||||
this.refreshTable()
|
||||
}
|
||||
|
||||
this.showFilters = !this.showFilters
|
||||
},
|
||||
async removeExpense (id) {
|
||||
|
||||
async removeExpense(id) {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('expenses.confirm_delete'),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
dangerMode: true,
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteExpense(id)
|
||||
let res = await this.deleteExpense({ ids: [id] })
|
||||
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$tc('expenses.deleted_message', 1))
|
||||
this.$refs.table.refresh()
|
||||
this.refreshTable()
|
||||
return true
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
@@ -397,16 +474,18 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
async removeMultipleExpenses () {
|
||||
|
||||
async removeMultipleExpenses() {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('expenses.confirm_delete', 2),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
dangerMode: true,
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let request = await this.deleteMultipleExpenses()
|
||||
|
||||
if (request.data.success) {
|
||||
window.toastr['success'](this.$tc('expenses.deleted_message', 2))
|
||||
this.$refs.table.refresh()
|
||||
@@ -415,7 +494,7 @@ export default {
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
213
resources/assets/js/views/invoices/CustomerSelect.vue
Normal file
213
resources/assets/js/views/invoices/CustomerSelect.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div class="col-span-5 pr-0">
|
||||
<div
|
||||
v-if="selectedCustomer"
|
||||
class="flex flex-col p-4 bg-white border border-gray-200 border-solid"
|
||||
style="min-height: 170px"
|
||||
>
|
||||
<div class="relative flex justify-between mb-2">
|
||||
<label class="flex-1 font-medium">{{ selectedCustomer.name }}</label>
|
||||
|
||||
<a
|
||||
class="relative my-0 ml-0 mr-6 text-sm font-medium cursor-pointer text-primary-500"
|
||||
@click.prevent="editCustomer"
|
||||
>
|
||||
{{ $t('general.edit') }}
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="relative my-0 ml-2 mr-6 text-sm font-medium cursor-pointer text-primary-500"
|
||||
@click.prevent="resetSelectedCustomer"
|
||||
>
|
||||
{{ $t('general.deselect') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mt-1">
|
||||
<div v-if="selectedCustomer.billing_address">
|
||||
<div class="flex flex-col">
|
||||
<label
|
||||
class="mb-1 text-sm font-medium text-gray-500 uppercase whitespace-no-wrap"
|
||||
>
|
||||
{{ $t('general.bill_to') }}
|
||||
</label>
|
||||
<div class="flex flex-col flex-1 p-0">
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.name"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.name }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.address_street_1"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.address_street_1 }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.address_street_2"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.address_street_2 }}
|
||||
</label>
|
||||
<label
|
||||
v-if="
|
||||
selectedCustomer.billing_address.city &&
|
||||
selectedCustomer.billing_address.state
|
||||
"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.city }},
|
||||
{{ selectedCustomer.billing_address.state }}
|
||||
{{ selectedCustomer.billing_address.zip }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.country"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.country.name }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.phone"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.phone }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedCustomer.shipping_address" class="col col-6">
|
||||
<div class="flex flex-col">
|
||||
<label
|
||||
class="mb-1 text-sm font-medium text-gray-500 uppercase whitespace-no-wrap"
|
||||
>
|
||||
{{ $t('general.ship_to') }}
|
||||
</label>
|
||||
<div class="flex flex-col flex-1 p-0">
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.name"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.name }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.address_street_1"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.address_street_1 }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.address_street_2"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.address_street_2 }}
|
||||
</label>
|
||||
<label
|
||||
v-if="
|
||||
selectedCustomer.shipping_address.city &&
|
||||
selectedCustomer.shipping_address
|
||||
"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.city }},
|
||||
{{ selectedCustomer.shipping_address.state }}
|
||||
{{ selectedCustomer.shipping_address.zip }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.country"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.country.name }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.phone"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.phone }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<sw-popup
|
||||
:class="[
|
||||
'add-customer p-0',
|
||||
{
|
||||
'border border-solid border-danger rounded': valid.$error,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div
|
||||
slot="activator"
|
||||
class="relative flex justify-center px-0 py-16 bg-white border border-gray-200 border-solid rounded-md"
|
||||
style="min-height: 170px"
|
||||
>
|
||||
<user-icon
|
||||
class="flex justify-center w-10 h-10 p-2 mr-5 text-sm text-white bg-gray-200 rounded-full font-base"
|
||||
/>
|
||||
<div class="mt-1">
|
||||
<label class="text-lg">
|
||||
{{ $t('customers.new_customer') }}
|
||||
<span class="text-danger"> * </span>
|
||||
</label>
|
||||
<p v-if="valid.$error && !valid.required" class="text-danger">
|
||||
{{ $t('validation.required') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<customer-select-popup :user-id="customerId" type="invoice" />
|
||||
</sw-popup>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { UserIcon } from '@vue-hero-icons/solid'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
valid: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
customerId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters('invoice', ['getTemplateId', 'selectedCustomer']),
|
||||
},
|
||||
|
||||
created() {
|
||||
this.resetSelectedCustomer()
|
||||
if (this.customerId) {
|
||||
this.selectCustomer(this.customerId)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('invoice', ['resetSelectedCustomer', 'selectCustomer']),
|
||||
|
||||
...mapActions('modal', ['openModal']),
|
||||
|
||||
editCustomer() {
|
||||
this.openModal({
|
||||
title: this.$t('customers.edit_customer'),
|
||||
componentName: 'CustomerModal',
|
||||
id: this.selectedCustomer.id,
|
||||
data: this.selectedCustomer,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,50 +1,49 @@
|
||||
<template>
|
||||
<div class="section mt-2">
|
||||
<label class="invoice-label">
|
||||
<div class="flex items-center justify-between w-full mt-2 text-sm">
|
||||
<label class="font-semibold leading-5 text-gray-500 uppercase">
|
||||
{{ tax.name }} ({{ tax.percent }}%)
|
||||
</label>
|
||||
<label class="invoice-amount">
|
||||
<label class="flex items-center justify-center text-lg text-black">
|
||||
<div v-html="$utils.formatMoney(tax.amount, currency)" />
|
||||
|
||||
<font-awesome-icon
|
||||
class="ml-2"
|
||||
icon="trash-alt"
|
||||
@click="$emit('remove', index)"
|
||||
/>
|
||||
<trash-icon class="h-5 ml-2" @click="$emit('remove', index)" />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { TrashIcon } from '@vue-hero-icons/solid'
|
||||
export default {
|
||||
components: {
|
||||
TrashIcon,
|
||||
},
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
tax: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0,
|
||||
},
|
||||
totalTax: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
taxAmount () {
|
||||
taxAmount() {
|
||||
if (this.tax.compound_tax && this.total) {
|
||||
return ((this.total + this.totalTax) * this.tax.percent) / 100
|
||||
}
|
||||
@@ -54,30 +53,26 @@ export default {
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
total: {
|
||||
handler: 'updateTax'
|
||||
handler: 'updateTax',
|
||||
},
|
||||
totalTax: {
|
||||
handler: 'updateTax'
|
||||
}
|
||||
handler: 'updateTax',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateTax () {
|
||||
updateTax() {
|
||||
this.$emit('update', {
|
||||
'index': this.index,
|
||||
'item': {
|
||||
index: this.index,
|
||||
item: {
|
||||
...this.tax,
|
||||
amount: this.taxAmount
|
||||
}
|
||||
amount: this.taxAmount,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
<template>
|
||||
<tr class="item-row invoice-item-row">
|
||||
<td colspan="5">
|
||||
<table class="full-width">
|
||||
<tr class="box-border bg-white border border-gray-200 border-solid rounded-b">
|
||||
<td colspan="5" class="p-0 text-left align-top">
|
||||
<table class="w-full">
|
||||
<colgroup>
|
||||
<col style="width: 40%;">
|
||||
<col style="width: 10%;">
|
||||
<col style="width: 15%;">
|
||||
<col v-if="discountPerItem === 'YES'" style="width: 15%;">
|
||||
<col style="width: 15%;">
|
||||
<col style="width: 40%" />
|
||||
<col style="width: 10%" />
|
||||
<col style="width: 15%" />
|
||||
<col v-if="discountPerItem === 'YES'" style="width: 15%" />
|
||||
<col style="width: 15%" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="">
|
||||
<div class="item-select-wrapper">
|
||||
<div class="sort-icon-wrapper handle">
|
||||
<font-awesome-icon
|
||||
class="sort-icon"
|
||||
icon="grip-vertical"
|
||||
/>
|
||||
<td class="px-5 py-4 text-left align-top">
|
||||
<div class="flex justify-start">
|
||||
<div
|
||||
class="flex items-center justify-center w-12 h-5 mt-2 text-gray-400 cursor-move handle"
|
||||
>
|
||||
<drag-icon />
|
||||
</div>
|
||||
<item-select
|
||||
ref="itemSelect"
|
||||
@@ -34,88 +33,94 @@
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<base-input
|
||||
<td class="px-5 py-4 text-right align-top">
|
||||
<sw-input
|
||||
v-model="item.quantity"
|
||||
:invalid="$v.item.quantity.$error"
|
||||
:is-input-group="!!item.unit_name"
|
||||
:input-group-text="item.unit_name"
|
||||
type="text"
|
||||
small
|
||||
@keyup="updateItem"
|
||||
@input="$v.item.quantity.$touch()"
|
||||
/>
|
||||
<div v-if="$v.item.quantity.$error">
|
||||
<span v-if="!$v.item.quantity.maxLength" class="text-danger">{{ $t('validation.quantity_maxlength') }}</span>
|
||||
<span v-if="!$v.item.quantity.maxLength" class="text-danger">
|
||||
{{ $t('validation.quantity_maxlength') }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-left">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="flex-fillbd-highlight">
|
||||
<div class="base-input">
|
||||
<money
|
||||
<td class="px-5 py-4 text-left align-top">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex-auto flex-fill bd-highlight">
|
||||
<div class="relative w-full">
|
||||
<sw-money
|
||||
v-model="price"
|
||||
v-bind="customerCurrency"
|
||||
class="input-field"
|
||||
:currency="customerCurrency"
|
||||
:invalid="$v.item.price.$error"
|
||||
@input="$v.item.price.$touch()"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="$v.item.price.$error">
|
||||
<span v-if="!$v.item.price.maxLength" class="text-danger">{{ $t('validation.price_maxlength') }}</span>
|
||||
<span v-if="!$v.item.price.maxLength" class="text-danger">
|
||||
{{ $t('validation.price_maxlength') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td v-if="discountPerItem === 'YES'" class="">
|
||||
<div class="d-flex flex-column bd-highlight">
|
||||
<div
|
||||
class="btn-group flex-fill bd-highlight"
|
||||
role="group"
|
||||
>
|
||||
<base-input
|
||||
<td
|
||||
v-if="discountPerItem === 'YES'"
|
||||
class="px-5 py-4 text-left align-top"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-auto" role="group">
|
||||
<sw-input
|
||||
v-model="discount"
|
||||
:invalid="$v.item.discount_val.$error"
|
||||
input-class="item-discount"
|
||||
class="border-r-0 rounded-tr-none rounded-br-none"
|
||||
@input="$v.item.discount_val.$touch()"
|
||||
/>
|
||||
<v-dropdown :show-arrow="false" theme-light>
|
||||
<button
|
||||
<sw-dropdown>
|
||||
<sw-button
|
||||
slot="activator"
|
||||
type="button"
|
||||
class="btn item-dropdown dropdown-toggle"
|
||||
class="flex items-center px-5 py-1 text-sm font-medium leading-none text-center text-gray-500 whitespace-no-wrap border border-gray-300 border-solid rounded rounded-tl-none rounded-bl-none dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
style="height: 43px"
|
||||
variant="white"
|
||||
>
|
||||
{{ item.discount_type == 'fixed' ? currency.symbol : '%' }}
|
||||
</button>
|
||||
<v-dropdown-item>
|
||||
<a class="dropdown-item" href="#" @click.prevent="selectFixed" >
|
||||
{{ $t('general.fixed') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<a class="dropdown-item" href="#" @click.prevent="selectPercentage">
|
||||
{{ $t('general.percentage') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<span class="flex items-center">
|
||||
{{
|
||||
item.discount_type == 'fixed' ? currency.symbol : '%'
|
||||
}}
|
||||
<chevron-down-icon class="h-5" />
|
||||
</span>
|
||||
</sw-button>
|
||||
|
||||
<sw-dropdown-item @click="selectFixed">
|
||||
{{ $t('general.fixed') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="selectPercentage">
|
||||
{{ $t('general.percentage') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</div>
|
||||
<!-- <div v-if="$v.item.discount.$error"> discount error </div> -->
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="item-amount">
|
||||
<td class="px-5 py-4 text-right align-top">
|
||||
<div class="flex items-center justify-end text-sm">
|
||||
<span>
|
||||
<div v-html="$utils.formatMoney(total, currency)" />
|
||||
</span>
|
||||
|
||||
<div class="remove-icon-wrapper">
|
||||
<font-awesome-icon
|
||||
<div
|
||||
class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer"
|
||||
>
|
||||
<trash-icon
|
||||
v-if="showRemoveItemIcon"
|
||||
class="remove-icon"
|
||||
icon="trash-alt"
|
||||
class="h-5 text-gray-700"
|
||||
@click="removeItem"
|
||||
/>
|
||||
</div>
|
||||
@@ -123,8 +128,8 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="taxPerItem === 'YES'" class="tax-tr">
|
||||
<td />
|
||||
<td colspan="4">
|
||||
<td class="px-5 py-4 text-left align-top" />
|
||||
<td colspan="4" class="px-5 py-4 text-left align-top">
|
||||
<tax
|
||||
v-for="(tax, index) in item.taxes"
|
||||
:key="tax.id"
|
||||
@@ -145,98 +150,102 @@
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Guid from 'guid'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import TaxStub from '../../stub/tax'
|
||||
import InvoiceStub from '../../stub/invoice'
|
||||
import ItemSelect from './ItemSelect'
|
||||
import Tax from './Tax'
|
||||
const { required, minValue, between, maxLength } = require('vuelidate/lib/validators')
|
||||
import { TrashIcon, ViewGridIcon, ChevronDownIcon } from '@vue-hero-icons/solid'
|
||||
import DragIcon from '@/components/icon/DragIcon'
|
||||
const {
|
||||
required,
|
||||
minValue,
|
||||
between,
|
||||
maxLength,
|
||||
} = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tax,
|
||||
ItemSelect
|
||||
ItemSelect,
|
||||
TrashIcon,
|
||||
ViewGridIcon,
|
||||
ChevronDownIcon,
|
||||
DragIcon,
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
props: {
|
||||
itemData: {
|
||||
type: Object,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
taxPerItem: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
discountPerItem: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
invoiceItems: {
|
||||
type: Array,
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
isClosePopup: false,
|
||||
itemSelect: null,
|
||||
item: {...this.itemData},
|
||||
item: { ...this.itemData },
|
||||
maxDiscount: 0,
|
||||
money: {
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
prefix: '$ ',
|
||||
precision: 2,
|
||||
masked: false
|
||||
masked: false,
|
||||
},
|
||||
isSelected: false
|
||||
isSelected: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('item', [
|
||||
'items'
|
||||
]),
|
||||
...mapGetters('modal', [
|
||||
'modalActive'
|
||||
]),
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrencyForInput'
|
||||
]),
|
||||
customerCurrency () {
|
||||
...mapGetters('item', ['items']),
|
||||
...mapGetters('modal', ['modalActive']),
|
||||
...mapGetters('company', ['defaultCurrencyForInput']),
|
||||
customerCurrency() {
|
||||
if (this.currency) {
|
||||
return {
|
||||
decimal: this.currency.decimal_separator,
|
||||
thousands: this.currency.thousand_separator,
|
||||
prefix: this.currency.symbol + ' ',
|
||||
precision: this.currency.precision,
|
||||
masked: false
|
||||
masked: false,
|
||||
}
|
||||
} else {
|
||||
return this.defaultCurrenctForInput
|
||||
}
|
||||
},
|
||||
showRemoveItemIcon () {
|
||||
showRemoveItemIcon() {
|
||||
if (this.invoiceItems.length == 1) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
subtotal () {
|
||||
subtotal() {
|
||||
return this.item.price * this.item.quantity
|
||||
},
|
||||
discount: {
|
||||
@@ -251,12 +260,12 @@ export default {
|
||||
}
|
||||
|
||||
this.item.discount = newValue
|
||||
}
|
||||
},
|
||||
},
|
||||
total () {
|
||||
total() {
|
||||
return this.subtotal - this.item.discount_val
|
||||
},
|
||||
totalSimpleTax () {
|
||||
totalSimpleTax() {
|
||||
return window._.sumBy(this.item.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
return tax.amount
|
||||
@@ -265,7 +274,7 @@ export default {
|
||||
return 0
|
||||
})
|
||||
},
|
||||
totalCompoundTax () {
|
||||
totalCompoundTax() {
|
||||
return window._.sumBy(this.item.taxes, function (tax) {
|
||||
if (tax.compound_tax) {
|
||||
return tax.amount
|
||||
@@ -274,7 +283,7 @@ export default {
|
||||
return 0
|
||||
})
|
||||
},
|
||||
totalTax () {
|
||||
totalTax() {
|
||||
return this.totalSimpleTax + this.totalCompoundTax
|
||||
},
|
||||
price: {
|
||||
@@ -292,51 +301,54 @@ export default {
|
||||
} else {
|
||||
this.item.price = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
item: {
|
||||
handler: 'updateItem',
|
||||
deep: true
|
||||
deep: true,
|
||||
},
|
||||
subtotal (newValue) {
|
||||
subtotal(newValue) {
|
||||
if (this.item.discount_type === 'percentage') {
|
||||
this.item.discount_val = (this.item.discount * newValue) / 100
|
||||
}
|
||||
},
|
||||
modalActive (val) {
|
||||
modalActive(val) {
|
||||
if (!val) {
|
||||
this.isSelected = false
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
validations () {
|
||||
validations() {
|
||||
return {
|
||||
item: {
|
||||
name: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
quantity: {
|
||||
required,
|
||||
minValue: minValue(1),
|
||||
maxLength: maxLength(20)
|
||||
minValue: minValue(0),
|
||||
maxLength: maxLength(20),
|
||||
},
|
||||
price: {
|
||||
required,
|
||||
minValue: minValue(1),
|
||||
maxLength: maxLength(20)
|
||||
maxLength: maxLength(20),
|
||||
},
|
||||
discount_val: {
|
||||
between: between(0, this.maxDiscount)
|
||||
between: between(0, this.maxDiscount),
|
||||
},
|
||||
description: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
}
|
||||
maxLength: maxLength(255),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
created () {
|
||||
mounted() {
|
||||
this.$v.item.$reset()
|
||||
},
|
||||
created() {
|
||||
window.hub.$on('checkItems', this.validateItem)
|
||||
window.hub.$on('newItem', (val) => {
|
||||
if (this.taxPerItem === 'YES') {
|
||||
@@ -348,52 +360,54 @@ export default {
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
updateTax (data) {
|
||||
updateTax(data) {
|
||||
this.$set(this.item.taxes, data.index, data.item)
|
||||
|
||||
let lastTax = this.item.taxes[this.item.taxes.length - 1]
|
||||
|
||||
if (lastTax.tax_type_id !== 0) {
|
||||
this.item.taxes.push({...TaxStub, id: Guid.raw()})
|
||||
this.item.taxes.push({ ...TaxStub, id: Guid.raw() })
|
||||
}
|
||||
|
||||
this.updateItem()
|
||||
},
|
||||
removeTax (index) {
|
||||
removeTax(index) {
|
||||
this.item.taxes.splice(index, 1)
|
||||
|
||||
this.updateItem()
|
||||
},
|
||||
taxWithPercentage ({ name, percent }) {
|
||||
taxWithPercentage({ name, percent }) {
|
||||
return `${name} (${percent}%)`
|
||||
},
|
||||
searchVal (val) {
|
||||
searchVal(val) {
|
||||
this.item.name = val
|
||||
},
|
||||
deselectItem () {
|
||||
this.item = {...InvoiceStub, id: this.item.id, taxes: [{...TaxStub, id: Guid.raw()}]}
|
||||
deselectItem() {
|
||||
this.item = {
|
||||
...InvoiceStub,
|
||||
id: this.item.id,
|
||||
taxes: [{ ...TaxStub, id: Guid.raw() }],
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.$refs.itemSelect.$refs.baseSelect.$refs.search.focus()
|
||||
})
|
||||
},
|
||||
onSelectItem (item) {
|
||||
onSelectItem(item) {
|
||||
this.item.name = item.name
|
||||
this.item.price = item.price
|
||||
this.item.item_id = item.id
|
||||
this.item.description = item.description
|
||||
this.item.unit_name = item.unit_name
|
||||
|
||||
if (this.taxPerItem === 'YES' && item.taxes) {
|
||||
let index = 0
|
||||
item.taxes.forEach(tax => {
|
||||
this.updateTax({index, item: { ...tax }})
|
||||
item.taxes.forEach((tax) => {
|
||||
this.updateTax({ index, item: { ...tax } })
|
||||
index++
|
||||
})
|
||||
}
|
||||
// if (this.item.taxes.length) {
|
||||
// this.item.taxes = {...item.taxes}
|
||||
// }
|
||||
},
|
||||
selectFixed () {
|
||||
selectFixed() {
|
||||
if (this.item.discount_type === 'fixed') {
|
||||
return
|
||||
}
|
||||
@@ -401,7 +415,7 @@ export default {
|
||||
this.item.discount_val = this.item.discount * 100
|
||||
this.item.discount_type = 'fixed'
|
||||
},
|
||||
selectPercentage () {
|
||||
selectPercentage() {
|
||||
if (this.item.discount_type === 'percentage') {
|
||||
return
|
||||
}
|
||||
@@ -410,24 +424,24 @@ export default {
|
||||
|
||||
this.item.discount_type = 'percentage'
|
||||
},
|
||||
updateItem () {
|
||||
updateItem() {
|
||||
this.$emit('update', {
|
||||
'index': this.index,
|
||||
'item': {
|
||||
index: this.index,
|
||||
item: {
|
||||
...this.item,
|
||||
total: this.total,
|
||||
totalSimpleTax: this.totalSimpleTax,
|
||||
totalCompoundTax: this.totalCompoundTax,
|
||||
totalTax: this.totalTax,
|
||||
tax: this.totalTax,
|
||||
taxes: [...this.item.taxes]
|
||||
}
|
||||
taxes: [...this.item.taxes],
|
||||
},
|
||||
})
|
||||
},
|
||||
removeItem () {
|
||||
removeItem() {
|
||||
this.$emit('remove', this.index)
|
||||
},
|
||||
validateItem () {
|
||||
validateItem() {
|
||||
this.$v.item.$touch()
|
||||
|
||||
if (this.item !== null) {
|
||||
@@ -435,7 +449,7 @@ export default {
|
||||
} else {
|
||||
this.$emit('itemValidate', this.index, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
<template>
|
||||
<div class="item-selector">
|
||||
<div v-if="item.item_id" class="selected-item">
|
||||
<div class="flex-1 text-sm">
|
||||
<div
|
||||
v-if="item.item_id"
|
||||
class="relative flex items-center h-10 pl-2 bg-gray-100 border border-gray-200 border-solid rounded"
|
||||
>
|
||||
{{ item.name }}
|
||||
|
||||
<span class="deselect-icon" @click="deselectItem">
|
||||
<font-awesome-icon icon="times-circle" />
|
||||
<span
|
||||
class="absolute text-gray-400 cursor-pointer"
|
||||
style="top: 8px; right: 10px"
|
||||
@click="deselectItem"
|
||||
>
|
||||
<x-circle-icon class="h-5" />
|
||||
</span>
|
||||
</div>
|
||||
<base-select
|
||||
<sw-select
|
||||
v-else
|
||||
ref="baseSelect"
|
||||
v-model="itemSelect"
|
||||
@@ -24,93 +31,107 @@
|
||||
@select="onSelect"
|
||||
>
|
||||
<div slot="afterList">
|
||||
<button type="button" class="list-add-button" @click="openItemModal">
|
||||
<font-awesome-icon class="icon" icon="cart-plus" />
|
||||
<label>{{ $t('general.add_new_item') }}</label>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center w-full p-3 bg-gray-200 border-none outline-none"
|
||||
@click="openItemModal"
|
||||
>
|
||||
<shopping-cart-icon
|
||||
class="h-5 mr-2 -ml-2 text-center text-primary-400"
|
||||
/>
|
||||
<label class="ml-2 text-sm leading-none text-primary-400">{{
|
||||
$t('general.add_new_item')
|
||||
}}</label>
|
||||
</button>
|
||||
</div>
|
||||
</base-select>
|
||||
<div class="item-description">
|
||||
<base-text-area
|
||||
</sw-select>
|
||||
<div class="w-full pt-1 text-xs text-light">
|
||||
<sw-textarea
|
||||
v-autoresize
|
||||
v-model="item.description"
|
||||
:invalid-description="invalidDescription"
|
||||
:placeholder="$t('invoices.item.type_item_description')"
|
||||
type="text"
|
||||
rows="1"
|
||||
class="description-input"
|
||||
variant="inv-desc"
|
||||
class="w-full text-xs text-gray-600 border-none resize-none"
|
||||
@input="$emit('onDesriptionInput')"
|
||||
/>
|
||||
<div v-if="invalidDescription">
|
||||
<span class="text-danger">{{ $tc('validation.description_maxlength') }}</span>
|
||||
<span class="text-xs text-danger">
|
||||
{{ $tc('validation.description_maxlength') }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- <textarea type="text" v-autoresize rows="1" class="description-input" v-model="item.description" placeholder="Type Item Description (optional)" /> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { XCircleIcon, ShoppingCartIcon } from '@vue-hero-icons/solid'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
XCircleIcon,
|
||||
ShoppingCartIcon,
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
invalid: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
invalidDescription: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
taxPerItem: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
itemSelect: null,
|
||||
loading: false
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('item', [
|
||||
'items'
|
||||
])
|
||||
...mapGetters('item', ['items']),
|
||||
},
|
||||
watch: {
|
||||
invalidDescription (newValue) {
|
||||
invalidDescription(newValue) {
|
||||
console.log(newValue)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
...mapActions('item', [
|
||||
'fetchItems'
|
||||
]),
|
||||
async searchItems (search) {
|
||||
...mapActions('modal', ['openModal']),
|
||||
|
||||
...mapActions('item', ['fetchItems']),
|
||||
|
||||
async searchItems(search) {
|
||||
let data = {
|
||||
search,
|
||||
filter: {
|
||||
name: search,
|
||||
unit: '',
|
||||
price: ''
|
||||
price: '',
|
||||
},
|
||||
orderByField: '',
|
||||
orderBy: '',
|
||||
page: 1
|
||||
page: 1,
|
||||
}
|
||||
|
||||
if (this.item) {
|
||||
data.item_id = this.item.item_id
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
@@ -119,27 +140,31 @@ export default {
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
onTextChange (val) {
|
||||
|
||||
onTextChange(val) {
|
||||
this.searchItems(val)
|
||||
|
||||
this.$emit('search', val)
|
||||
},
|
||||
openItemModal () {
|
||||
|
||||
openItemModal() {
|
||||
this.$emit('onSelectItem')
|
||||
this.openModal({
|
||||
'title': this.$t('items.add_item'),
|
||||
'componentName': 'ItemModal',
|
||||
'data': {taxPerItem: this.taxPerItem, taxes: this.taxes}
|
||||
title: this.$t('items.add_item'),
|
||||
componentName: 'ItemModal',
|
||||
data: { taxPerItem: this.taxPerItem, taxes: this.taxes },
|
||||
})
|
||||
},
|
||||
|
||||
onSelect(val) {
|
||||
this.$emit('select', val)
|
||||
this.fetchItems()
|
||||
},
|
||||
deselectItem () {
|
||||
|
||||
deselectItem() {
|
||||
this.itemSelect = null
|
||||
this.$emit('deselect')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="tax-row">
|
||||
<div class="d-flex align-items-center tax-select">
|
||||
<label class="bd-highlight pr-2 mb-0" align="right">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center" style="flex: 4">
|
||||
<label class="pr-2 mb-0" align="right">
|
||||
{{ $t('general.tax') }}
|
||||
</label>
|
||||
<base-select
|
||||
<sw-select
|
||||
v-model="selectedTax"
|
||||
:options="filteredTypes"
|
||||
:allow-empty="false"
|
||||
@@ -16,19 +16,29 @@
|
||||
@select="(val) => onSelectTax(val)"
|
||||
>
|
||||
<div slot="afterList">
|
||||
<button type="button" class="list-add-button" @click="openTaxModal">
|
||||
<font-awesome-icon class="icon" icon="check-circle" />
|
||||
<label>{{ $t('invoices.add_new_tax') }}</label>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center w-full px-2 py-2 bg-gray-200 border-none outline-none"
|
||||
@click="openTaxModal"
|
||||
>
|
||||
<check-circle-icon class="h-5 text-primary-400" />
|
||||
<label class="ml-2 text-sm leading-none text-primary-400">{{
|
||||
$t('invoices.add_new_tax')
|
||||
}}</label>
|
||||
</button>
|
||||
</div>
|
||||
</base-select> <br>
|
||||
</sw-select>
|
||||
<br />
|
||||
</div>
|
||||
<div class="text-right tax-amount" v-html="$utils.formatMoney(taxAmount, currency)" />
|
||||
<div class="remove-icon-wrapper">
|
||||
<font-awesome-icon
|
||||
<div
|
||||
class="text-sm text-right"
|
||||
style="flex: 3"
|
||||
v-html="$utils.formatMoney(taxAmount, currency)"
|
||||
/>
|
||||
<div class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer">
|
||||
<trash-icon
|
||||
v-if="taxes.length && index !== taxes.length - 1"
|
||||
class="remove-icon"
|
||||
icon="trash-alt"
|
||||
class="h-5 text-gray-700"
|
||||
@click="removeTax"
|
||||
/>
|
||||
</div>
|
||||
@@ -37,49 +47,52 @@
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { CheckCircleIcon, TrashIcon } from '@vue-hero-icons/solid'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CheckCircleIcon,
|
||||
TrashIcon,
|
||||
},
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
taxData: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
default: []
|
||||
default: [],
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0,
|
||||
},
|
||||
totalTax: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
tax: {...this.taxData},
|
||||
selectedTax: null
|
||||
tax: { ...this.taxData },
|
||||
selectedTax: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('taxType', [
|
||||
'taxTypes'
|
||||
]),
|
||||
filteredTypes () {
|
||||
const clonedTypes = this.taxTypes.map(a => ({...a}))
|
||||
...mapGetters('taxType', ['taxTypes']),
|
||||
filteredTypes() {
|
||||
const clonedTypes = this.taxTypes.map((a) => ({ ...a }))
|
||||
|
||||
return clonedTypes.map((taxType) => {
|
||||
let found = this.taxes.find(tax => tax.tax_type_id === taxType.id)
|
||||
let found = this.taxes.find((tax) => tax.tax_type_id === taxType.id)
|
||||
|
||||
if (found) {
|
||||
taxType.$isDisabled = true
|
||||
@@ -90,7 +103,7 @@ export default {
|
||||
return taxType
|
||||
})
|
||||
},
|
||||
taxAmount () {
|
||||
taxAmount() {
|
||||
if (this.tax.compound_tax && this.total) {
|
||||
return ((this.total + this.totalTax) * this.tax.percent) / 100
|
||||
}
|
||||
@@ -100,19 +113,21 @@ export default {
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
total: {
|
||||
handler: 'updateTax'
|
||||
handler: 'updateTax',
|
||||
},
|
||||
totalTax: {
|
||||
handler: 'updateTax'
|
||||
}
|
||||
handler: 'updateTax',
|
||||
},
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
if (this.taxData.tax_type_id > 0) {
|
||||
this.selectedTax = this.taxTypes.find(_type => _type.id === this.taxData.tax_type_id)
|
||||
this.selectedTax = this.taxTypes.find(
|
||||
(_type) => _type.id === this.taxData.tax_type_id
|
||||
)
|
||||
}
|
||||
|
||||
this.updateTax()
|
||||
@@ -124,13 +139,11 @@ export default {
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
customLabel ({ name, percent }) {
|
||||
...mapActions('modal', ['openModal']),
|
||||
customLabel({ name, percent }) {
|
||||
return `${name} - ${percent}%`
|
||||
},
|
||||
onSelectTax (val) {
|
||||
onSelectTax(val) {
|
||||
this.tax.percent = val.percent
|
||||
this.tax.tax_type_id = val.id
|
||||
this.tax.compound_tax = val.compound_tax
|
||||
@@ -138,28 +151,28 @@ export default {
|
||||
|
||||
this.updateTax()
|
||||
},
|
||||
updateTax () {
|
||||
updateTax() {
|
||||
if (this.tax.tax_type_id === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$emit('update', {
|
||||
'index': this.index,
|
||||
'item': {
|
||||
index: this.index,
|
||||
item: {
|
||||
...this.tax,
|
||||
amount: this.taxAmount
|
||||
}
|
||||
amount: this.taxAmount,
|
||||
},
|
||||
})
|
||||
},
|
||||
removeTax () {
|
||||
removeTax() {
|
||||
this.$emit('remove', this.index, this.tax)
|
||||
},
|
||||
openTaxModal () {
|
||||
openTaxModal() {
|
||||
this.openModal({
|
||||
'title': this.$t('settings.tax_types.add_tax'),
|
||||
'componentName': 'TaxTypeModal'
|
||||
title: this.$t('settings.tax_types.add_tax'),
|
||||
componentName: 'TaxTypeModal',
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,203 +1,259 @@
|
||||
<template>
|
||||
<div v-if="invoice" class="main-content invoice-view-page">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ invoice.invoice_number }}</h3>
|
||||
<div class="page-actions row">
|
||||
<div class="col-xs-2 mr-3">
|
||||
<base-button
|
||||
<base-page v-if="invoice" class="xl:pl-96">
|
||||
<sw-page-header :title="pageTitle">
|
||||
<template slot="actions">
|
||||
<div class="mr-3 text-sm">
|
||||
<sw-button
|
||||
v-if="invoice.status === 'DRAFT'"
|
||||
:loading="isMarkingAsSent"
|
||||
:disabled="isMarkingAsSent"
|
||||
:outline="true"
|
||||
color="theme"
|
||||
variant="primary-outline"
|
||||
@click="onMarkAsSent"
|
||||
>
|
||||
{{ $t('invoices.mark_as_sent') }}
|
||||
</base-button>
|
||||
</sw-button>
|
||||
</div>
|
||||
<base-button
|
||||
<sw-button
|
||||
v-if="invoice.status === 'DRAFT'"
|
||||
:loading="isSendingEmail"
|
||||
:disabled="isSendingEmail"
|
||||
color="theme"
|
||||
variant="primary"
|
||||
class="text-sm"
|
||||
@click="onSendInvoice"
|
||||
>
|
||||
{{ $t('invoices.send_invoice') }}
|
||||
</base-button>
|
||||
<router-link
|
||||
v-if="invoice.status === 'SENT'"
|
||||
</sw-button>
|
||||
<sw-button
|
||||
v-if="
|
||||
invoice.status === 'SENT' ||
|
||||
invoice.status === 'OVERDUE' ||
|
||||
invoice.status === 'VIEWED'
|
||||
"
|
||||
tag-name="router-link"
|
||||
:to="`/admin/payments/${$route.params.id}/create`"
|
||||
variant="primary"
|
||||
class="text-sm"
|
||||
>
|
||||
<base-button color="theme">
|
||||
{{ $t('payments.record_payment') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
<v-dropdown
|
||||
:close-on-select="true"
|
||||
align="left"
|
||||
class="filter-container"
|
||||
>
|
||||
<a slot="activator" href="#">
|
||||
<base-button color="theme">
|
||||
<font-awesome-icon icon="ellipsis-h" />
|
||||
</base-button>
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="copyPdfUrl">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'link']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</div>
|
||||
<router-link
|
||||
:to="{ path: `/admin/invoices/${$route.params.id}/edit` }"
|
||||
class="dropdown-item"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'pencil-alt']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</router-link>
|
||||
<div class="dropdown-item" @click="removeInvoice($route.params.id)">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'trash']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="invoice-sidebar">
|
||||
<div class="side-header">
|
||||
<base-input
|
||||
{{ $t('payments.record_payment') }}
|
||||
</sw-button>
|
||||
<sw-dropdown class="ml-3">
|
||||
<sw-button slot="activator" variant="primary" class="h-10">
|
||||
<dots-horizontal-icon class="h-5" />
|
||||
</sw-button>
|
||||
|
||||
<sw-dropdown-item @click="copyPdfUrl">
|
||||
<link-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`/admin/invoices/${$route.params.id}/edit`"
|
||||
>
|
||||
<pencil-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.edit') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="removeInvoice($route.params.id)">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</template>
|
||||
</sw-page-header>
|
||||
|
||||
<!-- sidebar -->
|
||||
<div
|
||||
class="fixed top-0 left-0 hidden h-full pt-16 pb-5 ml-56 bg-white xl:ml-64 w-88 xl:block"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between px-4 pt-8 pb-2 border border-gray-200 border-solid height-full"
|
||||
>
|
||||
<sw-input
|
||||
v-model="searchData.searchText"
|
||||
:placeholder="$t('general.search')"
|
||||
input-class="inv-search"
|
||||
icon="search"
|
||||
class="mb-6"
|
||||
type="text"
|
||||
align-icon="right"
|
||||
variant="gray"
|
||||
@input="onSearch"
|
||||
/>
|
||||
<div class="btn-group ml-3" role="group" aria-label="First group">
|
||||
<v-dropdown
|
||||
>
|
||||
<search-icon slot="rightIcon" class="h-5" />
|
||||
</sw-input>
|
||||
|
||||
<div class="flex mb-6 ml-3" role="group" aria-label="First group">
|
||||
<sw-dropdown
|
||||
:close-on-select="false"
|
||||
align="left"
|
||||
class="filter-container"
|
||||
position="bottom-start"
|
||||
>
|
||||
<a slot="activator" href="#">
|
||||
<base-button
|
||||
class="inv-button inv-filter-fields-btn"
|
||||
color="default"
|
||||
size="medium"
|
||||
>
|
||||
<font-awesome-icon icon="filter" />
|
||||
</base-button>
|
||||
</a>
|
||||
<div class="filter-title">
|
||||
<sw-button slot="activator" size="md" variant="gray-light">
|
||||
<filter-icon class="h-5" />
|
||||
</sw-button>
|
||||
|
||||
<div class="px-2 py-1 mb-2 border-b border-gray-200 border-solid">
|
||||
{{ $t('general.sort_by') }}
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
id="filter_invoice_date"
|
||||
v-model="searchData.orderByField"
|
||||
type="radio"
|
||||
name="filter"
|
||||
class="inv-radio"
|
||||
value="invoice_date"
|
||||
@change="onSearch"
|
||||
/>
|
||||
<label class="inv-label" for="filter_invoice_date">{{
|
||||
$t('invoices.invoice_date')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
id="filter_due_date"
|
||||
v-model="searchData.orderByField"
|
||||
type="radio"
|
||||
name="filter"
|
||||
class="inv-radio"
|
||||
value="due_date"
|
||||
@change="onSearch"
|
||||
/>
|
||||
<label class="inv-label" for="filter_due_date">{{
|
||||
$t('invoices.due_date')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
id="filter_invoice_number"
|
||||
v-model="searchData.orderByField"
|
||||
type="radio"
|
||||
name="filter"
|
||||
class="inv-radio"
|
||||
value="invoice_number"
|
||||
@change="onSearch"
|
||||
/>
|
||||
<label class="inv-label" for="filter_invoice_number">{{
|
||||
$t('invoices.invoice_number')
|
||||
}}</label>
|
||||
</div>
|
||||
</v-dropdown>
|
||||
<base-button
|
||||
|
||||
<sw-dropdown-item class="flex px-1 py-1 cursor-pointer">
|
||||
<sw-input-group class="-mt-2 text-sm font-normal">
|
||||
<sw-radio
|
||||
id="filter_invoice_date"
|
||||
v-model="searchData.orderByField"
|
||||
:label="$t('invoices.invoice_date')"
|
||||
name="filter"
|
||||
size="sm"
|
||||
value="invoice_date"
|
||||
@change="onSearch"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item class="flex px-1 py-1 cursor-pointer">
|
||||
<sw-input-group class="-mt-2 font-normal">
|
||||
<sw-radio
|
||||
id="filter_due_date"
|
||||
:label="$t('invoices.due_date')"
|
||||
v-model="searchData.orderByField"
|
||||
name="filter"
|
||||
size="sm"
|
||||
value="due_date"
|
||||
@change="onSearch"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item class="flex px-1 py-1 cursor-pointer">
|
||||
<sw-input-group class="-mt-2 font-normal">
|
||||
<sw-radio
|
||||
id="filter_invoice_number"
|
||||
v-model="searchData.orderByField"
|
||||
size="sm"
|
||||
type="radio"
|
||||
name="filter"
|
||||
:label="$t('invoices.invoice_number')"
|
||||
value="invoice_number"
|
||||
@change="onSearch"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
|
||||
<sw-button
|
||||
class="ml-1"
|
||||
v-tooltip.top-center="{ content: getOrderName }"
|
||||
class="inv-button inv-filter-sorting-btn"
|
||||
color="default"
|
||||
size="medium"
|
||||
size="md"
|
||||
variant="gray-light"
|
||||
@click="sortData"
|
||||
>
|
||||
<font-awesome-icon v-if="getOrderBy" icon="sort-amount-up" />
|
||||
<font-awesome-icon v-else icon="sort-amount-down" />
|
||||
</base-button>
|
||||
<sort-ascending-icon v-if="getOrderBy" class="h-5" />
|
||||
<sort-descending-icon v-else class="h-5" />
|
||||
</sw-button>
|
||||
</div>
|
||||
</div>
|
||||
<base-loader v-if="isSearching" />
|
||||
<div v-else class="side-content">
|
||||
|
||||
<base-loader v-if="isSearching" :show-bg-overlay="true" />
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="h-full pb-32 overflow-y-scroll border-l border-gray-200 border-solid sw-scroll"
|
||||
>
|
||||
<router-link
|
||||
v-for="(invoice, index) in invoices"
|
||||
:to="`/admin/invoices/${invoice.id}/view`"
|
||||
:id="'invoice-' + invoice.id"
|
||||
:key="index"
|
||||
class="side-invoice"
|
||||
:class="[
|
||||
'flex justify-between p-4 items-center cursor-pointer hover:bg-gray-100 border-l-4 border-transparent',
|
||||
{
|
||||
'bg-gray-100 border-l-4 border-primary-500 border-solid': hasActiveUrl(
|
||||
invoice.id
|
||||
),
|
||||
},
|
||||
]"
|
||||
style="border-bottom: 1px solid rgba(185, 193, 209, 0.41)"
|
||||
>
|
||||
<div class="left">
|
||||
<div class="inv-name">{{ invoice.user.name }}</div>
|
||||
<div class="inv-number">{{ invoice.invoice_number }}</div>
|
||||
<div class="flex-2">
|
||||
<div
|
||||
:class="'inv-status-' + invoice.status.toLowerCase()"
|
||||
class="inv-status"
|
||||
class="pr-2 mb-2 text-sm not-italic font-normal leading-5 text-black capitalize truncate"
|
||||
>
|
||||
{{ invoice.user.name }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-1 mb-2 text-xs not-italic font-medium leading-5 text-gray-600"
|
||||
>
|
||||
{{ invoice.invoice_number }}
|
||||
</div>
|
||||
|
||||
<sw-badge
|
||||
class="px-1 text-xs"
|
||||
:bg-color="$utils.getBadgeStatusColor(invoice.status).bgColor"
|
||||
:color="$utils.getBadgeStatusColor(invoice.status).color"
|
||||
:font-size="$utils.getBadgeStatusColor(invoice.status).fontSize"
|
||||
>
|
||||
{{ invoice.status }}
|
||||
</div>
|
||||
</sw-badge>
|
||||
</div>
|
||||
<div class="right">
|
||||
|
||||
<div class="flex-1 whitespace-no-wrap right">
|
||||
<div
|
||||
class="inv-amount"
|
||||
class="mb-2 text-xl not-italic font-semibold leading-8 text-right text-gray-900"
|
||||
v-html="
|
||||
$utils.formatMoney(invoice.due_amount, invoice.user.currency)
|
||||
"
|
||||
/>
|
||||
<div class="inv-date">{{ invoice.formattedInvoiceDate }}</div>
|
||||
<div
|
||||
class="text-sm not-italic font-normal leading-5 text-right text-gray-600"
|
||||
>
|
||||
{{ invoice.formattedInvoiceDate }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<p v-if="!invoices.length" class="no-result">
|
||||
|
||||
<p
|
||||
v-if="!invoices.length"
|
||||
class="flex justify-center px-4 mt-5 text-sm text-gray-600"
|
||||
>
|
||||
{{ $t('invoices.no_matching_invoices') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="invoice-view-page-container">
|
||||
<iframe :src="`${shareableLink}`" class="frame-style" />
|
||||
|
||||
<div
|
||||
class="flex flex-col min-h-0 mt-8 overflow-hidden"
|
||||
style="height: 75vh"
|
||||
>
|
||||
<iframe
|
||||
:src="`${shareableLink}`"
|
||||
class="flex-1 border border-gray-400 border-solid rounded-md frame-style"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</base-page>
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import {
|
||||
DotsHorizontalIcon,
|
||||
FilterIcon,
|
||||
SortAscendingIcon,
|
||||
SortDescendingIcon,
|
||||
SearchIcon,
|
||||
LinkIcon,
|
||||
TrashIcon,
|
||||
PencilIcon,
|
||||
} from '@vue-hero-icons/solid'
|
||||
|
||||
const _ = require('lodash')
|
||||
export default {
|
||||
data () {
|
||||
components: {
|
||||
DotsHorizontalIcon,
|
||||
FilterIcon,
|
||||
SortAscendingIcon,
|
||||
SortDescendingIcon,
|
||||
SearchIcon,
|
||||
LinkIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
count: null,
|
||||
@@ -207,37 +263,49 @@ export default {
|
||||
searchData: {
|
||||
orderBy: null,
|
||||
orderByField: null,
|
||||
searchText: null
|
||||
searchText: null,
|
||||
},
|
||||
isRequestOnGoing: false,
|
||||
isSearching: false,
|
||||
isSendingEmail: false,
|
||||
isMarkingAsSent: false
|
||||
isMarkingAsSent: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getOrderBy () {
|
||||
if (this.searchData.orderBy === 'asc' || this.searchData.orderBy == null) {
|
||||
pageTitle() {
|
||||
return this.invoice.invoice_number
|
||||
},
|
||||
getOrderBy() {
|
||||
if (
|
||||
this.searchData.orderBy === 'asc' ||
|
||||
this.searchData.orderBy == null
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
getOrderName () {
|
||||
getOrderName() {
|
||||
if (this.getOrderBy) {
|
||||
return this.$t('general.ascending')
|
||||
}
|
||||
return this.$t('general.descending')
|
||||
},
|
||||
shareableLink () {
|
||||
shareableLink() {
|
||||
return `/invoices/pdf/${this.invoice.unique_hash}`
|
||||
}
|
||||
},
|
||||
getCurrentInvoiceId() {
|
||||
if (this.invoice && this.invoice.id) {
|
||||
return this.invoice.id
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route (to, from) {
|
||||
$route(to, from) {
|
||||
this.loadInvoice()
|
||||
}
|
||||
},
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
this.loadInvoices()
|
||||
this.loadInvoice()
|
||||
this.onSearch = _.debounce(this.onSearch, 500)
|
||||
@@ -251,32 +319,60 @@ export default {
|
||||
'sendEmail',
|
||||
'deleteInvoice',
|
||||
'selectInvoice',
|
||||
'fetchViewInvoice'
|
||||
'fetchInvoice',
|
||||
]),
|
||||
async loadInvoices () {
|
||||
let response = await this.fetchInvoices()
|
||||
|
||||
...mapActions('modal', ['openModal']),
|
||||
|
||||
hasActiveUrl(id) {
|
||||
return this.$route.params.id == id
|
||||
},
|
||||
|
||||
async loadInvoices() {
|
||||
let response = await this.fetchInvoices({ limit: 'all' })
|
||||
if (response.data) {
|
||||
this.invoices = response.data.invoices.data
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.scrollToInvoice()
|
||||
}, 500)
|
||||
},
|
||||
async loadInvoice () {
|
||||
let response = await this.fetchViewInvoice(this.$route.params.id)
|
||||
scrollToInvoice() {
|
||||
const el = document.getElementById(`invoice-${this.$route.params.id}`)
|
||||
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth' })
|
||||
el.classList.add('shake')
|
||||
}
|
||||
},
|
||||
async loadInvoice() {
|
||||
let response = await this.fetchInvoice(this.$route.params.id)
|
||||
|
||||
if (response.data) {
|
||||
this.invoice = response.data.invoice
|
||||
}
|
||||
},
|
||||
async onSearch () {
|
||||
async onSearch() {
|
||||
let data = ''
|
||||
if (this.searchData.searchText !== '' && this.searchData.searchText !== null && this.searchData.searchText !== undefined) {
|
||||
if (
|
||||
this.searchData.searchText !== '' &&
|
||||
this.searchData.searchText !== null &&
|
||||
this.searchData.searchText !== undefined
|
||||
) {
|
||||
data += `search=${this.searchData.searchText}&`
|
||||
}
|
||||
|
||||
if (this.searchData.orderBy !== null && this.searchData.orderBy !== undefined) {
|
||||
if (
|
||||
this.searchData.orderBy !== null &&
|
||||
this.searchData.orderBy !== undefined
|
||||
) {
|
||||
data += `orderBy=${this.searchData.orderBy}&`
|
||||
}
|
||||
|
||||
if (this.searchData.orderByField !== null && this.searchData.orderByField !== undefined) {
|
||||
if (
|
||||
this.searchData.orderByField !== null &&
|
||||
this.searchData.orderByField !== undefined
|
||||
) {
|
||||
data += `orderByField=${this.searchData.orderByField}`
|
||||
}
|
||||
this.isSearching = true
|
||||
@@ -286,7 +382,7 @@ export default {
|
||||
this.invoices = response.data.invoices.data
|
||||
}
|
||||
},
|
||||
sortData () {
|
||||
sortData() {
|
||||
if (this.searchData.orderBy === 'asc') {
|
||||
this.searchData.orderBy = 'desc'
|
||||
this.onSearch()
|
||||
@@ -296,77 +392,68 @@ export default {
|
||||
this.onSearch()
|
||||
return true
|
||||
},
|
||||
async onMarkAsSent () {
|
||||
window.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('invoices.invoice_mark_as_sent'),
|
||||
icon: '/assets/icon/check-circle-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (value) => {
|
||||
if (value) {
|
||||
this.isMarkingAsSent = true
|
||||
let response = await this.markAsSent({id: this.invoice.id})
|
||||
this.isMarkingAsSent = false
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('invoices.marked_as_sent_message'))
|
||||
async onMarkAsSent() {
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('invoices.invoice_mark_as_sent'),
|
||||
icon: '/assets/icon/check-circle-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (value) => {
|
||||
if (value) {
|
||||
this.isMarkingAsSent = true
|
||||
let response = await this.markAsSent({
|
||||
id: this.invoice.id,
|
||||
status: 'SENT',
|
||||
})
|
||||
this.isMarkingAsSent = false
|
||||
if (response.data) {
|
||||
this.invoice.status = 'SENT'
|
||||
window.toastr['success'](
|
||||
this.$tc('invoices.marked_as_sent_message')
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async onSendInvoice() {
|
||||
this.openModal({
|
||||
title: this.$t('invoices.send_invoice'),
|
||||
componentName: 'SendInvoiceModal',
|
||||
id: this.invoice.id,
|
||||
data: this.invoice,
|
||||
})
|
||||
},
|
||||
async onSendInvoice () {
|
||||
window.swal({
|
||||
title: this.$tc('general.are_you_sure'),
|
||||
text: this.$tc('invoices.confirm_send_invoice'),
|
||||
icon: '/assets/icon/paper-plane-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (value) => {
|
||||
if (value) {
|
||||
this.isSendingEmail = true
|
||||
let response = await this.sendEmail({id: this.invoice.id})
|
||||
this.isSendingEmail = false
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](this.$tc('invoices.send_invoice_successfully'))
|
||||
return true
|
||||
}
|
||||
if (response.data.error === 'user_email_does_not_exist') {
|
||||
window.toastr['error'](this.$tc('invoices.user_email_does_not_exist'))
|
||||
return false
|
||||
}
|
||||
window.toastr['error'](this.$tc('invoices.something_went_wrong'))
|
||||
}
|
||||
})
|
||||
},
|
||||
copyPdfUrl () {
|
||||
copyPdfUrl() {
|
||||
let pdfUrl = `${window.location.origin}/invoices/pdf/${this.invoice.unique_hash}`
|
||||
|
||||
let response = this.$utils.copyTextToClipboard(pdfUrl)
|
||||
|
||||
window.toastr['success'](this.$tc('Copied PDF url to clipboard!'))
|
||||
|
||||
window.toastr['success'](this.$t('general.copied_pdf_url_clipboard'))
|
||||
},
|
||||
async removeInvoice (id) {
|
||||
this.selectInvoice([parseInt(id)])
|
||||
this.id = id
|
||||
window.swal({
|
||||
title: 'Deleted',
|
||||
text: 'you will not be able to recover this invoice!',
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (value) => {
|
||||
if (value) {
|
||||
let request = await this.deleteInvoice(this.id)
|
||||
if (request.data.success) {
|
||||
window.toastr['success'](this.$tc('invoices.deleted_message', 1))
|
||||
this.$router.push('/admin/invoices')
|
||||
} else if (request.data.error) {
|
||||
window.toastr['error'](request.data.message)
|
||||
async removeInvoice(id) {
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: 'you will not be able to recover this invoice!',
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (value) => {
|
||||
if (value) {
|
||||
let request = await this.deleteInvoice({ ids: [id] })
|
||||
if (request.data.success) {
|
||||
window.toastr['success'](this.$tc('invoices.deleted_message', 1))
|
||||
this.$router.push('/admin/invoices')
|
||||
} else if (request.data.error) {
|
||||
window.toastr['error'](request.data.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,132 +1,161 @@
|
||||
<template>
|
||||
<div class="main-content item-create">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ isEdit ? $t('items.edit_item') : $t('items.new_item') }}</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/items">{{ $tc('items.item',2) }}</router-link></li>
|
||||
<li class="breadcrumb-item"><a href="#"> {{ isEdit ? $t('items.edit_item') : $t('items.new_item') }}</a></li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<form action="" @submit.prevent="submitItem">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ $t('items.name') }}</label><span class="text-danger"> *</span>
|
||||
<base-input
|
||||
v-model.trim="formData.name"
|
||||
:invalid="$v.formData.name.$error"
|
||||
focus
|
||||
type="text"
|
||||
name="name"
|
||||
@input="$v.formData.name.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.name.$error">
|
||||
<span v-if="!$v.formData.name.required" class="text-danger">{{ $t('validation.required') }} </span>
|
||||
<span v-if="!$v.formData.name.minLength" class="text-danger">
|
||||
{{ $tc('validation.name_min_length', $v.formData.name.$params.minLength.min, { count: $v.formData.name.$params.minLength.min }) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ $t('items.price') }}</label><span class="text-danger"> *</span>
|
||||
<div class="base-input">
|
||||
<money
|
||||
:class="{'invalid' : $v.formData.price.$error}"
|
||||
v-model="price"
|
||||
v-bind="defaultCurrencyForInput"
|
||||
class="input-field"
|
||||
<base-page>
|
||||
<!-- Page Header -->
|
||||
<sw-page-header class="mb-3" :title="pageTitle">
|
||||
<sw-breadcrumb slot="breadcrumbs">
|
||||
<sw-breadcrumb-item to="/admin/dashboard" :title="$t('general.home')" />
|
||||
<sw-breadcrumb-item to="/admin/items" :title="$tc('items.item', 2)" />
|
||||
<sw-breadcrumb-item
|
||||
v-if="$route.name === 'items.edit'"
|
||||
to="#"
|
||||
:title="$t('items.edit_item')"
|
||||
active
|
||||
/>
|
||||
<sw-breadcrumb-item
|
||||
v-else
|
||||
to="#"
|
||||
:title="$t('items.new_item')"
|
||||
active
|
||||
/>
|
||||
</sw-breadcrumb>
|
||||
</sw-page-header>
|
||||
|
||||
<div class="grid grid-cols-12">
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<form action="" @submit.prevent="submitItem">
|
||||
<sw-card>
|
||||
<sw-input-group
|
||||
:label="$t('items.name')"
|
||||
:error="nameError"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<sw-input
|
||||
v-model.trim="formData.name"
|
||||
:invalid="$v.formData.name.$error"
|
||||
class="mt-2"
|
||||
focus
|
||||
type="text"
|
||||
name="name"
|
||||
@input="$v.formData.name.$touch()"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('items.price')"
|
||||
:error="priceError"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<sw-money
|
||||
v-model.trim="price"
|
||||
:invalid="$v.formData.price.$error"
|
||||
:currency="defaultCurrencyForInput"
|
||||
class="relative w-full focus:border focus:border-solid focus:border-primary-500"
|
||||
@input="$v.formData.price.$touch()"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group :label="$t('items.unit')" class="mb-4">
|
||||
<sw-select
|
||||
v-model="formData.unit"
|
||||
class="mt-2"
|
||||
:options="itemUnits"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('items.select_a_unit')"
|
||||
label="name"
|
||||
>
|
||||
<div
|
||||
slot="afterList"
|
||||
class="flex items-center justify-center w-full px-6 py-3 text-base bg-gray-200 cursor-pointer text-primary-400"
|
||||
@click="addItemUnit"
|
||||
>
|
||||
<shopping-cart-icon
|
||||
class="h-5 mr-2 -ml-2 text-center text-primary-400"
|
||||
/>
|
||||
|
||||
<label class="ml-2 text-sm leading-none text-primary-400">{{
|
||||
$t('settings.customization.items.add_item_unit')
|
||||
}}</label>
|
||||
</div>
|
||||
<div v-if="$v.formData.price.$error">
|
||||
<span v-if="!$v.formData.price.required" class="text-danger">{{ $t('validation.required') }} </span>
|
||||
<span v-if="!$v.formData.price.maxLength" class="text-danger">{{ $t('validation.price_maxlength') }}</span>
|
||||
<span v-if="!$v.formData.price.minValue" class="text-danger">{{ $t('validation.price_minvalue') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ $t('items.unit') }}</label>
|
||||
<base-select
|
||||
v-model="formData.unit"
|
||||
:options="itemUnits"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('items.select_a_unit')"
|
||||
label="name"
|
||||
>
|
||||
<div slot="afterList">
|
||||
<button type="button" class="list-add-button" @click="addItemUnit">
|
||||
<font-awesome-icon class="icon" icon="cart-plus" />
|
||||
<label>{{ $t('settings.customization.items.add_item_unit') }}</label>
|
||||
</button>
|
||||
</div>
|
||||
</base-select>
|
||||
</div>
|
||||
<div v-if="isTaxPerItem" class="form-group">
|
||||
<label>{{ $t('items.taxes') }}</label>
|
||||
<base-select
|
||||
v-model="formData.taxes"
|
||||
:options="getTaxTypes"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="true"
|
||||
:multiple="true"
|
||||
track-by="tax_type_id"
|
||||
label="tax_name"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">{{ $t('items.description') }}</label>
|
||||
<base-text-area
|
||||
v-model="formData.description"
|
||||
rows="2"
|
||||
name="description"
|
||||
@input="$v.formData.description.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.description.$error">
|
||||
<span v-if="!$v.formData.description.maxLength" class="text-danger">
|
||||
{{ $t('validation.description_maxlength') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<base-button
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
icon="save"
|
||||
color="theme"
|
||||
type="submit"
|
||||
class="collapse-button"
|
||||
>
|
||||
{{ isEdit ? $t('items.update_item') : $t('items.save_item') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</sw-select>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
v-if="isTaxPerItem"
|
||||
:label="$t('items.taxes')"
|
||||
class="mb-4"
|
||||
>
|
||||
<sw-select
|
||||
v-model="formData.taxes"
|
||||
class="mt-2"
|
||||
:options="getTaxTypes"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="true"
|
||||
:multiple="true"
|
||||
track-by="tax_type_id"
|
||||
label="tax_name"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('items.description')"
|
||||
:error="descriptionError"
|
||||
class="mb-4"
|
||||
>
|
||||
<sw-textarea
|
||||
v-model="formData.description"
|
||||
rows="2"
|
||||
name="description"
|
||||
@input="$v.formData.description.$touch()"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<div class="mb-4">
|
||||
<sw-button
|
||||
:loading="isLoading"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
class="flex w-full justify-center md:w-auto"
|
||||
>
|
||||
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
|
||||
{{ isEdit ? $t('items.update_item') : $t('items.save_item') }}
|
||||
</sw-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</sw-card>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</base-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
const { required, minLength, numeric, minValue, maxLength } = require('vuelidate/lib/validators')
|
||||
import { ShoppingCartIcon } from '@vue-hero-icons/solid'
|
||||
import TheSiteHeaderVue from '../layouts/partials/TheSiteHeader.vue'
|
||||
const {
|
||||
required,
|
||||
minLength,
|
||||
numeric,
|
||||
minValue,
|
||||
maxLength,
|
||||
} = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: {
|
||||
validationMixin
|
||||
components: {
|
||||
ShoppingCartIcon,
|
||||
},
|
||||
data () {
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
title: 'Add Item',
|
||||
units: [],
|
||||
taxes: [],
|
||||
taxPerItem: '',
|
||||
|
||||
formData: {
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -134,142 +163,246 @@ export default {
|
||||
unit_id: null,
|
||||
unit: null,
|
||||
taxes: [],
|
||||
tax_per_item: false
|
||||
},
|
||||
|
||||
money: {
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
prefix: '$ ',
|
||||
precision: 2,
|
||||
masked: false
|
||||
}
|
||||
masked: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrencyForInput'
|
||||
]),
|
||||
...mapGetters('item', [
|
||||
'itemUnits'
|
||||
]),
|
||||
...mapGetters('company', ['defaultCurrencyForInput']),
|
||||
|
||||
...mapGetters('item', ['itemUnits']),
|
||||
|
||||
...mapGetters('taxType', ['taxTypes']),
|
||||
|
||||
price: {
|
||||
get: function () {
|
||||
return this.formData.price / 100
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.formData.price = newValue * 100
|
||||
}
|
||||
},
|
||||
},
|
||||
...mapGetters('taxType', [
|
||||
'taxTypes'
|
||||
]),
|
||||
isEdit () {
|
||||
|
||||
pageTitle() {
|
||||
if (this.$route.name === 'items.edit') {
|
||||
return this.$t('items.edit_item')
|
||||
}
|
||||
return this.$t('items.new_item')
|
||||
},
|
||||
|
||||
...mapGetters('taxType', ['taxTypes']),
|
||||
|
||||
isEdit() {
|
||||
if (this.$route.name === 'items.edit') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
isTaxPerItem () {
|
||||
|
||||
isTaxPerItem() {
|
||||
return this.taxPerItem === 'YES' ? 1 : 0
|
||||
},
|
||||
getTaxTypes () {
|
||||
return this.taxTypes.map(tax => {
|
||||
return {...tax, tax_type_id: tax.id, tax_name: tax.name + ' (' + tax.percent + '%)'}
|
||||
|
||||
getTaxTypes() {
|
||||
return this.taxTypes.map((tax) => {
|
||||
return {
|
||||
...tax,
|
||||
tax_type_id: tax.id,
|
||||
tax_name: tax.name + ' (' + tax.percent + '%)',
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
nameError() {
|
||||
if (!this.$v.formData.name.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.formData.name.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
|
||||
if (!this.$v.formData.name.minLength) {
|
||||
return this.$tc(
|
||||
'validation.name_min_length',
|
||||
this.$v.formData.name.$params.minLength.min,
|
||||
{ count: this.$v.formData.name.$params.minLength.min }
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
priceError() {
|
||||
if (!this.$v.formData.price.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.formData.price.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
|
||||
if (!this.$v.formData.price.maxLength) {
|
||||
return this.$t('validation.price_maxlength')
|
||||
}
|
||||
|
||||
if (!this.$v.formData.price.minValue) {
|
||||
return this.$t('validation.price_minvalue')
|
||||
}
|
||||
},
|
||||
|
||||
descriptionError() {
|
||||
if (!this.$v.formData.description.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.formData.description.maxLength) {
|
||||
return this.$t('validation.description_maxlength')
|
||||
}
|
||||
},
|
||||
},
|
||||
created () {
|
||||
|
||||
created() {
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.setTaxPerItem()
|
||||
if (this.isEdit) {
|
||||
this.loadEditData()
|
||||
}
|
||||
|
||||
this.$v.formData.$reset()
|
||||
},
|
||||
|
||||
validations: {
|
||||
formData: {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(3)
|
||||
minLength: minLength(3),
|
||||
},
|
||||
|
||||
price: {
|
||||
required,
|
||||
numeric,
|
||||
maxLength: maxLength(20),
|
||||
minValue: minValue(0.1)
|
||||
minValue: minValue(0.1),
|
||||
},
|
||||
|
||||
description: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
}
|
||||
maxLength: maxLength(255),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('item', [
|
||||
'addItem',
|
||||
'fetchItem',
|
||||
'updateItem'
|
||||
'updateItem',
|
||||
'fetchItemUnits',
|
||||
]),
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
async setTaxPerItem () {
|
||||
let res = await axios.get('/api/settings/get-setting?key=tax_per_item')
|
||||
if (res.data && res.data.tax_per_item === 'YES') {
|
||||
this.taxPerItem = 'YES'
|
||||
} else {
|
||||
this.taxPerItem = 'FALSE'
|
||||
|
||||
...mapActions('taxType', ['fetchTaxTypes']),
|
||||
|
||||
...mapActions('company', ['fetchCompanySettings']),
|
||||
|
||||
...mapActions('modal', ['openModal']),
|
||||
|
||||
async setTaxPerItem() {
|
||||
let response = await this.fetchCompanySettings(['tax_per_item'])
|
||||
|
||||
if (response.data) {
|
||||
response.data.tax_per_item === 'YES'
|
||||
? (this.taxPerItem = 'YES')
|
||||
: (this.taxPerItem = 'NO')
|
||||
}
|
||||
},
|
||||
async loadEditData () {
|
||||
let response = await this.fetchItem(this.$route.params.id)
|
||||
|
||||
this.formData = {...response.data.item, unit: null}
|
||||
this.formData.taxes = response.data.item.taxes.map(tax => {
|
||||
return {...tax, tax_name: tax.name + ' (' + tax.percent + '%)'}
|
||||
})
|
||||
async loadData() {
|
||||
if (this.isEdit) {
|
||||
let response = await this.fetchItem(this.$route.params.id)
|
||||
|
||||
this.formData.unit = this.itemUnits.find(_unit => response.data.item.unit_id === _unit.id)
|
||||
this.fractional_price = response.data.item.price
|
||||
this.formData = { ...response.data.item, unit: null }
|
||||
|
||||
this.fractional_price = response.data.item.price
|
||||
|
||||
if (this.formData.unit_id) {
|
||||
await this.fetchItemUnits({ limit: 'all' })
|
||||
this.formData.unit = this.itemUnits.find(
|
||||
(_unit) => response.data.item.unit_id === _unit.id
|
||||
)
|
||||
}
|
||||
|
||||
if (this.formData.taxes) {
|
||||
await this.fetchTaxTypes({ limit: 'all' })
|
||||
this.formData.taxes = response.data.item.taxes.map((tax) => {
|
||||
return { ...tax, tax_name: tax.name + '(' + tax.percent + '%)' }
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.fetchItemUnits({ limit: 'all' })
|
||||
this.fetchTaxTypes({ limit: 'all' })
|
||||
}
|
||||
},
|
||||
async submitItem () {
|
||||
|
||||
async submitItem() {
|
||||
this.$v.formData.$touch()
|
||||
|
||||
if (this.$v.$invalid) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.formData.unit) {
|
||||
this.formData.unit_id = this.formData.unit.id
|
||||
}
|
||||
|
||||
let response
|
||||
this.isLoading = true
|
||||
|
||||
if (this.isEdit) {
|
||||
this.isLoading = true
|
||||
response = await this.updateItem(this.formData)
|
||||
} else {
|
||||
let data = {
|
||||
...this.formData,
|
||||
taxes: this.formData.taxes.map(tax => {
|
||||
taxes: this.formData.taxes.map((tax) => {
|
||||
return {
|
||||
tax_type_id: tax.id,
|
||||
amount: ((this.formData.price * tax.percent) / 100),
|
||||
amount: (this.formData.price * tax.percent) / 100,
|
||||
percent: tax.percent,
|
||||
name: tax.name,
|
||||
collective_tax: 0
|
||||
collective_tax: 0,
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
response = await this.addItem(data)
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
this.isLoading = false
|
||||
window.toastr['success'](this.$tc('items.updated_message'))
|
||||
this.$router.push('/admin/items')
|
||||
return true
|
||||
|
||||
if (!this.isEdit) {
|
||||
window.toastr['success'](this.$tc('items.created_message'))
|
||||
this.$router.push('/admin/items')
|
||||
return true
|
||||
} else {
|
||||
window.toastr['success'](this.$tc('items.updated_message'))
|
||||
this.$router.push('/admin/items')
|
||||
return true
|
||||
}
|
||||
window.toastr['error'](response.data.error)
|
||||
}
|
||||
window.toastr['error'](response.data.error)
|
||||
},
|
||||
async addItemUnit () {
|
||||
|
||||
async addItemUnit() {
|
||||
this.openModal({
|
||||
'title': this.$t('settings.customization.items.add_item_unit'),
|
||||
'componentName': 'ItemUnit'
|
||||
title: this.$t('settings.customization.items.add_item_unit'),
|
||||
componentName: 'ItemUnit',
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,295 +1,342 @@
|
||||
<template>
|
||||
<div class="items main-content">
|
||||
<div class="page-header">
|
||||
<div class="d-flex flex-row">
|
||||
<div>
|
||||
<h3 class="page-title">{{ $tc('items.item', 2) }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="dashboard">
|
||||
{{ $t('general.home') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="#">
|
||||
{{ $tc('items.item', 2) }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="page-actions row">
|
||||
<div class="col-xs-2 mr-4">
|
||||
<base-button
|
||||
v-show="totalItems || filtersApplied"
|
||||
:outline="true"
|
||||
:icon="filterIcon"
|
||||
color="theme"
|
||||
size="large"
|
||||
right-icon
|
||||
@click="toggleFilter"
|
||||
>
|
||||
{{ $t('general.filter') }}
|
||||
</base-button>
|
||||
</div>
|
||||
<router-link slot="item-title" class="col-xs-2" to="items/create">
|
||||
<base-button
|
||||
color="theme"
|
||||
icon="plus"
|
||||
size="large"
|
||||
>
|
||||
{{ $t('items.add_item') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<base-page>
|
||||
<sw-page-header :title="$t('items.title')">
|
||||
<sw-breadcrumb slot="breadcrumbs">
|
||||
<sw-breadcrumb-item to="dashboard" :title="$t('general.home')" />
|
||||
<sw-breadcrumb-item to="#" :title="$tc('items.item', 2)" active />
|
||||
</sw-breadcrumb>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-show="showFilters" class="filter-section">
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label"> {{ $tc('items.name') }} </label>
|
||||
<base-input
|
||||
v-model="filters.name"
|
||||
type="text"
|
||||
name="name"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label"> {{ $tc('items.unit') }} </label>
|
||||
<base-select
|
||||
v-model="filters.unit"
|
||||
:options="itemUnits"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('items.select_a_unit')"
|
||||
label="name"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label"> {{ $tc('items.price') }} </label>
|
||||
<base-input
|
||||
v-model="filters.price"
|
||||
type="text"
|
||||
name="name"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<label class="clear-filter" @click="clearFilter"> {{ $t('general.clear_all') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div v-cloak v-show="showEmptyScreen" class="col-xs-1 no-data-info" align="center">
|
||||
<satellite-icon class="mt-5 mb-4"/>
|
||||
<div class="row" align="center">
|
||||
<label class="col title">{{ $t('items.no_items') }}</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="description col mt-1" align="center">{{ $t('items.list_of_items') }}</label>
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<base-button
|
||||
:outline="true"
|
||||
color="theme"
|
||||
class="mt-3"
|
||||
size="large"
|
||||
@click="$router.push('items/create')"
|
||||
<template slot="actions">
|
||||
<sw-button
|
||||
v-show="totalItems"
|
||||
variant="primary-outline"
|
||||
size="lg"
|
||||
@click="toggleFilter"
|
||||
>
|
||||
{{ $t('items.add_new_item') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
{{ $t('general.filter') }}
|
||||
<component :is="filterIcon" class="w-4 h-4 ml-2 -mr-1" />
|
||||
</sw-button>
|
||||
|
||||
<div v-show="!showEmptyScreen" class="table-container">
|
||||
<div class="table-actions mt-5">
|
||||
<p class="table-stats">{{ $t('general.showing') }}: <b>{{ items.length }}</b> {{ $t('general.of') }} <b>{{ totalItems }}</b></p>
|
||||
<transition name="fade">
|
||||
<v-dropdown v-if="selectedItems.length" :show-arrow="false">
|
||||
<span slot="activator" href="#" class="table-actions-button dropdown-toggle">
|
||||
<sw-button
|
||||
tag-name="router-link"
|
||||
to="items/create"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
class="ml-4"
|
||||
>
|
||||
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
|
||||
{{ $t('items.add_item') }}
|
||||
</sw-button>
|
||||
</template>
|
||||
</sw-page-header>
|
||||
|
||||
<slide-y-up-transition>
|
||||
<sw-filter-wrapper v-show="showFilters">
|
||||
<sw-input-group :label="$tc('items.name')" class="flex-1 mt-2 ml-0">
|
||||
<sw-input
|
||||
v-model="filters.name"
|
||||
type="text"
|
||||
name="name"
|
||||
class="mt-2"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$tc('items.unit')"
|
||||
class="flex-1 mt-2 ml-0 lg:ml-6"
|
||||
>
|
||||
<sw-select
|
||||
v-model="filters.unit"
|
||||
:options="itemUnits"
|
||||
:searchable="true"
|
||||
class="mt-2"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('items.select_a_unit')"
|
||||
label="name"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$tc('items.price')"
|
||||
class="flex-1 mt-2 ml-0 lg:ml-6"
|
||||
>
|
||||
<sw-input
|
||||
v-model="filters.price"
|
||||
type="text"
|
||||
name="name"
|
||||
class="mt-2"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<label
|
||||
class="absolute text-sm leading-snug text-gray-900 cursor-pointer"
|
||||
style="top: 10px; right: 15px"
|
||||
@click="clearFilter"
|
||||
>
|
||||
{{ $t('general.clear_all') }}</label
|
||||
>
|
||||
</sw-filter-wrapper>
|
||||
</slide-y-up-transition>
|
||||
|
||||
<sw-empty-table-placeholder
|
||||
v-show="showEmptyScreen"
|
||||
:title="$t('items.no_items')"
|
||||
:description="$t('items.list_of_items')"
|
||||
>
|
||||
<satellite-icon class="mt-5 mb-4" />
|
||||
|
||||
<sw-button
|
||||
slot="actions"
|
||||
tag-name="router-link"
|
||||
to="/admin/items/create"
|
||||
size="lg"
|
||||
variant="primary-outline"
|
||||
>
|
||||
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
|
||||
{{ $t('items.add_new_item') }}
|
||||
</sw-button>
|
||||
</sw-empty-table-placeholder>
|
||||
|
||||
<div v-show="!showEmptyScreen" class="relative table-container">
|
||||
<div
|
||||
class="relative flex items-center justify-between h-10 mt-5 list-none border-b-2 border-gray-200 border-solid"
|
||||
>
|
||||
<p class="text-sm">
|
||||
{{ $t('general.showing') }}: <b>{{ items.length }}</b>
|
||||
{{ $t('general.of') }} <b>{{ totalItems }}</b>
|
||||
</p>
|
||||
|
||||
<sw-transition>
|
||||
<sw-dropdown v-if="selectedItems.length">
|
||||
<span
|
||||
slot="activator"
|
||||
class="flex block text-sm font-medium cursor-pointer select-none text-primary-400"
|
||||
>
|
||||
{{ $t('general.actions') }}
|
||||
<chevron-down-icon class="h-5" />
|
||||
</span>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeMultipleItems">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
</transition>
|
||||
|
||||
<sw-dropdown-item @click="removeMultipleItems">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</sw-transition>
|
||||
</div>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="select-all"
|
||||
<div class="absolute z-10 items-center pl-4 mt-2 select-none md:mt-12">
|
||||
<sw-checkbox
|
||||
v-model="selectAllFieldStatus"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="hidden md:inline"
|
||||
@change="selectAllItems"
|
||||
>
|
||||
<label v-show="!isRequestOngoing" for="select-all" class="custom-control-label selectall">
|
||||
<span class="select-all-label">{{ $t('general.select_all') }} </span>
|
||||
</label>
|
||||
/>
|
||||
|
||||
<sw-checkbox
|
||||
v-model="selectAllFieldStatus"
|
||||
:label="$t('general.select_all')"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="md:hidden"
|
||||
@change="selectAllItems"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<table-component
|
||||
<sw-table-component
|
||||
ref="table"
|
||||
:data="fetchData"
|
||||
:show-filter="false"
|
||||
table-class="table"
|
||||
>
|
||||
|
||||
<table-column
|
||||
<sw-table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
cell-class="no-click"
|
||||
>
|
||||
<div slot-scope="row" class="custom-control custom-checkbox">
|
||||
<sw-checkbox
|
||||
:id="row.id"
|
||||
v-model="selectField"
|
||||
:value="row.id"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column :sortable="true" :label="$t('items.name')" show="name">
|
||||
<template slot-scope="row">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
:id="row.id"
|
||||
v-model="selectField"
|
||||
:value="row.id"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
>
|
||||
<label :for="row.id" class="custom-control-label"/>
|
||||
</div>
|
||||
<span>{{ $t('items.name') }}</span>
|
||||
<router-link
|
||||
:to="{ path: `items/${row.id}/edit` }"
|
||||
class="font-medium text-primary-500"
|
||||
>
|
||||
{{ row.name }}
|
||||
</router-link>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
:label="$t('items.name')"
|
||||
show="name"
|
||||
/>
|
||||
<table-column
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('items.unit')"
|
||||
show="unit_name"
|
||||
/>
|
||||
<table-column
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('items.unit') }}</span>
|
||||
|
||||
<span>
|
||||
{{ row.unit_name ? row.unit_name : 'Not selected' }}
|
||||
</span>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('items.price')"
|
||||
show="price"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span> {{ $t('items.price') }} </span>
|
||||
|
||||
<div v-html="$utils.formatMoney(row.price, defaultCurrency)" />
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('items.added_on')"
|
||||
sort-as="created_at"
|
||||
show="formattedCreatedAt"
|
||||
/>
|
||||
<table-column
|
||||
:sortable="false"
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:filterable="false"
|
||||
cell-class="action-dropdown"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span> {{ $t('items.action') }} </span>
|
||||
<v-dropdown>
|
||||
<a slot="activator" href="#">
|
||||
<dot-icon />
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
|
||||
<router-link :to="{path: `items/${row.id}/edit`}" class="dropdown-item">
|
||||
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
|
||||
{{ $t('general.edit') }}
|
||||
</router-link>
|
||||
<sw-dropdown>
|
||||
<dot-icon slot="activator" />
|
||||
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeItems(row.id)">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`items/${row.id}/edit`"
|
||||
>
|
||||
<pencil-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.edit') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="removeItems(row.id)">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</template>
|
||||
</table-column>
|
||||
</table-component>
|
||||
</sw-table-column>
|
||||
</sw-table-component>
|
||||
</div>
|
||||
</div>
|
||||
</base-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import DotIcon from '../../components/icon/DotIcon'
|
||||
import {
|
||||
FilterIcon,
|
||||
XIcon,
|
||||
ChevronDownIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
PlusIcon,
|
||||
} from '@vue-hero-icons/solid'
|
||||
import SatelliteIcon from '../../components/icon/SatelliteIcon'
|
||||
import BaseButton from '../../../js/components/base/BaseButton'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DotIcon,
|
||||
SatelliteIcon,
|
||||
BaseButton
|
||||
FilterIcon,
|
||||
XIcon,
|
||||
PlusIcon,
|
||||
ChevronDownIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
},
|
||||
data () {
|
||||
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
showFilters: false,
|
||||
sortedBy: 'created_at',
|
||||
isRequestOngoing: true,
|
||||
filtersApplied: false,
|
||||
|
||||
filters: {
|
||||
name: '',
|
||||
unit: '',
|
||||
price: ''
|
||||
}
|
||||
price: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters('item', [
|
||||
'items',
|
||||
'selectedItems',
|
||||
'totalItems',
|
||||
'selectAllField',
|
||||
'itemUnits'
|
||||
'itemUnits',
|
||||
]),
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrency'
|
||||
]),
|
||||
showEmptyScreen () {
|
||||
return !this.totalItems && !this.isRequestOngoing && !this.filtersApplied
|
||||
|
||||
...mapGetters('company', ['defaultCurrency']),
|
||||
|
||||
showEmptyScreen() {
|
||||
return !this.totalItems && !this.isRequestOngoing
|
||||
},
|
||||
filterIcon () {
|
||||
return (this.showFilters) ? 'times' : 'filter'
|
||||
|
||||
filterIcon() {
|
||||
return this.showFilters ? 'x-icon' : 'filter-icon'
|
||||
},
|
||||
|
||||
selectField: {
|
||||
get: function () {
|
||||
return this.selectedItems
|
||||
},
|
||||
set: function (val) {
|
||||
this.selectItem(val)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
selectAllFieldStatus: {
|
||||
get: function () {
|
||||
return this.selectAllField
|
||||
},
|
||||
set: function (val) {
|
||||
this.setSelectAllState(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
filters: {
|
||||
handler: 'setFilters',
|
||||
deep: true
|
||||
}
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
|
||||
destroyed () {
|
||||
mounted() {
|
||||
this.fetchItemUnits({ limit: 'all' })
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
if (this.selectAllField) {
|
||||
this.selectAllItems()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('item', [
|
||||
'fetchItems',
|
||||
@@ -297,19 +344,22 @@ export default {
|
||||
'selectItem',
|
||||
'deleteItem',
|
||||
'deleteMultipleItems',
|
||||
'setSelectAllState'
|
||||
'setSelectAllState',
|
||||
'fetchItemUnits',
|
||||
]),
|
||||
refreshTable () {
|
||||
|
||||
refreshTable() {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
async fetchData ({ page, filter, sort }) {
|
||||
|
||||
async fetchData({ page, filter, sort }) {
|
||||
let data = {
|
||||
search: this.filters.name !== null ? this.filters.name : '',
|
||||
unit_id: this.filters.unit !== null ? this.filters.unit.id : '',
|
||||
price: this.filters.price * 100,
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page
|
||||
page,
|
||||
}
|
||||
|
||||
this.isRequestOngoing = true
|
||||
@@ -320,44 +370,43 @@ export default {
|
||||
data: response.data.items.data,
|
||||
pagination: {
|
||||
totalPages: response.data.items.last_page,
|
||||
currentPage: page
|
||||
}
|
||||
currentPage: page,
|
||||
},
|
||||
}
|
||||
},
|
||||
setFilters () {
|
||||
this.filtersApplied = true
|
||||
|
||||
setFilters() {
|
||||
this.refreshTable()
|
||||
},
|
||||
clearFilter () {
|
||||
|
||||
clearFilter() {
|
||||
this.filters = {
|
||||
name: '',
|
||||
unit: '',
|
||||
price: ''
|
||||
price: '',
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.filtersApplied = false
|
||||
})
|
||||
},
|
||||
toggleFilter () {
|
||||
if (this.showFilters && this.filtersApplied) {
|
||||
|
||||
toggleFilter() {
|
||||
if (this.showFilters) {
|
||||
this.clearFilter()
|
||||
this.refreshTable()
|
||||
}
|
||||
|
||||
this.showFilters = !this.showFilters
|
||||
},
|
||||
async removeItems (id) {
|
||||
|
||||
async removeItems(id) {
|
||||
this.id = id
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('items.confirm_delete'),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
dangerMode: true,
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteItem(this.id)
|
||||
let res = await this.deleteItem({ ids: [id] })
|
||||
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$tc('items.deleted_message', 1))
|
||||
this.$refs.table.refresh()
|
||||
@@ -365,7 +414,10 @@ export default {
|
||||
}
|
||||
|
||||
if (res.data.error === 'item_attached') {
|
||||
window.toastr['error'](this.$tc('items.item_attached_message'), this.$t('general.action_failed'))
|
||||
window.toastr['error'](
|
||||
this.$tc('items.item_attached_message'),
|
||||
this.$t('general.action_failed')
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -374,16 +426,18 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
async removeMultipleItems () {
|
||||
|
||||
async removeMultipleItems() {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('items.confirm_delete', 2),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
dangerMode: true,
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteMultipleItems()
|
||||
|
||||
if (res.data.success || res.data.items) {
|
||||
window.toastr['success'](this.$tc('items.deleted_message', 2))
|
||||
this.$refs.table.refresh()
|
||||
@@ -392,7 +446,7 @@ export default {
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,83 +1,61 @@
|
||||
<template>
|
||||
<div class="template-container" v-if="isAppLoaded">
|
||||
<div v-if="isAppLoaded" class="h-full">
|
||||
<base-modal />
|
||||
<site-header/>
|
||||
<site-sidebar type="basic"/>
|
||||
|
||||
<transition
|
||||
name="fade"
|
||||
mode="out-in">
|
||||
<site-header />
|
||||
<div class="flex h-screen pt-16 pb-10 overflow-hidden">
|
||||
<site-sidebar />
|
||||
<router-view />
|
||||
</transition>
|
||||
<site-footer/>
|
||||
</div>
|
||||
<site-footer />
|
||||
</div>
|
||||
<div v-else class="template-container">
|
||||
<font-awesome-icon icon="spinner" class="fa-spin"/>
|
||||
<div v-else class="h-full">
|
||||
<refresh-icon class="h-6 animate-spin" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
import SiteHeader from './partials/TheSiteHeader.vue'
|
||||
import SiteFooter from './partials/TheSiteFooter.vue'
|
||||
import SiteSidebar from './partials/TheSiteSidebar.vue'
|
||||
import Layout from '../../helpers/layout'
|
||||
import BaseModal from '../../components/base/modal/BaseModal'
|
||||
import { RefreshIcon } from '@vue-hero-icons/solid'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SiteHeader, SiteSidebar, SiteFooter, BaseModal
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
'header': 'header'
|
||||
}
|
||||
SiteHeader,
|
||||
SiteSidebar,
|
||||
SiteFooter,
|
||||
BaseModal,
|
||||
RefreshIcon,
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'isAppLoaded'
|
||||
]),
|
||||
...mapGetters(['isAppLoaded']),
|
||||
|
||||
...mapGetters('company', {
|
||||
selectedCompany: 'getSelectedCompany',
|
||||
companies: 'getCompanies'
|
||||
}),
|
||||
|
||||
isShow () {
|
||||
isShow() {
|
||||
return true
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
Layout.set('layout-default')
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.bootstrap().then((res) => {
|
||||
created() {
|
||||
this.bootstrap().then(() => {
|
||||
this.setInitialCompany()
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions(['bootstrap']),
|
||||
|
||||
...mapActions('company', ['setSelectedCompany']),
|
||||
setInitialCompany () {
|
||||
let selectedCompany = Ls.get('selectedCompany') !== null
|
||||
|
||||
if (selectedCompany) {
|
||||
let foundCompany = this.companies.find((company) => company.id === parseInt(selectedCompany))
|
||||
|
||||
if (foundCompany) {
|
||||
this.setSelectedCompany(foundCompany)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.setSelectedCompany(this.companies[0])
|
||||
}
|
||||
}
|
||||
setInitialCompany() {
|
||||
this.setSelectedCompany(this.selectedCompany)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
body {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,55 +1,83 @@
|
||||
<template>
|
||||
<div class="login-page login-3">
|
||||
<div class="site-wrapper">
|
||||
<div class="login-box">
|
||||
<div class="box-wrapper">
|
||||
<div class="logo-main">
|
||||
<a href="/admin">
|
||||
<img
|
||||
src="/assets/img/crater-logo.png"
|
||||
alt="Crater Logo">
|
||||
</a>
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
<div class="page-copyright">
|
||||
<p>{{ $t('layout_login.copyright_crater') }}</p>
|
||||
</div>
|
||||
<div class="grid h-full grid-cols-12 overflow-y-hidden bg-gray-100">
|
||||
<div
|
||||
class="flex items-center justify-center w-full max-w-sm col-span-12 p-4 mx-auto text-gray-900 md:p-8 md:col-span-6 lg:col-span-4 flex-2 md:pb-48 md:pt-40"
|
||||
>
|
||||
<div class="w-full">
|
||||
<a href="/admin">
|
||||
<img
|
||||
src="/assets/img/crater-logo.png"
|
||||
class="block w-48 h-auto max-w-full mb-32 text-primary-400"
|
||||
alt="Crater Logo"
|
||||
/>
|
||||
</a>
|
||||
<router-view></router-view>
|
||||
<div
|
||||
class="pt-24 mt-0 text-sm not-italic font-medium leading-relaxed text-left text-gray-500 md:pt-40"
|
||||
>
|
||||
<p class="mb-3">{{ $t('layout_login.copyright_crater') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-box">
|
||||
<h1>{{ $t('layout_login.super_simple_invoicing') }}<br>
|
||||
{{ $t('layout_login.for_freelancer') }}<br>
|
||||
{{ $t('layout_login.small_businesses') }} <br>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative flex-col items-center justify-center hidden w-full h-full pl-10 bg-no-repeat bg-cover md:col-span-6 lg:col-span-8 md:flex content-box"
|
||||
>
|
||||
<div class="pl-20 xl:pl-0">
|
||||
<h1
|
||||
class="hidden mb-3 text-3xl font-bold leading-normal text-white xl:text-5xl xl:leading-tight md:none lg:block"
|
||||
>
|
||||
{{ $t('layout_login.super_simple_invoicing') }} <br />
|
||||
{{ $t('layout_login.for_freelancer') }} <br />
|
||||
{{ $t('layout_login.small_businesses') }} <br />
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
{{ $t('layout_login.crater_help') }}<br>
|
||||
{{ $t('layout_login.invoices_and_estimates') }}<br>
|
||||
<p
|
||||
class="hidden text-sm not-italic font-normal leading-normal text-gray-100 xl:text-base xl:leading-6 md:none lg:block"
|
||||
>
|
||||
{{ $t('layout_login.crater_help') }}<br />
|
||||
{{ $t('layout_login.invoices_and_estimates') }}<br />
|
||||
</p>
|
||||
|
||||
<div class="content-bottom"/>
|
||||
</div>
|
||||
|
||||
<div class="absolute z-50 w-full bg-no-repeat content-bottom" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
export default {
|
||||
watch: {
|
||||
$route: 'onRouteChange'
|
||||
},
|
||||
mounted () {
|
||||
this.setLayoutClasses()
|
||||
},
|
||||
methods: {
|
||||
setLayoutClasses () {
|
||||
let body = $('body')
|
||||
body.removeClass()
|
||||
body.addClass('login-page login-1 skin-crater')
|
||||
}
|
||||
},
|
||||
onRouteChange () {
|
||||
$('body').removeClass('login-page')
|
||||
}
|
||||
<style lang="scss" scoped>
|
||||
.content-box {
|
||||
background-image: url('/images/login-vector1.svg');
|
||||
}
|
||||
</script>
|
||||
|
||||
.content-bottom {
|
||||
background-image: url('/images/login-vector3.svg');
|
||||
background-size: 100% 100%;
|
||||
height: 300px;
|
||||
right: 32%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.content-box::before {
|
||||
background-image: url('/images/frame.svg');
|
||||
content: '';
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
height: 300px;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 420px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content-box::after {
|
||||
background-image: url('/images/login-vector2.svg');
|
||||
content: '';
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
right: 7.5%;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="site-wrapper">
|
||||
<div class="container">
|
||||
<div class="site-wrapper h-full text-base">
|
||||
<div class="container mx-auto">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
@@ -9,19 +9,7 @@
|
||||
<script type="text/babel">
|
||||
export default {
|
||||
watch: {
|
||||
$route: 'onRouteChange'
|
||||
$route: 'onRouteChange',
|
||||
},
|
||||
mounted () {
|
||||
this.setLayoutBackground()
|
||||
},
|
||||
|
||||
destroyed () {
|
||||
document.body.style.backgroundColor = '#EBF1FA'
|
||||
},
|
||||
methods: {
|
||||
setLayoutBackground () {
|
||||
document.body.style.backgroundColor = '#f9fbff'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
<template>
|
||||
<footer class="site-footer">
|
||||
<div class="text-right">
|
||||
{{ $t('general.powered_by') }}
|
||||
<a
|
||||
href="http://bytefury.com/"
|
||||
target="_blank">{{ $t('general.bytefury') }}
|
||||
</a>
|
||||
</div>
|
||||
<footer
|
||||
class="absolute bottom-0 flex items-center justify-end w-full h-10 py-2 pr-8 text-sm font-normal text-gray-700 bg-white"
|
||||
>
|
||||
{{ $t('general.powered_by') }}
|
||||
|
||||
<a
|
||||
href="http://bytefury.com/"
|
||||
target="_blank"
|
||||
class="pl-1 font-normal text-gray-900"
|
||||
>
|
||||
{{ $t('general.bytefury') }}
|
||||
</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
footer: 'footer'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,116 +1,192 @@
|
||||
<template>
|
||||
<header class="site-header">
|
||||
<a href="/" class="brand-main">
|
||||
<header
|
||||
class="fixed top-0 left-0 z-40 flex items-center justify-between w-full px-4 py-3 md:h-16 md:px-8 bg-gradient-to-r from-primary-500 to-primary-400"
|
||||
>
|
||||
<a
|
||||
href="/admin/dashboard"
|
||||
class="float-none text-lg not-italic font-black tracking-wider text-white brand-main md:float-left font-base"
|
||||
>
|
||||
<img
|
||||
id="logo-white"
|
||||
src="/assets/img/logo-white.png"
|
||||
alt="Crater Logo"
|
||||
class="d-none d-md-inline"
|
||||
>
|
||||
class="hidden h-6 md:block"
|
||||
/>
|
||||
<img
|
||||
id="logo-mobile"
|
||||
src="/assets/img/crater-white-small.png"
|
||||
alt="Laraspace Logo"
|
||||
class="d-md-none">
|
||||
alt="Crater Logo"
|
||||
class="block h-8 md:hidden"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="nav-toggle"
|
||||
@click="onNavToggle"
|
||||
>
|
||||
<div class="hamburger hamburger--arrowturn">
|
||||
<div class="hamburger-box">
|
||||
<div class="hamburger-inner"/>
|
||||
<ul class="float-right h-8 m-0 list-none md:h-9">
|
||||
<global-search class="hidden float-left mr-2 md:block" />
|
||||
|
||||
<a
|
||||
:class="{ 'is-active': isSidebarOpen }"
|
||||
href="#"
|
||||
class="flex float-left p-1 ml-3 overflow-visible text-sm text-black ease-linear bg-white border-0 rounded cursor-pointer md:hidden md:ml-0 hamburger hamburger--arrowturn"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<div class="relative inline-block w-6 h-6">
|
||||
<div class="block hamburger-inner top-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<ul class="action-list">
|
||||
<li>
|
||||
<v-dropdown :show-arrow="false">
|
||||
<a slot="activator" href="#">
|
||||
<font-awesome-icon icon="plus" />
|
||||
</a>
|
||||
|
||||
<li class="relative hidden float-left m-0 md:block">
|
||||
<sw-dropdown>
|
||||
<a
|
||||
slot="activator"
|
||||
href="#"
|
||||
style="padding: 6px"
|
||||
class="inline-block text-sm text-black bg-white rounded-sm"
|
||||
>
|
||||
<plus-icon class="w-6 h-6" />
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
<router-link class="dropdown-item" to="/admin/invoices/create">
|
||||
<font-awesome-icon icon="file-alt" class="dropdown-item-icon" /> <span> {{ $t('invoices.new_invoice') }} </span>
|
||||
</router-link>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<router-link class="dropdown-item" to="/admin/estimates/create">
|
||||
<font-awesome-icon class="dropdown-item-icon" icon="file" /> <span> {{ $t('estimates.new_estimate') }} </span>
|
||||
</router-link>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<router-link class="dropdown-item" to="/admin/customers/create">
|
||||
<font-awesome-icon class="dropdown-item-icon" icon="user" /> <span> {{ $t('customers.new_customer') }} </span>
|
||||
</router-link>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
|
||||
<sw-dropdown-item tag-name="router-link" to="/admin/invoices/create">
|
||||
<document-text-icon class="h-5 mr-2 text-gray-600" />
|
||||
{{ $t('invoices.new_invoice') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item tag-name="router-link" to="/admin/estimates/create">
|
||||
<document-icon class="h-5 mr-2 text-gray-600" />
|
||||
{{ $t('estimates.new_estimate') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item tag-name="router-link" to="/admin/customers/create">
|
||||
<user-icon class="h-5 mr-2 text-gray-600" />
|
||||
{{ $t('customers.new_customer') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</li>
|
||||
<li>
|
||||
<v-dropdown :show-arrow="false">
|
||||
|
||||
<li class="relative block float-left ml-2">
|
||||
<sw-dropdown>
|
||||
<a
|
||||
slot="activator"
|
||||
href="#"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
class="avatar"
|
||||
class="inline-block text-sm text-black bg-white rounded-sm avatar"
|
||||
>
|
||||
<img :src="profilePicture" alt="Avatar">
|
||||
<img
|
||||
:src="profilePicture"
|
||||
alt="Avatar"
|
||||
class="w-8 h-8 rounded-sm md:h-9 md:w-9"
|
||||
/>
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
<router-link class="dropdown-item" to="/admin/settings">
|
||||
<font-awesome-icon icon="cogs" class="dropdown-item-icon"/> <span> {{ $t('navigation.settings') }} </span>
|
||||
</router-link>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<a
|
||||
href="#"
|
||||
class="dropdown-item"
|
||||
@click.prevent="logout"
|
||||
>
|
||||
<font-awesome-icon icon="sign-out-alt" class="dropdown-item-icon"/> <span> {{ $t('navigation.logout') }} </span>
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
|
||||
<sw-dropdown-item tag-name="router-link" to="/admin/settings">
|
||||
<cog-icon class="w-4 h-4 mr-2 text-gray-600" />
|
||||
{{ $t('navigation.settings') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="logout">
|
||||
<logout-icon class="w-4 h-4 mr-2 text-gray-600" />
|
||||
{{ $t('navigation.logout') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import {
|
||||
PlusIcon,
|
||||
DocumentTextIcon,
|
||||
DocumentIcon,
|
||||
UserIcon,
|
||||
CogIcon,
|
||||
} from '@vue-hero-icons/solid'
|
||||
|
||||
import { LogoutIcon } from '@vue-hero-icons/outline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PlusIcon,
|
||||
DocumentTextIcon,
|
||||
DocumentIcon,
|
||||
UserIcon,
|
||||
CogIcon,
|
||||
LogoutIcon,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('userProfile', [
|
||||
'user'
|
||||
]),
|
||||
profilePicture () {
|
||||
if (this.user && this.user.avatar !== null) {
|
||||
return this.user.avatar
|
||||
...mapGetters('user', ['currentUser']),
|
||||
...mapGetters(['isSidebarOpen']),
|
||||
profilePicture() {
|
||||
if (
|
||||
this.currentUser &&
|
||||
this.currentUser.avatar !== null &&
|
||||
this.currentUser.avatar !== 0
|
||||
) {
|
||||
return this.currentUser.avatar
|
||||
} else {
|
||||
return '/images/default-avatar.jpg'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
created () {
|
||||
this.loadData()
|
||||
created() {
|
||||
this.fetchCurrentUser()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('userProfile', [
|
||||
'loadData'
|
||||
]),
|
||||
...mapActions({
|
||||
companySelect: 'changeCompany'
|
||||
}),
|
||||
...mapActions('auth', [
|
||||
'logout'
|
||||
]),
|
||||
onNavToggle () {
|
||||
this.$utils.toggleSidebar()
|
||||
}
|
||||
}
|
||||
...mapActions('user', ['fetchCurrentUser']),
|
||||
...mapActions('auth', ['logout']),
|
||||
...mapActions('modal', ['openModal']),
|
||||
...mapActions(['toggleSidebar']),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.hamburger {
|
||||
transition-property: opacity, filter;
|
||||
transition-duration: 0.15s;
|
||||
}
|
||||
.hamburger-inner {
|
||||
top: 50%;
|
||||
left: 4.5px;
|
||||
right: 4.5px;
|
||||
}
|
||||
.hamburger-inner,
|
||||
.hamburger-inner::before,
|
||||
.hamburger-inner::after {
|
||||
height: 2px;
|
||||
background-color: black;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
transition-property: transform;
|
||||
transition-duration: 0.15s;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
.hamburger-inner::before,
|
||||
.hamburger-inner::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hamburger-inner::before {
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
.hamburger-inner::after {
|
||||
bottom: -5px;
|
||||
}
|
||||
|
||||
.hamburger--arrowturn.is-active .hamburger-inner {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
.hamburger--arrowturn.is-active .hamburger-inner::before {
|
||||
transform: translate3d(5px, 3px, 0) rotate(45deg) scale(0.5, 1);
|
||||
}
|
||||
|
||||
.hamburger--arrowturn.is-active .hamburger-inner::after {
|
||||
transform: translate3d(5px, -3px, 0) rotate(-45deg) scale(0.5, 1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,96 +1,184 @@
|
||||
<template>
|
||||
<div class="sidebar-left">
|
||||
<div class="sidebar-body scroll-pane">
|
||||
<div class="side-nav">
|
||||
<div
|
||||
v-for="(menuItems, index) in menu"
|
||||
<div>
|
||||
<!-- OVERLAY -->
|
||||
<sw-transition type="fade">
|
||||
<div
|
||||
v-show="isSidebarOpen"
|
||||
class="fixed top-0 left-0 z-20 w-full h-full"
|
||||
style="background: rgba(48, 75, 88, 0.5)"
|
||||
@click.prevent="toggleSidebar"
|
||||
></div>
|
||||
</sw-transition>
|
||||
|
||||
<!-- DESKTOP MENU -->
|
||||
<div
|
||||
class="hidden w-56 h-screen pb-32 overflow-y-auto bg-white border-r border-gray-200 border-solid xl:w-64 sw-scroll md:block"
|
||||
>
|
||||
<sw-list
|
||||
v-for="(menuItems, groupIndex) in menuItems"
|
||||
:key="groupIndex"
|
||||
variant="sidebar"
|
||||
>
|
||||
<sw-list-item
|
||||
v-for="(item, index) in menuItems"
|
||||
:title="$t(item.title)"
|
||||
:key="index"
|
||||
class="menu-group"
|
||||
:active="hasActiveUrl(item.route)"
|
||||
:to="item.route"
|
||||
tag-name="router-link"
|
||||
>
|
||||
<router-link
|
||||
v-for="(item, index1) in menuItems"
|
||||
:key="index1"
|
||||
:to="item.route"
|
||||
class="menu-item"
|
||||
@click.native="Toggle"
|
||||
>
|
||||
<font-awesome-icon :icon="item.icon" class="icon menu-icon" /> <span class="ml-3 menu-text">{{ $t(item.title) }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<component slot="icon" :is="item.icon" class="h-5" />
|
||||
</sw-list-item>
|
||||
</sw-list>
|
||||
</div>
|
||||
|
||||
<!-- MOBILE MENU -->
|
||||
<transition
|
||||
enter-class="-translate-x-full"
|
||||
enter-active-class="transition duration-300 ease-in-out transform"
|
||||
enter-to-class="translate-x-0"
|
||||
leave-active-class="transition duration-300 ease-in-out transform"
|
||||
leave-class="translate-x-0"
|
||||
leave-to-class="-translate-x-full"
|
||||
>
|
||||
<div
|
||||
v-show="isSidebarOpen"
|
||||
class="fixed top-0 z-30 w-64 h-screen pt-16 pb-32 overflow-y-auto bg-white border-r border-gray-200 border-solid sw-scroll md:hidden"
|
||||
>
|
||||
<sw-list
|
||||
v-for="(menuItems, groupIndex) in menuItems"
|
||||
:key="groupIndex"
|
||||
variant="sidebar"
|
||||
>
|
||||
<sw-list-item
|
||||
v-for="(item, index) in menuItems"
|
||||
:title="$t(item.title)"
|
||||
:key="index"
|
||||
:active="hasActiveUrl(item.route)"
|
||||
:to="item.route"
|
||||
tag-name="router-link"
|
||||
@click.native="toggleSidebar"
|
||||
>
|
||||
<component slot="icon" :is="item.icon" class="h-5" />
|
||||
</sw-list-item>
|
||||
</sw-list>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
sidebar: 'sidebar',
|
||||
menu: [
|
||||
import {
|
||||
HomeIcon,
|
||||
UserIcon,
|
||||
StarIcon,
|
||||
DocumentIcon,
|
||||
DocumentTextIcon,
|
||||
CreditCardIcon,
|
||||
CalculatorIcon,
|
||||
ChartBarIcon,
|
||||
CogIcon,
|
||||
UsersIcon,
|
||||
} from '@vue-hero-icons/outline'
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HomeIcon,
|
||||
UserIcon,
|
||||
StarIcon,
|
||||
DocumentIcon,
|
||||
DocumentTextIcon,
|
||||
CreditCardIcon,
|
||||
CalculatorIcon,
|
||||
ChartBarIcon,
|
||||
CogIcon,
|
||||
UsersIcon,
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['isSidebarOpen']),
|
||||
...mapGetters('user', ['currentUser']),
|
||||
menuItems() {
|
||||
let menu = [
|
||||
[
|
||||
{
|
||||
title: 'navigation.dashboard',
|
||||
icon: 'tachometer-alt',
|
||||
route: '/admin/dashboard'
|
||||
icon: 'home-icon',
|
||||
route: '/admin/dashboard',
|
||||
},
|
||||
{
|
||||
title: 'navigation.customers',
|
||||
icon: 'user',
|
||||
route: '/admin/customers'
|
||||
icon: 'user-icon',
|
||||
route: '/admin/customers',
|
||||
},
|
||||
{
|
||||
title: 'navigation.items',
|
||||
icon: 'star',
|
||||
route: '/admin/items'
|
||||
}
|
||||
icon: 'star-icon',
|
||||
route: '/admin/items',
|
||||
},
|
||||
],
|
||||
|
||||
[
|
||||
{
|
||||
title: 'navigation.estimates',
|
||||
icon: 'file',
|
||||
route: '/admin/estimates'
|
||||
icon: 'document-icon',
|
||||
route: '/admin/estimates',
|
||||
},
|
||||
{
|
||||
title: 'navigation.invoices',
|
||||
icon: 'file-alt',
|
||||
route: '/admin/invoices'
|
||||
icon: 'document-text-icon',
|
||||
route: '/admin/invoices',
|
||||
},
|
||||
{
|
||||
title: 'navigation.payments',
|
||||
icon: 'credit-card',
|
||||
route: '/admin/payments'
|
||||
icon: 'credit-card-icon',
|
||||
route: '/admin/payments',
|
||||
},
|
||||
{
|
||||
title: 'navigation.expenses',
|
||||
icon: 'space-shuttle',
|
||||
route: '/admin/expenses'
|
||||
}
|
||||
icon: 'calculator-icon',
|
||||
route: '/admin/expenses',
|
||||
},
|
||||
],
|
||||
|
||||
[
|
||||
{
|
||||
title: 'navigation.reports',
|
||||
icon: 'signal',
|
||||
route: '/admin/reports'
|
||||
icon: 'chart-bar-icon',
|
||||
route: '/admin/reports',
|
||||
},
|
||||
{
|
||||
title: 'navigation.settings',
|
||||
icon: 'cog',
|
||||
route: '/admin/settings'
|
||||
}
|
||||
]
|
||||
|
||||
icon: 'cog-icon',
|
||||
route: '/admin/settings',
|
||||
},
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
if (this.currentUser.role == 'super admin') {
|
||||
menu[2] = [
|
||||
{
|
||||
title: 'navigation.users',
|
||||
icon: 'users-icon',
|
||||
route: '/admin/users',
|
||||
},
|
||||
...menu[2],
|
||||
]
|
||||
}
|
||||
|
||||
return menu
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
Toggle () {
|
||||
this.$utils.toggleSidebar()
|
||||
}
|
||||
}
|
||||
...mapActions(['toggleSidebar']),
|
||||
|
||||
hasActiveUrl(url) {
|
||||
this.isActive = true
|
||||
return this.$route.path.indexOf(url) > -1
|
||||
},
|
||||
hasStaticUrl(url) {
|
||||
return this.$route.path.indexOf(url)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,262 +1,375 @@
|
||||
<template>
|
||||
<div class="payment-create main-content">
|
||||
<base-page class="relative payment-create">
|
||||
<form action="" @submit.prevent="submitPaymentData">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ isEdit ? $t('payments.edit_payment') : $t('payments.new_payment') }}</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/payments">{{ $tc('payments.payment', 2) }}</router-link></li>
|
||||
<li class="breadcrumb-item">{{ isEdit ? $t('payments.edit_payment') : $t('payments.new_payment') }}</li>
|
||||
</ol>
|
||||
<div class="page-actions header-button-container">
|
||||
<base-button
|
||||
<sw-page-header class="mb-5" :title="pageTitle">
|
||||
<sw-breadcrumb slot="breadcrumbs">
|
||||
<sw-breadcrumb-item
|
||||
to="/admin/dashboard"
|
||||
:title="$t('general.home')"
|
||||
/>
|
||||
<sw-breadcrumb-item
|
||||
to="/admin/payments"
|
||||
:title="$tc('payments.payment', 2)"
|
||||
/>
|
||||
<sw-breadcrumb-item
|
||||
v-if="$route.name === 'payments.edit'"
|
||||
to="#"
|
||||
:title="$t('payments.edit_payment')"
|
||||
active
|
||||
/>
|
||||
<sw-breadcrumb-item
|
||||
v-else
|
||||
to="#"
|
||||
:title="$t('payments.new_payment')"
|
||||
active
|
||||
/>
|
||||
</sw-breadcrumb>
|
||||
|
||||
<template slot="actions">
|
||||
<sw-button
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
icon="save"
|
||||
color="theme"
|
||||
type="submit">
|
||||
{{ isEdit ? $t('payments.update_payment') : $t('payments.save_payment') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="payment-card card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('payments.date') }}</label><span class="text-danger"> *</span>
|
||||
<base-date-picker
|
||||
v-model="formData.payment_date"
|
||||
:invalid="$v.formData.payment_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@change="$v.formData.payment_date.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.payment_date.$error">
|
||||
<span v-if="!$v.formData.payment_date.required" class="text-danger">{{ $t('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('payments.payment_number') }}</label><span class="text-danger"> *</span>
|
||||
<base-prefix-input
|
||||
:invalid="$v.paymentNumAttribute.$error"
|
||||
v-model.trim="paymentNumAttribute"
|
||||
:prefix="paymentPrefix"
|
||||
@input="$v.paymentNumAttribute.$touch()"
|
||||
/>
|
||||
<div v-if="$v.paymentNumAttribute.$error">
|
||||
<span v-if="!$v.paymentNumAttribute.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
<span v-if="!$v.paymentNumAttribute.numeric" class="text-danger">{{ $tc('validation.numbers_only') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label">{{ $t('payments.customer') }}</label><span class="text-danger"> *</span>
|
||||
<base-select
|
||||
ref="baseSelect"
|
||||
v-model="customer"
|
||||
:invalid="$v.customer.$error"
|
||||
:options="customerList"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:disabled="isEdit"
|
||||
:placeholder="$t('customers.select_a_customer')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
size="lg"
|
||||
class="hidden sm:flex"
|
||||
>
|
||||
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
|
||||
{{
|
||||
isEdit
|
||||
? $t('payments.update_payment')
|
||||
: $t('payments.save_payment')
|
||||
}}
|
||||
</sw-button>
|
||||
</template>
|
||||
</sw-page-header>
|
||||
|
||||
<base-loader v-if="isRequestOnGoing" :show-bg-overlay="true" />
|
||||
|
||||
<sw-card v-else>
|
||||
<div class="grid gap-6 grid-col-1 md:grid-cols-2">
|
||||
<sw-input-group
|
||||
:label="$t('payments.date')"
|
||||
:error="DateError"
|
||||
required
|
||||
>
|
||||
<base-date-picker
|
||||
v-model="formData.payment_date"
|
||||
:invalid="$v.formData.payment_date.$error"
|
||||
:calendar-button="true"
|
||||
class="mt-1"
|
||||
calendar-button-icon="calendar"
|
||||
@change="$v.formData.payment_date.$touch()"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('payments.payment_number')"
|
||||
:error="paymentNumError"
|
||||
required
|
||||
>
|
||||
<sw-input
|
||||
:prefix="`${paymentPrefix} - `"
|
||||
:invalid="$v.paymentNumAttribute.$error"
|
||||
v-model.trim="paymentNumAttribute"
|
||||
class="mt-1"
|
||||
@input="$v.paymentNumAttribute.$touch()"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('payments.customer')"
|
||||
:error="customerError"
|
||||
required
|
||||
>
|
||||
<sw-select
|
||||
v-model="customer"
|
||||
:options="customers"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:disabled="isEdit"
|
||||
:placeholder="$t('customers.select_a_customer')"
|
||||
label="name"
|
||||
class="mt-1"
|
||||
track-by="id"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group :label="$t('payments.invoice')">
|
||||
<sw-select
|
||||
v-model="invoice"
|
||||
:options="invoiceList"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:disabled="isEdit"
|
||||
:placeholder="$t('invoices.select_invoice')"
|
||||
:custom-label="invoiceWithAmount"
|
||||
class="mt-1"
|
||||
track-by="invoice_number"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('payments.amount')"
|
||||
:error="amountError"
|
||||
required
|
||||
>
|
||||
<div class="relative w-full mt-1">
|
||||
<sw-money
|
||||
v-model="amount"
|
||||
:currency="customerCurrency"
|
||||
:invalid="$v.formData.amount.$error"
|
||||
class="relative w-full focus:border focus:border-solid focus:border-primary-500"
|
||||
@input="$v.formData.amount.$touch()"
|
||||
/>
|
||||
<div v-if="$v.customer.$error">
|
||||
<span v-if="!$v.customer.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('payments.invoice') }}</label>
|
||||
<base-select
|
||||
v-model="invoice"
|
||||
:options="invoiceList"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:disabled="isEdit"
|
||||
:placeholder="$t('invoices.select_invoice')"
|
||||
:custom-label="invoiceWithAmount"
|
||||
track-by="invoice_number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('payments.amount') }}</label><span class="text-danger"> *</span>
|
||||
<div class="base-input">
|
||||
<money
|
||||
:class="{'invalid' : $v.formData.amount.$error}"
|
||||
v-model="amount"
|
||||
v-bind="customerCurrency"
|
||||
class="input-field"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="$v.formData.amount.$error">
|
||||
<span v-if="!$v.formData.amount.required" class="text-danger">{{ $t('validation.required') }}</span>
|
||||
<span v-if="!$v.formData.amount.between && $v.formData.amount.numeric && amount <= 0" class="text-danger">{{ $t('validation.payment_greater_than_zero') }}</span>
|
||||
<span v-if="!$v.formData.amount.between && amount > 0" class="text-danger">{{ $t('validation.payment_greater_than_due_amount') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('payments.payment_mode') }}</label>
|
||||
<base-select
|
||||
v-model="formData.payment_method"
|
||||
:options="paymentModes"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('payments.select_payment_mode')"
|
||||
label="name"
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group :label="$t('payments.payment_mode')">
|
||||
<sw-select
|
||||
v-model="formData.payment_method"
|
||||
:options="paymentModes"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('payments.select_payment_mode')"
|
||||
label="name"
|
||||
:maxHeight="150"
|
||||
class="mt-1"
|
||||
>
|
||||
<div slot="afterList">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center w-full px-2 py-2 bg-gray-200 border-none outline-none text-primary-400"
|
||||
@click="addPaymentMode"
|
||||
>
|
||||
<div slot="afterList">
|
||||
<button type="button" class="list-add-button" @click="addPaymentMode">
|
||||
<font-awesome-icon class="icon" icon="cart-plus" />
|
||||
<label>{{ $t('settings.customization.payments.add_payment_mode') }}</label>
|
||||
</button>
|
||||
</div>
|
||||
</base-select>
|
||||
<shopping-cart-icon class="h-5 mr-3 text-primary-400" />
|
||||
<label>{{
|
||||
$t('settings.customization.payments.add_payment_mode')
|
||||
}}</label>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 ">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('payments.note') }}</label>
|
||||
<base-text-area
|
||||
v-model="formData.notes"
|
||||
@input="$v.formData.notes.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.notes.$error">
|
||||
<span v-if="!$v.formData.notes.maxLength" class="text-danger">{{ $t('validation.notes_maxlength') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<div class="form-group collapse-button-container">
|
||||
<base-button
|
||||
:loading="isLoading"
|
||||
icon="save"
|
||||
color="theme"
|
||||
type="submit"
|
||||
class="collapse-button"
|
||||
>
|
||||
{{ $t('payments.save_payment') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
</sw-select>
|
||||
</sw-input-group>
|
||||
</div>
|
||||
|
||||
<div v-if="customFields.length > 0">
|
||||
<div class="grid gap-6 mt-6 grid-col-1 md:grid-cols-2">
|
||||
<sw-input-group
|
||||
v-for="(field, index) in customFields"
|
||||
:label="field.label"
|
||||
:required="field.is_required ? true : false"
|
||||
:key="index"
|
||||
>
|
||||
<component
|
||||
:type="field.type.label"
|
||||
:field="field"
|
||||
:isEdit="isEdit"
|
||||
:is="field.type + 'Field'"
|
||||
:invalid-fields="invalidFields"
|
||||
@update="setCustomFieldValue"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<sw-popup
|
||||
ref="notePopup"
|
||||
class="my-6 text-sm font-semibold leading-5 text-primary-400"
|
||||
>
|
||||
<div slot="activator" class="float-right mt-1">
|
||||
+ {{ $t('general.insert_note') }}
|
||||
</div>
|
||||
<note-select-popup type="Payment" @select="onSelectNote" />
|
||||
</sw-popup>
|
||||
|
||||
<sw-input-group :label="$t('payments.note')" class="mt-6 mb-4">
|
||||
<base-custom-input
|
||||
v-model="formData.notes"
|
||||
:fields="PaymentFields"
|
||||
class="mb-4"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-button
|
||||
:disabled="isLoading"
|
||||
:loading="isLoading"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
class="flex w-full mt-4 sm:hidden md:hidden"
|
||||
>
|
||||
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
|
||||
{{
|
||||
isEdit ? $t('payments.update_payment') : $t('payments.save_payment')
|
||||
}}
|
||||
</sw-button>
|
||||
</sw-card>
|
||||
</form>
|
||||
</div>
|
||||
</base-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import MultiSelect from 'vue-multiselect'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import moment from 'moment'
|
||||
const { required, between, maxLength, numeric } = require('vuelidate/lib/validators')
|
||||
import { ShoppingCartIcon } from '@vue-hero-icons/solid'
|
||||
import CustomFieldsMixin from '../../mixins/customFields'
|
||||
|
||||
const { required, between, numeric } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
components: { MultiSelect },
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
mixins: [CustomFieldsMixin],
|
||||
|
||||
components: { ShoppingCartIcon },
|
||||
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
user_id: null,
|
||||
payment_number: null,
|
||||
payment_date: null,
|
||||
amount: 0,
|
||||
payment_date: new Date(),
|
||||
amount: 100,
|
||||
payment_method: null,
|
||||
invoice_id: null,
|
||||
notes: null,
|
||||
payment_method_id: null
|
||||
payment_method_id: null,
|
||||
},
|
||||
money: {
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
prefix: '$ ',
|
||||
precision: 2,
|
||||
masked: false
|
||||
masked: false,
|
||||
},
|
||||
customer: null,
|
||||
invoice: null,
|
||||
customerList: [],
|
||||
invoiceList: [],
|
||||
isLoading: false,
|
||||
isRequestOnGoing: false,
|
||||
maxPayableAmount: Number.MAX_SAFE_INTEGER,
|
||||
isSettingInitialData: true,
|
||||
paymentNumAttribute: null,
|
||||
paymentPrefix: ''
|
||||
paymentPrefix: '',
|
||||
PaymentFields: [
|
||||
'customer',
|
||||
'company',
|
||||
'customerCustom',
|
||||
'payment',
|
||||
'paymentCustom',
|
||||
],
|
||||
}
|
||||
},
|
||||
validations () {
|
||||
validations() {
|
||||
return {
|
||||
customer: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
formData: {
|
||||
payment_date: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
amount: {
|
||||
required,
|
||||
between: between(1, this.maxPayableAmount + 1)
|
||||
between: between(1, this.maxPayableAmount + 1),
|
||||
},
|
||||
notes: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
},
|
||||
paymentNumAttribute: {
|
||||
required,
|
||||
numeric
|
||||
}
|
||||
numeric,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrencyForInput'
|
||||
]),
|
||||
...mapGetters('payment', [
|
||||
'paymentModes'
|
||||
]),
|
||||
...mapGetters('company', ['defaultCurrencyForInput']),
|
||||
...mapGetters('payment', ['paymentModes', 'selectedNote']),
|
||||
...mapGetters('customer', ['customers']),
|
||||
amount: {
|
||||
get: function () {
|
||||
return this.formData.amount / 100
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.formData.amount = newValue * 100
|
||||
}
|
||||
},
|
||||
},
|
||||
isEdit () {
|
||||
pageTitle() {
|
||||
if (this.$route.name === 'payments.edit') {
|
||||
return this.$t('payments.edit_payment')
|
||||
}
|
||||
return this.$t('payments.new_payment')
|
||||
},
|
||||
isEdit() {
|
||||
if (this.$route.name === 'payments.edit') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
customerCurrency () {
|
||||
customerCurrency() {
|
||||
if (this.customer && this.customer.currency) {
|
||||
return {
|
||||
decimal: this.customer.currency.decimal_separator,
|
||||
thousands: this.customer.currency.thousand_separator,
|
||||
prefix: this.customer.currency.symbol + ' ',
|
||||
precision: this.customer.currency.precision,
|
||||
masked: false
|
||||
masked: false,
|
||||
}
|
||||
} else {
|
||||
return this.defaultCurrencyForInput
|
||||
}
|
||||
}
|
||||
},
|
||||
customerError() {
|
||||
if (!this.$v.customer.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.customer.required) {
|
||||
return this.$tc('validation.required')
|
||||
}
|
||||
},
|
||||
DateError() {
|
||||
if (!this.$v.formData.payment_date.$error) {
|
||||
return ''
|
||||
}
|
||||
if (!this.$v.formData.payment_date.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
amountError() {
|
||||
if (!this.$v.formData.amount.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.formData.amount.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
|
||||
if (
|
||||
!this.$v.formData.amount.between &&
|
||||
this.$v.formData.amount.numeric &&
|
||||
this.amount <= 0
|
||||
) {
|
||||
return this.$t('validation.payment_greater_than_zero')
|
||||
}
|
||||
|
||||
if (!this.$v.formData.amount.between && this.amount > 0) {
|
||||
return this.$t('validation.payment_greater_than_due_amount')
|
||||
}
|
||||
},
|
||||
paymentNumError() {
|
||||
if (!this.$v.paymentNumAttribute.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.paymentNumAttribute.required) {
|
||||
return this.$tc('validation.required')
|
||||
}
|
||||
|
||||
if (!this.$v.paymentNumAttribute.numeric) {
|
||||
return this.$tc('validation.numbers_only')
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
customer (newValue) {
|
||||
customer(newValue) {
|
||||
this.formData.user_id = newValue.id
|
||||
if (!this.isEdit) {
|
||||
if (this.isSettingInitialData) {
|
||||
@@ -270,16 +383,23 @@ export default {
|
||||
this.fetchCustomerInvoices(newValue.id)
|
||||
}
|
||||
},
|
||||
invoice (newValue) {
|
||||
selectedNote() {
|
||||
if (this.selectedNote) {
|
||||
this.formData.notes = this.selectedNote
|
||||
}
|
||||
},
|
||||
invoice(newValue) {
|
||||
if (newValue) {
|
||||
this.formData.invoice_id = newValue.id
|
||||
if (!this.isEdit) {
|
||||
this.setPaymentAmountByInvoiceData(newValue.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
async mounted () {
|
||||
async mounted() {
|
||||
this.$v.formData.$reset()
|
||||
this.resetSelectedNote()
|
||||
this.$nextTick(() => {
|
||||
this.loadData()
|
||||
if (this.$route.params.id && !this.isEdit) {
|
||||
@@ -288,91 +408,159 @@ export default {
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions('invoice', [
|
||||
'fetchInvoice'
|
||||
]),
|
||||
...mapActions('invoice', ['fetchInvoice', 'fetchInvoices']),
|
||||
|
||||
...mapActions('payment', [
|
||||
'fetchCreatePayment',
|
||||
'addPayment',
|
||||
'updatePayment',
|
||||
'fetchEditPaymentData'
|
||||
'fetchPayment',
|
||||
'fetchPaymentModes',
|
||||
'resetSelectedNote',
|
||||
]),
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
invoiceWithAmount ({ invoice_number, due_amount }) {
|
||||
return `${invoice_number} (${this.$utils.formatGraphMoney(due_amount, this.customer.currency)})`
|
||||
|
||||
...mapActions('company', ['fetchCompanySettings']),
|
||||
|
||||
...mapActions('modal', ['openModal']),
|
||||
|
||||
...mapActions('customer', ['fetchCustomers']),
|
||||
|
||||
invoiceWithAmount({ invoice_number, due_amount }) {
|
||||
return `${invoice_number} (${this.$utils.formatGraphMoney(
|
||||
due_amount,
|
||||
this.customer.currency
|
||||
)})`
|
||||
},
|
||||
async addPaymentMode () {
|
||||
|
||||
async addPaymentMode() {
|
||||
this.openModal({
|
||||
'title': this.$t('settings.customization.payments.add_payment_mode'),
|
||||
'componentName': 'PaymentMode'
|
||||
title: this.$t('settings.customization.payments.add_payment_mode'),
|
||||
componentName: 'PaymentMode',
|
||||
})
|
||||
},
|
||||
async loadData () {
|
||||
|
||||
async checkAutoGenerate() {
|
||||
let response = await this.fetchCompanySettings(['payment_auto_generate'])
|
||||
|
||||
let response1 = await axios.get('/api/v1/next-number?key=payment')
|
||||
|
||||
if (response.data && response.data.payment_auto_generate === 'YES') {
|
||||
if (response1.data) {
|
||||
this.paymentNumAttribute = response1.data.nextNumber
|
||||
this.paymentPrefix = response1.data.prefix
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
this.paymentPrefix = response1.data.prefix
|
||||
}
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
if (this.isEdit) {
|
||||
let response = await this.fetchEditPaymentData(this.$route.params.id)
|
||||
this.customerList = response.data.customers
|
||||
this.formData = { ...response.data.payment }
|
||||
this.isRequestOnGoing = true
|
||||
let response = await this.fetchPayment(this.$route.params.id)
|
||||
this.formData = { ...this.formData, ...response.data.payment }
|
||||
this.customer = response.data.payment.user
|
||||
this.formData.payment_date = moment(response.data.payment.payment_date, 'YYYY-MM-DD').toString()
|
||||
this.formData.payment_date = moment(
|
||||
response.data.payment.payment_date,
|
||||
'YYYY-MM-DD'
|
||||
).toString()
|
||||
this.formData.amount = parseFloat(response.data.payment.amount)
|
||||
this.paymentPrefix = response.data.payment_prefix
|
||||
this.paymentNumAttribute = response.data.nextPaymentNumber
|
||||
this.formData.payment_method = response.data.payment.payment_method
|
||||
if (response.data.payment.invoice !== null) {
|
||||
this.maxPayableAmount = parseInt(response.data.payment.amount) + parseInt(response.data.payment.invoice.due_amount)
|
||||
this.maxPayableAmount =
|
||||
parseInt(response.data.payment.amount) +
|
||||
parseInt(response.data.payment.invoice.due_amount)
|
||||
this.invoice = response.data.payment.invoice
|
||||
}
|
||||
// this.fetchCustomerInvoices(this.customer.id)
|
||||
let res = await this.fetchCustomFields({
|
||||
type: 'Payment',
|
||||
limit: 'all',
|
||||
})
|
||||
this.setEditCustomFields(
|
||||
response.data.payment.fields,
|
||||
res.data.customFields.data
|
||||
)
|
||||
|
||||
if (this.formData.payment_method_id) {
|
||||
await this.fetchPaymentModes({ limit: 'all' })
|
||||
}
|
||||
|
||||
if (this.formData.user_id) {
|
||||
await this.fetchCustomers({ limit: 'all' })
|
||||
}
|
||||
this.isRequestOnGoing = false
|
||||
} else {
|
||||
let response = await this.fetchCreatePayment()
|
||||
this.customerList = response.data.customers
|
||||
this.paymentNumAttribute = response.data.nextPaymentNumberAttribute
|
||||
this.paymentPrefix = response.data.payment_prefix
|
||||
this.formData.payment_date = moment(new Date()).toString()
|
||||
this.isRequestOnGoing = true
|
||||
this.checkAutoGenerate()
|
||||
this.setInitialCustomFields('Payment')
|
||||
this.formData.payment_date = moment().toString()
|
||||
this.fetchPaymentModes({ limit: 'all' })
|
||||
await this.fetchCustomers({ limit: 'all' })
|
||||
if (this.$route.query.customer) {
|
||||
this.setPaymentCustomer(parseInt(this.$route.query.customer))
|
||||
}
|
||||
this.isRequestOnGoing = false
|
||||
}
|
||||
return true
|
||||
},
|
||||
async setInvoicePaymentData () {
|
||||
setPaymentCustomer(id) {
|
||||
this.customer = this.customers.find((c) => {
|
||||
return c.id === id
|
||||
})
|
||||
},
|
||||
async setInvoicePaymentData() {
|
||||
let data = await this.fetchInvoice(this.$route.params.id)
|
||||
this.customer = data.data.invoice.user
|
||||
this.invoice = data.data.invoice
|
||||
},
|
||||
async setPaymentAmountByInvoiceData (id) {
|
||||
async setPaymentAmountByInvoiceData(id) {
|
||||
let data = await this.fetchInvoice(id)
|
||||
this.formData.amount = data.data.invoice.due_amount
|
||||
this.maxPayableAmount = data.data.invoice.due_amount
|
||||
},
|
||||
async fetchCustomerInvoices (userID) {
|
||||
let response = await axios.get(`/api/invoices/unpaid/${userID}`)
|
||||
if (response.data) {
|
||||
this.invoiceList = response.data.invoices
|
||||
async fetchCustomerInvoices(userId) {
|
||||
let data = {
|
||||
customer_id: userId,
|
||||
status: 'UNPAID',
|
||||
}
|
||||
let response = await this.fetchInvoices(data)
|
||||
this.invoiceList = response.data.invoices.data
|
||||
},
|
||||
async submitPaymentData () {
|
||||
async submitPaymentData() {
|
||||
let validate = await this.touchCustomField()
|
||||
this.$v.customer.$touch()
|
||||
this.$v.formData.$touch()
|
||||
if (this.$v.$invalid) {
|
||||
if (this.$v.$invalid || validate.error) {
|
||||
return true
|
||||
}
|
||||
|
||||
this.formData.payment_number = this.paymentPrefix + '-' + this.paymentNumAttribute
|
||||
this.formData.payment_number =
|
||||
this.paymentPrefix + '-' + this.paymentNumAttribute
|
||||
|
||||
if (this.isEdit) {
|
||||
let data = {
|
||||
editData: {
|
||||
...this.formData,
|
||||
payment_method_id: this.formData.payment_method ? this.formData.payment_method.id : null,
|
||||
payment_date: moment(this.formData.payment_date).format('DD/MM/YYYY')
|
||||
payment_method_id: this.formData.payment_method
|
||||
? this.formData.payment_method.id
|
||||
: null,
|
||||
payment_date: moment(this.formData.payment_date).format(
|
||||
'YYYY-MM-DD'
|
||||
),
|
||||
},
|
||||
id: this.$route.params.id
|
||||
id: this.$route.params.id,
|
||||
}
|
||||
try {
|
||||
this.isLoading = true
|
||||
let response = await this.updatePayment(data)
|
||||
if (response.data.success) {
|
||||
this.isLoading = false
|
||||
this.$router.push(
|
||||
`/admin/payments/${response.data.payment.id}/view`
|
||||
)
|
||||
window.toastr['success'](this.$t('payments.updated_message'))
|
||||
this.$router.push('/admin/payments')
|
||||
return true
|
||||
}
|
||||
if (response.data.error === 'invalid_amount') {
|
||||
@@ -391,15 +579,19 @@ export default {
|
||||
} else {
|
||||
let data = {
|
||||
...this.formData,
|
||||
payment_method_id: this.formData.payment_method ? this.formData.payment_method.id : null,
|
||||
payment_date: moment(this.formData.payment_date).format('DD/MM/YYYY')
|
||||
payment_method_id: this.formData.payment_method
|
||||
? this.formData.payment_method.id
|
||||
: null,
|
||||
payment_date: moment(this.formData.payment_date).format('YYYY-MM-DD'),
|
||||
}
|
||||
this.isLoading = true
|
||||
try {
|
||||
let response = await this.addPayment(data)
|
||||
if (response.data.success) {
|
||||
this.$router.push(
|
||||
`/admin/payments/${response.data.payment.id}/view`
|
||||
)
|
||||
window.toastr['success'](this.$t('payments.created_message'))
|
||||
this.$router.push('/admin/payments')
|
||||
this.isLoading = true
|
||||
return true
|
||||
}
|
||||
@@ -417,7 +609,11 @@ export default {
|
||||
window.toastr['error'](err.response.data.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onSelectNote(data) {
|
||||
this.formData.notes = '' + data.notes
|
||||
this.$refs.notePopup.close()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,306 +1,365 @@
|
||||
<template>
|
||||
<div class="payments main-content">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ $t('payments.title') }}</h3>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="dashboard">
|
||||
{{ $t('general.home') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="#">
|
||||
{{ $tc('payments.payment',2) }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="page-actions row">
|
||||
<div class="col-xs-2 mr-4">
|
||||
<base-button
|
||||
v-show="totalPayments || filtersApplied"
|
||||
:outline="true"
|
||||
:icon="filterIcon"
|
||||
color="theme"
|
||||
right-icon
|
||||
size="large"
|
||||
@click="toggleFilter"
|
||||
>
|
||||
{{ $t('general.filter') }}
|
||||
</base-button>
|
||||
</div>
|
||||
<router-link slot="item-title" class="col-xs-2" to="payments/create">
|
||||
<base-button
|
||||
color="theme"
|
||||
icon="plus"
|
||||
size="large"
|
||||
>
|
||||
{{ $t('payments.add_payment') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<base-page class="payments">
|
||||
<sw-page-header :title="$t('payments.title')">
|
||||
<sw-breadcrumb slot="breadcrumbs">
|
||||
<sw-breadcrumb-item to="dashboard" :title="$t('general.home')" />
|
||||
<sw-breadcrumb-item to="#" :title="$tc('payments.payment', 2)" active />
|
||||
</sw-breadcrumb>
|
||||
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-show="showFilters" class="filter-section">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">{{ $t('payments.customer') }}</label>
|
||||
<base-customer-select
|
||||
ref="customerSelect"
|
||||
@select="onSelectCustomer"
|
||||
@deselect="clearCustomerSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label for="">{{ $t('payments.payment_number') }}</label>
|
||||
<base-input
|
||||
v-model="filters.payment_number"
|
||||
:placeholder="$t(payments.payment_number)"
|
||||
name="payment_number"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label class="form-label">{{ $t('payments.payment_mode') }}</label>
|
||||
<base-select
|
||||
v-model="filters.payment_mode"
|
||||
:options="paymentModes"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('payments.payment_mode')"
|
||||
label="name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label class="clear-filter" @click="clearFilter">{{ $t('general.clear_all') }}</label>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div v-cloak v-show="showEmptyScreen" class="col-xs-1 no-data-info" align="center">
|
||||
<capsule-icon class="mt-5 mb-4"/>
|
||||
<div class="row" align="center">
|
||||
<label class="col title">{{ $t('payments.no_payments') }}</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="description col mt-1" align="center">{{ $t('payments.list_of_payments') }}</label>
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<base-button
|
||||
:outline="true"
|
||||
color="theme"
|
||||
class="mt-3"
|
||||
size="large"
|
||||
@click="$router.push('payments/create')"
|
||||
<template slot="actions">
|
||||
<sw-button
|
||||
v-show="totalPayments"
|
||||
variant="primary-outline"
|
||||
size="lg"
|
||||
@click="toggleFilter"
|
||||
>
|
||||
{{ $t('payments.add_new_payment') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
{{ $t('general.filter') }}
|
||||
<component :is="filterIcon" class="w-4 h-4 ml-2 -mr-1" />
|
||||
</sw-button>
|
||||
|
||||
<div v-show="!showEmptyScreen" class="table-container">
|
||||
<div class="table-actions mt-5">
|
||||
<p class="table-stats">{{ $t('general.showing') }}: <b>{{ payments.length }}</b> {{ $t('general.of') }} <b>{{ totalPayments }}</b></p>
|
||||
<sw-button
|
||||
tag-name="router-link"
|
||||
to="payments/create"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
class="ml-4"
|
||||
>
|
||||
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
|
||||
{{ $t('payments.add_payment') }}
|
||||
</sw-button>
|
||||
</template>
|
||||
</sw-page-header>
|
||||
|
||||
<transition name="fade">
|
||||
<v-dropdown v-if="selectedPayments.length" :show-arrow="false">
|
||||
<span slot="activator" href="#" class="table-actions-button dropdown-toggle">
|
||||
<slide-y-up-transition>
|
||||
<sw-filter-wrapper v-show="showFilters" class="mt-3">
|
||||
<sw-input-group
|
||||
:label="$t('payments.customer')"
|
||||
color="black-light"
|
||||
class="flex-1 mt-2"
|
||||
>
|
||||
<base-customer-select
|
||||
ref="customerSelect"
|
||||
@select="onSelectCustomer"
|
||||
@deselect="clearCustomerSearch"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('payments.payment_number')"
|
||||
class="flex-1 mt-2 lg:ml-6"
|
||||
>
|
||||
<sw-input
|
||||
v-model="filters.payment_number"
|
||||
:placeholder="$t(payments.payment_number)"
|
||||
name="payment_number"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('payments.payment_mode')"
|
||||
class="flex-1 mt-2 lg:ml-6"
|
||||
>
|
||||
<sw-select
|
||||
v-model="filters.payment_mode"
|
||||
:options="paymentModes"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('payments.payment_mode')"
|
||||
label="name"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<label
|
||||
class="absolute text-sm leading-snug text-gray-900 cursor-pointer"
|
||||
style="top: 10px; right: 15px"
|
||||
@click="clearFilter"
|
||||
>{{ $t('general.clear_all') }}</label
|
||||
>
|
||||
</sw-filter-wrapper>
|
||||
</slide-y-up-transition>
|
||||
|
||||
<sw-empty-table-placeholder
|
||||
v-if="showEmptyScreen"
|
||||
:title="$t('payments.no_payments')"
|
||||
:description="$t('payments.list_of_payments')"
|
||||
>
|
||||
<capsule-icon class="mt-5 mb-4" />
|
||||
|
||||
<sw-button
|
||||
slot="actions"
|
||||
tag-name="router-link"
|
||||
to="/admin/payments/create"
|
||||
size="lg"
|
||||
variant="primary-outline"
|
||||
>
|
||||
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
|
||||
{{ $t('payments.add_new_payment') }}
|
||||
</sw-button>
|
||||
</sw-empty-table-placeholder>
|
||||
|
||||
<div v-show="!showEmptyScreen" class="relative table-container">
|
||||
<div
|
||||
class="relative flex items-center justify-between h-10 mt-5 list-none border-b-2 border-gray-200 border-solid"
|
||||
>
|
||||
<p class="text-sm">
|
||||
{{ $t('general.showing') }}: <b>{{ payments.length }}</b>
|
||||
{{ $t('general.of') }} <b>{{ totalPayments }}</b>
|
||||
</p>
|
||||
|
||||
<sw-transition type="fade">
|
||||
<sw-dropdown v-if="selectedPayments.length">
|
||||
<span
|
||||
slot="activator"
|
||||
class="flex block text-sm font-medium cursor-pointer select-none text-primary-400"
|
||||
>
|
||||
{{ $t('general.actions') }}
|
||||
<chevron-down-icon class="h-5" />
|
||||
</span>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeMultiplePayments">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
</transition>
|
||||
|
||||
<sw-dropdown-item @click="removeMultiplePayments">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</sw-transition>
|
||||
</div>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="select-all"
|
||||
<div class="absolute z-10 items-center pl-4 mt-2 select-none md:mt-12">
|
||||
<sw-checkbox
|
||||
v-model="selectAllFieldStatus"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="hidden md:inline"
|
||||
@change="selectAllPayments"
|
||||
>
|
||||
<label v-show="!isRequestOngoing" for="select-all" class="custom-control-label selectall">
|
||||
<span class="select-all-label">{{ $t('general.select_all') }} </span>
|
||||
</label>
|
||||
/>
|
||||
|
||||
<sw-checkbox
|
||||
v-model="selectAllFieldStatus"
|
||||
:label="$t('general.select_all')"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="md:hidden"
|
||||
@change="selectAllPayments"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<table-component
|
||||
<sw-table-component
|
||||
ref="table"
|
||||
:data="fetchData"
|
||||
:show-filter="false"
|
||||
table-class="table"
|
||||
>
|
||||
<table-column
|
||||
<sw-table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
cell-class="no-click"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
:id="row.id"
|
||||
v-model="selectField"
|
||||
:value="row.id"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
>
|
||||
<label :for="row.id" class="custom-control-label" />
|
||||
</div>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
<div slot-scope="row" class="relative block">
|
||||
<sw-checkbox
|
||||
:id="row.id"
|
||||
v-model="selectField"
|
||||
:value="row.id"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('payments.date')"
|
||||
sort-as="payment_date"
|
||||
show="formattedPaymentDate"
|
||||
/>
|
||||
<table-column
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('payments.payment_number')"
|
||||
show="payment_number"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('payments.payment_number') }}</span>
|
||||
<router-link
|
||||
:to="{ path: `payments/${row.id}/view` }"
|
||||
class="font-medium text-primary-500"
|
||||
>
|
||||
{{ row.payment_number }}
|
||||
</router-link>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('payments.customer')"
|
||||
show="name"
|
||||
/>
|
||||
<table-column
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('payments.payment_mode')"
|
||||
show="payment_mode"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('payments.payment_number')"
|
||||
show="payment_number"
|
||||
/>
|
||||
<table-column
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('payments.payment_mode') }}</span>
|
||||
<span>
|
||||
{{ row.payment_mode ? row.payment_mode : 'Not selected' }}
|
||||
</span>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('payments.invoice')"
|
||||
sort-as="invoice_id"
|
||||
show="invoice.invoice_number"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('payments.amount')"
|
||||
show="invoice_number"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('invoices.invoice_number') }}</span>
|
||||
<span>
|
||||
{{ row.invoice_number ? row.invoice_number : 'No Invoice' }}
|
||||
</span>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column :sortable="true" :label="$t('payments.amount')">
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('payments.amount') }}</span>
|
||||
<div v-html="$utils.formatMoney(row.amount, row.user.currency)" />
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
cell-class="action-dropdown no-click"
|
||||
cell-class="action-dropdown"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('payments.action') }}</span>
|
||||
<v-dropdown>
|
||||
<a slot="activator" href="#">
|
||||
<dot-icon />
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
<sw-dropdown>
|
||||
<dot-icon slot="activator" />
|
||||
|
||||
<router-link :to="{path: `payments/${row.id}/edit`}" class="dropdown-item">
|
||||
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
|
||||
{{ $t('general.edit') }}
|
||||
</router-link>
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`payments/${row.id}/edit`"
|
||||
>
|
||||
<pencil-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.edit') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`payments/${row.id}/view`"
|
||||
>
|
||||
<eye-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.view') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<router-link :to="{path: `payments/${row.id}/view`}" class="dropdown-item">
|
||||
<font-awesome-icon icon="eye" class="dropdown-item-icon" />
|
||||
{{ $t('general.view') }}
|
||||
</router-link>
|
||||
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removePayment(row.id)">
|
||||
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<sw-dropdown-item @click="removePayment(row.id)">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</template>
|
||||
</table-column>
|
||||
</table-component>
|
||||
</sw-table-column>
|
||||
</sw-table-component>
|
||||
</div>
|
||||
</div>
|
||||
</base-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { SweetModal, SweetModalTab } from 'sweet-modal-vue'
|
||||
import CapsuleIcon from '../../components/icon/CapsuleIcon'
|
||||
import BaseButton from '../../../js/components/base/BaseButton'
|
||||
import CapsuleIcon from '@/components/icon/CapsuleIcon'
|
||||
import {
|
||||
PlusIcon,
|
||||
FilterIcon,
|
||||
XIcon,
|
||||
ChevronDownIcon,
|
||||
EyeIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
} from '@vue-hero-icons/solid'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'capsule-icon': CapsuleIcon,
|
||||
'SweetModal': SweetModal,
|
||||
'SweetModalTab': SweetModalTab,
|
||||
BaseButton
|
||||
CapsuleIcon,
|
||||
PlusIcon,
|
||||
FilterIcon,
|
||||
XIcon,
|
||||
ChevronDownIcon,
|
||||
EyeIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
},
|
||||
data () {
|
||||
|
||||
data() {
|
||||
return {
|
||||
showFilters: false,
|
||||
sortedBy: 'created_at',
|
||||
filtersApplied: false,
|
||||
isRequestOngoing: true,
|
||||
|
||||
filters: {
|
||||
customer: null,
|
||||
customer: '',
|
||||
payment_mode: '',
|
||||
payment_number: ''
|
||||
}
|
||||
payment_number: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
showEmptyScreen () {
|
||||
return !this.totalPayments && !this.isRequestOngoing && !this.filtersApplied
|
||||
showEmptyScreen() {
|
||||
return !this.totalPayments && !this.isRequestOngoing
|
||||
},
|
||||
filterIcon () {
|
||||
return (this.showFilters) ? 'times' : 'filter'
|
||||
|
||||
filterIcon() {
|
||||
return this.showFilters ? 'x-icon' : 'filter-icon'
|
||||
},
|
||||
...mapGetters('customer', [
|
||||
'customers'
|
||||
]),
|
||||
|
||||
...mapGetters('customer', ['customers']),
|
||||
|
||||
...mapGetters('payment', [
|
||||
'selectedPayments',
|
||||
'totalPayments',
|
||||
'payments',
|
||||
'selectAllField',
|
||||
'paymentModes'
|
||||
'paymentModes',
|
||||
]),
|
||||
|
||||
selectField: {
|
||||
get: function () {
|
||||
return this.selectedPayments
|
||||
},
|
||||
set: function (val) {
|
||||
this.selectPayment(val)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
selectAllFieldStatus: {
|
||||
get: function () {
|
||||
return this.selectAllField
|
||||
},
|
||||
set: function (val) {
|
||||
this.setSelectAllState(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
filters: {
|
||||
handler: 'setFilters',
|
||||
deep: true
|
||||
}
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.fetchCustomers()
|
||||
|
||||
mounted() {
|
||||
this.fetchPaymentModes({ limit: 'all' })
|
||||
},
|
||||
destroyed () {
|
||||
|
||||
destroyed() {
|
||||
if (this.selectAllField) {
|
||||
this.selectAllPayments()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('payment', [
|
||||
'fetchPayments',
|
||||
@@ -308,25 +367,25 @@ export default {
|
||||
'selectPayment',
|
||||
'deletePayment',
|
||||
'deleteMultiplePayments',
|
||||
'setSelectAllState'
|
||||
'setSelectAllState',
|
||||
'fetchPaymentModes',
|
||||
]),
|
||||
...mapActions('customer', [
|
||||
'fetchCustomers'
|
||||
]),
|
||||
async fetchData ({ page, filter, sort }) {
|
||||
|
||||
async fetchData({ page, filter, sort }) {
|
||||
let data = {
|
||||
customer_id: this.filters.customer !== null ? this.filters.customer.id : '',
|
||||
customer_id: this.filters.customer ? this.filters.customer.id : '',
|
||||
payment_method_id:
|
||||
this.filters.payment_mode !== null
|
||||
? this.filters.payment_mode.id
|
||||
: '',
|
||||
payment_number: this.filters.payment_number,
|
||||
payment_method_id: this.filters.payment_mode ? this.filters.payment_mode.id : '',
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page
|
||||
page,
|
||||
}
|
||||
|
||||
this.isRequestOngoing = true
|
||||
|
||||
let response = await this.fetchPayments(data)
|
||||
|
||||
this.isRequestOngoing = false
|
||||
|
||||
return {
|
||||
@@ -334,71 +393,75 @@ export default {
|
||||
pagination: {
|
||||
totalPages: response.data.payments.last_page,
|
||||
currentPage: page,
|
||||
count: response.data.payments.scount
|
||||
}
|
||||
count: response.data.payments.count,
|
||||
},
|
||||
}
|
||||
},
|
||||
refreshTable () {
|
||||
|
||||
refreshTable() {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
setFilters () {
|
||||
this.filtersApplied = true
|
||||
|
||||
setFilters() {
|
||||
this.refreshTable()
|
||||
},
|
||||
clearFilter () {
|
||||
|
||||
clearFilter() {
|
||||
if (this.filters.customer) {
|
||||
this.$refs.customerSelect.$refs.baseSelect.removeElement(this.filters.customer)
|
||||
this.$refs.customerSelect.$refs.baseSelect.removeElement(
|
||||
this.filters.customer
|
||||
)
|
||||
}
|
||||
|
||||
this.filters = {
|
||||
customer: null,
|
||||
customer: '',
|
||||
payment_mode: '',
|
||||
payment_number: ''
|
||||
payment_number: '',
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.filtersApplied = false
|
||||
})
|
||||
},
|
||||
toggleFilter () {
|
||||
if (this.showFilters && this.filtersApplied) {
|
||||
|
||||
toggleFilter() {
|
||||
if (this.showFilters) {
|
||||
this.clearFilter()
|
||||
this.refreshTable()
|
||||
}
|
||||
|
||||
this.showFilters = !this.showFilters
|
||||
},
|
||||
onSelectCustomer (customer) {
|
||||
|
||||
onSelectCustomer(customer) {
|
||||
this.filters.customer = customer
|
||||
},
|
||||
async removePayment (id) {
|
||||
this.id = id
|
||||
|
||||
async removePayment(id) {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('payments.confirm_delete'),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
dangerMode: true,
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deletePayment(this.id)
|
||||
let res = await this.deletePayment({ ids: [id] })
|
||||
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$tc('payments.deleted_message', 1))
|
||||
this.$refs.table.refresh()
|
||||
return true
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
|
||||
window.toastr['error'](res.data.message)
|
||||
return true
|
||||
}
|
||||
})
|
||||
},
|
||||
async removeMultiplePayments () {
|
||||
|
||||
async removeMultiplePayments() {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('payments.confirm_delete', 2),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
dangerMode: true,
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let request = await this.deleteMultiplePayments()
|
||||
@@ -411,21 +474,24 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
async clearCustomerSearch (removedOption, id) {
|
||||
|
||||
async clearCustomerSearch(removedOption, id) {
|
||||
this.filters.customer = ''
|
||||
this.$refs.table.refresh()
|
||||
this.refreshTable()
|
||||
},
|
||||
showModel (selectedRow) {
|
||||
|
||||
showModel(selectedRow) {
|
||||
this.selectedRow = selectedRow
|
||||
this.$refs.Delete_modal.open()
|
||||
},
|
||||
async removeSelectedItems () {
|
||||
|
||||
async removeSelectedItems() {
|
||||
this.$refs.Delete_modal.close()
|
||||
await this.selectedRow.forEach(row => {
|
||||
await this.selectedRow.forEach((row) => {
|
||||
this.deletePayment(this.id)
|
||||
})
|
||||
this.$refs.table.refresh()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,176 +1,225 @@
|
||||
<template>
|
||||
<div v-if="payment" class="main-content payment-view-page">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ payment.payment_number }}</h3>
|
||||
<div class="page-actions row">
|
||||
<base-button
|
||||
:loading="isSendingEmail"
|
||||
<base-page v-if="payment" class="xl:pl-96">
|
||||
<sw-page-header :title="pageTitle">
|
||||
<template slot="actions">
|
||||
<sw-button
|
||||
:disabled="isSendingEmail"
|
||||
color="theme"
|
||||
variant="primary"
|
||||
@click="onPaymentSend"
|
||||
>
|
||||
{{ $t('payments.send_payment_receipt') }}
|
||||
</base-button>
|
||||
<v-dropdown
|
||||
:close-on-select="true"
|
||||
align="left"
|
||||
class="filter-container"
|
||||
>
|
||||
<a slot="activator" href="#">
|
||||
<base-button color="theme">
|
||||
<font-awesome-icon icon="ellipsis-h" />
|
||||
</base-button>
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="copyPdfUrl">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'link']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</div>
|
||||
<router-link
|
||||
:to="{ path: `/admin/payments/${$route.params.id}/edit` }"
|
||||
class="dropdown-item"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'pencil-alt']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</router-link>
|
||||
<div class="dropdown-item" @click="removePayment($route.params.id)">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'trash']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</div>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="payment-sidebar">
|
||||
<div class="side-header">
|
||||
<base-input
|
||||
</sw-button>
|
||||
<sw-dropdown class="ml-3">
|
||||
<sw-button slot="activator" variant="primary" class="h-10">
|
||||
<dots-horizontal-icon class="h-5" />
|
||||
</sw-button>
|
||||
|
||||
<sw-dropdown-item @click="copyPdfUrl">
|
||||
<link-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`/admin/payments/${$route.params.id}/edit`"
|
||||
>
|
||||
<pencil-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.edit') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="removePayment($route.params.id)">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</template>
|
||||
</sw-page-header>
|
||||
|
||||
<!-- sidebar -->
|
||||
<div
|
||||
class="fixed top-0 left-0 hidden h-full pt-16 pb-4 ml-56 bg-white xl:ml-64 w-88 xl:block"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between px-4 pt-8 pb-2 border border-gray-200 border-solid height-full"
|
||||
>
|
||||
<sw-input
|
||||
v-model="searchData.searchText"
|
||||
:placeholder="$t('general.search')"
|
||||
input-class="inv-search"
|
||||
icon="search"
|
||||
type="text"
|
||||
align-icon="right"
|
||||
class="mb-6"
|
||||
variant="gray"
|
||||
@input="onSearch"
|
||||
/>
|
||||
<div class="btn-group ml-3" role="group" aria-label="First group">
|
||||
<v-dropdown
|
||||
:close-on-select="false"
|
||||
align="left"
|
||||
class="filter-container"
|
||||
>
|
||||
<a slot="activator" href="#">
|
||||
<base-button
|
||||
class="inv-button inv-filter-fields-btn"
|
||||
color="default"
|
||||
size="medium"
|
||||
>
|
||||
<font-awesome-icon icon="filter" />
|
||||
</base-button>
|
||||
</a>
|
||||
<div class="filter-title">
|
||||
>
|
||||
<search-icon slot="rightIcon" class="h-5" />
|
||||
</sw-input>
|
||||
|
||||
<div class="flex mb-6 ml-3" role="group" aria-label="First group">
|
||||
<sw-dropdown position="bottom-start">
|
||||
<sw-button slot="activator" size="md" variant="gray-light">
|
||||
<filter-icon class="h-5" />
|
||||
</sw-button>
|
||||
|
||||
<div
|
||||
class="px-2 pb-2 mb-1 text-sm border-b border-gray-200 border-solid"
|
||||
>
|
||||
{{ $t('general.sort_by') }}
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
id="filter_invoice_number"
|
||||
v-model="searchData.orderByField"
|
||||
type="radio"
|
||||
name="filter"
|
||||
class="inv-radio"
|
||||
value="invoice_number"
|
||||
@change="onSearch"
|
||||
/>
|
||||
<label class="inv-label" for="filter_invoice_number">{{
|
||||
$t('invoices.title')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
id="filter_payment_date"
|
||||
v-model="searchData.orderByField"
|
||||
type="radio"
|
||||
name="filter"
|
||||
class="inv-radio"
|
||||
value="payment_date"
|
||||
@change="onSearch"
|
||||
/>
|
||||
<label class="inv-label" for="filter_payment_date">{{
|
||||
$t('payments.date')
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
id="filter_payment_number"
|
||||
v-model="searchData.orderByField"
|
||||
type="radio"
|
||||
name="filter"
|
||||
class="inv-radio"
|
||||
value="payment_number"
|
||||
@change="onSearch"
|
||||
/>
|
||||
<label class="inv-label" for="filter_payment_number">{{
|
||||
$t('payments.payment_number')
|
||||
}}</label>
|
||||
</div>
|
||||
</v-dropdown>
|
||||
<base-button
|
||||
|
||||
<sw-dropdown-item class="flex cursor-pointer">
|
||||
<sw-input-group class="-mt-3 font-normal">
|
||||
<sw-radio
|
||||
:label="$t('invoices.title')"
|
||||
size="sm"
|
||||
id="filter_invoice_number"
|
||||
v-model="searchData.orderByField"
|
||||
name="filter"
|
||||
value="invoice_number"
|
||||
@change="onSearch"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item class="flex cursor-pointer">
|
||||
<sw-input-group class="-mt-3 font-normal">
|
||||
<sw-radio
|
||||
:label="$t('payments.date')"
|
||||
size="sm"
|
||||
id="filter_payment_date"
|
||||
v-model="searchData.orderByField"
|
||||
name="filter"
|
||||
value="payment_date"
|
||||
@change="onSearch"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item class="flex cursor-pointer">
|
||||
<sw-input-group class="-mt-3 font-normal">
|
||||
<sw-radio
|
||||
id="filter_payment_number"
|
||||
:label="$t('payments.payment_number')"
|
||||
v-model="searchData.orderByField"
|
||||
size="sm"
|
||||
name="filter"
|
||||
value="payment_number"
|
||||
@change="onSearch"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
|
||||
<sw-button
|
||||
class="ml-1"
|
||||
v-tooltip.top-center="{ content: getOrderName }"
|
||||
class="inv-button inv-filter-sorting-btn"
|
||||
color="default"
|
||||
size="medium"
|
||||
size="md"
|
||||
variant="gray-light"
|
||||
@click="sortData"
|
||||
>
|
||||
<font-awesome-icon v-if="getOrderBy" icon="sort-amount-up" />
|
||||
<font-awesome-icon v-else icon="sort-amount-down" />
|
||||
</base-button>
|
||||
<sort-ascending-icon v-if="getOrderBy" class="h-5" />
|
||||
<sort-descending-icon v-else class="h-5" />
|
||||
</sw-button>
|
||||
</div>
|
||||
</div>
|
||||
<base-loader v-if="isSearching" />
|
||||
<div v-else class="side-content">
|
||||
|
||||
<base-loader v-if="isSearching" :show-bg-overlay="true" />
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="h-full pb-32 overflow-y-scroll border-l border-gray-200 border-solid sw-scroll"
|
||||
>
|
||||
<router-link
|
||||
v-for="(payment, index) in payments"
|
||||
:to="`/admin/payments/${payment.id}/view`"
|
||||
:id="'payment-' + payment.id"
|
||||
:key="index"
|
||||
class="side-payment"
|
||||
:class="[
|
||||
'flex justify-between p-4 items-center cursor-pointer hover:bg-gray-100 border-l-4 border-transparent',
|
||||
{
|
||||
'bg-gray-100 border-l-4 border-primary-500 border-solid': hasActiveUrl(
|
||||
payment.id
|
||||
),
|
||||
},
|
||||
]"
|
||||
style="border-bottom: 1px solid rgba(185, 193, 209, 0.41)"
|
||||
>
|
||||
<div class="left">
|
||||
<div class="inv-name">{{ payment.user.name }}</div>
|
||||
<div class="inv-number">{{ payment.payment_number }}</div>
|
||||
<div class="inv-number">{{ payment.invoice_number }}</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="flex-2">
|
||||
<div
|
||||
class="inv-amount"
|
||||
class="pr-2 mb-2 text-sm not-italic font-normal leading-5 text-black capitalize truncate"
|
||||
>
|
||||
{{ payment.user.name }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mb-1 text-xs not-italic font-medium leading-5 text-gray-500 capitalize"
|
||||
>
|
||||
{{ payment.payment_number }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mb-1 text-xs not-italic font-medium leading-5 text-gray-500 capitalize"
|
||||
>
|
||||
{{ payment.invoice_number }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 whitespace-no-wrap right">
|
||||
<div
|
||||
class="mb-2 text-xl not-italic font-semibold leading-8 text-right text-gray-900"
|
||||
v-html="$utils.formatMoney(payment.amount, payment.user.currency)"
|
||||
/>
|
||||
<div class="inv-date">{{ payment.formattedPaymentDate }}</div>
|
||||
<!-- <div class="inv-number">{{ payment.payment_method.name }}</div> -->
|
||||
|
||||
<div class="text-sm text-right text-gray-500 non-italic">
|
||||
{{ payment.formattedPaymentDate }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<p v-if="!payments.length" class="no-result">
|
||||
|
||||
<p
|
||||
v-if="!payments.length"
|
||||
class="flex justify-center px-4 mt-5 text-sm text-gray-600"
|
||||
>
|
||||
{{ $t('payments.no_matching_payments') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="payment-view-page-container">
|
||||
<iframe :src="`${shareableLink}`" class="frame-style" />
|
||||
|
||||
<!-- pdf -->
|
||||
<div
|
||||
class="flex flex-col min-h-0 mt-8 overflow-hidden"
|
||||
style="height: 75vh"
|
||||
>
|
||||
<iframe
|
||||
:src="`${shareableLink}`"
|
||||
class="flex-1 border border-gray-400 border-solid rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</base-page>
|
||||
</template>
|
||||
<script>
|
||||
import {
|
||||
DotsHorizontalIcon,
|
||||
FilterIcon,
|
||||
SortAscendingIcon,
|
||||
SortDescendingIcon,
|
||||
SearchIcon,
|
||||
LinkIcon,
|
||||
TrashIcon,
|
||||
PencilIcon,
|
||||
} from '@vue-hero-icons/solid'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
const _ = require('lodash')
|
||||
export default {
|
||||
data () {
|
||||
components: {
|
||||
DotsHorizontalIcon,
|
||||
FilterIcon,
|
||||
SortAscendingIcon,
|
||||
SortDescendingIcon,
|
||||
SearchIcon,
|
||||
TrashIcon,
|
||||
PencilIcon,
|
||||
LinkIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
count: null,
|
||||
@@ -180,82 +229,109 @@ export default {
|
||||
searchData: {
|
||||
orderBy: null,
|
||||
orderByField: null,
|
||||
searchText: null
|
||||
searchText: null,
|
||||
},
|
||||
isRequestOnGoing: false,
|
||||
isSearching: false,
|
||||
isSendingEmail: false,
|
||||
isMarkingAsSent: false
|
||||
isMarkingAsSent: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getOrderBy () {
|
||||
if (this.searchData.orderBy === 'asc' || this.searchData.orderBy == null) {
|
||||
pageTitle() {
|
||||
return this.payment.payment_number
|
||||
},
|
||||
getOrderBy() {
|
||||
if (
|
||||
this.searchData.orderBy === 'asc' ||
|
||||
this.searchData.orderBy == null
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
getOrderName () {
|
||||
getOrderName() {
|
||||
if (this.getOrderBy) {
|
||||
return this.$t('general.ascending')
|
||||
}
|
||||
return this.$t('general.descending')
|
||||
},
|
||||
shareableLink () {
|
||||
shareableLink() {
|
||||
return `/payments/pdf/${this.payment.unique_hash}`
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route (to, from) {
|
||||
$route(to, from) {
|
||||
this.loadPayment()
|
||||
}
|
||||
},
|
||||
},
|
||||
created () {
|
||||
|
||||
created() {
|
||||
this.loadPayments()
|
||||
this.loadPayment()
|
||||
this.onSearch = _.debounce(this.onSearch, 500)
|
||||
},
|
||||
|
||||
methods: {
|
||||
// ...mapActions('invoice', [
|
||||
// 'fetchInvoices',
|
||||
// 'getRecord',
|
||||
// 'searchInvoice',
|
||||
// 'markAsSent',
|
||||
// 'sendEmail',
|
||||
// 'deleteInvoice',
|
||||
// 'fetchViewInvoice'
|
||||
// ]),
|
||||
...mapActions('payment', [
|
||||
'fetchPayments',
|
||||
'fetchPayment',
|
||||
'sendEmail',
|
||||
'deletePayment',
|
||||
'searchPayment'
|
||||
'searchPayment',
|
||||
]),
|
||||
async loadPayments () {
|
||||
let response = await this.fetchPayments()
|
||||
...mapActions('modal', ['openModal']),
|
||||
|
||||
hasActiveUrl(id) {
|
||||
return this.$route.params.id == id
|
||||
},
|
||||
|
||||
async loadPayments() {
|
||||
let response = await this.fetchPayments({ limit: 'all' })
|
||||
if (response.data) {
|
||||
this.payments = response.data.payments.data
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.scrollToPayment()
|
||||
}, 500)
|
||||
},
|
||||
async loadPayment () {
|
||||
scrollToPayment() {
|
||||
const el = document.getElementById(`payment-${this.$route.params.id}`)
|
||||
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth' })
|
||||
el.classList.add('shake')
|
||||
}
|
||||
},
|
||||
async loadPayment() {
|
||||
let response = await this.fetchPayment(this.$route.params.id)
|
||||
|
||||
if (response.data) {
|
||||
this.payment = response.data.payment
|
||||
}
|
||||
},
|
||||
async onSearch () {
|
||||
async onSearch() {
|
||||
let data = ''
|
||||
if (this.searchData.searchText !== '' && this.searchData.searchText !== null && this.searchData.searchText !== undefined) {
|
||||
if (
|
||||
this.searchData.searchText !== '' &&
|
||||
this.searchData.searchText !== null &&
|
||||
this.searchData.searchText !== undefined
|
||||
) {
|
||||
data += `search=${this.searchData.searchText}&`
|
||||
}
|
||||
|
||||
if (this.searchData.orderBy !== null && this.searchData.orderBy !== undefined) {
|
||||
if (
|
||||
this.searchData.orderBy !== null &&
|
||||
this.searchData.orderBy !== undefined
|
||||
) {
|
||||
data += `orderBy=${this.searchData.orderBy}&`
|
||||
}
|
||||
|
||||
if (this.searchData.orderByField !== null && this.searchData.orderByField !== undefined) {
|
||||
if (
|
||||
this.searchData.orderByField !== null &&
|
||||
this.searchData.orderByField !== undefined
|
||||
) {
|
||||
data += `orderByField=${this.searchData.orderByField}`
|
||||
}
|
||||
this.isSearching = true
|
||||
@@ -265,7 +341,7 @@ export default {
|
||||
this.payments = response.data.payments.data
|
||||
}
|
||||
},
|
||||
sortData () {
|
||||
sortData() {
|
||||
if (this.searchData.orderBy === 'asc') {
|
||||
this.searchData.orderBy = 'desc'
|
||||
this.onSearch()
|
||||
@@ -275,57 +351,44 @@ export default {
|
||||
this.onSearch()
|
||||
return true
|
||||
},
|
||||
async onPaymentSend () {
|
||||
window.swal({
|
||||
title: this.$tc('general.are_you_sure'),
|
||||
text: this.$tc('payments.confirm_send_payment'),
|
||||
icon: '/assets/icon/paper-plane-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (value) => {
|
||||
if (value) {
|
||||
this.isSendingEmail = true
|
||||
let response = await this.sendEmail({id: this.payment.id})
|
||||
this.isSendingEmail = false
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](this.$tc('payments.send_payment_successfully'))
|
||||
return true
|
||||
}
|
||||
if (response.data.error === 'user_email_does_not_exist') {
|
||||
window.toastr['error'](this.$tc('payments.user_email_does_not_exist'))
|
||||
return false
|
||||
}
|
||||
window.toastr['error'](this.$tc('payments.something_went_wrong'))
|
||||
}
|
||||
async onPaymentSend() {
|
||||
this.openModal({
|
||||
title: this.$t('payments.send_payment'),
|
||||
componentName: 'SendPaymentModal',
|
||||
id: this.payment.id,
|
||||
data: this.payment,
|
||||
variant: 'lg',
|
||||
})
|
||||
},
|
||||
copyPdfUrl () {
|
||||
copyPdfUrl() {
|
||||
let pdfUrl = `${window.location.origin}/payments/pdf/${this.payment.unique_hash}`
|
||||
|
||||
let response = this.$utils.copyTextToClipboard(pdfUrl)
|
||||
|
||||
window.toastr['success'](this.$tc('Copied PDF url to clipboard!'))
|
||||
window.toastr['success'](this.$t('general.copied_pdf_url_clipboard'))
|
||||
},
|
||||
async removePayment (id) {
|
||||
async removePayment(id) {
|
||||
this.id = id
|
||||
window.swal({
|
||||
title: 'Deleted',
|
||||
text: 'you will not be able to recover this payment!',
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (value) => {
|
||||
if (value) {
|
||||
let request = await this.deletePayment(this.id)
|
||||
if (request.data.success) {
|
||||
window.toastr['success'](this.$tc('payments.deleted_message', 1))
|
||||
this.$router.push('/admin/payments')
|
||||
} else if (request.data.error) {
|
||||
window.toastr['error'](request.data.message)
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: 'you will not be able to recover this payment!',
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (value) => {
|
||||
if (value) {
|
||||
let request = await this.deletePayment({ ids: [id] })
|
||||
if (request.data.success) {
|
||||
window.toastr['success'](this.$tc('payments.deleted_message', 1))
|
||||
this.$router.push('/admin/payments')
|
||||
} else if (request.data.error) {
|
||||
window.toastr['error'](request.data.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,55 +1,76 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-md-4 reports-tab-container">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<label class="report-label">{{ $t('reports.expenses.date_range') }}</label>
|
||||
<base-select
|
||||
<div class="grid gap-8 md:grid-cols-12">
|
||||
<div class="col-span-8 mt-12 md:col-span-4">
|
||||
<div class="grid grid-cols-12">
|
||||
<sw-input-group
|
||||
:label="$t('reports.expenses.date_range')"
|
||||
:error="dateRangeError"
|
||||
class="col-span-12 md:col-span-8"
|
||||
>
|
||||
<sw-select
|
||||
v-model="selectedRange"
|
||||
:options="dateRange"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
class="mt-2"
|
||||
@input="onChangeDateRange"
|
||||
/>
|
||||
<span v-if="$v.range.$error && !$v.range.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
</sw-input-group>
|
||||
</div>
|
||||
<div class="row report-fields-container">
|
||||
<div class="col-md-6 report-field-container">
|
||||
<label class="report-label">{{ $t('reports.expenses.from_date') }}</label>
|
||||
|
||||
<div class="grid grid-cols-1 mt-6 md:gap-10 md:grid-cols-2">
|
||||
<sw-input-group
|
||||
:label="$t('reports.expenses.from_date')"
|
||||
:error="fromDateError"
|
||||
>
|
||||
<base-date-picker
|
||||
v-model="formData.from_date"
|
||||
:invalid="$v.formData.from_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
class="mt-2"
|
||||
@change="$v.formData.from_date.$touch()"
|
||||
/>
|
||||
<span v-if="$v.formData.from_date.$error && !$v.formData.from_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
<div class="col-md-6 report-field-container">
|
||||
<label class="report-label">{{ $t('reports.expenses.to_date') }}</label>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('reports.expenses.to_date')"
|
||||
:error="toDateError"
|
||||
class="mt-5 md:mt-0"
|
||||
>
|
||||
<base-date-picker
|
||||
v-model="formData.to_date"
|
||||
:invalid="$v.formData.to_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
class="mt-2"
|
||||
@change="$v.formData.to_date.$touch()"
|
||||
/>
|
||||
<span v-if="$v.formData.to_date.$error && !$v.formData.to_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row report-submit-button-container">
|
||||
<div class="col-md-6">
|
||||
<base-button outline color="theme" class="report-button" @click="getReports()">
|
||||
{{ $t('reports.update_report') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</sw-input-group>
|
||||
</div>
|
||||
|
||||
<sw-button
|
||||
variant="primary-outline"
|
||||
class="content-center hidden mt-0 w-md md:flex md:mt-8"
|
||||
@click="getReports()"
|
||||
>
|
||||
{{ $t('reports.update_report') }}
|
||||
</sw-button>
|
||||
</div>
|
||||
<div class="col-sm-8 reports-tab-container">
|
||||
<iframe :src="getReportUrl" class="reports-frame-style"/>
|
||||
<a class="base-button btn btn-primary btn-lg report-view-button" @click="viewReportsPDF">
|
||||
<font-awesome-icon icon="file-pdf" class="vue-icon icon-left svg-inline--fa fa-download fa-w-16 mr-2" /> <span>{{ $t('reports.view_pdf') }}</span>
|
||||
|
||||
<div class="col-span-8 mt-0 md:mt-12">
|
||||
<iframe
|
||||
:src="getReportUrl"
|
||||
class="hidden w-full h-screen border-gray-100 border-solid rounded md:flex"
|
||||
/>
|
||||
|
||||
<a
|
||||
class="flex items-center justify-center h-10 px-5 py-1 text-sm font-medium leading-none text-center text-white whitespace-no-wrap rounded md:hidden bg-primary-500"
|
||||
@click="viewReportsPDF"
|
||||
>
|
||||
<document-text-icon />
|
||||
|
||||
<span>{{ $t('reports.view_pdf') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,12 +79,16 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import moment from 'moment'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import { DocumentTextIcon } from '@vue-hero-icons/solid'
|
||||
|
||||
const { required } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
components: {
|
||||
DocumentTextIcon,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
range: new Date(),
|
||||
dateRange: [
|
||||
@@ -76,56 +101,94 @@ export default {
|
||||
'Previous Month',
|
||||
'Previous Quarter',
|
||||
'Previous Year',
|
||||
'Custom'
|
||||
'Custom',
|
||||
],
|
||||
|
||||
selectedRange: 'This Month',
|
||||
formData: {
|
||||
from_date: moment().startOf('month').toString(),
|
||||
to_date: moment().endOf('month').toString()
|
||||
to_date: moment().endOf('month').toString(),
|
||||
},
|
||||
url: null,
|
||||
siteURL: null
|
||||
siteURL: null,
|
||||
}
|
||||
},
|
||||
|
||||
validations: {
|
||||
range: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
formData: {
|
||||
from_date: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
to_date: {
|
||||
required
|
||||
}
|
||||
}
|
||||
required,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters('company', [
|
||||
'getSelectedCompany'
|
||||
]),
|
||||
getReportUrl () {
|
||||
...mapGetters('company', ['getSelectedCompany']),
|
||||
getReportUrl() {
|
||||
return this.url
|
||||
}
|
||||
},
|
||||
|
||||
dateRangeError() {
|
||||
if (!this.$v.range.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.range.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
|
||||
fromDateError() {
|
||||
if (!this.$v.formData.from_date.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.formData.from_date.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
|
||||
toDateError() {
|
||||
if (!this.$v.formData.to_date.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.formData.to_date.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
range (newRange) {
|
||||
range(newRange) {
|
||||
this.formData.from_date = moment(newRange).startOf('year').toString()
|
||||
this.formData.to_date = moment(newRange).endOf('year').toString()
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
|
||||
mounted() {
|
||||
this.siteURL = `/reports/expenses/${this.getSelectedCompany.unique_hash}`
|
||||
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
this.url = `${this.siteURL}?from_date=${moment(
|
||||
this.formData.from_date
|
||||
).format('YYYY-MM-DD')}&to_date=${moment(this.formData.to_date).format(
|
||||
'YYYY-MM-DD'
|
||||
)}`
|
||||
},
|
||||
|
||||
methods: {
|
||||
getThisDate (type, time) {
|
||||
getThisDate(type, time) {
|
||||
return moment()[type](time).toString()
|
||||
},
|
||||
getPreDate (type, time) {
|
||||
getPreDate(type, time) {
|
||||
return moment().subtract(1, time)[type](time).toString()
|
||||
},
|
||||
onChangeDateRange () {
|
||||
onChangeDateRange() {
|
||||
switch (this.selectedRange) {
|
||||
case 'Today':
|
||||
this.formData.from_date = moment().toString()
|
||||
@@ -176,15 +239,17 @@ export default {
|
||||
break
|
||||
}
|
||||
},
|
||||
setRangeToCustom () {
|
||||
|
||||
setRangeToCustom() {
|
||||
this.selectedRange = 'Custom'
|
||||
},
|
||||
async viewReportsPDF () {
|
||||
async viewReportsPDF() {
|
||||
let data = await this.getReports()
|
||||
window.open(this.getReportUrl, '_blank')
|
||||
return data
|
||||
},
|
||||
async getReports (isDownload = false) {
|
||||
|
||||
async getReports(isDownload = false) {
|
||||
this.$v.range.$touch()
|
||||
this.$v.formData.$touch()
|
||||
|
||||
@@ -192,10 +257,11 @@ export default {
|
||||
return true
|
||||
}
|
||||
|
||||
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
this.url = `${this.siteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
|
||||
return true
|
||||
},
|
||||
downloadReport () {
|
||||
|
||||
downloadReport() {
|
||||
if (!this.getReports()) {
|
||||
return false
|
||||
}
|
||||
@@ -203,9 +269,9 @@ export default {
|
||||
window.open(this.getReportUrl + '&download=true')
|
||||
|
||||
setTimeout(() => {
|
||||
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
this.url = `${this.siteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,55 +1,78 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-md-4 reports-tab-container">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<label class="report-label">{{ $t('reports.profit_loss.date_range') }}</label>
|
||||
<base-select
|
||||
<div class="grid gap-8 md:grid-cols-12">
|
||||
<div class="col-span-8 mt-12 md:col-span-4">
|
||||
<div class="grid grid-cols-12">
|
||||
<sw-input-group
|
||||
:label="$t('reports.profit_loss.date_range')"
|
||||
:error="dateRangeError"
|
||||
class="col-span-12 md:col-span-8"
|
||||
>
|
||||
<sw-select
|
||||
v-model="selectedRange"
|
||||
:options="dateRange"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
class="mt-2"
|
||||
@input="onChangeDateRange"
|
||||
/>
|
||||
<span v-if="$v.range.$error && !$v.range.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
</sw-input-group>
|
||||
</div>
|
||||
<div class="row report-fields-container">
|
||||
<div class="col-md-6 report-field-container">
|
||||
<label class="report-label">{{ $t('reports.profit_loss.from_date') }}</label>
|
||||
|
||||
<div class="grid grid-cols-1 mt-6 md:gap-10 md:grid-cols-2">
|
||||
<sw-input-group
|
||||
:label="$t('reports.profit_loss.from_date')"
|
||||
:error="fromDateError"
|
||||
>
|
||||
<base-date-picker
|
||||
v-model="formData.from_date"
|
||||
:invalid="$v.formData.from_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
class="mt-2"
|
||||
@change="$v.formData.from_date.$touch()"
|
||||
/>
|
||||
<span v-if="$v.formData.from_date.$error && !$v.formData.from_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
<div class="col-md-6 report-field-container">
|
||||
<label class="report-label">{{ $t('reports.profit_loss.to_date') }}</label>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('reports.profit_loss.to_date')"
|
||||
:error="toDateError"
|
||||
class="mt-5 md:mt-0"
|
||||
>
|
||||
<base-date-picker
|
||||
v-model="formData.to_date"
|
||||
:invalid="$v.formData.to_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
class="mt-2"
|
||||
@change="$v.formData.to_date.$touch()"
|
||||
/>
|
||||
<span v-if="$v.formData.to_date.$error && !$v.formData.to_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
</sw-input-group>
|
||||
</div>
|
||||
<div class="row report-submit-button-container">
|
||||
<div class="col-md-6">
|
||||
<base-button outline color="theme" class="report-button" @click="getReports()">
|
||||
{{ $t('reports.update_report') }}
|
||||
</base-button>
|
||||
</div>
|
||||
|
||||
<div class="mt-0 md:mt-8">
|
||||
<sw-button
|
||||
variant="primary-outline"
|
||||
class="content-center hidden text-sm w-md md:flex"
|
||||
@click="getReports()"
|
||||
>
|
||||
{{ $t('reports.update_report') }}
|
||||
</sw-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-8 reports-tab-container">
|
||||
<iframe :src="getReportUrl" class="reports-frame-style"/>
|
||||
<a class="base-button btn btn-primary btn-lg report-view-button" @click="viewReportsPDF">
|
||||
<font-awesome-icon icon="file-pdf" class="vue-icon icon-left svg-inline--fa fa-download fa-w-16 mr-2" /> <span>{{ $t('reports.view_pdf') }}</span>
|
||||
|
||||
<div class="col-span-8 mt-0 md:mt-12">
|
||||
<iframe
|
||||
:src="getReportUrl"
|
||||
class="hidden w-full h-screen border-gray-100 border-solid rounded md:flex"
|
||||
/>
|
||||
|
||||
<a
|
||||
class="flex items-center justify-center h-10 px-5 py-1 text-sm font-medium leading-none text-center text-white whitespace-no-wrap rounded md:hidden bg-primary-500"
|
||||
@click="viewReportsPDF"
|
||||
>
|
||||
<document-text-icon />
|
||||
|
||||
<span>{{ $t('reports.view_pdf') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,13 +80,16 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import { DocumentTextIcon } from '@vue-hero-icons/solid'
|
||||
|
||||
import moment from 'moment'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
const { required } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
components: {
|
||||
DocumentTextIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dateRange: [
|
||||
'Today',
|
||||
@@ -75,62 +101,94 @@ export default {
|
||||
'Previous Month',
|
||||
'Previous Quarter',
|
||||
'Previous Year',
|
||||
'Custom'
|
||||
'Custom',
|
||||
],
|
||||
|
||||
selectedRange: 'This Month',
|
||||
range: new Date(),
|
||||
formData: {
|
||||
from_date: moment().startOf('month').toString(),
|
||||
to_date: moment().endOf('month').toString()
|
||||
to_date: moment().endOf('month').toString(),
|
||||
},
|
||||
url: null,
|
||||
siteURL: null
|
||||
siteURL: null,
|
||||
}
|
||||
},
|
||||
|
||||
validations: {
|
||||
range: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
formData: {
|
||||
from_date: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
to_date: {
|
||||
required
|
||||
}
|
||||
}
|
||||
required,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters('company', [
|
||||
'getSelectedCompany'
|
||||
]),
|
||||
getReportUrl () {
|
||||
...mapGetters('company', ['getSelectedCompany']),
|
||||
getReportUrl() {
|
||||
return this.url
|
||||
}
|
||||
},
|
||||
dateRangeError() {
|
||||
if (!this.$v.range.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.range.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
|
||||
fromDateError() {
|
||||
if (!this.$v.formData.from_date.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.formData.from_date.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
|
||||
toDateError() {
|
||||
if (!this.$v.formData.to_date.$error) {
|
||||
}
|
||||
|
||||
if (!this.$v.formData.to_date.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
range (newRange) {
|
||||
range(newRange) {
|
||||
this.formData.from_date = moment(newRange).startOf('year').toString()
|
||||
this.formData.to_date = moment(newRange).endOf('year').toString()
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.siteURL = `/reports/profit-loss/${this.getSelectedCompany.unique_hash}`
|
||||
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
|
||||
this.loadProfitLossLink(this.url + '&download=true')
|
||||
mounted() {
|
||||
this.siteURL = `/reports/profit-loss/${this.getSelectedCompany.unique_hash}`
|
||||
this.url = `${this.siteURL}?from_date=${moment(
|
||||
this.formData.from_date
|
||||
).format('YYYY-MM-DD')}&to_date=${moment(this.formData.to_date).format(
|
||||
'YYYY-MM-DD'
|
||||
)}`
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('profitLossReport', [
|
||||
'loadProfitLossLink'
|
||||
]),
|
||||
getThisDate (type, time) {
|
||||
getThisDate(type, time) {
|
||||
return moment()[type](time).toString()
|
||||
},
|
||||
getPreDate (type, time) {
|
||||
getPreDate(type, time) {
|
||||
return moment().subtract(1, time)[type](time).toString()
|
||||
},
|
||||
onChangeDateRange () {
|
||||
|
||||
onChangeDateRange() {
|
||||
switch (this.selectedRange) {
|
||||
case 'Today':
|
||||
this.formData.from_date = moment().toString()
|
||||
@@ -181,34 +239,38 @@ export default {
|
||||
break
|
||||
}
|
||||
},
|
||||
setRangeToCustom () {
|
||||
|
||||
setRangeToCustom() {
|
||||
this.selectedRange = 'Custom'
|
||||
},
|
||||
async viewReportsPDF () {
|
||||
|
||||
async viewReportsPDF() {
|
||||
let data = await this.getReports()
|
||||
window.open(this.getReportUrl, '_blank')
|
||||
return data
|
||||
},
|
||||
async getReports (isDownload = false) {
|
||||
|
||||
async getReports(isDownload = false) {
|
||||
this.$v.range.$touch()
|
||||
this.$v.formData.$touch()
|
||||
if (this.$v.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
this.url = `${this.siteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
|
||||
return true
|
||||
},
|
||||
downloadReport () {
|
||||
|
||||
downloadReport() {
|
||||
if (!this.getReports()) {
|
||||
return false
|
||||
}
|
||||
|
||||
window.open(this.getReportUrl + '&download=true')
|
||||
setTimeout(() => {
|
||||
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
this.url = `${this.siteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,70 +1,90 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-md-4 reports-tab-container">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<label class="report-label">{{ $t('reports.sales.date_range') }}</label>
|
||||
<base-select
|
||||
<div class="grid gap-8 md:grid-cols-12">
|
||||
<div class="col-span-8 mt-12 md:col-span-4">
|
||||
<div class="grid grid-cols-12">
|
||||
<sw-input-group
|
||||
:label="$t('reports.sales.date_range')"
|
||||
:error="dateRangeError"
|
||||
class="col-span-12 md:col-span-8"
|
||||
>
|
||||
<sw-select
|
||||
v-model="selectedRange"
|
||||
:options="dateRange"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
class="mt-2"
|
||||
@input="onChangeDateRange"
|
||||
/>
|
||||
<span v-if="$v.range.$error && !$v.range.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
</sw-input-group>
|
||||
</div>
|
||||
<div class="row report-fields-container">
|
||||
<div class="col-md-6 report-field-container">
|
||||
<label class="report-label">{{ $t('reports.sales.from_date') }}</label>
|
||||
<div class="grid grid-cols-1 mt-6 md:gap-10 md:grid-cols-2">
|
||||
<sw-input-group
|
||||
:label="$t('reports.sales.from_date')"
|
||||
:error="fromDateError"
|
||||
>
|
||||
<base-date-picker
|
||||
v-model="formData.from_date"
|
||||
:invalid="$v.formData.from_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
class="mt-2"
|
||||
@change="$v.formData.from_date.$touch()"
|
||||
@input="setRangeToCustom"
|
||||
/>
|
||||
<span v-if="$v.formData.from_date.$error && !$v.formData.from_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
<div class="col-md-6 report-field-container">
|
||||
<label class="report-label">{{ $t('reports.sales.to_date') }}</label>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('reports.sales.to_date')"
|
||||
:error="toDateError"
|
||||
class="mt-5 md:mt-0"
|
||||
>
|
||||
<base-date-picker
|
||||
v-model="formData.to_date"
|
||||
:invalid="$v.formData.to_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
class="mt-2"
|
||||
@change="$v.formData.to_date.$touch()"
|
||||
@input="setRangeToCustom"
|
||||
/>
|
||||
<span v-if="$v.formData.to_date.$error && !$v.formData.to_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
</sw-input-group>
|
||||
</div>
|
||||
<div class="row report-fields-container">
|
||||
<div class="col-md-8 report-field-container">
|
||||
<label class="report-label">{{ $t('reports.sales.report_type') }}</label>
|
||||
<base-select
|
||||
|
||||
<div class="grid grid-cols-12 mt-6 md:mt-8">
|
||||
<sw-input-group
|
||||
:label="$t('reports.sales.report_type')"
|
||||
class="col-span-12 md:col-span-8"
|
||||
>
|
||||
<sw-select
|
||||
v-model="selectedType"
|
||||
:options="reportTypes"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
class="mt-2"
|
||||
:placeholder="$t('reports.sales.report_type')"
|
||||
@input="getInitialReport"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row report-submit-button-container">
|
||||
<div class="col-md-6">
|
||||
<base-button outline color="theme" class="report-button" @click="getReports()">
|
||||
{{ $t('reports.update_report') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</sw-input-group>
|
||||
</div>
|
||||
<sw-button
|
||||
variant="primary-outline"
|
||||
class="content-center hidden mt-0 w-md md:flex md:mt-8"
|
||||
type="submit"
|
||||
@click.prevent="getReports()"
|
||||
>
|
||||
{{ $t('reports.update_report') }}
|
||||
</sw-button>
|
||||
</div>
|
||||
<div class="col-sm-8 reports-tab-container">
|
||||
<iframe :src="getReportUrl" class="reports-frame-style"/>
|
||||
<a class="base-button btn btn-primary btn-lg report-view-button" @click="viewReportsPDF">
|
||||
<font-awesome-icon icon="file-pdf" class="vue-icon icon-left svg-inline--fa fa-download fa-w-16 mr-2" /> <span>{{ $t('reports.view_pdf') }}</span>
|
||||
<div class="col-span-8 mt-0 md:mt-12">
|
||||
<iframe
|
||||
:src="getReportUrl"
|
||||
class="hidden w-full h-screen border-gray-100 border-solid rounded md:flex"
|
||||
/>
|
||||
<a
|
||||
class="flex items-center justify-center h-10 px-5 py-1 text-sm font-medium leading-none text-center text-white whitespace-no-wrap rounded md:hidden bg-primary-500"
|
||||
@click="viewReportsPDF"
|
||||
>
|
||||
<document-text-icon />
|
||||
|
||||
<span>{{ $t('reports.view_pdf') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,13 +92,17 @@
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { DocumentTextIcon } from '@vue-hero-icons/solid'
|
||||
import moment from 'moment'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
|
||||
const { required } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
components: {
|
||||
DocumentTextIcon,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
reportTypes: ['By Customer', 'By Item'],
|
||||
selectedType: 'By Customer',
|
||||
@@ -92,63 +116,96 @@ export default {
|
||||
'Previous Month',
|
||||
'Previous Quarter',
|
||||
'Previous Year',
|
||||
'Custom'
|
||||
'Custom',
|
||||
],
|
||||
|
||||
selectedRange: 'This Month',
|
||||
range: new Date(),
|
||||
formData: {
|
||||
from_date: moment().startOf('month').toString(),
|
||||
to_date: moment().endOf('month').toString()
|
||||
to_date: moment().endOf('month').toString(),
|
||||
},
|
||||
url: null,
|
||||
customerSiteURL: null,
|
||||
itemsSiteURL: null
|
||||
itemsSiteURL: null,
|
||||
}
|
||||
},
|
||||
|
||||
validations: {
|
||||
range: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
formData: {
|
||||
from_date: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
to_date: {
|
||||
required
|
||||
}
|
||||
}
|
||||
required,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters('company', [
|
||||
'getSelectedCompany'
|
||||
]),
|
||||
getReportUrl () {
|
||||
...mapGetters('company', ['getSelectedCompany']),
|
||||
getReportUrl() {
|
||||
return this.url
|
||||
}
|
||||
},
|
||||
|
||||
dateRangeError() {
|
||||
if (!this.$v.range.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.range.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
|
||||
fromDateError() {
|
||||
if (!this.$v.formData.from_date.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.formData.from_date.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
|
||||
toDateError() {
|
||||
if (!this.$v.formData.to_date.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.formData.to_date.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
range (newRange) {
|
||||
range(newRange) {
|
||||
this.formData.from_date = moment(newRange).startOf('year').toString()
|
||||
this.formData.to_date = moment(newRange).endOf('year').toString()
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
|
||||
mounted() {
|
||||
this.customerSiteURL = `/reports/sales/customers/${this.getSelectedCompany.unique_hash}`
|
||||
this.itemsSiteURL = `/reports/sales/items/${this.getSelectedCompany.unique_hash}`
|
||||
this.getInitialReport()
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('salesReport', [
|
||||
'loadLinkByCustomer',
|
||||
'loadLinkByItems'
|
||||
]),
|
||||
getThisDate (type, time) {
|
||||
...mapActions('salesReport', ['loadLinkByCustomer', 'loadLinkByItems']),
|
||||
getThisDate(type, time) {
|
||||
return moment()[type](time).toString()
|
||||
},
|
||||
getPreDate (type, time) {
|
||||
|
||||
getPreDate(type, time) {
|
||||
return moment().subtract(1, time)[type](time).toString()
|
||||
},
|
||||
onChangeDateRange () {
|
||||
|
||||
onChangeDateRange() {
|
||||
switch (this.selectedRange) {
|
||||
case 'Today':
|
||||
this.formData.from_date = moment().toString()
|
||||
@@ -199,38 +256,49 @@ export default {
|
||||
break
|
||||
}
|
||||
},
|
||||
setRangeToCustom () {
|
||||
|
||||
setRangeToCustom() {
|
||||
this.selectedRange = 'Custom'
|
||||
},
|
||||
async getInitialReport () {
|
||||
|
||||
async getInitialReport() {
|
||||
if (this.selectedType === 'By Customer') {
|
||||
this.url = `${this.customerSiteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
this.url = `${this.customerSiteURL}?from_date=${moment(
|
||||
this.formData.from_date
|
||||
).format('YYYY-MM-DD')}&to_date=${moment(this.formData.to_date).format(
|
||||
'YYYY-MM-DD'
|
||||
)}`
|
||||
return true
|
||||
}
|
||||
this.url = `${this.itemsSiteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
this.url = `${this.itemsSiteURL}?from_date=${moment(
|
||||
this.formData.from_date
|
||||
).format('YYYY-MM-DD')}&to_date=${moment(this.formData.to_date).format(
|
||||
'YYYY-MM-DD'
|
||||
)}`
|
||||
return true
|
||||
},
|
||||
async viewReportsPDF () {
|
||||
|
||||
async viewReportsPDF() {
|
||||
let data = await this.getReports()
|
||||
window.open(this.getReportUrl, '_blank')
|
||||
return data
|
||||
},
|
||||
async getReports (isDownload = false) {
|
||||
|
||||
async getReports(isDownload = false) {
|
||||
this.$v.range.$touch()
|
||||
this.$v.formData.$touch()
|
||||
if (this.$v.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (this.selectedType === 'By Customer') {
|
||||
this.url = `${this.customerSiteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
this.url = `${this.customerSiteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
|
||||
return true
|
||||
}
|
||||
|
||||
this.url = `${this.itemsSiteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
this.url = `${this.itemsSiteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
|
||||
return true
|
||||
},
|
||||
downloadReport () {
|
||||
|
||||
downloadReport() {
|
||||
if (!this.getReports()) {
|
||||
return false
|
||||
}
|
||||
@@ -238,13 +306,13 @@ export default {
|
||||
window.open(this.getReportUrl + '&download=true')
|
||||
setTimeout(() => {
|
||||
if (this.selectedType === 'By Customer') {
|
||||
this.url = `${this.customerSiteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
this.url = `${this.customerSiteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
|
||||
return true
|
||||
}
|
||||
this.url = `${this.itemsSiteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
this.url = `${this.itemsSiteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
|
||||
return true
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,55 +1,70 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-md-4 reports-tab-container">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<label class="report-label">{{ $t('reports.taxes.date_range') }}</label>
|
||||
<base-select
|
||||
<div class="grid gap-8 md:grid-cols-12">
|
||||
<div class="col-span-8 mt-12 md:col-span-4">
|
||||
<div class="grid grid-cols-12">
|
||||
<sw-input-group
|
||||
:label="$t('reports.taxes.date_range')"
|
||||
:error="dateRangeError"
|
||||
class="col-span-12 md:col-span-8"
|
||||
>
|
||||
<sw-select
|
||||
v-model="selectedRange"
|
||||
:options="dateRange"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
class="mt-2"
|
||||
@input="onChangeDateRange"
|
||||
/>
|
||||
<span v-if="$v.range.$error && !$v.range.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
</sw-input-group>
|
||||
</div>
|
||||
<div class="row report-fields-container">
|
||||
<div class="col-md-6 report-field-container">
|
||||
<label class="report-label">{{ $t('reports.taxes.from_date') }}</label>
|
||||
<div class="grid grid-cols-1 mt-6 md:gap-10 md:grid-cols-2">
|
||||
<sw-input-group
|
||||
:label="$t('reports.taxes.from_date')"
|
||||
:error="fromDateError"
|
||||
>
|
||||
<base-date-picker
|
||||
v-model="formData.from_date"
|
||||
:invalid="$v.formData.from_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
class="mt-2"
|
||||
@change="$v.formData.from_date.$touch()"
|
||||
/>
|
||||
<span v-if="$v.formData.from_date.$error && !$v.formData.from_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
<div class="col-md-6 report-field-container">
|
||||
<label class="report-label">{{ $t('reports.taxes.to_date') }}</label>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('reports.taxes.to_date')"
|
||||
:error="toDateError"
|
||||
class="mt-5 md:mt-0"
|
||||
>
|
||||
<base-date-picker
|
||||
v-model="formData.to_date"
|
||||
:invalid="$v.formData.to_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
class="mt-2"
|
||||
@change="$v.formData.to_date.$touch()"
|
||||
/>
|
||||
<span v-if="$v.formData.to_date.$error && !$v.formData.to_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row report-submit-button-container">
|
||||
<div class="col-md-6">
|
||||
<base-button outline color="theme" class="report-button" @click="getReports()">
|
||||
{{ $t('reports.update_report') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</sw-input-group>
|
||||
</div>
|
||||
<sw-button
|
||||
variant="primary-outline"
|
||||
class="content-center hidden mt-0 w-md md:flex md:mt-8"
|
||||
@click="getReports()"
|
||||
>
|
||||
{{ $t('reports.update_report') }}
|
||||
</sw-button>
|
||||
</div>
|
||||
<div class="col-sm-8 reports-tab-container">
|
||||
<iframe :src="getReportUrl" class="reports-frame-style"/>
|
||||
<a class="base-button btn btn-primary btn-lg report-view-button" @click="viewReportsPDF">
|
||||
<font-awesome-icon icon="file-pdf" class="vue-icon icon-left svg-inline--fa fa-download fa-w-16 mr-2" />
|
||||
<div class="col-span-8 mt-0 md:mt-12">
|
||||
<iframe
|
||||
:src="getReportUrl"
|
||||
class="hidden w-full h-screen border-gray-100 border-solid rounded md:flex"
|
||||
/>
|
||||
<a
|
||||
class="flex items-center justify-center h-10 px-5 py-1 text-sm font-medium leading-none text-center text-white whitespace-no-wrap rounded md:hidden bg-primary-500"
|
||||
@click="viewReportsPDF"
|
||||
>
|
||||
<document-text-icon />
|
||||
<span>{{ $t('reports.view_pdf') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -58,13 +73,16 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import { DocumentTextIcon } from '@vue-hero-icons/solid'
|
||||
import moment from 'moment'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
const { required } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
components: {
|
||||
DocumentTextIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dateRange: [
|
||||
'Today',
|
||||
@@ -76,57 +94,86 @@ export default {
|
||||
'Previous Month',
|
||||
'Previous Quarter',
|
||||
'Previous Year',
|
||||
'Custom'
|
||||
'Custom',
|
||||
],
|
||||
selectedRange: 'This Month',
|
||||
range: new Date(),
|
||||
formData: {
|
||||
from_date: moment().startOf('month').toString(),
|
||||
to_date: moment().endOf('month').toString()
|
||||
to_date: moment().endOf('month').toString(),
|
||||
},
|
||||
url: null,
|
||||
siteURL: null
|
||||
siteURL: null,
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
range: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
formData: {
|
||||
from_date: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
to_date: {
|
||||
required
|
||||
}
|
||||
}
|
||||
required,
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('company', [
|
||||
'getSelectedCompany'
|
||||
]),
|
||||
getReportUrl () {
|
||||
...mapGetters('company', ['getSelectedCompany']),
|
||||
getReportUrl() {
|
||||
return this.url
|
||||
}
|
||||
},
|
||||
dateRangeError() {
|
||||
if (!this.$v.range.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.range.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
fromDateError() {
|
||||
if (!this.$v.formData.from_date.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.formData.from_date.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
toDateError() {
|
||||
if (!this.$v.formData.to_date.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.formData.to_date.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
range (newRange) {
|
||||
range(newRange) {
|
||||
this.formData.from_date = moment(newRange).startOf('year').toString()
|
||||
this.formData.to_date = moment(newRange).endOf('year').toString()
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
this.siteURL = `/reports/tax-summary/${this.getSelectedCompany.unique_hash}`
|
||||
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
this.url = `${this.siteURL}?from_date=${moment(
|
||||
this.formData.from_date
|
||||
).format('YYYY-MM-DD')}&to_date=${moment(this.formData.to_date).format(
|
||||
'YYYY-MM-DD'
|
||||
)}`
|
||||
},
|
||||
methods: {
|
||||
getThisDate (type, time) {
|
||||
getThisDate(type, time) {
|
||||
return moment()[type](time).toString()
|
||||
},
|
||||
getPreDate (type, time) {
|
||||
getPreDate(type, time) {
|
||||
return moment().subtract(1, time)[type](time).toString()
|
||||
},
|
||||
onChangeDateRange () {
|
||||
onChangeDateRange() {
|
||||
switch (this.selectedRange) {
|
||||
case 'Today':
|
||||
this.formData.from_date = moment().toString()
|
||||
@@ -177,15 +224,15 @@ export default {
|
||||
break
|
||||
}
|
||||
},
|
||||
setRangeToCustom () {
|
||||
setRangeToCustom() {
|
||||
this.selectedRange = 'Custom'
|
||||
},
|
||||
async viewReportsPDF () {
|
||||
async viewReportsPDF() {
|
||||
let data = await this.getReports()
|
||||
window.open(this.getReportUrl, '_blank')
|
||||
return data
|
||||
},
|
||||
async getReports (isDownload = false) {
|
||||
async getReports(isDownload = false) {
|
||||
this.$v.range.$touch()
|
||||
this.$v.formData.$touch()
|
||||
|
||||
@@ -193,10 +240,10 @@ export default {
|
||||
return false
|
||||
}
|
||||
|
||||
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
this.url = `${this.siteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
|
||||
return true
|
||||
},
|
||||
downloadReport () {
|
||||
downloadReport() {
|
||||
if (!this.getReports()) {
|
||||
return false
|
||||
}
|
||||
@@ -204,9 +251,9 @@ export default {
|
||||
window.open(this.url + '&download=true')
|
||||
|
||||
setTimeout(() => {
|
||||
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
|
||||
this.url = `${this.siteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,78 +1,93 @@
|
||||
<template>
|
||||
<div class="profit-loss-reports reports main-content">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title"> {{ $tc('reports.report', 2) }}</h3>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="dashboard">
|
||||
{{ $t('general.home') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="/admin/reports/sales">
|
||||
{{ $tc('reports.report', 2) }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="page-actions row">
|
||||
<div class="col-xs-2">
|
||||
<base-button icon="download" size="large" color="theme" @click="onDownload()">
|
||||
{{ $t('reports.download_pdf') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<base-page class="profit-loss-reports reports">
|
||||
<sw-page-header :title="$tc('reports.report', 2)">
|
||||
<sw-breadcrumb slot="breadcrumbs">
|
||||
<sw-breadcrumb-item
|
||||
:title="$t('general.home')"
|
||||
:to="`/admin/dashboard`"
|
||||
/>
|
||||
<sw-breadcrumb-item
|
||||
:title="$tc('reports.report', 2)"
|
||||
:to="`/admin/reports`"
|
||||
active
|
||||
/>
|
||||
</sw-breadcrumb>
|
||||
<template slot="actions">
|
||||
<sw-button size="lg" variant="primary" @click="onDownload()">
|
||||
<download-icon class="h-5 mr-1 -ml-2" />
|
||||
{{ $t('reports.download_pdf') }}
|
||||
</sw-button>
|
||||
</template>
|
||||
</sw-page-header>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<!-- Tabs -->
|
||||
<ul class="tabs">
|
||||
<li class="tab">
|
||||
<router-link class="tab-link" to="/admin/reports/sales">{{ $t('reports.sales.sales') }}</router-link>
|
||||
</li>
|
||||
<li class="tab">
|
||||
<router-link class="tab-link" to="/admin/reports/profit-loss">{{ $t('reports.profit_loss.profit_loss') }}</router-link>
|
||||
</li>
|
||||
<li class="tab">
|
||||
<router-link class="tab-link" to="/admin/reports/expenses">{{ $t('reports.expenses.expenses') }}</router-link>
|
||||
</li>
|
||||
<li class="tab">
|
||||
<router-link class="tab-link" to="/admin/reports/taxes">{{ $t('reports.taxes.taxes') }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Tabs -->
|
||||
<sw-tabs>
|
||||
<sw-tab-item
|
||||
:title="$t('reports.sales.sales')"
|
||||
route="/admin/reports/sales"
|
||||
>
|
||||
</sw-tab-item>
|
||||
|
||||
<sw-tab-item
|
||||
:title="$t('reports.profit_loss.profit_loss')"
|
||||
route="/admin/reports/profit-loss"
|
||||
>
|
||||
</sw-tab-item>
|
||||
|
||||
<sw-tab-item
|
||||
:title="$t('reports.expenses.expenses')"
|
||||
route="/admin/reports/expenses"
|
||||
>
|
||||
</sw-tab-item>
|
||||
|
||||
<sw-tab-item
|
||||
:title="$t('reports.taxes.taxes')"
|
||||
route="/admin/reports/taxes"
|
||||
>
|
||||
</sw-tab-item>
|
||||
</sw-tabs>
|
||||
</div>
|
||||
<transition
|
||||
name="fade"
|
||||
mode="out-in">
|
||||
<router-view ref="report"/>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div
|
||||
v-if="activeTab === 'SALES' || 'PROFIT_LOSS' || 'EXPENSES' || 'TAXES'"
|
||||
>
|
||||
<router-view ref="report" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</base-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DownloadIcon } from '@vue-hero-icons/solid'
|
||||
export default {
|
||||
components: {
|
||||
DownloadIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'SALES',
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.path' (newValue) {
|
||||
'$route.path'(newValue) {
|
||||
if (newValue === '/admin/reports') {
|
||||
this.$router.push('/admin/reports/sales')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
if (this.$route.path === '/admin/reports') {
|
||||
this.$router.push('/admin/reports/sales')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onDownload () {
|
||||
onDownload() {
|
||||
this.$refs.report.downloadReport()
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
setActiveTab(val) {
|
||||
this.activeTab = val
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -83,6 +98,6 @@ export default {
|
||||
|
||||
.tab-link {
|
||||
padding: 10px 30px;
|
||||
display: block
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
233
resources/assets/js/views/settings/BackupSetting.vue
Normal file
233
resources/assets/js/views/settings/BackupSetting.vue
Normal 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>
|
||||
@@ -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>
|
||||
336
resources/assets/js/views/settings/CompanyInfoSetting.vue
Normal file
336
resources/assets/js/views/settings/CompanyInfoSetting.vue
Normal 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>
|
||||
183
resources/assets/js/views/settings/CustomFieldsSetting.vue
Normal file
183
resources/assets/js/views/settings/CustomFieldsSetting.vue
Normal 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>
|
||||
@@ -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>
|
||||
87
resources/assets/js/views/settings/CustomizationSetting.vue
Normal file
87
resources/assets/js/views/settings/CustomizationSetting.vue
Normal 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>
|
||||
@@ -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>
|
||||
179
resources/assets/js/views/settings/ExpenseCategorySetting.vue
Normal file
179
resources/assets/js/views/settings/ExpenseCategorySetting.vue
Normal 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>
|
||||
297
resources/assets/js/views/settings/FileDiskSetting.vue
Normal file
297
resources/assets/js/views/settings/FileDiskSetting.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
125
resources/assets/js/views/settings/MailConfigSetting.vue
Normal file
125
resources/assets/js/views/settings/MailConfigSetting.vue
Normal 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>
|
||||
160
resources/assets/js/views/settings/NotesSetting.vue
Normal file
160
resources/assets/js/views/settings/NotesSetting.vue
Normal 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>
|
||||
@@ -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>
|
||||
228
resources/assets/js/views/settings/NotificationsSetting.vue
Normal file
228
resources/assets/js/views/settings/NotificationsSetting.vue
Normal 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>
|
||||
148
resources/assets/js/views/settings/PaymentsModeSetting.vue
Normal file
148
resources/assets/js/views/settings/PaymentsModeSetting.vue
Normal 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>
|
||||
@@ -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>
|
||||
365
resources/assets/js/views/settings/PreferencesSetting.vue
Normal file
365
resources/assets/js/views/settings/PreferencesSetting.vue
Normal 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>
|
||||
213
resources/assets/js/views/settings/SettingsIndex.vue
Normal file
213
resources/assets/js/views/settings/SettingsIndex.vue
Normal 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>
|
||||
@@ -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>
|
||||
243
resources/assets/js/views/settings/TaxTypesSetting.vue
Normal file
243
resources/assets/js/views/settings/TaxTypesSetting.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
372
resources/assets/js/views/settings/UserProfileSetting.vue
Normal file
372
resources/assets/js/views/settings/UserProfileSetting.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
334
resources/assets/js/views/settings/mail-driver/SesMailDriver.vue
Normal file
334
resources/assets/js/views/settings/mail-driver/SesMailDriver.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
281
resources/assets/js/views/users/Create.vue
Normal file
281
resources/assets/js/views/users/Create.vue
Normal file
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<base-page v-if="isSuperAdmin" class="item-create">
|
||||
<sw-page-header class="mb-3" :title="pageTitle">
|
||||
<sw-breadcrumb slot="breadcrumbs">
|
||||
<sw-breadcrumb-item to="/admin/dashboard" :title="$t('general.home')" />
|
||||
<sw-breadcrumb-item to="/admin/users" :title="$tc('users.user', 2)" />
|
||||
<sw-breadcrumb-item
|
||||
v-if="$route.name === 'users.edit'"
|
||||
to="#"
|
||||
:title="$t('users.edit_user')"
|
||||
active
|
||||
/>
|
||||
<sw-breadcrumb-item
|
||||
v-else
|
||||
to="#"
|
||||
:title="$t('users.new_user')"
|
||||
active
|
||||
/>
|
||||
</sw-breadcrumb>
|
||||
<template slot="actions"></template>
|
||||
</sw-page-header>
|
||||
|
||||
<div class="grid grid-cols-12">
|
||||
<div class="col-span-12 md:col-span-8">
|
||||
<form action="" @submit.prevent="submitUser">
|
||||
<sw-card>
|
||||
<sw-input-group
|
||||
:label="$t('users.name')"
|
||||
:error="nameError"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<sw-input
|
||||
v-model.trim="formData.name"
|
||||
:invalid="$v.formData.name.$error"
|
||||
class="mt-2"
|
||||
focus
|
||||
type="text"
|
||||
name="name"
|
||||
@input="$v.formData.name.$touch()"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$t('users.email')"
|
||||
class="mt-4"
|
||||
:error="emailError"
|
||||
required
|
||||
>
|
||||
<sw-input
|
||||
:invalid="$v.formData.email.$error"
|
||||
v-model.trim="formData.email"
|
||||
type="text"
|
||||
name="email"
|
||||
tab-index="3"
|
||||
@input="$v.formData.email.$touch()"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group
|
||||
:label="$tc('users.password')"
|
||||
:error="passwordError"
|
||||
required
|
||||
class="mt-4"
|
||||
>
|
||||
<sw-input
|
||||
v-model="formData.password"
|
||||
:invalid="$v.formData.password.$error"
|
||||
type="password"
|
||||
class="mt-2"
|
||||
@input="$v.formData.password.$touch()"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group :label="$t('users.phone')" class="mt-4 mb-6">
|
||||
<sw-input
|
||||
v-model.trim="formData.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
tab-index="4"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<div class="mt-6 mb-4">
|
||||
<sw-button
|
||||
:loading="isLoading"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
size="lg"
|
||||
class="flex justify-center w-full md:w-auto"
|
||||
>
|
||||
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
|
||||
{{ isEdit ? $t('users.update_user') : $t('users.save_user') }}
|
||||
</sw-button>
|
||||
</div>
|
||||
</sw-card>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</base-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
const {
|
||||
required,
|
||||
minLength,
|
||||
email,
|
||||
numeric,
|
||||
minValue,
|
||||
maxLength,
|
||||
requiredIf,
|
||||
} = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
title: 'Add User',
|
||||
|
||||
formData: {
|
||||
name: '',
|
||||
email: null,
|
||||
password: null,
|
||||
phone: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters('user', ['currentUser']),
|
||||
isSuperAdmin() {
|
||||
return this.currentUser.role == 'super admin'
|
||||
},
|
||||
|
||||
pageTitle() {
|
||||
if (this.$route.name === 'users.edit') {
|
||||
return this.$t('users.edit_user')
|
||||
}
|
||||
return this.$t('users.new_user')
|
||||
},
|
||||
|
||||
isEdit() {
|
||||
if (this.$route.name === 'users.edit') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
nameError() {
|
||||
if (!this.$v.formData.name.$error) {
|
||||
return ''
|
||||
}
|
||||
if (!this.$v.formData.name.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
if (!this.$v.formData.name.minLength) {
|
||||
return this.$tc(
|
||||
'validation.name_min_length',
|
||||
this.$v.formData.name.$params.minLength.min,
|
||||
{ count: this.$v.formData.name.$params.minLength.min }
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
emailError() {
|
||||
if (!this.$v.formData.email.$error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!this.$v.formData.email.email) {
|
||||
return this.$tc('validation.email_incorrect')
|
||||
}
|
||||
|
||||
if (!this.$v.formData.email.required) {
|
||||
return this.$tc('validation.required')
|
||||
}
|
||||
},
|
||||
|
||||
passwordError() {
|
||||
if (!this.$v.formData.password.$error) {
|
||||
return ''
|
||||
}
|
||||
if (!this.$v.formData.password.required) {
|
||||
return this.$t('validation.required')
|
||||
}
|
||||
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 }
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
if (!this.isSuperAdmin) {
|
||||
this.$router.push('/admin/dashboard')
|
||||
}
|
||||
if (this.isEdit) {
|
||||
this.loadEditData()
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$v.formData.$reset()
|
||||
},
|
||||
validations: {
|
||||
formData: {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(3),
|
||||
},
|
||||
email: {
|
||||
email,
|
||||
required,
|
||||
},
|
||||
|
||||
password: {
|
||||
required: requiredIf(function () {
|
||||
return !this.isEdit
|
||||
}),
|
||||
minLength: minLength(8),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('users', ['addUser', 'fetchUser', 'updateUser']),
|
||||
|
||||
async loadEditData() {
|
||||
let response = await this.fetchUser(this.$route.params.id)
|
||||
|
||||
if (response.data) {
|
||||
this.formData = { ...this.formData, ...response.data.user }
|
||||
}
|
||||
},
|
||||
|
||||
async submitUser() {
|
||||
this.$v.formData.$touch()
|
||||
|
||||
if (this.$v.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
let response
|
||||
this.isLoading = true
|
||||
if (this.isEdit) {
|
||||
response = await this.updateUser(this.formData)
|
||||
let data
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](this.$tc('users.updated_message'))
|
||||
this.$router.push('/admin/users')
|
||||
this.isLoading = false
|
||||
}
|
||||
if (response.data.error) {
|
||||
window.toastr['error'](this.$t('validation.email_already_taken'))
|
||||
}
|
||||
} else {
|
||||
response = await this.addUser(this.formData)
|
||||
let data
|
||||
if (response.data.success) {
|
||||
this.isLoading = false
|
||||
if (!this.isEdit) {
|
||||
window.toastr['success'](this.$tc('users.created_message'))
|
||||
this.$router.push('/admin/users')
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.response.data.errors.email) {
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
439
resources/assets/js/views/users/Index.vue
Normal file
439
resources/assets/js/views/users/Index.vue
Normal file
@@ -0,0 +1,439 @@
|
||||
<template>
|
||||
<base-page v-if="isSuperAdmin" class="items">
|
||||
<sw-page-header :title="$t('users.title')">
|
||||
<sw-breadcrumb slot="breadcrumbs">
|
||||
<sw-breadcrumb-item to="dashboard" :title="$t('general.home')" />
|
||||
<sw-breadcrumb-item to="#" :title="$tc('users.title', 2)" active />
|
||||
</sw-breadcrumb>
|
||||
|
||||
<template slot="actions">
|
||||
<sw-button
|
||||
v-show="totalUsers"
|
||||
variant="primary-outline"
|
||||
size="lg"
|
||||
@click="toggleFilter"
|
||||
>
|
||||
{{ $t('general.filter') }}
|
||||
<component :is="filterIcon" class="w-4 h-4 ml-2 -mr-1" />
|
||||
</sw-button>
|
||||
|
||||
<sw-button
|
||||
tag-name="router-link"
|
||||
to="users/create"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
class="ml-4"
|
||||
>
|
||||
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
|
||||
{{ $t('users.add_user') }}
|
||||
</sw-button>
|
||||
</template>
|
||||
</sw-page-header>
|
||||
|
||||
<slide-y-up-transition>
|
||||
<sw-filter-wrapper v-show="showFilters" class="mt-3">
|
||||
<sw-input-group :label="$tc('users.name')" class="flex-1 mt-2 mr-4">
|
||||
<sw-input
|
||||
v-model="filters.name"
|
||||
type="text"
|
||||
name="name"
|
||||
class="mt-2"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group :label="$tc('users.email')" class="flex-1 mt-2 mr-4">
|
||||
<sw-input
|
||||
v-model="filters.email"
|
||||
type="text"
|
||||
name="email"
|
||||
class="mt-2"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<sw-input-group :label="$tc('users.phone')" class="flex-1 mt-2">
|
||||
<sw-input
|
||||
v-model="filters.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
class="mt-2"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</sw-input-group>
|
||||
|
||||
<label
|
||||
class="absolute text-sm leading-snug text-gray-900 cursor-pointer"
|
||||
style="top: 10px; right: 15px"
|
||||
@click="clearFilter"
|
||||
>
|
||||
{{ $t('general.clear_all') }}</label
|
||||
>
|
||||
</sw-filter-wrapper>
|
||||
</slide-y-up-transition>
|
||||
|
||||
<sw-empty-table-placeholder
|
||||
v-show="showEmptyScreen"
|
||||
:title="$t('users.no_users')"
|
||||
:description="$t('users.list_of_users')"
|
||||
>
|
||||
<astronaut-icon class="mt-5 mb-4" />
|
||||
|
||||
<sw-button
|
||||
slot="actions"
|
||||
tag-name="router-link"
|
||||
to="/admin/users/create"
|
||||
size="lg"
|
||||
variant="primary-outline"
|
||||
>
|
||||
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
|
||||
{{ $t('users.add_user') }}
|
||||
</sw-button>
|
||||
</sw-empty-table-placeholder>
|
||||
|
||||
<div class="relative table-container" v-show="!showEmptyScreen">
|
||||
<div
|
||||
class="relative flex items-center justify-between h-10 mt-5 list-none border-b-2 border-gray-200 border-solid"
|
||||
>
|
||||
<p class="text-sm">
|
||||
{{ $t('general.showing') }}: <b>{{ users.length }}</b>
|
||||
|
||||
{{ $t('general.of') }}
|
||||
|
||||
<b>{{ totalUsers }}</b>
|
||||
</p>
|
||||
|
||||
<sw-transition type="fade">
|
||||
<sw-dropdown v-if="selectedUsers.length">
|
||||
<span
|
||||
slot="activator"
|
||||
class="flex block text-sm font-medium cursor-pointer select-none text-primary-400"
|
||||
>
|
||||
{{ $t('general.actions') }}
|
||||
<chevron-down-icon class="h-5" />
|
||||
</span>
|
||||
|
||||
<sw-dropdown-item @click="removeMultipleUsers">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</sw-transition>
|
||||
</div>
|
||||
|
||||
<div class="absolute z-10 items-center pl-4 mt-2 select-none md:mt-12">
|
||||
<sw-checkbox
|
||||
v-model="selectAllFieldStatus"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="hidden md:inline"
|
||||
@change="selectAllUsers"
|
||||
/>
|
||||
|
||||
<sw-checkbox
|
||||
v-model="selectAllFieldStatus"
|
||||
:label="$t('general.select_all')"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="md:hidden"
|
||||
@change="selectAllUsers"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<sw-table-component
|
||||
ref="table"
|
||||
:data="fetchData"
|
||||
:show-filter="false"
|
||||
table-class="table"
|
||||
>
|
||||
<sw-table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
cell-class="no-click"
|
||||
>
|
||||
<div slot-scope="row" class="custom-control custom-checkbox">
|
||||
<sw-checkbox
|
||||
:id="row.id"
|
||||
v-model="selectField"
|
||||
:value="row.id"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column :sortable="true" :label="$t('users.name')" show="name">
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('users.name') }}</span>
|
||||
<router-link
|
||||
:to="{ path: `users/${row.id}/edit` }"
|
||||
class="font-medium text-primary-500"
|
||||
>
|
||||
{{ row.name }}
|
||||
</router-link>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('users.email')"
|
||||
show="email"
|
||||
/>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('users.phone')"
|
||||
show="phone"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('users.phone') }}</span>
|
||||
<span>{{ row.phone ? row.phone : 'No Contact' }} </span>
|
||||
</template>
|
||||
</sw-table-column>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:label="$t('users.added_on')"
|
||||
sort-as="created_at"
|
||||
show="formattedCreatedAt"
|
||||
/>
|
||||
|
||||
<sw-table-column
|
||||
:sortable="true"
|
||||
:filterable="false"
|
||||
cell-class="action-dropdown"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span> {{ $t('users.action') }} </span>
|
||||
<sw-dropdown>
|
||||
<dot-icon slot="activator" />
|
||||
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`users/${row.id}/edit`"
|
||||
>
|
||||
<pencil-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.edit') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="removeUser(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>
|
||||
</base-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import AstronautIcon from '@/components/icon/AstronautIcon'
|
||||
import {
|
||||
FilterIcon,
|
||||
XIcon,
|
||||
ChevronDownIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
PlusIcon,
|
||||
} from '@vue-hero-icons/solid'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AstronautIcon,
|
||||
FilterIcon,
|
||||
XIcon,
|
||||
ChevronDownIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
PlusIcon,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
showFilters: false,
|
||||
sortedBy: 'created_at',
|
||||
isRequestOngoing: true,
|
||||
|
||||
filters: {
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('user', ['currentUser']),
|
||||
...mapGetters('users', [
|
||||
'users',
|
||||
'selectedUsers',
|
||||
'totalUsers',
|
||||
'selectAllField',
|
||||
]),
|
||||
isSuperAdmin() {
|
||||
return this.currentUser.role == 'super admin'
|
||||
},
|
||||
showEmptyScreen() {
|
||||
return !this.totalUsers && !this.isRequestOngoing
|
||||
},
|
||||
|
||||
filterIcon() {
|
||||
return this.showFilters ? 'x-icon' : 'filter-icon'
|
||||
},
|
||||
|
||||
selectField: {
|
||||
get: function () {
|
||||
return this.selectedUsers
|
||||
},
|
||||
set: function (val) {
|
||||
this.selectedUser(val)
|
||||
},
|
||||
},
|
||||
|
||||
selectAllFieldStatus: {
|
||||
get: function () {
|
||||
return this.selectAllField
|
||||
},
|
||||
set: function (val) {
|
||||
this.setSelectAllState(val)
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (!this.isSuperAdmin) {
|
||||
this.$router.push('/admin/dashboard')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
filters: {
|
||||
handler: 'setFilters',
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
if (this.selectAllField) {
|
||||
this.selectAllUsers()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('users', [
|
||||
'fetchUsers',
|
||||
'selectAllUsers',
|
||||
'selectedUser',
|
||||
'deleteUser',
|
||||
'deleteMultipleUsers',
|
||||
'setSelectAllState',
|
||||
]),
|
||||
|
||||
refreshTable() {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
|
||||
async fetchData({ page, filter, sort }) {
|
||||
let data = {
|
||||
display_name: this.filters.name !== null ? this.filters.name : '',
|
||||
phone: this.filters.phone !== null ? this.filters.phone : '',
|
||||
email: this.filters.email !== null ? this.filters.email : '',
|
||||
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page,
|
||||
}
|
||||
|
||||
this.isRequestOngoing = true
|
||||
|
||||
let response = await this.fetchUsers(data)
|
||||
|
||||
this.isRequestOngoing = false
|
||||
|
||||
return {
|
||||
data: response.data.users.data,
|
||||
pagination: {
|
||||
totalPages: response.data.users.last_page,
|
||||
currentPage: page,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
setFilters() {
|
||||
this.refreshTable()
|
||||
},
|
||||
|
||||
clearFilter() {
|
||||
this.filters = {
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
}
|
||||
},
|
||||
|
||||
toggleFilter() {
|
||||
if (this.showFilters) {
|
||||
this.clearFilter()
|
||||
}
|
||||
|
||||
this.showFilters = !this.showFilters
|
||||
},
|
||||
|
||||
async removeUser(id) {
|
||||
let user = []
|
||||
user.push(id)
|
||||
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('users.confirm_delete'),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteUser(user)
|
||||
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$tc('users.deleted_message', 1))
|
||||
this.$refs.table.refresh()
|
||||
return true
|
||||
}
|
||||
|
||||
if (res.data.error === 'user_attached') {
|
||||
window.toastr['error'](
|
||||
this.$tc('users.user_attached_message'),
|
||||
this.$t('general.action_failed')
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
window.toastr['error'](res.data.message)
|
||||
return true
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async removeMultipleUsers() {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('users.confirm_delete', 2),
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteMultipleUsers()
|
||||
|
||||
if (res.data.success || res.data.users) {
|
||||
window.toastr['success'](this.$tc('users.deleted_message', 2))
|
||||
this.$refs.table.refresh()
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -1,266 +0,0 @@
|
||||
<template>
|
||||
<div class="card-body">
|
||||
<form action="" @submit.prevent="next()">
|
||||
<p class="form-title">{{ $t('wizard.company_info') }}</p>
|
||||
<p class="form-desc">{{ $t('wizard.company_info_desc') }}</p>
|
||||
<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"> {{ $t('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">
|
||||
<label class="form-label">{{ $t('wizard.company_name') }}</label><span class="text-danger"> *</span>
|
||||
<base-input
|
||||
:invalid="$v.companyData.name.$error"
|
||||
v-model.trim="companyData.name"
|
||||
type="text"
|
||||
name="name"
|
||||
@input="$v.companyData.name.$touch()"
|
||||
/>
|
||||
<div v-if="$v.companyData.name.$error">
|
||||
<span v-if="!$v.companyData.name.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.country') }}</label><span class="text-danger"> *</span>
|
||||
<base-select
|
||||
v-model="country"
|
||||
:class="{'error': $v.companyData.country_id.$error }"
|
||||
:options="countries"
|
||||
:searchable="true"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('general.select_country')"
|
||||
track-by="id"
|
||||
label="name"
|
||||
/>
|
||||
<div v-if="$v.companyData.country_id.$error">
|
||||
<span v-if="!$v.companyData.country_id.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.state') }}</label>
|
||||
<base-input
|
||||
v-model="companyData.state"
|
||||
name="state"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.city') }}</label>
|
||||
<base-input
|
||||
v-model="companyData.city"
|
||||
name="city"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.address') }}</label>
|
||||
<base-text-area
|
||||
:invalid="$v.companyData.address_street_1.$error"
|
||||
v-model.trim="companyData.address_street_1"
|
||||
:placeholder="$t('general.street_1')"
|
||||
name="billing_street1"
|
||||
rows="2"
|
||||
@input="$v.companyData.address_street_1.$touch()"
|
||||
/>
|
||||
<div v-if="$v.companyData.address_street_1.$error">
|
||||
<span v-if="!$v.companyData.address_street_1.maxLength" class="text-danger">{{ $t('validation.description_maxlength') }}</span>
|
||||
</div>
|
||||
<base-text-area
|
||||
:invalid="$v.companyData.address_street_2.$error"
|
||||
v-model="companyData.address_street_2"
|
||||
:placeholder="$t('general.street_2')"
|
||||
name="billing_street2"
|
||||
rows="2"
|
||||
@input="$v.companyData.address_street_2.$touch()"
|
||||
/>
|
||||
<div v-if="$v.companyData.address_street_2.$error">
|
||||
<span v-if="!$v.companyData.address_street_2.maxLength" class="text-danger">{{ $t('validation.description_maxlength') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">{{ $t('wizard.zip_code') }}</label>
|
||||
<base-input
|
||||
v-model.trim="companyData.zip"
|
||||
type="text"
|
||||
name="zip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">{{ $t('wizard.phone') }}</label>
|
||||
<base-input
|
||||
v-model.trim="companyData.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<base-button
|
||||
:loading="loading"
|
||||
class="pull-right"
|
||||
icon="save"
|
||||
color="theme"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('wizard.save_cont') }}
|
||||
</base-button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import MultiSelect from 'vue-multiselect'
|
||||
import AvatarCropper from 'vue-avatar-cropper'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
const { required, maxLength } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MultiSelect,
|
||||
AvatarCropper
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
cropperOutputOptions: {
|
||||
width: 150,
|
||||
height: 150
|
||||
},
|
||||
cropperOptions: {
|
||||
autoCropArea: 1,
|
||||
viewMode: 0,
|
||||
movable: true,
|
||||
zoomable: true
|
||||
},
|
||||
companyData: {
|
||||
logo: '',
|
||||
name: null,
|
||||
address_street_1: '',
|
||||
address_street_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
country_id: '',
|
||||
zip: '',
|
||||
phone: ''
|
||||
},
|
||||
loading: false,
|
||||
step: 1,
|
||||
countries: [],
|
||||
country: null,
|
||||
previewLogo: null
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
companyData: {
|
||||
name: {
|
||||
required
|
||||
},
|
||||
country_id: {
|
||||
required
|
||||
},
|
||||
address_street_1: {
|
||||
maxLength: maxLength(255)
|
||||
},
|
||||
address_street_2: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
country ({ id }) {
|
||||
this.companyData.country_id = id
|
||||
return true
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.fetchCountry()
|
||||
},
|
||||
methods: {
|
||||
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 next () {
|
||||
this.$v.companyData.$touch()
|
||||
if (this.$v.companyData.$invalid) {
|
||||
return true
|
||||
}
|
||||
this.loading = true
|
||||
let response = await window.axios.post('/api/admin/onboarding/company', this.companyData)
|
||||
|
||||
if (response.data) {
|
||||
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/admin/onboarding/company/upload-logo', logoData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'company': response.data.user.company.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.$emit('next')
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
onFileChange (e) {
|
||||
var input = event.target
|
||||
this.companyData.logo = input.files[0]
|
||||
if (input.files && input.files[0]) {
|
||||
var reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
this.previewLogo = e.target.result
|
||||
}
|
||||
reader.readAsDataURL(input.files[0])
|
||||
}
|
||||
},
|
||||
async fetchCountry () {
|
||||
let res = await window.axios.get('/api/countries')
|
||||
if (res) {
|
||||
this.countries = res.data.countries
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,219 +0,0 @@
|
||||
<template>
|
||||
<div class="card-body">
|
||||
<form action="" @submit.prevent="next()">
|
||||
<p class="form-title">{{ $t('wizard.database.database') }}</p>
|
||||
<p class="form-desc">{{ $t('wizard.database.desc') }}</p>
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.database.app_url') }}</label>
|
||||
<span class="text-danger"> * </span>
|
||||
<base-input
|
||||
:invalid="$v.databaseData.app_url.$error"
|
||||
v-model.trim="databaseData.app_url"
|
||||
type="text"
|
||||
name="name"
|
||||
@input="$v.databaseData.app_url.$touch()"
|
||||
/>
|
||||
<div v-if="$v.databaseData.app_url.$error">
|
||||
<span v-if="!$v.databaseData.app_url.required" class="text-danger">
|
||||
{{ $tc('validation.required') }}
|
||||
</span>
|
||||
<span v-if="!$v.databaseData.app_url.isUrl" class="text-danger">
|
||||
{{ $tc('validation.invalid_url') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.database.connection') }}</label>
|
||||
<span class="text-danger"> *</span>
|
||||
<base-select
|
||||
v-model="databaseData.database_connection"
|
||||
:invalid="$v.databaseData.database_connection.$error"
|
||||
:options="connections"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
@change="$v.databaseData.database_connection.$touch()"
|
||||
/>
|
||||
<div v-if="$v.databaseData.database_connection.$error">
|
||||
<span v-if="!$v.databaseData.database_connection.required" class="text-danger">
|
||||
{{ $tc('validation.required') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.database.port') }}</label>
|
||||
<span class="text-danger"> *</span>
|
||||
<base-input
|
||||
:invalid="$v.databaseData.database_port.$error"
|
||||
v-model.trim="databaseData.database_port"
|
||||
type="text"
|
||||
name="database_port"
|
||||
@input="$v.databaseData.database_port.$touch()"
|
||||
/>
|
||||
<div v-if="$v.databaseData.database_port.$error">
|
||||
<span v-if="!$v.databaseData.database_port.required" class="text-danger">
|
||||
{{ $tc('validation.required') }}
|
||||
</span>
|
||||
<span v-if="!$v.databaseData.database_port.numeric" class="text-danger">
|
||||
{{ $tc('validation.numbers_only') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.database.db_name') }}</label>
|
||||
<span class="text-danger"> *</span>
|
||||
<base-input
|
||||
:invalid="$v.databaseData.database_name.$error"
|
||||
v-model.trim="databaseData.database_name"
|
||||
type="text"
|
||||
name="database_name"
|
||||
@input="$v.databaseData.database_name.$touch()"
|
||||
/>
|
||||
<div v-if="$v.databaseData.database_name.$error">
|
||||
<span v-if="!$v.databaseData.database_name.required" class="text-danger">
|
||||
{{ $tc('validation.required') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.database.username') }}</label>
|
||||
<span class="text-danger"> *</span>
|
||||
<base-input
|
||||
:invalid="$v.databaseData.database_username.$error"
|
||||
v-model.trim="databaseData.database_username"
|
||||
type="text"
|
||||
name="database_username"
|
||||
@input="$v.databaseData.database_username.$touch()"
|
||||
/>
|
||||
<div v-if="$v.databaseData.database_username.$error">
|
||||
<span v-if="!$v.databaseData.database_username.required" class="text-danger">
|
||||
{{ $tc('validation.required') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.database.password') }}</label>
|
||||
<base-input
|
||||
v-model.trim="databaseData.database_password"
|
||||
type="password"
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.database.host') }}</label>
|
||||
<span class="text-danger"> *</span>
|
||||
<base-input
|
||||
:invalid="$v.databaseData.database_hostname.$error"
|
||||
v-model.trim="databaseData.database_hostname"
|
||||
type="text"
|
||||
name="database_hostname"
|
||||
@input="$v.databaseData.database_hostname.$touch()"
|
||||
/>
|
||||
<div v-if="$v.databaseData.database_hostname.$error">
|
||||
<span v-if="!$v.databaseData.database_hostname.required" class="text-danger">
|
||||
{{ $tc('validation.required') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<base-button
|
||||
:loading="loading"
|
||||
class="pull-right mt-5"
|
||||
icon="save"
|
||||
color="theme"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('wizard.save_cont') }}
|
||||
</base-button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import MultiSelect from 'vue-multiselect'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
const { required, numeric, url } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MultiSelect
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
databaseData: {
|
||||
database_connection: 'mysql',
|
||||
database_hostname: '127.0.0.1',
|
||||
database_port: '3306',
|
||||
database_name: null,
|
||||
database_username: null,
|
||||
database_password: null,
|
||||
app_url: window.location.origin
|
||||
},
|
||||
loading: false,
|
||||
connections: [
|
||||
'sqlite',
|
||||
'mysql',
|
||||
'pgsql',
|
||||
'sqlsrv'
|
||||
]
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
databaseData: {
|
||||
database_connection: {
|
||||
required
|
||||
},
|
||||
database_hostname: {
|
||||
required
|
||||
},
|
||||
database_port: {
|
||||
required,
|
||||
numeric
|
||||
},
|
||||
database_name: {
|
||||
required
|
||||
},
|
||||
database_username: {
|
||||
required
|
||||
},
|
||||
app_url: {
|
||||
required,
|
||||
isUrl (val) {
|
||||
return this.$utils.checkValidUrl(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async next () {
|
||||
this.$v.databaseData.$touch()
|
||||
if (this.$v.databaseData.$invalid) {
|
||||
return true
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
let response = await window.axios.post('/api/admin/onboarding/environment/database', this.databaseData)
|
||||
if (response.data.success) {
|
||||
this.$emit('next')
|
||||
window.toastr['success'](this.$t('wizard.success.' + response.data.success))
|
||||
return true
|
||||
} else if (response.data.error) {
|
||||
window.toastr['error'](this.$t('wizard.errors.' + response.data.error))
|
||||
} else if (response.data.error_message) {
|
||||
window.toastr['error'](response.data.error_message)
|
||||
}
|
||||
} catch (e) {
|
||||
window.toastr['error'](e.response.data.message)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<div class="card-body">
|
||||
<form action="" @submit.prevent="next()">
|
||||
<p class="form-title">{{ $t('wizard.mail.mail_config') }}</p>
|
||||
<p class="form-desc">{{ $t('wizard.mail.mail_config_desc') }}</p>
|
||||
<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="next"
|
||||
/>
|
||||
</form>
|
||||
</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'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MultiSelect,
|
||||
Smtp,
|
||||
Mailgun,
|
||||
Ses,
|
||||
sendmail: Basic,
|
||||
mail: Basic
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
mailConfigData: {
|
||||
mail_driver: 'mail'
|
||||
},
|
||||
mail_driver: 'mail',
|
||||
loading: false,
|
||||
mail_drivers: []
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.getMailDrivers()
|
||||
},
|
||||
methods: {
|
||||
async getMailDrivers () {
|
||||
this.loading = true
|
||||
|
||||
let response = await window.axios.get('/api/admin/onboarding/environment/mail')
|
||||
|
||||
if (response.data) {
|
||||
this.mail_drivers = response.data
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async next (mailConfigData) {
|
||||
this.loading = true
|
||||
try {
|
||||
let response = await window.axios.post('/api/admin/onboarding/environment/mail', mailConfigData)
|
||||
if (response.data.success) {
|
||||
this.$emit('next')
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,113 +0,0 @@
|
||||
<template>
|
||||
<div class="wizard">
|
||||
<div class="step-indicator">
|
||||
<img
|
||||
id="logo-crater"
|
||||
src="/assets/img/crater-logo.png"
|
||||
alt="Crater Logo"
|
||||
class="logo-main"
|
||||
>
|
||||
<div class="indicator-line">
|
||||
<div class="center">
|
||||
<div class="steps" :class="{'active': step === 1, 'completed': step > 1}">
|
||||
<font-awesome-icon v-if="step > 1" icon="check" class="icon-check"/>
|
||||
</div>
|
||||
<div class="steps" :class="{'active': step === 2, 'completed': step > 2}">
|
||||
<font-awesome-icon v-if="step > 2" icon="check" class="icon-check"/>
|
||||
</div>
|
||||
<div class="steps" :class="{'active': step === 3, 'completed': step > 3}">
|
||||
<font-awesome-icon v-if="step > 3" icon="check" class="icon-check"/>
|
||||
</div>
|
||||
<div class="steps" :class="{'active': step === 4, 'completed': step > 4}">
|
||||
<font-awesome-icon v-if="step > 4" icon="check" class="icon-check"/>
|
||||
</div>
|
||||
<div class="steps" :class="{'active': step === 5, 'completed': step > 5}">
|
||||
<font-awesome-icon v-if="step > 5" icon="check" class="icon-check"/>
|
||||
</div>
|
||||
<div class="steps" :class="{'active': step === 6, 'completed': step > 6}">
|
||||
<font-awesome-icon v-if="step > 6" icon="check" class="icon-check"/>
|
||||
</div>
|
||||
<div class="steps" :class="{'active': step === 7, 'completed': step > 7}">
|
||||
<font-awesome-icon v-if="step > 7" icon="check" class="icon-check"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-content">
|
||||
<div class="card wizard-card">
|
||||
<component
|
||||
:is="tab"
|
||||
@next="setTab"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import SystemRequirement from './SystemRequirement'
|
||||
import Permission from './Permission'
|
||||
import Database from './Database'
|
||||
import EmailConfiguration from './EmailConfiguration'
|
||||
import UserProfile from './UserProfile'
|
||||
import CompanyInfo from './CompanyInfo'
|
||||
import Settings from './Settings'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
step_1: SystemRequirement,
|
||||
step_2: Permission,
|
||||
step_3: Database,
|
||||
step_4: EmailConfiguration,
|
||||
step_5: UserProfile,
|
||||
step_6: CompanyInfo,
|
||||
step_7: Settings
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
tab: 'step_1',
|
||||
step: 1
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.getOnboardingData()
|
||||
},
|
||||
methods: {
|
||||
async getOnboardingData () {
|
||||
let response = await window.axios.get('/api/admin/onboarding')
|
||||
if (response.data) {
|
||||
if (response.data.profile_complete === 'COMPLETED') {
|
||||
this.$router.push('/admin/dashboard')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let dbStep = parseInt(response.data.profile_complete)
|
||||
|
||||
if (dbStep) {
|
||||
this.step = dbStep + 1
|
||||
this.tab = `step_${dbStep + 1}`
|
||||
}
|
||||
|
||||
this.languages = response.data.languages
|
||||
this.currencies = response.data.currencies
|
||||
this.dateFormats = response.data.date_formats
|
||||
this.timeZones = response.data.time_zones
|
||||
|
||||
// this.settingData.currency = this.currencies.find(currency => currency.id === 1)
|
||||
// this.settingData.language = this.languages.find(language => language.code === 'en')
|
||||
// this.settingData.dateFormat = this.dateFormats.find(dateFormat => dateFormat.value === 'd M Y')
|
||||
}
|
||||
},
|
||||
setTab (data) {
|
||||
this.step++
|
||||
|
||||
if (this.step <= 7) {
|
||||
this.tab = 'step_' + this.step
|
||||
} else {
|
||||
// window.location.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<div class="card-body permissions">
|
||||
<p class="form-title">{{ $t('wizard.permissions.permissions') }}</p>
|
||||
<p class="form-desc">{{ $t('wizard.permissions.permission_desc') }}</p>
|
||||
<div class="d-flex justify-content-start">
|
||||
<div class="lists col-md-6">
|
||||
<div
|
||||
v-for="(permission, index) in permissions"
|
||||
:key="index"
|
||||
class="row list-items"
|
||||
>
|
||||
|
||||
<div class="col-sm-9 left-item">
|
||||
{{ permission.folder }}
|
||||
</div>
|
||||
<div class="col-sm-3 right-item">
|
||||
<span v-if="permission.isSet" class="verified"/>
|
||||
<span v-else class="not-verified"/>
|
||||
<span>{{ permission.permission }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<base-button
|
||||
v-if="isContinue"
|
||||
class="pull-right mt-5"
|
||||
icon="arrow-right"
|
||||
right-icon
|
||||
color="theme"
|
||||
@click="next"
|
||||
>
|
||||
{{ $t('wizard.continue') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
permissions: [],
|
||||
errors: false,
|
||||
isContinue: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.getPermissions()
|
||||
},
|
||||
methods: {
|
||||
async getPermissions () {
|
||||
this.loading = true
|
||||
|
||||
let response = await window.axios.get('/api/admin/onboarding/permissions', this.profileData)
|
||||
|
||||
if (response.data) {
|
||||
this.permissions = response.data.permissions.permissions
|
||||
this.errors = response.data.permissions.errors
|
||||
let self = this
|
||||
|
||||
if (this.errors) {
|
||||
swal({
|
||||
title: this.$t('wizard.permissions.permission_confirm_title'),
|
||||
text: this.$t('wizard.permissions.permission_confirm_desc'),
|
||||
icon: 'warning',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willConfirm) => {
|
||||
if (willConfirm) {
|
||||
self.isContinue = true
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.isContinue = true
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async next () {
|
||||
this.loading = true
|
||||
await this.$emit('next')
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,219 +0,0 @@
|
||||
<template>
|
||||
<div class="card-body">
|
||||
<form action="" @submit.prevent="next()">
|
||||
<p class="form-title">{{ $t('wizard.preferences') }}</p>
|
||||
<p class="form-desc">{{ $t('wizard.preferences_desc') }}</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.currency') }}</label>
|
||||
<span class="text-danger"> *</span>
|
||||
<base-select
|
||||
v-model="settingData.currency"
|
||||
:class="{'error': $v.settingData.currency.$error }"
|
||||
:options="currencies"
|
||||
:custom-label="currencyNameWithCode"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('settings.currencies.select_currency')"
|
||||
track-by="id"
|
||||
label="name"
|
||||
@input="$v.settingData.currency.$touch()"
|
||||
/>
|
||||
<div v-if="$v.settingData.currency.$error">
|
||||
<span v-if="!$v.settingData.currency.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.language') }}</label><span class="text-danger"> *</span>
|
||||
<base-select
|
||||
v-model="settingData.language"
|
||||
:class="{'error': $v.settingData.language.$error }"
|
||||
:options="languages"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('settings.preferences.select_language')"
|
||||
label="name"
|
||||
@input="$v.settingData.language.$touch()"
|
||||
/>
|
||||
<div v-if="$v.settingData.language.$error">
|
||||
<span v-if="!$v.settingData.language.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.date_format') }}</label><span class="text-danger"> *</span>
|
||||
<base-select
|
||||
v-model="settingData.dateFormat"
|
||||
:class="{'error': $v.settingData.dateFormat.$error }"
|
||||
:options="dateFormats"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('settings.preferences.select_date_formate')"
|
||||
label="display_date"
|
||||
@input="$v.settingData.dateFormat.$touch()"
|
||||
/>
|
||||
<div v-if="$v.settingData.dateFormat.$error">
|
||||
<span v-if="!$v.settingData.dateFormat.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.time_zone') }}</label><span class="text-danger"> *</span>
|
||||
<base-select
|
||||
v-model="settingData.timeZone"
|
||||
:class="{'error': $v.settingData.timeZone.$error }"
|
||||
:options="timeZones"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('settings.preferences.select_date_formate')"
|
||||
label="key"
|
||||
@input="$v.settingData.timeZone.$touch()"
|
||||
/>
|
||||
<div v-if="$v.settingData.timeZone.$error">
|
||||
<span v-if="!$v.settingData.timeZone.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.fiscal_year') }}</label><span class="text-danger"> *</span>
|
||||
<base-select
|
||||
v-model="settingData.fiscalYear"
|
||||
:class="{'error': $v.settingData.fiscalYear.$error }"
|
||||
:options="fiscalYears"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('settings.preferences.select_financial_year')"
|
||||
label="key"
|
||||
@input="$v.settingData.fiscalYear.$touch()"
|
||||
/>
|
||||
<div v-if="$v.settingData.fiscalYear.$error">
|
||||
<span v-if="!$v.settingData.fiscalYear.required" class="text-danger">{{ $tc('customers.errors.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<base-button :loading="loading" class="pull-right" icon="save" color="theme" type="submit">
|
||||
{{ $t('wizard.save_cont') }}
|
||||
</base-button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import MultiSelect from 'vue-multiselect'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import Ls from '../../services/ls'
|
||||
import { mapActions } from 'vuex'
|
||||
const { required, minLength, email } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MultiSelect
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
settingData: {
|
||||
language: null,
|
||||
currency: null,
|
||||
timeZone: null,
|
||||
dateFormat: null,
|
||||
fiscalYear: null
|
||||
},
|
||||
loading: false,
|
||||
step: 1,
|
||||
languages: [],
|
||||
currencies: [],
|
||||
timeZones: [],
|
||||
dateFormats: [],
|
||||
fiscalYears: []
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
settingData: {
|
||||
currency: {
|
||||
required
|
||||
},
|
||||
language: {
|
||||
required
|
||||
},
|
||||
dateFormat: {
|
||||
required
|
||||
},
|
||||
timeZone: {
|
||||
required
|
||||
},
|
||||
fiscalYear: {
|
||||
required
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getOnboardingData()
|
||||
},
|
||||
methods: {
|
||||
currencyNameWithCode ({name, code}) {
|
||||
return `${code} - ${name}`
|
||||
},
|
||||
...mapActions('auth', [
|
||||
'loginOnBoardingUser'
|
||||
]),
|
||||
async getOnboardingData () {
|
||||
let response = await window.axios.get('/api/admin/onboarding')
|
||||
if (response.data) {
|
||||
if (response.data.profile_complete === 'COMPLETED') {
|
||||
this.$router.push('/admin/dashboard')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let dbStep = parseInt(response.data.profile_complete)
|
||||
|
||||
if (dbStep) {
|
||||
this.step = dbStep + 1
|
||||
}
|
||||
|
||||
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.settingData.currency = this.currencies.find(currency => currency.id === 1)
|
||||
this.settingData.language = this.languages.find(language => language.code === 'en')
|
||||
this.settingData.dateFormat = response.data.date_formats.find(dateFormat => dateFormat.carbon_format_value == 'd M Y')
|
||||
this.settingData.timeZone = this.timeZones.find(timeZone => timeZone.value === 'UTC')
|
||||
this.settingData.fiscalYear = this.fiscalYears.find(fiscalYear => fiscalYear.value === '1-12')
|
||||
}
|
||||
},
|
||||
async next () {
|
||||
this.$v.settingData.$touch()
|
||||
|
||||
if (this.$v.settingData.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
let data = {
|
||||
currency: this.settingData.currency.id,
|
||||
time_zone: this.settingData.timeZone.value,
|
||||
language: this.settingData.language.code,
|
||||
fiscal_year: this.settingData.fiscalYear.value,
|
||||
carbon_date_format: this.settingData.dateFormat.carbon_format_value,
|
||||
moment_date_format: this.settingData.dateFormat.moment_format_value
|
||||
}
|
||||
|
||||
let response = await window.axios.post('/api/admin/onboarding/settings', data)
|
||||
|
||||
if (response.data) {
|
||||
// this.$emit('next')
|
||||
this.loading = false
|
||||
Ls.set('auth.token', response.data.token)
|
||||
this.loginOnBoardingUser(response.data.token)
|
||||
window.toastr['success']('Login Successful')
|
||||
this.$router.push('/admin/dashboard')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,112 +0,0 @@
|
||||
<template>
|
||||
<div class="card-body">
|
||||
<p class="form-title">{{ $t('wizard.req.system_req') }}</p>
|
||||
<p class="form-desc">{{ $t('wizard.req.system_req_desc') }}</p>
|
||||
<div v-if="phpSupportInfo" class="d-flex justify-content-start">
|
||||
<div class="col-md-6">
|
||||
<div class="row list-items">
|
||||
<div class="col-md-9 left-item">
|
||||
{{ $t('wizard.req.php_req_version', { version: phpSupportInfo.minimum }) }}
|
||||
</div>
|
||||
<div class="col-md-3 right-item justify-content-end">
|
||||
{{ phpSupportInfo.current }}
|
||||
<span v-if="phpSupportInfo.supported" class="verified"/>
|
||||
<span v-else class="not-verified"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="requirements" class="d-flex justify-content-start">
|
||||
<div class="col-md-6">
|
||||
<div
|
||||
v-for="(requirement, index) in requirements"
|
||||
:key="index"
|
||||
class="row list-items"
|
||||
>
|
||||
|
||||
<div class="col-md-9 left-item">
|
||||
{{ index }}
|
||||
</div>
|
||||
<div class="col-md-3 right-item justify-content-end">
|
||||
<span v-if="requirement" class="verified"/>
|
||||
<span v-else class="not-verified"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<base-button
|
||||
v-if="hasNext"
|
||||
:loading="loading"
|
||||
class="pull-right mt-4"
|
||||
icon="arrow-right"
|
||||
color="theme"
|
||||
right-icon
|
||||
@click="next"
|
||||
>
|
||||
{{ $t('wizard.continue') }}
|
||||
</base-button>
|
||||
<base-button
|
||||
v-if="!requirements"
|
||||
:loading="loading"
|
||||
class="pull-right mt-4"
|
||||
color="theme"
|
||||
@click="getRequirements"
|
||||
>
|
||||
{{ $t('wizard.req.check_req') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import MultiSelect from 'vue-multiselect'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MultiSelect
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
requirements: null,
|
||||
phpSupportInfo: null,
|
||||
loading: false,
|
||||
isShow: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasNext () {
|
||||
if (this.requirements) {
|
||||
let isRequired = true
|
||||
for (const key in this.requirements) {
|
||||
if (!this.requirements[key]) {
|
||||
isRequired = false
|
||||
}
|
||||
}
|
||||
return this.requirements && this.phpSupportInfo.supported && isRequired
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
listToggle () {
|
||||
this.isShow = !this.isShow
|
||||
},
|
||||
async getRequirements () {
|
||||
this.loading = true
|
||||
|
||||
let response = await window.axios.get('/api/admin/onboarding/requirements', this.profileData)
|
||||
|
||||
if (response.data) {
|
||||
this.requirements = response.data.requirements.requirements.php
|
||||
this.phpSupportInfo = response.data.phpSupportInfo
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async next () {
|
||||
this.loading = true
|
||||
await this.$emit('next')
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,204 +0,0 @@
|
||||
<template>
|
||||
<div class="card-body">
|
||||
<form action="" @submit.prevent="next()">
|
||||
<p class="form-title">{{ $t('wizard.account_info') }}</p>
|
||||
<p class="form-desc">{{ $t('wizard.account_info_desc') }}</p>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-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-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">
|
||||
<label class="form-label">{{ $t('wizard.name') }}</label><span class="text-danger"> *</span>
|
||||
<base-input
|
||||
:invalid="$v.profileData.name.$error"
|
||||
v-model.trim="profileData.name"
|
||||
type="text"
|
||||
name="name"
|
||||
@input="$v.profileData.name.$touch()"
|
||||
/>
|
||||
<div v-if="$v.profileData.name.$error">
|
||||
<span v-if="!$v.profileData.name.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
<span v-if="!$v.profileData.name.minLength" class="text-danger"> {{ $tc('validation.name_min_length', $v.profileData.name.$params.minLength.min, { count: $v.profileData.name.$params.minLength.min }) }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.email') }}</label><span class="text-danger"> *</span>
|
||||
<base-input
|
||||
:invalid="$v.profileData.email.$error"
|
||||
v-model.trim="profileData.email"
|
||||
type="text"
|
||||
name="email"
|
||||
@input="$v.profileData.email.$touch()"
|
||||
/>
|
||||
<div v-if="$v.profileData.email.$error">
|
||||
<span v-if="!$v.profileData.email.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
<span v-if="!$v.profileData.email.email" class="text-danger">{{ $tc('validation.email_incorrect') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.password') }}</label><span class="text-danger"> *</span>
|
||||
<base-input
|
||||
:invalid="$v.profileData.password.$error"
|
||||
v-model.trim="profileData.password"
|
||||
type="password"
|
||||
name="password"
|
||||
@input="$v.profileData.password.$touch()"
|
||||
/>
|
||||
<div v-if="$v.profileData.password.$error">
|
||||
<span v-if="!$v.profileData.password.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
<span v-if="!$v.profileData.password.minLength" class="text-danger"> {{ $tc('validation.password_min_length', $v.profileData.password.$params.minLength.min, {count: $v.profileData.password.$params.minLength.min}) }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.confirm_password') }}</label><span class="text-danger"> *</span>
|
||||
<base-input
|
||||
:invalid="$v.profileData.confirm_password.$error"
|
||||
v-model.trim="profileData.confirm_password"
|
||||
type="password"
|
||||
name="confirm_password"
|
||||
@input="$v.profileData.confirm_password.$touch()"
|
||||
/>
|
||||
<div v-if="$v.profileData.confirm_password.$error">
|
||||
<span v-if="!$v.profileData.confirm_password.sameAsPassword" class="text-danger">{{ $tc('validation.password_incorrect') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<base-button
|
||||
:loading="loading"
|
||||
class="pull-right mt-4"
|
||||
icon="save"
|
||||
color="theme"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('wizard.save_cont') }}
|
||||
</base-button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import AvatarCropper from 'vue-avatar-cropper'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import { mapActions } from 'vuex'
|
||||
const { required, requiredIf, sameAs, minLength, email } = 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
|
||||
},
|
||||
profileData: {
|
||||
name: null,
|
||||
email: null,
|
||||
password: null,
|
||||
confirm_password: null
|
||||
},
|
||||
loading: false,
|
||||
previewAvatar: '/images/default-avatar.jpg',
|
||||
fileObject: null
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
profileData: {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(3)
|
||||
},
|
||||
email: {
|
||||
email,
|
||||
required
|
||||
},
|
||||
password: {
|
||||
required,
|
||||
minLength: minLength(8)
|
||||
},
|
||||
confirm_password: {
|
||||
required: requiredIf('isRequired'),
|
||||
sameAsPassword: sameAs('password')
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isRequired () {
|
||||
if (this.profileData.password === null || this.profileData.password === undefined || this.profileData.password === '') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('userProfile', [
|
||||
'uploadOnboardAvatar'
|
||||
]),
|
||||
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 next () {
|
||||
this.$v.profileData.$touch()
|
||||
if (this.$v.profileData.$invalid) {
|
||||
return true
|
||||
}
|
||||
this.loading = true
|
||||
let response = await window.axios.post('/api/admin/onboarding/profile', this.profileData)
|
||||
console.log('user_id', response.data.user.id)
|
||||
|
||||
if (response.data) {
|
||||
if (this.fileObject && this.previewAvatar) {
|
||||
let avatarData = new FormData()
|
||||
avatarData.append('admin_avatar', JSON.stringify({
|
||||
name: this.fileObject.name,
|
||||
data: this.previewAvatar,
|
||||
id: response.data.user.id
|
||||
}))
|
||||
|
||||
this.uploadOnboardAvatar(avatarData)
|
||||
}
|
||||
this.$emit('next')
|
||||
this.loading = false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user