v5.0.0 update

This commit is contained in:
Mohit Panjwani
2021-11-30 18:58:19 +05:30
parent d332712c22
commit 082d5cacf2
1253 changed files with 88309 additions and 71741 deletions

View File

@ -1,29 +0,0 @@
/**
* First we will load all of this project's JavaScript dependencies which
* include Vue and Vue Resource. This gives a great starting point for
* building robust, powerful web applications using Vue and Laravel.
*/
import Vue from 'vue'
import router from './router.js'
import store from './store/index'
import utils from './helpers/utilities'
import i18n from './plugins/i18n'
require('./bootstrap')
Vue.prototype.$utils = utils
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
window.hub = new Vue()
window.i18n = i18n
new Vue({
router,
store,
i18n,
}).$mount('#app')

View File

@ -1,172 +0,0 @@
import VueRouter from 'vue-router'
import Vuex from 'vuex'
import Ls from './services/ls'
import store from './store/index.js'
import Vue from 'vue'
import Vuelidate from 'vuelidate'
import money from 'v-money'
import VTooltip from 'v-tooltip'
import Transitions from 'vue2-transitions'
import SpaceWind from '@bytefury/spacewind'
import swal from 'vue-sweetalert2'
import 'sweetalert2/dist/sweetalert2.min.css'
/**
* Theme
*/
import theme from './components/theme'
/**
* Global css plugins
*/
Vue.use(SpaceWind, { theme })
Vue.use(Vuelidate)
Vue.use(swal, {
customClass: {
container:
'fixed z-50 inset-0 overflow-y-auto bg-black bg-opacity-25 flex justify-center min-h-screen items-center sm:p-0 swal2-container',
popup:
'flex items-center flex-col justify-center align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6',
header: 'swal2-header',
title: 'swal2-title',
closeButton: '',
icon: 'swal2-icon',
image: '',
content: 'swal2-content',
input: '',
inputLabel: '',
validationMessage: '',
actions: 'swal2-actions',
confirmButton:
'w-full inline-flex py-2 px-4 text-sm leading-5 rounded items-center justify-center text-white font-normal transition duration-150 ease-in-out border border-transparent focus:outline-none bg-primary-500 hover:bg-opacity-75 whitespace-nowrap',
denyButton: '',
cancelButton:
'w-full inline-flex py-2 px-4 text-sm leading-5 rounded justify-center items-center focus:outline-none font-normal transition ease-in-out duration-150 border border-transparent border border-solid border-primary-500 text-primary-500 hover:bg-primary-200 shadow-inner whitespace-nowrap',
loader: '',
footer: '',
},
buttonsStyling: false,
})
Vue.use(Transitions)
window._ = require('lodash')
/**
* Custom Directives
*/
require('./helpers/directives')
/**
* Base Components
*/
require('./components/base')
/**
* We'll register a HTTP interceptor to attach the "CSRF" header to each of
* the outgoing requests issued by this application. The CSRF middleware
* included with Laravel will automatically verify the header's value.
*/
window.axios = require('axios')
window.axios.defaults.withCredentials = true
window.Ls = Ls
window.axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest',
}
/**
* Interceptors
*/
window.axios.interceptors.request.use(
function (config) {
if (store.getters['auth/isLoggedOut']) {
let source = window.axios.CancelToken.source()
config.cancelToken = source.token
setTimeout(() => {
store.dispatch('auth/setLogoutFalse')
}, 200)
return config
}
// Do something before request is sent
const companyId = Ls.get('selectedCompany')
if (companyId) {
config.headers.common['company'] = companyId
}
return config
},
function (error) {
// Do something with request error
return Promise.reject(error)
}
)
/**
* Global Axios Response Interceptor
*/
global.axios.interceptors.response.use(undefined, function (err) {
// Do something with request error
if (store.getters['auth/isLoggedOut']) {
return true
}
if (!err.response) {
store.dispatch('notification/showNotification', {
type: 'error',
message:
'Please check your internet connection or wait until servers are back online.',
})
} else {
if (
err.response.data &&
err.config.url !== '/api/v1/auth/check' &&
(err.response.statusText === 'Unauthorized' ||
err.response.data === ' Unauthorized.')
) {
console.log(err.response)
// Unauthorized and log out
store.dispatch('notification/showNotification', {
type: 'error',
message: err.response.data.message
? err.response.data.message
: 'Unauthorized',
})
store.dispatch('auth/logout', true)
} else if (err.response.data.errors) {
// Show a notification per error
const errors = JSON.parse(JSON.stringify(err.response.data.errors))
for (const i in errors) {
store.dispatch('notification/showNotification', {
type: 'error',
message: errors[i],
})
}
} else {
// Unknown error
store.dispatch('notification/showNotification', {
type: 'error',
message: err.response.data.message
? err.response.data.message
: err.response.data || 'Unknown error occurred',
})
}
}
return Promise.reject(err)
})
/**
* Global plugins
*/
Vue.use(VueRouter)
Vue.use(Vuex)
Vue.use(VTooltip)
// register directive v-money and component <money>
Vue.use(money, { precision: 2 })

View File

@ -1,157 +0,0 @@
<template>
<div>
<sw-dropdown :is-show="isShow" variant="search-dropdown">
<sw-input
slot="activator"
v-model="name"
:placeholder="$t('global_search.search')"
variant="search-input"
@input="throttledMethod"
>
<search-icon slot="leftIcon" class="h-5 m-1 text-gray-500" />
<loading-icon
slot="rightIcon"
v-show="isLoading"
class="absolute right-0 h-5 m-1 animate-spin text-primary-400"
/>
</sw-input>
<div class="w-64 h-40 overflow-y-scroll box">
<div v-if="getCustomerList.length > 0 && !isLoading">
<label class="text-xs text-gray-400 uppercase">
{{ $t('global_search.customers') }}
</label>
<router-link
v-for="d in getCustomerList"
:key="d.id"
:to="`/admin/customers/${d.id}/view`"
>
<sw-dropdown-item>
<span
class="flex items-center justify-center w-8 h-8 mr-4 text-xs font-semibold bg-gray-300 rounded-full text-primary-500"
>
{{ initGenerator(d.name) }}
</span>
<div v-if="d.contact_name" class="flex flex-col">
<span class="text-sm text-black">{{ d.name }}</span>
<span class="text-xs text-gray-500">{{ d.contact_name }}</span>
</div>
<div v-else class="flex items-center">
<span class="text-sm text-black">{{ d.name }}</span>
</div>
</sw-dropdown-item>
</router-link>
</div>
<div v-if="getUserList.length > 0 && !isLoading">
<label class="text-xs text-gray-400 uppercase">{{
$t('global_search.users')
}}</label>
<router-link
v-for="d in getUserList"
:key="d.id"
:to="`/admin/users/${d.id}/edit`"
>
<sw-dropdown-item>
<span
class="flex items-center justify-center w-8 h-8 mr-4 text-xs font-semibold bg-gray-300 rounded-full text-primary-500"
>
{{ initGenerator(d.name) }}
</span>
<div class="flex items-center">
<span class="text-sm text-black">{{ d.name }}</span>
</div>
</sw-dropdown-item>
</router-link>
</div>
<div
v-if="
getUserList.length === 0 &&
getCustomerList.length === 0 &&
!isLoading
"
>
<span
class="flex items-center justify-center text-sm font-normal text-gray-500"
>
{{ $t('global_search.no_results_found') }}
</span>
</div>
</div>
</sw-dropdown>
</div>
</template>
<script>
import { SearchIcon } from '@vue-hero-icons/solid'
import { mapActions, mapGetters } from 'vuex'
import LoadingIcon from '../components/icon/LoadingIcon'
import _ from 'lodash'
export default {
components: {
SearchIcon,
LoadingIcon,
},
data() {
return {
isShow: false,
isLoading: false,
name: '',
}
},
computed: {
...mapGetters('search', ['getCustomerList', 'getUserList']),
},
created() {
this.searchUsers()
},
methods: {
...mapActions('search', ['searchUsers']),
throttledMethod: _.debounce(async function () {
this.isLoading = true
await this.searchUsers({ search: this.name }).then(() => {
this.isShow = true
})
if (this.name === '') {
this.isShow = false
}
this.isLoading = false
}, 500),
initGenerator(name) {
if (name) {
let nameSplit = name.split('')
let initials =
nameSplit[0].charAt(0).toUpperCase() +
nameSplit[1].charAt(0).toUpperCase()
return initials
}
},
},
}
</script>
<style scoped>
.box::-webkit-scrollbar {
width: 4px;
}
.box::-webkit-scrollbar-thumb {
background-color: transparent;
outline: 1px solid white;
border-radius: 0.42rem !important;
}
.box::-webkit-scrollbar-thumb {
background-color: #e4e6ef;
}
</style>

View File

@ -1,302 +0,0 @@
<template>
<div class="relative">
<div class="absolute bottom-0 right-0 z-10">
<sw-dropdown
:close-on-select="true"
max-height="220"
position="bottom-end"
class="mb-2"
>
<sw-button
slot="activator"
variant="primary-outline"
type="button"
class="mr-2"
>
<plus-sm-icon class="h-5 mr-1 -ml-2" />
{{ $t('settings.customization.addresses.insert_fields') }}
</sw-button>
<div class="flex p-2">
<ul v-for="(type, index) in fieldList" :key="index" class="list-none">
<li class="mb-1 ml-2 text-xs font-semibold text-gray-500 uppercase">
{{ type.label }}
</li>
<li
v-for="(field, index) in type.fields"
:key="index"
class="w-48 text-sm font-normal cursor-pointer hover:bg-gray-200"
@click="insertField(field.value)"
>
<div class="flex">
<chevron-double-right-icon class="h-3 mt-1 text-gray-400" />{{
field.label
}}
</div>
</li>
</ul>
</div>
</sw-dropdown>
</div>
<sw-editor
v-model="inputValue"
:set-editor="inputValue"
:disabled="disabled"
:invalid="isFieldValid"
:placeholder="placeholder"
variant="header-editor"
input-class="border-none"
class="text-area-field"
@input="handleInput"
@change="handleChange"
@keyup="handleKeyupEnter"
/>
</div>
</template>
<script>
import { PlusSmIcon } from '@vue-hero-icons/outline'
import { ChevronDoubleRightIcon } from '@vue-hero-icons/solid'
import { mapActions, mapGetters } from 'vuex'
import customFields from '../../mixins/customFields'
export default {
components: {
PlusSmIcon,
ChevronDoubleRightIcon,
},
props: {
value: {
type: [String, Number, File],
default: '',
},
types: {
type: Array,
default: null,
},
placeholder: {
type: String,
default: '',
},
rows: {
type: String,
default: '10',
},
cols: {
type: String,
default: '30',
},
invalid: {
type: Boolean,
default: false,
},
fields: {
type: Array,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
fieldList: [],
invoiceFields: [],
estimateFields: [],
paymentFields: [],
customerFields: [],
position: null,
inputValue: this.value,
}
},
computed: {
...mapGetters('customFields', ['getCustomFields']),
isFieldValid() {
return this.invalid
},
},
watch: {
value() {
this.inputValue = this.value
},
fields() {
if (this.fields && this.fields.length > 0) {
this.getFields()
}
},
getCustomFields(newValue) {
this.invoiceFields = newValue
? newValue.filter((field) => field.model_type === 'Invoice')
: []
this.customerFields = newValue
? newValue.filter((field) => field.model_type === 'Customer')
: []
this.paymentFields = newValue
? newValue.filter((field) => field.model_type === 'Payment')
: []
this.estimateFields = newValue.filter(
(field) => field.model_type === 'Estimate'
)
this.getFields()
},
},
async mounted() {
this.getFields()
await this.fetchNoteCustomFields({ limit: 'all' })
},
methods: {
...mapActions('customFields', ['fetchNoteCustomFields']),
getFields() {
this.fieldList = []
if (this.fields && this.fields.length > 0) {
if (this.fields.find((field) => field == 'shipping')) {
this.fieldList.push({
label: 'Shipping Address',
fields: [
{ label: 'Address 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' },
],
})
}
if (this.fields.find((field) => field == 'billing')) {
this.fieldList.push({
label: 'Billing Address',
fields: [
{ label: 'Address 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' },
],
})
}
if (this.fields.find((field) => field == 'customer')) {
this.fieldList.push({
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' },
...this.customerFields.map((i) => ({
label: i.label,
value: i.slug,
})),
],
})
}
if (this.fields.find((field) => field == 'invoice')) {
this.fieldList.push({
label: 'Invoice',
fields: [
{ label: 'Date', value: 'INVOICE_DATE' },
{ label: 'Due Date', value: 'INVOICE_DUE_DATE' },
{ label: 'Number', value: 'INVOICE_NUMBER' },
{ label: 'Ref Number', value: 'INVOICE_REF_NUMBER' },
{ label: 'Invoice Link', value: 'INVOICE_LINK' },
...this.invoiceFields.map((i) => ({
label: i.label,
value: i.slug,
})),
],
})
}
if (this.fields.find((field) => field == 'estimate')) {
this.fieldList.push({
label: 'Estimate',
fields: [
{ label: 'Date', value: 'ESTIMATE_DATE' },
{ label: 'Expiry Date', value: 'ESTIMATE_EXPIRY_DATE' },
{ label: 'Number', value: 'ESTIMATE_NUMBER' },
{ label: 'Ref Number', value: 'ESTIMATE_REF_NUMBER' },
{ label: 'Estimate Link', value: 'ESTIMATE_LINK' },
...this.estimateFields.map((i) => ({
label: i.label,
value: i.slug,
})),
],
})
}
if (this.fields.find((field) => field == 'payment')) {
this.fieldList.push({
label: 'Payment',
fields: [
{ label: 'Date', value: 'PAYMENT_DATE' },
{ label: 'Number', value: 'PAYMENT_NUMBER' },
{ label: 'Mode', value: 'PAYMENT_MODE' },
{ label: 'Amount', value: 'PAYMENT_AMOUNT' },
{ label: 'Payment Link', value: 'PAYMENT_LINK' },
...this.paymentFields.map((i) => ({
label: i.label,
value: i.slug,
})),
],
})
}
if (this.fields.find((field) => field == 'company')) {
this.fieldList.push({
label: 'Company',
fields: [
{ label: 'Company Name', value: 'COMPANY_NAME' },
{ label: 'Country', value: 'COMPANY_COUNTRY' },
{ label: 'State', value: 'COMPANY_STATE' },
{ label: 'City', value: 'COMPANY_CITY' },
{ label: 'Address Street 1', value: 'COMPANY_ADDRESS_STREET_1' },
{ label: 'Address Street 2', value: 'COMPANY_ADDRESS_STREET_2' },
{ label: 'Phone', value: 'COMPANY_PHONE' },
{ label: 'Zip Code', value: 'COMPANY_ZIP_CODE' },
],
})
}
}
},
insertField(varName) {
if (this.inputValue) {
this.inputValue += `{${varName}}`
} else {
this.inputValue = `{${varName}}`
}
this.$emit('input', this.inputValue)
},
handleInput(e) {
this.$emit('input', this.inputValue)
},
handleChange(e) {
this.$emit('change', this.inputValue)
},
handleKeyupEnter(e) {
this.$emit('keyup', this.inputValue)
},
handleKeyDownEnter(e) {
this.$emit('keydown', e, this.inputValue)
},
handleFocusOut(e) {
this.$emit('blur', this.inputValue)
},
},
}
</script>

View File

@ -1,61 +0,0 @@
<template>
<div class="item-selector">
<sw-select
ref="baseSelect"
v-model="customerSelect"
:options="customers"
:show-labels="false"
:preserve-search="false"
:placeholder="$t('customers.type_or_click')"
label="name"
class="multi-select-item"
@close="checkCustomers"
@value="onTextChange"
@select="(val) => $emit('select', val)"
@remove="deselectCustomer"
/>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
data() {
return {
customerSelect: null,
loading: false,
}
},
computed: {
...mapGetters('customer', ['customers']),
},
created() {
this.fetchCustomers()
},
methods: {
...mapActions('customer', ['fetchCustomers']),
async searchCustomers(search) {
this.loading = true
await this.fetchCustomers({ search })
this.loading = false
},
onTextChange(val) {
this.searchCustomers(val)
},
checkCustomers(val) {
if (!this.customers.length) {
this.fetchCustomers()
}
},
deselectCustomer() {
this.customerSelect = null
this.$emit('deselect')
},
},
}
</script>

View File

@ -1,148 +0,0 @@
<template>
<div>
<sw-date-picker
ref="BaseDatepicker"
v-model="date"
:config="config"
:placeholder="placeholder"
:disabled="disabled"
:invalid="invalid"
:name="name"
:tabindex="tabindex"
@input="onDateChange"
/>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import SwDatePicker from '@bytefury/spacewind/src/components/SwDatePicker'
import moment from 'moment'
export default {
components: {
SwDatePicker,
},
props: {
placeholder: {
type: String,
default: null,
},
invalid: {
type: Boolean,
default: false,
},
enableTime: {
type: Boolean,
default: false,
},
time_24hr: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
name: {
type: String,
default: null,
},
value: {
type: [String, Date],
default: () => moment(new Date()),
},
tabindex: {
type: Number,
default: null,
},
},
data() {
return {
date: null,
config: {
altInput: true,
enableTime: this.enableTime,
time_24hr: this.time_24hr,
},
}
},
computed: {
...mapGetters('company', {
carbonFormat: 'getCarbonDateFormat',
momentFormat: 'getMomentDateFormat',
}),
},
watch: {
// value(val) {
// console.log(val)
// if (val && !this.enableTime) {
// this.date = moment(new Date(val), 'YYYY-MM-DD').format('YYYY-MM-DD')
// } else {
// this.date = moment(new Date(val), 'YYYY-MM-DD').format(
// 'YYYY-MM-DD H:m:s'
// )
// }
// },
enableTime(val) {
this.$set(this.config, 'enableTime', this.enableTime)
},
carbonFormat() {
if (!this.enableTime) {
this.$set(
this.config,
'altFormat',
this.carbonFormat ? this.carbonFormat : 'd M Y'
)
} else {
this.$set(
this.config,
'altFormat',
this.carbonFormat ? `${this.carbonFormat} H:i ` : 'd M Y H:i'
)
}
},
},
mounted() {
this.$set(this.config, 'enableTime', this.enableTime)
if (!this.enableTime) {
this.$set(
this.config,
'altFormat',
this.carbonFormat ? this.carbonFormat : 'd M Y'
)
} else {
this.$set(
this.config,
'altFormat',
this.carbonFormat ? `${this.carbonFormat} H:i ` : 'd M Y H:i'
)
}
if (this.value && !this.enableTime) {
this.date = moment(new Date(this.value), 'YYYY-MM-DD').format(
'YYYY-MM-DD'
)
return true
}
if (this.value) {
this.date = moment(new Date(this.value), 'YYYY-MM-DD').format(
'YYYY-MM-DD HH:mm:SS'
)
}
},
methods: {
onDateChange(date) {
this.$emit('input', date)
},
},
}
</script>
<style lang="scss">
.flatpickr-calendar.open {
z-index: 60 !important;
}
</style>

View File

@ -1,123 +0,0 @@
<template>
<div
:class="{ 'bg-gray-400': showBgOverlay }"
class="absolute top-0 left-0 z-20 flex items-center justify-center w-full h-full bg-opacity-25 base-loader"
>
<div class="absolute top-0 left-0 w-full h-full overlay">
<div
class="absolute flex items-center justify-center ball-scale-ripple-multiple"
>
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
showBgOverlay: {
default: false,
type: Boolean,
},
},
}
</script>
<style lang="scss">
// function.scss
@function delay($interval, $count, $index) {
@return ($index * $interval) - ($interval * $count);
}
// mixins.scss
@mixin global-bg() {
background-color: #5851d8;
}
@mixin global-animation() {
animation-fill-mode: both;
}
@mixin balls() {
@include global-bg();
width: 15px;
height: 15px;
border-radius: 100%;
margin: 2px;
}
@mixin lines() {
@include global-bg();
width: $line-width;
height: $line-height;
border-radius: 2px;
margin: $margin;
}
.base-loader {
.overlay {
@keyframes ball-scale-ripple-multiple {
0% {
transform: scale(0.1);
opacity: 1;
}
70% {
transform: scale(1);
opacity: 0.7;
}
100% {
opacity: 0;
}
}
@mixin ball-scale-ripple-multiple($n: 3, $start: 0) {
@for $i from $start through $n {
> div:nth-child(#{$i}) {
animation-delay: delay(0.2s, $n, $i - 1);
}
}
}
.loader {
width: 100%;
position: relative;
min-height: 500px;
}
.ball-scale-ripple-multiple {
transform: translateY(-25px);
top: 50%;
left: 50%;
@include ball-scale-ripple-multiple();
transform: translateY(-50px / 2);
> div {
@include global-animation();
position: absolute;
top: -2px;
left: -26px;
width: 50px;
height: 50px;
border-radius: 100%;
border: 2px solid #5851d8;
animation: ball-scale-ripple-multiple 1.25s 0s infinite
cubic-bezier(0.21, 0.53, 0.56, 0.8);
}
}
}
&.table-loader .overlay {
background: rgba(255, 255, 255, 0.5);
height: calc(100% - 80px);
top: 80px;
}
}
</style>

View File

