mirror of
https://github.com/mokuappio/serverless-invoices.git
synced 2025-10-28 00:11:08 -04:00
Separated client form into tabs. Fixed deleting client. Implemented custom fields for client and invoice client fields. Removed "hardcoded" client_reg_no and client_vat_no fields from client and invoice model - can be replaced with custom fields. Abstracted invoice-rows management to separate store namespace. Invoice client fields are prefilled from selected client.
This commit is contained in:
75
src/assets/scss/_tabs.scss
Normal file
75
src/assets/scss/_tabs.scss
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
.nav-tabs {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
// scrollbar styles
|
||||||
|
scrollbar-color: var(--text-caption);
|
||||||
|
scrollbar-width: 6px;
|
||||||
|
scrollbar-gutter: always;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: var(--shade);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--text-caption);
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> li {
|
||||||
|
display: inline-block;
|
||||||
|
float: none;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--simple {
|
||||||
|
&.nav-tabs {
|
||||||
|
> li {
|
||||||
|
> a {
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: transparent;
|
||||||
|
border-left-color: transparent;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-right-color: transparent;
|
||||||
|
border-bottom-color: var(--text-primary);
|
||||||
|
color: $nav-tabs-link-active-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:focus, &:active {
|
||||||
|
background-color: transparent;
|
||||||
|
border-left-color: transparent;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-right-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> li {
|
||||||
|
> a {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
min-width: 90px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $nav-tabs-link-active-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@
|
|||||||
@import "snackbars";
|
@import "snackbars";
|
||||||
@import "surfaces";
|
@import "surfaces";
|
||||||
@import "tables";
|
@import "tables";
|
||||||
|
@import "tabs";
|
||||||
@import "transitions";
|
@import "transitions";
|
||||||
@import "type";
|
@import "type";
|
||||||
@import "utilities";
|
@import "utilities";
|
||||||
|
|||||||
70
src/components/clients/ClientFields.vue
Normal file
70
src/components/clients/ClientFields.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-for="field in client.fields" :key="field.id" class="col-sm-6">
|
||||||
|
<AppEditable :value="field.label"
|
||||||
|
placeholder="Label"
|
||||||
|
@change="updateFieldProp({ label: $event }, field)"/>
|
||||||
|
<i class="material-icons md-18 float-right pointer" @click="removeField(field)">close</i>
|
||||||
|
<AppInput :value="field.value" @change="updateFieldProp({ value: $event }, field)"
|
||||||
|
:placeholder="field.label"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<button class="btn btn-sm btn-secondary" @click="addNewField">
|
||||||
|
<i class="material-icons md-18">add</i>
|
||||||
|
Field
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import NotificationService from '@/services/notification.service';
|
||||||
|
import AppInput from '@/components/form/AppInput';
|
||||||
|
import AppEditable from '@/components/form/AppEditable';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['client'],
|
||||||
|
components: {
|
||||||
|
AppEditable,
|
||||||
|
AppInput,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isNew() {
|
||||||
|
return this.client && this.client.$isNew;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addNewField() {
|
||||||
|
this.$store.dispatch('clientFields/addNewField', this.client.id);
|
||||||
|
},
|
||||||
|
async removeField(field) {
|
||||||
|
const confirmed = await this.$bvModal.msgBoxConfirm(`Delete field ${field.label}?`, {
|
||||||
|
okTitle: 'Delete',
|
||||||
|
okVariant: 'danger',
|
||||||
|
cancelTitle: 'Dismiss',
|
||||||
|
cancelVariant: 'btn-link',
|
||||||
|
contentClass: 'bg-base dp--24',
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
await this.$store.dispatch('clientFields/deleteClientField', field.id);
|
||||||
|
try {
|
||||||
|
NotificationService.success('Deleted');
|
||||||
|
} catch (err) {
|
||||||
|
NotificationService.error(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateFieldProp(props, field) {
|
||||||
|
if (this.isNew) {
|
||||||
|
return this.$store.dispatch('clientFields/clientFieldProps', {
|
||||||
|
props,
|
||||||
|
fieldId: field.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.$store.dispatch('clientFields/updateClientField', {
|
||||||
|
props,
|
||||||
|
fieldId: field.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -1,94 +1,103 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12 d-flex justify-content-between">
|
||||||
<h4>Client</h4>
|
<h4>Client</h4>
|
||||||
</div>
|
<div v-if="client">
|
||||||
</div>
|
<div v-if="!isNew">
|
||||||
|
<b-dropdown variant="link" size="sm" no-caret right>
|
||||||
<div v-if="client" class="row">
|
<template slot="button-content">
|
||||||
<div class="col-12">
|
<i class="material-icons">more_vert</i>
|
||||||
<h5>General</h5>
|
</template>
|
||||||
</div>
|
<b-dropdown-item-button @click="deleteClient">Delete</b-dropdown-item-button>
|
||||||
<AppInput :value="client.company_name" @change="updateProp({ company_name: $event })"
|
</b-dropdown>
|
||||||
label="Company Name" field="company_name" :errors="errors" class="col-12"/>
|
<button class="btn btn-sm btn-primary"
|
||||||
<AppInput :value="client.invoice_email" @change="updateProp({ invoice_email: $event })"
|
@click="$emit('done')">Done
|
||||||
label="Email" field="invoice_email" :errors="errors" class="col-sm-7"/>
|
</button>
|
||||||
<AppInput :value="client.company_reg_no" @change="updateProp({ company_reg_no: $event })"
|
</div>
|
||||||
label="Company reg no" field="company_reg_no" :errors="errors" class="col-sm-5"/>
|
<button v-else class="btn btn-primary ml-2"
|
||||||
|
:disabled="loading"
|
||||||
<div class="col-12">
|
@click="createClient">Create
|
||||||
<h5>Invoice Settings</h5>
|
|
||||||
<h6>Address</h6>
|
|
||||||
</div>
|
|
||||||
<AppInput :value="client.company_address" @change="updateProp({ company_address: $event })"
|
|
||||||
label="Company Address" field="company_address" :errors="errors"
|
|
||||||
class="col-12"/>
|
|
||||||
<AppInput :value="client.company_postal_code"
|
|
||||||
@change="updateProp({ company_postal_code: $event })"
|
|
||||||
label="Postal code" field="company_postal_code" :errors="errors"
|
|
||||||
class="col-sm-5"/>
|
|
||||||
<AppInput :value="client.company_city" @change="updateProp({ company_city: $event })"
|
|
||||||
label="City" field="company_city" :errors="errors" class="col-sm-7"/>
|
|
||||||
<AppInput :value="client.company_county" @change="updateProp({ company_county: $event })"
|
|
||||||
label="County/State" field="company_county" :errors="errors" class="col-sm-6"/>
|
|
||||||
<AppInput :value="client.company_country" @change="updateProp({ company_country: $event })"
|
|
||||||
label="Country" field="company_country" :errors="errors" class="col-sm-6"/>
|
|
||||||
<AppInput :value="client.currency" @change="updateProp({ currency: $event })"
|
|
||||||
label="Currency" field="currency" :errors="errors" class="col-sm-4"/>
|
|
||||||
<AppInput :value="client.rate" @change="updateProp({ rate: $event })"
|
|
||||||
label="Hourly rate" field="rate" :errors="errors" class="col-sm-4"/>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<h6>VAT</h6>
|
|
||||||
</div>
|
|
||||||
<AppInput :value="client.company_vat_no" @change="updateProp({ company_vat_no: $event })"
|
|
||||||
label="Company VAT no" field="company_vat_no" :errors="errors" class="col-sm-8"/>
|
|
||||||
<AppCheckbox :value="client.has_vat" @input="updateProp({ has_vat: $event })"
|
|
||||||
label="Apply VAT" field="has_vat" :errors="errors" class="col-sm-4"/>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<h6>Banking details</h6>
|
|
||||||
</div>
|
|
||||||
<AppSelect :value="client.bank_account"
|
|
||||||
track-by="id"
|
|
||||||
label="Bank account"
|
|
||||||
label-field="bank_name"
|
|
||||||
:options="bankAccounts || []"
|
|
||||||
@input="bankAccountChanged"
|
|
||||||
class="col-12"/>
|
|
||||||
</div>
|
|
||||||
<div v-if="!client">Loading</div>
|
|
||||||
|
|
||||||
<div class="row mt-3 text-right" v-if="client">
|
|
||||||
<div class="col-12">
|
|
||||||
<div v-if="!isNew">
|
|
||||||
<button class="btn btn-outline-danger mr-2" @click="deleteClient(client.id)">Delete</button>
|
|
||||||
<button class="btn btn-primary"
|
|
||||||
@click="$emit('done')">Done
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button v-else class="btn btn-primary ml-2"
|
|
||||||
:disabled="loading"
|
|
||||||
@click="createClient">Create
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<b-tabs v-if="client" nav-class="nav-tabs--simple mb-4" active-tab-class="active" class="row">
|
||||||
|
<b-tab title="General" class="col-12">
|
||||||
|
<div class="row">
|
||||||
|
<AppInput :value="client.company_name" @change="updateProp({ company_name: $event })"
|
||||||
|
label="Company Name" field="company_name" :errors="errors" class="col-12"/>
|
||||||
|
<AppInput :value="client.invoice_email" @change="updateProp({ invoice_email: $event })"
|
||||||
|
label="Email" field="invoice_email" :errors="errors" class="col-sm-7"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ClientFields class="row" :client="client"/>
|
||||||
|
</b-tab>
|
||||||
|
|
||||||
|
<b-tab title="Invoicing" class="col-12">
|
||||||
|
<div class="row">
|
||||||
|
<AppInput :value="client.currency" @change="updateProp({ currency: $event })"
|
||||||
|
label="Currency" field="currency" :errors="errors" class="col-sm-4"/>
|
||||||
|
<AppInput :value="client.rate" @change="updateProp({ rate: $event })"
|
||||||
|
label="Hourly rate" field="rate" :errors="errors" class="col-sm-4"/>
|
||||||
|
<AppCheckbox :value="client.has_vat" @input="updateProp({ has_vat: $event })"
|
||||||
|
label="Apply VAT" field="has_vat" :errors="errors" class="col-sm-4"/>
|
||||||
|
<AppSelect :value="client.bank_account"
|
||||||
|
track-by="id"
|
||||||
|
label="Bank account"
|
||||||
|
label-field="bank_name"
|
||||||
|
:options="bankAccounts || []"
|
||||||
|
@input="bankAccountChanged"
|
||||||
|
class="col-12"/>
|
||||||
|
</div>
|
||||||
|
</b-tab>
|
||||||
|
|
||||||
|
<b-tab title="Address" class="col-12">
|
||||||
|
<div class="row">
|
||||||
|
<AppInput :value="client.company_address" @change="updateProp({ company_address: $event })"
|
||||||
|
label="Company Address" field="company_address" :errors="errors"
|
||||||
|
class="col-12"/>
|
||||||
|
<AppInput :value="client.company_postal_code"
|
||||||
|
@change="updateProp({ company_postal_code: $event })"
|
||||||
|
label="Postal code" field="company_postal_code" :errors="errors"
|
||||||
|
class="col-sm-5"/>
|
||||||
|
<AppInput :value="client.company_city" @change="updateProp({ company_city: $event })"
|
||||||
|
label="City" field="company_city" :errors="errors" class="col-sm-7"/>
|
||||||
|
<AppInput :value="client.company_county" @change="updateProp({ company_county: $event })"
|
||||||
|
label="County/State" field="company_county" :errors="errors" class="col-sm-6"/>
|
||||||
|
<AppInput :value="client.company_country" @change="updateProp({ company_country: $event })"
|
||||||
|
label="Country" field="company_country" :errors="errors" class="col-sm-6"/>
|
||||||
|
</div>
|
||||||
|
</b-tab>
|
||||||
|
|
||||||
|
</b-tabs>
|
||||||
|
|
||||||
|
<div v-if="!client">Loading</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import {
|
||||||
|
BTab, BTabs, BDropdownItemButton, BDropdown,
|
||||||
|
} from 'bootstrap-vue';
|
||||||
import NotificationService from '@/services/notification.service';
|
import NotificationService from '@/services/notification.service';
|
||||||
import AppInput from '@/components/form/AppInput';
|
import AppInput from '@/components/form/AppInput';
|
||||||
import AppSelect from '@/components/form/AppSelect';
|
import AppSelect from '@/components/form/AppSelect';
|
||||||
import Errors from '@/utils/errors';
|
import Errors from '@/utils/errors';
|
||||||
import AppCheckbox from '@/components/form/AppCheckbox';
|
import AppCheckbox from '@/components/form/AppCheckbox';
|
||||||
|
import ClientFields from '@/components/clients/ClientFields';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
ClientFields,
|
||||||
AppCheckbox,
|
AppCheckbox,
|
||||||
AppInput,
|
AppInput,
|
||||||
AppSelect,
|
AppSelect,
|
||||||
|
BTab,
|
||||||
|
BTabs,
|
||||||
|
BDropdown,
|
||||||
|
BDropdownItemButton,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -148,7 +157,7 @@ export default {
|
|||||||
this.loading = false;
|
this.loading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async deleteClient(clientId) {
|
async deleteClient() {
|
||||||
const confirmed = await this.$bvModal.msgBoxConfirm(`Delete client ${this.client.company_name}?`, {
|
const confirmed = await this.$bvModal.msgBoxConfirm(`Delete client ${this.client.company_name}?`, {
|
||||||
okTitle: 'Delete',
|
okTitle: 'Delete',
|
||||||
okVariant: 'danger',
|
okVariant: 'danger',
|
||||||
@ -158,7 +167,7 @@ export default {
|
|||||||
});
|
});
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.$emit('done');
|
this.$emit('done');
|
||||||
await this.$store.dispatch('clients/deleteClient', clientId);
|
await this.$store.dispatch('clients/deleteClient', this.client.id);
|
||||||
try {
|
try {
|
||||||
NotificationService.success('Deleted');
|
NotificationService.success('Deleted');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -31,20 +31,8 @@
|
|||||||
<AppError :errors="errors" field="client_county"/>
|
<AppError :errors="errors" field="client_county"/>
|
||||||
<AppError :errors="errors" field="client_country"/>
|
<AppError :errors="errors" field="client_country"/>
|
||||||
|
|
||||||
<span :class="{'d-print-none': !invoice.client_reg_no }">Reg no: </span>
|
<InvoiceClientFields :invoice="invoice"/>
|
||||||
<AppEditable :value="invoice.client_reg_no"
|
|
||||||
:errors="errors"
|
|
||||||
field="client_reg_no"
|
|
||||||
placeholder="Enter reg no"
|
|
||||||
class="break-line"
|
|
||||||
@change="updateProp({ client_reg_no: $event })"/>
|
|
||||||
<span :class="{'d-print-none': !invoice.client_vat_no }">VAT no: </span>
|
|
||||||
<AppEditable :value="invoice.client_vat_no"
|
|
||||||
:errors="errors"
|
|
||||||
field="client_vat_no"
|
|
||||||
placeholder="Enter vat no"
|
|
||||||
class="break-line"
|
|
||||||
@change="updateProp({ client_vat_no: $event })"/>
|
|
||||||
<AppEditable :value="invoice.client_email"
|
<AppEditable :value="invoice.client_email"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
field="client_email"
|
field="client_email"
|
||||||
@ -58,6 +46,7 @@ import { mapGetters } from 'vuex';
|
|||||||
import AppError from '@/components/form/AppError';
|
import AppError from '@/components/form/AppError';
|
||||||
import AppEditable from '@/components/form/AppEditable';
|
import AppEditable from '@/components/form/AppEditable';
|
||||||
import ClientSelector from '@/components/clients/ClientSelector';
|
import ClientSelector from '@/components/clients/ClientSelector';
|
||||||
|
import InvoiceClientFields from '@/components/invoices/InvoiceClientFields';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['invoice', 'errors'],
|
props: ['invoice', 'errors'],
|
||||||
@ -65,6 +54,7 @@ export default {
|
|||||||
AppError,
|
AppError,
|
||||||
ClientSelector,
|
ClientSelector,
|
||||||
AppEditable,
|
AppEditable,
|
||||||
|
InvoiceClientFields,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
@ -82,6 +72,8 @@ export default {
|
|||||||
this.prefillClient(client);
|
this.prefillClient(client);
|
||||||
},
|
},
|
||||||
prefillClient(client) {
|
prefillClient(client) {
|
||||||
|
this.prefillClientFields(client);
|
||||||
|
|
||||||
return this.updateProp({
|
return this.updateProp({
|
||||||
client_id: client.id,
|
client_id: client.id,
|
||||||
client_name: client.company_name,
|
client_name: client.company_name,
|
||||||
@ -90,8 +82,6 @@ export default {
|
|||||||
client_city: client.company_city,
|
client_city: client.company_city,
|
||||||
client_county: client.company_county,
|
client_county: client.company_county,
|
||||||
client_country: client.company_country,
|
client_country: client.company_country,
|
||||||
client_reg_no: client.company_reg_no,
|
|
||||||
client_vat_no: client.company_vat_no,
|
|
||||||
client_email: client.invoice_email,
|
client_email: client.invoice_email,
|
||||||
currency: client.currency || 'USD',
|
currency: client.currency || 'USD',
|
||||||
vat_rate: client.has_vat ? this.team.vat_rate : 0,
|
vat_rate: client.has_vat ? this.team.vat_rate : 0,
|
||||||
@ -99,6 +89,20 @@ export default {
|
|||||||
bank_account_no: client.bank_account ? client.bank_account.account_no : null,
|
bank_account_no: client.bank_account ? client.bank_account.account_no : null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
prefillClientFields(client) {
|
||||||
|
this.$store.dispatch('invoiceClientFields/removeInvoiceClientFields', this.invoice.id);
|
||||||
|
|
||||||
|
client.fields.forEach((field) => {
|
||||||
|
this.$store.dispatch('invoiceClientFields/addInvoiceClientField', {
|
||||||
|
invoiceId: this.invoice.id,
|
||||||
|
props: {
|
||||||
|
label: field.label,
|
||||||
|
value: field.value,
|
||||||
|
client_field_id: field.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
29
src/components/invoices/InvoiceClientFields.vue
Normal file
29
src/components/invoices/InvoiceClientFields.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-for="field in invoice.client_fields">
|
||||||
|
<span :class="{'d-print-none': !field.value }">{{ field.label }}: </span>
|
||||||
|
<AppEditable :value="field.value"
|
||||||
|
:placeholder="field.label"
|
||||||
|
class="break-line"
|
||||||
|
@change="updateProp({ value: $event }, field)"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import AppEditable from '@/components/form/AppEditable';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['invoice'],
|
||||||
|
components: {
|
||||||
|
AppEditable,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateProp(props, field) {
|
||||||
|
this.$store.dispatch('invoiceClientFields/updateInvoiceClientField', {
|
||||||
|
props,
|
||||||
|
fieldId: field.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -137,7 +137,7 @@ export default {
|
|||||||
this.$store.dispatch('invoices/updateInvoice', props);
|
this.$store.dispatch('invoices/updateInvoice', props);
|
||||||
},
|
},
|
||||||
addRow() {
|
addRow() {
|
||||||
this.$store.dispatch('invoices/addRow');
|
this.$store.dispatch('invoiceRows/addRow', this.invoice.id);
|
||||||
},
|
},
|
||||||
updateTeam(props) {
|
updateTeam(props) {
|
||||||
this.$store.dispatch('teams/updateTeam', props);
|
this.$store.dispatch('teams/updateTeam', props);
|
||||||
|
|||||||
@ -52,13 +52,13 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateProp(props) {
|
updateProp(props) {
|
||||||
this.$store.dispatch('invoices/updateInvoiceRow', {
|
this.$store.dispatch('invoiceRows/updateInvoiceRow', {
|
||||||
props,
|
props,
|
||||||
id: this.row.id,
|
id: this.row.id,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async removeRow(row) {
|
async removeRow(row) {
|
||||||
await this.$store.dispatch('invoices/removeRow', row);
|
await this.$store.dispatch('invoiceRows/removeRow', row.id);
|
||||||
this.updateProp();
|
this.updateProp();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
31
src/store/client-fields.js
Normal file
31
src/store/client-fields.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import ClientField from '@/store/models/client-field';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {},
|
||||||
|
mutations: {},
|
||||||
|
actions: {
|
||||||
|
init() {},
|
||||||
|
terminate() {},
|
||||||
|
async clientFieldProps(store, payload) {
|
||||||
|
return ClientField.update({
|
||||||
|
where: payload.fieldId,
|
||||||
|
data: payload.props,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async updateClientField({ dispatch }, payload) {
|
||||||
|
await dispatch('clientFieldProps', payload);
|
||||||
|
return dispatch('clients/updateClient', null, { root: true }); // TODO: pass clientId to make generic
|
||||||
|
},
|
||||||
|
async addNewField(store, clientId) {
|
||||||
|
const field = await ClientField.createNew();
|
||||||
|
field.$update({
|
||||||
|
client_id: clientId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async deleteClientField({ dispatch }, fieldId) {
|
||||||
|
await ClientField.delete(fieldId);
|
||||||
|
return dispatch('clients/updateClient', null, { root: true }); // TODO: pass clientId to make generic
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -47,11 +47,15 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
async updateClient({ getters, dispatch }, props) {
|
async updateClient({ getters, dispatch }, props) {
|
||||||
await dispatch('clientProps', props);
|
if (props) {
|
||||||
|
await dispatch('clientProps', props);
|
||||||
|
}
|
||||||
return ClientService.updateClient(getters.client);
|
return ClientService.updateClient(getters.client);
|
||||||
},
|
},
|
||||||
async updateClientById(store, payload) {
|
async updateClientById(store, payload) {
|
||||||
const client = Client.find(payload.clientId);
|
const client = Client.query()
|
||||||
|
.with('fields')
|
||||||
|
.find(payload.clientId);
|
||||||
client.$update(payload.props);
|
client.$update(payload.props);
|
||||||
return ClientService.updateClient(client);
|
return ClientService.updateClient(client);
|
||||||
},
|
},
|
||||||
@ -60,24 +64,22 @@ export default {
|
|||||||
commit('clientId', client.id);
|
commit('clientId', client.id);
|
||||||
commit('isModalOpen', true);
|
commit('isModalOpen', true);
|
||||||
},
|
},
|
||||||
async deleteClient(clientId) {
|
async deleteClient(store, clientId) {
|
||||||
const res = await ClientService.deleteClient(clientId);
|
const res = await ClientService.deleteClient(clientId);
|
||||||
if ('client_id' in res) {
|
await Client.delete(clientId);
|
||||||
Client.delete(res.client_id);
|
|
||||||
}
|
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
client(state) {
|
client(state) {
|
||||||
return Client.query()
|
return Client.query()
|
||||||
.with(['bank_account'])
|
.with(['bank_account', 'fields'])
|
||||||
.find(state.clientId);
|
.find(state.clientId);
|
||||||
},
|
},
|
||||||
all() {
|
all() {
|
||||||
return Client.query()
|
return Client.query()
|
||||||
.where('$isNew', false)
|
.where('$isNew', false)
|
||||||
.with(['bank_account'])
|
.with(['bank_account', 'fields'])
|
||||||
.get();
|
.get();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
34
src/store/invoice-client-fields.js
Normal file
34
src/store/invoice-client-fields.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import InvoiceClientField from '@/store/models/invoice-client-field';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {},
|
||||||
|
mutations: {},
|
||||||
|
actions: {
|
||||||
|
init() {},
|
||||||
|
terminate() {},
|
||||||
|
invoiceClientFieldProps(store, payload) {
|
||||||
|
return InvoiceClientField.update({
|
||||||
|
where: payload.fieldId,
|
||||||
|
data: payload.props,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async updateInvoiceClientField({ dispatch }, payload) {
|
||||||
|
await dispatch('invoiceClientFieldProps', payload);
|
||||||
|
return dispatch('invoices/updateInvoice', null, { root: true });
|
||||||
|
},
|
||||||
|
async removeInvoiceClientFields(store, invoiceId) {
|
||||||
|
return InvoiceClientField.delete(field => field.invoice_id === invoiceId);
|
||||||
|
},
|
||||||
|
async addInvoiceClientField(store, payload) {
|
||||||
|
const field = await InvoiceClientField.createNew();
|
||||||
|
await field.$update({
|
||||||
|
...payload.props,
|
||||||
|
invoice_id: payload.invoiceId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async removeInvoiceClientField(store, fieldId) {
|
||||||
|
await InvoiceClientField.delete(fieldId);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
34
src/store/invoice-rows.js
Normal file
34
src/store/invoice-rows.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import InvoiceRow from '@/store/models/invoice-row';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
init() {},
|
||||||
|
terminate() {},
|
||||||
|
invoiceRowProps(store, payload) {
|
||||||
|
return InvoiceRow.update({
|
||||||
|
where: payload.id,
|
||||||
|
data: payload.props,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async updateInvoiceRow({ dispatch }, payload) {
|
||||||
|
await dispatch('invoiceRowProps', payload);
|
||||||
|
return dispatch('invoices/updateInvoice', null, { root: true });
|
||||||
|
},
|
||||||
|
async addRow(store, invoiceId) {
|
||||||
|
const row = await InvoiceRow.createNew();
|
||||||
|
const rowCount = InvoiceRow.query().where('invoice_id', invoiceId).count();
|
||||||
|
row.$update({
|
||||||
|
invoice_id: invoiceId,
|
||||||
|
order: rowCount,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async removeRow(store, rowId) {
|
||||||
|
await InvoiceRow.delete(rowId);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import InvoiceService from '@/services/invoice.service';
|
import InvoiceService from '@/services/invoice.service';
|
||||||
import Invoice from '@/store/models/invoice';
|
import Invoice from '@/store/models/invoice';
|
||||||
import InvoiceRow from '@/store/models/invoice-row';
|
|
||||||
import { pick } from '@/utils/helpers';
|
import { pick } from '@/utils/helpers';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Errors from '@/utils/errors';
|
import Errors from '@/utils/errors';
|
||||||
@ -55,16 +54,7 @@ export default {
|
|||||||
data: props,
|
data: props,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
invoiceRowProps(store, payload) {
|
async updateClient({ getters, dispatch }, props) {
|
||||||
return InvoiceRow.update({
|
|
||||||
where: payload.id,
|
|
||||||
data: payload.props,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async updateInvoice({ getters, dispatch, commit }, props) {
|
|
||||||
await dispatch('invoiceProps', props);
|
|
||||||
|
|
||||||
// Update client
|
|
||||||
const clientProps = pick(props, {
|
const clientProps = pick(props, {
|
||||||
bank_account_id: 'bank_account_id',
|
bank_account_id: 'bank_account_id',
|
||||||
client_name: 'company_name',
|
client_name: 'company_name',
|
||||||
@ -73,8 +63,6 @@ export default {
|
|||||||
client_country: 'company_country',
|
client_country: 'company_country',
|
||||||
client_county: 'company_county',
|
client_county: 'company_county',
|
||||||
client_city: 'company_city',
|
client_city: 'company_city',
|
||||||
client_reg_no: 'company_reg_no',
|
|
||||||
client_vat_no: 'company_vat_no',
|
|
||||||
client_email: 'invoice_email',
|
client_email: 'invoice_email',
|
||||||
currency: 'currency',
|
currency: 'currency',
|
||||||
});
|
});
|
||||||
@ -88,7 +76,8 @@ export default {
|
|||||||
clientId: getters.invoice.client_id,
|
clientId: getters.invoice.client_id,
|
||||||
}, { root: true });
|
}, { root: true });
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async updateTeam({ getters, dispatch }, props) {
|
||||||
const teamProps = pick(props, {
|
const teamProps = pick(props, {
|
||||||
late_fee: 'invoice_late_fee',
|
late_fee: 'invoice_late_fee',
|
||||||
from_name: 'company_name',
|
from_name: 'company_name',
|
||||||
@ -119,37 +108,23 @@ export default {
|
|||||||
if (Object.keys(teamProps).length > 0) {
|
if (Object.keys(teamProps).length > 0) {
|
||||||
dispatch('teams/updateTeam', teamProps, { root: true });
|
dispatch('teams/updateTeam', teamProps, { root: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
commit('clearErrors');
|
|
||||||
|
|
||||||
return InvoiceService.updateInvoice(getters.invoice)
|
|
||||||
.catch(err => commit('setErrors', err.errors));
|
|
||||||
},
|
},
|
||||||
async updateInvoiceRow({ getters, dispatch, commit }, payload) {
|
async updateInvoice({ dispatch, commit, getters }, props) {
|
||||||
await dispatch('invoiceRowProps', payload);
|
if (props) {
|
||||||
|
await dispatch('invoiceProps', props);
|
||||||
|
await dispatch('updateClient', props);
|
||||||
|
await dispatch('updateTeam', props);
|
||||||
|
}
|
||||||
|
|
||||||
commit('clearErrors');
|
commit('clearErrors');
|
||||||
|
|
||||||
return InvoiceService.updateInvoice(getters.invoice)
|
return InvoiceService.updateInvoice(getters.invoice)
|
||||||
.catch(err => commit('setErrors', err.errors));
|
.catch(err => commit('setErrors', err.errors));
|
||||||
},
|
},
|
||||||
async deleteInvoice(store, invoice) {
|
async deleteInvoice(store, invoice) {
|
||||||
const res = await InvoiceService.deleteInvoice(invoice.id);
|
const res = await InvoiceService.deleteInvoice(invoice.id);
|
||||||
if ('invoice_id' in res) {
|
await Invoice.delete(invoice.id);
|
||||||
Invoice.delete(res.invoice_id);
|
|
||||||
}
|
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
async addRow({ state, getters }) {
|
|
||||||
const row = await InvoiceRow.createNew();
|
|
||||||
row.$update({
|
|
||||||
invoice_id: state.invoiceId,
|
|
||||||
order: getters.invoice.rows.length,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async removeRow(store, row) {
|
|
||||||
await InvoiceRow.delete(row.id);
|
|
||||||
},
|
|
||||||
async bookInvoice({ getters, commit, dispatch }) {
|
async bookInvoice({ getters, commit, dispatch }) {
|
||||||
commit('clearErrors');
|
commit('clearErrors');
|
||||||
|
|
||||||
@ -164,7 +139,7 @@ export default {
|
|||||||
getters: {
|
getters: {
|
||||||
invoice(state) {
|
invoice(state) {
|
||||||
return Invoice.query()
|
return Invoice.query()
|
||||||
.with(['client'])
|
.with(['client', 'client_fields'])
|
||||||
.with('rows', query => query.orderBy('order', 'asc'))
|
.with('rows', query => query.orderBy('order', 'asc'))
|
||||||
.find(state.invoiceId);
|
.find(state.invoiceId);
|
||||||
},
|
},
|
||||||
|
|||||||
16
src/store/models/client-field.js
Normal file
16
src/store/models/client-field.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Model } from '@vuex-orm/core';
|
||||||
|
import { uuidv4 } from '@/utils/helpers';
|
||||||
|
|
||||||
|
export default class ClientField extends Model {
|
||||||
|
// This is the name used as module name of the Vuex Store.
|
||||||
|
static entity = 'client_fields';
|
||||||
|
|
||||||
|
static fields() {
|
||||||
|
return {
|
||||||
|
id: this.attr(() => uuidv4()),
|
||||||
|
client_id: this.attr(null),
|
||||||
|
label: this.attr(''),
|
||||||
|
value: this.attr(''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { Model } from '@vuex-orm/core';
|
import { Model } from '@vuex-orm/core';
|
||||||
import { uuidv4 } from '@/utils/helpers';
|
import { uuidv4 } from '@/utils/helpers';
|
||||||
import BankAccount from '@/store/models/bank-account';
|
import BankAccount from '@/store/models/bank-account';
|
||||||
|
import ClientField from '@/store/models/client-field';
|
||||||
|
|
||||||
export default class Client extends Model {
|
export default class Client extends Model {
|
||||||
// This is the name used as module name of the Vuex Store.
|
// This is the name used as module name of the Vuex Store.
|
||||||
@ -15,14 +16,13 @@ export default class Client extends Model {
|
|||||||
company_country: this.attr(''),
|
company_country: this.attr(''),
|
||||||
company_county: this.attr(''),
|
company_county: this.attr(''),
|
||||||
company_city: this.attr(''),
|
company_city: this.attr(''),
|
||||||
company_reg_no: this.attr(''),
|
|
||||||
company_vat_no: this.attr(''),
|
|
||||||
has_vat: this.attr(null),
|
has_vat: this.attr(null),
|
||||||
currency: this.attr(null),
|
currency: this.attr(null),
|
||||||
rate: this.attr(null),
|
rate: this.attr(null),
|
||||||
invoice_email: this.attr(''),
|
invoice_email: this.attr(''),
|
||||||
bank_account_id: this.attr(null),
|
bank_account_id: this.attr(null),
|
||||||
bank_account: this.belongsTo(BankAccount, 'bank_account_id', 'id'),
|
bank_account: this.belongsTo(BankAccount, 'bank_account_id'),
|
||||||
|
fields: this.hasMany(ClientField, 'client_id'),
|
||||||
updated_at: this.attr(''),
|
updated_at: this.attr(''),
|
||||||
created_at: this.attr(''),
|
created_at: this.attr(''),
|
||||||
};
|
};
|
||||||
|
|||||||
17
src/store/models/invoice-client-field.js
Normal file
17
src/store/models/invoice-client-field.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Model } from '@vuex-orm/core';
|
||||||
|
import { uuidv4 } from '@/utils/helpers';
|
||||||
|
|
||||||
|
export default class InvoiceClientField extends Model {
|
||||||
|
// This is the name used as module name of the Vuex Store.
|
||||||
|
static entity = 'invoice_client_fields';
|
||||||
|
|
||||||
|
static fields() {
|
||||||
|
return {
|
||||||
|
id: this.attr(() => uuidv4()),
|
||||||
|
invoice_id: this.attr(null),
|
||||||
|
client_field_id: this.attr(null),
|
||||||
|
label: this.attr(''),
|
||||||
|
value: this.attr(''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import { Model } from '@vuex-orm/core';
|
|||||||
import { uuidv4 } from '@/utils/helpers';
|
import { uuidv4 } from '@/utils/helpers';
|
||||||
import Client from '@/store/models/client';
|
import Client from '@/store/models/client';
|
||||||
import InvoiceRow from '@/store/models/invoice-row';
|
import InvoiceRow from '@/store/models/invoice-row';
|
||||||
|
import InvoiceClientField from '@/store/models/invoice-client-field';
|
||||||
|
|
||||||
export default class Invoice extends Model {
|
export default class Invoice extends Model {
|
||||||
// This is the name used as module name of the Vuex Store.
|
// This is the name used as module name of the Vuex Store.
|
||||||
@ -36,8 +37,6 @@ export default class Invoice extends Model {
|
|||||||
client_country: this.attr(''),
|
client_country: this.attr(''),
|
||||||
client_county: this.attr(''),
|
client_county: this.attr(''),
|
||||||
client_city: this.attr(''),
|
client_city: this.attr(''),
|
||||||
client_reg_no: this.attr(''),
|
|
||||||
client_vat_no: this.attr(''),
|
|
||||||
client_email: this.attr(''),
|
client_email: this.attr(''),
|
||||||
client_id: this.attr(null),
|
client_id: this.attr(null),
|
||||||
client: this.belongsTo(Client, 'client_id'),
|
client: this.belongsTo(Client, 'client_id'),
|
||||||
@ -46,6 +45,7 @@ export default class Invoice extends Model {
|
|||||||
updated_at: this.attr(''),
|
updated_at: this.attr(''),
|
||||||
created_at: this.attr(''),
|
created_at: this.attr(''),
|
||||||
total: this.attr(null), // Only used in lists.
|
total: this.attr(null), // Only used in lists.
|
||||||
|
client_fields: this.hasMany(InvoiceClientField, 'invoice_id'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
src/store/models/team-field.js
Normal file
16
src/store/models/team-field.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Model } from '@vuex-orm/core';
|
||||||
|
import { uuidv4 } from '@/utils/helpers';
|
||||||
|
|
||||||
|
export default class TeamField extends Model {
|
||||||
|
// This is the name used as module name of the Vuex Store.
|
||||||
|
static entity = 'team_fields';
|
||||||
|
|
||||||
|
static fields() {
|
||||||
|
return {
|
||||||
|
id: this.attr(() => uuidv4()),
|
||||||
|
team_id: this.attr(null),
|
||||||
|
label: this.attr(''),
|
||||||
|
value: this.attr(''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { Model } from '@vuex-orm/core';
|
import { Model } from '@vuex-orm/core';
|
||||||
import { uuidv4 } from '@/utils/helpers';
|
import { uuidv4 } from '@/utils/helpers';
|
||||||
|
import TeamField from '@/store/models/team-field';
|
||||||
|
|
||||||
export default class Team extends Model {
|
export default class Team extends Model {
|
||||||
// This is the name used as module name of the Vuex Store.
|
// This is the name used as module name of the Vuex Store.
|
||||||
@ -22,6 +23,7 @@ export default class Team extends Model {
|
|||||||
vat_rate: this.attr(null),
|
vat_rate: this.attr(null),
|
||||||
invoice_late_fee: this.attr(null),
|
invoice_late_fee: this.attr(null),
|
||||||
invoice_due_days: this.attr(null),
|
invoice_due_days: this.attr(null),
|
||||||
|
fields: this.hasMany(TeamField, 'team_id'),
|
||||||
updated_at: this.attr(''),
|
updated_at: this.attr(''),
|
||||||
created_at: this.attr(''),
|
created_at: this.attr(''),
|
||||||
logo_url: this.attr(''),
|
logo_url: this.attr(''),
|
||||||
|
|||||||
@ -9,10 +9,16 @@ import InvoiceRow from '@/store/models/invoice-row';
|
|||||||
import Team from '@/store/models/team';
|
import Team from '@/store/models/team';
|
||||||
import bankAccounts from '@/store/bank-accounts';
|
import bankAccounts from '@/store/bank-accounts';
|
||||||
import clients from '@/store/clients';
|
import clients from '@/store/clients';
|
||||||
|
import clientFields from '@/store/client-fields';
|
||||||
import invoices from '@/store/invoices';
|
import invoices from '@/store/invoices';
|
||||||
|
import invoiceRows from '@/store/invoice-rows';
|
||||||
|
import invoiceClientFields from '@/store/invoice-client-fields';
|
||||||
import teams from '@/store/teams';
|
import teams from '@/store/teams';
|
||||||
import themes from '@/store/themes';
|
import themes from '@/store/themes';
|
||||||
import data from '@/store/data';
|
import data from '@/store/data';
|
||||||
|
import ClientField from '@/store/models/client-field';
|
||||||
|
import TeamField from '@/store/models/team-field';
|
||||||
|
import InvoiceClientField from '@/store/models/invoice-client-field';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
@ -20,8 +26,11 @@ VuexORM.use(VuexORMisDirtyPlugin);
|
|||||||
const database = new VuexORM.Database();
|
const database = new VuexORM.Database();
|
||||||
|
|
||||||
database.register(Team);
|
database.register(Team);
|
||||||
|
database.register(TeamField);
|
||||||
database.register(Client);
|
database.register(Client);
|
||||||
|
database.register(ClientField);
|
||||||
database.register(Invoice);
|
database.register(Invoice);
|
||||||
|
database.register(InvoiceClientField);
|
||||||
database.register(InvoiceRow);
|
database.register(InvoiceRow);
|
||||||
database.register(BankAccount);
|
database.register(BankAccount);
|
||||||
|
|
||||||
@ -30,7 +39,10 @@ export default new Vuex.Store({
|
|||||||
modules: {
|
modules: {
|
||||||
bankAccounts,
|
bankAccounts,
|
||||||
clients,
|
clients,
|
||||||
|
clientFields,
|
||||||
invoices,
|
invoices,
|
||||||
|
invoiceRows,
|
||||||
|
invoiceClientFields,
|
||||||
teams,
|
teams,
|
||||||
themes,
|
themes,
|
||||||
data,
|
data,
|
||||||
|
|||||||
Reference in New Issue
Block a user