diff --git a/README.md b/README.md index f2fedcec..f67a6f6e 100644 --- a/README.md +++ b/README.md @@ -104,11 +104,11 @@ exit ## Contribuição -Certifique-se de ler o [Guia de Contribuição](https://github.com/fga-eps-mds/2023-1-CAPJu-Front/blob/main/.github/CONTRIBUTING.md) antes de realizar qualquer atividade no projeto! +Certifique-se de ler o [Guia de Contribuição](https://github.com/fga-eps-mds/2023-2-CAPJu-Front/blob/main/.github/CONTRIBUTING.md) antes de realizar qualquer atividade no projeto! ## Licença -O CAPJu está sob as regras aplicadas na licença [MIT](https://github.com/fga-eps-mds/2023-1-CAPJu-Front/blob/main/LICENSE) +O CAPJu está sob as regras aplicadas na licença [MIT](https://github.com/fga-eps-mds/2023-2-CAPJu-Front/blob/main/LICENSE) ## Contribuidores diff --git a/package.json b/package.json index 463269d3..f3157b46 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "react-input-mask": "^2.0.4", "react-paginate": "^8.2.0", "react-query": "^3.39.3", - "react-router-dom": "^6.10.0", + "react-router-dom": "^6.17.0", "react-select-event": "^5.5.1", "react-table": "^7.8.0", "reactflow": "^11.7.2", @@ -78,7 +78,7 @@ "jsdom": "^22.0.0", "lint-staged": "^11.1.2", "prettier": "^2.4.0", - "typescript": "^4.4.2", + "typescript": "^4.9.5", "vitest": "^0.31.1" } } diff --git a/src/components/CustomAccordion/index.tsx b/src/components/CustomAccordion/index.tsx new file mode 100644 index 00000000..2e9d7723 --- /dev/null +++ b/src/components/CustomAccordion/index.tsx @@ -0,0 +1,52 @@ +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Box, +} from "@chakra-ui/react"; + +interface CustomAccordionProps { + title: String; + children: JSX.Element; + marginBottom: number; +} +export default function CustomAccordion({ + title, + children, + marginBottom, +}: CustomAccordionProps) { + return ( + + +

+ + + + {title} + + +

