mirror of
https://github.com/apotdevin/thunderhub.git
synced 2025-02-22 14:22:33 +01:00
feat: ✨ accounting (#75)
* feat: ✨ accounting * chore: 🔧 accounting params * chore: 🔧 remove log * chore: 🔧 remove null year * chore: 🔧 disabled payment report * chore: 🔧 rebalance filters
This commit is contained in:
parent
4b9a568e63
commit
9a860ee141
13 changed files with 448 additions and 9 deletions
|
@ -2,12 +2,14 @@ import React from 'react';
|
|||
import { GridWrapper } from 'src/components/gridWrapper/GridWrapper';
|
||||
import { withApollo } from 'config/client';
|
||||
import { Bakery } from 'src/views/tools/bakery/Bakery';
|
||||
import { Accounting } from 'src/views/tools/accounting/Accounting';
|
||||
import { BackupsView } from '../src/views/tools/backups/Backups';
|
||||
import { MessagesView } from '../src/views/tools/messages/Messages';
|
||||
import { WalletVersion } from '../src/views/tools/WalletVersion';
|
||||
|
||||
const ToolsView = () => (
|
||||
<>
|
||||
<Accounting />
|
||||
<BackupsView />
|
||||
<MessagesView />
|
||||
<Bakery />
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { ContextType } from 'server/types/apiTypes';
|
||||
import { getLnd } from 'server/helpers/helpers';
|
||||
import { rebalance } from 'balanceofsatoshis/swaps';
|
||||
import { to } from 'server/helpers/async';
|
||||
import { logger } from 'server/helpers/logger';
|
||||
import { AuthType } from 'src/context/AccountContext';
|
||||
|
||||
import { rebalance } from 'balanceofsatoshis/swaps';
|
||||
import { getAccountingReport } from 'balanceofsatoshis/balances';
|
||||
import request from '@alexbosworth/request';
|
||||
|
||||
type RebalanceType = {
|
||||
auth: AuthType;
|
||||
avoid?: String[];
|
||||
|
@ -19,21 +22,79 @@ type RebalanceType = {
|
|||
target?: Number;
|
||||
};
|
||||
|
||||
type AccountingType = {
|
||||
auth: AuthType;
|
||||
category?: String;
|
||||
currency?: String;
|
||||
fiat?: String;
|
||||
month?: String;
|
||||
year?: String;
|
||||
};
|
||||
|
||||
export const bosResolvers = {
|
||||
Query: {
|
||||
getAccountingReport: async (
|
||||
_: undefined,
|
||||
params: AccountingType,
|
||||
context: ContextType
|
||||
) => {
|
||||
const { auth, ...settings } = params;
|
||||
const lnd = getLnd(auth, context);
|
||||
|
||||
const response = await to(
|
||||
getAccountingReport({
|
||||
lnd,
|
||||
logger,
|
||||
request,
|
||||
is_csv: true,
|
||||
...settings,
|
||||
})
|
||||
);
|
||||
|
||||
return response;
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
bosRebalance: async (
|
||||
_: undefined,
|
||||
params: RebalanceType,
|
||||
context: ContextType
|
||||
) => {
|
||||
const { auth, ...extraparams } = params;
|
||||
const {
|
||||
auth,
|
||||
avoid,
|
||||
in_through,
|
||||
is_avoiding_high_inbound,
|
||||
max_fee,
|
||||
max_fee_rate,
|
||||
max_rebalance,
|
||||
node,
|
||||
out_channels,
|
||||
out_through,
|
||||
target,
|
||||
} = params;
|
||||
const lnd = getLnd(auth, context);
|
||||
|
||||
const filteredParams = {
|
||||
...(avoid.length > 0 && { avoid }),
|
||||
...(in_through && { in_through }),
|
||||
...(is_avoiding_high_inbound && { is_avoiding_high_inbound }),
|
||||
...(max_fee > 0 && { max_fee }),
|
||||
...(max_fee_rate > 0 && { max_fee_rate }),
|
||||
...(max_rebalance > 0 && { max_rebalance }),
|
||||
...(node && { node }),
|
||||
...(out_channels.length > 0 && { out_channels }),
|
||||
...(out_through && { out_through }),
|
||||
...(target && { target }),
|
||||
};
|
||||
|
||||
logger.info('Rebalance Params: %o', filteredParams);
|
||||
|
||||
const response = await to(
|
||||
rebalance({
|
||||
lnd,
|
||||
logger,
|
||||
...extraparams,
|
||||
...filteredParams,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -36,6 +36,14 @@ export const generalTypes = gql`
|
|||
|
||||
export const queryTypes = gql`
|
||||
type Query {
|
||||
getAccountingReport(
|
||||
auth: authType!
|
||||
category: String
|
||||
currency: String
|
||||
fiat: String
|
||||
month: String
|
||||
year: String
|
||||
): String!
|
||||
getVolumeHealth(auth: authType!): channelsHealth
|
||||
getTimeHealth(auth: authType!): channelsTimeHealth
|
||||
getFeeHealth(auth: authType!): channelsFeeHealth
|
||||
|
|
3
server/tests/__mocks__/balanceofsatoshis/balances.ts
Normal file
3
server/tests/__mocks__/balanceofsatoshis/balances.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const getAccountingReport = jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({}));
|
|
@ -21,6 +21,7 @@ const StyledSingleButton = styled.button<StyledSingleProps>`
|
|||
background-color: transparent;
|
||||
color: ${multiSelectColor};
|
||||
flex-grow: 1;
|
||||
transition: background-color 0.5s ease;
|
||||
|
||||
${({ selected, buttonColor }) =>
|
||||
selected
|
||||
|
|
92
src/graphql/queries/__generated__/getAccountingReport.generated.tsx
generated
Normal file
92
src/graphql/queries/__generated__/getAccountingReport.generated.tsx
generated
Normal file
|
@ -0,0 +1,92 @@
|
|||
import gql from 'graphql-tag';
|
||||
import * as ApolloReactCommon from '@apollo/react-common';
|
||||
import * as ApolloReactHooks from '@apollo/react-hooks';
|
||||
import * as Types from '../../types';
|
||||
|
||||
export type GetAccountingReportQueryVariables = Types.Exact<{
|
||||
auth: Types.AuthType;
|
||||
category?: Types.Maybe<Types.Scalars['String']>;
|
||||
currency?: Types.Maybe<Types.Scalars['String']>;
|
||||
fiat?: Types.Maybe<Types.Scalars['String']>;
|
||||
month?: Types.Maybe<Types.Scalars['String']>;
|
||||
year?: Types.Maybe<Types.Scalars['String']>;
|
||||
}>;
|
||||
|
||||
export type GetAccountingReportQuery = { __typename?: 'Query' } & Pick<
|
||||
Types.Query,
|
||||
'getAccountingReport'
|
||||
>;
|
||||
|
||||
export const GetAccountingReportDocument = gql`
|
||||
query GetAccountingReport(
|
||||
$auth: authType!
|
||||
$category: String
|
||||
$currency: String
|
||||
$fiat: String
|
||||
$month: String
|
||||
$year: String
|
||||
) {
|
||||
getAccountingReport(
|
||||
auth: $auth
|
||||
category: $category
|
||||
currency: $currency
|
||||
fiat: $fiat
|
||||
month: $month
|
||||
year: $year
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetAccountingReportQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetAccountingReportQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetAccountingReportQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetAccountingReportQuery({
|
||||
* variables: {
|
||||
* auth: // value for 'auth'
|
||||
* category: // value for 'category'
|
||||
* currency: // value for 'currency'
|
||||
* fiat: // value for 'fiat'
|
||||
* month: // value for 'month'
|
||||
* year: // value for 'year'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetAccountingReportQuery(
|
||||
baseOptions?: ApolloReactHooks.QueryHookOptions<
|
||||
GetAccountingReportQuery,
|
||||
GetAccountingReportQueryVariables
|
||||
>
|
||||
) {
|
||||
return ApolloReactHooks.useQuery<
|
||||
GetAccountingReportQuery,
|
||||
GetAccountingReportQueryVariables
|
||||
>(GetAccountingReportDocument, baseOptions);
|
||||
}
|
||||
export function useGetAccountingReportLazyQuery(
|
||||
baseOptions?: ApolloReactHooks.LazyQueryHookOptions<
|
||||
GetAccountingReportQuery,
|
||||
GetAccountingReportQueryVariables
|
||||
>
|
||||
) {
|
||||
return ApolloReactHooks.useLazyQuery<
|
||||
GetAccountingReportQuery,
|
||||
GetAccountingReportQueryVariables
|
||||
>(GetAccountingReportDocument, baseOptions);
|
||||
}
|
||||
export type GetAccountingReportQueryHookResult = ReturnType<
|
||||
typeof useGetAccountingReportQuery
|
||||
>;
|
||||
export type GetAccountingReportLazyQueryHookResult = ReturnType<
|
||||
typeof useGetAccountingReportLazyQuery
|
||||
>;
|
||||
export type GetAccountingReportQueryResult = ApolloReactCommon.QueryResult<
|
||||
GetAccountingReportQuery,
|
||||
GetAccountingReportQueryVariables
|
||||
>;
|
21
src/graphql/queries/getAccountingReport.ts
Normal file
21
src/graphql/queries/getAccountingReport.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import gql from 'graphql-tag';
|
||||
|
||||
export const GET_ACCOUNTING_REPORT = gql`
|
||||
query GetAccountingReport(
|
||||
$auth: authType!
|
||||
$category: String
|
||||
$currency: String
|
||||
$fiat: String
|
||||
$month: String
|
||||
$year: String
|
||||
) {
|
||||
getAccountingReport(
|
||||
auth: $auth
|
||||
category: $category
|
||||
currency: $currency
|
||||
fiat: $fiat
|
||||
month: $month
|
||||
year: $year
|
||||
)
|
||||
}
|
||||
`;
|
|
@ -45,6 +45,7 @@ export type PermissionsType = {
|
|||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
getAccountingReport: Scalars['String'];
|
||||
getVolumeHealth?: Maybe<ChannelsHealth>;
|
||||
getTimeHealth?: Maybe<ChannelsTimeHealth>;
|
||||
getFeeHealth?: Maybe<ChannelsFeeHealth>;
|
||||
|
@ -90,6 +91,15 @@ export type Query = {
|
|||
getLatestVersion?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type QueryGetAccountingReportArgs = {
|
||||
auth: AuthType;
|
||||
category?: Maybe<Scalars['String']>;
|
||||
currency?: Maybe<Scalars['String']>;
|
||||
fiat?: Maybe<Scalars['String']>;
|
||||
month?: Maybe<Scalars['String']>;
|
||||
year?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type QueryGetVolumeHealthArgs = {
|
||||
auth: AuthType;
|
||||
};
|
||||
|
|
|
@ -93,12 +93,16 @@ export const getPercent = (
|
|||
return Math.round(percent);
|
||||
};
|
||||
|
||||
export const saveToPc = (jsonData: string, filename: string) => {
|
||||
export const saveToPc = (
|
||||
jsonData: string,
|
||||
filename: string,
|
||||
isCsv?: boolean
|
||||
) => {
|
||||
const fileData = jsonData;
|
||||
const blob = new Blob([fileData], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.download = `${filename}.txt`;
|
||||
link.download = isCsv ? `${filename}.csv` : `${filename}.txt`;
|
||||
link.href = url;
|
||||
link.click();
|
||||
};
|
||||
|
|
|
@ -231,7 +231,7 @@ export const AdvancedBalance = () => {
|
|||
{hasAvoid ? <Minus size={18} /> : <Plus size={18} />}
|
||||
</ColorButton>
|
||||
</SettingLine>
|
||||
<SettingLine title={'In Through Channel'}>
|
||||
<SettingLine title={'Decrease Inbound Of'}>
|
||||
{hasInChannel ? (
|
||||
<RebalanceTag>{state.in_through.alias}</RebalanceTag>
|
||||
) : null}
|
||||
|
@ -247,7 +247,7 @@ export const AdvancedBalance = () => {
|
|||
</ColorButton>
|
||||
</SettingLine>
|
||||
{!hasOutChannels && (
|
||||
<SettingLine title={'Out Through Channel'}>
|
||||
<SettingLine title={'Increase Inbound Of'}>
|
||||
{hasOutChannel ? (
|
||||
<RebalanceTag>{state.out_through.alias}</RebalanceTag>
|
||||
) : null}
|
||||
|
@ -427,7 +427,14 @@ export const AdvancedBalance = () => {
|
|||
</BetaNotification>
|
||||
<InputWithDeco title={'Type'} noInput={true}>
|
||||
<MultiButton>
|
||||
{renderButton(() => isDetailedSet(false), 'Auto', !isDetailed)}
|
||||
{renderButton(
|
||||
() => {
|
||||
dispatch({ type: 'clearFilters' });
|
||||
isDetailedSet(false);
|
||||
},
|
||||
'Auto',
|
||||
!isDetailed
|
||||
)}
|
||||
{renderButton(() => isDetailedSet(true), 'Detailed', isDetailed)}
|
||||
</MultiButton>
|
||||
</InputWithDeco>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import styled from 'styled-components';
|
||||
import { ResponsiveLine } from 'src/components/generic/Styled';
|
||||
|
||||
export const NoWrap = styled.div`
|
||||
margin-right: 16px;
|
||||
|
@ -22,3 +23,7 @@ export const Column = styled.div`
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const ToolsResponsiveLine = styled(ResponsiveLine)`
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
Card,
|
||||
Sub4Title,
|
||||
Separation,
|
||||
DarkSubTitle,
|
||||
} from '../../components/generic/Styled';
|
||||
import { useStatusState } from '../../context/StatusContext';
|
||||
import { LoadingCard } from '../../components/loading/LoadingCard';
|
||||
|
@ -30,7 +31,10 @@ export const WalletVersion = () => {
|
|||
if (minorVersion < 10) {
|
||||
return (
|
||||
<Card>
|
||||
Update to LND version 0.10.0 or higher to see your wallet build info.
|
||||
<DarkSubTitle>
|
||||
Update to LND version 0.10.0 or higher to see your wallet build
|
||||
info.
|
||||
</DarkSubTitle>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
221
src/views/tools/accounting/Accounting.tsx
Normal file
221
src/views/tools/accounting/Accounting.tsx
Normal file
|
@ -0,0 +1,221 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
CardWithTitle,
|
||||
SubTitle,
|
||||
Card,
|
||||
SingleLine,
|
||||
DarkSubTitle,
|
||||
Separation,
|
||||
} from 'src/components/generic/Styled';
|
||||
import { useGetAccountingReportLazyQuery } from 'src/graphql/queries/__generated__/getAccountingReport.generated';
|
||||
import { useAccountState } from 'src/context/AccountContext';
|
||||
import { ColorButton } from 'src/components/buttons/colorButton/ColorButton';
|
||||
import {
|
||||
MultiButton,
|
||||
SingleButton,
|
||||
} from 'src/components/buttons/multiButton/MultiButton';
|
||||
import { X } from 'react-feather';
|
||||
import { saveToPc } from 'src/utils/helpers';
|
||||
import { ToolsResponsiveLine } from '../Tools.styled';
|
||||
|
||||
type ReportType =
|
||||
| 'chain-fees'
|
||||
| 'chain-receives'
|
||||
| 'chain-sends'
|
||||
| 'forwards'
|
||||
| 'invoices'
|
||||
| 'payments';
|
||||
// type FiatType = 'eur' | 'usd';
|
||||
type YearType = 2017 | 2018 | 2019 | 2020;
|
||||
type MonthType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | null;
|
||||
|
||||
type StateType = {
|
||||
type: ReportType;
|
||||
// fiat?: FiatType;
|
||||
year?: YearType;
|
||||
month?: MonthType;
|
||||
};
|
||||
|
||||
export type ActionType =
|
||||
| {
|
||||
type: 'type';
|
||||
report: ReportType;
|
||||
}
|
||||
// | {
|
||||
// type: 'fiat';
|
||||
// fiat: FiatType;
|
||||
// }
|
||||
| {
|
||||
type: 'year';
|
||||
year: YearType;
|
||||
}
|
||||
| {
|
||||
type: 'month';
|
||||
month: MonthType;
|
||||
};
|
||||
|
||||
const initialState: StateType = {
|
||||
type: 'invoices',
|
||||
// fiat: 'eur',
|
||||
year: 2020,
|
||||
month: null,
|
||||
};
|
||||
|
||||
const reducer = (state: StateType, action: ActionType): StateType => {
|
||||
switch (action.type) {
|
||||
case 'type':
|
||||
return { ...state, type: action.report };
|
||||
// case 'fiat':
|
||||
// return { ...state, fiat: action.fiat };
|
||||
case 'year':
|
||||
return { ...state, year: action.year };
|
||||
case 'month':
|
||||
return { ...state, month: action.month };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const Accounting = () => {
|
||||
const { auth } = useAccountState();
|
||||
const [showDetails, setShowDetails] = React.useState(false);
|
||||
const [state, dispatch] = React.useReducer(reducer, initialState);
|
||||
|
||||
const [getReport, { data, loading }] = useGetAccountingReportLazyQuery();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!loading && data && data.getAccountingReport) {
|
||||
saveToPc(
|
||||
data.getAccountingReport,
|
||||
`accounting-${state.type}-${state.year || ''}-${state.month || ''}`,
|
||||
true
|
||||
);
|
||||
}
|
||||
}, [data, loading]);
|
||||
|
||||
const reportButton = (report: ReportType, title: string) => (
|
||||
<SingleButton
|
||||
selected={state.type === report}
|
||||
onClick={() => !loading && dispatch({ type: 'type', report })}
|
||||
>
|
||||
{title}
|
||||
</SingleButton>
|
||||
);
|
||||
|
||||
// const fiatButton = (fiat: FiatType, title: string) => (
|
||||
// <SingleButton
|
||||
// selected={state.fiat === fiat}
|
||||
// onClick={() => !loading && dispatch({ type: 'fiat', fiat })}
|
||||
// >
|
||||
// {title}
|
||||
// </SingleButton>
|
||||
// );
|
||||
|
||||
const yearButton = (year: YearType) => (
|
||||
<SingleButton
|
||||
selected={state.year === year}
|
||||
onClick={() => !loading && dispatch({ type: 'year', year })}
|
||||
>
|
||||
{year}
|
||||
</SingleButton>
|
||||
);
|
||||
|
||||
const monthButton = (month: MonthType) => (
|
||||
<SingleButton
|
||||
selected={state.month === month}
|
||||
onClick={() => !loading && dispatch({ type: 'month', month })}
|
||||
>
|
||||
{month ? month : 'All'}
|
||||
</SingleButton>
|
||||
);
|
||||
|
||||
const renderDetails = () => (
|
||||
<>
|
||||
<Separation />
|
||||
<ToolsResponsiveLine>
|
||||
<DarkSubTitle>Type</DarkSubTitle>
|
||||
<MultiButton>
|
||||
{reportButton('chain-fees', 'Chain Fees')}
|
||||
{reportButton('chain-receives', 'Chain Received')}
|
||||
{reportButton('chain-sends', 'Chain Sent')}
|
||||
{reportButton('forwards', 'Forwards')}
|
||||
{/* {reportButton('payments', 'Payments')} */}
|
||||
{reportButton('invoices', 'Invoices')}
|
||||
</MultiButton>
|
||||
</ToolsResponsiveLine>
|
||||
{/* <ToolsResponsiveLine>
|
||||
<DarkSubTitle>Fiat</DarkSubTitle>
|
||||
<MultiButton>
|
||||
{fiatButton('eur', 'Euro')}
|
||||
{fiatButton('usd', 'US Dollar')}
|
||||
</MultiButton>
|
||||
</ToolsResponsiveLine> */}
|
||||
<ToolsResponsiveLine>
|
||||
<DarkSubTitle>Year</DarkSubTitle>
|
||||
<MultiButton>
|
||||
{yearButton(2017)}
|
||||
{yearButton(2018)}
|
||||
{yearButton(2019)}
|
||||
{yearButton(2020)}
|
||||
</MultiButton>
|
||||
</ToolsResponsiveLine>
|
||||
<ToolsResponsiveLine>
|
||||
<DarkSubTitle>Month</DarkSubTitle>
|
||||
<MultiButton>
|
||||
{monthButton(null)}
|
||||
{monthButton(1)}
|
||||
{monthButton(2)}
|
||||
{monthButton(3)}
|
||||
{monthButton(4)}
|
||||
{monthButton(5)}
|
||||
{monthButton(6)}
|
||||
{monthButton(7)}
|
||||
{monthButton(8)}
|
||||
{monthButton(9)}
|
||||
{monthButton(10)}
|
||||
{monthButton(11)}
|
||||
{monthButton(12)}
|
||||
</MultiButton>
|
||||
</ToolsResponsiveLine>
|
||||
<ColorButton
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
onClick={() =>
|
||||
getReport({
|
||||
variables: {
|
||||
auth,
|
||||
// fiat: state.fiat,
|
||||
category: state.type,
|
||||
year: state.year.toString(),
|
||||
...(state.month && { month: state.month.toString() }),
|
||||
},
|
||||
})
|
||||
}
|
||||
fullWidth={true}
|
||||
withMargin={'16px 0 0'}
|
||||
>
|
||||
Generate
|
||||
</ColorButton>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<CardWithTitle>
|
||||
<SubTitle>Accounting</SubTitle>
|
||||
<Card>
|
||||
<SingleLine>
|
||||
<DarkSubTitle>Report</DarkSubTitle>
|
||||
<ColorButton
|
||||
arrow={!showDetails}
|
||||
onClick={() =>
|
||||
showDetails ? setShowDetails(false) : setShowDetails(true)
|
||||
}
|
||||
>
|
||||
{showDetails ? <X size={18} /> : 'Create'}
|
||||
</ColorButton>
|
||||
</SingleLine>
|
||||
{showDetails && renderDetails()}
|
||||
</Card>
|
||||
</CardWithTitle>
|
||||
);
|
||||
};
|
Loading…
Add table
Reference in a new issue