develop #83

Merged
M-Gabrielly merged 426 commits from develop into main 2025-12-04 04:13:15 +00:00
15 changed files with 77 additions and 81 deletions
Showing only changes of commit a7c9c90ebb - Show all commits

View File

@ -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: '' },

View File

@ -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>

View File

@ -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">

View File

@ -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">

View File

@ -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}`);
}; };

View File

@ -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">

View File

@ -1,4 +1,4 @@
// components/agendamento/ListaEspera.tsx
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';

View File

@ -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"

View File

@ -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" />

View File

@ -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">

View File

@ -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">

View File

@ -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}`);
}; };

View File

@ -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 {

View File

@ -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}`;

View File

@ -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)