Init commit

This commit is contained in:
Marek Fraczyk
2021-02-16 16:24:22 +02:00
parent 056a817632
commit 79e9705b01
106 changed files with 18400 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>