+ + {children} + +
+
+ ); +} diff --git a/src/pages/Statistics/StepDeadlineReports/index.tsx b/src/pages/Statistics/StepDeadlineReports/index.tsx new file mode 100644 index 00000000..6014940a --- /dev/null +++ b/src/pages/Statistics/StepDeadlineReports/index.tsx @@ -0,0 +1,292 @@ +import { useEffect, useState, useMemo } from "react"; +import { useToast, Box, Flex, Button, Text, Input } from "@chakra-ui/react"; +import CustomAccordion from "components/CustomAccordion"; +import { DataTable } from "components/DataTable"; +import { getFlows } from "services/processManagement/flows"; +import { createColumnHelper } from "@tanstack/react-table"; +import { useQuery } from "react-query"; +import { useAuth } from "hooks/useAuth"; +import { isActionAllowedToUser } from "utils/permissions"; +import { ViewIcon } from "@chakra-ui/icons"; +import { Pagination } from "components/Pagination"; +import { getProcessesByDueDate } from "services/processManagement/statistics"; +import { useLocation } from "react-router-dom"; + +export default function StepDeadlineReports() { + const toast = useToast(); + const { getUserData } = useAuth(); + const { data: userData, isFetched: isUserFetched } = useQuery({ + queryKey: ["user-data"], + queryFn: getUserData, + }); + + const [flows, setFlows] = useState([] as Flow[]); + const { state } = useLocation(); + const [tableVisible, setTableVisible] = useState(false); + const [minDate, setMinDate] = useState(""); + const [isFetching, setIsFetching] = useState(true); + const [maxDate, setMaxDate] = useState(""); + const [processData, setProcessData] = useState([]); + const [processDueTotalPages, setProcessDueTotalPages] = useState< + number | undefined + >(); + const [loading, setLoading] = useState(false); + const [currentPage, setCurrentPage] = useState(0); + + useEffect(() => { + const handlePageChange = async () => { + setLoading(true); + await handleProcessByDueDate(); + setLoading(false); + }; + + if (minDate && maxDate) handlePageChange(); + }, [currentPage]); + + const getDataFlows = async () => { + const dataFlows = await getFlows(); + if (dataFlows.value) setFlows(dataFlows.value); + }; + + const handleProcessByDueDate = async () => { + const res = await getProcessesByDueDate(minDate, maxDate, { + offset: currentPage * 5, + limit: 5, + }); + + if (res.type === "error") throw new Error(res.error.message); + + setProcessData(res.value); + setProcessDueTotalPages(res.totalPages); + }; + + useEffect(() => { + if (flows.length === 0) getDataFlows(); + }, []); + + const tableColumnHelper = createColumnHelper>(); + const tableActions = useMemo( + () => [ + { + label: "Visualizar Processo", + icon: , + isNavigate: true, + actionName: "see-process", + disabled: !isActionAllowedToUser( + userData?.value?.allowedActions || [], + "see-process" + ), + }, + ], + [isUserFetched, userData] + ); + + const tableColumns = [ + tableColumnHelper.accessor("record", { + cell: (info) => info.getValue(), + header: "Registro", + meta: { + isSortable: true, + }, + }), + tableColumnHelper.accessor("nickname", { + cell: (info) => info.getValue(), + header: "Apelido", + meta: { + isSortable: true, + }, + }), + tableColumnHelper.accessor("nameFlow", { + cell: (info) => info.getValue(), + header: "Fluxo", + meta: { + isSortable: true, + }, + }), + tableColumnHelper.accessor("nameStage", { + cell: (info) => info.getValue(), + header: "Etapa Atual", + meta: { + isSortable: true, + }, + }), + tableColumnHelper.accessor("tableActions", { + cell: (info) => info.getValue(), + header: "Ações", + meta: { + isTableActions: true, + isSortable: false, + }, + }), + ]; + + const filteredStepDeadlineReports = useMemo[]>(() => { + if (processData.length <= 0) return []; + + return ( + (processData.reduce( + ( + acc: TableRow[] | Process[], + curr: TableRow | Process + ) => { + return [ + ...acc, + { + ...curr, + tableActions, + actionsProps: { + process: curr, + pathname: `/processos/${curr.record}`, + state: { + process: curr, + ...(state || {}), + }, + }, + record: curr.record, + }, + ]; + }, + [] + ) as TableRow[]) || [] + ); + }, [processData, tableActions]); + + const handleConfirmClick = async () => { + const minDateValue = Date.parse(minDate); + const maxDateValue = Date.parse(maxDate); + + if ( + Number.isNaN(minDateValue) || + Number.isNaN(maxDateValue) || + minDateValue > maxDateValue + ) { + toast({ + id: "date-validation-error", + title: "Erro", + description: + "Por favor, insira datas válidas e data da direita menor que a da esquerda", + status: "error", + isClosable: true, + }); + } else { + setTableVisible(true); + try { + setIsFetching(true); + await handleProcessByDueDate(); + setIsFetching(false); + } catch (error) { + toast({ + id: "processes-error", + title: "Erro ao carregar processos", + description: "Insira o período de validade.", + status: "error", + isClosable: true, + }); + } + } + }; + + return ( + + + + Estatísticas + + + + + + <> + + + + { + const novoValor = event.target.value; + setMinDate(novoValor); + }} + /> + até + { + const novoValor = event.target.value; + setMaxDate(novoValor); + }} + /> + + + + + + {tableVisible && ( + <> + + + + )} + + + + + {tableVisible && ( + + )} + + + {processDueTotalPages !== undefined ? ( + + setCurrentPage(selectedPage.selected) + } + /> + ) : null} + + + + + + + ); +} diff --git a/src/pages/Statistics/index.tsx b/src/pages/Statistics/index.tsx new file mode 100644 index 00000000..2b8987af --- /dev/null +++ b/src/pages/Statistics/index.tsx @@ -0,0 +1,10 @@ +import { PrivateLayout } from "layouts/Private"; +import StepDeadlineReports from "./StepDeadlineReports"; + +export default function Statistics() { + return ( + + + + ); +} diff --git a/src/pages/ViewProcess/index.tsx b/src/pages/ViewProcess/index.tsx index 02ab822f..fbfcd9ff 100644 --- a/src/pages/ViewProcess/index.tsx +++ b/src/pages/ViewProcess/index.tsx @@ -105,7 +105,9 @@ function ViewProcess() { "flow", typeof process?.idFlow === "number" ? process?.idFlow - : process?.idFlow[0], + : Array.isArray(process?.idFlow) + ? process.idFlow[0] + : process.idFlow, ], queryFn: async () => { const res = process.idFlow @@ -288,8 +290,8 @@ function ViewProcess() { colorScheme="blue" onClick={() => navigate(-1)} > - Voltar aos - Processos{flow ? ` do Fluxo ${flow?.name}` : ""} + Voltar{" "} + {flow ? ` do Fluxo ${flow?.name}` : ""} import("pages/Processes")); const ViewProcess = lazy(() => import("pages/ViewProcess")); const About = lazy(() => import("pages/About")); const ActionsManager = lazy(() => import("pages/ProfilesActionsManager")); +const Statistics = lazy(() => import("pages/Statistics")); export const PrivateRoutes: MenuItem[] = [ { @@ -45,6 +46,12 @@ export const PrivateRoutes: MenuItem[] = [ actionName: "see-process", element: , }, + { + path: "estatisticas", + name: "Estatisticas", + actionName: "see-process", + element: , + }, { path: "acessos", name: "ProfilesManager", diff --git a/src/services/processManagement/statistics/index.ts b/src/services/processManagement/statistics/index.ts new file mode 100644 index 00000000..00bb4fbc --- /dev/null +++ b/src/services/processManagement/statistics/index.ts @@ -0,0 +1,34 @@ +import { api } from "services/api"; + +export const getProcessesByDueDate = async ( + minDate: string, + maxDate: string, + pagination?: Pagination +): Promise> => { + try { + const res = await api.processManagement.get<{ + processInDue: Process[]; + totalPages: number; + }>(`/statistics/${minDate}/${maxDate}`, { + params: { + offset: pagination?.offset ?? 0, + limit: pagination?.limit, + }, + }); + + return { + type: "success", + value: res.data.processInDue, + totalPages: res.data.totalPages, + }; + } catch (error) { + if (error instanceof Error) + return { type: "error", error, value: undefined }; + + return { + type: "error", + error: new Error("Erro desconhecido"), + value: undefined, + }; + } +}; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 0152bd9e..84046863 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -74,6 +74,8 @@ declare global { status: string; progress?: Progress[]; isNextSage?: boolean; + nameStage?: string; + nameFlow?: string; }; type Note = { diff --git a/src/utils/tabs.ts b/src/utils/tabs.ts index 622f55ae..ef038759 100644 --- a/src/utils/tabs.ts +++ b/src/utils/tabs.ts @@ -23,6 +23,12 @@ export const tabs = [ path: "/processos", action: "see-process", }, + { + label: "Estatisticas", + pathIndex: "/estatisticas", + path: "/estatisticas", + action: "see-process", + }, { label: "Cadastro", pathIndex: "/acessos",