Skip to content

Commit

Permalink
#268 - (feature/bills-tab-home)
Browse files Browse the repository at this point in the history
  • Loading branch information
daanvanberkel authored Apr 11, 2024
1 parent 8244cc5 commit c38224e
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 0 deletions.
74 changes: 74 additions & 0 deletions src/components/Screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { LinearGradient } from 'expo-linear-gradient';
import { RootDispatch, RootState } from '../../store';
import translate from '../../i18n/locale';
import { localNumberFormat, useThemeColors } from '../../lib/common';
import BillListItem from '../UI/Bills/BillListItem';

import Pagination from '../UI/Pagination';
import {
Expand Down Expand Up @@ -244,6 +245,76 @@ function InsightBudgets() {
);
}

function Bills() {
const { colors } = useThemeColors();

const billsSummary = useSelector((state: RootState) => state.firefly.bills);
const bills = useSelector((state: RootState) => state.bills.bills);
const loading = useSelector((state: RootState) => state.loading.effects.bills?.getBills?.loading);
const dispatch = useDispatch<RootDispatch>();

const totalPaid = useMemo(() => parseFloat(billsSummary?.paid?.monetaryValue || '0'), [billsSummary]);
const totalUnpaid = useMemo(() => Math.abs(parseFloat(billsSummary?.unpaid?.monetaryValue || '0')), [billsSummary]);
const total = useMemo(() => totalPaid + totalUnpaid, [totalPaid, totalUnpaid]);

return (
<AScrollView
showsVerticalScrollIndicator={false}
refreshControl={(
<RefreshControl
refreshing={false}
onRefresh={() => dispatch.bills.getBills()}
/>
)}
>
<AStack row>
<AText fontSize={25} lineHeight={27} style={{ margin: 15, flex: 1 }} bold>
{translate('home_bills')}
</AText>

{total !== 0 && (
<AStack
px={6}
py={2}
mx={15}
backgroundColor={colors.brandSuccessLight}
style={{ borderRadius: 5 }}
>
<AText
fontSize={15}
numberOfLines={1}
color={colors.brandSuccess}
style={{ textAlign: 'center' }}
bold
>
{`${((totalPaid / total) * 100).toFixed(0)}%`}
</AText>
</AStack>
)}
</AStack>

{total !== 0 && (
<AStack mx={15} justifyContent="flex-start">
<AProgressBar
color={colors.green}
value={(totalPaid / total) * 100}
/>
</AStack>
)}

{bills.map((bill, index) => (
<BillListItem
key={bill.id}
bill={bill}
loading={loading}
lastItem={index + 1 === bills.length}
/>
))}
<AView style={{ height: 150 }} />
</AScrollView>
);
}

