mirror of
https://github.com/mokuappio/serverless-invoices.git
synced 2025-10-27 07:51:08 -04:00
Abstract footer to separate component. Abstract logo to separate component. Be able to edit team in modal. Add custom fields to team. Removed vat and reg no from team (replaced by custom fields). Custom fields are also stored on the invoice. When creating invoice add team custom fields to invoice as well. Be able to set default invoice due date, late fee, vat rate and currency.
This commit is contained in:
64
src/components/TheFooter.vue
Normal file
64
src/components/TheFooter.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<footer class="col-12 d-flex justify-content-between align-items-center text-secondary px-0 mt-3 d-print-none">
|
||||
<button class="btn btn-sm text-secondary" @click="toggleTheme">
|
||||
Lights {{ theme === 'dark' ? 'on' : 'off' }}
|
||||
<i class="material-icons material-icons-round md-14 align-text-bottom ml-1">
|
||||
{{ theme === 'dark' ? 'wb_sunny' : 'brightness_2' }}
|
||||
</i>
|
||||
</button>
|
||||
<div>
|
||||
<small v-b-tooltip.hover
|
||||
title="All your data is saved in your browser and not on any server.
|
||||
This application is truly serverless and only you have access to your data."
|
||||
class="pointer">
|
||||
What about my data?
|
||||
</small>
|
||||
<small class="pl-2">
|
||||
Made with
|
||||
<i class="material-icons material-icons-round md-14 align-text-bottom">favorite</i>
|
||||
by
|
||||
<a href="https://mokuapp.io/" class="text-secondary" target="_blank">Moku</a>.
|
||||
</small>
|
||||
<a href="https://github.com/mokuappio/serverless-invoices"
|
||||
class="btn btn-sm btn--icon ml-2"
|
||||
target="_blank">
|
||||
<img src="@/assets/img/github.png"
|
||||
alt="Serverless Invoices Github"
|
||||
v-if="theme === 'dark'">
|
||||
<img src="@/assets/img/github-dark.png"
|
||||
alt="Serverless Invoices Github"
|
||||
v-else>
|
||||
</a>
|
||||
<a href="https://app.mokuapp.io/"
|
||||
class="btn btn-sm btn-primary ml-2"
|
||||
target="_blank">Upgrade</a>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { VBTooltip } from 'bootstrap-vue';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
'b-tooltip': VBTooltip,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
theme: state => state.themes.theme,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
toggleTheme() {
|
||||
if (this.theme === 'light') {
|
||||
this.$store.commit('themes/theme', 'dark');
|
||||
} else {
|
||||
this.$store.commit('themes/theme', 'light');
|
||||
}
|
||||
localStorage.setItem('theme', this.theme);
|
||||
document.documentElement.setAttribute('data-theme', this.theme);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<strong>
|
||||
<strong class="break-line">
|
||||
<AppEditable :value="invoice.from_name"
|
||||
:errors="errors"
|
||||
field="from_name"
|
||||
placeholder="Your company name"
|
||||
class="break-line"
|
||||
@change="updateProp({ from_name: $event })"/>
|
||||
<i class="material-icons md-18 ml-2 pointer d-print-none" @click="editTeam">edit</i>
|
||||
</strong>
|
||||
<AppEditable :value="invoice.from_address"
|
||||
suffix=", "
|
||||
@ -35,20 +35,8 @@
|
||||
<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 })"/>
|
||||
<InvoiceTeamFields :invoice="invoice"/>
|
||||
|
||||
<AppEditable :value="invoice.from_email"
|
||||
:errors="errors"
|
||||
field="from_email"
|
||||
@ -58,6 +46,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import AppError from '@/components/form/AppError';
|
||||
import InvoiceTeamFields from '@/components/invoices/InvoiceTeamFields';
|
||||
import AppEditable from '../form/AppEditable';
|
||||
|
||||
export default {
|
||||
@ -65,11 +54,15 @@ export default {
|
||||
components: {
|
||||
AppEditable,
|
||||
AppError,
|
||||
InvoiceTeamFields,
|
||||
},
|
||||
methods: {
|
||||
updateProp(props) {
|
||||
this.$emit('update', props);
|
||||
},
|
||||
editTeam() {
|
||||
this.$store.commit('teams/isModalOpen', true);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -2,15 +2,7 @@
|
||||
<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"
|
||||
v-b-modal.team_logo_url
|
||||
:src="team.logo_url" style="width:100%; max-width:200px;">
|
||||
<button class="btn btn-sm" v-b-modal.team_logo_url v-else>
|
||||
<i class="material-icons material-icons-round md-36">file_upload</i>
|
||||
</button>
|
||||
<AppError :errors="errors" field="logo_url"/>
|
||||
</div>
|
||||
<TeamLogo class="col-sm-4" :errors="errors"/>
|
||||
<InvoiceHeader :invoice="invoice" :errors="errors" @update="updateProp"
|
||||
class="col-8 text-right mb-2"/>
|
||||
</div>
|
||||
@ -60,23 +52,6 @@
|
||||
class="col-4 text-right"/>
|
||||
</div>
|
||||
</div>
|
||||
<BModal id="team_logo_url"
|
||||
centered
|
||||
title="Choose logo"
|
||||
hide-footer
|
||||
size="sm"
|
||||
content-class="bg-base dp--24 text-center">
|
||||
<AppFileInput accept="image/*" class="mb-4" @selected="logoSelected"
|
||||
button-text="Select from files" output-type="base64"/>
|
||||
or
|
||||
<AppInput :value="team.logo_url"
|
||||
class="mt-4"
|
||||
@change="updateTeam({ logo_url: $event })"
|
||||
label="Insert web url"
|
||||
field="logo_url"
|
||||
:errors="errors"
|
||||
type="url"/>
|
||||
</BModal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@ -90,16 +65,11 @@ import InvoiceHeader from '@/components/invoices/InvoiceHeader';
|
||||
import InvoiceTotals from '@/components/invoices/InvoiceTotals';
|
||||
import AppEditable from '@/components/form/AppEditable';
|
||||
import AppError from '@/components/form/AppError';
|
||||
import { BModal, VBModal } from 'bootstrap-vue';
|
||||
import AppInput from '@/components/form/AppInput';
|
||||
import AppFileInput from '@/components/form/AppFileInput';
|
||||
import TeamLogo from '@/components/team/TeamLogo';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
'b-modal': VBModal,
|
||||
},
|
||||
components: {
|
||||
AppFileInput,
|
||||
TeamLogo,
|
||||
InvoiceTotals,
|
||||
InvoiceHeader,
|
||||
InvoiceContactDetails,
|
||||
@ -109,15 +79,12 @@ export default {
|
||||
InvoiceClientDetails,
|
||||
AppEditable,
|
||||
AppError,
|
||||
AppInput,
|
||||
BModal,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
errors: state => state.invoices.errors,
|
||||
}),
|
||||
...mapGetters({
|
||||
team: 'teams/team',
|
||||
invoice: 'invoices/invoice',
|
||||
}),
|
||||
},
|
||||
@ -139,16 +106,6 @@ export default {
|
||||
addRow() {
|
||||
this.$store.dispatch('invoiceRows/addRow', this.invoice.id);
|
||||
},
|
||||
updateTeam(props) {
|
||||
this.$store.dispatch('teams/updateTeam', props);
|
||||
},
|
||||
logoSelected(payload) {
|
||||
this.errors.clear();
|
||||
if (payload.size / 1000 > 512) {
|
||||
return this.errors.set({ logo_url: ['Logo has to be under 512kb.'] });
|
||||
}
|
||||
this.updateTeam({ logo_url: payload.content });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
29
src/components/invoices/InvoiceTeamFields.vue
Normal file
29
src/components/invoices/InvoiceTeamFields.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="field in invoice.team_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('invoiceTeamFields/updateInvoiceTeamField', {
|
||||
props,
|
||||
fieldId: field.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
70
src/components/team/TeamFields.vue
Normal file
70
src/components/team/TeamFields.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="field in team.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: ['team'],
|
||||
components: {
|
||||
AppEditable,
|
||||
AppInput,
|
||||
},
|
||||
computed: {
|
||||
isNew() {
|
||||
return this.team && this.team.$isNew;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addNewField() {
|
||||
this.$store.dispatch('teamFields/addNewField', this.team.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('teamFields/deleteTeamField', field.id);
|
||||
try {
|
||||
NotificationService.success('Deleted');
|
||||
} catch (err) {
|
||||
NotificationService.error(err.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateFieldProp(props, field) {
|
||||
if (this.isNew) {
|
||||
return this.$store.dispatch('teamFields/teamFieldProps', {
|
||||
props,
|
||||
fieldId: field.id,
|
||||
});
|
||||
}
|
||||
this.$store.dispatch('teamFields/updateTeamField', {
|
||||
props,
|
||||
fieldId: field.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
111
src/components/team/TeamForm.vue
Normal file
111
src/components/team/TeamForm.vue
Normal file
@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex justify-content-between">
|
||||
<h4>Team</h4>
|
||||
<div v-if="team">
|
||||
<button class="btn btn-sm btn-primary"
|
||||
@click="$emit('done')">Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-tabs v-if="team" nav-class="nav-tabs--simple mb-4" active-tab-class="active" class="row">
|
||||
<b-tab title="General" class="col-12">
|
||||
<div class="row">
|
||||
<TeamLogo :errors="errors" class="col-sm-4"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<AppInput :value="team.company_name" @change="updateProp({ company_name: $event })"
|
||||
label="Company Name" field="company_name" :errors="errors" class="col-12"/>
|
||||
<AppInput :value="team.contact_email" @change="updateProp({ contact_email: $event })"
|
||||
label="Email" field="contact_email" :errors="errors" class="col-sm-7"/>
|
||||
<AppInput :value="team.contact_phone" @change="updateProp({ contact_phone: $event })"
|
||||
label="Phone" field="contact_phone" :errors="errors" class="col-sm-7"/>
|
||||
<AppInput :value="team.website" @change="updateProp({ website: $event })"
|
||||
label="Website" field="website" :errors="errors" class="col-sm-7"/>
|
||||
</div>
|
||||
|
||||
<TeamFields class="row" :team="team"/>
|
||||
</b-tab>
|
||||
|
||||
<b-tab title="Invoicing" class="col-12">
|
||||
<div class="row">
|
||||
<AppInput :value="team.vat_rate" @change="updateProp({ vat_rate: $event })" type="number"
|
||||
label="VAT rate" field="vat_rate" :errors="errors" class="col-sm-4"/>
|
||||
<AppInput :value="team.invoice_late_fee" @change="updateProp({ invoice_late_fee: $event })" type="number"
|
||||
label="Late fee (%)" field="invoice_late_fee" :errors="errors" class="col-sm-4"/>
|
||||
<AppInput :value="team.invoice_due_days" @change="updateProp({ invoice_due_days: $event })" type="number"
|
||||
label="Payment terms, days" field="invoice_due_days" :errors="errors" class="col-sm-4"/>
|
||||
<AppInput :value="team.currency" @change="updateProp({ currency: $event })"
|
||||
label="Default currency" field="currency" :errors="errors" class="col-sm-4"/>
|
||||
</div>
|
||||
</b-tab>
|
||||
|
||||
<b-tab title="Address" class="col-12">
|
||||
<div class="row">
|
||||
<AppInput :value="team.company_address" @change="updateProp({ company_address: $event })"
|
||||
label="Company Address" field="company_address" :errors="errors"
|
||||
class="col-12"/>
|
||||
<AppInput :value="team.company_postal_code"
|
||||
@change="updateProp({ company_postal_code: $event })"
|
||||
label="Postal code" field="company_postal_code" :errors="errors"
|
||||
class="col-sm-5"/>
|
||||
<AppInput :value="team.company_city" @change="updateProp({ company_city: $event })"
|
||||
label="City" field="company_city" :errors="errors" class="col-sm-7"/>
|
||||
<AppInput :value="team.company_county" @change="updateProp({ company_county: $event })"
|
||||
label="County/State" field="company_county" :errors="errors" class="col-sm-6"/>
|
||||
<AppInput :value="team.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="!team">Loading</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import {
|
||||
BTab, BTabs,
|
||||
} from 'bootstrap-vue';
|
||||
import NotificationService from '@/services/notification.service';
|
||||
import AppInput from '@/components/form/AppInput';
|
||||
import Errors from '@/utils/errors';
|
||||
import TeamFields from '@/components/team/TeamFields';
|
||||
import TeamLogo from '@/components/team/TeamLogo';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TeamLogo,
|
||||
TeamFields,
|
||||
AppInput,
|
||||
BTab,
|
||||
BTabs,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errors: new Errors(),
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
team: 'teams/team',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
updateProp(props) {
|
||||
this.errors.clear();
|
||||
|
||||
this.$store.dispatch('teams/updateTeam', props)
|
||||
.then(() => {
|
||||
NotificationService.success('Updated');
|
||||
})
|
||||
.catch(err => this.errors.set(err.errors));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
71
src/components/team/TeamLogo.vue
Normal file
71
src/components/team/TeamLogo.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div>
|
||||
<img v-if="team.logo_url"
|
||||
class="pointer"
|
||||
@click="openModal"
|
||||
:src="team.logo_url" style="width:100%; max-width:200px;">
|
||||
<button class="btn btn-sm" @click="openModal" v-else>
|
||||
<i class="material-icons material-icons-round md-36">file_upload</i>
|
||||
</button>
|
||||
<AppError :errors="errors" field="logo_url"/>
|
||||
<BModal v-model="isModalOpen"
|
||||
centered
|
||||
title="Choose logo"
|
||||
hide-footer
|
||||
size="sm"
|
||||
content-class="bg-base dp--24 text-center">
|
||||
<AppFileInput accept="image/*" class="mb-4" @selected="logoSelected"
|
||||
button-text="Select from files" output-type="base64"/>
|
||||
or
|
||||
<AppInput :value="team.logo_url"
|
||||
class="mt-4"
|
||||
@change="updateTeam({ logo_url: $event })"
|
||||
label="Insert web url"
|
||||
field="logo_url"
|
||||
:errors="errors"
|
||||
type="url"/>
|
||||
</BModal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import AppError from '@/components/form/AppError';
|
||||
import { BModal } from 'bootstrap-vue';
|
||||
import AppInput from '@/components/form/AppInput';
|
||||
import AppFileInput from '@/components/form/AppFileInput';
|
||||
|
||||
export default {
|
||||
props: ['errors'],
|
||||
components: {
|
||||
AppFileInput,
|
||||
AppError,
|
||||
AppInput,
|
||||
BModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isModalOpen: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
team: 'teams/team',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
updateTeam(props) {
|
||||
this.$store.dispatch('teams/updateTeam', props);
|
||||
},
|
||||
logoSelected(payload) {
|
||||
this.errors.clear();
|
||||
if (payload.size / 1000 > 512) {
|
||||
return this.errors.set({ logo_url: ['Logo has to be under 512kb.'] });
|
||||
}
|
||||
this.updateTeam({ logo_url: payload.content });
|
||||
},
|
||||
openModal() {
|
||||
this.isModalOpen = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
46
src/components/team/TeamModal.vue
Normal file
46
src/components/team/TeamModal.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<BModal v-model="isOpen"
|
||||
centered
|
||||
hide-footer
|
||||
hide-header
|
||||
content-class="bg-base dp--24">
|
||||
<TeamForm @done="close()"/>
|
||||
</BModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { BModal } from 'bootstrap-vue';
|
||||
import TeamForm from '@/components/team/TeamForm';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TeamForm,
|
||||
BModal,
|
||||
},
|
||||
computed: {
|
||||
isOpen: {
|
||||
get() {
|
||||
return this.$store.state.teams.isModalOpen;
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('teams/isModalOpen', val);
|
||||
},
|
||||
},
|
||||
...mapGetters({
|
||||
team: 'teams/team',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.getTeam();
|
||||
},
|
||||
methods: {
|
||||
getTeam() {
|
||||
this.$store.dispatch('teams/getTeam');
|
||||
},
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,6 +1,8 @@
|
||||
import storage from 'localforage';
|
||||
import TeamService from '@/services/team.service';
|
||||
import { validate, generateInvoiceNumber, removeVuexORMFlags } from '@/utils/helpers';
|
||||
import {
|
||||
validate, generateInvoiceNumber, removeVuexORMFlags, uuidv4,
|
||||
} from '@/utils/helpers';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
class InvoiceService {
|
||||
@ -22,26 +24,33 @@ class InvoiceService {
|
||||
invoice.issued_at = dayjs()
|
||||
.format('YYYY-MM-DD');
|
||||
invoice.due_at = dayjs()
|
||||
.add(14, 'days')
|
||||
.add(team.invoice_due_days || 14, 'days')
|
||||
.format('YYYY-MM-DD');
|
||||
invoice.number = generateInvoiceNumber(invoices);
|
||||
invoice.late_fee = 0.5;
|
||||
invoice.late_fee = team.invoice_late_fee || 0.5;
|
||||
invoice.from_name = team.company_name;
|
||||
invoice.from_address = team.company_address;
|
||||
invoice.from_postal_code = team.company_postal_code;
|
||||
invoice.from_city = team.company_city;
|
||||
invoice.from_country = team.company_country;
|
||||
invoice.from_county = team.company_county;
|
||||
invoice.from_reg_no = team.company_reg_no;
|
||||
invoice.from_vat_no = team.company_vat_no;
|
||||
invoice.from_website = team.website;
|
||||
invoice.from_email = team.contact_email;
|
||||
invoice.from_phone = team.contact_phone;
|
||||
invoice.vat_rate = 0;
|
||||
invoice.currency = 'USD';
|
||||
invoice.vat_rate = team.vat_rate || 0;
|
||||
invoice.currency = team.currency || 'USD';
|
||||
|
||||
// Add custom fields
|
||||
invoice.team_fields = team.fields.map(field => ({
|
||||
id: uuidv4(),
|
||||
label: field.label,
|
||||
value: field.value,
|
||||
invoice_id: invoice.id,
|
||||
}));
|
||||
|
||||
delete invoice.client;
|
||||
|
||||
|
||||
return this.saveInvoice(invoice);
|
||||
}
|
||||
|
||||
|
||||
@ -11,8 +11,6 @@ class TeamService {
|
||||
company_country: null,
|
||||
company_county: null,
|
||||
company_city: null,
|
||||
company_reg_no: null,
|
||||
company_vat_no: null,
|
||||
website: null,
|
||||
contact_email: null,
|
||||
contact_phone: null,
|
||||
|
||||
31
src/store/invoice-team-fields.js
Normal file
31
src/store/invoice-team-fields.js
Normal file
@ -0,0 +1,31 @@
|
||||
import InvoiceTeamField from '@/store/models/invoice-team-field';
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {},
|
||||
mutations: {},
|
||||
actions: {
|
||||
init() {},
|
||||
terminate() {},
|
||||
invoiceTeamFieldProps(store, payload) {
|
||||
return InvoiceTeamField.update({
|
||||
where: payload.fieldId,
|
||||
data: payload.props,
|
||||
});
|
||||
},
|
||||
async updateInvoiceTeamField({ dispatch }, payload) {
|
||||
await dispatch('invoiceTeamFieldProps', payload);
|
||||
return dispatch('invoices/updateInvoice', null, { root: true });
|
||||
},
|
||||
async addInvoiceTeamField(store, payload) {
|
||||
const field = await InvoiceTeamField.createNew();
|
||||
await field.$update({
|
||||
...payload.props,
|
||||
invoice_id: payload.invoiceId,
|
||||
});
|
||||
},
|
||||
async removeInvoiceTeamField(store, fieldId) {
|
||||
await InvoiceTeamField.delete(fieldId);
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -86,8 +86,6 @@ export default {
|
||||
from_city: 'company_city',
|
||||
from_country: 'company_country',
|
||||
from_county: 'company_county',
|
||||
from_reg_no: 'company_reg_no',
|
||||
from_vat_no: 'company_vat_no',
|
||||
from_website: 'website',
|
||||
from_email: 'contact_email',
|
||||
from_phone: 'contact_phone',
|
||||
@ -139,7 +137,7 @@ export default {
|
||||
getters: {
|
||||
invoice(state) {
|
||||
return Invoice.query()
|
||||
.with(['client', 'client_fields'])
|
||||
.with(['client', 'client_fields', 'team_fields'])
|
||||
.with('rows', query => query.orderBy('order', 'asc'))
|
||||
.find(state.invoiceId);
|
||||
},
|
||||
|
||||
16
src/store/models/invoice-team-field.js
Normal file
16
src/store/models/invoice-team-field.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { Model } from '@vuex-orm/core';
|
||||
import { uuidv4 } from '@/utils/helpers';
|
||||
|
||||
export default class InvoiceTeamField extends Model {
|
||||
// This is the name used as module name of the Vuex Store.
|
||||
static entity = 'invoice_team_fields';
|
||||
|
||||
static fields() {
|
||||
return {
|
||||
id: this.attr(() => uuidv4()),
|
||||
invoice_id: this.attr(null),
|
||||
label: this.attr(''),
|
||||
value: this.attr(''),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ import { uuidv4 } from '@/utils/helpers';
|
||||
import Client from '@/store/models/client';
|
||||
import InvoiceRow from '@/store/models/invoice-row';
|
||||
import InvoiceClientField from '@/store/models/invoice-client-field';
|
||||
import InvoiceTeamField from '@/store/models/invoice-team-field';
|
||||
|
||||
export default class Invoice extends Model {
|
||||
// This is the name used as module name of the Vuex Store.
|
||||
@ -24,8 +25,6 @@ export default class Invoice extends Model {
|
||||
from_city: this.attr(''),
|
||||
from_country: this.attr(''),
|
||||
from_county: this.attr(''),
|
||||
from_reg_no: this.attr(''),
|
||||
from_vat_no: this.attr(''),
|
||||
from_website: this.attr(''),
|
||||
from_email: this.attr(''),
|
||||
from_phone: this.attr(''),
|
||||
@ -46,6 +45,7 @@ export default class Invoice extends Model {
|
||||
created_at: this.attr(''),
|
||||
total: this.attr(null), // Only used in lists.
|
||||
client_fields: this.hasMany(InvoiceClientField, 'invoice_id'),
|
||||
team_fields: this.hasMany(InvoiceTeamField, 'invoice_id'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -15,12 +15,11 @@ export default class Team extends Model {
|
||||
company_country: this.attr(''),
|
||||
company_county: this.attr(''),
|
||||
company_city: this.attr(''),
|
||||
company_reg_no: this.attr(''),
|
||||
company_vat_no: this.attr(''),
|
||||
website: this.attr(''),
|
||||
contact_email: this.attr(''),
|
||||
contact_phone: this.attr(''),
|
||||
vat_rate: this.attr(null),
|
||||
currency: this.attr(null),
|
||||
invoice_late_fee: this.attr(null),
|
||||
invoice_due_days: this.attr(null),
|
||||
fields: this.hasMany(TeamField, 'team_id'),
|
||||
|
||||
@ -13,12 +13,15 @@ import clientFields from '@/store/client-fields';
|
||||
import invoices from '@/store/invoices';
|
||||
import invoiceRows from '@/store/invoice-rows';
|
||||
import invoiceClientFields from '@/store/invoice-client-fields';
|
||||
import invoiceTeamFields from '@/store/invoice-team-fields';
|
||||
import teams from '@/store/teams';
|
||||
import teamFields from '@/store/team-fields';
|
||||
import themes from '@/store/themes';
|
||||
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';
|
||||
import InvoiceTeamField from '@/store/models/invoice-team-field';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
@ -31,6 +34,7 @@ database.register(Client);
|
||||
database.register(ClientField);
|
||||
database.register(Invoice);
|
||||
database.register(InvoiceClientField);
|
||||
database.register(InvoiceTeamField);
|
||||
database.register(InvoiceRow);
|
||||
database.register(BankAccount);
|
||||
|
||||
@ -43,7 +47,9 @@ export default new Vuex.Store({
|
||||
invoices,
|
||||
invoiceRows,
|
||||
invoiceClientFields,
|
||||
invoiceTeamFields,
|
||||
teams,
|
||||
teamFields,
|
||||
themes,
|
||||
data,
|
||||
},
|
||||
|
||||
31
src/store/team-fields.js
Normal file
31
src/store/team-fields.js
Normal file
@ -0,0 +1,31 @@
|
||||
import TeamField from '@/store/models/team-field';
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {},
|
||||
mutations: {},
|
||||
actions: {
|
||||
init() {},
|
||||
terminate() {},
|
||||
async teamFieldProps(store, payload) {
|
||||
return TeamField.update({
|
||||
where: payload.fieldId,
|
||||
data: payload.props,
|
||||
});
|
||||
},
|
||||
async updateTeamField({ dispatch }, payload) {
|
||||
await dispatch('teamFieldProps', payload);
|
||||
return dispatch('teams/updateTeam', null, { root: true });
|
||||
},
|
||||
async addNewField(store, teamId) {
|
||||
const field = await TeamField.createNew();
|
||||
field.$update({
|
||||
team_id: teamId,
|
||||
});
|
||||
},
|
||||
async deleteTeamField({ dispatch }, fieldId) {
|
||||
await TeamField.delete(fieldId);
|
||||
return dispatch('teams/updateTeam', null, { root: true });
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -3,8 +3,14 @@ import Team from '@/store/models/team';
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {},
|
||||
mutations: {},
|
||||
state: {
|
||||
isModalOpen: false,
|
||||
},
|
||||
mutations: {
|
||||
isModalOpen(state, isOpen) {
|
||||
state.isModalOpen = isOpen;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async init({ dispatch }) {
|
||||
await Promise.all([
|
||||
@ -39,12 +45,7 @@ export default {
|
||||
},
|
||||
getters: {
|
||||
team() {
|
||||
return Team.query().first();
|
||||
},
|
||||
all() {
|
||||
return Team.query()
|
||||
.where('$isNew', false)
|
||||
.get();
|
||||
return Team.query().with(['fields']).first();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -6,63 +6,33 @@
|
||||
<transition name="fade" mode="out-in">
|
||||
<router-view/>
|
||||
</transition>
|
||||
<footer class="col-12 d-flex justify-content-between align-items-center text-secondary px-0 mt-3 d-print-none">
|
||||
<button class="btn btn-sm text-secondary" @click="toggleTheme">
|
||||
Lights {{ theme === 'dark' ? 'on' : 'off' }}
|
||||
<i class="material-icons material-icons-round md-14 align-text-bottom ml-1">
|
||||
{{ theme === 'dark' ? 'wb_sunny' : 'brightness_2' }}
|
||||
</i>
|
||||
</button>
|
||||
<div>
|
||||
<small v-b-tooltip.hover
|
||||
title="All your data is saved in your browser and not on any server.
|
||||
This application is truly serverless and only you have access to your data."
|
||||
class="pointer">
|
||||
What about my data?
|
||||
</small>
|
||||
<small class="pl-2">
|
||||
Made with
|
||||
<i class="material-icons material-icons-round md-14 align-text-bottom">favorite</i>
|
||||
by
|
||||
<a href="https://mokuapp.io/" class="text-secondary" target="_blank">Moku</a>.
|
||||
</small>
|
||||
<a href="https://github.com/mokuappio/serverless-invoices"
|
||||
class="btn btn-sm btn--icon ml-2"
|
||||
target="_blank">
|
||||
<img src="@/assets/img/github.png"
|
||||
alt="Serverless Invoices Github"
|
||||
v-if="theme === 'dark'">
|
||||
<img src="@/assets/img/github-dark.png"
|
||||
alt="Serverless Invoices Github"
|
||||
v-else>
|
||||
</a>
|
||||
<a href="https://app.mokuapp.io/"
|
||||
class="btn btn-sm btn-primary ml-2"
|
||||
target="_blank">Upgrade</a>
|
||||
</div>
|
||||
</footer>
|
||||
<TheFooter/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ClientModal v-if="team"/>
|
||||
<TeamModal v-if="team"/>
|
||||
<BankAccountModal v-if="team"/>
|
||||
<ImportModal/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import { mapGetters } from 'vuex';
|
||||
import ClientModal from '@/components/clients/ClientModal';
|
||||
import BankAccountModal from '@/components/bank-accounts/BankAccountModal';
|
||||
import { VBTooltip } from 'bootstrap-vue';
|
||||
import TeamModal from '@/components/team/TeamModal';
|
||||
import TheFooter from '@/components/TheFooter';
|
||||
import ImportModal from '../../components/ImportModal';
|
||||
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
'b-tooltip': VBTooltip,
|
||||
},
|
||||
components: {
|
||||
TheFooter,
|
||||
TeamModal,
|
||||
ImportModal,
|
||||
BankAccountModal,
|
||||
ClientModal,
|
||||
@ -71,20 +41,6 @@ export default {
|
||||
...mapGetters({
|
||||
team: 'teams/team',
|
||||
}),
|
||||
...mapState({
|
||||
theme: state => state.themes.theme,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
toggleTheme() {
|
||||
if (this.theme === 'light') {
|
||||
this.$store.commit('themes/theme', 'dark');
|
||||
} else {
|
||||
this.$store.commit('themes/theme', 'light');
|
||||
}
|
||||
localStorage.setItem('theme', this.theme);
|
||||
document.documentElement.setAttribute('data-theme', this.theme);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user