diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx index 0e2cf70..36f0e98 100644 --- a/app/doctor/disponibilidade/page.tsx +++ b/app/doctor/disponibilidade/page.tsx @@ -21,6 +21,8 @@ import { Eye, Edit, Calendar, Trash2 } from "lucide-react"; import { AvailabilityEditModal } from "@/components/ui/availability-edit-modal"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; +// ... (Interfaces de tipo omitidas para brevidade, pois não foram alteradas) + interface UserPermissions { isAdmin: boolean; isManager: boolean; @@ -52,32 +54,32 @@ interface UserData { } type Doctor = { - id: string; - user_id: string | null; - crm: string; - crm_uf: string; - specialty: string; - full_name: string; - cpf: string; - email: string; - phone_mobile: string | null; - phone2: string | null; - cep: string | null; - street: string | null; - number: string | null; - complement: string | null; - neighborhood: string | null; - city: string | null; - state: string | null; - birth_date: string | null; - rg: string | null; - active: boolean; - created_at: string; - updated_at: string; - created_by: string; - updated_by: string | null; - max_days_in_advance: number; - rating: number | null; + id: string; + user_id: string | null; + crm: string; + crm_uf: string; + specialty: string; + full_name: string; + cpf: string; + email: string; + phone_mobile: string | null; + phone2: string | null; + cep: string | null; + street: string | null; + number: string | null; + complement: string | null; + neighborhood: string | null; + city: string | null; + state: string | null; + birth_date: string | null; + rg: string | null; + active: boolean; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string | null; + max_days_in_advance: number; + rating: number | null; } type Availability = { @@ -190,7 +192,7 @@ export default function AvailabilityPage() { console.log(doctor); // Busca disponibilidade const availabilityList = await AvailabilityService.list(); - + // Filtra já com a variável local const filteredAvail = availabilityList.filter( (disp: { doctor_id: string }) => disp.doctor_id === doctor?.id @@ -335,43 +337,45 @@ export default function AvailabilityPage() {

Dados

