diff --git a/package-lock.json b/package-lock.json index 963283f..fe0f642 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,8 @@ "@headlessui/react": "^2.2.7", "@heroicons/react": "^2.2.0", "date-fns": "^4.1.0", - "react-big-calendar": "^1.19.4" + "react-big-calendar": "^1.19.4", + "react-signature-canvas": "^1.1.0-alpha.2" } }, "node_modules/@babel/runtime": { @@ -266,6 +267,12 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/signature_pad": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@types/signature_pad/-/signature_pad-2.3.6.tgz", + "integrity": "sha512-v3j92gCQJoxomHhd+yaG4Vsf8tRS/XbzWKqDv85UsqjMGy4zhokuwKe4b6vhbgncKkh+thF+gpz6+fypTtnFqQ==", + "license": "MIT" + }, "node_modules/@types/warning": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", @@ -520,6 +527,36 @@ "react-dom": ">=16.3.0" } }, + "node_modules/react-signature-canvas": { + "version": "1.1.0-alpha.2", + "resolved": "https://registry.npmjs.org/react-signature-canvas/-/react-signature-canvas-1.1.0-alpha.2.tgz", + "integrity": "sha512-tKUNk3Gmh04Ug4K8p5g8Is08BFUKvbXxi0PyetQ/f8OgCBzcx4vqNf9+OArY/TdNdfHtswXQNRwZD6tyELjkjQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.17.9", + "@types/signature_pad": "^2.3.0", + "signature_pad": "^2.3.2", + "trim-canvas": "^0.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/agilgur5" + }, + "peerDependencies": { + "@types/prop-types": "^15.7.3", + "@types/react": "0.14 - 19", + "prop-types": "^15.5.8", + "react": "0.14 - 19", + "react-dom": "0.14 - 19" + }, + "peerDependenciesMeta": { + "@types/prop-types": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -527,12 +564,24 @@ "license": "MIT", "peer": true }, + "node_modules/signature_pad": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-2.3.2.tgz", + "integrity": "sha512-peYXLxOsIY6MES2TrRLDiNg2T++8gGbpP2yaC+6Ohtxr+a2dzoaqWosWDY9sWqTAAk6E/TyQO+LJw9zQwyu5kA==", + "license": "MIT" + }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", "license": "MIT" }, + "node_modules/trim-canvas": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/trim-canvas/-/trim-canvas-0.1.2.tgz", + "integrity": "sha512-nd4Ga3iLFV94mdhW9JFMLpQbHUyCQuhFOD71PEAt1NjtMD5wbZctzhX8c3agHNybMR5zXD1XTGoIEWk995E6pQ==", + "license": "Apache-2.0" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", diff --git a/package.json b/package.json index e0d27d7..e0ec9ff 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "@headlessui/react": "^2.2.7", "@heroicons/react": "^2.2.0", "date-fns": "^4.1.0", - "react-big-calendar": "^1.19.4" + "react-big-calendar": "^1.19.4", + "react-signature-canvas": "^1.1.0-alpha.2" } } diff --git a/susconecta/.gitignore b/susconecta/.gitignore index f650315..29879dc 100644 --- a/susconecta/.gitignore +++ b/susconecta/.gitignore @@ -24,4 +24,6 @@ yarn-error.log* # typescript *.tsbuildinfo -next-env.d.ts \ No newline at end of file +next-env.d.tsriseup-squad20/ +susconecta/riseup-squad20/ +riseup-squad20/ diff --git a/susconecta/app/(main-routes)/consultas/page.tsx b/susconecta/app/(main-routes)/consultas/page.tsx new file mode 100644 index 0000000..68e07d3 --- /dev/null +++ b/susconecta/app/(main-routes)/consultas/page.tsx @@ -0,0 +1,368 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { + MoreHorizontal, + PlusCircle, + Search, + Eye, + Edit, + Trash2, + ArrowLeft, +} from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +import { mockAppointments, mockProfessionals } from "@/lib/mocks/appointment-mocks"; +import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form"; + +// --- Helper Functions --- +const formatDate = (date: string | Date) => { + if (!date) return ""; + return new Date(date).toLocaleDateString("pt-BR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +}; + +const capitalize = (s: string) => { + if (typeof s !== 'string' || s.length === 0) return ''; + return s.charAt(0).toUpperCase() + s.slice(1); +}; + +// --- Main Page Component --- +export default function ConsultasPage() { + const [appointments, setAppointments] = useState(mockAppointments); + const [showForm, setShowForm] = useState(false); + const [editingAppointment, setEditingAppointment] = useState(null); + const [viewingAppointment, setViewingAppointment] = useState(null); + + // Converte o objeto da consulta para o formato esperado pelo formulário + const mapAppointmentToFormData = (appointment: any) => { + const professional = mockProfessionals.find(p => p.id === appointment.professional); + const appointmentDate = new Date(appointment.time); + + return { + id: appointment.id, + patientName: appointment.patient, + professionalName: professional ? professional.name : '', + appointmentDate: appointmentDate.toISOString().split('T')[0], // Formato YYYY-MM-DD + startTime: appointmentDate.toTimeString().split(' ')[0].substring(0, 5), // Formato HH:MM + endTime: new Date(appointmentDate.getTime() + appointment.duration * 60000).toTimeString().split(' ')[0].substring(0, 5), + status: appointment.status, + appointmentType: appointment.type, + notes: appointment.notes, + // Adicione outros campos do paciente aqui se necessário (cpf, rg, etc.) + // Eles não existem no mock de agendamento, então virão vazios + cpf: '', + rg: '', + birthDate: '', + phoneCode: '+55', + phoneNumber: '', + email: '', + unit: 'nei', + }; + }; + + const handleDelete = (appointmentId: string) => { + if (window.confirm("Tem certeza que deseja excluir esta consulta?")) { + setAppointments((prev) => prev.filter((a) => a.id !== appointmentId)); + } + }; + + const handleEdit = (appointment: any) => { + const formData = mapAppointmentToFormData(appointment); + setEditingAppointment(formData); + setShowForm(true); + }; + + const handleView = (appointment: any) => { + setViewingAppointment(appointment); + }; + + const handleCancel = () => { + setEditingAppointment(null); + setShowForm(false); + }; + + const handleSave = (formData: any) => { + // Como o formulário edita campos que não estão na tabela, + // precisamos mapear de volta para o formato original do agendamento. + // Para a simulação, vamos atualizar apenas os campos que existem no mock. + const updatedAppointment = { + id: formData.id, + patient: formData.patientName, + time: new Date(`${formData.appointmentDate}T${formData.startTime}`).toISOString(), + duration: 30, // Duração não está no form, então mantemos um valor fixo + type: formData.appointmentType as any, + status: formData.status as any, + professional: appointments.find(a => a.id === formData.id)?.professional || '', // Mantém o ID do profissional + notes: formData.notes, + }; + + setAppointments(prev => + prev.map(a => a.id === updatedAppointment.id ? updatedAppointment : a) + ); + handleCancel(); // Fecha o formulário + }; + + if (showForm && editingAppointment) { + return ( +
+
+ +

