Merge pull request #2215 from bengu-thon-mai-mochi/invoice-form-enhancements

Improve UI on add/edit items on invoice & bill pages
This commit is contained in:
Cüneyt Şentürk 2021-09-02 23:43:42 +03:00 committed by GitHub
commit 79de8ed409
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 122 additions and 124 deletions

View File

@ -2124,7 +2124,7 @@ button.bg-red:focus
/*--------Form Error Color--------*/ /*--------Form Error Color--------*/
/*--Required Sign Color--*/ /*--Required Sign Color--*/
div.required > .form-control-label:not(span):after, td.required:after div.required > .form-control-label:not(span):after, td.required:after, button.document-contact-without-contact-box-btn > span.text-add-contact::after
{ {
content: ' *'; content: ' *';
color: #ef3232; color: #ef3232;

View File

@ -9,7 +9,7 @@
<div class="aka-box-content"> <div class="aka-box-content">
<div class="document-contact-without-contact-box"> <div class="document-contact-without-contact-box">
<button type="button" class="btn-aka-link aka-btn--fluid document-contact-without-contact-box-btn" @click="onContactList"> <button type="button" class="btn-aka-link aka-btn--fluid document-contact-without-contact-box-btn" @click="onContactList">
<i class="far fa-user fa-2x"></i> &nbsp; {{ addContactText }} <i class="far fa-user fa-2x"></i> &nbsp; <span class="text-add-contact"> {{ addContactText }} </span>
</button> </button>
</div> </div>
</div> </div>
@ -198,7 +198,6 @@ export default {
default: () => [], default: () => [],
description: 'List of Contacts' description: 'List of Contacts'
}, },
addNew: { addNew: {
type: Object, type: Object,
default: function () { default: function () {

View File

@ -66,7 +66,7 @@ export default {
period: { period: {
type: [Number, String], type: [Number, String],
default: 0, default: "0",
description: "Payment period" description: "Payment period"
}, },
@ -145,7 +145,6 @@ export default {
if (this.model) { if (this.model) {
this.real_model = this.model; this.real_model = this.model;
} }
this.$emit('interface', this.real_model); this.$emit('interface', this.real_model);
}, },

View File

@ -1,7 +1,7 @@
<template> <template>
<div :id="'select-item-button-' + _uid" class="product-select"> <div :id="'select-item-button-' + _uid" class="product-select">
<div class="item-add-new"> <div class="item-add-new">
<button type="button" class="btn btn-link w-100" @click="onItemList"> <button type="button" class="btn btn-link w-100" @click="showItems">
<i class="fas fa-plus-circle"></i> &nbsp; {{ addItemText }} <i class="fas fa-plus-circle"></i> &nbsp; {{ addItemText }}
</button> </button>
</div> </div>
@ -21,19 +21,26 @@
type="text" type="text"
data-input="true" data-input="true"
class="form-control" class="form-control"
autocapitalize="default" autocorrect="ON" autocapitalize="default"
autocorrect="ON"
:placeholder="placeholder" :placeholder="placeholder"
:ref="'input-item-field-' + _uid"
v-model="search" v-model="search"
@input="onInput" @input="onInput"
@keydown.enter="onItemCreate" :ref="'input-item-field-' + _uid"
@keydown.enter="inputEnterEvent"
/> />
</div> </div>
</span> </span>
</div> </div>
<ul class="aka-select-menu-options"> <ul class="aka-select-menu-options">
<div class="aka-select-menu-option" v-for="(item, index) in sortedItems" :key="index" @click="onItemSeleted(index, item.id)"> <div
class="aka-select-menu-option"
v-for="(item, index) in sortedItems"
:key="index"
:class="isItemMatched ? 'highlightItem' : ''"
@click="onItemSelected(item)"
>
<div class="item-select w-100"> <div class="item-select w-100">
<div class="item-select-column item-select-info w-75"> <div class="item-select-column item-select-info w-75">
<b class="item-select-info-name"><span>{{ item.name }}</span></b> <b class="item-select-info-name"><span>{{ item.name }}</span></b>
@ -202,12 +209,14 @@ export default {
item_selected: false, item_selected: false,
item_list: false, item_list: false,
}, },
isItemMatched: false,
form: {}, form: {},
add_new: { add_new: {
text: this.addNew.text, text: this.addNew.text,
show: false, show: false,
buttons: this.addNew.buttons, buttons: this.addNew.buttons,
}, },
newItems: [],
add_new_html: '', add_new_html: '',
money: { money: {
decimal: this.currency.decimal_mark, decimal: this.currency.decimal_mark,
@ -220,6 +229,10 @@ export default {
}; };
}, },
created() {
this.setItemList(this.items);
},
mounted() { mounted() {
if (this.dynamicCurrency.code != this.currency.code) { if (this.dynamicCurrency.code != this.currency.code) {
if (!this.dynamicCurrency.decimal) { if (!this.dynamicCurrency.decimal) {
@ -241,6 +254,8 @@ export default {
setItemList(items) { setItemList(items) {
this.item_list = []; this.item_list = [];
this.search.length === 0 ? this.isItemMatched = false : {}
// Option set sort_option data // Option set sort_option data
if (!Array.isArray(items)) { if (!Array.isArray(items)) {
let index = 0; let index = 0;
@ -277,7 +292,7 @@ export default {
} }
}, },
onItemList() { showItems() {
this.show.item_list = true; this.show.item_list = true;
setTimeout(function() { setTimeout(function() {
@ -286,8 +301,10 @@ export default {
}, },
onInput() { onInput() {
this.isItemMatched = false;
//to optimize performance we kept the condition that checks for if search exists or not //to optimize performance we kept the condition that checks for if search exists or not
if (!this.search) { if (!this.search) {
this.isItemMatched = false; //to remove the style from matched item on input is cleared (option)
return; return;
} }
@ -300,61 +317,70 @@ export default {
return; return;
} }
window.axios.get(url + '/common/items?search="' + this.search + '" limit:10') this.fetchMatchedItems().then(() => this.item_list.length > 0 ? this.isItemMatched = true : this.isItemMatched = false );
.then(response => {
this.item_list = [];
let items = response.data.data;
items.forEach(function (item, index) {
this.item_list.push({
index: index,
key: item.id,
value: (item.title) ? item.title : (item.display_name) ? item.display_name : item.name,
type: this.type,
id: item.id,
name: (item.title) ? item.title : (item.display_name) ? item.display_name : item.name,
description: (item.description) ? item.description : '',
price: (item.price) ? item.price : (this.price == 'purchase_price') ? item.purchase_price : item.sale_price,
tax_ids: (item.tax_ids) ? item.tax_ids : [],
});
}, this);
})
.catch(error => {
});
this.$emit('input', this.search); this.$emit('input', this.search);
this.isItemMatched === true && this.search.length > 0 ? this.isItemMatched = true : this.isItemMatched = true;
}, },
onItemSeleted(index, item_id) { inputEnterEvent() {
let item = ''; this.isItemMatched
? this.onItemSelected()
: this.onItemCreate()
},
this.item_list.forEach(function (item_list, item_index) { async fetchMatchedItems() {
if (item_list.id == item_id) { await window.axios.get(url + '/common/items?search="' + this.search + '" limit:10')
item = item_list; .then(response => {
} this.item_list = [];
}); let items = response.data.data;
items.forEach(function (item, index) {
this.item_list.push({
index: index,
key: item.id,
value: (item.title) ? item.title : (item.display_name) ? item.display_name : item.name,
type: this.type,
id: item.id,
name: (item.title) ? item.title : (item.display_name) ? item.display_name : item.name,
description: (item.description) ? item.description : '',
price: (item.price) ? item.price : (this.price == 'purchase_price') ? item.purchase_price : item.sale_price,
tax_ids: (item.tax_ids) ? item.tax_ids : [],
});
}, this);
})
.catch(error => {});
},
onItemSelected(clickSelectedItem) {
let item;
const firstMatchedItem = this.item_list[0];
const isClickSelectedItem = clickSelectedItem ? true : false;
isClickSelectedItem ? item = clickSelectedItem : item = firstMatchedItem;
const indexeditem = { ...item, index: this.currentIndex };
this.addItem(indexeditem, 'oldItem');
},
addItem(item, itemType) {
this.selected_items.push(item); this.selected_items.push(item);
this.$emit('item', item); this.$emit('item', { item, itemType } );
this.$emit('items', this.selected_items); this.$emit('items', this.selected_items);
this.show.item_selected = false; this.show.item_selected = false;
this.show.item_list = false; this.show.item_list = false;
this.search = ''; this.search = '';
// Set deault item list // Set default item list
this.setItemList(this.items); this.setItemList(this.items);
}, },
onItemCreate() { onItemCreate() {
let index = Object.keys(this.item_list).length;
index++;
let item = { let item = {
index: index, index: this.currentIndex,
key: 0, key: 0,
value: this.search, value: this.search,
type: this.type, type: this.type,
@ -364,68 +390,10 @@ export default {
price: 0, price: 0,
tax_ids: [], tax_ids: [],
}; };
this.newItems.push(item);
this.selected_items.push(item); this.addItem(item, 'newItem');
this.$emit('item', item);
this.$emit('items', this.selected_items);
this.setItemList(this.items);
this.show.item_selected = false;
this.show.item_list = false;
this.search = '';
/*
let add_new = this.add_new;
window.axios.get(this.createRoute)
.then(response => {
add_new.show = true;
add_new.html = response.data.html;
this.add_new_html = Vue.component('add-new-component', function (resolve, reject) {
resolve({
template: '<div><akaunting-modal-add-new :show="add_new.show" @submit="onSubmit" @cancel="onCancel" :buttons="add_new.buttons" :title="add_new.text" :is_component=true :message="add_new.html"></akaunting-modal-add-new></div>',
components: {
[Select.name]: Select,
[Option.name]: Option,
[OptionGroup.name]: OptionGroup,
[ColorPicker.name]: ColorPicker,
AkauntingModalAddNew,
AkauntingModal,
AkauntingMoney,
AkauntingRadioGroup,
AkauntingSelect,
AkauntingDate,
},
data: function () {
return {
add_new: add_new,
}
},
methods: {
onSubmit(event) {
this.$emit('submit', event);
},
onCancel(event) {
this.$emit('cancel', event);
}
}
})
});
})
.catch(e => {
console.log(e);
})
.finally(function () {
// always executed
});
*/
}, },
onSubmit(event) { onSubmit(event) {
@ -552,14 +520,13 @@ export default {
}, },
}, },
created() {
this.setItemList(this.items);
},
computed: { computed: {
sortedItems() { sortedItems() {
return this.sortItems(); return this.sortItems();
}, },
currentIndex() {
return this.$root.form.items.length;
},
}, },
watch: { watch: {
@ -589,3 +556,9 @@ export default {
}, },
}; };
</script> </script>
<style scoped>
.highlightItem:first-child {
background-color: #F5F7FA;
}
</style>

View File

@ -15,4 +15,12 @@ function getQueryVariable(variable) {
return(false); return(false);
} }
export {getQueryVariable} //This function wraps setTimeout function in a promise in order to display dom manipulations on root components asynchronously & fast
const setPromiseTimeout = time =>
new Promise(resolve =>
setTimeout(() =>
resolve(time)
, time)
);
export {getQueryVariable, setPromiseTimeout}

View File

@ -9,6 +9,7 @@ require('./../../bootstrap');
import Vue from 'vue'; import Vue from 'vue';
import DashboardPlugin from './../../plugins/dashboard-plugin'; import DashboardPlugin from './../../plugins/dashboard-plugin';
import { setPromiseTimeout } from './../../plugins/functions';
import Global from './../../mixins/global'; import Global from './../../mixins/global';
@ -48,8 +49,8 @@ const app = new Vue({
tax: false, tax: false,
discounts: [], discounts: [],
tax_id: [], tax_id: [],
items: [], items: [],
selected_items:[],
taxes: [], taxes: [],
page_loaded: false, page_loaded: false,
currencies: [], currencies: [],
@ -92,11 +93,16 @@ const app = new Vue({
} }
this.currency_symbol.symbol = default_currency_symbol; this.currency_symbol.symbol = default_currency_symbol;
} };
}, },
methods: { methods: {
onRefFocus(ref) {
let index = this.form.items.length - 1;
this.$refs['items-' + index + '-' + ref][0].focus();
},
onCalculateTotal() { onCalculateTotal() {
let global_discount = parseFloat(this.form.discount); let global_discount = parseFloat(this.form.discount);
let discount_total = 0; let discount_total = 0;
@ -302,11 +308,17 @@ const app = new Vue({
return totals_taxes; return totals_taxes;
}, },
// Select Item added form onSelectedItem(item){
onSelectedItem(item) { this.onAddItem(item);
},
// addItem to list
onAddItem(payload) {
let { item, itemType } = payload;
let inputRef = `${itemType === 'newItem' ? 'name' : 'description'}`; // indication for which input to focus first
let total = 1 * item.price; let total = 1 * item.price;
let item_taxes = []; let item_taxes = [];
if (item.tax_ids) { if (item.tax_ids) {
item.tax_ids.forEach(function (tax_id, index) { item.tax_ids.forEach(function (tax_id, index) {
if (this.taxes.includes(tax_id)) { if (this.taxes.includes(tax_id)) {
@ -351,6 +363,10 @@ const app = new Vue({
// invoice_item_checkbox_sample: [], // invoice_item_checkbox_sample: [],
}); });
setTimeout(function() {
this.onRefFocus(inputRef);
}.bind(this), 100);
setTimeout(function() { setTimeout(function() {
this.onCalculateTotal(); this.onCalculateTotal();
}.bind(this), 800); }.bind(this), 800);
@ -499,6 +515,7 @@ const app = new Vue({
methods: { methods: {
onSubmit(event) { onSubmit(event) {
this.form = event; this.form = event;
this.form.response = {}; this.form.response = {};
this.loading = true; this.loading = true;

View File

@ -1,4 +1,3 @@
<div class="card"> <div class="card">
<div class="card-footer"> <div class="card-footer">
<div class="row save-buttons"> <div class="row save-buttons">

View File

@ -32,7 +32,9 @@
<span class="aka-text aka-text--body" tabindex="0" v-html="row.name" v-if="row.item_id"></span> <span class="aka-text aka-text--body" tabindex="0" v-html="row.name" v-if="row.item_id"></span>
<div v-else> <div v-else>
@stack('name_input_start') @stack('name_input_start')
<input type="text" <input
type="text"
:ref="'items-' + index + '-name'"
class="form-control" class="form-control"
:name="'items.' + index + '.name'" :name="'items.' + index + '.name'"
autocomplete="off" autocomplete="off"
@ -40,8 +42,7 @@
data-item="name" data-item="name"
v-model="row.name" v-model="row.name"
@input="onBindingItemField(index, 'name')" @input="onBindingItemField(index, 'name')"
@change="form.errors.clear('items.' + index + '.name')"> @change="form.errors.clear('items.' + index + '.name')"/>
<div class="invalid-feedback d-block" <div class="invalid-feedback d-block"
v-if="form.errors.has('items.' + index + '.name')" v-if="form.errors.has('items.' + index + '.name')"
v-html="form.errors.get('items.' + index + '.name')"> v-html="form.errors.get('items.' + index + '.name')">
@ -57,6 +58,7 @@
@if (!$hideDescription) @if (!$hideDescription)
<textarea <textarea
class="form-control" class="form-control"
:ref="'items-' + index + '-description'"
placeholder="{{ trans('items.enter_item_description') }}" placeholder="{{ trans('items.enter_item_description') }}"
style="height: 46px; overflow: hidden;" style="height: 46px; overflow: hidden;"
:name="'items.' + index + '.description'" :name="'items.' + index + '.description'"
@ -79,6 +81,7 @@
<input <input
type="number" type="number"
min="0" min="0"
:ref="'items-' + index + '-quantity'"
class="form-control text-center p-0 input-number-disabled" class="form-control text-center p-0 input-number-disabled"
:name="'items.' + index + '.quantity'" :name="'items.' + index + '.quantity'"
autocomplete="off" autocomplete="off"
@ -87,7 +90,6 @@
v-model="row.quantity" v-model="row.quantity"
@input="onCalculateTotal" @input="onCalculateTotal"
@change="form.errors.clear('items.' + index + '.quantity')"> @change="form.errors.clear('items.' + index + '.quantity')">
<div class="invalid-feedback d-block" <div class="invalid-feedback d-block"
v-if="form.errors.has('items.' + index + '.quantity')" v-if="form.errors.has('items.' + index + '.quantity')"
v-html="form.errors.get('items.' + index + '.quantity')"> v-html="form.errors.get('items.' + index + '.quantity')">

View File

@ -74,6 +74,7 @@
@if (isset($attributes['row-input'])) @if (isset($attributes['row-input']))
:row-input="{{ $attributes['row-input'] }}" :row-input="{{ $attributes['row-input'] }}"
@endif @endif
></akaunting-money> ></akaunting-money>
@stack($name . '_input_end') @stack($name . '_input_end')