develop #83
@ -1,10 +1,10 @@
|
|||||||
// app/agendamento/page.tsx
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
// Importação dinâmica para evitar erros de SSR
|
|
||||||
const AgendaCalendar = dynamic(() => import('@/components/agendamento/AgendaCalendar'), {
|
const AgendaCalendar = dynamic(() => import('@/components/agendamento/AgendaCalendar'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => (
|
loading: () => (
|
||||||
@ -24,7 +24,7 @@ const AgendaCalendar = dynamic(() => import('@/components/agendamento/AgendaCale
|
|||||||
const AppointmentModal = dynamic(() => import('@/components/agendamento/AppointmentModal'), { ssr: false });
|
const AppointmentModal = dynamic(() => import('@/components/agendamento/AppointmentModal'), { ssr: false });
|
||||||
const ListaEspera = dynamic(() => import('@/components/agendamento/ListaEspera'), { ssr: false });
|
const ListaEspera = dynamic(() => import('@/components/agendamento/ListaEspera'), { ssr: false });
|
||||||
|
|
||||||
// Dados mockados
|
|
||||||
const mockAppointments = [
|
const mockAppointments = [
|
||||||
{ id: '1', patient: 'Ana Costa', time: '2025-09-10T09:00', duration: 30, type: 'consulta' as const, status: 'confirmed' as const, professional: '1', notes: '' },
|
{ id: '1', patient: 'Ana Costa', time: '2025-09-10T09:00', duration: 30, type: 'consulta' as const, status: 'confirmed' as const, professional: '1', notes: '' },
|
||||||
{ id: '2', patient: 'Pedro Alves', time: '2025-09-10T10:30', duration: 45, type: 'retorno' as const, status: 'pending' as const, professional: '2', notes: '' },
|
{ id: '2', patient: 'Pedro Alves', time: '2025-09-10T10:30', duration: 45, type: 'retorno' as const, status: 'pending' as const, professional: '2', notes: '' },
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
/* src/app/dashboard/pacientes/page.tsx */
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
@ -11,7 +11,7 @@ import { MoreHorizontal, Plus, Search, Eye, Edit, Trash2, ArrowLeft } from "luci
|
|||||||
import { Paciente, Endereco, listarPacientes, buscarPacientePorId, excluirPaciente } from "@/lib/api";
|
import { Paciente, Endereco, listarPacientes, buscarPacientePorId, excluirPaciente } from "@/lib/api";
|
||||||
import { PatientRegistrationForm } from "@/components/forms/patient-registration-form";
|
import { PatientRegistrationForm } from "@/components/forms/patient-registration-form";
|
||||||
|
|
||||||
// Converte qualquer formato que vier do mock para o shape do nosso tipo Paciente
|
|
||||||
function normalizePaciente(p: any): Paciente {
|
function normalizePaciente(p: any): Paciente {
|
||||||
const endereco: Endereco = {
|
const endereco: Endereco = {
|
||||||
cep: p.endereco?.cep ?? p.cep ?? "",
|
cep: p.endereco?.cep ?? p.cep ?? "",
|
||||||
@ -114,7 +114,7 @@ export default function PacientesPage() {
|
|||||||
const q = search.trim();
|
const q = search.trim();
|
||||||
if (!q) return loadAll();
|
if (!q) return loadAll();
|
||||||
|
|
||||||
// Se for apenas números, tentamos como ID no servidor
|
|
||||||
if (/^\d+$/.test(q)) {
|
if (/^\d+$/.test(q)) {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -130,7 +130,7 @@ export default function PacientesPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Senão, recarrega e filtra local (o mock nem sempre filtra por nome/CPF)
|
|
||||||
await loadAll();
|
await loadAll();
|
||||||
setTimeout(() => setSearch(q), 0);
|
setTimeout(() => setSearch(q), 0);
|
||||||
}
|
}
|
||||||
@ -161,7 +161,7 @@ export default function PacientesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Cabeçalho + Busca (um único input no padrão do print) */}
|
{}
|
||||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold">Pacientes</h1>
|
<h1 className="text-2xl font-bold">Pacientes</h1>
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
// Importações do FullCalendar
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
@ -54,7 +54,7 @@ const medico = {
|
|||||||
fotoUrl: "",
|
fotoUrl: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tipos de consulta com cores
|
|
||||||
const colorsByType = {
|
const colorsByType = {
|
||||||
Rotina: "#4dabf7",
|
Rotina: "#4dabf7",
|
||||||
Cardiologia: "#f76c6c",
|
Cardiologia: "#f76c6c",
|
||||||
@ -116,7 +116,7 @@ const ProfissionalPage = () => {
|
|||||||
setPacienteSelecionado(null);
|
setPacienteSelecionado(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clicar em um dia -> abrir popup 3 etapas
|
|
||||||
const handleDateClick = (arg: any) => {
|
const handleDateClick = (arg: any) => {
|
||||||
setSelectedDate(arg.dateStr);
|
setSelectedDate(arg.dateStr);
|
||||||
setNewEvent({ title: "", type: "", time: "", pacienteId: "" });
|
setNewEvent({ title: "", type: "", time: "", pacienteId: "" });
|
||||||
@ -125,7 +125,7 @@ const ProfissionalPage = () => {
|
|||||||
setShowPopup(true);
|
setShowPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Adicionar nova consulta
|
|
||||||
const handleAddEvent = () => {
|
const handleAddEvent = () => {
|
||||||
const paciente = pacientes.find(p => p.nome === newEvent.title);
|
const paciente = pacientes.find(p => p.nome === newEvent.title);
|
||||||
const eventToAdd = {
|
const eventToAdd = {
|
||||||
@ -141,7 +141,7 @@ const ProfissionalPage = () => {
|
|||||||
setShowPopup(false);
|
setShowPopup(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Editar consulta existente
|
|
||||||
const handleEditEvent = () => {
|
const handleEditEvent = () => {
|
||||||
setEvents((prevEvents) =>
|
setEvents((prevEvents) =>
|
||||||
prevEvents.map((ev) =>
|
prevEvents.map((ev) =>
|
||||||
@ -161,19 +161,19 @@ const ProfissionalPage = () => {
|
|||||||
setShowActionModal(false);
|
setShowActionModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Próxima etapa no popup
|
|
||||||
const handleNextStep = () => {
|
const handleNextStep = () => {
|
||||||
if (step < 3) setStep(step + 1);
|
if (step < 3) setStep(step + 1);
|
||||||
else editingEvent ? handleEditEvent() : handleAddEvent();
|
else editingEvent ? handleEditEvent() : handleAddEvent();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clicar em uma consulta -> abre modal de ação (Editar/Apagar)
|
|
||||||
const handleEventClick = (clickInfo: any) => {
|
const handleEventClick = (clickInfo: any) => {
|
||||||
setSelectedEvent(clickInfo.event);
|
setSelectedEvent(clickInfo.event);
|
||||||
setShowActionModal(true);
|
setShowActionModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apagar consulta
|
|
||||||
const handleDeleteEvent = () => {
|
const handleDeleteEvent = () => {
|
||||||
if (!selectedEvent) return;
|
if (!selectedEvent) return;
|
||||||
setEvents((prevEvents) =>
|
setEvents((prevEvents) =>
|
||||||
@ -182,7 +182,7 @@ const ProfissionalPage = () => {
|
|||||||
setShowActionModal(false);
|
setShowActionModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Começar a editar
|
|
||||||
const handleStartEdit = () => {
|
const handleStartEdit = () => {
|
||||||
if (!selectedEvent) return;
|
if (!selectedEvent) return;
|
||||||
setEditingEvent(selectedEvent);
|
setEditingEvent(selectedEvent);
|
||||||
@ -197,7 +197,7 @@ const ProfissionalPage = () => {
|
|||||||
setShowPopup(true);
|
setShowPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Aparência da consulta dentro do calendário
|
|
||||||
const renderEventContent = (eventInfo: any) => {
|
const renderEventContent = (eventInfo: any) => {
|
||||||
const bg = eventInfo.event.backgroundColor || eventInfo.event.extendedProps?.color || "#4dabf7";
|
const bg = eventInfo.event.backgroundColor || eventInfo.event.extendedProps?.color || "#4dabf7";
|
||||||
|
|
||||||
@ -241,7 +241,7 @@ const ProfissionalPage = () => {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-[220px_1fr] gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-[220px_1fr] gap-6">
|
||||||
{/* Sidebar */}
|
{}
|
||||||
<aside className="md:sticky md:top-8 h-fit">
|
<aside className="md:sticky md:top-8 h-fit">
|
||||||
<nav className="bg-white shadow-md rounded-lg p-3 space-y-1">
|
<nav className="bg-white shadow-md rounded-lg p-3 space-y-1">
|
||||||
<Button asChild variant="ghost" className="w-full justify-start hover:bg-primary hover:text-primary-foreground">
|
<Button asChild variant="ghost" className="w-full justify-start hover:bg-primary hover:text-primary-foreground">
|
||||||
@ -602,7 +602,7 @@ const ProfissionalPage = () => {
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* POPUP 3 etapas (Adicionar/Editar) */}
|
{}
|
||||||
{showPopup && (
|
{showPopup && (
|
||||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex justify-center items-center z-50">
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex justify-center items-center z-50">
|
||||||
|
|
||||||
@ -713,7 +713,7 @@ const ProfissionalPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* MODAL de ação ao clicar em consulta */}
|
{}
|
||||||
{showActionModal && selectedEvent && (
|
{showActionModal && selectedEvent && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
|
||||||
<div className="bg-white p-6 rounded-lg w-96">
|
<div className="bg-white p-6 rounded-lg w-96">
|
||||||
|
|||||||
@ -8,9 +8,9 @@ export function AboutSection() {
|
|||||||
<section className="py-16 lg:py-24 bg-muted/30">
|
<section className="py-16 lg:py-24 bg-muted/30">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||||
{/* Left Content */}
|
{}
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Professional Image */}
|
{}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img
|
<img
|
||||||
src="/Screenshot 2025-09-11 121911.png"
|
src="/Screenshot 2025-09-11 121911.png"
|
||||||
@ -19,7 +19,7 @@ export function AboutSection() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Objective Card */}
|
{}
|
||||||
<Card className="bg-primary text-primary-foreground p-8 rounded-2xl">
|
<Card className="bg-primary text-primary-foreground p-8 rounded-2xl">
|
||||||
<div className="flex items-start space-x-4">
|
<div className="flex items-start space-x-4">
|
||||||
<div className="flex-shrink-0 w-12 h-12 bg-primary-foreground/20 rounded-full flex items-center justify-center">
|
<div className="flex-shrink-0 w-12 h-12 bg-primary-foreground/20 rounded-full flex items-center justify-center">
|
||||||
@ -36,7 +36,7 @@ export function AboutSection() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Content */}
|
{}
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="inline-block px-4 py-2 bg-primary/10 text-primary rounded-full text-sm font-medium uppercase tracking-wide">
|
<div className="inline-block px-4 py-2 bg-primary/10 text-primary rounded-full text-sm font-medium uppercase tracking-wide">
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
// app/agenda/page.tsx
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { AgendaCalendar, AppointmentModal, ListaEspera } from '@/components/agendamento';
|
import { AgendaCalendar, AppointmentModal, ListaEspera } from '@/components/agendamento';
|
||||||
|
|
||||||
// Dados mockados - substitua pelos seus dados reais
|
|
||||||
const mockAppointments = [
|
const mockAppointments = [
|
||||||
{ id: '1', patient: 'Ana Costa', time: '2025-09-10T09:00', duration: 30, type: 'consulta' as const, status: 'confirmed' as const, professional: '1', notes: '' },
|
{ id: '1', patient: 'Ana Costa', time: '2025-09-10T09:00', duration: 30, type: 'consulta' as const, status: 'confirmed' as const, professional: '1', notes: '' },
|
||||||
{ id: '2', patient: 'Pedro Alves', time: '2025-09-10T10:30', duration: 45, type: 'retorno' as const, status: 'pending' as const, professional: '2', notes: '' },
|
{ id: '2', patient: 'Pedro Alves', time: '2025-09-10T10:30', duration: 45, type: 'retorno' as const, status: 'pending' as const, professional: '2', notes: '' },
|
||||||
@ -32,10 +32,10 @@ export default function AgendaPage() {
|
|||||||
|
|
||||||
const handleSaveAppointment = (appointment: any) => {
|
const handleSaveAppointment = (appointment: any) => {
|
||||||
if (appointment.id) {
|
if (appointment.id) {
|
||||||
// Editar agendamento existente
|
|
||||||
setAppointments(prev => prev.map(a => a.id === appointment.id ? appointment : a));
|
setAppointments(prev => prev.map(a => a.id === appointment.id ? appointment : a));
|
||||||
} else {
|
} else {
|
||||||
// Novo agendamento
|
|
||||||
const newAppointment = {
|
const newAppointment = {
|
||||||
...appointment,
|
...appointment,
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
@ -60,7 +60,7 @@ export default function AgendaPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleNotifyPatient = (patientId: string) => {
|
const handleNotifyPatient = (patientId: string) => {
|
||||||
// Lógica para notificar paciente
|
|
||||||
console.log(`Notificando paciente ${patientId}`);
|
console.log(`Notificando paciente ${patientId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// components/agendamento/AgendaCalendar.tsx (atualizado)
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -86,7 +86,7 @@ export default function AgendaCalendar({
|
|||||||
setCurrentDate(new Date());
|
setCurrentDate(new Date());
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filtra os agendamentos por profissional selecionado
|
|
||||||
const filteredAppointments = selectedProfessional === 'all'
|
const filteredAppointments = selectedProfessional === 'all'
|
||||||
? appointments
|
? appointments
|
||||||
: appointments.filter(app => app.professional === selectedProfessional);
|
: appointments.filter(app => app.professional === selectedProfessional);
|
||||||
@ -187,7 +187,7 @@ export default function AgendaCalendar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Visualização de Dia/Semana (calendário) */}
|
{}
|
||||||
{view !== 'month' && (
|
{view !== 'month' && (
|
||||||
<div className="overflow-auto">
|
<div className="overflow-auto">
|
||||||
<div className="min-w-full">
|
<div className="min-w-full">
|
||||||
@ -256,7 +256,7 @@ export default function AgendaCalendar({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Visualização de Mês (lista) */}
|
{}
|
||||||
{view === 'month' && (
|
{view === 'month' && (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// components/agendamento/ListaEspera.tsx
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|||||||
@ -12,10 +12,10 @@ export function Footer() {
|
|||||||
<footer className="bg-background border-t border-border">
|
<footer className="bg-background border-t border-border">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div className="flex flex-col md:flex-row items-center justify-between space-y-4 md:space-y-0">
|
<div className="flex flex-col md:flex-row items-center justify-between space-y-4 md:space-y-0">
|
||||||
{/* Copyright */}
|
{}
|
||||||
<div className="text-muted-foreground text-sm">© 2025 SUS Conecta</div>
|
<div className="text-muted-foreground text-sm">© 2025 SUS Conecta</div>
|
||||||
|
|
||||||
{/* Footer Links */}
|
{}
|
||||||
<nav className="flex items-center space-x-8">
|
<nav className="flex items-center space-x-8">
|
||||||
<a href="#" className="text-muted-foreground hover:text-primary transition-colors text-sm">
|
<a href="#" className="text-muted-foreground hover:text-primary transition-colors text-sm">
|
||||||
Termos
|
Termos
|
||||||
@ -28,7 +28,7 @@ export function Footer() {
|
|||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Back to Top Button */}
|
{}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
/* src/components/forms/patient-registration-form.tsx */
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
@ -101,7 +101,7 @@ export function PatientRegistrationForm({
|
|||||||
|
|
||||||
const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]);
|
const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]);
|
||||||
|
|
||||||
// Edição
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function load() {
|
async function load() {
|
||||||
if (mode !== "edit" || patientId == null) return;
|
if (mode !== "edit" || patientId == null) return;
|
||||||
@ -129,7 +129,7 @@ export function PatientRegistrationForm({
|
|||||||
const ax = await listarAnexos(String(patientId)).catch(() => []);
|
const ax = await listarAnexos(String(patientId)).catch(() => []);
|
||||||
setServerAnexos(Array.isArray(ax) ? ax : []);
|
setServerAnexos(Array.isArray(ax) ? ax : []);
|
||||||
} catch {
|
} catch {
|
||||||
// ignora
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
load();
|
load();
|
||||||
@ -208,7 +208,7 @@ export function PatientRegistrationForm({
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (!validateLocal()) return;
|
if (!validateLocal()) return;
|
||||||
|
|
||||||
// validação externa do CPF (mock → pode falhar, tratamos erro legível)
|
|
||||||
try {
|
try {
|
||||||
const { valido, existe } = await validarCPF(form.cpf);
|
const { valido, existe } = await validarCPF(form.cpf);
|
||||||
if (!valido) {
|
if (!valido) {
|
||||||
@ -220,7 +220,7 @@ export function PatientRegistrationForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// se o mock der 404/timeout, seguimos sem bloquear
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
@ -318,7 +318,7 @@ export function PatientRegistrationForm({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{/* DADOS PESSOAIS */}
|
{}
|
||||||
<Collapsible open={expanded.dados} onOpenChange={() => setExpanded((s) => ({ ...s, dados: !s.dados }))}>
|
<Collapsible open={expanded.dados} onOpenChange={() => setExpanded((s) => ({ ...s, dados: !s.dados }))}>
|
||||||
<Card>
|
<Card>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
@ -337,7 +337,7 @@ export function PatientRegistrationForm({
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-24 h-24 border-2 border-dashed border-muted-foreground rounded-lg flex items-center justify-center overflow-hidden">
|
<div className="w-24 h-24 border-2 border-dashed border-muted-foreground rounded-lg flex items-center justify-center overflow-hidden">
|
||||||
{photoPreview ? (
|
{photoPreview ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img src={photoPreview} alt="Preview" className="w-full h-full object-cover" />
|
<img src={photoPreview} alt="Preview" className="w-full h-full object-cover" />
|
||||||
) : (
|
) : (
|
||||||
<FileImage className="h-8 w-8 text-muted-foreground" />
|
<FileImage className="h-8 w-8 text-muted-foreground" />
|
||||||
@ -420,7 +420,7 @@ export function PatientRegistrationForm({
|
|||||||
</Card>
|
</Card>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
{/* CONTATO */}
|
{}
|
||||||
<Collapsible open={expanded.contato} onOpenChange={() => setExpanded((s) => ({ ...s, contato: !s.contato }))}>
|
<Collapsible open={expanded.contato} onOpenChange={() => setExpanded((s) => ({ ...s, contato: !s.contato }))}>
|
||||||
<Card>
|
<Card>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
@ -448,7 +448,7 @@ export function PatientRegistrationForm({
|
|||||||
</Card>
|
</Card>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
{/* ENDEREÇO */}
|
{}
|
||||||
<Collapsible open={expanded.endereco} onOpenChange={() => setExpanded((s) => ({ ...s, endereco: !s.endereco }))}>
|
<Collapsible open={expanded.endereco} onOpenChange={() => setExpanded((s) => ({ ...s, endereco: !s.endereco }))}>
|
||||||
<Card>
|
<Card>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
@ -517,7 +517,7 @@ export function PatientRegistrationForm({
|
|||||||
</Card>
|
</Card>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
{/* OBSERVAÇÕES & ANEXOS */}
|
{}
|
||||||
<Collapsible open={expanded.obs} onOpenChange={() => setExpanded((s) => ({ ...s, obs: !s.obs }))}>
|
<Collapsible open={expanded.obs} onOpenChange={() => setExpanded((s) => ({ ...s, obs: !s.obs }))}>
|
||||||
<Card>
|
<Card>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
@ -584,7 +584,7 @@ export function PatientRegistrationForm({
|
|||||||
</Card>
|
</Card>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
{/* AÇÕES */}
|
{}
|
||||||
<div className="flex justify-end gap-4 pt-6 border-t">
|
<div className="flex justify-end gap-4 pt-6 border-t">
|
||||||
<Button type="button" variant="outline" onClick={() => (inline ? onClose?.() : onOpenChange?.(false))} disabled={isSubmitting}>
|
<Button type="button" variant="outline" onClick={() => (inline ? onClose?.() : onOpenChange?.(false))} disabled={isSubmitting}>
|
||||||
<XCircle className="mr-2 h-4 w-4" />
|
<XCircle className="mr-2 h-4 w-4" />
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export function Header() {
|
|||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
{}
|
||||||
<nav className="hidden md:flex items-center space-x-8">
|
<nav className="hidden md:flex items-center space-x-8">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
@ -32,7 +32,7 @@ export function Header() {
|
|||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Desktop Action Buttons */}
|
{}
|
||||||
<div className="hidden md:flex items-center space-x-3">
|
<div className="hidden md:flex items-center space-x-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -53,13 +53,13 @@ export function Header() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{}
|
||||||
<button className="md:hidden p-2" onClick={() => setIsMenuOpen(!isMenuOpen)} aria-label="Toggle menu">
|
<button className="md:hidden p-2" onClick={() => setIsMenuOpen(!isMenuOpen)} aria-label="Toggle menu">
|
||||||
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Menu */}
|
{}
|
||||||
{isMenuOpen && (
|
{isMenuOpen && (
|
||||||
<div className="md:hidden py-4 border-t border-border">
|
<div className="md:hidden py-4 border-t border-border">
|
||||||
<nav className="flex flex-col space-y-4">
|
<nav className="flex flex-col space-y-4">
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export function HeroSection() {
|
|||||||
<section className="py-16 lg:py-24 bg-background">
|
<section className="py-16 lg:py-24 bg-background">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||||
{/* Content */}
|
{}
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="inline-block px-4 py-2 bg-accent/10 text-accent rounded-full text-sm font-medium">
|
<div className="inline-block px-4 py-2 bg-accent/10 text-accent rounded-full text-sm font-medium">
|
||||||
@ -23,7 +23,7 @@ export function HeroSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{}
|
||||||
<div className="flex flex-col sm:flex-row gap-4">
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
<Button size="lg" className="bg-primary hover:bg-primary/90 text-primary-foreground">
|
<Button size="lg" className="bg-primary hover:bg-primary/90 text-primary-foreground">
|
||||||
Sou Paciente
|
Sou Paciente
|
||||||
@ -38,7 +38,7 @@ export function HeroSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hero Image */}
|
{}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-accent/20 to-primary/20 p-8">
|
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-accent/20 to-primary/20 p-8">
|
||||||
<img
|
<img
|
||||||
@ -50,7 +50,7 @@ export function HeroSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Features */}
|
{}
|
||||||
<div className="mt-16 grid md:grid-cols-3 gap-8">
|
<div className="mt-16 grid md:grid-cols-3 gap-8">
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start space-x-3">
|
||||||
<div className="flex-shrink-0 w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
|
<div className="flex-shrink-0 w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// hooks/useAgenda.ts
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export interface Appointment {
|
export interface Appointment {
|
||||||
@ -42,10 +42,10 @@ export const useAgenda = () => {
|
|||||||
|
|
||||||
const handleSaveAppointment = (appointment: Appointment) => {
|
const handleSaveAppointment = (appointment: Appointment) => {
|
||||||
if (appointment.id) {
|
if (appointment.id) {
|
||||||
// Editar agendamento existente
|
|
||||||
setAppointments(prev => prev.map(a => a.id === appointment.id ? appointment : a));
|
setAppointments(prev => prev.map(a => a.id === appointment.id ? appointment : a));
|
||||||
} else {
|
} else {
|
||||||
// Novo agendamento
|
|
||||||
const newAppointment = {
|
const newAppointment = {
|
||||||
...appointment,
|
...appointment,
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
@ -70,7 +70,7 @@ export const useAgenda = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleNotifyPatient = (patientId: string) => {
|
const handleNotifyPatient = (patientId: string) => {
|
||||||
// Lógica para notificar paciente
|
|
||||||
console.log(`Notificando paciente ${patientId}`);
|
console.log(`Notificando paciente ${patientId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
// Inspired by react-hot-toast library
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -93,8 +93,7 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
case "DISMISS_TOAST": {
|
case "DISMISS_TOAST": {
|
||||||
const { toastId } = action
|
const { toastId } = action
|
||||||
|
|
||||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
|
||||||
// but I'll keep it here for simplicity
|
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
addToRemoveQueue(toastId)
|
addToRemoveQueue(toastId)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
/* src/lib/api.ts */
|
|
||||||
|
|
||||||
export type ApiOk<T = any> = {
|
export type ApiOk<T = any> = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@ -58,9 +58,7 @@ export type PacienteInput = {
|
|||||||
observacoes?: string | null;
|
observacoes?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// Config & helpers
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "https://mock.apidog.com/m1/1053378-0-default";
|
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "https://mock.apidog.com/m1/1053378-0-default";
|
||||||
|
|
||||||
@ -97,21 +95,20 @@ async function parse<T>(res: Response): Promise<T> {
|
|||||||
try {
|
try {
|
||||||
json = await res.json();
|
json = await res.json();
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const code = json?.apidogError?.code ?? res.status;
|
const code = json?.apidogError?.code ?? res.status;
|
||||||
const msg = json?.apidogError?.message ?? res.statusText;
|
const msg = json?.apidogError?.message ?? res.statusText;
|
||||||
throw new Error(`${code}: ${msg}`);
|
throw new Error(`${code}: ${msg}`);
|
||||||
}
|
}
|
||||||
// muitos endpoints do mock respondem { success, data }
|
|
||||||
return (json?.data ?? json) as T;
|
return (json?.data ?? json) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
//
|
||||||
// Pacientes (CRUD)
|
// Pacientes (CRUD)
|
||||||
// -----------------------------------------------------------------------------
|
//
|
||||||
|
|
||||||
export async function listarPacientes(params?: { page?: number; limit?: number; q?: string }): Promise<Paciente[]> {
|
export async function listarPacientes(params?: { page?: number; limit?: number; q?: string }): Promise<Paciente[]> {
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
if (params?.page) query.set("page", String(params.page));
|
if (params?.page) query.set("page", String(params.page));
|
||||||
@ -156,9 +153,9 @@ export async function excluirPaciente(id: string | number): Promise<void> {
|
|||||||
logAPI("excluirPaciente", { url, result: { ok: true } });
|
logAPI("excluirPaciente", { url, result: { ok: true } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
//
|
||||||
// Foto
|
// Foto
|
||||||
// -----------------------------------------------------------------------------
|
//
|
||||||
|
|
||||||
export async function uploadFotoPaciente(id: string | number, file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> {
|
export async function uploadFotoPaciente(id: string | number, file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> {
|
||||||
const url = `${API_BASE}${PATHS.foto(id)}`;
|
const url = `${API_BASE}${PATHS.foto(id)}`;
|
||||||
@ -178,9 +175,9 @@ export async function removerFotoPaciente(id: string | number): Promise<void> {
|
|||||||
logAPI("removerFotoPaciente", { url, result: { ok: true } });
|
logAPI("removerFotoPaciente", { url, result: { ok: true } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
//
|
||||||
// Anexos
|
// Anexos
|
||||||
// -----------------------------------------------------------------------------
|
//
|
||||||
|
|
||||||
export async function listarAnexos(id: string | number): Promise<any[]> {
|
export async function listarAnexos(id: string | number): Promise<any[]> {
|
||||||
const url = `${API_BASE}${PATHS.anexos(id)}`;
|
const url = `${API_BASE}${PATHS.anexos(id)}`;
|
||||||
@ -193,7 +190,7 @@ export async function listarAnexos(id: string | number): Promise<any[]> {
|
|||||||
export async function adicionarAnexo(id: string | number, file: File): Promise<any> {
|
export async function adicionarAnexo(id: string | number, file: File): Promise<any> {
|
||||||
const url = `${API_BASE}${PATHS.anexos(id)}`;
|
const url = `${API_BASE}${PATHS.anexos(id)}`;
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
// alguns mocks usam "arquivo" e outros "file"; tentamos ambos
|
|
||||||
fd.append("arquivo", file);
|
fd.append("arquivo", file);
|
||||||
const res = await fetch(url, { method: "POST", body: fd, headers: headers("form") });
|
const res = await fetch(url, { method: "POST", body: fd, headers: headers("form") });
|
||||||
const data = await parse<ApiOk<any>>(res);
|
const data = await parse<ApiOk<any>>(res);
|
||||||
@ -208,9 +205,9 @@ export async function removerAnexo(id: string | number, anexoId: string | number
|
|||||||
logAPI("removerAnexo", { url, result: { ok: true } });
|
logAPI("removerAnexo", { url, result: { ok: true } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
//
|
||||||
// Validações
|
// Validações
|
||||||
// -----------------------------------------------------------------------------
|
//
|
||||||
|
|
||||||
export async function validarCPF(cpf: string): Promise<{ valido: boolean; existe: boolean; paciente_id: string | null }> {
|
export async function validarCPF(cpf: string): Promise<{ valido: boolean; existe: boolean; paciente_id: string | null }> {
|
||||||
const url = `${API_BASE}${PATHS.validarCPF}`;
|
const url = `${API_BASE}${PATHS.validarCPF}`;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@import 'tw-animate-css';
|
@import 'tw-animate-css';
|
||||||
/* Removed invalid @custom-variant at-rule */
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: var(--primary)
|
--background: var(--primary)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user