* initial commit * add docs * black & prettier * mobile styles * add print view * prettier * make format * initial migrations un-messed * make migrations work for sqlite * add invoices table * clean migrations * add migration to conv * fix card size * hopefully fix test migration * add missing status * timestamp * init testing * remove draft invoice by default on create * what should i test * make format * raise if not invoice * new test and renaming * fix issue reported by @talvasconcelos which prevented users from setting status on creation * readme * run black * trying to make tests work * make it work again * send paid amount * partial pay flow * good coding * can't get these test to work * clean up and commenting * make format * validation for 2 decimals Co-authored-by: ben <ben@arc.wales> Co-authored-by: Tiago vasconcelos <talvasconcelos@gmail.com>
572 lines
14 KiB
HTML
572 lines
14 KiB
HTML
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
|
%} {% block page %}
|
|
<div class="row q-col-gutter-md">
|
|
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
|
<q-card>
|
|
<q-card-section>
|
|
<q-btn unelevated color="primary" @click="formDialog.show = true"
|
|
>New Invoice</q-btn
|
|
>
|
|
</q-card-section>
|
|
</q-card>
|
|
|
|
<q-card>
|
|
<q-card-section>
|
|
<div class="row items-center no-wrap q-mb-md">
|
|
<div class="col">
|
|
<h5 class="text-subtitle1 q-my-none">Invoices</h5>
|
|
</div>
|
|
<div class="col-auto">
|
|
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
|
</div>
|
|
</div>
|
|
<q-table
|
|
dense
|
|
flat
|
|
:data="invoices"
|
|
row-key="id"
|
|
:columns="invoicesTable.columns"
|
|
:pagination.sync="invoicesTable.pagination"
|
|
>
|
|
{% raw %}
|
|
<template v-slot:header="props">
|
|
<q-tr :props="props">
|
|
<q-th auto-width></q-th>
|
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
{{ col.label }}
|
|
</q-th>
|
|
<q-th auto-width></q-th>
|
|
</q-tr>
|
|
</template>
|
|
|
|
<template v-slot:body="props">
|
|
<q-tr :props="props">
|
|
<q-td auto-width>
|
|
<q-btn
|
|
unelevated
|
|
dense
|
|
size="xs"
|
|
icon="edit"
|
|
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
|
@click="showEditModal(props.row)"
|
|
></q-btn>
|
|
<q-btn
|
|
unelevated
|
|
dense
|
|
size="xs"
|
|
icon="launch"
|
|
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
|
type="a"
|
|
:href="'pay/' + props.row.id"
|
|
target="_blank"
|
|
></q-btn>
|
|
</q-td>
|
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
|
{{ col.value }}
|
|
</q-td>
|
|
</q-tr>
|
|
</template>
|
|
{% endraw %}
|
|
</q-table>
|
|
</q-card-section>
|
|
</q-card>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-5 q-gutter-y-md">
|
|
<q-card>
|
|
<q-card-section>
|
|
<h6 class="text-subtitle1 q-my-none">
|
|
{{SITE_TITLE}} Invoices extension
|
|
</h6>
|
|
</q-card-section>
|
|
<q-card-section class="q-pa-none">
|
|
<q-separator></q-separator>
|
|
<q-list> {% include "invoices/_api_docs.html" %} </q-list>
|
|
</q-card-section>
|
|
</q-card>
|
|
</div>
|
|
|
|
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
|
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
|
<q-form @submit="saveInvoice" class="q-gutter-md">
|
|
<q-select
|
|
filled
|
|
dense
|
|
emit-value
|
|
v-model="formDialog.data.wallet"
|
|
:options="g.user.walletOptions"
|
|
label="Wallet *"
|
|
></q-select>
|
|
<q-select
|
|
filled
|
|
dense
|
|
emit-value
|
|
v-model="formDialog.data.currency"
|
|
:options="currencyOptions"
|
|
label="Currency *"
|
|
></q-select>
|
|
<q-select
|
|
filled
|
|
dense
|
|
emit-value
|
|
v-model="formDialog.data.status"
|
|
:options="['draft', 'open', 'paid', 'canceled']"
|
|
label="Status *"
|
|
></q-select>
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.company_name"
|
|
label="Company Name"
|
|
placeholder="LNBits Labs"
|
|
></q-input>
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.first_name"
|
|
label="First Name"
|
|
placeholder="Satoshi"
|
|
></q-input>
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.last_name"
|
|
label="Last Name"
|
|
placeholder="Nakamoto"
|
|
></q-input>
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.email"
|
|
label="Email"
|
|
placeholder="satoshi@gmail.com"
|
|
></q-input>
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.phone"
|
|
label="Phone"
|
|
placeholder="+81 (012)-345-6789"
|
|
></q-input>
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="formDialog.data.address"
|
|
label="Address"
|
|
placeholder="1600 Pennsylvania Ave."
|
|
type="textarea"
|
|
></q-input>
|
|
|
|
<q-list bordered separator>
|
|
<q-item
|
|
clickable
|
|
v-ripple
|
|
v-for="(item, index) in formDialog.invoiceItems"
|
|
:key="index"
|
|
>
|
|
<q-item-section>
|
|
<q-input
|
|
filled
|
|
dense
|
|
label="Item"
|
|
placeholder="Jelly Beans"
|
|
v-model="formDialog.invoiceItems[index].description"
|
|
></q-input>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-input
|
|
filled
|
|
dense
|
|
label="Amount"
|
|
placeholder="4.20"
|
|
v-model="formDialog.invoiceItems[index].amount"
|
|
></q-input>
|
|
</q-item-section>
|
|
<q-item-section side>
|
|
<q-btn
|
|
unelevated
|
|
dense
|
|
size="xs"
|
|
icon="delete"
|
|
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
|
@click="formDialog.invoiceItems.splice(index, 1)"
|
|
></q-btn>
|
|
</q-item-section>
|
|
</q-item>
|
|
|
|
<q-item clickable v-ripple>
|
|
<q-btn flat icon="add" @click="formDialog.invoiceItems.push({})">
|
|
Add Line Item
|
|
</q-btn>
|
|
</q-item>
|
|
</q-list>
|
|
|
|
<div class="row q-mt-lg">
|
|
<q-btn
|
|
unelevated
|
|
color="primary"
|
|
:disable="formDialog.data.wallet == null || formDialog.data.currency == null"
|
|
type="submit"
|
|
v-if="typeof formDialog.data.id == 'undefined'"
|
|
>Create Invoice</q-btn
|
|
>
|
|
<q-btn
|
|
unelevated
|
|
color="primary"
|
|
:disable="formDialog.data.wallet == null || formDialog.data.currency == null"
|
|
type="submit"
|
|
v-if="typeof formDialog.data.id !== 'undefined'"
|
|
>Save Invoice</q-btn
|
|
>
|
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
>Cancel</q-btn
|
|
>
|
|
</div>
|
|
</q-form>
|
|
</q-card>
|
|
</q-dialog>
|
|
</div>
|
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
|
<script>
|
|
var mapInvoice = function (obj) {
|
|
obj.time = Quasar.utils.date.formatDate(
|
|
new Date(obj.time * 1000),
|
|
'YYYY-MM-DD HH:mm'
|
|
)
|
|
|
|
return obj
|
|
}
|
|
|
|
var mapInvoiceItems = function (obj) {
|
|
obj.amount = parseFloat(obj.amount / 100).toFixed(2)
|
|
|
|
return obj
|
|
}
|
|
|
|
new Vue({
|
|
el: '#vue',
|
|
mixins: [windowMixin],
|
|
data: function () {
|
|
return {
|
|
invoices: [],
|
|
currencyOptions: [
|
|
'USD',
|
|
'EUR',
|
|
'GBP',
|
|
'AED',
|
|
'AFN',
|
|
'ALL',
|
|
'AMD',
|
|
'ANG',
|
|
'AOA',
|
|
'ARS',
|
|
'AUD',
|
|
'AWG',
|
|
'AZN',
|
|
'BAM',
|
|
'BBD',
|
|
'BDT',
|
|
'BGN',
|
|
'BHD',
|
|
'BIF',
|
|
'BMD',
|
|
'BND',
|
|
'BOB',
|
|
'BRL',
|
|
'BSD',
|
|
'BTN',
|
|
'BWP',
|
|
'BYN',
|
|
'BZD',
|
|
'CAD',
|
|
'CDF',
|
|
'CHF',
|
|
'CLF',
|
|
'CLP',
|
|
'CNH',
|
|
'CNY',
|
|
'COP',
|
|
'CRC',
|
|
'CUC',
|
|
'CUP',
|
|
'CVE',
|
|
'CZK',
|
|
'DJF',
|
|
'DKK',
|
|
'DOP',
|
|
'DZD',
|
|
'EGP',
|
|
'ERN',
|
|
'ETB',
|
|
'EUR',
|
|
'FJD',
|
|
'FKP',
|
|
'GBP',
|
|
'GEL',
|
|
'GGP',
|
|
'GHS',
|
|
'GIP',
|
|
'GMD',
|
|
'GNF',
|
|
'GTQ',
|
|
'GYD',
|
|
'HKD',
|
|
'HNL',
|
|
'HRK',
|
|
'HTG',
|
|
'HUF',
|
|
'IDR',
|
|
'ILS',
|
|
'IMP',
|
|
'INR',
|
|
'IQD',
|
|
'IRR',
|
|
'IRT',
|
|
'ISK',
|
|
'JEP',
|
|
'JMD',
|
|
'JOD',
|
|
'JPY',
|
|
'KES',
|
|
'KGS',
|
|
'KHR',
|
|
'KMF',
|
|
'KPW',
|
|
'KRW',
|
|
'KWD',
|
|
'KYD',
|
|
'KZT',
|
|
'LAK',
|
|
'LBP',
|
|
'LKR',
|
|
'LRD',
|
|
'LSL',
|
|
'LYD',
|
|
'MAD',
|
|
'MDL',
|
|
'MGA',
|
|
'MKD',
|
|
'MMK',
|
|
'MNT',
|
|
'MOP',
|
|
'MRO',
|
|
'MUR',
|
|
'MVR',
|
|
'MWK',
|
|
'MXN',
|
|
'MYR',
|
|
'MZN',
|
|
'NAD',
|
|
'NGN',
|
|
'NIO',
|
|
'NOK',
|
|
'NPR',
|
|
'NZD',
|
|
'OMR',
|
|
'PAB',
|
|
'PEN',
|
|
'PGK',
|
|
'PHP',
|
|
'PKR',
|
|
'PLN',
|
|
'PYG',
|
|
'QAR',
|
|
'RON',
|
|
'RSD',
|
|
'RUB',
|
|
'RWF',
|
|
'SAR',
|
|
'SBD',
|
|
'SCR',
|
|
'SDG',
|
|
'SEK',
|
|
'SGD',
|
|
'SHP',
|
|
'SLL',
|
|
'SOS',
|
|
'SRD',
|
|
'SSP',
|
|
'STD',
|
|
'SVC',
|
|
'SYP',
|
|
'SZL',
|
|
'THB',
|
|
'TJS',
|
|
'TMT',
|
|
'TND',
|
|
'TOP',
|
|
'TRY',
|
|
'TTD',
|
|
'TWD',
|
|
'TZS',
|
|
'UAH',
|
|
'UGX',
|
|
'USD',
|
|
'UYU',
|
|
'UZS',
|
|
'VEF',
|
|
'VES',
|
|
'VND',
|
|
'VUV',
|
|
'WST',
|
|
'XAF',
|
|
'XAG',
|
|
'XAU',
|
|
'XCD',
|
|
'XDR',
|
|
'XOF',
|
|
'XPD',
|
|
'XPF',
|
|
'XPT',
|
|
'YER',
|
|
'ZAR',
|
|
'ZMW',
|
|
'ZWL'
|
|
],
|
|
invoicesTable: {
|
|
columns: [
|
|
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
|
{name: 'status', align: 'left', label: 'Status', field: 'status'},
|
|
{name: 'time', align: 'left', label: 'Created', field: 'time'},
|
|
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'},
|
|
{
|
|
name: 'currency',
|
|
align: 'left',
|
|
label: 'Currency',
|
|
field: 'currency'
|
|
},
|
|
{
|
|
name: 'company_name',
|
|
align: 'left',
|
|
label: 'Company Name',
|
|
field: 'company_name'
|
|
},
|
|
{
|
|
name: 'first_name',
|
|
align: 'left',
|
|
label: 'First Name',
|
|
field: 'first_name'
|
|
},
|
|
{
|
|
name: 'last_name',
|
|
align: 'left',
|
|
label: 'Last Name',
|
|
field: 'last_name'
|
|
},
|
|
{name: 'email', align: 'left', label: 'Email', field: 'email'},
|
|
{name: 'phone', align: 'left', label: 'Phone', field: 'phone'},
|
|
{name: 'address', align: 'left', label: 'Address', field: 'address'}
|
|
],
|
|
pagination: {
|
|
rowsPerPage: 10
|
|
}
|
|
},
|
|
formDialog: {
|
|
show: false,
|
|
data: {},
|
|
invoiceItems: []
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
closeFormDialog: function () {
|
|
this.formDialog.data = {}
|
|
this.formDialog.invoiceItems = []
|
|
},
|
|
showEditModal: function (obj) {
|
|
this.formDialog.data = obj
|
|
this.formDialog.show = true
|
|
|
|
this.getInvoice(obj.id)
|
|
},
|
|
getInvoice: function (invoice_id) {
|
|
var self = this
|
|
|
|
LNbits.api
|
|
.request('GET', '/invoices/api/v1/invoice/' + invoice_id)
|
|
.then(function (response) {
|
|
self.formDialog.invoiceItems = response.data.items.map(function (
|
|
obj
|
|
) {
|
|
return mapInvoiceItems(obj)
|
|
})
|
|
})
|
|
},
|
|
getInvoices: function () {
|
|
var self = this
|
|
|
|
LNbits.api
|
|
.request(
|
|
'GET',
|
|
'/invoices/api/v1/invoices?all_wallets=true',
|
|
this.g.user.wallets[0].inkey
|
|
)
|
|
.then(function (response) {
|
|
self.invoices = response.data.map(function (obj) {
|
|
return mapInvoice(obj)
|
|
})
|
|
})
|
|
},
|
|
saveInvoice: function () {
|
|
var data = this.formDialog.data
|
|
data.items = this.formDialog.invoiceItems
|
|
var self = this
|
|
|
|
LNbits.api
|
|
.request(
|
|
'POST',
|
|
'/invoices/api/v1/invoice' + (data.id ? '/' + data.id : ''),
|
|
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
|
|
.inkey,
|
|
data
|
|
)
|
|
.then(function (response) {
|
|
if (!data.id) {
|
|
self.invoices.push(mapInvoice(response.data))
|
|
} else {
|
|
self.getInvoices()
|
|
}
|
|
|
|
self.formDialog.invoiceItems = []
|
|
self.formDialog.show = false
|
|
})
|
|
.catch(function (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
})
|
|
},
|
|
deleteTPoS: function (tposId) {
|
|
var self = this
|
|
var tpos = _.findWhere(this.tposs, {id: tposId})
|
|
|
|
LNbits.utils
|
|
.confirmDialog('Are you sure you want to delete this TPoS?')
|
|
.onOk(function () {
|
|
LNbits.api
|
|
.request(
|
|
'DELETE',
|
|
'/tpos/api/v1/tposs/' + tposId,
|
|
_.findWhere(self.g.user.wallets, {id: tpos.wallet}).adminkey
|
|
)
|
|
.then(function (response) {
|
|
self.tposs = _.reject(self.tposs, function (obj) {
|
|
return obj.id == tposId
|
|
})
|
|
})
|
|
.catch(function (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
})
|
|
})
|
|
},
|
|
exportCSV: function () {
|
|
LNbits.utils.exportCSV(this.invoicesTable.columns, this.invoices)
|
|
}
|
|
},
|
|
created: function () {
|
|
if (this.g.user.wallets.length) {
|
|
this.getInvoices()
|
|
}
|
|
}
|
|
})
|
|
</script>
|
|
{% endblock %}
|