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:
parent
1cb8fe86b1
commit
51c4147e65
17 changed files with 934 additions and 610 deletions
|
|
@ -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>
|
||||
|
|
@ -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 () {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue