build version 400

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

View File

@@ -1,11 +1,7 @@
<template>
<form
id="loginForm"
@submit.prevent="validateBeforeSubmit"
>
<div :class="{'form-group' : true }">
<base-input
<form id="loginForm" @submit.prevent="validateBeforeSubmit">
<div class="mb-4">
<sw-input
:invalid="$v.formData.email.$error"
v-model.lazy="formData.email"
:disabled="isSent"
@@ -15,22 +11,27 @@
@blur="$v.formData.email.$touch()"
/>
<div v-if="$v.formData.email.$error">
<span v-if="!$v.formData.email.required" class="help-block text-danger">
<span v-if="!$v.formData.email.required" class="text-sm text-danger">
{{ $t('validation.required') }}
</span>
<span v-if="!$v.formData.email.email" class="help-block text-danger">
<span v-if="!$v.formData.email.email" class="text-sm text-danger">
{{ $t('validation.email_incorrect') }}
</span>
</div>
</div>
<base-button v-if="!isSent" :loading="isLoading" :disabled="isLoading" type="submit" color="theme">
<sw-button
v-if="!isSent"
:disabled="isLoading"
type="submit"
variant="primary"
>
{{ $t('validation.send_reset_link') }}
</base-button>
<base-button v-else :loading="isLoading" :disabled="isLoading" color="theme" type="submit">
</sw-button>
<sw-button v-else :disabled="isLoading" variant="primary" type="submit">
{{ $t('validation.not_yet') }}
</base-button>
</sw-button>
<div class="other-actions mb-4">
<div class="mt-4 mb-4 text-sm">
<router-link to="/login">
{{ $t('general.back_to_login') }}
</router-link>
@@ -39,43 +40,45 @@
</template>
<script type="text/babel">
import { validationMixin } from 'vuelidate'
import { async } from 'q'
import { mapActions } from 'vuex'
const { required, email } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
data () {
data() {
return {
formData: {
email: ''
email: '',
},
isSent: false,
isLoading: false,
isRegisteredUser: false
isRegisteredUser: false,
}
},
validations: {
formData: {
email: {
email,
required
}
}
required,
},
},
},
methods: {
async validateBeforeSubmit (e) {
...mapActions('auth', ['checkMail']),
async validateBeforeSubmit(e) {
this.$v.formData.$touch()
if (await this.checkMail() === false) {
let { data } = await this.checkMail()
if (data === false) {
toastr['error'](this.$t('validation.email_does_not_exist'))
return
}
if (!this.$v.formData.$invalid) {
try {
this.isLoading = true
let res = await axios.post('/api/auth/password/email', this.formData)
let res = await axios.post(
'/api/v1/auth/password/email',
this.formData
)
if (res.data) {
toastr['success']('Mail sent successfuly!', 'Success')
@@ -90,10 +93,13 @@ export default {
}
}
},
async checkMail () {
let response = await window.axios.post('/api/is-registered', this.formData)
return response.data
}
}
// async checkMail() {
// let response = await window.axios.post(
// '/api/v1/is-registered',
// this.formData
// )
// return response.data
// },
},
}
</script>

View File

@@ -1,126 +1,168 @@
<template>
<form
id="loginForm"
@submit.prevent="validateBeforeSubmit"
>
<div :class="{'form-group' : true }">
<p class="input-label">{{ $t('login.email') }} <span class="text-danger"> * </span></p>
<base-input
<form id="loginForm" @submit.prevent="validateBeforeSubmit">
<sw-input-group
:label="$t('login.email')"
:error="emailError"
class="mb-4"
required
>
<sw-input
:invalid="$v.loginData.email.$error"
v-model="loginData.email"
:placeholder="$t(login.login_placeholder)"
v-model="loginData.email"
focus
type="email"
name="email"
@input="$v.loginData.email.$touch()"
/>
<div v-if="$v.loginData.email.$error">
<span v-if="!$v.loginData.email.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
<span v-if="!$v.loginData.email.email" class="text-danger">
{{ $tc('validation.email_incorrect') }}
</span>
</div>
</div>
<div class="form-group">
<p class="input-label">{{ $t('login.password') }} <span class="text-danger"> * </span></p>
<base-input
</sw-input-group>
<sw-input-group
:label="$t('login.password')"
:error="passwordError"
class="mb-4"
required
>
<sw-input
v-model="loginData.password"
:invalid="$v.loginData.password.$error"
type="password"
:type="getInputType"
name="password"
show-password
@input="$v.loginData.password.$touch()"
/>
<div v-if="$v.loginData.password.$error">
<span v-if="!$v.loginData.password.required" class="text-danger">{{ $tc('validation.required') }}</span>
<span v-if="!$v.loginData.password.minLength" class="text-danger"> {{ $tc('validation.password_min_length', $v.loginData.password.$params.minLength.min, {count: $v.loginData.password.$params.minLength.min}) }} </span>
</div>
</div>
<div class="other-actions row">
<div class="col-sm-12 text-sm-left mb-4">
<router-link to="forgot-password" class="forgot-link">
>
<template v-slot:rightIcon>
<eye-off-icon
v-if="isShowPassword"
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
<eye-icon
v-else
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
</template>
</sw-input>
</sw-input-group>
<div class="mt-5 mb-8">
<div class="mb-4">
<router-link
to="forgot-password"
class="text-sm text-primary-400 hover:text-gray-700"
>
{{ $t('login.forgot_password') }}
</router-link>
</div>
</div>
<base-button :loading="isLoading" type="submit" color="theme">{{ $t('login.login') }}</base-button>
<!-- <div class="social-links">
<span class="link-text">{{ $t('login.or_signIn_with') }}</span>
<div class="social-logo">
<icon-facebook class="icon"/>
<icon-twitter class="icon"/>
<icon-google class="icon"/>
</div>
</div> -->
<sw-button
:loading="isLoading"
:disabled="isLoading"
type="submit"
variant="primary"
>
{{ $t('login.login') }}
</sw-button>
</form>
</template>
<script type="text/babel">
import { mapActions } from 'vuex'
import { EyeIcon, EyeOffIcon } from '@vue-hero-icons/outline'
import IconFacebook from '../../components/icon/facebook'
import IconTwitter from '../../components/icon/twitter'
import IconGoogle from '../../components/icon/google'
import { validationMixin } from 'vuelidate'
const { required, email, minLength } = require('vuelidate/lib/validators')
export default {
components: {
IconFacebook,
IconTwitter,
IconGoogle
IconGoogle,
EyeIcon,
EyeOffIcon,
},
mixins: [validationMixin],
data () {
data() {
return {
loginData: {
email: '',
password: '',
remember: ''
remember: '',
},
submitted: false,
isLoading: false
isLoading: false,
isShowPassword: false,
}
},
validations: {
loginData: {
email: {
required,
email
email,
},
password: {
required,
minLength: minLength(8)
minLength: minLength(8),
},
},
},
computed: {
emailError() {
if (!this.$v.loginData.email.$error) {
return ''
}
}
if (!this.$v.loginData.email.required) {
return this.$tc('validation.required')
}
if (!this.$v.loginData.email.email) {
return this.$tc('validation.email_incorrect')
}
},
passwordError() {
if (!this.$v.loginData.password.$error) {
return ''
}
if (!this.$v.loginData.password.required) {
return this.$tc('validation.required')
}
if (!this.$v.loginData.password.minLength) {
return this.$tc(
'validation.password_min_length',
this.$v.loginData.password.$params.minLength.min,
{ count: this.$v.loginData.password.$params.minLength.min }
)
}
},
getInputType() {
if (this.isShowPassword) {
return 'text'
}
return 'password'
},
},
methods: {
...mapActions('auth', [
'login'
]),
async validateBeforeSubmit () {
...mapActions('auth', ['login']),
async validateBeforeSubmit() {
axios.defaults.withCredentials = true
this.$v.loginData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
this.login(this.loginData).then((res) => {
try {
await this.login(this.loginData)
this.$router.push('/admin/dashboard')
this.isLoading = false
}).catch(() => {
} catch (error) {
this.isLoading = false
})
}
}
}
},
},
}
</script>

View File

@@ -1,9 +1,5 @@
<template>
<form
id="registerForm"
action=""
method="post"
>
<form id="registerForm" action="" method="post">
<!-- {{ csrf_field() }} -->
<div class="form-group">
<input
@@ -11,7 +7,7 @@
type="email"
class="form-control form-control-danger"
name="email"
>
/>
</div>
<div class="form-group">
<input
@@ -20,7 +16,7 @@
class="form-control form-control-danger"
placeholder="Enter Password"
name="password"
>
/>
</div>
<div class="form-group">
<input
@@ -28,30 +24,32 @@
class="form-control form-control-danger"
placeholder="Retype Password"
name="password_confirmation"
>
/>
</div>
<base-button class="btn btn-login btn-full">{{ $t('login.register') }}</base-button>
<sw-button class="btn btn-login btn-full">{{
$t('login.register')
}}</sw-button>
</form>
</template>
<script type="text/babel">
export default {
data () {
data() {
return {
name: '',
email: '',
password: '',
password_confirmation: ''
password_confirmation: '',
}
},
methods: {
validateBeforeSubmit (e) {
validateBeforeSubmit(e) {
this.$validator.validateAll().then((result) => {
if (result) {
// eslint-disable-next-line
alert('Form Submitted!')
}
})
}
}
},
},
}
</script>

View File

@@ -1,10 +1,7 @@
<template>
<form
id="loginForm"
@submit.prevent="validateBeforeSubmit"
>
<form id="loginForm" @submit.prevent="validateBeforeSubmit">
<div class="form-group">
<base-input
<sw-input
v-model.trim="formData.email"
:invalid="$v.formData.email.$error"
:placeholder="$t('login.enter_email')"
@@ -22,7 +19,7 @@
</div>
</div>
<div class="form-group">
<base-input
<sw-input
id="password"
v-model.trim="formData.password"
:invalid="$v.formData.password.$error"
@@ -32,16 +29,28 @@
@input="$v.formData.password.$touch()"
/>
<div v-if="$v.formData.password.$error">
<span v-if="!$v.formData.password.required" class="help-block text-danger">
<span
v-if="!$v.formData.password.required"
class="help-block text-danger"
>
{{ $t('validation.required') }}
</span>
<span v-if="!$v.formData.password.minLength" class="help-block text-danger">
{{ $tc('validation.password_length', $v.formData.password.minLength.min, { count: $v.formData.password.$params.minLength.min }) }}
<span
v-if="!$v.formData.password.minLength"
class="help-block text-danger"
>
{{
$tc(
'validation.password_length',
$v.formData.password.minLength.min,
{ count: $v.formData.password.$params.minLength.min }
)
}}
</span>
</div>
</div>
<div class="form-group">
<base-input
<sw-input
v-model.trim="formData.password_confirmation"
:invalid="$v.formData.password_confirmation.$error"
:placeholder="$t('login.retype_password')"
@@ -50,50 +59,56 @@
@input="$v.formData.password_confirmation.$touch()"
/>
<div v-if="$v.formData.password_confirmation.$error">
<span v-if="!$v.formData.password_confirmation.sameAsPassword" class="help-block text-danger">
<span
v-if="!$v.formData.password_confirmation.sameAsPassword"
class="help-block text-danger"
>
{{ $t('validation.password_incorrect') }}
</span>
</div>
</div>
<base-button :loading="isLoading" type="submit" color="theme">
<sw-button type="submit" variant="primary">
{{ $t('login.reset_password') }}
</base-button>
</sw-button>
</form>
</template>
<script type="text/babel">
import { validationMixin } from 'vuelidate'
const { required, email, sameAs, minLength } = require('vuelidate/lib/validators')
const {
required,
email,
sameAs,
minLength,
} = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
data () {
data() {
return {
formData: {
email: '',
password: '',
password_confirmation: ''
password_confirmation: '',
},
isLoading: false
isLoading: false,
}
},
validations: {
formData: {
email: {
required,
email
email,
},
password: {
required,
minLength: minLength(8)
minLength: minLength(8),
},
password_confirmation: {
sameAsPassword: sameAs('password')
}
}
sameAsPassword: sameAs('password'),
},
},
},
methods: {
async validateBeforeSubmit (e) {
async validateBeforeSubmit(e) {
this.$v.formData.$touch()
if (!this.$v.formData.$invalid) {
@@ -102,23 +117,29 @@ export default {
email: this.formData.email,
password: this.formData.password,
password_confirmation: this.formData.password_confirmation,
token: this.$route.params.token
token: this.$route.params.token,
}
this.isLoading = true
let res = await axios.post('/api/auth/reset/password', data)
let res = await axios.post('/api/v1/auth/reset/password', data)
this.isLoading = false
if (res.data) {
toastr['success'](this.$t('login.password_reset_successfully'), 'Success')
toastr['success'](
this.$t('login.password_reset_successfully'),
'Success'
)
this.$router.push('/login')
}
} catch (err) {
if (err.response && err.response.status === 403) {
toastr['error'](err.response.data, this.$t('validation.email_incorrect'))
toastr['error'](
err.response.data,
this.$t('validation.email_incorrect')
)
this.isLoading = false
}
}
}
}
}
},
},
}
</script>

View File

@@ -1,120 +0,0 @@
<template>
<div class="imgbox">
<vue-dropzone
id="dropzone"
ref="myVueDropzone"
:include-styling="true"
:options="dropzoneOptions"
@vdropzone-sending="sendingEvent"
@vdropzone-success="successEvent"
@vdropzone-max-files-exceeded="maximum"
@vdropzone-file-added="getCustomeFile"
@vdropzone-removed-file="removeFile"
/>
</div>
</template>
<script>
import vue2Dropzone from 'vue2-dropzone'
import 'vue2-dropzone/dist/vue2Dropzone.min.css'
export default {
components: {
vueDropzone: vue2Dropzone
},
props: {
additionaldata: {
type: Array,
default () {
return []
}
},
url: {
type: String,
default () {
return ''
}
},
router: {
type: Object,
default: null
},
paramname: {
type: String,
default () {
return ''
}
},
acceptedfiles: {
type: String,
default () {
return ''
}
},
dictdefaultmessage: {
type: String,
default () {
return ''
}
},
autoprocessqueue: {
type: Boolean,
default: true
},
method: {
type: String,
default: 'POST'
}
},
data () {
return {
dropzoneOptions: {
autoProcessQueue: this.autoprocessqueue,
url: this.url,
thumbnailWidth: 110,
maxFiles: 1,
paramName: this.paramname,
acceptedFiles: this.acceptedfiles,
uploadMultiple: false,
dictDefaultMessage: '<font-awesome-icon icon="trash"/> ' + this.dictdefaultmessage,
dictInvalidFileType: 'This file type is not supported.',
dictFileTooBig: 'File size too Big',
addRemoveLinks: true,
method: this.method,
headers: { 'Authorization': `Bearer ${window.Ls.get('auth.token')}`, 'Company': `${window.Ls.get('selectedCompany')}` }
}
}
},
watch: {
url (newURL) {
this.$refs.myVueDropzone.options.url = newURL
}
},
created () {
window.hub.$on('sendFile', this.customeSend)
},
methods: {
sendingEvent (file, xhr, formData) {
var i
for (i = 0; i < this.additionaldata.length; i++) {
for (var key in this.additionaldata[i]) {
formData.append(key, this.additionaldata[i][key])
}
}
},
successEvent (file, response) {
// window.toastr['success'](response.success)
},
maximum (file) {
this.$refs.myVueDropzone.removeFile(file)
},
getCustomeFile (file) {
this.$emit('takefile', true)
},
removeFile (file, error, xhr) {
this.$emit('takefile', false)
},
customeSend () {
this.$refs.myVueDropzone.processQueue()
}
}
}
</script>

View File

@@ -1,71 +0,0 @@
<template>
<div class="form-group image-radio">
<div
v-for="(pdfStyleList, index) in pdfStyleLists"
:key="index"
class="radio"
>
<label :for="pdfStyleList.val">
<input
v-model="checkedID"
:value="pdfStyleList.val"
:id="pdfStyleList.val"
:checked="pdfStyleList.val == checkedID"
type="radio"
name="pdfSet"
class="hidden"
>
<img
:src="srcMaker(pdfStyleList.src)"
alt="No Image"
class="special-img"
>
</label>
</div>
</div>
</template>
<script>
export default{
props: {
currentPDF: {
type: String,
default: ''
}
},
data () {
return {
pdfStyleLists: [
{src: 'assets/img/PDF/Invoice1.png', val: '1'},
{src: 'assets/img/PDF/Invoice2.png', val: '2'},
{src: 'assets/img/PDF/Invoice3.png', val: '3'},
{src: 'assets/img/PDF/Invoice4.png', val: '4'}
],
checkedID: ''
}
},
watch: {
checkedID (newID) {
if (newID !== null) {
this.$emit('selectedPDF', newID)
}
}
},
mounted () {
setTimeout(() => {
if (this.currentPDF === '') {
this.checkedID = null
} else {
this.checkedID = this.currentPDF
}
}, 1000)
},
methods: {
srcMaker (file) {
var url = '/'
var full = url + '' + file
return full
}
}
}
</script>

View File

@@ -1,129 +0,0 @@
<template>
<div class="setting-list-box">
<div id="myApp list-box-container">
<!-- <v-select
:value.sync="selected"
:options="list"
:on-change ="setValue"
/> -->
</div>
</div>
</template>
<script>
// import vSelect from 'vue-select'
export default {
// components: {vSelect},
props: {
type: {
type: String,
default: null
},
Options: {
type: [Array, Object],
required: false,
default () {
return []
}
},
getData: {
type: Object,
default () {
return {}
}
},
currentData: {
type: [String, Number],
default: null
}
},
data () {
return {
selected: null,
list: []
}
},
mounted () {
window.setTimeout(() => {
this.setList()
if (this.currentData !== null || this.currentData !== '') {
this.defaultValue(this.currentData)
}
}, 1000)
},
methods: {
setList () {
if (this.type === 'currencies') {
for (let i = 0; i < this.Options.length; i++) {
this.list.push(this.Options[i].name + ' - ' + this.Options[i].code)
}
} else if (this.type === 'time_zones' || this.type === 'languages' || this.type === 'date_formats') {
for (let key in this.Options) {
this.list.push(this.Options[key])
}
}
},
setValue (val) {
if (this.type === 'currencies') {
for (let i = 0; i < this.Options.length; i++) {
if (val === this.Options[i].name + ' - ' + this.Options[i].code) {
this.getData.currency = this.Options[i].id
break
}
}
} else if (this.type === 'time_zones') {
for (let key in this.Options) {
if (val === this.Options[key]) {
this.getData.time_zone = key
break
}
}
} else if (this.type === 'languages') {
for (let key in this.Options) {
if (val === this.Options[key]) {
this.getData.language = key
break
}
}
} else if (this.type === 'date_formats') {
for (let key in this.Options) {
if (val === this.Options[key]) {
this.getData.date_format = key
break
}
}
}
},
defaultValue (val) {
if (this.type === 'currencies') {
for (let i = 0; i < this.Options.length; i++) {
if (Number(val) === this.Options[i].id) {
this.selected = this.Options[i].name + ' - ' + this.Options[i].code
break
}
}
} else if (this.type === 'time_zones') {
for (let key in this.Options) {
if (val === key) {
this.selected = this.Options[key]
break
}
}
} else if (this.type === 'languages') {
for (let key in this.Options) {
if (val === key) {
this.selected = this.Options[key]
break
}
}
} else if (this.type === 'date_formats') {
for (let key in this.Options) {
if (val === key) {
this.selected = this.Options[key]
break
}
}
}
}
}
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,258 +1,322 @@
<template>
<div class="customer-create main-content">
<div class="page-header">
<h3 class="page-title">{{ $t('customers.title') }}</h3>
<ol class="breadcrumb">
<li class="breadcrumb-item">
<router-link
slot="item-title"
to="dashboard">
{{ $t('general.home') }}
</router-link>
</li>
<li class="breadcrumb-item">
<router-link
slot="item-title"
to="#">
{{ $tc('customers.customer',2) }}
</router-link>
</li>
</ol>
<div class="page-actions row">
<div class="col-xs-2 mr-4">
<base-button
v-show="totalCustomers || filtersApplied"
:outline="true"
:icon="filterIcon"
size="large"
color="theme"
right-icon
@click="toggleFilter"
>
{{ $t('general.filter') }}
</base-button>
</div>
<router-link slot="item-title" class="col-xs-2" to="customers/create">
<base-button
size="large"
icon="plus"
color="theme">
{{ $t('customers.new_customer') }}
</base-button>
</router-link>
</div>
</div>
<base-page class="customer-create">
<sw-page-header :title="$t('customers.title')">
<sw-breadcrumb slot="breadcrumbs">
<sw-breadcrumb-item to="dashboard" :title="$t('general.home')" />
<sw-breadcrumb-item
to="#"
:title="$tc('customers.customer', 2)"
active
/>
</sw-breadcrumb>
<transition name="fade">
<div v-show="showFilters" class="filter-section">
<div class="row">
<div class="col-sm-4">
<label class="form-label">{{ $t('customers.display_name') }}</label>
<base-input
v-model="filters.display_name"
type="text"
name="name"
autocomplete="off"
/>
</div>
<div class="col-sm-4">
<label class="form-label">{{ $t('customers.contact_name') }}</label>
<base-input
v-model="filters.contact_name"
type="text"
name="address_name"
autocomplete="off"
/>
</div>
<div class="col-sm-4">
<label class="form-label">{{ $t('customers.phone') }}</label>
<base-input
v-model="filters.phone"
type="text"
name="phone"
autocomplete="off"
/>
</div>
<label class="clear-filter" @click="clearFilter">{{ $t('general.clear_all') }}</label>
</div>
</div>
</transition>
<div v-cloak v-show="showEmptyScreen" class="col-xs-1 no-data-info" align="center">
<astronaut-icon class="mt-5 mb-4"/>
<div class="row" align="center">
<label class="col title">{{ $t('customers.no_customers') }}</label>
</div>
<div class="row">
<label class="description col mt-1" align="center">{{ $t('customers.list_of_customers') }}</label>
</div>
<div class="btn-container">
<base-button
:outline="true"
color="theme"
class="mt-3"
size="large"
@click="$router.push('customers/create')"
<template slot="actions">
<sw-button
v-show="totalCustomers"
size="lg"
variant="primary-outline"
@click="toggleFilter"
>
{{ $t('customers.add_new_customer') }}
</base-button>
</div>
</div>
{{ $t('general.filter') }}
<component :is="filterIcon" class="h-4 ml-1 -mr-1 font-bold" />
</sw-button>
<div v-show="!showEmptyScreen" class="table-container">
<div class="table-actions mt-5">
<p class="table-stats">{{ $t('general.showing') }}: <b>{{ customers.length }}</b> {{ $t('general.of') }} <b>{{ totalCustomers }}</b></p>
<sw-button
tag-name="router-link"
to="customers/create"
size="lg"
variant="primary"
class="ml-4"
>
<plus-sm-icon class="h-6 mr-1 -ml-2 font-bold" />
{{ $t('customers.new_customer') }}
</sw-button>
</template>
</sw-page-header>
<transition name="fade">
<v-dropdown v-if="selectedCustomers.length" :show-arrow="false">
<span slot="activator" href="#" class="table-actions-button dropdown-toggle">
<slide-y-up-transition>
<sw-filter-wrapper v-show="showFilters">
<sw-input-group
:label="$t('customers.display_name')"
class="flex-1 mt-2"
>
<sw-input
v-model="filters.display_name"
type="text"
name="name"
class="mt-2"
autocomplete="off"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.contact_name')"
class="flex-1 mt-2 ml-0 lg:ml-6"
>
<sw-input
v-model="filters.contact_name"
type="text"
name="address_name"
class="mt-2"
autocomplete="off"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.phone')"
class="flex-1 mt-2 ml-0 lg:ml-6"
>
<sw-input
v-model="filters.phone"
type="text"
name="phone"
class="mt-2"
autocomplete="off"
/>
</sw-input-group>
<label
class="absolute text-sm leading-snug text-black cursor-pointer"
style="top: 10px; right: 15px"
@click="clearFilter"
>{{ $t('general.clear_all') }}</label
>
</sw-filter-wrapper>
</slide-y-up-transition>
<sw-empty-table-placeholder
v-show="showEmptyScreen"
:title="$t('customers.no_customers')"
:description="$t('customers.list_of_customers')"
>
<astronaut-icon class="mt-5 mb-4" />
<sw-button
slot="actions"
tag-name="router-link"
to="/admin/customers/create"
size="lg"
variant="primary-outline"
>
{{ $t('customers.add_new_customer') }}
</sw-button>
</sw-empty-table-placeholder>
<div v-show="!showEmptyScreen" class="relative table-container">
<div
class="relative flex items-center justify-between h-10 mt-5 border-b-2 border-gray-200 border-solid"
>
<p class="text-sm">
{{ $t('general.showing') }}: <b>{{ customers.length }}</b>
{{ $t('general.of') }} <b>{{ totalCustomers }}</b>
</p>
<sw-transition type="fade">
<sw-dropdown v-if="selectedCustomers.length">
<span
slot="activator"
class="flex block text-sm font-medium cursor-pointer select-none text-primary-400"
>
{{ $t('general.actions') }}
<chevron-down-icon class="h-5" />
</span>
<v-dropdown-item>
<div class="dropdown-item" @click="removeMultipleCustomers">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
</transition>
<sw-dropdown-item @click="removeMultipleCustomers">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</sw-transition>
</div>
<div class="custom-control custom-checkbox">
<input
id="select-all"
<div class="absolute z-10 items-center pl-4 mt-2 select-none md:mt-12">
<sw-checkbox
v-model="selectAllFieldStatus"
type="checkbox"
class="custom-control-input"
variant="primary"
size="sm"
class="hidden md:inline"
@change="selectAllCustomers"
>
<label for="select-all" class="custom-control-label selectall">
<span class="select-all-label">{{ $t('general.select_all') }} </span>
</label>
/>
<sw-checkbox
v-model="selectAllFieldStatus"
:label="$t('general.select_all')"
variant="primary"
size="sm"
class="md:hidden"
@change="selectAllCustomers"
/>
</div>
<table-component
<sw-table-component
ref="table"
:show-filter="false"
:data="fetchData"
table-class="table"
>
<table-column
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="no-click"
>
<template slot-scope="row">
<div class="custom-control custom-checkbox">
<input
:id="row.id"
v-model="selectField"
:value="row.id"
type="checkbox"
class="custom-control-input"
>
<label :for="row.id" class="custom-control-label" />
</div>
</template>
</table-column>
<table-column
<div class="relative block" slot-scope="row">
<sw-checkbox
:id="row.id"
v-model="selectField"
:value="row.id"
variant="primary"
size="sm"
/>
</div>
</sw-table-column>
<sw-table-column
:sortable="true"
:filterable="true"
:label="$t('customers.display_name')"
show="name"
/>
<table-column
>
<template slot-scope="row">
<span>{{ $t('customers.display_name') }}</span>
<router-link
:to="{ path: `customers/${row.id}/view` }"
class="font-medium text-primary-500"
>
{{ row.name }}
</router-link>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('customers.contact_name')"
show="contact_name"
/>
<table-column
>
<template slot-scope="row">
<span>{{ $t('customers.contact_name') }}</span>
<span>
{{ row.contact_name ? row.contact_name : 'No Contact Name' }}
</span>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('customers.phone')"
show="phone"
/>
<table-column
>
<template slot-scope="row">
<span>{{ $t('customers.phone') }}</span>
<span>
{{ row.phone ? row.phone : 'No Contact' }}
</span>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('customers.amount_due')"
show="due_amount"
>
<template slot-scope="row">
<span> {{ $t('customers.amount_due') }} </span>
<div v-html="$utils.formatMoney(row.due_amount, row.currency)"/>
<div v-html="$utils.formatMoney(row.due_amount, row.currency)" />
</template>
</table-column>
<table-column
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('customers.added_on')"
sort-as="created_at"
show="formattedCreatedAt"
/>
<table-column
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span> {{ $t('customers.action') }} </span>
<v-dropdown>
<a slot="activator" href="#">
<dot-icon />
</a>
<v-dropdown-item>
<router-link :to="{path: `customers/${row.id}/edit`}" class="dropdown-item">
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon"/>
{{ $t('general.edit') }}
</router-link>
<sw-dropdown>
<dot-icon slot="activator" />
</v-dropdown-item>
<v-dropdown-item>
<div class="dropdown-item" @click="removeCustomer(row.id)">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
<sw-dropdown-item
tag-name="router-link"
:to="`customers/${row.id}/edit`"
>
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item
tag-name="router-link"
:to="`customers/${row.id}/view`"
>
<eye-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.view') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removeCustomer(row.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</table-column>
</table-component>
</sw-table-column>
</sw-table-component>
</div>
</div>
</base-page>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { SweetModal, SweetModalTab } from 'sweet-modal-vue'
import DotIcon from '../../components/icon/DotIcon'
import { PlusSmIcon } from '@vue-hero-icons/solid'
import {
FilterIcon,
XIcon,
ChevronDownIcon,
TrashIcon,
PencilIcon,
EyeIcon,
} from '@vue-hero-icons/solid'
import AstronautIcon from '../../components/icon/AstronautIcon'
import BaseButton from '../../../js/components/base/BaseButton'
import { request } from 'http'
export default {
components: {
DotIcon,
AstronautIcon,
SweetModal,
SweetModalTab,
BaseButton
ChevronDownIcon,
PlusSmIcon,
FilterIcon,
XIcon,
TrashIcon,
PencilIcon,
EyeIcon,
},
data () {
data() {
return {
showFilters: false,
filtersApplied: false,
isRequestOngoing: true,
filters: {
display_name: '',
contact_name: '',
phone: ''
}
phone: '',
},
}
},
computed: {
showEmptyScreen () {
return !this.totalCustomers && !this.isRequestOngoing && !this.filtersApplied
showEmptyScreen() {
return !this.totalCustomers && !this.isRequestOngoing
},
filterIcon () {
return (this.showFilters) ? 'times' : 'filter'
filterIcon() {
return this.showFilters ? 'x-icon' : 'filter-icon'
},
...mapGetters('customer', [
'customers',
'selectedCustomers',
'totalCustomers',
'selectAllField'
'selectAllField',
]),
selectField: {
get: function () {
@@ -260,7 +324,7 @@ export default {
},
set: function (val) {
this.selectCustomer(val)
}
},
},
selectAllFieldStatus: {
get: function () {
@@ -268,16 +332,16 @@ export default {
},
set: function (val) {
this.setSelectAllState(val)
}
}
},
},
},
watch: {
filters: {
handler: 'setFilters',
deep: true
}
deep: true,
},
},
destroyed () {
destroyed() {
if (this.selectAllField) {
this.selectAllCustomers()
}
@@ -289,19 +353,19 @@ export default {
'selectCustomer',
'deleteCustomer',
'deleteMultipleCustomers',
'setSelectAllState'
'setSelectAllState',
]),
refreshTable () {
refreshTable() {
this.$refs.table.refresh()
},
async fetchData ({ page, filter, sort }) {
async fetchData({ page, filter, sort }) {
let data = {
display_name: this.filters.display_name,
contact_name: this.filters.contact_name,
phone: this.filters.phone,
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page
page,
}
this.isRequestOngoing = true
@@ -312,60 +376,58 @@ export default {
data: response.data.customers.data,
pagination: {
totalPages: response.data.customers.last_page,
currentPage: page
}
currentPage: page,
},
}
},
setFilters () {
this.filtersApplied = true
setFilters() {
this.refreshTable()
},
clearFilter () {
clearFilter() {
this.filters = {
display_name: '',
contact_name: '',
phone: ''
phone: '',
}
this.$nextTick(() => {
this.filtersApplied = false
})
},
toggleFilter () {
if (this.showFilters && this.filtersApplied) {
toggleFilter() {
if (this.showFilters) {
this.clearFilter()
this.refreshTable()
}
this.showFilters = !this.showFilters
},
async removeCustomer (id) {
async removeCustomer(id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('customers.confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
dangerMode: true,
}).then(async (willDelete) => {
if (willDelete) {
let res = await this.deleteCustomer(id)
let res = await this.deleteCustomer({ ids: [id] })
if (res.data.success) {
window.toastr['success'](this.$tc('customers.deleted_message'))
this.refreshTable()
window.toastr['success'](this.$tc('customers.deleted_message', 1))
this.$refs.table.refresh()
return true
} else if (request.data.error) {
window.toastr['error'](res.data.message)
}
window.toastr['error'](res.data.message)
return true
}
})
},
async removeMultipleCustomers () {
async removeMultipleCustomers() {
swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('customers.confirm_delete', 2),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
dangerMode: true,
}).then(async (willDelete) => {
if (willDelete) {
let request = await this.deleteMultipleCustomers()
@@ -377,7 +439,7 @@ export default {
}
}
})
}
}
},
},
}
</script>

View File

@@ -0,0 +1,140 @@
<template>
<base-page class="xl:pl-96">
<sw-page-header :title="pageTitle">
<template slot="actions">
<sw-button
tag-name="router-link"
:to="`/admin/customers/${$route.params.id}/edit`"
class="mr-3"
variant="primary-outline"
>
{{ $t('general.edit') }}
</sw-button>
<sw-dropdown position="bottom-end">
<sw-button slot="activator" class="mr-3" variant="primary">
{{ $t('customers.new_transaction') }}
</sw-button>
<sw-dropdown-item
tag-name="router-link"
:to="`/admin/estimates/create?customer=${$route.params.id}`"
>
<document-icon class="h-5 mr-3 text-gray-600" />
{{ $t('estimates.new_estimate') }}
</sw-dropdown-item>
<sw-dropdown-item
tag-name="router-link"
:to="`/admin/invoices/create?customer=${$route.params.id}`"
>
<document-text-icon class="h-5 mr-3 text-gray-600" />
{{ $t('invoices.new_invoice') }}
</sw-dropdown-item>
<sw-dropdown-item
tag-name="router-link"
:to="`/admin/payments/create?customer=${$route.params.id}`"
>
<credit-card-icon class="h-5 mr-3 text-gray-600" />
{{ $t('payments.new_payment') }}
</sw-dropdown-item>
<sw-dropdown-item
tag-name="router-link"
:to="`/admin/expenses/create?customer=${$route.params.id}`"
>
<calculator-icon class="h-5 mr-3 text-gray-600" />
{{ $t('expenses.new_expense') }}
</sw-dropdown-item>
</sw-dropdown>
<sw-dropdown>
<sw-button slot="activator" variant="primary">
<dots-horizontal-icon class="h-5 -ml-1 -mr-1" />
</sw-button>
<sw-dropdown-item @click="removeCustomer($route.params.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</sw-page-header>
<!-- sidebar -->
<customer-view-sidebar />
<!-- Chart -->
<customer-chart />
</base-page>
</template>
<script>
import {
DotsHorizontalIcon,
TrashIcon,
DocumentIcon,
DocumentTextIcon,
CreditCardIcon,
CalculatorIcon,
} from '@vue-hero-icons/solid'
import LineChart from '../../components/chartjs/LineChart'
import CustomerViewSidebar from './partials/CustomerViewSidebar'
import CustomerChart from './partials/CustomerChart'
import { mapActions, mapGetters } from 'vuex'
export default {
components: {
LineChart,
DotsHorizontalIcon,
CustomerViewSidebar,
DocumentIcon,
DocumentTextIcon,
CreditCardIcon,
CalculatorIcon,
CustomerChart,
TrashIcon,
},
data() {
return {
customer: null,
}
},
computed: {
...mapGetters('customer', ['selectedViewCustomer']),
pageTitle() {
return this.selectedViewCustomer.customer
? this.selectedViewCustomer.customer.name
: ''
},
},
created() {
this.fetchViewCustomer({ id: this.$route.params.id })
},
methods: {
...mapActions('customer', [
'fetchViewCustomer',
'selectCustomer',
'deleteMultipleCustomers',
]),
async removeCustomer(id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('customers.confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
}).then(async (willDelete) => {
if (willDelete) {
let data = [id]
this.selectCustomer(data)
let res = await this.deleteMultipleCustomers()
if (res.data.success) {
window.toastr['success'](this.$tc('customers.deleted_message'))
this.$router.push('/admin/customers')
return true
} else if (request.data.error) {
window.toastr['error'](res.data.message)
}
}
})
},
},
}
</script>

View File

@@ -0,0 +1,219 @@
<template>
<sw-card v-if="chartData" class="flex flex-col mt-6">
<div class="grid grid-cols-12">
<div class="col-span-12 xl:col-span-9 xxl:col-span-10">
<div class="flex justify-between mt-1 mb-6">
<h6 class="flex items-center sw-section-title">
<chart-square-bar-icon class="h-5 text-primary-400" />{{
$t('dashboard.monthly_chart.title')
}}
</h6>
<div class="w-40 h-10">
<sw-select
v-model="selectedYear"
:options="years"
:allow-empty="false"
:show-labels="false"
:placeholder="$t('dashboard.select_year')"
@select="onChangeYear"
/>
</div>
</div>
<line-chart
:format-money="$utils.formatMoney"
:format-graph-money="$utils.formatGraphMoney"
:invoices="getChartInvoices"
:expenses="getChartExpenses"
:receipts="getReceiptTotals"
:income="getNetProfits"
:labels="getChartMonths"
class="sm:w-full"
/>
</div>
<div
class="grid col-span-12 mt-6 text-center xl:mt-0 sm:grid-cols-4 xl:text-right xl:col-span-3 xl:grid-cols-1 xxl:col-span-2"
>
<div class="px-6 py-2">
<span class="text-xs leading-5 lg:text-sm">
{{ $t('dashboard.chart_info.total_sales') }}
</span>
<br />
<span class="block mt-1 text-xl font-semibold leading-8">
<div v-html="getFormattedSalesTotal" />
</span>
</div>
<div class="px-6 py-2">
<span class="text-xs leading-5 lg:text-sm">
{{ $t('dashboard.chart_info.total_receipts') }}
</span>
<br />
<span
class="block mt-1 text-xl font-semibold leading-8"
style="color: #00c99c"
>
<div v-html="getFormattedTotalReceipts" />
</span>
</div>
<div class="px-6 py-2">
<span class="text-xs leading-5 lg:text-sm">
{{ $t('dashboard.chart_info.total_expense') }}
</span>
<br />
<span
class="block mt-1 text-xl font-semibold leading-8"
style="color: #fb7178"
>
<div v-html="getFormattedTotalExpenses" />
</span>
</div>
<div class="px-6 py-2">
<span class="text-xs leading-5 lg:text-sm">
{{ $t('dashboard.chart_info.net_income') }}
</span>
<br />
<span
class="block mt-1 text-xl font-semibold leading-8"
style="color: #5851d8"
>
<div v-html="getFormattedTotalNetProfit" />
</span>
</div>
</div>
</div>
<!-- basic info -->
<customer-info />
</sw-card>
</template>
<script>
import CustomerInfo from './CustomerInfo'
import LineChart from '../../../components/chartjs/LineChart'
import { mapActions, mapGetters } from 'vuex'
import { ChartSquareBarIcon } from '@vue-hero-icons/outline'
export default {
components: {
LineChart,
CustomerInfo,
ChartSquareBarIcon,
},
data() {
return {
id: null,
customers: [],
isLoaded: false,
chartData: null,
years: ['This year', 'Previous year'],
selectedYear: 'This year',
}
},
computed: {
...mapGetters('company', ['defaultCurrency']),
getChartInvoices() {
if (this.chartData && this.chartData.invoiceTotals) {
return this.chartData.invoiceTotals
}
return []
},
getChartExpenses() {
if (this.chartData && this.chartData.expenseTotals) {
return this.chartData.expenseTotals
}
return []
},
getReceiptTotals() {
if (this.chartData && this.chartData.receiptTotals) {
return this.chartData.receiptTotals
}
return []
},
getNetProfits() {
if (this.chartData && this.chartData.netProfits) {
return this.chartData.netProfits
}
return []
},
getChartMonths() {
if (this.chartData && this.chartData.months) {
return this.chartData.months
}
return []
},
getFormattedSalesTotal() {
if (this.chartData && this.chartData.salesTotal) {
return this.$utils.formatMoney(
this.chartData.salesTotal,
this.defaultCurrency
)
}
return 0
},
getFormattedTotalReceipts() {
if (this.chartData && this.chartData.totalReceipts) {
return this.$utils.formatMoney(
this.chartData.totalReceipts,
this.defaultCurrency
)
}
return 0
},
getFormattedTotalExpenses() {
if (this.chartData && this.chartData.totalExpenses) {
return this.$utils.formatMoney(
this.chartData.totalExpenses,
this.defaultCurrency
)
}
return 0
},
getFormattedTotalNetProfit() {
if (this.chartData && this.chartData.netProfit) {
return this.$utils.formatMoney(
this.chartData.netProfit,
this.defaultCurrency
)
}
return 0
},
},
watch: {
$route(to, from) {
this.loadCustomer()
this.selectedYear = 'This year'
},
},
created() {
this.loadCustomer()
},
methods: {
...mapActions('customer', ['fetchViewCustomer']),
async loadCustomer() {
this.isLoaded = false
let response = await this.fetchViewCustomer({ id: this.$route.params.id })
if (response.data) {
this.chartData = response.data.chartData
}
this.isLoaded = false
},
async onChangeYear(data) {
if (data == 'Previous year') {
let response = await this.fetchViewCustomer({
id: this.$route.params.id,
previous_year: true,
})
if (response.data) {
this.chartData = response.data.chartData
}
return true
}
let response = await this.fetchViewCustomer({ id: this.$route.params.id })
if (response.data) {
this.chartData = response.data.chartData
}
return true
},
},
}
</script>

View File

@@ -0,0 +1,273 @@
<template>
<div
class="pt-6 mt-5 border-t-2 border-solid lg:pt-8 md:pt-4"
style="border-top-color: #f9fbff"
>
<div class="col-span-12">
<p class="text-gray-500 uppercase sw-section-title">
{{ $t('customers.basic_info') }}
</p>
<div
class="grid grid-cols-1 gap-4 mt-5 lg:grid-cols-3 md:grid-cols-2 sm:grid-cols-1"
>
<div>
<p
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
>
{{ $t('customers.display_name') }}
</p>
<p class="text-sm font-bold leading-5 text-black non-italic">
{{
selectedViewCustomer.customer &&
selectedViewCustomer.customer.name
? selectedViewCustomer.customer.name
: ''
}}
</p>
</div>
<div>
<p
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
>
{{ $t('customers.primary_contact_name') }}
</p>
<p class="text-sm font-bold leading-5 text-black non-italic">
{{
selectedViewCustomer.customer &&
selectedViewCustomer.customer.contact_name
? selectedViewCustomer.customer.contact_name
: ''
}}
</p>
</div>
<div>
<p
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
>
{{ $t('customers.email') }}
</p>
<p class="text-sm font-bold leading-5 text-black non-italic">
{{
selectedViewCustomer.customer &&
selectedViewCustomer.customer.email
? selectedViewCustomer.customer.email
: ''
}}
</p>
</div>
</div>
<div
class="grid grid-cols-1 gap-4 mt-5 lg:grid-cols-3 md:grid-cols-2 sm:grid-cols-1"
>
<div>
<p
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
>
{{ $t('wizard.currency') }}
</p>
<p class="text-sm font-bold leading-5 text-black non-italic">
{{
selectedViewCustomer.customer.currency
? `${selectedViewCustomer.customer.currency.code} (${selectedViewCustomer.customer.currency.symbol})`
: ''
}}
</p>
</div>
<div>
<p
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
>
{{ $t('customers.phone_number') }}
</p>
<p class="text-sm font-bold leading-5 text-black non-italic">
{{
selectedViewCustomer.customer &&
selectedViewCustomer.customer.phone
? selectedViewCustomer.customer.phone
: ''
}}
</p>
</div>
<div>
<p
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
>
{{ $t('customers.website') }}
</p>
<p class="text-sm font-bold leading-5 text-black non-italic">
{{
selectedViewCustomer.customer &&
selectedViewCustomer.customer.website
? selectedViewCustomer.customer.website
: ''
}}
</p>
</div>
</div>
<p
v-if="
getFormattedShippingAddress.length ||
getFormattedBillingAddress.length
"
class="mt-8 text-gray-500 uppercase sw-section-title"
>
{{ $t('customers.address') }}
</p>
<div
class="grid grid-cols-1 gap-4 md:grid-cols-2 sm:grid-cols-1 lg:grid-cols-2"
>
<div v-if="getFormattedBillingAddress.length" class="mt-5">
<p
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
>
{{ $t('customers.billing_address') }}
</p>
<p
class="text-sm font-bold leading-5 text-black non-italic"
v-html="getFormattedBillingAddress"
/>
</div>
<div v-if="getFormattedShippingAddress.length" class="mt-5">
<p
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
>
{{ $t('customers.shipping_address') }}
</p>
<p
class="text-sm font-bold leading-5 text-black non-italic"
v-html="getFormattedShippingAddress"
/>
</div>
</div>
<!-- Custom Fields -->
<p
v-if="getCustomField.length > 0"
class="mt-8 text-gray-500 uppercase sw-section-title"
>
{{ $t('settings.custom_fields.title') }}
</p>
<div
class="grid grid-cols-1 gap-4 mt-5 lg:grid-cols-3 md:grid-cols-2 sm:grid-cols-1"
>
<div
v-for="(field, index) in getCustomField"
:key="index"
:required="field.is_required ? true : false"
>
<p
class="mb-1 text-sm font-normal leading-5 non-italic text-primary-800"
>
{{ field.custom_field.label }}
</p>
<p
v-if="field.type === 'Switch'"
class="text-sm font-bold leading-5 text-black non-italic"
>
<span v-if="field.defaultAnswer === 1"> Yes </span>
<span v-else> No </span>
</p>
<p v-else class="text-sm font-bold leading-5 text-black non-italic">
{{ field.defaultAnswer }}
</p>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data() {
return {
customer: null,
customFields: [],
}
},
computed: {
getFormattedBillingAddress() {
let billingAddress = ``
if (!this.selectedViewCustomer.customer) {
return billingAddress
}
if (!this.selectedViewCustomer.customer.billing_address) {
return billingAddress
}
if (this.selectedViewCustomer.customer.billing_address.address_street_1) {
billingAddress += `<span>${this.selectedViewCustomer.customer.billing_address.address_street_1},</span><br>`
}
if (this.selectedViewCustomer.customer.billing_address.address_street_2) {
billingAddress += `<span>${this.selectedViewCustomer.customer.billing_address.address_street_2},</span><br>`
}
if (this.selectedViewCustomer.customer.billing_address.city) {
billingAddress += `<span>${this.selectedViewCustomer.customer.billing_address.city},</span> `
}
if (this.selectedViewCustomer.customer.billing_address.state) {
billingAddress += `<span>${this.selectedViewCustomer.customer.billing_address.state},</span><br>`
}
if (this.selectedViewCustomer.customer.billing_address.country) {
billingAddress += `<span>${this.selectedViewCustomer.customer.billing_address.country.name}.</span> `
}
if (this.selectedViewCustomer.customer.billing_address.zip) {
billingAddress += `<span>${this.selectedViewCustomer.customer.billing_address.zip}.</span> `
}
return billingAddress
},
getFormattedShippingAddress() {
let shippingAddress = ``
if (!this.selectedViewCustomer.customer) {
return shippingAddress
}
if (!this.selectedViewCustomer.customer.shipping_address) {
return shippingAddress
}
if (
this.selectedViewCustomer.customer.shipping_address.address_street_1
) {
shippingAddress += `<span>${this.selectedViewCustomer.customer.shipping_address.address_street_1},</span><br>`
}
if (
this.selectedViewCustomer.customer.shipping_address.address_street_2
) {
shippingAddress += `<span>${this.selectedViewCustomer.customer.shipping_address.address_street_2},</span><br>`
}
if (this.selectedViewCustomer.customer.shipping_address.city) {
shippingAddress += `<span>${this.selectedViewCustomer.customer.shipping_address.city},</span> `
}
if (this.selectedViewCustomer.customer.shipping_address.state) {
shippingAddress += `<span>${this.selectedViewCustomer.customer.shipping_address.state},</span><br>`
}
if (this.selectedViewCustomer.customer.shipping_address.country) {
shippingAddress += `<span>${this.selectedViewCustomer.customer.shipping_address.country.name}.</span> `
}
if (this.selectedViewCustomer.customer.shipping_address.zip) {
shippingAddress += `<span>${this.selectedViewCustomer.customer.shipping_address.zip}.</span> `
}
return shippingAddress
},
getCustomField() {
if (this.selectedViewCustomer.customer.fields) {
return this.selectedViewCustomer.customer.fields
}
return []
},
...mapGetters('customer', ['selectedViewCustomer']),
},
watch: {
$route(to, from) {
this.customer = this.selectedViewCustomer
},
},
}
</script>

View File

@@ -0,0 +1,273 @@
<template>
<div
class="fixed top-0 left-0 hidden h-full pt-16 pb-4 ml-56 bg-white xl:ml-64 w-88 xl:block"
>
<div
class="flex items-center justify-between px-4 pt-8 pb-2 border border-gray-200 border-solid height-full"
>
<sw-input
v-model="searchData.searchText"
:placeholder="$t('general.search')"
class="mb-6"
type="text"
variant="gray"
@input="onSearch()"
>
<search-icon slot="rightIcon" class="h-5" />
</sw-input>
<div class="flex mb-6 ml-3" role="group" aria-label="First group">
<sw-dropdown
:close-on-select="false"
align="left"
position="bottom-start"
>
<sw-button slot="activator" size="md" variant="gray-light">
<filter-icon class="h-5" />
</sw-button>
<div
class="px-2 py-1 pb-2 mb-2 text-sm border-b border-gray-200 border-solid"
>
{{ $t('general.sort_by') }}
</div>
<sw-dropdown-item class="flex cursor-pointer">
<sw-input-group class="-mt-3 font-normal">
<sw-radio
:label="$t('customers.create_date')"
size="sm"
id="filter_create_date"
v-model="searchData.orderByField"
name="filter"
value="invoices.created_at"
@change="onSearch"
/>
</sw-input-group>
</sw-dropdown-item>
<sw-dropdown-item class="flex cursor-pointer">
<sw-input-group class="-mt-3 font-normal">
<sw-radio
:label="$t('customers.display_name')"
size="sm"
id="filter_display_name"
v-model="searchData.orderByField"
name="filter"
value="users.name"
@change="onSearch"
/>
</sw-input-group>
</sw-dropdown-item>
</sw-dropdown>
<sw-button
class="ml-1"
v-tooltip.top-center="{ content: getOrderName }"
size="md"
variant="gray-light"
@click="sortData"
>
<sort-ascending-icon v-if="getOrderBy" class="h-5" />
<sort-descending-icon v-else class="h-5" />
</sw-button>
</div>
</div>
<base-loader v-if="isSearching" :show-bg-overlay="true" />
<div
v-else
class="h-full pb-32 overflow-y-scroll border-l border-gray-200 border-solid sidebar sw-scroll"
>
<router-link
v-for="(customer, index) in customers"
:to="`/admin/customers/${customer.id}/view`"
:key="index"
:id="'customer-' + customer.id"
:class="[
'flex justify-between p-4 items-center cursor-pointer hover:bg-gray-100 border-l-4 border-transparent',
{
'bg-gray-100 border-l-4 border-primary-500 border-solid': hasActiveUrl(
customer.id
),
},
]"
style="border-top: 1px solid rgba(185, 193, 209, 0.41)"
>
<div>
<div
class="pr-2 text-sm not-italic font-normal leading-5 text-black capitalize truncate"
>
{{ customer.name }}
</div>
<div
class="mt-1 text-xs not-italic font-medium leading-5 text-gray-600"
v-if="customer.contact_name"
>
{{ customer.contact_name }}
</div>
</div>
<div class="flex-1 whitespace-no-wrap right">
<div
class="text-xl not-italic font-semibold leading-8 text-right text-gray-900"
v-html="$utils.formatMoney(customer.due_amount, customer.currency)"
/>
</div>
</router-link>
<p
v-if="!customers.length"
class="flex justify-center px-4 mt-5 text-sm text-gray-600"
>
{{ $t('customers.no_matching_customers') }}
</p>
</div>
</div>
</template>
<script>
import {
FilterIcon,
SortAscendingIcon,
SortDescendingIcon,
SearchIcon,
} from '@vue-hero-icons/solid'
import { mapActions, mapGetters } from 'vuex'
const _ = require('lodash')
export default {
components: {
FilterIcon,
SortAscendingIcon,
SortDescendingIcon,
SearchIcon,
},
data() {
return {
id: null,
customers: [],
customer: null,
currency: null,
searchData: {
orderBy: null,
orderByField: null,
searchText: null,
},
isSearching: false,
}
},
computed: {
getOrderBy() {
if (
this.searchData.orderBy === 'asc' ||
this.searchData.orderBy == null
) {
return true
}
return false
},
getOrderName() {
if (this.getOrderBy) {
return this.$t('general.ascending')
}
return this.$t('general.descending')
},
...mapGetters('company', ['defaultCurrency']),
...mapGetters('customer', ['selectedViewCustomer']),
},
watch: {
$route(to, from) {
this.loadCustomer()
},
},
created() {
this.loadCustomers()
this.loadCustomer()
this.onSearch = _.debounce(this.onSearch, 500)
},
methods: {
...mapActions('customer', ['fetchCustomers']),
hasActiveUrl(id) {
return this.$route.params.id == id
},
async loadCustomers() {
let response = await this.fetchCustomers({
limit: 'all',
})
if (response.data.customers) {
this.customers = response.data.customers.data
}
setTimeout(() => {
this.scrollToCustomer()
}, 500)
},
scrollToCustomer() {
const el = document.getElementById(`customer-${this.$route.params.id}`)
if (el) {
el.scrollIntoView({ behavior: 'smooth' })
el.classList.add('shake')
}
},
async loadCustomer() {
this.customer = this.selectedViewCustomer
this.currency = this.selectedViewCustomer.currency
},
async onSearch() {
let data = {}
if (
this.searchData.searchText !== '' &&
this.searchData.searchText !== null &&
this.searchData.searchText !== undefined
) {
data.display_name = this.searchData.searchText
}
if (
this.searchData.orderBy !== null &&
this.searchData.orderBy !== undefined
) {
data.orderBy = this.searchData.orderBy
}
if (
this.searchData.orderByField !== null &&
this.searchData.orderByField !== undefined
) {
data.orderByField = this.searchData.orderByField
}
this.isSearching = true
try {
let response = await this.fetchCustomers({ ...data })
this.isSearching = false
if (response.data) {
this.customers = response.data.customers.data
}
} catch (error) {
this.isSearching = false
}
},
sortData() {
if (this.searchData.orderBy === 'asc') {
this.searchData.orderBy = 'desc'
this.onSearch()
return true
}
this.searchData.orderBy = 'asc'
this.onSearch()
return true
},
},
}
</script>

View File

@@ -1,642 +1,21 @@
<template>
<div id="app" class="main-content dashboard">
<div class="row">
<div class="dash-item col-sm-6">
<router-link slot="item-title" to="/admin/invoices">
<div class="dashbox">
<div class="desc">
<span
v-if="isLoaded"
class="amount"
>
<div v-html="$utils.formatMoney(getTotalDueAmount, defaultCurrency)"/>
</span>
<span class="title">
{{ $t('dashboard.cards.due_amount') }}
</span>
</div>
<div class="icon">
<dollar-icon class="card-icon" />
</div>
</div>
</router-link>
</div>
<div class="dash-item col-sm-6">
<router-link slot="item-title" to="/admin/customers">
<div class="dashbox">
<div class="desc">
<span v-if="isLoaded"
class="amount" >
{{ getContacts }}
</span>
<span class="title">
{{ $t('dashboard.cards.customers') }}
</span>
</div>
<div class="icon">
<contact-icon class="card-icon" />
</div>
</div>
</router-link>
</div>
<div class="dash-item col-sm-6">
<router-link slot="item-title" to="/admin/invoices">
<div class="dashbox">
<div class="desc">
<span v-if="isLoaded"
class="amount">
{{ getInvoices }}
</span>
<span class="title">
{{ $t('dashboard.cards.invoices') }}
</span>
</div>
<div class="icon">
<invoice-icon class="card-icon" />
</div>
</div>
</router-link>
</div>
<div class="dash-item col-sm-6">
<router-link slot="item-title" to="/admin/estimates">
<div class="dashbox">
<div class="desc">
<span v-if="isLoaded"
class="amount">
{{ getEstimates }}
</span>
<span class="title">
{{ $t('dashboard.cards.estimates') }}
</span>
</div>
<div class="icon">
<estimate-icon class="card-icon" />
</div>
</div>
</router-link>
</div>
</div>
<div class="row">
<div class="col-lg-12 mt-2">
<div class="card dashboard-card">
<div class="graph-body">
<div class="card-body col-md-12 col-lg-12 col-xl-10">
<div class="card-header">
<h6><i class="fa fa-line-chart text-primary"/>{{ $t('dashboard.monthly_chart.title') }} </h6>
<div class="year-selector">
<base-select
v-model="selectedYear"
:options="years"
:allow-empty="false"
:show-labels="false"
:placeholder="$t('dashboard.select_year')"
/>
</div>
</div>
<line-chart
v-if="isLoaded"
:format-money="$utils.formatMoney"
:format-graph-money="$utils.formatGraphMoney"
:invoices="getChartInvoices"
:expenses="getChartExpenses"
:receipts="getReceiptTotals"
:income="getNetProfits"
:labels="getChartMonths"
class=""
/>
</div>
<div class="chart-desc col-md-12 col-lg-12 col-xl-2">
<div class="stats">
<div class="description">
<span class="title"> {{ $t('dashboard.chart_info.total_sales') }} </span>
<br>
<span v-if="isLoaded" class="total">
<div v-html="$utils.formatMoney(getTotalSales, defaultCurrency)"/>
</span>
</div>
<div class="description">
<span class="title"> {{ $t('dashboard.chart_info.total_receipts') }} </span>
<br>
<span v-if="isLoaded" class="total" style="color:#00C99C;">
<div v-html="$utils.formatMoney(getTotalReceipts, defaultCurrency)"/>
</span>
</div>
<div class="description">
<span class="title"> {{ $t('dashboard.chart_info.total_expense') }} </span>
<br>
<span v-if="isLoaded" class="total" style="color:#FB7178;">
<div v-html="$utils.formatMoney(getTotalExpenses, defaultCurrency)"/>
</span>
</div>
<div class="description">
<span class="title"> {{ $t('dashboard.chart_info.net_income') }} </span>
<br>
<span class="total" style="color:#5851D8;">
<div v-html="$utils.formatMoney(getNetProfit, defaultCurrency)"/>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<base-loader v-if="!getLoadedData"/>
<div class="row table-row">
<div class="col-lg-12 col-xl-6 mt-2">
<div class="table-header">
<h6 class="table-title">
{{ $t('dashboard.recent_invoices_card.title') }}
</h6>
<router-link to="/admin/invoices">
<base-button
:outline="true"
color="theme"
class="btn-sm"
>
{{ $t('dashboard.recent_invoices_card.view_all') }}
</base-button>
</router-link>
</div>
<div class="dashboard-table">
<table-component
ref="inv_table"
:data="getDueInvoices"
:show-filter="false"
table-class="table"
class="dashboard"
>
<table-column :label="$t('dashboard.recent_invoices_card.due_on')" show="formattedDueDate" />
<table-column :label="$t('dashboard.recent_invoices_card.customer')" show="user.name" />
<table-column
:label="$t('invoices.status')"
sort-as="status"
>
<template slot-scope="row" >
<span> {{ $t('invoices.status') }}</span>
<span :class="'inv-status-'+row.status.toLowerCase()">{{ (row.status != 'PARTIALLY_PAID')? row.status : row.status.replace('_', ' ') }}</span>
</template>
</table-column>
<table-column :label="$t('dashboard.recent_invoices_card.amount_due')" show="due_amount" sort-as="due_amount">
<template slot-scope="row">
<span>{{ $t('dashboard.recent_invoices_card.amount_due') }}</span>
<div v-html="$utils.formatMoney(row.due_amount, row.user.currency)"/>
</template>
</table-column>
<table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown dashboard-recent-invoice-options no-click"
>
<template slot-scope="row">
<v-dropdown>
<a slot="activator" href="#/">
<dot-icon />
</a>
<v-dropdown-item>
<router-link :to="{path: `invoices/${row.id}/edit`}" class="dropdown-item">
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon"/>
{{ $t('general.edit') }}
</router-link>
<router-link :to="{path: `invoices/${row.id}/view`}" class="dropdown-item">
<font-awesome-icon icon="eye" class="dropdown-item-icon" />
{{ $t('invoices.view') }}
</router-link>
</v-dropdown-item>
<v-dropdown-item v-if="row.status == 'DRAFT'">
<a class="dropdown-item" href="#/" @click="sendInvoice(row.id)" >
<font-awesome-icon icon="envelope" class="dropdown-item-icon" />
{{ $t('invoices.send_invoice') }}
</a>
</v-dropdown-item>
<v-dropdown-item v-if="row.status === 'DRAFT'">
<a class="dropdown-item" href="#/" @click="sentInvoice(row.id)">
<font-awesome-icon icon="check-circle" class="dropdown-item-icon" />
{{ $t('invoices.mark_as_sent') }}
</a>
</v-dropdown-item>
<v-dropdown-item>
<div class="dropdown-item" @click="removeInvoice(row.id)">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
</template>
</table-column>
</table-component>
</div>
</div>
<div class="col-lg-12 col-xl-6 mt-2 mob-table">
<div class="table-header">
<h6 class="table-title">
{{ $t('dashboard.recent_estimate_card.title') }}
</h6>
<router-link to="/admin/estimates">
<base-button
:outline="true"
color="theme"
class="btn-sm"
>
{{ $t('dashboard.recent_estimate_card.view_all') }}
</base-button>
</router-link>
</div>
<div class="dashboard-table">
<table-component
ref="est_table"
:data="getRecentEstimates"
:show-filter="false"
table-class="table"
>
<table-column :label="$t('dashboard.recent_estimate_card.date')" show="formattedExpiryDate" />
<table-column :label="$t('dashboard.recent_estimate_card.customer')" show="user.name" />
<table-column
:label="$t('estimates.status')"
show="status" >
<template slot-scope="row" >
<span> {{ $t('estimates.status') }}</span>
<span :class="'est-status-'+row.status.toLowerCase()">{{ row.status }}</span>
</template>
</table-column>
<table-column :label="$t('dashboard.recent_estimate_card.amount_due')" show="total" sort-as="total">
<template slot-scope="row">
<span>{{ $t('dashboard.recent_estimate_card.amount_due') }}</span>
<div v-html="$utils.formatMoney(row.total, row.user.currency)"/>
</template>
</table-column>
<table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown no-click"
>
<template slot-scope="row">
<v-dropdown>
<a slot="activator" href="#/">
<dot-icon />
</a>
<v-dropdown-item>
<router-link :to="{path: `estimates/${row.id}/edit`}" class="dropdown-item">
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
{{ $t('general.edit') }}
</router-link>
</v-dropdown-item>
<v-dropdown-item>
<div class="dropdown-item" @click="removeEstimate(row.id)">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
<v-dropdown-item>
<router-link :to="{path: `estimates/${row.id}/view`}" class="dropdown-item">
<font-awesome-icon icon="eye" class="dropdown-item-icon" />
{{ $t('general.view') }}
</router-link>
</v-dropdown-item>
<v-dropdown-item>
<a class="dropdown-item" href="#/" @click="convertInToinvoice(row.id)">
<font-awesome-icon icon="file-alt" class="dropdown-item-icon" />
{{ $t('estimates.convert_to_invoice') }}
</a>
</v-dropdown-item>
<v-dropdown-item v-if="row.status === 'DRAFT'">
<a class="dropdown-item" href="#/" @click.self="onMarkAsSent(row.id)">
<font-awesome-icon icon="check-circle" class="dropdown-item-icon" />
{{ $t('estimates.mark_as_sent') }}
</a>
</v-dropdown-item>
<v-dropdown-item v-if="row.status !== 'SENT'">
<a class="dropdown-item" href="#/" @click.self="sendEstimate(row.id)">
<font-awesome-icon icon="paper-plane" class="dropdown-item-icon" />
{{ $t('estimates.send_estimate') }}
</a>
</v-dropdown-item>
<v-dropdown-item v-if="row.status !== 'ACCEPTED'">
<a class="dropdown-item" href="#/" @click.self="onMarkAsAccepted(row.id)">
<font-awesome-icon icon="check-circle" class="dropdown-item-icon" />
{{ $t('estimates.mark_as_accepted') }}
</a>
</v-dropdown-item>
<v-dropdown-item v-if="row.status !== 'REJECTED'">
<a class="dropdown-item" href="#/" @click.self="onMarkAsRejected(row.id)">
<font-awesome-icon icon="times-circle" class="dropdown-item-icon" />
{{ $t('estimates.mark_as_rejected') }}
</a>
</v-dropdown-item>
</v-dropdown>
</template>
</table-column>
</table-component>
</div>
</div>
</div>
</div>
<base-page>
<dashboard-stats />
<dashboard-chart />
<dashboard-table />
</base-page>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import { SweetModal, SweetModalTab } from 'sweet-modal-vue'
import LineChart from '../../components/chartjs/LineChart'
import DollarIcon from '../../components/icon/DollarIcon'
import ContactIcon from '../../components/icon/ContactIcon'
import InvoiceIcon from '../../components/icon/InvoiceIcon'
import EstimateIcon from '../../components/icon/EstimateIcon'
import DashboardStats from '../dashboard/DashboardStats'
import DashboardChart from '../dashboard/DashboardChart'
import DashboardTable from '../dashboard/DashboardTable'
export default {
components: {
LineChart,
DollarIcon,
ContactIcon,
InvoiceIcon,
EstimateIcon
DashboardStats,
DashboardChart,
DashboardTable,
},
data () {
return {
incomeTotal: null,
...this.$store.state.dashboard,
currency: { precision: 2, thousand_separator: ',', decimal_separator: '.', symbol: '$' },
isLoaded: false,
fetching: false,
years: ['This year', 'Previous year'],
selectedYear: 'This year'
}
},
computed: {
...mapGetters('user', {
'user': 'currentUser'
}),
...mapGetters('dashboard', [
'getChartMonths',
'getChartInvoices',
'getChartExpenses',
'getNetProfits',
'getReceiptTotals',
'getContacts',
'getInvoices',
'getEstimates',
'getTotalDueAmount',
'getTotalSales',
'getTotalReceipts',
'getTotalExpenses',
'getNetProfit',
'getLoadedData',
'getDueInvoices',
'getRecentEstimates'
]),
...mapGetters('currency', [
'defaultCurrency'
])
},
watch: {
selectedYear (val) {
if (val === 'Previous year') {
let params = {previous_year: true}
this.loadData(params)
} else {
this.loadData()
}
}
},
created () {
this.loadData()
},
methods: {
...mapActions('dashboard', [
'getChart',
'loadData'
]),
...mapActions('invoice', [
'deleteInvoice',
'sendEmail',
'markAsSent'
]),
...mapActions('estimate', [
'deleteEstimate',
'markAsAccepted',
'markAsRejected',
'convertToInvoice'
]),
...mapActions('estimate', {
'sendEstimateEmail': 'sendEmail',
'markEstimateAsSent': 'markAsSent'
}),
async loadData (params) {
await this.$store.dispatch('dashboard/loadData', params)
this.isLoaded = true
},
async removeEstimate (id) {
this.id = id
window.swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('estimates.confirm_delete', 1),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
}).then(async (willDelete) => {
if (willDelete) {
let res = await this.deleteEstimate(this.id)
if (res.data.success) {
window.toastr['success'](this.$tc('estimates.deleted_message', 1))
this.refreshEstTable()
} else if (res.data.error) {
window.toastr['error'](res.data.message)
}
}
})
},
refreshInvTable () {
this.$refs.inv_table.refresh()
},
refreshEstTable () {
this.$refs.est_table.refresh()
},
async convertInToinvoice (id) {
window.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('estimates.confirm_conversion'),
icon: '/assets/icon/file-alt-solid.svg',
buttons: true,
dangerMode: true
}).then(async (willDelete) => {
if (willDelete) {
let res = await this.convertToInvoice(id)
this.selectAllField = false
if (res.data) {
window.toastr['success'](this.$t('estimates.conversion_message'))
this.$router.push(`invoices/${res.data.invoice.id}/edit`)
} else if (res.data.error) {
window.toastr['error'](res.data.message)
}
}
})
},
async onMarkAsSent (id) {
window.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('estimates.confirm_mark_as_sent'),
icon: '/assets/icon/check-circle-solid.svg',
buttons: true,
dangerMode: true
}).then(async (willMarkAsSent) => {
if (willMarkAsSent) {
const data = {
id: id
}
let response = await this.markEstimateAsSent(data)
this.refreshEstTable()
if (response.data) {
window.toastr['success'](this.$tc('estimates.mark_as_sent_successfully'))
}
}
})
},
async removeInvoice (id) {
this.id = id
window.swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('invoices.confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
}).then(async (willDelete) => {
if (willDelete) {
let res = await this.deleteInvoice(this.id)
if (res.data.success) {
window.toastr['success'](this.$tc('invoices.deleted_message'))
this.refreshInvTable()
} else if (res.data.error) {
window.toastr['error'](res.data.message)
}
}
})
},
async sendInvoice (id) {
window.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('invoices.confirm_send'),
icon: '/assets/icon/paper-plane-solid.svg',
buttons: true,
dangerMode: true
}).then(async (willSendInvoice) => {
if (willSendInvoice) {
const data = {
id: id
}
let response = await this.sendEmail(data)
this.refreshInvTable()
if (response.data.success) {
window.toastr['success'](this.$tc('invoices.send_invoice_successfully'))
return true
}
if (response.data.error === 'user_email_does_not_exist') {
window.toastr['error'](this.$tc('invoices.user_email_does_not_exist'))
return false
}
window.toastr['error'](this.$tc('invoices.something_went_wrong'))
}
})
},
async sentInvoice (id) {
window.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('invoices.invoice_mark_as_sent'),
icon: '/assets/icon/check-circle-solid.svg',
buttons: true,
dangerMode: true
}).then(async (willMarkAsSend) => {
if (willMarkAsSend) {
const data = {
id: id
}
let response = await this.markAsSent(data)
this.refreshInvTable()
if (response.data) {
window.toastr['success'](this.$tc('invoices.mark_as_sent_successfully'))
}
}
})
},
async onMarkAsAccepted (id) {
window.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('estimates.confirm_mark_as_accepted'),
icon: '/assets/icon/check-circle-solid.svg',
buttons: true,
dangerMode: true
}).then(async (markedAsRejected) => {
if (markedAsRejected) {
const data = {
id: id
}
let response = await this.markAsAccepted(data)
this.refreshEstTable()
if (response.data) {
this.refreshEstTable()
window.toastr['success'](this.$tc('estimates.marked_as_accepted_message'))
}
}
})
},
async onMarkAsRejected (id) {
window.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('estimates.confirm_mark_as_rejected'),
icon: '/assets/icon/times-circle-solid.svg',
buttons: true,
dangerMode: true
}).then(async (markedAsRejected) => {
if (markedAsRejected) {
const data = {
id: id
}
let response = await this.markAsRejected(data)
this.refreshEstTable()
if (response.data) {
this.refreshEstTable()
window.toastr['success'](this.$tc('estimates.marked_as_rejected_message'))
}
}
})
},
async sendEstimate (id) {
window.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('estimates.confirm_send_estimate'),
icon: '/assets/icon/paper-plane-solid.svg',
buttons: true,
dangerMode: true
}).then(async (willSendEstimate) => {
if (willSendEstimate) {
const data = {
id: id
}
let response = await this.sendEstimateEmail(data)
this.refreshEstTable()
if (response.data.success) {
window.toastr['success'](this.$tc('estimates.send_estimate_successfully'))
return true
}
if (response.data.error === 'user_email_does_not_exist') {
window.toastr['error'](this.$tc('estimates.user_email_does_not_exist'))
return true
}
window.toastr['error'](this.$tc('estimates.something_went_wrong'))
}
})
}
}
}
</script>

View File

@@ -0,0 +1,152 @@
<template>
<div class="grid grid-cols-10 mt-8 bg-white rounded shadow">
<!-- Chart -->
<div
class="grid grid-cols-1 col-span-10 px-4 py-5 lg:col-span-7 xl:col-span-8 sm:p-6"
>
<div class="flex justify-between mt-1 mb-6">
<h6 class="flex items-center sw-section-title">
<chart-square-bar-icon class="h-5 text-primary-400" />{{ $t('dashboard.monthly_chart.title') }}
</h6>
<div class="w-40 h-10" style="z-index: 0">
<sw-select
v-model="selectedYear"
:options="years"
:allow-empty="false"
:show-labels="false"
:placeholder="$t('dashboard.select_year')"
/>
</div>
</div>
<line-chart
v-if="isLoaded"
:format-money="$utils.formatMoney"
:format-graph-money="$utils.formatGraphMoney"
:invoices="getChartInvoices"
:expenses="getChartExpenses"
:receipts="getReceiptTotals"
:income="getNetProfits"
:labels="getChartMonths"
class="sm:w-full"
/>
</div>
<!-- Chart Labels -->
<div
class="grid grid-cols-1 grid-cols-3 col-span-10 text-center border-t border-l border-gray-200 border-solid lg:border-t-0 lg:text-right lg:col-span-3 xl:col-span-2 lg:grid-cols-1"
>
<div class="p-6">
<span class="text-xs leading-5 lg:text-sm">
{{ $t('dashboard.chart_info.total_sales') }}
</span>
<br />
<span
v-if="isLoaded"
class="block mt-1 text-xl font-semibold leading-8 lg:text-2xl"
>
<div v-html="$utils.formatMoney(getTotalSales, defaultCurrency)" />
</span>
</div>
<div class="p-6">
<span class="text-xs leading-5 lg:text-sm">
{{ $t('dashboard.chart_info.total_receipts') }}
</span>
<br />
<span
v-if="isLoaded"
class="block mt-1 text-xl font-semibold leading-8 lg:text-2xl"
style="color: #00c99c"
>
<div v-html="$utils.formatMoney(getTotalReceipts, defaultCurrency)" />
</span>
</div>
<div class="p-6">
<span class="text-xs leading-5 lg:text-sm">
{{ $t('dashboard.chart_info.total_expense') }}
</span>
<br />
<span
v-if="isLoaded"
class="block mt-1 text-xl font-semibold leading-8 lg:text-2xl"
style="color: #fb7178"
>
<div v-html="$utils.formatMoney(getTotalExpenses, defaultCurrency)" />
</span>
</div>
<div
class="col-span-3 p-6 border-t border-gray-200 border-solid lg:col-span-1"
>
<span class="text-xs leading-5 lg:text-sm">
{{ $t('dashboard.chart_info.net_income') }}
</span>
<br />
<span
class="block mt-1 text-xl font-semibold leading-8 lg:text-2xl"
style="color: #5851d8"
>
<div v-html="$utils.formatMoney(getNetProfit, defaultCurrency)" />
</span>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import { SweetModal, SweetModalTab } from 'sweet-modal-vue'
import LineChart from '../../components/chartjs/LineChart'
import { ChartSquareBarIcon } from "@vue-hero-icons/outline"
export default {
components: {
LineChart,
ChartSquareBarIcon
},
data() {
return {
...this.$store.state.dashboard,
isLoaded: false,
years: ['This year', 'Previous year'],
selectedYear: 'This year',
}
},
computed: {
...mapGetters('user', {
user: 'currentUser',
}),
...mapGetters('dashboard', [
'getChartMonths',
'getChartInvoices',
'getChartExpenses',
'getNetProfits',
'getReceiptTotals',
'getTotalSales',
'getTotalReceipts',
'getTotalExpenses',
'getNetProfit',
]),
...mapGetters('company', ['defaultCurrency']),
},
watch: {
selectedYear(val) {
if (val === 'Previous year') {
let params = { previous_year: true }
this.loadData(params)
} else {
this.loadData()
}
},
},
created() {
this.loadData()
},
methods: {
...mapActions('dashboard', ['loadData']),
async loadData(params) {
await this.$store.dispatch('dashboard/loadData', params)
this.isLoaded = true
},
},
}
</script>

View File

@@ -0,0 +1,128 @@
<template>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-9 xl:gap-8">
<!-- Amount Due -->
<router-link
slot="item-title"
class="relative flex justify-between p-3 bg-white rounded shadow hover:bg-gray-100 lg:col-span-3 xl:p-4"
to="/admin/invoices"
>
<div>
<span
v-if="getDashboardDataLoaded"
class="text-xl font-semibold leading-tight text-black xl:text-3xl"
>
<span
v-html="$utils.formatMoney(getTotalDueAmount, defaultCurrency)"
/>
</span>
<span class="block mt-1 text-sm leading-tight text-gray-500 xl:text-lg">
{{ $t('dashboard.cards.due_amount') }}
</span>
</div>
<div class="flex items-center">
<dollar-icon class="w-10 h-10 xl:w-12 xl:h-12" />
</div>
</router-link>
<!-- Customers -->
<router-link
slot="item-title"
class="relative flex justify-between p-3 bg-white rounded shadow hover:bg-gray-100 lg:col-span-2 xl:p-4"
to="/admin/customers"
>
<div>
<span
v-if="getDashboardDataLoaded"
class="text-xl font-semibold leading-tight text-black xl:text-3xl"
>
{{ getContacts }}
</span>
<span class="block mt-1 text-sm leading-tight text-gray-500 xl:text-lg">
{{ $t('dashboard.cards.customers') }}
</span>
</div>
<div class="flex items-center">
<contact-icon class="w-10 h-10 xl:w-12 xl:h-12" />
</div>
</router-link>
<!-- Invoices -->
<router-link
slot="item-title"
class="relative flex justify-between p-3 bg-white rounded shadow hover:bg-gray-100 lg:col-span-2 xl:p-4"
to="/admin/invoices"
>
<div>
<span
v-if="getDashboardDataLoaded"
class="text-xl font-semibold leading-tight text-black xl:text-3xl"
>
{{ getInvoices }}
</span>
<span class="block mt-1 text-sm leading-tight text-gray-500 xl:text-lg">
{{ $t('dashboard.cards.invoices') }}
</span>
</div>
<div class="flex items-center">
<invoice-icon class="w-10 h-10 xl:w-12 xl:h-12" />
</div>
</router-link>
<!-- Estimates -->
<router-link
slot="item-title"
class="relative flex justify-between p-3 bg-white rounded shadow hover:bg-gray-100 lg:col-span-2 xl:p-4"
to="/admin/estimates"
>
<div>
<span
v-if="getDashboardDataLoaded"
class="text-xl font-semibold leading-tight text-black xl:text-3xl"
>
{{ getEstimates }}
</span>
<span class="block mt-1 text-sm leading-tight text-gray-500 xl:text-lg">
{{ $t('dashboard.cards.estimates') }}
</span>
</div>
<div class="flex items-center">
<estimate-icon class="w-10 h-10 xl:w-12 xl:h-12" />
</div>
</router-link>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import DollarIcon from '../../components/icon/DollarIcon'
import ContactIcon from '../../components/icon/ContactIcon'
import InvoiceIcon from '../../components/icon/InvoiceIcon'
import EstimateIcon from '../../components/icon/EstimateIcon'
export default {
components: {
DollarIcon,
ContactIcon,
InvoiceIcon,
EstimateIcon,
},
data() {
return {
...this.$store.state.dashboard,
}
},
computed: {
...mapGetters('user', {
user: 'currentUser',
}),
...mapGetters('dashboard', [
'getContacts',
'getInvoices',
'getEstimates',
'getTotalDueAmount',
'getDashboardDataLoaded',
]),
...mapGetters('company', ['defaultCurrency']),
},
}
</script>

View File

@@ -0,0 +1,619 @@
<template>
<div>
<base-loader v-if="!getDashboardDataLoaded" />
<div class="grid grid-cols-1 gap-6 mt-10 xl:grid-cols-2">
<!-- Due Invoices -->
<div class="due-invoices">
<div class="relative z-10 flex items-center justify-between">
<h6 class="mb-0 text-xl font-semibold leading-normal">
{{ $t('dashboard.recent_invoices_card.title') }}
</h6>
<sw-button
tag-name="router-link"
to="/admin/invoices"
variant="primary-outline"
>
{{ $t('dashboard.recent_invoices_card.view_all') }}
</sw-button>
</div>
<sw-table-component
ref="inv_table"
:data="getDueInvoices"
:show-filter="false"
table-class="table"
>
<sw-table-column
:sortable="true"
:label="$t('dashboard.recent_invoices_card.due_on')"
show="formattedDueDate"
>
<template slot-scope="row">
<span>{{ $t('dashboard.recent_invoices_card.due_on') }}</span>
<span class="mt-6">{{ row.formattedDueDate }}</span>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('dashboard.recent_invoices_card.customer')"
show="user.name"
>
<template slot-scope="row">
<span>{{ $t('dashboard.recent_invoices_card.customer') }}</span>
<router-link
:to="{ path: `invoices/${row.id}/view` }"
class="font-medium text-primary-500"
>
{{ row.user.name }}
</router-link>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('invoices.status')"
sort-as="status"
>
<template slot-scope="row">
<span> {{ $t('invoices.status') }}</span>
<sw-badge
:bg-color="$utils.getBadgeStatusColor(row.status).bgColor"
:color="$utils.getBadgeStatusColor(row.status).color"
>
{{
row.status != 'PARTIALLY_PAID'
? row.status
: row.status.replace('_', ' ')
}}
</sw-badge>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('dashboard.recent_invoices_card.amount_due')"
show="due_amount"
sort-as="due_amount"
>
<template slot-scope="row">
<span>{{ $t('dashboard.recent_invoices_card.amount_due') }}</span>
<div
v-html="$utils.formatMoney(row.due_amount, row.user.currency)"
/>
</template>
</sw-table-column>
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown dashboard-recent-invoice-options no-click"
>
<sw-dropdown slot-scope="row">
<dot-icon slot="activator" />
<sw-dropdown-item
tag-name="router-link"
:to="`invoices/${row.id}/edit`"
>
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item
tag-name="router-link"
:to="`invoices/${row.id}/view`"
>
<eye-icon class="h-5 mr-3 text-gray-600" />
{{ $t('invoices.view') }}
</sw-dropdown-item>
<!-- <sw-dropdown-item
v-if="row.status == 'DRAFT'"
@click="sendInvoice(row.id)"
>
<paper-airplane-icon class="h-5 mr-3 text-gray-600" />
{{ $t('invoices.send_invoice') }}
</sw-dropdown-item> -->
<sw-dropdown-item
v-if="row.status === 'DRAFT'"
@click="sentInvoice(row.id)"
>
<check-circle-icon class="h-5 mr-3 text-gray-600" />
{{ $t('invoices.mark_as_sent') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removeInvoice(row.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</sw-table-column>
</sw-table-component>
</div>
<!-- Recent Estimates -->
<div class="recent-estimates">
<div class="relative z-10 flex items-center justify-between">
<h6 class="mb-0 text-xl font-semibold leading-normal">
{{ $t('dashboard.recent_estimate_card.title') }}
</h6>
<sw-button
tag-name="router-link"
to="/admin/estimates"
variant="primary-outline"
>
{{ $t('dashboard.recent_estimate_card.view_all') }}
</sw-button>
</div>
<sw-table-component
ref="est_table"
:data="getRecentEstimates"
:show-filter="false"
table-class="table"
>
<sw-table-column
:sortable="true"
:label="$t('dashboard.recent_estimate_card.date')"
show="formattedExpiryDate"
>
<template slot-scope="row">
<span>{{ $t('dashboard.recent_estimate_card.date') }}</span>
<span class="mt-6">{{ row.formattedExpiryDate }}</span>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('dashboard.recent_estimate_card.customer')"
show="user.name"
>
<template slot-scope="row">
<span>{{ $t('dashboard.recent_estimate_card.customer') }}</span>
<router-link
:to="{ path: `estimates/${row.id}/view` }"
class="font-medium text-primary-500"
>
{{ row.user.name }}
</router-link>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('estimates.status')"
show="status"
>
<template slot-scope="row">
<span> {{ $t('estimates.status') }}</span>
<sw-badge
:bg-color="$utils.getBadgeStatusColor(row.status).bgColor"
:color="$utils.getBadgeStatusColor(row.status).color"
class="px-3 py-1"
>
{{ row.status }}
</sw-badge>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('dashboard.recent_estimate_card.amount_due')"
show="total"
sort-as="total"
>
<template slot-scope="row">
<span>{{ $t('dashboard.recent_estimate_card.amount_due') }}</span>
<div v-html="$utils.formatMoney(row.total, row.user.currency)" />
</template>
</sw-table-column>
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown no-click"
>
<sw-dropdown slot-scope="row">
<dot-icon slot="activator" />
<sw-dropdown-item
tag-name="router-link"
:to="`estimates/${row.id}/edit`"
>
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item
tag-name="router-link"
:to="`estimates/${row.id}/view`"
>
<eye-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.view') }}
</sw-dropdown-item>
<sw-dropdown-item @click="convertInToinvoice(row.id)">
<document-text-icon class="h-5 mr-3 text-gray-600" />
{{ $t('estimates.convert_to_invoice') }}
</sw-dropdown-item>
<sw-dropdown-item @click="onMarkAsSent(row.id)">
<check-circle-icon class="h-5 mr-3 text-gray-600" />
{{ $t('estimates.mark_as_sent') }}
</sw-dropdown-item>
<!--
<sw-dropdown-item
v-if="row.status !== 'SENT'"
@click="sendEstimate(row.id)"
>
<paper-airplane-icon class="h-5 mr-3 text-gray-600" />
{{ $t('estimates.send_estimate') }}
</sw-dropdown-item> -->
<sw-dropdown-item
v-if="row.status !== 'ACCEPTED'"
@click="onMarkAsAccepted(row.id)"
>
<check-circle-icon class="h-5 mr-3 text-gray-600" />
{{ $t('estimates.mark_as_accepted') }}
</sw-dropdown-item>
<sw-dropdown-item
v-if="row.status !== 'REJECTED'"
@click="onMarkAsRejected(row.id)"
>
<x-circle-icon class="h-5 mr-3 text-gray-600" />
{{ $t('estimates.mark_as_rejected') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removeEstimate(row.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</sw-table-column>
</sw-table-component>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import { SweetModal, SweetModalTab } from 'sweet-modal-vue'
import {
PencilIcon,
EyeIcon,
PaperAirplaneIcon,
CheckCircleIcon,
TrashIcon,
DocumentTextIcon,
XCircleIcon,
} from '@vue-hero-icons/solid'
export default {
components: {
PencilIcon,
EyeIcon,
PaperAirplaneIcon,
CheckCircleIcon,
TrashIcon,
DocumentTextIcon,
XCircleIcon,
},
data() {
return {
...this.$store.state.dashboard,
currency: {
precision: 2,
thousand_separator: ',',
decimal_separator: '.',
symbol: '$',
},
isLoaded: false,
fetching: false,
}
},
computed: {
...mapGetters('user', {
user: 'currentUser',
}),
...mapGetters('dashboard', [
'getDashboardDataLoaded',
'getDueInvoices',
'getRecentEstimates',
]),
},
methods: {
...mapActions('dashboard', ['loadData']),
...mapActions('invoice', ['deleteInvoice', 'sendEmail', 'markAsSent']),
...mapActions('estimate', [
'deleteEstimate',
'markAsAccepted',
'markAsRejected',
'convertToInvoice',
]),
...mapActions('estimate', {
sendEstimateEmail: 'sendEmail',
markEstimateAsSent: 'markAsSent',
}),
async loadData(params) {
await this.$store.dispatch('dashboard/loadData', params)
this.isLoaded = true
},
async removeEstimate(id) {
this.id = id
window
.swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('estimates.confirm_delete', 1),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
})
.then(async (willDelete) => {
if (willDelete) {
let res = await this.deleteEstimate({ ids: [this.id] })
if (res.data.success) {
window.toastr['success'](this.$tc('estimates.deleted_message', 1))
this.refreshEstTable()
} else if (res.data.error) {
window.toastr['error'](res.data.message)
}
}
})
},
refreshInvTable() {
this.$refs.inv_table.refresh()
},
refreshEstTable() {
this.$refs.est_table.refresh()
},
async convertInToinvoice(id) {
window
.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('estimates.confirm_conversion'),
icon: '/assets/icon/file-alt-solid.svg',
buttons: true,
dangerMode: true,
})
.then(async (willDelete) => {
if (willDelete) {
let res = await this.convertToInvoice(id)
this.selectAllField = false
if (res.data) {
window.toastr['success'](this.$t('estimates.conversion_message'))
this.$router.push(`invoices/${res.data.invoice.id}/edit`)
} else if (res.data.error) {
window.toastr['error'](res.data.message)
}
}
})
},
async onMarkAsSent(id) {
window
.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('estimates.confirm_mark_as_sent'),
icon: '/assets/icon/check-circle-solid.svg',
buttons: true,
dangerMode: true,
})
.then(async (willMarkAsSent) => {
if (willMarkAsSent) {
const data = {
id: id,
status: 'SENT',
}
let response = await this.markEstimateAsSent(data)
this.refreshEstTable()
if (response.data) {
window.toastr['success'](
this.$tc('estimates.mark_as_sent_successfully')
)
}
}
})
},
async removeInvoice(id) {
this.id = id
window
.swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('invoices.confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
})
.then(async (willDelete) => {
if (willDelete) {
let res = await this.deleteInvoice({ ids: [this.id] })
if (res.data.success) {
window.toastr['success'](this.$tc('invoices.deleted_message'))
this.refreshInvTable()
} else if (res.data.error) {
window.toastr['error'](res.data.message)
}
}
})
},
async sendInvoice(id) {
window
.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('invoices.confirm_send'),
icon: '/assets/icon/paper-plane-solid.svg',
buttons: true,
dangerMode: true,
})
.then(async (willSendInvoice) => {
if (willSendInvoice) {
const data = {
id: id,
}
let response = await this.sendEmail(data)
this.refreshInvTable()
if (response.data.success) {
window.toastr['success'](
this.$tc('invoices.send_invoice_successfully')
)
return true
}
if (response.data.error === 'user_email_does_not_exist') {
window.toastr['error'](
this.$tc('invoices.user_email_does_not_exist')
)
return false
}
window.toastr['error'](this.$tc('invoices.something_went_wrong'))
}
})
},
async sentInvoice(id) {
window
.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('invoices.invoice_mark_as_sent'),
icon: '/assets/icon/check-circle-solid.svg',
buttons: true,
dangerMode: true,
})
.then(async (willMarkAsSend) => {
if (willMarkAsSend) {
const data = {
id: id,
status: 'SENT',
}
let response = await this.markAsSent(data)
this.refreshInvTable()
if (response.data) {
window.toastr['success'](
this.$tc('invoices.mark_as_sent_successfully')
)
}
}
})
},
async onMarkAsAccepted(id) {
window
.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('estimates.confirm_mark_as_accepted'),
icon: '/assets/icon/check-circle-solid.svg',
buttons: true,
dangerMode: true,
})
.then(async (markedAsRejected) => {
if (markedAsRejected) {
const data = {
id: id,
}
let response = await this.markAsAccepted(data)
this.refreshEstTable()
if (response.data) {
this.refreshEstTable()
window.toastr['success'](
this.$tc('estimates.marked_as_accepted_message')
)
}
}
})
},
async onMarkAsRejected(id) {
window
.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('estimates.confirm_mark_as_rejected'),
icon: '/assets/icon/times-circle-solid.svg',
buttons: true,
dangerMode: true,
})
.then(async (markedAsRejected) => {
if (markedAsRejected) {
const data = {
id: id,
}
let response = await this.markAsRejected(data)
this.refreshEstTable()
if (response.data) {
this.refreshEstTable()
window.toastr['success'](
this.$tc('estimates.marked_as_rejected_message')
)
}
}
})
},
async sendEstimate(id) {
window
.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('estimates.confirm_send_estimate'),
icon: '/assets/icon/paper-plane-solid.svg',
buttons: true,
dangerMode: true,
})
.then(async (willSendEstimate) => {
if (willSendEstimate) {
const data = {
id: id,
}
let response = await this.sendEstimateEmail(data)
this.refreshEstTable()
if (response.data.success) {
window.toastr['success'](
this.$tc('estimates.send_estimate_successfully')
)
return true
}
if (response.data.error === 'user_email_does_not_exist') {
window.toastr['error'](
this.$tc('estimates.user_email_does_not_exist')
)
return true
}
window.toastr['error'](this.$tc('estimates.something_went_wrong'))
}
})
},
},
}
</script>

View File

@@ -1,13 +1,19 @@
<template>
<div class="error-box">
<div class="row">
<div class="col-sm-12 text-sm-center">
<h1>{{ $t('general.four_zero_four') }}</h1>
<h5>{{ $t('general.you_got_lost') }}</h5>
<div class="w-full h-full">
<div class="flex items-center justify-center w-full h-full">
<div class="flex flex-col items-center justify-center">
<h1 class="text-primary-500" style="font-size: 10rem">
{{ $t('general.four_zero_four') }}
</h1>
<h5 class="mb-10 text-3xl text-primary-500">
{{ $t('general.you_got_lost') }}
</h5>
<router-link
class="btn btn-lg bg-yellow text-white"
to="/">
<font-awesome-icon icon="arrow-left" class="icon text-white mr-2"/> {{ $t('general.go_home') }}
class="flex items-center w-32 h-12 px-2 py-1 text-base font-medium leading-none text-center text-white whitespace-no-wrap rounded bg-primary-500 btn-lg hover:text-white"
to="/admin/dashboard"
>
<arrow-left-icon class="mr-2 text-white icon" />
{{ $t('general.go_home') }}
</router-link>
</div>
</div>
@@ -15,18 +21,23 @@
</template>
<script>
import { ArrowLeftIcon } from '@vue-hero-icons/solid'
export default {
mounted () {
components: {
ArrowLeftIcon,
},
mounted() {
this.setLayoutClasses()
},
destroyed () {
$('body').removeClass('page-error-404')
destroyed() {
let body = document.getElementsByTagName('body')
body[0].classList -= ' bg-black'
},
methods: {
setLayoutClasses () {
let body = $('body')
body.addClass('page-error-404')
}
}
setLayoutClasses() {
let body = document.getElementsByTagName('body')
body[0].classList += ' bg-black'
},
},
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
<template>
<div class="col-span-5 pr-0">
<div
v-if="selectedCustomer"
class="flex flex-col p-4 bg-white border border-gray-200 border-solid"
style="min-height: 170px"
>
<div class="relative flex justify-between mb-1">
<label class="flex-1 font-medium">{{ selectedCustomer.name }}</label>
<a
class="relative my-0 ml-0 mr-6 text-sm font-medium cursor-pointer text-primary-500"
@click.prevent="editCustomer"
>
{{ $t('general.edit') }}
</a>
<a
class="relative my-0 ml-0 mr-6 text-sm font-medium cursor-pointer text-primary-500"
@click.prevent="resetSelectedCustomer"
>
{{ $t('general.deselect') }}
</a>
</div>
<div class="grid grid-cols-2 gap-4 mt-1">
<div v-if="selectedCustomer.billing_address">
<div class="flex flex-col">
<label
class="mb-1 text-sm font-medium text-gray-500 uppercase whitespace-no-wrap"
>
{{ $t('general.bill_to') }}
</label>
<div class="flex flex-col flex-1 p-0">
<label
v-if="selectedCustomer.billing_address.name"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.billing_address.name }}
</label>
<label
v-if="selectedCustomer.billing_address.address_street_1"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.billing_address.address_street_1 }}
</label>
<label
v-if="selectedCustomer.billing_address.address_street_2"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.billing_address.address_street_2 }}
</label>
<label
v-if="
selectedCustomer.billing_address.city &&
selectedCustomer.billing_address.state
"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.billing_address.city }},
{{ selectedCustomer.billing_address.state }}
{{ selectedCustomer.billing_address.zip }}
</label>
<label
v-if="selectedCustomer.billing_address.country"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.billing_address.country.name }}
</label>
<label
v-if="selectedCustomer.billing_address.phone"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.billing_address.phone }}
</label>
</div>
</div>
</div>
<div v-if="selectedCustomer.shipping_address">
<div class="flex flex-col">
<label
class="mb-1 text-sm font-medium text-gray-500 uppercase whitespace-no-wrap"
>
{{ $t('general.ship_to') }}
</label>
<div class="flex flex-col flex-1 p-0">
<label
v-if="selectedCustomer.shipping_address.name"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.shipping_address.name }}
</label>
<label
v-if="selectedCustomer.shipping_address.address_street_1"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.shipping_address.address_street_1 }}
</label>
<label
v-if="selectedCustomer.shipping_address.address_street_2"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.shipping_address.address_street_2 }}
</label>
<label
v-if="
selectedCustomer.shipping_address.city &&
selectedCustomer.shipping_address
"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.shipping_address.city }},
{{ selectedCustomer.shipping_address.state }}
{{ selectedCustomer.shipping_address.zip }}
</label>
<label
v-if="selectedCustomer.shipping_address.country"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.shipping_address.country.name }}
</label>
<label
v-if="selectedCustomer.shipping_address.phone"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.shipping_address.phone }}
</label>
</div>
</div>
</div>
</div>
</div>
<div v-else>
<sw-popup
:class="[
'p-0',
{
'border border-solid border-danger rounded': valid.$error,
},
]"
>
<div
slot="activator"
class="relative flex justify-center px-0 py-16 bg-white border border-gray-200 border-solid rounded"
style="min-height: 170px"
>
<user-icon
class="flex justify-center w-10 h-10 p-2 mr-5 text-sm text-white bg-gray-200 rounded-full font-base"
/>
<div class="mt-1">
<label class="text-lg">
{{ $t('customers.new_customer') }}
<span class="text-danger"> * </span>
</label>
<p v-if="valid.$error && !valid.required" class="text-danger">
{{ $t('estimates.errors.required') }}
</p>
</div>
</div>
<customer-select-popup :user-id="customerId" type="estimate" />
</sw-popup>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { UserIcon } from '@vue-hero-icons/solid'
export default {
components: {
UserIcon,
},
props: {
valid: {
type: Object,
default: () => {},
},
customerId: {
type: Number,
default: null,
},
},
computed: {
...mapGetters('estimate', ['getTemplateId', 'selectedCustomer']),
},
created() {
this.resetSelectedCustomer()
if (this.customerId) {
this.selectCustomer(this.customerId)
}
},
methods: {
...mapActions('estimate', ['resetSelectedCustomer', 'selectCustomer']),
...mapActions('modal', ['openModal']),
editCustomer() {
this.openModal({
title: this.$t('customers.edit_customer'),
componentName: 'CustomerModal',
id: this.selectedCustomer.id,
data: this.selectedCustomer,
})
},
},
}
</script>

View File

@@ -1,50 +1,49 @@
<template>
<div class="section mt-2">
<label class="estimate-label">
<div class="flex items-center justify-between w-full mt-2 text-sm">
<label class="font-semibold leading-5 text-gray-500 uppercase">
{{ tax.name }} ({{ tax.percent }}%)
</label>
<label class="estimate-amount">
<label class="flex items-center justify-center text-lg text-black">
<div v-html="$utils.formatMoney(tax.amount, currency)" />
<font-awesome-icon
class="ml-2"
icon="trash-alt"
@click="$emit('remove', index)"
/>
<trash-icon class="h-5 ml-2" @click="$emit('remove', index)" />
</label>
</div>
</template>
<script>
import { TrashIcon } from '@vue-hero-icons/solid'
export default {
components: {
TrashIcon,
},
props: {
index: {
type: Number,
required: true
required: true,
},
tax: {
type: Object,
required: true
required: true,
},
taxes: {
type: Array,
required: true
required: true,
},
total: {
type: Number,
default: 0
default: 0,
},
totalTax: {
type: Number,
default: 0
default: 0,
},
currency: {
type: [Object, String],
required: true
}
required: true,
},
},
computed: {
taxAmount () {
taxAmount() {
if (this.tax.compound_tax && this.total) {
return ((this.total + this.totalTax) * this.tax.percent) / 100
}
@@ -54,30 +53,26 @@ export default {
}
return 0
}
},
},
watch: {
total: {
handler: 'updateTax'
handler: 'updateTax',
},
totalTax: {
handler: 'updateTax'
}
handler: 'updateTax',
},
},
methods: {
updateTax () {
updateTax() {
this.$emit('update', {
'index': this.index,
'item': {
index: this.index,
item: {
...this.tax,
amount: this.taxAmount
}
amount: this.taxAmount,
},
})
}
}
},
},
}
</script>
<style lang="scss" scoped>
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,22 @@
<template>
<tr class="item-row estimate-item-row">
<td colspan="5">
<table class="full-width">
<tr class="box-border bg-white border border-gray-200 border-solid rounded-b">
<td colspan="5" class="p-0 text-left align-top">
<table class="w-full">
<colgroup>
<col style="width: 40%;">
<col style="width: 10%;">
<col style="width: 15%;">
<col v-if="discountPerItem === 'YES'" style="width: 15%;">
<col style="width: 15%;">
<col style="width: 40%" />
<col style="width: 10%" />
<col style="width: 15%" />
<col v-if="discountPerItem === 'YES'" style="width: 15%" />
<col style="width: 15%" />
</colgroup>
<tbody>
<tr>
<td class="">
<div class="item-select-wrapper">
<div class="sort-icon-wrapper handle">
<font-awesome-icon
class="sort-icon"
icon="grip-vertical"
/>
<td class="px-5 py-4 text-left align-top">
<div class="flex justify-start">
<div
class="flex items-center justify-center w-12 h-5 mt-2 text-gray-400 cursor-move handle"
>
<drag-icon />
</div>
<item-select
ref="itemSelect"
@@ -34,87 +33,94 @@
/>
</div>
</td>
<td class="text-right">
<base-input
<td class="px-5 py-4 text-right align-top">
<sw-input
v-model="item.quantity"
:invalid="$v.item.quantity.$error"
:is-input-group="!!item.unit_name"
:input-group-text="item.unit_name"
type="text"
small
@keyup="updateItem"
@input="$v.item.quantity.$touch()"
/>
<div v-if="$v.item.quantity.$error">
<span v-if="!$v.item.quantity.maxLength" class="text-danger">{{ $t('validation.quantity_maxlength') }}</span>
<span v-if="!$v.item.quantity.maxLength" class="text-danger">
{{ $t('validation.quantity_maxlength') }}
</span>
</div>
</td>
<td class="text-left">
<div class="d-flex flex-column">
<div class="flex-fillbd-highlight">
<div class="base-input">
<money
<td class="px-5 py-4 text-left align-top">
<div class="flex flex-col">
<div class="flex-auto flex-fill bd-highlight">
<div class="relative w-full">
<sw-money
v-model="price"
v-bind="customerCurrency"
class="input-field"
:currency="customerCurrency"
:invalid="$v.item.price.$error"
@input="$v.item.price.$touch()"
/>
</div>
<div v-if="$v.item.price.$error">
<span v-if="!$v.item.price.maxLength" class="text-danger">{{ $t('validation.price_maxlength') }}</span>
<span v-if="!$v.item.price.maxLength" class="text-danger">
{{ $t('validation.price_maxlength') }}
</span>
</div>
</div>
</div>
</td>
<td v-if="discountPerItem === 'YES'" class="">
<div class="d-flex flex-column bd-highlight">
<div
class="btn-group flex-fill bd-highlight"
role="group"
>
<base-input
<td
v-if="discountPerItem === 'YES'"
class="px-5 py-4 text-left align-top"
>
<div class="flex flex-col">
<div class="flex flex-auto" role="group">
<sw-input
v-model="discount"
:invalid="$v.item.discount_val.$error"
input-class="item-discount"
class="border-r-0 rounded-tr-none rounded-br-none"
@input="$v.item.discount_val.$touch()"
/>
<v-dropdown :show-arrow="false" theme-light>
<button
<sw-dropdown>
<sw-button
slot="activator"
type="button"
class="btn item-dropdown dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
style="height: 43px; padding: 6px"
variant="white"
>
{{ item.discount_type == 'fixed' ? currency.symbol : '%' }}
</button>
<v-dropdown-item>
<a class="dropdown-item" href="#" @click.prevent="selectFixed" >
{{ $t('general.fixed') }}
</a>
</v-dropdown-item>
<v-dropdown-item>
<a class="dropdown-item" href="#" @click.prevent="selectPercentage">
{{ $t('general.percentage') }}
</a>
</v-dropdown-item>
</v-dropdown>
<span class="flex">
{{
item.discount_type == 'fixed' ? currency.symbol : '%'
}}
<chevron-down-icon class="h-5" />
</span>
</sw-button>
<sw-dropdown-item @click="selectFixed">
{{ $t('general.fixed') }}
</sw-dropdown-item>
<sw-dropdown-item @click="selectPercentage">
{{ $t('general.percentage') }}
</sw-dropdown-item>
</sw-dropdown>
</div>
<!-- <div v-if="$v.item.discount.$error"> discount error </div> -->
</div>
</td>
<td class="text-right">
<div class="item-amount">
<td class="px-5 py-4 text-right align-top">
<div class="flex items-center justify-end text-sm">
<span>
<div v-html="$utils.formatMoney(total, currency)" />
</span>
<div class="remove-icon-wrapper">
<font-awesome-icon
<div
class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer"
>
<trash-icon
v-if="isShowRemoveItemIcon"
class="remove-icon"
icon="trash-alt"
class="h-5 text-gray-700"
@click="removeItem"
/>
</div>
@@ -122,8 +128,8 @@
</td>
</tr>
<tr v-if="taxPerItem === 'YES'" class="tax-tr">
<td />
<td colspan="4">
<td class="px-5 py-4 text-left align-top" />
<td colspan="4" class="px-5 py-4 text-left align-top">
<tax
v-for="(tax, index) in item.taxes"
:key="tax.id"
@@ -146,96 +152,100 @@
</template>
<script>
import Guid from 'guid'
import { validationMixin } from 'vuelidate'
import { mapGetters } from 'vuex'
import TaxStub from '../../stub/tax'
import EstimateStub from '../../stub/estimate'
import ItemSelect from './ItemSelect'
import Tax from './Tax'
const { required, minValue, between, maxLength } = require('vuelidate/lib/validators')
import { TrashIcon, ChevronDownIcon } from '@vue-hero-icons/solid'
import DragIcon from '@/components/icon/DragIcon'
const {
required,
minValue,
between,
maxLength,
} = require('vuelidate/lib/validators')
export default {
components: {
Tax,
ItemSelect
ItemSelect,
TrashIcon,
ChevronDownIcon,
DragIcon,
},
mixins: [validationMixin],
props: {
itemData: {
type: Object,
default: null
default: null,
},
index: {
type: Number,
default: null
default: null,
},
type: {
type: String,
default: ''
default: '',
},
currency: {
type: [Object, String],
required: true
required: true,
},
taxPerItem: {
type: String,
default: ''
default: '',
},
discountPerItem: {
type: String,
default: ''
default: '',
},
estimateItems: {
type: Array,
required: true
}
required: true,
},
},
data () {
data() {
return {
isClosePopup: false,
itemSelect: null,
item: {...this.itemData},
item: { ...this.itemData },
maxDiscount: 0,
money: {
decimal: '.',
thousands: ',',
prefix: '$ ',
precision: 2,
masked: false
masked: false,
},
isSelected: false
isSelected: false,
}
},
computed: {
...mapGetters('item', [
'items'
]),
...mapGetters('modal', [
'modalActive'
]),
...mapGetters('currency', [
'defaultCurrencyForInput'
]),
customerCurrency () {
...mapGetters('item', ['items']),
...mapGetters('modal', ['modalActive']),
...mapGetters('company', ['defaultCurrencyForInput']),
customerCurrency() {
if (this.currency) {
return {
decimal: this.currency.decimal_separator,
thousands: this.currency.thousand_separator,
prefix: this.currency.symbol + ' ',
precision: this.currency.precision,
masked: false
masked: false,
}
} else {
return this.defaultCurrencyForInput
}
},
isShowRemoveItemIcon () {
isShowRemoveItemIcon() {
if (this.estimateItems.length == 1) {
return false
}
return true
},
subtotal () {
subtotal() {
return this.item.price * this.item.quantity
},
discount: {
@@ -250,12 +260,12 @@ export default {
}
this.item.discount = newValue
}
},
},
total () {
total() {
return this.subtotal - this.item.discount_val
},
totalSimpleTax () {
totalSimpleTax() {
return window._.sumBy(this.item.taxes, function (tax) {
if (!tax.compound_tax) {
return tax.amount
@@ -264,7 +274,7 @@ export default {
return 0
})
},
totalCompoundTax () {
totalCompoundTax() {
return window._.sumBy(this.item.taxes, function (tax) {
if (tax.compound_tax) {
return tax.amount
@@ -273,7 +283,7 @@ export default {
return 0
})
},
totalTax () {
totalTax() {
return this.totalSimpleTax + this.totalCompoundTax
},
price: {
@@ -291,51 +301,51 @@ export default {
} else {
this.item.price = newValue
}
}
}
},
},
},
watch: {
item: {
handler: 'updateItem',
deep: true
deep: true,
},
subtotal (newValue) {
subtotal(newValue) {
if (this.item.discount_type === 'percentage') {
this.item.discount_val = (this.item.discount * newValue) / 100
}
},
modalActive (val) {
modalActive(val) {
if (!val) {
this.isSelected = false
}
}
},
},
validations () {
validations() {
return {
item: {
name: {
required
required,
},
quantity: {
required,
minValue: minValue(1),
maxLength: maxLength(20)
minValue: minValue(0),
maxLength: maxLength(20),
},
price: {
required,
minValue: minValue(1),
maxLength: maxLength(20)
maxLength: maxLength(20),
},
discount_val: {
between: between(0, this.maxDiscount)
between: between(0, this.maxDiscount),
},
description: {
maxLength: maxLength(255)
}
}
maxLength: maxLength(255),
},
},
}
},
created () {
created() {
window.hub.$on('checkItems', this.validateItem)
window.hub.$on('newItem', (val) => {
if (this.taxPerItem === 'YES') {
@@ -346,36 +356,43 @@ export default {
}
})
},
mounted() {
this.$v.item.$reset()
},
methods: {
updateTax (data) {
updateTax(data) {
this.$set(this.item.taxes, data.index, data.item)
let lastTax = this.item.taxes[this.item.taxes.length - 1]
if (lastTax.tax_type_id !== 0) {
this.item.taxes.push({...TaxStub, id: Guid.raw()})
this.item.taxes.push({ ...TaxStub, id: Guid.raw() })
}
this.updateItem()
},
removeTax (index) {
removeTax(index) {
this.item.taxes.splice(index, 1)
this.updateItem()
},
taxWithPercentage ({ name, percent }) {
taxWithPercentage({ name, percent }) {
return `${name} (${percent}%)`
},
searchVal (val) {
searchVal(val) {
this.item.name = val
},
deselectItem () {
this.item = {...EstimateStub, id: this.item.id, taxes: [{...TaxStub, id: Guid.raw()}]}
deselectItem() {
this.item = {
...EstimateStub,
id: this.item.id,
taxes: [{ ...TaxStub, id: Guid.raw() }],
}
this.$nextTick(() => {
this.$refs.itemSelect.$refs.baseSelect.$refs.search.focus()
})
},
onSelectItem (item) {
onSelectItem(item) {
this.item.name = item.name
this.item.price = item.price
this.item.item_id = item.id
@@ -383,16 +400,13 @@ export default {
this.item.unit_name = item.unit_name
if (this.taxPerItem === 'YES' && item.taxes) {
let index = 0
item.taxes.forEach(tax => {
this.updateTax({index, item: { ...tax }})
item.taxes.forEach((tax) => {
this.updateTax({ index, item: { ...tax } })
index++
})
}
// if (this.item.taxes.length) {
// this.item.taxes = {...item.taxes}
// }
},
selectFixed () {
selectFixed() {
if (this.item.discount_type === 'fixed') {
return
}
@@ -400,7 +414,7 @@ export default {
this.item.discount_val = this.item.discount * 100
this.item.discount_type = 'fixed'
},
selectPercentage () {
selectPercentage() {
if (this.item.discount_type === 'percentage') {
return
}
@@ -409,24 +423,24 @@ export default {
this.item.discount_type = 'percentage'
},
updateItem () {
updateItem() {
this.$emit('update', {
'index': this.index,
'item': {
index: this.index,
item: {
...this.item,
total: this.total,
totalSimpleTax: this.totalSimpleTax,
totalCompoundTax: this.totalCompoundTax,
totalTax: this.totalTax,
tax: this.totalTax,
taxes: [...this.item.taxes]
}
taxes: [...this.item.taxes],
},
})
},
removeItem () {
removeItem() {
this.$emit('remove', this.index)
},
validateItem () {
validateItem() {
this.$v.item.$touch()
if (this.item !== null) {
@@ -434,7 +448,7 @@ export default {
} else {
this.$emit('itemValidate', this.index, false)
}
}
}
},
},
}
</script>

View File

@@ -1,13 +1,20 @@
<template>
<div class="item-selector">
<div v-if="item.item_id" class="selected-item">
<div class="flex-1 text-sm">
<div
v-if="item.item_id"
class="relative flex items-center h-10 pl-2 bg-gray-200 border border-gray-200 border-solid rounded"
>
{{ item.name }}
<span class="deselect-icon" @click="deselectItem">
<font-awesome-icon icon="times-circle" />
<span
class="absolute text-gray-400 cursor-pointer"
style="top: 8px; right: 10px"
@click="deselectItem"
>
<x-circle-icon class="h-5" />
</span>
</div>
<base-select
<sw-select
v-else
ref="baseSelect"
v-model="itemSelect"
@@ -24,25 +31,34 @@
@select="onSelect"
>
<div slot="afterList">
<button type="button" class="list-add-button" @click="openItemModal">
<font-awesome-icon class="icon" icon="cart-plus" />
<label>{{ $t('general.add_new_item') }}</label>
<button
type="button"
class="flex items-center justify-center w-full p-3 bg-gray-200 border-none outline-none"
@click="openItemModal"
>
<shopping-cart-icon class="h-5 text-primary-400" />
<label class="ml-2 text-sm leading-none text-primary-400">{{
$t('general.add_new_item')
}}</label>
</button>
</div>
</base-select>
<div class="item-description">
<base-text-area
</sw-select>
<div class="w-full pt-1 text-xs text-light">
<sw-textarea
v-autoresize
v-model.trim="item.description"
:invalid-description="invalidDescription"
:placeholder="$t('estimates.item.type_item_description')"
type="text"
rows="1"
class="description-input"
variant="inv-desc"
class="w-full text-gray-600 border-none resize-none"
@input="$emit('onDesriptionInput')"
/>
<div v-if="invalidDescription">
<span class="text-danger">{{ $tc('validation.description_maxlength') }}</span>
<span class="text-danger">{{
$tc('validation.description_maxlength')
}}</span>
</div>
</div>
</div>
@@ -50,78 +66,79 @@
<script>
import { mapActions, mapGetters } from 'vuex'
import { validationMixin } from 'vuelidate'
import { XCircleIcon, ShoppingCartIcon } from '@vue-hero-icons/solid'
const { maxLength } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
components: {
XCircleIcon,
ShoppingCartIcon,
},
props: {
item: {
type: Object,
required: true
required: true,
},
invalid: {
type: Boolean,
required: false,
default: false
default: false,
},
invalidDescription: {
type: Boolean,
required: false,
default: false
default: false,
},
taxPerItem: {
type: String,
default: ''
default: '',
},
taxes: {
type: Array,
default: null
}
default: null,
},
},
data () {
data() {
return {
itemSelect: null,
loading: false
loading: false,
}
},
validations () {
validations() {
return {
item: {
description: {
maxLength: maxLength(255)
}
}
maxLength: maxLength(255),
},
},
}
},
computed: {
...mapGetters('item', [
'items'
])
...mapGetters('item', ['items']),
},
watch: {
invalidDescription (newValue) {
invalidDescription(newValue) {
console.log(newValue)
}
},
},
methods: {
...mapActions('modal', [
'openModal'
]),
...mapActions('item', [
'fetchItems'
]),
async searchItems (search) {
...mapActions('modal', ['openModal']),
...mapActions('item', ['fetchItems']),
async searchItems(search) {
let data = {
search,
filter: {
name: search,
unit: '',
price: ''
price: '',
},
orderByField: '',
orderBy: '',
page: 1
page: 1,
}
if (this.item) {
data.item_id = this.item.item_id
}
this.loading = true
@@ -130,27 +147,27 @@ export default {
this.loading = false
},
onTextChange (val) {
onTextChange(val) {
this.searchItems(val)
this.$emit('search', val)
},
openItemModal () {
openItemModal() {
this.$emit('onSelectItem')
this.openModal({
'title': this.$t('items.add_item'),
'componentName': 'ItemModal',
'data': {taxPerItem: this.taxPerItem, taxes: this.taxes}
title: this.$t('items.add_item'),
componentName: 'ItemModal',
data: { taxPerItem: this.taxPerItem, taxes: this.taxes },
})
},
onSelect(val) {
this.$emit('select', val)
this.fetchItems()
},
deselectItem () {
},
deselectItem() {
this.itemSelect = null
this.$emit('deselect')
}
}
},
},
}
</script>

View File

@@ -1,10 +1,10 @@
<template>
<div class="tax-row">
<div class="d-flex align-items-center tax-select">
<label class="bd-highlight pr-2 mb-0" align="right">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center text-base" style="flex: 4">
<label class="pr-2 mb-0" align="right">
{{ $t('estimates.tax') }}
</label>
<base-select
<sw-select
v-model="selectedTax"
:options="filteredTypes"
:allow-empty="false"
@@ -16,20 +16,27 @@
@select="(val) => onSelectTax(val)"
>
<div slot="afterList">
<button type="button" class="list-add-button" @click="openTaxModal">
<font-awesome-icon class="icon" icon="check-circle" />
<label>{{ $t('estimates.add_new_tax') }}</label>
<button
type="button"
class="flex items-center justify-center w-full px-2 py-2 bg-gray-200 border-none outline-none"
@click="openTaxModal"
>
<check-circle-icon class="h-5 text-primary-400" />
<label class="ml-2 text-sm leading-none text-primary-400">{{
$t('estimates.add_new_tax')
}}</label>
</button>
</div>
</base-select> <br>
</sw-select>
<br />
</div>
<div class="text-right tax-amount">
<div class="text-sm text-right" style="flex: 3">
<div v-html="$utils.formatMoney(taxAmount, currency)" />
</div>
<div class="remove-icon-wrapper">
<font-awesome-icon
<div class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer">
<trash-icon
v-if="taxes.length && index !== taxes.length - 1"
class="remove-icon"
class="h-5 text-gray-700"
icon="trash-alt"
@click="removeTax"
/>
@@ -39,49 +46,52 @@
<script>
import { mapActions, mapGetters } from 'vuex'
import { CheckCircleIcon, TrashIcon } from '@vue-hero-icons/solid'
export default {
components: {
CheckCircleIcon,
TrashIcon,
},
props: {
index: {
type: Number,
required: true
required: true,
},
taxData: {
type: Object,
required: true
required: true,
},
taxes: {
type: Array,
default: []
default: [],
},
total: {
type: Number,
default: 0
default: 0,
},
totalTax: {
type: Number,
default: 0
default: 0,
},
currency: {
type: [Object, String],
required: true
}
required: true,
},
},
data () {
data() {
return {
tax: {...this.taxData},
selectedTax: null
tax: { ...this.taxData },
selectedTax: null,
}
},
computed: {
...mapGetters('taxType', [
'taxTypes'
]),
filteredTypes () {
const clonedTypes = this.taxTypes.map(a => ({...a}))
...mapGetters('taxType', ['taxTypes']),
filteredTypes() {
const clonedTypes = this.taxTypes.map((a) => ({ ...a }))
return clonedTypes.map((taxType) => {
let found = this.taxes.find(tax => tax.tax_type_id === taxType.id)
let found = this.taxes.find((tax) => tax.tax_type_id === taxType.id)
if (found) {
taxType.$isDisabled = true
@@ -92,7 +102,7 @@ export default {
return taxType
})
},
taxAmount () {
taxAmount() {
if (this.tax.compound_tax && this.total) {
return ((this.total + this.totalTax) * this.tax.percent) / 100
}
@@ -102,19 +112,21 @@ export default {
}
return 0
}
},
},
watch: {
total: {
handler: 'updateTax'
handler: 'updateTax',
},
totalTax: {
handler: 'updateTax'
}
handler: 'updateTax',
},
},
created () {
created() {
if (this.taxData.tax_type_id > 0) {
this.selectedTax = this.taxTypes.find(_type => _type.id === this.taxData.tax_type_id)
this.selectedTax = this.taxTypes.find(
(_type) => _type.id === this.taxData.tax_type_id
)
}
window.hub.$on('newTax', (val) => {
@@ -127,13 +139,11 @@ export default {
this.updateTax()
},
methods: {
...mapActions('modal', [
'openModal'
]),
customLabel ({ name, percent }) {
...mapActions('modal', ['openModal']),
customLabel({ name, percent }) {
return `${name} - ${percent}%`
},
onSelectTax (val) {
onSelectTax(val) {
this.tax.percent = val.percent
this.tax.tax_type_id = val.id
this.tax.compound_tax = val.compound_tax
@@ -141,28 +151,28 @@ export default {
this.updateTax()
},
updateTax () {
updateTax() {
if (this.tax.tax_type_id === 0) {
return
}
this.$emit('update', {
'index': this.index,
'item': {
index: this.index,
item: {
...this.tax,
amount: this.taxAmount
}
amount: this.taxAmount,
},
})
},
removeTax () {
removeTax() {
this.$emit('remove', this.index, this.tax)
},
openTaxModal () {
openTaxModal() {
this.openModal({
'title': this.$t('settings.tax_types.add_tax'),
'componentName': 'TaxTypeModal'
title: this.$t('settings.tax_types.add_tax'),
componentName: 'TaxTypeModal',
})
}
}
},
},
}
</script>

View File

@@ -1,202 +1,244 @@
<template>
<div v-if="estimate" class="main-content estimate-view-page">
<div class="page-header">
<h3 class="page-title">{{ estimate.estimate_number }}</h3>
<div class="page-actions row">
<div class="col-xs-2 mr-3">
<base-button
<base-page v-if="estimate" class="xl:pl-96">
<sw-page-header :title="pageTitle">
<template slot="actions">
<div class="mr-3 text-sm">
<sw-button
v-if="estimate.status === 'DRAFT'"
:loading="isMarkAsSent"
:disabled="isMarkAsSent"
:outline="true"
color="theme"
variant="primary-outline"
@click="onMarkAsSent"
>
{{ $t('estimates.mark_as_sent') }}
</base-button>
</sw-button>
</div>
<div class="col-xs-2">
<base-button
v-if="estimate.status === 'DRAFT'"
:loading="isSendingEmail"
:disabled="isSendingEmail"
color="theme"
@click="onSendEstimate"
>
{{ $t('estimates.send_estimate') }}
</base-button>
</div>
<v-dropdown
:close-on-select="true"
align="left"
class="filter-container"
<sw-button
v-if="estimate.status === 'DRAFT'"
:disabled="isSendingEmail"
variant="primary"
class="text-sm"
@click="onSendEstimate"
>
<a slot="activator" href="#">
<base-button color="theme">
<font-awesome-icon icon="ellipsis-h" />
</base-button>
</a>
<v-dropdown-item>
<div class="dropdown-item" @click="copyPdfUrl()">
<font-awesome-icon
:icon="['fas', 'link']"
class="dropdown-item-icon"
/>
{{ $t('general.copy_pdf_url') }}
</div>
<router-link
:to="{ path: `/admin/estimates/${$route.params.id}/edit` }"
class="dropdown-item"
>
<font-awesome-icon
:icon="['fas', 'pencil-alt']"
class="dropdown-item-icon"
/>
{{ $t('general.edit') }}
</router-link>
<div
class="dropdown-item"
@click="removeEstimate($route.params.id)"
>
<font-awesome-icon
:icon="['fas', 'trash']"
class="dropdown-item-icon"
/>
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
</div>
</div>
<div class="estimate-sidebar">
<base-loader v-if="isSearching" />
<div v-else class="side-header">
<base-input
{{ $t('estimates.send_estimate') }}
</sw-button>
<sw-dropdown class="ml-3">
<sw-button slot="activator" variant="primary">
<dots-horizontal-icon class="h-5" />
</sw-button>
<sw-dropdown-item @click="copyPdfUrl">
<link-icon class="h-5 mr-3 text-primary-800" />
{{ $t('general.copy_pdf_url') }}
</sw-dropdown-item>
<sw-dropdown-item
tag-name="router-link"
:to="`/admin/estimates/${$route.params.id}/edit`"
>
<pencil-icon class="h-5 mr-3 text-primary-800" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removeEstimate($route.params.id)">
<trash-icon class="h-5 mr-3 text-primary-800" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</sw-page-header>
<!-- sidebar -->
<div
class="fixed top-0 left-0 hidden h-full pt-16 pb-4 ml-56 bg-white xl:ml-64 w-88 xl:block"
>
<div
class="flex items-center justify-between px-4 pt-8 pb-2 border border-gray-200 border-solid height-full"
>
<sw-input
v-model="searchData.searchText"
:placeholder="$t('general.search')"
input-class="inv-search"
icon="search"
class="mb-6"
type="text"
align-icon="right"
variant="gray"
@input="onSearched()"
/>
<div class="btn-group ml-3" role="group" aria-label="First group">
<v-dropdown
:close-on-select="false"
align="left"
class="filter-container"
>
<a slot="activator" href="#">
<base-button
class="inv-button inv-filter-fields-btn"
color="default"
size="medium"
>
<font-awesome-icon icon="filter" />
</base-button>
</a>
<div class="filter-title">
>
<search-icon slot="rightIcon" class="h-5" />
</sw-input>
<div class="flex mb-6 ml-3" role="group" aria-label="First group">
<sw-dropdown class="ml-3" position="bottom-start">
<sw-button slot="activator" size="md" variant="gray-light">
<filter-icon class="h-5" />
</sw-button>
<div
class="px-2 py-1 pb-2 mb-1 mb-2 text-sm border-b border-gray-200 border-solid"
>
{{ $t('general.sort_by') }}
</div>
<div class="filter-items">
<input
id="filter_estimate_date"
v-model="searchData.orderByField"
type="radio"
name="filter"
class="inv-radio"
value="estimate_date"
@change="onSearched"
/>
<label class="inv-label" for="filter_estimate_date">{{
$t('reports.estimates.estimate_date')
}}</label>
</div>
<div class="filter-items">
<input
id="filter_due_date"
v-model="searchData.orderByField"
type="radio"
name="filter"
class="inv-radio"
value="expiry_date"
@change="onSearched"
/>
<label class="inv-label" for="filter_due_date">{{
$t('estimates.due_date')
}}</label>
</div>
<div class="filter-items">
<input
id="filter_estimate_number"
v-model="searchData.orderByField"
type="radio"
name="filter"
class="inv-radio"
value="estimate_number"
@change="onSearched"
/>
<label class="inv-label" for="filter_estimate_number">{{
$t('estimates.estimate_number')
}}</label>
</div>
</v-dropdown>
<base-button
<sw-dropdown-item class="flex px-1 py-2 cursor-pointer">
<sw-input-group class="-mt-3 font-normal">
<sw-radio
id="filter_estimate_date"
v-model="searchData.orderByField"
:label="$t('reports.estimates.estimate_date')"
size="sm"
name="filter"
value="estimate_date"
@change="onSearched"
/>
</sw-input-group>
</sw-dropdown-item>
<sw-dropdown-item class="flex px-1 py-2 cursor-pointer">
<sw-input-group class="-mt-3 font-normal">
<sw-radio
id="filter_due_date"
v-model="searchData.orderByField"
value="expiry_date"
:label="$t('estimates.due_date')"
size="sm"
name="filter"
@change="onSearched"
/>
</sw-input-group>
</sw-dropdown-item>
<sw-dropdown-item class="flex px-1 py-2 cursor-pointer">
<sw-input-group class="-mt-3 font-normal">
<sw-radio
id="filter_estimate_number"
v-model="searchData.orderByField"
value="estimate_number"
:label="$t('estimates.estimate_number')"
size="sm"
name="filter"
@change="onSearched"
/>
</sw-input-group>
</sw-dropdown-item>
</sw-dropdown>
<sw-button
class="ml-1"
v-tooltip.top-center="{ content: getOrderName }"
class="inv-button inv-filter-sorting-btn"
color="default"
size="medium"
size="md"
variant="gray-light"
@click="sortData"
>
<font-awesome-icon v-if="getOrderBy" icon="sort-amount-up" />
<font-awesome-icon v-else icon="sort-amount-down" />
</base-button>
<sort-ascending-icon v-if="getOrderBy" class="h-5" />
<sort-descending-icon v-else class="h-5" />
</sw-button>
</div>
</div>
<div class="side-content">
<base-loader v-if="isSearching" :show-bg-overlay="true" />
<div
v-else
class="h-full pb-32 overflow-y-scroll border-l border-gray-200 border-solid sw-scroll"
>
<router-link
v-for="(estimate, index) in estimates"
:to="`/admin/estimates/${estimate.id}/view`"
:id="'estimate-' + estimate.id"
:key="index"
class="side-estimate"
:class="[
'flex justify-between side-estimate p-4 cursor-pointer hover:bg-gray-100 items-center border-l-4 border-transparent',
{
'bg-gray-100 border-l-4 border-primary-500 border-solid': hasActiveUrl(
estimate.id
),
},
]"
style="border-bottom: 1px solid rgba(185, 193, 209, 0.41)"
>
<div class="left">
<div class="inv-name">{{ estimate.user.name }}</div>
<div class="inv-number">{{ estimate.estimate_number }}</div>
<div class="flex-2">
<div
:class="'est-status-' + estimate.status.toLowerCase()"
class="inv-status"
class="pr-2 mb-2 text-sm not-italic font-normal leading-5 text-black capitalize truncate"
>
{{ estimate.user.name }}
</div>
<div
class="mt-1 mb-2 text-xs not-italic font-medium leading-5 text-gray-600"
>
{{ estimate.estimate_number }}
</div>
<sw-badge
class="px-1 text-xs"
:bg-color="$utils.getBadgeStatusColor(estimate.status).bgColor"
:color="$utils.getBadgeStatusColor(estimate.status).color"
>
{{ estimate.status }}
</div>
</sw-badge>
</div>
<div class="right">
<div class="flex-1 whitespace-no-wrap right">
<div
class="inv-amount"
class="mb-2 text-xl not-italic font-semibold leading-8 text-right text-gray-900"
v-html="
$utils.formatMoney(estimate.total, estimate.user.currency)
"
/>
<div class="inv-date">{{ estimate.formattedEstimateDate }}</div>
<div
class="text-sm not-italic font-normal leading-5 text-right text-gray-600 est-date"
>
{{ estimate.formattedEstimateDate }}
</div>
</div>
</router-link>
<p v-if="!estimates.length" class="no-result">
<p
v-if="!estimates.length"
class="flex justify-center px-4 mt-5 text-sm text-gray-600"
>
{{ $t('estimates.no_matching_estimates') }}
</p>
</div>
</div>
<div class="estimate-view-page-container">
<iframe :src="`${shareableLink}`" class="frame-style" />
<div
class="flex flex-col min-h-0 mt-8 overflow-hidden sw-scroll"
style="height: 75vh"
>
<iframe
:src="`${shareableLink}`"
class="flex-1 border border-gray-400 border-solid rounded-md frame-style"
/>
</div>
</div>
</base-page>
</template>
<script>
import { mapActions } from 'vuex'
import {
DotsHorizontalIcon,
FilterIcon,
SortAscendingIcon,
SortDescendingIcon,
SearchIcon,
LinkIcon,
TrashIcon,
PencilIcon,
} from '@vue-hero-icons/solid'
const _ = require('lodash')
export default {
data () {
components: {
DotsHorizontalIcon,
FilterIcon,
SortAscendingIcon,
SortDescendingIcon,
SearchIcon,
LinkIcon,
TrashIcon,
PencilIcon,
},
data() {
return {
id: null,
count: null,
@@ -206,38 +248,50 @@ export default {
searchData: {
orderBy: null,
orderByField: null,
searchText: null
searchText: null,
},
status: ['DRAFT', 'SENT', 'VIEWED', 'EXPIRED', 'ACCEPTED', 'REJECTED'],
isMarkAsSent: false,
isSendingEmail: false,
isRequestOnGoing: false,
isSearching: false
isSearching: false,
}
},
computed: {
getOrderBy () {
if (this.searchData.orderBy === 'asc' || this.searchData.orderBy == null) {
pageTitle() {
return this.estimate.estimate_number
},
getOrderBy() {
if (
this.searchData.orderBy === 'asc' ||
this.searchData.orderBy == null
) {
return true
}
return false
},
getOrderName () {
getOrderName() {
if (this.getOrderBy) {
return this.$t('general.ascending')
}
return this.$t('general.descending')
},
shareableLink () {
shareableLink() {
return `/estimates/pdf/${this.estimate.unique_hash}`
}
},
getCurrentEstimateId() {
if (this.estimate && this.estimate.id) {
return this.estimate.id
}
return null
},
},
watch: {
$route (to, from) {
$route(to, from) {
this.loadEstimate()
}
},
},
created () {
created() {
this.loadEstimates()
this.loadEstimate()
this.onSearched = _.debounce(this.onSearched, 500)
@@ -251,32 +305,67 @@ export default {
'sendEmail',
'deleteEstimate',
'selectEstimate',
'fetchViewEstimate'
'fetchViewEstimate',
]),
async loadEstimates () {
let response = await this.fetchEstimates()
...mapActions('modal', ['openModal']),
hasActiveUrl(id) {
return this.$route.params.id == id
},
async loadEstimates() {
let response = await this.fetchEstimates({ limit: 'all' })
if (response.data) {
this.estimates = response.data.estimates.data
}
setTimeout(() => {
this.scrollToEstimate()
}, 500)
},
async loadEstimate () {
scrollToEstimate() {
const el = document.getElementById(`estimate-${this.$route.params.id}`)
if (el) {
el.scrollIntoView({ behavior: 'smooth' })
el.classList.add('shake')
}
},
async loadEstimate() {
let response = await this.fetchViewEstimate(this.$route.params.id)
if (response.data) {
this.estimate = response.data.estimate
this.estimate = { ...response.data.estimate }
}
},
async onSearched () {
copyPdfUrl() {
let pdfUrl = `${window.location.origin}/estimates/pdf/${this.estimate.unique_hash}`
let response = this.$utils.copyTextToClipboard(pdfUrl)
window.toastr['success'](this.$tc('general.copied_pdf_url_clipboard'))
},
async onSearched() {
let data = ''
if (this.searchData.searchText !== '' && this.searchData.searchText !== null && this.searchData.searchText !== undefined) {
if (
this.searchData.searchText !== '' &&
this.searchData.searchText !== null &&
this.searchData.searchText !== undefined
) {
data += `search=${this.searchData.searchText}&`
}
if (this.searchData.orderBy !== null && this.searchData.orderBy !== undefined) {
if (
this.searchData.orderBy !== null &&
this.searchData.orderBy !== undefined
) {
data += `orderBy=${this.searchData.orderBy}&`
}
if (this.searchData.orderByField !== null && this.searchData.orderByField !== undefined) {
if (
this.searchData.orderByField !== null &&
this.searchData.orderByField !== undefined
) {
data += `orderByField=${this.searchData.orderByField}`
}
this.isSearching = true
@@ -286,7 +375,7 @@ export default {
this.estimates = response.data.estimates.data
}
},
sortData () {
sortData() {
if (this.searchData.orderBy === 'asc') {
this.searchData.orderBy = 'desc'
this.onSearched()
@@ -296,74 +385,68 @@ export default {
this.onSearched()
return true
},
async onMarkAsSent () {
window.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('estimates.confirm_mark_as_sent'),
icon: '/assets/icon/check-circle-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
this.isMarkAsSent = true
let response = await this.markAsSent({id: this.estimate.id})
this.isMarkAsSent = false
if (response.data) {
window.toastr['success'](this.$tc('estimates.mark_as_sent_successfully'))
async onMarkAsSent() {
window
.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('estimates.confirm_mark_as_sent'),
icon: '/assets/icon/check-circle-solid.svg',
buttons: true,
dangerMode: true,
})
.then(async (value) => {
if (value) {
this.isMarkAsSent = true
let response = await this.markAsSent({
id: this.estimate.id,
status: 'SENT',
})
this.isMarkAsSent = false
if (response.data) {
this.estimate.status = 'SENT'
window.toastr['success'](
this.$tc('estimates.mark_as_sent_successfully')
)
}
}
}
})
},
async onSendEstimate(id) {
this.openModal({
title: this.$t('estimates.send_estimate'),
componentName: 'SendEstimateModal',
id: this.estimate.id,
data: this.estimate,
})
},
async onSendEstimate (id) {
window.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('estimates.confirm_send_estimate'),
icon: '/assets/icon/paper-plane-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
this.isSendingEmail = true
let response = await this.sendEmail({id: this.estimate.id})
this.isSendingEmail = false
if (response.data.success) {
window.toastr['success'](this.$tc('estimates.send_estimate_successfully'))
return true
}
if (response.data.error === 'user_email_does_not_exist') {
window.toastr['error'](this.$tc('estimates.user_email_does_not_exist'))
return true
}
window.toastr['error'](this.$tc('estimates.something_went_wrong'))
}
})
},
copyPdfUrl () {
copyPdfUrl() {
let pdfUrl = `${window.location.origin}/estimates/pdf/${this.estimate.unique_hash}`
let response = this.$utils.copyTextToClipboard(pdfUrl)
window.toastr['success'](this.$tc('Copied PDF url to clipboard!'))
window.toastr['success'](this.$tc('general.copied_pdf_url_clipboard'))
},
async removeEstimate (id) {
window.swal({
title: 'Deleted',
text: 'you will not be able to recover this estimate!',
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
let request = await this.deleteEstimate(id)
if (request.data.success) {
window.toastr['success'](this.$tc('estimates.deleted_message', 1))
this.$router.push('/admin/estimates')
} else if (request.data.error) {
window.toastr['error'](request.data.message)
async removeEstimate(id) {
window
.swal({
title: this.$t('general.are_you_sure'),
text: 'you will not be able to recover this estimate!',
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
})
.then(async (value) => {
if (value) {
let request = await this.deleteEstimate({ ids: [id] })
if (request.data.success) {
window.toastr['success'](this.$tc('estimates.deleted_message', 1))
this.$router.push('/admin/estimates')
} else if (request.data.error) {
window.toastr['error'](request.data.message)
}
}
}
})
}
}
})
},
},
}
</script>

View File

@@ -1,307 +1,437 @@
<template>
<div class="main-content expenses">
<base-page class="relative">
<form action="" @submit.prevent="sendData">
<div class="page-header">
<h3 class="page-title">{{ isEdit ? $t('expenses.edit_expense') : $t('expenses.new_expense') }}</h3>
<ol class="breadcrumb">
<li class="breadcrumb-item"><router-link slot="item-title" to="/admin/dashboard">{{ $t('general.home') }}</router-link></li>
<li class="breadcrumb-item"><router-link slot="item-title" to="/admin/expenses">{{ $tc('expenses.expense', 2) }}</router-link></li>
<li class="breadcrumb-item"><a href="#">{{ isEdit ? $t('expenses.edit_expense') : $t('expenses.new_expense') }}</a></li>
</ol>
<div class="page-actions row header-button-container">
<div v-if="isReceiptAvailable" class="col-xs-2 mr-4">
<a :href="getReceiptUrl">
<base-button
:loading="isLoading"
icon="download"
color="theme"
outline
>
{{ $t('expenses.download_receipt') }}
</base-button>
</a>
</div>
<div class="col-xs-2">
<base-button
<!-- Page Header -->
<sw-page-header class="mb-5" :title="pageTitle">
<sw-breadcrumb slot="breadcrumbs">
<sw-breadcrumb-item
to="/admin/dashboard"
:title="$t('general.home')"
/>
<sw-breadcrumb-item
to="/admin/expenses"
:title="$tc('expenses.expense', 2)"
/>
<sw-breadcrumb-item
v-if="$route.name === 'expenses.edit'"
to="#"
:title="$t('expenses.edit_expense')"
active
/>
<sw-breadcrumb-item
v-else
to="#"
:title="$t('expenses.new_expense')"
active
/>
</sw-breadcrumb>
<template slot="actions">
<sw-button
v-if="isReceiptAvailable"
tag-name="a"
:href="getReceiptUrl"
variant="primary"
outline
size="lg"
class="mr-2"
>
<download-icon class="h-5 mr-2 -ml-1" />
{{ $t('expenses.download_receipt') }}
</sw-button>
<div class="hidden md:block">
<sw-button
:loading="isLoading"
icon="save"
color="theme"
:disabled="isLoading"
variant="primary"
type="submit"
size="lg"
>
{{ isEdit ? $t('expenses.update_expense') : $t('expenses.save_expense') }}
</base-button>
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
{{
isEdit
? $t('expenses.update_expense')
: $t('expenses.save_expense')
}}
</sw-button>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="card">
<div class="card-body">
<div class="row">
<!-- <div class="form-group col-sm-6">
<label class="control-label">{{ $t('expenses.expense_title') }}</label>
<input v-model="formData.title" type="text" name="name" class="form-control">
</div> -->
<div class="form-group col-sm-6">
<label class="control-label">{{ $t('expenses.category') }}</label><span class="text-danger"> * </span>
<base-select
ref="baseSelect"
v-model="category"
:options="categories"
:invalid="$v.category.$error"
:searchable="true"
:show-labels="false"
:placeholder="$t('expenses.categories.select_a_category')"
label="name"
track-by="id"
@input="$v.category.$touch()"
</template>
</sw-page-header>
<base-loader v-if="isRequestOnGoing" :show-bg-overlay="true" />
<sw-card v-else>
<div class="grid gap-6 grid-col-1 md:grid-cols-2">
<sw-input-group
:label="$t('expenses.category')"
:error="categoryError"
required
>
<sw-select
ref="baseSelect"
v-model="category"
:options="categories"
:invalid="$v.category.$error"
:searchable="true"
:show-labels="false"
:placeholder="$t('expenses.categories.select_a_category')"
class="mt-2"
label="name"
track-by="id"
@input="$v.category.$touch()"
>
<sw-button
slot="afterList"
type="button"
variant="gray-light"
class="flex items-center justify-center w-full px-4 py-3 bg-gray-200 border-none outline-none"
@click="openCategoryModal"
>
<shopping-cart-icon class="h-5 text-center text-primary-400" />
<label class="ml-2 text-xs leading-none text-primary-400">{{
$t('settings.expense_category.add_new_category')
}}</label>
</sw-button>
</sw-select>
</sw-input-group>
<sw-input-group
:label="$t('expenses.expense_date')"
:error="dateError"
required
>
<base-date-picker
v-model="formData.expense_date"
:invalid="$v.formData.expense_date.$error"
:calendar-button="true"
class="mt-2"
calendar-button-icon="calendar"
@change="$v.formData.expense_date.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('expenses.amount')"
:error="amountError"
required
>
<sw-money
v-model="amount"
:currency="defaultCurrencyForInput"
class="focus:border focus:border-solid focus:border-primary-500"
:invalid="$v.formData.amount.$error"
@input="$v.formData.amount.$touch()"
/>
</sw-input-group>
<sw-input-group :label="$t('expenses.customer')">
<sw-select
ref="baseSelect"
v-model="customer"
:options="customers"
:searchable="true"
:show-labels="false"
:placeholder="$t('customers.select_a_customer')"
class="mt-1"
label="name"
track-by="id"
/>
</sw-input-group>
<sw-input-group :label="$t('expenses.note')" :error="notesError">
<sw-textarea
v-model="formData.notes"
rows="4"
@input="$v.formData.notes.$touch()"
/>
</sw-input-group>
<sw-input-group :label="$t('expenses.receipt')">
<div
id="receipt-box"
class="relative flex items-center justify-center h-24 p-6 bg-transparent border-2 border-gray-200 border-dashed rounded-md image-upload-box"
>
<img
v-if="previewReceipt"
:src="previewReceipt"
class="absolute opacity-100 preview-logo"
style="max-height: 80%; animation: fadeIn 2s ease"
/>
<div v-else class="flex flex-col items-center">
<cloud-upload-icon
class="h-5 mb-2 text-xl leading-6 text-gray-400"
/>
<p class="text-xs leading-4 text-center text-gray-400">
Drag a file here or
<span id="pick-avatar" class="cursor-pointer text-primary-500"
>browse</span
>
<div slot="afterList">
<button type="button" class="list-add-button" @click="openCategoryModal">
<font-awesome-icon class="icon" icon="cart-plus" />
<label>{{ $t('settings.expense_category.add_new_category') }}</label>
</button>
</div>
</base-select>
<div v-if="$v.category.$error">
<span v-if="!$v.category.required" class="text-danger">{{ $t('validation.required') }}</span>
</div>
</div>
<!-- <div class="form-group col-sm-6">
<label>{{ $t('expenses.contact') }}</label>
<select v-model="formData.contact" name="contact" class="form-control ls-select2">
<option v-for="(contact, index) in contacts" :key="index" :value="contact.id"> {{ contact.name }}</option>
</select>
</div> -->
<div class="form-group col-sm-6">
<label>{{ $t('expenses.expense_date') }}</label><span class="text-danger"> * </span>
<base-date-picker
v-model="formData.expense_date"
:invalid="$v.formData.expense_date.$error"
:calendar-button="true"
calendar-button-icon="calendar"
@change="$v.formData.expense_date.$touch()"
/>
<div v-if="$v.formData.expense_date.$error">
<span v-if="!$v.formData.expense_date.required" class="text-danger">{{ $t('validation.required') }}</span>
</div>
</div>
<div class="form-group col-sm-6">
<label>{{ $t('expenses.amount') }}</label> <span class="text-danger"> * </span>
<div class="base-input">
<money
:class="{'invalid' : $v.formData.amount.$error}"
v-model="amount"
v-bind="defaultCurrencyForInput"
class="input-field"
/>
</div>
<div v-if="$v.formData.amount.$error">
<span v-if="!$v.formData.amount.required" class="text-danger">{{ $t('validation.required') }} </span>
<span v-if="!$v.formData.amount.maxLength" class="text-danger">{{ $t('validation.price_maxlength') }}</span>
<span v-if="!$v.formData.amount.minValue" class="text-danger">{{ $t('validation.price_minvalue') }}</span>
</div>
</div>
<div class="form-group col-sm-6">
<label class="form-label">{{ $t('expenses.customer') }}</label>
<base-select
ref="baseSelect"
v-model="customer"
:options="customerList"
:searchable="true"
:show-labels="false"
:placeholder="$t('customers.select_a_customer')"
label="name"
track-by="id"
/>
</div>
<div class="form-group col-sm-6">
<label for="description">{{ $t('expenses.note') }}</label>
<base-text-area
v-model="formData.notes"
@input="$v.formData.notes.$touch()"
/>
<div v-if="$v.formData.notes.$error">
<span v-if="!$v.formData.notes.maxLength" class="text-danger">{{ $t('validation.notes_maxlength') }}</span>
</div>
</div>
<div class="form-group col-md-6">
<label for="description">{{ $t('expenses.receipt') }} : </label>
<div class="image-upload-box" @click="$refs.file.click()">
<input ref="file" class="d-none" type="file" @change="onFileChange">
<img v-if="previewReceipt" :src="previewReceipt" class="preview-logo">
<div v-else class="upload-content">
<font-awesome-icon class="upload-icon" icon="cloud-upload-alt"/>
<p class="upload-text"> {{ $t('general.choose_file') }} </p>
</div>
</div>
</div>
<div class="col-sm-12">
<div class="form-group collapse-button-container">
<base-button
:loading="isLoading"
icon="save"
color="theme"
type="submit"
class="collapse-button"
>
{{ isEdit ? $t('expenses.update_expense') : $t('expenses.save_expense') }}
</base-button>
</div>
</div>
to choose a file
</p>
</div>
</div>
<sw-avatar
trigger="#receipt-box"
:preview-avatar="previewReceipt"
:enable-cropper="false"
@changed="onChange"
>
<template v-slot:icon>
<cloud-upload-icon
class="h-5 mb-2 text-xl leading-6 text-gray-400"
/>
</template>
</sw-avatar>
</sw-input-group>
</div>
<div v-if="customFields.length > 0">
<div class="grid gap-6 mt-6 grid-col-1 md:grid-cols-2">
<sw-input-group
v-for="(field, index) in customFields"
:label="field.label"
:required="field.is_required ? true : false"
:key="index"
>
<component
:type="field.type.label"
:field="field"
:isEdit="isEdit"
:is="field.type + 'Field'"
:invalid-fields="invalidFields"
@update="setCustomFieldValue"
/>
</sw-input-group>
</div>
</div>
</div>
<div class="block mt-2 md:hidden">
<sw-button
:disabled="isLoading"
:loading="isLoading"
:tabindex="6"
variant="primary"
type="submit"
size="lg"
class="flex w-full"
>
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
{{
isEdit
? $t('expenses.update_expense')
: $t('expenses.save_expense')
}}
</sw-button>
</div>
</sw-card>
</form>
</div>
</base-page>
</template>
<script>
import MultiSelect from 'vue-multiselect'
import moment from 'moment'
import { mapActions, mapGetters } from 'vuex'
import { validationMixin } from 'vuelidate'
const { required, minValue, maxLength } = require('vuelidate/lib/validators')
import { DownloadIcon } from '@vue-hero-icons/outline'
import { CloudUploadIcon, ShoppingCartIcon } from '@vue-hero-icons/solid'
import CustomFieldsMixin from '../../mixins/customFields'
export default {
mixins: [CustomFieldsMixin],
components: {
MultiSelect
CloudUploadIcon,
ShoppingCartIcon,
DownloadIcon,
},
mixins: [validationMixin],
props: {
addname: {
type: String,
default: ''
}
default: '',
},
},
data () {
data() {
return {
formData: {
expense_category_id: null,
expense_date: new Date(),
amount: null,
amount: 100,
notes: '',
user_id: null
user_id: null,
},
money: {
decimal: '.',
thousands: ',',
prefix: '$ ',
precision: 2,
masked: false
masked: false,
},
isRequestOnGoing: false,
isReceiptAvailable: false,
isLoading: false,
file: null,
category: null,
passData: [],
contacts: [],
previewReceipt: null,
fileSendUrl: '/api/expenses',
fileSendUrl: '/api/v1/expenses',
customer: null,
customerList: []
fileObject: null,
}
},
validations: {
category: {
required
required,
},
formData: {
expense_date: {
required
required,
},
amount: {
required,
minValue: minValue(0.1),
maxLength: maxLength(20)
maxLength: maxLength(20),
},
notes: {
maxLength: maxLength(255)
}
}
maxLength: maxLength(255),
},
},
},
computed: {
...mapGetters('currency', [
'defaultCurrencyForInput'
]),
...mapGetters('company', ['defaultCurrencyForInput']),
amount: {
get: function () {
return this.formData.amount / 100
},
set: function (newValue) {
this.formData.amount = newValue * 100
}
},
},
isEdit () {
pageTitle() {
if (this.$route.name === 'expenses.edit') {
return this.$t('expenses.edit_expense')
}
return this.$t('expenses.new_expense')
},
isEdit() {
if (this.$route.name === 'expenses.edit') {
return true
}
return false
},
...mapGetters('category', [
'categories'
]),
...mapGetters('company', [
'getSelectedCompany'
]),
getReceiptUrl () {
...mapGetters('category', ['categories']),
...mapGetters('customer', ['customers']),
...mapGetters('company', ['getSelectedCompany']),
getReceiptUrl() {
if (this.isEdit) {
return `/expenses/${this.$route.params.id}/receipt/${this.getSelectedCompany.unique_hash}`
return `/expenses/${this.$route.params.id}/receipt`
}
}
},
categoryError() {
if (!this.$v.category.$error) {
return ''
}
if (!this.$v.category.required) {
return this.$t('validation.required')
}
},
dateError() {
if (!this.$v.formData.expense_date.$error) {
return ''
}
if (!this.$v.formData.expense_date.required) {
return this.$t('validation.required')
}
},
amountError() {
if (!this.$v.formData.amount.$error) {
return ''
}
if (!this.$v.formData.amount.required) {
return this.$t('validation.required')
}
if (!this.$v.formData.amount.maxLength) {
return this.$t('validation.price_maxlength')
}
if (!this.$v.formData.amount.minValue) {
return this.$t('validation.price_minvalue')
}
},
notesError() {
if (!this.$v.formData.notes.$error) {
return ''
}
if (!this.$v.formData.notes.maxLength) {
return this.$t('validation.notes_maxlength')
}
},
},
watch: {
category (newValue) {
category(newValue) {
this.formData.expense_category_id = newValue.id
}
},
},
mounted () {
// this.$refs.baseSelect.$refs.search.focus()
this.fetchInitialData()
if (this.isEdit) {
this.getReceipt()
}
mounted() {
this.$v.formData.$reset()
this.loadData()
window.hub.$on('newCategory', (val) => {
this.category = val
})
},
methods: {
...mapActions('expense', [
'fetchCreateExpense',
'getFile',
'sendFileWithData',
'getExpenseReceipt',
'addExpense',
'updateExpense',
'fetchExpense'
'fetchExpense',
]),
...mapActions('modal', [
'openModal'
]),
...mapActions('category', [
'fetchCategories'
]),
openCategoryModal () {
...mapActions('modal', ['openModal']),
...mapActions('category', ['fetchCategories']),
...mapActions('customer', ['fetchCustomers']),
openCategoryModal() {
this.openModal({
'title': 'Add Category',
'componentName': 'CategoryModal'
title: this.$t('settings.expense_category.add_category'),
componentName: 'CategoryModal',
})
// this.$refs.table.refresh()
},
onFileChange (e) {
var input = event.target
this.file = input.files[0]
if (input.files && input.files[0]) {
var reader = new FileReader()
reader.onload = (e) => {
this.previewReceipt = e.target.result
}
reader.readAsDataURL(input.files[0])
}
onChange(data) {
this.previewReceipt = data.image
this.fileObject = data.file
},
async getReceipt () {
let res = await axios.get(`/api/expenses/${this.$route.params.id}/show/receipt`)
async getReceipt() {
let res = await this.getExpenseReceipt(this.$route.params.id)
if (res.data.error) {
this.isReceiptAvailable = false
@@ -311,47 +441,99 @@ export default {
this.isReceiptAvailable = true
this.previewReceipt = res.data.image
},
async fetchInitialData () {
this.fetchCategories()
let fetchData = await this.fetchCreateExpense()
this.customerList = fetchData.data.customers
if (this.isEdit) {
let response = await this.fetchExpense(this.$route.params.id)
this.category = response.data.expense.category
this.formData = { ...response.data.expense }
this.formData.expense_date = moment(this.formData.expense_date).toString()
this.formData.amount = (response.data.expense.amount)
this.fileSendUrl = `/api/expenses/${this.$route.params.id}`
if (response.data.expense.user_id) {
this.customer = this.customerList.find(customer => customer.id === response.data.expense.user_id)
}
}
setExpenseCustomer(id) {
this.customer = this.customers.find((c) => {
return c.id == id
})
},
async sendData () {
async loadData() {
this.isRequestOnGoing = true
await this.fetchCategories({ limit: 'all' })
await this.fetchCustomers({ limit: 'all' })
if (this.isEdit) {
this.isRequestOnGoing = true
let response = await this.fetchExpense(this.$route.params.id)
this.formData = { ...this.formData, ...response.data.expense }
this.formData.expense_date = moment(
this.formData.expense_date
).toString()
this.formData.amount = response.data.expense.amount
this.fileSendUrl = `/api/v1/expenses/${this.$route.params.id}`
if (response.data.expense.expense_category_id) {
this.category = this.categories.find(
(category) =>
category.id === response.data.expense.expense_category_id
)
}
if (response.data.expense.user_id) {
this.customer = this.customers.find(
(customer) => customer.id === response.data.expense.user_id
)
}
let res = await this.fetchCustomFields({
type: 'Expense',
limit: 'all',
})
this.setEditCustomFields(
response.data.expense.fields,
res.data.customFields.data
)
this.getReceipt()
this.isRequestOnGoing = false
return true
}
await this.setInitialCustomFields('Expense')
if (this.$route.query.customer) {
this.setExpenseCustomer(parseInt(this.$route.query.customer))
}
this.isRequestOnGoing = false
},
async sendData() {
let validate = await this.touchCustomField()
this.$v.category.$touch()
this.$v.formData.$touch()
if (this.$v.$invalid) {
if (this.$v.$invalid || validate.error) {
return true
}
let data = new FormData()
if (this.file) {
data.append('attachment_receipt', this.file)
if (this.fileObject) {
data.append('attachment_receipt', this.fileObject)
}
data.append('expense_category_id', this.formData.expense_category_id)
data.append('expense_date', moment(this.formData.expense_date).format('DD/MM/YYYY'))
data.append('amount', (this.formData.amount))
data.append(
'expense_date',
moment(this.formData.expense_date).format('YYYY-MM-DD')
)
data.append('amount', this.formData.amount)
data.append('notes', this.formData.notes ? this.formData.notes : '')
data.append('user_id', this.customer ? this.customer.id : '')
data.append('customFields', JSON.stringify(this.formData.customFields))
if (this.isEdit) {
this.isLoading = true
data.append('_method', 'PUT')
let response = await this.updateExpense({id: this.$route.params.id, editData: data})
let response = await this.updateExpense({
id: this.$route.params.id,
editData: data,
})
if (response.data.success) {
window.toastr['success'](this.$t('expenses.updated_message'))
this.isLoading = false
window.toastr['success'](this.$t('expenses.updated_message'))
this.$router.push('/admin/expenses')
return true
}
@@ -359,16 +541,16 @@ export default {
} else {
this.isLoading = true
let response = await this.addExpense(data)
this.isLoading = false
if (response.data.success) {
window.toastr['success'](this.$t('expenses.created_message'))
this.isLoading = false
this.$router.push('/admin/expenses')
this.isLoading = false
return true
}
window.toastr['success'](response.data.success)
}
}
}
},
},
}
</script>

View File

@@ -1,200 +1,232 @@
<template>
<div class="expenses main-content">
<div class="page-header">
<h3 class="page-title">{{ $t('expenses.title') }}</h3>
<ol class="breadcrumb">
<li class="breadcrumb-item">
<router-link
slot="item-title"
to="dashboard">
{{ $t('general.home') }}
</router-link>
</li>
<li class="breadcrumb-item">
<router-link
slot="item-title"
to="#">
{{ $tc('expenses.expense',2) }}
</router-link>
</li>
</ol>
<div class="page-actions row">
<div class="col-xs-2 mr-4">
<base-button
v-show="totalExpenses || filtersApplied"
:outline="true"
:icon="filterIcon"
size="large"
right-icon
color="theme"
@click="toggleFilter"
>
{{ $t('general.filter') }}
</base-button>
</div>
<router-link slot="item-title" class="col-xs-2" to="expenses/create">
<base-button size="large" icon="plus" color="theme">
{{ $t('expenses.add_expense') }}
</base-button>
</router-link>
</div>
</div>
<base-page>
<!-- Page Header -->
<sw-page-header :title="$t('expenses.title')">
<sw-breadcrumb slot="breadcrumbs">
<sw-breadcrumb-item to="dashboard" :title="$t('general.home')" />
<transition name="fade">
<div v-show="showFilters" class="filter-section">
<div class="row">
<div class="col-md-3">
<label>{{ $t('expenses.customer') }}</label>
<base-select
v-model="filters.user"
:options="customers"
:searchable="true"
:show-labels="false"
:placeholder="$t('expenses.select_a_customer')"
label="name"
@click="filter = ! filter"
/>
</div>
<div class="col-md-3">
<label>{{ $t('expenses.category') }}</label>
<base-select
v-model="filters.category"
:options="categories"
:searchable="true"
:show-labels="false"
:placeholder="$t('expenses.categories.select_a_category')"
label="name"
@click="filter = ! filter"
/>
</div>
<div class="col-md-3">
<label>{{ $t('expenses.from_date') }}</label>
<base-date-picker
v-model="filters.from_date"
:calendar-button="true"
calendar-button-icon="calendar"
/>
</div>
<div class="col-md-3">
<label>{{ $t('expenses.to_date') }}</label>
<base-date-picker
v-model="filters.to_date"
:calendar-button="true"
calendar-button-icon="calendar"
/>
</div>
</div>
<label class="clear-filter" @click="clearFilter">{{ $t('general.clear_all') }}</label>
</div>
</transition>
<sw-breadcrumb-item to="#" :title="$tc('expenses.expense', 2)" active />
</sw-breadcrumb>
<div v-cloak v-show="showEmptyScreen" class="col-xs-1 no-data-info" align="center">
<observatory-icon class="mt-5 mb-4"/>
<div class="row" align="center">
<label class="col title">{{ $t('expenses.no_expenses') }}</label>
</div>
<div class="row">
<label class="description col mt-1" align="center">{{ $t('expenses.list_of_expenses') }}</label>
</div>
<div class="row">
<div class="col">
<base-button
:outline="true"
color="theme"
class="mt-3"
size="large"
@click="$router.push('expenses/create')"
>
{{ $t('expenses.add_new_expense') }}
</base-button>
</div>
</div>
</div>
<div v-show="!showEmptyScreen" class="table-container">
<div class="table-actions mt-5">
<p class="table-stats">{{ $t('general.showing') }}: <b>{{ expenses.length }}</b> {{ $t('general.of') }} <b>{{ totalExpenses }}</b></p>
<transition name="fade">
<v-dropdown v-if="selectedExpenses.length" :show-arrow="false" theme-light class="action mr-5">
<span slot="activator" href="#" class="table-actions-button dropdown-toggle">
{{ $t('general.actions') }}
</span>
<v-dropdown-item>
<div class="dropdown-item" @click="removeMultipleExpenses">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
</transition>
</div>
<div class="custom-control custom-checkbox">
<input
id="select-all"
v-model="selectAllFieldStatus"
type="checkbox"
class="custom-control-input"
@change="selectAllExpenses"
<template slot="actions">
<sw-button
v-show="totalExpenses"
size="lg"
variant="primary-outline"
@click="toggleFilter"
>
<label v-show="!isRequestOngoing" for="select-all" class="custom-control-label selectall">
<span class="select-all-label">{{ $t('general.select_all') }} </span>
</label>
{{ $t('general.filter') }}
<component :is="filterIcon" class="w-4 h-4 ml-2 -mr-1" />
</sw-button>
<sw-button
tag-name="router-link"
to="expenses/create"
class="ml-4"
size="lg"
variant="primary"
>
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('expenses.add_expense') }}
</sw-button>
</template>
</sw-page-header>
<!--Filter Wrapper -->
<slide-y-up-transition>
<sw-filter-wrapper v-show="showFilters" class="mt-3">
<sw-input-group :label="$t('expenses.customer')" class="flex-1 mt-3">
<base-customer-select
ref="customerSelect"
@select="onSelectCustomer"
@deselect="clearCustomerSearch"
/>
</sw-input-group>
<sw-input-group
:label="$t('expenses.category')"
class="flex-1 mt-2 ml-0 lg:ml-6"
>
<sw-select
v-model="filters.category"
:options="categories"
:searchable="true"
:show-labels="false"
:placeholder="$t('expenses.categories.select_a_category')"
label="name"
class="mt-2"
@click="filter = !filter"
/>
</sw-input-group>
<sw-input-group
:label="$t('expenses.from_date')"
class="flex-1 mt-2 ml-0 lg:ml-6"
>
<base-date-picker
v-model="filters.from_date"
:calendar-button="true"
class="mt-2"
calendar-button-icon="calendar"
/>
</sw-input-group>
<sw-input-group
:label="$t('expenses.to_date')"
class="flex-1 mt-2 ml-0 lg:ml-6"
>
<base-date-picker
v-model="filters.to_date"
:calendar-button="true"
class="mt-2"
calendar-button-icon="calendar"
/>
</sw-input-group>
<label
class="absolute text-sm leading-snug text-black cursor-pointer"
style="top: 10px; right: 15px"
@click="clearFilter"
>{{ $t('general.clear_all') }}</label
>
</sw-filter-wrapper>
</slide-y-up-transition>
<!-- Empty Table Placeholder -->
<sw-empty-table-placeholder
v-show="showEmptyScreen"
:title="$t('expenses.no_expenses')"
:description="$t('expenses.list_of_expenses')"
>
<observatory-icon class="mt-5 mb-4" />
<sw-button
slot="actions"
tag-name="router-link"
to="/admin/expenses/create"
size="lg"
variant="primary-outline"
>
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('expenses.add_new_expense') }}
</sw-button>
</sw-empty-table-placeholder>
<div v-show="!showEmptyScreen" class="relative table-container">
<div
class="relative flex items-center justify-between h-10 mt-5 list-none border-b-2 border-gray-200 border-solid"
>
<p class="text-sm">
{{ $t('general.showing') }}: <b>{{ expenses.length }}</b>
{{ $t('general.of') }} <b>{{ totalExpenses }}</b>
</p>
<sw-transition type="fade">
<sw-dropdown v-if="selectedExpenses.length">
<span
slot="activator"
class="flex block text-sm font-medium cursor-pointer select-none text-primary-400"
>
{{ $t('general.actions') }}
<chevron-down-icon class="h-5" />
</span>
<sw-dropdown-item @click="removeMultipleExpenses">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</sw-transition>
</div>
<table-component
ref="table"
:show-filter="false"
:data="fetchData"
table-class="table"
>
<div class="absolute z-10 items-center pl-4 mt-2 select-none md:mt-12">
<sw-checkbox
v-model="selectAllFieldStatus"
variant="primary"
size="sm"
class="hidden md:inline"
@change="selectAllExpenses"
/>
<table-column
<sw-checkbox
v-model="selectAllFieldStatus"
:label="$t('general.select_all')"
variant="primary"
size="sm"
class="md:hidden"
@change="selectAllExpenses"
/>
</div>
<sw-table-component ref="table" :show-filter="false" :data="fetchData">
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="no-click"
>
<template slot-scope="row">
<div class="custom-control custom-checkbox">
<input
:id="row.id"
v-model="selectField"
:value="row.id"
type="checkbox"
class="custom-control-input"
>
<label :for="row.id" class="custom-control-label" />
</div>
</template>
</table-column>
<table-column
:label="$tc('expenses.categories.category', 1)"
sort-as="name"
show="category.name"
/>
<table-column
:label="$t('expenses.customer')"
sort-as="user_name"
show="user_name"
/>
<table-column
<div slot-scope="row" class="relative block">
<sw-checkbox
:id="row.id"
v-model="selectField"
:value="row.id"
variant="primary"
size="sm"
/>
</div>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('expenses.date')"
sort-as="expense_date"
show="formattedExpenseDate"
/>
<table-column
<sw-table-column
:sortable="true"
:label="$tc('expenses.categories.category', 1)"
sort-as="name"
show="category.name"
>
<template slot-scope="row">
<span>{{ $tc('expenses.categories.category', 1) }}</span>
<router-link
:to="{ path: `expenses/${row.id}/edit` }"
class="font-medium text-primary-500"
>
{{ row.category.name }}
</router-link>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('expenses.customer')"
sort-as="user_name"
show="user_name"
>
<template slot-scope="row">
<span>{{ $t('expenses.customer') }}</span>
<span> {{ row.user_name ? row.user_name : 'Not selected' }} </span>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('expenses.note')"
sort-as="expense_date"
>
<template slot-scope="row">
<span>{{ $t('expenses.note') }}</span>
<div class="notes">
<div class="note">{{ row.notes }}</div>
<div class="truncate note w-60">{{ row.notes }}</div>
</div>
</template>
</table-column>
<table-column
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('expenses.amount')"
sort-as="amount"
show="category.amount"
@@ -203,116 +235,133 @@
<span>{{ $t('expenses.amount') }}</span>
<div v-html="$utils.formatMoney(row.amount, defaultCurrency)" />
</template>
</table-column>
<table-column
</sw-table-column>
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown no-click"
>
<template slot-scope="row">
<span>{{ $t('expenses.action') }}</span>
<v-dropdown>
<a slot="activator" href="#">
<dot-icon />
</a>
<v-dropdown-item>
<router-link :to="{path: `expenses/${row.id}/edit`}" class="dropdown-item">
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
{{ $t('general.edit') }}
</router-link>
</v-dropdown-item>
<v-dropdown-item>
<div class="dropdown-item" @click="removeExpense(row.id)">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
<sw-dropdown>
<dot-icon slot="activator" />
<sw-dropdown-item
tag-name="router-link"
:to="`expenses/${row.id}/edit`"
>
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removeExpense(row.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</table-column>
</table-component>
</sw-table-column>
</sw-table-component>
</div>
</div>
</base-page>
</template>
<script>
import { SweetModal, SweetModalTab } from 'sweet-modal-vue'
import { mapActions, mapGetters } from 'vuex'
import ObservatoryIcon from '../../components/icon/ObservatoryIcon'
import MultiSelect from 'vue-multiselect'
import moment, { invalid } from 'moment'
import {
PencilIcon,
TrashIcon,
FilterIcon,
XIcon,
ChevronDownIcon,
PlusIcon,
} from '@vue-hero-icons/solid'
export default {
components: {
MultiSelect,
'observatory-icon': ObservatoryIcon,
'SweetModal': SweetModal,
'SweetModalTab': SweetModalTab
ObservatoryIcon,
PlusIcon,
FilterIcon,
XIcon,
ChevronDownIcon,
PencilIcon,
TrashIcon,
},
data () {
data() {
return {
showFilters: false,
filtersApplied: false,
isRequestOngoing: true,
customers: [],
filters: {
category: null,
from_date: '',
to_date: '',
user: ''
}
user: '',
},
}
},
computed: {
showEmptyScreen () {
return !this.totalExpenses && !this.isRequestOngoing && !this.filtersApplied
showEmptyScreen() {
return !this.totalExpenses && !this.isRequestOngoing
},
filterIcon () {
return (this.showFilters) ? 'times' : 'filter'
filterIcon() {
return this.showFilters ? 'x-icon' : 'filter-icon'
},
...mapGetters('category', [
'categories'
]),
...mapGetters('category', ['categories']),
...mapGetters('expense', [
'selectedExpenses',
'totalExpenses',
'expenses',
'selectAllField'
]),
...mapGetters('currency', [
'defaultCurrency'
'selectAllField',
]),
...mapGetters('company', ['defaultCurrency']),
...mapGetters('customer', ['customers']),
selectField: {
get: function () {
return this.selectedExpenses
},
set: function (val) {
this.selectExpense(val)
}
},
},
selectAllFieldStatus: {
get: function () {
return this.selectAllField
},
set: function (val) {
this.setSelectAllState(val)
}
}
},
},
},
watch: {
filters: {
handler: 'setFilters',
deep: true
}
deep: true,
},
},
destroyed () {
destroyed() {
if (this.selectAllField) {
this.selectAllExpenses()
}
},
created () {
this.fetchCategories()
created() {
this.fetchCategories({ limit: 'all' })
},
methods: {
...mapActions('expense', [
'fetchExpenses',
@@ -320,76 +369,104 @@ export default {
'deleteExpense',
'deleteMultipleExpenses',
'selectAllExpenses',
'setSelectAllState'
'setSelectAllState',
]),
...mapActions('category', [
'fetchCategories'
]),
async fetchData ({ page, filter, sort }) {
...mapActions('category', ['fetchCategories']),
async fetchData({ page, filter, sort }) {
let data = {
user_id: this.filters.user ? this.filters.user.id : null,
expense_category_id: this.filters.category !== null ? this.filters.category.id : '',
from_date: this.filters.from_date === '' ? this.filters.from_date : moment(this.filters.from_date).format('DD/MM/YYYY'),
to_date: this.filters.to_date === '' ? this.filters.to_date : moment(this.filters.to_date).format('DD/MM/YYYY'),
expense_category_id:
this.filters.category !== null ? this.filters.category.id : '',
from_date:
this.filters.from_date === ''
? this.filters.from_date
: this.filters.from_date,
to_date:
this.filters.to_date === ''
? this.filters.to_date
: this.filters.to_date,
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page
page,
}
this.isRequestOngoing = true
let response = await this.fetchExpenses(data)
this.customers = response.data.customers
this.isRequestOngoing = false
return {
data: response.data.expenses.data,
pagination: {
totalPages: response.data.expenses.last_page,
currentPage: page,
count: response.data.expenses.count
}
count: response.data.expenses.count,
},
}
},
refreshTable () {
onSelectCustomer(customer) {
this.filters.user = customer
},
refreshTable() {
this.$refs.table.refresh()
},
setFilters () {
this.filtersApplied = true
setFilters() {
this.refreshTable()
},
clearFilter () {
clearFilter() {
if (this.filters.user) {
this.$refs.customerSelect.$refs.baseSelect.removeElement(
this.filters.user
)
}
this.filters = {
category: null,
from_date: '',
to_date: '',
user: null
user: null,
}
this.$nextTick(() => {
this.filtersApplied = false
})
},
toggleFilter () {
if (this.showFilters && this.filtersApplied) {
async clearCustomerSearch(removedOption, id) {
this.filters.user = ''
this.refreshTable()
},
toggleFilter() {
if (this.showFilters) {
this.clearFilter()
this.refreshTable()
}
this.showFilters = !this.showFilters
},
async removeExpense (id) {
async removeExpense(id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('expenses.confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
dangerMode: true,
}).then(async (willDelete) => {
if (willDelete) {
let res = await this.deleteExpense(id)
let res = await this.deleteExpense({ ids: [id] })
if (res.data.success) {
window.toastr['success'](this.$tc('expenses.deleted_message', 1))
this.$refs.table.refresh()
this.refreshTable()
return true
} else if (res.data.error) {
window.toastr['error'](res.data.message)
@@ -397,16 +474,18 @@ export default {
}
})
},
async removeMultipleExpenses () {
async removeMultipleExpenses() {
swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('expenses.confirm_delete', 2),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
dangerMode: true,
}).then(async (willDelete) => {
if (willDelete) {
let request = await this.deleteMultipleExpenses()
if (request.data.success) {
window.toastr['success'](this.$tc('expenses.deleted_message', 2))
this.$refs.table.refresh()
@@ -415,7 +494,7 @@ export default {
}
}
})
}
}
},
},
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,213 @@
<template>
<div class="col-span-5 pr-0">
<div
v-if="selectedCustomer"
class="flex flex-col p-4 bg-white border border-gray-200 border-solid"
style="min-height: 170px"
>
<div class="relative flex justify-between mb-2">
<label class="flex-1 font-medium">{{ selectedCustomer.name }}</label>
<a
class="relative my-0 ml-0 mr-6 text-sm font-medium cursor-pointer text-primary-500"
@click.prevent="editCustomer"
>
{{ $t('general.edit') }}
</a>
<a
class="relative my-0 ml-2 mr-6 text-sm font-medium cursor-pointer text-primary-500"
@click.prevent="resetSelectedCustomer"
>
{{ $t('general.deselect') }}
</a>
</div>
<div class="grid grid-cols-2 gap-4 mt-1">
<div v-if="selectedCustomer.billing_address">
<div class="flex flex-col">
<label
class="mb-1 text-sm font-medium text-gray-500 uppercase whitespace-no-wrap"
>
{{ $t('general.bill_to') }}
</label>
<div class="flex flex-col flex-1 p-0">
<label
v-if="selectedCustomer.billing_address.name"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.billing_address.name }}
</label>
<label
v-if="selectedCustomer.billing_address.address_street_1"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.billing_address.address_street_1 }}
</label>
<label
v-if="selectedCustomer.billing_address.address_street_2"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.billing_address.address_street_2 }}
</label>
<label
v-if="
selectedCustomer.billing_address.city &&
selectedCustomer.billing_address.state
"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.billing_address.city }},
{{ selectedCustomer.billing_address.state }}
{{ selectedCustomer.billing_address.zip }}
</label>
<label
v-if="selectedCustomer.billing_address.country"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.billing_address.country.name }}
</label>
<label
v-if="selectedCustomer.billing_address.phone"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.billing_address.phone }}
</label>
</div>
</div>
</div>
<div v-if="selectedCustomer.shipping_address" class="col col-6">
<div class="flex flex-col">
<label
class="mb-1 text-sm font-medium text-gray-500 uppercase whitespace-no-wrap"
>
{{ $t('general.ship_to') }}
</label>
<div class="flex flex-col flex-1 p-0">
<label
v-if="selectedCustomer.shipping_address.name"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.shipping_address.name }}
</label>
<label
v-if="selectedCustomer.shipping_address.address_street_1"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.shipping_address.address_street_1 }}
</label>
<label
v-if="selectedCustomer.shipping_address.address_street_2"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.shipping_address.address_street_2 }}
</label>
<label
v-if="
selectedCustomer.shipping_address.city &&
selectedCustomer.shipping_address
"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.shipping_address.city }},
{{ selectedCustomer.shipping_address.state }}
{{ selectedCustomer.shipping_address.zip }}
</label>
<label
v-if="selectedCustomer.shipping_address.country"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.shipping_address.country.name }}
</label>
<label
v-if="selectedCustomer.shipping_address.phone"
class="relative w-11/12 text-sm truncate"
>
{{ selectedCustomer.shipping_address.phone }}
</label>
</div>
</div>
</div>
</div>
</div>
<div v-else>
<sw-popup
:class="[
'add-customer p-0',
{
'border border-solid border-danger rounded': valid.$error,
},
]"
>
<div
slot="activator"
class="relative flex justify-center px-0 py-16 bg-white border border-gray-200 border-solid rounded-md"
style="min-height: 170px"
>
<user-icon
class="flex justify-center w-10 h-10 p-2 mr-5 text-sm text-white bg-gray-200 rounded-full font-base"
/>
<div class="mt-1">
<label class="text-lg">
{{ $t('customers.new_customer') }}
<span class="text-danger"> * </span>
</label>
<p v-if="valid.$error && !valid.required" class="text-danger">
{{ $t('validation.required') }}
</p>
</div>
</div>
<customer-select-popup :user-id="customerId" type="invoice" />
</sw-popup>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { UserIcon } from '@vue-hero-icons/solid'
export default {
components: {
UserIcon,
},
props: {
valid: {
type: Object,
default: () => {},
},
customerId: {
type: Number,
default: null,
},
},
computed: {
...mapGetters('invoice', ['getTemplateId', 'selectedCustomer']),
},
created() {
this.resetSelectedCustomer()
if (this.customerId) {
this.selectCustomer(this.customerId)
}
},
methods: {
...mapActions('invoice', ['resetSelectedCustomer', 'selectCustomer']),
...mapActions('modal', ['openModal']),
editCustomer() {
this.openModal({
title: this.$t('customers.edit_customer'),
componentName: 'CustomerModal',
id: this.selectedCustomer.id,
data: this.selectedCustomer,
})
},
},
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +1,49 @@
<template>
<div class="section mt-2">
<label class="invoice-label">
<div class="flex items-center justify-between w-full mt-2 text-sm">
<label class="font-semibold leading-5 text-gray-500 uppercase">
{{ tax.name }} ({{ tax.percent }}%)
</label>
<label class="invoice-amount">
<label class="flex items-center justify-center text-lg text-black">
<div v-html="$utils.formatMoney(tax.amount, currency)" />
<font-awesome-icon
class="ml-2"
icon="trash-alt"
@click="$emit('remove', index)"
/>
<trash-icon class="h-5 ml-2" @click="$emit('remove', index)" />
</label>
</div>
</template>
<script>
import { TrashIcon } from '@vue-hero-icons/solid'
export default {
components: {
TrashIcon,
},
props: {
index: {
type: Number,
required: true
required: true,
},
tax: {
type: Object,
required: true
required: true,
},
taxes: {
type: Array,
required: true
required: true,
},
total: {
type: Number,
default: 0
default: 0,
},
totalTax: {
type: Number,
default: 0
default: 0,
},
currency: {
type: [Object, String],
required: true
}
required: true,
},
},
computed: {
taxAmount () {
taxAmount() {
if (this.tax.compound_tax && this.total) {
return ((this.total + this.totalTax) * this.tax.percent) / 100
}
@@ -54,30 +53,26 @@ export default {
}
return 0
}
},
},
watch: {
total: {
handler: 'updateTax'
handler: 'updateTax',
},
totalTax: {
handler: 'updateTax'
}
handler: 'updateTax',
},
},
methods: {
updateTax () {
updateTax() {
this.$emit('update', {
'index': this.index,
'item': {
index: this.index,
item: {
...this.tax,
amount: this.taxAmount
}
amount: this.taxAmount,
},
})
}
}
},
},
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -1,23 +1,22 @@
<template>
<tr class="item-row invoice-item-row">
<td colspan="5">
<table class="full-width">
<tr class="box-border bg-white border border-gray-200 border-solid rounded-b">
<td colspan="5" class="p-0 text-left align-top">
<table class="w-full">
<colgroup>
<col style="width: 40%;">
<col style="width: 10%;">
<col style="width: 15%;">
<col v-if="discountPerItem === 'YES'" style="width: 15%;">
<col style="width: 15%;">
<col style="width: 40%" />
<col style="width: 10%" />
<col style="width: 15%" />
<col v-if="discountPerItem === 'YES'" style="width: 15%" />
<col style="width: 15%" />
</colgroup>
<tbody>
<tr>
<td class="">
<div class="item-select-wrapper">
<div class="sort-icon-wrapper handle">
<font-awesome-icon
class="sort-icon"
icon="grip-vertical"
/>
<td class="px-5 py-4 text-left align-top">
<div class="flex justify-start">
<div
class="flex items-center justify-center w-12 h-5 mt-2 text-gray-400 cursor-move handle"
>
<drag-icon />
</div>
<item-select
ref="itemSelect"
@@ -34,88 +33,94 @@
/>
</div>
</td>
<td class="text-right">
<base-input
<td class="px-5 py-4 text-right align-top">
<sw-input
v-model="item.quantity"
:invalid="$v.item.quantity.$error"
:is-input-group="!!item.unit_name"
:input-group-text="item.unit_name"
type="text"
small
@keyup="updateItem"
@input="$v.item.quantity.$touch()"
/>
<div v-if="$v.item.quantity.$error">
<span v-if="!$v.item.quantity.maxLength" class="text-danger">{{ $t('validation.quantity_maxlength') }}</span>
<span v-if="!$v.item.quantity.maxLength" class="text-danger">
{{ $t('validation.quantity_maxlength') }}
</span>
</div>
</td>
<td class="text-left">
<div class="d-flex flex-column">
<div class="flex-fillbd-highlight">
<div class="base-input">
<money
<td class="px-5 py-4 text-left align-top">
<div class="flex flex-col">
<div class="flex-auto flex-fill bd-highlight">
<div class="relative w-full">
<sw-money
v-model="price"
v-bind="customerCurrency"
class="input-field"
:currency="customerCurrency"
:invalid="$v.item.price.$error"
@input="$v.item.price.$touch()"
/>
</div>
<div v-if="$v.item.price.$error">
<span v-if="!$v.item.price.maxLength" class="text-danger">{{ $t('validation.price_maxlength') }}</span>
<span v-if="!$v.item.price.maxLength" class="text-danger">
{{ $t('validation.price_maxlength') }}
</span>
</div>
</div>
</div>
</td>
<td v-if="discountPerItem === 'YES'" class="">
<div class="d-flex flex-column bd-highlight">
<div
class="btn-group flex-fill bd-highlight"
role="group"
>
<base-input
<td
v-if="discountPerItem === 'YES'"
class="px-5 py-4 text-left align-top"
>
<div class="flex flex-col">
<div class="flex flex-auto" role="group">
<sw-input
v-model="discount"
:invalid="$v.item.discount_val.$error"
input-class="item-discount"
class="border-r-0 rounded-tr-none rounded-br-none"
@input="$v.item.discount_val.$touch()"
/>
<v-dropdown :show-arrow="false" theme-light>
<button
<sw-dropdown>
<sw-button
slot="activator"
type="button"
class="btn item-dropdown dropdown-toggle"
class="flex items-center px-5 py-1 text-sm font-medium leading-none text-center text-gray-500 whitespace-no-wrap border border-gray-300 border-solid rounded rounded-tl-none rounded-bl-none dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
style="height: 43px"
variant="white"
>
{{ item.discount_type == 'fixed' ? currency.symbol : '%' }}
</button>
<v-dropdown-item>
<a class="dropdown-item" href="#" @click.prevent="selectFixed" >
{{ $t('general.fixed') }}
</a>
</v-dropdown-item>
<v-dropdown-item>
<a class="dropdown-item" href="#" @click.prevent="selectPercentage">
{{ $t('general.percentage') }}
</a>
</v-dropdown-item>
</v-dropdown>
<span class="flex items-center">
{{
item.discount_type == 'fixed' ? currency.symbol : '%'
}}
<chevron-down-icon class="h-5" />
</span>
</sw-button>
<sw-dropdown-item @click="selectFixed">
{{ $t('general.fixed') }}
</sw-dropdown-item>
<sw-dropdown-item @click="selectPercentage">
{{ $t('general.percentage') }}
</sw-dropdown-item>
</sw-dropdown>
</div>
<!-- <div v-if="$v.item.discount.$error"> discount error </div> -->
</div>
</td>
<td class="text-right">
<div class="item-amount">
<td class="px-5 py-4 text-right align-top">
<div class="flex items-center justify-end text-sm">
<span>
<div v-html="$utils.formatMoney(total, currency)" />
</span>
<div class="remove-icon-wrapper">
<font-awesome-icon
<div
class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer"
>
<trash-icon
v-if="showRemoveItemIcon"
class="remove-icon"
icon="trash-alt"
class="h-5 text-gray-700"
@click="removeItem"
/>
</div>
@@ -123,8 +128,8 @@
</td>
</tr>
<tr v-if="taxPerItem === 'YES'" class="tax-tr">
<td />
<td colspan="4">
<td class="px-5 py-4 text-left align-top" />
<td colspan="4" class="px-5 py-4 text-left align-top">
<tax
v-for="(tax, index) in item.taxes"
:key="tax.id"
@@ -145,98 +150,102 @@
</td>
</tr>
</template>
<script>
import Guid from 'guid'
import { validationMixin } from 'vuelidate'
import { mapActions, mapGetters } from 'vuex'
import TaxStub from '../../stub/tax'
import InvoiceStub from '../../stub/invoice'
import ItemSelect from './ItemSelect'
import Tax from './Tax'
const { required, minValue, between, maxLength } = require('vuelidate/lib/validators')
import { TrashIcon, ViewGridIcon, ChevronDownIcon } from '@vue-hero-icons/solid'
import DragIcon from '@/components/icon/DragIcon'
const {
required,
minValue,
between,
maxLength,
} = require('vuelidate/lib/validators')
export default {
components: {
Tax,
ItemSelect
ItemSelect,
TrashIcon,
ViewGridIcon,
ChevronDownIcon,
DragIcon,
},
mixins: [validationMixin],
props: {
itemData: {
type: Object,
default: null
default: null,
},
index: {
type: Number,
default: null
default: null,
},
type: {
type: String,
default: ''
default: '',
},
currency: {
type: [Object, String],
required: true
required: true,
},
taxPerItem: {
type: String,
default: ''
default: '',
},
discountPerItem: {
type: String,
default: ''
default: '',
},
invoiceItems: {
type: Array,
default: null
}
default: null,
},
},
data () {
data() {
return {
isClosePopup: false,
itemSelect: null,
item: {...this.itemData},
item: { ...this.itemData },
maxDiscount: 0,
money: {
decimal: '.',
thousands: ',',
prefix: '$ ',
precision: 2,
masked: false
masked: false,
},
isSelected: false
isSelected: false,
}
},
computed: {
...mapGetters('item', [
'items'
]),
...mapGetters('modal', [
'modalActive'
]),
...mapGetters('currency', [
'defaultCurrencyForInput'
]),
customerCurrency () {
...mapGetters('item', ['items']),
...mapGetters('modal', ['modalActive']),
...mapGetters('company', ['defaultCurrencyForInput']),
customerCurrency() {
if (this.currency) {
return {
decimal: this.currency.decimal_separator,
thousands: this.currency.thousand_separator,
prefix: this.currency.symbol + ' ',
precision: this.currency.precision,
masked: false
masked: false,
}
} else {
return this.defaultCurrenctForInput
}
},
showRemoveItemIcon () {
showRemoveItemIcon() {
if (this.invoiceItems.length == 1) {
return false
}
return true
},
subtotal () {
subtotal() {
return this.item.price * this.item.quantity
},
discount: {
@@ -251,12 +260,12 @@ export default {
}
this.item.discount = newValue
}
},
},
total () {
total() {
return this.subtotal - this.item.discount_val
},
totalSimpleTax () {
totalSimpleTax() {
return window._.sumBy(this.item.taxes, function (tax) {
if (!tax.compound_tax) {
return tax.amount
@@ -265,7 +274,7 @@ export default {
return 0
})
},
totalCompoundTax () {
totalCompoundTax() {
return window._.sumBy(this.item.taxes, function (tax) {
if (tax.compound_tax) {
return tax.amount
@@ -274,7 +283,7 @@ export default {
return 0
})
},
totalTax () {
totalTax() {
return this.totalSimpleTax + this.totalCompoundTax
},
price: {
@@ -292,51 +301,54 @@ export default {
} else {
this.item.price = newValue
}
}
}
},
},
},
watch: {
item: {
handler: 'updateItem',
deep: true
deep: true,
},
subtotal (newValue) {
subtotal(newValue) {
if (this.item.discount_type === 'percentage') {
this.item.discount_val = (this.item.discount * newValue) / 100
}
},
modalActive (val) {
modalActive(val) {
if (!val) {
this.isSelected = false
}
}
},
},
validations () {
validations() {
return {
item: {
name: {
required
required,
},
quantity: {
required,
minValue: minValue(1),
maxLength: maxLength(20)
minValue: minValue(0),
maxLength: maxLength(20),
},
price: {
required,
minValue: minValue(1),
maxLength: maxLength(20)
maxLength: maxLength(20),
},
discount_val: {
between: between(0, this.maxDiscount)
between: between(0, this.maxDiscount),
},
description: {
maxLength: maxLength(255)
}
}
maxLength: maxLength(255),
},
},
}
},
created () {
mounted() {
this.$v.item.$reset()
},
created() {
window.hub.$on('checkItems', this.validateItem)
window.hub.$on('newItem', (val) => {
if (this.taxPerItem === 'YES') {
@@ -348,52 +360,54 @@ export default {
})
},
methods: {
updateTax (data) {
updateTax(data) {
this.$set(this.item.taxes, data.index, data.item)
let lastTax = this.item.taxes[this.item.taxes.length - 1]
if (lastTax.tax_type_id !== 0) {
this.item.taxes.push({...TaxStub, id: Guid.raw()})
this.item.taxes.push({ ...TaxStub, id: Guid.raw() })
}
this.updateItem()
},
removeTax (index) {
removeTax(index) {
this.item.taxes.splice(index, 1)
this.updateItem()
},
taxWithPercentage ({ name, percent }) {
taxWithPercentage({ name, percent }) {
return `${name} (${percent}%)`
},
searchVal (val) {
searchVal(val) {
this.item.name = val
},
deselectItem () {
this.item = {...InvoiceStub, id: this.item.id, taxes: [{...TaxStub, id: Guid.raw()}]}
deselectItem() {
this.item = {
...InvoiceStub,
id: this.item.id,
taxes: [{ ...TaxStub, id: Guid.raw() }],
}
this.$nextTick(() => {
this.$refs.itemSelect.$refs.baseSelect.$refs.search.focus()
})
},
onSelectItem (item) {
onSelectItem(item) {
this.item.name = item.name
this.item.price = item.price
this.item.item_id = item.id
this.item.description = item.description
this.item.unit_name = item.unit_name
if (this.taxPerItem === 'YES' && item.taxes) {
let index = 0
item.taxes.forEach(tax => {
this.updateTax({index, item: { ...tax }})
item.taxes.forEach((tax) => {
this.updateTax({ index, item: { ...tax } })
index++
})
}
// if (this.item.taxes.length) {
// this.item.taxes = {...item.taxes}
// }
},
selectFixed () {
selectFixed() {
if (this.item.discount_type === 'fixed') {
return
}
@@ -401,7 +415,7 @@ export default {
this.item.discount_val = this.item.discount * 100
this.item.discount_type = 'fixed'
},
selectPercentage () {
selectPercentage() {
if (this.item.discount_type === 'percentage') {
return
}
@@ -410,24 +424,24 @@ export default {
this.item.discount_type = 'percentage'
},
updateItem () {
updateItem() {
this.$emit('update', {
'index': this.index,
'item': {
index: this.index,
item: {
...this.item,
total: this.total,
totalSimpleTax: this.totalSimpleTax,
totalCompoundTax: this.totalCompoundTax,
totalTax: this.totalTax,
tax: this.totalTax,
taxes: [...this.item.taxes]
}
taxes: [...this.item.taxes],
},
})
},
removeItem () {
removeItem() {
this.$emit('remove', this.index)
},
validateItem () {
validateItem() {
this.$v.item.$touch()
if (this.item !== null) {
@@ -435,7 +449,7 @@ export default {
} else {
this.$emit('itemValidate', this.index, false)
}
}
}
},
},
}
</script>

View File

@@ -1,13 +1,20 @@
<template>
<div class="item-selector">
<div v-if="item.item_id" class="selected-item">
<div class="flex-1 text-sm">
<div
v-if="item.item_id"
class="relative flex items-center h-10 pl-2 bg-gray-100 border border-gray-200 border-solid rounded"
>
{{ item.name }}
<span class="deselect-icon" @click="deselectItem">
<font-awesome-icon icon="times-circle" />
<span
class="absolute text-gray-400 cursor-pointer"
style="top: 8px; right: 10px"
@click="deselectItem"
>
<x-circle-icon class="h-5" />
</span>
</div>
<base-select
<sw-select
v-else
ref="baseSelect"
v-model="itemSelect"
@@ -24,93 +31,107 @@
@select="onSelect"
>
<div slot="afterList">
<button type="button" class="list-add-button" @click="openItemModal">
<font-awesome-icon class="icon" icon="cart-plus" />
<label>{{ $t('general.add_new_item') }}</label>
<button
type="button"
class="flex items-center justify-center w-full p-3 bg-gray-200 border-none outline-none"
@click="openItemModal"
>
<shopping-cart-icon
class="h-5 mr-2 -ml-2 text-center text-primary-400"
/>
<label class="ml-2 text-sm leading-none text-primary-400">{{
$t('general.add_new_item')
}}</label>
</button>
</div>
</base-select>
<div class="item-description">
<base-text-area
</sw-select>
<div class="w-full pt-1 text-xs text-light">
<sw-textarea
v-autoresize
v-model="item.description"
:invalid-description="invalidDescription"
:placeholder="$t('invoices.item.type_item_description')"
type="text"
rows="1"
class="description-input"
variant="inv-desc"
class="w-full text-xs text-gray-600 border-none resize-none"
@input="$emit('onDesriptionInput')"
/>
<div v-if="invalidDescription">
<span class="text-danger">{{ $tc('validation.description_maxlength') }}</span>
<span class="text-xs text-danger">
{{ $tc('validation.description_maxlength') }}
</span>
</div>
<!-- <textarea type="text" v-autoresize rows="1" class="description-input" v-model="item.description" placeholder="Type Item Description (optional)" /> -->
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { XCircleIcon, ShoppingCartIcon } from '@vue-hero-icons/solid'
export default {
components: {
XCircleIcon,
ShoppingCartIcon,
},
props: {
item: {
type: Object,
required: true
required: true,
},
invalid: {
type: Boolean,
required: false,
default: false
default: false,
},
invalidDescription: {
type: Boolean,
required: false,
default: false
default: false,
},
taxPerItem: {
type: String,
default: ''
default: '',
},
taxes: {
type: Array,
default: null
}
default: null,
},
},
data () {
data() {
return {
itemSelect: null,
loading: false
loading: false,
}
},
computed: {
...mapGetters('item', [
'items'
])
...mapGetters('item', ['items']),
},
watch: {
invalidDescription (newValue) {
invalidDescription(newValue) {
console.log(newValue)
}
},
},
methods: {
...mapActions('modal', [
'openModal'
]),
...mapActions('item', [
'fetchItems'
]),
async searchItems (search) {
...mapActions('modal', ['openModal']),
...mapActions('item', ['fetchItems']),
async searchItems(search) {
let data = {
search,
filter: {
name: search,
unit: '',
price: ''
price: '',
},
orderByField: '',
orderBy: '',
page: 1
page: 1,
}
if (this.item) {
data.item_id = this.item.item_id
}
this.loading = true
@@ -119,27 +140,31 @@ export default {
this.loading = false
},
onTextChange (val) {
onTextChange(val) {
this.searchItems(val)
this.$emit('search', val)
},
openItemModal () {
openItemModal() {
this.$emit('onSelectItem')
this.openModal({
'title': this.$t('items.add_item'),
'componentName': 'ItemModal',
'data': {taxPerItem: this.taxPerItem, taxes: this.taxes}
title: this.$t('items.add_item'),
componentName: 'ItemModal',
data: { taxPerItem: this.taxPerItem, taxes: this.taxes },
})
},
onSelect(val) {
this.$emit('select', val)
this.fetchItems()
},
deselectItem () {
deselectItem() {
this.itemSelect = null
this.$emit('deselect')
}
}
},
},
}
</script>

View File

@@ -1,10 +1,10 @@
<template>
<div class="tax-row">
<div class="d-flex align-items-center tax-select">
<label class="bd-highlight pr-2 mb-0" align="right">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center" style="flex: 4">
<label class="pr-2 mb-0" align="right">
{{ $t('general.tax') }}
</label>
<base-select
<sw-select
v-model="selectedTax"
:options="filteredTypes"
:allow-empty="false"
@@ -16,19 +16,29 @@
@select="(val) => onSelectTax(val)"
>
<div slot="afterList">
<button type="button" class="list-add-button" @click="openTaxModal">
<font-awesome-icon class="icon" icon="check-circle" />
<label>{{ $t('invoices.add_new_tax') }}</label>
<button
type="button"
class="flex items-center justify-center w-full px-2 py-2 bg-gray-200 border-none outline-none"
@click="openTaxModal"
>
<check-circle-icon class="h-5 text-primary-400" />
<label class="ml-2 text-sm leading-none text-primary-400">{{
$t('invoices.add_new_tax')
}}</label>
</button>
</div>
</base-select> <br>
</sw-select>
<br />
</div>
<div class="text-right tax-amount" v-html="$utils.formatMoney(taxAmount, currency)" />
<div class="remove-icon-wrapper">
<font-awesome-icon
<div
class="text-sm text-right"
style="flex: 3"
v-html="$utils.formatMoney(taxAmount, currency)"
/>
<div class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer">
<trash-icon
v-if="taxes.length && index !== taxes.length - 1"
class="remove-icon"
icon="trash-alt"
class="h-5 text-gray-700"
@click="removeTax"
/>
</div>
@@ -37,49 +47,52 @@
<script>
import { mapActions, mapGetters } from 'vuex'
import { CheckCircleIcon, TrashIcon } from '@vue-hero-icons/solid'
export default {
components: {
CheckCircleIcon,
TrashIcon,
},
props: {
index: {
type: Number,
required: true
required: true,
},
taxData: {
type: Object,
required: true
required: true,
},
taxes: {
type: Array,
default: []
default: [],
},
total: {
type: Number,
default: 0
default: 0,
},
totalTax: {
type: Number,
default: 0
default: 0,
},
currency: {
type: [Object, String],
required: true
}
required: true,
},
},
data () {
data() {
return {
tax: {...this.taxData},
selectedTax: null
tax: { ...this.taxData },
selectedTax: null,
}
},
computed: {
...mapGetters('taxType', [
'taxTypes'
]),
filteredTypes () {
const clonedTypes = this.taxTypes.map(a => ({...a}))
...mapGetters('taxType', ['taxTypes']),
filteredTypes() {
const clonedTypes = this.taxTypes.map((a) => ({ ...a }))
return clonedTypes.map((taxType) => {
let found = this.taxes.find(tax => tax.tax_type_id === taxType.id)
let found = this.taxes.find((tax) => tax.tax_type_id === taxType.id)
if (found) {
taxType.$isDisabled = true
@@ -90,7 +103,7 @@ export default {
return taxType
})
},
taxAmount () {
taxAmount() {
if (this.tax.compound_tax && this.total) {
return ((this.total + this.totalTax) * this.tax.percent) / 100
}
@@ -100,19 +113,21 @@ export default {
}
return 0
}
},
},
watch: {
total: {
handler: 'updateTax'
handler: 'updateTax',
},
totalTax: {
handler: 'updateTax'
}
handler: 'updateTax',
},
},
created () {
created() {
if (this.taxData.tax_type_id > 0) {
this.selectedTax = this.taxTypes.find(_type => _type.id === this.taxData.tax_type_id)
this.selectedTax = this.taxTypes.find(
(_type) => _type.id === this.taxData.tax_type_id
)
}
this.updateTax()
@@ -124,13 +139,11 @@ export default {
})
},
methods: {
...mapActions('modal', [
'openModal'
]),
customLabel ({ name, percent }) {
...mapActions('modal', ['openModal']),
customLabel({ name, percent }) {
return `${name} - ${percent}%`
},
onSelectTax (val) {
onSelectTax(val) {
this.tax.percent = val.percent
this.tax.tax_type_id = val.id
this.tax.compound_tax = val.compound_tax
@@ -138,28 +151,28 @@ export default {
this.updateTax()
},
updateTax () {
updateTax() {
if (this.tax.tax_type_id === 0) {
return
}
this.$emit('update', {
'index': this.index,
'item': {
index: this.index,
item: {
...this.tax,
amount: this.taxAmount
}
amount: this.taxAmount,
},
})
},
removeTax () {
removeTax() {
this.$emit('remove', this.index, this.tax)
},
openTaxModal () {
openTaxModal() {
this.openModal({
'title': this.$t('settings.tax_types.add_tax'),
'componentName': 'TaxTypeModal'
title: this.$t('settings.tax_types.add_tax'),
componentName: 'TaxTypeModal',
})
}
}
},
},
}
</script>

View File

@@ -1,203 +1,259 @@
<template>
<div v-if="invoice" class="main-content invoice-view-page">
<div class="page-header">
<h3 class="page-title">{{ invoice.invoice_number }}</h3>
<div class="page-actions row">
<div class="col-xs-2 mr-3">
<base-button
<base-page v-if="invoice" class="xl:pl-96">
<sw-page-header :title="pageTitle">
<template slot="actions">
<div class="mr-3 text-sm">
<sw-button
v-if="invoice.status === 'DRAFT'"
:loading="isMarkingAsSent"
:disabled="isMarkingAsSent"
:outline="true"
color="theme"
variant="primary-outline"
@click="onMarkAsSent"
>
{{ $t('invoices.mark_as_sent') }}
</base-button>
</sw-button>
</div>
<base-button
<sw-button
v-if="invoice.status === 'DRAFT'"
:loading="isSendingEmail"
:disabled="isSendingEmail"
color="theme"
variant="primary"
class="text-sm"
@click="onSendInvoice"
>
{{ $t('invoices.send_invoice') }}
</base-button>
<router-link
v-if="invoice.status === 'SENT'"
</sw-button>
<sw-button
v-if="
invoice.status === 'SENT' ||
invoice.status === 'OVERDUE' ||
invoice.status === 'VIEWED'
"
tag-name="router-link"
:to="`/admin/payments/${$route.params.id}/create`"
variant="primary"
class="text-sm"
>
<base-button color="theme">
{{ $t('payments.record_payment') }}
</base-button>
</router-link>
<v-dropdown
:close-on-select="true"
align="left"
class="filter-container"
>
<a slot="activator" href="#">
<base-button color="theme">
<font-awesome-icon icon="ellipsis-h" />
</base-button>
</a>
<v-dropdown-item>
<div class="dropdown-item" @click="copyPdfUrl">
<font-awesome-icon
:icon="['fas', 'link']"
class="dropdown-item-icon"
/>
{{ $t('general.copy_pdf_url') }}
</div>
<router-link
:to="{ path: `/admin/invoices/${$route.params.id}/edit` }"
class="dropdown-item"
>
<font-awesome-icon
:icon="['fas', 'pencil-alt']"
class="dropdown-item-icon"
/>
{{ $t('general.edit') }}
</router-link>
<div class="dropdown-item" @click="removeInvoice($route.params.id)">
<font-awesome-icon
:icon="['fas', 'trash']"
class="dropdown-item-icon"
/>
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
</div>
</div>
<div class="invoice-sidebar">
<div class="side-header">
<base-input
{{ $t('payments.record_payment') }}
</sw-button>
<sw-dropdown class="ml-3">
<sw-button slot="activator" variant="primary" class="h-10">
<dots-horizontal-icon class="h-5" />
</sw-button>
<sw-dropdown-item @click="copyPdfUrl">
<link-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.copy_pdf_url') }}
</sw-dropdown-item>
<sw-dropdown-item
tag-name="router-link"
:to="`/admin/invoices/${$route.params.id}/edit`"
>
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removeInvoice($route.params.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</sw-page-header>
<!-- sidebar -->
<div
class="fixed top-0 left-0 hidden h-full pt-16 pb-5 ml-56 bg-white xl:ml-64 w-88 xl:block"
>
<div
class="flex items-center justify-between px-4 pt-8 pb-2 border border-gray-200 border-solid height-full"
>
<sw-input
v-model="searchData.searchText"
:placeholder="$t('general.search')"
input-class="inv-search"
icon="search"
class="mb-6"
type="text"
align-icon="right"
variant="gray"
@input="onSearch"
/>
<div class="btn-group ml-3" role="group" aria-label="First group">
<v-dropdown
>
<search-icon slot="rightIcon" class="h-5" />
</sw-input>
<div class="flex mb-6 ml-3" role="group" aria-label="First group">
<sw-dropdown
:close-on-select="false"
align="left"
class="filter-container"
position="bottom-start"
>
<a slot="activator" href="#">
<base-button
class="inv-button inv-filter-fields-btn"
color="default"
size="medium"
>
<font-awesome-icon icon="filter" />
</base-button>
</a>
<div class="filter-title">
<sw-button slot="activator" size="md" variant="gray-light">
<filter-icon class="h-5" />
</sw-button>
<div class="px-2 py-1 mb-2 border-b border-gray-200 border-solid">
{{ $t('general.sort_by') }}
</div>
<div class="filter-items">
<input
id="filter_invoice_date"
v-model="searchData.orderByField"
type="radio"
name="filter"
class="inv-radio"
value="invoice_date"
@change="onSearch"
/>
<label class="inv-label" for="filter_invoice_date">{{
$t('invoices.invoice_date')
}}</label>
</div>
<div class="filter-items">
<input
id="filter_due_date"
v-model="searchData.orderByField"
type="radio"
name="filter"
class="inv-radio"
value="due_date"
@change="onSearch"
/>
<label class="inv-label" for="filter_due_date">{{
$t('invoices.due_date')
}}</label>
</div>
<div class="filter-items">
<input
id="filter_invoice_number"
v-model="searchData.orderByField"
type="radio"
name="filter"
class="inv-radio"
value="invoice_number"
@change="onSearch"
/>
<label class="inv-label" for="filter_invoice_number">{{
$t('invoices.invoice_number')
}}</label>
</div>
</v-dropdown>
<base-button
<sw-dropdown-item class="flex px-1 py-1 cursor-pointer">
<sw-input-group class="-mt-2 text-sm font-normal">
<sw-radio
id="filter_invoice_date"
v-model="searchData.orderByField"
:label="$t('invoices.invoice_date')"
name="filter"
size="sm"
value="invoice_date"
@change="onSearch"
/>
</sw-input-group>
</sw-dropdown-item>
<sw-dropdown-item class="flex px-1 py-1 cursor-pointer">
<sw-input-group class="-mt-2 font-normal">
<sw-radio
id="filter_due_date"
:label="$t('invoices.due_date')"
v-model="searchData.orderByField"
name="filter"
size="sm"
value="due_date"
@change="onSearch"
/>
</sw-input-group>
</sw-dropdown-item>
<sw-dropdown-item class="flex px-1 py-1 cursor-pointer">
<sw-input-group class="-mt-2 font-normal">
<sw-radio
id="filter_invoice_number"
v-model="searchData.orderByField"
size="sm"
type="radio"
name="filter"
:label="$t('invoices.invoice_number')"
value="invoice_number"
@change="onSearch"
/>
</sw-input-group>
</sw-dropdown-item>
</sw-dropdown>
<sw-button
class="ml-1"
v-tooltip.top-center="{ content: getOrderName }"
class="inv-button inv-filter-sorting-btn"
color="default"
size="medium"
size="md"
variant="gray-light"
@click="sortData"
>
<font-awesome-icon v-if="getOrderBy" icon="sort-amount-up" />
<font-awesome-icon v-else icon="sort-amount-down" />
</base-button>
<sort-ascending-icon v-if="getOrderBy" class="h-5" />
<sort-descending-icon v-else class="h-5" />
</sw-button>
</div>
</div>
<base-loader v-if="isSearching" />
<div v-else class="side-content">
<base-loader v-if="isSearching" :show-bg-overlay="true" />
<div
v-else
class="h-full pb-32 overflow-y-scroll border-l border-gray-200 border-solid sw-scroll"
>
<router-link
v-for="(invoice, index) in invoices"
:to="`/admin/invoices/${invoice.id}/view`"
:id="'invoice-' + invoice.id"
:key="index"
class="side-invoice"
:class="[
'flex justify-between p-4 items-center cursor-pointer hover:bg-gray-100 border-l-4 border-transparent',
{
'bg-gray-100 border-l-4 border-primary-500 border-solid': hasActiveUrl(
invoice.id
),
},
]"
style="border-bottom: 1px solid rgba(185, 193, 209, 0.41)"
>
<div class="left">
<div class="inv-name">{{ invoice.user.name }}</div>
<div class="inv-number">{{ invoice.invoice_number }}</div>
<div class="flex-2">
<div
:class="'inv-status-' + invoice.status.toLowerCase()"
class="inv-status"
class="pr-2 mb-2 text-sm not-italic font-normal leading-5 text-black capitalize truncate"
>
{{ invoice.user.name }}
</div>
<div
class="mt-1 mb-2 text-xs not-italic font-medium leading-5 text-gray-600"
>
{{ invoice.invoice_number }}
</div>
<sw-badge
class="px-1 text-xs"
:bg-color="$utils.getBadgeStatusColor(invoice.status).bgColor"
:color="$utils.getBadgeStatusColor(invoice.status).color"
:font-size="$utils.getBadgeStatusColor(invoice.status).fontSize"
>
{{ invoice.status }}
</div>
</sw-badge>
</div>
<div class="right">
<div class="flex-1 whitespace-no-wrap right">
<div
class="inv-amount"
class="mb-2 text-xl not-italic font-semibold leading-8 text-right text-gray-900"
v-html="
$utils.formatMoney(invoice.due_amount, invoice.user.currency)
"
/>
<div class="inv-date">{{ invoice.formattedInvoiceDate }}</div>
<div
class="text-sm not-italic font-normal leading-5 text-right text-gray-600"
>
{{ invoice.formattedInvoiceDate }}
</div>
</div>
</router-link>
<p v-if="!invoices.length" class="no-result">
<p
v-if="!invoices.length"
class="flex justify-center px-4 mt-5 text-sm text-gray-600"
>
{{ $t('invoices.no_matching_invoices') }}
</p>
</div>
</div>
<div class="invoice-view-page-container">
<iframe :src="`${shareableLink}`" class="frame-style" />
<div
class="flex flex-col min-h-0 mt-8 overflow-hidden"
style="height: 75vh"
>
<iframe
:src="`${shareableLink}`"
class="flex-1 border border-gray-400 border-solid rounded-md frame-style"
/>
</div>
</div>
</base-page>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import {
DotsHorizontalIcon,
FilterIcon,
SortAscendingIcon,
SortDescendingIcon,
SearchIcon,
LinkIcon,
TrashIcon,
PencilIcon,
} from '@vue-hero-icons/solid'
const _ = require('lodash')
export default {
data () {
components: {
DotsHorizontalIcon,
FilterIcon,
SortAscendingIcon,
SortDescendingIcon,
SearchIcon,
LinkIcon,
PencilIcon,
TrashIcon,
},
data() {
return {
id: null,
count: null,
@@ -207,37 +263,49 @@ export default {
searchData: {
orderBy: null,
orderByField: null,
searchText: null
searchText: null,
},
isRequestOnGoing: false,
isSearching: false,
isSendingEmail: false,
isMarkingAsSent: false
isMarkingAsSent: false,
}
},
computed: {
getOrderBy () {
if (this.searchData.orderBy === 'asc' || this.searchData.orderBy == null) {
pageTitle() {
return this.invoice.invoice_number
},
getOrderBy() {
if (
this.searchData.orderBy === 'asc' ||
this.searchData.orderBy == null
) {
return true
}
return false
},
getOrderName () {
getOrderName() {
if (this.getOrderBy) {
return this.$t('general.ascending')
}
return this.$t('general.descending')
},
shareableLink () {
shareableLink() {
return `/invoices/pdf/${this.invoice.unique_hash}`
}
},
getCurrentInvoiceId() {
if (this.invoice && this.invoice.id) {
return this.invoice.id
}
return null
},
},
watch: {
$route (to, from) {
$route(to, from) {
this.loadInvoice()
}
},
},
created () {
created() {
this.loadInvoices()
this.loadInvoice()
this.onSearch = _.debounce(this.onSearch, 500)
@@ -251,32 +319,60 @@ export default {
'sendEmail',
'deleteInvoice',
'selectInvoice',
'fetchViewInvoice'
'fetchInvoice',
]),
async loadInvoices () {
let response = await this.fetchInvoices()
...mapActions('modal', ['openModal']),
hasActiveUrl(id) {
return this.$route.params.id == id
},
async loadInvoices() {
let response = await this.fetchInvoices({ limit: 'all' })
if (response.data) {
this.invoices = response.data.invoices.data
}
setTimeout(() => {
this.scrollToInvoice()
}, 500)
},
async loadInvoice () {
let response = await this.fetchViewInvoice(this.$route.params.id)
scrollToInvoice() {
const el = document.getElementById(`invoice-${this.$route.params.id}`)
if (el) {
el.scrollIntoView({ behavior: 'smooth' })
el.classList.add('shake')
}
},
async loadInvoice() {
let response = await this.fetchInvoice(this.$route.params.id)
if (response.data) {
this.invoice = response.data.invoice
}
},
async onSearch () {
async onSearch() {
let data = ''
if (this.searchData.searchText !== '' && this.searchData.searchText !== null && this.searchData.searchText !== undefined) {
if (
this.searchData.searchText !== '' &&
this.searchData.searchText !== null &&
this.searchData.searchText !== undefined
) {
data += `search=${this.searchData.searchText}&`
}
if (this.searchData.orderBy !== null && this.searchData.orderBy !== undefined) {
if (
this.searchData.orderBy !== null &&
this.searchData.orderBy !== undefined
) {
data += `orderBy=${this.searchData.orderBy}&`
}
if (this.searchData.orderByField !== null && this.searchData.orderByField !== undefined) {
if (
this.searchData.orderByField !== null &&
this.searchData.orderByField !== undefined
) {
data += `orderByField=${this.searchData.orderByField}`
}
this.isSearching = true
@@ -286,7 +382,7 @@ export default {
this.invoices = response.data.invoices.data
}
},
sortData () {
sortData() {
if (this.searchData.orderBy === 'asc') {
this.searchData.orderBy = 'desc'
this.onSearch()
@@ -296,77 +392,68 @@ export default {
this.onSearch()
return true
},
async onMarkAsSent () {
window.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('invoices.invoice_mark_as_sent'),
icon: '/assets/icon/check-circle-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
this.isMarkingAsSent = true
let response = await this.markAsSent({id: this.invoice.id})
this.isMarkingAsSent = false
if (response.data) {
window.toastr['success'](this.$tc('invoices.marked_as_sent_message'))
async onMarkAsSent() {
window
.swal({
title: this.$t('general.are_you_sure'),
text: this.$t('invoices.invoice_mark_as_sent'),
icon: '/assets/icon/check-circle-solid.svg',
buttons: true,
dangerMode: true,
})
.then(async (value) => {
if (value) {
this.isMarkingAsSent = true
let response = await this.markAsSent({
id: this.invoice.id,
status: 'SENT',
})
this.isMarkingAsSent = false
if (response.data) {
this.invoice.status = 'SENT'
window.toastr['success'](
this.$tc('invoices.marked_as_sent_message')
)
}
}
}
})
},
async onSendInvoice() {
this.openModal({
title: this.$t('invoices.send_invoice'),
componentName: 'SendInvoiceModal',
id: this.invoice.id,
data: this.invoice,
})
},
async onSendInvoice () {
window.swal({
title: this.$tc('general.are_you_sure'),
text: this.$tc('invoices.confirm_send_invoice'),
icon: '/assets/icon/paper-plane-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
this.isSendingEmail = true
let response = await this.sendEmail({id: this.invoice.id})
this.isSendingEmail = false
if (response.data.success) {
window.toastr['success'](this.$tc('invoices.send_invoice_successfully'))
return true
}
if (response.data.error === 'user_email_does_not_exist') {
window.toastr['error'](this.$tc('invoices.user_email_does_not_exist'))
return false
}
window.toastr['error'](this.$tc('invoices.something_went_wrong'))
}
})
},
copyPdfUrl () {
copyPdfUrl() {
let pdfUrl = `${window.location.origin}/invoices/pdf/${this.invoice.unique_hash}`
let response = this.$utils.copyTextToClipboard(pdfUrl)
window.toastr['success'](this.$tc('Copied PDF url to clipboard!'))
window.toastr['success'](this.$t('general.copied_pdf_url_clipboard'))
},
async removeInvoice (id) {
this.selectInvoice([parseInt(id)])
this.id = id
window.swal({
title: 'Deleted',
text: 'you will not be able to recover this invoice!',
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
let request = await this.deleteInvoice(this.id)
if (request.data.success) {
window.toastr['success'](this.$tc('invoices.deleted_message', 1))
this.$router.push('/admin/invoices')
} else if (request.data.error) {
window.toastr['error'](request.data.message)
async removeInvoice(id) {
window
.swal({
title: this.$t('general.are_you_sure'),
text: 'you will not be able to recover this invoice!',
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
})
.then(async (value) => {
if (value) {
let request = await this.deleteInvoice({ ids: [id] })
if (request.data.success) {
window.toastr['success'](this.$tc('invoices.deleted_message', 1))
this.$router.push('/admin/invoices')
} else if (request.data.error) {
window.toastr['error'](request.data.message)
}
}
}
})
}
}
})
},
},
}
</script>

View File

@@ -1,132 +1,161 @@
<template>
<div class="main-content item-create">
<div class="page-header">
<h3 class="page-title">{{ isEdit ? $t('items.edit_item') : $t('items.new_item') }}</h3>
<ol class="breadcrumb">
<li class="breadcrumb-item"><router-link slot="item-title" to="/admin/dashboard">{{ $t('general.home') }}</router-link></li>
<li class="breadcrumb-item"><router-link slot="item-title" to="/admin/items">{{ $tc('items.item',2) }}</router-link></li>
<li class="breadcrumb-item"><a href="#"> {{ isEdit ? $t('items.edit_item') : $t('items.new_item') }}</a></li>
</ol>
</div>
<div class="row">
<div class="col-sm-6">
<div class="card">
<form action="" @submit.prevent="submitItem">
<div class="card-body">
<div class="form-group">
<label class="control-label">{{ $t('items.name') }}</label><span class="text-danger"> *</span>
<base-input
v-model.trim="formData.name"
:invalid="$v.formData.name.$error"
focus
type="text"
name="name"
@input="$v.formData.name.$touch()"
/>
<div v-if="$v.formData.name.$error">
<span v-if="!$v.formData.name.required" class="text-danger">{{ $t('validation.required') }} </span>
<span v-if="!$v.formData.name.minLength" class="text-danger">
{{ $tc('validation.name_min_length', $v.formData.name.$params.minLength.min, { count: $v.formData.name.$params.minLength.min }) }}
</span>
</div>
</div>
<div class="form-group">
<label>{{ $t('items.price') }}</label><span class="text-danger"> *</span>
<div class="base-input">
<money
:class="{'invalid' : $v.formData.price.$error}"
v-model="price"
v-bind="defaultCurrencyForInput"
class="input-field"
<base-page>
<!-- Page Header -->
<sw-page-header class="mb-3" :title="pageTitle">
<sw-breadcrumb slot="breadcrumbs">
<sw-breadcrumb-item to="/admin/dashboard" :title="$t('general.home')" />
<sw-breadcrumb-item to="/admin/items" :title="$tc('items.item', 2)" />
<sw-breadcrumb-item
v-if="$route.name === 'items.edit'"
to="#"
:title="$t('items.edit_item')"
active
/>
<sw-breadcrumb-item
v-else
to="#"
:title="$t('items.new_item')"
active
/>
</sw-breadcrumb>
</sw-page-header>
<div class="grid grid-cols-12">
<div class="col-span-12 md:col-span-6">
<form action="" @submit.prevent="submitItem">
<sw-card>
<sw-input-group
:label="$t('items.name')"
:error="nameError"
class="mb-4"
required
>
<sw-input
v-model.trim="formData.name"
:invalid="$v.formData.name.$error"
class="mt-2"
focus
type="text"
name="name"
@input="$v.formData.name.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('items.price')"
:error="priceError"
class="mb-4"
required
>
<sw-money
v-model.trim="price"
:invalid="$v.formData.price.$error"
:currency="defaultCurrencyForInput"
class="relative w-full focus:border focus:border-solid focus:border-primary-500"
@input="$v.formData.price.$touch()"
/>
</sw-input-group>
<sw-input-group :label="$t('items.unit')" class="mb-4">
<sw-select
v-model="formData.unit"
class="mt-2"
:options="itemUnits"
:searchable="true"
:show-labels="false"
:placeholder="$t('items.select_a_unit')"
label="name"
>
<div
slot="afterList"
class="flex items-center justify-center w-full px-6 py-3 text-base bg-gray-200 cursor-pointer text-primary-400"
@click="addItemUnit"
>
<shopping-cart-icon
class="h-5 mr-2 -ml-2 text-center text-primary-400"
/>
<label class="ml-2 text-sm leading-none text-primary-400">{{
$t('settings.customization.items.add_item_unit')
}}</label>
</div>
<div v-if="$v.formData.price.$error">
<span v-if="!$v.formData.price.required" class="text-danger">{{ $t('validation.required') }} </span>
<span v-if="!$v.formData.price.maxLength" class="text-danger">{{ $t('validation.price_maxlength') }}</span>
<span v-if="!$v.formData.price.minValue" class="text-danger">{{ $t('validation.price_minvalue') }}</span>
</div>
</div>
<div class="form-group">
<label>{{ $t('items.unit') }}</label>
<base-select
v-model="formData.unit"
:options="itemUnits"
:searchable="true"
:show-labels="false"
:placeholder="$t('items.select_a_unit')"
label="name"
>
<div slot="afterList">
<button type="button" class="list-add-button" @click="addItemUnit">
<font-awesome-icon class="icon" icon="cart-plus" />
<label>{{ $t('settings.customization.items.add_item_unit') }}</label>
</button>
</div>
</base-select>
</div>
<div v-if="isTaxPerItem" class="form-group">
<label>{{ $t('items.taxes') }}</label>
<base-select
v-model="formData.taxes"
:options="getTaxTypes"
:searchable="true"
:show-labels="false"
:allow-empty="true"
:multiple="true"
track-by="tax_type_id"
label="tax_name"
/>
</div>
<div class="form-group">
<label for="description">{{ $t('items.description') }}</label>
<base-text-area
v-model="formData.description"
rows="2"
name="description"
@input="$v.formData.description.$touch()"
/>
<div v-if="$v.formData.description.$error">
<span v-if="!$v.formData.description.maxLength" class="text-danger">
{{ $t('validation.description_maxlength') }}
</span>
</div>
</div>
<div class="form-group">
<base-button
:loading="isLoading"
:disabled="isLoading"
icon="save"
color="theme"
type="submit"
class="collapse-button"
>
{{ isEdit ? $t('items.update_item') : $t('items.save_item') }}
</base-button>
</div>
</sw-select>
</sw-input-group>
<sw-input-group
v-if="isTaxPerItem"
:label="$t('items.taxes')"
class="mb-4"
>
<sw-select
v-model="formData.taxes"
class="mt-2"
:options="getTaxTypes"
:searchable="true"
:show-labels="false"
:allow-empty="true"
:multiple="true"
track-by="tax_type_id"
label="tax_name"
/>
</sw-input-group>
<sw-input-group
:label="$t('items.description')"
:error="descriptionError"
class="mb-4"
>
<sw-textarea
v-model="formData.description"
rows="2"
name="description"
@input="$v.formData.description.$touch()"
/>
</sw-input-group>
<div class="mb-4">
<sw-button
:loading="isLoading"
variant="primary"
size="lg"
class="flex w-full justify-center md:w-auto"
>
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
{{ isEdit ? $t('items.update_item') : $t('items.save_item') }}
</sw-button>
</div>
</form>
</div>
</sw-card>
</form>
</div>
</div>
</div>
</base-page>
</template>
<script>
import { validationMixin } from 'vuelidate'
import { mapActions, mapGetters } from 'vuex'
const { required, minLength, numeric, minValue, maxLength } = require('vuelidate/lib/validators')
import { ShoppingCartIcon } from '@vue-hero-icons/solid'
import TheSiteHeaderVue from '../layouts/partials/TheSiteHeader.vue'
const {
required,
minLength,
numeric,
minValue,
maxLength,
} = require('vuelidate/lib/validators')
export default {
mixins: {
validationMixin
components: {
ShoppingCartIcon,
},
data () {
data() {
return {
isLoading: false,
title: 'Add Item',
units: [],
taxes: [],
taxPerItem: '',
formData: {
name: '',
description: '',
@@ -134,142 +163,246 @@ export default {
unit_id: null,
unit: null,
taxes: [],
tax_per_item: false
},
money: {
decimal: '.',
thousands: ',',
prefix: '$ ',
precision: 2,
masked: false
}
masked: false,
},
}
},
computed: {
...mapGetters('currency', [
'defaultCurrencyForInput'
]),
...mapGetters('item', [
'itemUnits'
]),
...mapGetters('company', ['defaultCurrencyForInput']),
...mapGetters('item', ['itemUnits']),
...mapGetters('taxType', ['taxTypes']),
price: {
get: function () {
return this.formData.price / 100
},
set: function (newValue) {
this.formData.price = newValue * 100
}
},
},
...mapGetters('taxType', [
'taxTypes'
]),
isEdit () {
pageTitle() {
if (this.$route.name === 'items.edit') {
return this.$t('items.edit_item')
}
return this.$t('items.new_item')
},
...mapGetters('taxType', ['taxTypes']),
isEdit() {
if (this.$route.name === 'items.edit') {
return true
}
return false
},
isTaxPerItem () {
isTaxPerItem() {
return this.taxPerItem === 'YES' ? 1 : 0
},
getTaxTypes () {
return this.taxTypes.map(tax => {
return {...tax, tax_type_id: tax.id, tax_name: tax.name + ' (' + tax.percent + '%)'}
getTaxTypes() {
return this.taxTypes.map((tax) => {
return {
...tax,
tax_type_id: tax.id,
tax_name: tax.name + ' (' + tax.percent + '%)',
}
})
}
},
nameError() {
if (!this.$v.formData.name.$error) {
return ''
}
if (!this.$v.formData.name.required) {
return this.$t('validation.required')
}
if (!this.$v.formData.name.minLength) {
return this.$tc(
'validation.name_min_length',
this.$v.formData.name.$params.minLength.min,
{ count: this.$v.formData.name.$params.minLength.min }
)
}
},
priceError() {
if (!this.$v.formData.price.$error) {
return ''
}
if (!this.$v.formData.price.required) {
return this.$t('validation.required')
}
if (!this.$v.formData.price.maxLength) {
return this.$t('validation.price_maxlength')
}
if (!this.$v.formData.price.minValue) {
return this.$t('validation.price_minvalue')
}
},
descriptionError() {
if (!this.$v.formData.description.$error) {
return ''
}
if (!this.$v.formData.description.maxLength) {
return this.$t('validation.description_maxlength')
}
},
},
created () {
created() {
this.loadData()
},
mounted() {
this.setTaxPerItem()
if (this.isEdit) {
this.loadEditData()
}
this.$v.formData.$reset()
},
validations: {
formData: {
name: {
required,
minLength: minLength(3)
minLength: minLength(3),
},
price: {
required,
numeric,
maxLength: maxLength(20),
minValue: minValue(0.1)
minValue: minValue(0.1),
},
description: {
maxLength: maxLength(255)
}
}
maxLength: maxLength(255),
},
},
},
methods: {
...mapActions('item', [
'addItem',
'fetchItem',
'updateItem'
'updateItem',
'fetchItemUnits',
]),
...mapActions('modal', [
'openModal'
]),
async setTaxPerItem () {
let res = await axios.get('/api/settings/get-setting?key=tax_per_item')
if (res.data && res.data.tax_per_item === 'YES') {
this.taxPerItem = 'YES'
} else {
this.taxPerItem = 'FALSE'
...mapActions('taxType', ['fetchTaxTypes']),
...mapActions('company', ['fetchCompanySettings']),
...mapActions('modal', ['openModal']),
async setTaxPerItem() {
let response = await this.fetchCompanySettings(['tax_per_item'])
if (response.data) {
response.data.tax_per_item === 'YES'
? (this.taxPerItem = 'YES')
: (this.taxPerItem = 'NO')
}
},
async loadEditData () {
let response = await this.fetchItem(this.$route.params.id)
this.formData = {...response.data.item, unit: null}
this.formData.taxes = response.data.item.taxes.map(tax => {
return {...tax, tax_name: tax.name + ' (' + tax.percent + '%)'}
})
async loadData() {
if (this.isEdit) {
let response = await this.fetchItem(this.$route.params.id)
this.formData.unit = this.itemUnits.find(_unit => response.data.item.unit_id === _unit.id)
this.fractional_price = response.data.item.price
this.formData = { ...response.data.item, unit: null }
this.fractional_price = response.data.item.price
if (this.formData.unit_id) {
await this.fetchItemUnits({ limit: 'all' })
this.formData.unit = this.itemUnits.find(
(_unit) => response.data.item.unit_id === _unit.id
)
}
if (this.formData.taxes) {
await this.fetchTaxTypes({ limit: 'all' })
this.formData.taxes = response.data.item.taxes.map((tax) => {
return { ...tax, tax_name: tax.name + '(' + tax.percent + '%)' }
})
}
} else {
this.fetchItemUnits({ limit: 'all' })
this.fetchTaxTypes({ limit: 'all' })
}
},
async submitItem () {
async submitItem() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return false
}
if (this.formData.unit) {
this.formData.unit_id = this.formData.unit.id
}
let response
this.isLoading = true
if (this.isEdit) {
this.isLoading = true
response = await this.updateItem(this.formData)
} else {
let data = {
...this.formData,
taxes: this.formData.taxes.map(tax => {
taxes: this.formData.taxes.map((tax) => {
return {
tax_type_id: tax.id,
amount: ((this.formData.price * tax.percent) / 100),
amount: (this.formData.price * tax.percent) / 100,
percent: tax.percent,
name: tax.name,
collective_tax: 0
collective_tax: 0,
}
})
}),
}
response = await this.addItem(data)
}
if (response.data) {
this.isLoading = false
window.toastr['success'](this.$tc('items.updated_message'))
this.$router.push('/admin/items')
return true
if (!this.isEdit) {
window.toastr['success'](this.$tc('items.created_message'))
this.$router.push('/admin/items')
return true
} else {
window.toastr['success'](this.$tc('items.updated_message'))
this.$router.push('/admin/items')
return true
}
window.toastr['error'](response.data.error)
}
window.toastr['error'](response.data.error)
},
async addItemUnit () {
async addItemUnit() {
this.openModal({
'title': this.$t('settings.customization.items.add_item_unit'),
'componentName': 'ItemUnit'
title: this.$t('settings.customization.items.add_item_unit'),
componentName: 'ItemUnit',
})
}
}
},
},
}
</script>

View File

@@ -1,295 +1,342 @@
<template>
<div class="items main-content">
<div class="page-header">
<div class="d-flex flex-row">
<div>
<h3 class="page-title">{{ $tc('items.item', 2) }}</h3>
</div>
</div>
<ol class="breadcrumb">
<li class="breadcrumb-item">
<router-link
slot="item-title"
to="dashboard">
{{ $t('general.home') }}
</router-link>
</li>
<li class="breadcrumb-item">
<router-link
slot="item-title"
to="#">
{{ $tc('items.item', 2) }}
</router-link>
</li>
</ol>
<div class="page-actions row">
<div class="col-xs-2 mr-4">
<base-button
v-show="totalItems || filtersApplied"
:outline="true"
:icon="filterIcon"
color="theme"
size="large"
right-icon
@click="toggleFilter"
>
{{ $t('general.filter') }}
</base-button>
</div>
<router-link slot="item-title" class="col-xs-2" to="items/create">
<base-button
color="theme"
icon="plus"
size="large"
>
{{ $t('items.add_item') }}
</base-button>
</router-link>
</div>
</div>
<base-page>
<sw-page-header :title="$t('items.title')">
<sw-breadcrumb slot="breadcrumbs">
<sw-breadcrumb-item to="dashboard" :title="$t('general.home')" />
<sw-breadcrumb-item to="#" :title="$tc('items.item', 2)" active />
</sw-breadcrumb>
<transition name="fade">
<div v-show="showFilters" class="filter-section">
<div class="row">
<div class="col-sm-4">
<label class="form-label"> {{ $tc('items.name') }} </label>
<base-input
v-model="filters.name"
type="text"
name="name"
autocomplete="off"
/>
</div>
<div class="col-sm-4">
<label class="form-label"> {{ $tc('items.unit') }} </label>
<base-select
v-model="filters.unit"
:options="itemUnits"
:searchable="true"
:show-labels="false"
:placeholder="$t('items.select_a_unit')"
label="name"
autocomplete="off"
/>
</div>
<div class="col-sm-4">
<label class="form-label"> {{ $tc('items.price') }} </label>
<base-input
v-model="filters.price"
type="text"
name="name"
autocomplete="off"
/>
</div>
<label class="clear-filter" @click="clearFilter"> {{ $t('general.clear_all') }}</label>
</div>
</div>
</transition>
<div v-cloak v-show="showEmptyScreen" class="col-xs-1 no-data-info" align="center">
<satellite-icon class="mt-5 mb-4"/>
<div class="row" align="center">
<label class="col title">{{ $t('items.no_items') }}</label>
</div>
<div class="row">
<label class="description col mt-1" align="center">{{ $t('items.list_of_items') }}</label>
</div>
<div class="btn-container">
<base-button
:outline="true"
color="theme"
class="mt-3"
size="large"
@click="$router.push('items/create')"
<template slot="actions">
<sw-button
v-show="totalItems"
variant="primary-outline"
size="lg"
@click="toggleFilter"
>
{{ $t('items.add_new_item') }}
</base-button>
</div>
</div>
{{ $t('general.filter') }}
<component :is="filterIcon" class="w-4 h-4 ml-2 -mr-1" />
</sw-button>
<div v-show="!showEmptyScreen" class="table-container">
<div class="table-actions mt-5">
<p class="table-stats">{{ $t('general.showing') }}: <b>{{ items.length }}</b> {{ $t('general.of') }} <b>{{ totalItems }}</b></p>
<transition name="fade">
<v-dropdown v-if="selectedItems.length" :show-arrow="false">
<span slot="activator" href="#" class="table-actions-button dropdown-toggle">
<sw-button
tag-name="router-link"
to="items/create"
variant="primary"
size="lg"
class="ml-4"
>
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('items.add_item') }}
</sw-button>
</template>
</sw-page-header>
<slide-y-up-transition>
<sw-filter-wrapper v-show="showFilters">
<sw-input-group :label="$tc('items.name')" class="flex-1 mt-2 ml-0">
<sw-input
v-model="filters.name"
type="text"
name="name"
class="mt-2"
autocomplete="off"
/>
</sw-input-group>
<sw-input-group
:label="$tc('items.unit')"
class="flex-1 mt-2 ml-0 lg:ml-6"
>
<sw-select
v-model="filters.unit"
:options="itemUnits"
:searchable="true"
class="mt-2"
:show-labels="false"
:placeholder="$t('items.select_a_unit')"
label="name"
autocomplete="off"
/>
</sw-input-group>
<sw-input-group
:label="$tc('items.price')"
class="flex-1 mt-2 ml-0 lg:ml-6"
>
<sw-input
v-model="filters.price"
type="text"
name="name"
class="mt-2"
autocomplete="off"
/>
</sw-input-group>
<label
class="absolute text-sm leading-snug text-gray-900 cursor-pointer"
style="top: 10px; right: 15px"
@click="clearFilter"
>
{{ $t('general.clear_all') }}</label
>
</sw-filter-wrapper>
</slide-y-up-transition>
<sw-empty-table-placeholder
v-show="showEmptyScreen"
:title="$t('items.no_items')"
:description="$t('items.list_of_items')"
>
<satellite-icon class="mt-5 mb-4" />
<sw-button
slot="actions"
tag-name="router-link"
to="/admin/items/create"
size="lg"
variant="primary-outline"
>
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('items.add_new_item') }}
</sw-button>
</sw-empty-table-placeholder>
<div v-show="!showEmptyScreen" class="relative table-container">
<div
class="relative flex items-center justify-between h-10 mt-5 list-none border-b-2 border-gray-200 border-solid"
>
<p class="text-sm">
{{ $t('general.showing') }}: <b>{{ items.length }}</b>
{{ $t('general.of') }} <b>{{ totalItems }}</b>
</p>
<sw-transition>
<sw-dropdown v-if="selectedItems.length">
<span
slot="activator"
class="flex block text-sm font-medium cursor-pointer select-none text-primary-400"
>
{{ $t('general.actions') }}
<chevron-down-icon class="h-5" />
</span>
<v-dropdown-item>
<div class="dropdown-item" @click="removeMultipleItems">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
</transition>
<sw-dropdown-item @click="removeMultipleItems">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</sw-transition>
</div>
<div class="custom-control custom-checkbox">
<input
id="select-all"
<div class="absolute z-10 items-center pl-4 mt-2 select-none md:mt-12">
<sw-checkbox
v-model="selectAllFieldStatus"
type="checkbox"
class="custom-control-input"
variant="primary"
size="sm"
class="hidden md:inline"
@change="selectAllItems"
>
<label v-show="!isRequestOngoing" for="select-all" class="custom-control-label selectall">
<span class="select-all-label">{{ $t('general.select_all') }} </span>
</label>
/>
<sw-checkbox
v-model="selectAllFieldStatus"
:label="$t('general.select_all')"
variant="primary"
size="sm"
class="md:hidden"
@change="selectAllItems"
/>
</div>
<table-component
<sw-table-component
ref="table"
:data="fetchData"
:show-filter="false"
table-class="table"
>
<table-column
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="no-click"
>
<div slot-scope="row" class="custom-control custom-checkbox">
<sw-checkbox
:id="row.id"
v-model="selectField"
:value="row.id"
variant="primary"
size="sm"
/>
</div>
</sw-table-column>
<sw-table-column :sortable="true" :label="$t('items.name')" show="name">
<template slot-scope="row">
<div class="custom-control custom-checkbox">
<input
:id="row.id"
v-model="selectField"
:value="row.id"
type="checkbox"
class="custom-control-input"
>
<label :for="row.id" class="custom-control-label"/>
</div>
<span>{{ $t('items.name') }}</span>
<router-link
:to="{ path: `items/${row.id}/edit` }"
class="font-medium text-primary-500"
>
{{ row.name }}
</router-link>
</template>
</table-column>
<table-column
:label="$t('items.name')"
show="name"
/>
<table-column
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('items.unit')"
show="unit_name"
/>
<table-column
>
<template slot-scope="row">
<span>{{ $t('items.unit') }}</span>
<span>
{{ row.unit_name ? row.unit_name : 'Not selected' }}
</span>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('items.price')"
show="price"
>
<template slot-scope="row">
<span> {{ $t('items.price') }} </span>
<div v-html="$utils.formatMoney(row.price, defaultCurrency)" />
</template>
</table-column>
<table-column
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('items.added_on')"
sort-as="created_at"
show="formattedCreatedAt"
/>
<table-column
:sortable="false"
<sw-table-column
:sortable="true"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span> {{ $t('items.action') }} </span>
<v-dropdown>
<a slot="activator" href="#">
<dot-icon />
</a>
<v-dropdown-item>
<router-link :to="{path: `items/${row.id}/edit`}" class="dropdown-item">
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
{{ $t('general.edit') }}
</router-link>
<sw-dropdown>
<dot-icon slot="activator" />
</v-dropdown-item>
<v-dropdown-item>
<div class="dropdown-item" @click="removeItems(row.id)">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
<sw-dropdown-item
tag-name="router-link"
:to="`items/${row.id}/edit`"
>
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removeItems(row.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</table-column>
</table-component>
</sw-table-column>
</sw-table-component>
</div>
</div>
</base-page>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import DotIcon from '../../components/icon/DotIcon'
import {
FilterIcon,
XIcon,
ChevronDownIcon,
PencilIcon,
TrashIcon,
PlusIcon,
} from '@vue-hero-icons/solid'
import SatelliteIcon from '../../components/icon/SatelliteIcon'
import BaseButton from '../../../js/components/base/BaseButton'
export default {
components: {
DotIcon,
SatelliteIcon,
BaseButton
FilterIcon,
XIcon,
PlusIcon,
ChevronDownIcon,
PencilIcon,
TrashIcon,
},
data () {
data() {
return {
id: null,
showFilters: false,
sortedBy: 'created_at',
isRequestOngoing: true,
filtersApplied: false,
filters: {
name: '',
unit: '',
price: ''
}
price: '',
},
}
},
computed: {
...mapGetters('item', [
'items',
'selectedItems',
'totalItems',
'selectAllField',
'itemUnits'
'itemUnits',
]),
...mapGetters('currency', [
'defaultCurrency'
]),
showEmptyScreen () {
return !this.totalItems && !this.isRequestOngoing && !this.filtersApplied
...mapGetters('company', ['defaultCurrency']),
showEmptyScreen() {
return !this.totalItems && !this.isRequestOngoing
},
filterIcon () {
return (this.showFilters) ? 'times' : 'filter'
filterIcon() {
return this.showFilters ? 'x-icon' : 'filter-icon'
},
selectField: {
get: function () {
return this.selectedItems
},
set: function (val) {
this.selectItem(val)
}
},
},
selectAllFieldStatus: {
get: function () {
return this.selectAllField
},
set: function (val) {
this.setSelectAllState(val)
}
}
},
},
},
watch: {
filters: {
handler: 'setFilters',
deep: true
}
deep: true,
},
},
destroyed () {
mounted() {
this.fetchItemUnits({ limit: 'all' })
},
destroyed() {
if (this.selectAllField) {
this.selectAllItems()
}
},
methods: {
...mapActions('item', [
'fetchItems',
@@ -297,19 +344,22 @@ export default {
'selectItem',
'deleteItem',
'deleteMultipleItems',
'setSelectAllState'
'setSelectAllState',
'fetchItemUnits',
]),
refreshTable () {
refreshTable() {
this.$refs.table.refresh()
},
async fetchData ({ page, filter, sort }) {
async fetchData({ page, filter, sort }) {
let data = {
search: this.filters.name !== null ? this.filters.name : '',
unit_id: this.filters.unit !== null ? this.filters.unit.id : '',
price: this.filters.price * 100,
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page
page,
}
this.isRequestOngoing = true
@@ -320,44 +370,43 @@ export default {
data: response.data.items.data,
pagination: {
totalPages: response.data.items.last_page,
currentPage: page
}
currentPage: page,
},
}
},
setFilters () {
this.filtersApplied = true
setFilters() {
this.refreshTable()
},
clearFilter () {
clearFilter() {
this.filters = {
name: '',
unit: '',
price: ''
price: '',
}
this.$nextTick(() => {
this.filtersApplied = false
})
},
toggleFilter () {
if (this.showFilters && this.filtersApplied) {
toggleFilter() {
if (this.showFilters) {
this.clearFilter()
this.refreshTable()
}
this.showFilters = !this.showFilters
},
async removeItems (id) {
async removeItems(id) {
this.id = id
swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('items.confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
dangerMode: true,
}).then(async (willDelete) => {
if (willDelete) {
let res = await this.deleteItem(this.id)
let res = await this.deleteItem({ ids: [id] })
if (res.data.success) {
window.toastr['success'](this.$tc('items.deleted_message', 1))
this.$refs.table.refresh()
@@ -365,7 +414,10 @@ export default {
}
if (res.data.error === 'item_attached') {
window.toastr['error'](this.$tc('items.item_attached_message'), this.$t('general.action_failed'))
window.toastr['error'](
this.$tc('items.item_attached_message'),
this.$t('general.action_failed')
)
return true
}
@@ -374,16 +426,18 @@ export default {
}
})
},
async removeMultipleItems () {
async removeMultipleItems() {
swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('items.confirm_delete', 2),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
dangerMode: true,
}).then(async (willDelete) => {
if (willDelete) {
let res = await this.deleteMultipleItems()
if (res.data.success || res.data.items) {
window.toastr['success'](this.$tc('items.deleted_message', 2))
this.$refs.table.refresh()
@@ -392,7 +446,7 @@ export default {
}
}
})
}
}
},
},
}
</script>

View File

@@ -1,83 +1,61 @@
<template>
<div class="template-container" v-if="isAppLoaded">
<div v-if="isAppLoaded" class="h-full">
<base-modal />
<site-header/>
<site-sidebar type="basic"/>
<transition
name="fade"
mode="out-in">
<site-header />
<div class="flex h-screen pt-16 pb-10 overflow-hidden">
<site-sidebar />
<router-view />
</transition>
<site-footer/>
</div>
<site-footer />
</div>
<div v-else class="template-container">
<font-awesome-icon icon="spinner" class="fa-spin"/>
<div v-else class="h-full">
<refresh-icon class="h-6 animate-spin" />
</div>
</template>
<script type="text/babel">
import SiteHeader from './partials/TheSiteHeader.vue'
import SiteFooter from './partials/TheSiteFooter.vue'
import SiteSidebar from './partials/TheSiteSidebar.vue'
import Layout from '../../helpers/layout'
import BaseModal from '../../components/base/modal/BaseModal'
import { RefreshIcon } from '@vue-hero-icons/solid'
import { mapActions, mapGetters } from 'vuex'
export default {
components: {
SiteHeader, SiteSidebar, SiteFooter, BaseModal
},
data () {
return {
'header': 'header'
}
SiteHeader,
SiteSidebar,
SiteFooter,
BaseModal,
RefreshIcon,
},
computed: {
...mapGetters([
'isAppLoaded'
]),
...mapGetters(['isAppLoaded']),
...mapGetters('company', {
selectedCompany: 'getSelectedCompany',
companies: 'getCompanies'
}),
isShow () {
isShow() {
return true
}
},
mounted () {
Layout.set('layout-default')
},
},
created () {
this.bootstrap().then((res) => {
created() {
this.bootstrap().then(() => {
this.setInitialCompany()
})
},
methods: {
...mapActions(['bootstrap']),
...mapActions('company', ['setSelectedCompany']),
setInitialCompany () {
let selectedCompany = Ls.get('selectedCompany') !== null
if (selectedCompany) {
let foundCompany = this.companies.find((company) => company.id === parseInt(selectedCompany))
if (foundCompany) {
this.setSelectedCompany(foundCompany)
return
}
}
this.setSelectedCompany(this.companies[0])
}
}
setInitialCompany() {
this.setSelectedCompany(this.selectedCompany)
},
},
}
</script>
<style lang="scss" scoped>
body {
background-color: #f8f8f8;
}
</style>

View File

@@ -1,55 +1,83 @@
<template>
<div class="login-page login-3">
<div class="site-wrapper">
<div class="login-box">
<div class="box-wrapper">
<div class="logo-main">
<a href="/admin">
<img
src="/assets/img/crater-logo.png"
alt="Crater Logo">
</a>
</div>
<router-view></router-view>
<div class="page-copyright">
<p>{{ $t('layout_login.copyright_crater') }}</p>
</div>
<div class="grid h-full grid-cols-12 overflow-y-hidden bg-gray-100">
<div
class="flex items-center justify-center w-full max-w-sm col-span-12 p-4 mx-auto text-gray-900 md:p-8 md:col-span-6 lg:col-span-4 flex-2 md:pb-48 md:pt-40"
>
<div class="w-full">
<a href="/admin">
<img
src="/assets/img/crater-logo.png"
class="block w-48 h-auto max-w-full mb-32 text-primary-400"
alt="Crater Logo"
/>
</a>
<router-view></router-view>
<div
class="pt-24 mt-0 text-sm not-italic font-medium leading-relaxed text-left text-gray-500 md:pt-40"
>
<p class="mb-3">{{ $t('layout_login.copyright_crater') }}</p>
</div>
</div>
<div class="content-box">
<h1>{{ $t('layout_login.super_simple_invoicing') }}<br>
{{ $t('layout_login.for_freelancer') }}<br>
{{ $t('layout_login.small_businesses') }} <br>
</div>
<div
class="relative flex-col items-center justify-center hidden w-full h-full pl-10 bg-no-repeat bg-cover md:col-span-6 lg:col-span-8 md:flex content-box"
>
<div class="pl-20 xl:pl-0">
<h1
class="hidden mb-3 text-3xl font-bold leading-normal text-white xl:text-5xl xl:leading-tight md:none lg:block"
>
{{ $t('layout_login.super_simple_invoicing') }} <br />
{{ $t('layout_login.for_freelancer') }} <br />
{{ $t('layout_login.small_businesses') }} <br />
</h1>
<p>
{{ $t('layout_login.crater_help') }}<br>
{{ $t('layout_login.invoices_and_estimates') }}<br>
<p
class="hidden text-sm not-italic font-normal leading-normal text-gray-100 xl:text-base xl:leading-6 md:none lg:block"
>
{{ $t('layout_login.crater_help') }}<br />
{{ $t('layout_login.invoices_and_estimates') }}<br />
</p>
<div class="content-bottom"/>
</div>
<div class="absolute z-50 w-full bg-no-repeat content-bottom" />
</div>
</div>
</template>
<script type="text/babel">
export default {
watch: {
$route: 'onRouteChange'
},
mounted () {
this.setLayoutClasses()
},
methods: {
setLayoutClasses () {
let body = $('body')
body.removeClass()
body.addClass('login-page login-1 skin-crater')
}
},
onRouteChange () {
$('body').removeClass('login-page')
}
<style lang="scss" scoped>
.content-box {
background-image: url('/images/login-vector1.svg');
}
</script>
.content-bottom {
background-image: url('/images/login-vector3.svg');
background-size: 100% 100%;
height: 300px;
right: 32%;
bottom: 0;
}
.content-box::before {
background-image: url('/images/frame.svg');
content: '';
background-size: 100% 100%;
background-repeat: no-repeat;
height: 300px;
right: 0;
position: absolute;
top: 0;
width: 420px;
z-index: 1;
}
.content-box::after {
background-image: url('/images/login-vector2.svg');
content: '';
background-size: cover;
background-repeat: no-repeat;
height: 100%;
width: 100%;
right: 7.5%;
position: absolute;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div class="site-wrapper">
<div class="container">
<div class="site-wrapper h-full text-base">
<div class="container mx-auto">
<router-view></router-view>
</div>
</div>
@@ -9,19 +9,7 @@
<script type="text/babel">
export default {
watch: {
$route: 'onRouteChange'
$route: 'onRouteChange',
},
mounted () {
this.setLayoutBackground()
},
destroyed () {
document.body.style.backgroundColor = '#EBF1FA'
},
methods: {
setLayoutBackground () {
document.body.style.backgroundColor = '#f9fbff'
}
}
}
</script>

View File

@@ -1,21 +1,15 @@
<template>
<footer class="site-footer">
<div class="text-right">
{{ $t('general.powered_by') }}
<a
href="http://bytefury.com/"
target="_blank">{{ $t('general.bytefury') }}
</a>
</div>
<footer
class="absolute bottom-0 flex items-center justify-end w-full h-10 py-2 pr-8 text-sm font-normal text-gray-700 bg-white"
>
{{ $t('general.powered_by') }}
<a
href="http://bytefury.com/"
target="_blank"
class="pl-1 font-normal text-gray-900"
>
{{ $t('general.bytefury') }}
</a>
</footer>
</template>
<script type="text/babel">
export default {
data () {
return {
footer: 'footer'
}
}
}
</script>

View File

@@ -1,116 +1,192 @@
<template>
<header class="site-header">
<a href="/" class="brand-main">
<header
class="fixed top-0 left-0 z-40 flex items-center justify-between w-full px-4 py-3 md:h-16 md:px-8 bg-gradient-to-r from-primary-500 to-primary-400"
>
<a
href="/admin/dashboard"
class="float-none text-lg not-italic font-black tracking-wider text-white brand-main md:float-left font-base"
>
<img
id="logo-white"
src="/assets/img/logo-white.png"
alt="Crater Logo"
class="d-none d-md-inline"
>
class="hidden h-6 md:block"
/>
<img
id="logo-mobile"
src="/assets/img/crater-white-small.png"
alt="Laraspace Logo"
class="d-md-none">
alt="Crater Logo"
class="block h-8 md:hidden"
/>
</a>
<a
href="#"
class="nav-toggle"
@click="onNavToggle"
>
<div class="hamburger hamburger--arrowturn">
<div class="hamburger-box">
<div class="hamburger-inner"/>
<ul class="float-right h-8 m-0 list-none md:h-9">
<global-search class="hidden float-left mr-2 md:block" />
<a
:class="{ 'is-active': isSidebarOpen }"
href="#"
class="flex float-left p-1 ml-3 overflow-visible text-sm text-black ease-linear bg-white border-0 rounded cursor-pointer md:hidden md:ml-0 hamburger hamburger--arrowturn"
@click="toggleSidebar"
>
<div class="relative inline-block w-6 h-6">
<div class="block hamburger-inner top-1/2" />
</div>
</div>
</a>
<ul class="action-list">
<li>
<v-dropdown :show-arrow="false">
<a slot="activator" href="#">
<font-awesome-icon icon="plus" />
</a>
<li class="relative hidden float-left m-0 md:block">
<sw-dropdown>
<a
slot="activator"
href="#"
style="padding: 6px"
class="inline-block text-sm text-black bg-white rounded-sm"
>
<plus-icon class="w-6 h-6" />
</a>
<v-dropdown-item>
<router-link class="dropdown-item" to="/admin/invoices/create">
<font-awesome-icon icon="file-alt" class="dropdown-item-icon" /> <span> {{ $t('invoices.new_invoice') }} </span>
</router-link>
</v-dropdown-item>
<v-dropdown-item>
<router-link class="dropdown-item" to="/admin/estimates/create">
<font-awesome-icon class="dropdown-item-icon" icon="file" /> <span> {{ $t('estimates.new_estimate') }} </span>
</router-link>
</v-dropdown-item>
<v-dropdown-item>
<router-link class="dropdown-item" to="/admin/customers/create">
<font-awesome-icon class="dropdown-item-icon" icon="user" /> <span> {{ $t('customers.new_customer') }} </span>
</router-link>
</v-dropdown-item>
</v-dropdown>
<sw-dropdown-item tag-name="router-link" to="/admin/invoices/create">
<document-text-icon class="h-5 mr-2 text-gray-600" />
{{ $t('invoices.new_invoice') }}
</sw-dropdown-item>
<sw-dropdown-item tag-name="router-link" to="/admin/estimates/create">
<document-icon class="h-5 mr-2 text-gray-600" />
{{ $t('estimates.new_estimate') }}
</sw-dropdown-item>
<sw-dropdown-item tag-name="router-link" to="/admin/customers/create">
<user-icon class="h-5 mr-2 text-gray-600" />
{{ $t('customers.new_customer') }}
</sw-dropdown-item>
</sw-dropdown>
</li>
<li>
<v-dropdown :show-arrow="false">
<li class="relative block float-left ml-2">
<sw-dropdown>
<a
slot="activator"
href="#"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
class="avatar"
class="inline-block text-sm text-black bg-white rounded-sm avatar"
>
<img :src="profilePicture" alt="Avatar">
<img
:src="profilePicture"
alt="Avatar"
class="w-8 h-8 rounded-sm md:h-9 md:w-9"
/>
</a>
<v-dropdown-item>
<router-link class="dropdown-item" to="/admin/settings">
<font-awesome-icon icon="cogs" class="dropdown-item-icon"/> <span> {{ $t('navigation.settings') }} </span>
</router-link>
</v-dropdown-item>
<v-dropdown-item>
<a
href="#"
class="dropdown-item"
@click.prevent="logout"
>
<font-awesome-icon icon="sign-out-alt" class="dropdown-item-icon"/> <span> {{ $t('navigation.logout') }} </span>
</a>
</v-dropdown-item>
</v-dropdown>
<sw-dropdown-item tag-name="router-link" to="/admin/settings">
<cog-icon class="w-4 h-4 mr-2 text-gray-600" />
{{ $t('navigation.settings') }}
</sw-dropdown-item>
<sw-dropdown-item @click="logout">
<logout-icon class="w-4 h-4 mr-2 text-gray-600" />
{{ $t('navigation.logout') }}
</sw-dropdown-item>
</sw-dropdown>
</li>
</ul>
</header>
</template>
<script type="text/babel">
import { mapGetters, mapActions } from 'vuex'
import {
PlusIcon,
DocumentTextIcon,
DocumentIcon,
UserIcon,
CogIcon,
} from '@vue-hero-icons/solid'
import { LogoutIcon } from '@vue-hero-icons/outline'
export default {
components: {
PlusIcon,
DocumentTextIcon,
DocumentIcon,
UserIcon,
CogIcon,
LogoutIcon,
},
computed: {
...mapGetters('userProfile', [
'user'
]),
profilePicture () {
if (this.user && this.user.avatar !== null) {
return this.user.avatar
...mapGetters('user', ['currentUser']),
...mapGetters(['isSidebarOpen']),
profilePicture() {
if (
this.currentUser &&
this.currentUser.avatar !== null &&
this.currentUser.avatar !== 0
) {
return this.currentUser.avatar
} else {
return '/images/default-avatar.jpg'
}
}
},
},
created () {
this.loadData()
created() {
this.fetchCurrentUser()
},
methods: {
...mapActions('userProfile', [
'loadData'
]),
...mapActions({
companySelect: 'changeCompany'
}),
...mapActions('auth', [
'logout'
]),
onNavToggle () {
this.$utils.toggleSidebar()
}
}
...mapActions('user', ['fetchCurrentUser']),
...mapActions('auth', ['logout']),
...mapActions('modal', ['openModal']),
...mapActions(['toggleSidebar']),
},
}
</script>
<style lang="scss">
.hamburger {
transition-property: opacity, filter;
transition-duration: 0.15s;
}
.hamburger-inner {
top: 50%;
left: 4.5px;
right: 4.5px;
}
.hamburger-inner,
.hamburger-inner::before,
.hamburger-inner::after {
height: 2px;
background-color: black;
border-radius: 2px;
position: absolute;
transition-property: transform;
transition-duration: 0.15s;
transition-timing-function: ease;
}
.hamburger-inner::before,
.hamburger-inner::after {
content: '';
display: block;
width: 100%;
}
.hamburger-inner::before {
top: -5px;
}
.hamburger-inner::after {
bottom: -5px;
}
.hamburger--arrowturn.is-active .hamburger-inner {
transform: rotate(-180deg);
}
.hamburger--arrowturn.is-active .hamburger-inner::before {
transform: translate3d(5px, 3px, 0) rotate(45deg) scale(0.5, 1);
}
.hamburger--arrowturn.is-active .hamburger-inner::after {
transform: translate3d(5px, -3px, 0) rotate(-45deg) scale(0.5, 1);
}
</style>

View File

@@ -1,96 +1,184 @@
<template>
<div class="sidebar-left">
<div class="sidebar-body scroll-pane">
<div class="side-nav">
<div
v-for="(menuItems, index) in menu"
<div>
<!-- OVERLAY -->
<sw-transition type="fade">
<div
v-show="isSidebarOpen"
class="fixed top-0 left-0 z-20 w-full h-full"
style="background: rgba(48, 75, 88, 0.5)"
@click.prevent="toggleSidebar"
></div>
</sw-transition>
<!-- DESKTOP MENU -->
<div
class="hidden w-56 h-screen pb-32 overflow-y-auto bg-white border-r border-gray-200 border-solid xl:w-64 sw-scroll md:block"
>
<sw-list
v-for="(menuItems, groupIndex) in menuItems"
:key="groupIndex"
variant="sidebar"
>
<sw-list-item
v-for="(item, index) in menuItems"
:title="$t(item.title)"
:key="index"
class="menu-group"
:active="hasActiveUrl(item.route)"
:to="item.route"
tag-name="router-link"
>
<router-link
v-for="(item, index1) in menuItems"
:key="index1"
:to="item.route"
class="menu-item"
@click.native="Toggle"
>
<font-awesome-icon :icon="item.icon" class="icon menu-icon" /> <span class="ml-3 menu-text">{{ $t(item.title) }}</span>
</router-link>
</div>
</div>
<component slot="icon" :is="item.icon" class="h-5" />
</sw-list-item>
</sw-list>
</div>
<!-- MOBILE MENU -->
<transition
enter-class="-translate-x-full"
enter-active-class="transition duration-300 ease-in-out transform"
enter-to-class="translate-x-0"
leave-active-class="transition duration-300 ease-in-out transform"
leave-class="translate-x-0"
leave-to-class="-translate-x-full"
>
<div
v-show="isSidebarOpen"
class="fixed top-0 z-30 w-64 h-screen pt-16 pb-32 overflow-y-auto bg-white border-r border-gray-200 border-solid sw-scroll md:hidden"
>
<sw-list
v-for="(menuItems, groupIndex) in menuItems"
:key="groupIndex"
variant="sidebar"
>
<sw-list-item
v-for="(item, index) in menuItems"
:title="$t(item.title)"
:key="index"
:active="hasActiveUrl(item.route)"
:to="item.route"
tag-name="router-link"
@click.native="toggleSidebar"
>
<component slot="icon" :is="item.icon" class="h-5" />
</sw-list-item>
</sw-list>
</div>
</transition>
</div>
</template>
<script type="text/babel">
export default {
data () {
return {
sidebar: 'sidebar',
menu: [
import {
HomeIcon,
UserIcon,
StarIcon,
DocumentIcon,
DocumentTextIcon,
CreditCardIcon,
CalculatorIcon,
ChartBarIcon,
CogIcon,
UsersIcon,
} from '@vue-hero-icons/outline'
import { mapGetters, mapActions } from 'vuex'
export default {
components: {
HomeIcon,
UserIcon,
StarIcon,
DocumentIcon,
DocumentTextIcon,
CreditCardIcon,
CalculatorIcon,
ChartBarIcon,
CogIcon,
UsersIcon,
},
computed: {
...mapGetters(['isSidebarOpen']),
...mapGetters('user', ['currentUser']),
menuItems() {
let menu = [
[
{
title: 'navigation.dashboard',
icon: 'tachometer-alt',
route: '/admin/dashboard'
icon: 'home-icon',
route: '/admin/dashboard',
},
{
title: 'navigation.customers',
icon: 'user',
route: '/admin/customers'
icon: 'user-icon',
route: '/admin/customers',
},
{
title: 'navigation.items',
icon: 'star',
route: '/admin/items'
}
icon: 'star-icon',
route: '/admin/items',
},
],
[
{
title: 'navigation.estimates',
icon: 'file',
route: '/admin/estimates'
icon: 'document-icon',
route: '/admin/estimates',
},
{
title: 'navigation.invoices',
icon: 'file-alt',
route: '/admin/invoices'
icon: 'document-text-icon',
route: '/admin/invoices',
},
{
title: 'navigation.payments',
icon: 'credit-card',
route: '/admin/payments'
icon: 'credit-card-icon',
route: '/admin/payments',
},
{
title: 'navigation.expenses',
icon: 'space-shuttle',
route: '/admin/expenses'
}
icon: 'calculator-icon',
route: '/admin/expenses',
},
],
[
{
title: 'navigation.reports',
icon: 'signal',
route: '/admin/reports'
icon: 'chart-bar-icon',
route: '/admin/reports',
},
{
title: 'navigation.settings',
icon: 'cog',
route: '/admin/settings'
}
]
icon: 'cog-icon',
route: '/admin/settings',
},
],
]
}
if (this.currentUser.role == 'super admin') {
menu[2] = [
{
title: 'navigation.users',
icon: 'users-icon',
route: '/admin/users',
},
...menu[2],
]
}
return menu
},
},
methods: {
Toggle () {
this.$utils.toggleSidebar()
}
}
...mapActions(['toggleSidebar']),
hasActiveUrl(url) {
this.isActive = true
return this.$route.path.indexOf(url) > -1
},
hasStaticUrl(url) {
return this.$route.path.indexOf(url)
},
},
}
</script>

View File

@@ -1,262 +1,375 @@
<template>
<div class="payment-create main-content">
<base-page class="relative payment-create">
<form action="" @submit.prevent="submitPaymentData">
<div class="page-header">
<h3 class="page-title">{{ isEdit ? $t('payments.edit_payment') : $t('payments.new_payment') }}</h3>
<ol class="breadcrumb">
<li class="breadcrumb-item"><router-link slot="item-title" to="/admin/dashboard">{{ $t('general.home') }}</router-link></li>
<li class="breadcrumb-item"><router-link slot="item-title" to="/admin/payments">{{ $tc('payments.payment', 2) }}</router-link></li>
<li class="breadcrumb-item">{{ isEdit ? $t('payments.edit_payment') : $t('payments.new_payment') }}</li>
</ol>
<div class="page-actions header-button-container">
<base-button
<sw-page-header class="mb-5" :title="pageTitle">
<sw-breadcrumb slot="breadcrumbs">
<sw-breadcrumb-item
to="/admin/dashboard"
:title="$t('general.home')"
/>
<sw-breadcrumb-item
to="/admin/payments"
:title="$tc('payments.payment', 2)"
/>
<sw-breadcrumb-item
v-if="$route.name === 'payments.edit'"
to="#"
:title="$t('payments.edit_payment')"
active
/>
<sw-breadcrumb-item
v-else
to="#"
:title="$t('payments.new_payment')"
active
/>
</sw-breadcrumb>
<template slot="actions">
<sw-button
:loading="isLoading"
:disabled="isLoading"
icon="save"
color="theme"
type="submit">
{{ isEdit ? $t('payments.update_payment') : $t('payments.save_payment') }}
</base-button>
</div>
</div>
<div class="payment-card card">
<div class="card-body">
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label class="form-label">{{ $t('payments.date') }}</label><span class="text-danger"> *</span>
<base-date-picker
v-model="formData.payment_date"
:invalid="$v.formData.payment_date.$error"
:calendar-button="true"
calendar-button-icon="calendar"
@change="$v.formData.payment_date.$touch()"
/>
<div v-if="$v.formData.payment_date.$error">
<span v-if="!$v.formData.payment_date.required" class="text-danger">{{ $t('validation.required') }}</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="form-label">{{ $t('payments.payment_number') }}</label><span class="text-danger"> *</span>
<base-prefix-input
:invalid="$v.paymentNumAttribute.$error"
v-model.trim="paymentNumAttribute"
:prefix="paymentPrefix"
@input="$v.paymentNumAttribute.$touch()"
/>
<div v-if="$v.paymentNumAttribute.$error">
<span v-if="!$v.paymentNumAttribute.required" class="text-danger">{{ $tc('validation.required') }}</span>
<span v-if="!$v.paymentNumAttribute.numeric" class="text-danger">{{ $tc('validation.numbers_only') }}</span>
</div>
</div>
</div>
<div class="col-sm-6">
<label class="form-label">{{ $t('payments.customer') }}</label><span class="text-danger"> *</span>
<base-select
ref="baseSelect"
v-model="customer"
:invalid="$v.customer.$error"
:options="customerList"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:disabled="isEdit"
:placeholder="$t('customers.select_a_customer')"
label="name"
track-by="id"
variant="primary"
type="submit"
size="lg"
class="hidden sm:flex"
>
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
{{
isEdit
? $t('payments.update_payment')
: $t('payments.save_payment')
}}
</sw-button>
</template>
</sw-page-header>
<base-loader v-if="isRequestOnGoing" :show-bg-overlay="true" />
<sw-card v-else>
<div class="grid gap-6 grid-col-1 md:grid-cols-2">
<sw-input-group
:label="$t('payments.date')"
:error="DateError"
required
>
<base-date-picker
v-model="formData.payment_date"
:invalid="$v.formData.payment_date.$error"
:calendar-button="true"
class="mt-1"
calendar-button-icon="calendar"
@change="$v.formData.payment_date.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('payments.payment_number')"
:error="paymentNumError"
required
>
<sw-input
:prefix="`${paymentPrefix} - `"
:invalid="$v.paymentNumAttribute.$error"
v-model.trim="paymentNumAttribute"
class="mt-1"
@input="$v.paymentNumAttribute.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('payments.customer')"
:error="customerError"
required
>
<sw-select
v-model="customer"
:options="customers"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:disabled="isEdit"
:placeholder="$t('customers.select_a_customer')"
label="name"
class="mt-1"
track-by="id"
/>
</sw-input-group>
<sw-input-group :label="$t('payments.invoice')">
<sw-select
v-model="invoice"
:options="invoiceList"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:disabled="isEdit"
:placeholder="$t('invoices.select_invoice')"
:custom-label="invoiceWithAmount"
class="mt-1"
track-by="invoice_number"
/>
</sw-input-group>
<sw-input-group
:label="$t('payments.amount')"
:error="amountError"
required
>
<div class="relative w-full mt-1">
<sw-money
v-model="amount"
:currency="customerCurrency"
:invalid="$v.formData.amount.$error"
class="relative w-full focus:border focus:border-solid focus:border-primary-500"
@input="$v.formData.amount.$touch()"
/>
<div v-if="$v.customer.$error">
<span v-if="!$v.customer.required" class="text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="form-label">{{ $t('payments.invoice') }}</label>
<base-select
v-model="invoice"
:options="invoiceList"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:disabled="isEdit"
:placeholder="$t('invoices.select_invoice')"
:custom-label="invoiceWithAmount"
track-by="invoice_number"
/>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="form-label">{{ $t('payments.amount') }}</label><span class="text-danger"> *</span>
<div class="base-input">
<money
:class="{'invalid' : $v.formData.amount.$error}"
v-model="amount"
v-bind="customerCurrency"
class="input-field"
/>
</div>
<div v-if="$v.formData.amount.$error">
<span v-if="!$v.formData.amount.required" class="text-danger">{{ $t('validation.required') }}</span>
<span v-if="!$v.formData.amount.between && $v.formData.amount.numeric && amount <= 0" class="text-danger">{{ $t('validation.payment_greater_than_zero') }}</span>
<span v-if="!$v.formData.amount.between && amount > 0" class="text-danger">{{ $t('validation.payment_greater_than_due_amount') }}</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="form-label">{{ $t('payments.payment_mode') }}</label>
<base-select
v-model="formData.payment_method"
:options="paymentModes"
:searchable="true"
:show-labels="false"
:placeholder="$t('payments.select_payment_mode')"
label="name"
</sw-input-group>
<sw-input-group :label="$t('payments.payment_mode')">
<sw-select
v-model="formData.payment_method"
:options="paymentModes"
:searchable="true"
:show-labels="false"
:placeholder="$t('payments.select_payment_mode')"
label="name"
:maxHeight="150"
class="mt-1"
>
<div slot="afterList">
<button
type="button"
class="flex items-center justify-center w-full px-2 py-2 bg-gray-200 border-none outline-none text-primary-400"
@click="addPaymentMode"
>
<div slot="afterList">
<button type="button" class="list-add-button" @click="addPaymentMode">
<font-awesome-icon class="icon" icon="cart-plus" />
<label>{{ $t('settings.customization.payments.add_payment_mode') }}</label>
</button>
</div>
</base-select>
<shopping-cart-icon class="h-5 mr-3 text-primary-400" />
<label>{{
$t('settings.customization.payments.add_payment_mode')
}}</label>
</button>
</div>
</div>
<div class="col-sm-12 ">
<div class="form-group">
<label class="form-label">{{ $t('payments.note') }}</label>
<base-text-area
v-model="formData.notes"
@input="$v.formData.notes.$touch()"
/>
<div v-if="$v.formData.notes.$error">
<span v-if="!$v.formData.notes.maxLength" class="text-danger">{{ $t('validation.notes_maxlength') }}</span>
</div>
</div>
</div>
<div class="col-sm-12">
<div class="form-group collapse-button-container">
<base-button
:loading="isLoading"
icon="save"
color="theme"
type="submit"
class="collapse-button"
>
{{ $t('payments.save_payment') }}
</base-button>
</div>
</div>
</sw-select>
</sw-input-group>
</div>
<div v-if="customFields.length > 0">
<div class="grid gap-6 mt-6 grid-col-1 md:grid-cols-2">
<sw-input-group
v-for="(field, index) in customFields"
:label="field.label"
:required="field.is_required ? true : false"
:key="index"
>
<component
:type="field.type.label"
:field="field"
:isEdit="isEdit"
:is="field.type + 'Field'"
:invalid-fields="invalidFields"
@update="setCustomFieldValue"
/>
</sw-input-group>
</div>
</div>
</div>
<sw-popup
ref="notePopup"
class="my-6 text-sm font-semibold leading-5 text-primary-400"
>
<div slot="activator" class="float-right mt-1">
+ {{ $t('general.insert_note') }}
</div>
<note-select-popup type="Payment" @select="onSelectNote" />
</sw-popup>
<sw-input-group :label="$t('payments.note')" class="mt-6 mb-4">
<base-custom-input
v-model="formData.notes"
:fields="PaymentFields"
class="mb-4"
/>
</sw-input-group>
<sw-button
:disabled="isLoading"
:loading="isLoading"
variant="primary"
type="submit"
class="flex w-full mt-4 sm:hidden md:hidden"
>
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
{{
isEdit ? $t('payments.update_payment') : $t('payments.save_payment')
}}
</sw-button>
</sw-card>
</form>
</div>
</base-page>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import MultiSelect from 'vue-multiselect'
import { validationMixin } from 'vuelidate'
import moment from 'moment'
const { required, between, maxLength, numeric } = require('vuelidate/lib/validators')
import { ShoppingCartIcon } from '@vue-hero-icons/solid'
import CustomFieldsMixin from '../../mixins/customFields'
const { required, between, numeric } = require('vuelidate/lib/validators')
export default {
components: { MultiSelect },
mixins: [validationMixin],
data () {
mixins: [CustomFieldsMixin],
components: { ShoppingCartIcon },
data() {
return {
formData: {
user_id: null,
payment_number: null,
payment_date: null,
amount: 0,
payment_date: new Date(),
amount: 100,
payment_method: null,
invoice_id: null,
notes: null,
payment_method_id: null
payment_method_id: null,
},
money: {
decimal: '.',
thousands: ',',
prefix: '$ ',
precision: 2,
masked: false
masked: false,
},
customer: null,
invoice: null,
customerList: [],
invoiceList: [],
isLoading: false,
isRequestOnGoing: false,
maxPayableAmount: Number.MAX_SAFE_INTEGER,
isSettingInitialData: true,
paymentNumAttribute: null,
paymentPrefix: ''
paymentPrefix: '',
PaymentFields: [
'customer',
'company',
'customerCustom',
'payment',
'paymentCustom',
],
}
},
validations () {
validations() {
return {
customer: {
required
required,
},
formData: {
payment_date: {
required
required,
},
amount: {
required,
between: between(1, this.maxPayableAmount + 1)
between: between(1, this.maxPayableAmount + 1),
},
notes: {
maxLength: maxLength(255)
}
},
paymentNumAttribute: {
required,
numeric
}
numeric,
},
}
},
computed: {
...mapGetters('currency', [
'defaultCurrencyForInput'
]),
...mapGetters('payment', [
'paymentModes'
]),
...mapGetters('company', ['defaultCurrencyForInput']),
...mapGetters('payment', ['paymentModes', 'selectedNote']),
...mapGetters('customer', ['customers']),
amount: {
get: function () {
return this.formData.amount / 100
},
set: function (newValue) {
this.formData.amount = newValue * 100
}
},
},
isEdit () {
pageTitle() {
if (this.$route.name === 'payments.edit') {
return this.$t('payments.edit_payment')
}
return this.$t('payments.new_payment')
},
isEdit() {
if (this.$route.name === 'payments.edit') {
return true
}
return false
},
customerCurrency () {
customerCurrency() {
if (this.customer && this.customer.currency) {
return {
decimal: this.customer.currency.decimal_separator,
thousands: this.customer.currency.thousand_separator,
prefix: this.customer.currency.symbol + ' ',
precision: this.customer.currency.precision,
masked: false
masked: false,
}
} else {
return this.defaultCurrencyForInput
}
}
},
customerError() {
if (!this.$v.customer.$error) {
return ''
}
if (!this.$v.customer.required) {
return this.$tc('validation.required')
}
},
DateError() {
if (!this.$v.formData.payment_date.$error) {
return ''
}
if (!this.$v.formData.payment_date.required) {
return this.$t('validation.required')
}
},
amountError() {
if (!this.$v.formData.amount.$error) {
return ''
}
if (!this.$v.formData.amount.required) {
return this.$t('validation.required')
}
if (
!this.$v.formData.amount.between &&
this.$v.formData.amount.numeric &&
this.amount <= 0
) {
return this.$t('validation.payment_greater_than_zero')
}
if (!this.$v.formData.amount.between && this.amount > 0) {
return this.$t('validation.payment_greater_than_due_amount')
}
},
paymentNumError() {
if (!this.$v.paymentNumAttribute.$error) {
return ''
}
if (!this.$v.paymentNumAttribute.required) {
return this.$tc('validation.required')
}
if (!this.$v.paymentNumAttribute.numeric) {
return this.$tc('validation.numbers_only')
}
},
},
watch: {
customer (newValue) {
customer(newValue) {
this.formData.user_id = newValue.id
if (!this.isEdit) {
if (this.isSettingInitialData) {
@@ -270,16 +383,23 @@ export default {
this.fetchCustomerInvoices(newValue.id)
}
},
invoice (newValue) {
selectedNote() {
if (this.selectedNote) {
this.formData.notes = this.selectedNote
}
},
invoice(newValue) {
if (newValue) {
this.formData.invoice_id = newValue.id
if (!this.isEdit) {
this.setPaymentAmountByInvoiceData(newValue.id)
}
}
}
},
},
async mounted () {
async mounted() {
this.$v.formData.$reset()
this.resetSelectedNote()
this.$nextTick(() => {
this.loadData()
if (this.$route.params.id && !this.isEdit) {
@@ -288,91 +408,159 @@ export default {
})
},
methods: {
...mapActions('invoice', [
'fetchInvoice'
]),
...mapActions('invoice', ['fetchInvoice', 'fetchInvoices']),
...mapActions('payment', [
'fetchCreatePayment',
'addPayment',
'updatePayment',
'fetchEditPaymentData'
'fetchPayment',
'fetchPaymentModes',
'resetSelectedNote',
]),
...mapActions('modal', [
'openModal'
]),
invoiceWithAmount ({ invoice_number, due_amount }) {
return `${invoice_number} (${this.$utils.formatGraphMoney(due_amount, this.customer.currency)})`
...mapActions('company', ['fetchCompanySettings']),
...mapActions('modal', ['openModal']),
...mapActions('customer', ['fetchCustomers']),
invoiceWithAmount({ invoice_number, due_amount }) {
return `${invoice_number} (${this.$utils.formatGraphMoney(
due_amount,
this.customer.currency
)})`
},
async addPaymentMode () {
async addPaymentMode() {
this.openModal({
'title': this.$t('settings.customization.payments.add_payment_mode'),
'componentName': 'PaymentMode'
title: this.$t('settings.customization.payments.add_payment_mode'),
componentName: 'PaymentMode',
})
},
async loadData () {
async checkAutoGenerate() {
let response = await this.fetchCompanySettings(['payment_auto_generate'])
let response1 = await axios.get('/api/v1/next-number?key=payment')
if (response.data && response.data.payment_auto_generate === 'YES') {
if (response1.data) {
this.paymentNumAttribute = response1.data.nextNumber
this.paymentPrefix = response1.data.prefix
return true
}
} else {
this.paymentPrefix = response1.data.prefix
}
},
async loadData() {
if (this.isEdit) {
let response = await this.fetchEditPaymentData(this.$route.params.id)
this.customerList = response.data.customers
this.formData = { ...response.data.payment }
this.isRequestOnGoing = true
let response = await this.fetchPayment(this.$route.params.id)
this.formData = { ...this.formData, ...response.data.payment }
this.customer = response.data.payment.user
this.formData.payment_date = moment(response.data.payment.payment_date, 'YYYY-MM-DD').toString()
this.formData.payment_date = moment(
response.data.payment.payment_date,
'YYYY-MM-DD'
).toString()
this.formData.amount = parseFloat(response.data.payment.amount)
this.paymentPrefix = response.data.payment_prefix
this.paymentNumAttribute = response.data.nextPaymentNumber
this.formData.payment_method = response.data.payment.payment_method
if (response.data.payment.invoice !== null) {
this.maxPayableAmount = parseInt(response.data.payment.amount) + parseInt(response.data.payment.invoice.due_amount)
this.maxPayableAmount =
parseInt(response.data.payment.amount) +
parseInt(response.data.payment.invoice.due_amount)
this.invoice = response.data.payment.invoice
}
// this.fetchCustomerInvoices(this.customer.id)
let res = await this.fetchCustomFields({
type: 'Payment',
limit: 'all',
})
this.setEditCustomFields(
response.data.payment.fields,
res.data.customFields.data
)
if (this.formData.payment_method_id) {
await this.fetchPaymentModes({ limit: 'all' })
}
if (this.formData.user_id) {
await this.fetchCustomers({ limit: 'all' })
}
this.isRequestOnGoing = false
} else {
let response = await this.fetchCreatePayment()
this.customerList = response.data.customers
this.paymentNumAttribute = response.data.nextPaymentNumberAttribute
this.paymentPrefix = response.data.payment_prefix
this.formData.payment_date = moment(new Date()).toString()
this.isRequestOnGoing = true
this.checkAutoGenerate()
this.setInitialCustomFields('Payment')
this.formData.payment_date = moment().toString()
this.fetchPaymentModes({ limit: 'all' })
await this.fetchCustomers({ limit: 'all' })
if (this.$route.query.customer) {
this.setPaymentCustomer(parseInt(this.$route.query.customer))
}
this.isRequestOnGoing = false
}
return true
},
async setInvoicePaymentData () {
setPaymentCustomer(id) {
this.customer = this.customers.find((c) => {
return c.id === id
})
},
async setInvoicePaymentData() {
let data = await this.fetchInvoice(this.$route.params.id)
this.customer = data.data.invoice.user
this.invoice = data.data.invoice
},
async setPaymentAmountByInvoiceData (id) {
async setPaymentAmountByInvoiceData(id) {
let data = await this.fetchInvoice(id)
this.formData.amount = data.data.invoice.due_amount
this.maxPayableAmount = data.data.invoice.due_amount
},
async fetchCustomerInvoices (userID) {
let response = await axios.get(`/api/invoices/unpaid/${userID}`)
if (response.data) {
this.invoiceList = response.data.invoices
async fetchCustomerInvoices(userId) {
let data = {
customer_id: userId,
status: 'UNPAID',
}
let response = await this.fetchInvoices(data)
this.invoiceList = response.data.invoices.data
},
async submitPaymentData () {
async submitPaymentData() {
let validate = await this.touchCustomField()
this.$v.customer.$touch()
this.$v.formData.$touch()
if (this.$v.$invalid) {
if (this.$v.$invalid || validate.error) {
return true
}
this.formData.payment_number = this.paymentPrefix + '-' + this.paymentNumAttribute
this.formData.payment_number =
this.paymentPrefix + '-' + this.paymentNumAttribute
if (this.isEdit) {
let data = {
editData: {
...this.formData,
payment_method_id: this.formData.payment_method ? this.formData.payment_method.id : null,
payment_date: moment(this.formData.payment_date).format('DD/MM/YYYY')
payment_method_id: this.formData.payment_method
? this.formData.payment_method.id
: null,
payment_date: moment(this.formData.payment_date).format(
'YYYY-MM-DD'
),
},
id: this.$route.params.id
id: this.$route.params.id,
}
try {
this.isLoading = true
let response = await this.updatePayment(data)
if (response.data.success) {
this.isLoading = false
this.$router.push(
`/admin/payments/${response.data.payment.id}/view`
)
window.toastr['success'](this.$t('payments.updated_message'))
this.$router.push('/admin/payments')
return true
}
if (response.data.error === 'invalid_amount') {
@@ -391,15 +579,19 @@ export default {
} else {
let data = {
...this.formData,
payment_method_id: this.formData.payment_method ? this.formData.payment_method.id : null,
payment_date: moment(this.formData.payment_date).format('DD/MM/YYYY')
payment_method_id: this.formData.payment_method
? this.formData.payment_method.id
: null,
payment_date: moment(this.formData.payment_date).format('YYYY-MM-DD'),
}
this.isLoading = true
try {
let response = await this.addPayment(data)
if (response.data.success) {
this.$router.push(
`/admin/payments/${response.data.payment.id}/view`
)
window.toastr['success'](this.$t('payments.created_message'))
this.$router.push('/admin/payments')
this.isLoading = true
return true
}
@@ -417,7 +609,11 @@ export default {
window.toastr['error'](err.response.data.message)
}
}
}
}
},
onSelectNote(data) {
this.formData.notes = '' + data.notes
this.$refs.notePopup.close()
},
},
}
</script>

View File

@@ -1,306 +1,365 @@
<template>
<div class="payments main-content">
<div class="page-header">
<h3 class="page-title">{{ $t('payments.title') }}</h3>
<ol class="breadcrumb">
<li class="breadcrumb-item">
<router-link
slot="item-title"
to="dashboard">
{{ $t('general.home') }}
</router-link>
</li>
<li class="breadcrumb-item">
<router-link
slot="item-title"
to="#">
{{ $tc('payments.payment',2) }}
</router-link>
</li>
</ol>
<div class="page-actions row">
<div class="col-xs-2 mr-4">
<base-button
v-show="totalPayments || filtersApplied"
:outline="true"
:icon="filterIcon"
color="theme"
right-icon
size="large"
@click="toggleFilter"
>
{{ $t('general.filter') }}
</base-button>
</div>
<router-link slot="item-title" class="col-xs-2" to="payments/create">
<base-button
color="theme"
icon="plus"
size="large"
>
{{ $t('payments.add_payment') }}
</base-button>
</router-link>
</div>
</div>
<base-page class="payments">
<sw-page-header :title="$t('payments.title')">
<sw-breadcrumb slot="breadcrumbs">
<sw-breadcrumb-item to="dashboard" :title="$t('general.home')" />
<sw-breadcrumb-item to="#" :title="$tc('payments.payment', 2)" active />
</sw-breadcrumb>
<transition name="fade" mode="out-in">
<div v-show="showFilters" class="filter-section">
<div class="row">
<div class="col-md-4">
<label class="form-label">{{ $t('payments.customer') }}</label>
<base-customer-select
ref="customerSelect"
@select="onSelectCustomer"
@deselect="clearCustomerSearch"
/>
</div>
<div class="col-sm-4">
<label for="">{{ $t('payments.payment_number') }}</label>
<base-input
v-model="filters.payment_number"
:placeholder="$t(payments.payment_number)"
name="payment_number"
/>
</div>
<div class="col-sm-4">
<label class="form-label">{{ $t('payments.payment_mode') }}</label>
<base-select
v-model="filters.payment_mode"
:options="paymentModes"
:searchable="true"
:show-labels="false"
:placeholder="$t('payments.payment_mode')"
label="name"
/>
</div>
</div>
<label class="clear-filter" @click="clearFilter">{{ $t('general.clear_all') }}</label>
</div>
</transition>
<div v-cloak v-show="showEmptyScreen" class="col-xs-1 no-data-info" align="center">
<capsule-icon class="mt-5 mb-4"/>
<div class="row" align="center">
<label class="col title">{{ $t('payments.no_payments') }}</label>
</div>
<div class="row">
<label class="description col mt-1" align="center">{{ $t('payments.list_of_payments') }}</label>
</div>
<div class="btn-container">
<base-button
:outline="true"
color="theme"
class="mt-3"
size="large"
@click="$router.push('payments/create')"
<template slot="actions">
<sw-button
v-show="totalPayments"
variant="primary-outline"
size="lg"
@click="toggleFilter"
>
{{ $t('payments.add_new_payment') }}
</base-button>
</div>
</div>
{{ $t('general.filter') }}
<component :is="filterIcon" class="w-4 h-4 ml-2 -mr-1" />
</sw-button>
<div v-show="!showEmptyScreen" class="table-container">
<div class="table-actions mt-5">
<p class="table-stats">{{ $t('general.showing') }}: <b>{{ payments.length }}</b> {{ $t('general.of') }} <b>{{ totalPayments }}</b></p>
<sw-button
tag-name="router-link"
to="payments/create"
variant="primary"
size="lg"
class="ml-4"
>
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('payments.add_payment') }}
</sw-button>
</template>
</sw-page-header>
<transition name="fade">
<v-dropdown v-if="selectedPayments.length" :show-arrow="false">
<span slot="activator" href="#" class="table-actions-button dropdown-toggle">
<slide-y-up-transition>
<sw-filter-wrapper v-show="showFilters" class="mt-3">
<sw-input-group
:label="$t('payments.customer')"
color="black-light"
class="flex-1 mt-2"
>
<base-customer-select
ref="customerSelect"
@select="onSelectCustomer"
@deselect="clearCustomerSearch"
/>
</sw-input-group>
<sw-input-group
:label="$t('payments.payment_number')"
class="flex-1 mt-2 lg:ml-6"
>
<sw-input
v-model="filters.payment_number"
:placeholder="$t(payments.payment_number)"
name="payment_number"
/>
</sw-input-group>
<sw-input-group
:label="$t('payments.payment_mode')"
class="flex-1 mt-2 lg:ml-6"
>
<sw-select
v-model="filters.payment_mode"
:options="paymentModes"
:searchable="true"
:show-labels="false"
:placeholder="$t('payments.payment_mode')"
label="name"
/>
</sw-input-group>
<label
class="absolute text-sm leading-snug text-gray-900 cursor-pointer"
style="top: 10px; right: 15px"
@click="clearFilter"
>{{ $t('general.clear_all') }}</label
>
</sw-filter-wrapper>
</slide-y-up-transition>
<sw-empty-table-placeholder
v-if="showEmptyScreen"
:title="$t('payments.no_payments')"
:description="$t('payments.list_of_payments')"
>
<capsule-icon class="mt-5 mb-4" />
<sw-button
slot="actions"
tag-name="router-link"
to="/admin/payments/create"
size="lg"
variant="primary-outline"
>
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('payments.add_new_payment') }}
</sw-button>
</sw-empty-table-placeholder>
<div v-show="!showEmptyScreen" class="relative table-container">
<div
class="relative flex items-center justify-between h-10 mt-5 list-none border-b-2 border-gray-200 border-solid"
>
<p class="text-sm">
{{ $t('general.showing') }}: <b>{{ payments.length }}</b>
{{ $t('general.of') }} <b>{{ totalPayments }}</b>
</p>
<sw-transition type="fade">
<sw-dropdown v-if="selectedPayments.length">
<span
slot="activator"
class="flex block text-sm font-medium cursor-pointer select-none text-primary-400"
>
{{ $t('general.actions') }}
<chevron-down-icon class="h-5" />
</span>
<v-dropdown-item>
<div class="dropdown-item" @click="removeMultiplePayments">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
</transition>
<sw-dropdown-item @click="removeMultiplePayments">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</sw-transition>
</div>
<div class="custom-control custom-checkbox">
<input
id="select-all"
<div class="absolute z-10 items-center pl-4 mt-2 select-none md:mt-12">
<sw-checkbox
v-model="selectAllFieldStatus"
type="checkbox"
class="custom-control-input"
variant="primary"
size="sm"
class="hidden md:inline"
@change="selectAllPayments"
>
<label v-show="!isRequestOngoing" for="select-all" class="custom-control-label selectall">
<span class="select-all-label">{{ $t('general.select_all') }} </span>
</label>
/>
<sw-checkbox
v-model="selectAllFieldStatus"
:label="$t('general.select_all')"
variant="primary"
size="sm"
class="md:hidden"
@change="selectAllPayments"
/>
</div>
<table-component
<sw-table-component
ref="table"
:data="fetchData"
:show-filter="false"
table-class="table"
>
<table-column
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="no-click"
>
<template slot-scope="row">
<div class="custom-control custom-checkbox">
<input
:id="row.id"
v-model="selectField"
:value="row.id"
type="checkbox"
class="custom-control-input"
>
<label :for="row.id" class="custom-control-label" />
</div>
</template>
</table-column>
<table-column
<div slot-scope="row" class="relative block">
<sw-checkbox
:id="row.id"
v-model="selectField"
:value="row.id"
variant="primary"
size="sm"
/>
</div>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('payments.date')"
sort-as="payment_date"
show="formattedPaymentDate"
/>
<table-column
<sw-table-column
:sortable="true"
:label="$t('payments.payment_number')"
show="payment_number"
>
<template slot-scope="row">
<span>{{ $t('payments.payment_number') }}</span>
<router-link
:to="{ path: `payments/${row.id}/view` }"
class="font-medium text-primary-500"
>
{{ row.payment_number }}
</router-link>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('payments.customer')"
show="name"
/>
<table-column
<sw-table-column
:sortable="true"
:label="$t('payments.payment_mode')"
show="payment_mode"
/>
<table-column
:label="$t('payments.payment_number')"
show="payment_number"
/>
<table-column
>
<template slot-scope="row">
<span>{{ $t('payments.payment_mode') }}</span>
<span>
{{ row.payment_mode ? row.payment_mode : 'Not selected' }}
</span>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('payments.invoice')"
sort-as="invoice_id"
show="invoice.invoice_number"
/>
<table-column
:label="$t('payments.amount')"
show="invoice_number"
>
<template slot-scope="row">
<span>{{ $t('invoices.invoice_number') }}</span>
<span>
{{ row.invoice_number ? row.invoice_number : 'No Invoice' }}
</span>
</template>
</sw-table-column>
<sw-table-column :sortable="true" :label="$t('payments.amount')">
<template slot-scope="row">
<span>{{ $t('payments.amount') }}</span>
<div v-html="$utils.formatMoney(row.amount, row.user.currency)" />
</template>
</table-column>
<table-column
</sw-table-column>
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown no-click"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span>{{ $t('payments.action') }}</span>
<v-dropdown>
<a slot="activator" href="#">
<dot-icon />
</a>
<v-dropdown-item>
<sw-dropdown>
<dot-icon slot="activator" />
<router-link :to="{path: `payments/${row.id}/edit`}" class="dropdown-item">
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
{{ $t('general.edit') }}
</router-link>
<sw-dropdown-item
tag-name="router-link"
:to="`payments/${row.id}/edit`"
>
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
</v-dropdown-item>
<v-dropdown-item>
<sw-dropdown-item
tag-name="router-link"
:to="`payments/${row.id}/view`"
>
<eye-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.view') }}
</sw-dropdown-item>
<router-link :to="{path: `payments/${row.id}/view`}" class="dropdown-item">
<font-awesome-icon icon="eye" class="dropdown-item-icon" />
{{ $t('general.view') }}
</router-link>
</v-dropdown-item>
<v-dropdown-item>
<div class="dropdown-item" @click="removePayment(row.id)">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
<sw-dropdown-item @click="removePayment(row.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</table-column>
</table-component>
</sw-table-column>
</sw-table-component>
</div>
</div>
</base-page>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { SweetModal, SweetModalTab } from 'sweet-modal-vue'
import CapsuleIcon from '../../components/icon/CapsuleIcon'
import BaseButton from '../../../js/components/base/BaseButton'
import CapsuleIcon from '@/components/icon/CapsuleIcon'
import {
PlusIcon,
FilterIcon,
XIcon,
ChevronDownIcon,
EyeIcon,
PencilIcon,
TrashIcon,
} from '@vue-hero-icons/solid'
export default {
components: {
'capsule-icon': CapsuleIcon,
'SweetModal': SweetModal,
'SweetModalTab': SweetModalTab,
BaseButton
CapsuleIcon,
PlusIcon,
FilterIcon,
XIcon,
ChevronDownIcon,
EyeIcon,
PencilIcon,
TrashIcon,
},
data () {
data() {
return {
showFilters: false,
sortedBy: 'created_at',
filtersApplied: false,
isRequestOngoing: true,
filters: {
customer: null,
customer: '',
payment_mode: '',
payment_number: ''
}
payment_number: '',
},
}
},
computed: {
showEmptyScreen () {
return !this.totalPayments && !this.isRequestOngoing && !this.filtersApplied
showEmptyScreen() {
return !this.totalPayments && !this.isRequestOngoing
},
filterIcon () {
return (this.showFilters) ? 'times' : 'filter'
filterIcon() {
return this.showFilters ? 'x-icon' : 'filter-icon'
},
...mapGetters('customer', [
'customers'
]),
...mapGetters('customer', ['customers']),
...mapGetters('payment', [
'selectedPayments',
'totalPayments',
'payments',
'selectAllField',
'paymentModes'
'paymentModes',
]),
selectField: {
get: function () {
return this.selectedPayments
},
set: function (val) {
this.selectPayment(val)
}
},
},
selectAllFieldStatus: {
get: function () {
return this.selectAllField
},
set: function (val) {
this.setSelectAllState(val)
}
}
},
},
},
watch: {
filters: {
handler: 'setFilters',
deep: true
}
deep: true,
},
},
mounted () {
this.fetchCustomers()
mounted() {
this.fetchPaymentModes({ limit: 'all' })
},
destroyed () {
destroyed() {
if (this.selectAllField) {
this.selectAllPayments()
}
},
methods: {
...mapActions('payment', [
'fetchPayments',
@@ -308,25 +367,25 @@ export default {
'selectPayment',
'deletePayment',
'deleteMultiplePayments',
'setSelectAllState'
'setSelectAllState',
'fetchPaymentModes',
]),
...mapActions('customer', [
'fetchCustomers'
]),
async fetchData ({ page, filter, sort }) {
async fetchData({ page, filter, sort }) {
let data = {
customer_id: this.filters.customer !== null ? this.filters.customer.id : '',
customer_id: this.filters.customer ? this.filters.customer.id : '',
payment_method_id:
this.filters.payment_mode !== null
? this.filters.payment_mode.id
: '',
payment_number: this.filters.payment_number,
payment_method_id: this.filters.payment_mode ? this.filters.payment_mode.id : '',
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page
page,
}
this.isRequestOngoing = true
let response = await this.fetchPayments(data)
this.isRequestOngoing = false
return {
@@ -334,71 +393,75 @@ export default {
pagination: {
totalPages: response.data.payments.last_page,
currentPage: page,
count: response.data.payments.scount
}
count: response.data.payments.count,
},
}
},
refreshTable () {
refreshTable() {
this.$refs.table.refresh()
},
setFilters () {
this.filtersApplied = true
setFilters() {
this.refreshTable()
},
clearFilter () {
clearFilter() {
if (this.filters.customer) {
this.$refs.customerSelect.$refs.baseSelect.removeElement(this.filters.customer)
this.$refs.customerSelect.$refs.baseSelect.removeElement(
this.filters.customer
)
}
this.filters = {
customer: null,
customer: '',
payment_mode: '',
payment_number: ''
payment_number: '',
}
this.$nextTick(() => {
this.filtersApplied = false
})
},
toggleFilter () {
if (this.showFilters && this.filtersApplied) {
toggleFilter() {
if (this.showFilters) {
this.clearFilter()
this.refreshTable()
}
this.showFilters = !this.showFilters
},
onSelectCustomer (customer) {
onSelectCustomer(customer) {
this.filters.customer = customer
},
async removePayment (id) {
this.id = id
async removePayment(id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('payments.confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
dangerMode: true,
}).then(async (willDelete) => {
if (willDelete) {
let res = await this.deletePayment(this.id)
let res = await this.deletePayment({ ids: [id] })
if (res.data.success) {
window.toastr['success'](this.$tc('payments.deleted_message', 1))
this.$refs.table.refresh()
return true
} else if (res.data.error) {
window.toastr['error'](res.data.message)
}
window.toastr['error'](res.data.message)
return true
}
})
},
async removeMultiplePayments () {
async removeMultiplePayments() {
swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('payments.confirm_delete', 2),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
dangerMode: true,
}).then(async (willDelete) => {
if (willDelete) {
let request = await this.deleteMultiplePayments()
@@ -411,21 +474,24 @@ export default {
}
})
},
async clearCustomerSearch (removedOption, id) {
async clearCustomerSearch(removedOption, id) {
this.filters.customer = ''
this.$refs.table.refresh()
this.refreshTable()
},
showModel (selectedRow) {
showModel(selectedRow) {
this.selectedRow = selectedRow
this.$refs.Delete_modal.open()
},
async removeSelectedItems () {
async removeSelectedItems() {
this.$refs.Delete_modal.close()
await this.selectedRow.forEach(row => {
await this.selectedRow.forEach((row) => {
this.deletePayment(this.id)
})
this.$refs.table.refresh()
}
}
},
},
}
</script>

View File

@@ -1,176 +1,225 @@
<template>
<div v-if="payment" class="main-content payment-view-page">
<div class="page-header">
<h3 class="page-title">{{ payment.payment_number }}</h3>
<div class="page-actions row">
<base-button
:loading="isSendingEmail"
<base-page v-if="payment" class="xl:pl-96">
<sw-page-header :title="pageTitle">
<template slot="actions">
<sw-button
:disabled="isSendingEmail"
color="theme"
variant="primary"
@click="onPaymentSend"
>
{{ $t('payments.send_payment_receipt') }}
</base-button>
<v-dropdown
:close-on-select="true"
align="left"
class="filter-container"
>
<a slot="activator" href="#">
<base-button color="theme">
<font-awesome-icon icon="ellipsis-h" />
</base-button>
</a>
<v-dropdown-item>
<div class="dropdown-item" @click="copyPdfUrl">
<font-awesome-icon
:icon="['fas', 'link']"
class="dropdown-item-icon"
/>
{{ $t('general.copy_pdf_url') }}
</div>
<router-link
:to="{ path: `/admin/payments/${$route.params.id}/edit` }"
class="dropdown-item"
>
<font-awesome-icon
:icon="['fas', 'pencil-alt']"
class="dropdown-item-icon"
/>
{{ $t('general.edit') }}
</router-link>
<div class="dropdown-item" @click="removePayment($route.params.id)">
<font-awesome-icon
:icon="['fas', 'trash']"
class="dropdown-item-icon"
/>
{{ $t('general.delete') }}
</div>
</v-dropdown-item>
</v-dropdown>
</div>
</div>
<div class="payment-sidebar">
<div class="side-header">
<base-input
</sw-button>
<sw-dropdown class="ml-3">
<sw-button slot="activator" variant="primary" class="h-10">
<dots-horizontal-icon class="h-5" />
</sw-button>
<sw-dropdown-item @click="copyPdfUrl">
<link-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.copy_pdf_url') }}
</sw-dropdown-item>
<sw-dropdown-item
tag-name="router-link"
:to="`/admin/payments/${$route.params.id}/edit`"
>
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removePayment($route.params.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</sw-page-header>
<!-- sidebar -->
<div
class="fixed top-0 left-0 hidden h-full pt-16 pb-4 ml-56 bg-white xl:ml-64 w-88 xl:block"
>
<div
class="flex items-center justify-between px-4 pt-8 pb-2 border border-gray-200 border-solid height-full"
>
<sw-input
v-model="searchData.searchText"
:placeholder="$t('general.search')"
input-class="inv-search"
icon="search"
type="text"
align-icon="right"
class="mb-6"
variant="gray"
@input="onSearch"
/>
<div class="btn-group ml-3" role="group" aria-label="First group">
<v-dropdown
:close-on-select="false"
align="left"
class="filter-container"
>
<a slot="activator" href="#">
<base-button
class="inv-button inv-filter-fields-btn"
color="default"
size="medium"
>
<font-awesome-icon icon="filter" />
</base-button>
</a>
<div class="filter-title">
>
<search-icon slot="rightIcon" class="h-5" />
</sw-input>
<div class="flex mb-6 ml-3" role="group" aria-label="First group">
<sw-dropdown position="bottom-start">
<sw-button slot="activator" size="md" variant="gray-light">
<filter-icon class="h-5" />
</sw-button>
<div
class="px-2 pb-2 mb-1 text-sm border-b border-gray-200 border-solid"
>
{{ $t('general.sort_by') }}
</div>
<div class="filter-items">
<input
id="filter_invoice_number"
v-model="searchData.orderByField"
type="radio"
name="filter"
class="inv-radio"
value="invoice_number"
@change="onSearch"
/>
<label class="inv-label" for="filter_invoice_number">{{
$t('invoices.title')
}}</label>
</div>
<div class="filter-items">
<input
id="filter_payment_date"
v-model="searchData.orderByField"
type="radio"
name="filter"
class="inv-radio"
value="payment_date"
@change="onSearch"
/>
<label class="inv-label" for="filter_payment_date">{{
$t('payments.date')
}}</label>
</div>
<div class="filter-items">
<input
id="filter_payment_number"
v-model="searchData.orderByField"
type="radio"
name="filter"
class="inv-radio"
value="payment_number"
@change="onSearch"
/>
<label class="inv-label" for="filter_payment_number">{{
$t('payments.payment_number')
}}</label>
</div>
</v-dropdown>
<base-button
<sw-dropdown-item class="flex cursor-pointer">
<sw-input-group class="-mt-3 font-normal">
<sw-radio
:label="$t('invoices.title')"
size="sm"
id="filter_invoice_number"
v-model="searchData.orderByField"
name="filter"
value="invoice_number"
@change="onSearch"
/>
</sw-input-group>
</sw-dropdown-item>
<sw-dropdown-item class="flex cursor-pointer">
<sw-input-group class="-mt-3 font-normal">
<sw-radio
:label="$t('payments.date')"
size="sm"
id="filter_payment_date"
v-model="searchData.orderByField"
name="filter"
value="payment_date"
@change="onSearch"
/>
</sw-input-group>
</sw-dropdown-item>
<sw-dropdown-item class="flex cursor-pointer">
<sw-input-group class="-mt-3 font-normal">
<sw-radio
id="filter_payment_number"
:label="$t('payments.payment_number')"
v-model="searchData.orderByField"
size="sm"
name="filter"
value="payment_number"
@change="onSearch"
/>
</sw-input-group>
</sw-dropdown-item>
</sw-dropdown>
<sw-button
class="ml-1"
v-tooltip.top-center="{ content: getOrderName }"
class="inv-button inv-filter-sorting-btn"
color="default"
size="medium"
size="md"
variant="gray-light"
@click="sortData"
>
<font-awesome-icon v-if="getOrderBy" icon="sort-amount-up" />
<font-awesome-icon v-else icon="sort-amount-down" />
</base-button>
<sort-ascending-icon v-if="getOrderBy" class="h-5" />
<sort-descending-icon v-else class="h-5" />
</sw-button>
</div>
</div>
<base-loader v-if="isSearching" />
<div v-else class="side-content">
<base-loader v-if="isSearching" :show-bg-overlay="true" />
<div
v-else
class="h-full pb-32 overflow-y-scroll border-l border-gray-200 border-solid sw-scroll"
>
<router-link
v-for="(payment, index) in payments"
:to="`/admin/payments/${payment.id}/view`"
:id="'payment-' + payment.id"
:key="index"
class="side-payment"
:class="[
'flex justify-between p-4 items-center cursor-pointer hover:bg-gray-100 border-l-4 border-transparent',
{
'bg-gray-100 border-l-4 border-primary-500 border-solid': hasActiveUrl(
payment.id
),
},
]"
style="border-bottom: 1px solid rgba(185, 193, 209, 0.41)"
>
<div class="left">
<div class="inv-name">{{ payment.user.name }}</div>
<div class="inv-number">{{ payment.payment_number }}</div>
<div class="inv-number">{{ payment.invoice_number }}</div>
</div>
<div class="right">
<div class="flex-2">
<div
class="inv-amount"
class="pr-2 mb-2 text-sm not-italic font-normal leading-5 text-black capitalize truncate"
>
{{ payment.user.name }}
</div>
<div
class="mb-1 text-xs not-italic font-medium leading-5 text-gray-500 capitalize"
>
{{ payment.payment_number }}
</div>
<div
class="mb-1 text-xs not-italic font-medium leading-5 text-gray-500 capitalize"
>
{{ payment.invoice_number }}
</div>
</div>
<div class="flex-1 whitespace-no-wrap right">
<div
class="mb-2 text-xl not-italic font-semibold leading-8 text-right text-gray-900"
v-html="$utils.formatMoney(payment.amount, payment.user.currency)"
/>
<div class="inv-date">{{ payment.formattedPaymentDate }}</div>
<!-- <div class="inv-number">{{ payment.payment_method.name }}</div> -->
<div class="text-sm text-right text-gray-500 non-italic">
{{ payment.formattedPaymentDate }}
</div>
</div>
</router-link>
<p v-if="!payments.length" class="no-result">
<p
v-if="!payments.length"
class="flex justify-center px-4 mt-5 text-sm text-gray-600"
>
{{ $t('payments.no_matching_payments') }}
</p>
</div>
</div>
<div class="payment-view-page-container">
<iframe :src="`${shareableLink}`" class="frame-style" />
<!-- pdf -->
<div
class="flex flex-col min-h-0 mt-8 overflow-hidden"
style="height: 75vh"
>
<iframe
:src="`${shareableLink}`"
class="flex-1 border border-gray-400 border-solid rounded-md"
/>
</div>
</div>
</base-page>
</template>
<script>
import {
DotsHorizontalIcon,
FilterIcon,
SortAscendingIcon,
SortDescendingIcon,
SearchIcon,
LinkIcon,
TrashIcon,
PencilIcon,
} from '@vue-hero-icons/solid'
import { mapActions, mapGetters } from 'vuex'
const _ = require('lodash')
export default {
data () {
components: {
DotsHorizontalIcon,
FilterIcon,
SortAscendingIcon,
SortDescendingIcon,
SearchIcon,
TrashIcon,
PencilIcon,
LinkIcon,
},
data() {
return {
id: null,
count: null,
@@ -180,82 +229,109 @@ export default {
searchData: {
orderBy: null,
orderByField: null,
searchText: null
searchText: null,
},
isRequestOnGoing: false,
isSearching: false,
isSendingEmail: false,
isMarkingAsSent: false
isMarkingAsSent: false,
}
},
computed: {
getOrderBy () {
if (this.searchData.orderBy === 'asc' || this.searchData.orderBy == null) {
pageTitle() {
return this.payment.payment_number
},
getOrderBy() {
if (
this.searchData.orderBy === 'asc' ||
this.searchData.orderBy == null
) {
return true
}
return false
},
getOrderName () {
getOrderName() {
if (this.getOrderBy) {
return this.$t('general.ascending')
}
return this.$t('general.descending')
},
shareableLink () {
shareableLink() {
return `/payments/pdf/${this.payment.unique_hash}`
}
},
},
watch: {
$route (to, from) {
$route(to, from) {
this.loadPayment()
}
},
},
created () {
created() {
this.loadPayments()
this.loadPayment()
this.onSearch = _.debounce(this.onSearch, 500)
},
methods: {
// ...mapActions('invoice', [
// 'fetchInvoices',
// 'getRecord',
// 'searchInvoice',
// 'markAsSent',
// 'sendEmail',
// 'deleteInvoice',
// 'fetchViewInvoice'
// ]),
...mapActions('payment', [
'fetchPayments',
'fetchPayment',
'sendEmail',
'deletePayment',
'searchPayment'
'searchPayment',
]),
async loadPayments () {
let response = await this.fetchPayments()
...mapActions('modal', ['openModal']),
hasActiveUrl(id) {
return this.$route.params.id == id
},
async loadPayments() {
let response = await this.fetchPayments({ limit: 'all' })
if (response.data) {
this.payments = response.data.payments.data
}
setTimeout(() => {
this.scrollToPayment()
}, 500)
},
async loadPayment () {
scrollToPayment() {
const el = document.getElementById(`payment-${this.$route.params.id}`)
if (el) {
el.scrollIntoView({ behavior: 'smooth' })
el.classList.add('shake')
}
},
async loadPayment() {
let response = await this.fetchPayment(this.$route.params.id)
if (response.data) {
this.payment = response.data.payment
}
},
async onSearch () {
async onSearch() {
let data = ''
if (this.searchData.searchText !== '' && this.searchData.searchText !== null && this.searchData.searchText !== undefined) {
if (
this.searchData.searchText !== '' &&
this.searchData.searchText !== null &&
this.searchData.searchText !== undefined
) {
data += `search=${this.searchData.searchText}&`
}
if (this.searchData.orderBy !== null && this.searchData.orderBy !== undefined) {
if (
this.searchData.orderBy !== null &&
this.searchData.orderBy !== undefined
) {
data += `orderBy=${this.searchData.orderBy}&`
}
if (this.searchData.orderByField !== null && this.searchData.orderByField !== undefined) {
if (
this.searchData.orderByField !== null &&
this.searchData.orderByField !== undefined
) {
data += `orderByField=${this.searchData.orderByField}`
}
this.isSearching = true
@@ -265,7 +341,7 @@ export default {
this.payments = response.data.payments.data
}
},
sortData () {
sortData() {
if (this.searchData.orderBy === 'asc') {
this.searchData.orderBy = 'desc'
this.onSearch()
@@ -275,57 +351,44 @@ export default {
this.onSearch()
return true
},
async onPaymentSend () {
window.swal({
title: this.$tc('general.are_you_sure'),
text: this.$tc('payments.confirm_send_payment'),
icon: '/assets/icon/paper-plane-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
this.isSendingEmail = true
let response = await this.sendEmail({id: this.payment.id})
this.isSendingEmail = false
if (response.data.success) {
window.toastr['success'](this.$tc('payments.send_payment_successfully'))
return true
}
if (response.data.error === 'user_email_does_not_exist') {
window.toastr['error'](this.$tc('payments.user_email_does_not_exist'))
return false
}
window.toastr['error'](this.$tc('payments.something_went_wrong'))
}
async onPaymentSend() {
this.openModal({
title: this.$t('payments.send_payment'),
componentName: 'SendPaymentModal',
id: this.payment.id,
data: this.payment,
variant: 'lg',
})
},
copyPdfUrl () {
copyPdfUrl() {
let pdfUrl = `${window.location.origin}/payments/pdf/${this.payment.unique_hash}`
let response = this.$utils.copyTextToClipboard(pdfUrl)
window.toastr['success'](this.$tc('Copied PDF url to clipboard!'))
window.toastr['success'](this.$t('general.copied_pdf_url_clipboard'))
},
async removePayment (id) {
async removePayment(id) {
this.id = id
window.swal({
title: 'Deleted',
text: 'you will not be able to recover this payment!',
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
let request = await this.deletePayment(this.id)
if (request.data.success) {
window.toastr['success'](this.$tc('payments.deleted_message', 1))
this.$router.push('/admin/payments')
} else if (request.data.error) {
window.toastr['error'](request.data.message)
window
.swal({
title: this.$t('general.are_you_sure'),
text: 'you will not be able to recover this payment!',
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
})
.then(async (value) => {
if (value) {
let request = await this.deletePayment({ ids: [id] })
if (request.data.success) {
window.toastr['success'](this.$tc('payments.deleted_message', 1))
this.$router.push('/admin/payments')
} else if (request.data.error) {
window.toastr['error'](request.data.message)
}
}
}
})
}
}
})
},
},
}
</script>

View File

@@ -1,55 +1,76 @@
<template>
<div class="row">
<div class="col-md-4 reports-tab-container">
<div class="row">
<div class="col-md-8">
<label class="report-label">{{ $t('reports.expenses.date_range') }}</label>
<base-select
<div class="grid gap-8 md:grid-cols-12">
<div class="col-span-8 mt-12 md:col-span-4">
<div class="grid grid-cols-12">
<sw-input-group
:label="$t('reports.expenses.date_range')"
:error="dateRangeError"
class="col-span-12 md:col-span-8"
>
<sw-select
v-model="selectedRange"
:options="dateRange"
:allow-empty="false"
:show-labels="false"
class="mt-2"
@input="onChangeDateRange"
/>
<span v-if="$v.range.$error && !$v.range.required" class="text-danger"> {{ $t('validation.required') }} </span>
</div>
</sw-input-group>
</div>
<div class="row report-fields-container">
<div class="col-md-6 report-field-container">
<label class="report-label">{{ $t('reports.expenses.from_date') }}</label>
<div class="grid grid-cols-1 mt-6 md:gap-10 md:grid-cols-2">
<sw-input-group
:label="$t('reports.expenses.from_date')"
:error="fromDateError"
>
<base-date-picker
v-model="formData.from_date"
:invalid="$v.formData.from_date.$error"
:calendar-button="true"
calendar-button-icon="calendar"
class="mt-2"
@change="$v.formData.from_date.$touch()"
/>
<span v-if="$v.formData.from_date.$error && !$v.formData.from_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
</div>
<div class="col-md-6 report-field-container">
<label class="report-label">{{ $t('reports.expenses.to_date') }}</label>
</sw-input-group>
<sw-input-group
:label="$t('reports.expenses.to_date')"
:error="toDateError"
class="mt-5 md:mt-0"
>
<base-date-picker
v-model="formData.to_date"
:invalid="$v.formData.to_date.$error"
:calendar-button="true"
calendar-button-icon="calendar"
class="mt-2"
@change="$v.formData.to_date.$touch()"
/>
<span v-if="$v.formData.to_date.$error && !$v.formData.to_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
</div>
</div>
<div class="row report-submit-button-container">
<div class="col-md-6">
<base-button outline color="theme" class="report-button" @click="getReports()">
{{ $t('reports.update_report') }}
</base-button>
</div>
</sw-input-group>
</div>
<sw-button
variant="primary-outline"
class="content-center hidden mt-0 w-md md:flex md:mt-8"
@click="getReports()"
>
{{ $t('reports.update_report') }}
</sw-button>
</div>
<div class="col-sm-8 reports-tab-container">
<iframe :src="getReportUrl" class="reports-frame-style"/>
<a class="base-button btn btn-primary btn-lg report-view-button" @click="viewReportsPDF">
<font-awesome-icon icon="file-pdf" class="vue-icon icon-left svg-inline--fa fa-download fa-w-16 mr-2" /> <span>{{ $t('reports.view_pdf') }}</span>
<div class="col-span-8 mt-0 md:mt-12">
<iframe
:src="getReportUrl"
class="hidden w-full h-screen border-gray-100 border-solid rounded md:flex"
/>
<a
class="flex items-center justify-center h-10 px-5 py-1 text-sm font-medium leading-none text-center text-white whitespace-no-wrap rounded md:hidden bg-primary-500"
@click="viewReportsPDF"
>
<document-text-icon />
<span>{{ $t('reports.view_pdf') }}</span>
</a>
</div>
</div>
@@ -58,12 +79,16 @@
<script>
import { mapGetters } from 'vuex'
import moment from 'moment'
import { validationMixin } from 'vuelidate'
import { DocumentTextIcon } from '@vue-hero-icons/solid'
const { required } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
data () {
components: {
DocumentTextIcon,
},
data() {
return {
range: new Date(),
dateRange: [
@@ -76,56 +101,94 @@ export default {
'Previous Month',
'Previous Quarter',
'Previous Year',
'Custom'
'Custom',
],
selectedRange: 'This Month',
formData: {
from_date: moment().startOf('month').toString(),
to_date: moment().endOf('month').toString()
to_date: moment().endOf('month').toString(),
},
url: null,
siteURL: null
siteURL: null,
}
},
validations: {
range: {
required
required,
},
formData: {
from_date: {
required
required,
},
to_date: {
required
}
}
required,
},
},
},
computed: {
...mapGetters('company', [
'getSelectedCompany'
]),
getReportUrl () {
...mapGetters('company', ['getSelectedCompany']),
getReportUrl() {
return this.url
}
},
dateRangeError() {
if (!this.$v.range.$error) {
return ''
}
if (!this.$v.range.required) {
return this.$t('validation.required')
}
},
fromDateError() {
if (!this.$v.formData.from_date.$error) {
return ''
}
if (!this.$v.formData.from_date.required) {
return this.$t('validation.required')
}
},
toDateError() {
if (!this.$v.formData.to_date.$error) {
return ''
}
if (!this.$v.formData.to_date.required) {
return this.$t('validation.required')
}
},
},
watch: {
range (newRange) {
range(newRange) {
this.formData.from_date = moment(newRange).startOf('year').toString()
this.formData.to_date = moment(newRange).endOf('year').toString()
}
},
},
mounted () {
mounted() {
this.siteURL = `/reports/expenses/${this.getSelectedCompany.unique_hash}`
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.url = `${this.siteURL}?from_date=${moment(
this.formData.from_date
).format('YYYY-MM-DD')}&to_date=${moment(this.formData.to_date).format(
'YYYY-MM-DD'
)}`
},
methods: {
getThisDate (type, time) {
getThisDate(type, time) {
return moment()[type](time).toString()
},
getPreDate (type, time) {
getPreDate(type, time) {
return moment().subtract(1, time)[type](time).toString()
},
onChangeDateRange () {
onChangeDateRange() {
switch (this.selectedRange) {
case 'Today':
this.formData.from_date = moment().toString()
@@ -176,15 +239,17 @@ export default {
break
}
},
setRangeToCustom () {
setRangeToCustom() {
this.selectedRange = 'Custom'
},
async viewReportsPDF () {
async viewReportsPDF() {
let data = await this.getReports()
window.open(this.getReportUrl, '_blank')
return data
},
async getReports (isDownload = false) {
async getReports(isDownload = false) {
this.$v.range.$touch()
this.$v.formData.$touch()
@@ -192,10 +257,11 @@ export default {
return true
}
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.url = `${this.siteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
return true
},
downloadReport () {
downloadReport() {
if (!this.getReports()) {
return false
}
@@ -203,9 +269,9 @@ export default {
window.open(this.getReportUrl + '&download=true')
setTimeout(() => {
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.url = `${this.siteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
}, 200)
}
}
},
},
}
</script>

View File

@@ -1,55 +1,78 @@
<template>
<div class="row">
<div class="col-md-4 reports-tab-container">
<div class="row">
<div class="col-md-8">
<label class="report-label">{{ $t('reports.profit_loss.date_range') }}</label>
<base-select
<div class="grid gap-8 md:grid-cols-12">
<div class="col-span-8 mt-12 md:col-span-4">
<div class="grid grid-cols-12">
<sw-input-group
:label="$t('reports.profit_loss.date_range')"
:error="dateRangeError"
class="col-span-12 md:col-span-8"
>
<sw-select
v-model="selectedRange"
:options="dateRange"
:allow-empty="false"
:show-labels="false"
class="mt-2"
@input="onChangeDateRange"
/>
<span v-if="$v.range.$error && !$v.range.required" class="text-danger"> {{ $t('validation.required') }} </span>
</div>
</sw-input-group>
</div>
<div class="row report-fields-container">
<div class="col-md-6 report-field-container">
<label class="report-label">{{ $t('reports.profit_loss.from_date') }}</label>
<div class="grid grid-cols-1 mt-6 md:gap-10 md:grid-cols-2">
<sw-input-group
:label="$t('reports.profit_loss.from_date')"
:error="fromDateError"
>
<base-date-picker
v-model="formData.from_date"
:invalid="$v.formData.from_date.$error"
:calendar-button="true"
calendar-button-icon="calendar"
class="mt-2"
@change="$v.formData.from_date.$touch()"
/>
<span v-if="$v.formData.from_date.$error && !$v.formData.from_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
</div>
<div class="col-md-6 report-field-container">
<label class="report-label">{{ $t('reports.profit_loss.to_date') }}</label>
</sw-input-group>
<sw-input-group
:label="$t('reports.profit_loss.to_date')"
:error="toDateError"
class="mt-5 md:mt-0"
>
<base-date-picker
v-model="formData.to_date"
:invalid="$v.formData.to_date.$error"
:calendar-button="true"
calendar-button-icon="calendar"
class="mt-2"
@change="$v.formData.to_date.$touch()"
/>
<span v-if="$v.formData.to_date.$error && !$v.formData.to_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
</div>
</sw-input-group>
</div>
<div class="row report-submit-button-container">
<div class="col-md-6">
<base-button outline color="theme" class="report-button" @click="getReports()">
{{ $t('reports.update_report') }}
</base-button>
</div>
<div class="mt-0 md:mt-8">
<sw-button
variant="primary-outline"
class="content-center hidden text-sm w-md md:flex"
@click="getReports()"
>
{{ $t('reports.update_report') }}
</sw-button>
</div>
</div>
<div class="col-sm-8 reports-tab-container">
<iframe :src="getReportUrl" class="reports-frame-style"/>
<a class="base-button btn btn-primary btn-lg report-view-button" @click="viewReportsPDF">
<font-awesome-icon icon="file-pdf" class="vue-icon icon-left svg-inline--fa fa-download fa-w-16 mr-2" /> <span>{{ $t('reports.view_pdf') }}</span>
<div class="col-span-8 mt-0 md:mt-12">
<iframe
:src="getReportUrl"
class="hidden w-full h-screen border-gray-100 border-solid rounded md:flex"
/>
<a
class="flex items-center justify-center h-10 px-5 py-1 text-sm font-medium leading-none text-center text-white whitespace-no-wrap rounded md:hidden bg-primary-500"
@click="viewReportsPDF"
>
<document-text-icon />
<span>{{ $t('reports.view_pdf') }}</span>
</a>
</div>
</div>
@@ -57,13 +80,16 @@
<script>
import { mapGetters, mapActions } from 'vuex'
import { DocumentTextIcon } from '@vue-hero-icons/solid'
import moment from 'moment'
import { validationMixin } from 'vuelidate'
const { required } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
data () {
components: {
DocumentTextIcon,
},
data() {
return {
dateRange: [
'Today',
@@ -75,62 +101,94 @@ export default {
'Previous Month',
'Previous Quarter',
'Previous Year',
'Custom'
'Custom',
],
selectedRange: 'This Month',
range: new Date(),
formData: {
from_date: moment().startOf('month').toString(),
to_date: moment().endOf('month').toString()
to_date: moment().endOf('month').toString(),
},
url: null,
siteURL: null
siteURL: null,
}
},
validations: {
range: {
required
required,
},
formData: {
from_date: {
required
required,
},
to_date: {
required
}
}
required,
},
},
},
computed: {
...mapGetters('company', [
'getSelectedCompany'
]),
getReportUrl () {
...mapGetters('company', ['getSelectedCompany']),
getReportUrl() {
return this.url
}
},
dateRangeError() {
if (!this.$v.range.$error) {
return ''
}
if (!this.$v.range.required) {
return this.$t('validation.required')
}
},
fromDateError() {
if (!this.$v.formData.from_date.$error) {
return ''
}
if (!this.$v.formData.from_date.required) {
return this.$t('validation.required')
}
},
toDateError() {
if (!this.$v.formData.to_date.$error) {
}
if (!this.$v.formData.to_date.required) {
return this.$t('validation.required')
}
},
},
watch: {
range (newRange) {
range(newRange) {
this.formData.from_date = moment(newRange).startOf('year').toString()
this.formData.to_date = moment(newRange).endOf('year').toString()
}
},
},
mounted () {
this.siteURL = `/reports/profit-loss/${this.getSelectedCompany.unique_hash}`
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.loadProfitLossLink(this.url + '&download=true')
mounted() {
this.siteURL = `/reports/profit-loss/${this.getSelectedCompany.unique_hash}`
this.url = `${this.siteURL}?from_date=${moment(
this.formData.from_date
).format('YYYY-MM-DD')}&to_date=${moment(this.formData.to_date).format(
'YYYY-MM-DD'
)}`
},
methods: {
...mapActions('profitLossReport', [
'loadProfitLossLink'
]),
getThisDate (type, time) {
getThisDate(type, time) {
return moment()[type](time).toString()
},
getPreDate (type, time) {
getPreDate(type, time) {
return moment().subtract(1, time)[type](time).toString()
},
onChangeDateRange () {
onChangeDateRange() {
switch (this.selectedRange) {
case 'Today':
this.formData.from_date = moment().toString()
@@ -181,34 +239,38 @@ export default {
break
}
},
setRangeToCustom () {
setRangeToCustom() {
this.selectedRange = 'Custom'
},
async viewReportsPDF () {
async viewReportsPDF() {
let data = await this.getReports()
window.open(this.getReportUrl, '_blank')
return data
},
async getReports (isDownload = false) {
async getReports(isDownload = false) {
this.$v.range.$touch()
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.url = `${this.siteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
return true
},
downloadReport () {
downloadReport() {
if (!this.getReports()) {
return false
}
window.open(this.getReportUrl + '&download=true')
setTimeout(() => {
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.url = `${this.siteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
}, 200)
}
}
},
},
}
</script>

View File

@@ -1,70 +1,90 @@
<template>
<div class="row">
<div class="col-md-4 reports-tab-container">
<div class="row">
<div class="col-md-8">
<label class="report-label">{{ $t('reports.sales.date_range') }}</label>
<base-select
<div class="grid gap-8 md:grid-cols-12">
<div class="col-span-8 mt-12 md:col-span-4">
<div class="grid grid-cols-12">
<sw-input-group
:label="$t('reports.sales.date_range')"
:error="dateRangeError"
class="col-span-12 md:col-span-8"
>
<sw-select
v-model="selectedRange"
:options="dateRange"
:allow-empty="false"
:show-labels="false"
class="mt-2"
@input="onChangeDateRange"
/>
<span v-if="$v.range.$error && !$v.range.required" class="text-danger"> {{ $t('validation.required') }} </span>
</div>
</sw-input-group>
</div>
<div class="row report-fields-container">
<div class="col-md-6 report-field-container">
<label class="report-label">{{ $t('reports.sales.from_date') }}</label>
<div class="grid grid-cols-1 mt-6 md:gap-10 md:grid-cols-2">
<sw-input-group
:label="$t('reports.sales.from_date')"
:error="fromDateError"
>
<base-date-picker
v-model="formData.from_date"
:invalid="$v.formData.from_date.$error"
:calendar-button="true"
calendar-button-icon="calendar"
class="mt-2"
@change="$v.formData.from_date.$touch()"
@input="setRangeToCustom"
/>
<span v-if="$v.formData.from_date.$error && !$v.formData.from_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
</div>
<div class="col-md-6 report-field-container">
<label class="report-label">{{ $t('reports.sales.to_date') }}</label>
</sw-input-group>
<sw-input-group
:label="$t('reports.sales.to_date')"
:error="toDateError"
class="mt-5 md:mt-0"
>
<base-date-picker
v-model="formData.to_date"
:invalid="$v.formData.to_date.$error"
:calendar-button="true"
calendar-button-icon="calendar"
class="mt-2"
@change="$v.formData.to_date.$touch()"
@input="setRangeToCustom"
/>
<span v-if="$v.formData.to_date.$error && !$v.formData.to_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
</div>
</sw-input-group>
</div>
<div class="row report-fields-container">
<div class="col-md-8 report-field-container">
<label class="report-label">{{ $t('reports.sales.report_type') }}</label>
<base-select
<div class="grid grid-cols-12 mt-6 md:mt-8">
<sw-input-group
:label="$t('reports.sales.report_type')"
class="col-span-12 md:col-span-8"
>
<sw-select
v-model="selectedType"
:options="reportTypes"
:allow-empty="false"
:show-labels="false"
class="mt-2"
:placeholder="$t('reports.sales.report_type')"
@input="getInitialReport"
/>
</div>
</div>
<div class="row report-submit-button-container">
<div class="col-md-6">
<base-button outline color="theme" class="report-button" @click="getReports()">
{{ $t('reports.update_report') }}
</base-button>
</div>
</sw-input-group>
</div>
<sw-button
variant="primary-outline"
class="content-center hidden mt-0 w-md md:flex md:mt-8"
type="submit"
@click.prevent="getReports()"
>
{{ $t('reports.update_report') }}
</sw-button>
</div>
<div class="col-sm-8 reports-tab-container">
<iframe :src="getReportUrl" class="reports-frame-style"/>
<a class="base-button btn btn-primary btn-lg report-view-button" @click="viewReportsPDF">
<font-awesome-icon icon="file-pdf" class="vue-icon icon-left svg-inline--fa fa-download fa-w-16 mr-2" /> <span>{{ $t('reports.view_pdf') }}</span>
<div class="col-span-8 mt-0 md:mt-12">
<iframe
:src="getReportUrl"
class="hidden w-full h-screen border-gray-100 border-solid rounded md:flex"
/>
<a
class="flex items-center justify-center h-10 px-5 py-1 text-sm font-medium leading-none text-center text-white whitespace-no-wrap rounded md:hidden bg-primary-500"
@click="viewReportsPDF"
>
<document-text-icon />
<span>{{ $t('reports.view_pdf') }}</span>
</a>
</div>
</div>
@@ -72,13 +92,17 @@
<script>
import { mapActions, mapGetters } from 'vuex'
import { DocumentTextIcon } from '@vue-hero-icons/solid'
import moment from 'moment'
import { validationMixin } from 'vuelidate'
const { required } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
data () {
components: {
DocumentTextIcon,
},
data() {
return {
reportTypes: ['By Customer', 'By Item'],
selectedType: 'By Customer',
@@ -92,63 +116,96 @@ export default {
'Previous Month',
'Previous Quarter',
'Previous Year',
'Custom'
'Custom',
],
selectedRange: 'This Month',
range: new Date(),
formData: {
from_date: moment().startOf('month').toString(),
to_date: moment().endOf('month').toString()
to_date: moment().endOf('month').toString(),
},
url: null,
customerSiteURL: null,
itemsSiteURL: null
itemsSiteURL: null,
}
},
validations: {
range: {
required
required,
},
formData: {
from_date: {
required
required,
},
to_date: {
required
}
}
required,
},
},
},
computed: {
...mapGetters('company', [
'getSelectedCompany'
]),
getReportUrl () {
...mapGetters('company', ['getSelectedCompany']),
getReportUrl() {
return this.url
}
},
dateRangeError() {
if (!this.$v.range.$error) {
return ''
}
if (!this.$v.range.required) {
return this.$t('validation.required')
}
},
fromDateError() {
if (!this.$v.formData.from_date.$error) {
return ''
}
if (!this.$v.formData.from_date.required) {
return this.$t('validation.required')
}
},
toDateError() {
if (!this.$v.formData.to_date.$error) {
return ''
}
if (!this.$v.formData.to_date.required) {
return this.$t('validation.required')
}
},
},
watch: {
range (newRange) {
range(newRange) {
this.formData.from_date = moment(newRange).startOf('year').toString()
this.formData.to_date = moment(newRange).endOf('year').toString()
}
},
},
mounted () {
mounted() {
this.customerSiteURL = `/reports/sales/customers/${this.getSelectedCompany.unique_hash}`
this.itemsSiteURL = `/reports/sales/items/${this.getSelectedCompany.unique_hash}`
this.getInitialReport()
},
methods: {
...mapActions('salesReport', [
'loadLinkByCustomer',
'loadLinkByItems'
]),
getThisDate (type, time) {
...mapActions('salesReport', ['loadLinkByCustomer', 'loadLinkByItems']),
getThisDate(type, time) {
return moment()[type](time).toString()
},
getPreDate (type, time) {
getPreDate(type, time) {
return moment().subtract(1, time)[type](time).toString()
},
onChangeDateRange () {
onChangeDateRange() {
switch (this.selectedRange) {
case 'Today':
this.formData.from_date = moment().toString()
@@ -199,38 +256,49 @@ export default {
break
}
},
setRangeToCustom () {
setRangeToCustom() {
this.selectedRange = 'Custom'
},
async getInitialReport () {
async getInitialReport() {
if (this.selectedType === 'By Customer') {
this.url = `${this.customerSiteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.url = `${this.customerSiteURL}?from_date=${moment(
this.formData.from_date
).format('YYYY-MM-DD')}&to_date=${moment(this.formData.to_date).format(
'YYYY-MM-DD'
)}`
return true
}
this.url = `${this.itemsSiteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.url = `${this.itemsSiteURL}?from_date=${moment(
this.formData.from_date
).format('YYYY-MM-DD')}&to_date=${moment(this.formData.to_date).format(
'YYYY-MM-DD'
)}`
return true
},
async viewReportsPDF () {
async viewReportsPDF() {
let data = await this.getReports()
window.open(this.getReportUrl, '_blank')
return data
},
async getReports (isDownload = false) {
async getReports(isDownload = false) {
this.$v.range.$touch()
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
if (this.selectedType === 'By Customer') {
this.url = `${this.customerSiteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.url = `${this.customerSiteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
return true
}
this.url = `${this.itemsSiteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.url = `${this.itemsSiteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
return true
},
downloadReport () {
downloadReport() {
if (!this.getReports()) {
return false
}
@@ -238,13 +306,13 @@ export default {
window.open(this.getReportUrl + '&download=true')
setTimeout(() => {
if (this.selectedType === 'By Customer') {
this.url = `${this.customerSiteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.url = `${this.customerSiteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
return true
}
this.url = `${this.itemsSiteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.url = `${this.itemsSiteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
return true
}, 200)
}
}
},
},
}
</script>

View File

@@ -1,55 +1,70 @@
<template>
<div class="row">
<div class="col-md-4 reports-tab-container">
<div class="row">
<div class="col-md-8">
<label class="report-label">{{ $t('reports.taxes.date_range') }}</label>
<base-select
<div class="grid gap-8 md:grid-cols-12">
<div class="col-span-8 mt-12 md:col-span-4">
<div class="grid grid-cols-12">
<sw-input-group
:label="$t('reports.taxes.date_range')"
:error="dateRangeError"
class="col-span-12 md:col-span-8"
>
<sw-select
v-model="selectedRange"
:options="dateRange"
:allow-empty="false"
:show-labels="false"
class="mt-2"
@input="onChangeDateRange"
/>
<span v-if="$v.range.$error && !$v.range.required" class="text-danger"> {{ $t('validation.required') }} </span>
</div>
</sw-input-group>
</div>
<div class="row report-fields-container">
<div class="col-md-6 report-field-container">
<label class="report-label">{{ $t('reports.taxes.from_date') }}</label>
<div class="grid grid-cols-1 mt-6 md:gap-10 md:grid-cols-2">
<sw-input-group
:label="$t('reports.taxes.from_date')"
:error="fromDateError"
>
<base-date-picker
v-model="formData.from_date"
:invalid="$v.formData.from_date.$error"
:calendar-button="true"
calendar-button-icon="calendar"
class="mt-2"
@change="$v.formData.from_date.$touch()"
/>
<span v-if="$v.formData.from_date.$error && !$v.formData.from_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
</div>
<div class="col-md-6 report-field-container">
<label class="report-label">{{ $t('reports.taxes.to_date') }}</label>
</sw-input-group>
<sw-input-group
:label="$t('reports.taxes.to_date')"
:error="toDateError"
class="mt-5 md:mt-0"
>
<base-date-picker
v-model="formData.to_date"
:invalid="$v.formData.to_date.$error"
:calendar-button="true"
calendar-button-icon="calendar"
class="mt-2"
@change="$v.formData.to_date.$touch()"
/>
<span v-if="$v.formData.to_date.$error && !$v.formData.to_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
</div>
</div>
<div class="row report-submit-button-container">
<div class="col-md-6">
<base-button outline color="theme" class="report-button" @click="getReports()">
{{ $t('reports.update_report') }}
</base-button>
</div>
</sw-input-group>
</div>
<sw-button
variant="primary-outline"
class="content-center hidden mt-0 w-md md:flex md:mt-8"
@click="getReports()"
>
{{ $t('reports.update_report') }}
</sw-button>
</div>
<div class="col-sm-8 reports-tab-container">
<iframe :src="getReportUrl" class="reports-frame-style"/>
<a class="base-button btn btn-primary btn-lg report-view-button" @click="viewReportsPDF">
<font-awesome-icon icon="file-pdf" class="vue-icon icon-left svg-inline--fa fa-download fa-w-16 mr-2" />
<div class="col-span-8 mt-0 md:mt-12">
<iframe
:src="getReportUrl"
class="hidden w-full h-screen border-gray-100 border-solid rounded md:flex"
/>
<a
class="flex items-center justify-center h-10 px-5 py-1 text-sm font-medium leading-none text-center text-white whitespace-no-wrap rounded md:hidden bg-primary-500"
@click="viewReportsPDF"
>
<document-text-icon />
<span>{{ $t('reports.view_pdf') }}</span>
</a>
</div>
@@ -58,13 +73,16 @@
<script>
import { mapGetters } from 'vuex'
import { DocumentTextIcon } from '@vue-hero-icons/solid'
import moment from 'moment'
import { validationMixin } from 'vuelidate'
const { required } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
data () {
components: {
DocumentTextIcon,
},
data() {
return {
dateRange: [
'Today',
@@ -76,57 +94,86 @@ export default {
'Previous Month',
'Previous Quarter',
'Previous Year',
'Custom'
'Custom',
],
selectedRange: 'This Month',
range: new Date(),
formData: {
from_date: moment().startOf('month').toString(),
to_date: moment().endOf('month').toString()
to_date: moment().endOf('month').toString(),
},
url: null,
siteURL: null
siteURL: null,
}
},
validations: {
range: {
required
required,
},
formData: {
from_date: {
required
required,
},
to_date: {
required
}
}
required,
},
},
},
computed: {
...mapGetters('company', [
'getSelectedCompany'
]),
getReportUrl () {
...mapGetters('company', ['getSelectedCompany']),
getReportUrl() {
return this.url
}
},
dateRangeError() {
if (!this.$v.range.$error) {
return ''
}
if (!this.$v.range.required) {
return this.$t('validation.required')
}
},
fromDateError() {
if (!this.$v.formData.from_date.$error) {
return ''
}
if (!this.$v.formData.from_date.required) {
return this.$t('validation.required')
}
},
toDateError() {
if (!this.$v.formData.to_date.$error) {
return ''
}
if (!this.$v.formData.to_date.required) {
return this.$t('validation.required')
}
},
},
watch: {
range (newRange) {
range(newRange) {
this.formData.from_date = moment(newRange).startOf('year').toString()
this.formData.to_date = moment(newRange).endOf('year').toString()
}
},
},
mounted () {
mounted() {
this.siteURL = `/reports/tax-summary/${this.getSelectedCompany.unique_hash}`
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.url = `${this.siteURL}?from_date=${moment(
this.formData.from_date
).format('YYYY-MM-DD')}&to_date=${moment(this.formData.to_date).format(
'YYYY-MM-DD'
)}`
},
methods: {
getThisDate (type, time) {
getThisDate(type, time) {
return moment()[type](time).toString()
},
getPreDate (type, time) {
getPreDate(type, time) {
return moment().subtract(1, time)[type](time).toString()
},
onChangeDateRange () {
onChangeDateRange() {
switch (this.selectedRange) {
case 'Today':
this.formData.from_date = moment().toString()
@@ -177,15 +224,15 @@ export default {
break
}
},
setRangeToCustom () {
setRangeToCustom() {
this.selectedRange = 'Custom'
},
async viewReportsPDF () {
async viewReportsPDF() {
let data = await this.getReports()
window.open(this.getReportUrl, '_blank')
return data
},
async getReports (isDownload = false) {
async getReports(isDownload = false) {
this.$v.range.$touch()
this.$v.formData.$touch()
@@ -193,10 +240,10 @@ export default {
return false
}
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.url = `${this.siteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
return true
},
downloadReport () {
downloadReport() {
if (!this.getReports()) {
return false
}
@@ -204,9 +251,9 @@ export default {
window.open(this.url + '&download=true')
setTimeout(() => {
this.url = `${this.siteURL}?from_date=${moment(this.formData.from_date).format('DD/MM/YYYY')}&to_date=${moment(this.formData.to_date).format('DD/MM/YYYY')}`
this.url = `${this.siteURL}?from_date=${this.formData.from_date}&to_date=${this.formData.to_date}`
}, 200)
}
}
},
},
}
</script>

View File

@@ -1,78 +1,93 @@
<template>
<div class="profit-loss-reports reports main-content">
<div class="page-header">
<h3 class="page-title"> {{ $tc('reports.report', 2) }}</h3>
<ol class="breadcrumb">
<li class="breadcrumb-item">
<router-link
slot="item-title"
to="dashboard">
{{ $t('general.home') }}
</router-link>
</li>
<li class="breadcrumb-item">
<router-link
slot="item-title"
to="/admin/reports/sales">
{{ $tc('reports.report', 2) }}
</router-link>
</li>
</ol>
<div class="page-actions row">
<div class="col-xs-2">
<base-button icon="download" size="large" color="theme" @click="onDownload()">
{{ $t('reports.download_pdf') }}
</base-button>
</div>
</div>
</div>
<base-page class="profit-loss-reports reports">
<sw-page-header :title="$tc('reports.report', 2)">
<sw-breadcrumb slot="breadcrumbs">
<sw-breadcrumb-item
:title="$t('general.home')"
:to="`/admin/dashboard`"
/>
<sw-breadcrumb-item
:title="$tc('reports.report', 2)"
:to="`/admin/reports`"
active
/>
</sw-breadcrumb>
<template slot="actions">
<sw-button size="lg" variant="primary" @click="onDownload()">
<download-icon class="h-5 mr-1 -ml-2" />
{{ $t('reports.download_pdf') }}
</sw-button>
</template>
</sw-page-header>
<div class="row">
<div class="col-sm-12">
<!-- Tabs -->
<ul class="tabs">
<li class="tab">
<router-link class="tab-link" to="/admin/reports/sales">{{ $t('reports.sales.sales') }}</router-link>
</li>
<li class="tab">
<router-link class="tab-link" to="/admin/reports/profit-loss">{{ $t('reports.profit_loss.profit_loss') }}</router-link>
</li>
<li class="tab">
<router-link class="tab-link" to="/admin/reports/expenses">{{ $t('reports.expenses.expenses') }}</router-link>
</li>
<li class="tab">
<router-link class="tab-link" to="/admin/reports/taxes">{{ $t('reports.taxes.taxes') }}</router-link>
</li>
</ul>
</div>
<!-- Tabs -->
<sw-tabs>
<sw-tab-item
:title="$t('reports.sales.sales')"
route="/admin/reports/sales"
>
</sw-tab-item>
<sw-tab-item
:title="$t('reports.profit_loss.profit_loss')"
route="/admin/reports/profit-loss"
>
</sw-tab-item>
<sw-tab-item
:title="$t('reports.expenses.expenses')"
route="/admin/reports/expenses"
>
</sw-tab-item>
<sw-tab-item
:title="$t('reports.taxes.taxes')"
route="/admin/reports/taxes"
>
</sw-tab-item>
</sw-tabs>
</div>
<transition
name="fade"
mode="out-in">
<router-view ref="report"/>
<transition name="fade" mode="out-in">
<div
v-if="activeTab === 'SALES' || 'PROFIT_LOSS' || 'EXPENSES' || 'TAXES'"
>
<router-view ref="report" />
</div>
</transition>
</div>
</base-page>
</template>
<script>
import { DownloadIcon } from '@vue-hero-icons/solid'
export default {
components: {
DownloadIcon,
},
data() {
return {
activeTab: 'SALES',
}
},
watch: {
'$route.path' (newValue) {
'$route.path'(newValue) {
if (newValue === '/admin/reports') {
this.$router.push('/admin/reports/sales')
}
}
},
},
created () {
created() {
if (this.$route.path === '/admin/reports') {
this.$router.push('/admin/reports/sales')
}
},
methods: {
onDownload () {
onDownload() {
this.$refs.report.downloadReport()
}
}
},
setActiveTab(val) {
this.activeTab = val
},
},
}
</script>
@@ -83,6 +98,6 @@ export default {
.tab-link {
padding: 10px 30px;
display: block
display: block;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,281 @@
<template>
<base-page v-if="isSuperAdmin" class="item-create">
<sw-page-header class="mb-3" :title="pageTitle">
<sw-breadcrumb slot="breadcrumbs">
<sw-breadcrumb-item to="/admin/dashboard" :title="$t('general.home')" />
<sw-breadcrumb-item to="/admin/users" :title="$tc('users.user', 2)" />
<sw-breadcrumb-item
v-if="$route.name === 'users.edit'"
to="#"
:title="$t('users.edit_user')"
active
/>
<sw-breadcrumb-item
v-else
to="#"
:title="$t('users.new_user')"
active
/>
</sw-breadcrumb>
<template slot="actions"></template>
</sw-page-header>
<div class="grid grid-cols-12">
<div class="col-span-12 md:col-span-8">
<form action="" @submit.prevent="submitUser">
<sw-card>
<sw-input-group
:label="$t('users.name')"
:error="nameError"
class="mb-4"
required
>
<sw-input
v-model.trim="formData.name"
:invalid="$v.formData.name.$error"
class="mt-2"
focus
type="text"
name="name"
@input="$v.formData.name.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('users.email')"
class="mt-4"
:error="emailError"
required
>
<sw-input
:invalid="$v.formData.email.$error"
v-model.trim="formData.email"
type="text"
name="email"
tab-index="3"
@input="$v.formData.email.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$tc('users.password')"
:error="passwordError"
required
class="mt-4"
>
<sw-input
v-model="formData.password"
:invalid="$v.formData.password.$error"
type="password"
class="mt-2"
@input="$v.formData.password.$touch()"
/>
</sw-input-group>
<sw-input-group :label="$t('users.phone')" class="mt-4 mb-6">
<sw-input
v-model.trim="formData.phone"
type="text"
name="phone"
tab-index="4"
/>
</sw-input-group>
<div class="mt-6 mb-4">
<sw-button
:loading="isLoading"
variant="primary"
type="submit"
size="lg"
class="flex justify-center w-full md:w-auto"
>
<save-icon v-if="!isLoading" class="mr-2 -ml-1" />
{{ isEdit ? $t('users.update_user') : $t('users.save_user') }}
</sw-button>
</div>
</sw-card>
</form>
</div>
</div>
</base-page>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
const {
required,
minLength,
email,
numeric,
minValue,
maxLength,
requiredIf,
} = require('vuelidate/lib/validators')
export default {
data() {
return {
isLoading: false,
title: 'Add User',
formData: {
name: '',
email: null,
password: null,
phone: null,
},
}
},
computed: {
...mapGetters('user', ['currentUser']),
isSuperAdmin() {
return this.currentUser.role == 'super admin'
},
pageTitle() {
if (this.$route.name === 'users.edit') {
return this.$t('users.edit_user')
}
return this.$t('users.new_user')
},
isEdit() {
if (this.$route.name === 'users.edit') {
return true
}
return false
},
nameError() {
if (!this.$v.formData.name.$error) {
return ''
}
if (!this.$v.formData.name.required) {
return this.$t('validation.required')
}
if (!this.$v.formData.name.minLength) {
return this.$tc(
'validation.name_min_length',
this.$v.formData.name.$params.minLength.min,
{ count: this.$v.formData.name.$params.minLength.min }
)
}
},
emailError() {
if (!this.$v.formData.email.$error) {
return ''
}
if (!this.$v.formData.email.email) {
return this.$tc('validation.email_incorrect')
}
if (!this.$v.formData.email.required) {
return this.$tc('validation.required')
}
},
passwordError() {
if (!this.$v.formData.password.$error) {
return ''
}
if (!this.$v.formData.password.required) {
return this.$t('validation.required')
}
if (!this.$v.formData.password.minLength) {
return this.$tc(
'validation.password_min_length',
this.$v.formData.password.$params.minLength.min,
{ count: this.$v.formData.password.$params.minLength.min }
)
}
},
},
created() {
if (!this.isSuperAdmin) {
this.$router.push('/admin/dashboard')
}
if (this.isEdit) {
this.loadEditData()
}
},
mounted() {
this.$v.formData.$reset()
},
validations: {
formData: {
name: {
required,
minLength: minLength(3),
},
email: {
email,
required,
},
password: {
required: requiredIf(function () {
return !this.isEdit
}),
minLength: minLength(8),
},
},
},
methods: {
...mapActions('users', ['addUser', 'fetchUser', 'updateUser']),
async loadEditData() {
let response = await this.fetchUser(this.$route.params.id)
if (response.data) {
this.formData = { ...this.formData, ...response.data.user }
}
},
async submitUser() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
try {
let response
this.isLoading = true
if (this.isEdit) {
response = await this.updateUser(this.formData)
let data
if (response.data.success) {
window.toastr['success'](this.$tc('users.updated_message'))
this.$router.push('/admin/users')
this.isLoading = false
}
if (response.data.error) {
window.toastr['error'](this.$t('validation.email_already_taken'))
}
} else {
response = await this.addUser(this.formData)
let data
if (response.data.success) {
this.isLoading = false
if (!this.isEdit) {
window.toastr['success'](this.$tc('users.created_message'))
this.$router.push('/admin/users')
return true
}
}
}
} catch (err) {
if (err.response.data.errors.email) {
this.isLoading = false
}
}
},
},
}
</script>

View File

@@ -0,0 +1,439 @@
<template>
<base-page v-if="isSuperAdmin" class="items">
<sw-page-header :title="$t('users.title')">
<sw-breadcrumb slot="breadcrumbs">
<sw-breadcrumb-item to="dashboard" :title="$t('general.home')" />
<sw-breadcrumb-item to="#" :title="$tc('users.title', 2)" active />
</sw-breadcrumb>
<template slot="actions">
<sw-button
v-show="totalUsers"
variant="primary-outline"
size="lg"
@click="toggleFilter"
>
{{ $t('general.filter') }}
<component :is="filterIcon" class="w-4 h-4 ml-2 -mr-1" />
</sw-button>
<sw-button
tag-name="router-link"
to="users/create"
variant="primary"
size="lg"
class="ml-4"
>
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('users.add_user') }}
</sw-button>
</template>
</sw-page-header>
<slide-y-up-transition>
<sw-filter-wrapper v-show="showFilters" class="mt-3">
<sw-input-group :label="$tc('users.name')" class="flex-1 mt-2 mr-4">
<sw-input
v-model="filters.name"
type="text"
name="name"
class="mt-2"
autocomplete="off"
/>
</sw-input-group>
<sw-input-group :label="$tc('users.email')" class="flex-1 mt-2 mr-4">
<sw-input
v-model="filters.email"
type="text"
name="email"
class="mt-2"
autocomplete="off"
/>
</sw-input-group>
<sw-input-group :label="$tc('users.phone')" class="flex-1 mt-2">
<sw-input
v-model="filters.phone"
type="text"
name="phone"
class="mt-2"
autocomplete="off"
/>
</sw-input-group>
<label
class="absolute text-sm leading-snug text-gray-900 cursor-pointer"
style="top: 10px; right: 15px"
@click="clearFilter"
>
{{ $t('general.clear_all') }}</label
>
</sw-filter-wrapper>
</slide-y-up-transition>
<sw-empty-table-placeholder
v-show="showEmptyScreen"
:title="$t('users.no_users')"
:description="$t('users.list_of_users')"
>
<astronaut-icon class="mt-5 mb-4" />
<sw-button
slot="actions"
tag-name="router-link"
to="/admin/users/create"
size="lg"
variant="primary-outline"
>
<plus-icon class="w-6 h-6 mr-1 -ml-2" />
{{ $t('users.add_user') }}
</sw-button>
</sw-empty-table-placeholder>
<div class="relative table-container" v-show="!showEmptyScreen">
<div
class="relative flex items-center justify-between h-10 mt-5 list-none border-b-2 border-gray-200 border-solid"
>
<p class="text-sm">
{{ $t('general.showing') }}: <b>{{ users.length }}</b>
{{ $t('general.of') }}
<b>{{ totalUsers }}</b>
</p>
<sw-transition type="fade">
<sw-dropdown v-if="selectedUsers.length">
<span
slot="activator"
class="flex block text-sm font-medium cursor-pointer select-none text-primary-400"
>
{{ $t('general.actions') }}
<chevron-down-icon class="h-5" />
</span>
<sw-dropdown-item @click="removeMultipleUsers">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</sw-transition>
</div>
<div class="absolute z-10 items-center pl-4 mt-2 select-none md:mt-12">
<sw-checkbox
v-model="selectAllFieldStatus"
variant="primary"
size="sm"
class="hidden md:inline"
@change="selectAllUsers"
/>
<sw-checkbox
v-model="selectAllFieldStatus"
:label="$t('general.select_all')"
variant="primary"
size="sm"
class="md:hidden"
@change="selectAllUsers"
/>
</div>
<sw-table-component
ref="table"
:data="fetchData"
:show-filter="false"
table-class="table"
>
<sw-table-column
:sortable="false"
:filterable="false"
cell-class="no-click"
>
<div slot-scope="row" class="custom-control custom-checkbox">
<sw-checkbox
:id="row.id"
v-model="selectField"
:value="row.id"
variant="primary"
size="sm"
/>
</div>
</sw-table-column>
<sw-table-column :sortable="true" :label="$t('users.name')" show="name">
<template slot-scope="row">
<span>{{ $t('users.name') }}</span>
<router-link
:to="{ path: `users/${row.id}/edit` }"
class="font-medium text-primary-500"
>
{{ row.name }}
</router-link>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('users.email')"
show="email"
/>
<sw-table-column
:sortable="true"
:label="$t('users.phone')"
show="phone"
>
<template slot-scope="row">
<span>{{ $t('users.phone') }}</span>
<span>{{ row.phone ? row.phone : 'No Contact' }} </span>
</template>
</sw-table-column>
<sw-table-column
:sortable="true"
:label="$t('users.added_on')"
sort-as="created_at"
show="formattedCreatedAt"
/>
<sw-table-column
:sortable="true"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span> {{ $t('users.action') }} </span>
<sw-dropdown>
<dot-icon slot="activator" />
<sw-dropdown-item
tag-name="router-link"
:to="`users/${row.id}/edit`"
>
<pencil-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.edit') }}
</sw-dropdown-item>
<sw-dropdown-item @click="removeUser(row.id)">
<trash-icon class="h-5 mr-3 text-gray-600" />
{{ $t('general.delete') }}
</sw-dropdown-item>
</sw-dropdown>
</template>
</sw-table-column>
</sw-table-component>
</div>
</base-page>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import AstronautIcon from '@/components/icon/AstronautIcon'
import {
FilterIcon,
XIcon,
ChevronDownIcon,
PencilIcon,
TrashIcon,
PlusIcon,
} from '@vue-hero-icons/solid'
export default {
components: {
AstronautIcon,
FilterIcon,
XIcon,
ChevronDownIcon,
PencilIcon,
TrashIcon,
PlusIcon,
},
data() {
return {
id: null,
showFilters: false,
sortedBy: 'created_at',
isRequestOngoing: true,
filters: {
name: '',
email: '',
phone: '',
},
}
},
computed: {
...mapGetters('user', ['currentUser']),
...mapGetters('users', [
'users',
'selectedUsers',
'totalUsers',
'selectAllField',
]),
isSuperAdmin() {
return this.currentUser.role == 'super admin'
},
showEmptyScreen() {
return !this.totalUsers && !this.isRequestOngoing
},
filterIcon() {
return this.showFilters ? 'x-icon' : 'filter-icon'
},
selectField: {
get: function () {
return this.selectedUsers
},
set: function (val) {
this.selectedUser(val)
},
},
selectAllFieldStatus: {
get: function () {
return this.selectAllField
},
set: function (val) {
this.setSelectAllState(val)
},
},
},
created() {
if (!this.isSuperAdmin) {
this.$router.push('/admin/dashboard')
}
},
watch: {
filters: {
handler: 'setFilters',
deep: true,
},
},
destroyed() {
if (this.selectAllField) {
this.selectAllUsers()
}
},
methods: {
...mapActions('users', [
'fetchUsers',
'selectAllUsers',
'selectedUser',
'deleteUser',
'deleteMultipleUsers',
'setSelectAllState',
]),
refreshTable() {
this.$refs.table.refresh()
},
async fetchData({ page, filter, sort }) {
let data = {
display_name: this.filters.name !== null ? this.filters.name : '',
phone: this.filters.phone !== null ? this.filters.phone : '',
email: this.filters.email !== null ? this.filters.email : '',
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page,
}
this.isRequestOngoing = true
let response = await this.fetchUsers(data)
this.isRequestOngoing = false
return {
data: response.data.users.data,
pagination: {
totalPages: response.data.users.last_page,
currentPage: page,
},
}
},
setFilters() {
this.refreshTable()
},
clearFilter() {
this.filters = {
name: '',
email: '',
phone: '',
}
},
toggleFilter() {
if (this.showFilters) {
this.clearFilter()
}
this.showFilters = !this.showFilters
},
async removeUser(id) {
let user = []
user.push(id)
swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('users.confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
}).then(async (willDelete) => {
if (willDelete) {
let res = await this.deleteUser(user)
if (res.data.success) {
window.toastr['success'](this.$tc('users.deleted_message', 1))
this.$refs.table.refresh()
return true
}
if (res.data.error === 'user_attached') {
window.toastr['error'](
this.$tc('users.user_attached_message'),
this.$t('general.action_failed')
)
return true
}
window.toastr['error'](res.data.message)
return true
}
})
},
async removeMultipleUsers() {
swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('users.confirm_delete', 2),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true,
}).then(async (willDelete) => {
if (willDelete) {
let res = await this.deleteMultipleUsers()
if (res.data.success || res.data.users) {
window.toastr['success'](this.$tc('users.deleted_message', 2))
this.$refs.table.refresh()
} else if (res.data.error) {
window.toastr['error'](res.data.message)
}
}
})
},
},
}
</script>

View File

@@ -1,266 +0,0 @@
<template>
<div class="card-body">
<form action="" @submit.prevent="next()">
<p class="form-title">{{ $t('wizard.company_info') }}</p>
<p class="form-desc">{{ $t('wizard.company_info_desc') }}</p>
<div class="row mb-4">
<div class="col-md-6">
<label class="input-label">{{ $tc('settings.company_info.company_logo') }}</label>
<div id="pick-avatar" class="image-upload-box">
<div class="overlay">
<font-awesome-icon class="white-icon" icon="camera"/>
</div>
<img v-if="previewLogo" :src="previewLogo" class="preview-logo">
<div v-else class="upload-content">
<font-awesome-icon class="upload-icon" icon="cloud-upload-alt"/>
<p class="upload-text"> {{ $t('general.choose_file') }} </p>
</div>
</div>
</div>
<avatar-cropper
:labels="{ submit: 'Submit', cancel: 'Cancel'}"
:cropper-options="cropperOptions"
:output-options="cropperOutputOptions"
:output-quality="0.8"
:upload-handler="cropperHandler"
trigger="#pick-avatar"
@changed="setFileObject"
@error="handleUploadError"
/>
</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.company_name') }}</label><span class="text-danger"> *</span>
<base-input
:invalid="$v.companyData.name.$error"
v-model.trim="companyData.name"
type="text"
name="name"
@input="$v.companyData.name.$touch()"
/>
<div v-if="$v.companyData.name.$error">
<span v-if="!$v.companyData.name.required" class="text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.country') }}</label><span class="text-danger"> *</span>
<base-select
v-model="country"
:class="{'error': $v.companyData.country_id.$error }"
:options="countries"
:searchable="true"
:allow-empty="false"
:show-labels="false"
:placeholder="$t('general.select_country')"
track-by="id"
label="name"
/>
<div v-if="$v.companyData.country_id.$error">
<span v-if="!$v.companyData.country_id.required" class="text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.state') }}</label>
<base-input
v-model="companyData.state"
name="state"
type="text"
/>
</div>
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.city') }}</label>
<base-input
v-model="companyData.city"
name="city"
type="text"
/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.address') }}</label>
<base-text-area
:invalid="$v.companyData.address_street_1.$error"
v-model.trim="companyData.address_street_1"
:placeholder="$t('general.street_1')"
name="billing_street1"
rows="2"
@input="$v.companyData.address_street_1.$touch()"
/>
<div v-if="$v.companyData.address_street_1.$error">
<span v-if="!$v.companyData.address_street_1.maxLength" class="text-danger">{{ $t('validation.description_maxlength') }}</span>
</div>
<base-text-area
:invalid="$v.companyData.address_street_2.$error"
v-model="companyData.address_street_2"
:placeholder="$t('general.street_2')"
name="billing_street2"
rows="2"
@input="$v.companyData.address_street_2.$touch()"
/>
<div v-if="$v.companyData.address_street_2.$error">
<span v-if="!$v.companyData.address_street_2.maxLength" class="text-danger">{{ $t('validation.description_maxlength') }}</span>
</div>
</div>
<div class="col-md-6">
<div class="row">
<div class="col-md-12">
<label class="form-label">{{ $t('wizard.zip_code') }}</label>
<base-input
v-model.trim="companyData.zip"
type="text"
name="zip"
/>
</div>
</div>
<div class="row">
<div class="col-md-12">
<label class="form-label">{{ $t('wizard.phone') }}</label>
<base-input
v-model.trim="companyData.phone"
type="text"
name="phone"
/>
</div>
</div>
</div>
</div>
<base-button
:loading="loading"
class="pull-right"
icon="save"
color="theme"
type="submit"
>
{{ $t('wizard.save_cont') }}
</base-button>
</form>
</div>
</template>
<script>
import MultiSelect from 'vue-multiselect'
import AvatarCropper from 'vue-avatar-cropper'
import { validationMixin } from 'vuelidate'
const { required, maxLength } = require('vuelidate/lib/validators')
export default {
components: {
MultiSelect,
AvatarCropper
},
mixins: [validationMixin],
data () {
return {
cropperOutputOptions: {
width: 150,
height: 150
},
cropperOptions: {
autoCropArea: 1,
viewMode: 0,
movable: true,
zoomable: true
},
companyData: {
logo: '',
name: null,
address_street_1: '',
address_street_2: '',
city: '',
state: '',
country_id: '',
zip: '',
phone: ''
},
loading: false,
step: 1,
countries: [],
country: null,
previewLogo: null
}
},
validations: {
companyData: {
name: {
required
},
country_id: {
required
},
address_street_1: {
maxLength: maxLength(255)
},
address_street_2: {
maxLength: maxLength(255)
}
}
},
watch: {
country ({ id }) {
this.companyData.country_id = id
return true
}
},
mounted () {
this.fetchCountry()
},
methods: {
cropperHandler (cropper) {
this.previewLogo = cropper.getCroppedCanvas().toDataURL(this.cropperOutputMime)
},
setFileObject (file) {
this.fileObject = file
},
handleUploadError (message, type, xhr) {
window.toastr['error']('Oops! Something went wrong...')
},
async next () {
this.$v.companyData.$touch()
if (this.$v.companyData.$invalid) {
return true
}
this.loading = true
let response = await window.axios.post('/api/admin/onboarding/company', this.companyData)
if (response.data) {
if (this.fileObject && this.previewLogo) {
let logoData = new FormData()
logoData.append('company_logo', JSON.stringify({
name: this.fileObject.name,
data: this.previewLogo
}))
await axios.post('/api/admin/onboarding/company/upload-logo', logoData, {
headers: {
'Content-Type': 'multipart/form-data',
'company': response.data.user.company.id
}
})
}
this.$emit('next')
this.loading = false
}
},
onFileChange (e) {
var input = event.target
this.companyData.logo = input.files[0]
if (input.files && input.files[0]) {
var reader = new FileReader()
reader.onload = (e) => {
this.previewLogo = e.target.result
}
reader.readAsDataURL(input.files[0])
}
},
async fetchCountry () {
let res = await window.axios.get('/api/countries')
if (res) {
this.countries = res.data.countries
}
}
}
}
</script>

View File

@@ -1,219 +0,0 @@
<template>
<div class="card-body">
<form action="" @submit.prevent="next()">
<p class="form-title">{{ $t('wizard.database.database') }}</p>
<p class="form-desc">{{ $t('wizard.database.desc') }}</p>
<div class="row mt-5">
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.database.app_url') }}</label>
<span class="text-danger"> * </span>
<base-input
:invalid="$v.databaseData.app_url.$error"
v-model.trim="databaseData.app_url"
type="text"
name="name"
@input="$v.databaseData.app_url.$touch()"
/>
<div v-if="$v.databaseData.app_url.$error">
<span v-if="!$v.databaseData.app_url.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
<span v-if="!$v.databaseData.app_url.isUrl" class="text-danger">
{{ $tc('validation.invalid_url') }}
</span>
</div>
</div>
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.database.connection') }}</label>
<span class="text-danger"> *</span>
<base-select
v-model="databaseData.database_connection"
:invalid="$v.databaseData.database_connection.$error"
:options="connections"
:searchable="true"
:show-labels="false"
@change="$v.databaseData.database_connection.$touch()"
/>
<div v-if="$v.databaseData.database_connection.$error">
<span v-if="!$v.databaseData.database_connection.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.database.port') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.databaseData.database_port.$error"
v-model.trim="databaseData.database_port"
type="text"
name="database_port"
@input="$v.databaseData.database_port.$touch()"
/>
<div v-if="$v.databaseData.database_port.$error">
<span v-if="!$v.databaseData.database_port.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
<span v-if="!$v.databaseData.database_port.numeric" class="text-danger">
{{ $tc('validation.numbers_only') }}
</span>
</div>
</div>
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.database.db_name') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.databaseData.database_name.$error"
v-model.trim="databaseData.database_name"
type="text"
name="database_name"
@input="$v.databaseData.database_name.$touch()"
/>
<div v-if="$v.databaseData.database_name.$error">
<span v-if="!$v.databaseData.database_name.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.database.username') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.databaseData.database_username.$error"
v-model.trim="databaseData.database_username"
type="text"
name="database_username"
@input="$v.databaseData.database_username.$touch()"
/>
<div v-if="$v.databaseData.database_username.$error">
<span v-if="!$v.databaseData.database_username.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.database.password') }}</label>
<base-input
v-model.trim="databaseData.database_password"
type="password"
name="name"
/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.database.host') }}</label>
<span class="text-danger"> *</span>
<base-input
:invalid="$v.databaseData.database_hostname.$error"
v-model.trim="databaseData.database_hostname"
type="text"
name="database_hostname"
@input="$v.databaseData.database_hostname.$touch()"
/>
<div v-if="$v.databaseData.database_hostname.$error">
<span v-if="!$v.databaseData.database_hostname.required" class="text-danger">
{{ $tc('validation.required') }}
</span>
</div>
</div>
</div>
<base-button
:loading="loading"
class="pull-right mt-5"
icon="save"
color="theme"
type="submit"
>
{{ $t('wizard.save_cont') }}
</base-button>
</form>
</div>
</template>
<script>
import MultiSelect from 'vue-multiselect'
import { validationMixin } from 'vuelidate'
const { required, numeric, url } = require('vuelidate/lib/validators')
export default {
components: {
MultiSelect
},
mixins: [validationMixin],
data () {
return {
databaseData: {
database_connection: 'mysql',
database_hostname: '127.0.0.1',
database_port: '3306',
database_name: null,
database_username: null,
database_password: null,
app_url: window.location.origin
},
loading: false,
connections: [
'sqlite',
'mysql',
'pgsql',
'sqlsrv'
]
}
},
validations: {
databaseData: {
database_connection: {
required
},
database_hostname: {
required
},
database_port: {
required,
numeric
},
database_name: {
required
},
database_username: {
required
},
app_url: {
required,
isUrl (val) {
return this.$utils.checkValidUrl(val)
}
}
}
},
methods: {
async next () {
this.$v.databaseData.$touch()
if (this.$v.databaseData.$invalid) {
return true
}
this.loading = true
try {
let response = await window.axios.post('/api/admin/onboarding/environment/database', this.databaseData)
if (response.data.success) {
this.$emit('next')
window.toastr['success'](this.$t('wizard.success.' + response.data.success))
return true
} else if (response.data.error) {
window.toastr['error'](this.$t('wizard.errors.' + response.data.error))
} else if (response.data.error_message) {
window.toastr['error'](response.data.error_message)
}
} catch (e) {
window.toastr['error'](e.response.data.message)
} finally {
this.loading = false
}
}
}
}
</script>

View File

@@ -1,78 +0,0 @@
<template>
<div class="card-body">
<form action="" @submit.prevent="next()">
<p class="form-title">{{ $t('wizard.mail.mail_config') }}</p>
<p class="form-desc">{{ $t('wizard.mail.mail_config_desc') }}</p>
<component
:is="mail_driver"
:config-data="mailConfigData"
:loading="loading"
:mail-drivers="mail_drivers"
@on-change-driver="(val) => mail_driver = mailConfigData.mail_driver = val"
@submit-data="next"
/>
</form>
</div>
</template>
<script>
import MultiSelect from 'vue-multiselect'
import { validationMixin } from 'vuelidate'
import Smtp from './mailDriver/Smtp'
import Mailgun from './mailDriver/Mailgun'
import Ses from './mailDriver/Ses'
import Basic from './mailDriver/Basic'
export default {
components: {
MultiSelect,
Smtp,
Mailgun,
Ses,
sendmail: Basic,
mail: Basic
},
mixins: [validationMixin],
data () {
return {
mailConfigData: {
mail_driver: 'mail'
},
mail_driver: 'mail',
loading: false,
mail_drivers: []
}
},
created () {
this.getMailDrivers()
},
methods: {
async getMailDrivers () {
this.loading = true
let response = await window.axios.get('/api/admin/onboarding/environment/mail')
if (response.data) {
this.mail_drivers = response.data
this.loading = false
}
},
async next (mailConfigData) {
this.loading = true
try {
let response = await window.axios.post('/api/admin/onboarding/environment/mail', mailConfigData)
if (response.data.success) {
this.$emit('next')
window.toastr['success'](this.$t('wizard.success.' + response.data.success))
} else {
window.toastr['error'](this.$t('wizard.errors.' + response.data.error))
}
this.loading = false
return true
} catch (e) {
window.toastr['error']('Something went wrong')
}
}
}
}
</script>

View File

@@ -1,113 +0,0 @@
<template>
<div class="wizard">
<div class="step-indicator">
<img
id="logo-crater"
src="/assets/img/crater-logo.png"
alt="Crater Logo"
class="logo-main"
>
<div class="indicator-line">
<div class="center">
<div class="steps" :class="{'active': step === 1, 'completed': step > 1}">
<font-awesome-icon v-if="step > 1" icon="check" class="icon-check"/>
</div>
<div class="steps" :class="{'active': step === 2, 'completed': step > 2}">
<font-awesome-icon v-if="step > 2" icon="check" class="icon-check"/>
</div>
<div class="steps" :class="{'active': step === 3, 'completed': step > 3}">
<font-awesome-icon v-if="step > 3" icon="check" class="icon-check"/>
</div>
<div class="steps" :class="{'active': step === 4, 'completed': step > 4}">
<font-awesome-icon v-if="step > 4" icon="check" class="icon-check"/>
</div>
<div class="steps" :class="{'active': step === 5, 'completed': step > 5}">
<font-awesome-icon v-if="step > 5" icon="check" class="icon-check"/>
</div>
<div class="steps" :class="{'active': step === 6, 'completed': step > 6}">
<font-awesome-icon v-if="step > 6" icon="check" class="icon-check"/>
</div>
<div class="steps" :class="{'active': step === 7, 'completed': step > 7}">
<font-awesome-icon v-if="step > 7" icon="check" class="icon-check"/>
</div>
</div>
</div>
</div>
<div class="form-content">
<div class="card wizard-card">
<component
:is="tab"
@next="setTab"
/>
</div>
</div>
</div>
</template>
<script>
import SystemRequirement from './SystemRequirement'
import Permission from './Permission'
import Database from './Database'
import EmailConfiguration from './EmailConfiguration'
import UserProfile from './UserProfile'
import CompanyInfo from './CompanyInfo'
import Settings from './Settings'
export default {
components: {
step_1: SystemRequirement,
step_2: Permission,
step_3: Database,
step_4: EmailConfiguration,
step_5: UserProfile,
step_6: CompanyInfo,
step_7: Settings
},
data () {
return {
loading: false,
tab: 'step_1',
step: 1
}
},
created () {
this.getOnboardingData()
},
methods: {
async getOnboardingData () {
let response = await window.axios.get('/api/admin/onboarding')
if (response.data) {
if (response.data.profile_complete === 'COMPLETED') {
this.$router.push('/admin/dashboard')
return
}
let dbStep = parseInt(response.data.profile_complete)
if (dbStep) {
this.step = dbStep + 1
this.tab = `step_${dbStep + 1}`
}
this.languages = response.data.languages
this.currencies = response.data.currencies
this.dateFormats = response.data.date_formats
this.timeZones = response.data.time_zones
// this.settingData.currency = this.currencies.find(currency => currency.id === 1)
// this.settingData.language = this.languages.find(language => language.code === 'en')
// this.settingData.dateFormat = this.dateFormats.find(dateFormat => dateFormat.value === 'd M Y')
}
},
setTab (data) {
this.step++
if (this.step <= 7) {
this.tab = 'step_' + this.step
} else {
// window.location.reload()
}
}
}
}
</script>

View File

@@ -1,86 +0,0 @@
<template>
<div class="card-body permissions">
<p class="form-title">{{ $t('wizard.permissions.permissions') }}</p>
<p class="form-desc">{{ $t('wizard.permissions.permission_desc') }}</p>
<div class="d-flex justify-content-start">
<div class="lists col-md-6">
<div
v-for="(permission, index) in permissions"
:key="index"
class="row list-items"
>
<div class="col-sm-9 left-item">
{{ permission.folder }}
</div>
<div class="col-sm-3 right-item">
<span v-if="permission.isSet" class="verified"/>
<span v-else class="not-verified"/>
<span>{{ permission.permission }}</span>
</div>
</div>
</div>
</div>
<base-button
v-if="isContinue"
class="pull-right mt-5"
icon="arrow-right"
right-icon
color="theme"
@click="next"
>
{{ $t('wizard.continue') }}
</base-button>
</div>
</template>
<script>
export default {
data () {
return {
loading: false,
permissions: [],
errors: false,
isContinue: false
}
},
created () {
this.getPermissions()
},
methods: {
async getPermissions () {
this.loading = true
let response = await window.axios.get('/api/admin/onboarding/permissions', this.profileData)
if (response.data) {
this.permissions = response.data.permissions.permissions
this.errors = response.data.permissions.errors
let self = this
if (this.errors) {
swal({
title: this.$t('wizard.permissions.permission_confirm_title'),
text: this.$t('wizard.permissions.permission_confirm_desc'),
icon: 'warning',
buttons: true,
dangerMode: true
}).then(async (willConfirm) => {
if (willConfirm) {
self.isContinue = true
}
})
} else {
this.isContinue = true
}
this.loading = false
}
},
async next () {
this.loading = true
await this.$emit('next')
this.loading = false
}
}
}
</script>

View File

@@ -1,219 +0,0 @@
<template>
<div class="card-body">
<form action="" @submit.prevent="next()">
<p class="form-title">{{ $t('wizard.preferences') }}</p>
<p class="form-desc">{{ $t('wizard.preferences_desc') }}</p>
<div class="row">
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.currency') }}</label>
<span class="text-danger"> *</span>
<base-select
v-model="settingData.currency"
:class="{'error': $v.settingData.currency.$error }"
:options="currencies"
:custom-label="currencyNameWithCode"
:searchable="true"
:show-labels="false"
:placeholder="$t('settings.currencies.select_currency')"
track-by="id"
label="name"
@input="$v.settingData.currency.$touch()"
/>
<div v-if="$v.settingData.currency.$error">
<span v-if="!$v.settingData.currency.required" class="text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.language') }}</label><span class="text-danger"> *</span>
<base-select
v-model="settingData.language"
:class="{'error': $v.settingData.language.$error }"
:options="languages"
:searchable="true"
:show-labels="false"
:placeholder="$t('settings.preferences.select_language')"
label="name"
@input="$v.settingData.language.$touch()"
/>
<div v-if="$v.settingData.language.$error">
<span v-if="!$v.settingData.language.required" class="text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.date_format') }}</label><span class="text-danger"> *</span>
<base-select
v-model="settingData.dateFormat"
:class="{'error': $v.settingData.dateFormat.$error }"
:options="dateFormats"
:searchable="true"
:show-labels="false"
:placeholder="$t('settings.preferences.select_date_formate')"
label="display_date"
@input="$v.settingData.dateFormat.$touch()"
/>
<div v-if="$v.settingData.dateFormat.$error">
<span v-if="!$v.settingData.dateFormat.required" class="text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.time_zone') }}</label><span class="text-danger"> *</span>
<base-select
v-model="settingData.timeZone"
:class="{'error': $v.settingData.timeZone.$error }"
:options="timeZones"
:searchable="true"
:show-labels="false"
:placeholder="$t('settings.preferences.select_date_formate')"
label="key"
@input="$v.settingData.timeZone.$touch()"
/>
<div v-if="$v.settingData.timeZone.$error">
<span v-if="!$v.settingData.timeZone.required" class="text-danger">{{ $tc('validation.required') }}</span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.fiscal_year') }}</label><span class="text-danger"> *</span>
<base-select
v-model="settingData.fiscalYear"
:class="{'error': $v.settingData.fiscalYear.$error }"
:options="fiscalYears"
:searchable="true"
:show-labels="false"
:placeholder="$t('settings.preferences.select_financial_year')"
label="key"
@input="$v.settingData.fiscalYear.$touch()"
/>
<div v-if="$v.settingData.fiscalYear.$error">
<span v-if="!$v.settingData.fiscalYear.required" class="text-danger">{{ $tc('customers.errors.required') }}</span>
</div>
</div>
</div>
<base-button :loading="loading" class="pull-right" icon="save" color="theme" type="submit">
{{ $t('wizard.save_cont') }}
</base-button>
</form>
</div>
</template>
<script>
import MultiSelect from 'vue-multiselect'
import { validationMixin } from 'vuelidate'
import Ls from '../../services/ls'
import { mapActions } from 'vuex'
const { required, minLength, email } = require('vuelidate/lib/validators')
export default {
components: {
MultiSelect
},
mixins: [validationMixin],
data () {
return {
settingData: {
language: null,
currency: null,
timeZone: null,
dateFormat: null,
fiscalYear: null
},
loading: false,
step: 1,
languages: [],
currencies: [],
timeZones: [],
dateFormats: [],
fiscalYears: []
}
},
validations: {
settingData: {
currency: {
required
},
language: {
required
},
dateFormat: {
required
},
timeZone: {
required
},
fiscalYear: {
required
}
}
},
mounted () {
this.getOnboardingData()
},
methods: {
currencyNameWithCode ({name, code}) {
return `${code} - ${name}`
},
...mapActions('auth', [
'loginOnBoardingUser'
]),
async getOnboardingData () {
let response = await window.axios.get('/api/admin/onboarding')
if (response.data) {
if (response.data.profile_complete === 'COMPLETED') {
this.$router.push('/admin/dashboard')
return
}
let dbStep = parseInt(response.data.profile_complete)
if (dbStep) {
this.step = dbStep + 1
}
this.languages = response.data.languages
this.currencies = response.data.currencies
this.dateFormats = response.data.date_formats
this.timeZones = response.data.time_zones
this.fiscalYears = response.data.fiscal_years
this.settingData.currency = this.currencies.find(currency => currency.id === 1)
this.settingData.language = this.languages.find(language => language.code === 'en')
this.settingData.dateFormat = response.data.date_formats.find(dateFormat => dateFormat.carbon_format_value == 'd M Y')
this.settingData.timeZone = this.timeZones.find(timeZone => timeZone.value === 'UTC')
this.settingData.fiscalYear = this.fiscalYears.find(fiscalYear => fiscalYear.value === '1-12')
}
},
async next () {
this.$v.settingData.$touch()
if (this.$v.settingData.$invalid) {
return true
}
this.loading = true
let data = {
currency: this.settingData.currency.id,
time_zone: this.settingData.timeZone.value,
language: this.settingData.language.code,
fiscal_year: this.settingData.fiscalYear.value,
carbon_date_format: this.settingData.dateFormat.carbon_format_value,
moment_date_format: this.settingData.dateFormat.moment_format_value
}
let response = await window.axios.post('/api/admin/onboarding/settings', data)
if (response.data) {
// this.$emit('next')
this.loading = false
Ls.set('auth.token', response.data.token)
this.loginOnBoardingUser(response.data.token)
window.toastr['success']('Login Successful')
this.$router.push('/admin/dashboard')
}
}
}
}
</script>

View File

@@ -1,112 +0,0 @@
<template>
<div class="card-body">
<p class="form-title">{{ $t('wizard.req.system_req') }}</p>
<p class="form-desc">{{ $t('wizard.req.system_req_desc') }}</p>
<div v-if="phpSupportInfo" class="d-flex justify-content-start">
<div class="col-md-6">
<div class="row list-items">
<div class="col-md-9 left-item">
{{ $t('wizard.req.php_req_version', { version: phpSupportInfo.minimum }) }}
</div>
<div class="col-md-3 right-item justify-content-end">
{{ phpSupportInfo.current }}
<span v-if="phpSupportInfo.supported" class="verified"/>
<span v-else class="not-verified"/>
</div>
</div>
</div>
</div>
<div v-if="requirements" class="d-flex justify-content-start">
<div class="col-md-6">
<div
v-for="(requirement, index) in requirements"
:key="index"
class="row list-items"
>
<div class="col-md-9 left-item">
{{ index }}
</div>
<div class="col-md-3 right-item justify-content-end">
<span v-if="requirement" class="verified"/>
<span v-else class="not-verified"/>
</div>
</div>
</div>
</div>
<base-button
v-if="hasNext"
:loading="loading"
class="pull-right mt-4"
icon="arrow-right"
color="theme"
right-icon
@click="next"
>
{{ $t('wizard.continue') }}
</base-button>
<base-button
v-if="!requirements"
:loading="loading"
class="pull-right mt-4"
color="theme"
@click="getRequirements"
>
{{ $t('wizard.req.check_req') }}
</base-button>
</div>
</template>
<script>
import MultiSelect from 'vue-multiselect'
import { validationMixin } from 'vuelidate'
export default {
components: {
MultiSelect
},
mixins: [validationMixin],
data () {
return {
requirements: null,
phpSupportInfo: null,
loading: false,
isShow: true
}
},
computed: {
hasNext () {
if (this.requirements) {
let isRequired = true
for (const key in this.requirements) {
if (!this.requirements[key]) {
isRequired = false
}
}
return this.requirements && this.phpSupportInfo.supported && isRequired
}
return false
}
},
methods: {
listToggle () {
this.isShow = !this.isShow
},
async getRequirements () {
this.loading = true
let response = await window.axios.get('/api/admin/onboarding/requirements', this.profileData)
if (response.data) {
this.requirements = response.data.requirements.requirements.php
this.phpSupportInfo = response.data.phpSupportInfo
this.loading = false
}
},
async next () {
this.loading = true
await this.$emit('next')
this.loading = false
}
}
}
</script>

View File

@@ -1,204 +0,0 @@
<template>
<div class="card-body">
<form action="" @submit.prevent="next()">
<p class="form-title">{{ $t('wizard.account_info') }}</p>
<p class="form-desc">{{ $t('wizard.account_info_desc') }}</p>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label">{{ $tc('settings.account_settings.profile_picture') }}</label>
<div id="pick-avatar" class="image-upload-box avatar-upload">
<div class="overlay">
<font-awesome-icon class="white-icon" icon="camera"/>
</div>
<img v-if="previewAvatar" :src="previewAvatar" class="preview-logo">
<div v-else class="upload-content">
<font-awesome-icon class="upload-icon" icon="cloud-upload-alt"/>
<p class="upload-text"> {{ $tc('general.choose_file') }} </p>
</div>
</div>
</div>
<avatar-cropper
:labels="{ submit: 'submit', cancel: 'Cancel'}"
:cropper-options="cropperOptions"
:output-options="cropperOutputOptions"
:output-quality="0.8"
:upload-handler="cropperHandler"
trigger="#pick-avatar"
@changed="setFileObject"
@error="handleUploadError"
/>
</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.name') }}</label><span class="text-danger"> *</span>
<base-input
:invalid="$v.profileData.name.$error"
v-model.trim="profileData.name"
type="text"
name="name"
@input="$v.profileData.name.$touch()"
/>
<div v-if="$v.profileData.name.$error">
<span v-if="!$v.profileData.name.required" class="text-danger">{{ $tc('validation.required') }}</span>
<span v-if="!$v.profileData.name.minLength" class="text-danger"> {{ $tc('validation.name_min_length', $v.profileData.name.$params.minLength.min, { count: $v.profileData.name.$params.minLength.min }) }} </span>
</div>
</div>
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.email') }}</label><span class="text-danger"> *</span>
<base-input
:invalid="$v.profileData.email.$error"
v-model.trim="profileData.email"
type="text"
name="email"
@input="$v.profileData.email.$touch()"
/>
<div v-if="$v.profileData.email.$error">
<span v-if="!$v.profileData.email.required" class="text-danger">{{ $tc('validation.required') }}</span>
<span v-if="!$v.profileData.email.email" class="text-danger">{{ $tc('validation.email_incorrect') }}</span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.password') }}</label><span class="text-danger"> *</span>
<base-input
:invalid="$v.profileData.password.$error"
v-model.trim="profileData.password"
type="password"
name="password"
@input="$v.profileData.password.$touch()"
/>
<div v-if="$v.profileData.password.$error">
<span v-if="!$v.profileData.password.required" class="text-danger">{{ $tc('validation.required') }}</span>
<span v-if="!$v.profileData.password.minLength" class="text-danger"> {{ $tc('validation.password_min_length', $v.profileData.password.$params.minLength.min, {count: $v.profileData.password.$params.minLength.min}) }} </span>
</div>
</div>
<div class="col-md-6">
<label class="form-label">{{ $t('wizard.confirm_password') }}</label><span class="text-danger"> *</span>
<base-input
:invalid="$v.profileData.confirm_password.$error"
v-model.trim="profileData.confirm_password"
type="password"
name="confirm_password"
@input="$v.profileData.confirm_password.$touch()"
/>
<div v-if="$v.profileData.confirm_password.$error">
<span v-if="!$v.profileData.confirm_password.sameAsPassword" class="text-danger">{{ $tc('validation.password_incorrect') }}</span>
</div>
</div>
</div>
<base-button
:loading="loading"
class="pull-right mt-4"
icon="save"
color="theme"
type="submit"
>
{{ $t('wizard.save_cont') }}
</base-button>
</form>
</div>
</template>
<script>
import AvatarCropper from 'vue-avatar-cropper'
import { validationMixin } from 'vuelidate'
import { mapActions } from 'vuex'
const { required, requiredIf, sameAs, minLength, email } = require('vuelidate/lib/validators')
export default {
components: {
AvatarCropper
},
mixins: [validationMixin],
data () {
return {
cropperOutputOptions: {
width: 150,
height: 150
},
cropperOptions: {
autoCropArea: 1,
viewMode: 0,
movable: true,
zoomable: true
},
profileData: {
name: null,
email: null,
password: null,
confirm_password: null
},
loading: false,
previewAvatar: '/images/default-avatar.jpg',
fileObject: null
}
},
validations: {
profileData: {
name: {
required,
minLength: minLength(3)
},
email: {
email,
required
},
password: {
required,
minLength: minLength(8)
},
confirm_password: {
required: requiredIf('isRequired'),
sameAsPassword: sameAs('password')
}
}
},
computed: {
isRequired () {
if (this.profileData.password === null || this.profileData.password === undefined || this.profileData.password === '') {
return false
}
return true
}
},
methods: {
...mapActions('userProfile', [
'uploadOnboardAvatar'
]),
cropperHandler (cropper) {
this.previewAvatar = cropper.getCroppedCanvas().toDataURL(this.cropperOutputMime)
},
setFileObject (file) {
this.fileObject = file
},
handleUploadError (message, type, xhr) {
window.toastr['error']('Oops! Something went wrong...')
},
async next () {
this.$v.profileData.$touch()
if (this.$v.profileData.$invalid) {
return true
}
this.loading = true
let response = await window.axios.post('/api/admin/onboarding/profile', this.profileData)
console.log('user_id', response.data.user.id)
if (response.data) {
if (this.fileObject && this.previewAvatar) {
let avatarData = new FormData()
avatarData.append('admin_avatar', JSON.stringify({
name: this.fileObject.name,
data: this.previewAvatar,
id: response.data.user.id
}))
this.uploadOnboardAvatar(avatarData)
}
this.$emit('next')
this.loading = false
}
return true
}
}
}
</script>

Some files were not shown because too many files have changed in this diff Show More