mirror of
https://github.com/mokuappio/serverless-invoices.git
synced 2025-11-01 10:21:08 -04:00
Init commit
This commit is contained in:
53
src/components/invoices/InvoiceBankDetails.vue
Normal file
53
src/components/invoices/InvoiceBankDetails.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<strong v-b-modal.bank_details class="editable__item"
|
||||
:class="{'is-invalid': errors && errors.has('bank_account_no')}">
|
||||
{{ invoice.bank_account_no }}
|
||||
<span v-if="!invoice.bank_account_no">Add bank account no</span>
|
||||
</strong>
|
||||
<AppError :errors="errors" field="bank_account_no"/>
|
||||
<br>
|
||||
<span class="editable__item" v-b-modal.bank_details
|
||||
:class="{'is-invalid': errors && errors.has('bank_name')}">
|
||||
{{ invoice.bank_name }}
|
||||
<span v-if="!invoice.bank_name">Add bank name</span>
|
||||
</span>
|
||||
<AppError :errors="errors" field="bank_name"/>
|
||||
|
||||
<BModal id="bank_details"
|
||||
centered
|
||||
title="Choose bank account"
|
||||
hide-footer
|
||||
size="lg"
|
||||
content-class="bg-base dp--24">
|
||||
<BankAccountsList @select="accountSelected"/>
|
||||
</BModal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { BModal, VBModal } from 'bootstrap-vue';
|
||||
import BankAccountsList from '@/components/bank-accounts/BankAccountsList';
|
||||
import AppError from '@/components/form/AppError';
|
||||
|
||||
export default {
|
||||
props: ['invoice', 'errors'],
|
||||
components: {
|
||||
BModal,
|
||||
BankAccountsList,
|
||||
AppError,
|
||||
},
|
||||
directives: {
|
||||
'b-modal': VBModal,
|
||||
},
|
||||
methods: {
|
||||
accountSelected(account) {
|
||||
this.$emit('update', {
|
||||
bank_account_no: account.account_no,
|
||||
bank_name: account.bank_name,
|
||||
bank_account_id: account.id,
|
||||
});
|
||||
this.$bvModal.hide('bank_details');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
100
src/components/invoices/InvoiceClientDetails.vue
Normal file
100
src/components/invoices/InvoiceClientDetails.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<ClientSelector :value="invoice.client_name" btn-class="font-weight-bold" @selected="clientSelected"/>
|
||||
</div>
|
||||
<AppEditable :value="invoice.client_address"
|
||||
suffix=", "
|
||||
placeholder="Address"
|
||||
@change="updateProp({ client_address: $event })"/>
|
||||
<AppEditable :value="invoice.client_postal_code"
|
||||
placeholder="Postal code"
|
||||
class="break-line"
|
||||
@change="updateProp({ client_postal_code: $event })"/>
|
||||
<AppError :errors="errors" field="client_address"/>
|
||||
<AppError :errors="errors" field="client_postal_code"/>
|
||||
|
||||
<AppEditable :value="invoice.client_city"
|
||||
suffix=", "
|
||||
placeholder="City"
|
||||
@change="updateProp({ client_city: $event })"/>
|
||||
<AppEditable :value="invoice.client_county"
|
||||
suffix=", "
|
||||
placeholder="County/State"
|
||||
@change="updateProp({ client_county: $event })"/>
|
||||
<AppEditable :value="invoice.client_country"
|
||||
placeholder="Country"
|
||||
class="break-line"
|
||||
@change="updateProp({ client_country: $event })"/>
|
||||
<AppError :errors="errors" field="client_city"/>
|
||||
<AppError :errors="errors" field="client_county"/>
|
||||
<AppError :errors="errors" field="client_country"/>
|
||||
|
||||
<span :class="{'d-print-none': !invoice.client_reg_no }">Reg no: </span>
|
||||
<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"
|
||||
:errors="errors"
|
||||
field="client_email"
|
||||
class="break-line"
|
||||
placeholder="Client's email"
|
||||
@change="updateProp({ client_email: $event })"/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import AppError from '@/components/form/AppError';
|
||||
import AppEditable from '@/components/form/AppEditable';
|
||||
import ClientSelector from '@/components/clients/ClientSelector';
|
||||
|
||||
export default {
|
||||
props: ['invoice', 'errors'],
|
||||
components: {
|
||||
AppError,
|
||||
ClientSelector,
|
||||
AppEditable,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
team: 'teams/team',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
updateProp(props) {
|
||||
this.$emit('update', props);
|
||||
},
|
||||
clientSelected(client) {
|
||||
this.prefillClient(client);
|
||||
},
|
||||
prefillClient(client) {
|
||||
return this.updateProp({
|
||||
client_id: client.id,
|
||||
client_name: client.company_name,
|
||||
client_address: client.company_address,
|
||||
client_postal_code: client.company_postal_code,
|
||||
client_city: client.company_city,
|
||||
client_county: client.company_county,
|
||||
client_country: client.company_country,
|
||||
client_reg_no: client.company_reg_no,
|
||||
client_vat_no: client.company_vat_no,
|
||||
client_email: client.invoice_email,
|
||||
currency: client.currency || 'USD',
|
||||
vat_rate: client.has_vat ? this.team.vat_rate : 0,
|
||||
bank_name: client.bank_account ? client.bank_account.bank_name : null,
|
||||
bank_account_no: client.bank_account ? client.bank_account.account_no : null,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
75
src/components/invoices/InvoiceCompanyDetails.vue
Normal file
75
src/components/invoices/InvoiceCompanyDetails.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div>
|
||||
<strong>
|
||||
<AppEditable :value="invoice.from_name"
|
||||
:errors="errors"
|
||||
field="from_name"
|
||||
placeholder="Your company name"
|
||||
class="break-line"
|
||||
@change="updateProp({ from_name: $event })"/>
|
||||
</strong>
|
||||
<AppEditable :value="invoice.from_address"
|
||||
suffix=", "
|
||||
placeholder="Address"
|
||||
@change="updateProp({ from_address: $event })"/>
|
||||
<AppEditable :value="invoice.from_postal_code"
|
||||
placeholder="Postal code"
|
||||
class="break-line"
|
||||
@change="updateProp({ from_postal_code: $event })"/>
|
||||
<AppError :errors="errors" field="from_address"/>
|
||||
<AppError :errors="errors" field="from_postal_code"/>
|
||||
|
||||
<AppEditable :value="invoice.from_city"
|
||||
suffix=", "
|
||||
placeholder="City"
|
||||
@change="updateProp({ from_city: $event })"/>
|
||||
<AppEditable :value="invoice.from_county"
|
||||
suffix=", "
|
||||
placeholder="County/State"
|
||||
@change="updateProp({ from_county: $event })"/>
|
||||
<AppEditable :value="invoice.from_country"
|
||||
placeholder="Country"
|
||||
class="break-line"
|
||||
@change="updateProp({ from_country: $event })"/>
|
||||
<AppError :errors="errors" field="from_city"/>
|
||||
<AppError :errors="errors" field="from_county"/>
|
||||
<AppError :errors="errors" field="from_country"/>
|
||||
|
||||
<span :class="{'d-print-none': !invoice.from_reg_no }">Reg no: </span>
|
||||
<AppEditable :value="invoice.from_reg_no"
|
||||
:errors="errors"
|
||||
field="from_reg_no"
|
||||
placeholder="Enter reg no"
|
||||
class="break-line"
|
||||
@change="updateProp({ from_reg_no: $event })"/>
|
||||
<span :class="{'d-print-none': !invoice.from_vat_no }">VAT no: </span>
|
||||
<AppEditable :value="invoice.from_vat_no"
|
||||
:errors="errors"
|
||||
field="from_vat_no"
|
||||
placeholder="Enter vat no"
|
||||
class="break-line"
|
||||
@change="updateProp({ from_vat_no: $event })"/>
|
||||
<AppEditable :value="invoice.from_email"
|
||||
:errors="errors"
|
||||
field="from_email"
|
||||
placeholder="Your email"
|
||||
@change="updateProp({ from_email: $event })"/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import AppError from '@/components/form/AppError';
|
||||
import AppEditable from '../form/AppEditable';
|
||||
|
||||
export default {
|
||||
props: ['invoice', 'errors'],
|
||||
components: {
|
||||
AppEditable,
|
||||
AppError,
|
||||
},
|
||||
methods: {
|
||||
updateProp(props) {
|
||||
this.$emit('update', props);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
36
src/components/invoices/InvoiceContactDetails.vue
Normal file
36
src/components/invoices/InvoiceContactDetails.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div>
|
||||
<AppEditable :value="invoice.from_website"
|
||||
:errors="errors"
|
||||
field="from_website"
|
||||
placeholder="Add website"
|
||||
class="break-line"
|
||||
@change="updateProp({ from_website: $event })"/>
|
||||
<AppEditable :value="invoice.from_email"
|
||||
:errors="errors"
|
||||
field="from_email"
|
||||
placeholder="Add email"
|
||||
class="break-line"
|
||||
@change="updateProp({ from_email: $event })"/>
|
||||
<AppEditable :value="invoice.from_phone"
|
||||
:errors="errors"
|
||||
field="from_phone"
|
||||
placeholder="Add phone"
|
||||
@change="updateProp({ from_phone: $event })"/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import AppEditable from '../form/AppEditable';
|
||||
|
||||
export default {
|
||||
props: ['invoice', 'errors'],
|
||||
components: {
|
||||
AppEditable,
|
||||
},
|
||||
methods: {
|
||||
updateProp(props) {
|
||||
this.$emit('update', props);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
77
src/components/invoices/InvoiceControls.vue
Normal file
77
src/components/invoices/InvoiceControls.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="row" v-if="invoice">
|
||||
<div class="col-12 mb-4 d-flex justify-content-between align-items-start">
|
||||
<router-link class="btn btn-sm btn-light btn--icon-left"
|
||||
:to="{name: 'invoices'}">
|
||||
<i class="material-icons">arrow_back</i>
|
||||
<span class="d-inline-block">Back</span>
|
||||
<!-- Back-->
|
||||
</router-link>
|
||||
<div class="d-flex align-items-center">
|
||||
<!-- <button class="btn btn-sm btn-outline-danger mr-2" @click="deleteInvoice">Delete</button>-->
|
||||
<!-- <a :href="invoice.pdf_url" target="_blank" class="btn btn-sm btn-outline-primary mr-2">PDF</a>-->
|
||||
|
||||
<AppSelect :value="invoice.status"
|
||||
class="mb-0 mr-2 text-capitalize multiselect--capitalize"
|
||||
:options="['draft', 'booked', 'sent', 'paid', 'cancelled']"
|
||||
@input="updateProp({status: $event})"/>
|
||||
<button class="btn btn-sm btn-outline-dark"
|
||||
v-if="invoice.status === 'draft'"
|
||||
@click="bookInvoice">Book
|
||||
</button>
|
||||
<b-dropdown variant="link" size="sm" no-caret right>
|
||||
<template slot="button-content">
|
||||
<i class="material-icons">more_vert</i>
|
||||
</template>
|
||||
<b-dropdown-item :href="invoice.pdf_url" target="_blank">Download PDF</b-dropdown-item>
|
||||
<b-dropdown-item-button @click="deleteInvoice">Delete</b-dropdown-item-button>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import NotificationService from '@/services/notification.service';
|
||||
import { BDropdown, BDropdownItem, BDropdownItemButton } from 'bootstrap-vue';
|
||||
import AppSelect from '@/components/form/AppSelect';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BDropdown,
|
||||
BDropdownItem,
|
||||
BDropdownItemButton,
|
||||
AppSelect,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
invoice: 'invoices/invoice',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
async deleteInvoice() {
|
||||
const confirmed = await this.$bvModal.msgBoxConfirm(`Delete invoice ${this.invoice.number}?`, {
|
||||
okTitle: 'Delete',
|
||||
okVariant: 'danger',
|
||||
cancelTitle: 'Dismiss',
|
||||
cancelVariant: 'btn-link',
|
||||
contentClass: 'bg-base dp--24',
|
||||
});
|
||||
if (confirmed) {
|
||||
const res = await this.$store.dispatch('invoices/deleteInvoice', this.invoice);
|
||||
NotificationService.success(res.message);
|
||||
this.$router.push({
|
||||
name: 'invoices',
|
||||
});
|
||||
}
|
||||
},
|
||||
bookInvoice() {
|
||||
this.$store.dispatch('invoices/bookInvoice');
|
||||
},
|
||||
updateProp(props) {
|
||||
this.$store.dispatch('invoices/updateInvoice', props);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
115
src/components/invoices/InvoiceForm.vue
Normal file
115
src/components/invoices/InvoiceForm.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="card bg-base dp--02 invoice-box" v-if="invoice">
|
||||
<div class="card-body">
|
||||
<div class="row mb-5">
|
||||
<div class="col-4">
|
||||
<img v-if="team.logo_url"
|
||||
:src="team.logo_url" style="width:100%; max-width:200px;">
|
||||
<!-- TODO: logo url input -->
|
||||
<AppError :errors="errors" field="team.logos"/>
|
||||
</div>
|
||||
<InvoiceHeader :invoice="invoice" :errors="errors" @update="updateProp"
|
||||
class="col-8 text-right mb-2"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<InvoiceClientDetails :invoice="invoice" :errors="errors" @update="updateProp"
|
||||
class="col-6"/>
|
||||
<InvoiceCompanyDetails :invoice="invoice" :errors="errors" @update="updateProp"
|
||||
class="col-6 text-right"/>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<AppEditable :value="invoice.notes"
|
||||
class="col-12"
|
||||
placeholder="Insert note"
|
||||
@change="updateProp({ notes: $event })"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Quantity</th>
|
||||
<th>Unit</th>
|
||||
<th>Price</th>
|
||||
<th class="text-right">Sum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<InvoiceRow v-for="(row, index) in invoice.rows" :errors="errors"
|
||||
:row="row" :index="index" :key="row.id"/>
|
||||
<tr class="d-print-none">
|
||||
<td colspan="5">
|
||||
<button class="btn btn-sm" @click="addRow">
|
||||
<i class="material-icons md-18 pointer">add</i>
|
||||
</button>
|
||||
<AppError :errors="errors" field="rows"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<InvoiceTotals :invoice="invoice" :errors="errors" @update="updateProp"/>
|
||||
</table>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<InvoiceBankDetails :invoice="invoice" :errors="errors" @update="updateProp"
|
||||
class="col-8"/>
|
||||
<InvoiceContactDetails :invoice="invoice" :errors="errors" @update="updateProp"
|
||||
class="col-4 text-right"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import InvoiceRow from '@/components/invoices/InvoiceRow';
|
||||
import InvoiceClientDetails from '@/components/invoices/InvoiceClientDetails';
|
||||
import InvoiceCompanyDetails from '@/components/invoices/InvoiceCompanyDetails';
|
||||
import InvoiceBankDetails from '@/components/invoices/InvoiceBankDetails';
|
||||
import InvoiceContactDetails from '@/components/invoices/InvoiceContactDetails';
|
||||
import InvoiceHeader from '@/components/invoices/InvoiceHeader';
|
||||
import InvoiceTotals from '@/components/invoices/InvoiceTotals';
|
||||
import AppEditable from '@/components/form/AppEditable';
|
||||
import AppError from '@/components/form/AppError';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InvoiceTotals,
|
||||
InvoiceHeader,
|
||||
InvoiceContactDetails,
|
||||
InvoiceBankDetails,
|
||||
InvoiceCompanyDetails,
|
||||
InvoiceRow,
|
||||
InvoiceClientDetails,
|
||||
AppEditable,
|
||||
AppError,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
errors: state => state.invoices.errors,
|
||||
}),
|
||||
...mapGetters({
|
||||
team: 'teams/team',
|
||||
invoice: 'invoices/invoice',
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
'$route.params.id'() {
|
||||
this.getInvoice();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getInvoice();
|
||||
},
|
||||
methods: {
|
||||
getInvoice() {
|
||||
this.$store.dispatch('invoices/getInvoice', this.$route.params.id);
|
||||
},
|
||||
updateProp(props) {
|
||||
this.$store.dispatch('invoices/updateInvoice', props);
|
||||
},
|
||||
addRow() {
|
||||
this.$store.dispatch('invoices/addRow');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
73
src/components/invoices/InvoiceHeader.vue
Normal file
73
src/components/invoices/InvoiceHeader.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3>
|
||||
Invoice
|
||||
<AppEditable :value="invoice.number"
|
||||
:errors="errors"
|
||||
field="number"
|
||||
placeholder="NO."
|
||||
@change="updateProp({ number: $event })"/>
|
||||
</h3>
|
||||
Issued at: <span class="editable__item" v-b-modal.modal_issued_at>{{ invoice.issued_at | date('D. MMM YYYY', 'YYYY-MM-DD') }}</span>
|
||||
<BModal id="modal_issued_at"
|
||||
centered
|
||||
title="Issued at"
|
||||
hide-footer
|
||||
size="sm"
|
||||
content-class="bg-base dp--24">
|
||||
<AppDatePicker :value="invoice.issued_at"
|
||||
@change="updateProp({ issued_at: $event })"
|
||||
:errors="errors"
|
||||
:inline="true"
|
||||
field="issued_at"/>
|
||||
</BModal>
|
||||
<br>Due at: <span class="editable__item" v-b-modal.modal_due_at>{{ invoice.due_at | date('D. MMM YYYY', 'YYYY-MM-DD') }}</span>
|
||||
<BModal id="modal_due_at"
|
||||
centered
|
||||
title="Due at"
|
||||
hide-footer
|
||||
size="sm"
|
||||
content-class="bg-base dp--24">
|
||||
<AppDatePicker :value="invoice.due_at"
|
||||
@change="updateProp({ due_at: $event })"
|
||||
:errors="errors"
|
||||
:inline="true"
|
||||
field="due_at"/>
|
||||
</BModal>
|
||||
<br>Late fee:
|
||||
<AppEditable :value="invoice.late_fee | currency"
|
||||
:errors="errors"
|
||||
suffix="%"
|
||||
field="late_fee"
|
||||
placeholder="Add late fee"
|
||||
@change="updateProp({ late_fee: $event })"/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { BModal, VBModal } from 'bootstrap-vue';
|
||||
import AppEditable from '@/components/form/AppEditable';
|
||||
import AppDatePicker from '@/components/form/AppDatePicker';
|
||||
import { formatDate } from '@/filters/date.filter';
|
||||
import { formatCurrency } from '@/filters/currency.filter';
|
||||
|
||||
export default {
|
||||
props: ['invoice', 'errors'],
|
||||
components: {
|
||||
AppEditable,
|
||||
AppDatePicker,
|
||||
BModal,
|
||||
},
|
||||
directives: {
|
||||
'b-modal': VBModal,
|
||||
},
|
||||
filters: {
|
||||
date: formatDate,
|
||||
currency: formatCurrency,
|
||||
},
|
||||
methods: {
|
||||
updateProp(props) {
|
||||
this.$emit('update', props);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
66
src/components/invoices/InvoiceRow.vue
Normal file
66
src/components/invoices/InvoiceRow.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<tr>
|
||||
<td>
|
||||
<AppEditable :value="row.item"
|
||||
:errors="errors"
|
||||
:field="`rows.${index}.item`"
|
||||
placeholder="Enter item"
|
||||
@change="updateProp({ item: $event })"/>
|
||||
</td>
|
||||
<td>
|
||||
<AppEditable :value="row.quantity"
|
||||
:errors="errors"
|
||||
:field="`rows.${index}.quantity`"
|
||||
placeholder="Enter quantity"
|
||||
@change="updateProp({ quantity: $event })"/>
|
||||
</td>
|
||||
<td>
|
||||
<AppEditable :value="row.unit"
|
||||
:errors="errors"
|
||||
:field="`rows.${index}.unit`"
|
||||
placeholder="Enter unit"
|
||||
@change="updateProp({ unit: $event })"/>
|
||||
</td>
|
||||
<td>
|
||||
<AppEditable :value="row.price | currency"
|
||||
:errors="errors"
|
||||
:field="`rows.${index}.price`"
|
||||
placeholder="Enter price"
|
||||
@change="updateProp({ price: $event })"/>
|
||||
</td>
|
||||
<td class="text-right position-relative">
|
||||
{{ (row.quantity * row.price) | currency }}
|
||||
<button class="btn btn-sm remove-invoice-row d-print-none" @click="removeRow(row)">
|
||||
<i class="material-icons md-18 pointer">remove</i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatCurrency } from '../../filters/currency.filter';
|
||||
import AppEditable from '../form/AppEditable';
|
||||
|
||||
export default {
|
||||
props: ['row', 'errors', 'index'],
|
||||
name: 'InvoiceRow',
|
||||
components: {
|
||||
AppEditable,
|
||||
},
|
||||
filters: {
|
||||
currency: formatCurrency,
|
||||
},
|
||||
methods: {
|
||||
updateProp(props) {
|
||||
this.$store.dispatch('invoices/updateInvoiceRow', {
|
||||
props,
|
||||
id: this.row.id,
|
||||
});
|
||||
},
|
||||
async removeRow(row) {
|
||||
await this.$store.dispatch('invoices/removeRow', row);
|
||||
this.updateProp();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
62
src/components/invoices/InvoiceTotals.vue
Normal file
62
src/components/invoices/InvoiceTotals.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<tfoot>
|
||||
<tr class="text-right">
|
||||
<td colspan="4">Subtotal</td>
|
||||
<td>{{ subTotal | currency }}</td>
|
||||
</tr>
|
||||
<tr class="text-right">
|
||||
<td colspan="4">
|
||||
VAT
|
||||
(<AppEditable :value="invoice.vat_rate | currency"
|
||||
suffix="%"
|
||||
placeholder="Add VAT"
|
||||
@change="updateProp({ vat_rate: $event })"/>)
|
||||
<AppError :errors="errors" field="vat_rate"/>
|
||||
</td>
|
||||
<td>{{ totalVat | currency }}</td>
|
||||
</tr>
|
||||
<tr class="text-right">
|
||||
<th colspan="4">
|
||||
Total
|
||||
<AppEditable :value="invoice.currency"
|
||||
:errors="errors"
|
||||
field="currency"
|
||||
placeholder="Add currency"
|
||||
@change="updateProp({ currency: $event })"/>
|
||||
</th>
|
||||
<th class="text-nowrap">{{ total | currency }}</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import AppError from '@/components/form/AppError';
|
||||
import AppEditable from '../form/AppEditable';
|
||||
import { formatDate } from '../../filters/date.filter';
|
||||
import { formatCurrency } from '../../filters/currency.filter';
|
||||
|
||||
export default {
|
||||
props: ['invoice', 'errors'],
|
||||
components: {
|
||||
AppEditable,
|
||||
AppError,
|
||||
},
|
||||
filters: {
|
||||
date: formatDate,
|
||||
currency: formatCurrency,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
rows: 'invoices/rows',
|
||||
subTotal: 'invoices/subTotal',
|
||||
total: 'invoices/total',
|
||||
totalVat: 'invoices/totalVat',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
updateProp(props) {
|
||||
this.$emit('update', props);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
84
src/components/invoices/InvoicesList.vue
Normal file
84
src/components/invoices/InvoicesList.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!invoices" class="col-12">Loading</div>
|
||||
<table class="table table--card table-hover" v-else-if="invoices && invoices.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No.</th>
|
||||
<th>Client</th>
|
||||
<th>Issued at</th>
|
||||
<th>Total</th>
|
||||
<th class="text-right">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-if="invoices">
|
||||
<tr v-for="invoice in invoices"
|
||||
class="pointer"
|
||||
:key="invoice.id"
|
||||
@click="openInvoice(invoice)">
|
||||
<td>{{ invoice.number }}</td>
|
||||
<td>{{ invoice.client ? invoice.client.company_name : '' }}</td>
|
||||
<td>{{ invoice.issued_at | date('D MMM YYYY', 'YYYY-MM-DD') }}</td>
|
||||
<td>
|
||||
{{ invoice.total | currency }}
|
||||
<small v-if="invoice.vat_rate"><br>({{ totalWithVat(invoice) | currency }})</small>
|
||||
</td>
|
||||
<td class="text-right text-capitalize">
|
||||
<i class="material-icons material-icons-round md-18 mr-2 text-warning"
|
||||
v-if="isOverDue(invoice)"
|
||||
v-b-tooltip.hover title="Overdue">warning</i>
|
||||
<i class="material-icons material-icons-round md-18 mr-2 text-success"
|
||||
v-else-if="invoice.status === 'paid'">done</i>
|
||||
{{ invoice.status }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<EmptyState v-else/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { formatDate } from '@/filters/date.filter';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { formatCurrency } from '@/filters/currency.filter';
|
||||
import dayjs from 'dayjs';
|
||||
import { VBTooltip } from 'bootstrap-vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmptyState,
|
||||
},
|
||||
filters: {
|
||||
date: formatDate,
|
||||
currency: formatCurrency,
|
||||
},
|
||||
directives: {
|
||||
'b-tooltip': VBTooltip,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
invoices: 'invoices/all',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('invoices/getInvoices');
|
||||
},
|
||||
methods: {
|
||||
openInvoice(invoice) {
|
||||
this.$store.commit('invoices/invoiceId', invoice.id);
|
||||
this.$router.push({
|
||||
name: 'invoice',
|
||||
params: { id: invoice.id },
|
||||
});
|
||||
},
|
||||
totalWithVat(invoice) {
|
||||
return (invoice.vat_rate / 100 * invoice.total) + invoice.total;
|
||||
},
|
||||
isOverDue(invoice) {
|
||||
return invoice.status === 'sent' && invoice.due_at < dayjs()
|
||||
.format();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user