Files
lnbits/lnbits/extensions/invoices/templates/invoices/index.html
T
c32ff1de59 New Extension: Invoicing (#733)
* 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>
2022-08-13 21:37:44 +02:00

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 %}