diff --git a/.gitignore b/.gitignore index cb034b6..992029d 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,8 @@ yarn-error.log* .env.development.local .env.test.local .env.production.local +.env.prod # vercel -.vercel \ No newline at end of file +.vercel +.vscode \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 3662b37..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "typescript.tsdk": "node_modules/typescript/lib" -} \ No newline at end of file diff --git a/components/LateralMenu/index.tsx b/components/LateralMenu/index.tsx index 1bbc53e..047666a 100644 --- a/components/LateralMenu/index.tsx +++ b/components/LateralMenu/index.tsx @@ -20,6 +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 { TbBrandGoogleAnalytics, TbPigMoney } from 'react-icons/tb'; interface LinkItemProps { name: string; @@ -28,8 +29,10 @@ interface LinkItemProps { } const LinkItems: Array = [ { 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' }, { name: 'Categorias', icon: BiSolidCategoryAlt, link: '/category' }, { name: 'Sair', diff --git a/configs/collections.config.ts b/configs/collections.config.ts new file mode 100644 index 0000000..821b747 --- /dev/null +++ b/configs/collections.config.ts @@ -0,0 +1,4 @@ +import { firestore } from '@Configs/Firebase'; + +export const billsCollection = firestore.collection('bills'); +export const budgetsCollection = firestore.collection('budgets'); diff --git a/hooks/useBills.tsx b/hooks/useBills.tsx new file mode 100644 index 0000000..84b9321 --- /dev/null +++ b/hooks/useBills.tsx @@ -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([]); + 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; +}; diff --git a/hooks/useCategories.tsx b/hooks/useCategories.tsx new file mode 100644 index 0000000..8be7488 --- /dev/null +++ b/hooks/useCategories.tsx @@ -0,0 +1,41 @@ +import { useState, useEffect, useCallback } from 'react'; + +import { firestore } from '@Configs/Firebase'; +import { useUser } from '@Authentication/context/UserContext'; +import { BillTypes } from '@Modules/Bill/constants/Types'; + +export interface ICategory { + id?: string; + name: string; + color: string; + type: string; +} +interface UseCategoriesOptions { + type?: string; +} +export const useCategories = (options?: UseCategoriesOptions) => { + const [categories, setCategories] = useState([]); + const { userId } = useUser(); + const fetchUserCategories = useCallback(async () => { + if (!userId) return; + + const data = await firestore.collection('categories').where('userId', '==', userId).get(); + const docs = data.docs.map( + category => + ({ + id: category.id, + ...category.data(), + } as ICategory) + ); + + const processedDocs = docs.filter(item => (options?.type ? item.type === options.type : true)); + + setCategories(processedDocs); + }, [setCategories, userId]); + + useEffect(() => { + fetchUserCategories(); + }, [fetchUserCategories]); + + return categories; +}; diff --git a/modules/Analytics/components/categories-current-month.component.tsx b/modules/Analytics/components/categories-current-month.component.tsx new file mode 100644 index 0000000..e69e484 --- /dev/null +++ b/modules/Analytics/components/categories-current-month.component.tsx @@ -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 ( + + + Gastos por categoria + + + {/* @ts-ignore */} + <>} + /> + + + ); +}; diff --git a/modules/Analytics/components/month-by-month.component.tsx b/modules/Analytics/components/month-by-month.component.tsx new file mode 100644 index 0000000..9241a34 --- /dev/null +++ b/modules/Analytics/components/month-by-month.component.tsx @@ -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) { + 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 ( + + + Últimos meses + + + {/* @ts-ignore */} + { + return ( + + {keyToText[item.id]} {item.value} + + ); + }} + /> + + + ); +}; diff --git a/modules/Analytics/container/list-analytics.container.tsx b/modules/Analytics/container/list-analytics.container.tsx new file mode 100644 index 0000000..ea273e0 --- /dev/null +++ b/modules/Analytics/container/list-analytics.container.tsx @@ -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 ( + <> + + + + ); +}; diff --git a/modules/BaseModule/components/Bubble/index.tsx b/modules/BaseModule/components/Bubble/index.tsx index d9e57b1..8115f61 100644 --- a/modules/BaseModule/components/Bubble/index.tsx +++ b/modules/BaseModule/components/Bubble/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { FieldErrors, FieldValues, UseFormRegister } from 'react-hook-form'; -import { Input, Select } from '@Components'; +import { Input, Select, MoneyInput } from '@Components'; import SelectOption from '@Modules/BaseModule/interfaces/SelectOption'; import { BubbleEnum, BUBBLE_TYPES } from '../../constants/Bubble'; @@ -55,6 +55,17 @@ const Bubble = ({ /> ); + case BUBBLE_TYPES.MONEY: + return ( + + ); + case BUBBLE_TYPES.SELECT: return (