-
-
- -
- - - - - - - -
+ {/* **AJUSTE DE RESPONSIVIDADE: DIAS DA SEMANA** */} +
+ + {/* O antigo 'flex gap-4 mt-2 flex-nowrap' foi substituído por um grid responsivo: */} +
+ + + + + + +
-
+ {/* **AJUSTE DE RESPONSIVIDADE: HORÁRIO E DURAÇÃO** */} + {/* Ajustado para 1 coluna em móvel, 2 em tablet e 5 em desktop (mantendo o que já existia com ajustes) */} +
+ {/* O Select de modalidade fica fora deste grid para ocupar uma linha inteira em telas menores, como no original, garantindo clareza */}
@@ -409,53 +414,56 @@ export default function AvailabilityPage() {
-
- - + {/* **AJUSTE DE RESPONSIVIDADE: BOTÕES DE AÇÃO** */} + {/* Alinha à direita em telas maiores e empilha (com o botão primário no final) em telas menores */} +
+ + -
- - +
+ + -
+ + {/* **AJUSTE DE RESPONSIVIDADE: CARD DE HORÁRIO SEMANAL** */}
Horário Semanal Confira ou altere a sua disponibilidade da semana - + {/* Define um grid responsivo para os dias da semana (1 coluna em móvel, 2 em pequeno, 3 em médio e 7 em telas grandes) */} + {["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"].map((day) => { const times = schedule[day] || []; return (
-
-
-

{weekdaysPT[day]}

-
-
+
+

{weekdaysPT[day]}

+
{times.length > 0 ? ( times.map((t, i) => (
-

- {formatTime(t.start)}
{formatTime(t.end)} +

+ {formatTime(t.start)} - {formatTime(t.end)}

handleOpenModal(t, day)}> - + Editar openDeleteDialog(t, day)} - className="text-red-600"> + className="text-red-600 focus:bg-red-50 focus:text-red-600"> Excluir @@ -474,6 +482,8 @@ export default function AvailabilityPage() {
+ + {/* AlertDialog e Modal de Edição (não precisam de grandes ajustes de layout, apenas garantindo que os componentes sejam responsivos internamente) */} @@ -498,4 +508,4 @@ export default function AvailabilityPage() { ); -} +} \ No newline at end of file diff --git a/app/doctor/medicos/page.tsx b/app/doctor/medicos/page.tsx index 7e315c5..bc18221 100644 --- a/app/doctor/medicos/page.tsx +++ b/app/doctor/medicos/page.tsx @@ -1,6 +1,7 @@ +// app/doctor/pacientes/page.tsx (assumindo a localização) "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import DoctorLayout from "@/components/doctor-layout"; import Link from "next/link"; import { @@ -9,9 +10,17 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Eye, Edit, Calendar, Trash2 } from "lucide-react"; +import { Eye, Edit, Calendar, Trash2, Loader2 } from "lucide-react"; import { api } from "@/services/api.mjs"; import { PatientDetailsModal } from "@/components/ui/patient-details-modal"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; interface Paciente { id: string; @@ -41,6 +50,60 @@ export default function PacientesPage() { const [selectedPatient, setSelectedPatient] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); + // --- Lógica de Paginação INÍCIO --- + const [itemsPerPage, setItemsPerPage] = useState(5); + const [currentPage, setCurrentPage] = useState(1); + + const totalPages = Math.ceil(pacientes.length / itemsPerPage); + + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = pacientes.slice(indexOfFirstItem, indexOfLastItem); + + const paginate = (pageNumber: number) => setCurrentPage(pageNumber); + + // Funções de Navegação + const goToPrevPage = () => { + setCurrentPage((prev) => Math.max(1, prev - 1)); + }; + + const goToNextPage = () => { + setCurrentPage((prev) => Math.min(totalPages, prev + 1)); + }; + + // Lógica para gerar os números das páginas visíveis (máximo de 5) + const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { + const pages: number[] = []; + const maxVisiblePages = 5; + const halfRange = Math.floor(maxVisiblePages / 2); + let startPage = Math.max(1, currentPage - halfRange); + let endPage = Math.min(totalPages, currentPage + halfRange); + + if (endPage - startPage + 1 < maxVisiblePages) { + if (endPage === totalPages) { + startPage = Math.max(1, totalPages - maxVisiblePages + 1); + } + if (startPage === 1) { + endPage = Math.min(totalPages, maxVisiblePages); + } + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + return pages; + }; + + const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage); + + // Lógica para mudar itens por página, resetando para a página 1 + const handleItemsPerPageChange = (value: string) => { + setItemsPerPage(Number(value)); + setCurrentPage(1); + }; + // --- Lógica de Paginação FIM --- + + const handleOpenModal = (patient: Paciente) => { setSelectedPatient(patient); setIsModalOpen(true); @@ -51,92 +114,132 @@ export default function PacientesPage() { setIsModalOpen(false); }; - const formatDate = (dateString: string) => { - if (!dateString) return ""; - const date = new Date(dateString); - return new Intl.DateTimeFormat('pt-BR').format(date); + const formatDate = (dateString: string | null | undefined) => { + if (!dateString) return "N/A"; + try { + const date = new Date(dateString); + return new Intl.DateTimeFormat("pt-BR").format(date); + } catch (e) { + return dateString; // Retorna o string original se o formato for inválido + } }; - const [itemsPerPage, setItemsPerPage] = useState(5); - const [currentPage, setCurrentPage] = useState(1); + const fetchPacientes = useCallback(async () => { + try { + setLoading(true); + setError(null); + const json = await api.get("/rest/v1/patients"); + const items = Array.isArray(json) + ? json + : Array.isArray(json?.data) + ? json.data + : []; - const indexOfLastItem = currentPage * itemsPerPage; - const indexOfFirstItem = indexOfLastItem - itemsPerPage; - const currentItems = pacientes.slice(indexOfFirstItem, indexOfLastItem); + const mapped: Paciente[] = items.map((p: any) => ({ + id: String(p.id ?? ""), + nome: p.full_name ?? "—", + telefone: p.phone_mobile ?? "N/A", + cidade: p.city ?? "N/A", + estado: p.state ?? "N/A", + ultimoAtendimento: formatDate(p.created_at), + proximoAtendimento: "N/A", // Necessita de lógica de agendamento real + email: p.email ?? "N/A", + birth_date: p.birth_date ?? "N/A", + cpf: p.cpf ?? "N/A", + blood_type: p.blood_type ?? "N/A", + weight_kg: p.weight_kg ?? 0, + height_m: p.height_m ?? 0, + street: p.street ?? "N/A", + number: p.number ?? "N/A", + complement: p.complement ?? "N/A", + neighborhood: p.neighborhood ?? "N/A", + cep: p.cep ?? "N/A", + })); - const paginate = (pageNumber: number) => setCurrentPage(pageNumber); + setPacientes(mapped); + setCurrentPage(1); // Resetar a página ao carregar novos dados + } catch (e: any) { + console.error("Erro ao carregar pacientes:", e); + setError(e?.message || "Erro ao carregar pacientes"); + } finally { + setLoading(false); + } + }, []); useEffect(() => { - async function fetchPacientes() { - try { - setLoading(true); - setError(null); - const json = await api.get("/rest/v1/patients"); - const items = Array.isArray(json) ? json : (Array.isArray(json?.data) ? json.data : []); - - const mapped = items.map((p: any) => ({ - id: String(p.id ?? ""), - nome: p.full_name ?? "", - telefone: p.phone_mobile ?? "", - cidade: p.city ?? "", - estado: p.state ?? "", - ultimoAtendimento: formatDate(p.created_at) ?? "", - proximoAtendimento: "", - email: p.email ?? "", - birth_date: p.birth_date ?? "", - cpf: p.cpf ?? "", - blood_type: p.blood_type ?? "", - weight_kg: p.weight_kg ?? 0, - height_m: p.height_m ?? 0, - street: p.street ?? "", - number: p.number ?? "", - complement: p.complement ?? "", - neighborhood: p.neighborhood ?? "", - cep: p.cep ?? "", - })); - - setPacientes(mapped); - } catch (e: any) { - setError(e?.message || "Erro ao carregar pacientes"); - } finally { - setLoading(false); - } - } fetchPacientes(); - }, []); + }, [fetchPacientes]); return ( -
-
-

Pacientes

-

Lista de pacientes vinculados

+
+ {/* Cabeçalho */} +
+
+

Pacientes

+

+ Lista de pacientes vinculados +

+
+ {/* Adicione um seletor de itens por página ao lado de um botão de 'Novo Paciente' se aplicável */} +
+ + + + +
-
+ +
- +
- - - - - - - + + + + + + + {loading ? ( - ) : error ? ( - + ) : pacientes.length === 0 ? ( @@ -146,17 +249,32 @@ export default function PacientesPage() { ) : ( currentItems.map((p) => ( - - - - - - - - + + + + + + +
NomeTelefoneCidadeEstadoÚltimo atendimentoPróximo atendimentoAçõesNome + Telefone + + Cidade + + Estado + + Último atendimento + + Próximo atendimento + Ações
+ + Carregando pacientes...
{`Erro: ${error}`}{`Erro: ${error}`}
{p.nome}{p.telefone}{p.cidade}{p.estado}{p.ultimoAtendimento}{p.proximoAtendimento} +
{p.nome} + {p.telefone} + + {p.cidade} + + {p.estado} + + {p.ultimoAtendimento} + + {p.proximoAtendimento} + - + handleOpenModal(p)}> @@ -164,7 +282,7 @@ export default function PacientesPage() { Ver detalhes - + Laudos @@ -175,11 +293,14 @@ export default function PacientesPage() { { - const newPacientes = pacientes.filter((pac) => pac.id !== p.id) - setPacientes(newPacientes) - alert(`Paciente ID: ${p.id} excluído`) + // Simulação de exclusão (A exclusão real deve ser feita via API) + const newPacientes = pacientes.filter((pac) => pac.id !== p.id); + setPacientes(newPacientes); + alert(`Paciente ID: ${p.id} excluído`); + // Necessário chamar a API de exclusão aqui }} - className="text-red-600"> + className="text-red-600 focus:bg-red-50 focus:text-red-600" + > Excluir @@ -192,19 +313,51 @@ export default function PacientesPage() {
-
- {Array.from({ length: Math.ceil(pacientes.length / itemsPerPage) }, (_, i) => ( + + {/* Paginação ATUALIZADA */} + {totalPages > 1 && ( +
+ + {/* Botão Anterior */} - ))} -
+ + {/* Números das Páginas */} + {visiblePageNumbers.map((number) => ( + + ))} + + {/* Botão Próximo */} + + +
+ )} + {/* Fim da Paginação ATUALIZADA */} +
+ value.replace(/\D/g, ''); - -const formatCPF = (value: string): string => { - const cleaned = cleanNumber(value).substring(0, 11); - return cleaned.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4'); -}; - -const formatCEP = (value: string): string => { - const cleaned = cleanNumber(value).substring(0, 8); - return cleaned.replace(/(\d{5})(\d{3})/, '$1-$2'); -}; - -const formatPhoneMobile = (value: string): string => { - const cleaned = cleanNumber(value).substring(0, 11); - if (cleaned.length > 10) { - return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3'); - } - return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, '($1) $2-$3'); -}; - - - - -export default function NovoMedicoPage() { - const router = useRouter(); - const [formData, setFormData] = useState(defaultFormData); - const [isSaving, setIsSaving] = useState(false); - const [error, setError] = useState(null); - const [anexosOpen, setAnexosOpen] = useState(false); - - - const handleInputChange = (key: keyof DoctorFormData, value: string | boolean | { id: number, name: string }[]) => { - - - if (typeof value === 'string') { - let maskedValue = value; - if (key === 'cpf') maskedValue = formatCPF(value); - if (key === 'cep') maskedValue = formatCEP(value); - if (key === 'telefoneCelular' || key === 'telefone2') maskedValue = formatPhoneMobile(value); - - setFormData((prev) => ({ ...prev, [key]: maskedValue })); - } else { - setFormData((prev) => ({ ...prev, [key]: value })); - } - }; - - - const adicionarAnexo = () => { - const newId = Date.now(); - handleInputChange('anexos', [...formData.anexos, { id: newId, name: `Documento ${formData.anexos.length + 1}` }]); - } - - const removerAnexo = (id: number) => { - handleInputChange('anexos', formData.anexos.filter((anexo) => anexo.id !== id)); - } - - - const requiredFields = [ - { key: 'nomeCompleto', name: 'Nome Completo' }, - { key: 'crm', name: 'CRM' }, - { key: 'crmEstado', name: 'UF do CRM' }, - { key: 'cpf', name: 'CPF' }, - { key: 'email', name: 'E-mail' }, - ] as const; - - - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(null); - setIsSaving(true); - - - for (const field of requiredFields) { - let valueToCheck = formData[field.key]; - - - if (!valueToCheck || String(valueToCheck).trim() === '') { - setError(`O campo obrigatório "${field.name}" deve ser preenchido.`); - setIsSaving(false); - return; - } - } - - const finalPayload: { [key: string]: any } = {}; - const formKeys = Object.keys(formData) as Array; - - - formKeys.forEach((key) => { - const apiFieldName = apiMap[key]; - - if (!apiFieldName) return; - - let value = formData[key]; - - if (typeof value === 'string') { - let trimmedValue = value.trim(); - - - const isOptional = !requiredFields.some(f => f.key === key); - - if (isOptional && trimmedValue === '') { - finalPayload[apiFieldName] = null; - return; - } - - - if (key === 'crmEstado' || key === 'estado') { - trimmedValue = trimmedValue.toUpperCase(); - } - - value = trimmedValue; - } - - finalPayload[apiFieldName] = value; - }); - - - try { - - const response = await doctorsService.create(finalPayload); - router.push("/manager/home"); - } catch (e: any) { - console.error("Erro ao salvar o médico:", e); - - let detailedError = `Erro na requisição. Verifique se o **CRM** ou **CPF** já existem ou se as **Máscaras/Datas** estão incorretas.`; - - - if (e.message && e.message.includes("duplicate key value violates unique constraint")) { - - detailedError = "O CPF ou CRM informado já está cadastrado no sistema. Por favor, verifique os dados de identificação."; - } else if (e.message && e.message.includes("Detalhes:")) { - - detailedError = e.message.split("Detalhes:")[1].trim(); - } else if (e.message) { - detailedError = e.message; - } - - setError(`Erro ao cadastrar. Detalhes: ${detailedError}`); - } finally { - setIsSaving(false); - } - }; - - return ( - -
-
-
-

Novo Médico

-

- Preencha os dados do novo médico para cadastro. -

-
- - - -
- -
- - {error && ( -
-

Erro no Cadastro:

-

{error}

-
- )} - - -
-

- Dados Principais e Pessoais -

- - -
-
- - handleInputChange("nomeCompleto", e.target.value)} - placeholder="Nome do Médico" - required - /> -
-
- - handleInputChange("crm", e.target.value)} - placeholder="Ex: 123456" - required - /> -
-
- - -
-
- - -
-
- - handleInputChange("especialidade", e.target.value)} - placeholder="Ex: Cardiologia" - /> -
-
- - handleInputChange("cpf", e.target.value)} - placeholder="000.000.000-00" - maxLength={14} - required - /> -
-
- - handleInputChange("rg", e.target.value)} - placeholder="00.000.000-0" - /> -
-
- - -
-
- - handleInputChange("email", e.target.value)} - placeholder="exemplo@dominio.com" - required - /> -
-
- - handleInputChange("dataNascimento", e.target.value)} - /> -
-
-
- - -
-

- Contato e Endereço -

- - -
-
- - handleInputChange("telefoneCelular", e.target.value)} - placeholder="(00) 00000-0000" - maxLength={15} - /> -
-
- - handleInputChange("telefone2", e.target.value)} - placeholder="(00) 00000-0000" - maxLength={15} - /> -
-
-
- handleInputChange("ativo", checked === true)} - /> - -
-
-
- - -
-
- - handleInputChange("cep", e.target.value)} - placeholder="00000-000" - maxLength={9} - /> -
-
- - handleInputChange("endereco", e.target.value)} - placeholder="Rua, Avenida, etc." - /> -
-
-
-
- - handleInputChange("numero", e.target.value)} - placeholder="123" - /> -
-
- - handleInputChange("complemento", e.target.value)} - placeholder="Apto, Bloco, etc." - /> -
-
-
-
- - handleInputChange("bairro", e.target.value)} - placeholder="Bairro" - /> -
-
- - handleInputChange("estado", e.target.value)} - placeholder="SP" - /> -
-
- - handleInputChange("cidade", e.target.value)} - placeholder="São Paulo" - /> -
-
-
- - -
-

- Outras Informações (Internas) -

- -
-
- -