init crater

This commit is contained in:
Mohit Panjwani
2019-11-11 12:16:00 +05:30
commit bdf2ba51d6
668 changed files with 158503 additions and 0 deletions

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@ -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 &amp; 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>

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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