Product delete (#64)

* feat: restore stalls from `nostr` as pending

* feat: stall and prod last update time

* feat: restore products and stalls as `pending`

* feat: show pending stalls

* feat: restore stall

* feat: restore a stall from nostr

* feat: add  blank `Restore Product` button

* fix: handle no talls to restore case

* feat: show restore dialog

* feat: allow query for pending products

* feat: restore products

* chore: code clean-up

* fix: last dm and last order query

* chore: code clean-up

* fix: subscribe for stalls and products on merchant create/restore

* feat: add message type to orders

* feat: simplify messages; code format

* feat: add type to DMs; restore DMs from nostr

* fix: parsing ints

* fix: hide copy button if invoice not present

* fix: do not generate invoice if product not found

* feat: order restore: first version

* refactor: move some logic into `services`

* feat: improve restore UX

* fix: too many calls to customer DMs

* fix: allow `All` customers filter

* fix: ws reconnect on server restart

* fix: query for customer profiles only one

* fix: unread messages per customer per merchant

* fix: disable `user-profile-events`

* fix: customer profile is optional

* fix: get customers after new message debounced

* chore: code clean-up

* feat: auto-create zone

* feat: fixed ID for default zone

* feat: notify order paid
This commit is contained in:
Vlad Stan 2023-06-30 12:12:56 +02:00 committed by GitHub
parent 1cb8fe86b1
commit 51c4147e65
17 changed files with 934 additions and 610 deletions

View file

@ -1,72 +1,47 @@
<div>
<div class="row q-mb-md">
<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"
>
<div class="col-md-4 col-sm-6 q-pr-lg">
<q-select v-model="search.publicKey"
:options="customerOptions" 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
>
<div class="col-md-2 col-sm-6 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
>
<div class="col-md-2 col-sm-6 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
outline
icon="search"
@click="getOrders()"
class="float-right"
>Search Orders</q-btn
>
<div class="col-md-4 col-sm-6">
<q-btn-dropdown @click="getOrders()" :disable="search.restoring" outline unelevated split class="q-pt-md float-right"
:label="search.restoring ? 'Restoring Orders...' : 'Search Orders'">
<q-spinner v-if="search.restoring" color="primary" size="2.55em" class="q-pt-md float-right"></q-spinner>
<q-item @click="restoreOrders" clickable v-close-popup>
<q-item-section>
<q-item-label>Restore Orders</q-item-label>
<q-item-label caption>Restore previous orders from Nostr</q-item-label>
</q-item-section>
</q-item>
</q-btn-dropdown>
</div>
</div>
<div class="row q-mt-md">
<div class="col">
<q-table
flat
dense
:data="orders"
row-key="id"
:columns="ordersTable.columns"
:pagination.sync="ordersTable.pagination"
:filter="filter"
>
<q-table flat dense :data="orders" row-key="id" :columns="ordersTable.columns"
:pagination.sync="ordersTable.pagination" :filter="filter">
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
size="sm"
color="accent"
round
dense
@click="props.row.expanded= !props.row.expanded"
:icon="props.row.expanded? 'remove' : 'add'"
/>
<q-btn size="sm" color="accent" round dense @click="props.row.expanded= !props.row.expanded"
:icon="props.row.expanded? 'remove' : 'add'" />
</q-td>
<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-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>
@ -77,33 +52,21 @@
</q-td>
<q-td key="paid" :props="props">
<q-checkbox
v-model="props.row.paid"
:label="props.row.paid ? 'Yes' : 'No'"
disable
readonly
size="sm"
></q-checkbox>
<q-checkbox v-model="props.row.paid" :label="props.row.paid ? 'Yes' : 'No'" disable readonly
size="sm"></q-checkbox>
</q-td>
<q-td key="shipped" :props="props">
<q-checkbox
v-model="props.row.shipped"
@input="showShipOrderDialog(props.row)"
:label="props.row.shipped ? 'Yes' : 'No'"
size="sm"
></q-checkbox>
<q-checkbox v-model="props.row.shipped" @input="showShipOrderDialog(props.row)"
:label="props.row.shipped ? 'Yes' : 'No'" size="sm"></q-checkbox>
</q-td>
<q-td key="public_key" :props="props">
<span
@click="customerSelected(props.row.public_key)"
class="cursor-pointer"
>
<span @click="customerSelected(props.row.public_key)" class="cursor-pointer">
{{toShortId(props.row.public_key)}}
</span>
</q-td>
<q-td key="time" :props="props">
{{formatDate(props.row.time)}}
<q-td key="event_created_at" :props="props">
{{formatDate(props.row.event_created_at)}}
</q-td>
</q-tr>
<q-tr v-if="props.row.expanded" :props="props">
@ -124,10 +87,7 @@
<div class="row items-center no-wrap q-mb-md">
<div class="col-3 q-pr-lg"></div>
<div class="col-8">
<div
v-for="item in props.row.items"
class="row items-center no-wrap q-mb-md"
>
<div v-for="item in props.row.items" class="row items-center no-wrap q-mb-md">
<div class="col-1">{{item.quantity}}</div>
<div class="col-1">x</div>
<div class="col-4">
@ -141,34 +101,18 @@
</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 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>
<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">
<q-input
filled
dense
readonly
disabled
v-model.trim="props.row.id"
type="text"
></q-input>
<q-input filled dense readonly disabled v-model.trim="props.row.id" type="text"></q-input>
</div>
<div class="col-3 col-sm-1"></div>
</div>
@ -176,14 +120,7 @@
<div class="row items-center no-wrap q-mb-md">
<div class="col-3 q-pr-lg">Address:</div>
<div class="col-6 col-sm-8 q-pr-lg">
<q-input
filled
dense
readonly
disabled
v-model.trim="props.row.address"
type="text"
></q-input>
<q-input filled dense readonly disabled v-model.trim="props.row.address" type="text"></q-input>
</div>
<div class="col-3 col-sm-1"></div>
</div>
@ -191,63 +128,29 @@
<div class="row items-center no-wrap q-mb-md">
<div class="col-3 q-pr-lg">Customer Public Key:</div>
<div class="col-6 col-sm-8 q-pr-lg">
<q-input
filled
dense
readonly
disabled
v-model.trim="props.row.public_key"
type="text"
></q-input>
<q-input filled dense readonly disabled v-model.trim="props.row.public_key" type="text"></q-input>
</div>
<div class="col-3 col-sm-1"></div>
</div>
<div
v-if="props.row.contact.phone"
class="row items-center no-wrap q-mb-md"
>
<div v-if="props.row.contact.phone" class="row items-center no-wrap q-mb-md">
<div class="col-3 q-pr-lg">Phone:</div>
<div class="col-6 col-sm-8 q-pr-lg">
<q-input
filled
dense
readonly
disabled
v-model.trim="props.row.contact.phone"
type="text"
></q-input>
<q-input filled dense readonly disabled v-model.trim="props.row.contact.phone" type="text"></q-input>
</div>
<div class="col-3 col-sm-1"></div>
</div>
<div
v-if="props.row.contact.email"
class="row items-center no-wrap q-mb-md"
>
<div v-if="props.row.contact.email" class="row items-center no-wrap q-mb-md">
<div class="col-3 q-pr-lg">Email:</div>
<div class="col-6 col-sm-8 q-pr-lg">
<q-input
filled
dense
readonly
disabled
v-model.trim="props.row.contact.email"
type="text"
></q-input>
<q-input filled dense readonly disabled v-model.trim="props.row.contact.email" type="text"></q-input>
</div>
<div class="col-3 col-sm-1"></div>
</div>
<div class="row items-center no-wrap q-mb-md">
<div class="col-3 q-pr-lg">Invoice ID:</div>
<div class="col-6 col-sm-8 q-pr-lg">
<q-input
filled
dense
readonly
disabled
v-model.trim="props.row.invoice_id"
type="text"
></q-input>
<q-input filled dense readonly disabled v-model.trim="props.row.invoice_id" type="text"></q-input>
</div>
<div class="col-3 col-sm-1"></div>
</div>
@ -260,28 +163,16 @@
<q-dialog v-model="showShipDialog" position="top">
<q-card v-if="selectedOrder" class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="updateOrderShipped" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="shippingMessage"
label="Shipping Message"
type="textarea"
rows="4"
></q-input>
<q-input filled dense v-model.trim="shippingMessage" label="Shipping Message" type="textarea"
rows="4"></q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
type="submit"
:label="selectedOrder.shipped? 'Unship Order' : 'Ship Order'"
></q-btn>
<q-btn unelevated color="primary" type="submit"
:label="selectedOrder.shipped? 'Unship Order' : 'Ship Order'"></q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</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>
</div>

View file

@ -8,8 +8,8 @@ async function orderList(path) {
watch: {
customerPubkeyFilter: async function (n) {
this.search.publicKey = n
this.search.isPaid = {label: 'All', id: null}
this.search.isShipped = {label: 'All', id: null}
this.search.isPaid = { label: 'All', id: null }
this.search.isShipped = { label: 'All', id: null }
await this.getOrders()
}
},
@ -22,7 +22,7 @@ async function orderList(path) {
showShipDialog: false,
filter: '',
search: {
publicKey: '',
publicKey: null,
isPaid: {
label: 'All',
id: null
@ -30,7 +30,8 @@ async function orderList(path) {
isShipped: {
label: 'All',
id: null
}
},
restoring: false
},
customers: [],
ternaryOptions: [
@ -92,10 +93,10 @@ async function orderList(path) {
field: 'pubkey'
},
{
name: 'time',
name: 'event_created_at',
align: 'left',
label: 'Date',
field: 'time'
label: 'Created At',
field: 'event_created_at'
}
],
pagination: {
@ -104,6 +105,13 @@ async function orderList(path) {
}
}
},
computed: {
customerOptions: function () {
const options = this.customers.map(c => ({ label: this.buildCustomerLabel(c), value: c.public_key }))
options.unshift({ label: 'All', value: null, id: null })
return options
}
},
methods: {
toShortId: function (value) {
return value.substring(0, 5) + '...' + value.substring(value.length - 5)
@ -156,28 +164,48 @@ async function orderList(path) {
if (this.search.isShipped.id) {
query.push(`shipped=${this.search.isShipped.id}`)
}
const {data} = await LNbits.api.request(
const { data } = await LNbits.api.request(
'GET',
`/nostrmarket/api/v1/${ordersPath}?${query.join('&')}`,
this.inkey
)
this.orders = data.map(s => ({...s, expanded: false}))
this.orders = data.map(s => ({ ...s, expanded: false }))
console.log("### this.orders", this.orders)
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
getOrder: async function (orderId) {
try {
const {data} = await LNbits.api.request(
const { data } = await LNbits.api.request(
'GET',
`/nostrmarket/api/v1/order/${orderId}`,
this.inkey
)
return {...data, expanded: false, isNew: true}
return { ...data, expanded: false, isNew: true }
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
restoreOrders: async function () {
try {
this.search.restoring = true
await LNbits.api.request(
'PUT',
`/nostrmarket/api/v1/order/restore`,
this.adminkey
)
await this.getOrders()
this.$q.notify({
type: 'positive',
message: 'Orders restored!'
})
} catch (error) {
LNbits.utils.notifyApiError(error)
} finally {
this.search.restoring = false
}
},
updateOrderShipped: async function () {
this.selectedOrder.shipped = !this.selectedOrder.shipped
try {
@ -213,8 +241,8 @@ async function orderList(path) {
showShipOrderDialog: function (order) {
this.selectedOrder = order
this.shippingMessage = order.shipped
? `The order has been shipped! Order ID: '${order.id}' `
: `The order has NOT yet been shipped! Order ID: '${order.id}'`
? 'The order has been shipped!'
: 'The order has NOT yet been shipped!'
// do not change the status yet
this.selectedOrder.shipped = !order.shipped
@ -225,7 +253,7 @@ async function orderList(path) {
},
getCustomers: async function () {
try {
const {data} = await LNbits.api.request(
const { data } = await LNbits.api.request(
'GET',
'/nostrmarket/api/v1/customer',
this.inkey
@ -244,6 +272,12 @@ async function orderList(path) {
c.public_key.length - 16
)}`
return label
},
orderPaid: function(orderId) {
const order = this.orders.find(o => o.id === orderId)
if (order) {
order.paid = true
}
}
},
created: async function () {