have orders show on user page and invoice init
This commit is contained in:
parent
4cd86d0daf
commit
d6fe562067
5 changed files with 178 additions and 47 deletions
|
|
@ -80,14 +80,13 @@ class createOrder(BaseModel):
|
||||||
|
|
||||||
class Orders(BaseModel):
|
class Orders(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
productid: str
|
wallet: str
|
||||||
stall: str
|
username: Optional[str]
|
||||||
pubkey: str
|
pubkey: Optional[str]
|
||||||
product: str
|
|
||||||
quantity: int
|
|
||||||
shippingzone: str
|
shippingzone: str
|
||||||
address: str
|
address: str
|
||||||
email: str
|
email: str
|
||||||
|
total: int
|
||||||
invoiceid: str
|
invoiceid: str
|
||||||
paid: bool
|
paid: bool
|
||||||
shipped: bool
|
shipped: bool
|
||||||
|
|
|
||||||
|
|
@ -401,6 +401,7 @@
|
||||||
{% raw %}
|
{% raw %}
|
||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
|
<q-th auto-width></q-th>
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
{{ col.label }}
|
{{ col.label }}
|
||||||
</q-th>
|
</q-th>
|
||||||
|
|
@ -409,6 +410,16 @@
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="accent"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
@click="props.expand = !props.expand"
|
||||||
|
:icon="props.expand ? 'remove' : 'add'"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
{{ col.value }}
|
{{ col.value }}
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
@ -436,6 +447,61 @@
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</q-td>
|
</q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
|
<q-tr v-show="props.expand" :props="props">
|
||||||
|
<q-td colspan="100%">
|
||||||
|
<template>
|
||||||
|
<div class="q-pa-md">
|
||||||
|
<q-list>
|
||||||
|
<q-item-label header>Order Details</q-item-label>
|
||||||
|
|
||||||
|
<q-item v-for="col in props.row.details" :key="col.id">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Products</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>{{ products.length && (_.findWhere(products, {id:
|
||||||
|
col.product_id})).product }}</q-item-label
|
||||||
|
>
|
||||||
|
<q-item-label caption
|
||||||
|
>Quantity: {{ col.quantity }}</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Shipping to</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>{{ props.row.address }}</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>User info</q-item-label>
|
||||||
|
<q-item-label caption v-if="props.row.username"
|
||||||
|
>{{ props.row.username }}</q-item-label
|
||||||
|
>
|
||||||
|
<q-item-label caption
|
||||||
|
>{{ props.row.email }}</q-item-label
|
||||||
|
>
|
||||||
|
<q-item-label caption v-if="props.row.pubkey"
|
||||||
|
>{{ props.row.pubkey }}</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Total</q-item-label>
|
||||||
|
<q-item-label>{{ props.row.total }}</q-item-label>
|
||||||
|
<!-- <q-icon name="star" color="yellow" /> -->
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
{% endraw %}
|
{% endraw %}
|
||||||
</q-table>
|
</q-table>
|
||||||
|
|
@ -724,7 +790,6 @@
|
||||||
}
|
}
|
||||||
const mapProducts = obj => {
|
const mapProducts = obj => {
|
||||||
obj._data = _.clone(obj)
|
obj._data = _.clone(obj)
|
||||||
console.log(obj)
|
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
const mapZone = obj => {
|
const mapZone = obj => {
|
||||||
|
|
@ -733,6 +798,10 @@
|
||||||
}
|
}
|
||||||
const mapOrders = obj => {
|
const mapOrders = obj => {
|
||||||
obj._data = _.clone(obj)
|
obj._data = _.clone(obj)
|
||||||
|
obj.time = Quasar.utils.date.formatDate(
|
||||||
|
new Date(obj.time * 1000),
|
||||||
|
'YYYY-MM-DD HH:mm'
|
||||||
|
)
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
const mapKeys = obj => {
|
const mapKeys = obj => {
|
||||||
|
|
@ -822,7 +891,7 @@
|
||||||
label: '',
|
label: '',
|
||||||
ordersTable: {
|
ordersTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
/*{
|
||||||
name: 'product',
|
name: 'product',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'Product',
|
label: 'Product',
|
||||||
|
|
@ -833,12 +902,18 @@
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'Quantity',
|
label: 'Quantity',
|
||||||
field: 'quantity'
|
field: 'quantity'
|
||||||
|
},*/
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
align: 'left',
|
||||||
|
label: 'ID',
|
||||||
|
field: 'id'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'address',
|
name: 'time',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'Address',
|
label: 'Date',
|
||||||
field: 'address'
|
field: 'time'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'invoiceid',
|
name: 'invoiceid',
|
||||||
|
|
@ -1037,7 +1112,6 @@
|
||||||
////////////////STALLS//////////////////
|
////////////////STALLS//////////////////
|
||||||
////////////////////////////////////////
|
////////////////////////////////////////
|
||||||
getStalls: function () {
|
getStalls: function () {
|
||||||
console.log(this.g.user)
|
|
||||||
var self = this
|
var self = this
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
|
|
@ -1048,7 +1122,6 @@
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
self.stalls = response.data.map(mapStalls)
|
self.stalls = response.data.map(mapStalls)
|
||||||
console.log(self.stalls)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
|
|
@ -1072,7 +1145,6 @@
|
||||||
|
|
||||||
this.stallDialog.data.shippingzones = shippingzones //this.stallDialog.data.shippingzones.split(",")
|
this.stallDialog.data.shippingzones = shippingzones //this.stallDialog.data.shippingzones.split(",")
|
||||||
|
|
||||||
console.log(this.stallDialog.data)
|
|
||||||
//let zones = this.zoneOptions
|
//let zones = this.zoneOptions
|
||||||
// .filter(z => z.id == )
|
// .filter(z => z.id == )
|
||||||
this.stallDialog.show = true
|
this.stallDialog.show = true
|
||||||
|
|
@ -1096,7 +1168,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateStall: function (data) {
|
updateStall: function (data) {
|
||||||
console.log(data)
|
|
||||||
var self = this
|
var self = this
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
|
|
@ -1179,7 +1250,6 @@
|
||||||
self.g.user.wallets[0].inkey
|
self.g.user.wallets[0].inkey
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
console.log('RESP DATA', response.data)
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
self.products = response.data.map(mapProducts)
|
self.products = response.data.map(mapProducts)
|
||||||
}
|
}
|
||||||
|
|
@ -1271,7 +1341,6 @@
|
||||||
let self = this
|
let self = this
|
||||||
const walletId = _.findWhere(this.stalls, {id: data.stall}).wallet
|
const walletId = _.findWhere(this.stalls, {id: data.stall}).wallet
|
||||||
|
|
||||||
console.log('DATA', walletId, data)
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'POST',
|
'POST',
|
||||||
|
|
@ -1280,7 +1349,6 @@
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
console.log(response)
|
|
||||||
self.products.push(mapProducts(response.data))
|
self.products.push(mapProducts(response.data))
|
||||||
self.resetDialog('productDialog')
|
self.resetDialog('productDialog')
|
||||||
})
|
})
|
||||||
|
|
@ -1328,7 +1396,6 @@
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
console.log(response)
|
|
||||||
self.zones = response.data.map(mapZone)
|
self.zones = response.data.map(mapZone)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -1360,7 +1427,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateZone: function (data) {
|
updateZone: function (data) {
|
||||||
console.log(data)
|
|
||||||
var self = this
|
var self = this
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
|
|
@ -1370,7 +1436,6 @@
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
console.log(response)
|
|
||||||
self.zones = _.reject(self.zones, function (obj) {
|
self.zones = _.reject(self.zones, function (obj) {
|
||||||
return obj.id == data.id
|
return obj.id == data.id
|
||||||
})
|
})
|
||||||
|
|
@ -1492,7 +1557,6 @@
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
createShop(data) {
|
createShop(data) {
|
||||||
console.log('data')
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'POST',
|
'POST',
|
||||||
|
|
@ -1551,6 +1615,7 @@
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
self.orders = response.data.map(mapOrders)
|
self.orders = response.data.map(mapOrders)
|
||||||
|
console.log(self.orders)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
|
|
@ -1629,7 +1694,7 @@
|
||||||
this.getProducts()
|
this.getProducts()
|
||||||
this.getZones()
|
this.getZones()
|
||||||
this.getOrders()
|
this.getOrders()
|
||||||
this.getMarkets()
|
//this.getMarkets() # NOT YET IMPLEMENTED
|
||||||
this.customerKeys = [
|
this.customerKeys = [
|
||||||
'cb4c0164fe03fcdadcbfb4f76611c71620790944c24f21a1cd119395cdedfe1b',
|
'cb4c0164fe03fcdadcbfb4f76611c71620790944c24f21a1cd119395cdedfe1b',
|
||||||
'a9c17358a6dc4ceb3bb4d883eb87967a66b3453a0f3199f0b1c8eef8070c6a07'
|
'a9c17358a6dc4ceb3bb4d883eb87967a66b3453a0f3199f0b1c8eef8070c6a07'
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,31 @@
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
<!-- INVOICE DIALOG -->
|
||||||
|
<q-dialog
|
||||||
|
v-model="qrCodeDialog.show"
|
||||||
|
position="top"
|
||||||
|
@hide="qrCodeDialog.show = false"
|
||||||
|
>
|
||||||
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card text-center">
|
||||||
|
<a :href="'lightning:' + qrCodeDialog.data.payment_request">
|
||||||
|
<q-responsive :ratio="1" class="q-mx-xs">
|
||||||
|
<qrcode
|
||||||
|
:value="qrCodeDialog.data.payment_request"
|
||||||
|
:options="{width: 400}"
|
||||||
|
class="rounded-borders"
|
||||||
|
></qrcode>
|
||||||
|
</q-responsive>
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
@click="copyText('lightning:' + qrCodeDialog.data.payment_request, 'Invoice copied to clipboard!')"
|
||||||
|
>Copy Invoice</q-btn
|
||||||
|
>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block scripts %}
|
{% endblock %} {% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -227,6 +252,9 @@
|
||||||
return {
|
return {
|
||||||
stall: null,
|
stall: null,
|
||||||
products: [],
|
products: [],
|
||||||
|
wallet: {
|
||||||
|
inkey: null
|
||||||
|
},
|
||||||
searchText: null,
|
searchText: null,
|
||||||
cart: {
|
cart: {
|
||||||
total: 0,
|
total: 0,
|
||||||
|
|
@ -237,7 +265,14 @@
|
||||||
checkoutDialog: {
|
checkoutDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
data: {}
|
data: {}
|
||||||
}
|
},
|
||||||
|
qrCodeDialog: {
|
||||||
|
data: {
|
||||||
|
payment_request: null
|
||||||
|
},
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
cancelListener: () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -308,21 +343,61 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('POST', '/diagonalley/api/v1/orders', null, data)
|
.request(
|
||||||
|
'POST',
|
||||||
|
'/diagonalley/api/v1/orders',
|
||||||
|
this.wallet.inkey,
|
||||||
|
data
|
||||||
|
)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.checkoutDialog = {show: false, data: {}}
|
this.checkoutDialog = {show: false}
|
||||||
this.resetCart()
|
|
||||||
console.log(res.data)
|
return res.data
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
this.qrCodeDialog.data = data
|
||||||
|
this.qrCodeDialog.show = true
|
||||||
|
|
||||||
|
qrCodeDialog.dismissMsg = this.$q.notify({
|
||||||
|
timeout: 0,
|
||||||
|
message: 'Waiting for payment...'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.catch(error => LNbits.utils.notifyApiError(error))
|
.catch(error => LNbits.utils.notifyApiError(error))
|
||||||
|
|
||||||
return
|
return
|
||||||
|
},
|
||||||
|
startPaymentNotifier() {
|
||||||
|
this.cancelListener()
|
||||||
|
|
||||||
|
this.cancelListener = LNbits.events.onInvoicePaid(
|
||||||
|
this.wallet,
|
||||||
|
payment => {
|
||||||
|
this.qrCodeDialog = {
|
||||||
|
show: false,
|
||||||
|
data: {
|
||||||
|
payment_request: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkoutDialog = {data: {}}
|
||||||
|
this.resetCart()
|
||||||
|
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Sent, thank you!',
|
||||||
|
icon: 'thumb_up'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.stall = JSON.parse('{{ stall | tojson }}')
|
this.stall = JSON.parse('{{ stall | tojson }}')
|
||||||
this.products = JSON.parse('{{ products | tojson }}')
|
this.products = JSON.parse('{{ products | tojson }}')
|
||||||
|
this.wallet.inkey = '{{ inkey }}'
|
||||||
|
|
||||||
|
this.startPaymentNotifier()
|
||||||
//let stall_ids = new Set()
|
//let stall_ids = new Set()
|
||||||
//this.products.map(p => stall_ids.add(p.stall))
|
//this.products.map(p => stall_ids.add(p.stall))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from lnbits.core.models import User
|
||||||
from lnbits.decorators import check_user_exists # type: ignore
|
from lnbits.decorators import check_user_exists # type: ignore
|
||||||
from lnbits.extensions.diagonalley import diagonalley_ext, diagonalley_renderer
|
from lnbits.extensions.diagonalley import diagonalley_ext, diagonalley_renderer
|
||||||
|
|
||||||
|
from ...core.crud import get_wallet
|
||||||
from .crud import (
|
from .crud import (
|
||||||
get_diagonalley_products,
|
get_diagonalley_products,
|
||||||
get_diagonalley_stall,
|
get_diagonalley_stall,
|
||||||
|
|
@ -32,6 +33,7 @@ async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
async def display(request: Request, stall_id):
|
async def display(request: Request, stall_id):
|
||||||
stall = await get_diagonalley_stall(stall_id)
|
stall = await get_diagonalley_stall(stall_id)
|
||||||
products = await get_diagonalley_products(stall_id)
|
products = await get_diagonalley_products(stall_id)
|
||||||
|
wallet = await get_wallet(stall.wallet)
|
||||||
zones = []
|
zones = []
|
||||||
for id in stall.shippingzones.split(","):
|
for id in stall.shippingzones.split(","):
|
||||||
z = await get_diagonalley_zone(id)
|
z = await get_diagonalley_zone(id)
|
||||||
|
|
@ -55,6 +57,7 @@ async def display(request: Request, stall_id):
|
||||||
"request": request,
|
"request": request,
|
||||||
"stall": stall,
|
"stall": stall,
|
||||||
"products": [product.dict() for product in products],
|
"products": [product.dict() for product in products],
|
||||||
|
"inkey": wallet.inkey
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ from .crud import (
|
||||||
get_diagonalley_market,
|
get_diagonalley_market,
|
||||||
get_diagonalley_markets,
|
get_diagonalley_markets,
|
||||||
get_diagonalley_order,
|
get_diagonalley_order,
|
||||||
|
get_diagonalley_order_details,
|
||||||
get_diagonalley_orders,
|
get_diagonalley_orders,
|
||||||
get_diagonalley_product,
|
get_diagonalley_product,
|
||||||
get_diagonalley_products,
|
get_diagonalley_products,
|
||||||
|
|
@ -59,24 +60,6 @@ from .models import (
|
||||||
|
|
||||||
|
|
||||||
### Products
|
### Products
|
||||||
"""
|
|
||||||
@copilot_ext.get("/api/v1/copilot/{copilot_id}")
|
|
||||||
async def api_copilot_retrieve(
|
|
||||||
req: Request,
|
|
||||||
copilot_id: str = Query(None),
|
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
|
||||||
):
|
|
||||||
copilot = await get_copilot(copilot_id)
|
|
||||||
if not copilot:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Copilot not found"
|
|
||||||
)
|
|
||||||
if not copilot.lnurl_toggle:
|
|
||||||
return copilot.dict()
|
|
||||||
return {**copilot.dict(), **{"lnurl": copilot.lnurl(req)}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@diagonalley_ext.get("/api/v1/products")
|
@diagonalley_ext.get("/api/v1/products")
|
||||||
async def api_diagonalley_products(
|
async def api_diagonalley_products(
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||||
|
|
@ -245,12 +228,18 @@ async def api_diagonalley_orders(
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
||||||
):
|
):
|
||||||
wallet_ids = [wallet.wallet.id]
|
wallet_ids = [wallet.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
||||||
|
|
||||||
|
orders = await get_diagonalley_orders(wallet_ids)
|
||||||
|
orders_with_details = []
|
||||||
|
for order in orders:
|
||||||
|
order = order.dict()
|
||||||
|
order["details"] = await get_diagonalley_order_details(order["id"])
|
||||||
|
orders_with_details.append(order)
|
||||||
try:
|
try:
|
||||||
return [order.dict() for order in await get_diagonalley_orders(wallet_ids)]
|
return orders_with_details#[order for order in orders]
|
||||||
|
# return [order.dict() for order in await get_diagonalley_orders(wallet_ids)]
|
||||||
except:
|
except:
|
||||||
return {"message": "We could not retrieve the orders."}
|
return {"message": "We could not retrieve the orders."}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue