Merge pull request #33 from lnbits/listen_for_orders

Listen for orders
This commit is contained in:
Vlad Stan 2023-04-03 11:59:31 +03:00 committed by GitHub
commit d106879a09
17 changed files with 580 additions and 96 deletions

View file

@ -185,7 +185,7 @@ async function customerStall(path) {
items: Array.from(this.cart.products, p => {
return {product_id: p[0], quantity: p[1].quantity}
}),
shipping: orderData.shippingzone
shipping_id: orderData.shippingzone
}
orderObj.id = await hash(
[orderData.pubkey, created_at, JSON.stringify(orderObj)].join(':')
@ -269,7 +269,7 @@ async function customerStall(path) {
items: Array.from(this.cart.products, p => {
return {product_id: p[0], quantity: p[1].quantity}
}),
shipping: orderData.shippingzone
shipping_id: orderData.shippingzone
}
let created_at = Math.floor(Date.now() / 1000)
orderObj.id = await hash(
@ -375,8 +375,9 @@ async function customerStall(path) {
this.qrCodeDialog.data.message = json.message
return cb()
}
let payment_request = json.payment_options.find(o => o.type == 'ln')
.link
let payment_request = json.payment_options.find(
o => o.type == 'ln'
).link
if (!payment_request) return
this.loading = false
this.qrCodeDialog.data.payment_request = payment_request

View file

@ -1,7 +1,26 @@
<div>
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">Messages</h6>
<div class="row">
<div class="col-2">
<h6 class="text-subtitle1 q-my-none">Messages</h6>
</div>
<div class="col-4">
<q-badge v-if="unreadMessages" color="green"
><span v-text="unreadMessages"></span>&nbsp; new</q-badge
>
</div>
<div class="col-6">
<q-btn
v-if="activePublicKey"
@click="showClientOrders"
unelevated
outline
class="float-right"
>Client Orders</q-btn
>
</div>
</div>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
@ -9,7 +28,7 @@
<q-card-section>
<q-select
v-model="activePublicKey"
:options="customersPublicKeys.map(k => ({label: `${k.slice(0, 16)}...${k.slice(k.length - 16)}`, value: k}))"
:options="customers.map(c => ({label: buildCustomerLabel(c), value: c.public_key}))"
label="Select Customer"
emit-value
@input="selectActiveCustomer()"

View file

@ -2,23 +2,38 @@ async function directMessages(path) {
const template = await loadTemplateAsync(path)
Vue.component('direct-messages', {
name: 'direct-messages',
props: ['active-public-key', 'adminkey', 'inkey'],
props: ['active-chat-customer', 'adminkey', 'inkey'],
template,
watch: {
activeChatCustomer: async function (n) {
this.activePublicKey = n
},
activePublicKey: async function (n) {
await this.getDirectMessages(n)
}
},
data: function () {
return {
customersPublicKeys: [],
customers: [],
unreadMessages: 0,
activePublicKey: null,
messages: [],
newMessage: ''
}
},
methods: {
sendMessage: async function () {},
buildCustomerLabel: function (c) {
let label = `${c.profile.name || 'unknown'} ${c.profile.about || ''}`
if (c.unread_messages) {
label += `[new: ${c.unread_messages}]`
}
label += ` (${c.public_key.slice(0, 16)}...${c.public_key.slice(
c.public_key.length - 16
)}`
return label
},
getDirectMessages: async function (pubkey) {
if (!pubkey) {
this.messages = []
@ -31,23 +46,21 @@ async function directMessages(path) {
this.inkey
)
this.messages = data
console.log(
'### this.messages',
this.messages.map(m => m.message)
)
this.focusOnChatBox(this.messages.length - 1)
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
getCustomersPublicKeys: async function () {
getCustomers: async function () {
try {
const {data} = await LNbits.api.request(
'GET',
'/nostrmarket/api/v1/customers',
this.inkey
)
this.customersPublicKeys = data
this.customers = data
this.unreadMessages = data.filter(c => c.unread_messages).length
} catch (error) {
LNbits.utils.notifyApiError(error)
}
@ -70,8 +83,19 @@ async function directMessages(path) {
LNbits.utils.notifyApiError(error)
}
},
handleNewMessage: async function (data) {
if (data.customerPubkey === this.activePublicKey) {
await this.getDirectMessages(this.activePublicKey)
} else {
await this.getCustomers()
}
},
showClientOrders: function () {
this.$emit('customer-selected', this.activePublicKey)
},
selectActiveCustomer: async function () {
await this.getDirectMessages(this.activePublicKey)
await this.getCustomers()
},
focusOnChatBox: function (index) {
setTimeout(() => {
@ -85,7 +109,7 @@ async function directMessages(path) {
}
},
created: async function () {
await this.getCustomersPublicKeys()
await this.getCustomers()
}
})
}

View file

@ -1,18 +1,45 @@
<div>
<div class="row q-mb-md">
<div class="col">
<div class="col-3 q-pr-lg">
<q-select
v-model="search.publicKey"
:options="customers.map(c => ({label: buildCustomerLabel(c), value: c.public_key}))"
label="Customer"
emit-value
class="text-wrap"
>
</q-select>
</div>
<div class="col-3 q-pr-lg">
<q-select
v-model="search.isPaid"
:options="ternaryOptions"
label="Paid"
emit-value
>
</q-select>
</div>
<div class="col-3 q-pr-lg">
<q-select
v-model="search.isShipped"
:options="ternaryOptions"
label="Shipped"
emit-value
>
</q-select>
</div>
<div class="col-3">
<q-btn
unelevated
color="secondary"
outline
icon="refresh"
icon="search"
@click="getOrders()"
class="float-left"
>Refresh Orders</q-btn
class="float-right"
>Search Orders</q-btn
>
</div>
</div>
<div class="row">
<div class="row q-mt-md">
<div class="col">
<q-table
flat
@ -36,9 +63,18 @@
/>
</q-td>
<q-td key="id" :props="props"> {{toShortId(props.row.id)}} </q-td>
<q-td key="total" :props="props"> {{props.row.total}} </q-td>
<!-- todo: currency per order -->
<q-td key="id" :props="props">
{{toShortId(props.row.id)}}
<q-badge v-if="props.row.isNew" color="orange">new</q-badge></q-td
>
<q-td key="total" :props="props">
{{satBtc(props.row.total)}}
</q-td>
<q-td key="fiat" :props="props">
<span v-if="props.row.extra.currency !== 'sat'">
{{orderTotal(props.row)}} {{props.row.extra.currency}}
</span>
</q-td>
<q-td key="paid" :props="props">
<q-checkbox
@ -78,7 +114,9 @@
<div class="row items-center no-wrap q-mb-md">
<div class="col-1">Quantity</div>
<div class="col-1"></div>
<div class="col-10">Name</div>
<div class="col-4">Name</div>
<div class="col-2">Price</div>
<div class="col-4"></div>
</div>
</div>
<div class="col-1"></div>
@ -92,13 +130,34 @@
>
<div class="col-1">{{item.quantity}}</div>
<div class="col-1">x</div>
<div class="col-10">
{{productOverview(props.row, item.product_id)}}
<div class="col-4">
{{productName(props.row, item.product_id)}}
</div>
<div class="col-2">
{{productPrice(props.row, item.product_id)}}
</div>
<div class="col-4"></div>
</div>
</div>
<div class="col-1"></div>
</div>
<div
v-if="props.row.extra.currency !== 'sat'"
class="row items-center no-wrap q-mb-md q-mt-md"
>
<div class="col-3 q-pr-lg">Exchange Rate (1 BTC):</div>
<div class="col-6 col-sm-8 q-pr-lg">
<q-input
filled
dense
readonly
disabled
:value="formatFiat(props.row.extra.btc_price, props.row.extra.currency)"
type="text"
></q-input>
</div>
<div class="col-3 col-sm-1"></div>
</div>
<div class="row items-center no-wrap q-mb-md q-mt-md">
<div class="col-3 q-pr-lg">Order ID:</div>
<div class="col-6 col-sm-8 q-pr-lg">

View file

@ -2,9 +2,18 @@ async function orderList(path) {
const template = await loadTemplateAsync(path)
Vue.component('order-list', {
name: 'order-list',
props: ['stall-id', 'adminkey', 'inkey'],
props: ['stall-id', 'customer-pubkey-filter', 'adminkey', 'inkey'],
template,
watch: {
customerPubkeyFilter: async function (n) {
this.search.publicKey = n
this.search.isPaid = {label: 'All', id: null}
this.search.isShipped = {label: 'All', id: null}
await this.getOrders()
}
},
data: function () {
return {
orders: [],
@ -12,6 +21,32 @@ async function orderList(path) {
shippingMessage: '',
showShipDialog: false,
filter: '',
search: {
publicKey: '',
isPaid: {
label: 'All',
id: null
},
isShipped: {
label: 'All',
id: null
}
},
customers: [],
ternaryOptions: [
{
label: 'All',
id: null
},
{
label: 'Yes',
id: 'true'
},
{
label: 'No',
id: 'false'
}
],
ordersTable: {
columns: [
{
@ -23,15 +58,21 @@ async function orderList(path) {
{
name: 'id',
align: 'left',
label: 'ID',
label: 'Order ID',
field: 'id'
},
{
name: 'total',
align: 'left',
label: 'Total',
label: 'Total Sats',
field: 'total'
},
{
name: 'fiat',
align: 'left',
label: 'Total Fiat',
field: 'fiat'
},
{
name: 'paid',
align: 'left',
@ -73,21 +114,51 @@ async function orderList(path) {
'YYYY-MM-DD HH:mm'
)
},
productOverview: function (order, productId) {
satBtc(val, showUnit = true) {
return satOrBtc(val, showUnit, true)
},
formatFiat(value, currency) {
return Math.trunc(value) + ' ' + currency
},
productName: function (order, productId) {
product = order.extra.products.find(p => p.id === productId)
if (product) {
return `${product.name} (${product.price} ${order.extra.currency})`
return product.name
}
return ''
},
productPrice: function (order, productId) {
product = order.extra.products.find(p => p.id === productId)
if (product) {
return `${product.price} ${order.extra.currency}`
}
return ''
},
orderTotal: function (order) {
return order.items.reduce((t, item) => {
product = order.extra.products.find(p => p.id === item.product_id)
return t + item.quantity * product.price
}, 0)
},
getOrders: async function () {
try {
const ordersPath = this.stallId
? `/stall/order/${this.stallId}`
: '/order'
? `stall/order/${this.stallId}`
: 'order'
const query = []
if (this.search.publicKey) {
query.push(`pubkey=${this.search.publicKey}`)
}
if (this.search.isPaid.id) {
query.push(`paid=${this.search.isPaid.id}`)
}
if (this.search.isShipped.id) {
query.push(`shipped=${this.search.isShipped.id}`)
}
const {data} = await LNbits.api.request(
'GET',
'/nostrmarket/api/v1' + ordersPath,
`/nostrmarket/api/v1/${ordersPath}?${query.join('&')}`,
this.inkey
)
this.orders = data.map(s => ({...s, expanded: false}))
@ -95,6 +166,18 @@ async function orderList(path) {
LNbits.utils.notifyApiError(error)
}
},
getOrder: async function (orderId) {
try {
const {data} = await LNbits.api.request(
'GET',
`/nostrmarket/api/v1/order/${orderId}`,
this.inkey
)
return {...data, expanded: false, isNew: true}
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
updateOrderShipped: async function () {
this.selectedOrder.shipped = !this.selectedOrder.shipped
try {
@ -117,6 +200,16 @@ async function orderList(path) {
}
this.showShipDialog = false
},
addOrder: async function (data) {
if (
!this.search.publicKey ||
this.search.publicKey === data.customerPubkey
) {
const order = await this.getOrder(data.orderId)
this.orders.unshift(order)
}
},
showShipOrderDialog: function (order) {
this.selectedOrder = order
this.shippingMessage = order.shipped
@ -129,10 +222,35 @@ async function orderList(path) {
},
customerSelected: function (customerPubkey) {
this.$emit('customer-selected', customerPubkey)
},
getCustomers: async function () {
try {
const {data} = await LNbits.api.request(
'GET',
'/nostrmarket/api/v1/customers',
this.inkey
)
this.customers = data
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
buildCustomerLabel: function (c) {
let label = `${c.profile.name || 'unknown'} ${c.profile.about || ''}`
if (c.unread_messages) {
label += `[new: ${c.unread_messages}]`
}
label += ` (${c.public_key.slice(0, 16)}...${c.public_key.slice(
c.public_key.length - 16
)}`
return label
}
},
created: async function () {
await this.getOrders()
if (this.stallId) {
await this.getOrders()
}
await this.getCustomers()
}
})
}

View file

@ -21,6 +21,7 @@ const merchant = async () => {
merchant: {},
shippingZones: [],
activeChatCustomer: '',
orderPubkey: null,
showKeys: false,
importKeyDialog: {
show: false,
@ -102,10 +103,43 @@ const merchant = async () => {
},
customerSelectedForOrder: function (customerPubkey) {
this.activeChatCustomer = customerPubkey
},
filterOrdersForCustomer: function (customerPubkey) {
this.orderPubkey = customerPubkey
},
waitForNotifications: async function () {
try {
const scheme = location.protocol === 'http:' ? 'ws' : 'wss'
const port = location.port ? `:${location.port}` : ''
const wsUrl = `${scheme}://${document.domain}${port}/api/v1/ws/${this.merchant.id}`
const wsConnection = new WebSocket(wsUrl)
wsConnection.onmessage = async e => {
const data = JSON.parse(e.data)
if (data.type === 'new-order') {
this.$q.notify({
timeout: 5000,
type: 'positive',
message: 'New Order'
})
await this.$refs.orderListRef.addOrder(data)
} else if (data.type === 'new-customer') {
} else if (data.type === 'new-direct-message') {
await this.$refs.directMessagesRef.handleNewMessage(data)
}
}
} catch (error) {
this.$q.notify({
timeout: 5000,
type: 'warning',
message: 'Failed to watch for updated',
caption: `${error}`
})
}
}
},
created: async function () {
await this.getMerchant()
await this.waitForNotifications()
}
})
}

View file

@ -48,6 +48,16 @@ function isJson(str) {
}
}
function satOrBtc(val, showUnit = true, showSats = false) {
const value = showSats
? LNbits.utils.formatSat(val)
: val == 0
? 0.0
: (val / 100000000).toFixed(8)
if (!showUnit) return value
return showSats ? value + ' sat' : value + ' BTC'
}
function timeFromNow(time) {
// Get timestamps
let unixTime = new Date(time).getTime()