-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/analytics #20
base: develop
Are you sure you want to change the base?
Changes from all commits
8e19458
4bdf872
bf8ffd1
5bf5817
00b6ab2
5103c2c
eb2d158
d107579
06a5dd4
29eef3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import { firestore } from '@Configs/Firebase'; | ||
|
||
export const billsCollection = firestore.collection('bills'); | ||
export const budgetsCollection = firestore.collection('budgets'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { useState, useEffect, useCallback } from 'react'; | ||
|
||
import { firestore } from '@Configs/Firebase'; | ||
import { useUser } from '@Authentication/context/UserContext'; | ||
import { BillTypes } from '@Modules/Bill/constants/Types'; | ||
import { billsCollection } from '@Configs/collections.config'; | ||
|
||
export interface IBill { | ||
id?: string; | ||
name: string; | ||
description?: string; | ||
value: number; | ||
type: 'INCOME' | 'EXPENSE'; | ||
status: string; | ||
dueDate: number; | ||
createdAt: number; | ||
userId: string; | ||
category?: string; | ||
} | ||
interface UseBillsOptions { | ||
type?: string; | ||
} | ||
export const useBills = (options?: UseBillsOptions) => { | ||
const [bills, setBills] = useState<IBill[]>([]); | ||
const { userId } = useUser(); | ||
const fetchBills = useCallback(async () => { | ||
if (!userId) return; | ||
|
||
const data = await billsCollection.where('userId', '==', userId).get(); | ||
const docs = data.docs.map( | ||
category => | ||
({ | ||
id: category.id, | ||
...category.data(), | ||
} as IBill) | ||
); | ||
|
||
const processedDocs = docs.filter(item => (options?.type ? item.type === options.type : true)); | ||
|
||
setBills(processedDocs); | ||
}, [setBills, userId]); | ||
|
||
useEffect(() => { | ||
fetchBills(); | ||
}, [fetchBills]); | ||
|
||
return bills; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import dynamic from 'next/dynamic'; | ||
|
||
// @ts-ignore | ||
const ResponsivePieCanvas = dynamic(() => import('@nivo/pie').then(m => m.ResponsivePieCanvas), { ssr: false }); | ||
|
||
import { Text, Box as ChackraBox, Tag } from '@chakra-ui/react'; | ||
import { useMemo } from 'react'; | ||
|
||
import { Box } from '@Components'; | ||
import { ICategory } from '@/hooks/useCategories'; | ||
import { IBill } from '@/hooks/useBills'; | ||
import { BillTypes } from '@Modules/Bill/constants/Types'; | ||
|
||
interface CategoriesCurrentMonthProps { | ||
bills: IBill[]; | ||
categories: ICategory[]; | ||
} | ||
|
||
const keyToText = { | ||
rest: 'Retante', | ||
income: 'Receita', | ||
expense: 'Despesa', | ||
}; | ||
|
||
export const CategoriesCurrentMonth = ({ bills, categories }: CategoriesCurrentMonthProps) => { | ||
const graphData = useMemo(() => { | ||
return bills?.map(bill => { | ||
return { | ||
value: bill.value, | ||
id: categories?.find(({ id }) => id === bill.category)?.name || 'Outro', | ||
}; | ||
}); | ||
}, [bills, categories]); | ||
|
||
const data = [ | ||
{ | ||
id: 'Cartão de crédito', | ||
value: 440, | ||
}, | ||
{ | ||
id: 'Investimento', | ||
value: 251, | ||
}, | ||
{ | ||
id: 'Outros', | ||
value: 464, | ||
}, | ||
]; | ||
return ( | ||
<Box bg="gray.300"> | ||
<Text fontSize="lg" fontWeight="bold" color="gray.600"> | ||
Gastos por categoria | ||
</Text> | ||
<ChackraBox height="400px"> | ||
{/* @ts-ignore */} | ||
<ResponsivePieCanvas | ||
// @ts-ignore | ||
data={graphData} | ||
margin={{ top: 10, right: 50, bottom: 80, left: 50 }} | ||
innerRadius={0.5} | ||
padAngle={0.7} | ||
cornerRadius={3} | ||
activeOuterRadiusOffset={8} | ||
borderWidth={1} | ||
borderColor={{ | ||
from: 'color', | ||
modifiers: [['darker', 0.2]], | ||
}} | ||
arcLinkLabelsSkipAngle={10} | ||
arcLinkLabelsTextColor="#333333" | ||
arcLinkLabelsThickness={2} | ||
arcLinkLabelsColor={{ from: 'color' }} | ||
arcLabelsSkipAngle={10} | ||
arcLabelsTextColor={{ | ||
from: 'color', | ||
modifiers: [['darker', 2]], | ||
}} | ||
tooltip={() => <></>} | ||
/> | ||
</ChackraBox> | ||
</Box> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import dynamic from 'next/dynamic'; | ||
|
||
// @ts-ignore | ||
const ResponsiveBarCanvas = dynamic(() => import('@nivo/bar').then(m => m.ResponsiveBarCanvas), { ssr: false }); | ||
|
||
import { Text, Box as ChackraBox, Tag } from '@chakra-ui/react'; | ||
import { useMemo } from 'react'; | ||
|
||
import { Box } from '@Components'; | ||
import { IBill } from '@/hooks/useBills'; | ||
import { BillTypes } from '@Modules/Bill/constants/Types'; | ||
|
||
interface MonthByMonthProps { | ||
bills: IBill[]; | ||
} | ||
|
||
const keyToText = { | ||
rest: 'Retante', | ||
income: 'Receita', | ||
expense: 'Despesa', | ||
}; | ||
|
||
export const MonthByMonth = ({ bills }: MonthByMonthProps) => { | ||
const data = useMemo(() => { | ||
const billsReduce = bills.reduce((prev, curr) => { | ||
const dueDate = new Date(curr.dueDate); | ||
const month = `${dueDate.getMonth() + 1}/${dueDate.getFullYear()}`; | ||
|
||
if (!prev[month]) { | ||
prev[month] = {}; | ||
|
||
if (curr.type === BillTypes.EXPENSE) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From what I understand, this first check is to see if there are already values for that specific month/year, if not yet, I put the curr values as default If my understanding is right This part is not necessary if you initialize the Because in the next if you check if If not, do: Thus, repeating the code So if the values are initialized with zero, the += operation will work correctly and for cases where the information in the month/year did not yet exist, it becomes the curr value, exactly what the first if does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if (!prev[month]) {
prev[month] = {
expense: 0,
income: 0,
month,
rest: 0
};
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But thinking here, there depending on whether the value of as the prev for this so it's as if it did a |
||
prev[month] = { | ||
expense: curr.value, | ||
income: 0, | ||
month, | ||
}; | ||
} else { | ||
prev[month] = { | ||
expense: 0, | ||
income: curr.value, | ||
month, | ||
}; | ||
} | ||
|
||
prev[month].rest = prev[month].income - prev[month].expense; | ||
} | ||
|
||
if (prev?.[month]) { | ||
if (curr.type === BillTypes.EXPENSE) { | ||
prev[month].expense += curr.value; | ||
} else { | ||
prev[month].income += curr.value; | ||
} | ||
|
||
prev[month].rest = prev[month].income - prev[month].expense; | ||
} | ||
|
||
return prev; | ||
}, {}); | ||
|
||
return Object.values(billsReduce); | ||
}, [bills]); | ||
|
||
return ( | ||
<Box bg="gray.300"> | ||
<Text fontSize="lg" fontWeight="bold" color="gray.600"> | ||
Últimos meses | ||
</Text> | ||
<ChackraBox height="400px"> | ||
{/* @ts-ignore */} | ||
<ResponsiveBarCanvas | ||
// @ts-ignore | ||
data={data} | ||
keys={['income', 'expense', 'rest']} | ||
indexBy="month" | ||
margin={{ top: 10, right: 60, bottom: 50, left: 60 }} | ||
groupMode="grouped" | ||
layout="vertical" | ||
valueScale={{ type: 'linear' }} | ||
indexScale={{ type: 'band', round: true }} | ||
colors={['#38A169', '#E53E3E', '#3182ce']} | ||
colorBy="id" | ||
enableGridX={false} | ||
enableGridY={false} | ||
labelTextColor={{ | ||
from: 'color', | ||
modifiers: [['darker', 1.6]], | ||
}} | ||
tooltip={item => { | ||
return ( | ||
<Tag fontSize="xx-small" variant="solid"> | ||
{keyToText[item.id]} {item.value} | ||
</Tag> | ||
); | ||
}} | ||
/> | ||
</ChackraBox> | ||
</Box> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { MonthByMonth } from '@Modules/Analytics/components/month-by-month.component'; | ||
import { CategoriesCurrentMonth } from '@Modules/Analytics/components/categories-current-month.component'; | ||
import { useCategories } from '@/hooks/useCategories'; | ||
import { useBills } from '@/hooks/useBills'; | ||
import { BillTypes } from '@Modules/Bill/constants/Types'; | ||
|
||
export const ListAnalyticsContainer = () => { | ||
const categories = useCategories(); | ||
const bills = useBills(); | ||
const billsExpesne = bills.filter(bill => bill.type === BillTypes.EXPENSE); | ||
|
||
return ( | ||
<> | ||
<MonthByMonth bills={bills} /> | ||
<CategoriesCurrentMonth categories={categories} bills={billsExpesne} /> | ||
</> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import withLateralMenu from '@/hoc/withLateralMenu'; | ||
import { ListAnalyticsContainer } from '@Modules/Analytics/container/list-analytics.container'; | ||
|
||
const Analytics = () => { | ||
return <ListAnalyticsContainer />; | ||
}; | ||
|
||
export default withLateralMenu(Analytics); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rename the variable name
month
toFormattedDueDate
, as it doesn't actually represent the month, but rather themonth/year
of the due date