Editar Consulta

+
+ +
+ ) + } + + return ( +
+
+
+

Gerenciamento de Consultas

+

Visualize, filtre e gerencie todas as consultas da clínica.

+
+
+ + + +
+
+ + + + Consultas Agendadas + + Visualize, filtre e gerencie todas as consultas da clínica. + +
+
+ + +
+ + +
+
+ + + + + Paciente + Médico + Status + Data e Hora + Ações + + + + {appointments.map((appointment) => { + const professional = mockProfessionals.find( + (p) => p.id === appointment.professional + ); + return ( + + + {appointment.patient} + + + {professional ? professional.name : "Não encontrado"} + + + + {capitalize(appointment.status)} + + + {formatDate(appointment.time)} + + + + + + + handleView(appointment)} + > + + Ver + + handleEdit(appointment)}> + + Editar + + handleDelete(appointment.id)} + className="text-destructive" + > + + Excluir + + + + + + ); + })} + +
+
+
+ + {viewingAppointment && ( + setViewingAppointment(null)}> + + + Detalhes da Consulta + + Informações detalhadas da consulta de {viewingAppointment?.patient}. + + +
+
+ + {viewingAppointment?.patient} +
+
+ + + {mockProfessionals.find(p => p.id === viewingAppointment?.professional)?.name || "Não encontrado"} + +
+
+ + {viewingAppointment?.time ? formatDate(viewingAppointment.time) : ''} +
+
+ + + + {capitalize(viewingAppointment?.status || '')} + + +
+
+ + {capitalize(viewingAppointment?.type || '')} +
+
+ + {viewingAppointment?.notes || "Nenhuma"} +
+
+ + + +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx b/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx new file mode 100644 index 0000000..8147c76 --- /dev/null +++ b/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx @@ -0,0 +1,88 @@ +"use client"; + + +import { Button } from "@/components/ui/button"; +import { FileDown } from "lucide-react"; +import jsPDF from "jspdf"; +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"; + +export default function RelatoriosPage() { + // Dados fictícios para o gráfico financeiro + const financeiro = [ + { mes: "Jan", faturamento: 35000, despesas: 12000 }, + { mes: "Fev", faturamento: 29000, despesas: 15000 }, + { mes: "Mar", faturamento: 42000, despesas: 18000 }, + { mes: "Abr", faturamento: 38000, despesas: 14000 }, + { mes: "Mai", faturamento: 45000, despesas: 20000 }, + { mes: "Jun", faturamento: 41000, despesas: 17000 }, + ]; + // ============================ + // PASSO 3 - Funções de exportar + // ============================ + const exportConsultasPDF = () => { + const doc = new jsPDF(); + doc.text("Relatório de Consultas", 10, 10); + doc.text("Resumo das consultas realizadas.", 10, 20); + doc.save("relatorio-consultas.pdf"); + }; + + const exportPacientesPDF = () => { + const doc = new jsPDF(); + doc.text("Relatório de Pacientes", 10, 10); + doc.text("Informações gerais dos pacientes cadastrados.", 10, 20); + doc.save("relatorio-pacientes.pdf"); + }; + + const exportFinanceiroPDF = () => { + const doc = new jsPDF(); + doc.text("Relatório Financeiro", 10, 10); + doc.text("Receitas e despesas da clínica.", 10, 20); + doc.save("relatorio-financeiro.pdf"); + }; + + return ( +
+

Relatórios

+ +
+ {/* Card Consultas */} +
+

Relatório de Consultas

+

Resumo das consultas realizadas.

+ {/* PASSO 4 - Botão chama a função */} + +
+ + {/* Card Pacientes */} +
+

Relatório de Pacientes

+

Informações gerais dos pacientes cadastrados.

+ +
+ + {/* Card Financeiro com gráfico */} +
+

Relatório Financeiro

+ + + + + + + + + + + + +
+
+
+ ); +} diff --git a/susconecta/app/(main-routes)/dashboard/doutores/page.tsx b/susconecta/app/(main-routes)/doutores/page.tsx similarity index 74% rename from susconecta/app/(main-routes)/dashboard/doutores/page.tsx rename to susconecta/app/(main-routes)/doutores/page.tsx index 243c4ac..501e40d 100644 --- a/susconecta/app/(main-routes)/dashboard/doutores/page.tsx +++ b/susconecta/app/(main-routes)/doutores/page.tsx @@ -5,6 +5,8 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form"; @@ -18,6 +20,7 @@ export default function DoutoresPage() { const [search, setSearch] = useState(""); const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState(null); + const [viewingDoctor, setViewingDoctor] = useState(null); // Carrega da API async function load() { @@ -55,6 +58,10 @@ export default function DoutoresPage() { setShowForm(true); } + function handleView(doctor: Medico) { + setViewingDoctor(doctor); + } + // Excluir via API e recarregar async function handleDelete(id: string) { if (!confirm("Excluir este médico?")) return; @@ -70,7 +77,7 @@ export default function DoutoresPage() { if (showForm) { return ( -
+
- alert(JSON.stringify(doctor, null, 2))}> + handleView(doctor)}> Ver @@ -182,6 +189,47 @@ export default function DoutoresPage() {
+ + {viewingDoctor && ( + setViewingDoctor(null)}> + + + Detalhes do Médico + + Informações detalhadas de {viewingDoctor?.nome}. + + +
+
+ + {viewingDoctor?.nome} +
+
+ + + {viewingDoctor?.especialidade} + +
+
+ + {viewingDoctor?.crm} +
+
+ + {viewingDoctor?.email} +
+
+ + {viewingDoctor?.telefone} +
+
+ + + +
+
+ )} +
Mostrando {filtered.length} de {doctors.length}
diff --git a/susconecta/app/(main-routes)/layout.tsx b/susconecta/app/(main-routes)/layout.tsx index 68be841..d0e759f 100644 --- a/susconecta/app/(main-routes)/layout.tsx +++ b/susconecta/app/(main-routes)/layout.tsx @@ -1,4 +1,5 @@ import type React from "react"; +import ProtectedRoute from "@/components/ProtectedRoute"; import { Sidebar } from "@/components/dashboard/sidebar"; import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { PagesHeader } from "@/components/dashboard/header"; @@ -9,14 +10,16 @@ export default function MainRoutesLayout({ children: React.ReactNode; }) { return ( -
- - -
- - {children} -
-
-
+ +
+ + +
+ + {children} +
+
+
+
); } diff --git a/susconecta/app/(main-routes)/dashboard/pacientes/loading.tsx b/susconecta/app/(main-routes)/pacientes/loading.tsx similarity index 100% rename from susconecta/app/(main-routes)/dashboard/pacientes/loading.tsx rename to susconecta/app/(main-routes)/pacientes/loading.tsx diff --git a/susconecta/app/(main-routes)/dashboard/pacientes/page.tsx b/susconecta/app/(main-routes)/pacientes/page.tsx similarity index 77% rename from susconecta/app/(main-routes)/dashboard/pacientes/page.tsx rename to susconecta/app/(main-routes)/pacientes/page.tsx index 074b512..3f2d75f 100644 --- a/susconecta/app/(main-routes)/dashboard/pacientes/page.tsx +++ b/susconecta/app/(main-routes)/pacientes/page.tsx @@ -6,6 +6,8 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; import { MoreHorizontal, Plus, Search, Eye, Edit, Trash2, ArrowLeft } from "lucide-react"; import { Paciente, Endereco, listarPacientes, buscarPacientePorId, excluirPaciente } from "@/lib/api"; @@ -47,6 +49,7 @@ export default function PacientesPage() { const [search, setSearch] = useState(""); const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState(null); + const [viewingPatient, setViewingPatient] = useState(null); async function loadAll() { try { @@ -88,6 +91,10 @@ export default function PacientesPage() { setShowForm(true); } + function handleView(patient: Paciente) { + setViewingPatient(patient); + } + async function handleDelete(id: string) { if (!confirm("Excluir este paciente?")) return; try { @@ -161,7 +168,6 @@ export default function PacientesPage() { return (
- {}

Pacientes

@@ -217,7 +223,7 @@ export default function PacientesPage() { - alert(JSON.stringify(p, null, 2))}> + handleView(p)}> Ver @@ -245,6 +251,46 @@ export default function PacientesPage() {
+ {viewingPatient && ( + setViewingPatient(null)}> + + + Detalhes do Paciente + + Informações detalhadas de {viewingPatient.nome}. + + +
+
+ + {viewingPatient.nome} +
+
+ + {viewingPatient.cpf} +
+
+ + {viewingPatient.telefone} +
+
+ + + {`${viewingPatient.endereco.logradouro}, ${viewingPatient.endereco.numero} - ${viewingPatient.endereco.bairro}, ${viewingPatient.endereco.cidade} - ${viewingPatient.endereco.estado}`} + +
+
+ + {viewingPatient.observacoes || "Nenhuma"} +
+
+ + + +
+
+ )} +
Mostrando {filtered.length} de {patients.length}
); diff --git a/susconecta/app/agenda/page.tsx b/susconecta/app/agenda/page.tsx index 1cf8b4e..7c6fabb 100644 --- a/susconecta/app/agenda/page.tsx +++ b/susconecta/app/agenda/page.tsx @@ -1,373 +1,35 @@ "use client"; -import { useState } from "react"; -import Link from "next/link"; -import { usePathname, useRouter } from "next/navigation"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { Switch } from "@/components/ui/switch"; -import { Calendar } from "lucide-react"; - -import { - RotateCcw, - Accessibility, - Volume2, - Flame, - Settings, - Clipboard, - Search, - ChevronDown, - Upload, - FileDown, - Tag, - Save, -} from "lucide-react"; +import { useRouter } from "next/navigation"; +import { AppointmentForm } from "@/components/forms/appointment-form"; import HeaderAgenda from "@/components/agenda/HeaderAgenda"; import FooterAgenda from "@/components/agenda/FooterAgenda"; export default function NovoAgendamentoPage() { - const [bloqueio, setBloqueio] = useState(false); + const router = useRouter(); + + const handleSave = (data: any) => { + console.log("Salvando novo agendamento...", data); + // Aqui viria a chamada da API para criar um novo agendamento + alert("Novo agendamento salvo (simulado)!"); + router.push("/consultas"); // Volta para a lista após salvar + }; + + const handleCancel = () => { + router.back(); // Simplesmente volta para a página anterior + }; return ( - // ====== WRAPPER COM ESPAÇAMENTO GERAL ======
- {/* HEADER fora do
, usando o MESMO container do footer */} - - {/* Conteúdo */} -
- {/* ==== INFORMAÇÕES DO PACIENTE — layout idêntico ao print ==== */} -
-

Informações do paciente

- - {/* grade principal: 12 colunas para controlar as larguras */} -
- {/* ===== Linha 1 ===== */} -
- -
- - -
-
- -
- - -
- -
- - -
- - {/* ===== Linha 2 ===== */} - {/* 1ª coluna (span 6) com sub-grid: Data (5 col) + Telefone (7 col) */} -
-
-
- - -
- -
- -
- - -
-
-
-
- - {/* 2ª coluna da linha 2: E-mail (span 6) */} -
- - -
- - {/* ===== Linha 3 ===== */} -
- -
- - -
-
- -
-
- - -
-
- - -
-
-
- - {/* link Informações adicionais */} - - - {/* barra Documentos e anexos */} -
- Documentos e anexos -
- - - -
-
-
- - {/* ==== INFORMAÇÕES DO ATENDIMENTO ==== */} -
-

Informações do atendimento

- - {/* GRID PRINCIPAL: 12 colunas */} -
- {/* COLUNA ESQUERDA (span 6) */} -
- {/* Nome do profissional */} -
- -
- - - - RA - -
-
- -
- {/* Unidade */} -
- - -
- - {/* Data com ícone */} -
- -
- - -
-
-
- - {/* Início / Término / Profissional solicitante (na mesma linha) */} -
- {/* Início (maior) */} -
- - -
- - {/* Término (maior) */} -
- - -
- - {/* Profissional solicitante */} -
- -
- {/* ícone de busca à esquerda */} - - - - - - - - - - -
-
-
-
- - {/* COLUNA DIREITA — altura/posição como a imagem 1 */} -
- {/* toolbar */} -
- - - - - -
- - {/* Tipo de atendimento + campo de busca */} -
-
- - -
- -
- - - -
-
- - {/* Observações + imprimir */} -
- - -
- - {/* Textarea mais baixo e compacto */} -