V1 (#106)
This commit is contained in:
parent
83c94e94db
commit
0fc26d096f
52 changed files with 6684 additions and 3120 deletions
170
static/components/direct-messages.js
Normal file
170
static/components/direct-messages.js
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
window.app.component('direct-messages', {
|
||||
name: 'direct-messages',
|
||||
props: ['active-chat-customer', 'merchant-id', 'adminkey', 'inkey'],
|
||||
template: '#direct-messages',
|
||||
delimiters: ['${', '}'],
|
||||
watch: {
|
||||
activeChatCustomer: async function (n) {
|
||||
this.activePublicKey = n
|
||||
},
|
||||
activePublicKey: async function (n) {
|
||||
await this.getDirectMessages(n)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
messagesAsJson: function () {
|
||||
return this.messages.map(m => {
|
||||
const dateFrom = moment(m.event_created_at * 1000).fromNow()
|
||||
try {
|
||||
const message = JSON.parse(m.message)
|
||||
return {
|
||||
isJson: message.type >= 0,
|
||||
dateFrom,
|
||||
...m,
|
||||
message
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
isJson: false,
|
||||
dateFrom,
|
||||
...m,
|
||||
message: m.message
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
customers: [],
|
||||
unreadMessages: 0,
|
||||
activePublicKey: null,
|
||||
messages: [],
|
||||
newMessage: '',
|
||||
showAddPublicKey: false,
|
||||
newPublicKey: null,
|
||||
showRawMessage: false,
|
||||
rawMessage: null
|
||||
}
|
||||
},
|
||||
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 = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/message/' + pubkey,
|
||||
this.inkey
|
||||
)
|
||||
this.messages = data
|
||||
|
||||
this.focusOnChatBox(this.messages.length - 1)
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
getCustomers: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/customer',
|
||||
this.inkey
|
||||
)
|
||||
this.customers = data
|
||||
this.unreadMessages = data.filter(c => c.unread_messages).length
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
|
||||
sendDirectMesage: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/message',
|
||||
this.adminkey,
|
||||
{
|
||||
message: this.newMessage,
|
||||
public_key: this.activePublicKey
|
||||
}
|
||||
)
|
||||
this.messages = this.messages.concat([data])
|
||||
this.newMessage = ''
|
||||
this.focusOnChatBox(this.messages.length - 1)
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
addPublicKey: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/customer',
|
||||
this.adminkey,
|
||||
{
|
||||
public_key: this.newPublicKey,
|
||||
merchant_id: this.merchantId,
|
||||
unread_messages: 0
|
||||
}
|
||||
)
|
||||
this.newPublicKey = null
|
||||
this.activePublicKey = data.public_key
|
||||
await this.selectActiveCustomer()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
} finally {
|
||||
this.showAddPublicKey = false
|
||||
}
|
||||
},
|
||||
handleNewMessage: async function (data) {
|
||||
if (data.customerPubkey === this.activePublicKey) {
|
||||
this.messages.push(data.dm)
|
||||
this.focusOnChatBox(this.messages.length - 1)
|
||||
// focus back on input box
|
||||
}
|
||||
this.getCustomersDebounced()
|
||||
},
|
||||
showOrderDetails: function (orderId, eventId) {
|
||||
this.$emit('order-selected', {orderId, eventId})
|
||||
},
|
||||
showClientOrders: function () {
|
||||
this.$emit('customer-selected', this.activePublicKey)
|
||||
},
|
||||
selectActiveCustomer: async function () {
|
||||
await this.getDirectMessages(this.activePublicKey)
|
||||
await this.getCustomers()
|
||||
},
|
||||
showMessageRawData: function (index) {
|
||||
this.rawMessage = this.messages[index]?.message
|
||||
this.showRawMessage = true
|
||||
},
|
||||
focusOnChatBox: function (index) {
|
||||
setTimeout(() => {
|
||||
const lastChatBox = document.getElementsByClassName(
|
||||
`chat-mesage-index-${index}`
|
||||
)
|
||||
if (lastChatBox && lastChatBox[0]) {
|
||||
lastChatBox[0].scrollIntoView()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
await this.getCustomers()
|
||||
this.getCustomersDebounced = _.debounce(this.getCustomers, 2000, false)
|
||||
}
|
||||
})
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
<div>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<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="primary" outline><span v-text="unreadMessages"></span> 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>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-10">
|
||||
<q-select v-model="activePublicKey"
|
||||
:options="customers.map(c => ({label: buildCustomerLabel(c), value: c.public_key}))" label="Select Customer"
|
||||
emit-value @input="selectActiveCustomer()">
|
||||
</q-select>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<q-btn label="Add" color="primary" class="float-right q-mt-md" @click="showAddPublicKey = true">
|
||||
<q-tooltip>
|
||||
Add a public key to chat with
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="chat-container" ref="chatCard">
|
||||
<div class="chat-box">
|
||||
<div class="chat-messages" style="height: 45vh">
|
||||
<q-chat-message v-for="(dm, index) in messagesAsJson" :key="index" :name="dm.incoming ? 'customer': 'me'"
|
||||
:text="dm.isJson ? [] : [dm.message]" :sent="!dm.incoming"
|
||||
:stamp="dm.dateFrom"
|
||||
:bg-color="dm.incoming ? 'white' : 'light-green-2'" :class="'chat-mesage-index-'+index">
|
||||
<div v-if="dm.isJson">
|
||||
<div v-if="dm.message.type === 0">
|
||||
<strong>New order:</strong>
|
||||
</div>
|
||||
<div v-else-if="dm.message.type === 1">
|
||||
<strong>Reply sent for order: </strong>
|
||||
</div>
|
||||
<div v-else-if="dm.message.type === 2">
|
||||
<q-badge v-if="dm.message.paid" color="green">Paid </q-badge>
|
||||
<q-badge v-if="dm.message.shipped" color="green">Shipped </q-badge>
|
||||
</div>
|
||||
<div>
|
||||
<span v-text="dm.message.message"></span>
|
||||
<q-badge color="orange">
|
||||
<span v-text="dm.message.id" @click="showOrderDetails(dm.message.id, dm.event_id)" class="cursor-pointer"></span>
|
||||
</q-badge>
|
||||
</div>
|
||||
<q-badge @click="showMessageRawData(index)" class="cursor-pointer">...</q-badge>
|
||||
|
||||
</div>
|
||||
</q-chat-message>
|
||||
</div>
|
||||
</div>
|
||||
<q-card-section>
|
||||
<q-form @submit="sendDirectMesage" class="full-width chat-input">
|
||||
<q-input ref="newMessage" v-model="newMessage" placeholder="Message" class="full-width" dense outlined>
|
||||
<template>
|
||||
<q-btn round dense flat type="submit" icon="send" color="primary" />
|
||||
</template>
|
||||
</q-input>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<div>
|
||||
<q-dialog v-model="showAddPublicKey" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form @submit="addPublicKey" class="q-gutter-md">
|
||||
<q-input filled dense v-model.trim="newPublicKey" label="Public Key (hex or nsec)"></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn unelevated color="primary" :disable="!newPublicKey" type="submit">Add</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showRawMessage" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-input filled dense type="textarea" rows="20" v-model.trim="rawMessage" label="Raw Data"></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
async function directMessages(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('direct-messages', {
|
||||
name: 'direct-messages',
|
||||
props: ['active-chat-customer', 'merchant-id', 'adminkey', 'inkey'],
|
||||
template,
|
||||
|
||||
watch: {
|
||||
activeChatCustomer: async function (n) {
|
||||
this.activePublicKey = n
|
||||
},
|
||||
activePublicKey: async function (n) {
|
||||
await this.getDirectMessages(n)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
messagesAsJson: function () {
|
||||
return this.messages.map(m => {
|
||||
const dateFrom = moment(m.event_created_at * 1000).fromNow()
|
||||
try {
|
||||
const message = JSON.parse(m.message)
|
||||
return {
|
||||
isJson: message.type >= 0,
|
||||
dateFrom,
|
||||
...m,
|
||||
message
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
isJson: false,
|
||||
dateFrom,
|
||||
...m,
|
||||
message: m.message
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
customers: [],
|
||||
unreadMessages: 0,
|
||||
activePublicKey: null,
|
||||
messages: [],
|
||||
newMessage: '',
|
||||
showAddPublicKey: false,
|
||||
newPublicKey: null,
|
||||
showRawMessage: false,
|
||||
rawMessage: null,
|
||||
}
|
||||
},
|
||||
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 = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/message/' + pubkey,
|
||||
this.inkey
|
||||
)
|
||||
this.messages = data
|
||||
|
||||
this.focusOnChatBox(this.messages.length - 1)
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
getCustomers: async function () {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/customer',
|
||||
this.inkey
|
||||
)
|
||||
this.customers = data
|
||||
this.unreadMessages = data.filter(c => c.unread_messages).length
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
|
||||
sendDirectMesage: async function () {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/message',
|
||||
this.adminkey,
|
||||
{
|
||||
message: this.newMessage,
|
||||
public_key: this.activePublicKey
|
||||
}
|
||||
)
|
||||
this.messages = this.messages.concat([data])
|
||||
this.newMessage = ''
|
||||
this.focusOnChatBox(this.messages.length - 1)
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
addPublicKey: async function () {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/customer',
|
||||
this.adminkey,
|
||||
{
|
||||
public_key: this.newPublicKey,
|
||||
merchant_id: this.merchantId,
|
||||
unread_messages: 0
|
||||
}
|
||||
)
|
||||
this.newPublicKey = null
|
||||
this.activePublicKey = data.public_key
|
||||
await this.selectActiveCustomer()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
} finally {
|
||||
this.showAddPublicKey = false
|
||||
}
|
||||
},
|
||||
handleNewMessage: async function (data) {
|
||||
if (data.customerPubkey === this.activePublicKey) {
|
||||
this.messages.push(data.dm)
|
||||
this.focusOnChatBox(this.messages.length - 1)
|
||||
// focus back on input box
|
||||
}
|
||||
this.getCustomersDebounced()
|
||||
},
|
||||
showOrderDetails: function (orderId, eventId) {
|
||||
this.$emit('order-selected', { orderId, eventId })
|
||||
},
|
||||
showClientOrders: function () {
|
||||
this.$emit('customer-selected', this.activePublicKey)
|
||||
},
|
||||
selectActiveCustomer: async function () {
|
||||
await this.getDirectMessages(this.activePublicKey)
|
||||
await this.getCustomers()
|
||||
},
|
||||
showMessageRawData: function (index) {
|
||||
this.rawMessage = this.messages[index]?.message
|
||||
this.showRawMessage = true
|
||||
},
|
||||
focusOnChatBox: function (index) {
|
||||
setTimeout(() => {
|
||||
const lastChatBox = document.getElementsByClassName(
|
||||
`chat-mesage-index-${index}`
|
||||
)
|
||||
if (lastChatBox && lastChatBox[0]) {
|
||||
lastChatBox[0].scrollIntoView()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
await this.getCustomers()
|
||||
this.getCustomersDebounced = _.debounce(this.getCustomers, 2000, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
22
static/components/key-pair.js
Normal file
22
static/components/key-pair.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
window.app.component('key-pair', {
|
||||
name: 'key-pair',
|
||||
template: '#key-pair',
|
||||
delimiters: ['${', '}'],
|
||||
props: ['public-key', 'private-key'],
|
||||
data: function () {
|
||||
return {
|
||||
showPrivateKey: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyText: function (text, message, position) {
|
||||
var notify = this.$q.notify
|
||||
Quasar.copyToClipboard(text).then(function () {
|
||||
notify({
|
||||
message: message || 'Copied to clipboard!',
|
||||
position: position || 'bottom'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
<div>
|
||||
<q-separator></q-separator>
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-6 q-pl-xl">Public Key</div>
|
||||
<div class="col-6">
|
||||
<q-toggle v-model="showPrivateKey" class="q-pl-xl" color="secodary">
|
||||
Show Private Key
|
||||
</q-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="text-center q-mb-lg cursor-pointer">
|
||||
<q-responsive :ratio="1" class="q-mx-xl" @click="copyText(publicKey)">
|
||||
<qrcode
|
||||
:value="publicKey"
|
||||
:options="{width: 250}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
<small><span v-text="publicKey"></span><br />Click to copy</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 cursor-pointer">
|
||||
<div v-if="showPrivateKey">
|
||||
<div class="text-center q-mb-lg">
|
||||
<q-responsive
|
||||
:ratio="1"
|
||||
class="q-mx-xl"
|
||||
@click="copyText(privateKey)"
|
||||
>
|
||||
<qrcode
|
||||
:value="privateKey"
|
||||
:options="{width: 250}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
<small><span v-text="privateKey"></span><br />Click to copy</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
async function keyPair(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('key-pair', {
|
||||
name: 'key-pair',
|
||||
template,
|
||||
|
||||
props: ['public-key', 'private-key'],
|
||||
data: function () {
|
||||
return {
|
||||
showPrivateKey: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyText: function (text, message, position) {
|
||||
var notify = this.$q.notify
|
||||
Quasar.utils.copyToClipboard(text).then(function () {
|
||||
notify({
|
||||
message: message || 'Copied to clipboard!',
|
||||
position: position || 'bottom'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
102
static/components/merchant-details.js
Normal file
102
static/components/merchant-details.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
window.app.component('merchant-details', {
|
||||
name: 'merchant-details',
|
||||
template: '#merchant-details',
|
||||
props: ['merchant-id', 'adminkey', 'inkey', 'showKeys'],
|
||||
delimiters: ['${', '}'],
|
||||
data: function () {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
toggleShowKeys: async function () {
|
||||
this.$emit('toggle-show-keys')
|
||||
},
|
||||
|
||||
republishMerchantData: async function () {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
`/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
|
||||
this.adminkey
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Merchant data republished to Nostr',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
requeryMerchantData: async function () {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'GET',
|
||||
`/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
|
||||
this.adminkey
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Merchant data refreshed from Nostr',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
deleteMerchantTables: function () {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
`
|
||||
Stalls, products and orders will be deleted also!
|
||||
Are you sure you want to delete this merchant?
|
||||
`
|
||||
)
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
'/nostrmarket/api/v1/merchant/' + this.merchantId,
|
||||
this.adminkey
|
||||
)
|
||||
this.$emit('merchant-deleted', this.merchantId)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Merchant Deleted',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteMerchantFromNostr: function () {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
`
|
||||
Do you want to remove the merchant from Nostr?
|
||||
`
|
||||
)
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
`/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
|
||||
this.adminkey
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Merchant Deleted from Nostr',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created: async function () {}
|
||||
})
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
<div>
|
||||
<q-btn-dropdown
|
||||
split
|
||||
unelevated
|
||||
color="primary"
|
||||
icon="storefront"
|
||||
label="Merchant"
|
||||
>
|
||||
<q-list>
|
||||
<q-item disable clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label>Merchant Profile</q-item-label>
|
||||
<q-item-label caption
|
||||
>Edit the merchand name, description, etc</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item @click="toggleShowKeys" clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label v-if="!showKeys">Show Keys</q-item-label>
|
||||
<q-item-label v-else>Hide Keys</q-item-label>
|
||||
<q-item-label caption
|
||||
>Show merchant public and private keys</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item @click="requeryMerchantData" clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label>Refresh from Nostr</q-item-label>
|
||||
<q-item-label caption
|
||||
>Requery all stalls, products and orders from Nostr</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item @click="republishMerchantData" clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label>Republish to Nostr</q-item-label>
|
||||
<q-item-label caption
|
||||
>Republish all stalls, products and orders to Nostr</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item @click="deleteMerchantTables" clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label>Delete from DB</q-item-label>
|
||||
<q-item-label caption
|
||||
>Delete all stalls, products and orders from database</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item @click="deleteMerchantFromNostr" clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label>Delete from Nostr</q-item-label>
|
||||
<q-item-label caption
|
||||
>Delete all stalls, products and orders from Nostr</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list></q-btn-dropdown
|
||||
>
|
||||
</div>
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
async function merchantDetails(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('merchant-details', {
|
||||
name: 'merchant-details',
|
||||
props: ['merchant-id', 'adminkey', 'inkey','showKeys'],
|
||||
template,
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleShowKeys: async function () {
|
||||
this.$emit('toggle-show-keys')
|
||||
},
|
||||
|
||||
republishMerchantData: async function () {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
`/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
|
||||
this.adminkey
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Merchant data republished to Nostr',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
requeryMerchantData: async function () {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'GET',
|
||||
`/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
|
||||
this.adminkey
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Merchant data refreshed from Nostr',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
deleteMerchantTables: function () {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
`
|
||||
Stalls, products and orders will be deleted also!
|
||||
Are you sure you want to delete this merchant?
|
||||
`
|
||||
)
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
'/nostrmarket/api/v1/merchant/' + this.merchantId,
|
||||
this.adminkey
|
||||
)
|
||||
this.$emit('merchant-deleted', this.merchantId)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Merchant Deleted',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteMerchantFromNostr: function () {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
`
|
||||
Do you want to remove the merchant from Nostr?
|
||||
`
|
||||
)
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
`/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
|
||||
this.adminkey
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Merchant Deleted from Nostr',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created: async function () {}
|
||||
})
|
||||
}
|
||||
406
static/components/order-list.js
Normal file
406
static/components/order-list.js
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
window.app.component('order-list', {
|
||||
name: 'order-list',
|
||||
props: ['stall-id', 'customer-pubkey-filter', 'adminkey', 'inkey'],
|
||||
template: '#order-list',
|
||||
delimiters: ['${', '}'],
|
||||
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: [],
|
||||
stalls: [],
|
||||
selectedOrder: null,
|
||||
shippingMessage: '',
|
||||
showShipDialog: false,
|
||||
filter: '',
|
||||
search: {
|
||||
publicKey: null,
|
||||
isPaid: {
|
||||
label: 'All',
|
||||
id: null
|
||||
},
|
||||
isShipped: {
|
||||
label: 'All',
|
||||
id: null
|
||||
},
|
||||
restoring: false
|
||||
},
|
||||
customers: [],
|
||||
ternaryOptions: [
|
||||
{
|
||||
label: 'All',
|
||||
id: null
|
||||
},
|
||||
{
|
||||
label: 'Yes',
|
||||
id: 'true'
|
||||
},
|
||||
{
|
||||
label: 'No',
|
||||
id: 'false'
|
||||
}
|
||||
],
|
||||
zoneOptions: [],
|
||||
ordersTable: {
|
||||
columns: [
|
||||
{
|
||||
name: '',
|
||||
align: 'left',
|
||||
label: '',
|
||||
field: ''
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
align: 'left',
|
||||
label: 'Order ID',
|
||||
field: 'id'
|
||||
},
|
||||
{
|
||||
name: 'total',
|
||||
align: 'left',
|
||||
label: 'Total Sats',
|
||||
field: 'total'
|
||||
},
|
||||
{
|
||||
name: 'fiat',
|
||||
align: 'left',
|
||||
label: 'Total Fiat',
|
||||
field: 'fiat'
|
||||
},
|
||||
{
|
||||
name: 'paid',
|
||||
align: 'left',
|
||||
label: 'Paid',
|
||||
field: 'paid'
|
||||
},
|
||||
{
|
||||
name: 'shipped',
|
||||
align: 'left',
|
||||
label: 'Shipped',
|
||||
field: 'shipped'
|
||||
},
|
||||
{
|
||||
name: 'public_key',
|
||||
align: 'left',
|
||||
label: 'Customer',
|
||||
field: 'pubkey'
|
||||
},
|
||||
{
|
||||
name: 'event_created_at',
|
||||
align: 'left',
|
||||
label: 'Created At',
|
||||
field: 'event_created_at'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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)
|
||||
},
|
||||
formatDate: function (value) {
|
||||
return Quasar.date.formatDate(new Date(value * 1000), 'YYYY-MM-DD HH:mm')
|
||||
},
|
||||
satBtc(val, showUnit = true) {
|
||||
return satOrBtc(val, showUnit, true)
|
||||
},
|
||||
formatFiat(value, currency) {
|
||||
return Math.trunc(value) + ' ' + currency
|
||||
},
|
||||
shortLabel(value = '') {
|
||||
if (value.length <= 44) return value
|
||||
return value.substring(0, 20) + '...'
|
||||
},
|
||||
productName: function (order, productId) {
|
||||
product = order.extra.products.find(p => p.id === productId)
|
||||
if (product) {
|
||||
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) {
|
||||
const productCost = order.items.reduce((t, item) => {
|
||||
product = order.extra.products.find(p => p.id === item.product_id)
|
||||
return t + item.quantity * product.price
|
||||
}, 0)
|
||||
return productCost + order.extra.shipping_cost
|
||||
},
|
||||
getOrders: async function () {
|
||||
try {
|
||||
const ordersPath = this.stallId
|
||||
? `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}?${query.join('&')}`,
|
||||
this.inkey
|
||||
)
|
||||
this.orders = data.map(s => ({...s, expanded: false}))
|
||||
} catch (error) {
|
||||
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)
|
||||
}
|
||||
},
|
||||
restoreOrder: async function (eventId) {
|
||||
console.log('### restoreOrder', eventId)
|
||||
try {
|
||||
this.search.restoring = true
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
`/nostrmarket/api/v1/order/restore/${eventId}`,
|
||||
this.adminkey
|
||||
)
|
||||
await this.getOrders()
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Order restored!'
|
||||
})
|
||||
return data
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
} finally {
|
||||
this.search.restoring = false
|
||||
}
|
||||
},
|
||||
restoreOrders: async function () {
|
||||
try {
|
||||
this.search.restoring = true
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
`/nostrmarket/api/v1/orders/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
|
||||
}
|
||||
},
|
||||
reissueOrderInvoice: async function (order) {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
`/nostrmarket/api/v1/order/reissue`,
|
||||
this.adminkey,
|
||||
{
|
||||
id: order.id,
|
||||
shipping_id: order.shipping_id
|
||||
}
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Order invoice reissued!'
|
||||
})
|
||||
data.expanded = order.expanded
|
||||
|
||||
const i = this.orders.map(o => o.id).indexOf(order.id)
|
||||
if (i !== -1) {
|
||||
this.orders[i] = {...this.orders[i], ...data}
|
||||
}
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
updateOrderShipped: async function () {
|
||||
this.selectedOrder.shipped = !this.selectedOrder.shipped
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PATCH',
|
||||
`/nostrmarket/api/v1/order/${this.selectedOrder.id}`,
|
||||
this.adminkey,
|
||||
{
|
||||
id: this.selectedOrder.id,
|
||||
message: this.shippingMessage,
|
||||
shipped: this.selectedOrder.shipped
|
||||
}
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Order updated!'
|
||||
})
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
this.showShipDialog = false
|
||||
},
|
||||
addOrder: async function (data) {
|
||||
if (
|
||||
!this.search.publicKey ||
|
||||
this.search.publicKey === data.customerPubkey
|
||||
) {
|
||||
const orderData = JSON.parse(data.dm.message)
|
||||
const i = this.orders.map(o => o.id).indexOf(orderData.id)
|
||||
if (i === -1) {
|
||||
const order = await this.getOrder(orderData.id)
|
||||
this.orders.unshift(order)
|
||||
}
|
||||
}
|
||||
},
|
||||
orderSelected: async function (orderId, eventId) {
|
||||
const order = await this.getOrder(orderId)
|
||||
if (!order) {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
'Order could not be found. Do you want to restore it from this direct message?'
|
||||
)
|
||||
.onOk(async () => {
|
||||
const restoredOrder = await this.restoreOrder(eventId)
|
||||
console.log('### restoredOrder', restoredOrder)
|
||||
if (restoredOrder) {
|
||||
restoredOrder.expanded = true
|
||||
restoredOrder.isNew = false
|
||||
this.orders = [restoredOrder]
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
order.expanded = true
|
||||
order.isNew = false
|
||||
this.orders = [order]
|
||||
},
|
||||
getZones: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/zone',
|
||||
this.inkey
|
||||
)
|
||||
return data.map(z => ({
|
||||
id: z.id,
|
||||
value: z.id,
|
||||
label: z.name
|
||||
? `${z.name} (${z.countries.join(', ')})`
|
||||
: z.countries.join(', ')
|
||||
}))
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
return []
|
||||
},
|
||||
getStalls: async function (pending = false) {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/nostrmarket/api/v1/stall?pending=${pending}`,
|
||||
this.inkey
|
||||
)
|
||||
return data.map(s => ({...s, expanded: false}))
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
return []
|
||||
},
|
||||
getStallZones: function (stallId) {
|
||||
const stall = this.stalls.find(s => s.id === stallId)
|
||||
if (!stall) return []
|
||||
|
||||
return this.zoneOptions.filter(z =>
|
||||
stall.shipping_zones.find(s => s.id === z.id)
|
||||
)
|
||||
},
|
||||
showShipOrderDialog: function (order) {
|
||||
this.selectedOrder = order
|
||||
this.shippingMessage = order.shipped
|
||||
? 'The order has been shipped!'
|
||||
: 'The order has NOT yet been shipped!'
|
||||
|
||||
// do not change the status yet
|
||||
this.selectedOrder.shipped = !order.shipped
|
||||
this.showShipDialog = true
|
||||
},
|
||||
customerSelected: function (customerPubkey) {
|
||||
this.$emit('customer-selected', customerPubkey)
|
||||
},
|
||||
getCustomers: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/customer',
|
||||
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
|
||||
},
|
||||
orderPaid: function (orderId) {
|
||||
const order = this.orders.find(o => o.id === orderId)
|
||||
if (order) {
|
||||
order.paid = true
|
||||
}
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
if (this.stallId) {
|
||||
await this.getOrders()
|
||||
}
|
||||
await this.getCustomers()
|
||||
this.zoneOptions = await this.getZones()
|
||||
this.stalls = await this.getStalls()
|
||||
}
|
||||
})
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
<div>
|
||||
<div class="row q-mb-md">
|
||||
<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-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-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-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...' : 'Load 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">
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn size="sm" color="primary" 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-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 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-td>
|
||||
|
||||
<q-td key="public_key" :props="props">
|
||||
<span @click="customerSelected(props.row.public_key)" class="cursor-pointer">
|
||||
{{toShortId(props.row.public_key)}}
|
||||
</span>
|
||||
</q-td>
|
||||
<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">
|
||||
<q-td colspan="100%">
|
||||
<div class="row items-center no-wrap">
|
||||
<div class="col-3 q-pr-lg">Products:</div>
|
||||
<div class="col-8">
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col-1"><strong>Quantity</strong></div>
|
||||
<div class="col-1"></div>
|
||||
<div class="col-4"><strong>Name</strong></div>
|
||||
<div class="col-2"><strong>Price</strong></div>
|
||||
<div class="col-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-1"></div>
|
||||
</div>
|
||||
<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 class="col-1">{{item.quantity}}</div>
|
||||
<div class="col-1">x</div>
|
||||
<div class="col-4">
|
||||
<p :title="productName(props.row, item.product_id)">
|
||||
{{shortLabel(productName(props.row, item.product_id))}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
{{productPrice(props.row, item.product_id)}}
|
||||
</div>
|
||||
<div class="col-4"></div>
|
||||
</div>
|
||||
<div v-if="props.row.extra.shipping_cost" class="row items-center no-wrap q-mb-md">
|
||||
<div class="col-1"></div>
|
||||
<div class="col-1"></div>
|
||||
<div class="col-4">Shipping Cost</div>
|
||||
<div class="col-2">
|
||||
{{props.row.extra.shipping_cost}} {{props.row.extra.currency}}
|
||||
</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 v-if="props.row.extra.fail_message" class="row items-center no-wrap q-mb-md">
|
||||
<div class="col-3 q-pr-lg">Error:</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg">
|
||||
<q-badge color="pink"><span v-text="props.row.extra.fail_message"></span></q-badge>
|
||||
</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>
|
||||
</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">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>
|
||||
</div>
|
||||
<div class="col-3 col-sm-1"></div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.row.address" 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>
|
||||
</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 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>
|
||||
</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 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>
|
||||
</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">Shipping Zone:</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg">
|
||||
<q-select :options="getStallZones(props.row.stall_id)" filled dense emit-value
|
||||
v-model.trim="props.row.shipping_id" label="Shipping Zones"></q-select>
|
||||
</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>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col-3 q-pr-lg"></div>
|
||||
|
||||
<div class="col-9">
|
||||
<q-btn @click="reissueOrderInvoice(props.row)" unelevated color="primary" type="submit"
|
||||
class="float-left" label="Reissue Invoice"></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<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>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
|
|
@ -1,409 +0,0 @@
|
|||
async function orderList(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('order-list', {
|
||||
name: 'order-list',
|
||||
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: [],
|
||||
stalls: [],
|
||||
selectedOrder: null,
|
||||
shippingMessage: '',
|
||||
showShipDialog: false,
|
||||
filter: '',
|
||||
search: {
|
||||
publicKey: null,
|
||||
isPaid: {
|
||||
label: 'All',
|
||||
id: null
|
||||
},
|
||||
isShipped: {
|
||||
label: 'All',
|
||||
id: null
|
||||
},
|
||||
restoring: false
|
||||
},
|
||||
customers: [],
|
||||
ternaryOptions: [
|
||||
{
|
||||
label: 'All',
|
||||
id: null
|
||||
},
|
||||
{
|
||||
label: 'Yes',
|
||||
id: 'true'
|
||||
},
|
||||
{
|
||||
label: 'No',
|
||||
id: 'false'
|
||||
}
|
||||
],
|
||||
zoneOptions: [],
|
||||
ordersTable: {
|
||||
columns: [
|
||||
{
|
||||
name: '',
|
||||
align: 'left',
|
||||
label: '',
|
||||
field: ''
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
align: 'left',
|
||||
label: 'Order ID',
|
||||
field: 'id'
|
||||
},
|
||||
{
|
||||
name: 'total',
|
||||
align: 'left',
|
||||
label: 'Total Sats',
|
||||
field: 'total'
|
||||
},
|
||||
{
|
||||
name: 'fiat',
|
||||
align: 'left',
|
||||
label: 'Total Fiat',
|
||||
field: 'fiat'
|
||||
},
|
||||
{
|
||||
name: 'paid',
|
||||
align: 'left',
|
||||
label: 'Paid',
|
||||
field: 'paid'
|
||||
},
|
||||
{
|
||||
name: 'shipped',
|
||||
align: 'left',
|
||||
label: 'Shipped',
|
||||
field: 'shipped'
|
||||
},
|
||||
{
|
||||
name: 'public_key',
|
||||
align: 'left',
|
||||
label: 'Customer',
|
||||
field: 'pubkey'
|
||||
},
|
||||
{
|
||||
name: 'event_created_at',
|
||||
align: 'left',
|
||||
label: 'Created At',
|
||||
field: 'event_created_at'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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)
|
||||
},
|
||||
formatDate: function (value) {
|
||||
return Quasar.utils.date.formatDate(
|
||||
new Date(value * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
},
|
||||
satBtc(val, showUnit = true) {
|
||||
return satOrBtc(val, showUnit, true)
|
||||
},
|
||||
formatFiat(value, currency) {
|
||||
return Math.trunc(value) + ' ' + currency
|
||||
},
|
||||
shortLabel(value = ''){
|
||||
if (value.length <= 44) return value
|
||||
return value.substring(0, 20) + '...'
|
||||
},
|
||||
productName: function (order, productId) {
|
||||
product = order.extra.products.find(p => p.id === productId)
|
||||
if (product) {
|
||||
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) {
|
||||
const productCost = order.items.reduce((t, item) => {
|
||||
product = order.extra.products.find(p => p.id === item.product_id)
|
||||
return t + item.quantity * product.price
|
||||
}, 0)
|
||||
return productCost + order.extra.shipping_cost
|
||||
},
|
||||
getOrders: async function () {
|
||||
try {
|
||||
const ordersPath = this.stallId
|
||||
? `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}?${query.join('&')}`,
|
||||
this.inkey
|
||||
)
|
||||
this.orders = data.map(s => ({ ...s, expanded: false }))
|
||||
} catch (error) {
|
||||
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)
|
||||
}
|
||||
},
|
||||
restoreOrder: async function (eventId) {
|
||||
console.log('### restoreOrder', eventId)
|
||||
try {
|
||||
this.search.restoring = true
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
`/nostrmarket/api/v1/order/restore/${eventId}`,
|
||||
this.adminkey
|
||||
)
|
||||
await this.getOrders()
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Order restored!'
|
||||
})
|
||||
return data
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
} finally {
|
||||
this.search.restoring = false
|
||||
}
|
||||
},
|
||||
restoreOrders: async function () {
|
||||
try {
|
||||
this.search.restoring = true
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
`/nostrmarket/api/v1/orders/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
|
||||
}
|
||||
},
|
||||
reissueOrderInvoice: async function (order) {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'PUT',
|
||||
`/nostrmarket/api/v1/order/reissue`,
|
||||
this.adminkey,
|
||||
{
|
||||
id: order.id,
|
||||
shipping_id: order.shipping_id
|
||||
}
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Order invoice reissued!'
|
||||
})
|
||||
data.expanded = order.expanded
|
||||
|
||||
const i = this.orders.map(o => o.id).indexOf(order.id)
|
||||
if (i !== -1) {
|
||||
this.orders[i] = { ...this.orders[i], ...data }
|
||||
}
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
updateOrderShipped: async function () {
|
||||
this.selectedOrder.shipped = !this.selectedOrder.shipped
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PATCH',
|
||||
`/nostrmarket/api/v1/order/${this.selectedOrder.id}`,
|
||||
this.adminkey,
|
||||
{
|
||||
id: this.selectedOrder.id,
|
||||
message: this.shippingMessage,
|
||||
shipped: this.selectedOrder.shipped
|
||||
}
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Order updated!'
|
||||
})
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
this.showShipDialog = false
|
||||
},
|
||||
addOrder: async function (data) {
|
||||
if (
|
||||
!this.search.publicKey ||
|
||||
this.search.publicKey === data.customerPubkey
|
||||
) {
|
||||
const orderData = JSON.parse(data.dm.message)
|
||||
const i = this.orders.map(o => o.id).indexOf(orderData.id)
|
||||
if (i === -1) {
|
||||
const order = await this.getOrder(orderData.id)
|
||||
this.orders.unshift(order)
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
orderSelected: async function (orderId, eventId) {
|
||||
const order = await this.getOrder(orderId)
|
||||
if (!order) {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
"Order could not be found. Do you want to restore it from this direct message?"
|
||||
)
|
||||
.onOk(async () => {
|
||||
const restoredOrder = await this.restoreOrder(eventId)
|
||||
console.log('### restoredOrder', restoredOrder)
|
||||
if (restoredOrder) {
|
||||
restoredOrder.expanded = true
|
||||
restoredOrder.isNew = false
|
||||
this.orders = [restoredOrder]
|
||||
}
|
||||
|
||||
})
|
||||
return
|
||||
}
|
||||
order.expanded = true
|
||||
order.isNew = false
|
||||
this.orders = [order]
|
||||
},
|
||||
getZones: async function () {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/zone',
|
||||
this.inkey
|
||||
)
|
||||
return data.map(z => ({
|
||||
id: z.id,
|
||||
value: z.id,
|
||||
label: z.name
|
||||
? `${z.name} (${z.countries.join(', ')})`
|
||||
: z.countries.join(', ')
|
||||
}))
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
return []
|
||||
},
|
||||
getStalls: async function (pending = false) {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'GET',
|
||||
`/nostrmarket/api/v1/stall?pending=${pending}`,
|
||||
this.inkey
|
||||
)
|
||||
return data.map(s => ({ ...s, expanded: false }))
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
return []
|
||||
},
|
||||
getStallZones: function (stallId) {
|
||||
const stall = this.stalls.find(s => s.id === stallId)
|
||||
if (!stall) return []
|
||||
|
||||
return this.zoneOptions.filter(z => stall.shipping_zones.find(s => s.id === z.id))
|
||||
},
|
||||
showShipOrderDialog: function (order) {
|
||||
this.selectedOrder = order
|
||||
this.shippingMessage = order.shipped
|
||||
? 'The order has been shipped!'
|
||||
: 'The order has NOT yet been shipped!'
|
||||
|
||||
// do not change the status yet
|
||||
this.selectedOrder.shipped = !order.shipped
|
||||
this.showShipDialog = true
|
||||
},
|
||||
customerSelected: function (customerPubkey) {
|
||||
this.$emit('customer-selected', customerPubkey)
|
||||
},
|
||||
getCustomers: async function () {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/customer',
|
||||
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
|
||||
},
|
||||
orderPaid: function (orderId) {
|
||||
const order = this.orders.find(o => o.id === orderId)
|
||||
if (order) {
|
||||
order.paid = true
|
||||
}
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
if (this.stallId) {
|
||||
await this.getOrders()
|
||||
}
|
||||
await this.getCustomers()
|
||||
this.zoneOptions = await this.getZones()
|
||||
this.stalls = await this.getStalls()
|
||||
}
|
||||
})
|
||||
}
|
||||
183
static/components/shipping-zones.js
Normal file
183
static/components/shipping-zones.js
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
window.app.component('shipping-zones', {
|
||||
name: 'shipping-zones',
|
||||
props: ['adminkey', 'inkey'],
|
||||
template: '#shipping-zones',
|
||||
delimiters: ['${', '}'],
|
||||
data: function () {
|
||||
return {
|
||||
zones: [],
|
||||
zoneDialog: {
|
||||
showDialog: false,
|
||||
data: {
|
||||
id: null,
|
||||
name: '',
|
||||
countries: [],
|
||||
cost: 0,
|
||||
currency: 'sat'
|
||||
}
|
||||
},
|
||||
currencies: [],
|
||||
shippingZoneOptions: [
|
||||
'Free (digital)',
|
||||
'Flat rate',
|
||||
'Worldwide',
|
||||
'Europe',
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Belgium',
|
||||
'Brazil',
|
||||
'Canada',
|
||||
'Denmark',
|
||||
'Finland',
|
||||
'France',
|
||||
'Germany',
|
||||
'Greece',
|
||||
'Hong Kong',
|
||||
'Hungary',
|
||||
'Ireland',
|
||||
'Indonesia',
|
||||
'Israel',
|
||||
'Italy',
|
||||
'Japan',
|
||||
'Kazakhstan',
|
||||
'Korea',
|
||||
'Luxembourg',
|
||||
'Malaysia',
|
||||
'Mexico',
|
||||
'Netherlands',
|
||||
'New Zealand',
|
||||
'Norway',
|
||||
'Poland',
|
||||
'Portugal',
|
||||
'Romania',
|
||||
'Russia',
|
||||
'Saudi Arabia',
|
||||
'Singapore',
|
||||
'Spain',
|
||||
'Sweden',
|
||||
'Switzerland',
|
||||
'Thailand',
|
||||
'Turkey',
|
||||
'Ukraine',
|
||||
'United Kingdom**',
|
||||
'United States***',
|
||||
'Vietnam',
|
||||
'China'
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openZoneDialog: function (data) {
|
||||
data = data || {
|
||||
id: null,
|
||||
name: '',
|
||||
countries: [],
|
||||
cost: 0,
|
||||
currency: 'sat'
|
||||
}
|
||||
this.zoneDialog.data = data
|
||||
|
||||
this.zoneDialog.showDialog = true
|
||||
},
|
||||
createZone: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/zone',
|
||||
this.adminkey,
|
||||
{}
|
||||
)
|
||||
this.zones = data
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
getZones: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/zone',
|
||||
this.inkey
|
||||
)
|
||||
this.zones = data
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
sendZoneFormData: async function () {
|
||||
this.zoneDialog.showDialog = false
|
||||
if (this.zoneDialog.data.id) {
|
||||
await this.updateShippingZone(this.zoneDialog.data)
|
||||
} else {
|
||||
await this.createShippingZone(this.zoneDialog.data)
|
||||
}
|
||||
await this.getZones()
|
||||
},
|
||||
createShippingZone: async function (newZone) {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/zone',
|
||||
this.adminkey,
|
||||
newZone
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Zone created!'
|
||||
})
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
updateShippingZone: async function (updatedZone) {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PATCH',
|
||||
`/nostrmarket/api/v1/zone/${updatedZone.id}`,
|
||||
this.adminkey,
|
||||
updatedZone
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Zone updated!'
|
||||
})
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
deleteShippingZone: async function () {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
`/nostrmarket/api/v1/zone/${this.zoneDialog.data.id}`,
|
||||
this.adminkey
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Zone deleted!'
|
||||
})
|
||||
await this.getZones()
|
||||
this.zoneDialog.showDialog = false
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
async getCurrencies() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/currencies',
|
||||
this.inkey
|
||||
)
|
||||
|
||||
this.currencies = ['sat', ...data]
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
await this.getZones()
|
||||
await this.getCurrencies()
|
||||
}
|
||||
})
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
<div>
|
||||
<q-btn-dropdown
|
||||
split
|
||||
unelevated
|
||||
color="primary"
|
||||
icon="public"
|
||||
label="Zones"
|
||||
@click="openZoneDialog()"
|
||||
>
|
||||
<q-list>
|
||||
<q-item clickable v-close-popup @click="openZoneDialog()">
|
||||
<q-item-section>
|
||||
<q-item-label>New Shipping Zone</q-item-label>
|
||||
<q-item-label caption>Create a new shipping zone</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-for="zone of zones"
|
||||
:key="zone.id"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="openZoneDialog(zone)"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label>{{zone.name}}</q-item-label>
|
||||
<q-item-label caption>{{zone.countries.join(", ")}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list></q-btn-dropdown
|
||||
>
|
||||
|
||||
<q-dialog v-model="zoneDialog.showDialog" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form @submit="sendZoneFormData" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
label="Zone Name"
|
||||
type="text"
|
||||
v-model.trim="zoneDialog.data.name"
|
||||
></q-input>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
multiple
|
||||
:options="shippingZoneOptions"
|
||||
label="Countries"
|
||||
v-model="zoneDialog.data.countries"
|
||||
></q-select>
|
||||
<q-select
|
||||
:disabled="!!zoneDialog.data.id"
|
||||
:readonly="!!zoneDialog.data.id"
|
||||
filled
|
||||
dense
|
||||
v-model="zoneDialog.data.currency"
|
||||
type="text"
|
||||
label="Unit"
|
||||
:options="currencies"
|
||||
></q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
:label="'Cost of Shipping (' + zoneDialog.data.currency + ') *'"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
:step="zoneDialog.data.currency != 'sat' ? '0.01' : '1'"
|
||||
type="number"
|
||||
v-model.trim="zoneDialog.data.cost"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<div v-if="zoneDialog.data.id">
|
||||
<q-btn unelevated color="primary" type="submit">Update</q-btn>
|
||||
<q-btn
|
||||
@click="deleteShippingZone()"
|
||||
class="q-ml-md"
|
||||
unelevated
|
||||
color="pink"
|
||||
>Delete</q-btn
|
||||
>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="!zoneDialog.data.countries || !zoneDialog.data.countries.length"
|
||||
type="submit"
|
||||
>Create Shipping Zone</q-btn
|
||||
>
|
||||
</div>
|
||||
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
async function shippingZones(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('shipping-zones', {
|
||||
name: 'shipping-zones',
|
||||
props: ['adminkey', 'inkey'],
|
||||
template,
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
zones: [],
|
||||
zoneDialog: {
|
||||
showDialog: false,
|
||||
data: {
|
||||
id: null,
|
||||
name: '',
|
||||
countries: [],
|
||||
cost: 0,
|
||||
currency: 'sat'
|
||||
}
|
||||
},
|
||||
currencies: [],
|
||||
shippingZoneOptions: [
|
||||
'Free (digital)',
|
||||
'Flat rate',
|
||||
'Worldwide',
|
||||
'Europe',
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Belgium',
|
||||
'Brazil',
|
||||
'Canada',
|
||||
'Denmark',
|
||||
'Finland',
|
||||
'France',
|
||||
'Germany',
|
||||
'Greece',
|
||||
'Hong Kong',
|
||||
'Hungary',
|
||||
'Ireland',
|
||||
'Indonesia',
|
||||
'Israel',
|
||||
'Italy',
|
||||
'Japan',
|
||||
'Kazakhstan',
|
||||
'Korea',
|
||||
'Luxembourg',
|
||||
'Malaysia',
|
||||
'Mexico',
|
||||
'Netherlands',
|
||||
'New Zealand',
|
||||
'Norway',
|
||||
'Poland',
|
||||
'Portugal',
|
||||
'Romania',
|
||||
'Russia',
|
||||
'Saudi Arabia',
|
||||
'Singapore',
|
||||
'Spain',
|
||||
'Sweden',
|
||||
'Switzerland',
|
||||
'Thailand',
|
||||
'Turkey',
|
||||
'Ukraine',
|
||||
'United Kingdom**',
|
||||
'United States***',
|
||||
'Vietnam',
|
||||
'China'
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openZoneDialog: function (data) {
|
||||
data = data || {
|
||||
id: null,
|
||||
name: '',
|
||||
countries: [],
|
||||
cost: 0,
|
||||
currency: 'sat'
|
||||
}
|
||||
this.zoneDialog.data = data
|
||||
|
||||
this.zoneDialog.showDialog = true
|
||||
},
|
||||
createZone: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/zone',
|
||||
this.adminkey,
|
||||
{}
|
||||
)
|
||||
this.zones = data
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
getZones: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/zone',
|
||||
this.inkey
|
||||
)
|
||||
this.zones = data
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
sendZoneFormData: async function () {
|
||||
this.zoneDialog.showDialog = false
|
||||
if (this.zoneDialog.data.id) {
|
||||
await this.updateShippingZone(this.zoneDialog.data)
|
||||
} else {
|
||||
await this.createShippingZone(this.zoneDialog.data)
|
||||
}
|
||||
await this.getZones()
|
||||
},
|
||||
createShippingZone: async function (newZone) {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/zone',
|
||||
this.adminkey,
|
||||
newZone
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Zone created!'
|
||||
})
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
updateShippingZone: async function (updatedZone) {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PATCH',
|
||||
`/nostrmarket/api/v1/zone/${updatedZone.id}`,
|
||||
this.adminkey,
|
||||
updatedZone
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Zone updated!'
|
||||
})
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
deleteShippingZone: async function () {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
`/nostrmarket/api/v1/zone/${this.zoneDialog.data.id}`,
|
||||
this.adminkey
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Zone deleted!'
|
||||
})
|
||||
await this.getZones()
|
||||
this.zoneDialog.showDialog = false
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
async getCurrencies() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/currencies',
|
||||
this.inkey
|
||||
)
|
||||
|
||||
this.currencies = ['sat', ...data]
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
await this.getZones()
|
||||
await this.getCurrencies()
|
||||
}
|
||||
})
|
||||
}
|
||||
338
static/components/stall-details.js
Normal file
338
static/components/stall-details.js
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
window.app.component('stall-details', {
|
||||
name: 'stall-details',
|
||||
template: '#stall-details',
|
||||
delimiters: ['${', '}'],
|
||||
props: [
|
||||
'stall-id',
|
||||
'adminkey',
|
||||
'inkey',
|
||||
'wallet-options',
|
||||
'zone-options',
|
||||
'currencies'
|
||||
],
|
||||
data: function () {
|
||||
return {
|
||||
tab: 'products',
|
||||
stall: null,
|
||||
products: [],
|
||||
pendingProducts: [],
|
||||
productDialog: {
|
||||
showDialog: false,
|
||||
showRestore: false,
|
||||
url: true,
|
||||
data: null
|
||||
},
|
||||
productsFilter: '',
|
||||
productsTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'delete',
|
||||
align: 'left',
|
||||
label: '',
|
||||
field: ''
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
align: 'left',
|
||||
label: '',
|
||||
field: ''
|
||||
},
|
||||
{
|
||||
name: 'activate',
|
||||
align: 'left',
|
||||
label: '',
|
||||
field: ''
|
||||
},
|
||||
|
||||
{
|
||||
name: 'id',
|
||||
align: 'left',
|
||||
label: 'ID',
|
||||
field: 'id'
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
align: 'left',
|
||||
label: 'Name',
|
||||
field: 'name'
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
align: 'left',
|
||||
label: 'Price',
|
||||
field: 'price'
|
||||
},
|
||||
{
|
||||
name: 'quantity',
|
||||
align: 'left',
|
||||
label: 'Quantity',
|
||||
field: 'quantity'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredZoneOptions: function () {
|
||||
if (!this.stall) return []
|
||||
return this.zoneOptions.filter(z => z.currency === this.stall.currency)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mapStall: function (stall) {
|
||||
stall.shipping_zones.forEach(
|
||||
z =>
|
||||
(z.label = z.name
|
||||
? `${z.name} (${z.countries.join(', ')})`
|
||||
: z.countries.join(', '))
|
||||
)
|
||||
return stall
|
||||
},
|
||||
newEmtpyProductData: function () {
|
||||
return {
|
||||
id: null,
|
||||
name: '',
|
||||
categories: [],
|
||||
images: [],
|
||||
image: null,
|
||||
price: 0,
|
||||
|
||||
quantity: 0,
|
||||
config: {
|
||||
description: '',
|
||||
use_autoreply: false,
|
||||
autoreply_message: '',
|
||||
shipping: (this.stall.shipping_zones || []).map(z => ({
|
||||
id: z.id,
|
||||
name: z.name,
|
||||
cost: 0
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
getStall: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/stall/' + this.stallId,
|
||||
this.inkey
|
||||
)
|
||||
this.stall = this.mapStall(data)
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
updateStall: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
'/nostrmarket/api/v1/stall/' + this.stallId,
|
||||
this.adminkey,
|
||||
this.stall
|
||||
)
|
||||
this.stall = this.mapStall(data)
|
||||
this.$emit('stall-updated', this.stall)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Stall Updated',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
deleteStall: function () {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
`
|
||||
Products and orders will be deleted also!
|
||||
Are you sure you want to delete this stall?
|
||||
`
|
||||
)
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
'/nostrmarket/api/v1/stall/' + this.stallId,
|
||||
this.adminkey
|
||||
)
|
||||
this.$emit('stall-deleted', this.stallId)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Stall Deleted',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
},
|
||||
addProductImage: function () {
|
||||
if (!isValidImageUrl(this.productDialog.data.image)) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Not a valid image URL',
|
||||
timeout: 5000
|
||||
})
|
||||
return
|
||||
}
|
||||
this.productDialog.data.images.push(this.productDialog.data.image)
|
||||
this.productDialog.data.image = null
|
||||
},
|
||||
removeProductImage: function (imageUrl) {
|
||||
const index = this.productDialog.data.images.indexOf(imageUrl)
|
||||
if (index !== -1) {
|
||||
this.productDialog.data.images.splice(index, 1)
|
||||
}
|
||||
},
|
||||
getProducts: async function (pending = false) {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/nostrmarket/api/v1/stall/product/${this.stall.id}?pending=${pending}`,
|
||||
this.inkey
|
||||
)
|
||||
return data
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
sendProductFormData: function () {
|
||||
const data = {
|
||||
stall_id: this.stall.id,
|
||||
id: this.productDialog.data.id,
|
||||
name: this.productDialog.data.name,
|
||||
|
||||
images: this.productDialog.data.images,
|
||||
price: this.productDialog.data.price,
|
||||
quantity: this.productDialog.data.quantity,
|
||||
categories: this.productDialog.data.categories,
|
||||
config: this.productDialog.data.config
|
||||
}
|
||||
this.productDialog.showDialog = false
|
||||
if (this.productDialog.data.id) {
|
||||
data.pending = false
|
||||
this.updateProduct(data)
|
||||
} else {
|
||||
this.createProduct(data)
|
||||
}
|
||||
},
|
||||
updateProduct: async function (product) {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'PATCH',
|
||||
'/nostrmarket/api/v1/product/' + product.id,
|
||||
this.adminkey,
|
||||
product
|
||||
)
|
||||
const index = this.products.findIndex(r => r.id === product.id)
|
||||
if (index !== -1) {
|
||||
this.products.splice(index, 1, data)
|
||||
} else {
|
||||
this.products.unshift(data)
|
||||
}
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Product Updated',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
createProduct: async function (payload) {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/product',
|
||||
this.adminkey,
|
||||
payload
|
||||
)
|
||||
this.products.unshift(data)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Product Created',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
editProduct: async function (product) {
|
||||
const emptyShipping = this.newEmtpyProductData().config.shipping
|
||||
this.productDialog.data = {...product}
|
||||
this.productDialog.data.config.shipping = emptyShipping.map(
|
||||
shippingZone => {
|
||||
const existingShippingCost = (product.config.shipping || []).find(
|
||||
ps => ps.id === shippingZone.id
|
||||
)
|
||||
shippingZone.cost = existingShippingCost?.cost || 0
|
||||
return shippingZone
|
||||
}
|
||||
)
|
||||
|
||||
this.productDialog.showDialog = true
|
||||
},
|
||||
deleteProduct: async function (productId) {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this product?')
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
'/nostrmarket/api/v1/product/' + productId,
|
||||
this.adminkey
|
||||
)
|
||||
this.products = _.reject(this.products, function (obj) {
|
||||
return obj.id === productId
|
||||
})
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Product deleted',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
},
|
||||
showNewProductDialog: async function (data) {
|
||||
this.productDialog.data = data || this.newEmtpyProductData()
|
||||
this.productDialog.showDialog = true
|
||||
},
|
||||
openSelectPendingProductDialog: async function () {
|
||||
this.productDialog.showRestore = true
|
||||
this.pendingProducts = await this.getProducts(true)
|
||||
},
|
||||
openRestoreProductDialog: async function (pendingProduct) {
|
||||
pendingProduct.pending = true
|
||||
await this.showNewProductDialog(pendingProduct)
|
||||
},
|
||||
restoreAllPendingProducts: async function () {
|
||||
for (const p of this.pendingProducts) {
|
||||
p.pending = false
|
||||
await this.updateProduct(p)
|
||||
}
|
||||
},
|
||||
customerSelectedForOrder: function (customerPubkey) {
|
||||
this.$emit('customer-selected-for-order', customerPubkey)
|
||||
},
|
||||
shortLabel(value = '') {
|
||||
if (value.length <= 44) return value
|
||||
return value.substring(0, 40) + '...'
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
await this.getStall()
|
||||
this.products = await this.getProducts()
|
||||
this.productDialog.data = this.newEmtpyProductData()
|
||||
}
|
||||
})
|
||||
|
|
@ -1,255 +0,0 @@
|
|||
<div>
|
||||
<q-tabs v-model="tab" no-caps class="bg-dark text-white shadow-2">
|
||||
<q-tab name="info" label="Stall Info"></q-tab>
|
||||
<q-tab name="products" label="Products"></q-tab>
|
||||
<q-tab name="orders" label="Orders"></q-tab>
|
||||
</q-tabs>
|
||||
<q-tab-panels v-model="tab">
|
||||
<q-tab-panel name="info">
|
||||
<div v-if="stall">
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col-3 q-pr-lg">ID:</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg">
|
||||
<q-input filled dense readonly disabled v-model.trim="stall.id" 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">Name:</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg">
|
||||
<q-input filled dense v-model.trim="stall.name" 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">Description:</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg">
|
||||
<q-input filled dense v-model.trim="stall.config.description" type="textarea" rows="3"
|
||||
label="Description"></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">Wallet:</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg">
|
||||
<q-select filled dense emit-value v-model="stall.wallet" :options="walletOptions" label="Wallet *">
|
||||
</q-select>
|
||||
</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">Currency:</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg">
|
||||
<q-select filled dense v-model="stall.currency" type="text" label="Unit" :options="currencies"></q-select>
|
||||
</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">Shipping Zones:</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg">
|
||||
<q-select :options="filteredZoneOptions" filled dense multiple v-model.trim="stall.shipping_zones"
|
||||
label="Shipping Zones"></q-select>
|
||||
</div>
|
||||
<div class="col-3 col-sm-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row items-center q-mt-xl">
|
||||
<div class="col-6 q-pr-lg">
|
||||
<q-btn outline unelevated class="float-left" color="primary" @click="updateStall()">Update Stall</q-btn>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-btn outline unelevated icon="cancel" class="float-right" @click="deleteStall()">Delete Stall</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="products">
|
||||
<div v-if="stall">
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col-3 q-pr-lg">
|
||||
|
||||
<q-btn-dropdown @click="showNewProductDialog()" outline unelevated split class="float-left" color="primary"
|
||||
label="New Product">
|
||||
<q-item @click="showNewProductDialog()" clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label>New Product</q-item-label>
|
||||
<q-item-label caption>Create a new product</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item @click="openSelectPendingProductDialog" clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label>Restore Product</q-item-label>
|
||||
<q-item-label caption>Restore existing product from Nostr</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-btn-dropdown>
|
||||
|
||||
</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg"></div>
|
||||
<div class="col-3 col-sm-1"></div>
|
||||
</div>
|
||||
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col-12">
|
||||
<q-table flat dense :data="products" row-key="id" :columns="productsTable.columns"
|
||||
:pagination.sync="productsTable.pagination" :filter="productsFilter">
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn size="sm" color="grey" dense @click="deleteProduct(props.row.id)" icon="delete" />
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn size="sm" color="primary" dense @click="editProduct(props.row)" icon="edit" />
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-toggle
|
||||
@input="updateProduct({ ...props.row, active: props.row.active })"
|
||||
size="xs"
|
||||
checked-icon="check"
|
||||
v-model="props.row.active"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-td>
|
||||
|
||||
<q-td key="id" :props="props"> {{props.row.id}} </q-td>
|
||||
<q-td key="name" :props="props"> {{shortLabel(props.row.name)}} </q-td>
|
||||
<q-td key="price" :props="props"> {{props.row.price}} </q-td>
|
||||
<q-td key="quantity" :props="props">
|
||||
{{props.row.quantity}}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="orders">
|
||||
<div v-if="stall">
|
||||
<order-list :adminkey="adminkey" :inkey="inkey" :stall-id="stallId"
|
||||
@customer-selected="customerSelectedForOrder"></order-list>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
<q-dialog v-model="productDialog.showDialog" position="top">
|
||||
<q-card v-if="stall && productDialog.data" class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form @submit="sendProductFormData" class="q-gutter-md">
|
||||
<q-input filled dense v-model.trim="productDialog.data.name" label="Name"></q-input>
|
||||
|
||||
<q-input filled dense v-model.trim="productDialog.data.config.description" label="Description"></q-input>
|
||||
|
||||
<div class="row q-mb-sm">
|
||||
<div class="col">
|
||||
<q-input filled dense v-model.number="productDialog.data.price" type="number"
|
||||
:label="'Price (' + stall.currency + ') *'" :step="stall.currency != 'sat' ? '0.01' : '1'"
|
||||
:mask="stall.currency != 'sat' ? '#.##' : '#'" fill-mask="0" reverse-fill-mask></q-input>
|
||||
</div>
|
||||
<div class="col q-ml-md">
|
||||
<q-input filled dense v-model.number="productDialog.data.quantity" type="number" label="Quantity"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<q-expansion-item group="advanced" label="Categories"
|
||||
caption="Add tags to producsts, make them easy to search.">
|
||||
<div class="q-pl-sm q-pt-sm">
|
||||
<q-select filled multiple dense emit-value v-model.trim="productDialog.data.categories" use-input use-chips
|
||||
multiple hide-dropdown-icon input-debounce="0" new-value-mode="add-unique"
|
||||
label="Categories (Hit Enter to add)" placeholder="crafts,robots,etc"></q-select>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item group="advanced" label="Images" caption="Add images for product.">
|
||||
<div class="q-pl-sm q-pt-sm">
|
||||
<q-input filled dense v-model.trim="productDialog.data.image" @keydown.enter="addProductImage" type="url"
|
||||
label="Image URL">
|
||||
<q-btn @click="addProductImage" dense flat icon="add"></q-btn></q-input>
|
||||
|
||||
<q-chip v-for="imageUrl in productDialog.data.images" :key="imageUrl" removable
|
||||
@remove="removeProductImage(imageUrl)" color="primary" text-color="white">
|
||||
<span v-text="imageUrl.split('/').pop()"></span>
|
||||
</q-chip>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
|
||||
|
||||
<q-expansion-item group="advanced" label="Custom Shipping Cost"
|
||||
caption="Configure custom shipping costs for this product">
|
||||
<div v-for="zone of productDialog.data.config.shipping" class="row q-mb-sm q-ml-lg q-mt-sm">
|
||||
<div class="col">
|
||||
<span v-text="zone.name"></span>
|
||||
</div>
|
||||
<div class="col q-pr-md">
|
||||
<q-input v-model="zone.cost" filled dense type="number" label="Extra cost">
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="advanced" label="Autoreply" caption="Autoreply when paid">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row q-mb-sm">
|
||||
<div class="col">
|
||||
<q-checkbox v-model="productDialog.data.config.use_autoreply" dense
|
||||
label="Send a direct message when paid" class="q-ml-sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-mb-sm q-ml-sm">
|
||||
<div class="col">
|
||||
<q-input v-model="productDialog.data.config.autoreply_message" filled dense type="textarea" rows="5"
|
||||
label="Autoreply message" hint="It can include link to a digital asset">
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn v-if="productDialog.data.id" type="submit"
|
||||
:label="productDialog.data.pending ? 'Restore Product' : 'Update Product'" unelevated
|
||||
color="primary"></q-btn>
|
||||
|
||||
<q-btn v-else unelevated color="primary" :disable="!productDialog.data.price
|
||||
|| !productDialog.data.name
|
||||
|| !productDialog.data.quantity" type="submit">Create Product</q-btn>
|
||||
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
|
||||
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="productDialog.showRestore" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<div v-if="pendingProducts && pendingProducts.length" class="row q-mt-lg">
|
||||
<q-item v-for="pendingProduct of pendingProducts" :key="pendingProduct.id" tag="label" class="full-width"
|
||||
v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label><span v-text="pendingProduct.name"></span></q-item-label>
|
||||
<q-item-label caption><span v-text="pendingProduct.config?.description"></span></q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section class="q-pl-xl float-right">
|
||||
<q-btn @click="openRestoreProductDialog(pendingProduct)" v-close-popup flat color="green"
|
||||
class="q-ml-auto float-right">Restore</q-btn>
|
||||
</q-item-section>
|
||||
<q-item-section class="float-right">
|
||||
<q-btn @click="deleteProduct(pendingProduct.id)" v-close-popup color="red" class="q-ml-auto float-right"
|
||||
icon="cancel"></q-btn>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div v-else>
|
||||
There are no products to be restored.
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn @click="restoreAllPendingProducts" v-close-popup flat color="green">Restore All</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
|
|
@ -1,334 +0,0 @@
|
|||
async function stallDetails(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
|
||||
Vue.component('stall-details', {
|
||||
name: 'stall-details',
|
||||
template,
|
||||
|
||||
props: [
|
||||
'stall-id',
|
||||
'adminkey',
|
||||
'inkey',
|
||||
'wallet-options',
|
||||
'zone-options',
|
||||
'currencies'
|
||||
],
|
||||
data: function () {
|
||||
return {
|
||||
tab: 'products',
|
||||
stall: null,
|
||||
products: [],
|
||||
pendingProducts: [],
|
||||
productDialog: {
|
||||
showDialog: false,
|
||||
showRestore: false,
|
||||
url: true,
|
||||
data: null
|
||||
},
|
||||
productsFilter: '',
|
||||
productsTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'delete',
|
||||
align: 'left',
|
||||
label: '',
|
||||
field: ''
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
align: 'left',
|
||||
label: '',
|
||||
field: ''
|
||||
},
|
||||
{
|
||||
name: 'activate',
|
||||
align: 'left',
|
||||
label: '',
|
||||
field: ''
|
||||
},
|
||||
|
||||
{
|
||||
name: 'id',
|
||||
align: 'left',
|
||||
label: 'ID',
|
||||
field: 'id'
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
align: 'left',
|
||||
label: 'Name',
|
||||
field: 'name'
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
align: 'left',
|
||||
label: 'Price',
|
||||
field: 'price'
|
||||
},
|
||||
{
|
||||
name: 'quantity',
|
||||
align: 'left',
|
||||
label: 'Quantity',
|
||||
field: 'quantity'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredZoneOptions: function () {
|
||||
if (!this.stall) return []
|
||||
return this.zoneOptions.filter(z => z.currency === this.stall.currency)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mapStall: function (stall) {
|
||||
stall.shipping_zones.forEach(
|
||||
z =>
|
||||
(z.label = z.name
|
||||
? `${z.name} (${z.countries.join(', ')})`
|
||||
: z.countries.join(', '))
|
||||
)
|
||||
return stall
|
||||
},
|
||||
newEmtpyProductData: function() {
|
||||
return {
|
||||
id: null,
|
||||
name: '',
|
||||
categories: [],
|
||||
images: [],
|
||||
image: null,
|
||||
price: 0,
|
||||
|
||||
quantity: 0,
|
||||
config: {
|
||||
description: '',
|
||||
use_autoreply: false,
|
||||
autoreply_message: '',
|
||||
shipping: (this.stall.shipping_zones || []).map(z => ({id: z.id, name: z.name, cost: 0}))
|
||||
}
|
||||
}
|
||||
},
|
||||
getStall: async function () {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/stall/' + this.stallId,
|
||||
this.inkey
|
||||
)
|
||||
this.stall = this.mapStall(data)
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
updateStall: async function () {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'PUT',
|
||||
'/nostrmarket/api/v1/stall/' + this.stallId,
|
||||
this.adminkey,
|
||||
this.stall
|
||||
)
|
||||
this.stall = this.mapStall(data)
|
||||
this.$emit('stall-updated', this.stall)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Stall Updated',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
deleteStall: function () {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
`
|
||||
Products and orders will be deleted also!
|
||||
Are you sure you want to delete this stall?
|
||||
`
|
||||
)
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
'/nostrmarket/api/v1/stall/' + this.stallId,
|
||||
this.adminkey
|
||||
)
|
||||
this.$emit('stall-deleted', this.stallId)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Stall Deleted',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
},
|
||||
addProductImage: function () {
|
||||
if (!isValidImageUrl(this.productDialog.data.image)) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Not a valid image URL',
|
||||
timeout: 5000
|
||||
})
|
||||
return
|
||||
}
|
||||
this.productDialog.data.images.push(this.productDialog.data.image)
|
||||
this.productDialog.data.image = null
|
||||
},
|
||||
removeProductImage: function (imageUrl) {
|
||||
const index = this.productDialog.data.images.indexOf(imageUrl)
|
||||
if (index !== -1) {
|
||||
this.productDialog.data.images.splice(index, 1)
|
||||
}
|
||||
},
|
||||
getProducts: async function (pending = false) {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'GET',
|
||||
`/nostrmarket/api/v1/stall/product/${this.stall.id}?pending=${pending}`,
|
||||
this.inkey
|
||||
)
|
||||
return data
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
sendProductFormData: function () {
|
||||
const data = {
|
||||
stall_id: this.stall.id,
|
||||
id: this.productDialog.data.id,
|
||||
name: this.productDialog.data.name,
|
||||
|
||||
images: this.productDialog.data.images,
|
||||
price: this.productDialog.data.price,
|
||||
quantity: this.productDialog.data.quantity,
|
||||
categories: this.productDialog.data.categories,
|
||||
config: this.productDialog.data.config
|
||||
}
|
||||
this.productDialog.showDialog = false
|
||||
if (this.productDialog.data.id) {
|
||||
data.pending = false
|
||||
this.updateProduct(data)
|
||||
} else {
|
||||
this.createProduct(data)
|
||||
}
|
||||
},
|
||||
updateProduct: async function (product) {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'PATCH',
|
||||
'/nostrmarket/api/v1/product/' + product.id,
|
||||
this.adminkey,
|
||||
product
|
||||
)
|
||||
const index = this.products.findIndex(r => r.id === product.id)
|
||||
if (index !== -1) {
|
||||
this.products.splice(index, 1, data)
|
||||
} else {
|
||||
this.products.unshift(data)
|
||||
}
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Product Updated',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
createProduct: async function (payload) {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/product',
|
||||
this.adminkey,
|
||||
payload
|
||||
)
|
||||
this.products.unshift(data)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Product Created',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
editProduct: async function (product) {
|
||||
const emptyShipping = this.newEmtpyProductData().config.shipping
|
||||
this.productDialog.data = { ...product }
|
||||
this.productDialog.data.config.shipping = emptyShipping.map(shippingZone => {
|
||||
const existingShippingCost = (product.config.shipping || []).find(ps => ps.id === shippingZone.id)
|
||||
shippingZone.cost = existingShippingCost?.cost || 0
|
||||
return shippingZone
|
||||
})
|
||||
|
||||
this.productDialog.showDialog = true
|
||||
},
|
||||
deleteProduct: async function (productId) {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this product?')
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
'/nostrmarket/api/v1/product/' + productId,
|
||||
this.adminkey
|
||||
)
|
||||
this.products = _.reject(this.products, function (obj) {
|
||||
return obj.id === productId
|
||||
})
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Product deleted',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
},
|
||||
showNewProductDialog: async function (data) {
|
||||
this.productDialog.data = data || this.newEmtpyProductData()
|
||||
this.productDialog.showDialog = true
|
||||
},
|
||||
openSelectPendingProductDialog: async function () {
|
||||
this.productDialog.showRestore = true
|
||||
this.pendingProducts = await this.getProducts(true)
|
||||
},
|
||||
openRestoreProductDialog: async function (pendingProduct) {
|
||||
pendingProduct.pending = true
|
||||
await this.showNewProductDialog(pendingProduct)
|
||||
},
|
||||
restoreAllPendingProducts: async function () {
|
||||
for (const p of this.pendingProducts){
|
||||
p.pending = false
|
||||
await this.updateProduct(p)
|
||||
}
|
||||
},
|
||||
customerSelectedForOrder: function (customerPubkey) {
|
||||
this.$emit('customer-selected-for-order', customerPubkey)
|
||||
},
|
||||
shortLabel(value = ''){
|
||||
if (value.length <= 44) return value
|
||||
return value.substring(0, 40) + '...'
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
await this.getStall()
|
||||
this.products = await this.getProducts()
|
||||
this.productDialog.data = this.newEmtpyProductData()
|
||||
}
|
||||
})
|
||||
}
|
||||
262
static/components/stall-list.js
Normal file
262
static/components/stall-list.js
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
window.app.component('stall-list', {
|
||||
name: 'stall-list',
|
||||
template: '#stall-list',
|
||||
delimiters: ['${', '}'],
|
||||
props: [`adminkey`, 'inkey', 'wallet-options'],
|
||||
data: function () {
|
||||
return {
|
||||
filter: '',
|
||||
stalls: [],
|
||||
pendingStalls: [],
|
||||
currencies: [],
|
||||
stallDialog: {
|
||||
show: false,
|
||||
showRestore: false,
|
||||
data: {
|
||||
name: '',
|
||||
description: '',
|
||||
wallet: null,
|
||||
currency: 'sat',
|
||||
shippingZones: []
|
||||
}
|
||||
},
|
||||
zoneOptions: [],
|
||||
stallsTable: {
|
||||
columns: [
|
||||
{
|
||||
name: '',
|
||||
align: 'left',
|
||||
label: '',
|
||||
field: ''
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
align: 'left',
|
||||
label: 'Name',
|
||||
field: 'id'
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
align: 'left',
|
||||
label: 'Currency',
|
||||
field: 'currency'
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
align: 'left',
|
||||
label: 'Description',
|
||||
field: 'description'
|
||||
},
|
||||
{
|
||||
name: 'shippingZones',
|
||||
align: 'left',
|
||||
label: 'Shipping Zones',
|
||||
field: 'shippingZones'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredZoneOptions: function () {
|
||||
return this.zoneOptions.filter(
|
||||
z => z.currency === this.stallDialog.data.currency
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sendStallFormData: async function () {
|
||||
const stallData = {
|
||||
name: this.stallDialog.data.name,
|
||||
wallet: this.stallDialog.data.wallet,
|
||||
currency: this.stallDialog.data.currency,
|
||||
shipping_zones: this.stallDialog.data.shippingZones,
|
||||
config: {
|
||||
description: this.stallDialog.data.description
|
||||
}
|
||||
}
|
||||
if (this.stallDialog.data.id) {
|
||||
stallData.id = this.stallDialog.data.id
|
||||
await this.restoreStall(stallData)
|
||||
} else {
|
||||
await this.createStall(stallData)
|
||||
}
|
||||
},
|
||||
createStall: async function (stall) {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/stall',
|
||||
this.adminkey,
|
||||
stall
|
||||
)
|
||||
this.stallDialog.show = false
|
||||
data.expanded = false
|
||||
this.stalls.unshift(data)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Stall created!'
|
||||
})
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
restoreStall: async function (stallData) {
|
||||
try {
|
||||
stallData.pending = false
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
`/nostrmarket/api/v1/stall/${stallData.id}`,
|
||||
this.adminkey,
|
||||
stallData
|
||||
)
|
||||
this.stallDialog.show = false
|
||||
data.expanded = false
|
||||
this.stalls.unshift(data)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Stall restored!'
|
||||
})
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
deleteStall: async function (pendingStall) {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
`
|
||||
Are you sure you want to delete this pending stall '${pendingStall.name}'?
|
||||
`
|
||||
)
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
'/nostrmarket/api/v1/stall/' + pendingStall.id,
|
||||
this.adminkey
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Pending Stall Deleted',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
},
|
||||
getCurrencies: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/currencies',
|
||||
this.inkey
|
||||
)
|
||||
|
||||
return ['sat', ...data]
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
return []
|
||||
},
|
||||
getStalls: async function (pending = false) {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/nostrmarket/api/v1/stall?pending=${pending}`,
|
||||
this.inkey
|
||||
)
|
||||
return data.map(s => ({...s, expanded: false}))
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
return []
|
||||
},
|
||||
getZones: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/zone',
|
||||
this.inkey
|
||||
)
|
||||
return data.map(z => ({
|
||||
...z,
|
||||
label: z.name
|
||||
? `${z.name} (${z.countries.join(', ')})`
|
||||
: z.countries.join(', ')
|
||||
}))
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
return []
|
||||
},
|
||||
handleStallDeleted: function (stallId) {
|
||||
this.stalls = _.reject(this.stalls, function (obj) {
|
||||
return obj.id === stallId
|
||||
})
|
||||
},
|
||||
handleStallUpdated: function (stall) {
|
||||
const index = this.stalls.findIndex(r => r.id === stall.id)
|
||||
if (index !== -1) {
|
||||
stall.expanded = true
|
||||
this.stalls.splice(index, 1, stall)
|
||||
}
|
||||
},
|
||||
openCreateStallDialog: async function (stallData) {
|
||||
this.currencies = await this.getCurrencies()
|
||||
this.zoneOptions = await this.getZones()
|
||||
if (!this.zoneOptions || !this.zoneOptions.length) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Please create a Shipping Zone first!'
|
||||
})
|
||||
return
|
||||
}
|
||||
this.stallDialog.data = stallData || {
|
||||
name: '',
|
||||
description: '',
|
||||
wallet: null,
|
||||
currency: 'sat',
|
||||
shippingZones: []
|
||||
}
|
||||
this.stallDialog.show = true
|
||||
},
|
||||
openSelectPendingStallDialog: async function () {
|
||||
this.stallDialog.showRestore = true
|
||||
this.pendingStalls = await this.getStalls(true)
|
||||
},
|
||||
openRestoreStallDialog: async function (pendingStall) {
|
||||
const shippingZonesIds = this.zoneOptions.map(z => z.id)
|
||||
await this.openCreateStallDialog({
|
||||
id: pendingStall.id,
|
||||
name: pendingStall.name,
|
||||
description: pendingStall.config?.description,
|
||||
currency: pendingStall.currency,
|
||||
shippingZones: (pendingStall.shipping_zones || [])
|
||||
.filter(z => shippingZonesIds.indexOf(z.id) !== -1)
|
||||
.map(z => ({
|
||||
...z,
|
||||
label: z.name
|
||||
? `${z.name} (${z.countries.join(', ')})`
|
||||
: z.countries.join(', ')
|
||||
}))
|
||||
})
|
||||
},
|
||||
customerSelectedForOrder: function (customerPubkey) {
|
||||
this.$emit('customer-selected-for-order', customerPubkey)
|
||||
},
|
||||
shortLabel(value = '') {
|
||||
if (value.length <= 64) return value
|
||||
return value.substring(0, 60) + '...'
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
this.stalls = await this.getStalls()
|
||||
this.currencies = await this.getCurrencies()
|
||||
this.zoneOptions = await this.getZones()
|
||||
}
|
||||
})
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
<div>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col q-pr-lg">
|
||||
|
||||
<q-btn-dropdown @click="openCreateStallDialog()" outline unelevated split class="float-left" color="primary"
|
||||
label="New Stall (Store)">
|
||||
<q-item @click="openCreateStallDialog()" clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label>New Stall</q-item-label>
|
||||
<q-item-label caption>Create a new stall</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item @click="openSelectPendingStallDialog" clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label>Restore Stall</q-item-label>
|
||||
<q-item-label caption>Restore existing stall from Nostr</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-btn-dropdown>
|
||||
<q-input borderless dense debounce="300" v-model="filter" placeholder="Search" class="float-right">
|
||||
<template v-slot:append>
|
||||
<q-icon name="search"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-table flat dense :data="stalls" row-key="id" :columns="stallsTable.columns"
|
||||
:pagination.sync="stallsTable.pagination" :filter="filter">
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn size="sm" color="primary" round dense @click="props.row.expanded= !props.row.expanded"
|
||||
:icon="props.row.expanded? 'remove' : 'add'" />
|
||||
</q-td>
|
||||
|
||||
<q-td key="id" :props="props"> {{shortLabel(props.row.name)}} </q-td>
|
||||
<q-td key="currency" :props="props"> {{props.row.currency}} </q-td>
|
||||
<q-td key="description" :props="props">
|
||||
{{shortLabel(props.row.config.description)}}
|
||||
</q-td>
|
||||
<q-td key="shippingZones" :props="props">
|
||||
<div>
|
||||
{{shortLabel(props.row.shipping_zones.filter(z => !!z.name).map(z => z.name).join(', '))}}
|
||||
</div>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
<q-tr v-if="props.row.expanded" :props="props">
|
||||
<q-td colspan="100%">
|
||||
<div class="row items-center q-mb-lg">
|
||||
<div class="col-12">
|
||||
<stall-details :stall-id="props.row.id" :adminkey="adminkey" :inkey="inkey"
|
||||
:wallet-options="walletOptions" :zone-options="zoneOptions" :currencies="currencies"
|
||||
@stall-deleted="handleStallDeleted" @stall-updated="handleStallUpdated"
|
||||
@customer-selected-for-order="customerSelectedForOrder"></stall-details>
|
||||
</div>
|
||||
</div>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<div>
|
||||
<q-dialog v-model="stallDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form @submit="sendStallFormData" class="q-gutter-md">
|
||||
<q-input filled dense v-model.trim="stallDialog.data.name" label="Name"></q-input>
|
||||
<q-input filled dense v-model.trim="stallDialog.data.description" type="textarea" rows="3"
|
||||
label="Description"></q-input>
|
||||
<q-select filled dense emit-value v-model="stallDialog.data.wallet" :options="walletOptions" label="Wallet *">
|
||||
</q-select>
|
||||
<q-select filled dense v-model="stallDialog.data.currency" type="text" label="Unit"
|
||||
:options="currencies"></q-select>
|
||||
<q-select :options="filteredZoneOptions" filled dense multiple v-model.trim="stallDialog.data.shippingZones"
|
||||
label="Shipping Zones"></q-select>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn unelevated color="primary" :disable="!stallDialog.data.name
|
||||
|| !stallDialog.data.currency
|
||||
|| !stallDialog.data.wallet
|
||||
|| !stallDialog.data.shippingZones
|
||||
|| !stallDialog.data.shippingZones.length" type="submit"
|
||||
:label="stallDialog.data.id ? 'Restore Stall' : 'Create Stall'"></q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="stallDialog.showRestore" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<div v-if="pendingStalls && pendingStalls.length" class="row q-mt-lg">
|
||||
<q-item v-for="pendingStall of pendingStalls" :key="pendingStall.id" tag="label" class="full-width" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label><span v-text="pendingStall.name"></span></q-item-label>
|
||||
<q-item-label caption><span v-text="pendingStall.config?.description"></span></q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section class="q-pl-xl float-right">
|
||||
<q-btn @click="openRestoreStallDialog(pendingStall)" v-close-popup flat color="green"
|
||||
class="q-ml-auto float-right">Restore</q-btn>
|
||||
</q-item-section>
|
||||
<q-item-section class="float-right">
|
||||
<q-btn @click="deleteStall(pendingStall)" v-close-popup color="red" class="q-ml-auto float-right"
|
||||
icon="cancel"></q-btn>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div v-else>
|
||||
There are no stalls to be restored.
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
async function stallList(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('stall-list', {
|
||||
name: 'stall-list',
|
||||
template,
|
||||
|
||||
props: [`adminkey`, 'inkey', 'wallet-options'],
|
||||
data: function () {
|
||||
return {
|
||||
filter: '',
|
||||
stalls: [],
|
||||
pendingStalls: [],
|
||||
currencies: [],
|
||||
stallDialog: {
|
||||
show: false,
|
||||
showRestore: false,
|
||||
data: {
|
||||
name: '',
|
||||
description: '',
|
||||
wallet: null,
|
||||
currency: 'sat',
|
||||
shippingZones: []
|
||||
}
|
||||
},
|
||||
zoneOptions: [],
|
||||
stallsTable: {
|
||||
columns: [
|
||||
{
|
||||
name: '',
|
||||
align: 'left',
|
||||
label: '',
|
||||
field: ''
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
align: 'left',
|
||||
label: 'Name',
|
||||
field: 'id'
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
align: 'left',
|
||||
label: 'Currency',
|
||||
field: 'currency'
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
align: 'left',
|
||||
label: 'Description',
|
||||
field: 'description'
|
||||
},
|
||||
{
|
||||
name: 'shippingZones',
|
||||
align: 'left',
|
||||
label: 'Shipping Zones',
|
||||
field: 'shippingZones'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredZoneOptions: function () {
|
||||
return this.zoneOptions.filter(
|
||||
z => z.currency === this.stallDialog.data.currency
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sendStallFormData: async function () {
|
||||
const stallData = {
|
||||
name: this.stallDialog.data.name,
|
||||
wallet: this.stallDialog.data.wallet,
|
||||
currency: this.stallDialog.data.currency,
|
||||
shipping_zones: this.stallDialog.data.shippingZones,
|
||||
config: {
|
||||
description: this.stallDialog.data.description
|
||||
}
|
||||
}
|
||||
if (this.stallDialog.data.id) {
|
||||
stallData.id = this.stallDialog.data.id
|
||||
await this.restoreStall(stallData)
|
||||
} else {
|
||||
await this.createStall(stallData)
|
||||
}
|
||||
|
||||
},
|
||||
createStall: async function (stall) {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/stall',
|
||||
this.adminkey,
|
||||
stall
|
||||
)
|
||||
this.stallDialog.show = false
|
||||
data.expanded = false
|
||||
this.stalls.unshift(data)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Stall created!'
|
||||
})
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
restoreStall: async function (stallData) {
|
||||
try {
|
||||
stallData.pending = false
|
||||
const { data } = await LNbits.api.request(
|
||||
'PUT',
|
||||
`/nostrmarket/api/v1/stall/${stallData.id}`,
|
||||
this.adminkey,
|
||||
stallData
|
||||
)
|
||||
this.stallDialog.show = false
|
||||
data.expanded = false
|
||||
this.stalls.unshift(data)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Stall restored!'
|
||||
})
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
deleteStall: async function (pendingStall) {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
`
|
||||
Are you sure you want to delete this pending stall '${pendingStall.name}'?
|
||||
`
|
||||
)
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
'/nostrmarket/api/v1/stall/' + pendingStall.id,
|
||||
this.adminkey
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Pending Stall Deleted',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
},
|
||||
getCurrencies: async function () {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/currencies',
|
||||
this.inkey
|
||||
)
|
||||
|
||||
return ['sat', ...data]
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
return []
|
||||
},
|
||||
getStalls: async function (pending = false) {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'GET',
|
||||
`/nostrmarket/api/v1/stall?pending=${pending}`,
|
||||
this.inkey
|
||||
)
|
||||
return data.map(s => ({ ...s, expanded: false }))
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
return []
|
||||
},
|
||||
getZones: async function () {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/zone',
|
||||
this.inkey
|
||||
)
|
||||
return data.map(z => ({
|
||||
...z,
|
||||
label: z.name
|
||||
? `${z.name} (${z.countries.join(', ')})`
|
||||
: z.countries.join(', ')
|
||||
}))
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
return []
|
||||
},
|
||||
handleStallDeleted: function (stallId) {
|
||||
this.stalls = _.reject(this.stalls, function (obj) {
|
||||
return obj.id === stallId
|
||||
})
|
||||
},
|
||||
handleStallUpdated: function (stall) {
|
||||
const index = this.stalls.findIndex(r => r.id === stall.id)
|
||||
if (index !== -1) {
|
||||
stall.expanded = true
|
||||
this.stalls.splice(index, 1, stall)
|
||||
}
|
||||
},
|
||||
openCreateStallDialog: async function (stallData) {
|
||||
this.currencies = await this.getCurrencies()
|
||||
this.zoneOptions = await this.getZones()
|
||||
if (!this.zoneOptions || !this.zoneOptions.length) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Please create a Shipping Zone first!'
|
||||
})
|
||||
return
|
||||
}
|
||||
this.stallDialog.data = stallData || {
|
||||
name: '',
|
||||
description: '',
|
||||
wallet: null,
|
||||
currency: 'sat',
|
||||
shippingZones: []
|
||||
}
|
||||
this.stallDialog.show = true
|
||||
},
|
||||
openSelectPendingStallDialog: async function () {
|
||||
this.stallDialog.showRestore = true
|
||||
this.pendingStalls = await this.getStalls(true)
|
||||
},
|
||||
openRestoreStallDialog: async function (pendingStall) {
|
||||
const shippingZonesIds = this.zoneOptions.map(z => z.id)
|
||||
await this.openCreateStallDialog({
|
||||
id: pendingStall.id,
|
||||
name: pendingStall.name,
|
||||
description: pendingStall.config?.description,
|
||||
currency: pendingStall.currency,
|
||||
shippingZones: (pendingStall.shipping_zones || [])
|
||||
.filter(z => shippingZonesIds.indexOf(z.id) !== -1)
|
||||
.map(z => ({
|
||||
...z,
|
||||
label: z.name
|
||||
? `${z.name} (${z.countries.join(', ')})`
|
||||
: z.countries.join(', ')
|
||||
}))
|
||||
})
|
||||
},
|
||||
customerSelectedForOrder: function (customerPubkey) {
|
||||
this.$emit('customer-selected-for-order', customerPubkey)
|
||||
},
|
||||
shortLabel(value = ''){
|
||||
if (value.length <= 64) return value
|
||||
return value.substring(0, 60) + '...'
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
this.stalls = await this.getStalls()
|
||||
this.currencies = await this.getCurrencies()
|
||||
this.zoneOptions = await this.getZones()
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue