This commit is contained in:
Vlad Stan 2024-11-06 11:50:21 +02:00 committed by GitHub
parent 83c94e94db
commit 0fc26d096f
52 changed files with 6684 additions and 3120 deletions

View file

@ -0,0 +1,169 @@
<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>&nbsp; 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'"
: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>
<div v-else><span v-text="dm.message"></span></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-form>
<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>

View file

@ -0,0 +1,40 @@
<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)">
<lnbits-qrcode
:value="publicKey"
:options="{width: 250}"
class="rounded-borders"
></lnbits-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)"
>
<lnbits-qrcode :value="privateKey"></lnbits-qrcode>
</q-responsive>
<small><span v-text="privateKey"></span><br />Click to copy</small>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,62 @@
<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>

View file

@ -0,0 +1,369 @@
<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
:rows="orders"
row-key="id"
:columns="ordersTable.columns"
v-model:pagination="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">
<span v-text="toShortId(props.row.id)"></span>
<q-badge v-if="props.row.isNew" color="orange">new</q-badge></q-td
>
<q-td key="total" :props="props">
<span v-text="satBtc(props.row.total)"></span>
</q-td>
<q-td key="fiat" :props="props">
<span v-if="props.row.extra.currency !== 'sat'">
<span v-text="orderTotal(props.row)"></span
><span v-text="props.row.extra.currency"></span>
</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"
@update:model-value="showShipOrderDialog(props.row)"
:label="props.row.shipped ? 'Yes' : 'No'"
size="sm"
></q-checkbox>
</q-td>
<q-td key="public_key" :props="props">
<span
@click="customerSelected(props.row.public_key)"
class="cursor-pointer"
>
<span v-text="toShortId(props.row.public_key)"></span>
</span>
</q-td>
<q-td key="event_created_at" :props="props">
<span v-text="formatDate(props.row.event_created_at)"></span>
</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">
<span v-text="item.quantity"></span>
</div>
<div class="col-1">x</div>
<div class="col-4">
<p :title="productName(props.row, item.product_id)">
<span
v-text="shortLabel(productName(props.row, item.product_id))"
></span>
</p>
</div>
<div class="col-2">
<span
v-text="productPrice(props.row, item.product_id)"
></span>
</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">
<span v-text="props.row.extra.shipping_cost"></span>
<span v-text="props.row.extra.currency"></span>
</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>

View file

@ -0,0 +1,99 @@
<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><span v-text="zone.name"></span></q-item-label>
<q-item-label caption
><span v-text="zone.countries.join('', '')"></span
></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>

View file

@ -0,0 +1,466 @@
<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
:rows="products"
row-key="id"
:columns="productsTable.columns"
v-model:pagination="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
@update:model-value="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"
><span v-text="props.row.id"></span>
</q-td>
<q-td key="name" :props="props">
<span v-text="shortLabel(props.row.name)"></span>
</q-td>
<q-td key="price" :props="props"
><span v-text="props.row.price"></span>
</q-td>
<q-td key="quantity" :props="props">
<span v-text="props.row.quantity"></span>
</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>

View file

@ -0,0 +1,215 @@
<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
:rows="stalls"
row-key="id"
:columns="stallsTable.columns"
v-model:pagination="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"
><span v-text="shortLabel(props.row.name)"></span
></q-td>
<q-td key="currency" :props="props"
><span v-text="props.row.currency"></span>
</q-td>
<q-td key="description" :props="props">
<span v-text="shortLabel(props.row.config.description)"></span>
</q-td>
<q-td key="shippingZones" :props="props">
<div>
<span
v-text="shortLabel(props.row.shipping_zones.filter(z => !!z.name).map(z => z.name).join(', '))"
></span>
</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>

View file