@ -1,147 +0,0 @@
<template>
<transition
enter-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2 "
enter-active-class="transition duration-300 ease-out transform"
enter-to-class="duration-300 translate-y-0 opacity-100 sm:translate-x-0"
leave-active-class="transition duration-100 ease-in"
leave-class="duration-200 opacity-100"
leave-to-class="duration-200 opacity-0"
>
<div
v-if="notificationActive"
class="fixed inset-0 z-50 flex items-end justify-center px-4 py-6 pointer-events-none sm:p-6 sm:items-start sm:justify-end"
>
<div
:class="success || info ? 'bg-white' : 'bg-red-50'"
class="w-full max-w-sm rounded-lg shadow-lg cursor-pointer pointer-events-auto"
@click="hideNotification"
>
<div class="overflow-hidden rounded-lg shadow-xs">
<div class="p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<svg
v-if="success"
class="w-6 h-6 text-green-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<exclamation-circle-icon
v-if="info"
class="w-6 h-6 text-blue-400"
/>
<svg
v-if="error"
class="w-6 h-6 text-red-400"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class="flex-1 w-0 ml-3">
<p
:class="`text-sm leading-5 font-medium ${
success || info ? 'text-gray-900' : 'text-red-800'
}`"
>
{{
notificationTitle ? notificationTitle : success ? 'Success!' : 'Error'
}}
</p>
<p
:class="`mt-1 text-sm leading-5 ${
success || info ? 'text-gray-500' : 'text-red-700'
}`"
>
{{
notificationMessage
? notificationMessage
: success
? 'Successful'
: 'Something went wrong'
}}
</p>
</div>
<div class="flex flex-shrink-0">
<button
:class="
success || info
? ' text-gray-400 focus:text-gray-500'
: 'text-red-400 focus:text-red-500'
"
class="inline-flex w-5 h-5 transition duration-150 ease-in-out focus:outline-none"
@click="hideNotification"
>
<x-icon />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { XIcon } from '@vue-hero-icons/outline'
import { ExclamationCircleIcon } from '@vue-hero-icons/solid'
export default {
components: {
XIcon,
ExclamationCircleIcon,
},
data() {
return {
hasFocus: false,
}
},
computed: {
...mapGetters('notification', [
'notificationActive',
'notificationTitle',
'notificationType',
'notificationAutoHide',
'notificationMessage',
]),
success() {
return this.notificationType.toLowerCase() === 'success'
},
error() {
return this.notificationType.toLowerCase() === 'error'
},
info() {
return this.notificationType.toLowerCase() === 'info'
},
},
watch: {
notificationActive(val) {
if (val && this.notificationAutoHide) {
window.setTimeout(this.hideNotification, 5000)
}
},
},
mounted() {
if (this.notificationActive && this.notificationAutoHide) {
window.setTimeout(this.hideNotification, 5000)
}
},
methods: {
...mapActions('notification', ['showNotification', 'hideNotification']),
},
}
</script>

View File

@ -1,7 +0,0 @@
<template>
<main
class="flex flex-col flex-1 p-8 overflow-y-auto border-b border-gray-300 border-solid"
>
<slot />
</main>
</template>

View File

@ -1,81 +0,0 @@
<template>
<sw-date-picker
ref="BaseDatepicker"
v-model="time"
:config="config"
:placeholder="placeholder"
:disabled="disabled"
:invalid="invalid"
:name="name"
:tabindex="tabindex"
@input="onDateChange"
/>
</template>
<script>
import SwDatePicker from '@bytefury/spacewind/src/components/SwDatePicker'
export default {
components: {
SwDatePicker,
},
props: {
invalid: {
type: Boolean,
default: false,
require: false,
},
defaultTime: {
type: String,
default: null,
require: false,
},
hideClearButton: {
type: String,
default: null,
require: false,
},
value: {
type: [String, Object],
default: '',
},
placeholder: {
type: String,
default: 'HH:mm:ss',
},
tabindex: {
type: Number,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
name: {
type: String,
default: null,
},
},
data() {
return {
time: this.value,
config: {
enableTime: true,
noCalendar: true,
dateFormat: 'H:i',
time_24hr: true,
},
}
},
watch: {
value() {
this.time = this.value
},
},
methods: {
onDateChange(date) {
this.$emit('input', date)
this.$emit('change', date)
},
},
}
</script>

View File

@ -1,881 +0,0 @@
<template>
<div
:tabindex="searchable ? -1 : tabindex"
:class="multiSelectStyle"
:aria-owns="'listbox-' + id"
role="combobox"
@focus="activate()"
@blur="searchable ? false : deactivate()"
@keydown.self.down.prevent="pointerForward()"
@keydown.self.up.prevent="pointerBackward()"
@keypress.enter.tab.stop.self="addPointerElement($event)"
@keyup.esc="deactivate()"
>
<slot :toggle="toggle" name="caret">
<div :class="multiselectSelectStyle" @mousedown.prevent.stop="toggle()" />
</slot>
<!-- <slot name="clear" :search="search"></slot> -->
<div ref="tags" :class="multiSelectTagsStyle">
<slot
:search="search"
:remove="removeElement"
:values="visibleValues"
:is-open="isOpen"
name="selection"
>
<div
v-show="visibleValues.length > 0"
:class="multiselectTagsWrapStyle"
>
<template v-for="(option, index) of visibleValues" @mousedown.prevent>
<slot
:option="option"
:search="search"
:remove="removeElement"
name="tag"
>
<span :key="index" :class="multiselectTagStyle">
<span v-text="getOptionLabel(option)" />
<i
:class="multiselectTagIconStyle"
tabindex="1"
@keypress.enter.prevent="removeElement(option)"
@mousedown.prevent="removeElement(option)"
/>
</span>
</slot>
</template>
</div>
<template v-if="internalValue && internalValue.length > limit">
<slot name="limit">
<strong
:class="multiselectStrongStyle"
v-text="limitText(internalValue.length - limit)"
/>
</slot>
</template>
</slot>
<transition name="multiselect__loading">
<slot name="loading">
<div v-show="loading" :class="multiselectSpinnerStyle" />
</slot>
</transition>
<input
ref="search"
:name="name"
:id="id"
:placeholder="placeholder"
:style="inputStyle"
:value="search"
:disabled="disabled"
:tabindex="tabindex"
:aria-controls="'listbox-' + id"
:class="multiselectInputStyle"
type="text"
autocomplete="off"
spellcheck="false"
@input="updateSearch($event.target.value)"
@focus.prevent="activate()"
@blur.prevent="deactivate()"
@keyup.esc="deactivate()"
@keydown.down.prevent="pointerForward()"
@keydown.up.prevent="pointerBackward()"
@keypress.enter.prevent.stop.self="addPointerElement($event)"
@keydown.delete.stop="removeLastElement()"
/>
<span
v-if="isSingleLabelVisible"
:class="multiselectSingleStyle"
@mousedown.prevent="toggle"
>
<slot :option="singleValue" name="singleLabel">
<template>{{ currentOptionLabel }}</template>
</slot>
</span>
</div>
<transition name="multiselect">
<div
v-show="isOpen"
ref="list"
:style="{ maxHeight: optimizedHeight + 'px' }"
:class="multiselectContentWrapperStyle"
tabindex="-1"
@focus="activate"
@mousedown.prevent
>
<ul
:style="contentStyle"
:id="'listbox-' + id"
:class="multiselectContentStyle"
role="listbox"
>
<slot name="beforeList" />
<li v-if="multiple && max === internalValue.length">
<span :class="multiselectOptionStyle">
<slot name="maxElements">
{{ $t('validation.maximum_options_error', { max: max }) }}
</slot>
</span>
</li>
<template v-if="!max || internalValue.length < max">
<li
v-for="(option, index) of filteredOptions"
:key="index"
:id="id + '-' + index"
:role="
!(option && (option.$isLabel || option.$isDisabled))
? 'option'
: null
"
:class="multiselectElementStyle"
>
<span
v-if="!(option && (option.$isLabel || option.$isDisabled))"
:class="optionHighlight(index, option)"
:data-select="
option && option.isTag ? tagPlaceholder : selectLabelText
"
:data-selected="selectedLabelText"
:data-deselect="deselectLabelText"
@click.stop="select(option)"
@mouseenter.self="pointerSet(index)"
>
<slot :option="option" :search="search" name="option">
<span>{{ getOptionLabel(option) }}</span>
</slot>
</span>
<span
v-if="option && (option.$isLabel || option.$isDisabled)"
:data-select="groupSelect && selectGroupLabelText"
:data-deselect="groupSelect && deselectGroupLabelText"
:class="groupHighlight(index, option)"
@mouseenter.self="groupSelect && pointerSet(index)"
@mousedown.prevent="selectGroup(option)"
>
<slot :option="option" :search="search" name="option">
<span>{{ getOptionLabel(option) }}</span>
</slot>
</span>
</li>
</template>
<li
v-if="showNoOptions && (options.length === 0 && !search && !loading)"
>
<span :class="multiselectOptionStyle">
<slot name="noOptions">{{ $t('general.list_is_empty') }}</slot>
</span>
</li>
</ul>
<slot name="afterList" />
</div>
</transition>
</div>
</template>
<script>
import CraterTheme from '../theme/index'
import multiselectMixin from './multiselectMixin'
import pointerMixin from './pointerMixin'
const {
activeBaseSelectContainer,
disabledBaseSelectContainer,
baseSelectContainer,
multiSelect,
disabledMultiSelect,
multiSelectTags,
multiSelectTagsInvalid,
multiSelectTagsDefaultColor,
disabledMultiSelectTags,
multiselectTagsWrap,
multiselectTag,
multiselectTagIcon,
multiselectStrong,
multiselectSpinner,
multiselectInput,
multiselectSingle,
multiselectContentWrapper,
multiselectContent,
multiselectOption,
multiselectElement,
} = CraterTheme.BaseSelect
export default {
name: 'VueMultiselect',
mixins: [multiselectMixin, pointerMixin],
props: {
/**
* name attribute to match optional label element
* @default ''
* @type {String}
*/
name: {
type: String,
default: '',
},
/**
* String to show when pointing to an option
* @default 'Press enter to select'
* @type {String}
*/
selectLabel: {
type: String,
default: '',
},
/**
* String to show when pointing to an option
* @default 'Press enter to select'
* @type {String}
*/
selectGroupLabel: {
type: String,
default: '',
},
/**
* String to show next to selected option
* @default 'Selected'
* @type {String}
*/
selectedLabel: {
type: String,
default: 'Selected',
},
/**
* String to show when pointing to an already selected option
* @default 'Press enter to remove'
* @type {String}
*/
deselectLabel: {
type: String,
default: 'Press enter to remove',
},
/**
* String to show when pointing to an already selected option
* @default 'Press enter to remove'
* @type {String}
*/
deselectGroupLabel: {
type: String,
default: 'Press enter to deselect group',
},
/**
* Decide whether to show pointer labels
* @default true
* @type {Boolean}
*/
showLabels: {
type: Boolean,
default: true,
},
/**
* Limit the display of selected options. The rest will be hidden within the limitText string.
* @default 99999
* @type {Integer}
*/
limit: {
type: Number,
default: 99999,
},
/**
* Sets maxHeight style value of the dropdown
* @default 300
* @type {Integer}
*/
maxHeight: {
type: Number,
default: 300,
},
/**
* Function that process the message shown when selected
* elements pass the defined limit.
* @default 'and * more'
* @param {Int} count Number of elements more than limit
* @type {Function}
*/
limitText: {
type: Function,
default: (count) => `and ${count} more`,
},
/**
* Set true to trigger the loading spinner.
* @default False
* @type {Boolean}
*/
loading: {
type: Boolean,
default: false,
},
/**
* Disables the multiselect if true.
* @default false
* @type {Boolean}
*/
disabled: {
type: Boolean,
default: false,
},
/**
* Fixed opening direction
* @default ''
* @type {String}
*/
openDirection: {
type: String,
default: '',
},
/**
* Shows slot with message about empty options
* @default true
* @type {Boolean}
*/
showNoOptions: {
type: Boolean,
default: true,
},
showNoResults: {
type: Boolean,
default: true,
},
tabindex: {
type: Number,
default: 0,
},
invalid: {
type: Boolean,
default: false,
},
},
computed: {
isSingleLabelVisible() {
return (
(this.singleValue || this.singleValue === 0) &&
(!this.isOpen || !this.searchable) &&
!this.visibleValues.length
)
},
isPlaceholderVisible() {
return !this.internalValue.length && (!this.searchable || !this.isOpen)
},
visibleValues() {
return this.multiple ? this.internalValue.slice(0, this.limit) : []
},
singleValue() {
return this.internalValue[0]
},
deselectLabelText() {
return this.showLabels ? this.deselectLabel : ''
},
deselectGroupLabelText() {
return this.showLabels ? this.deselectGroupLabel : ''
},
selectLabelText() {
return this.showLabels ? this.selectLabel : ''
},
selectGroupLabelText() {
return this.showLabels ? this.selectGroupLabel : ''
},
selectedLabelText() {
return this.showLabels ? this.selectedLabel : ''
},
inputStyle() {
if (
this.searchable ||
(this.multiple && this.value && this.value.length)
) {
// Hide input by setting the width to 0 allowing it to receive focus
return this.isOpen
? { width: '100%' }
: this.value
? { width: '0', position: 'absolute', padding: '0' }
: ''
}
},
contentStyle() {
return this.options.length
? { display: 'inline-block' }
: { display: 'block' }
},
isAbove() {
if (this.openDirection === 'above' || this.openDirection === 'top') {
return true
} else if (
this.openDirection === 'below' ||
this.openDirection === 'bottom'
) {
return false
} else {
return this.preferredOpenDirection === 'above'
}
},
showSearchInput() {
return (
this.searchable &&
(this.hasSingleSelectedSlot &&
(this.visibleSingleValue || this.visibleSingleValue === 0)
? this.isOpen
: true)
)
},
multiSelectStyle() {
let style = ['multiselect--active', baseSelectContainer]
if (this.isOpen) {
style.push(activeBaseSelectContainer)
}
if (this.disabled) {
style.push(disabledBaseSelectContainer)
}
if (this.isAbove) {
style.push('multiselect--above')
}
return style
},
multiselectSelectStyle() {
let style = [multiSelect]
if (this.disabled) {
style.push(disabledMultiSelect)
}
return style
},
multiSelectTagsStyle() {
let style = [multiSelectTags]
if (this.invalid) {
style.push(multiSelectTagsInvalid)
} else {
style.push(multiSelectTagsDefaultColor)
}
if (this.disabled) {
style.push(disabledMultiSelectTags)
}
return style
},
multiselectTagsWrapStyle() {
return [multiselectTagsWrap]
},
multiselectTagStyle() {
return [multiselectTag]
},
multiselectTagIconStyle() {
return [multiselectTagIcon]
},
multiselectStrongStyle() {
return [multiselectStrong]
},
multiselectSpinnerStyle() {
return [multiselectSpinner]
},
multiselectInputStyle() {
return [multiselectInput]
},
multiselectSingleStyle() {
return [multiselectSingle]
},
multiselectContentWrapperStyle() {
return [multiselectContentWrapper]
},
multiselectContentStyle() {
return [multiselectContent]
},
multiselectOptionStyle() {
return [multiselectOption]
},
multiselectElementStyle() {
return [multiselectElement]
},
},
}
</script>
<style lang="scss">
fieldset[disabled] .multiselect {
pointer-events: none;
}
.multiselect {
min-height: 40px;
}
.multiselect__spinner {
right: 1px;
top: 1px;
}
.multiselect__spinner:before,
.multiselect__spinner:after {
position: absolute;
content: '';
top: 50%;
left: 50%;
margin: -8px 0 0 -8px;
z-index: 5;
width: 16px;
height: 16px;
border-radius: 100%;
border-color: #41b883 transparent transparent;
border-style: solid;
border-width: 2px;
box-shadow: 0 0 0 1px transparent;
}
.multiselect__spinner:before {
animation: spinning 2.4s cubic-bezier(0.41, 0.26, 0.2, 0.62);
animation-iteration-count: infinite;
}
.multiselect__spinner:after {
animation: spinning 2.4s cubic-bezier(0.51, 0.09, 0.21, 0.8);
animation-iteration-count: infinite;
}
.multiselect__loading-enter-active,
.multiselect__loading-leave-active {
transition: opacity 0.4s ease-in-out;
opacity: 1;
}
.multiselect__loading-enter,
.multiselect__loading-leave-active {
opacity: 0;
}
.multiselect,
.multiselect__input,
.multiselect__single {
font-family: inherit;
// font-size: 14px;
touch-action: manipulation;
}
.multiselect {
box-sizing: content-box;
display: block;
position: relative;
width: 100%;
min-height: 40px;
text-align: left;
color: #35495e;
}
.multiselect * {
box-sizing: border-box;
}
.multiselect:focus {
border: 1px solid #817ae3 !important;
}
.multiselect--disabled {
pointer-events: none;
opacity: 0.6;
}
.multiselect--active:not(.multiselect--above) .multiselect__current,
.multiselect--active:not(.multiselect--above) .multiselect__input,
.multiselect--active:not(.multiselect--above) .multiselect__tags {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.multiselect--active .multiselect__select {
transform: rotateZ(180deg);
}
.multiselect--above.multiselect--active .multiselect__current,
.multiselect--above.multiselect--active .multiselect__input,
.multiselect--above.multiselect--active .multiselect__tags {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.multiselect__input,
.multiselect__single {
min-height: 20px;
transition: border 0.1s ease;
}
.multiselect__input::placeholder {
color: #b9c1d1;
}
.multiselect__tag ~ .multiselect__input,
.multiselect__tag ~ .multiselect__single {
width: auto;
}
.multiselect__input:hover,
.multiselect__single:hover {
border-color: #cfcfcf;
}
.multiselect__input:focus,
.multiselect__single:focus {
border-color: #a8a8a8;
outline: none;
}
.multiselect__tag {
background: #41b883;
text-overflow: ellipsis;
}
.multiselect__tag-icon {
font-style: initial;
}
.multiselect__tag-icon:after {
content: '×';
color: #266d4d;
font-size: 14px;
}
.multiselect__tag-icon:focus,
.multiselect__tag-icon:hover {
background: #369a6e;
}
.multiselect__tag-icon:focus:after,
.multiselect__tag-icon:hover:after {
color: white;
}
.multiselect__current {
line-height: 16px;
min-height: 40px;
box-sizing: border-box;
display: block;
overflow: hidden;
padding: 8px 12px 0;
padding-right: 30px;
white-space: nowrap;
margin: 0;
text-decoration: none;
border-radius: 5px;
border: 1px solid #ebf1fa;
cursor: pointer;
}
.multiselect__select {
right: 1px;
top: 1px;
transition: transform 0.2s;
}
.multiselect__select:before {
position: relative;
right: 0;
top: 65%;
color: #a5acc1;
margin-top: 4px;
border-style: solid;
border-width: 5px 5px 0 5px;
border-color: #a5acc1 transparent transparent transparent;
content: '';
}
.multiselect__placeholder {
color: #b9c1d1;
display: inline-block;
margin-bottom: 10px;
padding-top: 2px;
}
.multiselect--active .multiselect__placeholder {
display: none;
}
.multiselect__content-wrapper {
max-height: 240px;
-webkit-overflow-scrolling: touch;
}
.multiselect--above .multiselect__content-wrapper {
bottom: 100%;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border-bottom: none;
border-top: 1px solid #e8e8e8;
}
.multiselect__content::webkit-scrollbar {
display: none;
}
.multiselect__option {
min-height: 40px;
}
.multiselect__option:after {
top: 0;
right: 0;
position: absolute;
line-height: 40px;
padding-right: 12px;
padding-left: 20px;
font-size: 13px;
}
.multiselect__option--highlight {
background: #41b883;
outline: none;
color: white;
}
.multiselect__option--highlight:after {
content: attr(data-select);
background: #41b883;
color: white;
}
.multiselect__option--selected {
background: #f3f3f3;
color: #35495e;
font-weight: bold;
}
.multiselect__option--selected:after {
content: attr(data-selected);
color: silver;
}
.multiselect__option--selected.multiselect__option--highlight {
background: #ff6a6a;
color: #fff;
}
.multiselect__option--selected.multiselect__option--highlight:after {
background: #ff6a6a;
content: attr(data-deselect);
color: #fff;
}
.multiselect--disabled .multiselect__current,
.multiselect--disabled .multiselect__select {
background: #ebf1fa;
color: #b9c1d1;
}
.multiselect--disabled .multiselect__input,
.multiselect--disabled .multiselect__single {
background: #ebf1fa;
color: #b9c1d1;
}
.multiselect__option--disabled {
background: transparent !important;
color: #dddddd !important;
cursor: text;
pointer-events: none;
}
.multiselect__option--group {
background: #ededed;
color: #35495e;
}
.multiselect__option--group.multiselect__option--highlight {
background: #35495e;
color: #fff;
}
.multiselect__option--group.multiselect__option--highlight:after {
background: #35495e;
}
.multiselect__option--disabled.multiselect__option--highlight {
background: #dedede;
}
.multiselect__option--group-selected.multiselect__option--highlight {
background: #ff6a6a;
color: #fff;
}
.multiselect__option--group-selected.multiselect__option--highlight:after {
background: #ff6a6a;
content: attr(data-deselect);
color: #fff;
}
.multiselect-enter-active,
.multiselect-leave-active {
transition: all 0.15s ease;
}
.multiselect-enter,
.multiselect-leave-active {
opacity: 0;
}
*[dir='rtl'] .multiselect {
text-align: right;
}
*[dir='rtl'] .multiselect__select {
right: auto;
left: 1px;
}
*[dir='rtl'] .multiselect__tags {
padding: 8px 8px 0px 40px;
}
*[dir='rtl'] .multiselect__content {
text-align: right;
}
*[dir='rtl'] .multiselect__option:after {
right: auto;
left: 0;
}
*[dir='rtl'] .multiselect__clear {
right: auto;
left: 12px;
}
*[dir='rtl'] .multiselect__spinner {
right: auto;
left: 1px;
}
@keyframes spinning {
from {
transform: rotate(0);
}
to {
transform: rotate(2turn);
}
}
.multiselect {
.multiselect__option--highlight {
background: #5851d8;
color: #040405;
font-weight: normal !important;
&.multiselect__option--selected {
background: #ebf1fa;
color: #040405;
font-size: 1rem;
font-weight: normal !important;
&::after {
background: #040405;
color: #fff;
}
}
&::after {
background: #040405;
color: #fff;
}
}
.multiselect__option--selected {
font-weight: normal !important;
background: #ebf1fa;
}
.multiselect__tags-wrap .multiselect__tag {
background: #5851d8;
color: #040405;
.multiselect__tag-icon {
&:hover {
background: #5851d8;
}
&::after {
color: #040405;
}
}
}
&.error {
border: 1px solid #fb7178;
border-radius: 5px;
}
}
</style>

View File

@ -1,7 +0,0 @@
import Multiselect from './Multiselect'
import multiselectMixin from './multiselectMixin'
import pointerMixin from './pointerMixin'
export default Multiselect
export { Multiselect, multiselectMixin, pointerMixin }

View File

@ -1,775 +0,0 @@
function isEmpty(opt) {
if (opt === 0) return false
if (Array.isArray(opt) && opt.length === 0) return true
return !opt
}
function not(fun) {
return (...params) => !fun(...params)
}
function includes(str, query) {
/* istanbul ignore else */
if (str === undefined) str = 'undefined'
if (str === null) str = 'null'
if (str === false) str = 'false'
const text = str.toString().toLowerCase()
return text.indexOf(query.trim()) !== -1
}
function filterOptions(options, search, label, customLabel) {
return options.filter((option) =>
includes(customLabel(option, label), search)
)
}
function stripGroups(options) {
return options.filter((option) => !option.$isLabel)
}
function flattenOptions(values, label) {
return (options) =>
options.reduce((prev, curr) => {
/* istanbul ignore else */
if (curr[values] && curr[values].length) {
prev.push({
$groupLabel: curr[label],
$isLabel: true,
})
return prev.concat(curr[values])
}
return prev
}, [])
}
function filterGroups(search, label, values, groupLabel, customLabel) {
return (groups) =>
groups.map((group) => {
/* istanbul ignore else */
if (!group[values]) {
console.warn(
`Options passed to vue-multiselect do not contain groups, despite the config.`
)
return []
}
const groupOptions = filterOptions(
group[values],
search,
label,
customLabel
)
return groupOptions.length
? {
[groupLabel]: group[groupLabel],
[values]: groupOptions,
}
: []
})
}
const flow = (...fns) => (x) => fns.reduce((v, f) => f(v), x)
export default {
data() {
return {
search: '',
isOpen: false,
preferredOpenDirection: 'below',
optimizedHeight: this.maxHeight,
}
},
props: {
initialSearch: {
type: String,
default: '',
},
/**
* Decide whether to filter the results based on search query.
* Useful for async filtering, where we search through more complex data.
* @type {Boolean}
*/
internalSearch: {
type: Boolean,
default: true,
},
/**
* Array of available options: Objects, Strings or Integers.
* If array of objects, visible label will default to option.label.
* If `labal` prop is passed, label will equal option['label']
* @type {Array}
*/
options: {
type: Array,
required: true,
},
/**
* Equivalent to the `multiple` attribute on a `<select>` input.
* @default false
* @type {Boolean}
*/
multiple: {
type: Boolean,
default: false,
},
/**
* Presets the selected options value.
* @type {Object||Array||String||Integer}
*/
value: {
type: null,
default() {
return []
},
},
/**
* Key to compare objects
* @default 'id'
* @type {String}
*/
trackBy: {
type: String,
},
/**
* Label to look for in option Object
* @default 'label'
* @type {String}
*/
label: {
type: String,
},
/**
* Enable/disable search in options
* @default true
* @type {Boolean}
*/
searchable: {
type: Boolean,
default: true,
},
/**
* Clear the search input after `)
* @default true
* @type {Boolean}
*/
clearOnSelect: {
type: Boolean,
default: true,
},
/**
* Hide already selected options
* @default false
* @type {Boolean}
*/
hideSelected: {
type: Boolean,
default: false,
},
/**
* Equivalent to the `placeholder` attribute on a `<select>` input.
* @default 'Select option'
* @type {String}
*/
placeholder: {
type: String,
default: 'Select option',
},
/**
* Allow to remove all selected values
* @default true
* @type {Boolean}
*/
allowEmpty: {
type: Boolean,
default: true,
},
/**
* Reset this.internalValue, this.search after this.internalValue changes.
* Useful if want to create a stateless dropdown.
* @default false
* @type {Boolean}
*/
resetAfter: {
type: Boolean,
default: false,
},
/**
* Enable/disable closing after selecting an option
* @default true
* @type {Boolean}
*/
closeOnSelect: {
type: Boolean,
default: true,
},
/**
* Function to interpolate the custom label
* @default false
* @type {Function}
*/
customLabel: {
type: Function,
default(option, label) {
if (isEmpty(option)) return ''
return label ? option[label] : option
},
},
/**
* Disable / Enable tagging
* @default false
* @type {Boolean}
*/
taggable: {
type: Boolean,
default: false,
},
/**
* String to show when highlighting a potential tag
* @default 'Press enter to create a tag'
* @type {String}
*/
tagPlaceholder: {
type: String,
default: 'Press enter to create a tag',
},
/**
* By default new tags will appear above the search results.
* Changing to 'bottom' will revert this behaviour
* and will proritize the search results
* @default 'top'
* @type {String}
*/
tagPosition: {
type: String,
default: 'top',
},
/**
* Number of allowed selected options. No limit if 0.
* @default 0
* @type {Number}
*/
max: {
type: [Number, Boolean],
default: false,
},
/**
* Will be passed with all events as second param.
* Useful for identifying events origin.
* @default null
* @type {String|Integer}
*/
id: {
default: null,
},
/**
* Limits the options displayed in the dropdown
* to the first X options.
* @default 1000
* @type {Integer}
*/
optionsLimit: {
type: Number,
default: 1000,
},
/**
* Name of the property containing
* the group values
* @default 1000
* @type {String}
*/
groupValues: {
type: String,
},
/**
* Name of the property containing
* the group label
* @default 1000
* @type {String}
*/
groupLabel: {
type: String,
},
/**
* Allow to select all group values
* by selecting the group label
* @default false
* @type {Boolean}
*/
groupSelect: {
type: Boolean,
default: false,
},
/**
* Array of keyboard keys to block
* when selecting
* @default 1000
* @type {String}
*/
blockKeys: {
type: Array,
default() {
return []
},
},
/**
* Prevent from wiping up the search value
* @default false
* @type {Boolean}
*/
preserveSearch: {
type: Boolean,
default: false,
},
/**
* Select 1st options if value is empty
* @default false
* @type {Boolean}
*/
preselectFirst: {
type: Boolean,
default: false,
},
},
mounted() {
/* istanbul ignore else */
if (!this.multiple && this.max) {
console.warn(
'[Vue-Multiselect warn]: Max prop should not be used when prop Multiple equals false.'
)
}
if (
this.preselectFirst &&
!this.internalValue.length &&
this.options.length
) {
this.select(this.filteredOptions[0])
}
if (this.initialSearch) {
this.search = this.initialSearch
}
},
computed: {
internalValue() {
return this.value || this.value === 0
? Array.isArray(this.value)
? this.value
: [this.value]
: []
},
filteredOptions() {
const search = this.search || ''
const normalizedSearch = search.toLowerCase().trim()
let options = this.options.concat()
/* istanbul ignore else */
if (this.internalSearch) {
options = this.groupValues
? this.filterAndFlat(options, normalizedSearch, this.label)
: filterOptions(
options,
normalizedSearch,
this.label,
this.customLabel
)
} else {
options = this.groupValues
? flattenOptions(this.groupValues, this.groupLabel)(options)
: options
}
options = this.hideSelected
? options.filter(not(this.isSelected))
: options
/* istanbul ignore else */
if (
this.taggable &&
normalizedSearch.length &&
!this.isExistingOption(normalizedSearch)
) {
if (this.tagPosition === 'bottom') {
options.push({ isTag: true, label: search })
} else {
options.unshift({ isTag: true, label: search })
}
}
return options.slice(0, this.optionsLimit)
},
valueKeys() {
if (this.trackBy) {
return this.internalValue.map((element) => element[this.trackBy])
} else {
return this.internalValue
}
},
optionKeys() {
const options = this.groupValues
? this.flatAndStrip(this.options)
: this.options
return options.map((element) =>
this.customLabel(element, this.label).toString().toLowerCase()
)
},
currentOptionLabel() {
return this.multiple
? this.searchable
? ''
: this.placeholder
: this.internalValue.length
? this.getOptionLabel(this.internalValue[0])
: this.searchable
? ''
: this.placeholder
},
},
watch: {
internalValue() {
/* istanbul ignore else */
if (this.resetAfter && this.internalValue.length) {
this.search = ''
this.$emit('input', this.multiple ? [] : null)
}
},
search() {
this.$emit('search-change', this.search, this.id)
},
},
methods: {
/**
* Returns the internalValue in a way it can be emitted to the parent
* @returns {Object||Array||String||Integer}
*/
getValue() {
return this.multiple
? this.internalValue
: this.internalValue.length === 0
? null
: this.internalValue[0]
},
/**
* Filters and then flattens the options list
* @param {Array}
* @returns {Array} returns a filtered and flat options list
*/
filterAndFlat(options, search, label) {
return flow(
filterGroups(
search,
label,
this.groupValues,
this.groupLabel,
this.customLabel
),
flattenOptions(this.groupValues, this.groupLabel)
)(options)
},
/**
* Flattens and then strips the group labels from the options list
* @param {Array}
* @returns {Array} returns a flat options list without group labels
*/
flatAndStrip(options) {
return flow(
flattenOptions(this.groupValues, this.groupLabel),
stripGroups
)(options)
},
/**
* Updates the search value
* @param {String}
*/
updateSearch(query) {
this.search = query
this.$emit('value', this.search)
},
/**
* Finds out if the given query is already present
* in the available options
* @param {String}
* @returns {Boolean} returns true if element is available
*/
isExistingOption(query) {
return !this.options ? false : this.optionKeys.indexOf(query) > -1
},
/**
* Finds out if the given element is already present
* in the result value
* @param {Object||String||Integer} option passed element to check
* @returns {Boolean} returns true if element is selected
*/
isSelected(option) {
const opt = this.trackBy ? option[this.trackBy] : option
return this.valueKeys.indexOf(opt) > -1
},
/**
* Finds out if the given option is disabled
* @param {Object||String||Integer} option passed element to check
* @returns {Boolean} returns true if element is disabled
*/
isOptionDisabled(option) {
return !!option.$isDisabled
},
/**
* Returns empty string when options is null/undefined
* Returns tag query if option is tag.
* Returns the customLabel() results and casts it to string.
*
* @param {Object||String||Integer} Passed option
* @returns {Object||String}
*/
getOptionLabel(option) {
if (isEmpty(option)) return ''
/* istanbul ignore else */
if (option.isTag) return option.label
/* istanbul ignore else */
if (option.$isLabel) return option.$groupLabel
let label = this.customLabel(option, this.label)
/* istanbul ignore else */
if (isEmpty(label)) return ''
return label
},
/**
* Add the given option to the list of selected options
* or sets the option as the selected option.
* If option is already selected -> remove it from the results.
*
* @param {Object||String||Integer} option to select/deselect
* @param {Boolean} block removing
*/
select(option, key) {
/* istanbul ignore else */
if (option.$isLabel && this.groupSelect) {
this.selectGroup(option)
return
}
if (
this.blockKeys.indexOf(key) !== -1 ||
this.disabled ||
option.$isDisabled ||
option.$isLabel
)
return
/* istanbul ignore else */
if (this.max && this.multiple && this.internalValue.length === this.max)
return
/* istanbul ignore else */
if (key === 'Tab' && !this.pointerDirty) return
if (option.isTag) {
this.$emit('tag', option.label, this.id)
this.search = ''
if (this.closeOnSelect && !this.multiple) this.deactivate()
} else {
const isSelected = this.isSelected(option)
if (isSelected) {
if (key !== 'Tab') this.removeElement(option)
return
}
this.$emit('select', option, this.id)
if (this.multiple) {
this.$emit('input', this.internalValue.concat([option]), this.id)
} else {
this.$emit('input', option, this.id)
}
/* istanbul ignore else */
if (this.clearOnSelect) this.search = ''
}
/* istanbul ignore else */
if (this.closeOnSelect) this.deactivate()
},
/**
* Add the given group options to the list of selected options
* If all group optiona are already selected -> remove it from the results.
*
* @param {Object||String||Integer} group to select/deselect
*/
selectGroup(selectedGroup) {
const group = this.options.find((option) => {
return option[this.groupLabel] === selectedGroup.$groupLabel
})
if (!group) return
if (this.wholeGroupSelected(group)) {
this.$emit('remove', group[this.groupValues], this.id)
const newValue = this.internalValue.filter(
(option) => group[this.groupValues].indexOf(option) === -1
)
this.$emit('input', newValue, this.id)
} else {
const optionsToAdd = group[this.groupValues].filter(
(option) =>
!(this.isOptionDisabled(option) || this.isSelected(option))
)
this.$emit('select', optionsToAdd, this.id)
this.$emit('input', this.internalValue.concat(optionsToAdd), this.id)
}
},
/**
* Helper to identify if all values in a group are selected
*
* @param {Object} group to validated selected values against
*/
wholeGroupSelected(group) {
return group[this.groupValues].every(
(option) => this.isSelected(option) || this.isOptionDisabled(option)
)
},
/**
* Helper to identify if all values in a group are disabled
*
* @param {Object} group to check for disabled values
*/
wholeGroupDisabled(group) {
return group[this.groupValues].every(this.isOptionDisabled)
},
/**
* Removes the given option from the selected options.
* Additionally checks this.allowEmpty prop if option can be removed when
* it is the last selected option.
*
* @param {type} option description
* @returns {type} description
*/
removeElement(option, shouldClose = true) {
/* istanbul ignore else */
if (this.disabled) return
/* istanbul ignore else */
if (option.$isDisabled) return
/* istanbul ignore else */
if (!this.allowEmpty && this.internalValue.length <= 1) {
this.deactivate()
return
}
const index =
typeof option === 'object'
? this.valueKeys.indexOf(option[this.trackBy])
: this.valueKeys.indexOf(option)
this.$emit('remove', option, this.id)
if (this.multiple) {
const newValue = this.internalValue
.slice(0, index)
.concat(this.internalValue.slice(index + 1))
this.$emit('input', newValue, this.id)
} else {
this.$emit('input', null, this.id)
}
/* istanbul ignore else */
if (this.closeOnSelect && shouldClose) this.deactivate()
},
/**
* Calls this.removeElement() with the last element
* from this.internalValue (selected element Array)
*
* @fires this#removeElement
*/
removeLastElement() {
/* istanbul ignore else */
if (this.blockKeys.indexOf('Delete') !== -1) return
/* istanbul ignore else */
if (
this.search.length === 0 &&
Array.isArray(this.internalValue) &&
this.internalValue.length
) {
this.removeElement(
this.internalValue[this.internalValue.length - 1],
false
)
}
},
/**
* Opens the multiselects dropdown.
* Sets this.isOpen to TRUE
*/
activate() {
/* istanbul ignore else */
if (this.isOpen || this.disabled) return
this.adjustPosition()
/* istanbul ignore else */
if (
this.groupValues &&
this.pointer === 0 &&
this.filteredOptions.length
) {
this.pointer = 1
}
this.isOpen = true
/* istanbul ignore else */
if (this.searchable) {
if (!this.preserveSearch) this.search = ''
this.$nextTick(() => this.$refs.search && this.$refs.search.focus())
} else {
this.$el.focus()
}
this.$emit('open', this.id)
},
/**
* Closes the multiselects dropdown.
* Sets this.isOpen to FALSE
*/
deactivate() {
/* istanbul ignore else */
if (!this.isOpen) return
this.isOpen = false
/* istanbul ignore else */
if (this.searchable) {
this.$refs.search && this.$refs.search.blur()
} else {
this.$el.blur()
}
if (!this.preserveSearch) this.search = ''
this.$emit('close', this.getValue(), this.id)
},
/**
* Call this.activate() or this.deactivate()
* depending on this.isOpen value.
*
* @fires this#activate || this#deactivate
* @property {Boolean} isOpen indicates if dropdown is open
*/
toggle() {
this.isOpen ? this.deactivate() : this.activate()
},
/**
* Updates the hasEnoughSpace variable used for
* detecting where to expand the dropdown
*/
adjustPosition() {
if (typeof window === 'undefined') return
const spaceAbove = this.$el.getBoundingClientRect().top
const spaceBelow =
window.innerHeight - this.$el.getBoundingClientRect().bottom
const hasEnoughSpaceBelow = spaceBelow > this.maxHeight
if (
hasEnoughSpaceBelow ||
spaceBelow > spaceAbove ||
this.openDirection === 'below' ||
this.openDirection === 'bottom'
) {
this.preferredOpenDirection = 'below'
this.optimizedHeight = Math.min(spaceBelow - 40, this.maxHeight)
} else {
this.preferredOpenDirection = 'above'
this.optimizedHeight = Math.min(spaceAbove - 40, this.maxHeight)
}
},
},
}

View File

@ -1,172 +0,0 @@
import CraterTheme from '../theme/index'
const { multiselectOption } = CraterTheme.BaseSelect
export default {
data() {
return {
pointer: 0,
pointerDirty: false,
}
},
props: {
/**
* Enable/disable highlighting of the pointed value.
* @type {Boolean}
* @default true
*/
showPointer: {
type: Boolean,
default: true,
},
optionHeight: {
type: Number,
default: 40,
},
},
computed: {
pointerPosition() {
return this.pointer * this.optionHeight
},
visibleElements() {
return this.optimizedHeight / this.optionHeight
},
},
watch: {
filteredOptions() {
this.pointerAdjust()
},
isOpen() {
this.pointerDirty = false
},
pointer() {
this.$refs.search.setAttribute(
'aria-activedescendant',
this.id + '-' + this.pointer.toString()
)
},
},
methods: {
optionHighlight(index, option) {
return [
{
'multiselect__option--highlight':
index === this.pointer && this.showPointer,
'multiselect__option--selected': this.isSelected(option),
},
multiselectOption,
]
},
groupHighlight(index, selectedGroup) {
if (!this.groupSelect) {
return [
'multiselect__option--group',
'multiselect__option--disabled',
multiselectOption,
]
}
const group = this.options.find((option) => {
return option[this.groupLabel] === selectedGroup.$groupLabel
})
return group && !this.wholeGroupDisabled(group)
? [
'multiselect__option--group',
{
'multiselect__option--highlight':
index === this.pointer && this.showPointer,
},
{
'multiselect__option--group-selected': this.wholeGroupSelected(
group
),
},
multiselectOption,
]
: ['multiselect__option--disabled', multiselectOption]
},
addPointerElement({ key } = 'Enter') {
/* istanbul ignore else */
if (this.filteredOptions.length > 0) {
this.select(this.filteredOptions[this.pointer], key)
}
this.pointerReset()
},
pointerForward() {
/* istanbul ignore else */
if (this.pointer < this.filteredOptions.length - 1) {
this.pointer++
/* istanbul ignore next */
if (
this.$refs.list.scrollTop <=
this.pointerPosition - (this.visibleElements - 1) * this.optionHeight
) {
this.$refs.list.scrollTop =
this.pointerPosition -
(this.visibleElements - 1) * this.optionHeight
}
/* istanbul ignore else */
if (
this.filteredOptions[this.pointer] &&
this.filteredOptions[this.pointer].$isLabel &&
!this.groupSelect
)
this.pointerForward()
}
this.pointerDirty = true
},
pointerBackward() {
if (this.pointer > 0) {
this.pointer--
/* istanbul ignore else */
if (this.$refs.list.scrollTop >= this.pointerPosition) {
this.$refs.list.scrollTop = this.pointerPosition
}
/* istanbul ignore else */
if (
this.filteredOptions[this.pointer] &&
this.filteredOptions[this.pointer].$isLabel &&
!this.groupSelect
)
this.pointerBackward()
} else {
/* istanbul ignore else */
if (
this.filteredOptions[this.pointer] &&
this.filteredOptions[0].$isLabel &&
!this.groupSelect
)
this.pointerForward()
}
this.pointerDirty = true
},
pointerReset() {
/* istanbul ignore else */
if (!this.closeOnSelect) return
this.pointer = 0
/* istanbul ignore else */
if (this.$refs.list) {
this.$refs.list.scrollTop = 0
}
},
pointerAdjust() {
/* istanbul ignore else */
if (this.pointer >= this.filteredOptions.length - 1) {
this.pointer = this.filteredOptions.length
? this.filteredOptions.length - 1
: 0
}
if (
this.filteredOptions.length > 0 &&
this.filteredOptions[this.pointer].$isLabel &&
!this.groupSelect
) {
this.pointerForward()
}
},
pointerSet(index) {
this.pointer = index
this.pointerDirty = true
},
},
}

View File

@ -1,46 +0,0 @@
import Vue from 'vue'
import BaseModal from './modal/BaseModal.vue'
import BaseLoader from './BaseLoader.vue'
import BaseCustomerSelect from './BaseCustomerSelect.vue'
import BaseCustomInput from './BaseCustomInput.vue'
import CustomerSelectPopup from './popup/CustomerSelectPopup.vue'
import TaxSelectPopup from './popup/TaxSelectPopup.vue'
import NoteSelectPopup from './popup/NoteSelectPopup.vue'
import BaseDatePicker from '../base/BaseDatePicker.vue'
import BaseTimePicker from './BaseTimePicker.vue'
import BasePage from './BasePage.vue'
import BaseNotification from './BaseNotification.vue'
import GlobalSearch from '../GlobalSearch.vue'
import DotIcon from '../../components/icon/DotIcon.vue'
import SaveIcon from '../../components/icon/SaveIcon.vue'
import SwSelect from '@bytefury/spacewind/src/components/sw-select'
Vue.component('base-modal', BaseModal)
Vue.component('global-search', GlobalSearch)
Vue.component('base-page', BasePage)
Vue.component('base-date-picker', BaseDatePicker)
Vue.component('base-loader', BaseLoader)
Vue.component('sw-select', SwSelect)
Vue.component('base-customer-select', BaseCustomerSelect)
Vue.component('base-custom-input', BaseCustomInput)
Vue.component('customer-select-popup', CustomerSelectPopup)
Vue.component('tax-select-popup', TaxSelectPopup)
Vue.component('note-select-popup', NoteSelectPopup)
Vue.component('base-time-picker', BaseTimePicker)
Vue.component('base-notification', BaseNotification)
Vue.component('dot-icon', DotIcon)
Vue.component('save-icon', SaveIcon)

View File

@ -1,187 +0,0 @@
<template>
<div class="relative customer-modal">
<base-loader
v-if="isRequestOngoing"
:show-bg-overlay="true"
class="h-130"
/>
<form @submit.prevent="createNewBackup">
<div class="p-6">
<sw-input-group
:label="$t('settings.backup.select_backup_type')"
:error="optionError"
horizontal
required
class="py-2"
>
<sw-select
v-model="formData.option"
:options="options"
:searchable="true"
:show-labels="false"
:placeholder="$t('settings.backup.select_backup_type')"
:allow-empty="false"
:max-height="100"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.select_disk')"
:error="selectDiskError"
horizontal
required
class="py-2"
>
<sw-select
v-model="formData.selected_disk"
:options="getDisks"
:searchable="true"
:show-labels="false"
:placeholder="$t('settings.disk.select_disk')"
:allow-empty="false"
:preselect-first="true"
:custom-label="getCustomLabel"
:max-height="100"
:loading="isLoading"
track-by="id"
/>
</sw-input-group>
</div>
<div
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
>
<sw-button
class="mr-3"
variant="primary-outline"
type="button"
@click="cancelBackup"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button
:loading="isCreateLoading"
:disabled="isCreateLoading"
variant="primary"
type="submit"
>
<save-icon v-if="!isCreateLoading" class="mr-2" />
{{ $t('general.create') }}
</sw-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import AddressStub from '../../../stub/address'
import _ from 'lodash'
const { required } = require('vuelidate/lib/validators')
export default {
data() {
return {
isLoading: false,
isCreateLoading: false,
isRequestOngoing: false,
formData: {
option: 'full',
selected_disk: { driver: 'local' },
},
options: ['full', 'only-db', 'only-files'],
}
},
validations: {
formData: {
option: {
required,
},
selected_disk: {
required,
},
},
},
computed: {
...mapGetters('disks', ['getDisks']),
...mapGetters('modal', ['refreshData']),
optionError() {
if (!this.$v.formData.option.$error) {
return ''
}
if (!this.$v.formData.option) {
return this.$tc('validation.required')
}
},
selectDiskError() {
if (!this.$v.formData.selected_disk.$error) {
return ''
}
if (!this.$v.formData.selected_disk) {
return this.$tc('validation.required')
}
},
},
created() {
this.loadData()
},
methods: {
...mapActions('backup', ['createBackup']),
...mapActions('disks', ['fetchDisks']),
...mapActions('modal', ['closeModal']),
...mapActions('notification', ['showNotification']),
getCustomLabel({ driver, name }) {
return `${name} — [${driver}]`
},
async createNewBackup() {
let data = {
option: this.formData.option,
file_disk_id: this.formData.selected_disk.id,
}
try {
this.isCreateLoading = true
await this.createBackup(data)
this.isCreateLoading = false
this.showNotification({
type: 'success',
message: this.$t('settings.backup.created_message'),
})
this.refreshData ? this.refreshData() : ''
this.cancelBackup()
} catch (e) {
this.isCreateLoading = false
this.showNotification({
type: 'error',
message: e.response.data.message,
})
}
},
async loadData() {
this.isRequestOngoing = true
let res = await this.fetchDisks({ limit: 'all' })
this.formData.selected_disk = res.data.disks.data[0]
this.isRequestOngoing = false
},
cancelBackup() {
this.closeModal()
},
},
}
</script>

View File

@ -1,96 +0,0 @@
<template>
<div>
<sw-modal ref="baseModal" :variant="variant">
<template v-slot:header>
<div
class="absolute flex content-center justify-center w-5 cursor-pointer"
style="top: 20px; right: 15px"
@click="closeModal"
>
<x-icon />
</div>
<span>{{ modalTitle }}</span>
</template>
<component :is="component" />
</sw-modal>
</div>
</template>
<script>
import { XIcon } from '@vue-hero-icons/solid'
import { mapActions, mapGetters } from 'vuex'
import TaxTypeModal from './TaxTypeModal'
import ItemModal from './ItemModal'
import EstimateTemplate from './EstimateTemplate'
import InvoiceTemplate from './InvoiceTemplate'
import CustomerModal from './CustomerModal'
import CategoryModal from './CategoryModal'
import BackupModal from './BackupModal'
import PaymentMode from './PaymentModeModal'
import ItemUnit from './ItemUnitModal'
import MailTestModal from './MailTestModal'
import SendInvoiceModal from './SendInvoiceModal'
import SendEstimateModal from './SendEstimateModal'
import SendPaymentModal from './SendPaymentModal'
import FileDiskModal from './FileDiskModal'
import SetDefaultDiskModal from './SetDefaultDiskModal'
import CustomFieldModal from './CustomField/Index'
import NoteSelectModal from './NoteModal'
export default {
components: {
TaxTypeModal,
ItemModal,
EstimateTemplate,
InvoiceTemplate,
CustomerModal,
CategoryModal,
BackupModal,
PaymentMode,
ItemUnit,
MailTestModal,
SendInvoiceModal,
SendEstimateModal,
SendPaymentModal,
XIcon,
FileDiskModal,
SetDefaultDiskModal,
CustomFieldModal,
NoteSelectModal,
},
data() {
return {
component: '',
}
},
computed: {
...mapGetters('modal', [
'modalActive',
'modalTitle',
'componentName',
'modalSize',
'modalData',
'variant',
]),
},
watch: {
componentName(component) {
if (!component) {
return
}
this.component = component
},
modalActive(status) {
if (status) {
this.$refs.baseModal.open()
return true
}
this.$refs.baseModal.close()
return false
},
},
methods: {
...mapActions('modal', ['openModal', 'closeModal']),
},
}
</script>

View File

@ -1,198 +0,0 @@
<template>
<form action="" @submit.prevent="submitCategoryData">
<div class="p-8 sm:p-6">
<sw-input-group
:label="$t('expenses.category')"
:error="nameError"
variant="horizontal"
required
>
<sw-input
ref="name"
:invalid="$v.formData.name.$error"
v-model="formData.name"
type="text"
@input="$v.formData.name.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('expenses.description')"
:error="descriptionError"
class="mt-5"
variant="horizontal"
>
<sw-textarea
v-model="formData.description"
rows="4"
cols="50"
@input="$v.formData.description.$touch()"
/>
</sw-input-group>
</div>
<div
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid border-modal-bg"
>
<sw-button
type="button"
variant="primary-outline"
class="mr-3 text-sm"
@click="closeCategoryModal"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button :loading="isLoading" variant="primary" type="submit">
<save-icon v-if="!isLoading" class="mr-2" />
{{ !isEdit ? $t('general.save') : $t('general.update') }}
</sw-button>
</div>
</form>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
const { required, minLength, maxLength } = require('vuelidate/lib/validators')
export default {
data() {
return {
isEdit: false,
isLoading: false,
formData: {
id: null,
name: null,
description: null,
},
}
},
computed: {
...mapGetters('modal', [
'modalDataID',
'modalData',
'modalActive',
'refreshData',
]),
nameError() {
if (!this.$v.formData.name.$error) {
return ''
}
if (!this.$v.formData.name.required) {
return this.$tc('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 }
)
}
},
descriptionError() {
if (!this.$v.formData.description.$error) {
return ''
}
if (!this.$v.formData.name.maxLength) {
return this.$tc('validation.description_maxlength')
}
},
},
validations: {
formData: {
name: {
required,
minLength: minLength(3),
},
description: {
maxLength: maxLength(255),
},
},
},
watch: {
modalDataID(val) {
if (val) {
this.isEdit = true
this.setData()
} else {
this.isEdit = false
}
},
modalActive(val) {
if (!this.modalActive) {
this.resetFormData()
}
},
},
mounted() {
this.$refs.name.focus = true
if (this.modalDataID) {
this.isEdit = true
this.setData()
}
},
methods: {
...mapActions('modal', ['closeModal']),
...mapActions('category', ['addCategory', 'updateCategory']),
...mapActions('notification', ['showNotification']),
resetFormData() {
this.formData = {
id: null,
name: null,
description: null,
}
this.$v.formData.$reset()
},
async submitCategoryData() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let response
if (!this.isEdit) {
response = await this.addCategory(this.formData)
} else {
response = await this.updateCategory(this.formData)
}
if (response.data) {
if (!this.isEdit) {
this.showNotification({
type: 'success',
message: this.$t('settings.expense_category.created_message'),
})
} else {
this.showNotification({
type: 'success',
message: this.$t('settings.expense_category.updated_message'),
})
}
window.hub.$emit('newCategory', response.data.category)
this.refreshData ? this.refreshData() : ''
this.closeCategoryModal()
this.isLoading = false
return true
}
this.showNotification({
type: 'error',
message: response.data.error,
})
},
async setData() {
this.formData = {
id: this.modalData.id,
name: this.modalData.name,
description: this.modalData.description,
}
},
closeCategoryModal() {
this.resetFormData()
this.closeModal()
},
},
}
</script>

View File

@ -1,505 +0,0 @@
<template>
<div class="custom-field-modal">
<form action="" @submit.prevent="submitCustomFieldData">
<div
class="px-8 py-8 overflow-y-auto sw-scroll sm:p-6"
style="max-height: 600px"
>
<sw-input-group
:label="$t('settings.custom_fields.name')"
:error="nameError"
horizontal
required
>
<sw-input
ref="name"
:invalid="$v.formData.name.$error"
v-model="formData.name"
type="text"
@input="$v.formData.name.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.custom_fields.model')"
:error="modalTypeError"
class="mt-5"
horizontal
required
>
<sw-select
v-model="formData.model_type"
:options="modelTypes"
:invalid="$v.formData.model_type.$error"
:searchable="true"
:show-labels="false"
:allow-empty="false"
@input="$v.formData.model_type.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.custom_fields.required')"
class="mt-5"
horizontal
>
<sw-switch v-model="formData.is_required" style="margin-top: -20px" />
</sw-input-group>
<sw-input-group
:label="$t('settings.custom_fields.type')"
:error="dataTypeError"
class="mt-5"
horizontal
required
>
<sw-select
v-model="selectType"
:options="dataTypes"
:invalid="$v.selectType.$error"
:searchable="true"
:show-labels="false"
:allow-empty="false"
track-by="label"
label="label"
@input="onSelectTypeChange"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.custom_fields.label')"
:error="labelError"
class="mt-5"
horizontal
required
>
<sw-input
ref="name"
:invalid="$v.formData.label.$error"
v-model="formData.label"
type="text"
@input="$v.formData.label.$touch()"
/>
</sw-input-group>
<sw-input-group
v-if="isDropdownSelected"
:label="$t('settings.custom_fields.options')"
class="mt-5"
horizontal
>
<option-create @onAdd="addNewOptions" />
<div
v-for="(option, index) in formData.options"
:key="index"
class="flex items-center"
style="margin-top: 5px"
>
<sw-input v-model="option.name" type="text" style="width: 90%" />
<minus-circle-icon
class="ml-1 cursor-pointer icon text-danger"
@click="removeOption(index)"
/>
</div>
</sw-input-group>
<sw-input-group
v-if="formData.type"
:label="$t('settings.custom_fields.default_value')"
horizontal
class="relative mt-5"
>
<component
:value="formData.default_answer"
:is="formData.type + 'Type'"
:options="formData.options"
:default-date-time="formData.dateTimeValue"
v-model="formData.default_answer"
/>
</sw-input-group>
<sw-input-group
v-if="!isSwitchTypeSelected"
:label="$t('settings.custom_fields.placeholder')"
class="mt-5"
horizontal
>
<sw-input v-model="formData.placeholder" type="text" />
</sw-input-group>
<sw-input-group
:label="$t('settings.custom_fields.order')"
:error="orderError"
class="mt-5"
horizontal
>
<sw-input
v-model="formData.order"
:invalid="$v.formData.order.$error"
type="number"
@input="$v.formData.order.$touch()"
/>
</sw-input-group>
</div>
<div
class="z-0 flex justify-end p-4 border-t border-solid border-gray-light border-modal-bg"
>
<sw-button
class="mr-3"
type="button"
variant="primary-outline"
@click="closeCategoryModal"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button
:loading="isLoading"
:disabled="isLoading"
variant="primary"
type="submit"
>
<save-icon v-if="!isLoading" class="mr-2" />
{{ !isEdit ? $t('general.save') : $t('general.update') }}
</sw-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { MinusCircleIcon } from '@vue-hero-icons/solid'
import InputType from './types/InputType'
import NumberType from './types/NumberType'
import SwitchType from './types/SwitchType'
import TextAreaType from './types/TextAreaType'
import TimeType from './types/TimeType'
import UrlType from './types/UrlType'
import PhoneType from './types/PhoneType'
import DateTimeType from './types/DateTimeType'
import DateType from './types/DateType'
import DropdownType from './types/DropdownType'
import OptionCreate from './types/OptionsCreate'
import moment from 'moment'
const {
required,
requiredIf,
numeric,
minLength,
} = require('vuelidate/lib/validators')
export default {
components: {
MinusCircleIcon,
OptionCreate,
InputType,
NumberType,
SwitchType,
TextAreaType,
TimeType,
UrlType,
PhoneType,
DateTimeType,
DateType,
DropdownType,
},
data() {
return {
isEdit: false,
dateType: 'custom',
isLoading: false,
relativeDateTypes: [
'Today',
'Tomorrow',
'Yesterday',
'Starting Date of Week',
'Ending Date of Week',
'Starting Date of Next Week',
'Ending Date of Next Week',
'Starting Date of Previous Week',
'Ending Date of Previous Week',
'Starting Date of Month',
'Ending Date of Month',
'Starting Date of Next Month',
'Ending Date of Next Month',
'Starting Date of Previous Month',
'Ending Date of Previous Month',
'Starting Date of Fiscal Month',
'Ending Date of Fiscal Month',
],
dataTypes: [
{ label: 'Text', value: 'Input' },
{ label: 'Textarea', value: 'TextArea' },
{ label: 'Phone', value: 'Phone' },
{ label: 'URL', value: 'Url' },
{ label: 'Number', value: 'Number' },
{ label: 'Select Field', value: 'Dropdown' },
{ label: 'Switch Toggle', value: 'Switch' },
{ label: 'Date', value: 'Date' },
{ label: 'Time', value: 'Time' },
{ label: 'Date & Time', value: 'DateTime' },
],
modelTypes: ['Customer', 'Invoice', 'Estimate', 'Expense', 'Payment'],
selectType: null,
formData: {
label: null,
type: null,
name: null,
default_answer: null,
is_required: false,
placeholder: null,
model_type: null,
order: 1,
options: [],
},
}
},
validations: {
selectType: {
required,
},
formData: {
name: {
required,
},
label: {
required,
},
model_type: {
required,
},
order: {
required,
numeric,
},
options: {
required: requiredIf('isDropdownSelected'),
minLength: minLength(1),
$each: {
name: {
required: requiredIf('isDropdownSelected'),
minLength: minLength(1),
},
},
},
},
},
computed: {
...mapGetters('modal', ['modalData', 'modalDataID', 'refreshData']),
isDropdownSelected() {
if (this.selectType && this.selectType.label === 'Select Field') {
return true
}
return false
},
isSwitchTypeSelected() {
if (this.selectType && this.selectType.label === 'Switch Toggle') {
return true
}
return false
},
nameError() {
if (!this.$v.formData.name.$error) {
return ''
}
if (!this.$v.formData.name.required) {
return this.$tc('validation.required')
}
},
labelError() {
if (!this.$v.formData.label.$error) {
return ''
}
if (!this.$v.formData.label.required) {
return this.$tc('validation.required')
}
},
modalTypeError() {
if (!this.$v.formData.model_type.$error) {
return ''
}
if (!this.$v.formData.model_type.required) {
return this.$tc('validation.required')
}
},
dataTypeError() {
if (!this.$v.selectType.$error) {
return ''
}
if (!this.$v.selectType.required) {
return this.$tc('validation.required')
}
},
hasPlaceHolder() {
if (this.selectType.label == 'Switch Toggle') {
return false
}
return true
},
orderError() {
if (!this.$v.formData.order.$error) {
return ''
}
if (!this.$v.formData.order.required) {
return this.$tc('validation.required')
}
if (!this.$v.formData.order.numeric) {
return this.$tc('validation.numbers_only')
}
},
},
watch: {
'formData.type'(newValue, oldvalue) {
if (oldvalue != null || oldvalue != undefined) {
this.onChangeReset()
}
},
dateType(newValue, oldvalue) {
if (oldvalue != null || oldvalue != undefined) {
this.onChangeReset()
}
},
},
mounted() {
if (this.modalDataID) {
this.setData()
return true
}
this.formData.model_type = this.modelTypes[0]
this.selectType = this.dataTypes[0]
this.formData.type = this.dataTypes[0].value
},
methods: {
...mapActions('customFields', [
'addCustomField',
'updateCustomField',
'fetchCustomField',
]),
...mapActions('modal', ['closeModal']),
...mapActions('notification', ['showNotification']),
resetFormData() {
this.formData = {
label: null,
label: null,
type: null,
dateTimeValue: null,
default_answer: null,
is_required: false,
placeholder: null,
model_type: null,
options: [{ name: '' }],
}
this.$v.$reset()
},
async submitCustomFieldData() {
this.$v.selectType.$touch()
this.$v.formData.$touch()
if (this.$v.$invalid) {
return false
}
let data = {
...this.formData,
options: this.formData.options.map((option) => option.name),
default_answer:
this.isDropdownSelected && this.formData.default_answer
? this.formData.default_answer.name
: this.formData.default_answer,
}
if (this.isSwitchTypeSelected && this.formData.default_answer == null) {
data.default_answer = false
}
if (data.type == 'Date') {
data.default_answer = data.default_answer
? moment(data.default_answer).format('YYYY-MM-DD')
: null
}
if (data.type == 'Time' && typeof data.default_answer == 'object') {
let HH =
data && data.default_answer && data.default_answer.HH
? data.default_answer.HH
: null
let mm =
data && data.default_answer && data.default_answer.mm
? data.default_answer.mm
: null
let ss =
data && data.default_answer && data.default_answer.ss
? data.default_answer.ss
: null
data.default_answer = `${HH}:${mm}:${ss}`
}
let response = null
if (this.isEdit) {
this.isLoading = true
response = await this.updateCustomField(data)
this.showNotification({
type: 'success',
message: this.$tc('settings.custom_fields.updated_message'),
})
this.refreshData()
this.closeCategoryModal()
return true
}
this.isLoading = true
response = await this.addCustomField(data)
this.showNotification({
type: 'success',
message: this.$tc('settings.custom_fields.added_message'),
})
this.refreshData()
this.closeCategoryModal()
return true
},
addNewOptions(option) {
this.formData.options = [{ name: option }, ...this.formData.options]
},
removeOption(index) {
this.formData.options.splice(index, 1)
},
async setData() {
let response = await this.fetchCustomField(this.modalDataID)
let fieldData = response.data.customField
this.isEdit = true
let data = {
...fieldData,
options: [],
dateTimeValue: fieldData.defaultAnswer,
default_answer: fieldData.defaultAnswer,
options: fieldData.options
? fieldData.options.map((option) => {
return { name: option ? option : '' }
})
: [],
}
this.selectType = this.dataTypes.find(
(type) => type.value == fieldData.type
)
if (data.type == 'Dropdown') {
data.default_answer = { name: fieldData.defaultAnswer }
}
this.formData = { ...data }
},
onChangeReset() {
this.formData = {
...this.formData,
default_answer: null,
is_required: false,
placeholder: null,
options: [],
}
},
onSelectTypeChange(data) {
this.formData.type = data.value
this.$v.selectType.$touch()
},
closeCategoryModal() {
this.resetFormData()
this.closeModal()
},
},
}
</script>

View File

@ -1,35 +0,0 @@
<template>
<base-date-picker
v-model="inputValue"
:enable-time="true"
@input="$emit('input', inputValue)"
/>
</template>
<script>
import moment from 'moment'
export default {
props: {
value: {
type: String,
default: null,
},
defaultDateTime: {
type: String,
default: null,
},
},
data() {
return {
inputValue: this.value ? this.value : moment().format('YYYY-MM-DD hh:mm'),
}
},
watch: {
value(data) {
this.inputValue = data
},
defaultDateTime(data) {
this.dateTimeValue = data
},
},
}
</script>

View File

@ -1,29 +0,0 @@
<template>
<base-date-picker
:calendar-button="true"
v-model="inputValue"
calendar-button-icon="calendar"
@input="$emit('input', inputValue)"
/>
</template>
<script>
import moment from 'moment'
export default {
props: {
value: {
type: [String, Date],
default: null,
},
},
data() {
return {
inputValue: moment().format('YYYY-MM-DD'),
}
},
watch: {
value(data) {
this.inputValue = data
},
},
}
</script>

View File

@ -1,37 +0,0 @@
<template>
<sw-select
v-model="inputValue"
:options="inputOptions"
:taggable="true"
:show-labels="false"
label="name"
track-by="name"
@input="$emit('input', inputValue)"
/>
</template>
<script>
export default {
props: {
value: {
type: [String, Object],
default: null,
},
options: {
type: Array,
default: () => [],
},
},
data() {
return {
inputValue: this.value,
inputOptions: this.options,
}
},
watch: {
options(data) {
this.inputOptions = data
this.inputValue = null
},
},
}
</script>

View File

@ -1,27 +0,0 @@
<template>
<sw-input
v-model="inputValue"
type="text"
@input="$emit('input', inputValue)"
/>
</template>
<script>
export default {
props: {
value: {
type: String,
default: null,
},
},
data() {
return {
inputValue: this.value,
}
},
watch: {
value(data) {
this.inputValue = data
},
},
}
</script>

View File

@ -1,27 +0,0 @@
<template>
<sw-input
v-model="inputValue"
type="number"
@input="$emit('input', inputValue)"
/>
</template>
<script>
export default {
props: {
value: {
type: [String, Number],
default: null,
},
},
data() {
return {
inputValue: this.value,
}
},
watch: {
value(data) {
this.inputValue = data
},
},
}
</script>

View File

@ -1,41 +0,0 @@
<template>
<div class="flex items-center" style="margin-top: 5px">
<sw-input
v-model="option"
type="text"
style="width: 90%"
@handleEnter.stop="onAddOption"
/>
<plus-circle-icon
class="ml-1 cursor-pointer text-danger"
@click="onAddOption"
/>
</div>
</template>
<script>
import { PlusCircleIcon } from '@vue-hero-icons/solid'
export default {
components: {
PlusCircleIcon,
},
data() {
return {
option: null,
}
},
methods: {
onAddOption() {
if (
this.option == null ||
this.option == '' ||
this.option == undefined
) {
return true
}
this.$emit('onAdd', this.option)
this.option = null
},
},
}
</script>

View File

@ -1,27 +0,0 @@
<template>
<sw-input
v-model="inputValue"
type="text"
@input="$emit('input', inputValue)"
/>
</template>
<script>
export default {
props: {
value: {
type: [String, Number],
default: null,
},
},
data() {
return {
inputValue: this.value,
}
},
watch: {
value(data) {
this.inputValue = data
},
},
}
</script>

View File

@ -1,27 +0,0 @@
<template>
<sw-switch
class="-mt-3"
v-model="inputValue"
@input="$emit('input', inputValue)"
/>
</template>
<script>
export default {
props: {
value: {
type: [Boolean, Number],
default: false,
},
},
data() {
return {
inputValue: this.value,
}
},
watch: {
value(data) {
this.inputValue = data
},
},
}
</script>

View File

@ -1,28 +0,0 @@
<template>
<sw-textarea
v-model="inputValue"
rows="2"
name="description"
@input="$emit('input', inputValue)"
/>
</template>
<script>
export default {
props: {
value: {
type: String,
default: null,
},
},
data() {
return {
inputValue: this.value,
}
},
watch: {
value(data) {
this.inputValue = data
},
},
}
</script>

View File

@ -1,28 +0,0 @@
<template>
<base-time-picker
:value="inputValue"
v-model="inputValue"
hide-clear-button
@input="$emit('input', inputValue)"
/>
</template>
<script>
export default {
props: {
value: {
type: [String, Object],
default: null,
},
},
data() {
return {
inputValue: this.value,
}
},
watch: {
value(data) {
this.inputValue = data
},
},
}
</script>

View File

@ -1,27 +0,0 @@
<template>
<sw-input
v-model="inputValue"
type="url"
@input="$emit('input', inputValue)"
/>
</template>
<script>
export default {
props: {
value: {
type: String,
default: null,
},
},
data() {
return {
inputValue: this.value,
}
},
watch: {
value(data) {
this.inputValue = data
},
},
}
</script>

View File

