Skip to content
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

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion components/LateralMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { FiHome, FiPower, FiTrendingUp, FiTrendingDown, FiMenu } from 'react-ico
import { IconType } from 'react-icons';
import Image from 'next/image';
import { BiSolidCategoryAlt } from 'react-icons/bi';
import { TbPigMoney } from 'react-icons/tb';
import { TbBrandGoogleAnalytics, TbPigMoney } from 'react-icons/tb';

interface LinkItemProps {
name: string;
Expand All @@ -29,6 +29,7 @@ interface LinkItemProps {
}
const LinkItems: Array<LinkItemProps> = [
{ name: 'Início', icon: FiHome, link: '/' },
{ name: 'Análises', icon: TbBrandGoogleAnalytics, link: '/analytics' },
{ name: 'Criar receita', icon: FiTrendingUp, link: '/bill?type=INCOME' },
{ name: 'Criar despesa', icon: FiTrendingDown, link: '/bill?type=EXPENSE' },
{ name: 'Orçamentos', icon: TbPigMoney, link: '/budget' },
Expand Down
1 change: 1 addition & 0 deletions configs/collections.config.ts
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');
48 changes: 48 additions & 0 deletions hooks/useBills.tsx
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
Expand Up @@ -14,7 +14,7 @@ interface UseCategoriesOptions {
type?: string;
}
export const useCategories = (options?: UseCategoriesOptions) => {
const [categories, setCategories] = useState(null);
const [categories, setCategories] = useState<ICategory[]>([]);
const { userId } = useUser();
const fetchUserCategories = useCallback(async () => {
if (!userId) return;
Expand Down
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>
);
};
101 changes: 101 additions & 0 deletions modules/Analytics/components/month-by-month.component.tsx
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()}`;
Copy link
Collaborator

@jefo3 jefo3 Mar 29, 2024

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 to FormattedDueDate, as it doesn't actually represent the month, but rather the month/year of the due date


if (!prev[month]) {
prev[month] = {};

if (curr.type === BillTypes.EXPENSE) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 expense and income values ​​to 0

Because in the next if you check if curr.type === billTypes.EXPENSE and do a prev[month].expense += curr.value;

If not, do: prev[month].income += curr.value;

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

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 if (!prev[month]) {
        prev[month] = {
          expense: 0,
          income: 0,
          month,
          rest: 0
        };
      }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But thinking here,
the code checks if there is no value in that table/year, if not, it initializes

there depending on whether the value of curr.type === BillTypes.EXPENSE
it initializes the expense with the value of curr.value
Then, as the value has already been initialized, it checks whether there is a value for the month/year ( if (prev?.[month]) )

as the prev for this month/year was initialized previously, it does: prev[month].expense += curr.value;

so it's as if it did a curr.values ​​+ curr.values for cases where the prev didn't yet have a value for that month

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>
);
};
18 changes: 18 additions & 0 deletions modules/Analytics/container/list-analytics.container.tsx
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} />
</>
);
};
12 changes: 5 additions & 7 deletions modules/Bill/container/FormBill/form-bill.container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { BillTypes, BillStatus } from '@Modules/Bill/constants/Types';
import { Input, MoneyInput, Select, DatePicker, Box, If } from '@Components';
import { useUser } from '@Modules/Authentication/context/UserContext';
import { defaultRequiredMessage, TO_YEAR } from '@Modules/Bill/constants/BillConsts';
import { useCategories } from '@Modules/Category/hooks/useCategories';
import { useCategories } from '@/hooks/useCategories';

const types = [
{
Expand All @@ -28,11 +28,6 @@ const recurrenceTypes = [
label: 'Cada mês',
value: 'EVERY_MONTH',
},
// TODO: every year recurrence
// {
// label: 'Cada ano',
// value: 'EVERY_YEAR',
// },
];

interface FormBillProps {
Expand Down Expand Up @@ -129,7 +124,10 @@ export const FormBillContainer = ({ billId }: FormBillProps) => {

router.push('/');
} catch (error) {
console.error(error);
toast({
description: error.message,
status: 'error',
});
}
};

Expand Down
2 changes: 1 addition & 1 deletion modules/Budget/components/form-budget.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import BaseForm from '@Modules/BaseModule/container/BaseForm';
import { BUBBLE_TYPES, BubbleEnum } from '@Modules/BaseModule/constants/Bubble';
import { BillTypes } from '@Modules/Bill/constants/Types';
import { useUser } from '@Authentication/context/UserContext';
import { useCategories } from '@Modules/Category/hooks/useCategories';
import { useCategories } from '@/hooks/useCategories';
import {
createBudget,
getOneBudget,
Expand Down
2 changes: 1 addition & 1 deletion modules/Budget/container/list-budget.container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import BaseList from '@Modules/BaseModule/container/BaseList';
import { BillTypes } from '@Modules/Bill/constants/Types';
import { useUser } from '@Authentication/context/UserContext';
import { deleteBudget, getAllBudgets, IBudget } from '@Modules/Budget/services/budget.service';
import { useCategories } from '@Modules/Category/hooks/useCategories';
import { useCategories } from '@/hooks/useCategories';
import { FormBudget } from '@Modules/Budget/components/form-budget.component';

export const ListBudgetContainer = () => {
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@hookform/resolvers": "^2.8.2",
"@nivo/bar": "^0.84.0",
"@nivo/core": "^0.84.0",
"@nivo/pie": "^0.84.0",
"firebase": "^8.9.1",
"framer-motion": "^4",
"next": "^12.0.0",
Expand All @@ -40,4 +43,4 @@
"sass": "^1.69.5",
"typescript": "^4.3.5"
}
}
}
8 changes: 8 additions & 0 deletions pages/analytics.tsx
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);
Loading