diff --git a/src/assets/scss/_tabs.scss b/src/assets/scss/_tabs.scss new file mode 100644 index 0000000..100785d --- /dev/null +++ b/src/assets/scss/_tabs.scss @@ -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; + } + } +} diff --git a/src/assets/scss/app.scss b/src/assets/scss/app.scss index 4aaa47b..11786be 100644 --- a/src/assets/scss/app.scss +++ b/src/assets/scss/app.scss @@ -13,6 +13,7 @@ @import "snackbars"; @import "surfaces"; @import "tables"; +@import "tabs"; @import "transitions"; @import "type"; @import "utilities"; diff --git a/src/components/clients/ClientFields.vue b/src/components/clients/ClientFields.vue new file mode 100644 index 0000000..f533537 --- /dev/null +++ b/src/components/clients/ClientFields.vue @@ -0,0 +1,70 @@ + + diff --git a/src/components/clients/ClientForm.vue b/src/components/clients/ClientForm.vue index 1556c11..1f0f25b 100644 --- a/src/components/clients/ClientForm.vue +++ b/src/components/clients/ClientForm.vue @@ -1,94 +1,103 @@ diff --git a/src/components/invoices/InvoiceClientFields.vue b/src/components/invoices/InvoiceClientFields.vue new file mode 100644 index 0000000..8d517b8 --- /dev/null +++ b/src/components/invoices/InvoiceClientFields.vue @@ -0,0 +1,29 @@ + + diff --git a/src/components/invoices/InvoiceForm.vue b/src/components/invoices/InvoiceForm.vue index 1a516a0..8cf4e42 100644 --- a/src/components/invoices/InvoiceForm.vue +++ b/src/components/invoices/InvoiceForm.vue @@ -137,7 +137,7 @@ export default { this.$store.dispatch('invoices/updateInvoice', props); }, addRow() { - this.$store.dispatch('invoices/addRow'); + this.$store.dispatch('invoiceRows/addRow', this.invoice.id); }, updateTeam(props) { this.$store.dispatch('teams/updateTeam', props); diff --git a/src/components/invoices/InvoiceRow.vue b/src/components/invoices/InvoiceRow.vue index aaf6fa8..4a8a5be 100644 --- a/src/components/invoices/InvoiceRow.vue +++ b/src/components/invoices/InvoiceRow.vue @@ -52,13 +52,13 @@ export default { }, methods: { updateProp(props) { - this.$store.dispatch('invoices/updateInvoiceRow', { + this.$store.dispatch('invoiceRows/updateInvoiceRow', { props, id: this.row.id, }); }, async removeRow(row) { - await this.$store.dispatch('invoices/removeRow', row); + await this.$store.dispatch('invoiceRows/removeRow', row.id); this.updateProp(); }, }, diff --git a/src/store/client-fields.js b/src/store/client-fields.js new file mode 100644 index 0000000..390f7fd --- /dev/null +++ b/src/store/client-fields.js @@ -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 + }, + }, +}; diff --git a/src/store/clients.js b/src/store/clients.js index efda1b8..b506e40 100644 --- a/src/store/clients.js +++ b/src/store/clients.js @@ -47,11 +47,15 @@ export default { }); }, async updateClient({ getters, dispatch }, props) { - await dispatch('clientProps', props); + if (props) { + await dispatch('clientProps', props); + } return ClientService.updateClient(getters.client); }, async updateClientById(store, payload) { - const client = Client.find(payload.clientId); + const client = Client.query() + .with('fields') + .find(payload.clientId); client.$update(payload.props); return ClientService.updateClient(client); }, @@ -60,24 +64,22 @@ export default { commit('clientId', client.id); commit('isModalOpen', true); }, - async deleteClient(clientId) { + async deleteClient(store, clientId) { const res = await ClientService.deleteClient(clientId); - if ('client_id' in res) { - Client.delete(res.client_id); - } + await Client.delete(clientId); return res; }, }, getters: { client(state) { return Client.query() - .with(['bank_account']) + .with(['bank_account', 'fields']) .find(state.clientId); }, all() { return Client.query() .where('$isNew', false) - .with(['bank_account']) + .with(['bank_account', 'fields']) .get(); }, }, diff --git a/src/store/invoice-client-fields.js b/src/store/invoice-client-fields.js new file mode 100644 index 0000000..35826ff --- /dev/null +++ b/src/store/invoice-client-fields.js @@ -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); + }, + }, +}; diff --git a/src/store/invoice-rows.js b/src/store/invoice-rows.js new file mode 100644 index 0000000..196eb25 --- /dev/null +++ b/src/store/invoice-rows.js @@ -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); + }, + }, +}; diff --git a/src/store/invoices.js b/src/store/invoices.js index ce91e75..c69b6be 100644 --- a/src/store/invoices.js +++ b/src/store/invoices.js @@ -1,6 +1,5 @@ import InvoiceService from '@/services/invoice.service'; import Invoice from '@/store/models/invoice'; -import InvoiceRow from '@/store/models/invoice-row'; import { pick } from '@/utils/helpers'; import dayjs from 'dayjs'; import Errors from '@/utils/errors'; @@ -55,16 +54,7 @@ export default { data: props, }); }, - invoiceRowProps(store, payload) { - return InvoiceRow.update({ - where: payload.id, - data: payload.props, - }); - }, - async updateInvoice({ getters, dispatch, commit }, props) { - await dispatch('invoiceProps', props); - - // Update client + async updateClient({ getters, dispatch }, props) { const clientProps = pick(props, { bank_account_id: 'bank_account_id', client_name: 'company_name', @@ -73,8 +63,6 @@ export default { client_country: 'company_country', client_county: 'company_county', client_city: 'company_city', - client_reg_no: 'company_reg_no', - client_vat_no: 'company_vat_no', client_email: 'invoice_email', currency: 'currency', }); @@ -88,7 +76,8 @@ export default { clientId: getters.invoice.client_id, }, { root: true }); } - + }, + async updateTeam({ getters, dispatch }, props) { const teamProps = pick(props, { late_fee: 'invoice_late_fee', from_name: 'company_name', @@ -119,37 +108,23 @@ export default { if (Object.keys(teamProps).length > 0) { 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) { - await dispatch('invoiceRowProps', payload); + async updateInvoice({ dispatch, commit, getters }, props) { + if (props) { + await dispatch('invoiceProps', props); + await dispatch('updateClient', props); + await dispatch('updateTeam', props); + } commit('clearErrors'); - return InvoiceService.updateInvoice(getters.invoice) .catch(err => commit('setErrors', err.errors)); }, async deleteInvoice(store, invoice) { const res = await InvoiceService.deleteInvoice(invoice.id); - if ('invoice_id' in res) { - Invoice.delete(res.invoice_id); - } + await Invoice.delete(invoice.id); 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 }) { commit('clearErrors'); @@ -164,7 +139,7 @@ export default { getters: { invoice(state) { return Invoice.query() - .with(['client']) + .with(['client', 'client_fields']) .with('rows', query => query.orderBy('order', 'asc')) .find(state.invoiceId); }, diff --git a/src/store/models/client-field.js b/src/store/models/client-field.js new file mode 100644 index 0000000..799f3d3 --- /dev/null +++ b/src/store/models/client-field.js @@ -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(''), + }; + } +} diff --git a/src/store/models/client.js b/src/store/models/client.js index 1c13f16..f5cdc36 100644 --- a/src/store/models/client.js +++ b/src/store/models/client.js @@ -1,6 +1,7 @@ import { Model } from '@vuex-orm/core'; import { uuidv4 } from '@/utils/helpers'; import BankAccount from '@/store/models/bank-account'; +import ClientField from '@/store/models/client-field'; export default class Client extends Model { // 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_county: this.attr(''), company_city: this.attr(''), - company_reg_no: this.attr(''), - company_vat_no: this.attr(''), has_vat: this.attr(null), currency: this.attr(null), rate: this.attr(null), invoice_email: this.attr(''), 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(''), created_at: this.attr(''), }; diff --git a/src/store/models/invoice-client-field.js b/src/store/models/invoice-client-field.js new file mode 100644 index 0000000..ed575cd --- /dev/null +++ b/src/store/models/invoice-client-field.js @@ -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(''), + }; + } +} diff --git a/src/store/models/invoice.js b/src/store/models/invoice.js index 5a5e252..14bb2f2 100644 --- a/src/store/models/invoice.js +++ b/src/store/models/invoice.js @@ -2,6 +2,7 @@ import { Model } from '@vuex-orm/core'; 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'; export default class Invoice extends Model { // 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_county: this.attr(''), client_city: this.attr(''), - client_reg_no: this.attr(''), - client_vat_no: this.attr(''), client_email: this.attr(''), client_id: this.attr(null), client: this.belongsTo(Client, 'client_id'), @@ -46,6 +45,7 @@ export default class Invoice extends Model { updated_at: this.attr(''), created_at: this.attr(''), total: this.attr(null), // Only used in lists. + client_fields: this.hasMany(InvoiceClientField, 'invoice_id'), }; } diff --git a/src/store/models/team-field.js b/src/store/models/team-field.js new file mode 100644 index 0000000..85e01de --- /dev/null +++ b/src/store/models/team-field.js @@ -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(''), + }; + } +} diff --git a/src/store/models/team.js b/src/store/models/team.js index cdfb62e..6d60f44 100644 --- a/src/store/models/team.js +++ b/src/store/models/team.js @@ -1,5 +1,6 @@ import { Model } from '@vuex-orm/core'; import { uuidv4 } from '@/utils/helpers'; +import TeamField from '@/store/models/team-field'; export default class Team extends Model { // 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), invoice_late_fee: this.attr(null), invoice_due_days: this.attr(null), + fields: this.hasMany(TeamField, 'team_id'), updated_at: this.attr(''), created_at: this.attr(''), logo_url: this.attr(''), diff --git a/src/store/store.js b/src/store/store.js index 22298b3..6fff037 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -9,10 +9,16 @@ import InvoiceRow from '@/store/models/invoice-row'; import Team from '@/store/models/team'; import bankAccounts from '@/store/bank-accounts'; import clients from '@/store/clients'; +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 teams from '@/store/teams'; 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'; Vue.use(Vuex); @@ -20,8 +26,11 @@ VuexORM.use(VuexORMisDirtyPlugin); const database = new VuexORM.Database(); database.register(Team); +database.register(TeamField); database.register(Client); +database.register(ClientField); database.register(Invoice); +database.register(InvoiceClientField); database.register(InvoiceRow); database.register(BankAccount); @@ -30,7 +39,10 @@ export default new Vuex.Store({ modules: { bankAccounts, clients, + clientFields, invoices, + invoiceRows, + invoiceClientFields, teams, themes, data,