@ -1,742 +0,0 @@
<template>
<div class="customer-modal">
<form action="" @submit.prevent="submitCustomerData">
<div class="flex-1 p-5 sm:p-6">
<sw-tabs>
<!-- tab1 -->
<sw-tab-item title="Basic Info" class="mt-5">
<sw-input-group
:label="$t('customers.display_name')"
:error="nameError"
variant="horizontal"
required
>
<sw-input
ref="name"
:invalid="$v.formData.name.$error"
v-model.trim="formData.name"
type="text"
name="name"
class="mt-1 md:mt-0"
@input="$v.formData.name.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.primary_display_name')"
class="mt-4"
variant="horizontal"
>
<sw-input
v-model="formData.contact_name"
type="text"
class="mt-1 md:mt-0"
/>
</sw-input-group>
<sw-input-group
:label="$t('login.email')"
:error="emailError"
class="mt-4"
variant="horizontal"
>
<sw-input
:invalid="$v.formData.email.$error"
v-model.trim="formData.email"
type="text"
name="email"
class="mt-1 md:mt-0"
@input="$v.formData.email.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$tc('settings.currencies.currency')"
class="mt-4"
variant="horizontal"
>
<sw-select
v-model="currency"
:options="currencies"
:searchable="true"
:allow-empty="false"
:show-labels="false"
:placeholder="$t('customers.select_currency')"
:max-height="200"
label="name"
class="mt-1 md:mt-0"
track-by="id"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.phone')"
class="mt-4"
variant="horizontal"
>
<sw-input
v-model.trim="formData.phone"
type="text"
name="phone"
class="mt-1 md:mt-0"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.website')"
:error="websiteError"
class="mt-4"
variant="horizontal"
>
<sw-input
v-model="formData.website"
:invalid="$v.formData.website.$error"
type="url"
class="mt-1 md:mt-0"
@input="$v.formData.website.$touch()"
/>
</sw-input-group>
</sw-tab-item>
<!-- tab2 -->
<sw-tab-item title="Billing Address" class="mt-5">
<sw-input-group :label="$t('customers.name')" variant="horizontal">
<sw-input
v-model="billing.name"
type="text"
class="mt-1 md:mt-0"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.phone')"
class="mt-4"
variant="horizontal"
>
<sw-input
v-model.trim="billing.phone"
type="text"
name="phone"
class="mt-1 md:mt-0"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.address')"
:error="bill1Error"
class="mt-4"
variant="horizontal"
>
<sw-textarea
v-model="billing.address_street_1"
:placeholder="$t('general.street_1')"
rows="2"
cols="50"
class="mt-1 md:mt-0"
@input="$v.billing.address_street_1.$touch()"
/>
<br />
</sw-input-group>
<sw-input-group :error="bill2Error" variant="horizontal">
<sw-textarea
v-model="billing.address_street_2"
:placeholder="$t('general.street_2')"
rows="2"
cols="50"
@input="$v.billing.address_street_2.$touch()"
/>
<br />
</sw-input-group>
<sw-input-group
:label="$t('customers.country')"
class="mt-4"
variant="horizontal"
>
<sw-select
v-model="billingCountry"
:options="countries"
:searchable="true"
:show-labels="false"
:placeholder="$t('general.select_country')"
:allow-empty="false"
track-by="id"
label="name"
class="mt-1 md:mt-0"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.state')"
class="mt-4"
variant="horizontal"
>
<sw-input
v-model="billing.state"
type="text"
name="billingState"
class="mt-1 md:mt-0"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.city')"
class="mt-4"
variant="horizontal"
>
<sw-input
v-model="billing.city"
type="text"
name="billingCity"
class="mt-1 md:mt-0"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.zip_code')"
class="mt-4"
variant="horizontal"
>
<sw-input
v-model="billing.zip"
type="text"
class="mt-1 md:mt-0"
/>
</sw-input-group>
</sw-tab-item>
<!-- tab3 -->
<sw-tab-item title="Shipping Address" class="mt-5">
<div class="grid md:grid-cols-12">
<div class="flex justify-end col-span-12">
<sw-button
ref="sameAddress"
variant="primary"
type="button"
@click="copyAddress(true)"
>
{{ $t('customers.copy_billing_address') }}
</sw-button>
</div>
</div>
<sw-input-group
:label="$t('customers.name')"
class="mt-4"
variant="horizontal"
>
<sw-input
v-model="shipping.name"
type="text"
class="mt-1 md:mt-0"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.phone')"
class="mt-4"
variant="horizontal"
>
<sw-input
v-model.trim="shipping.phone"
type="text"
name="phone"
class="mt-1 md:mt-0"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.address')"
:error="ship1Error"
class="mt-4"
variant="horizontal"
>
<sw-textarea
v-model="shipping.address_street_1"
:placeholder="$t('general.street_1')"
rows="2"
cols="50"
class="mt-1 md:mt-0"
@input="$v.shipping.address_street_1.$touch()"
/>
<br />
</sw-input-group>
<sw-input-group :error="ship2Error" variant="horizontal">
<sw-textarea
v-model="shipping.address_street_2"
:placeholder="$t('general.street_2')"
rows="2"
cols="50"
@input="$v.shipping.address_street_2.$touch()"
/>
<br />
</sw-input-group>
<sw-input-group
:label="$t('customers.country')"
class="mt-4"
variant="horizontal"
>
<sw-select
v-model="shippingCountry"
:options="countries"
:searchable="true"
:show-labels="false"
:allow-empty="false"
:placeholder="$t('general.select_country')"
track-by="id"
label="name"
class="mt-1 md:mt-0"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.state')"
class="mt-4"
variant="horizontal"
>
<sw-input
v-model="shipping.state"
type="text"
name="shippingState"
class="mt-1 md:mt-0"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.city')"
class="mt-4"
variant="horizontal"
>
<sw-input
v-model="shipping.city"
type="text"
name="shippingCity"
class="mt-1 md:mt-0"
/>
</sw-input-group>
<sw-input-group
:label="$t('customers.zip_code')"
class="mt-4"
variant="horizontal"
>
<sw-input
v-model="shipping.zip"
type="text"
class="mt-1 md:mt-0"
/>
</sw-input-group>
</sw-tab-item>
</sw-tabs>
</div>
<div
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
>
<sw-button
class="mr-3 text-sm"
type="button"
variant="primary-outline"
@click="cancelCustomer"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button :loading="isLoading" variant="primary" type="submit">
<save-icon v-if="!isLoading" class="mr-2" />
{{ $t('general.save') }}
</sw-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import AddressStub from '../../../stub/address'
const {
required,
minLength,
email,
numeric,
url,
maxLength,
} = require('vuelidate/lib/validators')
export default {
data() {
return {
isEdit: false,
isLoading: false,
billingCountry: null,
shippingCountry: null,
isCopyFromBilling: false,
currency: '',
isDisabledBillingState: true,
isDisabledBillingCity: true,
isDisabledShippingState: true,
isDisabledShippingCity: true,
formData: {
id: null,
name: null,
currency_id: null,
phone: null,
website: null,
contact_name: null,
addresses: [],
},
billing: {
name: null,
country_id: null,
state: null,
city: null,
phone: null,
zip: null,
address_street_1: null,
address_street_2: null,
type: 'billing',
},
shipping: {
name: null,
country_id: null,
state: null,
city: null,
phone: null,
zip: null,
address_street_1: null,
address_street_2: null,
type: 'shipping',
},
}
},
validations: {
formData: {
name: {
required,
minLength: minLength(3),
},
email: {
email,
},
website: {
url,
},
},
billing: {
address_street_1: {
maxLength: maxLength(255),
},
address_street_2: {
maxLength: maxLength(255),
},
},
shipping: {
address_street_1: {
maxLength: maxLength(255),
},
address_street_2: {
maxLength: maxLength(255),
},
},
},
computed: {
...mapGetters(['currencies', 'countries']),
...mapGetters('company', ['defaultCurrency']),
...mapGetters('modal', ['modalDataID', 'modalData', 'modalActive']),
nameError() {
if (!this.$v.formData.name.$error) {
return ''
}
if (!this.$v.formData.name.required) {
return this.$tc('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 }
)
}
if (!this.$v.formData.name.alpha) {
return this.$tc('validation.characters_only')
}
},
emailError() {
if (!this.$v.formData.email.$error) {
return ''
}
if (!this.$v.formData.email.email) {
return this.$t('validation.email_incorrect')
}
},
websiteError() {
if (!this.$v.formData.website.$error) {
return ''
}
if (!this.$v.formData.website.url) {
return this.$tc('validation.invalid_url')
}
},
bill1Error() {
if (!this.$v.billing.address_street_1.$error) {
return ''
}
if (!this.$v.billing.address_street_1.maxLength) {
return this.$t('validation.address_maxlength')
}
},
bill2Error() {
if (!this.$v.billing.address_street_2.$error) {
return ''
}
if (!this.$v.billing.address_street_2.maxLength) {
return this.$t('validation.address_maxlength')
}
},
ship1Error() {
if (!this.$v.shipping.address_street_1.$error) {
return ''
}
if (!this.$v.shipping.address_street_1.maxLength) {
return this.$t('validation.address_maxlength')
}
},
ship2Error() {
if (!this.$v.shipping.address_street_2.$error) {
return ''
}
if (!this.$v.shipping.address_street_2.maxLength) {
return this.$t('validation.address_maxlength')
}
},
hasBillingAdd() {
let billing = this.billing
if (
billing.name ||
billing.country_id ||
billing.state ||
billing.city ||
billing.phone ||
billing.zip ||
billing.address_street_1 ||
billing.address_street_2
) {
return true
}
return false
},
hasShippingAdd() {
let shipping = this.shipping
if (
shipping.name ||
shipping.country_id ||
shipping.state ||
shipping.city ||
shipping.phone ||
shipping.zip ||
shipping.address_street_1 ||
shipping.address_street_2
) {
return true
}
return false
},
},
watch: {
modalDataID(val) {
if (val) {
this.isEdit = true
this.setData()
} else {
this.isEdit = false
}
},
billingCountry() {
if (this.billingCountry) {
this.billing.country_id = this.billingCountry.id
return true
}
},
shippingCountry() {
if (this.shippingCountry) {
this.shipping.country_id = this.shippingCountry.id
return true
}
},
},
mounted() {
this.$refs.name.focus = true
this.currency = this.defaultCurrency
if (this.modalDataID) {
this.setData()
}
},
methods: {
...mapActions('invoice', {
setInvoiceCustomer: 'selectCustomer',
}),
...mapActions('estimate', {
setEstimateCustomer: 'selectCustomer',
}),
...mapActions('customer', [
'fetchCustomer',
'addCustomer',
'updateCustomer',
]),
...mapActions('modal', ['closeModal']),
...mapActions('notification', ['showNotification']),
resetData() {
this.formData = {
name: null,
currency_id: null,
phone: null,
website: null,
contact_name: null,
addresses: [],
}
this.billingCountry = null
this.shippingCountry = null
this.billing = { ...AddressStub }
this.shipping = { ...AddressStub }
this.$v.formData.$reset()
},
cancelCustomer() {
this.resetData()
this.closeModal()
},
copyAddress(val) {
if (val === true) {
this.isCopyFromBilling = true
this.shipping = { ...this.billing, type: 'shipping' }
this.shippingCountry = this.billingCountry
} else {
this.shipping = { ...AddressStub, type: 'shipping' }
this.shippingCountry = null
}
},
async loadData() {
let response = await this.fetchCustomer()
this.formData.currency_id = response.data.currency.id
return true
},
checkAddress() {
const isBillingEmpty = Object.values(this.billing).every(
(val) => val === null || val === ''
)
const isShippingEmpty = Object.values(this.shipping).every(
(val) => val === null || val === ''
)
if (isBillingEmpty === true && isBillingEmpty === true) {
this.formData.addresses = []
return true
}
if (isBillingEmpty === false && isShippingEmpty === false) {
this.formData.addresses = [
{ ...this.billing, type: 'billing' },
{ ...this.shipping, type: 'shipping' },
]
return true
}
if (isBillingEmpty === false) {
this.formData.addresses.push({ ...this.billing, type: 'billing' })
return true
}
this.formData.addresses = [{ ...this.shipping, type: 'shipping' }]
return true
},
async setData() {
this.formData.id = this.modalData.id
this.formData.name = this.modalData.name
this.formData.email = this.modalData.email
this.formData.contact_name = this.modalData.contact_name
this.formData.phone = this.modalData.phone
this.formData.website = this.modalData.website
this.currency = this.modalData.currency
if (this.modalData.billing_address) {
this.billing = this.modalData.billing_address
this.billingCountry = this.modalData.billing_address.country
}
if (this.modalData.shipping_address) {
this.shipping = this.modalData.shipping_address
this.shippingCountry = this.modalData.shipping_address.country
}
},
async submitCustomerData() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
// this.checkAddress()
if (this.hasBillingAdd && this.hasShippingAdd) {
this.formData.addresses = [{ ...this.billing }, { ...this.shipping }]
} else if (this.hasBillingAdd) {
this.formData.addresses = [{ ...this.billing }]
} else if (this.hasShippingAdd) {
this.formData.addresses = [{ ...this.shipping }]
}
this.isLoading = true
if (this.currency) {
this.formData.currency_id = this.currency.id
} else {
this.formData.currency_id = this.defaultCurrency.id
}
try {
let response = null
if (this.modalDataID) {
response = await this.updateCustomer(this.formData)
} else {
response = await this.addCustomer(this.formData)
}
if (response.data) {
if (this.modalDataID) {
this.showNotification({
type: 'success',
message: this.$tc('customers.updated_message'),
})
} else {
this.showNotification({
type: 'success',
message: this.$tc('customers.created_message'),
})
}
this.isLoading = false
if (
this.$route.name === 'invoices.create' ||
this.$route.name === 'invoices.edit'
) {
this.setInvoiceCustomer(response.data.customer.id)
}
if (
this.$route.name === 'estimates.create' ||
this.$route.name === 'estimates.edit'
) {
this.setEstimateCustomer(response.data.customer.id)
}
this.resetData()
this.closeModal()
return true
}
} catch (err) {
this.isLoading = false
// if (err.response.data.errors.email) {
// window.toastr['error'](this.$t('validation.email_already_taken'))
// }
}
},
},
}
</script>

View File

@ -1,91 +0,0 @@
<template>
<div class="template-modal">
<div class="px-8 py-8 sm:p-6">
<div class="grid grid-cols-3 gap-2 p-1 overflow-x-auto sw-scroll">
<div
v-for="(template, index) in modalData"
:key="index"
:class="{
'border border-solid border-primary-500':
selectedTemplate === template.name,
}"
class="relative flex flex-col m-2 border border-gray-200 border-solid cursor-pointer hover:border-primary-300"
>
<img
:src="template.path"
:alt="template.name"
class="w-full"
@click="selectedTemplate = template.name"
/>
<img
v-if="selectedTemplate === template.name"
:alt="template.name"
class="absolute z-10 w-5 h-5 text-primary-500"
style="top: -6px; right: -5px"
src="/assets/img/tick.png"
/>
<span
:class="[
'w-full p-1 bg-gray-200 text-sm text-center absolute bottom-0 left-0',
{
'text-primary-500 bg-primary-100':
selectedTemplate === template.name,
'text-gray-600': selectedTemplate != template.name,
},
]"
>
{{ template.name }}
</span>
</div>
</div>
</div>
<div class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid">
<sw-button
class="mr-3"
variant="primary-outline"
@click="closeEstimateModal"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button variant="primary" @click="chooseTemplate()">
{{ $t('general.choose') }}
</sw-button>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
data() {
return {
selectedTemplate: null,
isLoading: false,
}
},
computed: {
...mapGetters('modal', ['modalData']),
...mapGetters('estimate', ['getTemplateName']),
},
mounted() {
this.selectedTemplate = this.getTemplateName
},
methods: {
...mapActions('estimate', ['setTemplate']),
...mapActions('modal', ['closeModal', 'resetModalData']),
async chooseTemplate() {
this.isLoading = true
let resp = await this.setTemplate(this.selectedTemplate)
if (resp) {
this.isLoading = false
this.resetModalData()
this.closeModal()
}
},
closeEstimateModal() {
this.selectedTemplate = this.getTemplateName
this.closeModal()
this.resetModalData()
},
},
}
</script>

View File

@ -1,160 +0,0 @@
<template>
<div class="file-disk-modal">
<div v-if="getDiskDrivers.length">
<component
:is="selected_disk"
:loading="isLoading"
:disks="getDiskDrivers"
:is-edit="isEdit"
@on-change-disk="(disk) => (selected_disk = disk.value)"
@submit="createNewDisk"
>
<template v-slot="slotProps">
<div
class="z-0 flex justify-end p-4 border-t border-solid border-gray-light"
>
<sw-button
class="mr-3 text-sm"
variant="primary-outline"
type="button"
@click="closeDisk"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button
:loading="isRequestFire(slotProps)"
:disabled="isRequestFire(slotProps)"
variant="primary"
type="submit"
>
<save-icon v-if="!isRequestFire(slotProps)" class="mr-2" />
{{ $t('general.save') }}
</sw-button>
</div>
</template>
</component>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import Dropbox from './disks/DropboxDisk'
import Local from './disks/LocalDisk'
import S3 from './disks/S3Disk'
import DoSpaces from './disks/DoSpacesDisk'
const {
required,
minLength,
email,
numeric,
url,
maxLength,
} = require('vuelidate/lib/validators')
export default {
components: {
Dropbox,
Local,
S3,
DoSpaces,
},
data() {
return {
isLoading: false,
isEdit: false,
set_as_default: false,
name: 'local',
formData: {},
selected_disk: 'local',
diskConfigData: {},
}
},
computed: {
...mapGetters('modal', [
'modalDataID',
'modalData',
'modalActive',
'refreshData',
]),
...mapGetters('disks', ['getDiskDrivers']),
},
created() {
if (this.modalDataID) {
this.isEdit = true
}
this.loadData()
},
methods: {
...mapActions('disks', ['fetchDiskDrivers', 'createDisk', 'updateDisk']),
...mapActions('modal', ['closeModal']),
...mapActions('notification', ['showNotification']),
isRequestFire(slotProps) {
return slotProps && (slotProps.diskData.isLoading || this.isLoading)
},
async loadData() {
this.isLoading = true
let res = await this.fetchDiskDrivers()
if (this.isEdit) {
this.selected_disk = this.modalData.driver
} else {
this.selected_disk = res.data.drivers[0].value
}
this.isLoading = false
},
async createNewDisk(data) {
this.isLoading = true
let formData = {
id: this.modalDataID,
...data,
}
let response
if (this.isEdit) {
response = await this.updateDisk(formData)
} else {
response = await this.createDisk(formData)
}
if (response.data.success) {
this.refreshData()
this.closeDisk()
if (this.isEdit) {
this.showNotification({
type: 'success',
message: this.$t('settings.disk.success_update'),
})
} else {
this.showNotification({
type: 'success',
message: this.$t('settings.disk.success_create'),
})
}
} else {
this.showNotification({
type: 'error',
message: this.$t('settings.disk.invalid_disk_credentials'),
})
}
this.isLoading = false
},
closeDisk() {
this.closeModal()
},
},
}
</script>

View File

@ -1,91 +0,0 @@
<template>
<div class="template-modal">
<div class="px-8 py-8 sm:p-6">
<div class="grid grid-cols-3 gap-2 p-1 overflow-x-auto sw-scroll">
<div
v-for="(template, index) in modalData"
:key="index"
:class="{
'border border-solid border-primary-500':
selectedTemplate === template.name,
}"
class="relative flex flex-col m-2 border border-gray-200 border-solid cursor-pointer hover:border-primary-300"
>
<img
:src="template.path"
:alt="template.name"
class="w-full"
@click="selectedTemplate = template.name"
/>
<img
v-if="selectedTemplate === template.name"
:alt="template.name"
class="absolute z-10 w-5 h-5 text-primary-500"
style="top: -6px; right: -5px"
src="/assets/img/tick.png"
/>
<span
:class="[
'w-full p-1 bg-gray-200 text-sm text-center absolute bottom-0 left-0',
{
'text-primary-500 bg-primary-100':
selectedTemplate === template.id,
'text-gray-600': selectedTemplate != template.id,
},
]"
>
{{ template.name }}
</span>
</div>
</div>
</div>
<div class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid">
<sw-button
class="mr-3"
variant="primary-outline"
@click="closeInvoiceModal"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button variant="primary" @click="chooseTemplate()">
{{ $t('general.choose') }}
</sw-button>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
data() {
return {
selectedTemplate: null,
isLoading: false,
}
},
computed: {
...mapGetters('modal', ['modalData']),
...mapGetters('invoice', ['getTemplateName']),
},
mounted() {
this.selectedTemplate = this.getTemplateName
},
methods: {
...mapActions('invoice', ['setTemplate']),
...mapActions('modal', ['closeModal', 'resetModalData']),
async chooseTemplate() {
this.isLoading = true
let resp = await this.setTemplate(this.selectedTemplate)
if (resp) {
this.isLoading = false
this.resetModalData()
this.closeModal()
}
},
closeInvoiceModal() {
this.selectedTemplate = this.getTemplateName
this.closeModal()
this.resetModalData()
},
},
}
</script>

View File

@ -1,334 +0,0 @@
<template>
<div class="item-modal">
<form action="" @submit.prevent="submitItemData">
<div class="px-8 py-8 sm:p-6">
<sw-input-group
:label="$t('items.name')"
:error="nameError"
class="mb-4"
variant="horizontal"
required
>
<sw-input
ref="name"
:invalid="$v.formData.name.$error"
v-model="formData.name"
type="text"
@input="$v.formData.name.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('items.price')"
:error="priceError"
class="mb-4"
variant="horizontal"
required
>
<sw-money
v-model="price"
:currency="defaultCurrencyForInput"
:invalid="$v.formData.price.$error"
class="relative w-full focus:border focus:border-solid focus:border-primary"
@input="$v.formData.price.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('items.unit')"
class="mb-4"
variant="horizontal"
>
<sw-select
v-model="formData.unit"
:options="itemUnits"
:searchable="true"
:show-labels="false"
:max-height="200"
label="name"
>
</sw-select>
</sw-input-group>
<sw-input-group
v-if="isTexPerItem"
:label="$t('items.taxes')"
class="mb-4"
variant="horizontal"
>
<sw-select
v-model="formData.taxes"
:options="getTaxTypes"
:searchable="true"
:show-labels="false"
:allow-empty="true"
:multiple="true"
label="tax_name"
/>
</sw-input-group>
<sw-input-group
:label="$t('items.description')"
:error="descriptionError"
variant="horizontal"
>
<sw-textarea
v-model="formData.description"
rows="4"
cols="50"
@input="$v.formData.description.$touch()"
/>
</sw-input-group>
</div>
<div
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
>
<sw-button
class="mr-3"
variant="primary-outline"
type="button"
@click="closeItemModal"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button
:loading="isLoading"
:disabled="isLoading"
variant="primary"
type="submit"
>
<save-icon v-if="!isLoading" class="mr-2" />
{{ isEdit ? $t('general.update') : $t('general.save') }}
</sw-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { ShoppingCartIcon } from '@vue-hero-icons/solid'
const {
required,
minLength,
numeric,
maxLength,
minValue,
} = require('vuelidate/lib/validators')
export default {
components: {
ShoppingCartIcon,
},
data() {
return {
isEdit: false,
isLoading: false,
tempData: null,
taxes: [],
formData: {
name: null,
price: null,
description: null,
unit: null,
taxes: [],
},
}
},
validations: {
formData: {
name: {
required,
minLength: minLength(3),
},
price: {
required,
minValue: minValue(0.1),
maxLength: maxLength(20),
},
description: {
maxLength: maxLength(255),
},
},
},
computed: {
...mapGetters('company', ['defaultCurrencyForInput']),
price: {
get: function () {
return this.formData.price / 100
},
set: function (newValue) {
this.formData.price = Math.round(newValue * 100)
},
},
...mapGetters('modal', ['modalDataID', 'modalData']),
...mapGetters('item', ['getItemById', 'itemUnits']),
...mapGetters('taxType', ['taxTypes']),
isTexPerItem() {
return this.modalData.taxPerItem === 'YES'
},
getTaxTypes() {
return this.taxTypes.map((tax) => {
return { ...tax, tax_name: tax.name + ' (' + tax.percent + '%)' }
})
},
nameError() {
if (!this.$v.formData.name.$error) {
return ''
}
if (!this.$v.formData.name.required) {
return this.$tc('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.$tc('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')
}
},
},
watch: {
modalDataID() {
this.isEdit = true
this.fetchEditData()
},
},
created() {
if (this.modalDataID) {
this.isEdit = true
this.fetchEditData()
}
if (this.isEdit) {
this.loadEditData()
}
},
mounted() {
this.$v.formData.$reset()
this.$refs.name.focus = true
this.fetchItemUnits({ limit: 'all' })
},
methods: {
...mapActions('modal', ['closeModal', 'resetModalData']),
...mapActions('item', ['addItem', 'updateItem', 'fetchItemUnits']),
...mapActions('invoice', ['setItem']),
...mapActions('notification', ['showNotification']),
resetFormData() {
this.formData = {
name: null,
price: null,
description: null,
unit: null,
id: null,
}
this.$v.$reset()
},
fetchEditData() {
this.tempData = this.getItemById(this.modalDataID)
if (this.tempData) {
this.formData.name = this.tempData.name
this.formData.price = this.tempData.price
this.formData.description = this.tempData.description
this.formData.unit = this.tempData.unit
this.formData.id = this.tempData.id
}
},
async submitItemData() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
if (this.formData.unit) {
this.formData.unit_id = this.formData.unit.id
}
this.isLoading = true
let response
if (this.isEdit) {
response = await this.updateItem(this.formData)
} else {
let data = {
...this.formData,
taxes: this.formData.taxes.map((tax) => {
return {
tax_type_id: tax.id,
amount: (this.formData.price * tax.percent) / 100,
percent: tax.percent,
name: tax.name,
collective_tax: 0,
}
}),
}
response = await this.addItem(data)
}
if (response.data) {
this.showNotification({
type: 'success',
message: this.$tc('items.created_message'),
})
this.setItem(response.data.item)
window.hub.$emit('newItem', response.data.item)
this.isLoading = false
this.resetModalData()
this.resetFormData()
this.closeModal()
return true
}
this.showNotification({
type: 'error',
message: response.data.error,
})
},
closeItemModal() {
this.resetFormData()
this.closeModal()
this.resetModalData()
},
},
}
</script>

View File

@ -1,164 +0,0 @@
<template>
<form action="" @submit.prevent="submitItemUnit">
<div class="p-8 sm:p-6">
<sw-input-group
:label="$t('settings.customization.items.unit_name')"
:error="nameError"
variant="horizontal"
required
>
<sw-input
ref="name"
:invalid="$v.formData.name.$error"
v-model="formData.name"
type="text"
@input="$v.formData.name.$touch()"
/>
</sw-input-group>
</div>
<div class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid">
<sw-button
class="mr-3"
variant="primary-outline"
type="button"
@click="closeItemUnitModal"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button
:loading="isLoading"
variant="primary"
icon="save"
type="submit"
>
<save-icon v-if="!isLoading" class="mr-2" />
{{ !isEdit ? $t('general.save') : $t('general.update') }}
</sw-button>
</div>
</form>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
const { required, minLength } = require('vuelidate/lib/validators')
export default {
data() {
return {
isEdit: false,
isLoading: false,
formData: {
id: null,
name: null,
},
}
},
computed: {
...mapGetters('modal', [
'modalDataID',
'modalData',
'modalActive',
'refreshData',
]),
nameError() {
if (!this.$v.formData.name.$error) {
return ''
}
if (!this.$v.formData.name.required) {
return this.$tc('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 }
)
}
},
},
validations: {
formData: {
name: {
required,
minLength: minLength(2),
},
},
},
async mounted() {
this.$refs.name.focus = true
if (this.modalDataID) {
this.isEdit = true
this.setData()
}
},
methods: {
...mapActions('modal', ['closeModal', 'resetModalData']),
...mapActions('item', ['addItemUnit', 'updateItemUnit', 'fatchItemUnit']),
...mapActions('notification', ['showNotification']),
resetFormData() {
this.formData = {
id: null,
name: null,
}
this.$v.formData.$reset()
},
async submitItemUnit() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let response
try {
if (!this.isEdit) {
response = await this.addItemUnit(this.formData)
} else {
response = await this.updateItemUnit(this.formData)
}
if (response.data) {
this.isLoading = false
if (!this.isEdit) {
this.showNotification({
type: 'success',
message: this.$t('settings.customization.items.item_unit_added'),
})
} else {
this.showNotification({
type: 'success',
message: this.$t(
'settings.customization.items.item_unit_updated'
),
})
}
this.refreshData ? this.refreshData() : ''
this.closeItemUnitModal()
return true
}
} catch (error) {
this.isLoading = false
this.showNotification({
type: 'error',
message: response.data.error,
})
}
},
async setData() {
this.formData = {
id: this.modalData.id,
name: this.modalData.name,
}
},
closeItemUnitModal() {
this.resetModalData()
this.resetFormData()
this.closeModal()
},
},
}
</script>

View File

@ -1,201 +0,0 @@
<template>
<div class="mail-config-modal">
<form action="" @submit.prevent="onTestMailSend">
<div class="p-4 md:p-8">
<sw-input-group
:label="$t('general.to')"
:error="emailError"
class="mt-3"
variant="horizontal"
required
>
<sw-input
ref="to"
:invalid="$v.formData.to.$error"
v-model="formData.to"
type="text"
@input="$v.formData.to.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('general.subject')"
:error="subjectError"
class="mt-3"
variant="horizontal"
required
>
<sw-input
:invalid="$v.formData.subject.$error"
v-model="formData.subject"
type="text"
@input="$v.formData.subject.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('general.message')"
:error="messageError"
class="mt-3"
variant="horizontal"
required
>
<sw-textarea
v-model="formData.message"
:invalid="$v.formData.message.$error"
rows="4"
cols="50"
@input="$v.formData.message.$touch()"
/>
</sw-input-group>
</div>
<div
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
>
<sw-button
variant="primary-outline"
class="mr-3"
@click="closeTaxModal"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button :loading="isLoading" variant="primary" type="submit">
<paper-airplane-icon v-if="!isLoading" class="mr-2" />
{{ !isEdit ? $t('general.send') : $t('general.update') }}
</sw-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { PaperAirplaneIcon } from '@vue-hero-icons/outline'
const {
required,
minLength,
email,
maxLength,
} = require('vuelidate/lib/validators')
export default {
components: {
PaperAirplaneIcon,
},
data() {
return {
isEdit: false,
isLoading: false,
formData: {
to: null,
subject: null,
message: null,
},
}
},
computed: {
...mapGetters('modal', ['modalDataID', 'modalData', 'modalActive']),
emailError() {
if (!this.$v.formData.to.$error) {
return ''
}
if (!this.$v.formData.to.required) {
return this.$tc('validation.required')
}
if (!this.$v.formData.to.email) {
return this.$tc('validation.email_incorrect')
}
},
subjectError() {
if (!this.$v.formData.subject.$error) {
return ''
}
if (!this.$v.formData.subject.required) {
return this.$tc('validation.required')
}
if (!this.$v.formData.subject.maxLength) {
return this.$tc('validation.subject_maxlength')
}
},
messageError() {
if (!this.$v.formData.message.$error) {
return ''
}
if (!this.$v.formData.message.required) {
return this.$tc('validation.required')
}
if (!this.$v.formData.message.maxLength) {
return this.$tc('validation.message_maxlength')
}
},
},
validations: {
formData: {
to: {
required,
email,
},
subject: {
required,
maxLength: maxLength(100),
},
message: {
required,
maxLength: maxLength(255),
},
},
},
async mounted() {
this.$refs.to.focus = true
},
methods: {
...mapActions('modal', ['closeModal', 'resetModalData']),
...mapActions('company', ['sendTestMail']),
...mapActions('notification', ['showNotification']),
resetFormData() {
this.formData = {
to: null,
subject: null,
message: null,
}
this.$v.formData.$reset()
},
async onTestMailSend() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let response = await this.sendTestMail(this.formData)
if (response.data) {
if (response.data.success) {
this.showNotification({
type: 'success',
message: this.$tc('general.send_mail_successfully'),
})
this.closeTaxModal()
this.isLoading = false
return true
}
this.showNotification({
type: 'error',
message: this.$tc('validation.something_went_wrong'),
})
this.closeTaxModal()
this.isLoading = false
return true
}
this.showNotification({
type: 'error',
message: response.data.error,
})
},
closeTaxModal() {
this.resetModalData()
this.resetFormData()
this.closeModal()
},
},
}
</script>

View File

@ -1,316 +0,0 @@
<template>
<div class="note-modal">
<form action="" @submit.prevent="submitNote">
<div class="px-8 py-8 sm:p-6">
<sw-input-group
:label="$t('settings.customization.notes.name')"
:error="nameError"
class="mb-4"
variant="vertical"
required
>
<sw-input
ref="name"
:invalid="$v.formData.name.$error"
v-model="formData.name"
type="text"
@input="$v.formData.name.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.customization.notes.type')"
:error="typeError"
class="mb-4"
variant="vertical"
required
>
<sw-select
v-model="noteType"
:options="types"
:allow-empty="false"
:show-labels="false"
class="mt-2"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.customization.notes.notes')"
:error="noteError"
variant="vertical"
required
>
<base-custom-input
v-model="formData.notes"
:fields="fields"
class="mt-2"
/>
</sw-input-group>
</div>
<div
class="z-0 flex justify-end px-4 py-4 border-t border-solid border-gray-light"
>
<sw-button
class="mr-2"
variant="primary-outline"
type="button"
@click="closeNoteModal"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button
:loading="isLoading"
variant="primary"
icon="save"
type="submit"
>
<save-icon v-if="!isLoading" class="mr-2" />
{{ !isEdit ? $t('general.save') : $t('general.update') }}
</sw-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
const { required, minLength } = require('vuelidate/lib/validators')
export default {
data() {
return {
isEdit: false,
isLoading: false,
types: ['Invoice', 'Estimate', 'Payment'],
selectType: null,
formData: {
type: '',
name: '',
notes: '',
},
noteType: null,
fields: [],
}
},
computed: {
...mapGetters('modal', [
'modalDataID',
'modalData',
'modalActive',
'refreshData',
]),
nameError() {
if (!this.$v.formData.name.$error) {
return ''
}
if (!this.$v.formData.name.required) {
return this.$tc('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 }
)
}
},
noteError() {
if (!this.$v.formData.notes.$error) {
return ''
}
if (!this.$v.formData.notes.required) {
return this.$tc('validation.required')
}
},
typeError() {
if (!this.$v.noteType.$error) {
return ''
}
if (!this.$v.noteType.required) {
return this.$tc('validation.required')
}
},
},
validations: {
formData: {
name: {
required,
minLength: minLength(2),
},
notes: {
required,
},
},
noteType: {
required,
},
},
watch: {
noteType() {
this.setFields()
},
},
async mounted() {
this.setFields()
if (this.modalDataID) {
this.isEdit = true
this.setData()
} else {
this.modalData
? (this.noteType = this.modalData)
: (this.noteType = 'Invoice')
}
},
methods: {
...mapActions('modal', ['closeModal', 'resetModalData']),
...mapActions('notes', ['addNote', 'updateNote']),
...mapActions('notification', ['showNotification']),
...mapActions('invoice', {
setInvoiceNote: 'selectNote',
}),
...mapActions('estimate', {
setEstimateNote: 'selectNote',
}),
...mapActions('payment', {
setPaymentNote: 'selectNote',
}),
setFields() {
this.fields = ['customer', 'customerCustom']
if (this.noteType === 'Invoice') {
this.fields.push('invoice', 'invoiceCustom')
}
if (this.noteType === 'Estimate') {
this.fields.push('estimate', 'estimateCustom')
}
if (this.noteType === 'Payment') {
this.fields.push('payment', 'paymentCustom')
}
return true
},
resetFormData() {
this.formData = {
name: null,
notes: null,
}
this.notetype = null
this.$v.formData.$reset()
},
async submitNote() {
this.$v.formData.$touch()
this.$v.noteType.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
if (this.isEdit) {
let data = {
id: this.modalDataID,
type: this.noteType,
name: this.formData.name,
notes: this.formData.notes,
}
let res = await this.updateNote(data)
if (res.data) {
this.showNotification({
type: 'success',
message: this.$t('settings.customization.notes.note_updated'),
})
this.refreshData ? this.refreshData() : ''
this.closeNoteModal()
return true
}
this.showNotification({
type: 'error',
message: res.data.error,
})
} else {
try {
let data = {
type: this.noteType,
name: this.formData.name,
notes: this.formData.notes,
}
let response = await this.addNote(data)
if (response.data && response.data.note) {
this.isLoading = false
this.showNotification({
type: 'success',
message: this.$t('settings.customization.notes.note_added'),
})
if (
(this.$route.name === 'invoices.create' &&
response.data.note.type === 'Invoice') ||
(this.$route.name === 'invoices.edit' &&
response.data.note.type === 'Invoice')
) {
this.setInvoiceNote(response.data.note)
}
if (
(this.$route.name === 'estimates.create' &&
response.data.note.type === 'Estimate') ||
(this.$route.name === 'estimates.edit' &&
response.data.note.type === 'Estimate')
) {
this.setEstimateNote(response.data.note)
}
if (
(this.$route.name === 'payments.create' &&
response.data.note.type === 'Payment') ||
(this.$route.name === 'payments.edit' &&
response.data.note.type === 'Payment')
) {
this.setPaymentNote(response.data.note)
}
this.refreshData ? this.refreshData() : ''
this.closeNoteModal()
return true
}
this.showNotification({
type: 'error',
message: response.data.error,
})
} catch (err) {
if (err.response.data.errors.name) {
this.isLoading = true
}
}
}
},
async setData() {
this.noteType = this.modalData.type
this.formData.name = this.modalData.name
this.formData.notes = this.modalData.notes
},
closeNoteModal() {
this.closeModal()
this.resetFormData()
},
},
}
</script>
<style lang="scss">
.note-modal {
.header-editior .editor-menu-bar {
margin-left: 0.5px;
margin-right: 0px;
}
}
</style>