function NetWorth() {
const { colors } = useThemeColors();
const hideBalance = useSelector((state: RootState) => state.configuration.hideBalance);
Expand Down Expand Up @@ -338,6 +409,7 @@ export default function HomeScreen() {
<Ionicons key="wallet" name="wallet" size={22} color={colors.text} />,
<Ionicons key="pricetag" name="pricetags" size={22} color={colors.text} />,
<MaterialCommunityIcons key="progress-check" name="progress-check" size={22} color={colors.text} />,
<Ionicons key="calendar-clear" name="calendar-clear" size={22} color={colors.text} />,
];

useEffect(() => {
Expand Down Expand Up @@ -365,6 +437,7 @@ export default function HomeScreen() {
dispatch.accounts.getAccounts();
dispatch.categories.getInsightCategories();
dispatch.budgets.getInsightBudgets();
dispatch.bills.getBills();
}
};

Expand Down Expand Up @@ -437,6 +510,7 @@ export default function HomeScreen() {
<AssetsAccounts key="1" />
<InsightCategories key="2" />
<InsightBudgets key="3" />
<Bills key="4" />
</AnimatedPagerView>
</AView>
</View>
Expand Down
82 changes: 82 additions & 0 deletions src/components/UI/Bills/BillListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import moment from 'moment/moment';
import React, { useMemo } from 'react';
import translate from '../../../i18n/locale';
import { useThemeColors } from '../../../lib/common';
import { BillType } from '../../../models/bills';
import { ASkeleton, AStack, AText } from '../ALibrary';

type Props = {
bill: BillType;
loading: boolean;
lastItem: boolean;
}

export default function BillListItem({ bill, loading, lastItem }: Props) {
const { colors } = useThemeColors();
const paidDate = useMemo(() => bill.attributes.paidDates[0]?.date || null, [bill]);

const statusColor = useMemo(() => {
// The bill is paid
if (paidDate !== null) {
return colors.green;
}

// The bill is not expected in the current period
if (bill.attributes.nextExpectedMatch === null) {
return colors.brandStyle4;
}

// The bill should be paid by now
if (moment(bill.attributes.nextExpectedMatch).diff(moment(), 'days') < 0) {
return colors.brandWarning;
}

// The expected date is in the future
return undefined;
}, [paidDate]);

const billContent = useMemo(() => {
if (paidDate !== null) {
return translate('bills_paid');
}

if (bill.attributes.nextExpectedMatch === null) {
return translate('bills_not_expected');
}

return moment(bill.attributes.nextExpectedMatch).format('ll');
}, [bill, paidDate]);

return (
<AStack
key={bill.id}
row
mx={15}
style={{
height: 45,
borderColor: colors.listBorderColor,
borderBottomWidth: lastItem ? 0 : 0.5,
}}
justifyContent="space-between"
>
<AText
fontSize={14}
maxWidth="60%"
numberOfLines={1}
>
{bill.attributes.name}
</AText>

<ASkeleton loading={loading}>
<AText
fontSize={14}
maxWidth={100}
numberOfLines={1}
color={statusColor}
>
{billContent}
</AText>
</ASkeleton>
</AStack>
);
}
4 changes: 4 additions & 0 deletions src/i18n/locale/translations/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,8 @@ export default {
configuration_logout_alert_title: 'Logout',
load_more: 'Load more',

// from X.X.X
home_bills: 'Bills',
bills_paid: 'Paid',
bills_not_expected: 'Not expected',
};
82 changes: 82 additions & 0 deletions src/models/bills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { createModel } from '@rematch/core';
import moment from 'moment/moment';
import { RootModel } from './index';

export type BillType = {
id: string,
type: string,
attributes: {
name: string,
active: boolean;
amountMin: string,
amountMax: string,
currencyId: string,
currencyCode: string,
currencySymbol: string,
nextExpectedMatch: string | null,
payDates: string[],
paidDates: {
transactionGroupId: string,
transactionJournalId: string,
date: string,
}[],
},
}

type BillsStateType = {
bills: BillType[],
}

const INITIAL_STATE = {
bills: [],
} as BillsStateType;

export default createModel<RootModel>()({

state: INITIAL_STATE,

reducers: {
setBills(state, payload): BillsStateType {
const {
bills = state.bills,
} = payload;

return {
...state,
bills,
};
},

resetState() {
return INITIAL_STATE;
},
},

effects: (dispatch) => ({
/**
* Get bills
*
* @returns {Promise}
*/
async getBills(_: void, rootState): Promise<void> {
const {
firefly: {
rangeDetails: {
start,
end,
},
},
} = rootState;

const { data: bills } = await dispatch.configuration.apiFetch({ url: `/api/v1/bills?start=${start}&end=${end}` }) as { data: BillType[] };

const sortedBills = [...bills]
// Order by next expected date
.sort((a, b) => moment(a.attributes.nextExpectedMatch).diff(moment(b.attributes.nextExpectedMatch)))
// Make sure all paid bills are at the end of the list
.sort((a, b) => a.attributes.paidDates.length - b.attributes.paidDates.length);

dispatch.bills.setBills({ bills: sortedBills });
},
}),
});
12 changes: 12 additions & 0 deletions src/models/firefly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export type FireflyStateType = {
earned: HomeDisplayType[],
balance: HomeDisplayType[],
accounts: AssetAccountType[],
bills: { paid: HomeDisplayType | null, unpaid: HomeDisplayType | null },
earnedChart: { x: number, y: number }[],
spentChart:{ x: number, y: number }[],
}
Expand Down Expand Up @@ -102,6 +103,7 @@ const INITIAL_STATE = {
accounts: [],
earnedChart: [],
spentChart: [],
bills: { paid: null, unpaid: null },
} as FireflyStateType;

export default createModel<RootModel>()({
Expand All @@ -118,6 +120,7 @@ export default createModel<RootModel>()({
accounts = state.accounts,
earnedChart = state.earnedChart,
spentChart = state.spentChart,
bills = state.bills,
} = payload;

return {
Expand All @@ -129,6 +132,7 @@ export default createModel<RootModel>()({
accounts,
earnedChart,
spentChart,
bills,
};
},

Expand Down Expand Up @@ -242,6 +246,7 @@ export default createModel<RootModel>()({
const balance = [];
const earned = [];
const spent = [];
const bills = { paid: null, unpaid: null };
Object.keys(summary).forEach((key) => {
if (key.includes('net-worth-in')) {
netWorth.push(summary[key]);
Expand All @@ -255,13 +260,20 @@ export default createModel<RootModel>()({
if (key.includes('spent-in')) {
spent.push(summary[key]);
}
if (key.includes('bills-paid-in')) {
bills.paid = summary[key];
}
if (key.includes('bills-unpaid-in')) {
bills.unpaid = summary[key];
}
});

dispatch.firefly.setData({
netWorth,
balance,
earned,
spent,
bills,
});
}
},
Expand Down
3 changes: 3 additions & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Models } from '@rematch/core';
import accounts from './accounts';
import bills from './bills';
import budgets from './budgets';
import categories from './categories';
import configuration from './configuration';
Expand All @@ -9,6 +10,7 @@ import transactions from './transactions';

export interface RootModel extends Models<RootModel> {
accounts: typeof accounts
bills: typeof bills
budgets: typeof budgets
categories: typeof categories
configuration: typeof configuration
Expand All @@ -19,6 +21,7 @@ export interface RootModel extends Models<RootModel> {

export const models: RootModel = {
accounts,
bills,
budgets,
categories,
configuration,
Expand Down

0 comments on commit c38224e

Please sign in to comment.