inv & with satistics + filtering

This commit is contained in:
keyan 2021-12-16 11:27:12 -06:00
parent 06f5ed731e
commit d92f58aaf4
9 changed files with 159 additions and 84 deletions

View File

@ -49,46 +49,62 @@ export default {
connectAddress: async (parent, args, { lnd }) => { connectAddress: async (parent, args, { lnd }) => {
return process.env.LND_CONNECT_ADDRESS return process.env.LND_CONNECT_ADDRESS
}, },
walletHistory: async (parent, { cursor }, { me, models, lnd }) => { walletHistory: async (parent, { cursor, inc }, { me, models, lnd }) => {
const decodedCursor = decodeCursor(cursor) const decodedCursor = decodeCursor(cursor)
if (!me) { if (!me) {
throw new AuthenticationError('you must be logged in') throw new AuthenticationError('you must be logged in')
} }
const include = new Set(inc.split(','))
const queries = []
if (include.has('invoice')) {
queries.push(
`(SELECT ('invoice' || id) as id, id as "factId", bolt11, created_at as "createdAt",
COALESCE("msatsReceived", "msatsRequested") as msats, NULL as "msatsFee",
CASE WHEN "confirmedAt" IS NOT NULL THEN 'CONFIRMED'
WHEN "expiresAt" <= $2 THEN 'EXPIRED'
WHEN cancelled THEN 'CANCELLED'
ELSE 'PENDING' END as status,
'invoice' as type
FROM "Invoice"
WHERE "userId" = $1
AND created_at <= $2)`)
}
if (include.has('withdrawal')) {
queries.push(
`(SELECT ('withdrawal' || id) as id, id as "factId", bolt11, created_at as "createdAt",
CASE WHEN status = 'CONFIRMED' THEN "msatsPaid"
ELSE "msatsPaying" END as msats,
CASE WHEN status = 'CONFIRMED' THEN "msatsFeePaid"
ELSE "msatsFeePaying" END as "msatsFee",
COALESCE(status::text, 'PENDING') as status,
'withdrawal' as type
FROM "Withdrawl"
WHERE "userId" = $1
AND created_at <= $2)`)
}
// TODO // TODO
// 1. union invoices and withdrawals (check) // 1. union invoices and withdrawals (check)
// 2. add to union spending and receiving // 2. add to union spending and receiving
if (queries.length === 0) {
return {
cursor: null,
facts: []
}
}
let history = await models.$queryRaw(` let history = await models.$queryRaw(`
(SELECT id, bolt11, created_at as "createdAt", ${queries.join(' UNION ALL ')}
COALESCE("msatsReceived", "msatsRequested") as msats, NULL as "msatsFee",
CASE WHEN "confirmedAt" IS NOT NULL THEN 'CONFIRMED'
WHEN "expiresAt" <= $2 THEN 'EXPIRED'
WHEN cancelled THEN 'CANCELLED'
ELSE 'PENDING' END as status,
'invoice' as type
FROM "Invoice"
WHERE "userId" = $1
AND created_at <= $2
ORDER BY created_at desc
LIMIT ${LIMIT}+$3)
UNION ALL
(SELECT id, bolt11, created_at as "createdAt",
CASE WHEN status = 'CONFIRMED' THEN "msatsPaid"
ELSE "msatsPaying" END as msats,
CASE WHEN status = 'CONFIRMED' THEN "msatsFeePaid"
ELSE "msatsFeePaying" END as "msatsFee",
COALESCE(status::text, 'PENDING') as status,
'withdrawal' as type
FROM "Withdrawl"
WHERE "userId" = $1
AND created_at <= $2
ORDER BY created_at desc
LIMIT ${LIMIT}+$3)
ORDER BY "createdAt" DESC ORDER BY "createdAt" DESC
OFFSET $3 OFFSET $3
LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset) LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset)
console.log(history)
history = history.map(f => { history = history.map(f => {
if (f.bolt11) { if (f.bolt11) {
const inv = lnpr.decode(f.bolt11) const inv = lnpr.decode(f.bolt11)
@ -113,6 +129,8 @@ export default {
return f return f
}) })
console.log(history)
return { return {
cursor: history.length === LIMIT ? nextCursorEncoded(decodedCursor) : null, cursor: history.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
facts: history facts: history

View File

@ -30,7 +30,7 @@ export default async function getSSRApolloClient (req, me = null) {
} }
export function getGetServerSideProps (query, variables = null, foundField) { export function getGetServerSideProps (query, variables = null, foundField) {
return async function ({ req, params }) { return async function ({ req, query: params }) {
const client = await getSSRApolloClient(req) const client = await getSSRApolloClient(req)
const { error, data } = await client.query({ const { error, data } = await client.query({
query, query,

View File

@ -5,7 +5,7 @@ export default gql`
invoice(id: ID!): Invoice! invoice(id: ID!): Invoice!
withdrawl(id: ID!): Withdrawl! withdrawl(id: ID!): Withdrawl!
connectAddress: String! connectAddress: String!
walletHistory(cursor: String): History walletHistory(cursor: String, inc: String): History
} }
extend type Mutation { extend type Mutation {
@ -41,6 +41,7 @@ export default gql`
type Fact { type Fact {
id: ID! id: ID!
factId: ID!
bolt11: String bolt11: String
createdAt: String! createdAt: String!
msats: Int! msats: Int!

View File

@ -175,33 +175,31 @@ export function Input ({ label, groupClassName, ...props }) {
) )
} }
export function Checkbox ({ children, label, extra, handleChange, ...props }) { export function Checkbox ({ children, label, extra, handleChange, inline, ...props }) {
// React treats radios and checkbox inputs differently other input types, select, and textarea. // React treats radios and checkbox inputs differently other input types, select, and textarea.
// Formik does this too! When you specify `type` to useField(), it will // Formik does this too! When you specify `type` to useField(), it will
// return the correct bag of props for you // return the correct bag of props for you
const [field, { value }] = useField({ ...props, type: 'checkbox' }) const [field] = useField({ ...props, type: 'checkbox' })
return ( return (
<div className={value ? styles.checkboxChecked : styles.checkboxUnchecked}> <BootstrapForm.Check
<BootstrapForm.Check custom
custom id={props.id || props.name}
id={props.id || props.name} inline={inline}
> >
<BootstrapForm.Check.Input <BootstrapForm.Check.Input
{...field} {...props} type='checkbox' onChange={(e) => { {...field} {...props} type='checkbox' onChange={(e) => {
field.onChange(e) field.onChange(e)
handleChange && handleChange(e.target.checked) handleChange && handleChange(e.target.checked)
}} }}
/> />
<BootstrapForm.Check.Label className='d-flex'> <BootstrapForm.Check.Label className='d-flex'>
<div className='flex-grow-1'>{label}</div> <div className='flex-grow-1'>{label}</div>
{extra && {extra &&
<div className={styles.checkboxExtra}> <div className={styles.checkboxExtra}>
{extra} {extra}
</div>} </div>}
</BootstrapForm.Check.Label> </BootstrapForm.Check.Label>
</BootstrapForm.Check> </BootstrapForm.Check>
{children}
</div>
) )
} }

View File

@ -113,7 +113,7 @@ export default function UserHeader ({ user }) {
</div> </div>
<Nav <Nav
className={styles.nav} className={styles.nav}
activeKey={router.asPath} activeKey={router.asPath.split('?')[0]}
> >
<Nav.Item> <Nav.Item>
<Link href={'/' + user.name} passHref> <Link href={'/' + user.name} passHref>
@ -132,8 +132,8 @@ export default function UserHeader ({ user }) {
</Nav.Item> </Nav.Item>
{isMe && {isMe &&
<Nav.Item> <Nav.Item>
<Link href='/satistics' passHref> <Link href='/satistics?inc=invoice,withdrawal,stacked,spent' passHref>
<Nav.Link>satistics</Nav.Link> <Nav.Link eventKey='/satistics'>satistics</Nav.Link>
</Link> </Link>
</Nav.Item>} </Nav.Item>}
</Nav> </Nav>

View File

@ -25,10 +25,11 @@ export const WITHDRAWL = gql`
}` }`
export const WALLET_HISTORY = gql` export const WALLET_HISTORY = gql`
query WalletHistory($cursor: String) { query WalletHistory($cursor: String, $inc: String) {
walletHistory(cursor: $cursor) { walletHistory(cursor: $cursor, inc: $inc) {
facts { facts {
id id
factId
type type
createdAt createdAt
msats msats

View File

@ -66,7 +66,7 @@ export default function getApolloClient () {
} }
}, },
walletHistory: { walletHistory: {
keyArgs: false, keyArgs: ['inc'],
merge (existing, incoming) { merge (existing, incoming) {
if (isFirstPage(incoming.cursor, existing?.facts)) { if (isFirstPage(incoming.cursor, existing?.facts)) {
return incoming return incoming

View File

@ -12,6 +12,8 @@ import styles from '../styles/satistics.module.css'
import Moon from '../svgs/moon-fill.svg' import Moon from '../svgs/moon-fill.svg'
import Check from '../svgs/check-double-line.svg' import Check from '../svgs/check-double-line.svg'
import ThumbDown from '../svgs/thumb-down-fill.svg' import ThumbDown from '../svgs/thumb-down-fill.svg'
import { Checkbox, Form } from '../components/form'
import { useRouter } from 'next/router'
export const getServerSideProps = getGetServerSideProps(WALLET_HISTORY) export const getServerSideProps = getGetServerSideProps(WALLET_HISTORY)
@ -83,7 +85,29 @@ function Satus ({ status }) {
export default function Satistics ({ data: { walletHistory: { facts, cursor } } }) { export default function Satistics ({ data: { walletHistory: { facts, cursor } } }) {
const me = useMe() const me = useMe()
const { value: darkMode } = useDarkMode() const { value: darkMode } = useDarkMode()
const { data, fetchMore } = useQuery(WALLET_HISTORY) const router = useRouter()
const { data, fetchMore } = useQuery(WALLET_HISTORY, { variables: { inc: router.query.inc } })
console.log(router.query.inc, data)
function filterRoutePush (filter, add) {
const inc = new Set(router.query.inc.split(','))
inc.delete('')
// depending on addrem, add or remove filter
if (add) {
inc.add(filter)
} else {
inc.delete(filter)
}
const incstr = [...inc].join(',')
router.push(`/satistics?inc=${incstr}`)
}
function included (filter) {
const inc = new Set(router.query.inc.split(','))
return inc.has(filter)
}
if (data) { if (data) {
({ walletHistory: { facts, cursor } } = data) ({ walletHistory: { facts, cursor } } = data)
@ -97,32 +121,61 @@ export default function Satistics ({ data: { walletHistory: { facts, cursor } }
return ( return (
<Layout noSeo> <Layout noSeo>
<UserHeader user={me} /> <UserHeader user={me} />
<Table className='mt-3 mb-0' bordered hover size='sm' variant={darkMode ? 'dark' : undefined}> <div className='mt-3'>
<thead> <Form
<tr> initial={{
<th className={styles.type}>type</th> invoice: included('invoice'),
<th>detail</th> withdrawal: included('withdrawal'),
<th className={styles.sats}>sats</th> stacked: included('stacked'),
</tr> spent: included('spent')
</thead> }}
<tbody> >
{facts.map((f, i) => ( <div className='d-flex justify-content-around flex-wrap'>
<Link href={`${f.type}s/${f.id}`} key={`${f.type}-${f.id}`}> <Checkbox
<tr className={styles.row}> label='invoice' name='invoice' inline
<td className={`${styles.type} ${satusClass(f.status)}`}>{f.type}</td> handleChange={c => filterRoutePush('invoice', c)}
<td className={styles.description}> />
<div className={satusClass(f.status)}> <Checkbox
{f.description || 'no description'} label='withdrawal' name='withdrawal' inline
</div> handleChange={c => filterRoutePush('withdrawal', c)}
<Satus status={f.status} /> />
</td> <Checkbox
<td className={`${styles.sats} ${satusClass(f.status)}`}>{f.msats / 1000}</td> label='stacked' name='stacked' inline
</tr> handleChange={c => filterRoutePush('stacked', c)}
</Link> />
))} <Checkbox
</tbody> label='spent' name='spent' inline
</Table> handleChange={c => filterRoutePush('spent', c)}
<MoreFooter cursor={cursor} fetchMore={fetchMore} Skeleton={SatisticsSkeleton} /> />
</div>
</Form>
<Table className='mt-3 mb-0' bordered hover size='sm' variant={darkMode ? 'dark' : undefined}>
<thead>
<tr>
<th className={styles.type}>type</th>
<th>detail</th>
<th className={styles.sats}>sats</th>
</tr>
</thead>
<tbody>
{facts.map((f, i) => (
<Link href={`${f.type}s/${f.factId}`} key={f.id}>
<tr className={styles.row}>
<td className={`${styles.type} ${satusClass(f.status)}`}>{f.type}</td>
<td className={styles.description}>
<div className={satusClass(f.status)}>
{f.description || 'no description'}
</div>
<Satus status={f.status} />
</td>
<td className={`${styles.sats} ${satusClass(f.status)}`}>{f.msats / 1000}</td>
</tr>
</Link>
))}
</tbody>
</Table>
<MoreFooter cursor={cursor} fetchMore={fetchMore} Skeleton={SatisticsSkeleton} />
</div>
</Layout> </Layout>
) )
} }

View File

@ -64,6 +64,10 @@ $tooltip-bg: #5c8001;
line-height: 1.2rem; line-height: 1.2rem;
} }
.custom-checkbox.custom-control-inline {
margin-right: .5rem;
}
.table { .table {
color: var(--theme-color); color: var(--theme-color);
background-color: var(--theme-body); background-color: var(--theme-body);