View File

@ -1,163 +0,0 @@
<template>
<form action="" @submit.prevent="submitPaymentMode">
<div class="p-8 sm:p-6">
<sw-input-group
:label="$t('settings.customization.payments.mode_name')"
:error="nameError"
variant="horizontal"
required
>
<sw-input
ref="name"
:invalid="$v.formData.name.$error"
v-model="formData.name"
type="text"
@input="$v.formData.name.$touch()"
/>
</sw-input-group>
</div>
<div class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid">
<sw-button
class="mr-3"
variant="primary-outline"
type="button"
@click="closePaymentModeModal"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button :loading="isLoading" variant="primary" type="submit">
<save-icon v-if="!isLoading" class="mr-2" />
{{ !isEdit ? $t('general.save') : $t('general.update') }}
</sw-button>
</div>
</form>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
const { required, minLength } = require('vuelidate/lib/validators')
export default {
data() {
return {
isEdit: false,
isLoading: false,
formData: {
id: null,
name: null,
},
}
},
computed: {
...mapGetters('modal', [
'modalDataID',
'modalData',
'modalActive',
'refreshData',
]),
nameError() {
if (!this.$v.formData.name.$error) {
return ''
}
if (!this.$v.formData.name.required) {
return this.$tc('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 }
)
}
},
},
validations: {
formData: {
name: {
required,
minLength: minLength(2),
},
},
},
async mounted() {
this.$refs.name.focus = true
if (this.modalDataID) {
this.isEdit = true
this.setData()
}
},
methods: {
...mapActions('modal', ['closeModal', 'resetModalData']),
...mapActions('payment', ['addPaymentMode', 'updatePaymentMode']),
...mapActions('notification', ['showNotification']),
resetFormData() {
this.formData = {
id: null,
name: null,
}
this.$v.formData.$reset()
},
async submitPaymentMode() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let response
if (this.isEdit) {
response = await this.updatePaymentMode(this.formData)
if (response.data) {
this.showNotification({
type: 'success',
message: this.$t(
'settings.customization.payments.payment_mode_updated'
),
})
this.refreshData ? this.refreshData() : ''
this.closePaymentModeModal()
return true
}
this.showNotification({
type: 'error',
message: response.data.error,
})
} else {
try {
response = await this.addPaymentMode(this.formData)
if (response.data) {
this.isLoading = false
this.showNotification({
type: 'success',
message: this.$t(
'settings.customization.payments.payment_mode_added'
),
})
this.refreshData ? this.refreshData() : ''
this.closePaymentModeModal()
return true
}
this.showNotification({
type: 'error',
message: response.data.error,
})
} catch (err) {
this.isLoading = false
}
}
},
async setData() {
this.formData = {
id: this.modalData.id,
name: this.modalData.name,
}
},
closePaymentModeModal() {
this.resetModalData()
this.resetFormData()
this.closeModal()
},
},
}
</script>

View File

@ -1,293 +0,0 @@
<template>
<div>
<form action="" @submit.prevent="sendEstimateData">
<div class="px-8 py-8 sm:p-6">
<sw-input-group
:label="$t('general.from')"
:error="fromError"
class="mb-4"
variant="vertical"
required
>
<sw-input
v-model="formData.from"
:invalid="$v.formData.from.$error"
type="text"
@input="$v.formData.from.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('general.to')"
:error="toError"
class="mb-4"
variant="vertical"
required
>
<sw-input
v-model="formData.to"
:invalid="$v.formData.to.$error"
type="text"
@input="$v.formData.to.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('general.subject')"
:error="subjectError"
class="mb-4"
variant="vertical"
required
>
<sw-input
v-model="formData.subject"
:invalid="$v.formData.subject.$error"
type="text"
@input="$v.formData.subject.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('general.body')"
:error="bodyError"
class="mb-4"
variant="vertical"
required
>
<!-- <sw-editor
v-model="formData.body"
:set-editor="formData.body"
:invalid="$v.formData.body.$error"
@input="$v.formData.body.$touch()"
/> -->
<base-custom-input
v-model="formData.body"
:fields="estimateMailFields"
:invalid="$v.formData.body.$error"
class="mt-2"
@input="$v.formData.body.$touch()"
/>
</sw-input-group>
</div>
<div
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
>
<sw-button
class="mr-3"
variant="primary-outline"
type="button"
@click="closeSendEstimateModal"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button
:loading="isLoading"
:disabled="isLoading"
variant="primary"
type="submit"
>
<paper-airplane-icon v-if="!isLoading" class="h-5 mr-2" />
{{ $t('general.send') }}
</sw-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { PaperAirplaneIcon } from '@vue-hero-icons/solid'
const { required, email } = require('vuelidate/lib/validators')
const _ = require('lodash')
export default {
components: {
PaperAirplaneIcon,
},
data() {
return {
isLoading: false,
estimateMailFields: [
'customer',
'customerCustom',
'estimate',
'estimateCustom',
'company',
],
formData: {
from: null,
to: null,
subject: 'New Estimate',
body: null,
},
}
},
validations: {
formData: {
from: {
required,
email,
},
to: {
required,
email,
},
subject: {
required,
},
body: {
required,
},
},
},
computed: {
...mapGetters('modal', ['modalDataID', 'modalData', 'modalActive']),
...mapGetters('user', ['currentUser']),
...mapActions('notification', ['showNotification']),
getEmailUrl() {
return this.url
},
fromError() {
if (!this.$v.formData.from.$error) {
return ''
}
if (!this.$v.formData.from.required) {
return this.$tc('validation.required')
}
if (!this.$v.formData.from.email) {
return this.$tc('validation.email_incorrect')
}
},
toError() {
if (!this.$v.formData.to.$error) {
return ''
}
if (!this.$v.formData.to.required) {
return this.$tc('validation.required')
}
if (!this.$v.formData.to.email) {
return this.$tc('validation.email_incorrect')
}
},
subjectError() {
if (!this.$v.formData.subject.$error) {
return ''
}
if (!this.$v.formData.subject.required) {
return this.$tc('validation.required')
}
},
bodyError() {
if (!this.$v.formData.body.$error) {
return ''
}
if (!this.$v.formData.body.required) {
return this.$tc('validation.required')
}
},
},
mounted() {
this.setInitialData()
},
methods: {
...mapActions('modal', ['closeModal']),
...mapActions('estimate', ['sendEmail']),
...mapActions('company', ['fetchCompanySettings', 'fetchMailConfig']),
async setInitialData() {
let admin = await this.fetchMailConfig()
if (this.modalData) {
this.formData.from = admin.data.from_mail
this.formData.to = this.modalData.user.email
}
let res = await this.fetchCompanySettings(['estimate_mail_body'])
this.formData.body = res.data.estimate_mail_body
},
resetFormData() {
this.formData = {
from: null,
to: null,
subject: null,
body: null,
}
},
async sendEstimateData() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.$swal({
title: this.$t('general.are_you_sure'),
text: this.$t('estimates.confirm_send_estimate'),
icon: 'question',
iconHtml: `<svg
aria-hidden="true"
class="w-6 h-6"
focusable="false"
data-prefix="fas"
data-icon="check-circle"
class="svg-inline--fa fa-check-circle fa-w-16"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
fill="#55547A"
d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"
></path>
</svg>`,
showCancelButton: true,
showConfirmButton: true,
}).then(async (result) => {
try {
if (result.value) {
let data = {
...this.formData,
id: this.modalDataID,
status: 'SENT',
}
this.isLoading = true
let res = await this.sendEmail(data)
this.closeModal()
if (res.data.success) {
this.isLoading = false
this.showNotification({
type: 'success',
message: this.$tc('estimates.send_estimate_successfully'),
})
return true
}
if (res.data.error === 'estimates.user_email_does_not_exist') {
this.showNotification({
type: 'error',
message: this.$tc('estimates.user_email_does_not_exist'),
})
return false
}
}
} catch (error) {
this.isLoading = false
this.showNotification({
type: 'error',
message: this.$tc('estimates.something_went_wrong'),
})
}
})
},
closeSendEstimateModal() {
this.resetFormData()
this.closeModal()
},
},
}
</script>

View File

@ -1,289 +0,0 @@
<template>
<div>
<form action="" @submit.prevent="sendInvoiceData">
<div class="gap-4 px-8 py-8 sm:p-6">
<sw-input-group
:label="$t('general.from')"
:error="fromError"
class="mb-4"
variant="vertical"
required
>
<sw-input
v-model="formData.from"
:invalid="$v.formData.from.$error"
type="text"
@input="$v.formData.from.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('general.to')"
:error="toError"
class="mb-4"
variant="vertical"
required
>
<sw-input
v-model="formData.to"
:invalid="$v.formData.to.$error"
type="text"
@input="$v.formData.to.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('general.subject')"
:error="subjectError"
class="mb-4"
variant="vertical"
required
>
<sw-input
v-model="formData.subject"
:invalid="$v.formData.subject.$error"
type="text"
@input="$v.formData.subject.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('general.body')"
:error="bodyError"
class="mb-4"
variant="vertical"
required
>
<base-custom-input
v-model="formData.body"
:fields="InvoiceMailFields"
:invalid="$v.formData.body.$error"
class="mt-2"
@input="$v.formData.body.$touch()"
/>
</sw-input-group>
</div>
<div
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
>
<sw-button
class="mr-3"
variant="primary-outline"
type="button"
@click="closeSendInvoiceModal"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button
:loading="isLoading"
:disabled="isLoading"
variant="primary"
type="submit"
>
<paper-airplane-icon v-if="!isLoading" class="h-5 mr-2" />
{{ $t('general.send') }}
</sw-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { PaperAirplaneIcon } from '@vue-hero-icons/solid'
const { required, email } = require('vuelidate/lib/validators')
const _ = require('lodash')
export default {
components: {
PaperAirplaneIcon,
},
data() {
return {
isLoading: false,
InvoiceMailFields: [
'customer',
'customerCustom',
'invoice',
'invoiceCustom',
'company',
],
formData: {
from: null,
to: null,
subject: 'New Invoice',
body: null,
},
}
},
validations: {
formData: {
from: {
required,
email,
},
to: {
required,
email,
},
subject: {
required,
},
body: {
required,
},
},
},
computed: {
...mapGetters('modal', ['modalDataID', 'modalData', 'modalActive']),
...mapGetters('user', ['currentUser']),
fromError() {
if (!this.$v.formData.from.$error) {
return ''
}
if (!this.$v.formData.from.required) {
return this.$tc('validation.required')
}
if (!this.$v.formData.from.email) {
return this.$tc('validation.email_incorrect')
}
},
toError() {
if (!this.$v.formData.to.$error) {
return ''
}
if (!this.$v.formData.to.required) {
return this.$tc('validation.required')
}
if (!this.$v.formData.from.email) {
return this.$tc('validation.email_incorrect')
}
},
subjectError() {
if (!this.$v.formData.subject.$error) {
return ''
}
if (!this.$v.formData.subject.required) {
return this.$tc('validation.required')
}
},
bodyError() {
if (!this.$v.formData.body.$error) {
return ''
}
if (!this.$v.formData.body.required) {
return this.$tc('validation.required')
}
},
},
mounted() {
this.setInitialData()
},
methods: {
...mapActions('modal', ['closeModal']),
...mapActions('invoice', ['sendEmail']),
...mapActions('company', ['fetchCompanySettings', 'fetchMailConfig']),
...mapActions('notification', ['showNotification']),
async setInitialData() {
let admin = await this.fetchMailConfig()
if (this.modalData) {
this.formData.from = admin.data.from_mail
this.formData.to = this.modalData.user.email
}
let res = await this.fetchCompanySettings(['invoice_mail_body'])
this.formData.body = res.data.invoice_mail_body
},
resetFormData() {
this.formData = {
from: null,
to: null,
subject: null,
body: null,
}
},
async sendInvoiceData() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.$swal({
title: this.$t('general.are_you_sure'),
text: this.$t('invoices.confirm_send_invoice'),
icon: 'question',
iconHtml: `<svg
aria-hidden="true"
class="w-6 h-6"
focusable="false"
data-prefix="fas"
data-icon="check-circle"
class="svg-inline--fa fa-check-circle fa-w-16"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
fill="#55547A"
d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"
></path>
</svg>`,
showCancelButton: true,
showConfirmButton: true,
}).then(async (result) => {
try {
if (result.value) {
let data = {
...this.formData,
id: this.modalDataID,
status: 'SENT',
}
this.isLoading = true
let res = await this.sendEmail(data)
this.closeModal()
if (res.data.success) {
this.isLoading = false
this.showNotification({
type: 'success',
message: this.$tc('invoices.send_invoice_successfully'),
})
return true
}
if (res.data.error === 'invoices.user_email_does_not_exist') {
this.showNotification({
type: 'error',
message: this.$tc('invoices.user_email_does_not_exist'),
})
return false
}
}
} catch (error) {
this.isLoading = false
this.showNotification({
type: 'error',
message: this.$tc('invoices.something_went_wrong'),
})
}
})
},
closeSendInvoiceModal() {
this.resetFormData()
this.closeModal()
},
},
}
</script>

View File

@ -1,286 +0,0 @@
<template>
<div>
<form action="" @submit.prevent="sendPaymentData">
<div class="px-8 py-8 sm:p-6">
<sw-input-group
:label="$t('general.from')"
:error="fromError"
class="mb-4"
variant="vertical"
required
>
<sw-input
v-model="formData.from"
:invalid="$v.formData.from.$error"
type="text"
@input="$v.formData.from.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('general.to')"
:error="toError"
class="mb-4"
variant="vertical"
required
>
<sw-input
v-model="formData.to"
:invalid="$v.formData.to.$error"
type="text"
@input="$v.formData.to.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('general.subject')"
:error="subjectError"
class="mb-4"
variant="vertical"
required
>
<sw-input
v-model="formData.subject"
:invalid="$v.formData.subject.$error"
type="text"
@input="$v.formData.subject.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('general.body')"
:error="bodyError"
class="mb-4"
variant="vertical"
required
>
<sw-editor
v-model="formData.body"
:set-editor="formData.body"
:invalid="$v.formData.body.$error"
@input="$v.formData.body.$touch()"
/>
</sw-input-group>
</div>
<div
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
>
<sw-button
class="mr-3"
variant="primary-outline"
type="button"
@click="closeSendPaymentModal"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button
:loading="isLoading"
:disabled="isLoading"
variant="primary"
type="submit"
>
<paper-airplane-icon v-if="!isLoading" class="h-5 mr-2" />
{{ $t('general.send') }}
</sw-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { PaperAirplaneIcon } from '@vue-hero-icons/solid'
const { required, email } = require('vuelidate/lib/validators')
const _ = require('lodash')
export default {
components: {
PaperAirplaneIcon,
},
data() {
return {
isLoading: false,
formData: {
from: null,
to: null,
subject: null,
body: null,
},
}
},
validations: {
formData: {
from: {
required,
email,
},
to: {
required,
email,
},
subject: {
required,
},
body: {
required,
},
},
},
computed: {
...mapGetters('modal', ['modalDataID', 'modalData', 'modalActive']),
fromError() {
if (!this.$v.formData.from.$error) {
return ''
}
if (!this.$v.formData.from.required) {
return this.$tc('validation.required')
}
if (!this.$v.formData.from.email) {
return this.$tc('validation.email_incorrect')
}
},
toError() {
if (!this.$v.formData.to.$error) {
return ''
}
if (!this.$v.formData.to.required) {
return this.$tc('validation.required')
}
if (!this.$v.formData.to.email) {
return this.$tc('validation.email_incorrect')
}
},
subjectError() {
if (!this.$v.formData.subject.$error) {
return ''
}
if (!this.$v.formData.subject.required) {
return this.$tc('validation.required')
}
},
bodyError() {
if (!this.$v.formData.body.$error) {
return ''
}
if (!this.$v.formData.body.required) {
return this.$tc('validation.required')
}
},
},
mounted() {
this.setInitialData()
},
methods: {
...mapActions('modal', ['closeModal']),
...mapActions('payment', ['sendEmail']),
...mapActions('company', ['fetchCompanySettings', 'fetchMailConfig']),
...mapActions('notification', ['showNotification']),
async setInitialData() {
let admin = await this.fetchMailConfig()
if (this.modalData) {
this.formData.from = admin.data.from_mail
this.formData.to = this.modalData.user.email
}
let res = await this.fetchCompanySettings(['payment_mail_body'])
this.formData.body = res.data.payment_mail_body
},
resetFormData() {
this.formData = {
from: null,
to: null,
subject: null,
body: null,
}
},
async sendPaymentData() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.$swal({
title: this.$t('general.are_you_sure'),
text: this.$t('payments.confirm_send_payment'),
icon: 'question',
iconHtml: `<svg
aria-hidden="true"
class="w-6 h-6"
focusable="false"
data-prefix="fas"
data-icon="check-circle"
class="svg-inline--fa fa-check-circle fa-w-16"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
fill="#55547A"
d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"
></path>
</svg>`,
showCancelButton: true,
showConfirmButton: true,
}).then(async (result) => {
try {
if (result.value) {
let data = {
...this.formData,
id: this.modalDataID,
status: 'SENT',
}
this.isLoading = true
let res = await this.sendEmail(data)
this.closeModal()
if (res.data.success) {
this.isLoading = false
this.showNotification({
type: 'success',
message: this.$tc('payments.send_payment_successfully'),
})
return true
}
if (res.data.error === 'payments.user_email_does_not_exist') {
this.showNotification({
type: 'error',
message: this.$tc('payments.user_email_does_not_exist'),
})
return false
}
}
} catch (error) {
this.isLoading = false
this.showNotification({
type: 'error',
message: this.$tc('payments.something_went_wrong'),
})
}
})
},
closeSendPaymentModal() {
this.resetFormData()
this.closeModal()
},
},
}
</script>

View File

@ -1,129 +0,0 @@
<template>
<div class="file-disk-modal">
<form @submit.prevent="submitData">
<div class="px-8 py-6">
<sw-input-group :label="$t('settings.disk.driver')" required>
<sw-select
v-model="selected_disk"
:options="getDisks"
:searchable="true"
:allow-empty="false"
:show-labels="false"
:custom-label="getCustomLabel"
class="mt-2"
track-by="id"
/>
</sw-input-group>
</div>
<div
class="z-0 flex justify-end p-4 border-t border-solid border-gray-light"
>
<sw-button
class="mr-3 text-sm"
type="button"
variant="primary-outline"
@click="closeDisk"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button
:loading="isLoading"
icon="save"
type="submit"
variant="primary"
class="text-sm"
>
{{ $t('general.save') }}
</sw-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import Dropbox from './disks/DropboxDisk'
import Local from './disks/LocalDisk'
import S3 from './disks/S3Disk'
import DoSpaces from './disks/DoSpacesDisk'
const {
required,
minLength,
email,
numeric,
url,
maxLength,
} = require('vuelidate/lib/validators')
export default {
components: {
Dropbox,
Local,
S3,
DoSpaces,
},
data() {
return {
isLoading: false,
set_as_default: false,
name: 'local',
formData: null,
selected_disk: null,
diskConfigData: {},
}
},
computed: {
...mapGetters('modal', ['modalData', 'refreshData']),
...mapGetters('disks', ['getDisks']),
},
created() {
this.loadData()
},
methods: {
...mapActions('disks', ['fetchDisks', 'setDefaultDisk']),
...mapActions('modal', ['closeModal']),
...mapActions('notification', ['showNotification']),
async loadData() {
this.loading = true
let res = await this.fetchDisks()
this.selected_disk = res.data.disks.find((v) => v.set_as_default == true)
this.loading = false
},
async submitData() {
this.isLoading = true
let response = await this.setDefaultDisk(this.selected_disk)
if (response.data.success) {
this.refreshData()
this.closeDisk()
this.showNotification({
type: 'success',
message: this.$t('settings.disk.success'),
})
}
this.isLoading = true
},
closeDisk() {
this.closeModal()
},
getCustomLabel({ driver, name }) {
return `${name} — [${driver}]`
},
},
}
</script>

View File

@ -1,239 +0,0 @@
<template>
<div class="tax-type-modal">
<form action="" @submit.prevent="submitTaxTypeData">
<div class="p-8 sm:p-6">
<sw-input-group
:label="$t('tax_types.name')"
:error="nameError"
class="mt-3"
variant="horizontal"
required
>
<sw-input
ref="name"
:invalid="$v.formData.name.$error"
v-model="formData.name"
type="text"
@input="$v.formData.name.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('tax_types.percent')"
:error="percentError"
class="mt-3"
variant="horizontal"
required
>
<sw-money
v-model="formData.percent"
:currency="defaultInput"
:invalid="$v.formData.percent.$error"
class="relative w-full focus:border focus:border-solid focus:border-primary"
@input="$v.formData.percent.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('tax_types.description')"
:error="descriptionError"
class="mt-3"
variant="horizontal"
>
<sw-textarea
v-model="formData.description"
rows="4"
cols="50"
@input="$v.formData.description.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('tax_types.compound_tax')"
class="mt-3"
variant="horizontal"
>
<sw-switch
v-model="formData.compound_tax"
class="flex items-center mt-1"
/>
</sw-input-group>
</div>
<div
class="z-0 flex justify-end p-4 border-t border-solid border--200 border-modal-bg"
>
<sw-button
class="mr-3 text-sm"
variant="primary-outline"
type="button"
@click="closeTaxModal"
>
{{ $t('general.cancel') }}
</sw-button>
<sw-button :loading="isLoading" variant="primary" type="submit">
<save-icon v-if="!isLoading" class="mr-2" />
{{ !isEdit ? $t('general.save') : $t('general.update') }}
</sw-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
const {
required,
minLength,
between,
maxLength,
} = require('vuelidate/lib/validators')
export default {
data() {
return {
isEdit: false,
isLoading: false,
formData: {
id: null,
name: null,
percent: 0,
description: null,
compound_tax: false,
collective_tax: 0,
},
defaultInput: {
decimal: '.',
thousands: ',',
prefix: '% ',
precision: 2,
masked: false,
},
}
},
computed: {
...mapGetters('modal', [
'modalDataID',
'modalData',
'modalActive',
'refreshData',
]),
descriptionError() {
if (!this.$v.formData.description.$error) {
return ''
}
if (!this.$v.formData.description.maxLength) {
return this.$t('validation.description_maxlength')
}
},
nameError() {
if (!this.$v.formData.name.$error) {
return ''
}
if (!this.$v.formData.name.required) {
return this.$tc('validation.required')
} else {
return this.$tc(
'validation.name_min_length',
this.$v.formData.name.$params.minLength.min,
{ count: this.$v.formData.name.$params.minLength.min }
)
}
},
percentError() {
if (!this.$v.formData.percent.$error) {
return ''
}
if (!this.$v.formData.percent.required) {
return this.$t('validation.required')
} else {
return this.$t('validation.enter_valid_tax_rate')
}
},
},
validations: {
formData: {
name: {
required,
minLength: minLength(3),
},
percent: {
required,
between: between(-100, 100),
},
description: {
maxLength: maxLength(255),
},
},
},
async mounted() {
this.$refs.name.focus = true
if (this.modalDataID) {
this.isEdit = true
this.setData()
}
},
methods: {
...mapActions('modal', ['closeModal', 'resetModalData']),
...mapActions('taxType', ['addTaxType', 'updateTaxType', 'fetchTaxType']),
...mapActions('notification', ['showNotification']),
resetFormData() {
this.formData = {
id: null,
name: null,
percent: 0,
description: null,
collective_tax: 0,
}
this.$v.formData.$reset()
},
async submitTaxTypeData() {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let response
if (!this.isEdit) {
response = await this.addTaxType(this.formData)
} else {
response = await this.updateTaxType(this.formData)
}
if (response.data) {
if (!this.isEdit) {
this.showNotification({
type: 'success',
message: this.$t('settings.tax_types.created_message'),
})
} else {
this.showNotification({
type: 'success',
message: this.$t('settings.tax_types.updated_message'),
})
}
window.hub.$emit('newTax', response.data.taxType)
this.refreshData ? this.refreshData() : ''
this.closeTaxModal()
this.isLoading = false
return true
}
this.showNotification({
type: 'error',
message: response.data.error,
})
},
async setData() {
this.formData = {
id: this.modalData.id,
name: this.modalData.name,
percent: this.modalData.percent,
description: this.modalData.description,
compound_tax: this.modalData.compound_tax ? true : false,
}
},
closeTaxModal() {
this.resetModalData()
this.resetFormData()
this.closeModal()
},
},
}
</script>

View File

@ -1,345 +0,0 @@
<template>
<form @submit.prevent="submitData">
<div class="px-8 py-6">
<div class="grid gap-6 grid-col-1 md:grid-cols-2">
<sw-input-group
:label="$t('settings.disk.name')"
:error="nameError"
required
>
<sw-input
v-model="name"
type="text"
name="name"
class="mt-2"
:invalid="$v.name.$error"
@input="$v.name.$touch()"
/>
</sw-input-group>
<sw-input-group :label="$t('settings.disk.driver')" required>
<sw-select
v-model="selected_disk"
:invalid="$v.selected_disk.$error"
:options="disks"
:searchable="true"
:allow-empty="false"
:show-labels="false"
class="mt-2"
track-by="value"
label="name"
@input="onChangeDriver"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.do_spaces_root')"
:error="rootError"
required
>
<sw-input
v-model.trim="diskConfigData.root"
:invalid="$v.diskConfigData.root.$error"
type="text"
name="name"
class="mt-2"
placeholder="Ex. /user/root/"
@input="$v.diskConfigData.root.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.do_spaces_key')"
:error="keyError"
required
>
<sw-input
v-model.trim="diskConfigData.key"
:invalid="$v.diskConfigData.key.$error"
type="text"
name="name"
placeholder="Ex. KEIS4S39SERSDS"
class="mt-2"
@input="$v.diskConfigData.key.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.do_spaces_secret')"
:error="secretError"
required
>
<sw-input
v-model.trim="diskConfigData.secret"
:invalid="$v.diskConfigData.secret.$error"
type="text"
name="name"
placeholder="Ex. ********"
class="mt-2"
@input="$v.diskConfigData.secret.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.do_spaces_region')"
:error="regionError"
required
>
<sw-input
v-model.trim="diskConfigData.region"
:invalid="$v.diskConfigData.region.$error"
type="text"
name="name"
class="mt-2"
placeholder="Ex. nyc3"
@input="$v.diskConfigData.region.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.do_spaces_endpoint')"
:error="endpointError"
required
>
<sw-input
v-model.trim="diskConfigData.endpoint"
:invalid="$v.diskConfigData.endpoint.$error"
type="text"
name="name"
class="mt-2"
placeholder="Ex. https://nyc3.digitaloceanspaces.com"
@input="$v.diskConfigData.endpoint.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.do_spaces_bucket')"
:error="bucketError"
required
>
<sw-input
v-model.trim="diskConfigData.bucket"
:invalid="$v.diskConfigData.bucket.$error"
type="text"
name="name"
class="mt-2"
placeholder="Ex. my-new-space"
@input="$v.diskConfigData.bucket.$touch()"
/>
</sw-input-group>
</div>
<div class="flex items-center mt-6" v-if="!isDisabled">
<div class="relative flex items-center w-12">
<sw-switch class="flex" v-model="set_as_default"/>
</div>
<div class="ml-4 right">
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
{{ $t('settings.disk.is_default') }}
</p>
</div>
</div>
</div>
<slot :disk-data="{ isLoading, submitData }" />
</form>
</template>
<script>
const { required, url } = require('vuelidate/lib/validators')
import { mapActions, mapGetters } from 'vuex'
export default {
props: {
isEdit: {
type: Boolean,
require: true,
default: false,
},
loading: {
type: Boolean,
require: true,
default: false,
},
disks: {
type: Array,
require: true,
default: Array,
},
},
data() {
return {
diskConfigData: {
selected_driver: 'doSpaces',
key: '',
secret: '',
region: '',
bucket: '',
endpoint: '',
root: '',
},
name: '',
isLoading: false,
set_as_default: false,
selected_disk: null,
is_current_disk: null,
}
},
validations: {
diskConfigData: {
key: {
required,
},
secret: {
required,
},
region: {
required,
},
bucket: {
required,
},
root: {
required,
},
endpoint: {
required,
url
},
},
name: {
required,
},
selected_disk: {
required,
},
},
computed: {
...mapGetters('modal', ['modalData']),
nameError() {
if (!this.$v.name.$error) {
return ''
}
if (!this.$v.name.required) {
return this.$tc('validation.required')
}
},
typeError() {
if (!this.$v.diskConfigData.type.$error) {
return ''
}
if (!this.$v.diskConfigData.type.required) {
return this.$tc('validation.required')
}
},
keyError() {
if (!this.$v.diskConfigData.key.$error) {
return ''
}
if (!this.$v.diskConfigData.key.required) {
return this.$tc('validation.required')
}
},
secretError() {
if (!this.$v.diskConfigData.secret.$error) {
return ''
}
if (!this.$v.diskConfigData.secret.required) {
return this.$tc('validation.required')
}
},
regionError() {
if (!this.$v.diskConfigData.region.$error) {
return ''
}
if (!this.$v.diskConfigData.region.required) {
return this.$tc('validation.required')
}
},
bucketError() {
if (!this.$v.diskConfigData.bucket.$error) {
return ''
}
if (!this.$v.diskConfigData.bucket.required) {
return this.$tc('validation.required')
}
},
rootError() {
if (!this.$v.diskConfigData.root.$error) {
return ''
}
if (!this.$v.diskConfigData.root.required) {
return this.$tc('validation.required')
}
},
endpointError() {
if (!this.$v.diskConfigData.endpoint.$error) {
return ''
}
if (!this.$v.diskConfigData.endpoint.required) {
return this.$tc('validation.required')
}
if (!this.$v.diskConfigData.endpoint.url) {
return this.$tc('validation.invalid_url')
}
},
isDisabled() {
return (this.isEdit && this.set_as_default && this.is_current_disk) ? true : false
}
},
created() {
this.loadData()
},
methods: {
...mapActions('disks', ['fetchDiskEnv', 'updateDisk']),
async loadData() {
this.isLoading = true
let data = {
disk: 'doSpaces',
}
if(this.isEdit) {
this.diskConfigData = JSON.parse(this.modalData.credentials)
this.set_as_default = this.modalData.set_as_default
if(this.set_as_default) {
this.is_current_disk = true
}
this.name = this.modalData.name
} else {
let diskData = await this.fetchDiskEnv(data)
this.diskConfigData = diskData.data
}
this.selected_disk = this.disks.find((v) => v.value == 'doSpaces')
this.isLoading = false
},
async submitData() {
this.$v.$touch()
if (this.$v.$invalid) {
return
}
let data = {
credentials: this.diskConfigData,
name: this.name,
driver: this.selected_disk.value,
set_as_default: this.set_as_default,
}
this.$emit('submit', data)
return false
},
onChangeDriver() {
this.$emit('on-change-disk', this.selected_disk)
},
},
}
</script>

View File

@ -1,311 +0,0 @@
<template>
<form @submit.prevent="submitData">
<div class="px-8 py-6">
<div class="grid gap-6 grid-col-1 md:grid-cols-2">
<sw-input-group
:label="$t('settings.disk.name')"
:error="nameError"
required
>
<sw-input
v-model="name"
type="text"
name="name"
class="mt-2"
:invalid="$v.name.$error"
@input="$v.name.$touch()"
/>
</sw-input-group>
<sw-input-group :label="$t('settings.disk.driver')" required>
<sw-select
v-model="selected_disk"
:invalid="$v.selected_disk.$error"
:options="disks"
:searchable="true"
:allow-empty="false"
:show-labels="false"
track-by="value"
label="name"
class="mt-2"
@input="onChangeDriver"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.dropbox_root')"
:error="rootError"
required
>
<sw-input
v-model.trim="diskConfigData.root"
:invalid="$v.diskConfigData.root.$error"
type="text"
name="name"
class="mt-2"
placeholder="Ex. /user/root/"
@input="$v.diskConfigData.root.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.dropbox_token')"
:error="tokenError"
required
>
<sw-input
v-model.trim="diskConfigData.token"
:invalid="$v.diskConfigData.token.$error"
type="text"
name="name"
class="mt-2"
@input="$v.diskConfigData.token.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.dropbox_key')"
:error="keyError"
required
>
<sw-input
v-model.trim="diskConfigData.key"
:invalid="$v.diskConfigData.key.$error"
type="text"
name="name"
placeholder="Ex. KEIS4S39SERSDS"
class="mt-2"
@input="$v.diskConfigData.key.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.dropbox_secret')"
:error="secretError"
required
>
<sw-input
v-model.trim="diskConfigData.secret"
:invalid="$v.diskConfigData.secret.$error"
type="text"
name="name"
placeholder="Ex. ********"
class="mt-2"
@input="$v.diskConfigData.secret.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.dropbox_app')"
:error="appError"
required
>
<sw-input
v-model.trim="diskConfigData.app"
:invalid="$v.diskConfigData.app.$error"
type="text"
name="name"
class="mt-2"
@input="$v.diskConfigData.app.$touch()"
/>
</sw-input-group>
</div>
<div class="flex items-center mt-6" v-if="!isDisabled">
<div class="relative flex items-center w-12">
<sw-switch class="flex" v-model="set_as_default"/>
</div>
<div class="ml-4 right">
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
{{ $t('settings.disk.is_default') }}
</p>
</div>
</div>
</div>
<slot :disk-data="{ isLoading, submitData }" />
</form>
</template>
<script>
const { required, edisk } = require('vuelidate/lib/validators')
import { mapActions, mapGetters } from 'vuex'
export default {
props: {
isEdit: {
type: Boolean,
require: true,
default: false,
},
loading: {
type: Boolean,
require: true,
default: false,
},
disks: {
type: Array,
require: true,
default: Array,
},
},
data() {
return {
diskConfigData: {
selected_driver: 'dropbox',
token: '',
key: '',
secret: '',
app: '',
},
name: '',
set_as_default: false,
isLoading: false,
is_current_disk: null,
selected_disk: 'dropbox',
}
},
validations: {
diskConfigData: {
token: {
required,
},
key: {
required,
},
secret: {
required,
},
app: {
required,
},
root: {
required,
},
},
name: {
required,
},
selected_disk: {
required,
},
},
computed: {
...mapGetters('modal', ['modalData']),
nameError() {
if (!this.$v.name.$error) {
return ''
}
if (!this.$v.name.required) {
return this.$tc('validation.required')
}
},
typeError() {
if (!this.$v.diskConfigData.type.$error) {
return ''
}
if (!this.$v.diskConfigData.type.required) {
return this.$tc('validation.required')
}
},
tokenError() {
if (!this.$v.diskConfigData.token.$error) {
return ''
}
if (!this.$v.diskConfigData.token.required) {
return this.$tc('validation.required')
}
},
keyError() {
if (!this.$v.diskConfigData.key.$error) {
return ''
}
if (!this.$v.diskConfigData.key.required) {
return this.$tc('validation.required')
}
},
secretError() {
if (!this.$v.diskConfigData.secret.$error) {
return ''
}
if (!this.$v.diskConfigData.secret.required) {
return this.$tc('validation.required')
}
},
appError() {
if (!this.$v.diskConfigData.app.$error) {
return ''
}
if (!this.$v.diskConfigData.app.required) {
return this.$tc('validation.required')
}
},
rootError() {
if (!this.$v.diskConfigData.root.$error) {
return ''
}
if (!this.$v.diskConfigData.root.required) {
return this.$tc('validation.required')
}
},
isDisabled() {
return (this.isEdit && this.set_as_default && this.is_current_disk) ? true : false
}
},
created() {
this.loadData()
},
methods: {
...mapActions('disks', ['fetchDiskEnv', 'updateDisk']),
async loadData() {
this.isLoading = true
let data = {
disk: this.selected_disk,
}
if(this.isEdit) {
this.diskConfigData = JSON.parse(this.modalData.credentials)
this.set_as_default = this.modalData.set_as_default
if(this.set_as_default) {
this.is_current_disk = true
}
this.name = this.modalData.name
} else {
let diskData = await this.fetchDiskEnv(data)
this.diskConfigData = diskData.data
}
this.selected_disk = this.disks.find((v) => v.value == 'dropbox')
this.isLoading = false
},
async submitData() {
this.$v.$touch()
if (this.$v.$invalid) {
return
}
let data = {
credentials: this.diskConfigData,
name: this.name,
driver: this.selected_disk.value,
set_as_default: this.set_as_default,
}
this.$emit('submit', data)
return false
},
onChangeDriver() {
this.$emit('on-change-disk', this.selected_disk)
},
},
}
</script>

View File

@ -1,177 +0,0 @@
<template>
<form @submit.prevent="submitData">
<div class="px-8 py-6">
<div class="grid gap-6 grid-col-1 md:grid-cols-2">
<sw-input-group
:label="$t('settings.disk.name')"
:error="nameError"
required
>
<sw-input
v-model="name"
type="text"
name="name"
class="mt-2"
:invalid="$v.name.$error"
@input="$v.name.$touch()"
/>
</sw-input-group>
<sw-input-group :label="$tc('settings.disk.driver')" required>
<sw-select
v-model="selected_disk"
:invalid="$v.selected_disk.$error"
:options="disks"
:searchable="true"
:allow-empty="false"
:show-labels="false"
track-by="value"
label="name"
class="mt-2"
@input="onChangeDriver"
/>
</sw-input-group>
<sw-input-group :label="$t('settings.disk.local_root')" required>
<sw-input
v-model.trim="diskConfigData.root"
type="text"
name="name"
placeholder="Ex. /user/root/"
class="mt-2"
/>
</sw-input-group>
</div>
<div class="flex items-center mt-6" v-if="!isDisabled">
<div class="relative flex items-center w-12">
<sw-switch class="flex" v-model="set_as_default"/>
</div>
<div class="ml-4 right">
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
{{ $t('settings.disk.is_default') }}
</p>
</div>
</div>
</div>
<slot :disk-data="{ isLoading, submitData }" />
</form>
</template>
<script>
const { required, edisk } = require('vuelidate/lib/validators')
import { mapActions, mapGetters } from 'vuex'
export default {
props: {
isEdit: {
type: Boolean,
require: true,
default: false,
},
loading: {
type: Boolean,
require: true,
default: false,
},
disks: {
type: Array,
require: true,
default: Array,
},
},
data() {
return {
diskConfigData: {
selected_driver: 'local',
root: '',
},
name: '',
isLoading: false,
set_as_default: false,
selected_disk: null,
is_current_disk: null,
is_current_disk: null,
}
},
validations: {
diskConfigData: {
root: {
required,
},
},
name: {
required,
},
selected_disk: {
required,
},
},
computed: {
...mapGetters('modal', ['modalData']),
nameError() {
if (!this.$v.name.$error) {
return ''
}
if (!this.$v.name.required) {
return this.$tc('validation.required')
}
},
isDisabled() {
return (this.isEdit && this.set_as_default && this.is_current_disk) ? true : false
}
},
created() {
this.loadData()
},
methods: {
...mapActions('disks', ['fetchDiskEnv', 'updateDisk']),
async loadData() {
this.isLoading = true
let data = {
disk: 'local',
}
if(this.isEdit) {
this.diskConfigData = JSON.parse(this.modalData.credentials)
this.set_as_default = this.modalData.set_as_default
if(this.set_as_default) {
this.is_current_disk = true
}
this.name = this.modalData.name
} else {
let diskData = await this.fetchDiskEnv(data)
this.diskConfigData = diskData.data
}
this.selected_disk = this.disks.find((v) => v.value == 'local')
this.isLoading = false
},
async submitData() {
this.$v.$touch()
if (this.$v.$invalid) {
return
}
let data = {
credentials: this.diskConfigData,
name: this.name,
driver: this.selected_disk.value,
set_as_default: this.set_as_default,
}
this.$emit('submit', data)
return false
},
onChangeDriver() {
this.$emit('on-change-disk', this.selected_disk)
},
},
}
</script>

View File

@ -1,313 +0,0 @@
<template>
<form @submit.prevent="submitData">
<div class="px-8 py-6">
<div class="grid gap-6 grid-col-1 md:grid-cols-2">
<sw-input-group
:label="$t('settings.disk.name')"
:error="nameError"
required
>
<sw-input
v-model="name"
type="text"
name="name"
class="mt-2"
:invalid="$v.name.$error"
@input="$v.name.$touch()"
/>
</sw-input-group>
<sw-input-group :label="$tc('settings.disk.driver')" required>
<sw-select
v-model="selected_disk"
:invalid="$v.selected_disk.$error"
:options="disks"
:searchable="true"
:allow-empty="false"
:show-labels="false"
track-by="value"
label="name"
class="mt-2"
@input="onChangeDriver"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.aws_root')"
:error="rootError"
required
>
<sw-input
v-model.trim="diskConfigData.root"
:invalid="$v.diskConfigData.root.$error"
type="text"
name="name"
placeholder="Ex. /user/root/"
class="mt-2"
@input="$v.diskConfigData.root.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.aws_key')"
:error="keyError"
required
>
<sw-input
v-model.trim="diskConfigData.key"
:invalid="$v.diskConfigData.key.$error"
type="text"
name="name"
placeholder="Ex. KEIS4S39SERSDS"
class="mt-2"
@input="$v.diskConfigData.key.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.aws_secret')"
:error="secretError"
required
>
<sw-input
v-model.trim="diskConfigData.secret"
:invalid="$v.diskConfigData.secret.$error"
type="text"
name="name"
placeholder="Ex. ********"
class="mt-2"
@input="$v.diskConfigData.secret.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.aws_region')"
:error="regionError"
required
>
<sw-input
v-model.trim="diskConfigData.region"
:invalid="$v.diskConfigData.region.$error"
type="text"
name="name"
class="mt-2"
placeholder="Ex. us-west"
@input="$v.diskConfigData.region.$touch()"
/>
</sw-input-group>
<sw-input-group
:label="$t('settings.disk.aws_bucket')"
:error="bucketError"
required
>
<sw-input
v-model.trim="diskConfigData.bucket"
:invalid="$v.diskConfigData.bucket.$error"
type="text"
name="name"
class="mt-2"
placeholder="Ex. AppName"
@input="$v.diskConfigData.bucket.$touch()"
/>
</sw-input-group>
</div>
<div class="flex items-center mt-6" v-if="!isDisabled">
<div class="relative flex items-center w-12">
<sw-switch class="flex" v-model="set_as_default"/>
</div>
<div class="ml-4 right">
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
{{ $t('settings.disk.is_default') }}
</p>
</div>
</div>
</div>
<slot :disk-data="{ isLoading, submitData }" />
</form>
</template>
<script>
const { required, edisk } = require('vuelidate/lib/validators')
import { mapActions, mapGetters } from 'vuex'
export default {
props: {
isEdit: {
type: Boolean,
require: true,
default: false,
},
loading: {
type: Boolean,
require: true,
default: false,
},
disks: {
type: Array,
require: true,
default: Array,
},
},
data() {
return {
diskConfigData: {
selected_driver: 's3',
key: '',
secret: '',
region: '',
bucket: '',
root: '',
},
name: '',
set_as_default: false,
isLoading: false,
selected_disk: null,
is_current_disk: null,
}
},
validations: {
diskConfigData: {
key: {
required,
},
secret: {
required,
},
region: {
required,
},
bucket: {
required,
},
root: {
required,
},
},
name: {
required,
},
selected_disk: {
required,
},
},
computed: {
...mapGetters('modal', ['modalData']),
nameError() {
if (!this.$v.name.$error) {
return ''
}
if (!this.$v.name.required) {
return this.$tc('validation.required')
}
},
driverError() {
if (!this.$v.diskConfigData.driver.$error) {
return ''
}
if (!this.$v.diskConfigData.driver.required) {
return this.$tc('validation.required')
}
},
keyError() {
if (!this.$v.diskConfigData.key.$error) {
return ''
}
if (!this.$v.diskConfigData.key.required) {
return this.$tc('validation.required')
}
},
secretError() {
if (!this.$v.diskConfigData.secret.$error) {
return ''
}
if (!this.$v.diskConfigData.secret.required) {
return this.$tc('validation.required')
}
},
regionError() {
if (!this.$v.diskConfigData.region.$error) {
return ''
}
if (!this.$v.diskConfigData.region.required) {
return this.$tc('validation.required')
}
},
bucketError() {
if (!this.$v.diskConfigData.bucket.$error) {
return ''
}
if (!this.$v.diskConfigData.bucket.required) {
return this.$tc('validation.required')
}
},
rootError() {
if (!this.$v.diskConfigData.root.$error) {
return ''
}
if (!this.$v.diskConfigData.root.required) {
return this.$tc('validation.required')
}
},
isDisabled() {
return (this.isEdit && this.set_as_default && this.is_current_disk) ? true : false
}
},
created() {
this.loadData()
},
methods: {
...mapActions('disks', ['fetchDiskEnv', 'updateDisk']),
async loadData() {
this.isLoading = true
let data = {
disk: 's3',
}
if(this.isEdit) {
this.diskConfigData = JSON.parse(this.modalData.credentials)
this.set_as_default = this.modalData.set_as_default
if(this.set_as_default) {
this.is_current_disk = true
}
this.name = this.modalData.name
} else {
let diskData = await this.fetchDiskEnv(data)
this.diskConfigData = diskData.data
}
this.selected_disk = this.disks.find((v) => v.value == 's3')
this.isLoading = false
},
async submitData() {
this.$v.$touch()
if (this.$v.$invalid) {
return
}
let data = {
credentials: this.diskConfigData,
name: this.name,
driver: this.selected_disk.value,
set_as_default: this.set_as_default,
}
this.$emit('submit', data)
return false
},
onChangeDriver() {
this.$emit('on-change-disk', this.selected_disk)
},
},
}
</script>

View File

@ -1,176 +0,0 @@
<template>
<div class="customer-select">
<div class="flex flex-col w-full pb-4">
<div class="flex px-4 pt-4 pb-2">
<sw-input
v-model="search"
:placeholder="$t('general.search')"
focus
type="text"
icon="search"
@input="searchCustomer"
>
<template v-slot:leftIcon>
<search-icon class="h-5 m-2 text-gray-500" />
</template>
</sw-input>
</div>
<div
v-if="customers.length > 0 && !loading"
class="relative flex flex-col overflow-auto sw-scroll list"
>
<div
v-for="(customer, index) in customers"
:key="index"
class="flex px-6 py-2 border-b border-gray-200 border-solid cursor-pointer hover:cursor-pointer hover:bg-gray-100 last:border-b-0"
@click="selectNewCustomer(customer.id)"
>
<span
class="flex items-center content-center justify-center w-10 h-10 mr-4 text-xl font-semibold leading-9 text-white bg-gray-400 rounded-full avatar"
>{{ initGenerator(customer.name) }}</span
>
<div class="flex flex-col justify-center">
<label class="m-0 leading-tight cursor-pointer font-base">{{
customer.name
}}</label>
<label
class="m-0 text-sm font-medium text-gray-500 cursor-pointer font-base"
>{{ customer.contact_name }}</label
>
</div>
</div>
</div>
<div v-if="loading" class="flex items-center justify-center list">
<refresh-icon class="animate-spin" />
</div>
<div
v-if="customers.length === 0"
class="flex justify-center p-5 text-gray-400"
>
<label class="cursor-pointer">
{{ $t('customers.no_customers_found') }}
</label>
</div>
</div>
<button
type="button"
class="flex items-center justify-center w-full px-2 py-3 bg-gray-200 border-none outline-none"
@click="openCustomerModal"
>
<user-add-icon class="text-primary-400" />
<label
class="m-0 ml-3 text-sm leading-none cursor-pointer font-base text-primary-400"
>{{ $t('customers.add_new_customer') }}</label
>
</button>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { UserAddIcon, SearchIcon, RefreshIcon } from '@vue-hero-icons/solid'
export default {
components: { UserAddIcon, SearchIcon, RefreshIcon },
props: {
type: {
type: String,
required: true,
},
userId: {
type: Number,
required: false,
},
},
data() {
return {
search: null,
loading: false,
}
},
computed: {
...mapGetters('customer', ['customers']),
},
created() {
this.fetchInitialCustomers()
},
methods: {
...mapActions('modal', ['openModal']),
...mapActions('customer', ['fetchCustomers']),
...mapActions('invoice', {
setInvoiceCustomer: 'selectCustomer',
}),
...mapActions('estimate', {
setEstimateCustomer: 'selectCustomer',
}),
async fetchInitialCustomers() {
await this.fetchCustomers({
filter: {},
orderByField: '',
orderBy: '',
customer_id: this.userId,
})
},
async searchCustomer() {
let data = {
display_name: this.search,
email: '',
phone: '',
orderByField: '',
orderBy: '',
page: 1,
}
this.loading = true
await this.fetchCustomers(data)
this.loading = false
},
openCustomerModal() {
this.openModal({
title: this.$t('customers.add_customer'),
componentName: 'CustomerModal',
variant: 'lg',
})
},
initGenerator(name) {
if (name) {
let nameSplit = name.split(' ')
let initials = nameSplit[0].charAt(0).toUpperCase()
return initials
}
},
selectNewCustomer(id) {
if (this.type === 'estimate') {
this.setEstimateCustomer(id)
} else {
this.setInvoiceCustomer(id)
}
},
},
}
</script>
<style lang="scss">
.customer-select {
.list {
max-height: 173px;
min-height: 173px;
}
}
</style>

View File

@ -1,114 +0,0 @@
<template>
<div class="tax-select">
<div class="flex flex-col w-full px-4 py-4">
<div class="relative flex w-full mb-2">
<sw-input
v-model="textSearch"
:placeholder="$t('general.search')"
focus
class="text-black"
icon="search"
type="text"
/>
</div>
<div
v-if="filteredNotes.length > 0"
class="relative flex flex-col overflow-auto sw-scroll list"
style="max-height: 112px"
>
<div
v-for="(note, index) in filteredNotes"
:key="index"
class="flex justify-between p-4 border-b border-gray-200 border-solid cursor-pointer list-item last:border-b-0 hover:bg-gray-100"
@click="selectNote(index)"
>
<label
class="inline-block m-0 text-base font-normal leading-tight text-black font-base"
>
{{ note.name }}
</label>
</div>
</div>
<div v-else class="flex justify-center p-5 text-gray-400">
<label class="m-0">{{ $t('general.no_note_found') }}</label>
</div>
</div>
<button
type="button"
class="flex items-center justify-center w-full px-2 py-3 bg-gray-200 border-none outline-none hover:bg-gray-300"
@click="openNoteModal"
>
<check-circle-icon class="h-5" />
<label
class="m-0 ml-1 text-sm leading-none cursor-pointer font-base text-primary-400"
>
{{ $t('settings.customization.notes.add_new_note') }}
</label>
</button>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { CheckCircleIcon } from '@vue-hero-icons/solid'
export default {
components: {
CheckCircleIcon,
},
props: {
type: {
type: String,
default: null,
},
},
data() {
return {
textSearch: null,
}
},
computed: {
...mapGetters('notes', ['notes']),
filteredNotes() {
if (this.textSearch) {
var textSearch = this.textSearch
return this.notes.filter(function (el) {
return el.name.toLowerCase().indexOf(textSearch.toLowerCase()) !== -1
})
} else {
return this.notes
}
},
},
created() {
this.fetchInitialData()
},
methods: {
...mapActions('modal', ['openModal']),
...mapActions('notes', ['fetchNotes']),
selectNote(index) {
this.$emit('select', { ...this.notes[index] })
},
async fetchInitialData() {
await this.fetchNotes({
filter: {},
orderByField: '',
orderBy: '',
type: this.type ? this.type : '',
})
},
openNoteModal() {
this.openModal({
title: this.$t('settings.customization.notes.add_note'),
componentName: 'NoteSelectModal',
variant: 'lg',
data: this.type,
})
},
},
}
</script>

View File

@ -1,110 +0,0 @@
<template>
<div class="tax-select">
<div class="flex flex-col w-full p-4">
<div class="relative flex w-full mb-2">
<sw-input
v-model="textSearch"
:placeholder="$t('general.search')"
focus
class="text-black"
icon="search"
type="text"
/>
</div>
<div
v-if="filteredTaxType.length > 0"
class="relative flex flex-col overflow-auto sw-scroll list"
style="max-height: 112px"
>
<div
v-for="(taxType, index) in filteredTaxType"
:key="index"
:class="{
'bg-gray-100 cursor-not-allowed opacity-50 pointer-events-none': taxes.find(
(val) => {
return val.tax_type_id === taxType.id
}
),
}"
class="flex justify-between p-4 border-b border-gray-200 border-solid cursor-pointer list-item last:border-b-0 hover:bg-gray-100"
@click="selectTaxType(index)"
>
<label
class="inline-block m-0 text-base font-normal leading-tight text-black font-base"
>
{{ taxType.name }}
</label>
<label
class="inline-block m-0 text-base font-normal leading-tight text-black font-base"
>
{{ taxType.percent }} %
</label>
</div>
</div>
<div v-else class="flex justify-center p-5 text-gray-400">
<label class="m-0">{{ $t('general.no_tax_found') }}</label>
</div>
</div>
<button
type="button"
class="flex items-center justify-center w-full px-2 py-3 bg-gray-200 border-none outline-none"
@click="openTaxModal"
>
<check-circle-icon class="h-5" />
<label
class="m-0 ml-3 text-sm leading-none cursor-pointer font-base text-primary-400"
>
{{ $t('invoices.add_new_tax') }}
</label>
</button>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { CheckCircleIcon } from '@vue-hero-icons/solid'
export default {
components: {
CheckCircleIcon,
},
props: {
taxes: {
type: Array,
required: false,
default: null,
},
},
data() {
return {
textSearch: null,
}
},
computed: {
...mapGetters('taxType', ['taxTypes']),
filteredTaxType() {
if (this.textSearch) {
var textSearch = this.textSearch
return this.taxTypes.filter(function (el) {
return el.name.toLowerCase().indexOf(textSearch.toLowerCase()) !== -1
})
} else {
return this.taxTypes
}
},
},
methods: {
...mapActions('modal', ['openModal']),
selectTaxType(index) {
this.$emit('select', { ...this.taxTypes[index] })
},
openTaxModal() {
this.openModal({
title: this.$t('settings.tax_types.add_tax'),
componentName: 'TaxTypeModal',
})
},
},
}
</script>

View File

@ -1,31 +0,0 @@
export default {
activeBaseSelectContainer: 'z-50',
disabledBaseSelectContainer: 'pointer-events-none opacity-50',
baseSelectContainer: 'base-select multiselect min-h-10 z-40',
multiSelect:
'multiselect__select h-10 m-0 py-1 px-2 no-underline text-center cursor-pointer leading-5 block absolute box-border w-8',
disabledMultiSelect: 'bg-gray-200 text-gray-400',
multiSelectTags:
'multiselect__tags min-h-10 block pt-2 pr-10 pb-0 pl-2 rounded border border-solid text-sm',
multiSelectTagsDefaultColor: 'border-gray-200 bg-white',
multiSelectTagsInvalid: 'border-danger bg-white',
disabledMultiSelectTags: 'bg-gray-200 text-gray-400',
multiselectTagsWrap: 'multiselect__tags-wrap inline',
multiselectTag:
'multiselect__tag relative inline-block pt-1 pr-6 pb-1 pl-2 rounded mr-2 text-white leading-none mb-1 whitespace-nowrap overflow-hidden max-w-full',
multiselectTagIcon:
'multiselect__tag-icon cursor-pointer ml-2 absolute right-0 top-0 bottom-0 font-bold w-5 text-center leading-5 delay-200 transition-all ease-linear rounded',
multiselectStrong: 'mb-2 leading-5 inline-block align-top',
multiselectSpinner: 'multiselect__spinner absolute w-12 h-8 bg-white block',
multiselectInput:
'multiselect__input relative inline-block border-none leading-5 rounded pl-1 w-full box-border align-top text-sm',
multiselectSingle:
'multiselect__single relative inline-block border-none leading-5 rounded bg-white pl-1 w-full box-border align-top pl-1 mb-2 text-sm',
multiselectContentWrapper:
'multiselect__content-wrapper absolute block bg-white w-full overflow-auto border border-solid border-gray-200 border-t-0 rounded-bl rounded-br z-50',
multiselectContent:
'multiselect__content list-none inline-block p-0 m-0 min-w-full align-top',
multiselectOption:
'multiselect__option block p-3 no-underline leading-4 normal-case align-middle relative cursor-pointer whitespace-nowrap text-sm',
multiselectElement: 'multiselect__element block',
}

View File

@ -1,11 +0,0 @@
export default {
baseClass:
'not-italic font-normal leading-tight text-left border border-solid rounded-md input-field box-border-2 base-date-picker-input',
baseSize: 'w-full h-10 px-3 py-3 text-sm',
baseColorClass:
'placeholder-gray-400 border-gray-200 focus:border-primary-500',
invalidClass: 'border-red',
disabledClass: 'bg-gray-200 text-gray-500',
calendarIconStyle:
'absolute w-4 h-5 text-sm not-italic font-black cursor-pointer text-gray-400',
}

View File

@ -1,15 +0,0 @@
/*
|--------------------------------------------------------------------------
| Crater Base Components Theme
|--------------------------------------------------------------------------|
*/
import BaseSelect from './BaseSelect'
import DatePicker from './DatePicker'
const CraterTheme = {
BaseSelect,
DatePicker,
}
export default CraterTheme

View File

@ -1,212 +0,0 @@
<template>
<div class="graph-container">
<canvas id="graph" ref="graph" />
</div>
</template>
<script>
import Chart from 'chart.js'
import { mapGetters } from 'vuex'
export default {
props: {
labels: {
type: Array,
require: true,
default: Array,
},
values: {
type: Array,
require: true,
default: Array,
},
invoices: {
type: Array,
require: true,
default: Array,
},
expenses: {
type: Array,
require: true,
default: Array,
},
receipts: {
type: Array,
require: true,
default: Array,
},
income: {
type: Array,
require: true,
default: Array,
},
formatMoney: {
type: Function,
require: false,
default: Function,
},
FormatGraphMoney: {
type: Function,
require: false,
default: Function,
},
},
computed: {
...mapGetters('company', ['defaultCurrency']),
},
watch: {
labels(val) {
this.update()
},
},
mounted() {
let self = this
let context = this.$refs.graph.getContext('2d')
let options = {
responsive: true,
maintainAspectRatio: false,
tooltips: {
enabled: true,
callbacks: {
label: function (tooltipItem, data) {
return self.FormatGraphMoney(
Math.round(tooltipItem.value * 100),
self.defaultCurrency
)
},
},
},
legend: {
display: false,
},
}
let data = {
labels: this.labels,
datasets: [
{
label: 'Sales',
fill: false,
lineTension: 0.3,
backgroundColor: 'rgba(230, 254, 249)',
borderColor: '#040405',
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: '#040405',
pointBackgroundColor: '#fff',
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: '#040405',
pointHoverBorderColor: 'rgba(220,220,220,1)',
pointHoverBorderWidth: 2,
pointRadius: 4,
pointHitRadius: 10,
data: this.invoices.map((invoice) => invoice / 100),
},
{
label: 'Receipts',
fill: false,
lineTension: 0.3,
backgroundColor: 'rgba(230, 254, 249)',
borderColor: 'rgb(2, 201, 156)',
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: 'rgb(2, 201, 156)',
pointBackgroundColor: '#fff',
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: 'rgb(2, 201, 156)',
pointHoverBorderColor: 'rgba(220,220,220,1)',
pointHoverBorderWidth: 2,
pointRadius: 4,
pointHitRadius: 10,
data: this.receipts.map((receipt) => receipt / 100),
},
{
label: 'Expenses',
fill: false,
lineTension: 0.3,
backgroundColor: 'rgba(245, 235, 242)',
borderColor: 'rgb(255,0,0)',
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: 'rgb(255,0,0)',
pointBackgroundColor: '#fff',
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: 'rgb(255,0,0)',
pointHoverBorderColor: 'rgba(220,220,220,1)',
pointHoverBorderWidth: 2,
pointRadius: 4,
pointHitRadius: 10,
data: this.expenses.map((expense) => expense / 100),
},
{
label: 'Net Income',
fill: false,
lineTension: 0.3,
backgroundColor: 'rgba(236, 235, 249)',
borderColor: 'rgba(88, 81, 216, 1)',
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: 'rgba(88, 81, 216, 1)',
pointBackgroundColor: '#fff',
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: 'rgba(88, 81, 216, 1)',
pointHoverBorderColor: 'rgba(220,220,220,1)',
pointHoverBorderWidth: 2,
pointRadius: 4,
pointHitRadius: 10,
data: this.income.map((_i) => _i / 100),
},
],
}
this.myLineChart = new Chart(context, {
type: 'line',
data: data,
options: options,
})
},
methods: {
update() {
this.myLineChart.data.labels = this.labels
this.myLineChart.data.datasets[0].data = this.invoices.map(
(invoice) => invoice / 100
)
this.myLineChart.data.datasets[1].data = this.receipts.map(
(receipt) => receipt / 100
)
this.myLineChart.data.datasets[2].data = this.expenses.map(
(expense) => expense / 100
)
this.myLineChart.data.datasets[3].data = this.income.map((_i) => _i / 100)
this.myLineChart.update({
lazy: true,
})
},
beforeDestroy() {
this.myLineChart.destroy()
},
},
}
</script>
<style scoped>
.graph-container {
height: 300px;
}
</style>