@ -18,14 +18,17 @@
</div>
<div class="col-6">
<q-toggle
@input="toggleMerchantState()"
size="md"
checked-icon="check"
v-model="merchant.config.active"
color="primary"
unchecked-icon="clear"
class="float-left"
/> <span v-text="merchant.config.active ? 'Accepting Orders': 'Orders Paused'"></span>
@update:model-value="toggleMerchantState()"
size="md"
checked-icon="check"
v-model="merchant.config.active"
color="primary"
unchecked-icon="clear"
class="float-left"
/>
<span
v-text="merchant.config.active ? 'Accepting Orders': 'Orders Paused'"
></span>
</div>
<div class="col-2">
<shipping-zones
@ -232,15 +235,38 @@
}
</style>
<template id="key-pair"
>{% include("nostrmarket/components/key-pair.html") %}</template
>
<template id="shipping-zones"
>{% include("nostrmarket/components/shipping-zones.html") %}</template
>
<template id="stall-details"
>{% include("nostrmarket/components/stall-details.html") %}</template
>
<template id="stall-list"
>{% include("nostrmarket/components/stall-list.html") %}</template
>
<template id="order-list"
>{% include("nostrmarket/components/order-list.html") %}</template
><template id="direct-messages"
>{% include("nostrmarket/components/direct-messages.html") %}</template
>
<template id="merchant-details"
>{% include("nostrmarket/components/merchant-details.html") %}</template
>
<script src="{{ url_for('nostrmarket_static', path='js/nostr.bundle.js') }}"></script>
<script src="{{ url_for('nostrmarket_static', path='js/utils.js') }}"></script>
<script src="{{ url_for('nostrmarket_static', path='components/key-pair/key-pair.js') }}"></script>
<script src="{{ url_for('nostrmarket_static', path='components/shipping-zones/shipping-zones.js') }}"></script>
<script src="{{ url_for('nostrmarket_static', path='components/stall-details/stall-details.js') }}"></script>
<script src="{{ url_for('nostrmarket_static', path='components/stall-list/stall-list.js') }}"></script>
<script src="{{ url_for('nostrmarket_static', path='components/order-list/order-list.js') }}"></script>
<script src="{{ url_for('nostrmarket_static', path='components/direct-messages/direct-messages.js') }}"></script>
<script src="{{ url_for('nostrmarket_static', path='components/merchant-details/merchant-details.js') }}"></script>
<script src="{{ url_for('nostrmarket_static', path='js/index.js') }}"></script>
<script src="{{ static_url_for('nostrmarket/static', 'components/key-pair.js') }}"></script>
<script src="{{ static_url_for('nostrmarket/static', 'components/shipping-zones.js') }}"></script>
<script src="{{ static_url_for('nostrmarket/static', 'components/stall-details.js') }}"></script>
<script src="{{ static_url_for('nostrmarket/static', 'components/stall-list.js') }}"></script>
<script src="{{ static_url_for('nostrmarket/static', 'components/order-list.js') }}"></script>
<script src="{{ static_url_for('nostrmarket/static', 'components/direct-messages.js') }}"></script>
<script src="{{ static_url_for('nostrmarket/static', 'components/merchant-details.js') }}"></script>
{% endblock %}

View file

@ -1,36 +1,59 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<head>
<title>Nostr Market App</title>
<meta charset=utf-8>
<meta name=description content="A Nostr marketplace">
<meta name=format-detection content="telephone=no">
<meta name=msapplication-tap-highlight content=no>
<meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width">
<meta charset="utf-8" />
<meta name="description" content="A Nostr marketplace" />
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<meta
name="viewport"
content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width"
/>
<script src="{{ url_for('nostrmarket_static', path='market/js/nostr.bundle.js') }}"></script>
<script src="{{ url_for('nostrmarket_static', path='market/js/bolt11-decoder.js') }}"></script>
<script src="{{ url_for('nostrmarket_static', path='market/js/utils.js') }}"></script>
<link rel=icon type=image/png sizes=128x128
href="{{ url_for('nostrmarket_static', path='market/icons/favicon-128x128.png')}}">
<link
rel="icon"
type="image/png"
sizes="128x128"
href="{{ url_for('nostrmarket_static', path='market/icons/favicon-128x128.png')}}"
/>
<link rel=icon type=image/png sizes=128x128
href="{{ url_for('nostrmarket_static', path='market/icons/favicon-96x96.png')}}">
<link rel=icon type=image/png sizes=128x128
href="{{ url_for('nostrmarket_static', path='market/icons/favicon-32x32.png')}}">
<link rel=icon type=image/png sizes=128x128 href="{{ url_for('nostrmarket_static', path='market/favicon.ico')}}">
<link
rel="icon"
type="image/png"
sizes="128x128"
href="{{ url_for('nostrmarket_static', path='market/icons/favicon-96x96.png')}}"
/>
<link
rel="icon"
type="image/png"
sizes="128x128"
href="{{ url_for('nostrmarket_static', path='market/icons/favicon-32x32.png')}}"
/>
<link
rel="icon"
type="image/png"
sizes="128x128"
href="{{ url_for('nostrmarket_static', path='market/favicon.ico')}}"
/>
<!-- Note: the .js and .css build IDs must be updated when a new version si released for 'static/market/index.html'-->
<script type="module" crossorigin
src="{{ url_for('nostrmarket_static', path='market/assets/index.923cbbf9.js')}}"></script>
<link rel="stylesheet" href="{{ url_for('nostrmarket_static', path='market/assets/index.73d462e5.css')}}">
</head>
<script
type="module"
crossorigin
src="{{ url_for('nostrmarket_static', path='market/assets/index.923cbbf9.js')}}"
></script>
<link
rel="stylesheet"
href="{{ url_for('nostrmarket_static', path='market/assets/index.73d462e5.css')}}"
/>
</head>
<body>
<div id=q-app></div>
</body>
</html>
<body>
<div id="q-app"></div>
</body>
</html>