From 75d39cd1f8b600ad3295754fe63017af84801620 Mon Sep 17 00:00:00 2001 From: Allan Date: Fri, 12 Jun 2026 18:21:37 -0300 Subject: [PATCH] refactor: padronizar dominios service-types e documents --- .../controllers/ServiceAreaController.java | 2 +- .../ServiceAreaControllerTest.java | 19 +++ .../service-types/[id]/edit/page.tsx | 5 + .../app/(protected)/service-types/page.tsx | 5 + .../tipo-atendimento/[id]/edit/page.tsx | 119 ------------------ .../app/(protected)/tipo-atendimento/page.tsx | 103 --------------- .../app/api/patients/[id]/documents/route.ts | 105 ++++++++++++++++ .../src/app/api/service-types/[id]/route.ts | 57 +++++++++ .../route.ts | 25 ++-- .../app/api/tipo-atendimento/[id]/route.ts | 33 ----- .../components/AnnualRegistryEditModal.tsx | 34 ++--- .../src/components/ui/fileViewer.tsx | 45 +++---- .../src/domains/documents/documents.api.ts | 55 ++++++++ .../src/domains/documents/documents.types.ts | 23 ++++ .../components/service-type-list-item.tsx} | 25 ++-- .../hooks/use-edit-service-type.ts | 57 +++++++++ .../service-types/hooks/use-service-types.ts | 79 ++++++++++++ .../pages/edit-service-type-page.tsx | 71 +++++++++++ .../pages/service-types-page.tsx | 117 +++++++++++++++++ .../service-types/service-types.api.ts | 55 ++++++++ .../service-types/service-types.schema.ts | 12 ++ .../service-types/service-types.types.ts | 10 ++ apps/management-app/src/lib/axios.ts | 27 ++++ apps/management-app/src/lib/routes.ts | 2 +- .../src/schemas/service-type-schemas.ts | 27 ++-- 25 files changed, 765 insertions(+), 347 deletions(-) create mode 100644 apps/management-app/src/app/(protected)/service-types/[id]/edit/page.tsx create mode 100644 apps/management-app/src/app/(protected)/service-types/page.tsx delete mode 100644 apps/management-app/src/app/(protected)/tipo-atendimento/[id]/edit/page.tsx delete mode 100644 apps/management-app/src/app/(protected)/tipo-atendimento/page.tsx create mode 100644 apps/management-app/src/app/api/patients/[id]/documents/route.ts create mode 100644 apps/management-app/src/app/api/service-types/[id]/route.ts rename apps/management-app/src/app/api/{tipo-atendimento => service-types}/route.ts (62%) delete mode 100644 apps/management-app/src/app/api/tipo-atendimento/[id]/route.ts create mode 100644 apps/management-app/src/domains/documents/documents.api.ts create mode 100644 apps/management-app/src/domains/documents/documents.types.ts rename apps/management-app/src/{app/(protected)/tipo-atendimento/service-type-item.tsx => domains/service-types/components/service-type-list-item.tsx} (57%) create mode 100644 apps/management-app/src/domains/service-types/hooks/use-edit-service-type.ts create mode 100644 apps/management-app/src/domains/service-types/hooks/use-service-types.ts create mode 100644 apps/management-app/src/domains/service-types/pages/edit-service-type-page.tsx create mode 100644 apps/management-app/src/domains/service-types/pages/service-types-page.tsx create mode 100644 apps/management-app/src/domains/service-types/service-types.api.ts create mode 100644 apps/management-app/src/domains/service-types/service-types.schema.ts create mode 100644 apps/management-app/src/domains/service-types/service-types.types.ts create mode 100644 apps/management-app/src/lib/axios.ts diff --git a/apps/api/src/main/java/br/org/apae/api/servicearea/interfaces/controllers/ServiceAreaController.java b/apps/api/src/main/java/br/org/apae/api/servicearea/interfaces/controllers/ServiceAreaController.java index 2484418c2..8009a2251 100644 --- a/apps/api/src/main/java/br/org/apae/api/servicearea/interfaces/controllers/ServiceAreaController.java +++ b/apps/api/src/main/java/br/org/apae/api/servicearea/interfaces/controllers/ServiceAreaController.java @@ -18,7 +18,7 @@ import java.util.List; -@RequestMapping("/service-areas") +@RequestMapping({ "/service-areas", "/service-types" }) public interface ServiceAreaController { @Operation(summary = "Cadastrar área de atendimento", description = "Cria uma nova área de atendimento no sistema.", responses = { diff --git a/apps/api/src/test/java/br/org/apae/api/controllers/servicearea/ServiceAreaControllerTest.java b/apps/api/src/test/java/br/org/apae/api/controllers/servicearea/ServiceAreaControllerTest.java index cbbd25207..53a94006f 100644 --- a/apps/api/src/test/java/br/org/apae/api/controllers/servicearea/ServiceAreaControllerTest.java +++ b/apps/api/src/test/java/br/org/apae/api/controllers/servicearea/ServiceAreaControllerTest.java @@ -40,6 +40,7 @@ class ServiceAreaControllerTest { private static final String URI = "/service-areas"; + private static final String SERVICE_TYPES_URI = "/service-types"; @Autowired private MockMvc mockMvc; @@ -150,6 +151,24 @@ void shouldReturnEmptyListWhenThereAreNoServiceAreas() throws Exception { verify(service).findAllServiceAreas(); } + + @Test + @DisplayName("Deve listar areas de atendimento pela rota padronizada /service-types") + void shouldGetAllServiceAreasThroughServiceTypesRoute() throws Exception { + List response = List.of( + new ServiceAreaResponseDTO(1, "Fisioterapia") + ); + + when(service.findAllServiceAreas()).thenReturn(response); + + mockMvc.perform(get(SERVICE_TYPES_URI)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(1)) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].area").value("Fisioterapia")); + + verify(service).findAllServiceAreas(); + } } @Nested diff --git a/apps/management-app/src/app/(protected)/service-types/[id]/edit/page.tsx b/apps/management-app/src/app/(protected)/service-types/[id]/edit/page.tsx new file mode 100644 index 000000000..2d2594098 --- /dev/null +++ b/apps/management-app/src/app/(protected)/service-types/[id]/edit/page.tsx @@ -0,0 +1,5 @@ +import { EditServiceTypePage } from "@/domains/service-types/pages/edit-service-type-page"; + +export default function Page() { + return ; +} diff --git a/apps/management-app/src/app/(protected)/service-types/page.tsx b/apps/management-app/src/app/(protected)/service-types/page.tsx new file mode 100644 index 000000000..166ce1878 --- /dev/null +++ b/apps/management-app/src/app/(protected)/service-types/page.tsx @@ -0,0 +1,5 @@ +import { ServiceTypesPage } from "@/domains/service-types/pages/service-types-page"; + +export default function Page() { + return ; +} diff --git a/apps/management-app/src/app/(protected)/tipo-atendimento/[id]/edit/page.tsx b/apps/management-app/src/app/(protected)/tipo-atendimento/[id]/edit/page.tsx deleted file mode 100644 index 0b039cdcc..000000000 --- a/apps/management-app/src/app/(protected)/tipo-atendimento/[id]/edit/page.tsx +++ /dev/null @@ -1,119 +0,0 @@ -"use client"; - -import { useRouter, useParams } from "next/navigation"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { updateserviceTypeSchema, UpdateserviceTypeDTO } from "@/schemas/service-type-schemas"; -import { toast } from "react-toastify"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useEffect } from "react"; -import { Loader2, ArrowLeft } from "lucide-react"; - -export default function EditServiceTypePage() { - const router = useRouter(); - const params = useParams(); - const { id } = params; - - const { - register, - handleSubmit, - setValue, - formState: { errors, isSubmitting }, - } = useForm({ - resolver: zodResolver(updateserviceTypeSchema), - }); - - useEffect(() => { - if (id) { - const fetchserviceType = async () => { - try { - const response = await fetch(`/api/service-types/${id}`); - if (!response.ok) throw new Error("Tipo de atendimento não encontrado."); - const data = await response.json(); - setValue("area", data.area); - } catch (error: any) { - toast.error(error.message); - router.push("/tipo-atendimento"); - } - }; - fetchserviceType(); - } - }, [id, setValue, router]); - - const onSubmit = async (data: UpdateserviceTypeDTO) => { - try { - const response = await fetch(`/api/service-types/${id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data), - }); - if (!response.ok) { - throw new Error("Falha ao atualizar o tipo de atendimento."); - } - toast.success("Tipo de atendimento atualizado com sucesso!"); - router.push("/tipo-atendimento"); - router.refresh(); - } catch (error: any) { - toast.error(error.message); - } - }; - - if (!id) { - return ( -
- -
- ); - } - - - return ( -
-
-
- - -

Editar tipo de atendimento

- -
-
- - - {errors.area && ( -

{errors.area.message}

- )} -
- -
- - -
-
-
-
-
- ); -} \ No newline at end of file diff --git a/apps/management-app/src/app/(protected)/tipo-atendimento/page.tsx b/apps/management-app/src/app/(protected)/tipo-atendimento/page.tsx deleted file mode 100644 index 7d4756e3f..000000000 --- a/apps/management-app/src/app/(protected)/tipo-atendimento/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import { toast } from "react-toastify"; -import { ServiceTypeListItemItem } from "./service-type-item"; -import { ServiceType } from "@/schemas/service-type-schemas"; -import { Loader2 } from "lucide-react"; -import { SearchFilters } from "@/components/search-filters"; - -export default function ServiceTypesPage() { - const router = useRouter(); - const [serviceTypes, setServiceTypes] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - const [searchName, setSearchName] = useState(""); - - useEffect(() => { - async function fetchserviceTypes() { - try { - setIsLoading(true); - setError(null); - const response = await fetch("/api/service-types"); - if (!response.ok) { - throw new Error("Falha ao buscar os tipos de atendimentos."); - } - const data = await response.json(); - setServiceTypes(data); - } catch (err: any) { - setError(err.message); - toast.error(err.message); - } finally { - setIsLoading(false); - } - } - fetchserviceTypes(); - }, []); - - const filteredserviceTypes = serviceTypes.filter((serviceType) => - serviceType.area.toLowerCase().includes(searchName.toLowerCase()) - ); - - const renderContent = () => { - if (isLoading) { - return ( -
- -
- ); - } - if (error) { - return

{error}

; - } - if (filteredserviceTypes.length === 0) { - return

Nenhum tipo de atendimento encontrado.

; - } - return ( -
- {filteredserviceTypes.map((service) => ( - router.push(`/tipo-atendimento/${service.id}/edit`)} - /> - ))} -
- ); - }; - - return ( -
-
-
- -
- -
-
-

- Tipos de atendimentos cadastrados -

-
- -
-

- Tipos de atendimentos cadastrados -

-
- -

- {filteredserviceTypes.length} tipos de atendimentos encontrados -

- {renderContent()} -
- -
-
- ); -} \ No newline at end of file diff --git a/apps/management-app/src/app/api/patients/[id]/documents/route.ts b/apps/management-app/src/app/api/patients/[id]/documents/route.ts new file mode 100644 index 000000000..80b460f60 --- /dev/null +++ b/apps/management-app/src/app/api/patients/[id]/documents/route.ts @@ -0,0 +1,105 @@ +import { AxiosError } from "axios"; +import { NextRequest, NextResponse } from "next/server"; + +import { createBaseApi } from "@/lib/axios"; + +const categoryRouteMap: Record = { + medical: "medicals", + medicals: "medicals", + medicos: "medicals", + personal: "personals", + personals: "personals", + pessoais: "personals", + school: "schools", + schools: "schools", + escolares: "schools", +}; + +function normalizeCategory(category: string | null) { + if (!category) { + return null; + } + + return categoryRouteMap[category.toLowerCase()] || null; +} + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params; + const category = normalizeCategory(request.nextUrl.searchParams.get("category")); + const year = request.nextUrl.searchParams.get("year"); + const type = request.nextUrl.searchParams.get("type"); + + if (!category) { + return NextResponse.json({ message: "Categoria de documento invalida." }, { status: 400 }); + } + + try { + const api = await createBaseApi(); + const searchParams = new URLSearchParams(); + + if (year) { + searchParams.set("year", year); + } + + const suffix = searchParams.size > 0 ? `?${searchParams.toString()}` : ""; + const response = await api.get(`/patients/${id}/documents/${category}${suffix}`); + const documents = Array.isArray(response.data) ? response.data : []; + const filteredDocuments = type + ? documents.filter((document: { type?: string }) => document.type === type) + : documents; + + return NextResponse.json(filteredDocuments); + } catch (error) { + const err = error as AxiosError; + return NextResponse.json( + { message: err.response?.data || "Erro ao buscar documentos" }, + { status: err.response?.status || 500 }, + ); + } +} + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params; + + try { + const incomingFormData = await request.formData(); + const file = incomingFormData.get("file"); + const category = incomingFormData.get("category"); + const type = incomingFormData.get("type"); + const year = incomingFormData.get("year"); + + if (!(file instanceof File) || typeof category !== "string" || typeof type !== "string") { + return NextResponse.json({ message: "Dados de upload invalidos." }, { status: 400 }); + } + + const formData = new FormData(); + formData.append("file", file); + formData.append("category", category); + formData.append("type", type); + + if (typeof year === "string" && year.trim()) { + formData.append("year", year); + } + + const api = await createBaseApi(); + const response = await api.post(`/patients/${id}/documents`, formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + + return NextResponse.json(response.data, { status: response.status }); + } catch (error) { + const err = error as AxiosError; + return NextResponse.json( + { message: err.response?.data || "Erro ao enviar documento" }, + { status: err.response?.status || 500 }, + ); + } +} diff --git a/apps/management-app/src/app/api/service-types/[id]/route.ts b/apps/management-app/src/app/api/service-types/[id]/route.ts new file mode 100644 index 000000000..3d3a8392f --- /dev/null +++ b/apps/management-app/src/app/api/service-types/[id]/route.ts @@ -0,0 +1,57 @@ +import { NextResponse } from "next/server"; + +import { updateServiceTypeSchema } from "@/domains/service-types/service-types.schema"; +import { createBaseApi } from "@/lib/axios"; + +interface RouteParams { + params: Promise<{ id: string }>; +} + +export async function GET(request: Request, { params }: RouteParams) { + try { + const { id } = await params; + const api = await createBaseApi(); + const { data } = await api.get(`/service-types/${id}`); + return NextResponse.json(data); + } catch (error: any) { + return new NextResponse( + JSON.stringify(error.response?.data || { message: error.message }), + { status: error.response?.status || 500 }, + ); + } +} + +export async function PUT(request: Request, { params }: RouteParams) { + try { + const { id } = await params; + const body = await request.json(); + const validation = updateServiceTypeSchema.safeParse(body); + + if (!validation.success) { + return NextResponse.json({ errors: validation.error.flatten() }, { status: 400 }); + } + + const api = await createBaseApi(); + const { data } = await api.put(`/service-types/${id}`, validation.data); + return NextResponse.json(data); + } catch (error: any) { + return new NextResponse( + JSON.stringify(error.response?.data || { message: error.message }), + { status: error.response?.status || 500 }, + ); + } +} + +export async function DELETE(request: Request, { params }: RouteParams) { + try { + const { id } = await params; + const api = await createBaseApi(); + await api.delete(`/service-types/${id}`); + return new NextResponse(null, { status: 204 }); + } catch (error: any) { + return new NextResponse( + JSON.stringify(error.response?.data || { message: error.message }), + { status: error.response?.status || 500 }, + ); + } +} diff --git a/apps/management-app/src/app/api/tipo-atendimento/route.ts b/apps/management-app/src/app/api/service-types/route.ts similarity index 62% rename from apps/management-app/src/app/api/tipo-atendimento/route.ts rename to apps/management-app/src/app/api/service-types/route.ts index b971deadd..44712905d 100644 --- a/apps/management-app/src/app/api/tipo-atendimento/route.ts +++ b/apps/management-app/src/app/api/service-types/route.ts @@ -1,31 +1,30 @@ -import { createBaseApi } from "@/lib/axios"; -import { createserviceTypeSchema } from "@/schemas/service-type-schemas"; import { NextResponse } from "next/server"; +import { createBaseApi } from "@/lib/axios"; +import { createServiceTypeSchema } from "@/domains/service-types/service-types.schema"; + export async function POST(request: Request) { try { const body = await request.json(); + const validation = createServiceTypeSchema.safeParse(body); - const validation = createserviceTypeSchema.safeParse(body); if (!validation.success) { return new NextResponse( JSON.stringify({ - message: "Dados inválidos.", + message: "Dados invalidos.", errors: validation.error.flatten().fieldErrors, }), - { status: 400 } + { status: 400 }, ); } const api = await createBaseApi(); - - const { data } = await api.post("/service-areas", validation.data); - - return NextResponse.json(data); + const { data } = await api.post("/service-types", validation.data); + return NextResponse.json(data, { status: 201 }); } catch (error: any) { return new NextResponse( JSON.stringify(error.response?.data || { message: error.message }), - { status: error.response?.status || 500 } + { status: error.response?.status || 500 }, ); } } @@ -33,12 +32,12 @@ export async function POST(request: Request) { export async function GET() { try { const api = await createBaseApi(); - const { data } = await api.get("/service-areas"); + const { data } = await api.get("/service-types"); return NextResponse.json(data); } catch (error: any) { return new NextResponse( JSON.stringify(error.response?.data || { message: error.message }), - { status: error.response?.status || 500 } + { status: error.response?.status || 500 }, ); } -} \ No newline at end of file +} diff --git a/apps/management-app/src/app/api/tipo-atendimento/[id]/route.ts b/apps/management-app/src/app/api/tipo-atendimento/[id]/route.ts deleted file mode 100644 index 85be4fe05..000000000 --- a/apps/management-app/src/app/api/tipo-atendimento/[id]/route.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createBaseApi } from "@/lib/axios"; -import { updateserviceTypeSchema } from "@/schemas/service-type-schemas"; -import { NextResponse } from "next/server"; - -interface IParams { - params: Promise<{ id: string }>; -} - -export async function GET(request: Request, { params }: IParams) { - try { - const { id } = await params; - const api = await createBaseApi(); - const { data } = await api.get(`/service-areas/${id}`); - return NextResponse.json(data); - } catch (error: any) { - return new NextResponse(JSON.stringify(error.response?.data), { status: 500 }); - } -} - -export async function PUT(request: Request, { params }: IParams) { - try { - const { id } = await params; - const body = await request.json(); - const validation = updateserviceTypeSchema.safeParse(body); - if (!validation.success) return NextResponse.json({ errors: validation.error.flatten() }, { status: 400 }); - - const api = await createBaseApi(); - const { data } = await api.put(`/service-areas/${id}`, validation.data); - return NextResponse.json(data); - } catch (error: any) { - return new NextResponse(JSON.stringify(error.response?.data), { status: 500 }); - } -} \ No newline at end of file diff --git a/apps/management-app/src/components/AnnualRegistryEditModal.tsx b/apps/management-app/src/components/AnnualRegistryEditModal.tsx index cb8ac26cc..ba09069f5 100644 --- a/apps/management-app/src/components/AnnualRegistryEditModal.tsx +++ b/apps/management-app/src/components/AnnualRegistryEditModal.tsx @@ -14,14 +14,8 @@ import { Loader2, Upload, FileText, RefreshCw, ExternalLink } from "lucide-react import { StringMultiSelect } from "@/components/StringMultiSelect"; import { GenericDatabaseSelect } from "@/components/GenericDatabaseSelect"; - -interface DocumentDTO { - id: string; - name: string; - category: string; - type: string; - url: string; -} +import { listPatientDocuments, uploadPatientDocument } from "@/domains/documents/documents.api"; +import type { PatientDocument } from "@/domains/documents/documents.types"; interface AnnualRegistryEditModalProps { isOpen: boolean; @@ -48,7 +42,7 @@ export default function AnnualRegistryEditModal({ mode = "edit" }: AnnualRegistryEditModalProps) { - const [documents, setDocuments] = useState([]); + const [documents, setDocuments] = useState([]); const [isLoadingDocs, setIsLoadingDocs] = useState(false); const [isUploading, setIsUploading] = useState(false); const [docType, setDocType] = useState("MEDICAL_REPORT"); @@ -139,18 +133,15 @@ export default function AnnualRegistryEditModal({ const fetchDocuments = async () => { setIsLoadingDocs(true); try { - const response = await fetch(`/api/patients/${patientId}/documents?category=medicos`); - if (response.ok) { - const data = await response.json().catch(() => []); - setDocuments(Array.isArray(data) ? data : []); - } + const data = await listPatientDocuments(patientId, { category: "medical" }); + setDocuments(Array.isArray(data) ? data : []); } catch (error) { console.error("Erro fetch docs:", error); } finally { setIsLoadingDocs(false); } }; const fetchPatientData = async () => { try { - const response = await fetch(`/api/patients/${patientId}`); + const response = await fetch(`/api/pessoas/${patientId}`); if (response.ok) setFullPatientData(await response.json()); } catch (error) { console.error(error); } }; @@ -159,13 +150,12 @@ export default function AnnualRegistryEditModal({ const file = e.target.files?.[0]; if (!file) return; setIsUploading(true); - const formData = new FormData(); - formData.append("file", file); - formData.append("category", "MEDICAL"); - formData.append("type", docType); try { - const res = await fetch(`/api/patients/${patientId}/documents`, { method: "POST", body: formData }); - if (!res.ok) throw new Error("Falha no upload"); + await uploadPatientDocument(patientId, { + file, + category: "MEDICAL", + type: docType, + }); toast.success("Documento anexado!"); fetchDocuments(); if (fileInputRef.current) fileInputRef.current.value = ""; @@ -265,7 +255,7 @@ export default function AnnualRegistryEditModal({ })) ?? [] }; - await fetch(`/api/patients/${patientId}`, { + await fetch(`/api/pessoas/${patientId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patientPayload) }); } diff --git a/apps/management-app/src/components/ui/fileViewer.tsx b/apps/management-app/src/components/ui/fileViewer.tsx index cac373ff2..728bdecc4 100644 --- a/apps/management-app/src/components/ui/fileViewer.tsx +++ b/apps/management-app/src/components/ui/fileViewer.tsx @@ -6,17 +6,10 @@ import { ArrowLeft } from "lucide-react"; import { Button } from "@/components/ui/button"; import FileCard from "@/components/fileCard"; import { toast } from "react-toastify"; +import { listPatientDocuments } from "@/domains/documents/documents.api"; +import type { DocumentCategory, PatientDocument } from "@/domains/documents/documents.types"; -export interface FileItem { - id: string; - name: string; - category: "personal" | "medical" | "school"; - type: string; - url: string; - year: string; -} - -const documentCategory = { +const documentCategory: Record = { personal: "Documentos pessoais", medical: "Documentos médicos", school: "Documentos escolares", @@ -26,35 +19,25 @@ export default function FileViewer() { const router = useRouter(); const params = useParams(); const patientId = params?.id as string; - const category = params?.type as "personal" | "medical" | "school"; + const category = params?.type as DocumentCategory; const [yearFilter, setYearFilter] = React.useState( new Date().getFullYear().toString() ); - const [typeFilter, setTypeFilter] = React.useState("LAUDO"); - const [files, setFiles] = React.useState([]); + const [typeFilter] = React.useState(""); + const [files, setFiles] = React.useState([]); React.useEffect(() => { async function fetchDocuments() { try { if (!patientId || !category || !yearFilter) return; - const params = new URLSearchParams({ - category: category, - year: yearFilter, - ...(typeFilter && { type: typeFilter }), + const data = await listPatientDocuments(patientId, { + category, + year: yearFilter, + ...(typeFilter ? { type: typeFilter } : {}), }); - const response = await fetch( - `/api/pessoa/${patientId}/documents?${params.toString()}` - ); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Erro ao buscar os documentos"); - } - - const data = await response.json(); - setFiles(data.urls); + setFiles(data); } catch (err: any) { console.error("Erro ao buscar documentos:", err); @@ -100,14 +83,14 @@ export default function FileViewer() { {files.length === 0 ? (

Nenhum arquivo encontrado.

) : ( - files.map((file: any, index: number) => ( + files.map((file, index: number) => ( )) )} ); -} \ No newline at end of file +} diff --git a/apps/management-app/src/domains/documents/documents.api.ts b/apps/management-app/src/domains/documents/documents.api.ts new file mode 100644 index 000000000..95bc9ca71 --- /dev/null +++ b/apps/management-app/src/domains/documents/documents.api.ts @@ -0,0 +1,55 @@ +import type { + ListPatientDocumentsParams, + PatientDocument, + UploadPatientDocumentParams, +} from "./documents.types"; + +async function parseResponse(response: Response): Promise { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || "Erro ao processar documentos."); + } + + return response.json(); +} + +export async function listPatientDocuments( + patientId: string, + params: ListPatientDocumentsParams, +) { + const searchParams = new URLSearchParams({ + category: params.category, + }); + + if (params.year) { + searchParams.set("year", String(params.year)); + } + + if (params.type) { + searchParams.set("type", params.type); + } + + const response = await fetch(`/api/patients/${patientId}/documents?${searchParams.toString()}`); + return parseResponse(response); +} + +export async function uploadPatientDocument( + patientId: string, + params: UploadPatientDocumentParams, +) { + const formData = new FormData(); + formData.append("file", params.file); + formData.append("category", params.category); + formData.append("type", params.type); + + if (params.year) { + formData.append("year", String(params.year)); + } + + const response = await fetch(`/api/patients/${patientId}/documents`, { + method: "POST", + body: formData, + }); + + return parseResponse(response); +} diff --git a/apps/management-app/src/domains/documents/documents.types.ts b/apps/management-app/src/domains/documents/documents.types.ts new file mode 100644 index 000000000..8044e2f76 --- /dev/null +++ b/apps/management-app/src/domains/documents/documents.types.ts @@ -0,0 +1,23 @@ +export type DocumentCategory = "medical" | "personal" | "school"; + +export interface PatientDocument { + id: string; + name: string; + category: string; + type: string; + url: string; + year: string | number; +} + +export interface ListPatientDocumentsParams { + category: DocumentCategory; + type?: string; + year?: string | number; +} + +export interface UploadPatientDocumentParams { + category: string; + file: File; + type: string; + year?: string | number; +} diff --git a/apps/management-app/src/app/(protected)/tipo-atendimento/service-type-item.tsx b/apps/management-app/src/domains/service-types/components/service-type-list-item.tsx similarity index 57% rename from apps/management-app/src/app/(protected)/tipo-atendimento/service-type-item.tsx rename to apps/management-app/src/domains/service-types/components/service-type-list-item.tsx index 9c73fb7a0..309019422 100644 --- a/apps/management-app/src/app/(protected)/tipo-atendimento/service-type-item.tsx +++ b/apps/management-app/src/domains/service-types/components/service-type-list-item.tsx @@ -1,18 +1,18 @@ "use client"; +import { Edit, Trash2 } from "lucide-react"; + import { Button } from "@/components/ui/button"; -import { Edit } from "lucide-react"; -import { ServiceType } from "@/schemas/service-type-schemas"; + +import type { ServiceType } from "../service-types.types"; interface ServiceTypeListItemProps { - service: ServiceType; + onDelete: (service: ServiceType) => void; onEdit: () => void; + service: ServiceType; } -export function ServiceTypeListItemItem({ - service, - onEdit, -}: ServiceTypeListItemProps) { +export function ServiceTypeListItem({ service, onEdit, onDelete }: ServiceTypeListItemProps) { return (
@@ -28,7 +28,16 @@ export function ServiceTypeListItemItem({ > +
); -} \ No newline at end of file +} diff --git a/apps/management-app/src/domains/service-types/hooks/use-edit-service-type.ts b/apps/management-app/src/domains/service-types/hooks/use-edit-service-type.ts new file mode 100644 index 000000000..0a3ccc390 --- /dev/null +++ b/apps/management-app/src/domains/service-types/hooks/use-edit-service-type.ts @@ -0,0 +1,57 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { toast } from "react-toastify"; + +import { getServiceType, updateServiceType } from "../service-types.api"; +import { updateServiceTypeSchema } from "../service-types.schema"; +import type { UpdateServiceTypeDTO } from "../service-types.types"; + +export function useEditServiceType(id: string | undefined) { + const router = useRouter(); + const form = useForm({ + resolver: zodResolver(updateServiceTypeSchema), + }); + + useEffect(() => { + async function loadServiceType(serviceTypeId: string) { + try { + const data = await getServiceType(serviceTypeId); + form.setValue("area", data.area); + } catch (error) { + const message = error instanceof Error ? error.message : "Tipo de atendimento nao encontrado."; + toast.error(message); + router.push("/service-types"); + } + } + + if (id) { + loadServiceType(id); + } + }, [form, id, router]); + + async function onSubmit(data: UpdateServiceTypeDTO) { + if (!id) { + return; + } + + try { + await updateServiceType(id, data); + toast.success("Tipo de atendimento atualizado com sucesso!"); + router.push("/service-types"); + router.refresh(); + } catch (error) { + const message = error instanceof Error ? error.message : "Falha ao atualizar o tipo de atendimento."; + toast.error(message); + } + } + + return { + form, + onSubmit, + router, + }; +} diff --git a/apps/management-app/src/domains/service-types/hooks/use-service-types.ts b/apps/management-app/src/domains/service-types/hooks/use-service-types.ts new file mode 100644 index 000000000..55cfbee70 --- /dev/null +++ b/apps/management-app/src/domains/service-types/hooks/use-service-types.ts @@ -0,0 +1,79 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; + +import { + createServiceType, + deleteServiceType, + listServiceTypes, +} from "../service-types.api"; +import type { ServiceType } from "../service-types.types"; + +export function useServiceTypes() { + const [serviceTypes, setServiceTypes] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + const [searchName, setSearchName] = useState(""); + + async function loadServiceTypes() { + try { + setIsLoading(true); + setError(null); + setServiceTypes(await listServiceTypes()); + } catch (error) { + const message = error instanceof Error ? error.message : "Falha ao buscar os tipos de atendimento."; + setError(message); + toast.error(message); + } finally { + setIsLoading(false); + } + } + + useEffect(() => { + loadServiceTypes(); + }, []); + + async function handleCreateServiceType(area: string) { + try { + setIsSaving(true); + await createServiceType({ area }); + toast.success("Tipo de atendimento criado com sucesso!"); + await loadServiceTypes(); + } catch (error) { + const message = error instanceof Error ? error.message : "Falha ao criar tipo de atendimento."; + toast.error(message); + throw error; + } finally { + setIsSaving(false); + } + } + + async function handleDeleteServiceType(id: string | number) { + try { + await deleteServiceType(id); + setServiceTypes((current) => current.filter((serviceType) => serviceType.id !== id)); + toast.success("Tipo de atendimento removido com sucesso!"); + } catch (error) { + const message = error instanceof Error ? error.message : "Falha ao remover tipo de atendimento."; + toast.error(message); + throw error; + } + } + + const filteredServiceTypes = serviceTypes.filter((serviceType) => + serviceType.area.toLowerCase().includes(searchName.toLowerCase()), + ); + + return { + error, + filteredServiceTypes, + isLoading, + isSaving, + searchName, + setSearchName, + createServiceType: handleCreateServiceType, + deleteServiceType: handleDeleteServiceType, + }; +} diff --git a/apps/management-app/src/domains/service-types/pages/edit-service-type-page.tsx b/apps/management-app/src/domains/service-types/pages/edit-service-type-page.tsx new file mode 100644 index 000000000..779f18f4b --- /dev/null +++ b/apps/management-app/src/domains/service-types/pages/edit-service-type-page.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { ArrowLeft, Loader2 } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +import { useEditServiceType } from "../hooks/use-edit-service-type"; + +export function EditServiceTypePage() { + const params = useParams(); + const id = typeof params.id === "string" ? params.id : undefined; + const { form, onSubmit, router } = useEditServiceType(id); + const { + handleSubmit, + register, + formState: { errors, isSubmitting }, + } = form; + + if (!id) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+ + +

Editar tipo de atendimento

+ +
+
+ + + {errors.area &&

{errors.area.message}

} +
+ +
+ + +
+
+
+
+
+ ); +} diff --git a/apps/management-app/src/domains/service-types/pages/service-types-page.tsx b/apps/management-app/src/domains/service-types/pages/service-types-page.tsx new file mode 100644 index 000000000..09b35239b --- /dev/null +++ b/apps/management-app/src/domains/service-types/pages/service-types-page.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { FormEvent, useState } from "react"; +import { useRouter } from "next/navigation"; +import { Loader2, Plus } from "lucide-react"; + +import { SearchFilters } from "@/components/search-filters"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +import { useServiceTypes } from "../hooks/use-service-types"; +import { ServiceTypeListItem } from "../components/service-type-list-item"; +import type { ServiceType } from "../service-types.types"; + +export function ServiceTypesPage() { + const router = useRouter(); + const [newArea, setNewArea] = useState(""); + const { + createServiceType, + deleteServiceType, + error, + filteredServiceTypes, + isLoading, + isSaving, + searchName, + setSearchName, + } = useServiceTypes(); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + + const normalizedArea = newArea.trim(); + if (!normalizedArea) { + return; + } + + await createServiceType(normalizedArea); + setNewArea(""); + } + + async function handleDelete(service: ServiceType) { + const shouldDelete = window.confirm(`Deseja remover o tipo de atendimento \"${service.area}\"?`); + + if (!shouldDelete) { + return; + } + + await deleteServiceType(service.id); + } + + function renderContent() { + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return

{error}

; + } + + if (filteredServiceTypes.length === 0) { + return

Nenhum tipo de atendimento encontrado.

; + } + + return ( +
+ {filteredServiceTypes.map((service) => ( + router.push(`/service-types/${service.id}/edit`)} + onDelete={handleDelete} + /> + ))} +
+ ); + } + + return ( +
+
+
+ +
+ setNewArea(event.target.value)} + placeholder="Novo tipo de atendimento" + /> + +
+
+ +
+
+

Tipos de atendimento cadastrados

+
+ +
+

Tipos de atendimento cadastrados

+
+ +

+ {filteredServiceTypes.length} tipos de atendimento encontrados +

+ {renderContent()} +
+
+
+ ); +} diff --git a/apps/management-app/src/domains/service-types/service-types.api.ts b/apps/management-app/src/domains/service-types/service-types.api.ts new file mode 100644 index 000000000..7493781c9 --- /dev/null +++ b/apps/management-app/src/domains/service-types/service-types.api.ts @@ -0,0 +1,55 @@ +import type { + CreateServiceTypeDTO, + ServiceType, + UpdateServiceTypeDTO, +} from "./service-types.types"; + +async function parseResponse(response: Response): Promise { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || "Erro ao processar tipos de atendimento."); + } + + return response.json(); +} + +export async function listServiceTypes() { + const response = await fetch("/api/service-types"); + return parseResponse(response); +} + +export async function getServiceType(id: string) { + const response = await fetch(`/api/service-types/${id}`); + return parseResponse(response); +} + +export async function createServiceType(payload: CreateServiceTypeDTO) { + const response = await fetch("/api/service-types", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + return parseResponse(response); +} + +export async function updateServiceType(id: string, payload: UpdateServiceTypeDTO) { + const response = await fetch(`/api/service-types/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + return parseResponse(response); +} + +export async function deleteServiceType(id: string | number) { + const response = await fetch(`/api/service-types/${id}`, { + method: "DELETE", + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || "Erro ao remover tipo de atendimento."); + } +} diff --git a/apps/management-app/src/domains/service-types/service-types.schema.ts b/apps/management-app/src/domains/service-types/service-types.schema.ts new file mode 100644 index 000000000..4523c50ed --- /dev/null +++ b/apps/management-app/src/domains/service-types/service-types.schema.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +export const serviceTypeSchema = z.object({ + id: z.union([z.string(), z.number()]), + area: z.string(), +}); + +export const createServiceTypeSchema = z.object({ + area: z.string().min(1, "A area e obrigatoria."), +}); + +export const updateServiceTypeSchema = createServiceTypeSchema; diff --git a/apps/management-app/src/domains/service-types/service-types.types.ts b/apps/management-app/src/domains/service-types/service-types.types.ts new file mode 100644 index 000000000..38b48d811 --- /dev/null +++ b/apps/management-app/src/domains/service-types/service-types.types.ts @@ -0,0 +1,10 @@ +export interface ServiceType { + id: string | number; + area: string; +} + +export interface CreateServiceTypeDTO { + area: string; +} + +export type UpdateServiceTypeDTO = CreateServiceTypeDTO; diff --git a/apps/management-app/src/lib/axios.ts b/apps/management-app/src/lib/axios.ts new file mode 100644 index 000000000..e30dfff8b --- /dev/null +++ b/apps/management-app/src/lib/axios.ts @@ -0,0 +1,27 @@ +import axios from "axios"; +import { cookies } from "next/headers"; + +const LOCAL_API_BASE_URL = "http://localhost:8090/apae-geral/api"; + +function trimTrailingSlash(baseURL?: string) { + return baseURL?.replace(/\/+$/, ""); +} + +function getBaseApiURL() { + return ( + trimTrailingSlash(process.env.API_URL) || + trimTrailingSlash(process.env.NEXT_PUBLIC_API_URL) || + LOCAL_API_BASE_URL + ); +} + +export async function createBaseApi() { + const session = (await cookies()).get("session")?.value; + + return axios.create({ + baseURL: getBaseApiURL(), + headers: { + ...(session ? { Authorization: `Bearer ${session}` } : {}), + }, + }); +} diff --git a/apps/management-app/src/lib/routes.ts b/apps/management-app/src/lib/routes.ts index 161e0747c..99f82c4a0 100644 --- a/apps/management-app/src/lib/routes.ts +++ b/apps/management-app/src/lib/routes.ts @@ -5,5 +5,5 @@ export const NAV = [ { label: "Dashboard", href: "/dashboard", icon: BarChart }, { label: "Transtornos", href: "/transtornos", icon: SquareActivity}, { label: "Vacinas", href: "/vaccines", icon: Syringe}, - { label: "Tipos de Atendimento", href: "/tipo-atendimento", icon: BriefcaseMedical}, + { label: "Tipos de Atendimento", href: "/service-types", icon: BriefcaseMedical}, ] diff --git a/apps/management-app/src/schemas/service-type-schemas.ts b/apps/management-app/src/schemas/service-type-schemas.ts index b57929552..d23f7b238 100644 --- a/apps/management-app/src/schemas/service-type-schemas.ts +++ b/apps/management-app/src/schemas/service-type-schemas.ts @@ -1,16 +1,11 @@ -import { z } from "zod"; - -export const serviceTypeSchema = z.object({ - id: z.string(), - area: z.string(), -}); - -export const createserviceTypeSchema = z.object({ - area: z.string().min(1, "A área é obrigatória."), -}); - -export const updateserviceTypeSchema = createserviceTypeSchema; - -export type ServiceType = z.infer; -export type CreateserviceTypeDTO = z.infer; -export type UpdateserviceTypeDTO = z.infer; \ No newline at end of file +export { + createServiceTypeSchema, + serviceTypeSchema, + updateServiceTypeSchema, +} from "@/domains/service-types/service-types.schema"; + +export type { + CreateServiceTypeDTO, + ServiceType, + UpdateServiceTypeDTO, +} from "@/domains/service-types/service-types.types";