View File

@ -1,102 +0,0 @@
<template>
<div>
<base-date-picker
v-model="date"
:calendar-button="true"
:invalid="isInvalid"
:placeholder="placeholder"
calendar-button-icon="calendar"
@input="onDateChanged"
/>
<span v-if="isInvalid" class="text-sm text-danger">
{{ $t('validation.required') }}
</span>
</div>
</template>
<script>
import moment from 'moment'
const { required, requiredIf } = require('vuelidate/lib/validators')
export default {
props: {
field: {
type: Object,
default: null,
required: true,
},
isEdit: {
type: Boolean,
default: false,
},
invalidFields: {
type: Array,
default: () => [],
},
},
data() {
return {
date: null,
placeholder: '',
invalidFieldIds: [],
}
},
validations: {
date: {
required: requiredIf('isRequired'),
},
},
computed: {
isRequired() {
if (this.field && this.field.is_required) {
return true
}
return false
},
isInvalid() {
if (
this.invalidFieldIds.indexOf(this.field.cfid) >= 0 ||
(this.$v.date.$error && !this.$v.date.required)
) {
return true
}
return false
},
},
watch: {
invalidFields: {
handler: 'setInvalidFieldIds',
deep: true,
},
field: {
handler: 'handleData',
deep: true,
},
},
created() {
this.date =
this.field && this.field.defaultAnswer && this.field.defaultAnswer
this.placeholder =
this.field && this.field.placeholder ? this.field.placeholder : ''
},
methods: {
handleData() {
this.date =
this.field && this.field.defaultAnswer
? this.field.defaultAnswer
: new Date()
this.placeholder =
this.field && this.field.placeholder ? this.field.placeholder : ''
},
onDateChanged(date) {
this.$v.date.$touch()
this.$emit('update', {
field: this.field,
value: moment(date).format('YYYY-MM-DD'),
})
},
setInvalidFieldIds() {
this.invalidFieldIds = this.invalidFields.map((field) => field.id)
},
},
}
</script>

View File

@ -1,85 +0,0 @@
<template>
<div>
<base-date-picker
v-model="dateTime"
:invalid="isInvalid"
:enable-time="true"
:placeholder="placeholder"
@input="onChanged"
/>
<span v-if="isInvalid" class="text-sm text-danger">
{{ $t('validation.required') }}
</span>
</div>
</template>
<script>
const { required, requiredIf } = require('vuelidate/lib/validators')
import moment from 'moment'
export default {
props: {
field: {
type: Object,
default: null,
required: true,
},
invalidFields: {
type: Array,
default: () => [],
},
},
data() {
return {
dateTime: null,
placeholder: '',
defaultValue: null,
invalidFieldIds: [],
}
},
validations: {
dateTime: {
required: requiredIf('isRequired'),
},
},
computed: {
isRequired() {
if (this.field && this.field.is_required) {
return true
}
return false
},
isInvalid() {
if (
this.invalidFieldIds.indexOf(this.field.cfid) >= 0 ||
(this.$v.dateTime.$error && !this.$v.dateTime.required)
) {
return true
}
return false
},
},
watch: {
invalidFields: {
handler: 'setInvalidFieldIds',
deep: true,
},
},
mounted() {
this.dateTime =
this.field && this.field.defaultAnswer
? moment(this.field.defaultAnswer).format('YYYY-MM-DD H:m')
: moment().format('YYYY-MM-DD H:m')
this.placeholder =
this.field && this.field.placeholder ? this.field.placeholder : ''
},
methods: {
setInvalidFieldIds() {
this.invalidFieldIds = this.invalidFields.map((field) => field.id)
},
onChanged() {
this.$v.dateTime.$touch()
this.$emit('update', { field: this.field, value: this.dateTime })
},
},
}
</script>

View File

@ -1,89 +0,0 @@
<template>
<div>
<sw-select
v-model="selectedValue"
:options="options"
:searchable="true"
:show-labels="false"
:allow-empty="true"
:invalid="isInvalid"
:placeholder="placeholder"
:tabindex="tabindex"
@select="onSelectedValueChanged"
/>
<span v-if="isInvalid" class="text-sm text-danger">
{{ $t('validation.required') }}
</span>
</div>
</template>
<script>
const { required, requiredIf } = require('vuelidate/lib/validators')
export default {
props: {
field: {
type: Object,
default: null,
require: true,
},
invalidFields: {
type: Array,
default: () => [],
},
tabindex: {
type: Number,
default: null,
},
},
data() {
return {
selectedValue: null,
options: [],
placeholder: '',
invalidFieldIds: [],
}
},
validations: {
selectedValue: {
required: requiredIf('isRequired'),
},
},
computed: {
isRequired() {
if (this.field && this.field.is_required) {
return true
}
return false
},
isInvalid() {
if (
this.invalidFieldIds.indexOf(this.field.cfid) >= 0 ||
(this.$v.selectedValue.$error && !this.$v.selectedValue.required)
) {
return true
}
return false
},
},
watch: {
invalidFields: {
handler: 'setInvalidFieldIds',
deep: true,
},
},
mounted() {
this.options = this.field && this.field.options ? this.field.options : []
this.selectedValue = this.field && this.field.defaultAnswer
this.placeholder = this.field && this.field.placeholder
},
methods: {
setInvalidFieldIds() {
this.invalidFieldIds = this.invalidFields.map((field) => field.id)
},
onSelectedValueChanged(data) {
this.$v.selectedValue.$touch()
this.$emit('update', { field: this.field, value: data })
},
},
}
</script>

View File

@ -1,111 +0,0 @@
<template>
<div>
<sw-input
:type="type"
:invalid="isInvalid"
:placeholder="placeholder"
v-model="inputValue"
:tabindex="tabindex"
@input="handleInput"
@change="handleChange"
/>
<span v-if="isInvalid" class="text-sm text-danger">
{{ $t('validation.required') }}
</span>
</div>
</template>
<script>
const {
required,
minLength,
numeric,
minValue,
maxLength,
requiredIf,
} = require('vuelidate/lib/validators')
export default {
props: {
type: {
type: String,
default: 'text',
required: false,
},
field: {
type: Object,
default: null,
required: true,
},
invalidFields: {
type: Array,
default: () => [],
},
tabindex: {
type: Number,
default: null,
},
},
data() {
return {
inputValue: null,
placeholder: '',
invalidFieldIds: [],
}
},
validations: {
inputValue: {
required: requiredIf('isRequired'),
},
},
computed: {
isRequired() {
if (this.field && this.field.is_required) {
return true
}
return false
},
isInvalid() {
if (
this.invalidFieldIds.indexOf(this.field.cfid) >= 0 ||
(this.$v.inputValue.$error && !this.$v.inputValue.required)
) {
return true
}
return false
},
},
watch: {
field: {
handler: 'handleData',
deep: true,
},
invalidFields: {
handler: 'setInvalidFieldIds',
deep: true,
},
},
mounted() {
this.inputValue = this.field && this.field.defaultAnswer
this.placeholder =
this.field && this.field.placeholder ? this.field.placeholder : ''
},
methods: {
handleData() {
this.inputValue = this.field && this.field.defaultAnswer
this.placeholder =
this.field && this.field.placeholder ? this.field.placeholder : ''
},
setInvalidFieldIds() {
this.invalidFieldIds = this.invalidFields.map((field) => field.id)
},
handleInput() {
this.$emit('update', { field: this.field, value: this.inputValue })
this.$v.inputValue.$touch()
},
handleChange() {
this.$v.inputValue.$touch()
this.$emit('update', { field: this.field, value: this.inputValue })
},
},
}
</script>

View File

@ -1,114 +0,0 @@
<template>
<div>
<sw-input
:invalid="$v.inputValue.$error || isInvalid"
:placeholder="placeholder"
:tabindex="tabindex"
v-model="inputValue"
type="number"
@input="handleInput"
@change="handleChange"
/>
<div v-if="$v.inputValue.$error || isInvalid">
<span v-if="isInvalid" class="text-sm text-danger">
{{ $t('validation.required') }}
</span>
<span
v-if="!isInvalid && $v.inputValue.numeric"
class="text-sm text-danger"
>
{{ $t('validation.required') }}
</span>
</div>
</div>
</template>
<script>
const { required, numeric, requiredIf } = require('vuelidate/lib/validators')
export default {
props: {
type: {
type: String,
default: 'text',
required: false,
},
field: {
type: Object,
default: null,
required: true,
},
invalidFields: {
type: Array,
default: () => [],
},
tabindex: {
type: Number,
default: null,
},
},
data() {
return {
inputValue: null,
placeholder: '',
invalidFieldIds: [],
}
},
validations: {
inputValue: {
required: requiredIf('isRequired'),
numeric,
},
},
computed: {
isRequired() {
if (this.field && this.field.is_required) {
return true
}
return false
},
isInvalid() {
if (
this.invalidFieldIds.indexOf(this.field.cfid) >= 0 ||
(this.$v.inputValue.$error && !this.$v.inputValue.required)
) {
return true
}
return false
},
},
watch: {
field: {
handler: 'handleData',
deep: true,
},
invalidFields: {
handler: 'setInvalidFieldIds',
deep: true,
},
},
mounted() {
this.inputValue = this.field && this.field.defaultAnswer
this.placeholder =
this.field && this.field.placeholder ? this.field.placeholder : ''
},
methods: {
setInvalidFieldIds() {
this.invalidFieldIds = this.invalidFields.map((field) => field.id)
},
handleData() {
this.inputValue = this.field && this.field.defaultAnswer
this.placeholder =
this.field && this.field.placeholder ? this.field.placeholder : ''
},
handleInput() {
this.$v.inputValue.$touch()
this.$emit('update', { field: this.field, value: this.inputValue })
},
handleChange() {
this.$v.inputValue.$touch()
this.$emit('update', { field: this.field, value: this.inputValue })
},
},
}
</script>

View File

@ -1,113 +0,0 @@
<template>
<div>
<sw-input
:invalid="$v.inputValue.$error || isInvalid"
:placeholder="placeholder"
:tabindex="tabindex"
v-model="inputValue"
type="text"
@input="handleInput"
@change="handleChange"
/>
<div v-if="$v.inputValue.$error || isInvalid">
<span
v-if="!isInvalid && !$v.inputValue.phone"
class="text-sm text-danger"
>
{{ $t('validation.invalid_phone') }}
</span>
<span v-if="isInvalid" class="text-sm text-danger">
{{ $t('validation.required') }}
</span>
</div>
</div>
</template>
<script>
const {
required,
minLength,
numeric,
minValue,
maxLength,
requiredIf,
} = require('vuelidate/lib/validators')
const isPhone = (value) => /^\+?[0-9]+$/.test(value)
export default {
props: {
type: {
type: String,
default: 'text',
required: false,
},
field: {
type: Object,
default: null,
required: true,
},
invalidFields: {
type: Array,
default: () => [],
},
tabindex: {
type: Number,
default: null,
},
},
data() {
return {
inputValue: null,
placeholder: '',
invalidFieldIds: [],
}
},
validations: {
inputValue: {
phone: isPhone,
required: requiredIf('isRequired'),
},
},
computed: {
isRequired() {
if (this.field && this.field.is_required) {
return true
}
return false
},
isInvalid() {
if (
this.invalidFieldIds.indexOf(this.field.cfid) >= 0 ||
(this.$v.inputValue.$error && !this.$v.inputValue.required)
) {
return true
}
return false
},
},
watch: {
invalidFields: {
handler: 'setInvalidFieldIds',
deep: true,
},
},
mounted() {
this.inputValue = this.field && this.field.defaultAnswer
this.placeholder =
this.field && this.field.placeholder ? this.field.placeholder : ''
},
methods: {
handleInput() {
this.$v.inputValue.$touch()
this.$emit('update', { field: this.field, value: this.inputValue })
},
handleChange() {
this.$v.inputValue.$touch()
this.$emit('update', { field: this.field, value: this.inputValue })
},
setInvalidFieldIds() {
this.invalidFieldIds = this.invalidFields.map((field) => field.id)
},
},
}
</script>

View File

@ -1,37 +0,0 @@
<template>
<sw-switch
v-model="switchData"
class="btn-switch"
@change="onChange"
style="margin-top: -15px"
/>
</template>
<script>
export default {
props: {
type: {
type: String,
default: 'text',
required: false,
},
field: {
type: Object,
default: null,
required: true,
},
},
data() {
return {
switchData: false,
}
},
mounted() {
this.switchData = this.field && this.field.defaultAnswer ? true : false
},
methods: {
onChange() {
this.$emit('update', { field: this.field, value: this.switchData })
},
},
}
</script>

View File

@ -1,96 +0,0 @@
<template>
<div>
<sw-textarea
v-model="text"
:invalid="isInvalid"
:placeholder="placeholder"
:tabindex="tabindex"
@input="handleInput"
@change="handleChange"
/>
<span v-if="isInvalid" class="text-sm text-danger">
{{ $t('validation.required') }}
</span>
</div>
</template>
<script>
const {
required,
minLength,
numeric,
minValue,
maxLength,
requiredIf,
} = require('vuelidate/lib/validators')
export default {
props: {
field: {
type: Object,
default: null,
required: true,
},
invalidFields: {
type: Array,
default: () => [],
},
tabindex: {
type: Number,
default: null,
},
},
data() {
return {
text: null,
placeholder: '',
invalidFieldIds: [],
}
},
validations: {
text: {
required: requiredIf('isRequired'),
},
},
computed: {
isRequired() {
if (this.field && this.field.is_required) {
return true
}
return false
},
isInvalid() {
if (
this.invalidFieldIds.indexOf(this.field.cfid) >= 0 ||
(this.$v.text.$error && !this.$v.text.required)
) {
return true
}
return false
},
},
watch: {
invalidFields: {
handler: 'setInvalidFieldIds',
deep: true,
},
},
mounted() {
this.text = this.field && this.field.defaultAnswer
this.placeholder =
this.field && this.field.placeholder ? this.field.placeholder : ''
},
methods: {
setInvalidFieldIds() {
this.invalidFieldIds = this.invalidFields.map((field) => field.id)
},
handleInput() {
this.$v.text.$touch()
this.$emit('update', { field: this.field, value: this.text })
},
handleChange() {
this.$v.text.$touch()
this.$emit('update', { field: this.field, value: this.text })
},
},
}
</script>

View File

@ -1,103 +0,0 @@
<template>
<div>
<base-time-picker
v-model="time"
:set-value="defaultValue"
:invalid="$v.time.$error"
:placeholder="placeholder"
:tabindex="tabindex"
hide-clear-button
@input="onTimeSelect"
/>
<div v-if="$v.time.$error">
<span v-if="!$v.time.required" class="text-sm text-danger">
{{ $t('validation.required') }}
</span>
</div>
</div>
</template>
<script>
const { required, requiredIf } = require('vuelidate/lib/validators')
export default {
props: {
field: {
type: Object,
default: null,
required: true,
},
invalidFields: {
type: Array,
default: () => [],
},
tabindex: {
type: Number,
default: null,
},
},
data() {
return {
time: null,
defaultValue: '00:00:00',
placeholder: '',
invalidFieldIds: [],
}
},
validations: {
time: {
required: requiredIf('isRequired'),
},
},
computed: {
isRequired() {
if (this.field && this.field.is_required) {
return true
}
return false
},
isInvalid() {
if (
this.invalidFieldIds.indexOf(this.field.cfid) >= 0 ||
(this.$v.inputValue.$error && !this.$v.inputValue.required)
) {
return true
}
return false
},
},
watch: {
field: {
handler: 'handleData',
deep: true,
},
invalidFields: {
handler: 'setInvalidFieldIds',
deep: true,
},
},
mounted() {
this.placeholder =
this.field && this.field.placeholder ? this.field.placeholder : ''
this.handleData()
},
methods: {
handleData() {
this.defaultValue =
this.field && this.field.defaultAnswer
? this.field.defaultAnswer
: '00-00-00'
this.time =
this.field && this.field.defaultAnswer
? this.field.defaultAnswer
: '00-00-00'
},
setInvalidFieldIds() {
this.invalidFieldIds = this.invalidFields.map((field) => field.id)
},
onTimeSelect() {
this.$v.time.$touch()
this.$emit('update', { field: this.field, value: this.time })
},
},
}
</script>

View File

@ -1,101 +0,0 @@
<template>
<div>
<sw-input
:invalid="$v.inputValue.$error || isInvalid"
:placeholder="placeholder"
:tabindex="tabindex"
v-model="inputValue"
type="url"
@input="handleInput"
@change="handleChange"
/>
<div v-if="$v.inputValue.$error || isInvalid">
<span v-if="!$v.inputValue.url" class="text-sm text-danger">
{{ $t('validation.invalid_url') }}
</span>
<span v-if="isInvalid" class="text-sm text-danger">
{{ $t('validation.required') }}
</span>
</div>
</div>
</template>
<script>
const { required, url, requiredIf } = require('vuelidate/lib/validators')
export default {
props: {
type: {
type: String,
default: 'text',
required: false,
},
field: {
type: Object,
default: null,
required: true,
},
invalidFields: {
type: Array,
default: () => [],
},
tabindex: {
type: Number,
default: null,
},
},
data() {
return {
inputValue: null,
placeholder: '',
invalidFieldIds: [],
}
},
validations: {
inputValue: {
url,
required: requiredIf('isRequired'),
},
},
computed: {
isRequired() {
if (this.field && this.field.is_required) {
return true
}
return false
},
isInvalid() {
if (
this.invalidFieldIds.indexOf(this.field.cfid) >= 0 ||
(this.$v.inputValue.$error && !this.$v.inputValue.required)
) {
return true
}
return false
},
},
watch: {
invalidFields: {
handler: 'setInvalidFieldIds',
deep: true,
},
},
mounted() {
this.inputValue = this.field && this.field.defaultAnswer
this.placeholder =
this.field && this.field.placeholder ? this.field.placeholder : ''
},
methods: {
setInvalidFieldIds() {
this.invalidFieldIds = this.invalidFields.map((field) => field.id)
},
handleInput() {
this.$v.inputValue.$touch()
this.$emit('update', { field: this.field, value: this.inputValue })
},
handleChange() {
this.$v.inputValue.$touch()
this.$emit('update', { field: this.field, value: this.inputValue })
},
},
}
</script>

View File

@ -1,84 +0,0 @@
<template>
<div>
<div class="dot-icon" @click="checktoggle">
<span :class="{ 'move-right': toggle }" class="dot dot1" />
<span class="dot dot2" />
<span :class="{ 'move-left': toggle }" class="dot dot3" />
</div>
</div>
</template>
<script>
export default {
data() {
return {
toggle: false,
}
},
methods: {
checktoggle: function () {
var v = this
v.toggle = true
setTimeout(function () {
v.toggle = false
}, 300)
},
},
}
</script>
<style>
.dot-icon {
display: flex;
cursor: pointer;
padding: 8px 5px 5px 5px;
justify-content: flex-end;
}
.dot {
display: inline-block;
background: #a5acc1;
display: block;
width: 6px;
height: 6px;
border-radius: 50%;
position: relative;
}
.dot1 {
margin-right: 3px;
}
.dot2 {
margin-right: 3px;
}
.move-right {
animation: moveright 0.2s;
animation-fill-mode: forwards;
}
.move-left {
animation: moveleft 0.2s;
animation-fill-mode: forwards;
}
@keyframes moveleft {
from {
left: 0px;
}
to {
left: -18px;
}
}
@keyframes moveright {
from {
left: 0px;
}
to {
left: 18px;
}
}
</style>

View File

@ -1,18 +0,0 @@
<template>
<svg
width="39"
height="39"
viewBox="0 0 39 39"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.22 38.44C29.8349 38.44 38.44 29.8349 38.44 19.22C38.44 8.60509 29.8349 0 19.22 0C8.60509 0 0 8.60509 0 19.22C0 29.8349 8.60509 38.44 19.22 38.44Z"
fill="#3B5998"
/>
<path
d="M23.442 18.5216H20.833V28.08H16.88V18.5216H15V15.1624H16.88V12.9887C16.88 11.4342 17.6184 9 20.8682 9L23.7962 9.01225V12.2729H21.6717C21.3232 12.2729 20.8332 12.447 20.8332 13.1886V15.1656H23.7874L23.442 18.5216Z"
fill="white"
/>
</svg>
</template>

View File

@ -1,34 +0,0 @@
<template>
<svg
width="40"
height="39"
viewBox="0 0 40 39"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="20.0078"
cy="19.7782"
r="18.7188"
fill="#F2F2F2"
stroke="#F2F2F2"
stroke-width="1.00639"
/>
<path
d="M13.744 21.9871L12.9733 24.8641L10.1565 24.9237C9.31465 23.3623 8.83716 21.5759 8.83716 19.6775C8.83716 17.8418 9.2836 16.1107 10.075 14.5864H10.0756L12.5833 15.0462L13.6819 17.5389C13.4519 18.2092 13.3266 18.9288 13.3266 19.6775C13.3267 20.4902 13.4739 21.2688 13.744 21.9871Z"
fill="#FBBB00"
/>
<path
d="M30.7842 17.6089C30.9114 18.2786 30.9777 18.9701 30.9777 19.677C30.9777 20.4695 30.8943 21.2426 30.7356 21.9883C30.1967 24.526 28.7886 26.7419 26.8379 28.3099L26.8373 28.3093L23.6786 28.1482L23.2316 25.3575C24.5259 24.5984 25.5375 23.4104 26.0703 21.9883H20.1508V17.6089H26.1567H30.7842Z"
fill="#518EF8"
/>
<path
d="M26.8374 28.3096L26.838 28.3102C24.9409 29.8351 22.531 30.7475 19.9076 30.7475C15.6918 30.7475 12.0264 28.3911 10.1566 24.9235L13.7441 21.9868C14.679 24.4819 17.0859 26.258 19.9076 26.258C21.1204 26.258 22.2567 25.9301 23.2317 25.3578L26.8374 28.3096Z"
fill="#28B446"
/>
<path
d="M26.9737 11.1555L23.3874 14.0916C22.3783 13.4608 21.1855 13.0964 19.9075 13.0964C17.022 13.0964 14.5701 14.954 13.682 17.5386L10.0757 14.5861H10.0751C11.9175 11.0339 15.6291 8.60693 19.9075 8.60693C22.5936 8.60693 25.0564 9.56373 26.9737 11.1555Z"
fill="#F14336"
/>
</svg>
</template>

View File

@ -1,113 +0,0 @@
<template>
<svg
width="125"
height="110"
viewBox="0 0 125 110"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M46.8031 84.4643C46.8031 88.8034 43.3104 92.3215 39.0026 92.3215C34.6948 92.3215 31.2021 88.8034 31.2021 84.4643C31.2021 80.1252 34.6948 76.6072 39.0026 76.6072C43.3104 76.6072 46.8031 80.1252 46.8031 84.4643Z"
fill="#817AE3"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M60.4536 110H64.3539V72.6785H60.4536V110Z"
fill="#55547A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M85.8055 76.6072H70.2045C69.1319 76.6072 68.2544 77.4911 68.2544 78.5715V82.5C68.2544 83.5804 69.1319 84.4643 70.2045 84.4643H85.8055C86.878 84.4643 87.7556 83.5804 87.7556 82.5V78.5715C87.7556 77.4911 86.878 76.6072 85.8055 76.6072ZM70.2045 82.5H85.8055V78.5715H70.2045V82.5Z"
fill="#817AE3"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M91.6556 1.96429C94.8811 1.96429 97.506 4.60821 97.506 7.85714V19.6429H83.8181L85.308 21.6071H99.4561V7.85714C99.4561 3.53571 95.9459 0 91.6556 0H33.152C28.8618 0 25.3516 3.53571 25.3516 7.85714V21.6071H39.3203L40.8745 19.6429H27.3017V7.85714C27.3017 4.60821 29.9265 1.96429 33.152 1.96429H91.6556Z"
fill="#55547A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M122.858 92.3213H117.007C115.935 92.3213 115.057 93.2052 115.057 94.2856V102.143C115.057 103.223 115.935 104.107 117.007 104.107H122.858C123.93 104.107 124.808 103.223 124.808 102.143V94.2856C124.808 93.2052 123.93 92.3213 122.858 92.3213ZM117.007 102.143H122.858V94.2856H117.007V102.143Z"
fill="#817AE3"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M103.356 43.2142V70.7142H21.4511V43.2142H26.1821V41.2498H19.501V72.6783H105.306V41.2498H98.3541L98.2839 43.2142H103.356Z"
fill="#55547A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M101.406 21.6071C104.632 21.6071 107.257 24.251 107.257 27.5V41.25H98.2257L98.0853 43.2142H109.207V27.5C109.207 23.1609 105.714 19.6428 101.406 19.6428H83.8182L85.0878 21.6071H101.406ZM40.8746 19.6428H23.4016C19.0937 19.6428 15.6011 23.1609 15.6011 27.5V43.2142H26.1961L26.3365 41.25H17.5512V27.5C17.5512 24.251 20.1761 21.6071 23.4016 21.6071H39.3204L40.8746 19.6428Z"
fill="#55547A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M62.4041 9.82153C45.1709 9.82153 31.2021 23.8917 31.2021 41.2501C31.2021 58.6085 45.1709 72.6787 62.4041 72.6787C79.6373 72.6787 93.606 58.6085 93.606 41.2501C93.606 23.8917 79.6373 9.82153 62.4041 9.82153ZM62.4041 11.7858C78.5335 11.7858 91.6559 25.0035 91.6559 41.2501C91.6559 57.4967 78.5335 70.7144 62.4041 70.7144C46.2746 70.7144 33.1523 57.4967 33.1523 41.2501C33.1523 25.0035 46.2746 11.7858 62.4041 11.7858Z"
fill="#55547A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M62.4041 19.6428C45.1709 19.6428 31.2021 23.8916 31.2021 41.25C31.2021 58.6084 45.1709 66.7857 62.4041 66.7857C79.6373 66.7857 93.606 58.6084 93.606 41.25C93.606 23.8916 79.6373 19.6428 62.4041 19.6428ZM62.4041 21.6071C82.6346 21.6071 91.6559 27.665 91.6559 41.25C91.6559 56.0096 80.7216 64.8214 62.4041 64.8214C44.0866 64.8214 33.1523 56.0096 33.1523 41.25C33.1523 27.665 42.1735 21.6071 62.4041 21.6071Z"
fill="#55547A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M101.406 70.7144H23.4014C10.478 70.7144 0 81.2685 0 94.2858V110H124.808V94.2858C124.808 81.2685 114.33 70.7144 101.406 70.7144ZM101.406 72.6786C113.234 72.6786 122.858 82.3724 122.858 94.2858V108.036H1.95012V94.2858C1.95012 82.3724 11.574 72.6786 23.4014 72.6786H101.406Z"
fill="#55547A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M33.152 33.3928H29.2518C27.0969 33.3928 25.3516 35.1509 25.3516 37.3214V45.1785C25.3516 47.3491 27.0969 49.1071 29.2518 49.1071H33.152V33.3928ZM31.2019 35.3571V47.1428H29.2518C28.1773 47.1428 27.3017 46.2609 27.3017 45.1785V37.3214C27.3017 36.2391 28.1773 35.3571 29.2518 35.3571H31.2019Z"
fill="#55547A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M95.556 33.3928H91.6558V49.1071H95.556C97.7109 49.1071 99.4562 47.3491 99.4562 45.1785V37.3214C99.4562 35.1509 97.7109 33.3928 95.556 33.3928ZM95.556 35.3571C96.6305 35.3571 97.5061 36.2391 97.5061 37.3214V45.1785C97.5061 46.2609 96.6305 47.1428 95.556 47.1428H93.6059V35.3571H95.556Z"
fill="#55547A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M94.581 15.7144C94.0447 15.7144 93.606 16.1563 93.606 16.6965V34.3751C93.606 34.9152 94.0447 35.3572 94.581 35.3572C95.1173 35.3572 95.5561 34.9152 95.5561 34.3751V16.6965C95.5561 16.1563 95.1173 15.7144 94.581 15.7144Z"
fill="#55547A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M38.0273 41.2499C37.4891 41.2499 37.0522 40.8099 37.0522 40.2678C37.0522 33.3142 44.1409 25.5356 53.6283 25.5356C54.1665 25.5356 54.6033 25.9756 54.6033 26.5178C54.6033 27.0599 54.1665 27.4999 53.6283 27.4999C45.2564 27.4999 39.0024 34.2414 39.0024 40.2678C39.0024 40.8099 38.5655 41.2499 38.0273 41.2499Z"
fill="#817AE3"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M97.5059 110H99.456V72.6785H97.5059V110Z"
fill="#55547A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M25.3516 110H27.3017V72.6785H25.3516V110Z"
fill="#55547A"
/>
</g>
<defs>
<clipPath id="clip0">
<rect width="124.808" height="110" fill="white" />
</clipPath>
</defs>
</svg>
</template>

View File

@ -1,18 +0,0 @@
<template>
<svg
width="39"
height="39"
viewBox="0 0 39 39"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.4104 39.0002C30.0264 39.0002 38.6324 30.3942 38.6324 19.7782C38.6324 9.16215 30.0264 0.556152 19.4104 0.556152C8.79435 0.556152 0.188354 9.16215 0.188354 19.7782C0.188354 30.3942 8.79435 39.0002 19.4104 39.0002Z"
fill="#55ACEE"
/>
<path
d="M31.1843 14.3704C30.3605 14.7357 29.4744 14.9827 28.5452 15.0931C29.494 14.5246 30.2221 13.6251 30.5658 12.5516C29.678 13.0783 28.6942 13.4603 27.6481 13.6663C26.81 12.7737 25.6159 12.2158 24.2936 12.2158C21.7566 12.2158 19.699 14.2734 19.699 16.8104C19.699 17.1706 19.7397 17.5211 19.8185 17.8576C16 17.666 12.6143 15.837 10.348 13.0563C9.95261 13.7348 9.72577 14.5246 9.72577 15.3665C9.72577 16.9602 10.5375 18.3671 11.7697 19.1908C11.0169 19.1672 10.3079 18.9606 9.68876 18.6155C9.68842 18.635 9.68842 18.6546 9.68842 18.6738C9.68842 20.9 11.2728 22.7568 13.3743 23.1786C12.9892 23.2841 12.5825 23.34 12.1641 23.34C11.8673 23.34 11.5799 23.3115 11.2996 23.2581C11.8841 25.083 13.5806 26.4115 15.5916 26.4489C14.0188 27.6814 12.038 28.4157 9.88476 28.4157C9.5147 28.4157 9.14806 28.3941 8.78931 28.3512C10.8216 29.6554 13.2373 30.4157 15.8318 30.4157C24.2829 30.4157 28.9046 23.4147 28.9046 17.3426C28.9046 17.1435 28.9002 16.9451 28.8913 16.7484C29.7897 16.1008 30.5685 15.2918 31.1843 14.3704Z"
fill="#F1F2F2"
/>
</svg>
</template>

