feat: operator admin UI (rooms, bookings, calendar blocks, settings)
Replaces the placeholder page with a real 4-tab admin (Vue 3 + Quasar 2 UMD,
no build). Rooms table with create/edit dialog + publish-toggle + delete;
per-room bookings view; per-room calendar blocks add/delete; operator
settings form (identity, relays, hold, deposit, times, policy).
UMD gotchas honored: no self-closing tags, ${ } interpolation, :style for
typography, static_url_for(path=...). Follows the spirekeeper admin pattern.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
This commit is contained in:
parent
f715d728ef
commit
e6973fa192
2 changed files with 487 additions and 16 deletions
|
|
@ -1,15 +1,247 @@
|
||||||
// Chatelet operator admin page — placeholder.
|
// Chatelet operator admin — LNbits page shell (Vue 3 + Quasar 2 UMD, no build).
|
||||||
// Quasar 2 + Vue 3 as UMD globals (no build step). Remember: no
|
// Gotchas honored: no self-closing tags in the template, `${ }` interpolation,
|
||||||
// self-closing tags in UMD templates, and use :style bindings (not <style>
|
// `:style` bindings for typography. See workspace CLAUDE.md.
|
||||||
// blocks) for per-element typography overrides on LNbits pages.
|
|
||||||
|
const API = '/chatelet/api/v1'
|
||||||
|
|
||||||
window.app = Vue.createApp({
|
window.app = Vue.createApp({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [window.windowMixin],
|
mixins: [windowMixin],
|
||||||
|
delimiters: ['${', '}'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
tab: 'rooms',
|
||||||
|
|
||||||
rooms: [],
|
rooms: [],
|
||||||
|
roomsLoading: false,
|
||||||
|
roomDialog: {show: false, data: {}},
|
||||||
|
currencyOptions: ['sat', 'EUR', 'USD', 'GBP', 'CHF'],
|
||||||
|
frequencyOptions: ['night', 'week', 'month'],
|
||||||
|
|
||||||
|
bookingsRoomId: null,
|
||||||
bookings: [],
|
bookings: [],
|
||||||
|
|
||||||
|
blocksRoomId: null,
|
||||||
|
blocks: [],
|
||||||
|
blockForm: {start_date: '', end_date: '', reason: ''},
|
||||||
|
|
||||||
|
settings: {},
|
||||||
|
settingsLoading: false,
|
||||||
|
|
||||||
|
roomsColumns: [
|
||||||
|
{name: 'title', label: 'Room', field: 'title', align: 'left'},
|
||||||
|
{
|
||||||
|
name: 'price', label: 'Price', align: 'left',
|
||||||
|
field: row => `${row.price_amount} ${row.price_currency} / ${row.price_frequency}`
|
||||||
|
},
|
||||||
|
{name: 'max_guests', label: 'Max guests', field: 'max_guests', align: 'center'},
|
||||||
|
{name: 'status', label: 'Status', field: 'status', align: 'center'},
|
||||||
|
{name: 'actions', label: '', field: 'id', align: 'right'}
|
||||||
|
],
|
||||||
|
bookingsColumns: [
|
||||||
|
{name: 'check_in', label: 'Check-in', field: 'check_in', align: 'left'},
|
||||||
|
{name: 'check_out', label: 'Check-out', field: 'check_out', align: 'left'},
|
||||||
|
{name: 'nights', label: 'Nights', field: 'nights', align: 'center'},
|
||||||
|
{name: 'num_guests', label: 'Guests', field: 'num_guests', align: 'center'},
|
||||||
|
{name: 'status', label: 'Status', field: 'status', align: 'left'},
|
||||||
|
{name: 'amount_sat', label: 'Sats', field: 'amount_sat', align: 'right'},
|
||||||
|
{
|
||||||
|
name: 'guest', label: 'Guest', align: 'left',
|
||||||
|
field: row => (row.guest_pubkey || '').slice(0, 12) + '…'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
blocksColumns: [
|
||||||
|
{name: 'start_date', label: 'From', field: 'start_date', align: 'left'},
|
||||||
|
{name: 'end_date', label: 'To (exclusive)', field: 'end_date', align: 'left'},
|
||||||
|
{name: 'reason', label: 'Reason', field: 'reason', align: 'left'},
|
||||||
|
{name: 'actions', label: '', field: 'id', align: 'right'}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// TODO: wire GET /chatelet/api/v1/rooms, room CRUD, calendar, bookings.
|
|
||||||
|
computed: {
|
||||||
|
adminkey() {
|
||||||
|
const w = this.g.user.wallets[0]
|
||||||
|
return w && w.adminkey
|
||||||
|
},
|
||||||
|
roomOptions() {
|
||||||
|
return this.rooms.map(r => ({label: r.title, value: r.id}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
_err(err, fallback) {
|
||||||
|
const msg = (err && err.response && err.response.data && err.response.data.detail)
|
||||||
|
|| (err && err.message) || fallback
|
||||||
|
Quasar.Notify.create({type: 'negative', message: msg, timeout: 5000})
|
||||||
|
},
|
||||||
|
statusColor(status) {
|
||||||
|
return {
|
||||||
|
active: 'green', inactive: 'grey',
|
||||||
|
held: 'orange', awaiting_payment: 'amber', confirmed: 'green',
|
||||||
|
checked_in: 'teal', completed: 'blue-grey',
|
||||||
|
cancelled: 'red', declined: 'red', expired: 'grey'
|
||||||
|
}[status] || 'grey'
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- rooms ---
|
||||||
|
async getRooms() {
|
||||||
|
this.roomsLoading = true
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request('GET', `${API}/rooms`, this.adminkey)
|
||||||
|
this.rooms = data
|
||||||
|
} catch (err) {
|
||||||
|
this._err(err, 'Could not load rooms')
|
||||||
|
} finally {
|
||||||
|
this.roomsLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
newRoom() {
|
||||||
|
this.roomDialog = {
|
||||||
|
show: true,
|
||||||
|
data: {
|
||||||
|
price_currency: 'sat', price_frequency: 'night',
|
||||||
|
max_guests: 2, min_nights: 1, amenities: [], images: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
editRoom(room) {
|
||||||
|
this.roomDialog = {show: true, data: JSON.parse(JSON.stringify(room))}
|
||||||
|
},
|
||||||
|
async saveRoom() {
|
||||||
|
const d = this.roomDialog.data
|
||||||
|
const body = {
|
||||||
|
title: d.title,
|
||||||
|
description: d.description || '',
|
||||||
|
price_amount: Number(d.price_amount),
|
||||||
|
price_currency: d.price_currency,
|
||||||
|
price_frequency: d.price_frequency || 'night',
|
||||||
|
max_guests: Number(d.max_guests),
|
||||||
|
min_nights: Number(d.min_nights),
|
||||||
|
amenities: d.amenities || [],
|
||||||
|
location: d.location || '',
|
||||||
|
geohash: d.geohash || '',
|
||||||
|
images: d.images || [],
|
||||||
|
checkin_instructions: d.checkin_instructions || ''
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (d.id) {
|
||||||
|
await LNbits.api.request('PUT', `${API}/rooms/${d.id}`, this.adminkey, body)
|
||||||
|
} else {
|
||||||
|
await LNbits.api.request('POST', `${API}/rooms`, this.adminkey, body)
|
||||||
|
}
|
||||||
|
this.roomDialog.show = false
|
||||||
|
Quasar.Notify.create({type: 'positive', message: 'Room saved'})
|
||||||
|
this.getRooms()
|
||||||
|
} catch (err) {
|
||||||
|
this._err(err, 'Could not save room')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteRoom(room) {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog(`Delete room "${room.title}"? Its bookings + blocks go too.`)
|
||||||
|
.onOk(async () => {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request('DELETE', `${API}/rooms/${room.id}`, this.adminkey)
|
||||||
|
this.getRooms()
|
||||||
|
} catch (err) {
|
||||||
|
this._err(err, 'Could not delete room')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async setPublished(room, on) {
|
||||||
|
const path = on ? 'publish' : 'unpublish'
|
||||||
|
try {
|
||||||
|
await LNbits.api.request('POST', `${API}/rooms/${room.id}/${path}`, this.adminkey)
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: on ? 'Published to relays' : 'Unpublished'
|
||||||
|
})
|
||||||
|
this.getRooms()
|
||||||
|
} catch (err) {
|
||||||
|
this._err(err, 'Could not change publish state')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- bookings ---
|
||||||
|
async loadBookings() {
|
||||||
|
if (!this.bookingsRoomId) return
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET', `${API}/rooms/${this.bookingsRoomId}/bookings`, this.adminkey
|
||||||
|
)
|
||||||
|
this.bookings = data
|
||||||
|
} catch (err) {
|
||||||
|
this._err(err, 'Could not load bookings')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- blocks ---
|
||||||
|
async loadBlocks() {
|
||||||
|
if (!this.blocksRoomId) return
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET', `${API}/rooms/${this.blocksRoomId}/blocks`, this.adminkey
|
||||||
|
)
|
||||||
|
this.blocks = data
|
||||||
|
} catch (err) {
|
||||||
|
this._err(err, 'Could not load blocks')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async addBlock() {
|
||||||
|
if (!this.blocksRoomId) return
|
||||||
|
const body = {
|
||||||
|
room_id: this.blocksRoomId,
|
||||||
|
start_date: this.blockForm.start_date,
|
||||||
|
end_date: this.blockForm.end_date,
|
||||||
|
reason: this.blockForm.reason || ''
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await LNbits.api.request('POST', `${API}/blocks`, this.adminkey, body)
|
||||||
|
this.blockForm = {start_date: '', end_date: '', reason: ''}
|
||||||
|
Quasar.Notify.create({type: 'positive', message: 'Dates blocked'})
|
||||||
|
this.loadBlocks()
|
||||||
|
} catch (err) {
|
||||||
|
this._err(err, 'Could not add block')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async deleteBlock(block) {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request('DELETE', `${API}/blocks/${block.id}`, this.adminkey)
|
||||||
|
this.loadBlocks()
|
||||||
|
} catch (err) {
|
||||||
|
this._err(err, 'Could not delete block')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- settings ---
|
||||||
|
async getSettings() {
|
||||||
|
this.settingsLoading = true
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request('GET', `${API}/settings`, this.adminkey)
|
||||||
|
this.settings = data
|
||||||
|
} catch (err) {
|
||||||
|
this._err(err, 'Could not load settings')
|
||||||
|
} finally {
|
||||||
|
this.settingsLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveSettings() {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PUT', `${API}/settings`, this.adminkey, this.settings
|
||||||
|
)
|
||||||
|
this.settings = data
|
||||||
|
Quasar.Notify.create({type: 'positive', message: 'Settings saved'})
|
||||||
|
} catch (err) {
|
||||||
|
this._err(err, 'Could not save settings')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.getRooms()
|
||||||
|
this.getSettings()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,257 @@
|
||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context %}
|
{% extends "base.html" %}
|
||||||
|
{% from "macros.jinja" import window_vars with context %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{{ window_vars(user) }}
|
||||||
|
<script src="{{ static_url_for('chatelet/static', path='js/index.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
<div class="row items-center q-mb-md">
|
||||||
<h5 class="text-subtitle1 q-my-none">Chatelet — Room Rentals</h5>
|
<div class="col">
|
||||||
<p class="text-caption">
|
<h4 class="q-my-none">Chatelet — Room Rentals</h4>
|
||||||
Nostr-native room rentals. Operator admin UI goes here (rooms,
|
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||||||
calendar, bookings). Guest booking UI lives in the webapp.
|
Operator admin. The guest booking UI lives in the webapp.
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-card>
|
||||||
|
<q-tabs
|
||||||
|
v-model="tab" align="left"
|
||||||
|
active-color="primary" indicator-color="primary" class="text-grey">
|
||||||
|
<q-tab name="rooms" icon="hotel" label="Rooms"></q-tab>
|
||||||
|
<q-tab name="bookings" icon="event_available" label="Bookings"></q-tab>
|
||||||
|
<q-tab name="blocks" icon="event_busy" label="Calendar blocks"></q-tab>
|
||||||
|
<q-tab name="settings" icon="settings" label="Settings"></q-tab>
|
||||||
|
</q-tabs>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
|
||||||
|
<q-tab-panels v-model="tab" animated>
|
||||||
|
|
||||||
|
<!-- ROOMS -->
|
||||||
|
<q-tab-panel name="rooms">
|
||||||
|
<div class="row items-center q-mb-md">
|
||||||
|
<div class="col"><div class="text-h6">Rooms</div></div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-btn color="primary" icon="add" label="Add room" @click="newRoom"></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
:rows="rooms" :columns="roomsColumns" row-key="id"
|
||||||
|
:loading="roomsLoading" flat :pagination="{rowsPerPage: 0}"
|
||||||
|
no-data-label="No rooms yet — add one to get started">
|
||||||
|
<template v-slot:body-cell-status="props">
|
||||||
|
<q-td :props="props">
|
||||||
|
<q-chip :color="statusColor(props.value)" text-color="white" dense square>
|
||||||
|
${ props.value }
|
||||||
|
</q-chip>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body-cell-actions="props">
|
||||||
|
<q-td :props="props" class="text-right">
|
||||||
|
<q-btn
|
||||||
|
v-if="props.row.status !== 'active'"
|
||||||
|
flat dense round icon="cloud_upload" color="green"
|
||||||
|
@click="setPublished(props.row, true)">
|
||||||
|
<q-tooltip>Publish to relays</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-else flat dense round icon="cloud_off" color="grey"
|
||||||
|
@click="setPublished(props.row, false)">
|
||||||
|
<q-tooltip>Unpublish</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn flat dense round icon="edit" color="primary"
|
||||||
|
@click="editRoom(props.row)">
|
||||||
|
<q-tooltip>Edit</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn flat dense round icon="delete" color="negative"
|
||||||
|
@click="deleteRoom(props.row)">
|
||||||
|
<q-tooltip>Delete</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!-- BOOKINGS -->
|
||||||
|
<q-tab-panel name="bookings">
|
||||||
|
<div class="row items-center q-mb-md q-gutter-sm">
|
||||||
|
<q-select
|
||||||
|
style="min-width: 260px" outlined dense
|
||||||
|
v-model="bookingsRoomId" :options="roomOptions"
|
||||||
|
emit-value map-options label="Room"
|
||||||
|
@update:model-value="loadBookings"></q-select>
|
||||||
|
<q-btn flat dense icon="refresh" @click="loadBookings"
|
||||||
|
:disable="!bookingsRoomId">
|
||||||
|
<q-tooltip>Refresh</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
:rows="bookings" :columns="bookingsColumns" row-key="id" flat
|
||||||
|
:pagination="{rowsPerPage: 0}"
|
||||||
|
no-data-label="Pick a room to see its bookings">
|
||||||
|
<template v-slot:body-cell-status="props">
|
||||||
|
<q-td :props="props">
|
||||||
|
<q-chip :color="statusColor(props.value)" text-color="white" dense square>
|
||||||
|
${ props.value }
|
||||||
|
</q-chip>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!-- CALENDAR BLOCKS -->
|
||||||
|
<q-tab-panel name="blocks">
|
||||||
|
<div class="row items-center q-mb-md q-gutter-sm">
|
||||||
|
<q-select
|
||||||
|
style="min-width: 260px" outlined dense
|
||||||
|
v-model="blocksRoomId" :options="roomOptions"
|
||||||
|
emit-value map-options label="Room"
|
||||||
|
@update:model-value="loadBlocks"></q-select>
|
||||||
|
<q-btn flat dense icon="refresh" @click="loadBlocks"
|
||||||
|
:disable="!blocksRoomId">
|
||||||
|
<q-tooltip>Refresh</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<div v-if="blocksRoomId" class="row q-col-gutter-sm q-mb-md items-end">
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-input outlined dense type="date" v-model="blockForm.start_date"
|
||||||
|
label="From"></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-input outlined dense type="date" v-model="blockForm.end_date"
|
||||||
|
label="To (exclusive)"></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-input outlined dense v-model="blockForm.reason"
|
||||||
|
label="Reason (optional)"></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-btn color="primary" icon="add" label="Block" @click="addBlock"
|
||||||
|
:disable="!blockForm.start_date || !blockForm.end_date"></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
:rows="blocks" :columns="blocksColumns" row-key="id" flat
|
||||||
|
:pagination="{rowsPerPage: 0}"
|
||||||
|
no-data-label="No manual blocks for this room">
|
||||||
|
<template v-slot:body-cell-actions="props">
|
||||||
|
<q-td :props="props" class="text-right">
|
||||||
|
<q-btn flat dense round icon="delete" color="negative"
|
||||||
|
@click="deleteBlock(props.row)"></q-btn>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!-- SETTINGS -->
|
||||||
|
<q-tab-panel name="settings">
|
||||||
|
<div class="text-h6 q-mb-md">Operator settings</div>
|
||||||
|
<div class="row q-col-gutter-md" style="max-width: 760px">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input outlined dense v-model="settings.operator_id"
|
||||||
|
label="Operator account id"
|
||||||
|
hint="LNbits account whose Nostr signer publishes listings + check-in DMs (bunker-backed for the encrypted flows)"></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<q-select outlined dense v-model="settings.relays" label="Relays"
|
||||||
|
use-chips multiple use-input hide-dropdown-icon
|
||||||
|
new-value-mode="add-unique"
|
||||||
|
hint="Relays where listings + availability are published"></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<q-input outlined dense type="number" label="Hold minutes"
|
||||||
|
v-model.number="settings.default_hold_minutes"></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<q-input outlined dense type="number" label="Deposit %"
|
||||||
|
v-model.number="settings.deposit_percent"></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<q-input outlined dense label="Check-in time"
|
||||||
|
v-model="settings.checkin_time"></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<q-input outlined dense label="Check-out time"
|
||||||
|
v-model="settings.checkout_time"></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input outlined dense type="textarea" autogrow
|
||||||
|
label="Cancellation policy"
|
||||||
|
v-model="settings.cancellation_policy"></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<q-toggle v-model="settings.publish_availability"
|
||||||
|
label="Publish blocked dates to a public calendar (kind:31923)"></q-toggle>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<q-btn color="primary" label="Save settings" @click="saveSettings"
|
||||||
|
:loading="settingsLoading"></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
</q-tab-panels>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
|
||||||
<script src="{{ static_url_for('chatelet/static', 'js/index.js') }}"></script>
|
<!-- ROOM CREATE / EDIT DIALOG -->
|
||||||
|
<q-dialog v-model="roomDialog.show">
|
||||||
|
<q-card style="min-width: 480px; max-width: 640px">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">${ roomDialog.data.id ? 'Edit room' : 'New room' }</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-gutter-sm scroll" style="max-height: 70vh">
|
||||||
|
<q-input outlined dense v-model="roomDialog.data.title" label="Title *"></q-input>
|
||||||
|
<q-input outlined dense type="textarea" autogrow
|
||||||
|
v-model="roomDialog.data.description" label="Description (markdown)"></q-input>
|
||||||
|
<div class="row q-col-gutter-sm">
|
||||||
|
<div class="col-4">
|
||||||
|
<q-input outlined dense type="number" label="Price *"
|
||||||
|
v-model.number="roomDialog.data.price_amount"></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<q-select outlined dense label="Currency" :options="currencyOptions"
|
||||||
|
v-model="roomDialog.data.price_currency"></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<q-select outlined dense label="Per" :options="frequencyOptions"
|
||||||
|
v-model="roomDialog.data.price_frequency"></q-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-col-gutter-sm">
|
||||||
|
<div class="col-6">
|
||||||
|
<q-input outlined dense type="number" label="Max guests"
|
||||||
|
v-model.number="roomDialog.data.max_guests"></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<q-input outlined dense type="number" label="Min nights"
|
||||||
|
v-model.number="roomDialog.data.min_nights"></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-input outlined dense v-model="roomDialog.data.location" label="Location"></q-input>
|
||||||
|
<q-input outlined dense v-model="roomDialog.data.geohash"
|
||||||
|
label="Geohash (optional)"></q-input>
|
||||||
|
<q-select outlined dense v-model="roomDialog.data.amenities" label="Amenities"
|
||||||
|
use-chips multiple use-input hide-dropdown-icon
|
||||||
|
new-value-mode="add-unique"></q-select>
|
||||||
|
<q-select outlined dense v-model="roomDialog.data.images" label="Image URLs"
|
||||||
|
use-chips multiple use-input hide-dropdown-icon
|
||||||
|
new-value-mode="add-unique"></q-select>
|
||||||
|
<q-input outlined dense type="textarea" autogrow
|
||||||
|
v-model="roomDialog.data.checkin_instructions"
|
||||||
|
label="Check-in instructions (private — sent only in the encrypted DM)"></q-input>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn flat label="Cancel" v-close-popup></q-btn>
|
||||||
|
<q-btn color="primary" label="Save" @click="saveRoom"
|
||||||
|
:disable="!roomDialog.data.title || roomDialog.data.price_amount == null"></q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue