LNbits convention: extends base.html, declares window.app =
Vue.createApp({mixins: [windowMixin], data, methods, created}); the
LNbits init-app.js loads after extension scripts and finishes the
mount with app.use(Quasar) + app.mount('#vue').
Pages
- index.html restaurant list / dashboard with create dialog;
scoped to the logged-in user's wallets.
- menu.html category sidebar + items grid; full item dialog
with price/currency/images/dietary/allergens/
ingredients/calories/stock/availability/featured.
Modifier groups managed in a separate dialog
with required|optional + one|many semantics.
- orders.html filterable q-table with status colors and inline
state-machine actions (accept/ready/complete/
cancel). Polls every 8s.
- kds.html kitchen display: card-per-order, items + selected
modifiers + notes, age-based color escalation
(>5min orange, >15min red), polls every 5s. The
poll loop is a stand-in until SSE/Nostr push
lands.
- settings.html restaurant profile editor + delete + per-instance
ext settings panel (Nostr publish toggle, auto-
accept, invoice expiry).
Static
- js/api.js single REST client (LNbits.api.request wrapper)
used by all pages.
- js/index.js dashboard logic.
- js/menu.js menu CRUD.
- js/orders.js order monitor.
- js/kds.js kitchen display.
- js/settings.js settings persistence.
Customer kiosk UI lives in ~/dev/webapp; this extension only ships
the operator console.
108 lines
3.4 KiB
JavaScript
108 lines
3.4 KiB
JavaScript
window.app = Vue.createApp({
|
|
el: '#vue',
|
|
mixins: [windowMixin],
|
|
data() {
|
|
return {
|
|
restaurant: window.RESTAURANT_BOOTSTRAP || {},
|
|
orders: [],
|
|
statusFilter: ['pending', 'paid', 'accepted', 'ready'],
|
|
statusOptions: [
|
|
{label: 'Pending', value: 'pending'},
|
|
{label: 'Paid', value: 'paid'},
|
|
{label: 'Accepted', value: 'accepted'},
|
|
{label: 'Ready', value: 'ready'},
|
|
{label: 'Completed', value: 'completed'},
|
|
{label: 'Canceled', value: 'canceled'},
|
|
{label: 'Refunded', value: 'refunded'}
|
|
],
|
|
orderDialog: {show: false, order: null, items: []},
|
|
columns: [
|
|
{
|
|
name: 'time',
|
|
label: 'When',
|
|
align: 'left',
|
|
field: (r) => r.time,
|
|
format: (v) => LNbits.utils.formatTimestamp(v)
|
|
},
|
|
{name: 'id', label: 'ID', align: 'left', field: (r) => r.id.slice(0, 8)},
|
|
{
|
|
name: 'customer',
|
|
label: 'Customer',
|
|
align: 'left',
|
|
field: (r) => r.customer_name || (r.customer_pubkey ? r.customer_pubkey.slice(0, 12) + '…' : '—')
|
|
},
|
|
{name: 'status', label: 'Status', align: 'left', field: 'status'},
|
|
{name: 'channel', label: 'Channel', align: 'left', field: 'channel'},
|
|
{name: 'total', label: 'Total', align: 'right', field: 'total_msat'},
|
|
{name: 'actions', label: '', align: 'right'}
|
|
],
|
|
pollHandle: null
|
|
}
|
|
},
|
|
computed: {
|
|
invoicekey() {
|
|
const w = this.g.user.wallets.find((w) => w.id === this.restaurant.wallet)
|
|
return (w && w.inkey) || (this.g.user.wallets[0] && this.g.user.wallets[0].inkey)
|
|
},
|
|
adminkey() {
|
|
const w = this.g.user.wallets.find((w) => w.id === this.restaurant.wallet)
|
|
return (w && w.adminkey) || (this.g.user.wallets[0] && this.g.user.wallets[0].adminkey)
|
|
}
|
|
},
|
|
methods: {
|
|
formatSat(msat) {
|
|
const sats = Math.round((msat || 0) / 1000)
|
|
const fmt = new Intl.NumberFormat(this.g.locale || 'en-US')
|
|
return `${fmt.format(sats)} sat`
|
|
},
|
|
statusColor(status) {
|
|
return {
|
|
pending: 'grey',
|
|
paid: 'positive',
|
|
accepted: 'blue',
|
|
ready: 'amber',
|
|
completed: 'teal',
|
|
canceled: 'negative',
|
|
refunded: 'purple'
|
|
}[status] || 'grey'
|
|
},
|
|
async fetchOrders() {
|
|
try {
|
|
const {data} = await RestaurantAPI.listOrders(
|
|
this.invoicekey,
|
|
this.restaurant.id,
|
|
this.statusFilter
|
|
)
|
|
this.orders = data
|
|
} catch (err) {
|
|
LNbits.utils.notifyApiError(err)
|
|
}
|
|
},
|
|
async viewOrder(order) {
|
|
try {
|
|
const {data} = await RestaurantAPI.getOrder(order.id)
|
|
this.orderDialog.order = data.order
|
|
this.orderDialog.items = data.items
|
|
this.orderDialog.show = true
|
|
} catch (err) {
|
|
LNbits.utils.notifyApiError(err)
|
|
}
|
|
},
|
|
async transition(order, newStatus) {
|
|
try {
|
|
await RestaurantAPI.transitionOrder(this.adminkey, order.id, newStatus)
|
|
await this.fetchOrders()
|
|
} catch (err) {
|
|
LNbits.utils.notifyApiError(err)
|
|
}
|
|
}
|
|
},
|
|
async created() {
|
|
await this.fetchOrders()
|
|
// Poll every 8s; replaced by SSE/Nostr push in a future iteration.
|
|
this.pollHandle = setInterval(() => this.fetchOrders(), 8000)
|
|
},
|
|
beforeUnmount() {
|
|
if (this.pollHandle) clearInterval(this.pollHandle)
|
|
}
|
|
})
|