View File

@ -1,14 +0,0 @@
<template>
<svg
width="25"
height="19"
viewBox="0 0 25 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M21.0156 8.36719C21.7708 8.4974 22.4479 8.79688 23.0469 9.26562C23.6458 9.73438 24.1146 10.3203 24.4531 11.0234C24.8177 11.7266 25 12.4688 25 13.25C25 14.6302 24.5052 15.8151 23.5156 16.8047C22.5521 17.7682 21.3802 18.25 20 18.25H5.625C4.0625 18.25 2.73438 17.7031 1.64062 16.6094C0.546875 15.5156 0 14.1875 0 12.625C0 11.401 0.351562 10.3073 1.05469 9.34375C1.75781 8.38021 2.65625 7.70312 3.75 7.3125C3.75 7.18229 3.75 7.07812 3.75 7C3.75 5.28125 4.36198 3.8099 5.58594 2.58594C6.8099 1.36198 8.28125 0.75 10 0.75C11.1458 0.75 12.2005 1.03646 13.1641 1.60938C14.1276 2.18229 14.8828 2.9375 15.4297 3.875C16.0547 3.45833 16.7448 3.25 17.5 3.25C18.5417 3.25 19.4271 3.61458 20.1562 4.34375C20.8854 5.07292 21.25 5.95833 21.25 7C21.25 7.46875 21.1719 7.92448 21.0156 8.36719ZM15.3516 10.75C15.638 10.75 15.8333 10.6198 15.9375 10.3594C16.0417 10.099 16.0026 9.8776 15.8203 9.69531L11.6797 5.55469C11.3932 5.26823 11.1068 5.26823 10.8203 5.55469L6.67969 9.69531C6.4974 9.8776 6.45833 10.099 6.5625 10.3594C6.66667 10.6198 6.86198 10.75 7.14844 10.75H9.6875V15.125C9.6875 15.3073 9.73958 15.4635 9.84375 15.5938C9.97396 15.6979 10.1302 15.75 10.3125 15.75H12.1875C12.3698 15.75 12.513 15.6979 12.6172 15.5938C12.7474 15.4635 12.8125 15.3073 12.8125 15.125V10.75H15.3516Z"
fill="#B9C1D1"
/>
</svg>
</template>

View File

@ -1,45 +0,0 @@
<script>
let mailgunComponent = {
template: '#mailgun-template',
}
let sendgridComponent = {
template: '#sendgrid-template',
}
let sparkPostComponent = {
template: '#sparkpost-template',
}
let smtpComponent = {
template: '#smtp-template',
}
export default {
components: {
mailgun: mailgunComponent,
sendgrid: sendgridComponent,
sparkpost: sparkPostComponent,
smtp: smtpComponent,
},
props: {
view: {
type: Array,
require: true,
default: Array,
},
},
data() {
return {
currentView: 'mailgun',
}
},
mounted() {
let views = ['mailgun', 'sendgrid', 'sparkpost', 'smtp']
if (this.view && views.indexOf(this.view) > -1) {
this.currentView = this.view
}
},
}
</script>

View File

@ -1,17 +0,0 @@
export default {
classes:
'px-2 py-1 text-xs bg-success text-green-800 uppercase font-normal text-center',
variants: {
success:
'px-2 py-1 text-xs bg-success text-green-800 uppercase font-normal text-center',
info:
'px-2 py-1 text-xs bg-info text-blue-800 uppercase text-center font-normal',
danger:
'px-2 py-1 text-xs bg-danger text-red-700 uppercase font-normal text-center',
warning:
'px-2 py-1 text-xs bg-warning text-indigo-900 uppercase font-normal text-center',
},
}

View File

@ -1,22 +0,0 @@
export default {
variants: {
grayLight: {
button:
'inline-flex items-center justify-center text-gray-400 transition duration-150 ease-in-out border border-transparent focus:outline-none bg-gray-100 border border-gray-200 hover:bg-gray-200 hover:border-gray-400 hover:text-gray-600',
},
gray: {
button:
'inline-flex items-center justify-center text-gray-400 transition duration-150 ease-in-out border border-transparent focus:outline-none bg-gray-300 border border-gray-200',
},
white: {
button:
'inline-flex items-center justify-center text-black transition px-2 duration-150 ease-in-out border border-gray-300 border-solid focus:outline-none bg-white',
},
},
sizes: {
discount: {
button: 'py-2 px-2 text-sm leading-5 rounded',
loadingIcon: 'w-4 h-4 -ml-2',
},
},
}

View File

@ -1,18 +0,0 @@
export default {
classes: {
container: 'bg-white rounded shadow',
header: 'px-5 py-4 text-black border-b border-solid border-gray-300',
body: 'px-4 py-5 sm:p-8',
footer: 'px-5 py-4 border-t border-solid sm:px-6 border-gray-300',
},
variants: {
customerCard: {
body: 'px-4 py-5 sm:p-8',
},
settingCard: {
header: 'px-4 pt-5 sm:px-8 sm:pt-8',
footer: 'px-5 border-t-none py-4 sm:px-6',
},
},
}

View File

@ -1,25 +0,0 @@
export default {
classes: {
container: 'relative sw-dropdown',
activator: 'cursor-pointer',
divider: 'border-t border-solid border-gray-200 my-2 mx-0 overflow-hidden',
itemContainer:
'z-10 p-2 max-h-60 text-base text-left list-none rounded border-0 shadow bg-white text-black overflow-auto sw-scroll',
item:
'flex p-2 text-sm font-light text-left text-black bg-transparent rounded cursor-pointer none hover:bg-gray-200 whitespace-nowrap',
itemIcon: 'w-5 h-5 mr-3 text-secondary',
},
variants: {
searchDropdown: {
activator: 'cursor-pointer',
container: 'relative',
divider:
'border-t border-solid border-gray-200 my-2 mx-0 overflow-hidden',
item:
'flex p-0 text-sm font-light text-left text-black bg-transparent rounded cursor-pointer none hover:bg-gray-200 whitespace-nowrap',
itemContainer:
'z-10 p-2 text-base text-left list-none rounded border-0 shadow bg-white text-black',
itemIcon: 'w-5 h-5 mr-3 text-secondary',
},
},
}

View File

@ -1,7 +0,0 @@
export default {
classes: {
container: 'relative p-8 mt-5 rounded bg-gray-200',
body: 'lg:flex block flex-col md:flex-row',
},
variants: {},
}

View File

@ -1,31 +0,0 @@
export default {
variants: {
gray: {
container:
'relative flex items-center w-full border border-solid rounded-md bg-gray-100 hover:border-gray-400',
baseInput:
'not-italic font-normal leading-tight text-left outline-none rounded-md input-field box-border-2 placeholder-gray-400 text-black w-full h-8 px-0 py-0 text-xs bg-gray-100',
rightIconInput:
'not-italic font-normal leading-tight text-left outline-none min-w-0 rounded-md input-field box-border-2 placeholder-gray-400 text-black w-full pl-3 pr-1 py-2 text-sm bg-gray-100',
rightIconContainer:
'flex flex-col justify-center align-middle pr-2 text-gray-400 min-w-0',
containerFocusIn: 'border-primary-500 ',
containerFocusOut: 'border-gray-300 focus:border-transparent',
},
searchInput: {
container:
'relative flex items-center w-full border border-solid rounded-md bg-white',
baseInput:
'not-italic font-normal leading-tight text-left outline-none rounded-md input-field box-border-2 placeholder-gray-400 text-black w-full h-8 px-0 py-0 text-xs',
rightIconInput:
'not-italic font-normal leading-tight text-left outline-none rounded-md input-field box-border-2 placeholder-gray-400 text-black w-full pl-3 pr-1 py-2 text-sm',
leftIconInput:
'not-italic font-normal leading-tight text-left outline-none rounded-md input-field box-border-2 placeholder-gray-400 text-black w-full pl-1 pr-3 py-2 text-sm',
rightIconContainer: 'flex flex-col justify-center align-middle pr-2',
leftIconContainer: 'flex flex-col justify-center align-middle',
containerFocusIn: 'border-primary-500',
containerFocusOut: 'border-gray-300 focus:border-transparent',
},
},
}

View File

@ -1,33 +0,0 @@
export default {
classes: {
container: 'pr-4 pl-0 list-none',
itemContainer:
'cursor-pointer pb-2 pr-0 text-sm font-medium leading-5 text-gray-500 flex items-center',
title: '',
iconContainer: 'mr-3',
listGroup: {
container: 'p-0 list-none',
titleContainer:
'flex items-center justify-between pb-2 pr-0 text-sm font-medium leading-5 text-gray-500 cursor-pointer',
title: 'text-sm',
icon: 'w-5 h-5 leading-4 transform rotate-90',
itemsContainer: 'pl-4 list-none',
itemContainer:
'cursor-pointer pb-2 pr-0 text-sm font-medium leading-5 text-gray-500 flex items-center',
},
active: {
itemContainer:
'cursor-pointer pb-2 pr-0 text-sm font-medium flex items-center leading-5 text-primary-500',
listGroup: {
container: 'p-0 list-none',
titleContainer:
'flex items-center justify-between pb-2 pr-0 text-sm font-medium leading-5 text-primary-500 cursor-pointer',
title: 'text-sm',
icon: 'w-5 h-5 leading-4 ',
itemsContainer: 'pl-4 list-none',
itemContainer:
'cursor-pointer pb-2 pr-0 text-sm font-medium leading-5 text-gray-500 flex items-center',
},
},
},
}

View File

@ -1,20 +0,0 @@
export default {
classes: {
overlayContainer:
'fixed z-50 inset-0 overflow-y-auto sw-scroll bg-opacity-25 bg-gray-700 flex justify-center min-h-screen items-center text-center sm:p-0',
centering: 'hidden sm:inline-block sm:align-middle sm:h-screen',
base:
'inline-block border-t-8 border-solid border-primary-500 w-full align-bottom bg-white rounded text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-xl sm:w-full m-6 sm:m-0',
header:
'py-4 px-6 h-16 text-black font-medium text-lg border-b border-solid border-gray-light flex justify-between items-center',
body: 'modal body text-sm',
footer:
'border-t border-solid border-gray-light py-4 px-6 flex justify-end',
},
variants: {
lg: {
base:
'inline-block border-t-8 border-solid border-primary-500 w-full align-bottom bg-white rounded text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full m-6 sm:m-0',
},
},
}

View File

@ -1,8 +0,0 @@
export default {
classes: {
container: 'relative w-full',
activator: 'relative w-full cursor-pointer',
base: 'flex flex-col absolute w-full top-0 bg-white rounded z-20 shadow-xl',
above: 'bottom-full top-unset',
},
}

View File

@ -1,34 +0,0 @@
export default {
classes: {
container: 'radio',
label: 'cursor-pointer',
input:
'cursor-pointer flex-shrink-0 inline-block text-primary-500 align-middle bg-white border border-gray-300 rounded-full outline-none appearance-none select-none transition duration-200 ease-in-out',
},
variants: {
success: {
input:
'cursor-pointer flex-shrink-0 inline-block text-success align-middle bg-white border border-gray-300 rounded-full outline-none appearance-none select-none transition duration-200 ease-in-out',
label: 'cursor-pointer',
},
danger: {
input:
'cursor-pointer flex-shrink-0 inline-block text-danger align-middle bg-white border border-gray-300 rounded-full outline-none appearance-none select-none transition duration-200 ease-in-out',
label: 'cursor-pointer',
},
},
sizes: {
sm: {
input: 'w-4 h-4',
label: 'ml-2',
},
default: {
input: 'w-6 h-6',
label: 'ml-3 text-lg',
},
lg: {
input: 'w-8 h-8',
label: 'ml-4 text-xl',
},
},
}

View File

@ -1,8 +0,0 @@
export default {
classes: {
container: 'switch-container focus:outline-none',
switch: 'switch',
label:
'relative block h-4 bg-white border border-solid cursor-pointer border-gray switch-label',
},
}

View File

@ -1,8 +0,0 @@
export default {
variants: {
gray: {
tdStyles:
'text-left text-base h-20 px-3 py-6 align-middle bg-gray-100 first:rounded-bl-md first:rounded-tl-md last:rounded-br-md last:rounded-tr-md',
},
},
}

View File

@ -1,6 +0,0 @@
export default {
variants: {
invDesc:
'text-left border border-solid box-border rounded not-italic leading-snug placeholder-gray-400 bg-white border-gray-300 focus:border-primary-400 outline-none w-full py-2 px-3 font-normal text-xs text-black',
},
}

View File

@ -1,25 +0,0 @@
export default {
classes: {
wizardContainer: 'wizard w-full',
wizardStepsContainer: 'relative flex items-center justify-center',
navigationContainer:
'flex flex-col items-center justify-between h-32 step-indicator',
progressesContainer:
'box-border relative flex justify-around mt-16 border-4 border-gray-200 border-solid rounded-md indicator-line',
progressesSubContainer: 'absolute flex justify-between center',
progress: 'rounded-full steps cursor-pointer',
currentStep: 'bg-white border-primary-500',
previousStep:
'bg-primary-500 border-primary-500 flex justify-center items-center',
nextStep: 'border-gray-200 bg-white',
icon:
'flex items-center justify-center w-full h-full text-sm font-black text-center text-white',
stepContainer:
'w-full mb-8 bg-white border border-gray-200 border-solid rounded p-8 lg:w-9/12 md:w-full relative',
stepHeadingContainer: 'heading-section',
stepTitle: 'text-2xl not-italic font-semibold leading-7 text-black',
stepDescription:
'w-full mt-2.5 mb-8 text-sm not-italic text-gray-600 lg:w-7/12 md:w-7/12 sm:w-7/12',
},
variants: {},
}

View File

@ -1,29 +0,0 @@
import SwModal from './SwModal'
import SwInput from './SwInput'
import SwTable from './SwTable'
import SwButton from './SwButton'
import SwDropdown from './SwDropdown'
import SwCard from './SwCard'
import SwList from './SwList'
import SwWizard from './SwWizard'
import SwTextarea from './SwTextarea'
import SwPopup from './SwPopup'
import SwBadge from './SwBadge'
import SwFilterWrapper from './SwFilterWrapper'
import SwSwitch from './SwSwitch'
export default {
SwModal,
SwInput,
SwTable,
SwButton,
SwCard,
SwList,
SwWizard,
SwTextarea,
SwPopup,
SwDropdown,
SwBadge,
SwFilterWrapper,
SwSwitch,
}

View File

@ -1,34 +0,0 @@
import Vue from 'vue'
Vue.directive('click-outside', {
bind: function (el, binding, vnode) {
el.event = function (event) {
// here I check that click was outside the el and his childrens
if (!(el === event.target || el.contains(event.target))) {
// and if it did, call method provided in attribute value
vnode.context[binding.expression](event)
}
}
document.body.addEventListener('click', el.event)
},
unbind: function (el) {
document.body.removeEventListener('click', el.event)
},
})
Vue.directive('autoresize', {
inserted: function (el) {
el.style.height = el.scrollHeight + 'px'
if (el.style.overflow && el.style.overflow.y) {
el.style.overflow.y = 'hidden'
}
el.style.resize = 'none'
function OnInput() {
this.style.height = 'auto'
this.style.height = this.scrollHeight + 'px'
this.scrollTop = this.scrollHeight
window.scrollTo(window.scrollLeft, this.scrollTop + this.scrollHeight)
}
el.addEventListener('input', OnInput, false)
},
})

View File

@ -1,133 +0,0 @@
import InputField from '../components/custom-fields/InputField.vue'
import SwitchField from '../components/custom-fields/SwitchField'
import TimeField from '../components/custom-fields/TimeField'
import DropdownField from '../components/custom-fields/DropdownField'
import DateTimeField from '../components/custom-fields/DateTimeField'
import DateField from '../components/custom-fields/DateField'
import TextAreaField from '../components/custom-fields/TextAreaField'
import UrlField from '../components/custom-fields/UrlField.vue'
import PhoneField from '../components/custom-fields/PhoneField.vue'
import NumberField from '../components/custom-fields/NumberField.vue'
import { mapActions } from 'vuex'
export default {
components: {
InputField,
SwitchField,
TimeField,
DropdownField,
DateTimeField,
DateField,
TextAreaField,
UrlField,
PhoneField,
NumberField,
},
data() {
return {
formData: {
customFields: [],
},
invalidFields: [],
customFields: [],
}
},
methods: {
...mapActions('customFields', ['fetchCustomFields']),
async setInitialCustomFields(type = null) {
let response = await this.fetchCustomFields({ type: type, limit: 'all' })
this.customFields = response.data.customFields.data.map((_f) => {
return { ..._f, cfid: _f.id }
})
this.setRemainingCustomFieldsValue()
},
setEditCustomFields(fields, customFields) {
this.customFields = fields.map((field) => {
field.label = field.custom_field.label
field.cfid = field.custom_field.id
field.options = field.custom_field.options
field.placeholder = field.custom_field.placeholder
field.is_required = field.custom_field.is_required
field.order = field.custom_field.order
return field
})
let currentCustomFieldIds = customFields.map((field) => field.id)
let editCustomFieldIds = fields.map((field) => field.custom_field_id)
let remainingCustomFieldIds = this.$utils.arrayDifference(
currentCustomFieldIds,
editCustomFieldIds
)
remainingCustomFieldIds.forEach((id) => {
let data = customFields.find((field) => field.id == id)
this.customFields.push({ ...data, cfid: data.id })
})
this.setRemainingCustomFieldsValue(true)
},
setCustomFieldValue(data) {
let position = this.formData.customFields.findIndex(
(field) => field.id == data.field.cfid
)
// check field has value so removed in invalidFields array
let indexInInvalidField = this.invalidFields.findIndex(
(field) => field.id == data.field.cfid
)
if (indexInInvalidField >= 0) {
if (data.value) {
this.invalidFields.splice(indexInInvalidField, 1)
}
}
// set data in formData.customFields
if (position >= 0) {
this.formData.customFields[position].value = data.value
return true
}
this.formData.customFields.push({
id: data.field.cfid,
value: data.value,
})
return true
},
setRemainingCustomFieldsValue() {
let existingCustomFieldIds = this.formData.customFields.map((_f) => _f.id)
let customFieldIds = this.customFields.map((_f) => _f.cfid)
let remainingCustomFieldIds = this.$utils.arrayDifference(
customFieldIds,
existingCustomFieldIds
)
remainingCustomFieldIds.forEach((id) => {
let field = this.customFields.find((field) => field.cfid == id)
this.formData.customFields.push({
id: field.cfid,
isRequired: field.is_required,
value: field.defaultAnswer,
})
})
this.customFields = _.sortBy(this.customFields, (_cf) => _cf.order)
},
getInvalidFields() {
return this.formData.customFields.filter(
(field) =>
field.isRequired &&
(field.value == null || field.value == undefined || field.value == '')
)
},
touchCustomField() {
return new Promise((resolve, reject) => {
try {
if (this.getInvalidFields() <= 0) {
resolve({ error: false })
}
this.invalidFields = this.getInvalidFields()
resolve({ error: true })
} catch (error) {
reject(error)
}
})
},
},
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,417 +0,0 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
/*
|--------------------------------------------------------------------------
| Views
|--------------------------------------------------------------------------|
*/
// Layouts
import LayoutBasic from './views/layouts/LayoutBasic.vue'
import LayoutLogin from './views/layouts/LayoutLogin.vue'
import LayoutWizard from './views/layouts/LayoutWizard.vue'
// Auth
import Login from './views/auth/Login.vue'
import ForgotPassword from './views/auth/ForgotPassword.vue'
import ResetPassword from './views/auth/ResetPassword.vue'
import Register from './views/auth/Register.vue'
import NotFoundPage from './views/errors/404.vue'
// Dashboard
import Dashboard from './views/dashboard/Dashboard.vue'
// Customers
import CustomerIndex from './views/customers/Index.vue'
import CustomerCreate from './views/customers/Create.vue'
import CustomerView from './views/customers/View.vue'
// Items
import ItemsIndex from './views/items/Index.vue'
import ItemCreate from './views/items/Create.vue'
// Invoices
import InvoiceIndex from './views/invoices/Index.vue'
import InvoiceCreate from './views/invoices/Create.vue'
import InvoiceView from './views/invoices/View.vue'
// Payments
import PaymentsIndex from './views/payments/Index.vue'
import PaymentCreate from './views/payments/Create.vue'
import PaymentView from './views/payments/View.vue'
// Estimates
import EstimateIndex from './views/estimates/Index.vue'
import EstimateCreate from './views/estimates/Create.vue'
import EstimateView from './views/estimates/View.vue'
// Expenses
import ExpensesIndex from './views/expenses/Index'
import ExpenseCreate from './views/expenses/Create.vue'
//User
import UserIndex from './views/users/Index.vue'
import UserCreate from './views/users/Create.vue'
// Report
import SalesReports from './views/reports/SalesReports'
import ExpensesReport from './views/reports/ExpensesReport'
import ProfitLossReport from './views/reports/ProfitLossReport'
import TaxReport from './views/reports/TaxReport.vue'
import ReportLayout from './views/reports/layout/Index.vue'
// Settings
import SettingsLayout from './views/settings/SettingsIndex.vue'
import CompanyInfo from './views/settings/CompanyInfoSetting.vue'
import Customization from './views/settings/CustomizationSetting.vue'
import Notifications from './views/settings/NotificationsSetting.vue'
import Preferences from './views/settings/PreferencesSetting.vue'
import UserProfile from './views/settings/UserProfileSetting.vue'
import TaxTypes from './views/settings/TaxTypesSetting.vue'
import NotesSetting from './views/settings/NotesSetting.vue'
import ExpenseCategory from './views/settings/ExpenseCategorySetting.vue'
import MailConfig from './views/settings/MailConfigSetting.vue'
import UpdateApp from './views/settings/UpdateAppSetting.vue'
import Backup from './views/settings/BackupSetting.vue'
import FileDisk from './views/settings/FileDiskSetting.vue'
import CustomFieldsIndex from './views/settings/CustomFieldsSetting.vue'
import PaymentMode from './views/settings/PaymentsModeSetting.vue'
import Wizard from './views/wizard/Wizard.vue'
Vue.use(VueRouter)
const routes = [
/*
|--------------------------------------------------------------------------
| Auth & Registration
|--------------------------------------------------------------------------|
*/
{
path: '/',
component: LayoutLogin,
meta: { redirectIfAuthenticated: true },
children: [
{
path: '/',
component: Login,
},
{
path: 'login',
component: Login,
name: 'login',
},
{
path: '/forgot-password',
component: ForgotPassword,
name: 'forgot-password',
},
{
path: '/reset-password/:token',
component: ResetPassword,
name: 'reset-password',
},
{
path: 'register',
component: Register,
name: 'register',
},
],
},
/*
|--------------------------------------------------------------------------
| Onboarding
|--------------------------------------------------------------------------|
*/
{
path: '/on-boarding',
component: LayoutWizard,
children: [
{
path: '/',
component: Wizard,
name: 'wizard',
},
],
},
/*
|--------------------------------------------------------------------------
| Admin
|--------------------------------------------------------------------------|
*/
{
path: '/admin',
component: LayoutBasic,
meta: { requiresAuth: true },
children: [
// Dashboard
{
path: '/',
component: Dashboard,
name: 'dashboard',
},
{
path: 'dashboard',
component: Dashboard,
},
// Customers
{
path: 'customers',
component: CustomerIndex,
},
{
path: 'customers/create',
name: 'customers.create',
component: CustomerCreate,
},
{
path: 'customers/:id/edit',
name: 'customers.edit',
component: CustomerCreate,
},
{
path: 'customers/:id/view',
name: 'customers.view',
component: CustomerView,
},
// Items
{
path: 'items',
component: ItemsIndex,
},
{
path: 'items/create',
name: 'items.create',
component: ItemCreate,
},
{
path: 'items/:id/edit',
name: 'items.edit',
component: ItemCreate,
},
// Estimates
{
path: 'estimates',
name: 'estimates.index',
component: EstimateIndex,
},
{
path: 'estimates/create',
name: 'estimates.create',
component: EstimateCreate,
},
{
path: 'estimates/:id/view',
name: 'estimates.view',
component: EstimateView,
},
{
path: 'estimates/:id/edit',
name: 'estimates.edit',
component: EstimateCreate,
},
// Invoices
{
path: 'invoices',
name: 'invoices.index',
component: InvoiceIndex,
},
{
path: 'invoices/create',
name: 'invoices.create',
component: InvoiceCreate,
},
{
path: 'invoices/:id/view',
name: 'invoices.view',
component: InvoiceView,
},
{
path: 'invoices/:id/edit',
name: 'invoices.edit',
component: InvoiceCreate,
},
// Payments
{
path: 'payments',
name: 'payments.index',
component: PaymentsIndex,
},
{
path: 'payments/create',
name: 'payments.create',
component: PaymentCreate,
},
{
path: 'payments/:id/create',
name: 'invoice.payments.create',
component: PaymentCreate,
},
{
path: 'payments/:id/edit',
name: 'payments.edit',
component: PaymentCreate,
},
{
path: 'payments/:id/view',
name: 'payments.view',
component: PaymentView,
},
// Expenses
{
path: 'expenses',
component: ExpensesIndex,
},
{
path: 'expenses/create',
name: 'expenses.create',
component: ExpenseCreate,
},
{
path: 'expenses/:id/edit',
name: 'expenses.edit',
component: ExpenseCreate,
},
// User
{
path: 'users',
component: UserIndex,
},
{
path: 'users/create',
name: 'users.create',
component: UserCreate,
},
{
path: 'users/:id/edit',
name: 'users.edit',
component: UserCreate,
},
// Reports
{
path: 'reports',
component: ReportLayout,
children: [
{
path: 'sales',
component: SalesReports,
},
{
path: 'expenses',
component: ExpensesReport,
},
{
path: 'profit-loss',
component: ProfitLossReport,
},
{
path: 'taxes',
component: TaxReport,
},
],
},
// Settings
{
path: 'settings',
component: SettingsLayout,
children: [
{
path: 'company-info',
name: 'company.info',
component: CompanyInfo,
},
{
path: 'customization',
name: 'customization',
component: Customization,
},
{
path: 'payment-mode',
name: 'payment.mode',
component: PaymentMode,
},
{
path: 'custom-fields',
name: 'custom.fields',
component: CustomFieldsIndex,
},
{
path: 'user-profile',
name: 'user.profile',
component: UserProfile,
},
{
path: 'preferences',
name: 'preferences',
component: Preferences,
},
{
path: 'tax-types',
name: 'tax.types',
component: TaxTypes,
},
{
path: 'notes',
name: 'notes',
component: NotesSetting,
},
{
path: 'expense-category',
name: 'expense.category',
component: ExpenseCategory,
},
{
path: 'mail-configuration',
name: 'mailconfig',
component: MailConfig,
},
{
path: 'notifications',
name: 'notifications',
component: Notifications,
},
{
path: 'update-app',
name: 'updateapp',
component: UpdateApp,
},
{
path: 'backup',
name: 'backup',
component: Backup,
},
{
path: 'file-disk',
name: 'file-disk',
component: FileDisk,
},
],
},
],
},
// DEFAULT ROUTE
{ path: '*', component: NotFoundPage },
]
const router = new VueRouter({
routes,
mode: 'history',
linkActiveClass: 'active',
})
export default router

View File

@ -1,146 +0,0 @@
import * as types from './mutation-types'
import * as userTypes from './modules/user/mutation-types'
import * as companyTypes from './modules/company/mutation-types'
export default {
bootstrap({ commit, dispatch, state }) {
return new Promise((resolve, reject) => {
window.axios
.get('/api/v1/bootstrap')
.then((response) => {
commit('user/' + userTypes.BOOTSTRAP_CURRENT_USER, response.data.user)
commit(
'company/' + companyTypes.SET_SELECTED_COMPANY,
response.data.company
)
commit('company/' + companyTypes.SET_DEFAULT_CURRENCY, response.data)
commit(
'user/' + userTypes.SET_DEFAULT_LANGUAGE,
response.data.default_language
)
commit(
'company/' + companyTypes.SET_MOMENT_DATE_FORMAT,
response.data.moment_date_format
)
commit(
'company/' + companyTypes.SET_CARBON_DATE_FORMAT,
response.data.carbon_date_format
)
commit(
'company/' + companyTypes.SET_DEFAULT_FISCAL_YEAR,
response.data.fiscal_year
)
commit(
'company/' + companyTypes.SET_DEFAULT_TIME_ZONE,
response.data.time_zone
)
commit(types.SET_CURRENCIES, response.data.currencies)
commit(types.SET_COUNTRIES, response.data.countries)
commit(types.UPDATE_APP_LOADING_STATUS, true)
resolve(response)
})
.catch((err) => {
reject(err)
})
})
},
fetchLanguages({ commit, dispatch, state }) {
return new Promise((resolve, reject) => {
window.axios
.get('/api/v1/languages')
.then((response) => {
commit(types.SET_LANGUAGES, response.data.languages)
resolve(response)
})
.catch((err) => {
reject(err)
})
})
},
fetchCurrencies({ commit, dispatch, state }) {
return new Promise((resolve, reject) => {
window.axios
.get('/api/v1/currencies')
.then((response) => {
commit(types.SET_CURRENCIES, response.data.currencies)
resolve(response)
})
.catch((err) => {
reject(err)
})
})
},
fetchDateFormats({ commit, dispatch, state }) {
return new Promise((resolve, reject) => {
window.axios
.get('/api/v1/date/formats')
.then((response) => {
commit(types.SET_DATE_FORMATS, response.data.date_formats)
resolve(response)
})
.catch((err) => {
reject(err)
})
})
},
fetchFiscalYears({ commit, dispatch, state }) {
return new Promise((resolve, reject) => {
window.axios
.get('/api/v1/fiscal/years')
.then((response) => {
commit(types.SET_FISCAL_YEARS, response.data.fiscal_years)
resolve(response)
})
.catch((err) => {
reject(err)
})
})
},
fetchTimeZones({ commit, dispatch, state }) {
return new Promise((resolve, reject) => {
window.axios
.get('/api/v1/timezones')
.then((response) => {
commit(types.SET_TIMEZONES, response.data.time_zones)
resolve(response)
})
.catch((err) => {
reject(err)
})
})
},
fetchCountries({ commit, dispatch, state }) {
return new Promise((resolve, reject) => {
window.axios
.get('/api/v1/countries')
.then((response) => {
commit(types.SET_COUNTRIES, response.data.countries)
resolve(response)
})
.catch((err) => {
reject(err)
})
})
},
toggleSidebar({ commit }) {
commit(types.TOGGLE_SIDEBAR)
},
}

View File

@ -1,15 +0,0 @@
export const isAppLoaded = (state) => state.isAppLoaded
export const languages = (state) => state.languages
export const currencies = (state) => state.currencies
export const timeZones = (state) => state.timeZones
export const dateFormats = (state) => state.dateFormats
export const fiscalYears = (state) => state.fiscalYears
export const countries = (state) => state.countries
export const isSidebarOpen = (state) => state.isSidebarOpen

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