diff --git a/TODO.md b/TODO.md index 39984ff..6e3c13b 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,8 @@ push offers cover off admin routes and commands -remove want on offer complete -do we keep historical offers / wants? +decline / unaccept +minimum quality + +listen / notify diff --git a/api/src/offers.ts b/api/src/offers.ts index 71f0e9a..a8e0574 100644 --- a/api/src/offers.ts +++ b/api/src/offers.ts @@ -118,11 +118,34 @@ export default function createOffers(log: Log, pg: Pg) { }); } + async function acpOffers() { + const offers = await pg.offersAdmin(); + + const grouped = groupBy(offers, o => o.listing.data.seller.id); + + const sellerOffers = sortBy(Object.values(grouped).map(offers => { + const shipping = offers[0]?.listing.data.seller.shipping; + const bundle = { + username: offers[0]?.listing.data.seller.username, + seller: offers[0]?.listing.data.seller.id, + shipsFrom: offers[0]?.listing.data.ships_from, + shipping, + offers, + mentionsAustralia: shipping.toLowerCase().includes('australia'), + mentionsWorldwide: shipping.toLowerCase().includes('world'), + } + + return bundle; + }), bundle => -bundle.offers.length); + + return sellerOffers; + } return { generate, accept, bundles, statusSet, + acpOffers, }; } diff --git a/api/src/rpc.ts b/api/src/rpc.ts index 6b1230e..7eb81ad 100644 --- a/api/src/rpc.ts +++ b/api/src/rpc.ts @@ -8,6 +8,7 @@ import type { Want, Offer, User } from '@prisma/client'; import { Pg } from './pg'; import { Status } from './offers'; +import { isAdmin } from './utils'; function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType { if (RpcMessageType[msgType]) { @@ -28,7 +29,9 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off assert(Status[status], 'unknown status'); const offer = await offers.statusSet(user, id, status); - return [RpcMessageType.Offers, []] + const userOffers = await pg.userOffers(user); + + return [RpcMessageType.Offers, userOffers] } async function wantRm(params: WantRmParams): Promise { @@ -41,6 +44,22 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off return [RpcMessageType.Wants, wants]; } + async function acpOfferStatusSet(params: OfferStatusSetParams): Promise { + const { id, status } = params; + assert(isAdmin(user), 'not authorised'); + assert(id, 'no id supplied'); + assert(Status[status], 'unknown status'); + + const offer = await offers.statusSet(user, id, status); + + return [RpcMessageType.AcpOffers, await offers.acpOffers()] + } + + async function acpOffers(): Promise { + assert(isAdmin(user), 'not authorised'); + return [RpcMessageType.AcpOffers, await offers.acpOffers()]; + } + async function receive(message: any): Promise { const [msgType, params] = message; @@ -53,6 +72,10 @@ export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, off return ping(); case RpcMessageType.OfferStatusSet: return offerStatusSet(params); + case RpcMessageType.AcpOfferStatusSet: + return acpOfferStatusSet(params); + case RpcMessageType.AcpOffers: + return acpOffers(); case RpcMessageType.WantRm: return wantRm(params); default: @@ -103,6 +126,9 @@ export enum RpcMessageType { OfferStatusSet = 'OfferStatusSet', + AcpOfferStatusSet = 'AcpOfferStatusSet', + AcpOffers = 'AcpOffers', + WantRm = 'WantRm', } @@ -114,6 +140,9 @@ type WantsMessage = [RpcMessageType.Wants, Want[]]; type OffersMessage = [RpcMessageType.Offers, Offer[]]; type OfferStatusSetMessage = [RpcMessageType.OfferStatusSet, Offer[]]; +type AcpOfferStatusSetMessage = [RpcMessageType.AcpOfferStatusSet, Offer[]]; +type AcpOffersMessage = [RpcMessageType.AcpOffers, Offer[]]; + export type RpcMessage = PingMessage | PongMessage | @@ -124,6 +153,10 @@ export type RpcMessage = OffersMessage | OfferStatusSetMessage | + + AcpOfferStatusSetMessage | + AcpOffersMessage | + ErrorMessage; diff --git a/api/src/utils.ts b/api/src/utils.ts index 8092d17..0c06b96 100644 --- a/api/src/utils.ts +++ b/api/src/utils.ts @@ -1,4 +1,4 @@ -import type { Want } from '@prisma/client'; +import type { Want, User } from '@prisma/client'; export function logAxiosError(e) { console.warn({ @@ -25,4 +25,8 @@ export function wantDetails(want: Want) { } } +export function isAdmin(user: User) { + return user.email === 'ntr@mnml.gg'; +} + export function offerDetails() {} diff --git a/client/src/components/acp.offer.tsx b/client/src/components/acp.offer.tsx deleted file mode 100644 index 125f677..0000000 --- a/client/src/components/acp.offer.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { h } from 'preact'; -import { ExternalLink, Check } from 'react-feather'; -import { useState } from 'preact/hooks'; - -import http from '../http'; - -export default function Offer(offer) { - const { want: { data: details } } = offer; - - const [accepted, setAccepted] = useState(offer.status !== 'pending'); - - async function onClick() { - const [error] = await http.postOfferAccept(offer.id); - if (error) return false; - setAccepted(true); - } - - const classes = `${accepted ? 'accepted' : ''}`; - const btnText = accepted - ? offer.status - : 'Accept'; - - return ( -
-
-

{details.artists_sort} - {details.title}

-
{details.year}
-
{details.formats[0].descriptions.join(' ')}
-
${offer.total}
-
- - -
- ) -} diff --git a/client/src/components/bundle.tsx b/client/src/components/bundle.tsx index 4fc8a96..09d8981 100644 --- a/client/src/components/bundle.tsx +++ b/client/src/components/bundle.tsx @@ -41,17 +41,8 @@ export default function Bundle(bundle) { const initialState = { checkedIds: bundle.items.map(i => i.id) } const [state, dispatch] = useReducer(reducer, initialState) - const [shipping, setShipping] = useState(0); - const [baseShipping, setBaseShipping] = useState(0); - const items = bundle.items.map(item => Item(item, state, dispatch)); - async function submit(e) { - e.preventDefault(); - const [error, offers] = await http.postOffers(state.checkedIds, shipping, baseShipping); - console.log(error, offers); - } - return (
@@ -59,23 +50,6 @@ export default function Bundle(bundle) {

{bundle.items.length} items - {bundle.total.toFixed(2)} {bundle.currency}

Ships: {bundle.mentionsAustralia && 'AU'} {bundle.mentionsWorldwide && 'WORLD'}

- -
-
-
- - setShipping(Number.parseFloat((e.target as HTMLInputElement).value))} step="0.01" min="0" required /> -
-
- - setBaseShipping(Number.parseFloat((e.target as HTMLInputElement).value))} step="0.01" min="0" required /> -
-
- submit(e)} /> -
-
-
-
{items}
diff --git a/client/src/components/offer.tsx b/client/src/components/offer.tsx index 2ab7ff0..2bffb65 100644 --- a/client/src/components/offer.tsx +++ b/client/src/components/offer.tsx @@ -2,24 +2,30 @@ import { h } from 'preact'; import { ExternalLink, Check } from 'react-feather'; import { useState } from 'preact/hooks'; +import { useSelector, useDispatch } from 'react-redux' +import { State } from './../store'; + import http from '../http'; export default function Offer(offer) { + const ws = useSelector((state: State) => state.ws); + const { want: { data: details } } = offer; - const [accepted, setAccepted] = useState(offer.status !== 'Pending'); - - async function onClick() { - const [error] = await http.postOfferAccept(offer.id); - if (error) return false; - setAccepted(true); + function setStatus(status) { + ws.sendOfferStatusSet(offer.id, status); } + const accepted = offer.status === 'Accepted'; const classes = `${accepted ? 'accepted' : ''}`; const btnText = accepted ? offer.status : 'Accept'; + const negBtn = accepted + ? 'Cancel' + : 'decline'; + return (
@@ -30,7 +36,8 @@ export default function Offer(offer) {
shipped by itself: ${offer.comparison}
- + +
) } diff --git a/client/src/http.ts b/client/src/http.ts index 0ce4500..6ac3225 100644 --- a/client/src/http.ts +++ b/client/src/http.ts @@ -90,19 +90,6 @@ const actions = { baseShipping, }); }, - - postOfferAccept: async function postOffers(id) { - return api.post('/offer/accept', { - id, - }); - }, - - postOfferStatus: async function postOfferStatus(id, status) { - return api.post('/acp/offer/status', { - id, - status, - }); - }, }; export default actions; diff --git a/client/src/routes/acp.tsx b/client/src/routes/acp.tsx index e8fb260..4a83fa1 100644 --- a/client/src/routes/acp.tsx +++ b/client/src/routes/acp.tsx @@ -2,7 +2,6 @@ import { h } from 'preact'; import { useState, useEffect } from 'preact/hooks'; import http from './../http'; -import AcpOffer from './../components/acp.offer'; import { useSelector, useDispatch } from 'react-redux' import { State } from './../store'; @@ -24,7 +23,7 @@ function Item({ item }) { )); function onChange(e) { - ws.sendOfferStatusSet(item.id, e.target.value); + ws.sendAcpOfferStatusSet(item.id, e.target.value); } return ( @@ -58,16 +57,12 @@ function SellerGroup(group) { } function Home() { - const [offers, setOffers] = useState([]); + const offers = useSelector((state: State) => state.acpOffers); + const ws = useSelector((state: State) => state.ws); - useEffect(() => { - async function getOffers() { - const [error, offers] = await http.getOffersAcp(); - if (error) return console.error(error); - setOffers(offers); - } - getOffers(); - }, []); + // useEffect(() => { + // ws.sendAcpOffers(); + // }, [offers, ws]); if (!offers.length) { return ( @@ -77,7 +72,6 @@ function Home() { ); } - return (
{offers.map(SellerGroup)} diff --git a/client/src/store.tsx b/client/src/store.tsx index 87400f5..9277344 100644 --- a/client/src/store.tsx +++ b/client/src/store.tsx @@ -17,6 +17,14 @@ export const offersState = createSlice({ }, }); +export const acpOffersState = createSlice({ + name: 'acpOffers', + initialState: [], + reducers: { + set: (state, action) => action.payload + }, +}); + export const wantsState = createSlice({ name: 'wants', initialState: [], @@ -38,6 +46,8 @@ const reducer = combineReducers({ wants: wantsState.reducer, ws: wsState.reducer, user: userState.reducer, + + acpOffers: acpOffersState.reducer, }); const store = createStore( diff --git a/client/src/style/index.less b/client/src/style/index.less index 989c5e4..0f1b53e 100644 --- a/client/src/style/index.less +++ b/client/src/style/index.less @@ -329,7 +329,7 @@ header { display: grid; grid-template-areas: "details img" - "btn btn"; + "btn neg"; padding: var(--base-spacing); margin: var(--base-spacing-half); @@ -352,6 +352,11 @@ header { background: var(--color-accent-blue); cursor: auto; } + + &.neg { + margin-left: var(--base-spacing-half); + grid-area: neg; + } } } } diff --git a/client/src/ws.ts b/client/src/ws.ts index 769ebee..4f57f4f 100644 --- a/client/src/ws.ts +++ b/client/src/ws.ts @@ -1,4 +1,4 @@ -import { wantsState, wsState, userState } from './store' +import { wantsState, wsState, userState, offersState, acpOffersState } from './store' const SOCKET_URL = `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${window.location.host}/api/ws`; @@ -34,6 +34,14 @@ export default function createSocket(dispatch) { send(['WantRm', { id }]); } + function sendAcpOffers() { + send(['AcpOffers', {}]); + } + + function sendAcpOfferStatusSet(id, status) { + send(['AcpOfferStatusSet', { id, status }]); + } + let pongTimeout; function onPong() { pongTimeout = setTimeout(sendPing, 5000); @@ -50,6 +58,14 @@ export default function createSocket(dispatch) { dispatch(wantsState.actions.set(wants)); } + function onOffers(offers) { + dispatch(offersState.actions.set(offers)); + } + + function onAcpOffers(offers) { + dispatch(acpOffersState.actions.set(offers)); + } + // ------------- // Setup // ------------- @@ -57,8 +73,11 @@ export default function createSocket(dispatch) { // this object wraps the reply types to a function const handlers = { Wants: onWants, + Offers: onOffers, User: onUser, + AcpOffers: onAcpOffers, + Pong: onPong, Error: errHandler, }; @@ -84,6 +103,7 @@ export default function createSocket(dispatch) { function onOpen() { console.log('websocket connected', SOCKET_URL) sendPing(); + sendAcpOffers(); } function onError(event) { @@ -122,5 +142,8 @@ export default function createSocket(dispatch) { sendOfferStatusSet, sendWantRm, + + sendAcpOffers, + sendAcpOfferStatusSet, }; }