mirror of
https://github.com/crater-invoice/crater.git
synced 2026-02-09 20:32:40 -05:00
init crater
This commit is contained in:
99
resources/assets/js/views/auth/ForgotPassword.vue
Normal file
99
resources/assets/js/views/auth/ForgotPassword.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<form
|
||||
id="loginForm"
|
||||
@submit.prevent="validateBeforeSubmit"
|
||||
>
|
||||
|
||||
<div :class="{'form-group' : true }">
|
||||
<base-input
|
||||
:invalid="$v.formData.email.$error"
|
||||
v-model.lazy="formData.email"
|
||||
:disabled="isSent"
|
||||
:placeholder="$t('login.enter_email')"
|
||||
focus
|
||||
name="email"
|
||||
@blur="$v.formData.email.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.email.$error">
|
||||
<span v-if="!$v.formData.email.required" class="help-block text-danger">
|
||||
{{ $t('validation.required') }}
|
||||
</span>
|
||||
<span v-if="!$v.formData.email.email" class="help-block text-danger">
|
||||
{{ $t('validation.email_incorrect') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<base-button v-if="!isSent" :loading="isLoading" :disabled="isLoading" type="submit" color="theme">
|
||||
{{ $t('validation.send_reset_link') }}
|
||||
</base-button>
|
||||
<base-button v-else :loading="isLoading" :disabled="isLoading" color="theme" type="submit">
|
||||
{{ $t('validation.not_yet') }}
|
||||
</base-button>
|
||||
|
||||
<div class="other-actions mb-4">
|
||||
<router-link to="/login">
|
||||
{{ $t('general.back_to_login') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import { async } from 'q'
|
||||
const { required, email } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
formData: {
|
||||
email: ''
|
||||
},
|
||||
isSent: false,
|
||||
isLoading: false,
|
||||
isRegisteredUser: false
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
formData: {
|
||||
email: {
|
||||
email,
|
||||
required
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
async validateBeforeSubmit (e) {
|
||||
this.$v.formData.$touch()
|
||||
|
||||
if (await this.checkMail() === 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)
|
||||
|
||||
if (res.data) {
|
||||
toastr['success']('Mail sent successfuly!', 'Success')
|
||||
}
|
||||
|
||||
this.isSent = true
|
||||
this.isLoading = false
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 403) {
|
||||
toastr['error'](err.response.data, 'Error')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async checkMail () {
|
||||
let response = await window.axios.post('/api/is-registered', this.formData)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
119
resources/assets/js/views/auth/Login.vue
Normal file
119
resources/assets/js/views/auth/Login.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<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
|
||||
:invalid="$v.loginData.email.$error"
|
||||
v-model="loginData.email"
|
||||
:placeholder="$t(login.login_placeholder)"
|
||||
focus
|
||||
type="email"
|
||||
name="email"
|
||||
/>
|
||||
<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
|
||||
v-model="loginData.password"
|
||||
:invalid="$v.loginData.password.$error"
|
||||
type="password"
|
||||
name="password"
|
||||
/>
|
||||
<div v-if="$v.loginData.email.$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">
|
||||
{{ $t('login.forgot_password') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<base-button 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> -->
|
||||
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
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
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
loginData: {
|
||||
email: '',
|
||||
password: '',
|
||||
remember: ''
|
||||
},
|
||||
submitted: false
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
loginData: {
|
||||
email: {
|
||||
required,
|
||||
email
|
||||
},
|
||||
password: {
|
||||
required,
|
||||
minLength: minLength(8)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('auth', [
|
||||
'login'
|
||||
]),
|
||||
async validateBeforeSubmit () {
|
||||
this.$v.loginData.$touch()
|
||||
if (this.$v.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
this.isLoading = true
|
||||
|
||||
this.login(this.loginData).then((res) => {
|
||||
this.$router.push('/admin/dashboard')
|
||||
this.isLoading = false
|
||||
}).catch(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
57
resources/assets/js/views/auth/Register.vue
Normal file
57
resources/assets/js/views/auth/Register.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<form
|
||||
id="registerForm"
|
||||
action=""
|
||||
method="post"
|
||||
>
|
||||
<!-- {{ csrf_field() }} -->
|
||||
<div class="form-group">
|
||||
<input
|
||||
:placeholder="$t('login.enter_email')"
|
||||
type="email"
|
||||
class="form-control form-control-danger"
|
||||
name="email"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
class="form-control form-control-danger"
|
||||
placeholder="Enter Password"
|
||||
name="password"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="password"
|
||||
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>
|
||||
</form>
|
||||
</template>
|
||||
<script type="text/babel">
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateBeforeSubmit (e) {
|
||||
this.$validator.validateAll().then((result) => {
|
||||
if (result) {
|
||||
// eslint-disable-next-line
|
||||
alert('Form Submitted!')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
124
resources/assets/js/views/auth/ResetPassword.vue
Normal file
124
resources/assets/js/views/auth/ResetPassword.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<form
|
||||
id="loginForm"
|
||||
@submit.prevent="validateBeforeSubmit"
|
||||
>
|
||||
<div class="form-group">
|
||||
<base-input
|
||||
v-model.trim="formData.email"
|
||||
:invalid="$v.formData.email.$error"
|
||||
:placeholder="$t('login.enter_email')"
|
||||
type="email"
|
||||
name="email"
|
||||
@input="$v.formData.email.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.email.$error">
|
||||
<span v-if="!$v.formData.email.required" class="help-block text-danger">
|
||||
{{ $t('validation.required') }}
|
||||
</span>
|
||||
<span v-if="!$v.formData.email.email" class="help-block text-danger">
|
||||
{{ $t('validation.email_incorrect') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<base-input
|
||||
id="password"
|
||||
v-model.trim="formData.password"
|
||||
:invalid="$v.formData.password.$error"
|
||||
:placeholder="$t('login.enter_password')"
|
||||
type="password"
|
||||
name="password"
|
||||
@input="$v.formData.password.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.password.$error">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<base-input
|
||||
v-model.trim="formData.password_confirmation"
|
||||
:invalid="$v.formData.password_confirmation.$error"
|
||||
:placeholder="$t('login.retype_password')"
|
||||
type="password"
|
||||
name="password_confirmation"
|
||||
@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">
|
||||
{{ $t('validation.password_incorrect') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<base-button :loading="isLoading" type="submit" color="theme">
|
||||
{{ $t('login.reset_password') }}
|
||||
</base-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
import { validationMixin } from 'vuelidate'
|
||||
const { required, email, sameAs, minLength } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
formData: {
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: ''
|
||||
},
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
formData: {
|
||||
email: {
|
||||
required,
|
||||
email
|
||||
},
|
||||
password: {
|
||||
required,
|
||||
minLength: minLength(8)
|
||||
},
|
||||
password_confirmation: {
|
||||
sameAsPassword: sameAs('password')
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async validateBeforeSubmit (e) {
|
||||
this.$v.formData.$touch()
|
||||
|
||||
if (!this.$v.formData.$invalid) {
|
||||
try {
|
||||
let data = {
|
||||
email: this.formData.email,
|
||||
password: this.formData.password,
|
||||
password_confirmation: this.formData.password_confirmation,
|
||||
token: this.$route.params.token
|
||||
}
|
||||
this.isLoading = true
|
||||
let res = await axios.post('/api/auth/reset/password', data)
|
||||
this.isLoading = false
|
||||
if (res.data) {
|
||||
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'))
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
96
resources/assets/js/views/categories/Create.vue
Normal file
96
resources/assets/js/views/categories/Create.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="main-content categoriescreate">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ $t('categories.new_category') }}</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('categorie.category',2) }}</router-link></li>
|
||||
<li class="breadcrumb-item"><a href="#">{{ $t('categories.new_category') }}</a></li>
|
||||
</ol>
|
||||
<div class="page-actions">
|
||||
<router-link slot="item-title" to="/admin/expenses">
|
||||
<base-button class="btn btn-primary" color="theme">
|
||||
<font-awesome-icon icon="backward" class="mr-2"/> {{ $t('general.go_back') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<form action="" @submit.prevent="submitCategoryData">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ $t('expenses.categories.title') }}</label><span class="text-danger"> *</span>
|
||||
<base-input
|
||||
:invalid="$v.formData.name.$error"
|
||||
v-model.trim="formData.name"
|
||||
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 for="description">{{ $t('expenses.categories.description') }}</label>
|
||||
<base-text-area v-model="formData.description" rows="5" name="description" />
|
||||
</div>
|
||||
<base-button icon="save" type="submit" color="theme">
|
||||
{{ $t('general.save') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import { mapActions } from 'vuex'
|
||||
const { required, minLength } = require('vuelidate/lib/validators')
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
formData: {
|
||||
name: null,
|
||||
description: null
|
||||
}
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
formData: {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(3)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
},
|
||||
methods: {
|
||||
...mapActions('category', [
|
||||
'loadData',
|
||||
'addCategory'
|
||||
]),
|
||||
async submitCategoryData () {
|
||||
this.$v.formData.$touch()
|
||||
if (this.$v.$invalid) {
|
||||
return true
|
||||
}
|
||||
let response = await this.addCategory({ ...this.formData })
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](response.data.success)
|
||||
this.$router.push('/admin/expenses')
|
||||
return true
|
||||
}
|
||||
window.toastr['error'](response.data.error)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
114
resources/assets/js/views/categories/Edit.vue
Normal file
114
resources/assets/js/views/categories/Edit.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="main-content categoriescreate">
|
||||
<div class="page-header">
|
||||
<h3 v-if="!isEdit" class="page-title">{{ $t('categories.add_category') }}</h3>
|
||||
<h3 v-else class="page-title">{{ $t('navigation.edit') }} {{ $tc('navigation.category',1) }}</h3>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><router-link slot="item-title" to="/admin/dashboard">{{ $t('navigation.home') }}</router-link></li>
|
||||
<li class="breadcrumb-item"><router-link slot="item-title" to="/admin/expenses">{{ $tc('navigation.category',2) }}</router-link></li>
|
||||
<li v-if="!isEdit" class="breadcrumb-item"><a href="#">{{ $t('expenses.categories.add_category') }} {{ $tc('navigation.category', 1) }}</a></li>
|
||||
<li v-else class="breadcrumb-item"><a href="#">{{ $t('navigation.edit') }} {{ $tc('navigation.category', 1) }}</a></li>
|
||||
</ol>
|
||||
<div class="page-actions">
|
||||
<router-link slot="item-title" to="/admin/expenses">
|
||||
<base-button icon="backward" color="theme">
|
||||
{{ $t('navigation.go_back') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<form action="" @submit.prevent="submitCategoryData">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ $t('expenses.categories.title') }}</label><span class="text-danger"> *</span>
|
||||
<input
|
||||
:class="{ error: $v.formData.name.$error }"
|
||||
v-model.trim="formData.name"
|
||||
type="text"
|
||||
name="name"
|
||||
class="form-control"
|
||||
@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 for="description">{{ $t('expenses.categories.description') }}</label>
|
||||
<textarea v-model="formData.description" class="form-control" rows="5" name="description" />
|
||||
</div>
|
||||
<base-button icon="save" color="theme" type="submit">
|
||||
{{ $t('navigation.save') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import { mapActions } from 'vuex'
|
||||
const { required, minLength } = require('vuelidate/lib/validators')
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
formData: {
|
||||
name: null,
|
||||
description: null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isEdit () {
|
||||
if (this.$route.name === 'categoryedit') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
formData: {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(3)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.fetchInitialData()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('category', [
|
||||
'loadData',
|
||||
'addCategory',
|
||||
'editCategory'
|
||||
]),
|
||||
async fetchInitialData () {
|
||||
let response = await this.loadData(this.$route.params.id)
|
||||
this.formData.name = response.data.category.name
|
||||
this.formData.description = response.data.category.description
|
||||
},
|
||||
async submitCategoryData () {
|
||||
this.$v.formData.$touch()
|
||||
if (this.$v.$invalid) {
|
||||
return true
|
||||
}
|
||||
let response = await this.editCategory({ id: this.$route.params.id, editData: { ...this.formData } })
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](response.data.success)
|
||||
this.$router.push('/admin/expenses')
|
||||
return true
|
||||
}
|
||||
window.toastr['error'](response.data.error)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
120
resources/assets/js/views/components/ImageBox.vue
Normal file
120
resources/assets/js/views/components/ImageBox.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<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>
|
||||
71
resources/assets/js/views/components/ImageRadio.vue
Normal file
71
resources/assets/js/views/components/ImageRadio.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<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>
|
||||
129
resources/assets/js/views/components/SettingListBox.vue
Normal file
129
resources/assets/js/views/components/SettingListBox.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<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>
|
||||
678
resources/assets/js/views/customers/Create.vue
Normal file
678
resources/assets/js/views/customers/Create.vue
Normal file
@@ -0,0 +1,678 @@
|
||||
<template>
|
||||
<div class="customer-create main-content">
|
||||
<form action="" @submit.prevent="submitCustomerData">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ isEdit ? $t('customers.edit_customer') : $t('customers.new_customer') }}</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/customers">{{ $tc('customers.customer', 2) }}</router-link></li>
|
||||
<li class="breadcrumb-item">{{ isEdit ? $t('customers.edit_customer') : $t('customers.new_customer') }}</li>
|
||||
</ol>
|
||||
<div class="page-actions header-button-container">
|
||||
<base-button
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
:tabindex="23"
|
||||
icon="save"
|
||||
color="theme"
|
||||
type="submit"
|
||||
>
|
||||
{{ isEdit ? $t('customers.update_customer') : $t('customers.save_customer') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="customer-card card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="section-title col-sm-2">{{ $t('customers.basic_info') }}</div>
|
||||
<div class="col-sm-5">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.display_name') }}</label><span class="text-danger"> *</span>
|
||||
<base-input
|
||||
:invalid="$v.formData.name.$error"
|
||||
v-model="formData.name"
|
||||
focus
|
||||
type="text"
|
||||
name="name"
|
||||
tab-index="1"
|
||||
@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>
|
||||
<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 class="form-label">{{ $t('customers.email') }}</label>
|
||||
<base-input
|
||||
:invalid="$v.formData.email.$error"
|
||||
v-model.trim="formData.email"
|
||||
type="text"
|
||||
name="email"
|
||||
tab-index="3"
|
||||
@input="$v.formData.email.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.email.$error">
|
||||
<span v-if="!$v.formData.email.email" class="text-danger"> {{ $tc('validation.email_incorrect') }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.primary_currency') }}</label>
|
||||
<base-select
|
||||
v-model="currency"
|
||||
:options="currencies"
|
||||
:allow-empty="false"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:tabindex="5"
|
||||
:placeholder="$t('customers.select_currency')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.primary_contact_name') }}</label>
|
||||
<base-input
|
||||
v-model.trim="formData.contact_name"
|
||||
:label="$t('customers.contact_name')"
|
||||
type="text"
|
||||
tab-index="2"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.phone') }}</label>
|
||||
<base-input
|
||||
:invalid="$v.formData.phone.$error"
|
||||
v-model.trim="formData.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
tab-index="4"
|
||||
@input="$v.formData.phone.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.phone.$error">
|
||||
<span v-if="!$v.formData.phone.numeric" class="text-danger">{{ $tc('validation.numbers_only') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.website') }}</label>
|
||||
<base-input
|
||||
v-model="formData.website"
|
||||
:invalid="$v.formData.website.$error"
|
||||
type="url"
|
||||
@input="$v.formData.website.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.website.$error">
|
||||
<span v-if="!$v.formData.website.url" class="text-danger">{{ $tc('validation.invalid_url') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr> <!-- first row complete -->
|
||||
<div class="row">
|
||||
<div class="section-title col-sm-2">{{ $t('customers.billing_address') }}</div>
|
||||
<div class="col-sm-5">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.name') }}</label>
|
||||
<base-input
|
||||
v-model.trim="billing.name"
|
||||
type="text"
|
||||
name="address_name"
|
||||
tab-index="7"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.state') }}</label>
|
||||
<base-select
|
||||
v-model="billing_state"
|
||||
:options="billingStates"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:tabindex="9"
|
||||
:disabled="isDisabledBillingState"
|
||||
:placeholder="$t('general.select_state')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.address') }}</label>
|
||||
<base-text-area
|
||||
v-model.trim="billing.address_street_1"
|
||||
:tabindex="11"
|
||||
:placeholder="$t('general.street_1')"
|
||||
type="text"
|
||||
name="billing_street1"
|
||||
rows="2"
|
||||
@input="$v.billing.address_street_1.$touch()"
|
||||
/>
|
||||
<div v-if="$v.billing.address_street_1.$error">
|
||||
<span v-if="!$v.billing.address_street_1.maxLength" class="text-danger">{{ $t('validation.address_maxlength') }}</span>
|
||||
</div>
|
||||
<base-text-area
|
||||
:tabindex="12"
|
||||
v-model.trim="billing.address_street_2"
|
||||
:placeholder="$t('general.street_2')"
|
||||
type="text"
|
||||
name="billing_street2"
|
||||
rows="2"
|
||||
@input="$v.billing.address_street_2.$touch()"
|
||||
/>
|
||||
<div v-if="$v.billing.address_street_2.$error">
|
||||
<span v-if="!$v.billing.address_street_2.maxLength" class="text-danger">{{ $t('validation.address_maxlength') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.country') }}</label>
|
||||
<base-select
|
||||
v-model="billing_country"
|
||||
:options="billingCountries"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:tabindex="8"
|
||||
:placeholder="$t('general.select_country')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.city') }}</label>
|
||||
<base-select
|
||||
v-model="billing_city"
|
||||
:options="billingCities"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:disabled="isDisabledBillingCity"
|
||||
:tabindex="10"
|
||||
:placeholder="$t('general.select_city')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.phone') }}</label>
|
||||
<base-input
|
||||
:invalid="$v.billing.phone.$error"
|
||||
v-model.trim="billing.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
tab-index="13"
|
||||
@input="$v.billing.phone.$touch()"
|
||||
/>
|
||||
<div v-if="$v.billing.phone.$error">
|
||||
<span v-if="!$v.billing.phone.numberic" class="text-danger">{{ $tc('validation.numbers_only') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.zip_code') }}</label>
|
||||
<base-input
|
||||
v-model.trim="billing.zip"
|
||||
type="text"
|
||||
name="zip"
|
||||
tab-index="14"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr> <!-- second row complete -->
|
||||
<div class="row same-address-checkbox-container">
|
||||
<div class="p-1">
|
||||
<base-button ref="sameAddress" icon="copy" color="theme" class="btn-sm" @click="copyAddress(true)">
|
||||
{{ $t('customers.copy_billing_address') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="section-title col-sm-2">
|
||||
{{ $t('customers.shipping_address') }}
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.name') }}</label>
|
||||
<base-input
|
||||
v-model.trim="shipping.name"
|
||||
type="text"
|
||||
name="address_name"
|
||||
tab-index="15"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.state') }}</label>
|
||||
<base-select
|
||||
v-model="shipping_state"
|
||||
:options="shippingStates"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:tabindex="17"
|
||||
:disabled="isDisabledShippingState"
|
||||
:placeholder="$t('general.select_state')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.address') }}</label>
|
||||
<base-text-area
|
||||
v-model.trim="shipping.address_street_1"
|
||||
:tabindex="19"
|
||||
:placeholder="$t('general.street_1')"
|
||||
type="text"
|
||||
name="street_1"
|
||||
rows="2"
|
||||
@input="$v.shipping.address_street_1.$touch()"
|
||||
/>
|
||||
<div v-if="$v.shipping.address_street_1.$error">
|
||||
<span v-if="!$v.shipping.address_street_1.maxLength" class="text-danger">{{ $t('validation.address_maxlength') }}</span>
|
||||
</div>
|
||||
<base-text-area
|
||||
v-model.trim="shipping.address_street_2"
|
||||
:tabindex="20"
|
||||
:placeholder="$t('general.street_2')"
|
||||
type="text"
|
||||
name="street_2"
|
||||
rows="2"
|
||||
@input="$v.shipping.address_street_2.$touch()"
|
||||
/>
|
||||
<div v-if="$v.shipping.address_street_2.$error">
|
||||
<span v-if="!$v.shipping.address_street_2.maxLength" class="text-danger">{{ $t('validation.address_maxlength') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.country') }}</label>
|
||||
<base-select
|
||||
v-model="shipping_country"
|
||||
:options="shippingCountries"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:tabindex="16"
|
||||
:allow-empty="false"
|
||||
:placeholder="$t('general.select_country')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.city') }}</label>
|
||||
<base-select
|
||||
v-model="shipping_city"
|
||||
:options="shippingCities"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:tabindex="18"
|
||||
:disabled="isDisabledShippingCity"
|
||||
:placeholder="$t('general.select_city')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.phone') }}</label>
|
||||
<base-input
|
||||
:invalid="$v.shipping.phone.$error"
|
||||
v-model.trim="shipping.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
tab-index="21"
|
||||
@input="$v.shipping.phone.$touch()"
|
||||
/>
|
||||
<div v-if="$v.shipping.phone.$error">
|
||||
<span v-if="!$v.shipping.phone.numberic" class="text-danger">{{ $tc('validation.numbers_only') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('customers.zip_code') }}</label>
|
||||
<base-input
|
||||
v-model.trim="shipping.zip"
|
||||
type="text"
|
||||
name="zip"
|
||||
tab-index="22"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group collapse-button-container">
|
||||
<base-button
|
||||
:tabindex="23"
|
||||
icon="save"
|
||||
color="theme"
|
||||
class="collapse-button"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('customers.save_customer') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import MultiSelect from 'vue-multiselect'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import AddressStub from '../../stub/address'
|
||||
const { required, minLength, email, numeric, url, maxLength } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
components: { MultiSelect },
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
isCopyFromBilling: false,
|
||||
isLoading: false,
|
||||
formData: {
|
||||
name: null,
|
||||
contact_name: null,
|
||||
email: null,
|
||||
phone: null,
|
||||
currency_id: null,
|
||||
website: null,
|
||||
addresses: []
|
||||
},
|
||||
currency: null,
|
||||
billing: {
|
||||
name: null,
|
||||
country_id: null,
|
||||
state_id: null,
|
||||
city_id: null,
|
||||
phone: null,
|
||||
zip: null,
|
||||
address_street_1: null,
|
||||
address_street_2: null,
|
||||
type: 'billing'
|
||||
},
|
||||
shipping: {
|
||||
name: null,
|
||||
country_id: null,
|
||||
state_id: null,
|
||||
city_id: null,
|
||||
phone: null,
|
||||
zip: null,
|
||||
address_street_1: null,
|
||||
address_street_2: null,
|
||||
type: 'shipping'
|
||||
},
|
||||
currencyList: [],
|
||||
isDisabledBillingState: true,
|
||||
isDisabledBillingCity: true,
|
||||
isDisabledShippingState: true,
|
||||
isDisabledShippingCity: true,
|
||||
|
||||
billing_country: null,
|
||||
billing_city: null,
|
||||
billing_state: null,
|
||||
|
||||
shipping_country: null,
|
||||
shipping_city: null,
|
||||
shipping_state: null,
|
||||
|
||||
billingCountries: [],
|
||||
billingStates: [],
|
||||
billingCities: [],
|
||||
|
||||
shippingCountries: [],
|
||||
shippingStates: [],
|
||||
shippingCities: []
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
formData: {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(3)
|
||||
},
|
||||
email: {
|
||||
email
|
||||
},
|
||||
phone: {
|
||||
numeric
|
||||
},
|
||||
website: {
|
||||
url
|
||||
}
|
||||
},
|
||||
billing: {
|
||||
phone: {
|
||||
numeric
|
||||
},
|
||||
address_street_1: {
|
||||
maxLength: maxLength(255)
|
||||
},
|
||||
address_street_2: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
},
|
||||
shipping: {
|
||||
phone: {
|
||||
numeric
|
||||
},
|
||||
address_street_1: {
|
||||
maxLength: maxLength(255)
|
||||
},
|
||||
address_street_2: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrency',
|
||||
'currencies'
|
||||
]),
|
||||
isEdit () {
|
||||
if (this.$route.name === 'customers.edit') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
billing_country (newCountry) {
|
||||
if (newCountry) {
|
||||
this.billing.country_id = newCountry.id
|
||||
this.isDisabledBillingState = false
|
||||
this.billing_state = null
|
||||
this.billing_city = null
|
||||
this.fetchBillingState()
|
||||
}
|
||||
},
|
||||
billing_state (newState) {
|
||||
if (newState) {
|
||||
this.billing.state_id = newState.id
|
||||
this.isDisabledBillingCity = false
|
||||
this.billing_city = null
|
||||
this.fetchBillingCities()
|
||||
return true
|
||||
}
|
||||
this.billing_city = null
|
||||
this.isDisabledBillingCity = true
|
||||
return true
|
||||
},
|
||||
billing_city (newCity) {
|
||||
if (newCity) {
|
||||
this.billing.city_id = newCity.id
|
||||
}
|
||||
},
|
||||
shipping_country (newCountry) {
|
||||
if (newCountry) {
|
||||
this.shipping.country_id = newCountry.id
|
||||
this.isDisabledShippingState = false
|
||||
this.fetchShippingState()
|
||||
if (this.isCopyFromBilling) {
|
||||
return true
|
||||
}
|
||||
this.shipping_state = null
|
||||
this.shipping_city = null
|
||||
return true
|
||||
}
|
||||
},
|
||||
shipping_state (newState) {
|
||||
if (newState) {
|
||||
this.shipping.state_id = newState.id
|
||||
this.isDisabledShippingCity = false
|
||||
this.fetchShippingCities()
|
||||
if (this.isCopyFromBilling) {
|
||||
this.isCopyFromBilling = false
|
||||
return true
|
||||
}
|
||||
this.shipping_city = null
|
||||
return true
|
||||
}
|
||||
this.shipping_city = null
|
||||
this.isDisabledShippingCity = true
|
||||
return true
|
||||
},
|
||||
shipping_city (newCity) {
|
||||
if (newCity) {
|
||||
this.shipping.city_id = newCity.id
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.fetchCountry()
|
||||
if (this.isEdit) {
|
||||
this.loadCustomer()
|
||||
} else {
|
||||
this.currency = this.defaultCurrency
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('customer', [
|
||||
'addCustomer',
|
||||
'fetchCustomer',
|
||||
'updateCustomer'
|
||||
]),
|
||||
async loadCustomer () {
|
||||
let { data: { customer, currencies, currency } } = await this.fetchCustomer(this.$route.params.id)
|
||||
this.formData = customer
|
||||
|
||||
if (customer.billing_address) {
|
||||
this.billing = customer.billing_address
|
||||
|
||||
if (customer.billing_address.country_id) {
|
||||
this.billing_country = this.billingCountries.find((c) => c.id === customer.billing_address.country_id)
|
||||
}
|
||||
}
|
||||
|
||||
if (customer.shipping_address) {
|
||||
this.shipping = customer.shipping_address
|
||||
|
||||
if (customer.shipping_address.country_id) {
|
||||
this.shipping_country = this.shippingCountries.find((c) => c.id === customer.shipping_address.country_id)
|
||||
}
|
||||
}
|
||||
|
||||
this.currencyList = currencies
|
||||
this.formData.currency_id = customer.currency_id
|
||||
this.currency = currency
|
||||
},
|
||||
async fetchCountry () {
|
||||
let res = await window.axios.get('/api/countries')
|
||||
if (res) {
|
||||
this.billingCountries = res.data.countries
|
||||
this.shippingCountries = res.data.countries
|
||||
}
|
||||
},
|
||||
copyAddress (val) {
|
||||
if (val === true) {
|
||||
this.isCopyFromBilling = true
|
||||
this.shipping = {...this.billing, type: 'shipping'}
|
||||
this.shipping_country = this.billing_country
|
||||
this.shipping_state = this.billing_state
|
||||
this.shipping_city = this.billing_city
|
||||
} else {
|
||||
this.shipping = {...AddressStub, type: 'shipping'}
|
||||
this.shipping_country = null
|
||||
this.shipping_state = null
|
||||
this.shipping_city = null
|
||||
}
|
||||
},
|
||||
async submitCustomerData () {
|
||||
this.$v.formData.$touch()
|
||||
|
||||
if (this.$v.$invalid) {
|
||||
return true
|
||||
}
|
||||
this.formData.addresses = [{...this.billing}, {...this.shipping}]
|
||||
|
||||
if (this.isEdit) {
|
||||
if (this.currency) {
|
||||
this.formData.currency_id = this.currency.id
|
||||
}
|
||||
this.isLoading = true
|
||||
let response = await this.updateCustomer(this.formData)
|
||||
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$t('customers.updated_message'))
|
||||
this.$router.push('/admin/customers')
|
||||
this.isLoading = false
|
||||
return true
|
||||
}
|
||||
|
||||
window.toastr['error'](response.data.error)
|
||||
} else {
|
||||
this.isLoading = true
|
||||
if (this.currency) {
|
||||
this.isLoading = true
|
||||
this.formData.currency_id = this.currency.id
|
||||
}
|
||||
|
||||
let response = await this.addCustomer(this.formData)
|
||||
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](this.$t('customers.created_message'))
|
||||
this.$router.push('/admin/customers')
|
||||
this.isLoading = false
|
||||
return true
|
||||
}
|
||||
|
||||
window.toastr['error'](response.data.error)
|
||||
}
|
||||
},
|
||||
async fetchBillingState () {
|
||||
let res = await window.axios.get(`/api/states/${this.billing_country.id}`)
|
||||
if (res) {
|
||||
this.billingStates = res.data.states
|
||||
}
|
||||
if (this.isEdit) {
|
||||
this.billing_state = this.billingStates.find((state) => state.id === this.billing.state_id)
|
||||
}
|
||||
},
|
||||
async fetchBillingCities () {
|
||||
let res = await window.axios.get(`/api/cities/${this.billing_state.id}`)
|
||||
if (res) {
|
||||
this.billingCities = res.data.cities
|
||||
}
|
||||
if (this.isEdit) {
|
||||
this.billing_city = this.billingCities.find((city) => city.id === this.billing.city_id)
|
||||
}
|
||||
},
|
||||
async fetchShippingState () {
|
||||
let res = await window.axios.get(`/api/states/${this.shipping_country.id}`)
|
||||
if (res) {
|
||||
this.shippingStates = res.data.states
|
||||
}
|
||||
if (this.isEdit) {
|
||||
this.shipping_state = this.shippingStates.find((s) => s.id === this.shipping.state_id)
|
||||
}
|
||||
},
|
||||
async fetchShippingCities () {
|
||||
let res = await window.axios.get(`/api/cities/${this.shipping_state.id}`)
|
||||
if (res) {
|
||||
this.shippingCities = res.data.cities
|
||||
}
|
||||
if (this.isEdit) {
|
||||
this.shipping_city = this.shippingCities.find((c) => c.id === this.shipping.city_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
383
resources/assets/js/views/customers/Index.vue
Normal file
383
resources/assets/js/views/customers/Index.vue
Normal file
@@ -0,0 +1,383 @@
|
||||
<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>
|
||||
|
||||
<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')"
|
||||
>
|
||||
{{ $t('customers.add_new_customer') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<transition name="fade">
|
||||
<v-dropdown v-if="selectedCustomers.length" :show-arrow="false">
|
||||
<span slot="activator" href="#" class="table-actions-button dropdown-toggle">
|
||||
{{ $t('general.actions') }}
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="select-all"
|
||||
v-model="selectAllFieldStatus"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
@change="selectAllCustomers"
|
||||
>
|
||||
<label for="select-all" class="custom-control-label selectall">
|
||||
<span class="select-all-label">{{ $t('general.select_all') }} </span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<table-component
|
||||
ref="table"
|
||||
:show-filter="false"
|
||||
:data="fetchData"
|
||||
table-class="table"
|
||||
>
|
||||
<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="$t('customers.display_name')"
|
||||
show="name"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('customers.contact_name')"
|
||||
show="contact_name"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('customers.phone')"
|
||||
show="phone"
|
||||
/>
|
||||
<table-column
|
||||
: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)"/>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
:label="$t('customers.added_on')"
|
||||
sort-as="created_at"
|
||||
show="formattedCreatedAt"
|
||||
/>
|
||||
<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>
|
||||
|
||||
</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>
|
||||
</template>
|
||||
</table-column>
|
||||
</table-component>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { SweetModal, SweetModalTab } from 'sweet-modal-vue'
|
||||
import DotIcon from '../../components/icon/DotIcon'
|
||||
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
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showFilters: false,
|
||||
filtersApplied: false,
|
||||
isRequestOngoing: true,
|
||||
filters: {
|
||||
display_name: '',
|
||||
contact_name: '',
|
||||
phone: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showEmptyScreen () {
|
||||
return !this.totalCustomers && !this.isRequestOngoing && !this.filtersApplied
|
||||
},
|
||||
filterIcon () {
|
||||
return (this.showFilters) ? 'times' : 'filter'
|
||||
},
|
||||
...mapGetters('customer', [
|
||||
'customers',
|
||||
'selectedCustomers',
|
||||
'totalCustomers',
|
||||
'selectAllField'
|
||||
]),
|
||||
selectField: {
|
||||
get: function () {
|
||||
return this.selectedCustomers
|
||||
},
|
||||
set: function (val) {
|
||||
this.selectCustomer(val)
|
||||
}
|
||||
},
|
||||
selectAllFieldStatus: {
|
||||
get: function () {
|
||||
return this.selectAllField
|
||||
},
|
||||
set: function (val) {
|
||||
this.setSelectAllState(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
filters: {
|
||||
handler: 'setFilters',
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
if (this.selectAllField) {
|
||||
this.selectAllCustomers()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('customer', [
|
||||
'fetchCustomers',
|
||||
'selectAllCustomers',
|
||||
'selectCustomer',
|
||||
'deleteCustomer',
|
||||
'deleteMultipleCustomers',
|
||||
'setSelectAllState'
|
||||
]),
|
||||
refreshTable () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
this.isRequestOngoing = true
|
||||
let response = await this.fetchCustomers(data)
|
||||
this.isRequestOngoing = false
|
||||
|
||||
return {
|
||||
data: response.data.customers.data,
|
||||
pagination: {
|
||||
totalPages: response.data.customers.last_page,
|
||||
currentPage: page
|
||||
}
|
||||
}
|
||||
},
|
||||
setFilters () {
|
||||
this.filtersApplied = true
|
||||
this.refreshTable()
|
||||
},
|
||||
clearFilter () {
|
||||
this.filters = {
|
||||
display_name: '',
|
||||
contact_name: '',
|
||||
phone: ''
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.filtersApplied = false
|
||||
})
|
||||
},
|
||||
toggleFilter () {
|
||||
if (this.showFilters && this.filtersApplied) {
|
||||
this.clearFilter()
|
||||
this.refreshTable()
|
||||
}
|
||||
|
||||
this.showFilters = !this.showFilters
|
||||
},
|
||||
async removeCustomer (id) {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('customers.confirm_delete'),
|
||||
icon: 'error',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteCustomer(id)
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$tc('customers.deleted_message'))
|
||||
this.refreshTable()
|
||||
return true
|
||||
} else if (request.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async removeMultipleCustomers () {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('customers.confirm_delete', 2),
|
||||
icon: 'error',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let request = await this.deleteMultipleCustomers()
|
||||
if (request.data.success) {
|
||||
window.toastr['success'](this.$tc('customers.deleted_message', 2))
|
||||
this.refreshTable()
|
||||
} else if (request.data.error) {
|
||||
window.toastr['error'](request.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
489
resources/assets/js/views/dashboard/Dashboard.vue
Normal file
489
resources/assets/js/views/dashboard/Dashboard.vue
Normal file
@@ -0,0 +1,489 @@
|
||||
<template>
|
||||
<div id="app" class="main-content">
|
||||
<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"
|
||||
: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="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('dashboard.recent_invoices_card.amount_due')" 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 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>
|
||||
<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="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('dashboard.recent_estimate_card.amount_due')" 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="envelope" 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>
|
||||
</template>
|
||||
</table-column>
|
||||
</table-component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LineChart,
|
||||
DollarIcon,
|
||||
ContactIcon,
|
||||
InvoiceIcon,
|
||||
EstimateIcon
|
||||
},
|
||||
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.loadChart()
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('dashboard', [
|
||||
'getChart',
|
||||
'loadData'
|
||||
]),
|
||||
...mapActions('estimate', [
|
||||
'deleteEstimate',
|
||||
'convertToInvoice'
|
||||
]),
|
||||
...mapActions('invoice', [
|
||||
'deleteInvoice',
|
||||
'sendEmail',
|
||||
'markAsSent'
|
||||
]),
|
||||
|
||||
async loadChart () {
|
||||
await this.$store.dispatch('dashboard/getChart')
|
||||
this.isLoaded = true
|
||||
},
|
||||
|
||||
async loadData (params) {
|
||||
await this.$store.dispatch('dashboard/loadData', params)
|
||||
this.isLoaded = true
|
||||
},
|
||||
|
||||
async removeEstimate (id) {
|
||||
this.id = id
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('estimates.confirm_delete', 1),
|
||||
icon: 'error',
|
||||
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.$refs.table.refresh()
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async convertInToinvoice (id) {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_conversion'),
|
||||
icon: 'error',
|
||||
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) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.markAsSent(data)
|
||||
this.$refs.table.refresh()
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('estimates.mark_as_sent'))
|
||||
}
|
||||
},
|
||||
|
||||
async removeInvoice (id) {
|
||||
this.id = id
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('invoices.confirm_delete'),
|
||||
icon: 'error',
|
||||
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.$refs.table.refresh()
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async sendInvoice (id) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.sendEmail(data)
|
||||
this.$refs.table.refresh()
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('invoices.send_invoice'))
|
||||
}
|
||||
},
|
||||
async sentInvoice (id) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.markAsSent(data)
|
||||
this.$refs.table.refresh()
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('invoices.mark_as_sent'))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
32
resources/assets/js/views/errors/404.vue
Normal file
32
resources/assets/js/views/errors/404.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<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.yot_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') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
mounted () {
|
||||
this.setLayoutClasses()
|
||||
},
|
||||
destroyed () {
|
||||
$('body').removeClass('page-error-404')
|
||||
},
|
||||
methods: {
|
||||
setLayoutClasses () {
|
||||
let body = $('body')
|
||||
body.addClass('page-error-404')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
698
resources/assets/js/views/estimates/Create.vue
Normal file
698
resources/assets/js/views/estimates/Create.vue
Normal file
@@ -0,0 +1,698 @@
|
||||
<template>
|
||||
<div class="estimate-create-page main-content">
|
||||
<form v-if="!initLoading" action="" @submit.prevent="submitEstimateData">
|
||||
<div class="page-header">
|
||||
<h3 v-if="$route.name === 'estimates.edit'" class="page-title">{{ $t('estimates.edit_estimate') }}</h3>
|
||||
<h3 v-else class="page-title">{{ $t('estimates.new_estimate') }}</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/estimates">{{ $tc('estimates.estimate', 2) }}</router-link></li>
|
||||
<li v-if="$route.name === 'estimates.edit'" class="breadcrumb-item">{{ $t('estimates.edit_estimate') }}</li>
|
||||
<li v-else class="breadcrumb-item">{{ $t('estimates.new_estimate') }}</li>
|
||||
</ol>
|
||||
<div class="page-actions row">
|
||||
<a v-if="$route.name === 'estimates.edit'" :href="`/estimates/pdf/${newEstimate.unique_hash}`" target="_blank" class="mr-3 base-button btn btn-outline-primary default-size" outline color="theme">
|
||||
{{ $t('general.view_pdf') }}
|
||||
</a>
|
||||
<base-button
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
icon="save"
|
||||
color="theme"
|
||||
type="submit">
|
||||
{{ $t('estimates.save_estimate') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row estimate-input-group">
|
||||
<div class="col-md-5">
|
||||
<div
|
||||
v-if="selectedCustomer"
|
||||
class="show-customer"
|
||||
>
|
||||
<div class="row px-2 mt-1">
|
||||
<div class="col col-6">
|
||||
<div v-if="selectedCustomer.billing_address != null" class="row address-menu">
|
||||
<label class="col-sm-4 px-2 title">{{ $t('general.bill_to') }}</label>
|
||||
<div class="col-sm p-0 px-2 content">
|
||||
<label v-if="selectedCustomer.billing_address.name">
|
||||
{{ selectedCustomer.billing_address.name }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.billing_address.address_street_1">
|
||||
{{ selectedCustomer.billing_address.address_street_1 }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.billing_address.address_street_2">
|
||||
{{ selectedCustomer.billing_address.address_street_2 }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.billing_address.city && selectedCustomer.billing_address.state">
|
||||
{{ selectedCustomer.billing_address.city.name }}, {{ selectedCustomer.billing_address.state.name }} {{ selectedCustomer.billing_address.zip }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.billing_address.country">
|
||||
{{ selectedCustomer.billing_address.country.name }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.billing_address.phone">
|
||||
{{ selectedCustomer.billing_address.phone }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-6">
|
||||
<div v-if="selectedCustomer.shipping_address != null" class="row address-menu">
|
||||
<label class="col-sm-4 px-2 title">{{ $t('general.ship_to') }}</label>
|
||||
<div class="col-sm p-0 px-2 content">
|
||||
<label v-if="selectedCustomer.shipping_address.name">
|
||||
{{ selectedCustomer.shipping_address.name }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.shipping_address.address_street_1">
|
||||
{{ selectedCustomer.shipping_address.address_street_1 }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.shipping_address.address_street_2">
|
||||
{{ selectedCustomer.shipping_address.address_street_2 }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.shipping_address.city && selectedCustomer.shipping_address">
|
||||
{{ selectedCustomer.shipping_address.city.name }}, {{ selectedCustomer.shipping_address.state.name }} {{ selectedCustomer.shipping_address.zip }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.shipping_address.country" class="country">
|
||||
{{ selectedCustomer.shipping_address.country.name }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.shipping_address.phone" class="phone">
|
||||
{{ selectedCustomer.shipping_address.phone }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="customer-content mb-1">
|
||||
<label class="email">{{ selectedCustomer.email ? selectedCustomer.email : selectedCustomer.name }}</label>
|
||||
<label class="action" @click="removeCustomer">{{ $t('general.remove') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<base-popup v-else :class="['add-customer', {'customer-required': $v.selectedCustomer.$error}]" >
|
||||
<div slot="activator" class="add-customer-action">
|
||||
<font-awesome-icon icon="user" class="customer-icon"/>
|
||||
<div>
|
||||
<label>{{ $t('customers.new_customer') }} <span class="text-danger"> * </span></label>
|
||||
<p v-if="$v.selectedCustomer.$error && !$v.selectedCustomer.required" class="text-danger"> {{ $t('estimates.errors.required') }} </p>
|
||||
</div>
|
||||
</div>
|
||||
<customer-select-popup type="estimate" />
|
||||
</base-popup>
|
||||
</div>
|
||||
<div class="col estimate-input">
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<label>{{ $t('reports.estimates.estimate_date') }}<span class="text-danger"> * </span></label>
|
||||
<base-date-picker
|
||||
v-model="newEstimate.estimate_date"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@change="$v.newEstimate.estimate_date.$touch()"
|
||||
/>
|
||||
<span v-if="$v.newEstimate.estimate_date.$error && !$v.newEstimate.estimate_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label>{{ $t('estimates.due_date') }}<span class="text-danger"> * </span></label>
|
||||
<base-date-picker
|
||||
v-model="newEstimate.expiry_date"
|
||||
:invalid="$v.newEstimate.expiry_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@change="$v.newEstimate.expiry_date.$touch()"
|
||||
/>
|
||||
<span v-if="$v.newEstimate.expiry_date.$error && !$v.newEstimate.expiry_date.required" class="text-danger mt-1"> {{ $t('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4">
|
||||
<div class="col">
|
||||
<label>{{ $t('estimates.estimate_number') }}<span class="text-danger"> * </span></label>
|
||||
<base-input
|
||||
:invalid="$v.newEstimate.estimate_number.$error"
|
||||
:read-only="true"
|
||||
v-model="newEstimate.estimate_number"
|
||||
icon="hashtag"
|
||||
@input="$v.newEstimate.estimate_number.$touch()"
|
||||
/>
|
||||
<span v-show="$v.newEstimate.estimate_number.$error && !$v.newEstimate.estimate_number.required" class="text-danger mt-1"> {{ $tc('estimates.errors.required') }} </span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label>{{ $t('estimates.ref_number') }}</label>
|
||||
<base-input
|
||||
v-model="newEstimate.reference_number"
|
||||
:invalid="$v.newEstimate.reference_number.$error"
|
||||
icon="hashtag"
|
||||
type="number"
|
||||
@input="$v.newEstimate.reference_number.$touch()"
|
||||
/>
|
||||
<div v-if="$v.newEstimate.reference_number.$error" class="text-danger">{{ $tc('validation.ref_number_maxlength') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="item-table">
|
||||
<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%;">
|
||||
</colgroup>
|
||||
<thead class="item-table-header">
|
||||
<tr>
|
||||
<th class="text-left">
|
||||
<span class="column-heading item-heading">
|
||||
{{ $tc('items.item',2) }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span class="column-heading">
|
||||
{{ $t('estimates.item.quantity') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="text-left">
|
||||
<span class="column-heading">
|
||||
{{ $t('estimates.item.price') }}
|
||||
</span>
|
||||
</th>
|
||||
<th v-if="discountPerItem === 'YES'" class="text-right">
|
||||
<span class="column-heading">
|
||||
{{ $t('estimates.item.discount') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span class="column-heading amount-heading">
|
||||
{{ $t('estimates.item.amount') }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<draggable v-model="newEstimate.items" class="item-body" tag="tbody" handle=".handle">
|
||||
<estimate-item
|
||||
v-for="(item, index) in newEstimate.items"
|
||||
:key="item.id"
|
||||
:index="index"
|
||||
:item-data="item"
|
||||
:currency="currency"
|
||||
:tax-per-item="taxPerItem"
|
||||
:discount-per-item="discountPerItem"
|
||||
@remove="removeItem"
|
||||
@update="updateItem"
|
||||
@itemValidate="checkItemsData"
|
||||
/>
|
||||
</draggable>
|
||||
</table>
|
||||
<div class="add-item-action" @click="addItem">
|
||||
<font-awesome-icon icon="shopping-basket" class="mr-2"/>
|
||||
{{ $t('estimates.add_item') }}
|
||||
</div>
|
||||
|
||||
<div class="estimate-foot">
|
||||
<div>
|
||||
<label>{{ $t('estimates.notes') }}</label>
|
||||
<base-text-area
|
||||
v-model="newEstimate.notes"
|
||||
rows="3"
|
||||
cols="50"
|
||||
@input="$v.newEstimate.notes.$touch()"
|
||||
/>
|
||||
<div v-if="$v.newEstimate.notes.$error">
|
||||
<span v-if="!$v.newEstimate.notes.maxLength" class="text-danger">{{ $t('validation.notes_maxlength') }}</span>
|
||||
</div>
|
||||
<label class="mt-3 mb-1 d-block">{{ $t('estimates.estimate_template') }} <span class="text-danger"> * </span></label>
|
||||
<base-button type="button" class="btn-template" icon="pencil-alt" right-icon @click="openTemplateModal" >
|
||||
<span class="mr-4"> {{ $t('estimates.estimate_template') }} {{ getTemplateId }} </span>
|
||||
</base-button>
|
||||
</div>
|
||||
|
||||
<div class="estimate-total">
|
||||
<div class="section">
|
||||
<label class="estimate-label">{{ $t('estimates.sub_total') }}</label>
|
||||
<label class="estimate-amount">
|
||||
<div v-html="$utils.formatMoney(subtotal, currency)" />
|
||||
</label>
|
||||
</div>
|
||||
<div v-for="tax in allTaxes" :key="tax.tax_type_id" class="section">
|
||||
<label class="estimate-label">{{ tax.name }} - {{ tax.percent }}% </label>
|
||||
<label class="estimate-amount">
|
||||
<div v-html="$utils.formatMoney(tax.amount, currency)" />
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="discountPerItem === 'NO' || discountPerItem === null" class="section mt-2">
|
||||
<label class="estimate-label">{{ $t('estimates.discount') }}</label>
|
||||
<div
|
||||
class="btn-group discount-drop-down"
|
||||
role="group"
|
||||
>
|
||||
<base-input
|
||||
v-model="discount"
|
||||
:invalid="$v.newEstimate.discount_val.$error"
|
||||
input-class="item-discount"
|
||||
@input="$v.newEstimate.discount_val.$touch()"
|
||||
/>
|
||||
<v-dropdown :show-arrow="false">
|
||||
<button
|
||||
slot="activator"
|
||||
type="button"
|
||||
class="btn item-dropdown dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{{ newEstimate.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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="taxPerItem === 'NO' || taxPerItem === null">
|
||||
<tax
|
||||
v-for="(tax, index) in newEstimate.taxes"
|
||||
:index="index"
|
||||
:total="subtotalWithDiscount"
|
||||
:key="tax.id"
|
||||
:tax="tax"
|
||||
:taxes="newEstimate.taxes"
|
||||
:currency="currency"
|
||||
:total-tax="totalSimpleTax"
|
||||
@remove="removeEstimateTax"
|
||||
@update="updateTax"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<base-popup v-if="taxPerItem === 'NO' || taxPerItem === null" ref="taxModal" class="tax-selector">
|
||||
<div slot="activator" class="float-right">
|
||||
+ {{ $t('estimates.add_tax') }}
|
||||
</div>
|
||||
<tax-select-popup :taxes="newEstimate.taxes" @select="onSelectTax" />
|
||||
</base-popup>
|
||||
|
||||
<div class="section border-top mt-3">
|
||||
<label class="estimate-label">{{ $t('estimates.total') }} {{ $t('estimates.amount') }}:</label>
|
||||
<label class="estimate-amount total">
|
||||
<div v-html="$utils.formatMoney(total, currency)" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from 'vuedraggable'
|
||||
import MultiSelect from 'vue-multiselect'
|
||||
import EstimateItem from './Item'
|
||||
import EstimateStub from '../../stub/estimate'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import moment from 'moment'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import Guid from 'guid'
|
||||
import TaxStub from '../../stub/tax'
|
||||
import Tax from './EstimateTax'
|
||||
const { required, between, maxLength } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EstimateItem,
|
||||
MultiSelect,
|
||||
Tax,
|
||||
draggable
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
newEstimate: {
|
||||
estimate_date: null,
|
||||
expiry_date: null,
|
||||
estimate_number: null,
|
||||
user_id: null,
|
||||
estimate_template_id: 1,
|
||||
sub_total: null,
|
||||
total: null,
|
||||
tax: null,
|
||||
notes: null,
|
||||
discount_type: 'fixed',
|
||||
discount_val: 0,
|
||||
reference_number: null,
|
||||
discount: 0,
|
||||
items: [{
|
||||
...EstimateStub,
|
||||
id: Guid.raw(),
|
||||
taxes: [{...TaxStub, id: Guid.raw()}]
|
||||
}],
|
||||
taxes: []
|
||||
},
|
||||
customers: [],
|
||||
itemList: [],
|
||||
estimateTemplates: [],
|
||||
selectedCurrency: '',
|
||||
taxPerItem: null,
|
||||
discountPerItem: null,
|
||||
initLoading: false,
|
||||
isLoading: false,
|
||||
maxDiscount: 0
|
||||
}
|
||||
},
|
||||
validations () {
|
||||
return {
|
||||
newEstimate: {
|
||||
estimate_date: {
|
||||
required
|
||||
},
|
||||
expiry_date: {
|
||||
required
|
||||
},
|
||||
estimate_number: {
|
||||
required
|
||||
},
|
||||
discount_val: {
|
||||
between: between(0, this.subtotal)
|
||||
},
|
||||
notes: {
|
||||
maxLength: maxLength(255)
|
||||
},
|
||||
reference_number: {
|
||||
maxLength: maxLength(10)
|
||||
}
|
||||
},
|
||||
selectedCustomer: {
|
||||
required
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('general', [
|
||||
'itemDiscount'
|
||||
]),
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrency'
|
||||
]),
|
||||
...mapGetters('estimate', [
|
||||
'getTemplateId',
|
||||
'selectedCustomer'
|
||||
]),
|
||||
currency () {
|
||||
return this.selectedCurrency
|
||||
},
|
||||
subtotalWithDiscount () {
|
||||
return this.subtotal - this.newEstimate.discount_val
|
||||
},
|
||||
total () {
|
||||
return this.subtotalWithDiscount + this.totalTax
|
||||
},
|
||||
subtotal () {
|
||||
return this.newEstimate.items.reduce(function (a, b) {
|
||||
return a + b['total']
|
||||
}, 0)
|
||||
},
|
||||
discount: {
|
||||
get: function () {
|
||||
return this.newEstimate.discount
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (this.newEstimate.discount_type === 'percentage') {
|
||||
this.newEstimate.discount_val = (this.subtotal * newValue) / 100
|
||||
} else {
|
||||
this.newEstimate.discount_val = newValue * 100
|
||||
}
|
||||
|
||||
this.newEstimate.discount = newValue
|
||||
}
|
||||
},
|
||||
totalSimpleTax () {
|
||||
return window._.sumBy(this.newEstimate.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
},
|
||||
|
||||
totalCompoundTax () {
|
||||
return window._.sumBy(this.newEstimate.taxes, function (tax) {
|
||||
if (tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
},
|
||||
totalTax () {
|
||||
if (this.taxPerItem === 'NO' || this.taxPerItem === null) {
|
||||
return this.totalSimpleTax + this.totalCompoundTax
|
||||
}
|
||||
|
||||
return window._.sumBy(this.newEstimate.items, function (tax) {
|
||||
return tax.tax
|
||||
})
|
||||
},
|
||||
allTaxes () {
|
||||
let taxes = []
|
||||
|
||||
this.newEstimate.items.forEach((item) => {
|
||||
item.taxes.forEach((tax) => {
|
||||
let found = taxes.find((_tax) => {
|
||||
return _tax.tax_type_id === tax.tax_type_id
|
||||
})
|
||||
|
||||
if (found) {
|
||||
found.amount += tax.amount
|
||||
} else if (tax.tax_type_id) {
|
||||
taxes.push({
|
||||
tax_type_id: tax.tax_type_id,
|
||||
amount: tax.amount,
|
||||
percent: tax.percent,
|
||||
name: tax.name
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return taxes
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedCustomer (newVal) {
|
||||
if (newVal && newVal.currency) {
|
||||
this.selectedCurrency = newVal.currency
|
||||
} else {
|
||||
this.selectedCurrency = this.defaultCurrency
|
||||
}
|
||||
},
|
||||
subtotal (newValue) {
|
||||
if (this.newEstimate.discount_type === 'percentage') {
|
||||
this.newEstimate.discount_val = (this.newEstimate.discount * newValue) / 100
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.loadData()
|
||||
this.fetchInitialItems()
|
||||
this.resetSelectedCustomer()
|
||||
window.hub.$on('newTax', this.onSelectTax)
|
||||
},
|
||||
methods: {
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
...mapActions('estimate', [
|
||||
'addEstimate',
|
||||
'fetchCreateEstimate',
|
||||
'fetchEstimate',
|
||||
'resetSelectedCustomer',
|
||||
'selectCustomer',
|
||||
'updateEstimate'
|
||||
]),
|
||||
...mapActions('item', [
|
||||
'fetchItems'
|
||||
]),
|
||||
selectFixed () {
|
||||
if (this.newEstimate.discount_type === 'fixed') {
|
||||
return
|
||||
}
|
||||
|
||||
this.newEstimate.discount_val = this.newEstimate.discount * 100
|
||||
this.newEstimate.discount_type = 'fixed'
|
||||
},
|
||||
selectPercentage () {
|
||||
if (this.newEstimate.discount_type === 'percentage') {
|
||||
return
|
||||
}
|
||||
|
||||
this.newEstimate.discount_val = (this.subtotal * this.newEstimate.discount) / 100
|
||||
|
||||
this.newEstimate.discount_type = 'percentage'
|
||||
},
|
||||
updateTax (data) {
|
||||
Object.assign(this.newEstimate.taxes[data.index], {...data.item})
|
||||
},
|
||||
async fetchInitialItems () {
|
||||
await this.fetchItems({
|
||||
filter: {},
|
||||
orderByField: '',
|
||||
orderBy: ''
|
||||
})
|
||||
},
|
||||
async loadData () {
|
||||
if (this.$route.name === 'estimates.edit') {
|
||||
this.initLoading = true
|
||||
let response = await this.fetchEstimate(this.$route.params.id)
|
||||
|
||||
if (response.data) {
|
||||
this.selectCustomer(response.data.estimate.user_id)
|
||||
this.newEstimate = response.data.estimate
|
||||
this.discountPerItem = response.data.discount_per_item
|
||||
this.taxPerItem = response.data.tax_per_item
|
||||
this.selectedCurrency = this.defaultCurrency
|
||||
this.estimateTemplates = response.data.estimateTemplates
|
||||
}
|
||||
this.initLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
this.initLoading = true
|
||||
let response = await this.fetchCreateEstimate()
|
||||
if (response.data) {
|
||||
this.discountPerItem = response.data.discount_per_item
|
||||
this.taxPerItem = response.data.tax_per_item
|
||||
this.selectedCurrency = this.defaultCurrency
|
||||
this.estimateTemplates = response.data.estimateTemplates
|
||||
let today = new Date()
|
||||
this.newEstimate.estimate_date = moment(today).toString()
|
||||
this.newEstimate.expiry_date = moment(today).add(7, 'days').toString()
|
||||
this.newEstimate.estimate_number = response.data.nextEstimateNumber
|
||||
this.itemList = response.data.items
|
||||
}
|
||||
this.initLoading = false
|
||||
},
|
||||
removeCustomer () {
|
||||
this.resetSelectedCustomer()
|
||||
},
|
||||
openTemplateModal () {
|
||||
this.openModal({
|
||||
'title': 'Choose a template',
|
||||
'componentName': 'EstimateTemplate',
|
||||
'data': this.estimateTemplates
|
||||
})
|
||||
},
|
||||
addItem () {
|
||||
this.newEstimate.items.push({...EstimateStub, id: Guid.raw(), taxes: [{...TaxStub, id: Guid.raw()}]})
|
||||
},
|
||||
removeItem (index) {
|
||||
this.newEstimate.items.splice(index, 1)
|
||||
},
|
||||
updateItem (data) {
|
||||
Object.assign(this.newEstimate.items[data.index], {...data.item})
|
||||
},
|
||||
submitEstimateData () {
|
||||
if (!this.checkValid()) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.isLoading = true
|
||||
|
||||
let data = {
|
||||
...this.newEstimate,
|
||||
estimate_date: moment(this.newEstimate.estimate_date).format('DD/MM/YYYY'),
|
||||
expiry_date: moment(this.newEstimate.expiry_date).format('DD/MM/YYYY'),
|
||||
sub_total: this.subtotal,
|
||||
total: this.total,
|
||||
tax: this.totalTax,
|
||||
user_id: null,
|
||||
estimate_template_id: this.getTemplateId
|
||||
}
|
||||
|
||||
if (this.selectedCustomer != null) {
|
||||
data.user_id = this.selectedCustomer.id
|
||||
}
|
||||
|
||||
if (this.$route.name === 'estimates.edit') {
|
||||
this.submitUpdate(data)
|
||||
return
|
||||
}
|
||||
|
||||
this.submitSave(data)
|
||||
},
|
||||
submitSave (data) {
|
||||
this.addEstimate(data).then((res) => {
|
||||
if (res.data) {
|
||||
window.toastr['success'](this.$t('estimates.created_message'))
|
||||
this.$router.push('/admin/estimates')
|
||||
}
|
||||
|
||||
this.isLoading = false
|
||||
}).catch((err) => {
|
||||
this.isLoading = false
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
submitUpdate (data) {
|
||||
this.updateEstimate(data).then((res) => {
|
||||
if (res.data) {
|
||||
window.toastr['success'](this.$t('estimates.updated_message'))
|
||||
this.$router.push('/admin/estimates')
|
||||
}
|
||||
|
||||
this.isLoading = false
|
||||
}).catch((err) => {
|
||||
this.isLoading = false
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
checkItemsData (index, isValid) {
|
||||
this.newEstimate.items[index].valid = isValid
|
||||
},
|
||||
onSelectTax (selectedTax) {
|
||||
let amount = 0
|
||||
|
||||
if (selectedTax.compound_tax && this.subtotalWithDiscount) {
|
||||
amount = ((this.subtotalWithDiscount + this.totalSimpleTax) * selectedTax.percent) / 100
|
||||
} else if (this.subtotalWithDiscount && selectedTax.percent) {
|
||||
amount = (this.subtotalWithDiscount * selectedTax.percent) / 100
|
||||
}
|
||||
|
||||
this.newEstimate.taxes.push({
|
||||
...TaxStub,
|
||||
id: Guid.raw(),
|
||||
name: selectedTax.name,
|
||||
percent: selectedTax.percent,
|
||||
compound_tax: selectedTax.compound_tax,
|
||||
tax_type_id: selectedTax.id,
|
||||
amount
|
||||
})
|
||||
|
||||
this.$refs.taxModal.close()
|
||||
},
|
||||
removeEstimateTax (index) {
|
||||
this.newEstimate.taxes.splice(index, 1)
|
||||
},
|
||||
checkValid () {
|
||||
this.$v.newEstimate.$touch()
|
||||
this.$v.selectedCustomer.$touch()
|
||||
window.hub.$emit('checkItems')
|
||||
let isValid = true
|
||||
this.newEstimate.items.forEach((item) => {
|
||||
if (!item.valid) {
|
||||
isValid = false
|
||||
}
|
||||
})
|
||||
if (this.$v.newEstimate.$invalid === false && isValid === true) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
83
resources/assets/js/views/estimates/EstimateTax.vue
Normal file
83
resources/assets/js/views/estimates/EstimateTax.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="section mt-2">
|
||||
<label class="estimate-label">
|
||||
{{ tax.name }} ({{ tax.percent }}%)
|
||||
</label>
|
||||
<label class="estimate-amount">
|
||||
<div v-html="$utils.formatMoney(tax.amount, currency)" />
|
||||
|
||||
<font-awesome-icon
|
||||
class="ml-2"
|
||||
icon="trash-alt"
|
||||
@click="$emit('remove', index)"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
tax: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
totalTax: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
taxAmount () {
|
||||
if (this.tax.compound_tax && this.total) {
|
||||
return ((this.total + this.totalTax) * this.tax.percent) / 100
|
||||
}
|
||||
|
||||
if (this.total && this.tax.percent) {
|
||||
return (this.total * this.tax.percent) / 100
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
total: {
|
||||
handler: 'updateTax'
|
||||
},
|
||||
totalTax: {
|
||||
handler: 'updateTax'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateTax () {
|
||||
this.$emit('update', {
|
||||
'index': this.index,
|
||||
'item': {
|
||||
...this.tax,
|
||||
amount: this.taxAmount
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
560
resources/assets/js/views/estimates/Index.vue
Normal file
560
resources/assets/js/views/estimates/Index.vue
Normal file
@@ -0,0 +1,560 @@
|
||||
<template>
|
||||
<div class="estimate-index-page estimates main-content">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ $t('estimates.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('estimates.estimate', 2) }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="page-actions row">
|
||||
<div class="col-xs-2 mr-4">
|
||||
<base-button
|
||||
v-show="totalEstimates || 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="estimates/create">
|
||||
<base-button
|
||||
size="large"
|
||||
icon="plus"
|
||||
color="theme" >
|
||||
{{ $t('estimates.new_estimate') }}</base-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-show="showFilters" class="filter-section">
|
||||
<div class="filter-container">
|
||||
<div class="filter-customer">
|
||||
<label>{{ $tc('customers.customer',1) }} </label>
|
||||
<base-customer-select
|
||||
ref="customerSelect"
|
||||
@select="onSelectCustomer"
|
||||
@deselect="clearCustomerSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-status">
|
||||
<label>{{ $t('estimates.status') }}</label>
|
||||
<base-select
|
||||
v-model="filters.status"
|
||||
:options="status"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('general.select_a_status')"
|
||||
@remove="clearStatusSearch()"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-date">
|
||||
<div class="from pr-3">
|
||||
<label>{{ $t('general.from') }}</label>
|
||||
<base-date-picker
|
||||
v-model="filters.from_date"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
/>
|
||||
</div>
|
||||
<div class="dashed" />
|
||||
<div class="to pl-3">
|
||||
<label>{{ $t('general.to') }}</label>
|
||||
<base-date-picker
|
||||
v-model="filters.to_date"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-estimate">
|
||||
<label>{{ $t('estimates.estimate_number') }}</label>
|
||||
<base-input
|
||||
v-model="filters.estimate_number"
|
||||
icon="hashtag"/>
|
||||
</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">
|
||||
<moon-walker-icon class="mt-5 mb-4"/>
|
||||
<div class="row" align="center">
|
||||
<label class="col title">{{ $t('estimates.no_estimates') }}</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="description col mt-1" align="center">{{ $t('estimates.list_of_estimates') }}</label>
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<base-button
|
||||
:outline="true"
|
||||
color="theme"
|
||||
class="mt-3"
|
||||
size="large"
|
||||
@click="$router.push('estimates/create')"
|
||||
>
|
||||
{{ $t('estimates.add_new_estimate') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="!showEmptyScreen" class="table-container">
|
||||
<div class="table-actions mt-5">
|
||||
<p class="table-stats">{{ $t('general.showing') }}: <b>{{ estimates.length }}</b> {{ $t('general.of') }} <b>{{ totalEstimates }}</b></p>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="tabs">
|
||||
<li class="tab" @click="getStatus('DRAFT')">
|
||||
<a :class="['tab-link', {'a-active': filters.status === 'DRAFT'}]" href="#">{{ $t('general.draft') }}</a>
|
||||
</li>
|
||||
<li class="tab" @click="getStatus('SENT')">
|
||||
<a :class="['tab-link', {'a-active': filters.status === 'SENT'}]" href="#" >{{ $t('general.sent') }}</a>
|
||||
</li>
|
||||
<li class="tab" @click="getStatus('')">
|
||||
<a :class="['tab-link', {'a-active': filters.status === '' || filters.status === null}]" href="#">{{ $t('general.all') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<transition name="fade">
|
||||
<v-dropdown v-if="selectedEstimates.length" :show-arrow="false">
|
||||
<span slot="activator" href="#" class="table-actions-button dropdown-toggle">
|
||||
{{ $t('general.actions') }}
|
||||
</span>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeMultipleEstimates">
|
||||
<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"
|
||||
:disabled="estimates.length <= 0"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
@change="selectAllEstimates"
|
||||
>
|
||||
<label v-show="!isRequestOngoing" for="select-all" class="custom-control-label selectall">
|
||||
<span class="select-all-label">{{ $t('general.select_all') }} </span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<table-component
|
||||
ref="table"
|
||||
:show-filter="false"
|
||||
:data="fetchData"
|
||||
table-class="table"
|
||||
>
|
||||
<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="$t('estimates.date')"
|
||||
sort-as="estimate_date"
|
||||
show="formattedEstimateDate" />
|
||||
<table-column
|
||||
:label="$t('estimates.contact')"
|
||||
sort-as="name"
|
||||
show="name" />
|
||||
<table-column
|
||||
:label="$t('estimates.expiry_date')"
|
||||
sort-as="expiry_date"
|
||||
show="formattedExpiryDate" />
|
||||
<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="$tc('estimates.estimate', 1)"
|
||||
show="estimate_number"/>
|
||||
<table-column
|
||||
:label="$t('invoices.total')"
|
||||
sort-as="total"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<div v-html="$utils.formatMoney(row.total, row.user.currency)" />
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
cell-class="action-dropdown"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span> {{ $t('estimates.action') }} </span>
|
||||
<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="envelope" class="dropdown-item-icon" />
|
||||
{{ $t('estimates.convert_to_invoice') }}
|
||||
</a>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<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>
|
||||
<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 === 'DRAFT'">
|
||||
<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 === 'DRAFT'">
|
||||
<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>
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import MoonWalkerIcon from '../../../js/components/icon/MoonwalkerIcon'
|
||||
import { SweetModal, SweetModalTab } from 'sweet-modal-vue'
|
||||
import ObservatoryIcon from '../../components/icon/ObservatoryIcon'
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'moon-walker-icon': MoonWalkerIcon
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showFilters: false,
|
||||
currency: null,
|
||||
status: ['DRAFT', 'SENT', 'VIEWED', 'EXPIRED', 'ACCEPTED', 'REJECTED'],
|
||||
filtersApplied: false,
|
||||
isRequestOngoing: true,
|
||||
filters: {
|
||||
customer: '',
|
||||
status: 'DRAFT',
|
||||
from_date: '',
|
||||
to_date: '',
|
||||
estimate_number: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
focus,
|
||||
showEmptyScreen () {
|
||||
return !this.totalEstimates && !this.isRequestOngoing && !this.filtersApplied
|
||||
},
|
||||
filterIcon () {
|
||||
return (this.showFilters) ? 'times' : 'filter'
|
||||
},
|
||||
...mapGetters('customer', [
|
||||
'customers'
|
||||
]),
|
||||
...mapGetters('estimate', [
|
||||
'selectedEstimates',
|
||||
'totalEstimates',
|
||||
'estimates',
|
||||
'selectAllField'
|
||||
]),
|
||||
selectField: {
|
||||
get: function () {
|
||||
return this.selectedEstimates
|
||||
},
|
||||
set: function (val) {
|
||||
this.selectEstimate(val)
|
||||
}
|
||||
},
|
||||
selectAllFieldStatus: {
|
||||
get: function () {
|
||||
return this.selectAllField
|
||||
},
|
||||
set: function (val) {
|
||||
this.setSelectAllState(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
filters: {
|
||||
handler: 'setFilters',
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchCustomers()
|
||||
},
|
||||
destroyed () {
|
||||
if (this.selectAllField) {
|
||||
this.selectAllEstimates()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('estimate', [
|
||||
'fetchEstimates',
|
||||
'resetSelectedEstimates',
|
||||
'getRecord',
|
||||
'selectEstimate',
|
||||
'selectAllEstimates',
|
||||
'deleteEstimate',
|
||||
'deleteMultipleEstimates',
|
||||
'markAsSent',
|
||||
'convertToInvoice',
|
||||
'setSelectAllState',
|
||||
'markAsAccepted',
|
||||
'markAsRejected',
|
||||
'sendEmail'
|
||||
]),
|
||||
...mapActions('customer', [
|
||||
'fetchCustomers'
|
||||
]),
|
||||
refreshTable () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
getStatus (val) {
|
||||
this.filters.status = val
|
||||
},
|
||||
async fetchData ({ page, filter, sort }) {
|
||||
let data = {
|
||||
customer_id: this.filters.customer === '' ? this.filters.customer : this.filters.customer.id,
|
||||
status: this.filters.status,
|
||||
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'),
|
||||
estimate_number: this.filters.estimate_number,
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page
|
||||
}
|
||||
|
||||
this.isRequestOngoing = true
|
||||
let response = await this.fetchEstimates(data)
|
||||
this.isRequestOngoing = false
|
||||
|
||||
this.currency = response.data.currency
|
||||
|
||||
return {
|
||||
data: response.data.estimates.data,
|
||||
pagination: {
|
||||
totalPages: response.data.estimates.last_page,
|
||||
currentPage: page,
|
||||
count: response.data.estimates.count
|
||||
}
|
||||
}
|
||||
},
|
||||
async onMarkAsAccepted (id) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.markAsAccepted(data)
|
||||
this.refreshTable()
|
||||
if (response.data) {
|
||||
this.filters.status = 'ACCEPTED'
|
||||
this.$refs.table.refresh()
|
||||
window.toastr['success'](this.$tc('estimates.marked_as_rejected_message'))
|
||||
}
|
||||
},
|
||||
async onMarkAsRejected (id) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.markAsRejected(data)
|
||||
this.refreshTable()
|
||||
if (response.data) {
|
||||
this.filters.status = 'REJECTED'
|
||||
this.$refs.table.refresh()
|
||||
window.toastr['success'](this.$tc('estimates.marked_as_rejected_message'))
|
||||
}
|
||||
},
|
||||
setFilters () {
|
||||
this.filtersApplied = true
|
||||
this.resetSelectedEstimates()
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
clearFilter () {
|
||||
if (this.filters.customer) {
|
||||
this.$refs.customerSelect.$refs.baseSelect.removeElement(this.filters.customer)
|
||||
}
|
||||
|
||||
this.filters = {
|
||||
customer: '',
|
||||
status: '',
|
||||
from_date: '',
|
||||
to_date: '',
|
||||
estimate_number: ''
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.filtersApplied = false
|
||||
})
|
||||
},
|
||||
toggleFilter () {
|
||||
if (this.showFilters && this.filtersApplied) {
|
||||
this.clearFilter()
|
||||
this.refreshTable()
|
||||
}
|
||||
|
||||
this.showFilters = !this.showFilters
|
||||
},
|
||||
onSelectCustomer (customer) {
|
||||
this.filters.customer = customer
|
||||
},
|
||||
async removeEstimate (id) {
|
||||
this.id = id
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('estimates.confirm_delete', 1),
|
||||
icon: 'error',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteEstimate(this.id)
|
||||
if (res.data.success) {
|
||||
this.$refs.table.refresh()
|
||||
this.filtersApplied = false
|
||||
this.resetSelectedEstimates()
|
||||
window.toastr['success'](this.$tc('estimates.deleted_message', 1))
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async convertInToinvoice (id) {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('estimates.confirm_conversion'),
|
||||
icon: 'error',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.convertToInvoice(id)
|
||||
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 removeMultipleEstimates () {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('estimates.confirm_delete', 2),
|
||||
icon: 'error',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteMultipleEstimates()
|
||||
if (res.data.success) {
|
||||
this.$refs.table.refresh()
|
||||
this.resetSelectedEstimates()
|
||||
this.filtersApplied = false
|
||||
window.toastr['success'](this.$tc('estimates.deleted_message', 2))
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async clearCustomerSearch (removedOption, id) {
|
||||
this.filters.customer = ''
|
||||
this.refreshTable()
|
||||
},
|
||||
async clearStatusSearch (removedOption, id) {
|
||||
this.filters.status = ''
|
||||
this.refreshTable()
|
||||
},
|
||||
async onMarkAsSent (id) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.markAsSent(data)
|
||||
this.refreshTable()
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('estimates.mark_as_sent'))
|
||||
}
|
||||
},
|
||||
async sendEstimate (id) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.sendEmail(data)
|
||||
this.refreshTable()
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('estimates.mark_as_sent'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
402
resources/assets/js/views/estimates/Item.vue
Normal file
402
resources/assets/js/views/estimates/Item.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
<template>
|
||||
<tr class="item-row estimate-item-row">
|
||||
<td colspan="5">
|
||||
<table class="full-width">
|
||||
<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%;">
|
||||
</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"
|
||||
/>
|
||||
</div>
|
||||
<item-select
|
||||
ref="itemSelect"
|
||||
:invalid="$v.item.name.$error"
|
||||
:invalid-description="$v.item.description.$error"
|
||||
:item="item"
|
||||
@search="searchVal"
|
||||
@select="onSelectItem"
|
||||
@deselect="deselectItem"
|
||||
@onDesriptionInput="$v.item.description.$touch()"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<base-input
|
||||
v-model="item.quantity"
|
||||
:invalid="$v.item.quantity.$error"
|
||||
type="number"
|
||||
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>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-left">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="flex-fillbd-highlight">
|
||||
<div class="base-input">
|
||||
<money
|
||||
v-model="price"
|
||||
v-bind="customerCurrency"
|
||||
class="input-field"
|
||||
@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>
|
||||
</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
|
||||
v-model="discount"
|
||||
:invalid="$v.item.discount_val.$error"
|
||||
input-class="item-discount"
|
||||
@input="$v.item.discount_val.$touch()"
|
||||
/>
|
||||
<v-dropdown :show-arrow="false" theme-light>
|
||||
<button
|
||||
slot="activator"
|
||||
type="button"
|
||||
class="btn item-dropdown dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{{ 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>
|
||||
</div>
|
||||
<!-- <div v-if="$v.item.discount.$error"> discount error </div> -->
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="item-amount">
|
||||
<span>
|
||||
<div v-html="$utils.formatMoney(total, currency)" />
|
||||
</span>
|
||||
|
||||
<div class="remove-icon-wrapper">
|
||||
<font-awesome-icon
|
||||
v-if="index > 0"
|
||||
class="remove-icon"
|
||||
icon="trash-alt"
|
||||
@click="removeItem"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="taxPerItem === 'YES'" class="tax-tr">
|
||||
<td />
|
||||
<td colspan="4">
|
||||
<tax
|
||||
v-for="(tax, index) in item.taxes"
|
||||
:key="tax.id"
|
||||
:index="index"
|
||||
:tax-data="tax"
|
||||
:taxes="item.taxes"
|
||||
:discounted-total="total"
|
||||
:total-tax="totalSimpleTax"
|
||||
:total="total"
|
||||
:currency="currency"
|
||||
@update="updateTax"
|
||||
@remove="removeTax"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</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')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tax,
|
||||
ItemSelect
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
props: {
|
||||
itemData: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
},
|
||||
taxPerItem: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
discountPerItem: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isClosePopup: false,
|
||||
itemSelect: null,
|
||||
item: {...this.itemData},
|
||||
maxDiscount: 0,
|
||||
money: {
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
prefix: '$ ',
|
||||
precision: 2,
|
||||
masked: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('item', [
|
||||
'items'
|
||||
]),
|
||||
...mapGetters('currency', [
|
||||
'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
|
||||
}
|
||||
} else {
|
||||
return this.defaultCurrencyForInput
|
||||
}
|
||||
},
|
||||
subtotal () {
|
||||
return this.item.price * this.item.quantity
|
||||
},
|
||||
discount: {
|
||||
get: function () {
|
||||
return this.item.discount
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (this.item.discount_type === 'percentage') {
|
||||
this.item.discount_val = (this.subtotal * newValue) / 100
|
||||
} else {
|
||||
this.item.discount_val = newValue * 100
|
||||
}
|
||||
|
||||
this.item.discount = newValue
|
||||
}
|
||||
},
|
||||
total () {
|
||||
return this.subtotal - this.item.discount_val
|
||||
},
|
||||
totalSimpleTax () {
|
||||
return window._.sumBy(this.item.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
},
|
||||
totalCompoundTax () {
|
||||
return window._.sumBy(this.item.taxes, function (tax) {
|
||||
if (tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
},
|
||||
totalTax () {
|
||||
return this.totalSimpleTax + this.totalCompoundTax
|
||||
},
|
||||
price: {
|
||||
get: function () {
|
||||
if (parseFloat(this.item.price) > 0) {
|
||||
return this.item.price / 100
|
||||
}
|
||||
|
||||
return this.item.price
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (parseFloat(newValue) > 0) {
|
||||
this.item.price = newValue * 100
|
||||
this.maxDiscount = this.item.price
|
||||
} else {
|
||||
this.item.price = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
item: {
|
||||
handler: 'updateItem',
|
||||
deep: true
|
||||
},
|
||||
subtotal (newValue) {
|
||||
if (this.item.discount_type === 'percentage') {
|
||||
this.item.discount_val = (this.item.discount * newValue) / 100
|
||||
}
|
||||
}
|
||||
},
|
||||
validations () {
|
||||
return {
|
||||
item: {
|
||||
name: {
|
||||
required
|
||||
},
|
||||
quantity: {
|
||||
required,
|
||||
minValue: minValue(1),
|
||||
maxLength: maxLength(10)
|
||||
},
|
||||
price: {
|
||||
required,
|
||||
minValue: minValue(1),
|
||||
maxLength: maxLength(10)
|
||||
},
|
||||
discount_val: {
|
||||
between: between(0, this.maxDiscount)
|
||||
},
|
||||
description: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.hub.$on('checkItems', this.validateItem)
|
||||
window.hub.$on('newItem', this.onSelectItem)
|
||||
},
|
||||
methods: {
|
||||
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.updateItem()
|
||||
},
|
||||
removeTax (index) {
|
||||
this.item.taxes.splice(index, 1)
|
||||
|
||||
this.updateItem()
|
||||
},
|
||||
taxWithPercentage ({ name, percent }) {
|
||||
return `${name} (${percent}%)`
|
||||
},
|
||||
searchVal (val) {
|
||||
this.item.name = val
|
||||
},
|
||||
deselectItem () {
|
||||
this.item = {...EstimateStub, id: this.item.id}
|
||||
this.$nextTick(() => {
|
||||
this.$refs.itemSelect.$refs.baseSelect.$refs.search.focus()
|
||||
})
|
||||
},
|
||||
onSelectItem (item) {
|
||||
this.item.name = item.name
|
||||
this.item.price = item.price
|
||||
this.item.item_id = item.id
|
||||
this.item.description = item.description
|
||||
|
||||
// if (this.item.taxes.length) {
|
||||
// this.item.taxes = {...item.taxes}
|
||||
// }
|
||||
},
|
||||
selectFixed () {
|
||||
if (this.item.discount_type === 'fixed') {
|
||||
return
|
||||
}
|
||||
|
||||
this.item.discount_val = this.item.discount * 100
|
||||
this.item.discount_type = 'fixed'
|
||||
},
|
||||
selectPercentage () {
|
||||
if (this.item.discount_type === 'percentage') {
|
||||
return
|
||||
}
|
||||
|
||||
this.item.discount_val = (this.subtotal * this.item.discount) / 100
|
||||
|
||||
this.item.discount_type = 'percentage'
|
||||
},
|
||||
updateItem () {
|
||||
this.$emit('update', {
|
||||
'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]
|
||||
}
|
||||
})
|
||||
},
|
||||
removeItem () {
|
||||
this.$emit('remove', this.index)
|
||||
},
|
||||
validateItem () {
|
||||
this.$v.item.$touch()
|
||||
|
||||
if (this.item !== null) {
|
||||
this.$emit('itemValidate', this.index, !this.$v.$invalid)
|
||||
} else {
|
||||
this.$emit('itemValidate', this.index, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
140
resources/assets/js/views/estimates/ItemSelect.vue
Normal file
140
resources/assets/js/views/estimates/ItemSelect.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="item-selector">
|
||||
<div v-if="item.item_id" class="selected-item">
|
||||
{{ item.name }}
|
||||
|
||||
<span class="deselect-icon" @click="deselectItem">
|
||||
<font-awesome-icon icon="times-circle" />
|
||||
</span>
|
||||
</div>
|
||||
<base-select
|
||||
v-else
|
||||
ref="baseSelect"
|
||||
v-model="itemSelect"
|
||||
:options="items"
|
||||
:show-labels="false"
|
||||
:preserve-search="true"
|
||||
:initial-search="item.name"
|
||||
:invalid="invalid"
|
||||
:placeholder="$t('estimates.item.select_an_item')"
|
||||
label="name"
|
||||
class="multi-select-item"
|
||||
@value="onTextChange"
|
||||
@select="(val) => $emit('select', val)"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</base-select>
|
||||
<div class="item-description">
|
||||
<base-text-area
|
||||
v-autoresize
|
||||
v-model.trim="item.description"
|
||||
:invalid-description="invalidDescription"
|
||||
:placeholder="$t('estimates.item.type_item_description')"
|
||||
type="text"
|
||||
rows="1"
|
||||
class="description-input"
|
||||
@input="$emit('onDesriptionInput')"
|
||||
/>
|
||||
<div v-if="invalidDescription">
|
||||
<span class="text-danger">{{ $tc('validation.description_maxlength') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
const { maxLength } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
invalid: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
invalidDescription: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
itemSelect: null,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
validations () {
|
||||
return {
|
||||
item: {
|
||||
description: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('item', [
|
||||
'items'
|
||||
])
|
||||
},
|
||||
watch: {
|
||||
invalidDescription (newValue) {
|
||||
console.log(newValue)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
...mapActions('item', [
|
||||
'fetchItems'
|
||||
]),
|
||||
async searchItems (search) {
|
||||
let data = {
|
||||
filter: {
|
||||
name: search,
|
||||
unit: '',
|
||||
price: ''
|
||||
},
|
||||
orderByField: '',
|
||||
orderBy: '',
|
||||
page: 1
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
await this.fetchItems(data)
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
onTextChange (val) {
|
||||
this.searchItems(val)
|
||||
|
||||
this.$emit('search', val)
|
||||
},
|
||||
openItemModal () {
|
||||
this.openModal({
|
||||
'title': 'Add Item',
|
||||
'componentName': 'ItemModal'
|
||||
})
|
||||
},
|
||||
deselectItem () {
|
||||
this.itemSelect = null
|
||||
this.$emit('deselect')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
168
resources/assets/js/views/estimates/Tax.vue
Normal file
168
resources/assets/js/views/estimates/Tax.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div class="tax-row">
|
||||
<div class="d-flex align-items-center tax-select">
|
||||
<label class="bd-highlight pr-2 mb-0" align="right">
|
||||
{{ $t('estimates.tax') }}
|
||||
</label>
|
||||
<base-select
|
||||
v-model="selectedTax"
|
||||
:options="filteredTypes"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
:custom-label="customLabel"
|
||||
:placeholder="$t('general.select_a_tax')"
|
||||
track-by="name"
|
||||
label="name"
|
||||
@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>
|
||||
</div>
|
||||
</base-select> <br>
|
||||
</div>
|
||||
<div class="text-right tax-amount">
|
||||
<div v-html="$utils.formatMoney(taxAmount, currency)" />
|
||||
</div>
|
||||
<div class="remove-icon-wrapper">
|
||||
<font-awesome-icon
|
||||
v-if="taxes.length && index !== taxes.length - 1"
|
||||
class="remove-icon"
|
||||
icon="trash-alt"
|
||||
@click="removeTax"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
taxData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
totalTax: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
tax: {...this.taxData},
|
||||
selectedTax: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...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)
|
||||
|
||||
if (found) {
|
||||
taxType.$isDisabled = true
|
||||
} else {
|
||||
taxType.$isDisabled = false
|
||||
}
|
||||
|
||||
return taxType
|
||||
})
|
||||
},
|
||||
taxAmount () {
|
||||
if (this.tax.compound_tax && this.total) {
|
||||
return ((this.total + this.totalTax) * this.tax.percent) / 100
|
||||
}
|
||||
|
||||
if (this.total && this.tax.percent) {
|
||||
return (this.total * this.tax.percent) / 100
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
total: {
|
||||
handler: 'updateTax'
|
||||
},
|
||||
totalTax: {
|
||||
handler: 'updateTax'
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.taxData.tax_type_id > 0) {
|
||||
this.selectedTax = this.taxTypes.find(_type => _type.id === this.taxData.tax_type_id)
|
||||
}
|
||||
|
||||
window.hub.$on('newTax', (val) => {
|
||||
if (!this.selectedTax) {
|
||||
this.selectedTax = val
|
||||
this.onSelectTax(val)
|
||||
}
|
||||
})
|
||||
|
||||
this.updateTax()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
customLabel ({ name, percent }) {
|
||||
return `${name} - ${percent}%`
|
||||
},
|
||||
onSelectTax (val) {
|
||||
this.tax.percent = val.percent
|
||||
this.tax.tax_type_id = val.id
|
||||
this.tax.compound_tax = val.compound_tax
|
||||
this.tax.name = val.name
|
||||
|
||||
this.updateTax()
|
||||
},
|
||||
updateTax () {
|
||||
if (this.tax.tax_type_id === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$emit('update', {
|
||||
'index': this.index,
|
||||
'item': {
|
||||
...this.tax,
|
||||
amount: this.taxAmount
|
||||
}
|
||||
})
|
||||
},
|
||||
removeTax () {
|
||||
this.$emit('remove', this.index, this.tax)
|
||||
},
|
||||
openTaxModal () {
|
||||
this.openModal({
|
||||
'title': 'Add Tax',
|
||||
'componentName': 'TaxTypeModal'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
255
resources/assets/js/views/estimates/View.vue
Normal file
255
resources/assets/js/views/estimates/View.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<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">
|
||||
<base-button
|
||||
:loading="isRequestOnGoing"
|
||||
:disabled="isRequestOnGoing"
|
||||
:outline="true"
|
||||
color="theme"
|
||||
@click="onMarkAsSent"
|
||||
>
|
||||
{{ $t('estimates.mark_as_sent') }}
|
||||
</base-button>
|
||||
</div>
|
||||
<v-dropdown :close-on-select="false" 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>
|
||||
<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
|
||||
v-model="searchData.searchText"
|
||||
:placeholder="$t('general.search')"
|
||||
input-class="inv-search"
|
||||
icon="search"
|
||||
type="text"
|
||||
align-icon="right"
|
||||
@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-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 class="inv-button inv-filter-sorting-btn" color="default" size="medium" @click="sortData">
|
||||
<font-awesome-icon v-if="getOrderBy" icon="sort-amount-up" />
|
||||
<font-awesome-icon v-else icon="sort-amount-down" />
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-content">
|
||||
<router-link
|
||||
v-for="(estimate,index) in estimates"
|
||||
:to="`/admin/estimates/${estimate.id}/view`"
|
||||
:key="index"
|
||||
class="side-estimate"
|
||||
>
|
||||
<div class="left">
|
||||
<div class="inv-name">{{ estimate.user.name }}</div>
|
||||
<div class="inv-number">{{ estimate.estimate_number }}</div>
|
||||
<div :class="'est-status-'+estimate.status.toLowerCase()" class="inv-status">{{ estimate.status }}</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="inv-amount" v-html="$utils.formatMoney(estimate.total, estimate.user.currency)" />
|
||||
<div class="inv-date">{{ estimate.formattedEstimateDate }}</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<p v-if="!estimates.length" class="no-result">
|
||||
{{ $t('estimates.no_matching_estimates') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="estimate-view-page-container">
|
||||
<iframe :src="`${shareableLink}`" class="frame-style"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
const _ = require('lodash')
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
id: null,
|
||||
count: null,
|
||||
estimates: [],
|
||||
estimate: null,
|
||||
currency: null,
|
||||
shareableLink: null,
|
||||
searchData: {
|
||||
orderBy: null,
|
||||
orderByField: null,
|
||||
searchText: null
|
||||
},
|
||||
isRequestOnGoing: false,
|
||||
isSearching: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getOrderBy () {
|
||||
if (this.searchData.orderBy === 'asc' || this.searchData.orderBy == null) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.params.id' (val) {
|
||||
this.fetchEstimate()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.loadEstimates()
|
||||
this.onSearched = _.debounce(this.onSearched, 500)
|
||||
},
|
||||
methods: {
|
||||
...mapActions('estimate', [
|
||||
'fetchEstimates',
|
||||
'fetchViewEstimate',
|
||||
'getRecord',
|
||||
'searchEstimate',
|
||||
'markAsSent',
|
||||
'deleteEstimate',
|
||||
'selectEstimate'
|
||||
]),
|
||||
async loadEstimates () {
|
||||
let response = await this.fetchEstimates()
|
||||
if (response.data) {
|
||||
this.estimates = response.data.estimates.data
|
||||
}
|
||||
this.fetchEstimate()
|
||||
},
|
||||
async onSearched () {
|
||||
let data = ''
|
||||
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) {
|
||||
data += `orderBy=${this.searchData.orderBy}&`
|
||||
}
|
||||
|
||||
if (this.searchData.orderByField !== null && this.searchData.orderByField !== undefined) {
|
||||
data += `orderByField=${this.searchData.orderByField}`
|
||||
}
|
||||
this.isSearching = true
|
||||
let response = await this.searchEstimate(data)
|
||||
this.isSearching = false
|
||||
if (response.data) {
|
||||
this.estimates = response.data.estimates.data
|
||||
}
|
||||
},
|
||||
async fetchEstimate () {
|
||||
let estimate = await this.fetchViewEstimate(this.$route.params.id)
|
||||
|
||||
if (estimate.data) {
|
||||
this.estimate = estimate.data.estimate
|
||||
this.shareableLink = estimate.data.shareable_link
|
||||
this.currency = estimate.data.estimate.user.currency
|
||||
}
|
||||
},
|
||||
sortData () {
|
||||
if (this.searchData.orderBy === 'asc') {
|
||||
this.searchData.orderBy = 'desc'
|
||||
this.onSearched()
|
||||
return true
|
||||
}
|
||||
this.searchData.orderBy = 'asc'
|
||||
this.onSearched()
|
||||
return true
|
||||
},
|
||||
async onMarkAsSent () {
|
||||
this.isRequestOnGoing = true
|
||||
let response = await this.markAsSent({id: this.estimate.id})
|
||||
this.isRequestOnGoing = false
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('estimates.mark_as_sent'))
|
||||
}
|
||||
},
|
||||
async removeEstimate (id) {
|
||||
this.selectEstimate([parseInt(id)])
|
||||
this.id = id
|
||||
swal({
|
||||
title: 'Deleted',
|
||||
text: 'you will not be able to recover this estimate!',
|
||||
icon: 'error',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let request = await this.deleteEstimate(this.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>
|
||||
351
resources/assets/js/views/expenses/Create.vue
Normal file
351
resources/assets/js/views/expenses/Create.vue
Normal file
@@ -0,0 +1,351 @@
|
||||
<template>
|
||||
<div class="main-content expenses">
|
||||
<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
|
||||
:loading="isLoading"
|
||||
icon="save"
|
||||
color="theme"
|
||||
type="submit"
|
||||
>
|
||||
{{ isEdit ? $t('expenses.update_expense') : $t('expenses.save_expense') }}
|
||||
</base-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()"
|
||||
>
|
||||
<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
|
||||
v-model="amount"
|
||||
v-bind="defaultCurrencyForInput"
|
||||
class="input-field"
|
||||
@input="$v.formData.amount.$touch()"
|
||||
/>
|
||||
</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.amount_maxlength') }}</span>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</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')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MultiSelect
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
props: {
|
||||
addname: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
formData: {
|
||||
expense_category_id: null,
|
||||
expense_date: new Date(),
|
||||
amount: null,
|
||||
notes: ''
|
||||
},
|
||||
money: {
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
prefix: '$ ',
|
||||
precision: 2,
|
||||
masked: false
|
||||
},
|
||||
isReceiptAvailable: false,
|
||||
isLoading: false,
|
||||
file: null,
|
||||
category: null,
|
||||
passData: [],
|
||||
contacts: [],
|
||||
previewReceipt: null,
|
||||
fileSendUrl: '/api/expenses'
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
category: {
|
||||
required
|
||||
},
|
||||
formData: {
|
||||
expense_date: {
|
||||
required
|
||||
},
|
||||
amount: {
|
||||
required,
|
||||
maxLength: maxLength(10),
|
||||
minValue: minValue(0.1)
|
||||
},
|
||||
notes: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrencyForInput'
|
||||
]),
|
||||
amount: {
|
||||
get: function () {
|
||||
return this.formData.amount / 100
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.formData.amount = newValue * 100
|
||||
}
|
||||
},
|
||||
isEdit () {
|
||||
if (this.$route.name === 'expenses.edit') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
...mapGetters('category', [
|
||||
'categories'
|
||||
]),
|
||||
...mapGetters('company', [
|
||||
'getSelectedCompany'
|
||||
]),
|
||||
getReceiptUrl () {
|
||||
if (this.isEdit) {
|
||||
return `/expenses/${this.$route.params.id}/receipt/${this.getSelectedCompany.unique_hash}`
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
category (newValue) {
|
||||
this.formData.expense_category_id = newValue.id
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// this.$refs.baseSelect.$refs.search.focus()
|
||||
this.fetchInitialData()
|
||||
if (this.isEdit) {
|
||||
this.getReceipt()
|
||||
}
|
||||
window.hub.$on('newCategory', (val) => {
|
||||
this.category = val
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions('expense', [
|
||||
'fetchCreateExpense',
|
||||
'getFile',
|
||||
'sendFileWithData',
|
||||
'addExpense',
|
||||
'updateExpense',
|
||||
'fetchExpense'
|
||||
]),
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
...mapActions('category', [
|
||||
'fetchCategories'
|
||||
]),
|
||||
openCategoryModal () {
|
||||
this.openModal({
|
||||
'title': '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])
|
||||
}
|
||||
},
|
||||
async getReceipt () {
|
||||
let res = await axios.get(`/api/expenses/${this.$route.params.id}/show/receipt`)
|
||||
|
||||
if (res.data.error) {
|
||||
this.isReceiptAvailable = false
|
||||
return true
|
||||
}
|
||||
|
||||
this.isReceiptAvailable = true
|
||||
this.previewReceipt = res.data.image
|
||||
},
|
||||
async fetchInitialData () {
|
||||
this.fetchCategories()
|
||||
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}`
|
||||
}
|
||||
},
|
||||
async sendData () {
|
||||
this.$v.category.$touch()
|
||||
this.$v.formData.$touch()
|
||||
if (this.$v.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = new FormData()
|
||||
|
||||
if (this.file) {
|
||||
data.append('attachment_receipt', this.file)
|
||||
}
|
||||
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('notes', this.formData.notes)
|
||||
|
||||
if (this.isEdit) {
|
||||
this.isLoading = true
|
||||
data.append('_method', 'PUT')
|
||||
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
|
||||
this.$router.push('/admin/expenses')
|
||||
return true
|
||||
}
|
||||
window.toastr['error'](response.data.error)
|
||||
} else {
|
||||
this.isLoading = true
|
||||
let response = await this.addExpense(data)
|
||||
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>
|
||||
398
resources/assets/js/views/expenses/Index.vue
Normal file
398
resources/assets/js/views/expenses/Index.vue
Normal file
@@ -0,0 +1,398 @@
|
||||
<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>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-show="showFilters" class="filter-section">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<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-4">
|
||||
<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-4">
|
||||
<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>
|
||||
|
||||
<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"
|
||||
>
|
||||
<label v-show="!isRequestOngoing" for="select-all" class="custom-control-label selectall">
|
||||
<span class="select-all-label">{{ $t('general.select_all') }} </span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<table-component
|
||||
ref="table"
|
||||
:show-filter="false"
|
||||
:data="fetchData"
|
||||
table-class="table"
|
||||
>
|
||||
|
||||
<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.date')"
|
||||
sort-as="expense_date"
|
||||
show="formattedExpenseDate"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('expenses.note')"
|
||||
sort-as="expense_date"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<div class="notes">
|
||||
<div class="note">{{ row.notes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
:label="$t('expenses.amount')"
|
||||
sort-as="amount"
|
||||
show="category.amount"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('expenses.amount') }}</span>
|
||||
<div v-html="$utils.formatMoney(row.amount, defaultCurrency)" />
|
||||
</template>
|
||||
</table-column>
|
||||
<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>
|
||||
</template>
|
||||
</table-column>
|
||||
</table-component>
|
||||
</div>
|
||||
</div>
|
||||
</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'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MultiSelect,
|
||||
'observatory-icon': ObservatoryIcon,
|
||||
'SweetModal': SweetModal,
|
||||
'SweetModalTab': SweetModalTab
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showFilters: false,
|
||||
filtersApplied: false,
|
||||
isRequestOngoing: true,
|
||||
filters: {
|
||||
category: null,
|
||||
from_date: '',
|
||||
to_date: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showEmptyScreen () {
|
||||
return !this.totalExpenses && !this.isRequestOngoing && !this.filtersApplied
|
||||
},
|
||||
filterIcon () {
|
||||
return (this.showFilters) ? 'times' : 'filter'
|
||||
},
|
||||
...mapGetters('category', [
|
||||
'categories'
|
||||
]),
|
||||
...mapGetters('expense', [
|
||||
'selectedExpenses',
|
||||
'totalExpenses',
|
||||
'expenses',
|
||||
'selectAllField'
|
||||
]),
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrency'
|
||||
]),
|
||||
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
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
if (this.selectAllField) {
|
||||
this.selectAllExpenses()
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchCategories()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('expense', [
|
||||
'fetchExpenses',
|
||||
'selectExpense',
|
||||
'deleteExpense',
|
||||
'deleteMultipleExpenses',
|
||||
'selectAllExpenses',
|
||||
'setSelectAllState'
|
||||
]),
|
||||
...mapActions('category', [
|
||||
'fetchCategories'
|
||||
]),
|
||||
async fetchData ({ page, filter, sort }) {
|
||||
let data = {
|
||||
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'),
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page
|
||||
}
|
||||
|
||||
this.isRequestOngoing = true
|
||||
let response = await this.fetchExpenses(data)
|
||||
this.isRequestOngoing = false
|
||||
|
||||
return {
|
||||
data: response.data.expenses.data,
|
||||
pagination: {
|
||||
totalPages: response.data.expenses.last_page,
|
||||
currentPage: page,
|
||||
count: response.data.expenses.count
|
||||
}
|
||||
}
|
||||
},
|
||||
refreshTable () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
setFilters () {
|
||||
this.filtersApplied = true
|
||||
this.refreshTable()
|
||||
},
|
||||
clearFilter () {
|
||||
this.filters = {
|
||||
category: null,
|
||||
from_date: '',
|
||||
to_date: ''
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.filtersApplied = false
|
||||
})
|
||||
},
|
||||
toggleFilter () {
|
||||
if (this.showFilters && this.filtersApplied) {
|
||||
this.clearFilter()
|
||||
this.refreshTable()
|
||||
}
|
||||
|
||||
this.showFilters = !this.showFilters
|
||||
},
|
||||
async removeExpense (id) {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('expenses.confirm_delete'),
|
||||
icon: 'error',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteExpense(id)
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$tc('expenses.deleted_message', 1))
|
||||
this.$refs.table.refresh()
|
||||
return true
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async removeMultipleExpenses () {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('expenses.confirm_delete', 2),
|
||||
icon: 'error',
|
||||
buttons: 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()
|
||||
} else if (request.data.error) {
|
||||
window.toastr['error'](request.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
711
resources/assets/js/views/invoices/Create.vue
Normal file
711
resources/assets/js/views/invoices/Create.vue
Normal file
@@ -0,0 +1,711 @@
|
||||
<template>
|
||||
<div class="invoice-create-page main-content">
|
||||
<form v-if="!initLoading" action="" @submit.prevent="submitInvoiceData">
|
||||
<div class="page-header">
|
||||
<h3 v-if="$route.name === 'invoices.edit'" class="page-title">{{ $t('invoices.edit_invoice') }}</h3>
|
||||
<h3 v-else class="page-title">{{ $t('invoices.new_invoice') }} </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/invoices">{{ $tc('invoices.invoice', 2) }}</router-link></li>
|
||||
<li v-if="$route.name === 'invoices.edit'" class="breadcrumb-item">{{ $t('invoices.edit_invoice') }}</li>
|
||||
<li v-else class="breadcrumb-item">{{ $t('invoices.new_invoice') }}</li>
|
||||
</ol>
|
||||
<div class="page-actions row">
|
||||
<a v-if="$route.name === 'invoices.edit'" :href="`/invoices/pdf/${newInvoice.unique_hash}`" target="_blank" class="mr-3 base-button btn btn-outline-primary default-size" outline color="theme">
|
||||
{{ $t('general.view_pdf') }}
|
||||
</a>
|
||||
<base-button
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
icon="save"
|
||||
color="theme"
|
||||
type="submit">
|
||||
{{ $t('invoices.save_invoice') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row invoice-input-group">
|
||||
<div class="col-md-5">
|
||||
<div
|
||||
v-if="selectedCustomer" class="show-customer">
|
||||
<div class="row px-2 mt-1">
|
||||
<div class="col col-6">
|
||||
<div v-if="selectedCustomer.billing_address" class="row address-menu">
|
||||
<label class="col-sm-4 px-2 title">{{ $t('general.bill_to') }}</label>
|
||||
<div class="col-sm p-0 px-2 content">
|
||||
<label v-if="selectedCustomer.billing_address.name">
|
||||
{{ selectedCustomer.billing_address.name }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.billing_address.address_street_1">
|
||||
{{ selectedCustomer.billing_address.address_street_1 }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.billing_address.address_street_2">
|
||||
{{ selectedCustomer.billing_address.address_street_2 }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.billing_address.city && selectedCustomer.billing_address.state">
|
||||
{{ selectedCustomer.billing_address.city.name }}, {{ selectedCustomer.billing_address.state.name }} {{ selectedCustomer.billing_address.zip }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.billing_address.country">
|
||||
{{ selectedCustomer.billing_address.country.name }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.billing_address.phone">
|
||||
{{ selectedCustomer.billing_address.phone }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-6">
|
||||
<div v-if="selectedCustomer.shipping_address" class="row address-menu">
|
||||
<label class="col-sm-4 px-2 title">{{ $t('general.ship_to') }}</label>
|
||||
<div class="col-sm p-0 px-2 content">
|
||||
<label v-if="selectedCustomer.shipping_address.name">
|
||||
{{ selectedCustomer.shipping_address.name }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.shipping_address.address_street_1">
|
||||
{{ selectedCustomer.shipping_address.address_street_1 }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.shipping_address.address_street_2">
|
||||
{{ selectedCustomer.shipping_address.address_street_2 }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.shipping_address.city && selectedCustomer.shipping_address">
|
||||
{{ selectedCustomer.shipping_address.city.name }}, {{ selectedCustomer.shipping_address.state.name }} {{ selectedCustomer.shipping_address.zip }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.shipping_address.country" class="country">
|
||||
{{ selectedCustomer.shipping_address.country.name }}
|
||||
</label>
|
||||
<label v-if="selectedCustomer.shipping_address.phone" class="phone">
|
||||
{{ selectedCustomer.shipping_address.phone }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="customer-content mb-1">
|
||||
<label class="email">{{ selectedCustomer.name }}</label>
|
||||
<label class="action" @click="removeCustomer">{{ $t('general.remove') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<base-popup v-else :class="['add-customer', {'customer-required': $v.selectedCustomer.$error}]" >
|
||||
<div slot="activator" class="add-customer-action">
|
||||
<font-awesome-icon icon="user" class="customer-icon"/>
|
||||
<div>
|
||||
<label>{{ $t('customers.new_customer') }} <span class="text-danger"> * </span></label>
|
||||
<p v-if="$v.selectedCustomer.$error && !$v.selectedCustomer.required" class="text-danger">
|
||||
{{ $t('validation.required') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<customer-select-popup type="invoice" />
|
||||
</base-popup>
|
||||
</div>
|
||||
<div class="col invoice-input">
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<label>{{ $tc('invoices.invoice',1) }} {{ $t('invoices.date') }}<span class="text-danger"> * </span></label>
|
||||
<base-date-picker
|
||||
v-model="newInvoice.invoice_date"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@change="$v.newInvoice.invoice_date.$touch()"
|
||||
/>
|
||||
<span v-if="$v.newInvoice.invoice_date.$error && !$v.newInvoice.invoice_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label>{{ $t('invoices.due_date') }}<span class="text-danger"> * </span></label>
|
||||
<base-date-picker
|
||||
v-model="newInvoice.due_date"
|
||||
:invalid="$v.newInvoice.due_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@change="$v.newInvoice.due_date.$touch()"
|
||||
/>
|
||||
<span v-if="$v.newInvoice.due_date.$error && !$v.newInvoice.due_date.required" class="text-danger mt-1"> {{ $t('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4">
|
||||
<div class="col">
|
||||
<label>{{ $t('invoices.invoice_number') }}<span class="text-danger"> * </span></label>
|
||||
<base-input
|
||||
:invalid="$v.newInvoice.invoice_number.$error"
|
||||
:read-only="true"
|
||||
v-model="newInvoice.invoice_number"
|
||||
icon="hashtag"
|
||||
@input="$v.newInvoice.invoice_number.$touch()"
|
||||
/>
|
||||
<span v-show="$v.newInvoice.invoice_number.$error && !$v.newInvoice.invoice_number.required" class="text-danger mt-1"> {{ $tc('validation.required') }} </span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label>{{ $t('invoices.ref_number') }}</label>
|
||||
<base-input
|
||||
v-model="newInvoice.reference_number"
|
||||
:invalid="$v.newInvoice.reference_number.$error"
|
||||
icon="hashtag"
|
||||
type="number"
|
||||
@input="$v.newInvoice.reference_number.$touch()"
|
||||
/>
|
||||
<div v-if="$v.newInvoice.reference_number.$error" class="text-danger">{{ $tc('validation.ref_number_maxlength') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="item-table">
|
||||
<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%;">
|
||||
</colgroup>
|
||||
<thead class="item-table-header">
|
||||
<tr>
|
||||
<th class="text-left">
|
||||
<span class="column-heading item-heading">
|
||||
{{ $tc('items.item',2) }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span class="column-heading">
|
||||
{{ $t('invoices.item.quantity') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="text-left">
|
||||
<span class="column-heading">
|
||||
{{ $t('invoices.item.price') }}
|
||||
</span>
|
||||
</th>
|
||||
<th v-if="discountPerItem === 'YES'" class="text-right">
|
||||
<span class="column-heading">
|
||||
{{ $t('invoices.item.discount') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span class="column-heading amount-heading">
|
||||
{{ $t('invoices.item.amount') }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<draggable v-model="newInvoice.items" class="item-body" tag="tbody" handle=".handle">
|
||||
<invoice-item
|
||||
v-for="(item, index) in newInvoice.items"
|
||||
:key="item.id"
|
||||
:index="index"
|
||||
:item-data="item"
|
||||
:currency="currency"
|
||||
:tax-per-item="taxPerItem"
|
||||
:discount-per-item="discountPerItem"
|
||||
@remove="removeItem"
|
||||
@update="updateItem"
|
||||
@itemValidate="checkItemsData"
|
||||
/>
|
||||
</draggable>
|
||||
</table>
|
||||
<div class="add-item-action" @click="addItem">
|
||||
<font-awesome-icon icon="shopping-basket" class="mr-2"/>
|
||||
{{ $t('invoices.add_item') }}
|
||||
</div>
|
||||
|
||||
<div class="invoice-foot">
|
||||
<div>
|
||||
<label>{{ $t('invoices.notes') }}</label>
|
||||
<base-text-area
|
||||
v-model="newInvoice.notes"
|
||||
rows="3"
|
||||
cols="50"
|
||||
@input="$v.newInvoice.notes.$touch()"
|
||||
/>
|
||||
<div v-if="$v.newInvoice.notes.$error">
|
||||
<span v-if="!$v.newInvoice.notes.maxLength" class="text-danger">{{ $t('validation.notes_maxlength') }}</span>
|
||||
</div>
|
||||
<label class="mt-3 mb-1 d-block">{{ $t('invoices.invoice_template') }} <span class="text-danger"> * </span></label>
|
||||
<base-button type="button" class="btn-template" icon="pencil-alt" right-icon @click="openTemplateModal" >
|
||||
<span class="mr-4"> {{ $t('invoices.template') }} {{ getTemplateId }} </span>
|
||||
</base-button>
|
||||
</div>
|
||||
|
||||
<div class="invoice-total">
|
||||
<div class="section">
|
||||
<label class="invoice-label">{{ $t('invoices.sub_total') }}</label>
|
||||
<label class="invoice-amount">
|
||||
<div v-html="$utils.formatMoney(subtotal, currency)" />
|
||||
</label>
|
||||
</div>
|
||||
<div v-for="tax in allTaxes" :key="tax.tax_type_id" class="section">
|
||||
<label class="invoice-label">{{ tax.name }} - {{ tax.percent }}% </label>
|
||||
<label class="invoice-amount">
|
||||
<div v-html="$utils.formatMoney(tax.amount, currency)" />
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="discountPerItem === 'NO' || discountPerItem === null" class="section mt-2">
|
||||
<label class="invoice-label">{{ $t('invoices.discount') }}</label>
|
||||
<div
|
||||
class="btn-group discount-drop-down"
|
||||
role="group"
|
||||
>
|
||||
<base-input
|
||||
v-model="discount"
|
||||
:invalid="$v.newInvoice.discount_val.$error"
|
||||
input-class="item-discount"
|
||||
@input="$v.newInvoice.discount_val.$touch()"
|
||||
/>
|
||||
<v-dropdown :show-arrow="false">
|
||||
<button
|
||||
slot="activator"
|
||||
type="button"
|
||||
class="btn item-dropdown dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{{ newInvoice.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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="taxPerItem === 'NO' || taxPerItem === null">
|
||||
<tax
|
||||
v-for="(tax, index) in newInvoice.taxes"
|
||||
:index="index"
|
||||
:total="subtotalWithDiscount"
|
||||
:key="tax.id"
|
||||
:tax="tax"
|
||||
:taxes="newInvoice.taxes"
|
||||
:currency="currency"
|
||||
:total-tax="totalSimpleTax"
|
||||
@remove="removeInvoiceTax"
|
||||
@update="updateTax"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<base-popup v-if="taxPerItem === 'NO' || taxPerItem === null" ref="taxModal" class="tax-selector">
|
||||
<div slot="activator" class="float-right">
|
||||
+ {{ $t('invoices.add_tax') }}
|
||||
</div>
|
||||
<tax-select-popup :taxes="newInvoice.taxes" @select="onSelectTax"/>
|
||||
</base-popup>
|
||||
|
||||
<div class="section border-top mt-3">
|
||||
<label class="invoice-label">{{ $t('invoices.total') }} {{ $t('invoices.amount') }}:</label>
|
||||
<label class="invoice-amount total">
|
||||
<div v-html="$utils.formatMoney(total, currency)" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<base-loader v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from 'vuedraggable'
|
||||
import MultiSelect from 'vue-multiselect'
|
||||
import InvoiceItem from './Item'
|
||||
import InvoiceStub from '../../stub/invoice'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import moment from 'moment'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import Guid from 'guid'
|
||||
import TaxStub from '../../stub/tax'
|
||||
import Tax from './InvoiceTax'
|
||||
const { required, between, maxLength } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InvoiceItem,
|
||||
MultiSelect,
|
||||
Tax,
|
||||
draggable
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
newInvoice: {
|
||||
invoice_date: null,
|
||||
due_date: null,
|
||||
invoice_number: null,
|
||||
user_id: null,
|
||||
invoice_template_id: 1,
|
||||
sub_total: null,
|
||||
total: null,
|
||||
tax: null,
|
||||
notes: null,
|
||||
discount_type: 'fixed',
|
||||
discount_val: 0,
|
||||
discount: 0,
|
||||
reference_number: null,
|
||||
items: [{
|
||||
...InvoiceStub,
|
||||
id: Guid.raw(),
|
||||
taxes: [{...TaxStub, id: Guid.raw()}]
|
||||
}],
|
||||
taxes: []
|
||||
},
|
||||
customers: [],
|
||||
itemList: [],
|
||||
invoiceTemplates: [],
|
||||
selectedCurrency: '',
|
||||
taxPerItem: null,
|
||||
discountPerItem: null,
|
||||
initLoading: false,
|
||||
isLoading: false,
|
||||
maxDiscount: 0
|
||||
}
|
||||
},
|
||||
validations () {
|
||||
return {
|
||||
newInvoice: {
|
||||
invoice_date: {
|
||||
required
|
||||
},
|
||||
due_date: {
|
||||
required
|
||||
},
|
||||
invoice_number: {
|
||||
required
|
||||
},
|
||||
discount_val: {
|
||||
between: between(0, this.subtotal)
|
||||
},
|
||||
notes: {
|
||||
maxLength: maxLength(255)
|
||||
},
|
||||
reference_number: {
|
||||
maxLength: maxLength(10)
|
||||
}
|
||||
},
|
||||
selectedCustomer: {
|
||||
required
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('general', [
|
||||
'itemDiscount'
|
||||
]),
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrency'
|
||||
]),
|
||||
...mapGetters('invoice', [
|
||||
'getTemplateId',
|
||||
'selectedCustomer'
|
||||
]),
|
||||
currency () {
|
||||
return this.selectedCurrency
|
||||
},
|
||||
subtotalWithDiscount () {
|
||||
return this.subtotal - this.newInvoice.discount_val
|
||||
},
|
||||
total () {
|
||||
return this.subtotalWithDiscount + this.totalTax
|
||||
},
|
||||
subtotal () {
|
||||
return this.newInvoice.items.reduce(function (a, b) {
|
||||
return a + b['total']
|
||||
}, 0)
|
||||
},
|
||||
discount: {
|
||||
get: function () {
|
||||
return this.newInvoice.discount
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (this.newInvoice.discount_type === 'percentage') {
|
||||
this.newInvoice.discount_val = (this.subtotal * newValue) / 100
|
||||
} else {
|
||||
this.newInvoice.discount_val = newValue * 100
|
||||
}
|
||||
|
||||
this.newInvoice.discount = newValue
|
||||
}
|
||||
},
|
||||
totalSimpleTax () {
|
||||
return window._.sumBy(this.newInvoice.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
},
|
||||
|
||||
totalCompoundTax () {
|
||||
return window._.sumBy(this.newInvoice.taxes, function (tax) {
|
||||
if (tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
},
|
||||
totalTax () {
|
||||
if (this.taxPerItem === 'NO' || this.taxPerItem === null) {
|
||||
return this.totalSimpleTax + this.totalCompoundTax
|
||||
}
|
||||
|
||||
return window._.sumBy(this.newInvoice.items, function (tax) {
|
||||
return tax.tax
|
||||
})
|
||||
},
|
||||
allTaxes () {
|
||||
let taxes = []
|
||||
|
||||
this.newInvoice.items.forEach((item) => {
|
||||
item.taxes.forEach((tax) => {
|
||||
let found = taxes.find((_tax) => {
|
||||
return _tax.tax_type_id === tax.tax_type_id
|
||||
})
|
||||
|
||||
if (found) {
|
||||
found.amount += tax.amount
|
||||
} else if (tax.tax_type_id) {
|
||||
taxes.push({
|
||||
tax_type_id: tax.tax_type_id,
|
||||
amount: tax.amount,
|
||||
percent: tax.percent,
|
||||
name: tax.name
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return taxes
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedCustomer (newVal) {
|
||||
if (newVal && newVal.currency) {
|
||||
this.selectedCurrency = newVal.currency
|
||||
} else {
|
||||
this.selectedCurrency = this.defaultCurrency
|
||||
}
|
||||
},
|
||||
subtotal (newValue) {
|
||||
if (this.newInvoice.discount_type === 'percentage') {
|
||||
this.newInvoice.discount_val = (this.newInvoice.discount * newValue) / 100
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.loadData()
|
||||
this.fetchInitialItems()
|
||||
this.resetSelectedCustomer()
|
||||
window.hub.$on('newTax', this.onSelectTax)
|
||||
},
|
||||
methods: {
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
...mapActions('invoice', [
|
||||
'addInvoice',
|
||||
'fetchCreateInvoice',
|
||||
'fetchInvoice',
|
||||
'resetSelectedCustomer',
|
||||
'selectCustomer',
|
||||
'updateInvoice'
|
||||
]),
|
||||
...mapActions('item', [
|
||||
'fetchItems'
|
||||
]),
|
||||
isEmpty (obj) {
|
||||
for (let key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
selectFixed () {
|
||||
if (this.newInvoice.discount_type === 'fixed') {
|
||||
return
|
||||
}
|
||||
|
||||
this.newInvoice.discount_val = this.newInvoice.discount * 100
|
||||
this.newInvoice.discount_type = 'fixed'
|
||||
},
|
||||
selectPercentage () {
|
||||
if (this.newInvoice.discount_type === 'percentage') {
|
||||
return
|
||||
}
|
||||
|
||||
this.newInvoice.discount_val = (this.subtotal * this.newInvoice.discount) / 100
|
||||
|
||||
this.newInvoice.discount_type = 'percentage'
|
||||
},
|
||||
updateTax (data) {
|
||||
Object.assign(this.newInvoice.taxes[data.index], {...data.item})
|
||||
},
|
||||
async fetchInitialItems () {
|
||||
await this.fetchItems({
|
||||
filter: {},
|
||||
orderByField: '',
|
||||
orderBy: ''
|
||||
})
|
||||
},
|
||||
async loadData () {
|
||||
if (this.$route.name === 'invoices.edit') {
|
||||
this.initLoading = true
|
||||
let response = await this.fetchInvoice(this.$route.params.id)
|
||||
|
||||
if (response.data) {
|
||||
this.selectCustomer(response.data.invoice.user_id)
|
||||
this.newInvoice = response.data.invoice
|
||||
this.discountPerItem = response.data.discount_per_item
|
||||
this.taxPerItem = response.data.tax_per_item
|
||||
this.selectedCurrency = this.defaultCurrency
|
||||
this.invoiceTemplates = response.data.invoiceTemplates
|
||||
}
|
||||
this.initLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
this.initLoading = true
|
||||
let response = await this.fetchCreateInvoice()
|
||||
if (response.data) {
|
||||
this.discountPerItem = response.data.discount_per_item
|
||||
this.taxPerItem = response.data.tax_per_item
|
||||
this.selectedCurrency = this.defaultCurrency
|
||||
this.invoiceTemplates = response.data.invoiceTemplates
|
||||
let today = new Date()
|
||||
this.newInvoice.invoice_date = moment(today).toString()
|
||||
this.newInvoice.due_date = moment(today).add(7, 'days').toString()
|
||||
this.newInvoice.invoice_number = response.data.nextInvoiceNumber
|
||||
this.itemList = response.data.items
|
||||
}
|
||||
this.initLoading = false
|
||||
},
|
||||
removeCustomer () {
|
||||
this.resetSelectedCustomer()
|
||||
},
|
||||
openTemplateModal () {
|
||||
this.openModal({
|
||||
'title': 'Choose a template',
|
||||
'componentName': 'InvoiceTemplate',
|
||||
'data': this.invoiceTemplates
|
||||
})
|
||||
},
|
||||
addItem () {
|
||||
this.newInvoice.items.push({...InvoiceStub, id: Guid.raw(), taxes: [{...TaxStub, id: Guid.raw()}]})
|
||||
},
|
||||
removeItem (index) {
|
||||
this.newInvoice.items.splice(index, 1)
|
||||
},
|
||||
updateItem (data) {
|
||||
Object.assign(this.newInvoice.items[data.index], {...data.item})
|
||||
},
|
||||
submitInvoiceData () {
|
||||
if (!this.checkValid()) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.isLoading = true
|
||||
|
||||
let data = {
|
||||
...this.newInvoice,
|
||||
invoice_date: moment(this.newInvoice.invoice_date).format('DD/MM/YYYY'),
|
||||
due_date: moment(this.newInvoice.due_date).format('DD/MM/YYYY'),
|
||||
sub_total: this.subtotal,
|
||||
total: this.total,
|
||||
tax: this.totalTax,
|
||||
user_id: null,
|
||||
invoice_template_id: this.getTemplateId
|
||||
}
|
||||
|
||||
if (this.selectedCustomer != null) {
|
||||
data.user_id = this.selectedCustomer.id
|
||||
}
|
||||
|
||||
if (this.$route.name === 'invoices.edit') {
|
||||
this.submitUpdate(data)
|
||||
return
|
||||
}
|
||||
|
||||
this.submitSave(data)
|
||||
},
|
||||
submitSave (data) {
|
||||
this.addInvoice(data).then((res) => {
|
||||
if (res.data) {
|
||||
window.toastr['success'](this.$t('invoices.created_message'))
|
||||
this.$router.push('/admin/invoices')
|
||||
}
|
||||
|
||||
this.isLoading = false
|
||||
}).catch((err) => {
|
||||
this.isLoading = false
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
submitUpdate (data) {
|
||||
this.updateInvoice(data).then((res) => {
|
||||
this.isLoading = false
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$t('invoices.updated_message'))
|
||||
this.$router.push('/admin/invoices')
|
||||
}
|
||||
|
||||
if (res.data.error === 'invalid_due_amount') {
|
||||
window.toastr['error'](this.$t('invoices.invalid_due_amount_message'))
|
||||
}
|
||||
}).catch((err) => {
|
||||
this.isLoading = false
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
checkItemsData (index, isValid) {
|
||||
this.newInvoice.items[index].valid = isValid
|
||||
},
|
||||
onSelectTax (selectedTax) {
|
||||
let amount = 0
|
||||
|
||||
if (selectedTax.compound_tax && this.subtotalWithDiscount) {
|
||||
amount = ((this.subtotalWithDiscount + this.totalSimpleTax) * selectedTax.percent) / 100
|
||||
} else if (this.subtotalWithDiscount && selectedTax.percent) {
|
||||
amount = (this.subtotalWithDiscount * selectedTax.percent) / 100
|
||||
}
|
||||
|
||||
this.newInvoice.taxes.push({
|
||||
...TaxStub,
|
||||
id: Guid.raw(),
|
||||
name: selectedTax.name,
|
||||
percent: selectedTax.percent,
|
||||
compound_tax: selectedTax.compound_tax,
|
||||
tax_type_id: selectedTax.id,
|
||||
amount
|
||||
})
|
||||
|
||||
this.$refs.taxModal.close()
|
||||
},
|
||||
removeInvoiceTax (index) {
|
||||
this.newInvoice.taxes.splice(index, 1)
|
||||
},
|
||||
checkValid () {
|
||||
this.$v.newInvoice.$touch()
|
||||
this.$v.selectedCustomer.$touch()
|
||||
|
||||
window.hub.$emit('checkItems')
|
||||
let isValid = true
|
||||
this.newInvoice.items.forEach((item) => {
|
||||
if (!item.valid) {
|
||||
isValid = false
|
||||
}
|
||||
})
|
||||
if (!this.$v.selectedCustomer.$invalid && this.$v.newInvoice.$invalid === false && isValid === true) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
589
resources/assets/js/views/invoices/Edit.vue
Normal file
589
resources/assets/js/views/invoices/Edit.vue
Normal file
@@ -0,0 +1,589 @@
|
||||
<template>
|
||||
<div class="invoice-create-page main-content">
|
||||
<form action="" @submit.prevent="submitInvoiceData">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ $t('invoices.new_invoice') }}</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/invoices">{{ $tc('invoices.invoice', 2) }}</router-link></li>
|
||||
<li class="breadcrumb-item">{{ $t('invoices.new_invoice') }}</li>
|
||||
</ol>
|
||||
<div class="page-actions row">
|
||||
<base-button class="mr-3" outline color="theme">
|
||||
{{ $t('general.download_pdf') }}
|
||||
</base-button>
|
||||
<base-button
|
||||
:loading="isLoading"
|
||||
icon="save"
|
||||
color="theme"
|
||||
type="submit">
|
||||
{{ $t('invoices.save_invoice') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row invoice-input-group">
|
||||
<div class="col-md-5">
|
||||
<div
|
||||
v-if="selectedCustomer"
|
||||
class="show-customer"
|
||||
>
|
||||
<div class="row p-2">
|
||||
<div class="col col-6">
|
||||
<div v-if="selectedCustomer.billing_address != null" class="row address-menu">
|
||||
<label class="col-sm-4 px-2 title">{{ $t('general.bill_to') }}</label>
|
||||
<div class="col-sm p-0 px-2 content">
|
||||
<label>{{ selectedCustomer.billing_address.name }}</label>
|
||||
<label>{{ selectedCustomer.billing_address.address_street_1 }}</label>
|
||||
<label>{{ selectedCustomer.billing_address.address_street_2 }}</label>
|
||||
<label>
|
||||
{{ selectedCustomer.billing_address.city.name }}, {{ selectedCustomer.billing_address.state.name }} {{ selectedCustomer.billing_address.zip }}
|
||||
</label>
|
||||
<label>{{ selectedCustomer.billing_address.country.name }}</label>
|
||||
<label>{{ selectedCustomer.billing_address.phone }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-6">
|
||||
<div v-if="selectedCustomer.shipping_address != null" class="row address-menu">
|
||||
<label class="col-sm-4 px-2 title">{{ $t('general.ship_to') }}</label>
|
||||
<div class="col-sm p-0 px-2 content">
|
||||
<label>{{ selectedCustomer.shipping_address.name }}</label>
|
||||
<label>{{ selectedCustomer.shipping_address.address_street_1 }}</label>
|
||||
<label>{{ selectedCustomer.shipping_address.address_street_2 }}</label>
|
||||
<label v-show="selectedCustomer.shipping_address.city">
|
||||
{{ selectedCustomer.shipping_address.city.name }}, {{ selectedCustomer.shipping_address.state.name }} {{ selectedCustomer.shipping_address.zip }}
|
||||
</label>
|
||||
<label class="country">{{ selectedCustomer.shipping_address.country.name }}</label>
|
||||
<label class="phone">{{ selectedCustomer.shipping_address.phone }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="customer-content">
|
||||
<label class="email">{{ selectedCustomer.email ? selectedCustomer.email : selectedCustomer.name }}</label>
|
||||
<label class="action" @click="removeCustomer">{{ $t('general.remove') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<base-popup v-else class="add-customer">
|
||||
<div slot="activator" class="add-customer-action">
|
||||
<font-awesome-icon icon="user" class="customer-icon"/>
|
||||
<label>{{ $t('customers.new_customer') }}<span class="text-danger"> * </span></label>
|
||||
</div>
|
||||
<customer-select />
|
||||
</base-popup>
|
||||
</div>
|
||||
<div class="col invoice-input">
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<label>{{ $t('invoices.invoice_date') }}<span class="text-danger"> * </span></label>
|
||||
<base-date-picker
|
||||
v-model="newInvoice.invoice_date"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@change="$v.newInvoice.invoice_date.$touch()"
|
||||
/>
|
||||
<span v-if="$v.newInvoice.invoice_date.$error && !$v.newInvoice.invoice_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label>{{ $t('invoices.due_date') }}<span class="text-danger"> * </span></label>
|
||||
<base-date-picker
|
||||
v-model="newInvoice.due_date"
|
||||
:invalid="$v.newInvoice.due_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@change="$v.newInvoice.due_date.$touch()"
|
||||
/>
|
||||
<span v-if="$v.newInvoice.due_date.$error && !$v.newInvoice.due_date.required" class="text-danger mt-1"> {{ $t('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4">
|
||||
<div class="col">
|
||||
<label>{{ $t('invoices.invoice_number') }}<span class="text-danger"> * </span></label>
|
||||
<base-input
|
||||
:invalid="$v.newInvoice.invoice_number.$error"
|
||||
:read-only="true"
|
||||
v-model="newInvoice.invoice_number"
|
||||
icon="hashtag"
|
||||
@input="$v.newInvoice.invoice_number.$touch()"
|
||||
/>
|
||||
<span v-show="$v.newInvoice.invoice_number.$error && !$v.newInvoice.invoice_number.required" class="text-danger mt-1"> {{ $tc('validation.required') }} </span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label>{{ $t('invoices.ref_number') }}</label>
|
||||
<base-input icon="hashtag" type="number"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="item-table">
|
||||
<colgroup>
|
||||
<col style="width: 50%;">
|
||||
<col style="width: 10%;">
|
||||
<col style="width: 10%;">
|
||||
<col v-if="discountPerItem === 'YES'" style="width: 15%;">
|
||||
<col style="width: 15%;">
|
||||
</colgroup>
|
||||
<thead class="item-table-header">
|
||||
<tr>
|
||||
<th class="text-left">
|
||||
<span class="column-heading item-heading">
|
||||
{{ $tc('items.item',2) }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span class="column-heading">
|
||||
{{ $t('invoices.item.quantity') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="text-left">
|
||||
<span class="column-heading">
|
||||
{{ $t('invoices.item.price') }}
|
||||
</span>
|
||||
</th>
|
||||
<th v-if="discountPerItem === 'YES'" class="text-right">
|
||||
<span class="column-heading">
|
||||
{{ $t('invoices.item.discount') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span class="column-heading amount-heading">
|
||||
{{ $t('invoices.item.amount') }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="item-body">
|
||||
<invoice-item
|
||||
v-for="(item, index) in newInvoice.items"
|
||||
:key="'inv-item-' + item.id"
|
||||
:index="index"
|
||||
:item-data="item"
|
||||
:currency="currency"
|
||||
:tax-per-item="taxPerItem"
|
||||
:discount-per-item="discountPerItem"
|
||||
@remove="removeItem"
|
||||
@update="updateItem"
|
||||
@itemValidate="checkItemsData"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="add-item-action" @click="addItem">
|
||||
<font-awesome-icon icon="shopping-basket" class="mr-2"/>
|
||||
{{ $t('invoices.add_item') }}
|
||||
</div>
|
||||
|
||||
<div class="invoice-foot">
|
||||
<div>
|
||||
<label>{{ $t('invoices.notes') }}</label>
|
||||
<base-text-area
|
||||
v-model="newInvoice.notes"
|
||||
rows="3"
|
||||
cols="50"
|
||||
/>
|
||||
<label class="mt-3 mb-1 d-block">{{ $t('invoices.invoice_template') }} <span class="text-danger"> * </span></label>
|
||||
<base-button class="btn-template" icon="pencil-alt" right-icon @click="openTemplateModal" >
|
||||
<span class="mr-4"> {{ $t('invoices.invoice_template') }} {{ getTemplateId }} </span>
|
||||
</base-button>
|
||||
</div>
|
||||
|
||||
<div class="invoice-total">
|
||||
<div class="section">
|
||||
<label class="invoice-label">{{ $t('invoices.sub_total') }}</label>
|
||||
<label class="invoice-amount">
|
||||
<div v-html="$utils.formatMoney(subtotal, currency)" />
|
||||
</label>
|
||||
</div>
|
||||
<div v-for="tax in allTaxes" :key="tax.tax_type_id" class="section">
|
||||
<label class="invoice-label">{{ tax.name }} - {{ tax.percent }}% </label>
|
||||
<label class="invoice-amount">
|
||||
<div v-html="$utils.formatMoney(tax.amount, currency)" />
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="discountPerItem === 'NO' || discountPerItem === null" class="section mt-2">
|
||||
<label class="invoice-label">{{ $t('invoices.discount') }}</label>
|
||||
<div
|
||||
class="btn-group discount-drop-down"
|
||||
role="group"
|
||||
>
|
||||
<base-input
|
||||
v-model="discount"
|
||||
input-class="item-discount"
|
||||
/>
|
||||
<v-dropdown :show-arrow="false">
|
||||
<button
|
||||
slot="activator"
|
||||
type="button"
|
||||
class="btn item-dropdown dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{{ newInvoice.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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="taxPerItem === 'NO' || taxPerItem === null">
|
||||
<tax
|
||||
v-for="(tax, index) in newInvoice.taxes"
|
||||
:index="index"
|
||||
:total="subtotalWithDiscount"
|
||||
:key="tax.taxKey"
|
||||
:tax="tax"
|
||||
:taxes="newInvoice.taxes"
|
||||
:currency="currency"
|
||||
:total-tax="totalSimpleTax"
|
||||
@remove="removeInvoiceTax"
|
||||
@update="updateTax"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<base-popup v-if="taxPerItem === 'NO' || taxPerItem === null" ref="taxModal" class="tax-selector">
|
||||
<div slot="activator" class="float-right">
|
||||
+ {{ $t('invoices.add_tax') }}
|
||||
</div>
|
||||
<tax-select @select="onSelectTax"/>
|
||||
</base-popup>
|
||||
|
||||
<div class="section border-top mt-3">
|
||||
<label class="invoice-label">{{ $t('invoices.total') }} {{ $t('invoices.amount') }}:</label>
|
||||
<label class="invoice-amount total">
|
||||
<div v-html="$utils.formatMoney(total, currency)" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import MultiSelect from 'vue-multiselect'
|
||||
import InvoiceItem from './Item'
|
||||
import InvoiceStub from '../../stub/invoice'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import moment from 'moment'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import Guid from 'guid'
|
||||
import TaxStub from '../../stub/tax'
|
||||
import Tax from './InvoiceTax'
|
||||
const { required } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InvoiceItem,
|
||||
MultiSelect,
|
||||
Tax
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
newInvoice: {
|
||||
invoice_date: null,
|
||||
due_date: null,
|
||||
invoice_number: null,
|
||||
user_id: null,
|
||||
invoice_template_id: 1,
|
||||
sub_total: null,
|
||||
total: null,
|
||||
tax: null,
|
||||
notes: null,
|
||||
discount_type: 'fixed',
|
||||
discount_val: 0,
|
||||
discount: 0,
|
||||
items: [{
|
||||
...InvoiceStub,
|
||||
id: 1
|
||||
}],
|
||||
taxes: []
|
||||
},
|
||||
invoiceTemplates: [],
|
||||
selectedCurrency: '',
|
||||
newItem: {
|
||||
...InvoiceStub
|
||||
},
|
||||
taxPerItem: null,
|
||||
discountPerItem: null,
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
newInvoice: {
|
||||
invoice_date: {
|
||||
required
|
||||
},
|
||||
due_date: {
|
||||
required
|
||||
},
|
||||
invoice_number: {
|
||||
required
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrency'
|
||||
]),
|
||||
currency () {
|
||||
return this.selectedCurrency
|
||||
},
|
||||
subtotalWithDiscount () {
|
||||
return this.subtotal - this.newInvoice.discount_val
|
||||
},
|
||||
total () {
|
||||
return this.subtotalWithDiscount + this.totalTax
|
||||
},
|
||||
subtotal () {
|
||||
return this.newInvoice.items.reduce(function (a, b) {
|
||||
return a + b['total']
|
||||
}, 0)
|
||||
},
|
||||
discount: {
|
||||
get: function () {
|
||||
return this.newInvoice.discount
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (this.newInvoice.discount_type === 'percentage') {
|
||||
this.newInvoice.discount_val = (this.subtotal * newValue) / 100
|
||||
} else {
|
||||
this.newInvoice.discount_val = newValue * 100
|
||||
}
|
||||
|
||||
this.newInvoice.discount = newValue
|
||||
}
|
||||
},
|
||||
totalSimpleTax () {
|
||||
return window._.sumBy(this.newInvoice.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
},
|
||||
|
||||
totalCompoundTax () {
|
||||
return window._.sumBy(this.newInvoice.taxes, function (tax) {
|
||||
if (tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
},
|
||||
totalTax () {
|
||||
if (this.taxPerItem === 'NO' || this.taxPerItem === null) {
|
||||
return this.totalSimpleTax + this.totalCompoundTax
|
||||
}
|
||||
|
||||
return window._.sumBy(this.newInvoice.items, function (tax) {
|
||||
return tax.totalTax
|
||||
})
|
||||
},
|
||||
allTaxes () {
|
||||
let taxes = []
|
||||
|
||||
this.newInvoice.items.forEach((item) => {
|
||||
item.taxes.forEach((tax) => {
|
||||
let found = taxes.find((_tax) => {
|
||||
return _tax.tax_type_id === tax.tax_type_id
|
||||
})
|
||||
|
||||
if (found) {
|
||||
found.amount += tax.amount
|
||||
} else if (tax.tax_type_id) {
|
||||
taxes.push({
|
||||
tax_type_id: tax.tax_type_id,
|
||||
amount: tax.amount,
|
||||
percent: tax.percent,
|
||||
name: tax.name
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return taxes
|
||||
},
|
||||
...mapGetters('customer', [
|
||||
'selectedCustomer'
|
||||
]),
|
||||
...mapGetters('invoice', [
|
||||
'getTemplateId'
|
||||
]),
|
||||
...mapGetters('general', [
|
||||
'itemDiscount'
|
||||
])
|
||||
},
|
||||
watch: {
|
||||
selectedCustomer (newVal) {
|
||||
|
||||
if (newVal.currency !== null) {
|
||||
this.selectedCurrency = newVal.currency
|
||||
} else {
|
||||
this.selectedCurrency = this.defaultCurrency
|
||||
}
|
||||
},
|
||||
subtotal (newValue) {
|
||||
if (this.newInvoice.discount_type === 'percentage') {
|
||||
this.newInvoice.discount_val = (this.newInvoice.discount * newValue) / 100
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
this.loadData()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
...mapActions('customer', [
|
||||
'resetSelectedCustomer'
|
||||
]),
|
||||
...mapActions('taxType', {
|
||||
loadTaxTypes: 'indexLoadData'
|
||||
}),
|
||||
...mapActions('invoice', [
|
||||
'addInvoice',
|
||||
'fetchInvoice'
|
||||
]),
|
||||
selectFixed () {
|
||||
if (this.newInvoice.discount_type === 'fixed') {
|
||||
return
|
||||
}
|
||||
|
||||
this.newInvoice.discount_val = this.newInvoice.discount * 100
|
||||
this.newInvoice.discount_type = 'fixed'
|
||||
},
|
||||
selectPercentage () {
|
||||
if (this.newInvoice.discount_type === 'percentage') {
|
||||
return
|
||||
}
|
||||
|
||||
this.newInvoice.discount_val = (this.subtotal * this.newInvoice.discount) / 100
|
||||
|
||||
this.newInvoice.discount_type = 'percentage'
|
||||
},
|
||||
updateTax (data) {
|
||||
Object.assign(this.newInvoice.taxes[data.index], {...data.item})
|
||||
},
|
||||
async loadData () {
|
||||
let response = await this.fetchInvoice(this.$route.params.id)
|
||||
|
||||
this.loadTaxTypes()
|
||||
|
||||
if (response.data) {
|
||||
this.newInvoice = response.data.invoice
|
||||
this.discountPerItem = response.data.discount_per_item
|
||||
this.taxPerItem = response.data.tax_per_item
|
||||
this.selectedCurrency = this.defaultCurrency
|
||||
this.invoiceTemplates = response.data.invoiceTemplates
|
||||
}
|
||||
},
|
||||
removeCustomer () {
|
||||
this.resetSelectedCustomer()
|
||||
},
|
||||
openTemplateModal () {
|
||||
this.openModal({
|
||||
'title': 'Choose a template',
|
||||
'componentName': 'InvoiceTemplate',
|
||||
'data': this.invoiceTemplates
|
||||
})
|
||||
},
|
||||
addItem () {
|
||||
this.newInvoice.items.push({...this.newItem, id: (this.newInvoice.items.length + 1)})
|
||||
},
|
||||
removeItem (index) {
|
||||
this.newInvoice.items.splice(index, 1)
|
||||
},
|
||||
updateItem (data) {
|
||||
Object.assign(this.newInvoice.items[data.index], {...data.item})
|
||||
},
|
||||
async submitInvoiceData () {
|
||||
if (!this.checkValid()) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.isLoading = true
|
||||
|
||||
let data = {
|
||||
...this.newInvoice,
|
||||
invoice_date: moment(this.newInvoice.invoice_date).format('DD/MM/YYYY'),
|
||||
due_date: moment(this.newInvoice.due_date).format('DD/MM/YYYY'),
|
||||
sub_total: this.subtotal,
|
||||
total: this.total,
|
||||
tax: this.totalTax,
|
||||
user_id: null,
|
||||
invoice_template_id: this.getTemplateId
|
||||
}
|
||||
|
||||
if (this.selectedCustomer != null) {
|
||||
data.user_id = this.selectedCustomer.id
|
||||
}
|
||||
let response = await this.addInvoice(data)
|
||||
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$t('invoices.created_message'))
|
||||
this.isLoading = false
|
||||
this.$route.push('/admin/invoices')
|
||||
}
|
||||
},
|
||||
checkItemsData (index, isValid) {
|
||||
this.newInvoice.items[index].valid = isValid
|
||||
},
|
||||
onSelectTax (selectedTax) {
|
||||
let amount = 0
|
||||
|
||||
if (selectedTax.compound_tax && this.subtotalWithDiscount) {
|
||||
amount = ((this.subtotalWithDiscount + this.totalSimpleTax) * selectedTax.percent) / 100
|
||||
} else if (this.subtotalWithDiscount && selectedTax.percent) {
|
||||
amount = (this.subtotalWithDiscount * selectedTax.percent) / 100
|
||||
}
|
||||
|
||||
this.newInvoice.taxes.push({
|
||||
...TaxStub,
|
||||
taxKey: Guid.raw(),
|
||||
name: selectedTax.name,
|
||||
percent: selectedTax.percent,
|
||||
compound_tax: selectedTax.compound_tax,
|
||||
tax_type_id: selectedTax.id,
|
||||
amount
|
||||
})
|
||||
|
||||
this.$refs.taxModal.close()
|
||||
},
|
||||
removeInvoiceTax (index) {
|
||||
this.newInvoice.taxes.splice(index, 1)
|
||||
},
|
||||
checkValid () {
|
||||
this.$v.newInvoice.$touch()
|
||||
window.hub.$emit('checkItems')
|
||||
let isValid = true
|
||||
this.newInvoice.items.forEach((item) => {
|
||||
if (!item.valid) {
|
||||
isValid = false
|
||||
}
|
||||
})
|
||||
if (this.$v.newInvoice.$invalid === false && isValid === true) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
540
resources/assets/js/views/invoices/Index.vue
Normal file
540
resources/assets/js/views/invoices/Index.vue
Normal file
@@ -0,0 +1,540 @@
|
||||
<template>
|
||||
<div class="invoice-index-page invoices main-content">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title"> {{ $t('invoices.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('invoices.invoice', 2) }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="page-actions row">
|
||||
<div class="col-xs-2 mr-4">
|
||||
<base-button
|
||||
v-show="totalInvoices || 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="/admin/invoices/create">
|
||||
<base-button size="large" icon="plus" color="theme">
|
||||
{{ $t('invoices.new_invoice') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-show="showFilters" class="filter-section">
|
||||
<div class="filter-container">
|
||||
<div class="filter-customer">
|
||||
<label>{{ $tc('customers.customer',1) }} </label>
|
||||
<base-customer-select
|
||||
ref="customerSelect"
|
||||
@select="onSelectCustomer"
|
||||
@deselect="clearCustomerSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-status">
|
||||
<label>{{ $t('invoices.status') }}</label>
|
||||
<base-select
|
||||
v-model="filters.status"
|
||||
:options="status"
|
||||
:group-select="false"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('general.select_a_status')"
|
||||
group-values="options"
|
||||
group-label="label"
|
||||
track-by="name"
|
||||
label="name"
|
||||
@remove="clearStatusSearch()"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-date">
|
||||
<div class="from pr-3">
|
||||
<label>{{ $t('general.from') }}</label>
|
||||
<base-date-picker
|
||||
v-model="filters.from_date"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
/>
|
||||
</div>
|
||||
<div class="dashed" />
|
||||
<div class="to pl-3">
|
||||
<label>{{ $t('general.to') }}</label>
|
||||
<base-date-picker
|
||||
v-model="filters.to_date"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-invoice">
|
||||
<label>{{ $t('invoices.invoice_number') }}</label>
|
||||
<base-input
|
||||
v-model="filters.invoice_number"
|
||||
icon="hashtag"/>
|
||||
</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">
|
||||
<moon-walker-icon class="mt-5 mb-4"/>
|
||||
<div class="row" align="center">
|
||||
<label class="col title">{{ $t('invoices.no_invoices') }}</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="description col mt-1" align="center">{{ $t('invoices.list_of_invoices') }}</label>
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<base-button
|
||||
:outline="true"
|
||||
color="theme"
|
||||
class="mt-3"
|
||||
size="large"
|
||||
@click="$router.push('invoices/create')"
|
||||
>
|
||||
{{ $t('invoices.new_invoice') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="!showEmptyScreen" class="table-container">
|
||||
<div class="table-actions mt-5">
|
||||
<p class="table-stats">{{ $t('general.showing') }}: <b>{{ invoices.length }}</b> {{ $t('general.of') }} <b>{{ totalInvoices }}</b></p>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="tabs">
|
||||
<li class="tab" @click="getStatus('UNPAID')">
|
||||
<a :class="['tab-link', {'a-active': filters.status.value === 'UNPAID'}]" href="#" >{{ $t('general.due') }}</a>
|
||||
</li>
|
||||
<li class="tab" @click="getStatus('DRAFT')">
|
||||
<a :class="['tab-link', {'a-active': filters.status.value === 'DRAFT'}]" href="#">{{ $t('general.draft') }}</a>
|
||||
</li>
|
||||
<li class="tab" @click="getStatus('')">
|
||||
<a :class="['tab-link', {'a-active': filters.status.value === '' || filters.status.value === null || filters.status.value !== 'DRAFT' && filters.status.value !== 'UNPAID'}]" href="#">{{ $t('general.all') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<transition name="fade">
|
||||
<v-dropdown v-if="selectedInvoices.length" :show-arrow="false">
|
||||
<span slot="activator" href="#" class="table-actions-button dropdown-toggle">
|
||||
{{ $t('general.actions') }}
|
||||
</span>
|
||||
<v-dropdown-item>
|
||||
<div class="dropdown-item" @click="removeMultipleInvoices">
|
||||
<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="selectAllInvoices"
|
||||
>
|
||||
<label v-show="!isRequestOngoing" for="select-all" class="custom-control-label selectall">
|
||||
<span class="select-all-label">{{ $t('general.select_all') }} </span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<table-component
|
||||
ref="table"
|
||||
:show-filter="false"
|
||||
:data="fetchData"
|
||||
table-class="table"
|
||||
>
|
||||
<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="$t('invoices.date')"
|
||||
sort-as="invoice_date"
|
||||
show="formattedInvoiceDate"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('invoices.customer')"
|
||||
width="20%"
|
||||
show="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('invoices.paid_status')"
|
||||
sort-as="paid_status"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('invoices.paid_status') }}</span>
|
||||
<span :class="'inv-status-'+row.paid_status.toLowerCase()">{{ (row.paid_status != 'PARTIALLY_PAID')? row.paid_status : row.paid_status.replace('_', ' ') }}</span>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
:label="$t('invoices.number')"
|
||||
show="invoice_number"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('invoices.amount_due')"
|
||||
sort-as="due_amount"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('invoices.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 no-click"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('invoices.action') }}</span>
|
||||
<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>
|
||||
<a class="dropdown-item" href="#" @click="sendInvoice(row.id)" >
|
||||
<font-awesome-icon icon="paper-plane" 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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import MoonWalkerIcon from '../../../js/components/icon/MoonwalkerIcon'
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'moon-walker-icon': MoonWalkerIcon
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showFilters: false,
|
||||
currency: null,
|
||||
status: [
|
||||
{
|
||||
label: 'Status',
|
||||
isDisable: true,
|
||||
options: [
|
||||
{ name: 'DRAFT', value: 'DRAFT' },
|
||||
{ name: 'DUE', value: 'UNPAID' },
|
||||
{ name: 'SENT', value: 'SENT' },
|
||||
{ name: 'VIEWED', value: 'VIEWED' },
|
||||
{ name: 'OVERDUE', value: 'OVERDUE' },
|
||||
{ name: 'COMPLETED', value: 'COMPLETED' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Paid Status',
|
||||
options: [
|
||||
{ name: 'UNPAID', value: 'UNPAID' },
|
||||
{ name: 'PAID', value: 'PAID' },
|
||||
{ name: 'PARTIALLY PAID', value: 'PARTIALLY_PAID' }
|
||||
]
|
||||
}
|
||||
],
|
||||
filtersApplied: false,
|
||||
isRequestOngoing: true,
|
||||
filters: {
|
||||
customer: '',
|
||||
status: { name: 'DUE', value: 'UNPAID' },
|
||||
from_date: '',
|
||||
to_date: '',
|
||||
invoice_number: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
showEmptyScreen () {
|
||||
return !this.totalInvoices && !this.isRequestOngoing && !this.filtersApplied
|
||||
},
|
||||
filterIcon () {
|
||||
return (this.showFilters) ? 'times' : 'filter'
|
||||
},
|
||||
...mapGetters('customer', [
|
||||
'customers'
|
||||
]),
|
||||
...mapGetters('invoice', [
|
||||
'selectedInvoices',
|
||||
'totalInvoices',
|
||||
'invoices',
|
||||
'selectAllField'
|
||||
]),
|
||||
selectField: {
|
||||
get: function () {
|
||||
return this.selectedInvoices
|
||||
},
|
||||
set: function (val) {
|
||||
this.selectInvoice(val)
|
||||
}
|
||||
},
|
||||
selectAllFieldStatus: {
|
||||
get: function () {
|
||||
return this.selectAllField
|
||||
},
|
||||
set: function (val) {
|
||||
this.setSelectAllState(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
filters: {
|
||||
handler: 'setFilters',
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchCustomers()
|
||||
},
|
||||
destroyed () {
|
||||
if (this.selectAllField) {
|
||||
this.selectAllInvoices()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('invoice', [
|
||||
'fetchInvoices',
|
||||
'getRecord',
|
||||
'selectInvoice',
|
||||
'resetSelectedInvoices',
|
||||
'selectAllInvoices',
|
||||
'deleteInvoice',
|
||||
'deleteMultipleInvoices',
|
||||
'sendEmail',
|
||||
'markAsSent',
|
||||
'setSelectAllState'
|
||||
]),
|
||||
...mapActions('customer', [
|
||||
'fetchCustomers'
|
||||
]),
|
||||
async sendInvoice (id) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.sendEmail(data)
|
||||
this.refreshTable()
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('invoices.send_invoice'))
|
||||
}
|
||||
},
|
||||
async sentInvoice (id) {
|
||||
const data = {
|
||||
id: id
|
||||
}
|
||||
let response = await this.markAsSent(data)
|
||||
this.refreshTable()
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('invoices.mark_as_sent'))
|
||||
}
|
||||
},
|
||||
getStatus (val) {
|
||||
this.filters.status = {
|
||||
name: val,
|
||||
value: val
|
||||
}
|
||||
},
|
||||
refreshTable () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
async fetchData ({ page, filter, sort }) {
|
||||
let data = {
|
||||
customer_id: this.filters.customer === '' ? this.filters.customer : this.filters.customer.id,
|
||||
status: this.filters.status.value,
|
||||
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'),
|
||||
invoice_number: this.filters.invoice_number,
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page
|
||||
}
|
||||
|
||||
this.isRequestOngoing = true
|
||||
let response = await this.fetchInvoices(data)
|
||||
this.isRequestOngoing = false
|
||||
|
||||
this.currency = response.data.currency
|
||||
|
||||
return {
|
||||
data: response.data.invoices.data,
|
||||
pagination: {
|
||||
totalPages: response.data.invoices.last_page,
|
||||
currentPage: page,
|
||||
count: response.data.invoices.count
|
||||
}
|
||||
}
|
||||
},
|
||||
setFilters () {
|
||||
this.filtersApplied = true
|
||||
this.resetSelectedInvoices()
|
||||
this.refreshTable()
|
||||
},
|
||||
clearFilter () {
|
||||
if (this.filters.customer) {
|
||||
this.$refs.customerSelect.$refs.baseSelect.removeElement(this.filters.customer)
|
||||
}
|
||||
this.filters = {
|
||||
customer: '',
|
||||
status: '',
|
||||
from_date: '',
|
||||
to_date: '',
|
||||
invoice_number: ''
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.filtersApplied = false
|
||||
})
|
||||
},
|
||||
toggleFilter () {
|
||||
if (this.showFilters && this.filtersApplied) {
|
||||
this.clearFilter()
|
||||
this.refreshTable()
|
||||
}
|
||||
|
||||
this.showFilters = !this.showFilters
|
||||
},
|
||||
onSelectCustomer (customer) {
|
||||
this.filters.customer = customer
|
||||
},
|
||||
async removeInvoice (id) {
|
||||
this.id = id
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('invoices.confirm_delete'),
|
||||
icon: 'error',
|
||||
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'))
|
||||
return true
|
||||
}
|
||||
|
||||
if (res.data.error === 'payment_attached') {
|
||||
window.toastr['error'](this.$t('invoices.payment_attached_message'), this.$t('general.action_failed'))
|
||||
return true
|
||||
}
|
||||
|
||||
window.toastr['error'](res.data.error)
|
||||
return true
|
||||
}
|
||||
|
||||
this.$refs.table.refresh()
|
||||
this.filtersApplied = false
|
||||
this.resetSelectedInvoices()
|
||||
})
|
||||
},
|
||||
async removeMultipleInvoices () {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('invoices.confirm_delete', 2),
|
||||
icon: 'error',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteMultipleInvoices()
|
||||
if (res.data.error === 'payment_attached') {
|
||||
window.toastr['error'](this.$t('invoices.payment_attached_message'), this.$t('general.action_failed'))
|
||||
return true
|
||||
}
|
||||
if (res.data) {
|
||||
this.$refs.table.refresh()
|
||||
this.filtersApplied = false
|
||||
this.resetSelectedInvoices()
|
||||
window.toastr['success'](this.$tc('invoices.deleted_message', 2))
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async clearCustomerSearch (removedOption, id) {
|
||||
this.filters.customer = ''
|
||||
this.refreshTable()
|
||||
},
|
||||
async clearStatusSearch (removedOption, id) {
|
||||
this.filters.status = ''
|
||||
this.refreshTable()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
83
resources/assets/js/views/invoices/InvoiceTax.vue
Normal file
83
resources/assets/js/views/invoices/InvoiceTax.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="section mt-2">
|
||||
<label class="invoice-label">
|
||||
{{ tax.name }} ({{ tax.percent }}%)
|
||||
</label>
|
||||
<label class="invoice-amount">
|
||||
<div v-html="$utils.formatMoney(tax.amount, currency)" />
|
||||
|
||||
<font-awesome-icon
|
||||
class="ml-2"
|
||||
icon="trash-alt"
|
||||
@click="$emit('remove', index)"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
tax: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
totalTax: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
taxAmount () {
|
||||
if (this.tax.compound_tax && this.total) {
|
||||
return ((this.total + this.totalTax) * this.tax.percent) / 100
|
||||
}
|
||||
|
||||
if (this.total && this.tax.percent) {
|
||||
return (this.total * this.tax.percent) / 100
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
total: {
|
||||
handler: 'updateTax'
|
||||
},
|
||||
totalTax: {
|
||||
handler: 'updateTax'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateTax () {
|
||||
this.$emit('update', {
|
||||
'index': this.index,
|
||||
'item': {
|
||||
...this.tax,
|
||||
amount: this.taxAmount
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
403
resources/assets/js/views/invoices/Item.vue
Normal file
403
resources/assets/js/views/invoices/Item.vue
Normal file
@@ -0,0 +1,403 @@
|
||||
<template>
|
||||
<tr class="item-row invoice-item-row">
|
||||
<td colspan="5">
|
||||
<table class="full-width">
|
||||
<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%;">
|
||||
</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"
|
||||
/>
|
||||
</div>
|
||||
<item-select
|
||||
ref="itemSelect"
|
||||
:invalid="$v.item.name.$error"
|
||||
:invalid-description="$v.item.description.$error"
|
||||
:item="item"
|
||||
@search="searchVal"
|
||||
@select="onSelectItem"
|
||||
@deselect="deselectItem"
|
||||
@onDesriptionInput="$v.item.description.$touch()"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<base-input
|
||||
v-model="item.quantity"
|
||||
:invalid="$v.item.quantity.$error"
|
||||
type="number"
|
||||
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>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-left">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="flex-fillbd-highlight">
|
||||
<div class="base-input">
|
||||
<money
|
||||
v-model="price"
|
||||
v-bind="customerCurrency"
|
||||
class="input-field"
|
||||
@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>
|
||||
</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
|
||||
v-model="discount"
|
||||
:invalid="$v.item.discount_val.$error"
|
||||
input-class="item-discount"
|
||||
@input="$v.item.discount_val.$touch()"
|
||||
/>
|
||||
<v-dropdown :show-arrow="false" theme-light>
|
||||
<button
|
||||
slot="activator"
|
||||
type="button"
|
||||
class="btn item-dropdown dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{{ 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>
|
||||
</div>
|
||||
<!-- <div v-if="$v.item.discount.$error"> discount error </div> -->
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="item-amount">
|
||||
<span>
|
||||
<div v-html="$utils.formatMoney(total, currency)" />
|
||||
</span>
|
||||
|
||||
<div class="remove-icon-wrapper">
|
||||
<font-awesome-icon
|
||||
v-if="index > 0"
|
||||
class="remove-icon"
|
||||
icon="trash-alt"
|
||||
@click="removeItem"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="taxPerItem === 'YES'" class="tax-tr">
|
||||
<td />
|
||||
<td colspan="4">
|
||||
<tax
|
||||
v-for="(tax, index) in item.taxes"
|
||||
:key="tax.id"
|
||||
:index="index"
|
||||
:tax-data="tax"
|
||||
:taxes="item.taxes"
|
||||
:discounted-total="total"
|
||||
:total-tax="totalSimpleTax"
|
||||
:total="total"
|
||||
:currency="currency"
|
||||
@update="updateTax"
|
||||
@remove="removeTax"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</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')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tax,
|
||||
ItemSelect
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
props: {
|
||||
itemData: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
},
|
||||
taxPerItem: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
discountPerItem: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isClosePopup: false,
|
||||
itemSelect: null,
|
||||
item: {...this.itemData},
|
||||
maxDiscount: 0,
|
||||
money: {
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
prefix: '$ ',
|
||||
precision: 2,
|
||||
masked: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('item', [
|
||||
'items'
|
||||
]),
|
||||
...mapGetters('currency', [
|
||||
'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
|
||||
}
|
||||
} else {
|
||||
return this.defaultCurrenctForInput
|
||||
}
|
||||
},
|
||||
subtotal () {
|
||||
return this.item.price * this.item.quantity
|
||||
},
|
||||
discount: {
|
||||
get: function () {
|
||||
return this.item.discount
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (this.item.discount_type === 'percentage') {
|
||||
this.item.discount_val = (this.subtotal * newValue) / 100
|
||||
} else {
|
||||
this.item.discount_val = newValue * 100
|
||||
}
|
||||
|
||||
this.item.discount = newValue
|
||||
}
|
||||
},
|
||||
total () {
|
||||
return this.subtotal - this.item.discount_val
|
||||
},
|
||||
totalSimpleTax () {
|
||||
return window._.sumBy(this.item.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
},
|
||||
totalCompoundTax () {
|
||||
return window._.sumBy(this.item.taxes, function (tax) {
|
||||
if (tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
},
|
||||
totalTax () {
|
||||
return this.totalSimpleTax + this.totalCompoundTax
|
||||
},
|
||||
price: {
|
||||
get: function () {
|
||||
if (parseFloat(this.item.price) > 0) {
|
||||
return this.item.price / 100
|
||||
}
|
||||
|
||||
return this.item.price
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (parseFloat(newValue) > 0) {
|
||||
this.item.price = newValue * 100
|
||||
this.maxDiscount = this.item.price
|
||||
} else {
|
||||
this.item.price = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
item: {
|
||||
handler: 'updateItem',
|
||||
deep: true
|
||||
},
|
||||
subtotal (newValue) {
|
||||
if (this.item.discount_type === 'percentage') {
|
||||
this.item.discount_val = (this.item.discount * newValue) / 100
|
||||
}
|
||||
}
|
||||
},
|
||||
validations () {
|
||||
return {
|
||||
item: {
|
||||
name: {
|
||||
required
|
||||
},
|
||||
quantity: {
|
||||
required,
|
||||
minValue: minValue(1),
|
||||
maxLength: maxLength(10)
|
||||
},
|
||||
price: {
|
||||
required,
|
||||
minValue: minValue(1),
|
||||
maxLength: maxLength(10)
|
||||
},
|
||||
discount_val: {
|
||||
between: between(0, this.maxDiscount)
|
||||
},
|
||||
description: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.hub.$on('checkItems', this.validateItem)
|
||||
window.hub.$on('newItem', this.onSelectItem)
|
||||
},
|
||||
methods: {
|
||||
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.updateItem()
|
||||
},
|
||||
removeTax (index) {
|
||||
this.item.taxes.splice(index, 1)
|
||||
|
||||
this.updateItem()
|
||||
},
|
||||
taxWithPercentage ({ name, percent }) {
|
||||
return `${name} (${percent}%)`
|
||||
},
|
||||
searchVal (val) {
|
||||
this.item.name = val
|
||||
},
|
||||
deselectItem () {
|
||||
this.item = {...InvoiceStub, id: this.item.id}
|
||||
this.$nextTick(() => {
|
||||
this.$refs.itemSelect.$refs.baseSelect.$refs.search.focus()
|
||||
})
|
||||
},
|
||||
onSelectItem (item) {
|
||||
this.item.name = item.name
|
||||
this.item.price = item.price
|
||||
this.item.item_id = item.id
|
||||
this.item.description = item.description
|
||||
|
||||
// if (this.item.taxes.length) {
|
||||
// this.item.taxes = {...item.taxes}
|
||||
// }
|
||||
},
|
||||
selectFixed () {
|
||||
if (this.item.discount_type === 'fixed') {
|
||||
return
|
||||
}
|
||||
|
||||
this.item.discount_val = this.item.discount * 100
|
||||
this.item.discount_type = 'fixed'
|
||||
},
|
||||
selectPercentage () {
|
||||
if (this.item.discount_type === 'percentage') {
|
||||
return
|
||||
}
|
||||
|
||||
this.item.discount_val = (this.subtotal * this.item.discount) / 100
|
||||
|
||||
this.item.discount_type = 'percentage'
|
||||
},
|
||||
updateItem () {
|
||||
this.$emit('update', {
|
||||
'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]
|
||||
}
|
||||
})
|
||||
},
|
||||
removeItem () {
|
||||
this.$emit('remove', this.index)
|
||||
},
|
||||
validateItem () {
|
||||
this.$v.item.$touch()
|
||||
|
||||
if (this.item !== null) {
|
||||
this.$emit('itemValidate', this.index, !this.$v.$invalid)
|
||||
} else {
|
||||
this.$emit('itemValidate', this.index, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
129
resources/assets/js/views/invoices/ItemSelect.vue
Normal file
129
resources/assets/js/views/invoices/ItemSelect.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="item-selector">
|
||||
<div v-if="item.item_id" class="selected-item">
|
||||
{{ item.name }}
|
||||
|
||||
<span class="deselect-icon" @click="deselectItem">
|
||||
<font-awesome-icon icon="times-circle" />
|
||||
</span>
|
||||
</div>
|
||||
<base-select
|
||||
v-else
|
||||
ref="baseSelect"
|
||||
v-model="itemSelect"
|
||||
:options="items"
|
||||
:show-labels="false"
|
||||
:preserve-search="true"
|
||||
:initial-search="item.name"
|
||||
:invalid="invalid"
|
||||
:placeholder="$t('invoices.item.select_an_item')"
|
||||
label="name"
|
||||
class="multi-select-item"
|
||||
@value="onTextChange"
|
||||
@select="(val) => $emit('select', val)"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</base-select>
|
||||
<div class="item-description">
|
||||
<base-text-area
|
||||
v-autoresize
|
||||
v-model="item.description"
|
||||
:invalid-description="invalidDescription"
|
||||
:placeholder="$t('invoices.item.type_item_description')"
|
||||
type="text"
|
||||
rows="1"
|
||||
class="description-input"
|
||||
@input="$emit('onDesriptionInput')"
|
||||
/>
|
||||
<div v-if="invalidDescription">
|
||||
<span class="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'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
invalid: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
invalidDescription: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
itemSelect: null,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('item', [
|
||||
'items'
|
||||
])
|
||||
},
|
||||
watch: {
|
||||
invalidDescription (newValue) {
|
||||
console.log(newValue)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
...mapActions('item', [
|
||||
'fetchItems'
|
||||
]),
|
||||
async searchItems (search) {
|
||||
let data = {
|
||||
filter: {
|
||||
name: search,
|
||||
unit: '',
|
||||
price: ''
|
||||
},
|
||||
orderByField: '',
|
||||
orderBy: '',
|
||||
page: 1
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
await this.fetchItems(data)
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
onTextChange (val) {
|
||||
this.searchItems(val)
|
||||
|
||||
this.$emit('search', val)
|
||||
},
|
||||
openItemModal () {
|
||||
this.openModal({
|
||||
'title': 'Add Item',
|
||||
'componentName': 'ItemModal'
|
||||
})
|
||||
},
|
||||
deselectItem () {
|
||||
this.itemSelect = null
|
||||
this.$emit('deselect')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
165
resources/assets/js/views/invoices/Tax.vue
Normal file
165
resources/assets/js/views/invoices/Tax.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="tax-row">
|
||||
<div class="d-flex align-items-center tax-select">
|
||||
<label class="bd-highlight pr-2 mb-0" align="right">
|
||||
{{ $t('general.tax') }}
|
||||
</label>
|
||||
<base-select
|
||||
v-model="selectedTax"
|
||||
:options="filteredTypes"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
:custom-label="customLabel"
|
||||
:placeholder="$t('general.select_a_tax')"
|
||||
track-by="name"
|
||||
label="name"
|
||||
@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>
|
||||
</div>
|
||||
</base-select> <br>
|
||||
</div>
|
||||
<div class="text-right tax-amount" v-html="$utils.formatMoney(taxAmount, currency)" />
|
||||
<div class="remove-icon-wrapper">
|
||||
<font-awesome-icon
|
||||
v-if="taxes.length && index !== taxes.length - 1"
|
||||
class="remove-icon"
|
||||
icon="trash-alt"
|
||||
@click="removeTax"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
taxData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
totalTax: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
tax: {...this.taxData},
|
||||
selectedTax: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...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)
|
||||
|
||||
if (found) {
|
||||
taxType.$isDisabled = true
|
||||
} else {
|
||||
taxType.$isDisabled = false
|
||||
}
|
||||
|
||||
return taxType
|
||||
})
|
||||
},
|
||||
taxAmount () {
|
||||
if (this.tax.compound_tax && this.total) {
|
||||
return ((this.total + this.totalTax) * this.tax.percent) / 100
|
||||
}
|
||||
|
||||
if (this.total && this.tax.percent) {
|
||||
return (this.total * this.tax.percent) / 100
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
total: {
|
||||
handler: 'updateTax'
|
||||
},
|
||||
totalTax: {
|
||||
handler: 'updateTax'
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.taxData.tax_type_id > 0) {
|
||||
this.selectedTax = this.taxTypes.find(_type => _type.id === this.taxData.tax_type_id)
|
||||
}
|
||||
|
||||
this.updateTax()
|
||||
window.hub.$on('newTax', (val) => {
|
||||
if (!this.selectedTax) {
|
||||
this.selectedTax = val
|
||||
this.onSelectTax(val)
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
customLabel ({ name, percent }) {
|
||||
return `${name} - ${percent}%`
|
||||
},
|
||||
onSelectTax (val) {
|
||||
this.tax.percent = val.percent
|
||||
this.tax.tax_type_id = val.id
|
||||
this.tax.compound_tax = val.compound_tax
|
||||
this.tax.name = val.name
|
||||
|
||||
this.updateTax()
|
||||
},
|
||||
updateTax () {
|
||||
if (this.tax.tax_type_id === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$emit('update', {
|
||||
'index': this.index,
|
||||
'item': {
|
||||
...this.tax,
|
||||
amount: this.taxAmount
|
||||
}
|
||||
})
|
||||
},
|
||||
removeTax () {
|
||||
this.$emit('remove', this.index, this.tax)
|
||||
},
|
||||
openTaxModal () {
|
||||
this.openModal({
|
||||
'title': 'Add Tax',
|
||||
'componentName': 'TaxTypeModal'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
262
resources/assets/js/views/invoices/View.vue
Normal file
262
resources/assets/js/views/invoices/View.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<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
|
||||
:loading="isRequestOnGoing"
|
||||
:disabled="isRequestOnGoing"
|
||||
:outline="true"
|
||||
color="theme"
|
||||
@click="onMarkAsSent"
|
||||
>
|
||||
{{ $t('invoices.mark_as_sent') }}
|
||||
</base-button>
|
||||
</div>
|
||||
<router-link :to="`/admin/payments/${$route.params.id}/create`">
|
||||
<base-button
|
||||
color="theme"
|
||||
>
|
||||
{{ $t('payments.record_payment') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
<v-dropdown :close-on-select="false" 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>
|
||||
<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
|
||||
v-model="searchData.searchText"
|
||||
:placeholder="$t('general.search')"
|
||||
input-class="inv-search"
|
||||
icon="search"
|
||||
type="text"
|
||||
align-icon="right"
|
||||
@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-items">
|
||||
<input
|
||||
id="filter_invoice_date"
|
||||
v-model="searchData.orderByField"
|
||||
type="radio"
|
||||
name="filter"
|
||||
class="inv-radio"
|
||||
value="invoice_date"
|
||||
@change="onSearched"
|
||||
>
|
||||
<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="onSearched"
|
||||
>
|
||||
<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="onSearched"
|
||||
>
|
||||
<label class="inv-label" for="filter_invoice_number">{{ $t('invoices.invoice_number') }}</label>
|
||||
</div>
|
||||
</v-dropdown>
|
||||
<base-button class="inv-button inv-filter-sorting-btn" color="default" size="medium" @click="sortData">
|
||||
<font-awesome-icon v-if="getOrderBy" icon="sort-amount-up" />
|
||||
<font-awesome-icon v-else icon="sort-amount-down" />
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
<base-loader v-if="isSearching" />
|
||||
<div v-else class="side-content">
|
||||
<router-link
|
||||
v-for="(invoice,index) in invoices"
|
||||
:to="`/admin/invoices/${invoice.id}/view`"
|
||||
:key="index"
|
||||
class="side-invoice"
|
||||
>
|
||||
<div class="left">
|
||||
<div class="inv-name">{{ invoice.user.name }}</div>
|
||||
<div class="inv-number">{{ invoice.invoice_number }}</div>
|
||||
<div :class="'inv-status-'+invoice.status.toLowerCase()" class="inv-status">{{ invoice.status }}</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="inv-amount" v-html="$utils.formatMoney(invoice.due_amount, invoice.user.currency)" />
|
||||
<div class="inv-date">{{ invoice.formattedInvoiceDate }}</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<p v-if="!invoices.length" class="no-result">
|
||||
{{ $t('invoices.no_matching_invoices') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="invoice-view-page-container" >
|
||||
<iframe :src="`${shareableLink}`" class="frame-style"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
const _ = require('lodash')
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
id: null,
|
||||
count: null,
|
||||
invoices: [],
|
||||
invoice: null,
|
||||
currency: null,
|
||||
shareableLink: null,
|
||||
searchData: {
|
||||
orderBy: null,
|
||||
orderByField: null,
|
||||
searchText: null
|
||||
},
|
||||
isRequestOnGoing: false,
|
||||
isSearching: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getOrderBy () {
|
||||
if (this.searchData.orderBy === 'asc' || this.searchData.orderBy == null) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.params.id' (val) {
|
||||
this.fetchInvoice()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.loadInvoices()
|
||||
this.onSearched = _.debounce(this.onSearched, 500)
|
||||
},
|
||||
methods: {
|
||||
...mapActions('invoice', [
|
||||
'fetchInvoices',
|
||||
'fetchViewInvoice',
|
||||
'getRecord',
|
||||
'searchInvoice',
|
||||
'markAsSent',
|
||||
'deleteInvoice',
|
||||
'selectInvoice'
|
||||
]),
|
||||
async loadInvoices () {
|
||||
let response = await this.fetchInvoices()
|
||||
if (response.data) {
|
||||
this.invoices = response.data.invoices.data
|
||||
}
|
||||
this.fetchInvoice()
|
||||
},
|
||||
async onSearched () {
|
||||
let data = ''
|
||||
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) {
|
||||
data += `orderBy=${this.searchData.orderBy}&`
|
||||
}
|
||||
|
||||
if (this.searchData.orderByField !== null && this.searchData.orderByField !== undefined) {
|
||||
data += `orderByField=${this.searchData.orderByField}`
|
||||
}
|
||||
this.isSearching = true
|
||||
let response = await this.searchInvoice(data)
|
||||
this.isSearching = false
|
||||
if (response.data) {
|
||||
this.invoices = response.data.invoices.data
|
||||
}
|
||||
},
|
||||
async fetchInvoice () {
|
||||
let invoice = await this.fetchViewInvoice(this.$route.params.id)
|
||||
|
||||
if (invoice.data) {
|
||||
this.invoice = invoice.data.invoice
|
||||
this.shareableLink = invoice.data.shareable_link
|
||||
this.currency = invoice.data.invoice.user.currency
|
||||
}
|
||||
},
|
||||
sortData () {
|
||||
if (this.searchData.orderBy === 'asc') {
|
||||
this.searchData.orderBy = 'desc'
|
||||
this.onSearched()
|
||||
return true
|
||||
}
|
||||
this.searchData.orderBy = 'asc'
|
||||
this.onSearched()
|
||||
return true
|
||||
},
|
||||
async onMarkAsSent () {
|
||||
this.isRequestOnGoing = true
|
||||
let response = await this.markAsSent({id: this.invoice.id})
|
||||
this.isRequestOnGoing = false
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('invoices.marked_as_sent_message'))
|
||||
}
|
||||
},
|
||||
async removeInvoice (id) {
|
||||
this.selectInvoice([parseInt(id)])
|
||||
this.id = id
|
||||
swal({
|
||||
title: 'Deleted',
|
||||
text: 'you will not be able to recover this invoice!',
|
||||
icon: 'error',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
217
resources/assets/js/views/items/Create.vue
Normal file
217
resources/assets/js/views/items/Create.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<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
|
||||
:invalid="$v.formData.price.$error"
|
||||
v-model="price"
|
||||
v-bind="defaultCurrencyForInput"
|
||||
class="input-field"
|
||||
@input="$v.formData.price.$touch()"
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ $t('items.unit') }}</label>
|
||||
<base-select
|
||||
v-model="formData.unit"
|
||||
:options="units"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('items.select_a_unit')"
|
||||
label="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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
const { required, minLength, numeric, alpha, minValue, maxLength} = require('vuelidate/lib/validators')
|
||||
export default {
|
||||
mixins: {
|
||||
validationMixin
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isLoading: false,
|
||||
title: 'Add Item',
|
||||
units: [
|
||||
{ name: 'box', value: 'box' },
|
||||
{ name: 'cm', value: 'cm' },
|
||||
{ name: 'dz', value: 'dz' },
|
||||
{ name: 'ft', value: 'ft' },
|
||||
{ name: 'g', value: 'g' },
|
||||
{ name: 'in', value: 'in' },
|
||||
{ name: 'kg', value: 'kg' },
|
||||
{ name: 'km', value: 'km' },
|
||||
{ name: 'lb', value: 'lb' },
|
||||
{ name: 'mg', value: 'mg' }
|
||||
],
|
||||
formData: {
|
||||
name: '',
|
||||
description: '',
|
||||
price: '',
|
||||
unit: null
|
||||
},
|
||||
money: {
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
prefix: '$ ',
|
||||
precision: 2,
|
||||
masked: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrencyForInput'
|
||||
]),
|
||||
price: {
|
||||
get: function () {
|
||||
return this.formData.price / 100
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.formData.price = newValue * 100
|
||||
}
|
||||
},
|
||||
isEdit () {
|
||||
if (this.$route.name === 'items.edit') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.isEdit) {
|
||||
this.loadEditData()
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
formData: {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(3)
|
||||
},
|
||||
price: {
|
||||
required,
|
||||
numeric,
|
||||
maxLength: maxLength(10),
|
||||
minValue: minValue(0.1)
|
||||
},
|
||||
description: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('item', [
|
||||
'addItem',
|
||||
'fetchItem',
|
||||
'updateItem'
|
||||
]),
|
||||
async loadEditData () {
|
||||
let response = await this.fetchItem(this.$route.params.id)
|
||||
this.formData = response.data.item
|
||||
this.formData.unit = this.units.find(_unit => response.data.item.unit === _unit.name)
|
||||
this.fractional_price = response.data.item.price
|
||||
},
|
||||
async submitItem () {
|
||||
this.$v.formData.$touch()
|
||||
if (this.$v.$invalid) {
|
||||
return false
|
||||
}
|
||||
if (this.formData.unit) {
|
||||
this.formData.unit = this.formData.unit.name
|
||||
}
|
||||
if (this.isEdit) {
|
||||
this.isLoading = true
|
||||
let response = await this.updateItem(this.formData)
|
||||
if (response.data) {
|
||||
this.isLoading = false
|
||||
window.toastr['success'](this.$tc('items.updated_message'))
|
||||
this.$router.push('/admin/items')
|
||||
return true
|
||||
}
|
||||
window.toastr['error'](response.data.error)
|
||||
} else {
|
||||
this.isLoading = true
|
||||
let response = await this.addItem(this.formData)
|
||||
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('items.created_message'))
|
||||
this.$router.push('/admin/items')
|
||||
this.isLoading = false
|
||||
return true
|
||||
}
|
||||
window.toastr['success'](response.data.success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
408
resources/assets/js/views/items/Index.vue
Normal file
408
resources/assets/js/views/items/Index.vue
Normal file
@@ -0,0 +1,408 @@
|
||||
<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>
|
||||
|
||||
<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="units"
|
||||
: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')"
|
||||
>
|
||||
{{ $t('items.add_new_item') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{{ $t('general.actions') }}
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="select-all"
|
||||
v-model="selectAllFieldStatus"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
@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>
|
||||
</div>
|
||||
|
||||
<table-component
|
||||
ref="table"
|
||||
:data="fetchData"
|
||||
:show-filter="false"
|
||||
table-class="table"
|
||||
>
|
||||
|
||||
<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="$t('items.name')"
|
||||
show="name"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('items.unit')"
|
||||
show="unit"
|
||||
/>
|
||||
<table-column
|
||||
: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
|
||||
:label="$t('items.added_on')"
|
||||
sort-as="created_at"
|
||||
show="formattedCreatedAt"
|
||||
/>
|
||||
<table-column
|
||||
:sortable="false"
|
||||
: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>
|
||||
|
||||
</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>
|
||||
</template>
|
||||
</table-column>
|
||||
</table-component>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import DotIcon from '../../components/icon/DotIcon'
|
||||
import SatelliteIcon from '../../components/icon/SatelliteIcon'
|
||||
import BaseButton from '../../../js/components/base/BaseButton'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DotIcon,
|
||||
SatelliteIcon,
|
||||
BaseButton
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
id: null,
|
||||
showFilters: false,
|
||||
sortedBy: 'created_at',
|
||||
units: [
|
||||
{ name: 'box', value: 'box' },
|
||||
{ name: 'cm', value: 'cm' },
|
||||
{ name: 'dz', value: 'dz' },
|
||||
{ name: 'ft', value: 'ft' },
|
||||
{ name: 'g', value: 'g' },
|
||||
{ name: 'in', value: 'in' },
|
||||
{ name: 'kg', value: 'kg' },
|
||||
{ name: 'km', value: 'km' },
|
||||
{ name: 'lb', value: 'lb' },
|
||||
{ name: 'mg', value: 'mg' }
|
||||
],
|
||||
isRequestOngoing: true,
|
||||
filtersApplied: false,
|
||||
filters: {
|
||||
name: '',
|
||||
unit: '',
|
||||
price: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('item', [
|
||||
'items',
|
||||
'selectedItems',
|
||||
'totalItems',
|
||||
'selectAllField'
|
||||
]),
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrency'
|
||||
]),
|
||||
showEmptyScreen () {
|
||||
return !this.totalItems && !this.isRequestOngoing && !this.filtersApplied
|
||||
},
|
||||
filterIcon () {
|
||||
return (this.showFilters) ? 'times' : 'filter'
|
||||
},
|
||||
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
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
if (this.selectAllField) {
|
||||
this.selectAllItems()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('item', [
|
||||
'fetchItems',
|
||||
'selectAllItems',
|
||||
'selectItem',
|
||||
'deleteItem',
|
||||
'deleteMultipleItems',
|
||||
'setSelectAllState'
|
||||
]),
|
||||
refreshTable () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
async fetchData ({ page, filter, sort }) {
|
||||
let data = {
|
||||
search: this.filters.name !== null ? this.filters.name : '',
|
||||
unit: this.filters.unit !== null ? this.filters.unit.name : '',
|
||||
price: this.filters.price * 100,
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page
|
||||
}
|
||||
|
||||
this.isRequestOngoing = true
|
||||
let response = await this.fetchItems(data)
|
||||
this.isRequestOngoing = false
|
||||
|
||||
return {
|
||||
data: response.data.items.data,
|
||||
pagination: {
|
||||
totalPages: response.data.items.last_page,
|
||||
currentPage: page
|
||||
}
|
||||
}
|
||||
},
|
||||
setFilters () {
|
||||
this.filtersApplied = true
|
||||
this.refreshTable()
|
||||
},
|
||||
clearFilter () {
|
||||
this.filters = {
|
||||
name: '',
|
||||
unit: '',
|
||||
price: ''
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.filtersApplied = false
|
||||
})
|
||||
},
|
||||
toggleFilter () {
|
||||
if (this.showFilters && this.filtersApplied) {
|
||||
this.clearFilter()
|
||||
this.refreshTable()
|
||||
}
|
||||
|
||||
this.showFilters = !this.showFilters
|
||||
},
|
||||
async removeItems (id) {
|
||||
this.id = id
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('items.confirm_delete'),
|
||||
icon: 'error',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteItem(this.id)
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$tc('items.deleted_message', 1))
|
||||
this.$refs.table.refresh()
|
||||
return true
|
||||
}
|
||||
|
||||
if (res.data.error === 'item_attached') {
|
||||
window.toastr['error'](this.$tc('items.item_attached_message'), this.$t('general.action_failed'))
|
||||
return true
|
||||
}
|
||||
|
||||
window.toastr['error'](res.data.message)
|
||||
return true
|
||||
}
|
||||
})
|
||||
},
|
||||
async removeMultipleItems () {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('items.confirm_delete', 2),
|
||||
icon: 'error',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deleteMultipleItems()
|
||||
if (res.data.success) {
|
||||
window.toastr['success'](this.$tc('items.deleted_message', 2))
|
||||
this.$refs.table.refresh()
|
||||
} else if (res.data.error) {
|
||||
window.toastr['error'](res.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
83
resources/assets/js/views/layouts/LayoutBasic.vue
Normal file
83
resources/assets/js/views/layouts/LayoutBasic.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="template-container" v-if="isAppLoaded">
|
||||
<base-modal />
|
||||
<site-header/>
|
||||
<site-sidebar type="basic"/>
|
||||
|
||||
<transition
|
||||
name="fade"
|
||||
mode="out-in">
|
||||
<router-view />
|
||||
</transition>
|
||||
<site-footer/>
|
||||
</div>
|
||||
<div v-else class="template-container">
|
||||
<font-awesome-icon icon="spinner" class="fa-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 { mapActions, mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SiteHeader, SiteSidebar, SiteFooter, BaseModal
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
'header': 'header'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'isAppLoaded'
|
||||
]),
|
||||
|
||||
...mapGetters('company', {
|
||||
selectedCompany: 'getSelectedCompany',
|
||||
companies: 'getCompanies'
|
||||
}),
|
||||
|
||||
isShow () {
|
||||
return true
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
Layout.set('layout-default')
|
||||
},
|
||||
|
||||
created() {
|
||||
this.bootstrap().then((res) => {
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
body {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
</style>
|
||||
55
resources/assets/js/views/layouts/LayoutLogin.vue
Normal file
55
resources/assets/js/views/layouts/LayoutLogin.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<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>
|
||||
</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>
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
{{ $t('layout_login.crater_help') }}<br>
|
||||
{{ $t('layout_login.invoices_and_estimates') }}<br>
|
||||
</p>
|
||||
|
||||
<div class="content-bottom"/>
|
||||
</div>
|
||||
</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')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
27
resources/assets/js/views/layouts/LayoutWizard.vue
Normal file
27
resources/assets/js/views/layouts/LayoutWizard.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="site-wrapper">
|
||||
<div class="container">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
export default {
|
||||
watch: {
|
||||
$route: 'onRouteChange'
|
||||
},
|
||||
mounted () {
|
||||
this.setLayoutBackground()
|
||||
},
|
||||
|
||||
destroyed () {
|
||||
document.body.style.backgroundColor = '#EBF1FA'
|
||||
},
|
||||
methods: {
|
||||
setLayoutBackground () {
|
||||
document.body.style.backgroundColor = '#f9fbff'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
21
resources/assets/js/views/layouts/partials/TheSiteFooter.vue
Normal file
21
resources/assets/js/views/layouts/partials/TheSiteFooter.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
footer: 'footer'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
98
resources/assets/js/views/layouts/partials/TheSiteHeader.vue
Normal file
98
resources/assets/js/views/layouts/partials/TheSiteHeader.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<header class="site-header">
|
||||
<a href="/" class="brand-main">
|
||||
<img
|
||||
id="logo-white"
|
||||
src="/assets/img/logo-white.png"
|
||||
alt="Crater Logo"
|
||||
class="d-none d-md-inline"
|
||||
>
|
||||
<img
|
||||
id="logo-mobile"
|
||||
src="/assets/img/crater-white-small.png"
|
||||
alt="Laraspace Logo"
|
||||
class="d-md-none">
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="nav-toggle"
|
||||
@click="onNavToggle"
|
||||
>
|
||||
<div class="hamburger hamburger--arrowturn">
|
||||
<div class="hamburger-box">
|
||||
<div class="hamburger-inner"/>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<ul class="action-list">
|
||||
<li>
|
||||
<v-dropdown :show-arrow="false">
|
||||
<a slot="activator" href="#">
|
||||
<font-awesome-icon icon="plus" />
|
||||
</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>
|
||||
</li>
|
||||
<li>
|
||||
<v-dropdown :show-arrow="false">
|
||||
<a
|
||||
slot="activator"
|
||||
href="#"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
class="avatar"
|
||||
>
|
||||
<img src="/images/avatar.png" alt="Avatar">
|
||||
</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>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</template>
|
||||
<script type="text/babel">
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
...mapActions({
|
||||
companySelect: 'changeCompany'
|
||||
}),
|
||||
...mapActions('auth', [
|
||||
'logout'
|
||||
]),
|
||||
onNavToggle () {
|
||||
this.$utils.toggleSidebar()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,459 @@
|
||||
<template>
|
||||
<div class="header-bottom">
|
||||
<div class="header-nav vue-dropdown-menu">
|
||||
<v-dropdown active-url="/admin/dashboard">
|
||||
<template slot="title">
|
||||
<a href="#">
|
||||
<i class="icon-fa icon-fa-dashboard"/>{{ $t('navigation.dashboard') }}
|
||||
<span class="icon-fa arrow icon-fa-fw"/>
|
||||
</a>
|
||||
</template>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/dashboard/basic">
|
||||
Basic
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/dashboard/ecommerce">
|
||||
Ecommerce
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/dashboard/finance">
|
||||
Finance
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<v-dropdown active-url="/admin/layouts">
|
||||
<template slot="title">
|
||||
<a href="#">
|
||||
<i class="icon-fa icon-fa-th-large"/>Layouts
|
||||
<span class="icon-fa arrow icon-fa-fw"/>
|
||||
</a>
|
||||
</template>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/layouts/sidebar">
|
||||
Sidebar
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/layouts/horizontal">
|
||||
Horizontal
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/layouts/icons-sidebar">
|
||||
Icon Sidebar
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<v-dropdown active-url="/admin/basic-ui">
|
||||
<template slot="title">
|
||||
<a href="#">
|
||||
<i class="icon-fa icon-fa-star"/>Basic UI
|
||||
<span class="icon-fa arrow icon-fa-fw"/>
|
||||
</a>
|
||||
</template>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/basic-ui/buttons">
|
||||
Buttons
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/basic-ui/cards">
|
||||
Cards
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/basic-ui/tabs">
|
||||
Tabs & Accordians
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/basic-ui/typography">
|
||||
Typography
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/basic-ui/tables">
|
||||
Tables
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/basic-ui/modals">
|
||||
Modals
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/basic-ui/progress-bars">
|
||||
Progress Bar
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<v-dropdown active-url="/admin/components">
|
||||
<template slot="title">
|
||||
<a href="#">
|
||||
<i class="icon-fa icon-fa-puzzle-piece"/>Components
|
||||
<span class="icon-fa arrow icon-fa-fw"/>
|
||||
</a>
|
||||
</template>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/components/calendar">
|
||||
Calendar
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/components/datatables">
|
||||
Jquery Datatables
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/components/mail-box">
|
||||
MailBox
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/components/calendar">
|
||||
Calendar
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/components/datatables">
|
||||
Jquery Datatables
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/components/image-cropper">
|
||||
ImageCropper
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/components/image-zoom">
|
||||
ImageZoom
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/components/nestable-list">
|
||||
Nestable List
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/components/nestable-tree">
|
||||
Nestable Tree
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/components/notifications">
|
||||
Notifications
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown active-url="/admin/layouts">
|
||||
<template slot="title">
|
||||
<a href="#">
|
||||
<i class="icon-fa icon-fa-th-large"/>Layouts
|
||||
<span class="icon-fa arrow icon-fa-fw"/>
|
||||
</a>
|
||||
</template>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/layouts/sidebar">
|
||||
Sidebar
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/layouts/horizontal">
|
||||
Horizontal
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/components/sweet-modals">
|
||||
Sweet Modals
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/components/image-zoom">
|
||||
ImageZoom
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/components/mail-box">
|
||||
MailBox
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<v-dropdown active-url="/admin/chart">
|
||||
<template slot="title">
|
||||
<a href="#">
|
||||
<i class="icon-fa icon-fa-bar-chart"/>Charts
|
||||
<span class="icon-fa arrow icon-fa-fw"/>
|
||||
</a>
|
||||
</template>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/charts/amchart">
|
||||
AM Charts
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/charts/chartjs">
|
||||
Chart JS
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/charts/gauge">
|
||||
Gauges
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/charts/morris">
|
||||
Morris
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/charts/sparkline">
|
||||
Sparkline
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<v-dropdown active-url="/admin/icons">
|
||||
<template slot="title">
|
||||
<a href="#">
|
||||
<i class="icon-fa icon-fa-eye"/>Icons
|
||||
<span class="icon-fa arrow icon-fa-fw"/>
|
||||
</a>
|
||||
</template>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/icons/icomoon">
|
||||
IcoMoon
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/icons/fontawesome">
|
||||
Font Awesome
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<v-dropdown active-url="/admin/forms">
|
||||
<template slot="title">
|
||||
<a href="#">
|
||||
<i class="icon-fa icon-fa-rocket"/>Form
|
||||
<span class="icon-fa arrow icon-fa-fw"/>
|
||||
</a>
|
||||
</template>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/forms/general">
|
||||
General Elements
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/forms/advanced">
|
||||
Advanced Elements
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item><v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/forms/layouts">
|
||||
Form Layouts
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/forms/validation">
|
||||
Form Validation
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/forms/wizards">
|
||||
Form Wizard
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/forms/wizards-2">
|
||||
Form Wizard 2
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/forms/wizards-3">
|
||||
Form Wizard 3
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/forms/editors">
|
||||
Editors
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/forms/vee">
|
||||
Vee Validate
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/forms/vuelidate">
|
||||
Vuelidate
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<v-dropdown active-url="/admin/gallery">
|
||||
<template slot="title">
|
||||
<a href="#">
|
||||
<i class="icon-fa icon-fa-image"/>Gallery
|
||||
<span class="icon-fa arrow icon-fa-fw"/>
|
||||
</a>
|
||||
</template>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/gallery/grid">
|
||||
Grid
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/gallery/masonry-grid">
|
||||
Masonry Grid
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<v-dropdown active-url="/admin/users">
|
||||
<template slot="title">
|
||||
<a href="#">
|
||||
<i class="icon-fa icon-fa-user"/>Users
|
||||
<span class="icon-fa arrow icon-fa-fw"/>
|
||||
</a>
|
||||
</template>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/users/profile">
|
||||
Profile
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
<v-dropdown-item>
|
||||
<template slot="item-title">
|
||||
<router-link to="/admin/users">
|
||||
All Users
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown-item>
|
||||
</v-dropdown>
|
||||
<v-dropdown active-url="/admin/todo-item">
|
||||
<template slot="title">
|
||||
<router-link to="/admin/todo-item">
|
||||
<i class="icon-fa icon-fa-check"/>Todos
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown>
|
||||
<v-dropdown active-url="/admin/settings">
|
||||
<template slot="title">
|
||||
<router-link to="/admin/settings">
|
||||
<i class="icon-fa icon-fa-cogs"/>Settings
|
||||
</router-link>
|
||||
</template>
|
||||
</v-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
import VDropdown from '../../../components/dropdown/VDropdown'
|
||||
import VDropdownItem from '../../../components/dropdown/VDropdownItem'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VDropdown,
|
||||
VDropdownItem
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
sidebar: 'sidebar'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="sidebar-left">
|
||||
<div class="sidebar-body scroll-pane">
|
||||
<div class="side-nav">
|
||||
<div
|
||||
v-for="(menuItems, index) in menu"
|
||||
:key="index"
|
||||
class="menu-group"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
sidebar: 'sidebar',
|
||||
menu: [
|
||||
|
||||
[
|
||||
{
|
||||
title: 'navigation.dashboard',
|
||||
icon: 'tachometer-alt',
|
||||
route: '/admin/dashboard'
|
||||
},
|
||||
{
|
||||
title: 'navigation.customers',
|
||||
icon: 'user',
|
||||
route: '/admin/customers'
|
||||
},
|
||||
{
|
||||
title: 'navigation.items',
|
||||
icon: 'star',
|
||||
route: '/admin/items'
|
||||
}
|
||||
],
|
||||
|
||||
[
|
||||
{
|
||||
title: 'navigation.estimates',
|
||||
icon: 'file',
|
||||
route: '/admin/estimates'
|
||||
},
|
||||
{
|
||||
title: 'navigation.invoices',
|
||||
icon: 'file-alt',
|
||||
route: '/admin/invoices'
|
||||
},
|
||||
{
|
||||
title: 'navigation.payments',
|
||||
icon: 'credit-card',
|
||||
route: '/admin/payments'
|
||||
},
|
||||
{
|
||||
title: 'navigation.expenses',
|
||||
icon: 'space-shuttle',
|
||||
route: '/admin/expenses'
|
||||
}
|
||||
],
|
||||
|
||||
[
|
||||
{
|
||||
title: 'navigation.reports',
|
||||
icon: 'signal',
|
||||
route: '/admin/reports'
|
||||
},
|
||||
{
|
||||
title: 'navigation.settings',
|
||||
icon: 'cog',
|
||||
route: '/admin/settings'
|
||||
}
|
||||
]
|
||||
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
Toggle () {
|
||||
this.$utils.toggleSidebar()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
368
resources/assets/js/views/payments/Create.vue
Normal file
368
resources/assets/js/views/payments/Create.vue
Normal file
@@ -0,0 +1,368 @@
|
||||
<template>
|
||||
<div class="payment-create main-content">
|
||||
<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
|
||||
: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-input
|
||||
:invalid="$v.formData.payment_number.$error"
|
||||
v-model.trim="formData.payment_number"
|
||||
read-only
|
||||
type="text"
|
||||
name="email"
|
||||
@input="$v.formData.payment_number.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.payment_number.$error">
|
||||
<span v-if="!$v.formData.payment_number.required" class="text-danger">{{ $tc('validation.required') }}</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"
|
||||
/>
|
||||
<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')"
|
||||
label="invoice_number"
|
||||
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.numeric" class="text-danger">{{ $t('validation.numbers_only') }}</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_grater_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_mode"
|
||||
:options="getPaymentMode"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('payments.select_payment_mode')"
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import MultiSelect from 'vue-multiselect'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import moment from 'moment'
|
||||
const { required, numeric, between, maxLength } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
components: { MultiSelect },
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
formData: {
|
||||
user_id: null,
|
||||
payment_number: null,
|
||||
payment_date: null,
|
||||
amount: 100,
|
||||
payment_mode: null,
|
||||
invoice_id: null,
|
||||
notes: null
|
||||
},
|
||||
money: {
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
prefix: '$ ',
|
||||
precision: 2,
|
||||
masked: false
|
||||
},
|
||||
customer: null,
|
||||
invoice: null,
|
||||
customerList: [],
|
||||
invoiceList: [],
|
||||
isLoading: false,
|
||||
maxPayableAmount: Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
},
|
||||
validations () {
|
||||
return {
|
||||
customer: {
|
||||
required
|
||||
},
|
||||
formData: {
|
||||
payment_number: {
|
||||
required
|
||||
},
|
||||
payment_date: {
|
||||
required
|
||||
},
|
||||
amount: {
|
||||
required,
|
||||
numeric,
|
||||
between: between(1, this.maxPayableAmount + 1)
|
||||
},
|
||||
notes: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrencyForInput'
|
||||
]),
|
||||
getPaymentMode () {
|
||||
return ['Cash', 'Check', 'Credit Card', 'Bank Transfer']
|
||||
},
|
||||
amount: {
|
||||
get: function () {
|
||||
return this.formData.amount / 100
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.formData.amount = newValue * 100
|
||||
}
|
||||
},
|
||||
isEdit () {
|
||||
if (this.$route.name === 'payments.edit') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
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
|
||||
}
|
||||
} else {
|
||||
return this.defaultCurrencyForInput
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
customer (newValue) {
|
||||
this.formData.user_id = newValue.id
|
||||
if (!this.isEdit) {
|
||||
this.fetchCustomerInvoices(newValue.id)
|
||||
}
|
||||
},
|
||||
invoice (newValue) {
|
||||
this.formData.invoice_id = newValue.id
|
||||
if (!this.isEdit) {
|
||||
this.setPaymentAmountByInvoiceData(newValue.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
// if (!this.$route.params.id) {
|
||||
// this.$refs.baseSelect.$refs.search.focus()
|
||||
// }
|
||||
this.$nextTick(() => {
|
||||
this.loadData()
|
||||
if (this.$route.params.id && !this.isEdit) {
|
||||
this.setInvoicePaymentData()
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions('invoice', [
|
||||
'fetchInvoice'
|
||||
]),
|
||||
...mapActions('payment', [
|
||||
'fetchCreatePayment',
|
||||
'addPayment',
|
||||
'updatePayment',
|
||||
'fetchPayment'
|
||||
]),
|
||||
async loadData () {
|
||||
if (this.isEdit) {
|
||||
let response = await this.fetchPayment(this.$route.params.id)
|
||||
this.customerList = response.data.customers
|
||||
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.amount = parseFloat(response.data.payment.amount)
|
||||
this.maxPayableAmount = response.data.payment.amount
|
||||
if (response.data.payment.invoice !== null) {
|
||||
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)
|
||||
} else {
|
||||
let response = await this.fetchCreatePayment()
|
||||
this.customerList = response.data.customers
|
||||
this.formData.payment_number = response.data.nextPaymentNumber
|
||||
this.formData.payment_date = moment(new Date()).toString()
|
||||
}
|
||||
return true
|
||||
},
|
||||
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) {
|
||||
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 submitPaymentData () {
|
||||
this.$v.customer.$touch()
|
||||
this.$v.formData.$touch()
|
||||
if (this.$v.$invalid) {
|
||||
return true
|
||||
}
|
||||
if (this.isEdit) {
|
||||
let data = {
|
||||
editData: {
|
||||
...this.formData,
|
||||
payment_date: moment(this.formData.payment_date).format('DD/MM/YYYY')
|
||||
},
|
||||
id: this.$route.params.id
|
||||
}
|
||||
let response = await this.updatePayment(data)
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](this.$t('payments.updated_message'))
|
||||
this.$router.push('/admin/payments')
|
||||
return true
|
||||
}
|
||||
if (response.data.error === 'invalid_amount') {
|
||||
window.toastr['error'](this.$t('invalid_amount_message'))
|
||||
return false
|
||||
}
|
||||
window.toastr['error'](response.data.error)
|
||||
} else {
|
||||
let data = {
|
||||
...this.formData,
|
||||
payment_date: moment(this.formData.payment_date).format('DD/MM/YYYY')
|
||||
}
|
||||
this.isLoading = true
|
||||
let response = await this.addPayment(data)
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](this.$t('payments.created_message'))
|
||||
this.$router.push('/admin/payments')
|
||||
this.isLoading = true
|
||||
return true
|
||||
}
|
||||
if (response.data.error === 'invalid_amount') {
|
||||
window.toastr['error'](this.$t('invalid_amount_message'))
|
||||
return false
|
||||
}
|
||||
window.toastr['error'](response.data.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
423
resources/assets/js/views/payments/Index.vue
Normal file
423
resources/assets/js/views/payments/Index.vue
Normal file
@@ -0,0 +1,423 @@
|
||||
<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>
|
||||
|
||||
<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="payment_mode"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('payments.payment_mode')"
|
||||
/>
|
||||
</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')"
|
||||
>
|
||||
{{ $t('payments.add_new_payment') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<transition name="fade">
|
||||
<v-dropdown v-if="selectedPayments.length" :show-arrow="false">
|
||||
<span slot="activator" href="#" class="table-actions-button dropdown-toggle">
|
||||
{{ $t('general.actions') }}
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="select-all"
|
||||
v-model="selectAllFieldStatus"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
@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>
|
||||
</div>
|
||||
|
||||
<table-component
|
||||
ref="table"
|
||||
:data="fetchData"
|
||||
:show-filter="false"
|
||||
table-class="table"
|
||||
>
|
||||
<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="$t('payments.date')"
|
||||
sort-as="payment_date"
|
||||
show="formattedPaymentDate"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('payments.customer')"
|
||||
show="name"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('payments.payment_mode')"
|
||||
show="payment_mode"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('payments.payment_number')"
|
||||
show="payment_number"
|
||||
/>
|
||||
<table-column
|
||||
:label="$t('payments.invoice')"
|
||||
sort-as="invoice_id"
|
||||
show="invoice.invoice_number"
|
||||
/>
|
||||
<table-column
|
||||
: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
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
cell-class="action-dropdown no-click"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('payments.action') }}</span>
|
||||
<v-dropdown>
|
||||
<a slot="activator" href="#">
|
||||
<dot-icon />
|
||||
</a>
|
||||
<v-dropdown-item>
|
||||
|
||||
<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>
|
||||
|
||||
</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>
|
||||
</template>
|
||||
</table-column>
|
||||
</table-component>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</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 { request } from 'http'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'capsule-icon': CapsuleIcon,
|
||||
'SweetModal': SweetModal,
|
||||
'SweetModalTab': SweetModalTab,
|
||||
BaseButton
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showFilters: false,
|
||||
sortedBy: 'created_at',
|
||||
filtersApplied: false,
|
||||
isRequestOngoing: true,
|
||||
payment_mode: ['Cash', 'Check', 'Credit Card', 'Bank Transfer'],
|
||||
filters: {
|
||||
customer: null,
|
||||
payment_mode: '',
|
||||
payment_number: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showEmptyScreen () {
|
||||
return !this.totalPayments && !this.isRequestOngoing && !this.filtersApplied
|
||||
},
|
||||
filterIcon () {
|
||||
return (this.showFilters) ? 'times' : 'filter'
|
||||
},
|
||||
...mapGetters('customer', [
|
||||
'customers'
|
||||
]),
|
||||
...mapGetters('payment', [
|
||||
'selectedPayments',
|
||||
'totalPayments',
|
||||
'payments',
|
||||
'selectAllField'
|
||||
]),
|
||||
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
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.fetchCustomers()
|
||||
},
|
||||
destroyed () {
|
||||
if (this.selectAllField) {
|
||||
this.selectAllPayments()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('payment', [
|
||||
'fetchPayments',
|
||||
'selectAllPayments',
|
||||
'selectPayment',
|
||||
'deletePayment',
|
||||
'deleteMultiplePayments',
|
||||
'setSelectAllState'
|
||||
]),
|
||||
...mapActions('customer', [
|
||||
'fetchCustomers'
|
||||
]),
|
||||
async fetchData ({ page, filter, sort }) {
|
||||
let data = {
|
||||
customer_id: this.filters.customer !== null ? this.filters.customer.id : '',
|
||||
payment_number: this.filters.payment_number,
|
||||
payment_mode: this.filters.payment_mode ? this.filters.payment_mode : '',
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page
|
||||
}
|
||||
|
||||
this.isRequestOngoing = true
|
||||
|
||||
let response = await this.fetchPayments(data)
|
||||
|
||||
this.isRequestOngoing = false
|
||||
|
||||
return {
|
||||
data: response.data.payments.data,
|
||||
pagination: {
|
||||
totalPages: response.data.payments.last_page,
|
||||
currentPage: page,
|
||||
count: response.data.payments.scount
|
||||
}
|
||||
}
|
||||
},
|
||||
refreshTable () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
setFilters () {
|
||||
this.filtersApplied = true
|
||||
this.refreshTable()
|
||||
},
|
||||
clearFilter () {
|
||||
if (this.filters.customer) {
|
||||
this.$refs.customerSelect.$refs.baseSelect.removeElement(this.filters.customer)
|
||||
}
|
||||
|
||||
this.filters = {
|
||||
customer: null,
|
||||
payment_mode: '',
|
||||
payment_number: ''
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.filtersApplied = false
|
||||
})
|
||||
},
|
||||
toggleFilter () {
|
||||
if (this.showFilters && this.filtersApplied) {
|
||||
this.clearFilter()
|
||||
this.refreshTable()
|
||||
}
|
||||
|
||||
this.showFilters = !this.showFilters
|
||||
},
|
||||
onSelectCustomer (customer) {
|
||||
this.filters.customer = customer
|
||||
},
|
||||
async removePayment (id) {
|
||||
this.id = id
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('payments.confirm_delete'),
|
||||
icon: 'error',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let res = await this.deletePayment(this.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)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async removeMultiplePayments () {
|
||||
swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$tc('payments.confirm_delete', 2),
|
||||
icon: 'error',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (willDelete) => {
|
||||
if (willDelete) {
|
||||
let request = await this.deleteMultiplePayments()
|
||||
if (request.data.success) {
|
||||
window.toastr['success'](this.$tc('payments.deleted_message', 2))
|
||||
this.$refs.table.refresh()
|
||||
} else if (request.data.error) {
|
||||
window.toastr['error'](request.data.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async clearCustomerSearch (removedOption, id) {
|
||||
this.filters.customer = ''
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
showModel (selectedRow) {
|
||||
this.selectedRow = selectedRow
|
||||
this.$refs.Delete_modal.open()
|
||||
},
|
||||
async removeSelectedItems () {
|
||||
this.$refs.Delete_modal.close()
|
||||
await this.selectedRow.forEach(row => {
|
||||
this.deletePayment(this.id)
|
||||
})
|
||||
this.$refs.table.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
201
resources/assets/js/views/reports/ExpensesReport.vue
Normal file
201
resources/assets/js/views/reports/ExpensesReport.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<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
|
||||
v-model="selectedRange"
|
||||
:options="dateRange"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
@input="onChangeDateRange"
|
||||
/>
|
||||
<span v-if="$v.range.$error && !$v.range.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
</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>
|
||||
<base-date-picker
|
||||
v-model="formData.from_date"
|
||||
:invalid="$v.formData.from_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@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>
|
||||
<base-date-picker
|
||||
v-model="formData.to_date"
|
||||
:invalid="$v.formData.to_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-8 reports-tab-container">
|
||||
<iframe :src="getReportUrl" class="reports-frame-style"/>
|
||||
<a :href="getReportUrl" class="base-button btn btn-primary btn-lg report-view-button" target="_blank">
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import moment from 'moment'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
const { required } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
range: new Date(),
|
||||
dateRange: [
|
||||
'Today',
|
||||
'This Week',
|
||||
'This Month',
|
||||
'This Quarter',
|
||||
'This Year',
|
||||
'Previous Week',
|
||||
'Previous Month',
|
||||
'Previous Quarter',
|
||||
'Previous Year',
|
||||
'Custom'
|
||||
],
|
||||
selectedRange: 'This Month',
|
||||
formData: {
|
||||
from_date: moment().startOf('month').toString(),
|
||||
to_date: moment().endOf('month').toString()
|
||||
},
|
||||
url: null,
|
||||
siteURL: null
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
range: {
|
||||
required
|
||||
},
|
||||
formData: {
|
||||
from_date: {
|
||||
required
|
||||
},
|
||||
to_date: {
|
||||
required
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('company', [
|
||||
'getSelectedCompany'
|
||||
]),
|
||||
getReportUrl () {
|
||||
return this.url
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
range (newRange) {
|
||||
this.formData.from_date = moment(newRange).startOf('year').toString()
|
||||
this.formData.to_date = moment(newRange).endOf('year').toString()
|
||||
}
|
||||
},
|
||||
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')}`
|
||||
},
|
||||
methods: {
|
||||
getThisDate (type, time) {
|
||||
return moment()[type](time).toString()
|
||||
},
|
||||
getPreDate (type, time) {
|
||||
return moment().subtract(1, time)[type](time).toString()
|
||||
},
|
||||
onChangeDateRange () {
|
||||
switch (this.selectedRange) {
|
||||
case 'Today':
|
||||
this.formData.from_date = moment().toString()
|
||||
this.formData.to_date = moment().toString()
|
||||
break
|
||||
|
||||
case 'This Week':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'isoWeek')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'isoWeek')
|
||||
break
|
||||
|
||||
case 'This Month':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'month')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'month')
|
||||
break
|
||||
|
||||
case 'This Quarter':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'quarter')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'quarter')
|
||||
break
|
||||
|
||||
case 'This Year':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'year')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'year')
|
||||
break
|
||||
|
||||
case 'Previous Week':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'isoWeek')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'isoWeek')
|
||||
break
|
||||
|
||||
case 'Previous Month':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'month')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'month')
|
||||
break
|
||||
|
||||
case 'Previous Quarter':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'quarter')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'quarter')
|
||||
break
|
||||
|
||||
case 'Previous Year':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'year')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'year')
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
setRangeToCustom () {
|
||||
this.selectedRange = 'Custom'
|
||||
},
|
||||
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')}`
|
||||
return true
|
||||
},
|
||||
downloadReport () {
|
||||
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')}`
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
205
resources/assets/js/views/reports/ProfitLossReport.vue
Normal file
205
resources/assets/js/views/reports/ProfitLossReport.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<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
|
||||
v-model="selectedRange"
|
||||
:options="dateRange"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
@input="onChangeDateRange"
|
||||
/>
|
||||
<span v-if="$v.range.$error && !$v.range.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
</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>
|
||||
<base-date-picker
|
||||
v-model="formData.from_date"
|
||||
:invalid="$v.formData.from_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@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>
|
||||
<base-date-picker
|
||||
v-model="formData.to_date"
|
||||
:invalid="$v.formData.to_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-8 reports-tab-container">
|
||||
<iframe :src="getReportUrl" class="reports-frame-style"/>
|
||||
<a :href="getReportUrl" class="base-button btn btn-primary btn-lg report-view-button" target="_blank">
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import moment from 'moment'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
const { required } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
dateRange: [
|
||||
'Today',
|
||||
'This Week',
|
||||
'This Month',
|
||||
'This Quarter',
|
||||
'This Year',
|
||||
'Previous Week',
|
||||
'Previous Month',
|
||||
'Previous Quarter',
|
||||
'Previous Year',
|
||||
'Custom'
|
||||
],
|
||||
selectedRange: 'This Month',
|
||||
range: new Date(),
|
||||
formData: {
|
||||
from_date: moment().startOf('month').toString(),
|
||||
to_date: moment().endOf('month').toString()
|
||||
},
|
||||
url: null,
|
||||
siteURL: null
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
range: {
|
||||
required
|
||||
},
|
||||
formData: {
|
||||
from_date: {
|
||||
required
|
||||
},
|
||||
to_date: {
|
||||
required
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('company', [
|
||||
'getSelectedCompany'
|
||||
]),
|
||||
getReportUrl () {
|
||||
return this.url
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
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')
|
||||
},
|
||||
methods: {
|
||||
...mapActions('profitLossReport',[
|
||||
'loadProfitLossLink'
|
||||
]),
|
||||
getThisDate (type, time) {
|
||||
return moment()[type](time).toString()
|
||||
},
|
||||
getPreDate (type, time) {
|
||||
return moment().subtract(1, time)[type](time).toString()
|
||||
},
|
||||
onChangeDateRange () {
|
||||
switch (this.selectedRange) {
|
||||
case 'Today':
|
||||
this.formData.from_date = moment().toString()
|
||||
this.formData.to_date = moment().toString()
|
||||
break
|
||||
|
||||
case 'This Week':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'isoWeek')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'isoWeek')
|
||||
break
|
||||
|
||||
case 'This Month':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'month')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'month')
|
||||
break
|
||||
|
||||
case 'This Quarter':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'quarter')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'quarter')
|
||||
break
|
||||
|
||||
case 'This Year':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'year')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'year')
|
||||
break
|
||||
|
||||
case 'Previous Week':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'isoWeek')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'isoWeek')
|
||||
break
|
||||
|
||||
case 'Previous Month':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'month')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'month')
|
||||
break
|
||||
|
||||
case 'Previous Quarter':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'quarter')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'quarter')
|
||||
break
|
||||
|
||||
case 'Previous Year':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'year')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'year')
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
setRangeToCustom () {
|
||||
this.selectedRange = 'Custom'
|
||||
},
|
||||
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')}`
|
||||
return true
|
||||
},
|
||||
downloadReport () {
|
||||
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')}`
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
248
resources/assets/js/views/reports/SalesReports.vue
Normal file
248
resources/assets/js/views/reports/SalesReports.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<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-date-picker
|
||||
v-model="range"
|
||||
:invalid="$v.range.$error"
|
||||
format="yyyy"
|
||||
minimum-view="year"
|
||||
@change="$v.range.$touch()"
|
||||
/> -->
|
||||
<base-select
|
||||
v-model="selectedRange"
|
||||
:options="dateRange"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
@input="onChangeDateRange"
|
||||
/>
|
||||
<span v-if="$v.range.$error && !$v.range.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
</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>
|
||||
<base-date-picker
|
||||
v-model="formData.from_date"
|
||||
:invalid="$v.formData.from_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@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>
|
||||
<base-date-picker
|
||||
v-model="formData.to_date"
|
||||
:invalid="$v.formData.to_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@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>
|
||||
</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
|
||||
v-model="selectedType"
|
||||
:options="reportTypes"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
: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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-8 reports-tab-container">
|
||||
<iframe :src="getReportUrl" class="reports-frame-style"/>
|
||||
<a :href="getReportUrl" class="base-button btn btn-primary btn-lg report-view-button" target="_blank">
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import moment from 'moment'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
const { required } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
reportTypes: ['By Customer', 'By Item'],
|
||||
selectedType: 'By Customer',
|
||||
dateRange: [
|
||||
'Today',
|
||||
'This Week',
|
||||
'This Month',
|
||||
'This Quarter',
|
||||
'This Year',
|
||||
'Previous Week',
|
||||
'Previous Month',
|
||||
'Previous Quarter',
|
||||
'Previous Year',
|
||||
'Custom'
|
||||
],
|
||||
selectedRange: 'This Month',
|
||||
range: new Date(),
|
||||
formData: {
|
||||
from_date: moment().startOf('month').toString(),
|
||||
to_date: moment().endOf('month').toString()
|
||||
},
|
||||
url: null,
|
||||
customerSiteURL: null,
|
||||
itemsSiteURL: null
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
range: {
|
||||
required
|
||||
},
|
||||
formData: {
|
||||
from_date: {
|
||||
required
|
||||
},
|
||||
to_date: {
|
||||
required
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('company', [
|
||||
'getSelectedCompany'
|
||||
]),
|
||||
getReportUrl () {
|
||||
return this.url
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
range (newRange) {
|
||||
this.formData.from_date = moment(newRange).startOf('year').toString()
|
||||
this.formData.to_date = moment(newRange).endOf('year').toString()
|
||||
}
|
||||
},
|
||||
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) {
|
||||
return moment()[type](time).toString()
|
||||
},
|
||||
getPreDate (type, time) {
|
||||
return moment().subtract(1, time)[type](time).toString()
|
||||
},
|
||||
onChangeDateRange () {
|
||||
switch (this.selectedRange) {
|
||||
case 'Today':
|
||||
this.formData.from_date = moment().toString()
|
||||
this.formData.to_date = moment().toString()
|
||||
break
|
||||
|
||||
case 'This Week':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'isoWeek')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'isoWeek')
|
||||
break
|
||||
|
||||
case 'This Month':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'month')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'month')
|
||||
break
|
||||
|
||||
case 'This Quarter':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'quarter')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'quarter')
|
||||
break
|
||||
|
||||
case 'This Year':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'year')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'year')
|
||||
break
|
||||
|
||||
case 'Previous Week':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'isoWeek')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'isoWeek')
|
||||
break
|
||||
|
||||
case 'Previous Month':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'month')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'month')
|
||||
break
|
||||
|
||||
case 'Previous Quarter':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'quarter')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'quarter')
|
||||
break
|
||||
|
||||
case 'Previous Year':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'year')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'year')
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
setRangeToCustom () {
|
||||
this.selectedRange = 'Custom'
|
||||
},
|
||||
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')}`
|
||||
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')}`
|
||||
return true
|
||||
},
|
||||
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')}`
|
||||
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')}`
|
||||
return true
|
||||
},
|
||||
downloadReport () {
|
||||
this.url += '&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')}`
|
||||
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')}`
|
||||
return true
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
201
resources/assets/js/views/reports/TaxReport.vue
Normal file
201
resources/assets/js/views/reports/TaxReport.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<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
|
||||
v-model="selectedRange"
|
||||
:options="dateRange"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
@input="onChangeDateRange"
|
||||
/>
|
||||
<span v-if="$v.range.$error && !$v.range.required" class="text-danger"> {{ $t('validation.required') }} </span>
|
||||
</div>
|
||||
</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>
|
||||
<base-date-picker
|
||||
v-model="formData.from_date"
|
||||
:invalid="$v.formData.from_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@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>
|
||||
<base-date-picker
|
||||
v-model="formData.to_date"
|
||||
:invalid="$v.formData.to_date.$error"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
@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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-8 reports-tab-container">
|
||||
<iframe :src="getReportUrl" class="reports-frame-style"/>
|
||||
<a :href="getReportUrl" class="base-button btn btn-primary btn-lg report-view-button" target="_blank">
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import moment from 'moment'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
const { required } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
dateRange: [
|
||||
'Today',
|
||||
'This Week',
|
||||
'This Month',
|
||||
'This Quarter',
|
||||
'This Year',
|
||||
'Previous Week',
|
||||
'Previous Month',
|
||||
'Previous Quarter',
|
||||
'Previous Year',
|
||||
'Custom'
|
||||
],
|
||||
selectedRange: 'This Month',
|
||||
range: new Date(),
|
||||
formData: {
|
||||
from_date: moment().startOf('month').toString(),
|
||||
to_date: moment().endOf('month').toString()
|
||||
},
|
||||
url: null,
|
||||
siteURL: null
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
range: {
|
||||
required
|
||||
},
|
||||
formData: {
|
||||
from_date: {
|
||||
required
|
||||
},
|
||||
to_date: {
|
||||
required
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('company', [
|
||||
'getSelectedCompany'
|
||||
]),
|
||||
getReportUrl () {
|
||||
return this.url
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
range (newRange) {
|
||||
this.formData.from_date = moment(newRange).startOf('year').toString()
|
||||
this.formData.to_date = moment(newRange).endOf('year').toString()
|
||||
}
|
||||
},
|
||||
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')}`
|
||||
},
|
||||
methods: {
|
||||
getThisDate (type, time) {
|
||||
return moment()[type](time).toString()
|
||||
},
|
||||
getPreDate (type, time) {
|
||||
return moment().subtract(1, time)[type](time).toString()
|
||||
},
|
||||
onChangeDateRange () {
|
||||
switch (this.selectedRange) {
|
||||
case 'Today':
|
||||
this.formData.from_date = moment().toString()
|
||||
this.formData.to_date = moment().toString()
|
||||
break
|
||||
|
||||
case 'This Week':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'isoWeek')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'isoWeek')
|
||||
break
|
||||
|
||||
case 'This Month':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'month')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'month')
|
||||
break
|
||||
|
||||
case 'This Quarter':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'quarter')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'quarter')
|
||||
break
|
||||
|
||||
case 'This Year':
|
||||
this.formData.from_date = this.getThisDate('startOf', 'year')
|
||||
this.formData.to_date = this.getThisDate('endOf', 'year')
|
||||
break
|
||||
|
||||
case 'Previous Week':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'isoWeek')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'isoWeek')
|
||||
break
|
||||
|
||||
case 'Previous Month':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'month')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'month')
|
||||
break
|
||||
|
||||
case 'Previous Quarter':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'quarter')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'quarter')
|
||||
break
|
||||
|
||||
case 'Previous Year':
|
||||
this.formData.from_date = this.getPreDate('startOf', 'year')
|
||||
this.formData.to_date = this.getPreDate('endOf', 'year')
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
setRangeToCustom () {
|
||||
this.selectedRange = 'Custom'
|
||||
},
|
||||
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')}`
|
||||
return true
|
||||
},
|
||||
downloadReport () {
|
||||
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')}`
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
87
resources/assets/js/views/reports/layout/Index.vue
Normal file
87
resources/assets/js/views/reports/layout/Index.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<transition
|
||||
name="fade"
|
||||
mode="out-in">
|
||||
<router-view ref="report"/>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
watch: {
|
||||
'$route.path' (newValue) {
|
||||
if (newValue === '/admin/reports') {
|
||||
this.$router.push('/admin/reports/sales')
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.$route.path === '/admin/reports') {
|
||||
this.$router.push('/admin/reports/sales')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onDownload () {
|
||||
this.$refs.report.downloadReport()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tab {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.tab-link {
|
||||
padding: 10px 30px;
|
||||
display: block
|
||||
}
|
||||
</style>
|
||||
334
resources/assets/js/views/settings/CompanyInfo.vue
Normal file
334
resources/assets/js/views/settings/CompanyInfo.vue
Normal file
@@ -0,0 +1,334 @@
|
||||
<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">
|
||||
<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: 'Cancle'}"
|
||||
:cropper-options="cropperOptions"
|
||||
:output-options="cropperOutputOptions"
|
||||
:output-quality="0.8"
|
||||
:upload-handler="cropperHandler"
|
||||
trigger="#pick-avatar"
|
||||
@changed="setFileObject"
|
||||
/>
|
||||
</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"
|
||||
:invalid="$v.formData.phone.$error"
|
||||
:placeholder="$t('settings.company_info.phone')"
|
||||
@input="$v.formData.phone.$touch()"
|
||||
/>
|
||||
<div v-if="$v.formData.phone.$error">
|
||||
<span v-if="!$v.formData.phone.phone" class="text-danger">{{ $tc('validation.numbers_only') }}</span>
|
||||
</div>
|
||||
</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-select
|
||||
v-model="state"
|
||||
:options="states"
|
||||
:searchable="true"
|
||||
:disabled="isDisabledState"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('general.select_state')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-4">
|
||||
<label class="input-label">{{ $tc('settings.company_info.city') }}</label>
|
||||
<base-select
|
||||
v-model="city"
|
||||
:options="cities"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:disabled="isDisabledCity"
|
||||
:placeholder="$t('general.select_city')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="col-md-6 mb-3">
|
||||
<label class="input-label">Website</label>
|
||||
<base-input
|
||||
v-model="formData.website"
|
||||
placeholder="Website"
|
||||
/>
|
||||
</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')"
|
||||
rows="2"
|
||||
/>
|
||||
<base-text-area
|
||||
v-model="formData.address_street_2"
|
||||
:placeholder="$tc('general.street_1')"
|
||||
rows="2"
|
||||
/>
|
||||
</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, numeric } = 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: '',
|
||||
logo: null,
|
||||
email: '',
|
||||
phone: null,
|
||||
zip: null,
|
||||
address_street_1: null,
|
||||
address_street_2: null,
|
||||
website: null,
|
||||
country_id: null,
|
||||
state_id: '',
|
||||
city_id: ''
|
||||
},
|
||||
isLoading: false,
|
||||
isHidden: false,
|
||||
country: null,
|
||||
previewLogo: null,
|
||||
city: null,
|
||||
state: null,
|
||||
countries: [],
|
||||
isDisabledState: true,
|
||||
isDisabledCity: true,
|
||||
states: [],
|
||||
cities: [],
|
||||
passData: [],
|
||||
fileSendUrl: '/api/settings/company',
|
||||
fileObject: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
country (newCountry) {
|
||||
this.formData.country_id = newCountry.id
|
||||
if (this.formData.country_id) {
|
||||
this.isDisabledState = false
|
||||
}
|
||||
this.fetchState()
|
||||
if (this.isFetchingData) {
|
||||
return true
|
||||
}
|
||||
|
||||
this.state = null
|
||||
this.city = null
|
||||
},
|
||||
state (newState) {
|
||||
if (newState !== null && newState !== undefined) {
|
||||
this.formData.state_id = newState.id
|
||||
this.fetchCities()
|
||||
this.isDisabledCity = false
|
||||
|
||||
if (this.isFetchingData) {
|
||||
this.isFetchingData = false
|
||||
return true
|
||||
}
|
||||
this.city = null
|
||||
return true
|
||||
}
|
||||
// this.formData.state_id = null
|
||||
this.cities = []
|
||||
this.city = null
|
||||
// this.formData.city_id = null
|
||||
this.isDisabledCity = true
|
||||
return true
|
||||
},
|
||||
city (newCity) {
|
||||
if (newCity !== null && newCity !== undefined) {
|
||||
this.formData.city_id = newCity.id
|
||||
return true
|
||||
}
|
||||
// this.formData.city_id = null
|
||||
// return true
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
formData: {
|
||||
name: {
|
||||
required
|
||||
},
|
||||
country_id: {
|
||||
required
|
||||
},
|
||||
email: {
|
||||
email
|
||||
},
|
||||
phone: {
|
||||
numeric
|
||||
}
|
||||
}
|
||||
},
|
||||
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
|
||||
},
|
||||
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.country = response.data.user.addresses[0].country
|
||||
this.state = response.data.user.addresses[0].state
|
||||
this.city = response.data.user.addresses[0].city
|
||||
this.previewLogo = response.data.user.company.logo
|
||||
},
|
||||
async updateCompany () {
|
||||
this.$v.formData.$touch()
|
||||
if (this.$v.$invalid) {
|
||||
return true
|
||||
}
|
||||
this.isLoading = true
|
||||
let data = new FormData()
|
||||
data.append('name', this.formData.name)
|
||||
data.append('address_street_1', this.formData.address_street_1)
|
||||
data.append('address_street_2', this.formData.address_street_2)
|
||||
data.append('city_id', this.formData.city_id)
|
||||
data.append('state_id', this.formData.state_id)
|
||||
data.append('country_id', this.formData.country_id)
|
||||
data.append('zip', this.formData.zip)
|
||||
data.append('phone', this.formData.phone)
|
||||
if (this.fileObject) {
|
||||
data.append('logo', this.fileObject)
|
||||
}
|
||||
let response = await this.editCompany(data)
|
||||
if (response.data.success) {
|
||||
this.isLoading = false
|
||||
window.toastr['success'](this.$t('settings.company_info.updated_message'))
|
||||
return true
|
||||
}
|
||||
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
|
||||
}
|
||||
},
|
||||
async fetchState () {
|
||||
this.$v.formData.country_id.$touch()
|
||||
let res = await window.axios.get(`/api/states/${this.country.id}`)
|
||||
if (res) {
|
||||
this.states = res.data.states
|
||||
}
|
||||
},
|
||||
async fetchCities () {
|
||||
let res = await window.axios.get(`/api/cities/${this.state.id}`)
|
||||
if (res) {
|
||||
this.cities = res.data.cities
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
131
resources/assets/js/views/settings/ExpenseCategory.vue
Normal file
131
resources/assets/js/views/settings/ExpenseCategory.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<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) {
|
||||
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['success'](this.$t('settings.expense_category.already_in_use'))
|
||||
},
|
||||
openCategoryModal () {
|
||||
this.openModal({
|
||||
'title': 'Add Category',
|
||||
'componentName': 'CategoryModal'
|
||||
})
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
async EditCategory (id) {
|
||||
let response = await this.fetchCategory(id)
|
||||
this.openModal({
|
||||
'title': 'Edit Category',
|
||||
'componentName': 'CategoryModal',
|
||||
'id': id,
|
||||
'data': response.data.category
|
||||
})
|
||||
this.$refs.table.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
102
resources/assets/js/views/settings/GeneralSetting.vue
Normal file
102
resources/assets/js/views/settings/GeneralSetting.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<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>
|
||||
153
resources/assets/js/views/settings/Notifications.vue
Normal file
153
resources/assets/js/views/settings/Notifications.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<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>
|
||||
74
resources/assets/js/views/settings/PDFSetting.vue
Normal file
74
resources/assets/js/views/settings/PDFSetting.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="main-content pdfsetting">
|
||||
<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.pdf.title') }}</router-link></li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="caption">
|
||||
<h6>{{ $t('settings.pdf.title') }}</h6>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<base-button color="theme" size="small" @click="submitData">
|
||||
{{ $t('general.update') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<label class="col-md-2 form-control-label">{{ $t('settings.pdf.footer_text') }} : </label>
|
||||
<div class="col-md-12">
|
||||
<input v-model="footerText" type="text" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pdfsetting__img-row">
|
||||
<label class="col-md-2 form-control-label">{{ $t('settings.pdf.pdf_layout') }} : </label>
|
||||
<div class="col-md-12">
|
||||
<image-radio :current-p-d-f="pdfSet" @selectedPDF="selectedPDF"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ImageRadio from '../components/ImageRadio.vue'
|
||||
import { mapActions, mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'image-radio': ImageRadio
|
||||
},
|
||||
data () {
|
||||
return this.$store.state.pdf_setting
|
||||
// return {
|
||||
// pdfSet: '1',
|
||||
// footerText: null
|
||||
// }
|
||||
},
|
||||
mounted () {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('pdf_setting', [
|
||||
'loadData',
|
||||
'submitData'
|
||||
]),
|
||||
// async submitData () {
|
||||
|
||||
// },
|
||||
...mapMutations('pdf_setting', [
|
||||
'selectedPDF'
|
||||
])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
243
resources/assets/js/views/settings/Preferences.vue
Normal file
243
resources/assets/js/views/settings/Preferences.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<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"
|
||||
: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: {
|
||||
...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>
|
||||
189
resources/assets/js/views/settings/TaxTypes.vue
Normal file
189
resources/assets/js/views/settings/TaxTypes.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<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"
|
||||
:filterable="true"
|
||||
:label="$t('settings.tax_types.tax_name')"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span>{{ $t('settings.tax_types.tax_name') }}</span>
|
||||
<span class="tax-name">
|
||||
{{ row.name }}
|
||||
</span>
|
||||
</template>
|
||||
</table-column>
|
||||
<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 :
|
||||
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) {
|
||||
let response = await this.deleteTaxType(id)
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](this.$t('settings.sales_taxes.deleted_message'))
|
||||
this.id = null
|
||||
this.$refs.table.refresh()
|
||||
return true
|
||||
}window.toastr['success'](this.$t('settings.sales_taxes.already_in_use'))
|
||||
},
|
||||
openTaxModal () {
|
||||
this.openModal({
|
||||
'title': 'Add Tax',
|
||||
'componentName': 'TaxTypeModal'
|
||||
})
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
async EditTax (id) {
|
||||
let response = await this.fetchTaxType(id)
|
||||
this.openModal({
|
||||
'title': 'Edit Tax',
|
||||
'componentName': 'TaxTypeModal',
|
||||
'id': id,
|
||||
'data': response.data.taxType
|
||||
})
|
||||
this.$refs.table.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
158
resources/assets/js/views/settings/UserProfile.vue
Normal file
158
resources/assets/js/views/settings/UserProfile.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<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">
|
||||
<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>
|
||||
<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'
|
||||
const { required, requiredIf, sameAs, email } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
isLoading: false,
|
||||
formData: {
|
||||
name: null,
|
||||
email: null,
|
||||
password: null,
|
||||
confirm_password: null
|
||||
}
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
formData: {
|
||||
name: {
|
||||
required
|
||||
},
|
||||
email: {
|
||||
required,
|
||||
email
|
||||
},
|
||||
password: {
|
||||
},
|
||||
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'
|
||||
]),
|
||||
async setInitialData () {
|
||||
let response = await this.loadData()
|
||||
this.formData.name = response.data.name
|
||||
this.formData.email = response.data.email
|
||||
},
|
||||
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
|
||||
window.toastr['success'](this.$t('settings.account_settings.updated_message'))
|
||||
return true
|
||||
}
|
||||
window.toastr['error'](response.data.error)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
126
resources/assets/js/views/settings/currency/Index.vue
Normal file
126
resources/assets/js/views/settings/currency/Index.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="main-content">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ $tc('navigation.currency', 2) }}</h3>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="/admin/dashboard">
|
||||
{{ $t('navigation.home') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<router-link
|
||||
slot="item-title"
|
||||
to="#">
|
||||
{{ $tc('navigation.currency', 2) }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="caption">
|
||||
<h6>{{ $t('settings.currencies.select_currency') }}:</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<select
|
||||
v-model.trim="currencyId"
|
||||
class="form-control"
|
||||
@change="selectCurrency()"
|
||||
>
|
||||
<option
|
||||
v-for="(currency, index) in currencies"
|
||||
:key="index"
|
||||
:value="currency.id"
|
||||
>
|
||||
{{ currency.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="caption">
|
||||
<h6>{{ $t('settings.currencies.currencies_list') }}</h6>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<router-link slot="item-title" to="currencies/create">
|
||||
<base-button icon="plus" color="theme" size="small">
|
||||
{{ $t('navigation.add') }} {{ $t('navigation.new') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table-component
|
||||
ref="table"
|
||||
:data="currencies"
|
||||
table-class="table"
|
||||
sort-by="name"
|
||||
sort-order="asc"
|
||||
>
|
||||
<table-column :label="$t('settings.currencies.name')" show="name" />
|
||||
<table-column :label="$t('settings.currencies.code')" show="code" />
|
||||
<table-column :label="$t('settings.currencies.symbol')" show="symbol" />
|
||||
<table-column :label="$t('settings.currencies.precision')" show="precision" />
|
||||
<table-column :label="$t('settings.currencies.thousand_separator')" show="thousand_separator" />
|
||||
<table-column :label="$t('settings.currencies.decimal_separator')" show="decimal_separator" />
|
||||
<table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
:label="$t('settings.currencies.position')"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<span v-if="row.swap_currency_symbol === 0">{{ $t('settings.currencies.right') }}</span>
|
||||
<span v-if="row.swap_currency_symbol === 1">{{ $t('settings.currencies.left') }}</span>
|
||||
</template>
|
||||
</table-column>
|
||||
<table-column
|
||||
:sortable="false"
|
||||
:filterable="false"
|
||||
:label="$t('settings.currencies.action')"
|
||||
>
|
||||
<template slot-scope="row">
|
||||
<div class="table__actions">
|
||||
<router-link slot="item-title" :to="{path: `currencies/${row.id}/edit`}">{{ $t('navigation.edit') }}</router-link>
|
||||
<div class="table__item--cursor-pointer" @click="removeItems(row.id)">{{ $t('navigation.delete') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</table-column>
|
||||
</table-component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return this.$store.state.currency
|
||||
},
|
||||
mounted () {
|
||||
this.indexLoadData()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('currency', [
|
||||
'indexLoadData',
|
||||
'removeItems',
|
||||
'selectCurrency'
|
||||
])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
185
resources/assets/js/views/settings/currency/currency.vue
Normal file
185
resources/assets/js/views/settings/currency/currency.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="main-content currencycreate">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">{{ $t('settings.currencies.add_currency') }}</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/currencies">{{ $tc('settings.currencies.currency',2) }}</router-link></li>
|
||||
<li class="breadcrumb-item"><a href="#">{{ $t('navigation.add') }}</a></li>
|
||||
</ol>
|
||||
<div class="page-actions">
|
||||
<router-link slot="item-title" to="/admin/settings/currencies">
|
||||
<base-button icon="backward" color="theme">
|
||||
{{ $t('navigation.go_back') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<form action="" @submit.prevent="submiteCurrency">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ $t('settings.currencies.name') }}:</label><span class="required text-danger"> *</span>
|
||||
<input
|
||||
:class="{ error: $v.formData.name.$error }"
|
||||
v-model.trim="formData.name"
|
||||
type="text"
|
||||
name="name"
|
||||
class="form-control"
|
||||
@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="form-group">
|
||||
<label class="control-label">{{ $t('settings.currencies.code') }}:</label><span class="required"> *</span>
|
||||
<input
|
||||
:class="{ error: $v.formData.code.$error }"
|
||||
v-model="formData.code"
|
||||
type="text"
|
||||
name="code"
|
||||
class="form-control"
|
||||
@input="$v.formData.code.$touch()"
|
||||
>
|
||||
<div v-if="$v.formData.code.$error">
|
||||
<span v-if="!$v.formData.code.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ $t('settings.currencies.symbol') }}:</label>
|
||||
<input
|
||||
v-model="formData.symbol"
|
||||
type="text"
|
||||
name="symbol"
|
||||
class="form-control"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ $t('settings.currencies.precision') }}:</label>
|
||||
<input
|
||||
v-model="formData.precision"
|
||||
type="text"
|
||||
name="precision"
|
||||
class="form-control"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ $t('settings.currencies.thousand_separator') }}:</label><span class="required"> *</span>
|
||||
<input
|
||||
:class="{ error: $v.formData.thousand_separator.$error }"
|
||||
v-model="formData.thousand_separator"
|
||||
type="text"
|
||||
name="thousand_separator"
|
||||
class="form-control"
|
||||
@input="$v.formData.thousand_separator.$touch()"
|
||||
>
|
||||
<div v-if="$v.formData.thousand_separator.$error">
|
||||
<span v-if="!$v.formData.thousand_separator.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ $t('settings.currencies.decimal_separator') }}:</label><span class="required"> *</span>
|
||||
<input
|
||||
:class="{ error: $v.formData.decimal_separator.$error }"
|
||||
v-model="formData.decimal_separator"
|
||||
type="text"
|
||||
name="decimal_separator"
|
||||
class="form-control"
|
||||
@input="$v.formData.decimal_separator.$touch()"
|
||||
>
|
||||
<div v-if="$v.formData.decimal_separator.$error">
|
||||
<span v-if="!$v.formData.decimal_separator.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ $t('settings.currencies.position_of_symbol') }}:</label><span class="required"> *</span>
|
||||
<select
|
||||
v-model="formData.swap_currency_symbol"
|
||||
:class="{ error: $v.formData.swap_currency_symbol.$error }"
|
||||
class="form-control ls-select2"
|
||||
name="swap_currency_symbol"
|
||||
@select="$v.formData.swap_currency_symbol.$touch()"
|
||||
>
|
||||
<option value="0">{{ $t('settings.currencies.right') }}</option>
|
||||
<option value="1">{{ $t('settings.currencies.left') }}</option>
|
||||
</select>
|
||||
<div v-if="$v.formData.swap_currency_symbol.$error">
|
||||
<span v-if="!$v.formData.swap_currency_symbol.required" class="text-danger">{{ $tc('validation.required') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<base-button color="theme" type="submit">
|
||||
{{ $t('navigation.add') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
const { required } = require('vuelidate/lib/validators')
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return this.$store.state.currency
|
||||
},
|
||||
computed: {
|
||||
isEdit () {
|
||||
if (this.$route.name === 'currencyedit') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
formData: {
|
||||
name: {
|
||||
required
|
||||
},
|
||||
code: {
|
||||
required
|
||||
},
|
||||
thousand_separator: {
|
||||
required
|
||||
},
|
||||
decimal_separator: {
|
||||
required
|
||||
},
|
||||
swap_currency_symbol: {
|
||||
required
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (!this.isEdit) {
|
||||
return true
|
||||
}
|
||||
this.loadData(this.$route.params.id)
|
||||
},
|
||||
methods: {
|
||||
...mapActions('currency', [
|
||||
'loadData',
|
||||
'addCurrency',
|
||||
'editCurrency'
|
||||
]),
|
||||
async submiteCurrency () {
|
||||
this.$v.formData.$touch()
|
||||
if (this.$v.$invalid) {
|
||||
return false
|
||||
}
|
||||
if (this.isEdit) {
|
||||
this.editCurrency(this.$route.params.id)
|
||||
return true
|
||||
}
|
||||
this.addCurrency()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
93
resources/assets/js/views/settings/layout/Index.vue
Normal file
93
resources/assets/js/views/settings/layout/Index.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<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/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/notifications',
|
||||
title: 'settings.menu_title.notifications',
|
||||
icon: 'bell',
|
||||
iconType: 'far'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
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>
|
||||
311
resources/assets/js/views/wizard/CompanyInfo.vue
Normal file
311
resources/assets/js/views/wizard/CompanyInfo.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<template>
|
||||
<div class="card-body">
|
||||
<form action="" @submit.prevent="next()">
|
||||
<!-- <div v-if="previewLogo" class="upload-logo">
|
||||
<label class="form-label">{{ $t('wizard.logo_preview') }}</label><br>
|
||||
<img v-if="previewLogo" :src="previewLogo" class="preview-logo">
|
||||
</div> -->
|
||||
<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">
|
||||
<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: 'Cancle'}"
|
||||
:cropper-options="cropperOptions"
|
||||
:output-options="cropperOutputOptions"
|
||||
:output-quality="0.8"
|
||||
:upload-handler="cropperHandler"
|
||||
trigger="#pick-avatar"
|
||||
@changed="setFileObject"
|
||||
/>
|
||||
</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"
|
||||
@input="fetchState()"
|
||||
/>
|
||||
<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-select
|
||||
v-model="state"
|
||||
:options="states"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:disabled="isDisabledState"
|
||||
:placeholder="$t('general.select_state')"
|
||||
track-by="id"
|
||||
label="name"
|
||||
@input="fetchCities"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.city') }}</label>
|
||||
<base-select
|
||||
v-model="city"
|
||||
:options="cities"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:disabled="isDisabledCity"
|
||||
:placeholder="$t('general.select_city')"
|
||||
track-by="id"
|
||||
label="name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ $t('wizard.address') }}</label>
|
||||
<base-text-area
|
||||
v-model.trim="companyData.address_street_1"
|
||||
:placeholder="$t('general.street_1')"
|
||||
name="billing_street1"
|
||||
rows="2"
|
||||
/>
|
||||
<base-text-area
|
||||
v-model="companyData.address_street_2"
|
||||
:placeholder="$t('general.street_2')"
|
||||
name="billing_street2"
|
||||
rows="2"
|
||||
/>
|
||||
</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'
|
||||
import Ls from '../../services/ls'
|
||||
const { required, minLength, email } = 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_id: '',
|
||||
state_id: '',
|
||||
country_id: '',
|
||||
zip: '',
|
||||
phone: ''
|
||||
},
|
||||
loading: false,
|
||||
step: 1,
|
||||
countries: [],
|
||||
country: null,
|
||||
states: [],
|
||||
state: null,
|
||||
cities: [],
|
||||
city: null,
|
||||
previewLogo: null,
|
||||
isDisabledCity: true,
|
||||
isDisabledState: true
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
companyData: {
|
||||
name: {
|
||||
required
|
||||
},
|
||||
country_id: {
|
||||
required
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
country ({ id }) {
|
||||
this.companyData.country_id = id
|
||||
this.state = null
|
||||
this.city = null
|
||||
if (id !== null && id !== undefined) {
|
||||
this.isDisabledState = false
|
||||
return true
|
||||
}
|
||||
this.isDisabledState = true
|
||||
return true
|
||||
},
|
||||
state (newState) {
|
||||
if (newState !== null && newState !== undefined) {
|
||||
this.city = null
|
||||
this.companyData.state_id = newState.id
|
||||
this.isDisabledCity = false
|
||||
return true
|
||||
}
|
||||
this.companyData.state_id = null
|
||||
this.isDisabledCity = true
|
||||
this.cities = []
|
||||
this.city = null
|
||||
this.companyData.city_id = null
|
||||
return true
|
||||
},
|
||||
city (newCity) {
|
||||
if (newCity !== null && newCity !== undefined) {
|
||||
this.companyData.city_id = newCity.id
|
||||
return true
|
||||
}
|
||||
this.companyData.city_id = null
|
||||
return true
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.fetchCountry()
|
||||
},
|
||||
methods: {
|
||||
cropperHandler (cropper) {
|
||||
this.previewLogo = cropper.getCroppedCanvas().toDataURL(this.cropperOutputMime)
|
||||
},
|
||||
setFileObject (file) {
|
||||
this.fileObject = file
|
||||
},
|
||||
async next () {
|
||||
this.$v.companyData.$touch()
|
||||
if (this.$v.companyData.$invalid) {
|
||||
return true
|
||||
}
|
||||
this.loading = true
|
||||
let data = new FormData()
|
||||
data.append('logo', this.fileObject)
|
||||
data.append('name', this.companyData.name)
|
||||
data.append('address_street_1', this.companyData.address_street_1)
|
||||
data.append('address_street_2', this.companyData.address_street_2)
|
||||
data.append('city_id', this.companyData.city_id)
|
||||
data.append('state_id', this.companyData.state_id)
|
||||
data.append('country_id', this.companyData.country_id)
|
||||
data.append('zip', this.companyData.zip)
|
||||
data.append('phone', this.companyData.phone)
|
||||
|
||||
let response = await window.axios.post('/api/admin/onboarding/company', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data) {
|
||||
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
|
||||
}
|
||||
},
|
||||
async fetchState () {
|
||||
this.$v.companyData.country_id.$touch()
|
||||
let res = await window.axios.get(`/api/states/${this.country.id}`)
|
||||
if (res) {
|
||||
this.states = res.data.states
|
||||
}
|
||||
},
|
||||
async fetchCities () {
|
||||
if (this.state === null || this.state === undefined) {
|
||||
return false
|
||||
}
|
||||
let res = await window.axios.get(`/api/cities/${this.state.id}`)
|
||||
if (res) {
|
||||
this.cities = res.data.cities
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
216
resources/assets/js/views/wizard/Database.vue
Normal file
216
resources/assets/js/views/wizard/Database.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<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.url" 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>
|
||||
<span class="text-danger"> *</span>
|
||||
<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: null
|
||||
},
|
||||
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,
|
||||
url
|
||||
}
|
||||
}
|
||||
},
|
||||
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 {
|
||||
window.toastr['error'](this.$t('wizard.errors.' + response.data.error))
|
||||
}
|
||||
this.loading = false
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
window.toastr['error']('Somethig went wrong')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
208
resources/assets/js/views/wizard/EmailConfiguration.vue
Normal file
208
resources/assets/js/views/wizard/EmailConfiguration.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<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>
|
||||
<div class="row my-2 mt-5">
|
||||
<div class="col-md-6 my-2">
|
||||
<label class="form-label">{{ $t('wizard.mail.driver') }}</label>
|
||||
<span class="text-danger"> *</span>
|
||||
<base-select
|
||||
v-model="mailConfigData.mail_driver"
|
||||
:invalid="$v.mailConfigData.mail_driver.$error"
|
||||
:options="mail_drivers"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
@change="$v.mailConfigData.mail_driver.$touch()"
|
||||
/>
|
||||
<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('wizard.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('wizard.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('wizard.mail.password') }}</label>
|
||||
<span class="text-danger"> *</span>
|
||||
<base-input
|
||||
:invalid="$v.mailConfigData.mail_password.$error"
|
||||
v-model.trim="mailConfigData.mail_password"
|
||||
type="mail_password"
|
||||
name="name"
|
||||
@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('wizard.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('wizard.mail.encryption') }}</label>
|
||||
<span class="text-danger"> *</span>
|
||||
<base-input
|
||||
:invalid="$v.mailConfigData.mail_encryption.$error"
|
||||
v-model.trim="mailConfigData.mail_encryption"
|
||||
type="text"
|
||||
name="name"
|
||||
@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>
|
||||
<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'
|
||||
import Ls from '../../services/ls'
|
||||
const { required, email, numeric } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MultiSelect
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
mailConfigData: {
|
||||
mail_driver: 'smtp',
|
||||
mail_host: 'mailtrap.io',
|
||||
mail_port: 2525,
|
||||
mail_username: 'cc3c64516febd4',
|
||||
mail_password: 'e6a0176301f587',
|
||||
mail_encryption: 'tls'
|
||||
},
|
||||
loading: false,
|
||||
mail_drivers: []
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
mailConfigData: {
|
||||
mail_driver: {
|
||||
required
|
||||
},
|
||||
mail_host: {
|
||||
required
|
||||
},
|
||||
mail_port: {
|
||||
required,
|
||||
numeric
|
||||
},
|
||||
mail_username: {
|
||||
required
|
||||
},
|
||||
mail_password: {
|
||||
required
|
||||
},
|
||||
mail_encryption: {
|
||||
required
|
||||
}
|
||||
}
|
||||
},
|
||||
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 () {
|
||||
this.$v.mailConfigData.$touch()
|
||||
if (this.$v.mailConfigData.$invalid) {
|
||||
return true
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
let response = await window.axios.post('/api/admin/onboarding/environment/mail', this.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']('Somethig went wrong')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
115
resources/assets/js/views/wizard/Index.vue
Normal file
115
resources/assets/js/views/wizard/Index.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="wizard">
|
||||
<div class="step-indicator">
|
||||
<img
|
||||
id="logo-crater"
|
||||
src="/assets/img/crater-logo.png"
|
||||
alt="Crater Logo"
|
||||
width="225"
|
||||
height="50"
|
||||
class="logo"
|
||||
>
|
||||
<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>
|
||||
69
resources/assets/js/views/wizard/Permission.vue
Normal file
69
resources/assets/js/views/wizard/Permission.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<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="!errors"
|
||||
class="pull-right mt-5"
|
||||
icon="arrow-right"
|
||||
right-icon
|
||||
color="theme"
|
||||
@click="next"
|
||||
>
|
||||
{{ $t('wizard.continue') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Ls from '../../services/ls'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
permissions: [],
|
||||
errors: 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
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async next () {
|
||||
this.loading = true
|
||||
await this.$emit('next')
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
209
resources/assets/js/views/wizard/Settings.vue
Normal file
209
resources/assets/js/views/wizard/Settings.vue
Normal file
@@ -0,0 +1,209 @@
|
||||
<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"
|
||||
: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'
|
||||
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: {
|
||||
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.$router.push('/admin/dashboard')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
99
resources/assets/js/views/wizard/SystemRequirement.vue
Normal file
99
resources/assets/js/views/wizard/SystemRequirement.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<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="requirements"
|
||||
:loading="loading"
|
||||
class="pull-right mt-4"
|
||||
icon="arrow-right"
|
||||
color="theme"
|
||||
right-icon
|
||||
@click="next"
|
||||
>
|
||||
{{ $t('wizard.continue') }}
|
||||
</base-button>
|
||||
<base-button
|
||||
v-else
|
||||
: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'
|
||||
import Ls from '../../services/ls'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MultiSelect
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
requirements: null,
|
||||
phpSupportInfo: null,
|
||||
loading: false,
|
||||
isShow: true
|
||||
}
|
||||
},
|
||||
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>
|
||||
141
resources/assets/js/views/wizard/UserProfile.vue
Normal file
141
resources/assets/js/views/wizard/UserProfile.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<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">
|
||||
<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.required') }}</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>
|
||||
</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 MultiSelect from 'vue-multiselect'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import Ls from '../../services/ls'
|
||||
const { required, requiredIf, sameAs, minLength, email } = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MultiSelect
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
data () {
|
||||
return {
|
||||
profileData: {
|
||||
name: null,
|
||||
email: null,
|
||||
password: null,
|
||||
confirm_password: null
|
||||
},
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
profileData: {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(3)
|
||||
},
|
||||
email: {
|
||||
email,
|
||||
required
|
||||
},
|
||||
password: {
|
||||
required
|
||||
},
|
||||
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: {
|
||||
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)
|
||||
if (response.data) {
|
||||
this.$emit('next')
|
||||
this.loading = false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user