diff --git a/.eslintrc.json b/.eslintrc.json
index ae922392..90745205 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -31,6 +31,4 @@
"consistent-return": "off",
"react/jsx-no-bind": "off"
}
-
-
}
diff --git a/package.json b/package.json
index ebcf1362..d5fe1e13 100755
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"jspdf-autotable": "^3.5.28",
"lodash": "^4.17.21",
"lucide-react": "^0.233.0",
+ "moment": "^2.29.4",
"react": "^18.2.0",
"react-datepicker": "^4.8.0",
"react-dom": "^18.2.0",
diff --git a/src/components/action-buttons/approve-button-homologation/approve-button-homologation.spec.tsx b/src/components/action-buttons/approve-button-homologation/approve-button-homologation.spec.tsx
new file mode 100644
index 00000000..4bbb925b
--- /dev/null
+++ b/src/components/action-buttons/approve-button-homologation/approve-button-homologation.spec.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import { vi } from 'vitest';
+import { ApproveButton } from '.';
+
+describe('ApproveButton', () => {
+ const handleApproveHomolog = vi.fn();
+ it('deve exibir o botão de aprovação e interagir corretamente', () => {
+ const passDateTime = new Date();
+
+ const { getByText, getByLabelText } = render(
+
+ );
+
+ // Verificar se o botão de aprovação está presente
+ const button = getByText('Aprovar Atendimento');
+ expect(button).toBeInTheDocument();
+
+ // Simular clique no botão de aprovação
+ fireEvent.click(button);
+ expect(handleApproveHomolog).toHaveBeenCalledTimes(1);
+
+ // Verificar se o campo de data está presente e tem o valor correto
+ // const dateTimeInput = getByLabelText('Data do Evento') as HTMLInputElement;
+ // expect(dateTimeInput).toBeInTheDocument();
+ // expect(dateTimeInput.value).toEqual(
+ // passDateTime.toISOString().substring(0, 16)
+ // );
+ });
+
+ // it('deve adicionar e remover alertas corretamente', () => {
+ // const handleApproveHomolog = vi.fn();
+ // const passDateTime = new Date();
+
+ // const { getByText, getByLabelText, queryByLabelText, getAllByLabelText } = render(
+ //
+ // );
+
+ // Simular clique no botão "Adicionar Alerta"
+ // const addButton = queryByLabelText('Adicionar Alerta');
+
+ // if (addButton) {
+ // fireEvent.click(addButton);
+ // expect(handleApproveHomolog).toHaveBeenCalledWith(handleApproveHomolog);
+ // }
+
+ // Verificar se o campo de alerta foi adicionado
+ // const alertInputs = getAllByLabelText('alert_dates');
+ // expect(alertInputs.length).toBe(1);
+
+ // Simular clique no botão de remover alerta
+ // const deleteButton = getByLabelText('Excluir Alerta 1');
+ // fireEvent.click(deleteButton);
+
+ // Verificar se o campo de alerta foi removido
+ // const updatedAlertInputs = getAllByLabelText('alert_dates');
+ // expect(updatedAlertInputs.length).toBe(0);
+ // });
+});
diff --git a/src/components/action-buttons/approve-button-homologation/index.tsx b/src/components/action-buttons/approve-button-homologation/index.tsx
new file mode 100644
index 00000000..3c170441
--- /dev/null
+++ b/src/components/action-buttons/approve-button-homologation/index.tsx
@@ -0,0 +1,239 @@
+import { useCallback, useState } from 'react';
+import { FaPlus, FaTrash } from 'react-icons/fa';
+import {
+ Box,
+ Button,
+ Flex,
+ Grid,
+ Divider,
+ Heading,
+ Popover,
+ PopoverAnchor,
+ PopoverArrow,
+ PopoverBody,
+ PopoverCloseButton,
+ PopoverContent,
+ PopoverFooter,
+ PopoverHeader,
+ Text,
+ useDisclosure,
+} from '@chakra-ui/react';
+import { Controller, useFieldArray, useForm } from 'react-hook-form';
+import { ActionButtonProps } from '../types';
+import { ActionButton } from '..';
+import { Input } from '@/components/form-fields/input';
+import { DeleteButton } from '../delete-button';
+import { IssuePayloadOpen } from '@/features/issues/types';
+import { parseSelectedDatetime } from '@/utils/format-date';
+
+interface ApproveButtonProps extends ActionButtonProps {
+ handleApproveHomolog: (justify: string) => void;
+ passDateTime: Date;
+}
+
+const tooltipStyle = {
+ bg: 'green.500',
+ color: 'white',
+};
+
+export function ApproveButton({
+ label,
+ onClick,
+ passDateTime,
+ ...props
+}: ApproveButtonProps) {
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const [justification] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ const { control, watch, register } = useForm({
+ defaultValues: {
+ dateTime: passDateTime ?? '',
+ },
+ });
+
+ const { fields, append, remove } = useFieldArray({
+ control,
+ shouldUnregister: true,
+ name: 'problem_types_payload',
+ });
+
+ const handleAddDate = useCallback(() => {
+ append({ label: '', value: '' });
+ }, [append]);
+
+ const handleRemoveDate = useCallback(
+ (index: number) => () => {
+ remove(index);
+ },
+ [remove]
+ );
+
+ return (
+
+
+ }
+ onClick={onOpen}
+ isLoading={isLoading}
+ color="red.500"
+ tooltipProps={tooltipStyle}
+ tabIndex={0}
+ {...props}
+ />
+
+
+
+
+
+
+
+ Confirmação
+
+
+
+
+
+ (
+ <>
+
+
+ {error ? error.message : null}
+
+ >
+ )}
+ />
+
+
+
+ Alertas
+
+
+
+ {fields?.map((field, index) => {
+ return (
+
+ (
+
+
+
+ {error ? error.message : null}
+
+
+ )}
+ />
+
+
+
+ );
+ })}
+
+
+ }
+ variant="outline"
+ color="primary"
+ tooltipProps={{
+ placement: 'bottom',
+ }}
+ onClick={handleAddDate}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/action-buttons/delete-button-homologation/delete-button-homologation.spec.tsx b/src/components/action-buttons/delete-button-homologation/delete-button-homologation.spec.tsx
new file mode 100644
index 00000000..14ef68d6
--- /dev/null
+++ b/src/components/action-buttons/delete-button-homologation/delete-button-homologation.spec.tsx
@@ -0,0 +1,33 @@
+import { screen, render } from '@testing-library/react';
+import { DeleteButton } from '.';
+
+describe('DeleteButton', () => {
+ it('has the correct aria-label', () => {
+ render(
+ {}}
+ handleDeleteHomolog={function (justify: string): void {
+ throw new Error('Function not implemented.');
+ }}
+ />
+ );
+ expect(screen.getByText('Reprovar homologação')).toBeInTheDocument();
+ });
+
+ it('should be able to call DeleteButton onClick function', async () => {
+ render(
+ {}}
+ handleDeleteHomolog={function (justify: string): void {
+ throw new Error('Function not implemented.');
+ }}
+ />
+ );
+
+ const button = screen.getByText('Reprovar homologação');
+
+ expect(button).toBeInTheDocument();
+ });
+});
diff --git a/src/components/action-buttons/delete-button-homologation/index.tsx b/src/components/action-buttons/delete-button-homologation/index.tsx
new file mode 100644
index 00000000..6f1444c4
--- /dev/null
+++ b/src/components/action-buttons/delete-button-homologation/index.tsx
@@ -0,0 +1,109 @@
+import { useState } from 'react';
+import { FaTrash } from 'react-icons/fa';
+import {
+ Button,
+ Flex,
+ Heading,
+ Popover,
+ PopoverAnchor,
+ PopoverArrow,
+ PopoverBody,
+ PopoverCloseButton,
+ PopoverContent,
+ PopoverFooter,
+ PopoverHeader,
+ Text,
+ Textarea,
+ useDisclosure,
+} from '@chakra-ui/react';
+import { ActionButtonProps } from '../types';
+import { ActionButton } from '..';
+
+interface DeleteButtonProps extends ActionButtonProps {
+ handleDeleteHomolog: (justify: string) => void;
+}
+
+const tooltipStyle = {
+ bg: 'red.500',
+ color: 'white',
+};
+
+export function DeleteButton({
+ label,
+ onClick,
+ ...props
+}: DeleteButtonProps) {
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const [justification, setJustification] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ return (
+
+
+ }
+ onClick={onOpen}
+ isLoading={isLoading}
+ color="red.500"
+ tooltipProps={tooltipStyle}
+ tabIndex={0}
+ {...props}
+ />
+
+
+
+
+
+
+
+ Reprovar homologação
+
+
+
+
+ Você realmente deseja reprovar a homologação?
+
+ Justifique o motivo da reprovação:
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/form-fields/input/index.tsx b/src/components/form-fields/input/index.tsx
index 2d49a8b5..a794ef8a 100644
--- a/src/components/form-fields/input/index.tsx
+++ b/src/components/form-fields/input/index.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
import { ReactElement } from 'react';
import { FieldError } from 'react-hook-form';
import {
diff --git a/src/components/forms/categoria-form/categoria-form.spec.tsx b/src/components/forms/categoria-form/categoria-form.spec.tsx
index e2ff8c91..46e29873 100644
--- a/src/components/forms/categoria-form/categoria-form.spec.tsx
+++ b/src/components/forms/categoria-form/categoria-form.spec.tsx
@@ -6,6 +6,7 @@ const category: Category = {
id: '1',
description: 'Descrição da Categoria',
name: 'Categoria',
+ visible_user_external: false,
};
describe('CategoriaForm', () => {
diff --git a/src/components/side-bar-open/index.tsx b/src/components/side-bar-open/index.tsx
index cd1fefd6..b04fd9b6 100644
--- a/src/components/side-bar-open/index.tsx
+++ b/src/components/side-bar-open/index.tsx
@@ -10,6 +10,7 @@ import {
Text,
VStack,
} from '@chakra-ui/react';
+import { BsTelephonePlus } from 'react-icons/bs';
import { CalendarClock } from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { SideBarItem } from '@/components/side-bar/sidebar-item';
@@ -28,6 +29,12 @@ export const SideBarOpen = memo(() => {
icon: CalendarClock,
allowedUsersPath: ['ADMIN', 'BASIC', 'USER'],
},
+ {
+ label: 'Registrar Agendamento',
+ pathname: '/agendamento_externo',
+ icon: BsTelephonePlus,
+ allowedUsersPath: ['ADMIN', 'BASIC', 'USER'],
+ },
];
return (
diff --git a/src/components/side-bar/index.tsx b/src/components/side-bar/index.tsx
index 7e136466..91786eae 100644
--- a/src/components/side-bar/index.tsx
+++ b/src/components/side-bar/index.tsx
@@ -10,9 +10,11 @@ import {
Text,
VStack,
} from '@chakra-ui/react';
+import { AiOutlineBell } from 'react-icons/ai';
import { useAuth } from '@/contexts/AuthContext';
import { routes } from '@/constants/routes';
import { SideBarItem } from '@/components/side-bar/sidebar-item';
+import { SideBarNotification } from '@/components/side-bar/sidebar-notification';
export const SideBar = memo(() => {
const { user, signOut } = useAuth();
@@ -41,8 +43,15 @@ export const SideBar = memo(() => {
))}
-
-
+
+
+
+
diff --git a/src/components/side-bar/sidebar-notification/index.tsx b/src/components/side-bar/sidebar-notification/index.tsx
new file mode 100644
index 00000000..7ef022d9
--- /dev/null
+++ b/src/components/side-bar/sidebar-notification/index.tsx
@@ -0,0 +1,83 @@
+/* eslint-disable react/display-name */
+import { memo } from 'react';
+import { Box, LinkBox, HStack, Icon, Text, Badge } from '@chakra-ui/react';
+import { useLocation, Link } from 'react-router-dom';
+import { IRoute } from '@/constants/routes';
+import { Permission } from '@/components/permission';
+import { useGetAllNotification } from '@/features/notifications/api/get-all-notifications';
+
+const hoverStyle = {
+ border: '1px solid',
+ borderColor: 'primary',
+ textDecoration: 'none',
+ backgroundPosition: 'right center',
+};
+
+export const SideBarNotification = memo(
+ ({ label, pathname, icon, allowedUsersPath }: IRoute) => {
+ const location = useLocation();
+ const isActive = location.pathname === pathname;
+
+ const { data: notifications } = useGetAllNotification();
+
+ const user = JSON.parse(localStorage.getItem('@schedula:user') || '[]');
+
+ const filteredNotifications = notifications?.filter((notification) => {
+ return (
+ notification.targetEmail === user.email && notification.read === false
+ );
+ });
+
+ return (
+
+
+
+
+
+
+ {label}
+
+
+
+ {filteredNotifications?.length}
+
+
+
+
+
+ );
+ }
+);
diff --git a/src/components/side-bar/sidebar.spec.tsx b/src/components/side-bar/sidebar.spec.tsx
index be1f624b..dceb6fdb 100644
--- a/src/components/side-bar/sidebar.spec.tsx
+++ b/src/components/side-bar/sidebar.spec.tsx
@@ -1,14 +1,19 @@
import { render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { SideBar } from '@/components/side-bar';
import { routes } from '@/constants/routes';
+const queryClient = new QueryClient();
+
describe('Sidebar', () => {
it('should display all routes', async () => {
const { findByText } = render(
-
-
-
+
+
+
+
+
);
let text;
diff --git a/src/config/routes/Routes.tsx b/src/config/routes/Routes.tsx
index 1ff47840..cce5fc95 100644
--- a/src/config/routes/Routes.tsx
+++ b/src/config/routes/Routes.tsx
@@ -9,13 +9,17 @@ import { ListaProblemas } from '@/pages/categorias/problemas';
import { RequireAuth } from '@/config/routes/require-auth';
import { DefaultLayout } from '@/components/layout/default-layout';
import { RegistrarChamado } from '@/pages/chamados/registrar';
+import { EditarChamadoExterno } from '@/pages/homologacao/editar-agendamentos-externos';
import { Agendamentos } from '@/pages/agendamentos';
import { ScheduleExport } from '@/pages/exportacao_agendamentos';
import { Tutoriais } from '@/pages/tutoriais';
import { CategoriasTutorial } from '@/pages/categorias_de_tutorial';
import { GerenciarTutoriais } from '@/pages/gerenciar-tutorial';
+import { GerenciarHomologacao } from '@/pages/homologacao';
+import { RegistrarAgendamento } from '@/pages/agendamento_externo/index';
import { AgendamentosAbertos } from '@/pages/agendamentos_abertos';
import { DefaultLayoutOpen } from '@/components/layout/default-layout-open';
+import { Notificacoes } from '@/pages/notificacoes';
export function Router() {
return (
@@ -129,12 +133,35 @@ export function Router() {
}
/>
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
-
{/* ROTAS PUBLICAS */}
} />
-
}>
+ } />
} />
404
} />
diff --git a/src/constants/alertas.ts b/src/constants/alertas.ts
new file mode 100644
index 00000000..5e5b15bf
--- /dev/null
+++ b/src/constants/alertas.ts
@@ -0,0 +1,5 @@
+export enum ALERTA_STATUS {
+ 'unsolved' = 'NÃO RESOLVIDO',
+ 'pending' = 'PENDENTE',
+ 'solved' = 'RESOLVIDO',
+}
diff --git a/src/constants/requests.ts b/src/constants/requests.ts
index 9fb9f152..8e4b83fc 100644
--- a/src/constants/requests.ts
+++ b/src/constants/requests.ts
@@ -9,3 +9,6 @@ export const SCHEDULES_ENDPOINT =
export const TUTORIALS_ENDPOINT =
import.meta.env.VITE_PUBLIC_GERENCIADOR_DE_TUTORIAIS_URL ?? '';
+
+export const ALERTS_ENDPOINT =
+ import.meta.env.VITE_PUBLIC_GESTOR_DE_ALERTAS_URL ?? '';
diff --git a/src/constants/routes.ts b/src/constants/routes.ts
index f56624db..467be421 100644
--- a/src/constants/routes.ts
+++ b/src/constants/routes.ts
@@ -1,5 +1,10 @@
import { IconType } from 'react-icons';
-import { BsSignpost2, BsTags, BsTelephonePlus } from 'react-icons/bs';
+import {
+ BsSignpost2,
+ BsTags,
+ BsTelephonePlus,
+ BsCardChecklist,
+} from 'react-icons/bs';
import { FaUsersCog } from 'react-icons/fa';
import { FiMapPin } from 'react-icons/fi';
import { MdOutlineViewAgenda } from 'react-icons/md';
@@ -69,4 +74,16 @@ export const routes: IRoute[] = [
icon: TbBulb,
allowedUsersPath: ['ADMIN', 'BASIC', 'USER'],
},
+ {
+ label: 'Homologação',
+ pathname: '/homologacao',
+ icon: BsCardChecklist,
+ allowedUsersPath: ['ADMIN'],
+ },
+ {
+ label: 'Registrar Agendamento',
+ pathname: '/agendamento_externo/registrar',
+ icon: BsTelephonePlus,
+ allowedUsersPath: ['USER'],
+ },
];
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
index 435f03c2..b870d46a 100644
--- a/src/contexts/AuthContext.tsx
+++ b/src/contexts/AuthContext.tsx
@@ -81,7 +81,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
navigate(from, { replace: true });
} catch (err) {
toast.error(
- 'Não foi possível realizar o login! Verifique o email e a senha e tente novamente.'
+ 'Não foi possível realizar o login! Verifique o nome de usuário e a senha e tente novamente.'
);
}
},
diff --git a/src/features/alerts/api/delete-alerts.tsx b/src/features/alerts/api/delete-alerts.tsx
new file mode 100644
index 00000000..93523410
--- /dev/null
+++ b/src/features/alerts/api/delete-alerts.tsx
@@ -0,0 +1,53 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { api } from '@/config/lib/axios';
+import { ALERT_ENDPOINT } from '@/features/alerts/constants/requests';
+import { toast } from '@/utils/toast';
+import { ALERTS_CACHE_KEYS } from '@/features/alerts/constants/cache';
+import {
+ DeleteAlertParams,
+ DeleteAlertsParams,
+} from '@/features/alerts/api/types';
+
+function deleteAlert({ alertId }: DeleteAlertParams) {
+ return api.delete(`${ALERT_ENDPOINT}/notifications/${alertId}`);
+}
+
+function deleteAlerts({ alertsIds }: DeleteAlertsParams) {
+ return api.delete(
+ `${ALERT_ENDPOINT}/notifications/delete-alerts/${alertsIds}`
+ );
+}
+
+export function useDeleteAlert() {
+ const queryClient = useQueryClient();
+
+ return useMutation(deleteAlert, {
+ onSuccess() {
+ toast.success('Alerta removido com sucesso!');
+
+ queryClient.invalidateQueries([ALERTS_CACHE_KEYS.allAlerts]);
+ },
+ onError() {
+ toast.error(
+ 'Não foi possível remover o alerta. Tente novamente mais tarde!'
+ );
+ },
+ });
+}
+
+export function useDeleteAlerts() {
+ const queryClient = useQueryClient();
+
+ return useMutation(deleteAlerts, {
+ onSuccess() {
+ toast.success('Alerta removidos com sucesso!');
+
+ queryClient.invalidateQueries([ALERTS_CACHE_KEYS.allAlerts]);
+ },
+ onError() {
+ toast.error(
+ 'Não foi possível remover os alerta. Tente novamente mais tarde!'
+ );
+ },
+ });
+}
diff --git a/src/features/alerts/api/detele-alerts.tsx b/src/features/alerts/api/detele-alerts.tsx
new file mode 100644
index 00000000..93523410
--- /dev/null
+++ b/src/features/alerts/api/detele-alerts.tsx
@@ -0,0 +1,53 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { api } from '@/config/lib/axios';
+import { ALERT_ENDPOINT } from '@/features/alerts/constants/requests';
+import { toast } from '@/utils/toast';
+import { ALERTS_CACHE_KEYS } from '@/features/alerts/constants/cache';
+import {
+ DeleteAlertParams,
+ DeleteAlertsParams,
+} from '@/features/alerts/api/types';
+
+function deleteAlert({ alertId }: DeleteAlertParams) {
+ return api.delete(`${ALERT_ENDPOINT}/notifications/${alertId}`);
+}
+
+function deleteAlerts({ alertsIds }: DeleteAlertsParams) {
+ return api.delete(
+ `${ALERT_ENDPOINT}/notifications/delete-alerts/${alertsIds}`
+ );
+}
+
+export function useDeleteAlert() {
+ const queryClient = useQueryClient();
+
+ return useMutation(deleteAlert, {
+ onSuccess() {
+ toast.success('Alerta removido com sucesso!');
+
+ queryClient.invalidateQueries([ALERTS_CACHE_KEYS.allAlerts]);
+ },
+ onError() {
+ toast.error(
+ 'Não foi possível remover o alerta. Tente novamente mais tarde!'
+ );
+ },
+ });
+}
+
+export function useDeleteAlerts() {
+ const queryClient = useQueryClient();
+
+ return useMutation(deleteAlerts, {
+ onSuccess() {
+ toast.success('Alerta removidos com sucesso!');
+
+ queryClient.invalidateQueries([ALERTS_CACHE_KEYS.allAlerts]);
+ },
+ onError() {
+ toast.error(
+ 'Não foi possível remover os alerta. Tente novamente mais tarde!'
+ );
+ },
+ });
+}
diff --git a/src/features/alerts/api/get-all-alerts.tsx b/src/features/alerts/api/get-all-alerts.tsx
new file mode 100644
index 00000000..0df91c5f
--- /dev/null
+++ b/src/features/alerts/api/get-all-alerts.tsx
@@ -0,0 +1,46 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { toast } from 'utils/toast';
+import { api } from '@/config/lib/axios';
+
+import { ALERTS_CACHE_KEYS } from '@/features/alerts/constants/cache';
+import { ALERT_ENDPOINT } from '@/features/alerts/constants/requests';
+import { Alert } from '@/features/alerts/api/types';
+
+export type GetAllAlertsResponse = Array;
+
+let errorShown = false; // Variável para controlar se o erro já foi mostrado
+
+export const getAllAlerts = async () =>
+ api
+ .get(`${ALERT_ENDPOINT}/notifications`)
+ .then((response) => response.data)
+ .catch((err) => {
+ const errMessage =
+ err?.response?.data?.message ??
+ 'Não foi possível carregar os alertas. Tente novamente mais tarde!';
+ if (!errorShown) {
+ toast.error(errMessage);
+ errorShown = true;
+ }
+ return [] as GetAllAlertsResponse;
+ });
+
+const getAlert = async (alertId: string) =>
+ api
+ .get(`${ALERT_ENDPOINT}/notifications/${alertId}`)
+ .then((response) => response.data)
+ .catch(() => {
+ toast.error(
+ 'Não foi possível carregar o alerta. Tente novamente mais tarde!'
+ );
+ return null;
+ });
+
+export const useGetallAlerts = () =>
+ useQuery([ALERTS_CACHE_KEYS.allAlerts], getAllAlerts, {
+ refetchInterval: 500,
+ });
+
+export const useGetAlert = (alertId: string) =>
+ useQuery([ALERTS_CACHE_KEYS.alert], () => getAlert(alertId));
diff --git a/src/features/alerts/api/post-create-alerts.tsx b/src/features/alerts/api/post-create-alerts.tsx
new file mode 100644
index 00000000..38fd761e
--- /dev/null
+++ b/src/features/alerts/api/post-create-alerts.tsx
@@ -0,0 +1,45 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { AxiosError } from 'axios';
+import { api } from '@/config/lib/axios';
+import { ALERT_ENDPOINT } from '@/features/alerts/constants/requests';
+import {
+ PostCreateAlertParams,
+ PostCreateAlertResponse,
+} from '@/features/alerts/api/types';
+import { ALERTS_CACHE_KEYS } from '@/features/alerts/constants/cache';
+import { toast } from '@/utils/toast';
+import { ApiError } from '@/config/lib/axios/types';
+
+function postCreateAlert(data: PostCreateAlertParams) {
+ return api.post(
+ `${ALERT_ENDPOINT}/notifications`,
+ data
+ );
+}
+
+export function usePostCreateAlert({
+ onSuccessCallBack,
+}: {
+ onSuccessCallBack?: () => void;
+}) {
+ const queryClient = useQueryClient();
+
+ return useMutation(postCreateAlert, {
+ onSuccess() {
+ queryClient.invalidateQueries([ALERTS_CACHE_KEYS.allAlerts]);
+
+ toast.success('Alerta criado com sucesso!');
+
+ onSuccessCallBack?.();
+ },
+ onError(error: AxiosError) {
+ const errorMessage = Array.isArray(error?.response?.data?.message)
+ ? error?.response?.data?.message[0]
+ : error?.response?.data?.message;
+ toast.error(
+ errorMessage ?? '',
+ 'Houve um problema ao tentar criar o alerta.'
+ );
+ },
+ });
+}
diff --git a/src/features/alerts/api/types.tsx b/src/features/alerts/api/types.tsx
new file mode 100644
index 00000000..4f1513dc
--- /dev/null
+++ b/src/features/alerts/api/types.tsx
@@ -0,0 +1,66 @@
+export interface Alert {
+ id: string;
+ sourceName: string;
+ targetName: string;
+ sourceEmail: string;
+ targetEmail: string;
+ message: string;
+ status: string;
+ pendency: string;
+ read: boolean;
+ createdAt: Date;
+}
+
+export interface PostCreateAlertParams {
+ sourceName: string | undefined;
+ targetName: string;
+ sourceEmail: string | undefined;
+ targetEmail: string;
+ message: string;
+ status: string;
+ pendency: string;
+ read: boolean;
+ createdAt: Date;
+}
+
+export interface PostCreateAlertResponse {
+ id: string;
+ sourceName: string;
+ targetName: string;
+ sourceEmail: string;
+ targetEmail: string;
+ message: string;
+ status: string;
+ pendency: string;
+ read: boolean;
+}
+
+/* export interface PutUpdateAlertParams {
+ AlertId: string;
+ data: {
+ name: string;
+ file?: any;
+ category_id: {
+ value: string;
+ };
+ };
+}
+
+export interface PutUpdateAlertResponse {
+ id: string;
+ name: string;
+ filename: string;
+ data: any;
+ category: {
+ id: string;
+ name: string;
+ };
+} */
+
+export interface DeleteAlertParams {
+ alertId: string;
+}
+
+export interface DeleteAlertsParams {
+ alertsIds: string[];
+}
diff --git a/src/features/alerts/components/alert-delete-form/alert-delete-form.spec.tsx b/src/features/alerts/components/alert-delete-form/alert-delete-form.spec.tsx
new file mode 100644
index 00000000..1f0bf5eb
--- /dev/null
+++ b/src/features/alerts/components/alert-delete-form/alert-delete-form.spec.tsx
@@ -0,0 +1,45 @@
+import { screen, render, waitFor, act } from '@testing-library/react';
+import { vi } from 'vitest';
+import { DeleteAlertForm } from '.';
+
+const mockedOnClose = vi.fn(() => {});
+const mockedOnClear = vi.fn(() => {});
+const mockedOnDelete = vi.fn(() => {});
+
+describe('DeleteAlertForm', () => {
+ it('should be able to delete alert', async () => {
+ render(
+
+ );
+
+ await act(async () => {
+ screen.getByText('Confirmar').click();
+ });
+
+ await waitFor(() => {
+ expect(mockedOnDelete).toHaveBeenCalled();
+ });
+ });
+
+ it('should be able to cancel the delete alert option', async () => {
+ render(
+
+ );
+
+ await act(async () => {
+ screen.getByText('Cancelar').click();
+ });
+
+ await waitFor(() => {
+ expect(mockedOnDelete).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/features/alerts/components/alert-delete-form/index.tsx b/src/features/alerts/components/alert-delete-form/index.tsx
new file mode 100644
index 00000000..a4053478
--- /dev/null
+++ b/src/features/alerts/components/alert-delete-form/index.tsx
@@ -0,0 +1,53 @@
+import { Button, Grid, ModalProps } from '@chakra-ui/react';
+import { useCallback } from 'react';
+
+interface DeleteAlertFormProps extends Partial {
+ onClose: () => void;
+ onClear: () => void;
+ onDelete: () => void;
+}
+
+export function DeleteAlertForm({
+ onClose,
+ onClear,
+ onDelete,
+}: DeleteAlertFormProps) {
+ const handleDeleteAlerts = useCallback(
+ (e: React.MouseEvent) => {
+ e.preventDefault();
+ onDelete();
+ onClear();
+ onClose();
+ },
+ [onDelete, onClear, onClose]
+ );
+
+ const handleClose = (e: React.MouseEvent) => {
+ e.preventDefault();
+ onClose();
+ };
+
+ return (
+
+ );
+}
diff --git a/src/features/alerts/components/alert-form/alert-form.spec.tsx b/src/features/alerts/components/alert-form/alert-form.spec.tsx
new file mode 100644
index 00000000..2e9b5771
--- /dev/null
+++ b/src/features/alerts/components/alert-form/alert-form.spec.tsx
@@ -0,0 +1,96 @@
+import {
+ screen,
+ render,
+ waitFor,
+ act,
+ fireEvent,
+} from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import {
+ GetAllUsersResponse,
+ getAllUsers,
+ useGetAllUsers,
+} from '@/features/users/api/get-all-users';
+import { AlertForm } from '.';
+
+const alert: AlertPayload = {
+ target: { value: 'joao@email.com', label: 'João Amoedo' },
+ message: 'Cheque o chamado n 56',
+};
+
+describe('AlertForm', () => {
+ const queryClient = new QueryClient();
+ let users: GetAllUsersResponse;
+ let isLoadingUsers: boolean;
+
+ beforeAll(async () => {
+ const response = await useGetAllUsers();
+ users = await getAllUsers();
+ isLoadingUsers = response.isLoading;
+ });
+ it.todo('should have the correct data', () => {
+ render(
+
+ {}}
+ isSubmitting={false}
+ users={users}
+ isLoadingUsers={isLoadingUsers}
+ />
+
+ );
+ expect(screen.getByLabelText('Nome')).toBeInTheDocument();
+ expect(screen.getByLabelText('Mensagem')).toHaveValue(
+ 'Cheque o chamado n 56'
+ );
+ });
+
+ it.todo('should be able to update an alert', async () => {
+ const handleSubmit = vi.fn();
+ render(
+
+
+
+ );
+
+ await act(async () => screen.getByText('Enviar').click());
+
+ await waitFor(() => {
+ expect(handleSubmit).toHaveBeenCalled();
+ });
+ });
+
+ it.todo('should be able to create a alert', async () => {
+ const handleSubmit = vi.fn();
+ render(
+
+ );
+
+ await act(async () => {
+ fireEvent.change(screen.getByLabelText('Nome'), {
+ target: { value: 'João Amoedo' },
+ });
+ fireEvent.change(screen.getByLabelText('Mensagem'), {
+ target: { value: 'Cheque o chamado n 56' },
+ });
+
+ screen.getByText('Enviar').click();
+ });
+
+ await waitFor(() => {
+ expect(handleSubmit).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/features/alerts/components/alert-form/index.tsx b/src/features/alerts/components/alert-form/index.tsx
new file mode 100644
index 00000000..16015749
--- /dev/null
+++ b/src/features/alerts/components/alert-form/index.tsx
@@ -0,0 +1,73 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { Button, FormControl, FormLabel, Textarea } from '@chakra-ui/react';
+import { useForm } from 'react-hook-form';
+import { ControlledSelect } from '@/components/form-fields';
+import { AlertPayload } from '../../type';
+import { GetAllUsersResponse } from '@/features/users/api/get-all-users';
+
+interface AlertFromProps {
+ defaultValues?: AlertPayload;
+ onSubmit: (data: any) => void;
+ isSubmitting: boolean;
+ users: GetAllUsersResponse | undefined;
+ isLoadingUsers: boolean;
+}
+
+export function AlertForm({
+ defaultValues,
+ onSubmit,
+ isSubmitting,
+ users,
+ isLoadingUsers,
+}: AlertFromProps) {
+ const {
+ register,
+ handleSubmit,
+ control,
+ formState: { errors },
+ } = useForm({
+ defaultValues: {
+ ...defaultValues,
+ },
+ });
+
+ const usersOptions = users?.map((users) => ({
+ label: users?.name,
+ value: users?.email,
+ }));
+
+ return (
+
+ );
+}
diff --git a/src/features/alerts/components/alert-item-manager/index.tsx b/src/features/alerts/components/alert-item-manager/index.tsx
new file mode 100644
index 00000000..160a70f2
--- /dev/null
+++ b/src/features/alerts/components/alert-item-manager/index.tsx
@@ -0,0 +1,81 @@
+import { Badge, HStack, VStack } from '@chakra-ui/react';
+import { CheckCircleIcon } from '@chakra-ui/icons';
+import { DeleteButton } from '@/components/action-buttons/delete-button';
+import { Item } from '@/components/list-item';
+import { Alert } from '../../type';
+import { ALERTA_STATUS } from '@/constants/alertas';
+
+interface AlertItemManagerProps {
+ alert: Alert;
+ onDelete: (alertId: string) => void;
+ isDeleting: boolean;
+}
+
+export function AlertItemManager({
+ alert,
+ onDelete,
+ isDeleting,
+}: AlertItemManagerProps) {
+ const alertReadIcon = (read: boolean) => {
+ if (read) {
+ return ;
+ }
+ return ;
+ };
+
+ const alertStatusBadge = (status: string) => {
+ if (status === 'solved') {
+ return (
+
+ {ALERTA_STATUS[status]}
+
+ );
+ }
+ if (status === 'pending') {
+ return (
+
+ {ALERTA_STATUS[status]}
+
+ );
+ }
+ if (status === 'unsolved') {
+ return (
+
+ {ALERTA_STATUS[status]}
+
+ );
+ }
+ };
+
+ return (
+
+
-
+
+
{alert?.message}
+
+
+ {alert?.pendency}
+
+
+ }
+ >
+
+ {alertStatusBadge(alert.status)}
+ {alertReadIcon(alert.read)}
+ {
+ if (alert.id) {
+ onDelete(alert.id);
+ }
+ }}
+ label="Alerta"
+ isLoading={isDeleting}
+ />
+
+
+
+ );
+}
diff --git a/src/features/alerts/components/alert-item/alert-item.spec.tsx b/src/features/alerts/components/alert-item/alert-item.spec.tsx
new file mode 100644
index 00000000..b78c0331
--- /dev/null
+++ b/src/features/alerts/components/alert-item/alert-item.spec.tsx
@@ -0,0 +1,28 @@
+import { render } from '@testing-library/react';
+import { AlertItem } from '@/features/alerts/components/alert-item';
+import { Alert } from '@/features/alerts/api/types';
+
+const mockedAlert: Alert = {
+ id: '11d2d5b0-6a5a-4b5b-b99c-c3c3d0e04e66',
+ sourceName: 'Filipe Souto',
+ sourceEmail: 'filipe50@email.com',
+ targetName: 'Arthur Rodrigues',
+ targetEmail: 'arthur992@email.com',
+ message: 'Olhe o chamado n° 46',
+ status: 'unsolved',
+ pendency: '',
+ read: false,
+ createdAt: new Date(),
+};
+
+describe('AlertItem', () => {
+ it('should render alert target name and message', async () => {
+ const { findAllByText } = render();
+
+ const alertTargetName = await findAllByText('Arthur Rodrigues');
+ expect(alertTargetName[0]).toBeInTheDocument();
+
+ const alertMessage = await findAllByText('Olhe o chamado n° 46');
+ expect(alertMessage[0]).toBeInTheDocument();
+ });
+});
diff --git a/src/features/alerts/components/alert-item/index.tsx b/src/features/alerts/components/alert-item/index.tsx
new file mode 100644
index 00000000..d60f87c9
--- /dev/null
+++ b/src/features/alerts/components/alert-item/index.tsx
@@ -0,0 +1,22 @@
+import { HStack } from '@chakra-ui/react';
+import { Item } from '@/components/list-item';
+import { Alert } from '@/features/alerts/api/types';
+
+interface AlertItemProps {
+ alert: Alert;
+}
+
+export function AlertItem({ alert }: AlertItemProps) {
+ return (
+
+
-
+
{alert?.message}
+
+ }
+ />
+
+ );
+}
diff --git a/src/features/alerts/components/alert-modal/index.tsx b/src/features/alerts/components/alert-modal/index.tsx
new file mode 100644
index 00000000..bde6fbf5
--- /dev/null
+++ b/src/features/alerts/components/alert-modal/index.tsx
@@ -0,0 +1,80 @@
+import { useCallback, useEffect, useState } from 'react';
+import { ModalProps } from '@chakra-ui/react';
+import { Modal } from '@/components/modal';
+import { AlertForm } from '../alert-form';
+import { Alert, AlertPayload } from '../../type';
+import { usePostCreateAlert } from '../../api/post-create-alerts';
+import { PostCreateAlertParams } from '../../api/types';
+import { useAuth } from '@/contexts/AuthContext';
+import { useGetAllUsers } from '@/features/users/api/get-all-users';
+
+interface AlertModalProps extends Partial {
+ alert?: Alert;
+ isOpen: boolean;
+ onClose: () => void;
+ handleSubmitAlert: () => void;
+}
+
+export function AlertModal({
+ onClose,
+ alert,
+ handleSubmitAlert,
+ ...props
+}: AlertModalProps) {
+ const { mutate: createAlert, isLoading: isCreatingAlert } =
+ usePostCreateAlert({
+ onSuccessCallBack: onClose,
+ });
+
+ const { data: users, isLoading: isLoadingUsers } = useGetAllUsers();
+ const [filterBasicUsers, setFilterBasicUsers] = useState(users || []);
+ const { user } = useAuth();
+
+ const handleSubmit = useCallback(
+ async ({ target, message }: AlertPayload) => {
+ const { label, value } = target;
+ const targetName = label;
+ const targetEmail = value;
+ const sourceName = user?.name;
+ const sourceEmail = user?.email;
+ const pendency = '';
+ const read = false;
+ const status = 'unsolved';
+ const date = new Date();
+ date.setHours(date.getHours() - 3);
+ const createdAt = date;
+ const payload: PostCreateAlertParams = {
+ sourceName,
+ targetName,
+ sourceEmail,
+ targetEmail,
+ message,
+ status,
+ pendency,
+ read,
+ createdAt,
+ };
+ createAlert(payload);
+ handleSubmitAlert();
+ },
+ [createAlert, handleSubmitAlert, user]
+ );
+
+ useEffect(() => {
+ if (users) {
+ const filteredUsers = users.filter((user) => user.profile === 'BASIC');
+ setFilterBasicUsers(filteredUsers);
+ }
+ }, [users]);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/features/alerts/components/delete-alert-modal/index.tsx b/src/features/alerts/components/delete-alert-modal/index.tsx
new file mode 100644
index 00000000..7685a952
--- /dev/null
+++ b/src/features/alerts/components/delete-alert-modal/index.tsx
@@ -0,0 +1,29 @@
+import { ModalProps } from '@chakra-ui/react';
+import { Modal } from '@/components/modal';
+import { DeleteAlertForm } from '../alert-delete-form';
+
+interface AlertModalProps extends Partial {
+ isOpen: boolean;
+ alertsIds: string[];
+ onClose: () => void;
+ onClear: () => void;
+ onDelete: () => void;
+}
+
+export function DeleteAlertModal({
+ onClose,
+ onClear,
+ onDelete,
+ alertsIds,
+ ...props
+}: AlertModalProps) {
+ return (
+
+
+
+ );
+}
diff --git a/src/features/alerts/constants/cache.tsx b/src/features/alerts/constants/cache.tsx
new file mode 100644
index 00000000..e189fb5c
--- /dev/null
+++ b/src/features/alerts/constants/cache.tsx
@@ -0,0 +1,4 @@
+export enum ALERTS_CACHE_KEYS {
+ allAlerts = 'all-alerts-cache',
+ alert = 'alert-cache',
+}
diff --git a/src/features/alerts/constants/requests.tsx b/src/features/alerts/constants/requests.tsx
new file mode 100644
index 00000000..89dfc1be
--- /dev/null
+++ b/src/features/alerts/constants/requests.tsx
@@ -0,0 +1,2 @@
+export const ALERT_ENDPOINT =
+ import.meta.env.VITE_PUBLIC_GESTOR_DE_ALERTAS_URL ?? '';
diff --git a/src/features/alerts/type.tsx b/src/features/alerts/type.tsx
new file mode 100644
index 00000000..f2f147d2
--- /dev/null
+++ b/src/features/alerts/type.tsx
@@ -0,0 +1,20 @@
+export interface Alert {
+ id: string;
+ sourceName: string;
+ targetName: string;
+ sourceEmail: string;
+ targetEmail: string;
+ message: string;
+ status: string;
+ pendency: string;
+ read: boolean;
+ createdAt: Date;
+}
+
+export interface AlertPayload {
+ target: {
+ label: string;
+ value: string;
+ };
+ message: string;
+}
diff --git a/src/features/categories-tutorial/api/get-all-categories-tutorial.ts b/src/features/categories-tutorial/api/get-all-categories-tutorial.ts
index 7a381c18..b26abc12 100644
--- a/src/features/categories-tutorial/api/get-all-categories-tutorial.ts
+++ b/src/features/categories-tutorial/api/get-all-categories-tutorial.ts
@@ -9,7 +9,7 @@ import { CategoryTutorial } from '@/features/categories-tutorial/api/types';
export type GetAllCategoryTutorialResponse = Array;
-export const getAllCategoryTutorial = async () =>
+export const getAllCategoryTutorial = async (count: number) =>
api
.get(
`${CATEGORIES_TUTORIAL_ENDPOINT}/categories`
@@ -19,7 +19,9 @@ export const getAllCategoryTutorial = async () =>
const errMessage =
err?.response?.data?.message ??
'Não foi possível carregar as categorias. Tente novamente mais tarde!';
- toast.error(errMessage);
+ if (count === 1) {
+ toast.error(errMessage);
+ }
return [] as GetAllCategoryTutorialResponse;
});
@@ -36,10 +38,9 @@ const getCategoryTutorial = async (categoryTutorialId: string) =>
return null;
});
-export const useGetAllCategoryTutorial = () =>
- useQuery(
- [CATEGORIES_TUTORIAL_CACHE_KEYS.allCategoriesTutorial],
- getAllCategoryTutorial
+export const useGetAllCategoryTutorial = (count: number) =>
+ useQuery([CATEGORIES_TUTORIAL_CACHE_KEYS.allCategoriesTutorial], () =>
+ getAllCategoryTutorial(count)
);
export const useGetCategoryTutorial = (categoryTutorialId: string) =>
diff --git a/src/features/homologations/api/delete-issue-open.ts b/src/features/homologations/api/delete-issue-open.ts
new file mode 100644
index 00000000..ba706c27
--- /dev/null
+++ b/src/features/homologations/api/delete-issue-open.ts
@@ -0,0 +1,27 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { api } from '@/config/lib/axios';
+import { toast } from '@/utils/toast';
+import { DeleteIssuesParams } from '@/features/homologations/api/types';
+import { ISSUES_ENDPOINT } from '@/features/issues/constants/requests';
+import { ISSUES_CACHE_KEYS } from '@/features/issues/constants/cache';
+
+function deleteIssues({ id }: DeleteIssuesParams) {
+ return api.delete(`${ISSUES_ENDPOINT}/issuesOpen/${id}`);
+}
+
+export function useDeleteHomologation() {
+ const queryClient = useQueryClient();
+
+ return useMutation(deleteIssues, {
+ onSuccess() {
+ toast.success('Agendamento removido com sucesso!');
+
+ queryClient.invalidateQueries([ISSUES_CACHE_KEYS.allIssues]);
+ },
+ onError() {
+ toast.error(
+ 'Não foi possível remover o agendamento. Tente novamente mais tarde!'
+ );
+ },
+ });
+}
diff --git a/src/features/homologations/api/get-all-issues-open.ts b/src/features/homologations/api/get-all-issues-open.ts
new file mode 100644
index 00000000..19bc9b62
--- /dev/null
+++ b/src/features/homologations/api/get-all-issues-open.ts
@@ -0,0 +1,24 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { toast } from 'utils/toast';
+import { api } from '@/config/lib/axios';
+
+import { ISSUES_CACHE_KEYS } from '@/features/issues/constants/cache';
+import { ISSUES_ENDPOINT } from '@/features/issues/constants/requests';
+import { IssueOpen } from '@/features/issues/types';
+
+type GetAllIssuesResponse = Array;
+
+const getAllIssues = async () =>
+ api
+ .get(`${ISSUES_ENDPOINT}/issuesOpen`)
+ .then((response) => response.data)
+ .catch(() => {
+ toast.error(
+ 'Não foi possível carregar os chamados. Tente novamente mais tarde!'
+ );
+ return [] as GetAllIssuesResponse;
+ });
+
+export const useGetAllIssues = () =>
+ useQuery([ISSUES_CACHE_KEYS.allIssues], getAllIssues);
diff --git a/src/features/homologations/api/post-create-issue-open.ts b/src/features/homologations/api/post-create-issue-open.ts
new file mode 100644
index 00000000..e1e40d2d
--- /dev/null
+++ b/src/features/homologations/api/post-create-issue-open.ts
@@ -0,0 +1,41 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { AxiosError } from 'axios';
+import { api } from '@/config/lib/axios';
+import { ISSUES_ENDPOINT } from '@/features/issues/constants/requests';
+import { IssueOpen, PostCreateIssueParamsOpen } from '@/features/issues/types';
+import { ISSUES_CACHE_KEYS } from '@/features/issues/constants/cache';
+import { toast } from '@/utils/toast';
+import { ApiError } from '@/config/lib/axios/types';
+
+function postCreateIssue(data: PostCreateIssueParamsOpen) {
+ return api
+ .post(`${ISSUES_ENDPOINT}/issuesOpen`, data)
+ .then((response) => response.data);
+}
+
+export function usePostCreateIssueOpen({
+ onSuccessCallBack,
+}: {
+ onSuccessCallBack?: (data: IssueOpen) => void;
+}) {
+ const queryClient = useQueryClient();
+
+ return useMutation(postCreateIssue, {
+ onSuccess(data: IssueOpen) {
+ queryClient.invalidateQueries([ISSUES_CACHE_KEYS.allIssues]);
+
+ toast.success('Agendamento Externo criado com sucesso!');
+
+ onSuccessCallBack?.(data);
+ },
+ onError(error: AxiosError) {
+ const errorMessage = Array.isArray(error?.response?.data?.message)
+ ? error?.response?.data?.message[0]
+ : error?.response?.data?.message;
+ toast.error(
+ errorMessage ?? '',
+ 'Houve um problema ao tentar criar um agendamento externo.'
+ );
+ },
+ });
+}
diff --git a/src/features/homologations/api/post-create-schedule-open.ts b/src/features/homologations/api/post-create-schedule-open.ts
new file mode 100644
index 00000000..d9371b84
--- /dev/null
+++ b/src/features/homologations/api/post-create-schedule-open.ts
@@ -0,0 +1,46 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { AxiosError } from 'axios';
+import { api } from '@/config/lib/axios';
+import { ISSUES_ENDPOINT } from '@/features/issues/constants/requests';
+import {
+ PostCreateScheduleParams,
+ PostCreateScheduleResponse,
+} from '@/features/issues/types';
+import { toast } from '@/utils/toast';
+import { ApiError } from '@/config/lib/axios/types';
+import { SCHEDULE_CACHE_KEYS } from '@/features/schedules/constants/cache';
+
+async function postCreateScheduleOpen(data: PostCreateScheduleParams) {
+ const response = await api.post(
+ `${ISSUES_ENDPOINT}/schedules-open`,
+ data
+ );
+ return response.data;
+}
+
+export function usePostCreateScheduleOpen({
+ onSuccessCallBack,
+}: {
+ onSuccessCallBack?: () => void;
+}) {
+ const queryClient = useQueryClient();
+
+ return useMutation(postCreateScheduleOpen, {
+ onSuccess() {
+ queryClient.invalidateQueries([SCHEDULE_CACHE_KEYS.allSchedules]);
+
+ toast.success('Agendamento criado com sucesso!');
+
+ onSuccessCallBack?.();
+ },
+ onError(error: AxiosError) {
+ const errorMessage = Array.isArray(error?.response?.data?.message)
+ ? error?.response?.data?.message[0]
+ : error?.response?.data?.message;
+ toast.error(
+ errorMessage ?? '',
+ 'Houve um problema ao tentar criar o agendamento.'
+ );
+ },
+ });
+}
diff --git a/src/features/homologations/api/post-send-mail-issue-open.ts b/src/features/homologations/api/post-send-mail-issue-open.ts
new file mode 100644
index 00000000..1f92ffc4
--- /dev/null
+++ b/src/features/homologations/api/post-send-mail-issue-open.ts
@@ -0,0 +1,47 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { AxiosError } from 'axios';
+import { api } from '@/config/lib/axios';
+import { ISSUES_ENDPOINT } from '@/features/issues/constants/requests';
+import {
+ PostSendMailIssueParamsOpen,
+ PostSendMailIssueResponseOpen,
+} from '@/features/issues/types';
+import { ISSUES_CACHE_KEYS } from '@/features/issues/constants/cache';
+import { toast } from '@/utils/toast';
+import { ApiError } from '@/config/lib/axios/types';
+
+function postSendMailExternIssue(data: PostSendMailIssueParamsOpen) {
+ return api
+ .post(
+ `${ISSUES_ENDPOINT}/issuesOpen/email`,
+ data
+ )
+ .then((response) => response.data);
+}
+
+export function usePostSendMailExternIssue({
+ onSuccessCallBack,
+}: {
+ onSuccessCallBack?: (data: PostSendMailIssueResponseOpen) => void;
+}) {
+ const queryClient = useQueryClient();
+
+ return useMutation(postSendMailExternIssue, {
+ onSuccess(data: PostSendMailIssueResponseOpen) {
+ queryClient.invalidateQueries([ISSUES_CACHE_KEYS.allIssues]);
+
+ toast.success('E-mail enviado com sucesso!');
+
+ onSuccessCallBack?.(data);
+ },
+ onError(error: AxiosError) {
+ const errorMessage = Array.isArray(error?.response?.data?.message)
+ ? error?.response?.data?.message[0]
+ : error?.response?.data?.message;
+ toast.error(
+ errorMessage ?? '',
+ 'Houve um problema ao tentar enviar o e-mail.'
+ );
+ },
+ });
+}
diff --git a/src/features/homologations/api/put-edit-issues-open.ts b/src/features/homologations/api/put-edit-issues-open.ts
new file mode 100644
index 00000000..83d92011
--- /dev/null
+++ b/src/features/homologations/api/put-edit-issues-open.ts
@@ -0,0 +1,77 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { AxiosError } from 'axios';
+import { api } from '@/config/lib/axios';
+import { ISSUES_ENDPOINT } from '@/features/issues/constants/requests';
+import { toast } from '@/utils/toast';
+import { ApiError } from '@/config/lib/axios/types';
+import { ISSUES_CACHE_KEYS } from '@/features/issues/constants/cache';
+import { PutEditIssuesParams } from '@/features/homologations/api/types';
+import { PutUpdateIssueParamsOpen } from '@/features/issues/types';
+import { SCHEDULE_CACHE_KEYS } from '@/features/schedules/constants/cache';
+
+async function putUpdateHomologIssues(id: string) {
+ const response = await api.put(
+ `${ISSUES_ENDPOINT}/issuesOpen/homolog/${id}`
+ );
+
+ return response.data;
+}
+
+export function usePutUpdateHomologIssues({
+ onSuccessCallBack,
+}: {
+ onSuccessCallBack?: () => void;
+}) {
+ const queryClient = useQueryClient();
+
+ return useMutation(putUpdateHomologIssues, {
+ onSuccess() {
+ queryClient.invalidateQueries([ISSUES_CACHE_KEYS.allIssues]);
+ queryClient.invalidateQueries([ISSUES_CACHE_KEYS.allIssues]);
+
+ onSuccessCallBack?.();
+ },
+ onError(error: AxiosError) {
+ const errorMessage = Array.isArray(error?.response?.data?.message)
+ ? error?.response?.data?.message[0]
+ : error?.response?.data?.message;
+ },
+ });
+}
+
+async function putUpdateExternIssue(data: PutUpdateIssueParamsOpen) {
+ const { issueId } = data;
+ const response = await api.put(
+ `${ISSUES_ENDPOINT}/issuesOpen/${issueId}`,
+ data
+ );
+ return response.data;
+}
+
+export function usePutUpdateExternIssue({
+ onSuccessCallBack,
+}: {
+ onSuccessCallBack?: () => void;
+}) {
+ const queryClient = useQueryClient();
+
+ return useMutation(putUpdateExternIssue, {
+ onSuccess() {
+ queryClient.invalidateQueries([SCHEDULE_CACHE_KEYS.allSchedules]);
+ queryClient.invalidateQueries([SCHEDULE_CACHE_KEYS.allSchedules]);
+
+ toast.success('Agendamento externo atualizado com sucesso!');
+
+ onSuccessCallBack?.();
+ },
+ onError(error: AxiosError) {
+ const errorMessage = Array.isArray(error?.response?.data?.message)
+ ? error?.response?.data?.message[0]
+ : error?.response?.data?.message;
+ toast.error(
+ errorMessage ?? '',
+ 'Houve um problema ao tentar atualizar o agendamento externo.'
+ );
+ },
+ });
+}
diff --git a/src/features/homologations/api/types.ts b/src/features/homologations/api/types.ts
new file mode 100644
index 00000000..a96cfda1
--- /dev/null
+++ b/src/features/homologations/api/types.ts
@@ -0,0 +1,29 @@
+import { IssueOpen, ProblemTypeOption } from '@/features/issues/types';
+
+export type GetAllIssuesResponse = Array;
+
+export interface DeleteIssuesParams {
+ id: string;
+}
+
+export interface PutEditIssuesParams {
+ id: string;
+ data: {
+ requester: string;
+ phone: string;
+ city_id: string;
+ workstation_id: string;
+ email: string;
+ date: Date | string;
+ dateTime: Date;
+ alerts: Date[];
+ problem_category: {
+ id: string;
+ name: string;
+ description: string;
+ problem_types: ProblemTypeOption[];
+ };
+ problem_types: ProblemTypeOption[];
+ isHomolog?: boolean;
+ };
+}
diff --git a/src/features/homologations/components/issue-open-edit-form/index.tsx b/src/features/homologations/components/issue-open-edit-form/index.tsx
new file mode 100644
index 00000000..613d163b
--- /dev/null
+++ b/src/features/homologations/components/issue-open-edit-form/index.tsx
@@ -0,0 +1,492 @@
+import {
+ Box,
+ Button,
+ Divider,
+ Flex,
+ Grid,
+ GridItem,
+ Icon,
+ InputLeftElement,
+ Text,
+} from '@chakra-ui/react';
+import { useCallback, useEffect, useState } from 'react';
+import { Controller, useFieldArray, useForm } from 'react-hook-form';
+import { BsPersonCircle, BsTelephoneFill } from 'react-icons/bs';
+import { HiOutlineMail } from 'react-icons/hi';
+import { useLocation } from 'react-router-dom';
+import { FaPlus } from 'react-icons/fa';
+import { ControlledSelect } from '@/components/form-fields';
+import { Input } from '@/components/form-fields/input';
+import { useGetAllCities } from '@/features/cities/api/get-all-cities';
+import { usePutUpdateExternIssue } from '@/features/homologations/api/put-edit-issues-open';
+import {
+ IssueOpen,
+ IssuePayloadOpen,
+ PutUpdateIssueParamsOpen,
+} from '@/features/issues/types';
+import { parseSelectedDatetime } from '@/utils/format-date';
+import { useGetAllWorkstations } from '@/features/workstations/api/get-all-workstations';
+import { useGetAllProblemCategories } from '@/features/problem/api/get-all-problem-category';
+import { maskPhoneField } from '@/utils/form-utils';
+import { DeleteButton } from '@/components/action-buttons/delete-button';
+import { ActionButton } from '@/components/action-buttons';
+
+export function UpdateExternIssueForm() {
+ const locate = useLocation();
+ const { externIssue } = locate.state;
+
+ const externIssueOpen = externIssue as IssueOpen;
+
+ const { data: cities, isLoading: isLoadingCities } = useGetAllCities(0);
+ const {
+ register,
+ control,
+ handleSubmit,
+ watch,
+ formState: { errors },
+ } = useForm({
+ // define alguns valores padrão para os campos abaixo.
+
+ defaultValues: {
+ city_payload: {
+ label: locate.state.city.name ?? '',
+ value: locate.state.city.id ?? '',
+ },
+ workstation_payload: {
+ label: locate.state.workstation.name ?? '',
+ value: locate.state.workstation.id ?? '',
+ },
+ problem_category_payload: {
+ label: locate.state.problem_category.name ?? '',
+ value: locate.state.problem_category.id ?? '',
+ },
+ problem_types_payload:
+ externIssue?.problem_types.map((type: { name: string; id: any }) => ({
+ label: type.name,
+ value: type.id,
+ })) || [],
+ alert_dates: externIssueOpen?.alerts.map((alert) => ({
+ date: alert,
+ })),
+ dateTime: parseSelectedDatetime(locate.state.externIssue.dateTime) ?? '',
+ phone: locate.state.externIssue.phone ?? '',
+ ...locate.state.externIssue,
+ },
+ });
+
+ const [selectedProblemTypes, setSelectedProblemTypes] = useState(
+ locate.state.problem_types_payload
+ );
+
+ const { mutate: updateIssue, isLoading: isUpdatingIssue } =
+ usePutUpdateExternIssue({
+ onSuccessCallBack: () => {},
+ });
+
+ const { data: problem_categories, isLoading: isLoadingProblems } =
+ useGetAllProblemCategories();
+
+ const { data: workstations, isLoading: isLoadingWorkstations } =
+ useGetAllWorkstations();
+
+ const citiesOptions = cities?.map((city) => {
+ return {
+ label: city?.name ?? '',
+ value: city?.id ?? '',
+ };
+ });
+
+ const problemCategoriesOptions = problem_categories?.map((category) => {
+ return {
+ label: category?.name ?? '',
+ value: category?.id ?? '',
+ };
+ });
+
+ const city = watch('city_payload');
+ const category = watch('problem_category_payload');
+ const problemTypes = watch('problem_types_payload');
+
+ const workstationsOptions = city
+ ? workstations
+ ?.filter((workstation) => workstation.city.id === city.value)
+ .map((workstation) => ({
+ value: workstation?.id ?? '',
+ label: workstation?.name ?? '',
+ }))
+ : [];
+
+ const problemTypesOptions = category
+ ? problem_categories
+ ?.filter((cat) => cat.id === category.value)
+ .map((category) => category.problem_types)[0]
+ .map((problemType) => ({
+ value: problemType?.id ?? '',
+ label: problemType?.name ?? '',
+ }))
+ : [];
+
+ const onSubmit = useCallback(
+ ({
+ requester,
+ city_payload,
+ phone,
+ cellphone,
+ dateTime,
+ email,
+ alerts,
+ problem_category_payload,
+ problem_types_payload,
+ workstation_payload,
+ description,
+ }: IssuePayloadOpen) => {
+ const issueId = locate.state.externIssue?.id ?? '';
+ const payload: PutUpdateIssueParamsOpen = {
+ issueId,
+ phone: phone ?? watch('phone'),
+ requester,
+ dateTime,
+ alerts,
+ cellphone: maskPhoneField(cellphone) ?? '',
+ city_id: city_payload?.value,
+ description,
+ date: new Date(),
+ problem_category_id: problem_category_payload?.value,
+ problem_types_ids: problem_types_payload.map((type) => type.value),
+ workstation_id: workstation_payload?.value,
+ email,
+ };
+
+ updateIssue(payload);
+ },
+ [updateIssue, locate.state.externIssue?.id, watch]
+ );
+
+ useEffect(() => {
+ if (problemTypes) {
+ setSelectedProblemTypes(problemTypes);
+ }
+ }, [problemTypes]);
+
+ const { fields, append, remove } = useFieldArray({
+ control,
+ shouldUnregister: true,
+ name: 'alerts' as any,
+ });
+
+ const handleAddDate = useCallback(() => {
+ append({ label: '', value: '' });
+ }, [append]);
+
+ const handleRemoveDate = useCallback(
+ (index: number) => () => {
+ remove(index);
+ },
+ [remove]
+ );
+
+ return (
+
+ );
+}
diff --git a/src/features/homologations/components/issue-open-edit-modal/index.tsx b/src/features/homologations/components/issue-open-edit-modal/index.tsx
new file mode 100644
index 00000000..622a199a
--- /dev/null
+++ b/src/features/homologations/components/issue-open-edit-modal/index.tsx
@@ -0,0 +1,61 @@
+import { useCallback } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { ScheduleStatus, ScheduleOpen } from '@/features/schedules/types';
+import { ScheduleEditForm } from '@/features/schedules/components/schedule-edit-form';
+import { usePutEditSchedule } from '@/features/schedules/api/put-edit-schedule';
+
+interface ScheduleEditModalProps {
+ schedule?: ScheduleOpen;
+ onClose: () => void;
+}
+
+export function ScheduleEditModal({
+ schedule,
+ onClose,
+}: ScheduleEditModalProps) {
+ const history = useNavigate();
+
+ const { mutate: editSchedule, isLoading: isEditingSchedule } =
+ usePutEditSchedule({
+ onSuccessCallBack: () => {
+ onClose();
+ history('/homologacao/editar');
+ },
+ });
+
+ const handleSubmit = useCallback(
+ (values: any) => {
+ const { description, status, dateTime } = schedule ?? {};
+ const alerts =
+ values.alert_dates?.map((alert: { date: string }) => alert.date) || [];
+
+ const defaultStatus = status
+ ? Object.keys(ScheduleStatus)[
+ Object.values(ScheduleStatus).indexOf(status)
+ ]
+ : 'PROGRESS';
+
+ editSchedule({
+ id: schedule?.id ?? '',
+ data: {
+ issue_id: schedule?.issue.id ?? '',
+ description: values.description ?? description,
+ status_e: values.status_e.value ?? defaultStatus,
+ dateTime: values.date_time ?? dateTime,
+ alerts,
+ },
+ });
+ },
+ [editSchedule, schedule]
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/src/features/homologations/components/issue-open-form/create-issue-form-open.spec.tsx b/src/features/homologations/components/issue-open-form/create-issue-form-open.spec.tsx
new file mode 100644
index 00000000..4921f15b
--- /dev/null
+++ b/src/features/homologations/components/issue-open-form/create-issue-form-open.spec.tsx
@@ -0,0 +1,145 @@
+import { ChakraProvider } from '@chakra-ui/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { BrowserRouter } from 'react-router-dom';
+import { theme } from '@/styles/theme';
+import { CreateIssueForm } from './create-issue-form-open';
+import { AuthProvider } from '@/contexts/AuthContext';
+
+import 'intersection-observer';
+
+describe('renders form fields correctly', () => {
+ const queryClient = new QueryClient();
+
+ it('should have an input for the forms', async () => {
+ render(
+
+
+
+
+
+
+
+
+
+ );
+
+ expect(screen.getByLabelText('Solicitante')).toBeInTheDocument();
+ expect(screen.getByLabelText('Celular')).toBeInTheDocument();
+ expect(screen.getByLabelText('Email')).toBeInTheDocument();
+ expect(screen.getByLabelText('Cidade')).toBeInTheDocument();
+ expect(screen.getByLabelText('Posto de Trabalho')).toBeInTheDocument();
+ expect(screen.getByLabelText('Telefone')).toBeInTheDocument();
+ expect(screen.getByLabelText('Categoria do Problema')).toBeInTheDocument();
+ expect(screen.getByLabelText('Tipos de Problema')).toBeInTheDocument();
+ expect(screen.getByLabelText('Descrição do Problema')).toBeInTheDocument();
+ });
+
+ it('submits the form successfully', async () => {
+ render(
+
+
+
+
+
+
+
+
+
+ );
+
+ fireEvent.change(screen.getByLabelText('Solicitante'), {
+ target: { value: 'Mocked Requester' },
+ });
+ fireEvent.change(screen.getByLabelText('Celular'), {
+ target: { value: '61123456789' },
+ });
+ fireEvent.change(screen.getByLabelText('Email'), {
+ target: { value: 'mock.@policiacivil.go.gov.br' },
+ });
+ fireEvent.change(screen.getByLabelText('Cidade'), {
+ target: { value: 'Mocked City' },
+ });
+ fireEvent.change(screen.getByLabelText('Posto de Trabalho'), {
+ target: { value: 'Mocked Workstation' },
+ });
+ fireEvent.change(screen.getByLabelText('Telefone'), {
+ target: { value: '12345678912' },
+ });
+ fireEvent.change(screen.getByLabelText('Categoria do Problema'), {
+ target: { value: 'Mocked Problem Categorie' },
+ });
+ fireEvent.change(screen.getByLabelText('Tipos de Problema'), {
+ target: { value: 'Mocked Problem Types' },
+ });
+ fireEvent.change(screen.getByLabelText('Descrição do Problema'), {
+ target: { value: 'Mocked Problem Description' },
+ });
+ fireEvent.click(screen.getByText('Registrar Agendamento'));
+ });
+
+ it('displays a validation error message for invalid email format', async () => {
+ render(
+
+
+
+
+
+
+
+
+
+ );
+
+ fireEvent.change(screen.getByLabelText('Email'), {
+ target: { value: 'Formato invalido' },
+ });
+
+ fireEvent.click(screen.getByText('Registrar Agendamento'));
+ await expect(screen.findByText('Formato de e-mail inválido'));
+ });
+
+ it('displays a success message after submitting the form', async () => {
+ render(
+
+
+
+
+
+
+
+
+
+ );
+
+ fireEvent.change(screen.getByLabelText('Solicitante'), {
+ target: { value: 'Mocked Requester' },
+ });
+ fireEvent.change(screen.getByLabelText('Celular'), {
+ target: { value: '61123456789' },
+ });
+ fireEvent.change(screen.getByLabelText('Email'), {
+ target: { value: 'mocked@policiacivil.go.gov.br' },
+ });
+ fireEvent.change(screen.getByLabelText('Cidade'), {
+ target: { value: 'Mocked City' },
+ });
+ fireEvent.change(screen.getByLabelText('Posto de Trabalho'), {
+ target: { value: 'Mocked Workstation' },
+ });
+ fireEvent.change(screen.getByLabelText('Telefone'), {
+ target: { value: '12345678912' },
+ });
+ fireEvent.change(screen.getByLabelText('Categoria do Problema'), {
+ target: { value: 'Mocked Problem Categorie' },
+ });
+ fireEvent.change(screen.getByLabelText('Tipos de Problema'), {
+ target: { value: 'Mocked Problem Types' },
+ });
+ fireEvent.change(screen.getByLabelText('Descrição do Problema'), {
+ target: { value: 'Mocked Problem Description' },
+ });
+ fireEvent.click(screen.getByText('Registrar Agendamento'));
+ await expect(screen.findByText('Ir para os agendamentos'));
+ });
+});
diff --git a/src/features/homologations/components/issue-open-form/create-issue-form-open.tsx b/src/features/homologations/components/issue-open-form/create-issue-form-open.tsx
new file mode 100644
index 00000000..033b6bdf
--- /dev/null
+++ b/src/features/homologations/components/issue-open-form/create-issue-form-open.tsx
@@ -0,0 +1,347 @@
+import {
+ Button,
+ Grid,
+ GridItem,
+ Icon,
+ InputLeftElement,
+ useDisclosure,
+} from '@chakra-ui/react';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { Controller, useForm } from 'react-hook-form';
+import { BsPersonCircle, BsTelephoneFill } from 'react-icons/bs';
+import { useNavigate } from 'react-router-dom';
+import { MdEmail } from 'react-icons/md';
+import { ControlledSelect } from '@/components/form-fields';
+import { Input } from '@/components/form-fields/input';
+import { useGetAllCities } from '@/features/cities/api/get-all-cities';
+import { usePostCreateIssueOpen } from '@/features/homologations/api/post-create-issue-open';
+import {
+ IssueOpen,
+ IssuePayloadOpen,
+ PostCreateIssueParamsOpen,
+} from '@/features/issues/types';
+
+import { useGetAllWorkstations } from '@/features/workstations/api/get-all-workstations';
+import { useGetAllProblemCategories } from '@/features/problem/api/get-all-problem-category';
+import { maskPhoneField } from '@/utils/form-utils';
+import { ScheduleModal } from '@/features/schedules/components/schedule-modal';
+
+interface Option {
+ label: string;
+ value: string;
+}
+
+export function CreateIssueForm() {
+ const navigate = useNavigate();
+
+ const [createdIssue, setCreatedIssue] = useState();
+
+ const cityRef = useRef