V1 (#106)
This commit is contained in:
parent
83c94e94db
commit
0fc26d096f
52 changed files with 6684 additions and 3120 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -16,6 +16,7 @@ __pycache__
|
|||
htmlcov
|
||||
test-reports
|
||||
tests/data/*.sqlite3
|
||||
node_modules
|
||||
|
||||
*.swo
|
||||
*.swp
|
||||
|
|
|
|||
14
.prettierignore
Normal file
14
.prettierignore
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
**/.git
|
||||
**/.svn
|
||||
**/.hg
|
||||
**/node_modules
|
||||
|
||||
*.yml
|
||||
|
||||
**/static/market/*
|
||||
**/static/js/nostr.bundle.js*
|
||||
|
||||
|
||||
flake.lock
|
||||
|
||||
.venv
|
||||
12
.prettierrc
Normal file
12
.prettierrc
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"semi": false,
|
||||
"arrowParens": "avoid",
|
||||
"insertPragma": false,
|
||||
"printWidth": 80,
|
||||
"proseWrap": "preserve",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false,
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": false
|
||||
}
|
||||
47
Makefile
Normal file
47
Makefile
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
all: format check
|
||||
|
||||
format: prettier black ruff
|
||||
|
||||
check: mypy pyright checkblack checkruff checkprettier
|
||||
|
||||
prettier:
|
||||
poetry run ./node_modules/.bin/prettier --write .
|
||||
pyright:
|
||||
poetry run ./node_modules/.bin/pyright
|
||||
|
||||
mypy:
|
||||
poetry run mypy .
|
||||
|
||||
black:
|
||||
poetry run black .
|
||||
|
||||
ruff:
|
||||
poetry run ruff check . --fix
|
||||
|
||||
checkruff:
|
||||
poetry run ruff check .
|
||||
|
||||
checkprettier:
|
||||
poetry run ./node_modules/.bin/prettier --check .
|
||||
|
||||
checkblack:
|
||||
poetry run black --check .
|
||||
|
||||
checkeditorconfig:
|
||||
editorconfig-checker
|
||||
|
||||
test:
|
||||
PYTHONUNBUFFERED=1 \
|
||||
DEBUG=true \
|
||||
poetry run pytest
|
||||
install-pre-commit-hook:
|
||||
@echo "Installing pre-commit hook to git"
|
||||
@echo "Uninstall the hook with poetry run pre-commit uninstall"
|
||||
poetry run pre-commit install
|
||||
|
||||
pre-commit:
|
||||
poetry run pre-commit run --all-files
|
||||
|
||||
|
||||
checkbundle:
|
||||
@echo "skipping checkbundle"
|
||||
22
README.md
22
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# Nostr Market ([NIP-15](https://github.com/nostr-protocol/nips/blob/master/15.md)) - <small>[LNbits](https://github.com/lnbits/lnbits) extension</small>
|
||||
<small>For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions).</small>
|
||||
|
||||
<small>For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions).</small>
|
||||
|
||||
**Demo at Nostrica <a href="https://www.youtube.com/live/2NueacYJovA?feature=share&t=6846">here</a>**.
|
||||
|
||||
|
|
@ -8,15 +8,15 @@
|
|||
|
||||
> The concepts around resilience in Diagon Alley helped influence the creation of the NOSTR protocol, now we get to build Diagon Alley on NOSTR!
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This extension uses the LNbits [nostrclient](https://github.com/lnbits/nostrclient) extension, an extension that makes _nostrfying_ other extensions easy.
|
||||

|
||||
|
||||
- before you continue, please make sure that [nostrclient](https://github.com/lnbits/nostrclient) extension is installed, activated and correctly configured.
|
||||
- [nostrclient](https://github.com/lnbits/nostrclient) is usually installed as admin-only extension, so if you do not have admin access please ask an admin to confirm that [nostrclient](https://github.com/lnbits/nostrclient) is OK.
|
||||
- see the [Troubleshoot](https://github.com/lnbits/nostrclient#troubleshoot) section for more details on how to check the health of `nostrclient` extension
|
||||
|
||||
|
||||
## Create, or import, a merchant account
|
||||
|
||||
As a merchant you need to provide a Nostr key pair, or the extension can generate one for you.
|
||||
|
|
@ -97,35 +97,39 @@ Make sure to add your `merchant` public key to the list:
|
|||

|
||||
|
||||
### Styling
|
||||
|
||||
In order to create a customized Marketplace, we use `naddr` as defined in [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md#shareable-identifiers-with-extra-metadata). You must create an event (kind: `30019`) that has all the custom properties, including merchants and relays, of your marketplace. Start by going to the marketplace page:
|
||||

|
||||
|
||||
You'll need to Login, and head over to *Marketplace Info*. Optionally import some merchants and relays, that will be included in the event. Click on *Edit* and fill out your marketplace custom info:
|
||||
You'll need to Login, and head over to _Marketplace Info_. Optionally import some merchants and relays, that will be included in the event. Click on _Edit_ and fill out your marketplace custom info:
|
||||

|
||||
|
||||
Fill in the optional fields:
|
||||
|
||||
- Add a name to the Marketplace
|
||||
- Add a small description
|
||||
- Add a logo image URL
|
||||
- Add a banner image URL (max height: 250px)
|
||||
- Choose a theme
|
||||
- Choose a theme
|
||||
|
||||
By clicking *Publish*, a `kind: 30019` event will be sent to the defined relays containing all the information about your custom Marketplace. On the left drawer, a button with *Copy Naddr* will show up.
|
||||
By clicking _Publish_, a `kind: 30019` event will be sent to the defined relays containing all the information about your custom Marketplace. On the left drawer, a button with _Copy Naddr_ will show up.
|
||||

|
||||
|
||||
You can then share your Marketplace, with the merchants and relays, banner, and style by using that Nostr identifier. The URL for the marketplace will be for example: `https://legend.lnbits.com/nostrmarket/market?naddr=naddr1qqfy6ctjddjhgurvv93k....`, you need to include the URL parameter `naddr=<your naddr>`. When a user visits that URL, the client will get the `30019` event and configure the Marketplace to what you defined. In the example bellow, a couple of merchants, relays, `autumn` theme, name (*Veggies Market*) and a header banner:
|
||||
You can then share your Marketplace, with the merchants and relays, banner, and style by using that Nostr identifier. The URL for the marketplace will be for example: `https://legend.lnbits.com/nostrmarket/market?naddr=naddr1qqfy6ctjddjhgurvv93k....`, you need to include the URL parameter `naddr=<your naddr>`. When a user visits that URL, the client will get the `30019` event and configure the Marketplace to what you defined. In the example bellow, a couple of merchants, relays, `autumn` theme, name (_Veggies Market_) and a header banner:
|
||||

|
||||
|
||||
The nostr event is a replaceable event, so you can change it to what you like and publish a new one to replace a previous one. For example adding a new merchant, or remove, change theme, add more relays,e tc...
|
||||
|
||||
|
||||
## Troubleshoot
|
||||
|
||||
### Check communication with Nostr
|
||||
|
||||
In order to test that the integration with Nostr is working fine, one can add an `npub` to the chat box and check that DMs are working as expected:
|
||||
|
||||
https://user-images.githubusercontent.com/2951406/236777983-259f81d8-136f-48b3-bb73-80749819b5f9.mov
|
||||
|
||||
### Restart connection to Nostr
|
||||
|
||||
If the communication with Nostr is not working then an admin user can `Restart` the Nostr connection.
|
||||
|
||||
Merchants can afterwards re-publish their products.
|
||||
|
|
@ -133,8 +137,8 @@ Merchants can afterwards re-publish their products.
|
|||
https://user-images.githubusercontent.com/2951406/236778651-7ada9f6d-07a1-491c-ac9c-55530326c32a.mp4
|
||||
|
||||
### Check Nostrclient extension
|
||||
- see the [Troubleshoot](https://github.com/lnbits/nostrclient#troubleshoot) section for more details on how to check the health of `nostrclient` extension
|
||||
|
||||
- see the [Troubleshoot](https://github.com/lnbits/nostrclient#troubleshoot) section for more details on how to check the health of `nostrclient` extension
|
||||
|
||||
## Aditional info
|
||||
|
||||
|
|
|
|||
19
__init__.py
19
__init__.py
|
|
@ -1,11 +1,10 @@
|
|||
import asyncio
|
||||
|
||||
from fastapi import APIRouter
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
from lnbits.tasks import create_permanent_unique_task
|
||||
from loguru import logger
|
||||
|
||||
from .nostr.nostr_client import NostrClient
|
||||
|
||||
|
|
@ -24,14 +23,14 @@ nostrmarket_static_files = [
|
|||
def nostrmarket_renderer():
|
||||
return template_renderer(["nostrmarket/templates"])
|
||||
|
||||
|
||||
nostr_client: NostrClient = NostrClient()
|
||||
|
||||
|
||||
from .tasks import wait_for_nostr_events, wait_for_paid_invoices
|
||||
from .tasks import wait_for_nostr_events, wait_for_paid_invoices # noqa
|
||||
from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
|
||||
|
||||
scheduled_tasks: list[asyncio.Task] = []
|
||||
|
||||
|
||||
|
|
@ -57,7 +56,13 @@ def nostrmarket_start():
|
|||
await asyncio.sleep(15)
|
||||
await wait_for_nostr_events(nostr_client)
|
||||
|
||||
task1 = create_permanent_unique_task("ext_nostrmarket_paid_invoices", wait_for_paid_invoices)
|
||||
task2 = create_permanent_unique_task("ext_nostrmarket_subscribe_to_nostr_client", _subscribe_to_nostr_client)
|
||||
task3 = create_permanent_unique_task("ext_nostrmarket_wait_for_events", _wait_for_nostr_events)
|
||||
task1 = create_permanent_unique_task(
|
||||
"ext_nostrmarket_paid_invoices", wait_for_paid_invoices
|
||||
)
|
||||
task2 = create_permanent_unique_task(
|
||||
"ext_nostrmarket_subscribe_to_nostr_client", _subscribe_to_nostr_client
|
||||
)
|
||||
task3 = create_permanent_unique_task(
|
||||
"ext_nostrmarket_wait_for_events", _wait_for_nostr_events
|
||||
)
|
||||
scheduled_tasks.extend([task1, task2, task3])
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "Nostr Market",
|
||||
"short_description": "Nostr Webshop/market on LNbits",
|
||||
"tile": "/nostrmarket/static/images/bitcoin-shop.png",
|
||||
"min_lnbits_version": "0.12.6",
|
||||
"min_lnbits_version": "1.0.0",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "motorina0",
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ Buy and sell things over Nostr, using NIP15 https://github.com/nostr-protocol/ni
|
|||
Nostr was partly based on the the previous version of this extension "Diagon Alley", so lends itself very well to buying and sellinng over Nostr.
|
||||
|
||||
The Nostr Market extension includes:
|
||||
* A merchant client to manage products, sales and communication with customers.
|
||||
* A customer client to find and order products from merchants, communicate with merchants and track status of ordered products.
|
||||
|
||||
- A merchant client to manage products, sales and communication with customers.
|
||||
- A customer client to find and order products from merchants, communicate with merchants and track status of ordered products.
|
||||
|
||||
All communication happens over NIP04 encrypted DMs.
|
||||
|
||||
|
|
|
|||
11
helpers.py
11
helpers.py
|
|
@ -1,7 +1,6 @@
|
|||
import base64
|
||||
import json
|
||||
import secrets
|
||||
from typing import Any, Optional, Tuple
|
||||
from typing import Optional
|
||||
|
||||
import secp256k1
|
||||
from bech32 import bech32_decode, convertbits
|
||||
|
|
@ -44,12 +43,14 @@ def encrypt_message(message: str, encryption_key, iv: Optional[bytes] = None) ->
|
|||
encryptor = cipher.encryptor()
|
||||
encrypted_message = encryptor.update(padded_data) + encryptor.finalize()
|
||||
|
||||
return f"{base64.b64encode(encrypted_message).decode()}?iv={base64.b64encode(iv).decode()}"
|
||||
base64_message = base64.b64encode(encrypted_message).decode()
|
||||
base64_iv = base64.b64encode(iv).decode()
|
||||
return f"{base64_message}?iv={base64_iv}"
|
||||
|
||||
|
||||
def sign_message_hash(private_key: str, hash: bytes) -> str:
|
||||
def sign_message_hash(private_key: str, hash_: bytes) -> str:
|
||||
privkey = secp256k1.PrivateKey(bytes.fromhex(private_key))
|
||||
sig = privkey.schnorr_sign(hash, None, raw=True)
|
||||
sig = privkey.schnorr_sign(hash_, None, raw=True)
|
||||
return sig.hex()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
async def m001_initial(db):
|
||||
|
||||
"""
|
||||
Initial merchants table.
|
||||
"""
|
||||
|
|
@ -121,7 +120,10 @@ async def m001_initial(db):
|
|||
Create indexes for message fetching
|
||||
"""
|
||||
await db.execute(
|
||||
"CREATE INDEX idx_messages_timestamp ON nostrmarket.direct_messages (time DESC)"
|
||||
"""
|
||||
CREATE INDEX idx_messages_timestamp
|
||||
ON nostrmarket.direct_messages (time DESC)
|
||||
"""
|
||||
)
|
||||
await db.execute(
|
||||
"CREATE INDEX idx_event_id ON nostrmarket.direct_messages (event_id)"
|
||||
|
|
@ -142,23 +144,26 @@ async def m001_initial(db):
|
|||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m002_update_stall_and_product(db):
|
||||
await db.execute(
|
||||
"ALTER TABLE nostrmarket.stalls ADD COLUMN pending BOOLEAN NOT NULL DEFAULT false;"
|
||||
)
|
||||
await db.execute(
|
||||
"ALTER TABLE nostrmarket.stalls ADD COLUMN event_id TEXT;"
|
||||
"""
|
||||
ALTER TABLE nostrmarket.stalls
|
||||
ADD COLUMN pending BOOLEAN NOT NULL DEFAULT false;
|
||||
"""
|
||||
)
|
||||
await db.execute("ALTER TABLE nostrmarket.stalls ADD COLUMN event_id TEXT;")
|
||||
await db.execute(
|
||||
"ALTER TABLE nostrmarket.stalls ADD COLUMN event_created_at INTEGER;"
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"ALTER TABLE nostrmarket.products ADD COLUMN pending BOOLEAN NOT NULL DEFAULT false;"
|
||||
)
|
||||
await db.execute(
|
||||
"ALTER TABLE nostrmarket.products ADD COLUMN event_id TEXT;"
|
||||
"""
|
||||
ALTER TABLE nostrmarket.products
|
||||
ADD COLUMN pending BOOLEAN NOT NULL DEFAULT false;
|
||||
"""
|
||||
)
|
||||
await db.execute("ALTER TABLE nostrmarket.products ADD COLUMN event_id TEXT;")
|
||||
await db.execute(
|
||||
"ALTER TABLE nostrmarket.products ADD COLUMN event_created_at INTEGER;"
|
||||
)
|
||||
|
|
@ -166,15 +171,21 @@ async def m002_update_stall_and_product(db):
|
|||
|
||||
async def m003_update_direct_message_type(db):
|
||||
await db.execute(
|
||||
"ALTER TABLE nostrmarket.direct_messages ADD COLUMN type INTEGER NOT NULL DEFAULT -1;"
|
||||
"""
|
||||
ALTER TABLE nostrmarket.direct_messages
|
||||
ADD COLUMN type INTEGER NOT NULL DEFAULT -1;
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m004_add_merchant_timestamp(db):
|
||||
await db.execute(
|
||||
f"ALTER TABLE nostrmarket.merchants ADD COLUMN time TIMESTAMP;"
|
||||
)
|
||||
await db.execute("ALTER TABLE nostrmarket.merchants ADD COLUMN time TIMESTAMP;")
|
||||
|
||||
|
||||
async def m005_update_product_activation(db):
|
||||
await db.execute(
|
||||
"ALTER TABLE nostrmarket.products ADD COLUMN active BOOLEAN NOT NULL DEFAULT true;"
|
||||
)
|
||||
"""
|
||||
ALTER TABLE nostrmarket.products
|
||||
ADD COLUMN active BOOLEAN NOT NULL DEFAULT true;
|
||||
"""
|
||||
)
|
||||
|
|
|
|||
197
models.py
197
models.py
|
|
@ -2,12 +2,10 @@ import json
|
|||
import time
|
||||
from abc import abstractmethod
|
||||
from enum import Enum
|
||||
from sqlite3 import Row
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from lnbits.utils.exchange_rates import btc_price, fiat_amount_as_satoshis
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .helpers import (
|
||||
decrypt_message,
|
||||
|
|
@ -30,17 +28,17 @@ class Nostrable:
|
|||
pass
|
||||
|
||||
|
||||
######################################## MERCHANT ########################################
|
||||
######################################## MERCHANT ######################################
|
||||
|
||||
|
||||
class MerchantProfile(BaseModel):
|
||||
name: Optional[str]
|
||||
about: Optional[str]
|
||||
picture: Optional[str]
|
||||
name: Optional[str] = None
|
||||
about: Optional[str] = None
|
||||
picture: Optional[str] = None
|
||||
|
||||
|
||||
class MerchantConfig(MerchantProfile):
|
||||
event_id: Optional[str]
|
||||
event_id: Optional[str] = None
|
||||
sync_from_nostr = False
|
||||
active: bool = False
|
||||
restore_in_progress: Optional[bool] = False
|
||||
|
|
@ -56,8 +54,8 @@ class Merchant(PartialMerchant, Nostrable):
|
|||
id: str
|
||||
time: Optional[int] = 0
|
||||
|
||||
def sign_hash(self, hash: bytes) -> str:
|
||||
return sign_message_hash(self.private_key, hash)
|
||||
def sign_hash(self, hash_: bytes) -> str:
|
||||
return sign_message_hash(self.private_key, hash_)
|
||||
|
||||
def decrypt_message(self, encrypted_message: str, public_key: str) -> str:
|
||||
encryption_key = get_shared_secret(self.private_key, public_key)
|
||||
|
|
@ -82,8 +80,8 @@ class Merchant(PartialMerchant, Nostrable):
|
|||
return event
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Merchant":
|
||||
merchant = cls(**dict(row))
|
||||
def from_row(cls, row: dict) -> "Merchant":
|
||||
merchant = cls(**row)
|
||||
merchant.config = MerchantConfig(**json.loads(row["meta"]))
|
||||
return merchant
|
||||
|
||||
|
|
@ -123,20 +121,16 @@ class Merchant(PartialMerchant, Nostrable):
|
|||
|
||||
|
||||
######################################## ZONES ########################################
|
||||
class PartialZone(BaseModel):
|
||||
id: Optional[str]
|
||||
name: Optional[str]
|
||||
class Zone(BaseModel):
|
||||
id: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
currency: str
|
||||
cost: float
|
||||
countries: List[str] = []
|
||||
|
||||
|
||||
class Zone(PartialZone):
|
||||
id: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Zone":
|
||||
zone = cls(**dict(row))
|
||||
def from_row(cls, row: dict) -> "Zone":
|
||||
zone = cls(**row)
|
||||
zone.countries = json.loads(row["regions"])
|
||||
return zone
|
||||
|
||||
|
|
@ -145,12 +139,12 @@ class Zone(PartialZone):
|
|||
|
||||
|
||||
class StallConfig(BaseModel):
|
||||
image_url: Optional[str]
|
||||
description: Optional[str]
|
||||
image_url: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class PartialStall(BaseModel):
|
||||
id: Optional[str]
|
||||
class Stall(BaseModel, Nostrable):
|
||||
id: Optional[str] = None
|
||||
wallet: str
|
||||
name: str
|
||||
currency: str = "sat"
|
||||
|
|
@ -159,8 +153,8 @@ class PartialStall(BaseModel):
|
|||
pending: bool = False
|
||||
|
||||
"""Last published nostr event for this Stall"""
|
||||
event_id: Optional[str]
|
||||
event_created_at: Optional[int]
|
||||
event_id: Optional[str] = None
|
||||
event_created_at: Optional[int] = None
|
||||
|
||||
def validate_stall(self):
|
||||
for z in self.shipping_zones:
|
||||
|
|
@ -169,10 +163,6 @@ class PartialStall(BaseModel):
|
|||
f"Sipping zone '{z.name}' has different currency than stall."
|
||||
)
|
||||
|
||||
|
||||
class Stall(PartialStall, Nostrable):
|
||||
id: str
|
||||
|
||||
def to_nostr_event(self, pubkey: str) -> NostrEvent:
|
||||
content = {
|
||||
"id": self.id,
|
||||
|
|
@ -181,6 +171,7 @@ class Stall(PartialStall, Nostrable):
|
|||
"currency": self.currency,
|
||||
"shipping": [dict(z) for z in self.shipping_zones],
|
||||
}
|
||||
assert self.id
|
||||
event = NostrEvent(
|
||||
pubkey=pubkey,
|
||||
created_at=round(time.time()),
|
||||
|
|
@ -197,7 +188,7 @@ class Stall(PartialStall, Nostrable):
|
|||
pubkey=pubkey,
|
||||
created_at=round(time.time()),
|
||||
kind=5,
|
||||
tags=[["e", self.event_id]],
|
||||
tags=[["e", self.event_id or ""]],
|
||||
content=f"Stall '{self.name}' deleted",
|
||||
)
|
||||
delete_event.id = delete_event.event_id
|
||||
|
|
@ -205,14 +196,14 @@ class Stall(PartialStall, Nostrable):
|
|||
return delete_event
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Stall":
|
||||
stall = cls(**dict(row))
|
||||
def from_row(cls, row: dict) -> "Stall":
|
||||
stall = cls(**row)
|
||||
stall.config = StallConfig(**json.loads(row["meta"]))
|
||||
stall.shipping_zones = [Zone(**z) for z in json.loads(row["zones"])]
|
||||
return stall
|
||||
|
||||
|
||||
######################################## PRODUCTS ########################################
|
||||
######################################## PRODUCTS ######################################
|
||||
|
||||
|
||||
class ProductShippingCost(BaseModel):
|
||||
|
|
@ -221,15 +212,15 @@ class ProductShippingCost(BaseModel):
|
|||
|
||||
|
||||
class ProductConfig(BaseModel):
|
||||
description: Optional[str]
|
||||
currency: Optional[str]
|
||||
description: Optional[str] = None
|
||||
currency: Optional[str] = None
|
||||
use_autoreply: Optional[bool] = False
|
||||
autoreply_message: Optional[str]
|
||||
shipping: Optional[List[ProductShippingCost]] = []
|
||||
autoreply_message: Optional[str] = None
|
||||
shipping: List[ProductShippingCost] = []
|
||||
|
||||
|
||||
class PartialProduct(BaseModel):
|
||||
id: Optional[str]
|
||||
class Product(BaseModel, Nostrable):
|
||||
id: Optional[str] = None
|
||||
stall_id: str
|
||||
name: str
|
||||
categories: List[str] = []
|
||||
|
|
@ -241,12 +232,8 @@ class PartialProduct(BaseModel):
|
|||
config: ProductConfig = ProductConfig()
|
||||
|
||||
"""Last published nostr event for this Product"""
|
||||
event_id: Optional[str]
|
||||
event_created_at: Optional[int]
|
||||
|
||||
|
||||
class Product(PartialProduct, Nostrable):
|
||||
id: str
|
||||
event_id: Optional[str] = None
|
||||
event_created_at: Optional[int] = None
|
||||
|
||||
def to_nostr_event(self, pubkey: str) -> NostrEvent:
|
||||
content = {
|
||||
|
|
@ -259,16 +246,17 @@ class Product(PartialProduct, Nostrable):
|
|||
"price": self.price,
|
||||
"quantity": self.quantity,
|
||||
"active": self.active,
|
||||
"shipping": [dict(s) for s in self.config.shipping or []]
|
||||
"shipping": [dict(s) for s in self.config.shipping or []],
|
||||
}
|
||||
categories = [["t", tag] for tag in self.categories]
|
||||
|
||||
assert self.id
|
||||
if self.active:
|
||||
event = NostrEvent(
|
||||
pubkey=pubkey,
|
||||
created_at=round(time.time()),
|
||||
kind=30018,
|
||||
tags=[["d", self.id]] + categories,
|
||||
tags=[["d", self.id], *categories],
|
||||
content=json.dumps(content, separators=(",", ":"), ensure_ascii=False),
|
||||
)
|
||||
event.id = event.event_id
|
||||
|
|
@ -282,7 +270,7 @@ class Product(PartialProduct, Nostrable):
|
|||
pubkey=pubkey,
|
||||
created_at=round(time.time()),
|
||||
kind=5,
|
||||
tags=[["e", self.event_id]],
|
||||
tags=[["e", self.event_id or ""]],
|
||||
content=f"Product '{self.name}' deleted",
|
||||
)
|
||||
delete_event.id = delete_event.event_id
|
||||
|
|
@ -290,8 +278,8 @@ class Product(PartialProduct, Nostrable):
|
|||
return delete_event
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Product":
|
||||
product = cls(**dict(row))
|
||||
def from_row(cls, row: dict) -> "Product":
|
||||
product = cls(**row)
|
||||
product.config = ProductConfig(**json.loads(row["meta"]))
|
||||
product.images = json.loads(row["image_urls"]) if "image_urls" in row else []
|
||||
product.categories = json.loads(row["category_list"])
|
||||
|
|
@ -302,6 +290,12 @@ class ProductOverview(BaseModel):
|
|||
id: str
|
||||
name: str
|
||||
price: float
|
||||
product_shipping_cost: Optional[float] = None
|
||||
|
||||
@classmethod
|
||||
def from_product(cls, p: Product) -> "ProductOverview":
|
||||
assert p.id
|
||||
return ProductOverview(id=p.id, name=p.name, price=p.price)
|
||||
|
||||
|
||||
######################################## ORDERS ########################################
|
||||
|
|
@ -313,9 +307,9 @@ class OrderItem(BaseModel):
|
|||
|
||||
|
||||
class OrderContact(BaseModel):
|
||||
nostr: Optional[str]
|
||||
phone: Optional[str]
|
||||
email: Optional[str]
|
||||
nostr: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
|
||||
|
||||
class OrderExtra(BaseModel):
|
||||
|
|
@ -324,27 +318,33 @@ class OrderExtra(BaseModel):
|
|||
btc_price: str
|
||||
shipping_cost: float = 0
|
||||
shipping_cost_sat: float = 0
|
||||
fail_message: Optional[str]
|
||||
fail_message: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
async def from_products(cls, products: List[Product]):
|
||||
currency = products[0].config.currency if len(products) else "sat"
|
||||
exchange_rate = (
|
||||
(await btc_price(currency)) if currency and currency != "sat" else 1
|
||||
await btc_price(currency) if currency and currency != "sat" else 1
|
||||
)
|
||||
|
||||
products_overview = [ProductOverview.from_product(p) for p in products]
|
||||
return OrderExtra(
|
||||
products=products_overview,
|
||||
currency=currency or "sat",
|
||||
btc_price=str(exchange_rate),
|
||||
)
|
||||
return OrderExtra(products=products, currency=currency, btc_price=exchange_rate)
|
||||
|
||||
|
||||
class PartialOrder(BaseModel):
|
||||
id: str
|
||||
event_id: Optional[str]
|
||||
event_created_at: Optional[int]
|
||||
event_id: Optional[str] = None
|
||||
event_created_at: Optional[int] = None
|
||||
public_key: str
|
||||
merchant_public_key: str
|
||||
shipping_id: str
|
||||
items: List[OrderItem]
|
||||
contact: Optional[OrderContact]
|
||||
address: Optional[str]
|
||||
contact: Optional[OrderContact] = None
|
||||
address: Optional[str] = None
|
||||
|
||||
def validate_order(self):
|
||||
assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"
|
||||
|
|
@ -383,10 +383,11 @@ class PartialOrder(BaseModel):
|
|||
}
|
||||
|
||||
product_cost: float = 0 # todo
|
||||
currency = "sat"
|
||||
for item in self.items:
|
||||
assert item.quantity > 0, "Quantity cannot be negative"
|
||||
price = product_prices[item.product_id]["price"]
|
||||
currency = product_prices[item.product_id]["currency"]
|
||||
price = float(str(product_prices[item.product_id]["price"]))
|
||||
currency = str(product_prices[item.product_id]["currency"])
|
||||
if currency != "sat":
|
||||
price = await fiat_amount_as_satoshis(price, currency)
|
||||
product_cost += item.quantity * price
|
||||
|
|
@ -404,30 +405,39 @@ class PartialOrder(BaseModel):
|
|||
if len(products) == 0:
|
||||
return "[No Products]"
|
||||
receipt = ""
|
||||
product_prices = {}
|
||||
product_prices: dict[str, ProductOverview] = {}
|
||||
for p in products:
|
||||
product_shipping_cost = next(
|
||||
(s.cost for s in p.config.shipping if s.id == shipping_id), 0
|
||||
)
|
||||
product_prices[p.id] = {
|
||||
"name": p.name,
|
||||
"price": p.price,
|
||||
"product_shipping_cost": product_shipping_cost
|
||||
}
|
||||
assert p.id
|
||||
product_prices[p.id] = ProductOverview(
|
||||
id=p.id,
|
||||
name=p.name,
|
||||
price=p.price,
|
||||
product_shipping_cost=product_shipping_cost,
|
||||
)
|
||||
|
||||
currency = products[0].config.currency or "sat"
|
||||
products_cost: float = 0 # todo
|
||||
items_receipts = []
|
||||
for item in self.items:
|
||||
prod = product_prices[item.product_id]
|
||||
price = prod["price"] + prod["product_shipping_cost"]
|
||||
price = prod.price + (prod.product_shipping_cost or 0)
|
||||
|
||||
products_cost += item.quantity * price
|
||||
|
||||
items_receipts.append(f"""[{prod["name"]}: {item.quantity} x ({prod["price"]} + {prod["product_shipping_cost"]}) = {item.quantity * price} {currency}] """)
|
||||
items_receipts.append(
|
||||
f"""[{prod.name}: {item.quantity} x ({prod.price}"""
|
||||
f""" + {prod.product_shipping_cost})"""
|
||||
f""" = {item.quantity * price} {currency}] """
|
||||
)
|
||||
|
||||
receipt = "; ".join(items_receipts)
|
||||
receipt += f"[Products cost: {products_cost} {currency}] [Stall shipping cost: {stall_shipping_cost} {currency}]; "
|
||||
receipt += (
|
||||
f"[Products cost: {products_cost} {currency}] "
|
||||
f"[Stall shipping cost: {stall_shipping_cost} {currency}]; "
|
||||
)
|
||||
receipt += f"[Total: {products_cost + stall_shipping_cost} {currency}]"
|
||||
|
||||
return receipt
|
||||
|
|
@ -439,23 +449,23 @@ class Order(PartialOrder):
|
|||
total: float
|
||||
paid: bool = False
|
||||
shipped: bool = False
|
||||
time: Optional[int]
|
||||
time: Optional[int] = None
|
||||
extra: OrderExtra
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Order":
|
||||
def from_row(cls, row: dict) -> "Order":
|
||||
contact = OrderContact(**json.loads(row["contact_data"]))
|
||||
extra = OrderExtra(**json.loads(row["extra_data"]))
|
||||
items = [OrderItem(**z) for z in json.loads(row["order_items"])]
|
||||
order = cls(**dict(row), contact=contact, items=items, extra=extra)
|
||||
order = cls(**row, contact=contact, items=items, extra=extra)
|
||||
return order
|
||||
|
||||
|
||||
class OrderStatusUpdate(BaseModel):
|
||||
id: str
|
||||
message: Optional[str]
|
||||
paid: Optional[bool]
|
||||
shipped: Optional[bool]
|
||||
message: Optional[str] = None
|
||||
paid: Optional[bool] = False
|
||||
shipped: Optional[bool] = None
|
||||
|
||||
|
||||
class OrderReissue(BaseModel):
|
||||
|
|
@ -470,11 +480,11 @@ class PaymentOption(BaseModel):
|
|||
|
||||
class PaymentRequest(BaseModel):
|
||||
id: str
|
||||
message: Optional[str]
|
||||
message: Optional[str] = None
|
||||
payment_options: List[PaymentOption]
|
||||
|
||||
|
||||
######################################## MESSAGE ########################################
|
||||
######################################## MESSAGE #######################################
|
||||
|
||||
|
||||
class DirectMessageType(Enum):
|
||||
|
|
@ -487,13 +497,13 @@ class DirectMessageType(Enum):
|
|||
|
||||
|
||||
class PartialDirectMessage(BaseModel):
|
||||
event_id: Optional[str]
|
||||
event_created_at: Optional[int]
|
||||
event_id: Optional[str] = None
|
||||
event_created_at: Optional[int] = None
|
||||
message: str
|
||||
public_key: str
|
||||
type: int = DirectMessageType.PLAIN_TEXT.value
|
||||
incoming: bool = False
|
||||
time: Optional[int]
|
||||
time: Optional[int] = None
|
||||
|
||||
@classmethod
|
||||
def parse_message(cls, msg) -> Tuple[DirectMessageType, Optional[Any]]:
|
||||
|
|
@ -511,29 +521,28 @@ class DirectMessage(PartialDirectMessage):
|
|||
id: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "DirectMessage":
|
||||
dm = cls(**dict(row))
|
||||
return dm
|
||||
def from_row(cls, row: dict) -> "DirectMessage":
|
||||
return cls(**row)
|
||||
|
||||
|
||||
######################################## CUSTOMERS ########################################
|
||||
######################################## CUSTOMERS #####################################
|
||||
|
||||
|
||||
class CustomerProfile(BaseModel):
|
||||
name: Optional[str]
|
||||
about: Optional[str]
|
||||
name: Optional[str] = None
|
||||
about: Optional[str] = None
|
||||
|
||||
|
||||
class Customer(BaseModel):
|
||||
merchant_id: str
|
||||
public_key: str
|
||||
event_created_at: Optional[int]
|
||||
profile: Optional[CustomerProfile]
|
||||
event_created_at: Optional[int] = None
|
||||
profile: Optional[CustomerProfile] = None
|
||||
unread_messages: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Customer":
|
||||
customer = cls(**dict(row))
|
||||
def from_row(cls, row: dict) -> "Customer":
|
||||
customer = cls(**row)
|
||||
customer.profile = (
|
||||
CustomerProfile(**json.loads(row["meta"])) if "meta" in row else None
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class NostrEvent(BaseModel):
|
|||
kind: int
|
||||
tags: List[List[str]] = []
|
||||
content: str = ""
|
||||
sig: Optional[str]
|
||||
sig: Optional[str] = None
|
||||
|
||||
def serialize(self) -> List:
|
||||
return [0, self.pubkey, self.created_at, self.kind, self.tags, self.content]
|
||||
|
|
@ -41,7 +41,7 @@ class NostrEvent(BaseModel):
|
|||
f"Invalid public key: '{self.pubkey}' for event '{self.id}'"
|
||||
)
|
||||
|
||||
valid_signature = pub_key.schnorr_verify(
|
||||
valid_signature = self.sig and pub_key.schnorr_verify(
|
||||
bytes.fromhex(event_id), bytes.fromhex(self.sig), None, raw=True
|
||||
)
|
||||
if not valid_signature:
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import asyncio
|
|||
import json
|
||||
from asyncio import Queue
|
||||
from threading import Thread
|
||||
from typing import Callable, List
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
from loguru import logger
|
||||
from websocket import WebSocketApp
|
||||
|
||||
from lnbits.app import settings
|
||||
from lnbits.settings import settings
|
||||
from lnbits.helpers import encrypt_internal_message, urlsafe_short_hash
|
||||
|
||||
from .event import NostrEvent
|
||||
|
|
@ -17,7 +17,7 @@ class NostrClient:
|
|||
def __init__(self):
|
||||
self.recieve_event_queue: Queue = Queue()
|
||||
self.send_req_queue: Queue = Queue()
|
||||
self.ws: WebSocketApp = None
|
||||
self.ws: Optional[WebSocketApp] = None
|
||||
self.subscription_id = "nostrmarket-" + urlsafe_short_hash()[:32]
|
||||
self.running = False
|
||||
|
||||
|
|
@ -30,7 +30,6 @@ class NostrClient:
|
|||
async def connect_to_nostrclient_ws(self) -> WebSocketApp:
|
||||
logger.debug(f"Connecting to websockets for 'nostrclient' extension...")
|
||||
|
||||
|
||||
relay_endpoint = encrypt_internal_message("relay")
|
||||
on_open, on_message, on_error, on_close = self._ws_handlers()
|
||||
ws = WebSocketApp(
|
||||
|
|
@ -57,19 +56,18 @@ class NostrClient:
|
|||
await asyncio.sleep(5)
|
||||
|
||||
req = await self.send_req_queue.get()
|
||||
assert self.ws
|
||||
self.ws.send(json.dumps(req))
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
await asyncio.sleep(60)
|
||||
|
||||
|
||||
async def get_event(self):
|
||||
value = await self.recieve_event_queue.get()
|
||||
if isinstance(value, ValueError):
|
||||
raise value
|
||||
return value
|
||||
|
||||
|
||||
async def publish_nostr_event(self, e: NostrEvent):
|
||||
await self.send_req_queue.put(["EVENT", e.dict()])
|
||||
|
||||
|
|
@ -119,7 +117,7 @@ class NostrClient:
|
|||
|
||||
asyncio.create_task(unsubscribe_with_delay(subscription_id, duration))
|
||||
|
||||
async def user_profile_temp_subscribe(self, public_key: str, duration=5) -> List:
|
||||
async def user_profile_temp_subscribe(self, public_key: str, duration=5):
|
||||
try:
|
||||
profile_filter = [{"kinds": [0], "authors": [public_key]}]
|
||||
subscription_id = "profile-" + urlsafe_short_hash()[:32]
|
||||
|
|
|
|||
15
package.json
Normal file
15
package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "nostrmarket",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"prettier": "^3.2.5",
|
||||
"pyright": "^1.1.358"
|
||||
}
|
||||
}
|
||||
2616
poetry.lock
generated
Normal file
2616
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
96
pyproject.toml
Normal file
96
pyproject.toml
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
[tool.poetry]
|
||||
name = "lnbits-nostrmarket"
|
||||
version = "0.0.0"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10 | ^3.9"
|
||||
lnbits = {version = "*", allow-prereleases = true}
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^24.3.0"
|
||||
pytest-asyncio = "^0.21.0"
|
||||
pytest = "^7.3.2"
|
||||
mypy = "^1.5.1"
|
||||
pre-commit = "^3.2.2"
|
||||
ruff = "^0.3.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.mypy]
|
||||
exclude = "(nostr/*)"
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"secp256k1.*",
|
||||
"embit.*",
|
||||
"lnbits.*",
|
||||
"lnurl.*",
|
||||
"loguru.*",
|
||||
"fastapi.*",
|
||||
"pydantic.*",
|
||||
"pyqrcode.*",
|
||||
"shortuuid.*",
|
||||
"httpx.*",
|
||||
]
|
||||
ignore_missing_imports = "True"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
log_cli = false
|
||||
testpaths = [
|
||||
"tests"
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
|
||||
[tool.ruff]
|
||||
# Same as Black. + 10% rule of black
|
||||
line-length = 88
|
||||
exclude = [
|
||||
"nostr",
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Enable:
|
||||
# F - pyflakes
|
||||
# E - pycodestyle errors
|
||||
# W - pycodestyle warnings
|
||||
# I - isort
|
||||
# A - flake8-builtins
|
||||
# C - mccabe
|
||||
# N - naming
|
||||
# UP - pyupgrade
|
||||
# RUF - ruff
|
||||
# B - bugbear
|
||||
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
|
||||
ignore = ["C901"]
|
||||
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
# needed for pydantic
|
||||
[tool.ruff.lint.pep8-naming]
|
||||
classmethod-decorators = [
|
||||
"root_validator",
|
||||
]
|
||||
|
||||
# Ignore unused imports in __init__.py files.
|
||||
# [tool.ruff.lint.extend-per-file-ignores]
|
||||
# "__init__.py" = ["F401", "F403"]
|
||||
|
||||
# [tool.ruff.lint.mccabe]
|
||||
# max-complexity = 10
|
||||
|
||||
[tool.ruff.lint.flake8-bugbear]
|
||||
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
|
||||
extend-immutable-calls = [
|
||||
"fastapi.Depends",
|
||||
"fastapi.Query",
|
||||
]
|
||||
105
services.py
105
services.py
|
|
@ -2,10 +2,10 @@ import asyncio
|
|||
import json
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.bolt11 import decode
|
||||
from lnbits.core.services import websocket_updater, create_invoice, get_wallet
|
||||
from lnbits.core.crud import get_wallet
|
||||
from lnbits.core.services import create_invoice, websocket_updater
|
||||
from loguru import logger
|
||||
|
||||
from . import nostr_client
|
||||
from .crud import (
|
||||
|
|
@ -75,7 +75,9 @@ async def create_new_order(
|
|||
await create_order(merchant.id, order)
|
||||
|
||||
return PaymentRequest(
|
||||
id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)], message=receipt
|
||||
id=data.id,
|
||||
payment_options=[PaymentOption(type="ln", link=invoice)],
|
||||
message=receipt,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -89,12 +91,11 @@ async def build_order_with_payment(
|
|||
shipping_zone = await get_zone(merchant_id, data.shipping_id)
|
||||
assert shipping_zone, f"Shipping zone not found for order '{data.id}'"
|
||||
|
||||
assert shipping_zone.id
|
||||
product_cost_sat, shipping_cost_sat = await data.costs_in_sats(
|
||||
products, shipping_zone.id, shipping_zone.cost
|
||||
)
|
||||
receipt = data.receipt(
|
||||
products, shipping_zone.id, shipping_zone.cost
|
||||
)
|
||||
receipt = data.receipt(products, shipping_zone.id, shipping_zone.cost)
|
||||
|
||||
wallet_id = await get_wallet_for_product(data.items[0].product_id)
|
||||
assert wallet_id, "Missing wallet for order `{data.id}`"
|
||||
|
|
@ -106,7 +107,7 @@ async def build_order_with_payment(
|
|||
if not success:
|
||||
raise ValueError(message)
|
||||
|
||||
payment_hash, invoice = await create_invoice(
|
||||
payment = await create_invoice(
|
||||
wallet_id=wallet_id,
|
||||
amount=round(product_cost_sat + shipping_cost_sat),
|
||||
memo=f"Order '{data.id}' for pubkey '{data.public_key}'",
|
||||
|
|
@ -124,19 +125,21 @@ async def build_order_with_payment(
|
|||
order = Order(
|
||||
**data.dict(),
|
||||
stall_id=products[0].stall_id,
|
||||
invoice_id=payment_hash,
|
||||
invoice_id=payment.payment_hash,
|
||||
total=product_cost_sat + shipping_cost_sat,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
return order, invoice, receipt
|
||||
return order, payment.bolt11, receipt
|
||||
|
||||
|
||||
async def update_merchant_to_nostr(
|
||||
merchant: Merchant, delete_merchant=False
|
||||
) -> Merchant:
|
||||
stalls = await get_stalls(merchant.id)
|
||||
event: Optional[NostrEvent] = None
|
||||
for stall in stalls:
|
||||
assert stall.id
|
||||
products = await get_products(merchant.id, stall.id)
|
||||
for product in products:
|
||||
event = await sign_and_send_to_nostr(merchant, product, delete_merchant)
|
||||
|
|
@ -150,6 +153,7 @@ async def update_merchant_to_nostr(
|
|||
if delete_merchant:
|
||||
# merchant profile updates not supported yet
|
||||
event = await sign_and_send_to_nostr(merchant, merchant, delete_merchant)
|
||||
assert event
|
||||
merchant.config.event_id = event.id
|
||||
return merchant
|
||||
|
||||
|
|
@ -227,25 +231,29 @@ async def update_products_for_order(
|
|||
return success, message
|
||||
|
||||
for p in products:
|
||||
assert p.id
|
||||
product = await update_product_quantity(p.id, p.quantity)
|
||||
assert product
|
||||
event = await sign_and_send_to_nostr(merchant, product)
|
||||
product.event_id = event.id
|
||||
await update_product(merchant.id, product)
|
||||
|
||||
return True, "ok"
|
||||
|
||||
async def autoreply_for_products_in_order(
|
||||
merchant: Merchant, order: Order
|
||||
) -> Tuple[bool, str]:
|
||||
|
||||
async def autoreply_for_products_in_order(merchant: Merchant, order: Order):
|
||||
product_ids = [i.product_id for i in order.items]
|
||||
|
||||
products = await get_products_by_ids(merchant.id, product_ids)
|
||||
products_with_autoreply = [p for p in products if p.config.use_autoreply]
|
||||
|
||||
for p in products_with_autoreply:
|
||||
dm_content = p.config.autoreply_message
|
||||
dm_content = p.config.autoreply_message or ""
|
||||
await send_dm(
|
||||
merchant, order.public_key, DirectMessageType.PLAIN_TEXT.value, dm_content
|
||||
merchant,
|
||||
order.public_key,
|
||||
DirectMessageType.PLAIN_TEXT.value,
|
||||
dm_content,
|
||||
)
|
||||
await asyncio.sleep(1) # do not send all autoreplies at once
|
||||
|
||||
|
|
@ -253,7 +261,7 @@ async def autoreply_for_products_in_order(
|
|||
async def send_dm(
|
||||
merchant: Merchant,
|
||||
other_pubkey: str,
|
||||
type: str,
|
||||
type_: int,
|
||||
dm_content: str,
|
||||
):
|
||||
dm_event = merchant.build_dm_event(dm_content, other_pubkey)
|
||||
|
|
@ -263,7 +271,7 @@ async def send_dm(
|
|||
event_created_at=dm_event.created_at,
|
||||
message=dm_content,
|
||||
public_key=other_pubkey,
|
||||
type=type,
|
||||
type=type_,
|
||||
)
|
||||
dm_reply = await create_direct_message(merchant.id, dm)
|
||||
|
||||
|
|
@ -296,7 +304,8 @@ async def compute_products_new_quantity(
|
|||
return (
|
||||
False,
|
||||
[],
|
||||
f"Quantity not sufficient for product: '{p.name}' ({p.id}). Required '{required_quantity}' but only have '{p.quantity}'.",
|
||||
f"Quantity not sufficient for product: '{p.name}' ({p.id})."
|
||||
f" Required '{required_quantity}' but only have '{p.quantity}'.",
|
||||
)
|
||||
|
||||
p.quantity -= required_quantity
|
||||
|
|
@ -306,9 +315,9 @@ async def compute_products_new_quantity(
|
|||
|
||||
async def process_nostr_message(msg: str):
|
||||
try:
|
||||
type, *rest = json.loads(msg)
|
||||
type_, *rest = json.loads(msg)
|
||||
|
||||
if type.upper() == "EVENT":
|
||||
if type_.upper() == "EVENT":
|
||||
_, event = rest
|
||||
event = NostrEvent(**event)
|
||||
if event.kind == 0:
|
||||
|
|
@ -328,11 +337,11 @@ async def process_nostr_message(msg: str):
|
|||
async def create_or_update_order_from_dm(
|
||||
merchant_id: str, merchant_pubkey: str, dm: DirectMessage
|
||||
):
|
||||
type, json_data = PartialDirectMessage.parse_message(dm.message)
|
||||
if "id" not in json_data:
|
||||
type_, json_data = PartialDirectMessage.parse_message(dm.message)
|
||||
if not json_data or "id" not in json_data:
|
||||
return
|
||||
|
||||
if type == DirectMessageType.CUSTOMER_ORDER:
|
||||
if type_ == DirectMessageType.CUSTOMER_ORDER:
|
||||
order = await extract_customer_order_from_dm(
|
||||
merchant_id, merchant_pubkey, dm, json_data
|
||||
)
|
||||
|
|
@ -348,7 +357,7 @@ async def create_or_update_order_from_dm(
|
|||
)
|
||||
return
|
||||
|
||||
if type == DirectMessageType.PAYMENT_REQUEST:
|
||||
if type_ == DirectMessageType.PAYMENT_REQUEST:
|
||||
payment_request = PaymentRequest(**json_data)
|
||||
pr = next(
|
||||
(o.link for o in payment_request.payment_options if o.type == "ln"), None
|
||||
|
|
@ -356,14 +365,15 @@ async def create_or_update_order_from_dm(
|
|||
if not pr:
|
||||
return
|
||||
invoice = decode(pr)
|
||||
total = invoice.amount_msat / 1000 if invoice.amount_msat else 0
|
||||
await update_order(
|
||||
merchant_id,
|
||||
payment_request.id,
|
||||
**{"total": invoice.amount_msat / 1000, "invoice_id": invoice.payment_hash},
|
||||
**{"total": total, "invoice_id": invoice.payment_hash},
|
||||
)
|
||||
return
|
||||
|
||||
if type == DirectMessageType.ORDER_PAID_OR_SHIPPED:
|
||||
if type_ == DirectMessageType.ORDER_PAID_OR_SHIPPED:
|
||||
order_update = OrderStatusUpdate(**json_data)
|
||||
if order_update.paid:
|
||||
await update_order_paid_status(order_update.id, True)
|
||||
|
|
@ -380,16 +390,18 @@ async def extract_customer_order_from_dm(
|
|||
)
|
||||
extra = await OrderExtra.from_products(products)
|
||||
order = Order(
|
||||
id=json_data.get("id"),
|
||||
id=str(json_data.get("id")),
|
||||
event_id=dm.event_id,
|
||||
event_created_at=dm.event_created_at,
|
||||
public_key=dm.public_key,
|
||||
merchant_public_key=merchant_pubkey,
|
||||
shipping_id=json_data.get("shipping_id", "None"),
|
||||
items=order_items,
|
||||
contact=OrderContact(**json_data.get("contact"))
|
||||
if json_data.get("contact")
|
||||
else None,
|
||||
contact=(
|
||||
OrderContact(**json_data.get("contact", {}))
|
||||
if json_data.get("contact")
|
||||
else None
|
||||
),
|
||||
address=json_data.get("address"),
|
||||
stall_id=products[0].stall_id if len(products) else "None",
|
||||
invoice_id="None",
|
||||
|
|
@ -406,12 +418,9 @@ async def _handle_nip04_message(event: NostrEvent):
|
|||
|
||||
if not merchant:
|
||||
p_tags = event.tag_values("p")
|
||||
merchant_public_key = p_tags[0] if len(p_tags) else None
|
||||
merchant = (
|
||||
await get_merchant_by_pubkey(merchant_public_key)
|
||||
if merchant_public_key
|
||||
else None
|
||||
)
|
||||
if len(p_tags) and p_tags[0]:
|
||||
merchant_public_key = p_tags[0]
|
||||
merchant = await get_merchant_by_pubkey(merchant_public_key)
|
||||
|
||||
assert merchant, f"Merchant not found for public key '{merchant_public_key}'"
|
||||
|
||||
|
|
@ -461,21 +470,21 @@ async def _handle_outgoing_dms(
|
|||
event: NostrEvent, merchant: Merchant, clear_text_msg: str
|
||||
):
|
||||
sent_to = event.tag_values("p")
|
||||
type, _ = PartialDirectMessage.parse_message(clear_text_msg)
|
||||
type_, _ = PartialDirectMessage.parse_message(clear_text_msg)
|
||||
if len(sent_to) != 0:
|
||||
dm = PartialDirectMessage(
|
||||
event_id=event.id,
|
||||
event_created_at=event.created_at,
|
||||
message=clear_text_msg,
|
||||
public_key=sent_to[0],
|
||||
type=type.value,
|
||||
type=type_.value,
|
||||
)
|
||||
await create_direct_message(merchant.id, dm)
|
||||
|
||||
|
||||
async def _handle_incoming_structured_dm(
|
||||
merchant: Merchant, dm: DirectMessage, json_data: dict
|
||||
) -> Tuple[DirectMessageType, str]:
|
||||
) -> Tuple[DirectMessageType, Optional[str]]:
|
||||
try:
|
||||
if dm.type == DirectMessageType.CUSTOMER_ORDER.value and merchant.config.active:
|
||||
json_resp = await _handle_new_order(
|
||||
|
|
@ -487,7 +496,7 @@ async def _handle_incoming_structured_dm(
|
|||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
||||
return None, None
|
||||
return DirectMessageType.PLAIN_TEXT, None
|
||||
|
||||
|
||||
async def _persist_dm(
|
||||
|
|
@ -570,9 +579,13 @@ async def _handle_new_order(
|
|||
except Exception as e:
|
||||
logger.debug(e)
|
||||
payment_req = await create_new_failed_order(
|
||||
merchant_id, merchant_public_key, dm, json_data, "Order received, but cannot be processed. Please contact merchant."
|
||||
merchant_id,
|
||||
merchant_public_key,
|
||||
dm,
|
||||
json_data,
|
||||
"Order received, but cannot be processed. Please contact merchant.",
|
||||
)
|
||||
|
||||
assert payment_req
|
||||
response = {
|
||||
"type": DirectMessageType.PAYMENT_REQUEST.value,
|
||||
**payment_req.dict(),
|
||||
|
|
@ -594,12 +607,14 @@ async def create_new_failed_order(
|
|||
await create_order(merchant_id, order)
|
||||
return PaymentRequest(id=order.id, message=fail_message, payment_options=[])
|
||||
|
||||
|
||||
async def resubscribe_to_all_merchants():
|
||||
await nostr_client.unsubscribe_merchants()
|
||||
# give some time for the message to propagate
|
||||
asyncio.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
await subscribe_to_all_merchants()
|
||||
|
||||
|
||||
async def subscribe_to_all_merchants():
|
||||
ids = await get_merchants_ids_with_pubkeys()
|
||||
public_keys = [pk for _, pk in ids]
|
||||
|
|
@ -608,7 +623,9 @@ async def subscribe_to_all_merchants():
|
|||
last_stall_time = await get_last_stall_update_time()
|
||||
last_prod_time = await get_last_product_update_time()
|
||||
|
||||
await nostr_client.subscribe_merchants(public_keys, last_dm_time, last_stall_time, last_prod_time, 0)
|
||||
await nostr_client.subscribe_merchants(
|
||||
public_keys, last_dm_time, last_stall_time, last_prod_time, 0
|
||||
)
|
||||
|
||||
|
||||
async def _handle_new_customer(event: NostrEvent, merchant: Merchant):
|
||||
|
|
|
|||
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,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,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,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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,241 +1,228 @@
|
|||
const merchant = async () => {
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
const nostr = window.NostrTools
|
||||
|
||||
await keyPair('static/components/key-pair/key-pair.html')
|
||||
await shippingZones('static/components/shipping-zones/shipping-zones.html')
|
||||
await stallDetails('static/components/stall-details/stall-details.html')
|
||||
await stallList('static/components/stall-list/stall-list.html')
|
||||
await orderList('static/components/order-list/order-list.html')
|
||||
await directMessages('static/components/direct-messages/direct-messages.html')
|
||||
await merchantDetails(
|
||||
'static/components/merchant-details/merchant-details.html'
|
||||
)
|
||||
|
||||
const nostr = window.NostrTools
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
merchant: {},
|
||||
shippingZones: [],
|
||||
activeChatCustomer: '',
|
||||
orderPubkey: null,
|
||||
showKeys: false,
|
||||
importKeyDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
privateKey: null
|
||||
}
|
||||
},
|
||||
wsConnection: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
generateKeys: async function () {
|
||||
const privateKey = nostr.generatePrivateKey()
|
||||
await this.createMerchant(privateKey)
|
||||
},
|
||||
importKeys: async function () {
|
||||
this.importKeyDialog.show = false
|
||||
let privateKey = this.importKeyDialog.data.privateKey
|
||||
if (!privateKey) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (privateKey.toLowerCase().startsWith('nsec')) {
|
||||
privateKey = nostr.nip19.decode(privateKey).data
|
||||
}
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
type: 'negative',
|
||||
message: `${error}`
|
||||
})
|
||||
}
|
||||
await this.createMerchant(privateKey)
|
||||
},
|
||||
showImportKeysDialog: async function () {
|
||||
this.importKeyDialog.show = true
|
||||
},
|
||||
toggleShowKeys: function () {
|
||||
this.showKeys = !this.showKeys
|
||||
},
|
||||
toggleMerchantState: async function () {
|
||||
const merchant = await this.getMerchant()
|
||||
if (!merchant) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: "Cannot fetch merchant!"
|
||||
})
|
||||
return
|
||||
}
|
||||
const message = merchant.config.active ?
|
||||
'New orders will not be processed. Are you sure you want to deactivate?' :
|
||||
merchant.config.restore_in_progress ?
|
||||
'Merchant restore from nostr in progress. Please wait!! ' +
|
||||
'Activating now can lead to duplicate order processing. Click "OK" if you want to activate anyway?' :
|
||||
'Are you sure you want activate this merchant?'
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog(message)
|
||||
.onOk(async () => {
|
||||
await this.toggleMerchant()
|
||||
})
|
||||
},
|
||||
toggleMerchant: async function () {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'PUT',
|
||||
`/nostrmarket/api/v1/merchant/${this.merchant.id}/toggle`,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
)
|
||||
const state = data.config.active ? 'activated' : 'disabled'
|
||||
this.merchant = data
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: `'Merchant ${state}`,
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [window.windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
merchant: {},
|
||||
shippingZones: [],
|
||||
activeChatCustomer: '',
|
||||
orderPubkey: null,
|
||||
showKeys: false,
|
||||
importKeyDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
privateKey: null
|
||||
}
|
||||
},
|
||||
handleMerchantDeleted: function () {
|
||||
this.merchant = null
|
||||
this.shippingZones = []
|
||||
this.activeChatCustomer = ''
|
||||
this.showKeys = false
|
||||
},
|
||||
createMerchant: async function (privateKey) {
|
||||
try {
|
||||
const pubkey = nostr.getPublicKey(privateKey)
|
||||
const payload = {
|
||||
private_key: privateKey,
|
||||
public_key: pubkey,
|
||||
config: {}
|
||||
}
|
||||
const { data } = await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/merchant',
|
||||
this.g.user.wallets[0].adminkey,
|
||||
payload
|
||||
)
|
||||
this.merchant = data
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Merchant Created!'
|
||||
})
|
||||
this.waitForNotifications()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
getMerchant: async function () {
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/merchant',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
this.merchant = data
|
||||
return data
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
customerSelectedForOrder: function (customerPubkey) {
|
||||
this.activeChatCustomer = customerPubkey
|
||||
},
|
||||
filterOrdersForCustomer: function (customerPubkey) {
|
||||
this.orderPubkey = customerPubkey
|
||||
},
|
||||
showOrderDetails: async function (orderData) {
|
||||
await this.$refs.orderListRef.orderSelected(orderData.orderId, orderData.eventId)
|
||||
},
|
||||
waitForNotifications: async function () {
|
||||
if (!this.merchant) return
|
||||
try {
|
||||
const scheme = location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
const port = location.port ? `:${location.port}` : ''
|
||||
const wsUrl = `${scheme}://${document.domain}${port}/api/v1/ws/${this.merchant.id}`
|
||||
console.log('Reconnecting to websocket: ', wsUrl)
|
||||
this.wsConnection = new WebSocket(wsUrl)
|
||||
this.wsConnection.onmessage = async e => {
|
||||
const data = JSON.parse(e.data)
|
||||
if (data.type === 'dm:0') {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'positive',
|
||||
message: 'New Order'
|
||||
})
|
||||
|
||||
await this.$refs.directMessagesRef.handleNewMessage(data)
|
||||
return
|
||||
}
|
||||
if (data.type === 'dm:1') {
|
||||
await this.$refs.directMessagesRef.handleNewMessage(data)
|
||||
await this.$refs.orderListRef.addOrder(data)
|
||||
return
|
||||
}
|
||||
if (data.type === 'dm:2') {
|
||||
const orderStatus = JSON.parse(data.dm.message)
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'positive',
|
||||
message: orderStatus.message
|
||||
})
|
||||
if (orderStatus.paid) {
|
||||
await this.$refs.orderListRef.orderPaid(orderStatus.id)
|
||||
}
|
||||
await this.$refs.directMessagesRef.handleNewMessage(data)
|
||||
return
|
||||
}
|
||||
if (data.type === 'dm:-1') {
|
||||
await this.$refs.directMessagesRef.handleNewMessage(data)
|
||||
}
|
||||
// order paid
|
||||
// order shipped
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: 'Failed to watch for updates',
|
||||
caption: `${error}`
|
||||
})
|
||||
}
|
||||
},
|
||||
restartNostrConnection: async function () {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
'Are you sure you want to reconnect to the nostrcient extension?'
|
||||
)
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
'/nostrmarket/api/v1/restart',
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
await this.getMerchant()
|
||||
setInterval(async () => {
|
||||
if (!this.wsConnection || this.wsConnection.readyState !== WebSocket.OPEN) {
|
||||
await this.waitForNotifications()
|
||||
}
|
||||
}, 1000)
|
||||
wsConnection: null
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
generateKeys: async function () {
|
||||
const privateKey = nostr.generatePrivateKey()
|
||||
await this.createMerchant(privateKey)
|
||||
},
|
||||
importKeys: async function () {
|
||||
this.importKeyDialog.show = false
|
||||
let privateKey = this.importKeyDialog.data.privateKey
|
||||
if (!privateKey) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (privateKey.toLowerCase().startsWith('nsec')) {
|
||||
privateKey = nostr.nip19.decode(privateKey).data
|
||||
}
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
type: 'negative',
|
||||
message: `${error}`
|
||||
})
|
||||
}
|
||||
await this.createMerchant(privateKey)
|
||||
},
|
||||
showImportKeysDialog: async function () {
|
||||
this.importKeyDialog.show = true
|
||||
},
|
||||
toggleShowKeys: function () {
|
||||
this.showKeys = !this.showKeys
|
||||
},
|
||||
toggleMerchantState: async function () {
|
||||
const merchant = await this.getMerchant()
|
||||
if (!merchant) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: 'Cannot fetch merchant!'
|
||||
})
|
||||
return
|
||||
}
|
||||
const message = merchant.config.active
|
||||
? 'New orders will not be processed. Are you sure you want to deactivate?'
|
||||
: merchant.config.restore_in_progress
|
||||
? 'Merchant restore from nostr in progress. Please wait!! ' +
|
||||
'Activating now can lead to duplicate order processing. Click "OK" if you want to activate anyway?'
|
||||
: 'Are you sure you want activate this merchant?'
|
||||
|
||||
merchant()
|
||||
LNbits.utils.confirmDialog(message).onOk(async () => {
|
||||
await this.toggleMerchant()
|
||||
})
|
||||
},
|
||||
toggleMerchant: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
`/nostrmarket/api/v1/merchant/${this.merchant.id}/toggle`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
const state = data.config.active ? 'activated' : 'disabled'
|
||||
this.merchant = data
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: `'Merchant ${state}`,
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
handleMerchantDeleted: function () {
|
||||
this.merchant = null
|
||||
this.shippingZones = []
|
||||
this.activeChatCustomer = ''
|
||||
this.showKeys = false
|
||||
},
|
||||
createMerchant: async function (privateKey) {
|
||||
try {
|
||||
const pubkey = nostr.getPublicKey(privateKey)
|
||||
const payload = {
|
||||
private_key: privateKey,
|
||||
public_key: pubkey,
|
||||
config: {}
|
||||
}
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/merchant',
|
||||
this.g.user.wallets[0].adminkey,
|
||||
payload
|
||||
)
|
||||
this.merchant = data
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Merchant Created!'
|
||||
})
|
||||
this.waitForNotifications()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
getMerchant: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/merchant',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
this.merchant = data
|
||||
return data
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
customerSelectedForOrder: function (customerPubkey) {
|
||||
this.activeChatCustomer = customerPubkey
|
||||
},
|
||||
filterOrdersForCustomer: function (customerPubkey) {
|
||||
this.orderPubkey = customerPubkey
|
||||
},
|
||||
showOrderDetails: async function (orderData) {
|
||||
await this.$refs.orderListRef.orderSelected(
|
||||
orderData.orderId,
|
||||
orderData.eventId
|
||||
)
|
||||
},
|
||||
waitForNotifications: async function () {
|
||||
if (!this.merchant) return
|
||||
try {
|
||||
const scheme = location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
const port = location.port ? `:${location.port}` : ''
|
||||
const wsUrl = `${scheme}://${document.domain}${port}/api/v1/ws/${this.merchant.id}`
|
||||
console.log('Reconnecting to websocket: ', wsUrl)
|
||||
this.wsConnection = new WebSocket(wsUrl)
|
||||
this.wsConnection.onmessage = async e => {
|
||||
const data = JSON.parse(e.data)
|
||||
if (data.type === 'dm:0') {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'positive',
|
||||
message: 'New Order'
|
||||
})
|
||||
|
||||
await this.$refs.directMessagesRef.handleNewMessage(data)
|
||||
return
|
||||
}
|
||||
if (data.type === 'dm:1') {
|
||||
await this.$refs.directMessagesRef.handleNewMessage(data)
|
||||
await this.$refs.orderListRef.addOrder(data)
|
||||
return
|
||||
}
|
||||
if (data.type === 'dm:2') {
|
||||
const orderStatus = JSON.parse(data.dm.message)
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'positive',
|
||||
message: orderStatus.message
|
||||
})
|
||||
if (orderStatus.paid) {
|
||||
await this.$refs.orderListRef.orderPaid(orderStatus.id)
|
||||
}
|
||||
await this.$refs.directMessagesRef.handleNewMessage(data)
|
||||
return
|
||||
}
|
||||
if (data.type === 'dm:-1') {
|
||||
await this.$refs.directMessagesRef.handleNewMessage(data)
|
||||
}
|
||||
// order paid
|
||||
// order shipped
|
||||
}
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: 'Failed to watch for updates',
|
||||
caption: `${error}`
|
||||
})
|
||||
}
|
||||
},
|
||||
restartNostrConnection: async function () {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
'Are you sure you want to reconnect to the nostrcient extension?'
|
||||
)
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
'/nostrmarket/api/v1/restart',
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
await this.getMerchant()
|
||||
setInterval(async () => {
|
||||
if (
|
||||
!this.wsConnection ||
|
||||
this.wsConnection.readyState !== WebSocket.OPEN
|
||||
) {
|
||||
await this.waitForNotifications()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ function imgSizeFit(img, maxWidth = 1024, maxHeight = 768) {
|
|||
maxWidth / img.naturalWidth,
|
||||
maxHeight / img.naturalHeight
|
||||
)
|
||||
return { width: img.naturalWidth * ratio, height: img.naturalHeight * ratio }
|
||||
return {width: img.naturalWidth * ratio, height: img.naturalHeight * ratio}
|
||||
}
|
||||
|
||||
async function hash(string) {
|
||||
|
|
@ -125,7 +125,7 @@ function isValidImageUrl(string) {
|
|||
function isValidKey(key, prefix = 'n') {
|
||||
try {
|
||||
if (key && key.startsWith(prefix)) {
|
||||
let { _, data } = NostrTools.nip19.decode(key)
|
||||
let {_, data} = NostrTools.nip19.decode(key)
|
||||
key = data
|
||||
}
|
||||
return isValidKeyHex(key)
|
||||
|
|
@ -143,4 +143,4 @@ function formatCurrency(value, currency) {
|
|||
style: 'currency',
|
||||
currency: currency
|
||||
}).format(value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@
|
|||
<script src="/nostrmarket/static/market/js/nostr.bundle.js"></script>
|
||||
<script src="/nostrmarket/static/market/js/bolt11-decoder.js"></script>
|
||||
<script src="/nostrmarket/static/market/js/utils.js"></script>
|
||||
<link rel=icon type=image/png sizes=128x128 href="/nostrmarket/static/market/icons/favicon-128x128.png">
|
||||
<link rel=icon type=image/png sizes=96x96 href="/nostrmarket/static/market/icons/favicon-96x96.png">
|
||||
<link rel=icon type=image/png sizes=32x32 href="/nostrmarket/static/market/icons/favicon-32x32.png">
|
||||
<link rel=icon type=image/png sizes=16x16 href="/nostrmarket/static/market/icons/favicon-16x16.png">
|
||||
<link rel=icon type=image/ico href="/nostrmarket/static/market/favicon.ico">
|
||||
<link rel="icon" type="image/png" sizes="128x128" href="/nostrmarket/static/market/icons/favicon-128x128.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/nostrmarket/static/market/icons/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/nostrmarket/static/market/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/nostrmarket/static/market/icons/favicon-16x16.png">
|
||||
<link rel="icon" type="image/ico" href="/nostrmarket/static/market/favicon.ico">
|
||||
<script type="module" crossorigin src="/nostrmarket/static/market/assets/index.923cbbf9.js"></script>
|
||||
<link rel="stylesheet" href="/nostrmarket/static/market/assets/index.73d462e5.css">
|
||||
</head>
|
||||
|
|
|
|||
5
tasks.py
5
tasks.py
|
|
@ -1,10 +1,9 @@
|
|||
from asyncio import Queue
|
||||
import asyncio
|
||||
|
||||
from loguru import logger
|
||||
from asyncio import Queue
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
from loguru import logger
|
||||
|
||||
from .nostr.nostr_client import NostrClient
|
||||
from .services import (
|
||||
|
|
|
|||
169
templates/nostrmarket/components/direct-messages.html
Normal file
169
templates/nostrmarket/components/direct-messages.html
Normal 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> 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>
|
||||
|
|
@ -13,11 +13,11 @@
|
|||
<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
|
||||
<lnbits-qrcode
|
||||
:value="publicKey"
|
||||
:options="{width: 250}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
></lnbits-qrcode>
|
||||
</q-responsive>
|
||||
<small><span v-text="publicKey"></span><br />Click to copy</small>
|
||||
</div>
|
||||
|
|
@ -30,11 +30,7 @@
|
|||
class="q-mx-xl"
|
||||
@click="copyText(privateKey)"
|
||||
>
|
||||
<qrcode
|
||||
:value="privateKey"
|
||||
:options="{width: 250}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
<lnbits-qrcode :value="privateKey"></lnbits-qrcode>
|
||||
</q-responsive>
|
||||
<small><span v-text="privateKey"></span><br />Click to copy</small>
|
||||
</div>
|
||||
369
templates/nostrmarket/components/order-list.html
Normal file
369
templates/nostrmarket/components/order-list.html
Normal 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>
|
||||
|
|
@ -22,12 +22,13 @@
|
|||
@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-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-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">
|
||||
466
templates/nostrmarket/components/stall-details.html
Normal file
466
templates/nostrmarket/components/stall-details.html
Normal 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>
|
||||
215
templates/nostrmarket/components/stall-list.html
Normal file
215
templates/nostrmarket/components/stall-list.html
Normal 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>
|
||||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
9
toc.md
9
toc.md
|
|
@ -1,22 +1,29 @@
|
|||
# Terms and Conditions for LNbits Extension
|
||||
|
||||
## 1. Acceptance of Terms
|
||||
|
||||
By installing and using the LNbits extension ("Extension"), you agree to be bound by these terms and conditions ("Terms"). If you do not agree to these Terms, do not use the Extension.
|
||||
|
||||
## 2. License
|
||||
|
||||
The Extension is free and open-source software, released under [specify the FOSS license here, e.g., GPL-3.0, MIT, etc.]. You are permitted to use, copy, modify, and distribute the Extension under the terms of that license.
|
||||
|
||||
## 3. No Warranty
|
||||
|
||||
The Extension is provided "as is" and with all faults, and the developer expressly disclaims all warranties of any kind, whether express, implied, statutory, or otherwise, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, and any warranties arising out of course of dealing or usage of trade. No advice or information, whether oral or written, obtained from the developer or elsewhere will create any warranty not expressly stated in this Terms.
|
||||
|
||||
## 4. Limitation of Liability
|
||||
|
||||
In no event will the developer be liable to you or any third party for any direct, indirect, incidental, special, consequential, or punitive damages, including lost profit, lost revenue, loss of data, or other damages arising out of or in connection with your use of the Extension, even if the developer has been advised of the possibility of such damages. The foregoing limitation of liability shall apply to the fullest extent permitted by law in the applicable jurisdiction.
|
||||
|
||||
## 5. Modification of Terms
|
||||
|
||||
The developer reserves the right to modify these Terms at any time. You are advised to review these Terms periodically for any changes. Changes to these Terms are effective when they are posted on the appropriate location within or associated with the Extension.
|
||||
|
||||
## 6. General Provisions
|
||||
|
||||
If any provision of these Terms is held to be invalid or unenforceable, that provision will be enforced to the maximum extent permissible, and the other provisions of these Terms will remain in full force and effect. These Terms constitute the entire agreement between you and the developer regarding the use of the Extension.
|
||||
|
||||
## 7. Contact Information
|
||||
If you have any questions about these Terms, please contact the developer at [developer's contact information].
|
||||
|
||||
If you have any questions about these Terms, please contact the developer at [developer's contact information].
|
||||
|
|
|
|||
11
views.py
11
views.py
|
|
@ -1,13 +1,8 @@
|
|||
import json
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Depends, Query, Request
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from loguru import logger
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from . import nostrmarket_ext, nostrmarket_renderer
|
||||
|
||||
|
|
@ -18,7 +13,7 @@ templates = Jinja2Templates(directory="templates")
|
|||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
return nostrmarket_renderer().TemplateResponse(
|
||||
"nostrmarket/index.html",
|
||||
{"request": request, "user": user.dict()},
|
||||
{"request": request, "user": user.json()},
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
211
views_api.py
211
views_api.py
|
|
@ -4,16 +4,14 @@ from typing import List, Optional
|
|||
|
||||
from fastapi import Depends
|
||||
from fastapi.exceptions import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.services import websocket_updater
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
get_key_type,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.utils.exchange_rates import currencies
|
||||
from loguru import logger
|
||||
|
||||
from . import nostr_client, nostrmarket_ext
|
||||
from .crud import (
|
||||
|
|
@ -71,9 +69,6 @@ from .models import (
|
|||
PartialDirectMessage,
|
||||
PartialMerchant,
|
||||
PartialOrder,
|
||||
PartialProduct,
|
||||
PartialStall,
|
||||
PartialZone,
|
||||
PaymentOption,
|
||||
PaymentRequest,
|
||||
Product,
|
||||
|
|
@ -81,16 +76,16 @@ from .models import (
|
|||
Zone,
|
||||
)
|
||||
from .services import (
|
||||
reply_to_structured_dm,
|
||||
build_order_with_payment,
|
||||
create_or_update_order_from_dm,
|
||||
reply_to_structured_dm,
|
||||
resubscribe_to_all_merchants,
|
||||
sign_and_send_to_nostr,
|
||||
subscribe_to_all_merchants,
|
||||
update_merchant_to_nostr,
|
||||
)
|
||||
|
||||
######################################## MERCHANT ########################################
|
||||
######################################## MERCHANT ######################################
|
||||
|
||||
|
||||
@nostrmarket_ext.post("/api/v1/merchant")
|
||||
|
|
@ -101,16 +96,16 @@ async def api_create_merchant(
|
|||
|
||||
try:
|
||||
merchant = await get_merchant_by_pubkey(data.public_key)
|
||||
assert merchant == None, "A merchant already uses this public key"
|
||||
assert merchant is None, "A merchant already uses this public key"
|
||||
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant == None, "A merchant already exists for this user"
|
||||
assert merchant is None, "A merchant already exists for this user"
|
||||
|
||||
merchant = await create_merchant(wallet.wallet.user, data)
|
||||
|
||||
await create_zone(
|
||||
merchant.id,
|
||||
PartialZone(
|
||||
Zone(
|
||||
id=f"online-{merchant.public_key}",
|
||||
name="Online",
|
||||
currency="sat",
|
||||
|
|
@ -128,13 +123,13 @@ async def api_create_merchant(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create merchant",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.get("/api/v1/merchant")
|
||||
|
|
@ -145,11 +140,12 @@ async def api_get_merchant(
|
|||
try:
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
if not merchant:
|
||||
return
|
||||
return None
|
||||
|
||||
merchant = await touch_merchant(wallet.wallet.user, merchant.id)
|
||||
assert merchant
|
||||
last_dm_time = await get_last_direct_messages_time(merchant.id)
|
||||
|
||||
assert merchant.time
|
||||
merchant.config.restore_in_progress = (merchant.time - last_dm_time) < 30
|
||||
|
||||
return merchant
|
||||
|
|
@ -158,7 +154,7 @@ async def api_get_merchant(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get merchant",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.delete("/api/v1/merchant/{merchant_id}")
|
||||
|
|
@ -186,16 +182,17 @@ async def api_delete_merchant(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get merchant",
|
||||
)
|
||||
) from ex
|
||||
finally:
|
||||
await subscribe_to_all_merchants()
|
||||
|
||||
|
||||
@nostrmarket_ext.put("/api/v1/merchant/{merchant_id}/nostr")
|
||||
async def api_republish_merchant(
|
||||
merchant_id: str,
|
||||
|
|
@ -213,13 +210,14 @@ async def api_republish_merchant(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot republish to nostr",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.get("/api/v1/merchant/{merchant_id}/nostr")
|
||||
async def api_refresh_merchant(
|
||||
|
|
@ -237,13 +235,13 @@ async def api_refresh_merchant(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot refresh from nostr",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.put("/api/v1/merchant/{merchant_id}/toggle")
|
||||
|
|
@ -264,17 +262,17 @@ async def api_toggle_merchant(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get merchant",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.delete("/api/v1/merchant/{merchant_id}/nostr")
|
||||
async def api_delete_merchant(
|
||||
async def api_delete_merchant_on_nostr(
|
||||
merchant_id: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
|
|
@ -290,20 +288,22 @@ async def api_delete_merchant(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get merchant",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
######################################## ZONES ########################################
|
||||
|
||||
|
||||
@nostrmarket_ext.get("/api/v1/zone")
|
||||
async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[Zone]:
|
||||
async def api_get_zones(
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
) -> List[Zone]:
|
||||
try:
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
|
|
@ -312,18 +312,18 @@ async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get zone",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.post("/api/v1/zone")
|
||||
async def api_create_zone(
|
||||
data: PartialZone, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
data: Zone, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
try:
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
|
|
@ -334,13 +334,13 @@ async def api_create_zone(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create zone",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.patch("/api/v1/zone/{zone_id}")
|
||||
|
|
@ -365,15 +365,14 @@ async def api_update_zone(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
except HTTPException as ex:
|
||||
raise ex
|
||||
) from ex
|
||||
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot update zone",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.delete("/api/v1/zone/{zone_id}")
|
||||
|
|
@ -394,13 +393,13 @@ async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admi
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot delete zone",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
######################################## STALLS ########################################
|
||||
|
|
@ -408,7 +407,7 @@ async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admi
|
|||
|
||||
@nostrmarket_ext.post("/api/v1/stall")
|
||||
async def api_create_stall(
|
||||
data: PartialStall,
|
||||
data: Stall,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> Stall:
|
||||
try:
|
||||
|
|
@ -430,13 +429,13 @@ async def api_create_stall(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create stall",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.put("/api/v1/stall/{stall_id}")
|
||||
|
|
@ -459,23 +458,24 @@ async def api_update_stall(
|
|||
await update_stall(merchant.id, stall)
|
||||
|
||||
return stall
|
||||
except HTTPException as ex:
|
||||
raise ex
|
||||
|
||||
except (ValueError, AssertionError) as ex:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot update stall",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.get("/api/v1/stall/{stall_id}")
|
||||
async def api_get_stall(stall_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
async def api_get_stall(
|
||||
stall_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||
):
|
||||
try:
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
|
|
@ -490,7 +490,7 @@ async def api_get_stall(stall_id: str, wallet: WalletTypeInfo = Depends(get_key_
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except HTTPException as ex:
|
||||
raise ex
|
||||
except Exception as ex:
|
||||
|
|
@ -498,12 +498,13 @@ async def api_get_stall(stall_id: str, wallet: WalletTypeInfo = Depends(get_key_
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get stall",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.get("/api/v1/stall")
|
||||
async def api_get_stalls(
|
||||
pending: Optional[bool] = False, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
pending: Optional[bool] = False,
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
):
|
||||
try:
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
|
|
@ -514,13 +515,13 @@ async def api_get_stalls(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get stalls",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.get("/api/v1/stall/product/{stall_id}")
|
||||
|
|
@ -538,13 +539,13 @@ async def api_get_stall_products(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get stall products",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.get("/api/v1/stall/order/{stall_id}")
|
||||
|
|
@ -566,13 +567,13 @@ async def api_get_stall_orders(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get stall products",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.delete("/api/v1/stall/{stall_id}")
|
||||
|
|
@ -600,23 +601,21 @@ async def api_delete_stall(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
except HTTPException as ex:
|
||||
raise ex
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot delete stall",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
######################################## PRODUCTS ########################################
|
||||
######################################## PRODUCTS ######################################
|
||||
|
||||
|
||||
@nostrmarket_ext.post("/api/v1/product")
|
||||
async def api_create_product(
|
||||
data: PartialProduct,
|
||||
data: Product,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> Product:
|
||||
try:
|
||||
|
|
@ -639,13 +638,13 @@ async def api_create_product(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create product",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.patch("/api/v1/product/{product_id}")
|
||||
|
|
@ -675,13 +674,13 @@ async def api_update_product(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot update product",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.get("/api/v1/product/{product_id}")
|
||||
|
|
@ -699,13 +698,13 @@ async def api_get_product(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get product",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.delete("/api/v1/product/{product_id}")
|
||||
|
|
@ -731,15 +730,13 @@ async def api_delete_product(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
except HTTPException as ex:
|
||||
raise ex
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot delete product",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
######################################## ORDERS ########################################
|
||||
|
|
@ -764,15 +761,13 @@ async def api_get_order(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
except HTTPException as ex:
|
||||
raise ex
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get order",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.get("/api/v1/order")
|
||||
|
|
@ -780,7 +775,7 @@ async def api_get_orders(
|
|||
paid: Optional[bool] = None,
|
||||
shipped: Optional[bool] = None,
|
||||
pubkey: Optional[str] = None,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
):
|
||||
try:
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
|
|
@ -794,13 +789,13 @@ async def api_get_orders(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get orders",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.patch("/api/v1/order/{order_id}")
|
||||
|
|
@ -809,7 +804,7 @@ async def api_update_order_status(
|
|||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> Order:
|
||||
try:
|
||||
assert data.shipped != None, "Shipped value is required for order"
|
||||
assert data.shipped is not None, "Shipped value is required for order"
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found for order {data.id}"
|
||||
|
||||
|
|
@ -852,20 +847,20 @@ async def api_update_order_status(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot update order",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.put("/api/v1/order/restore/{event_id}")
|
||||
async def api_restore_order(
|
||||
event_id: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> Order:
|
||||
) -> Optional[Order]:
|
||||
try:
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
|
|
@ -881,13 +876,13 @@ async def api_restore_order(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot restore order",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.put("/api/v1/orders/restore")
|
||||
|
|
@ -906,20 +901,20 @@ async def api_restore_orders(
|
|||
)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"Failed to restore order from event '{dm.event_id}': '{str(e)}'."
|
||||
f"Failed to restore order from event '{dm.event_id}': '{e!s}'."
|
||||
)
|
||||
|
||||
except AssertionError as ex:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot restore orders",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.put("/api/v1/order/reissue")
|
||||
|
|
@ -955,7 +950,9 @@ async def api_reissue_order_invoice(
|
|||
**order_update,
|
||||
)
|
||||
payment_req = PaymentRequest(
|
||||
id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)], message=receipt
|
||||
id=data.id,
|
||||
payment_options=[PaymentOption(type="ln", link=invoice)],
|
||||
message=receipt,
|
||||
)
|
||||
response = {
|
||||
"type": DirectMessageType.PAYMENT_REQUEST.value,
|
||||
|
|
@ -975,25 +972,25 @@ async def api_reissue_order_invoice(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot reissue order invoice",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
######################################## DIRECT MESSAGES ########################################
|
||||
######################################## DIRECT MESSAGES ###############################
|
||||
|
||||
|
||||
@nostrmarket_ext.get("/api/v1/message/{public_key}")
|
||||
async def api_get_messages(
|
||||
public_key: str, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
public_key: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||
) -> List[DirectMessage]:
|
||||
try:
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, f"Merchant cannot be found"
|
||||
assert merchant, "Merchant cannot be found"
|
||||
|
||||
messages = await get_direct_messages(merchant.id, public_key)
|
||||
await update_customer_no_unread_messages(merchant.id, public_key)
|
||||
|
|
@ -1002,13 +999,13 @@ async def api_get_messages(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get direct message",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.post("/api/v1/message")
|
||||
|
|
@ -1017,7 +1014,7 @@ async def api_create_message(
|
|||
) -> DirectMessage:
|
||||
try:
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, f"Merchant cannot be found"
|
||||
assert merchant, "Merchant cannot be found"
|
||||
|
||||
dm_event = merchant.build_dm_event(data.message, data.public_key)
|
||||
data.event_id = dm_event.id
|
||||
|
|
@ -1031,38 +1028,38 @@ async def api_create_message(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create message",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
######################################## CUSTOMERS ########################################
|
||||
######################################## CUSTOMERS #####################################
|
||||
|
||||
|
||||
@nostrmarket_ext.get("/api/v1/customer")
|
||||
async def api_get_customers(
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
) -> List[Customer]:
|
||||
try:
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, f"Merchant cannot be found"
|
||||
assert merchant, "Merchant cannot be found"
|
||||
return await get_customers(merchant.id)
|
||||
|
||||
except AssertionError as ex:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create message",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.post("/api/v1/customer")
|
||||
|
|
@ -1079,7 +1076,7 @@ async def api_create_customer(
|
|||
assert merchant.id == data.merchant_id, "Invalid merchant id for user"
|
||||
|
||||
existing_customer = await get_customer(merchant.id, pubkey)
|
||||
assert existing_customer == None, "This public key already exists"
|
||||
assert existing_customer is None, "This public key already exists"
|
||||
|
||||
customer = await create_customer(
|
||||
merchant.id, Customer(merchant_id=merchant.id, public_key=pubkey)
|
||||
|
|
@ -1092,13 +1089,13 @@ async def api_create_customer(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create customer",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
######################################## OTHER ########################################
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue