mirror of
https://github.com/frappe/books.git
synced 2024-11-08 14:50:56 +00:00
feat: Add Make Payment button
- Generic Dropdown component
This commit is contained in:
parent
c848ce6812
commit
36db289c6e
@ -111,6 +111,21 @@ module.exports = {
|
||||
}
|
||||
],
|
||||
|
||||
quickEditFields: [
|
||||
'party',
|
||||
'date',
|
||||
'account',
|
||||
'paymentType',
|
||||
'paymentAccount',
|
||||
'paymentMethod',
|
||||
'referenceId',
|
||||
'referenceDate',
|
||||
'clearanceDate',
|
||||
'amount',
|
||||
'writeoff',
|
||||
'for'
|
||||
],
|
||||
|
||||
layout: [
|
||||
{
|
||||
columns: [
|
||||
|
@ -4,6 +4,11 @@ module.exports = {
|
||||
isSingle: 0,
|
||||
isChild: 1,
|
||||
keywordFields: [],
|
||||
tableFields: [
|
||||
'referenceType',
|
||||
'referenceName',
|
||||
'amount'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'referenceType',
|
||||
|
@ -1,51 +1,38 @@
|
||||
<template>
|
||||
<div class="relative" v-on-outside-click="() => showDropdown = false">
|
||||
<input
|
||||
ref="input"
|
||||
:class="inputClasses"
|
||||
type="text"
|
||||
:value="linkValue"
|
||||
:placeholder="inputPlaceholder"
|
||||
:readonly="df.readOnly"
|
||||
@focus="onFocus"
|
||||
@input="onInput"
|
||||
@keydown.up="highlightItemUp"
|
||||
@keydown.down="highlightItemDown"
|
||||
@keydown.enter="selectHighlightedItem"
|
||||
@keydown.tab="showDropdown = false"
|
||||
@keydown.esc="showDropdown = false"
|
||||
/>
|
||||
<div
|
||||
class="mt-1 absolute left-0 z-10 bg-white rounded-5px border w-full min-w-56"
|
||||
v-if="showDropdown"
|
||||
<Dropdown :items="suggestions">
|
||||
<template
|
||||
v-slot="{ toggleDropdown, highlightItemUp, highlightItemDown, selectHighlightedItem }"
|
||||
>
|
||||
<div class="p-1 max-h-64 overflow-auto" v-if="suggestions.length">
|
||||
<a
|
||||
ref="suggestionItems"
|
||||
class="block p-2 rounded mt-1 first:mt-0 cursor-pointer"
|
||||
v-for="(s, index) in suggestions"
|
||||
:key="s.value"
|
||||
:class="index === highlightedIndex ? 'bg-gray-200' : ''"
|
||||
@mouseenter="highlightedIndex = index"
|
||||
@mouseleave="highlightedIndex = -1"
|
||||
@click="selectItem(s)"
|
||||
>
|
||||
<component :is="s.component" v-if="s.component" />
|
||||
<template v-else>{{ s.label }}</template>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref="input"
|
||||
:class="inputClasses"
|
||||
type="text"
|
||||
:value="linkValue"
|
||||
:placeholder="inputPlaceholder"
|
||||
:readonly="df.readOnly"
|
||||
@focus="e => onFocus(e, toggleDropdown)"
|
||||
@input="onInput"
|
||||
@keydown.up="highlightItemUp"
|
||||
@keydown.down="highlightItemDown"
|
||||
@keydown.enter="selectHighlightedItem"
|
||||
@keydown.tab="toggleDropdown(false)"
|
||||
@keydown.esc="toggleDropdown(false)"
|
||||
/>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import Base from './Base';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
|
||||
export default {
|
||||
name: 'AutoComplete',
|
||||
extends: Base,
|
||||
components: {},
|
||||
components: {
|
||||
Dropdown
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
linkValue: '',
|
||||
@ -58,7 +45,7 @@ export default {
|
||||
value: {
|
||||
immediate: true,
|
||||
handler(newValue) {
|
||||
this.linkValue = this.value;
|
||||
this.linkValue = newValue;
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -70,7 +57,13 @@ export default {
|
||||
keyword = e.target.value;
|
||||
this.linkValue = keyword;
|
||||
}
|
||||
this.suggestions = await this.getSuggestions(keyword);
|
||||
let suggestions = await this.getSuggestions(keyword);
|
||||
this.suggestions = suggestions.map(d => {
|
||||
if (!d.action) {
|
||||
d.action = () => this.setSuggestion(d);
|
||||
}
|
||||
return d;
|
||||
});
|
||||
},
|
||||
async getSuggestions(keyword = '') {
|
||||
keyword = keyword.toLowerCase();
|
||||
@ -93,57 +86,19 @@ export default {
|
||||
);
|
||||
});
|
||||
},
|
||||
selectHighlightedItem() {
|
||||
if (![-1, this.suggestions.length].includes(this.highlightedIndex)) {
|
||||
// valid selection
|
||||
let suggestion = this.suggestions[this.highlightedIndex];
|
||||
this.setSuggestion(suggestion);
|
||||
}
|
||||
},
|
||||
selectItem(suggestion) {
|
||||
if (suggestion.action) {
|
||||
suggestion.action();
|
||||
return;
|
||||
}
|
||||
this.setSuggestion(suggestion);
|
||||
},
|
||||
setSuggestion(suggestion) {
|
||||
this.linkValue = suggestion.value;
|
||||
this.triggerChange(suggestion.value);
|
||||
this.showDropdown = false;
|
||||
this.toggleDropdown(false);
|
||||
},
|
||||
onFocus() {
|
||||
this.showDropdown = true;
|
||||
onFocus(e, toggleDropdown) {
|
||||
this.toggleDropdown = toggleDropdown;
|
||||
this.toggleDropdown(true);
|
||||
this.updateSuggestions();
|
||||
},
|
||||
onInput(e) {
|
||||
this.showDropdown = true;
|
||||
this.toggleDropdown(true);
|
||||
this.updateSuggestions(e);
|
||||
},
|
||||
highlightItemUp() {
|
||||
this.highlightedIndex -= 1;
|
||||
if (this.highlightedIndex < 0) {
|
||||
this.highlightedIndex = 0;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
let index = this.highlightedIndex;
|
||||
if (index !== 0) {
|
||||
index -= 1;
|
||||
}
|
||||
let highlightedElement = this.$refs.suggestionItems[index];
|
||||
highlightedElement && highlightedElement.scrollIntoView();
|
||||
});
|
||||
},
|
||||
highlightItemDown() {
|
||||
this.highlightedIndex += 1;
|
||||
if (this.highlightedIndex > this.suggestions.length) {
|
||||
this.highlightedIndex = this.suggestions.length;
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
let index = this.highlightedIndex;
|
||||
let highlightedElement = this.$refs.suggestionItems[index];
|
||||
highlightedElement && highlightedElement.scrollIntoView();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,16 +1,11 @@
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import Base from './Base';
|
||||
import AutoComplete from './AutoComplete';
|
||||
import Badge from '@/components/Badge';
|
||||
|
||||
export default {
|
||||
name: 'Link',
|
||||
extends: AutoComplete,
|
||||
components: {
|
||||
Badge
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
async getSuggestions(keyword = '') {
|
||||
let doctype = this.df.target;
|
||||
|
102
src/components/Dropdown.vue
Normal file
102
src/components/Dropdown.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="relative" v-on-outside-click="() => isShown = false">
|
||||
<div>
|
||||
<slot
|
||||
:toggleDropdown="toggleDropdown"
|
||||
:highlightItemUp="highlightItemUp"
|
||||
:highlightItemDown="highlightItemDown"
|
||||
:selectHighlightedItem="selectHighlightedItem"
|
||||
></slot>
|
||||
</div>
|
||||
<div
|
||||
:class="right ? 'right-0' : 'left-0'"
|
||||
class="mt-1 absolute z-10 bg-white rounded-5px border w-full min-w-56"
|
||||
v-if="isShown"
|
||||
>
|
||||
<div class="p-1 max-h-64 overflow-auto">
|
||||
<a
|
||||
ref="items"
|
||||
class="block p-2 rounded mt-1 first:mt-0 cursor-pointer whitespace-no-wrap text-sm"
|
||||
v-for="(d, index) in items"
|
||||
:key="d.label"
|
||||
:class="index === highlightedIndex ? 'bg-gray-100' : ''"
|
||||
@mouseenter="highlightedIndex = index"
|
||||
@mouseleave="highlightedIndex = -1"
|
||||
@click="selectItem(d)"
|
||||
>
|
||||
<component :is="d.component" v-if="d.component" />
|
||||
<template v-else>{{ d.label }}</template>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Dropdown',
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
right: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShown: false,
|
||||
highlightedIndex: -1
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
selectItem(d) {
|
||||
if (d.action) {
|
||||
d.action();
|
||||
}
|
||||
},
|
||||
toggleDropdown(flag) {
|
||||
if (flag == null) {
|
||||
this.isShown = !this.isShown;
|
||||
} else {
|
||||
this.isShown = Boolean(flag);
|
||||
}
|
||||
},
|
||||
selectHighlightedItem() {
|
||||
if (![-1, this.items.length].includes(this.highlightedIndex)) {
|
||||
// valid selection
|
||||
let item = this.items[this.highlightedIndex];
|
||||
this.selectItem(item);
|
||||
}
|
||||
},
|
||||
highlightItemUp() {
|
||||
this.highlightedIndex -= 1;
|
||||
if (this.highlightedIndex < 0) {
|
||||
this.highlightedIndex = 0;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
let index = this.highlightedIndex;
|
||||
if (index !== 0) {
|
||||
index -= 1;
|
||||
}
|
||||
let highlightedElement = this.$refs.items[index];
|
||||
highlightedElement && highlightedElement.scrollIntoView();
|
||||
});
|
||||
},
|
||||
highlightItemDown() {
|
||||
this.highlightedIndex += 1;
|
||||
if (this.highlightedIndex > this.items.length) {
|
||||
this.highlightedIndex = this.items.length;
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
let index = this.highlightedIndex;
|
||||
let highlightedElement = this.$refs.items[index];
|
||||
highlightedElement && highlightedElement.scrollIntoView();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
9
src/components/Icons/DotHorizontal.vue
Normal file
9
src/components/Icons/DotHorizontal.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<svg class="w-3 h-3" viewBox="0 0 8 2" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.633 1.033a.833.833 0 11-1.666 0 .833.833 0 011.666 0zm3.2 0a.833.833 0 11-1.666 0 .833.833 0 011.666 0zm3.2 0a.833.833 0 11-1.666 0 .833.833 0 011.666 0z"
|
||||
fill="#112B42"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
@ -4,6 +4,16 @@
|
||||
<a class="cursor-pointer font-semibold" slot="title" @click="$router.go(-1)">{{ _('Back') }}</a>
|
||||
<template slot="actions">
|
||||
<Button class="text-gray-900 text-xs">{{ _('Customise') }}</Button>
|
||||
<Dropdown
|
||||
:items="[{label: 'Make Payment', value: 'Make Payment', action: makePayment }]"
|
||||
right
|
||||
>
|
||||
<template v-slot="{ toggleDropdown }">
|
||||
<Button class="text-gray-900 text-xs ml-2" :icon="true" @click="toggleDropdown()">
|
||||
<DotHorizontalIcon />
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<Button
|
||||
v-if="doc._notInserted || doc._dirty"
|
||||
type="primary"
|
||||
@ -118,7 +128,9 @@ import PageHeader from '@/components/PageHeader';
|
||||
import Button from '@/components/Button';
|
||||
import FormControl from '@/components/Controls/FormControl';
|
||||
import Row from '@/components/Row';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import AddIcon from '@/components/Icons/Add';
|
||||
import DotHorizontalIcon from '@/components/Icons/DotHorizontal';
|
||||
|
||||
export default {
|
||||
name: 'InvoiceForm',
|
||||
@ -128,7 +140,9 @@ export default {
|
||||
Button,
|
||||
FormControl,
|
||||
Row,
|
||||
AddIcon
|
||||
AddIcon,
|
||||
DotHorizontalIcon,
|
||||
Dropdown
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
@ -158,7 +172,7 @@ export default {
|
||||
PurchaseInvoice: 'supplier'
|
||||
}[this.doctype];
|
||||
return this.meta.getField(fieldname);
|
||||
},
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.meta = frappe.getMeta(this.doctype);
|
||||
@ -181,6 +195,33 @@ export default {
|
||||
},
|
||||
async onSubmitClick() {
|
||||
await this.doc.submit();
|
||||
},
|
||||
async makePayment() {
|
||||
let payment = await frappe.getNewDoc('Payment');
|
||||
payment.once('afterInsert', () => {
|
||||
payment.submit();
|
||||
});
|
||||
this.$router.push({
|
||||
query: {
|
||||
edit: 1,
|
||||
doctype: 'Payment',
|
||||
name: payment.name,
|
||||
hideFields: ['party', 'date', 'account', 'paymentType', 'for'],
|
||||
values: {
|
||||
party: this.doc.customer,
|
||||
account: this.doc.account,
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
paymentType: 'Receive',
|
||||
for: [
|
||||
{
|
||||
referenceType: 'SalesInvoice',
|
||||
referenceName: this.doc.name,
|
||||
amount: this.doc.outstandingAmount
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,10 +1,16 @@
|
||||
<template>
|
||||
<div class="border-l h-full">
|
||||
<div class="flex justify-end px-4 pt-4">
|
||||
<Button :icon="true" @click="routeToList">
|
||||
<Button :icon="true" @click="$router.back()">
|
||||
<XIcon class="w-3 h-3 stroke-current text-gray-700" />
|
||||
</Button>
|
||||
<Button :icon="true" @click="insertDoc" type="primary" v-if="doc._notInserted" class="ml-2 flex">
|
||||
<Button
|
||||
:icon="true"
|
||||
@click="insertDoc"
|
||||
type="primary"
|
||||
v-if="doc._notInserted"
|
||||
class="ml-2 flex"
|
||||
>
|
||||
<feather-icon name="check" class="text-white" />
|
||||
</Button>
|
||||
</div>
|
||||
@ -53,7 +59,7 @@ import FormControl from '@/components/Controls/FormControl';
|
||||
|
||||
export default {
|
||||
name: 'QuickEditForm',
|
||||
props: ['doctype', 'name', 'values'],
|
||||
props: ['doctype', 'name', 'values', 'hideFields'],
|
||||
components: {
|
||||
Button,
|
||||
XIcon,
|
||||
@ -80,20 +86,26 @@ export default {
|
||||
methods: {
|
||||
async fetchMetaAndDoc() {
|
||||
this.meta = frappe.getMeta(this.doctype);
|
||||
this.fields = this.meta.getQuickEditFields();
|
||||
this.fields = this.meta
|
||||
.getQuickEditFields()
|
||||
.filter(df => !(this.hideFields || []).includes(df.fieldname));
|
||||
this.titleDocField = this.meta.getField(this.meta.titleField);
|
||||
await this.fetchDoc();
|
||||
|
||||
// setup the title field
|
||||
if (this.doc._notInserted) {
|
||||
if (
|
||||
this.doc._notInserted &&
|
||||
!this.titleDocField.readOnly &&
|
||||
this.doc[this.titleDocField.fieldname]
|
||||
) {
|
||||
this.doc.set(this.titleDocField.fieldname, '');
|
||||
setTimeout(() => {
|
||||
this.$refs.titleControl.focus();
|
||||
}, 300);
|
||||
}
|
||||
if (this.values) {
|
||||
this.doc.set(this.values);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.$refs.titleControl.focus();
|
||||
}, 300);
|
||||
},
|
||||
valueChange(df, value) {
|
||||
if (!value) return;
|
||||
|
@ -10,6 +10,9 @@ module.exports = {
|
||||
minWidth: {
|
||||
'56': '14rem'
|
||||
},
|
||||
maxWidth: {
|
||||
'56': '14rem'
|
||||
},
|
||||
spacing: {
|
||||
'14': '3.5rem',
|
||||
'72': '18rem',
|
||||
|
Loading…
Reference in New Issue
Block a user